想象一家新开的餐厅后厨,主厨端来一道从未做过的新菜:一个卷积算子。它对学徒厨师说:“这道菜,你必须在今天之内做到又快又好。”学徒面前有几十个旋钮——切法怎么切、火候用多大、摆盘怎么摆、配菜放多少。每一个旋钮有几十种选择,全部组合起来比天上星星还多。学徒不可能一道菜做几十亿次让客人试吃,那该怎么办?他想到一个办法:每做一次菜就记笔记,记下当时看到了什么、做了哪些操作、客人打了多少分,然后慢慢摸索出“哪种做法最受欢迎”的规律。这个“边做菜、边记笔记、边改进”的循环,就是强化学习调优的核心思路。AutoTVM 和 Ansor 这类工具,正是把“给算子找最优调度”变成一场有章法的厨房修行,让计算机自己学会如何把代码调得飞快。

岗位关联

掌握强化学习调优的思路,你就能从“手写手工优化经验”进阶到“让编译器自动从千万种方案里学习最优调度”,在模型上线、跨硬件移植时省下数周人力。

目录
  1. 可视化演示
  2. 调优困境:组合爆炸的厨房
  3. 强化学习框架:试菜记笔记的循环
  4. 状态空间设计:给后厨拍一张结构化快照
  5. 动作空间设计:合法做菜手法清单
  6. 奖励函数设计:客人的评分标准
  7. 学习探索策略:守成与尝新
  8. 效果对比分析:学徒对决掷骰子
  9. 工程选型建议:何时启用学徒制
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示强化学习调优机制:状态-动作-环境-奖励循环、策略动作搜索与最优动作选择、RL调优相对基线的训练效果,通过交互理解AutoTVM/Ansor的RL调度搜索原理。

本节配有交互动效(进入视口后自动加载,约 3KB)。

RL循环:状态-动作-奖励 调度搜索建模为序列决策问题 RL代理自动探索高效调度策略 S → A → Env → R 奖励驱动策略迭代优化

调优困境:组合爆炸的厨房

学徒厨师第一天上班就傻了眼。主厨给的那道菜,光可调的旋钮就有七八个:块的大小、循环的顺序、向量化的宽度、数据摆放的方式……每个旋钮还有十几个档位。他随手算了一下:几千万种组合。就算十秒钟试一次,也要连续试好几年。这还没算上每次试菜都要重新切菜、热锅、摆盘——在真实的调优任务里,每试一种调度方案,都要重新编译一次算子再跑一遍,单次试验往往要花几秒甚至几分钟。

这道菜的困境,本质是“组合爆炸”:每个旋钮的档位数相乘,会得到一个天文数字。传统做法靠资深工程师写启发式规则,比如“在 GPU 上优先用某某分块大小”。但规则是针对某个硬件手工调出来的,换一块新芯片、换一个输入尺寸,规则立刻失效。而且规则本身也不覆盖所有算子结构,很多形状复杂的算子根本套不上现成模板。

学徒垂头丧气地放下菜刀,意识到自己缺的不是力气,而是一套“边做边学”的方法。他决定从明天起,每次做菜都认真记笔记,看看能不能从几千次尝试里总结出规律——这就是强化学习登场的原因。

N组合=∏i=1kci\displaystyle N_{\text{组合}} = \prod_{i=1}^{k} c_i
这个公式告诉你:把每个旋钮的档位数连乘,就是全部可能的调度方案数。档位稍微多一点,总数就 exponentially 膨胀,手工枚举不可能。
  1. 旋钮档位设第 \(i\) 个可调旋钮有 \(c_i\) 种档位,比如“分块大小”有 8 档。
    ci=第 i 个旋钮的档位数\displaystyle c_i = \text{第 } i \text{ 个旋钮的档位数}
  2. 连乘取积把所有旋钮的档位数相乘,得到总组合数。
    ∏i=1kci=c1×c2×⋯×ck\displaystyle \prod_{i=1}^{k} c_i = c_1 \times c_2 \times \cdots \times c_k
  3. 爆炸含义只要 \(k\) 稍大或单个档位稍多,总数便超过手工可枚举的范围。
    N组合=∏i=1kci\displaystyle N_{\text{组合}} = \prod_{i=1}^{k} c_i

强化学习框架:试菜记笔记的循环

从第二天开始,学徒换了一种干活方式。他每做一次菜,就掏出一个小本子:第一行写“我看到了什么”——食材种类、切菜台的状态、灶台的型号;第二行写“我做了什么”——选了哪种切法、哪个火候;第三行写“客人打了几分”——这道菜上桌后比正常慢了还是快了。试的次数多了,他慢慢发现:某些动作组合总是拿高分,于是他在本子上把这些动作偷偷圈起来,下次多做。

这就是强化学习调优的完整骨架。在 AutoTVM/Ansor 的场景里,学徒是“策略网络”,厨房是“编译器 + 真实硬件”,本子上的“看到什么”叫状态(state),“做了什么”叫动作(action),“客人打分”叫奖励(reward)。一次完整的做菜过程叫一条轨迹,包含多步“状态→动作→奖励”的交替。调度搜索因此被建模成一个序列决策问题:代理一步一步地选择调度变换,最终拼出一个完整的 schedule。

收集了一堆轨迹之后,怎么用?核心思想是:凡是带来高奖励的动作,就提高它下次被选中的概率;凡是低奖励的动作,就降低概率。这个“提优降劣”的数学操作,叫策略梯度。

学徒的本子越记越厚,动作也从盲目乱试变成了“优先尝试划圈的做法”。接下来他要解决三个更细的问题:如何把厨房的状态记清楚?如何保证动作合法?客人打分怎么打才公平?下面四节逐一拆解。

θ←θ+α∑tRt∇θln⁡πθ(at∣st)\displaystyle \theta \leftarrow \theta + \alpha \sum_{t} R_t \nabla_\theta \ln \pi_\theta(a_t \mid s_t)
这个式子是说:把每条轨迹里每一步的对数概率梯度,用该步获得的奖励做加权,再累加起来更新策略参数。高奖励步骤贡献大,低奖励步骤贡献小甚至反向。
  1. 策略参数\(\theta\) 是代理脑中的“做菜经验”权重,也就是策略网络的参数。
    θ←θ+Δθ\displaystyle \theta \leftarrow \theta + \Delta\theta
  2. 对数概率梯度\(\nabla_\theta \ln \pi_\theta(a_t \mid s_t)\) 告诉我们在状态 \(s_t\) 下,往哪个方向调整参数能明显提高选中动作 \(a_t\) 的概率。
    ∇θln⁡πθ(at∣st)\displaystyle \nabla_\theta \ln \pi_\theta(a_t \mid s_t)
  3. 奖励加权把梯度乘以 \(R_t\),让高奖励的动作被大幅强化,低奖励的动作被压低。
    αRt∇θln⁡πθ(at∣st)\displaystyle \alpha R_t \nabla_\theta \ln \pi_\theta(a_t \mid s_t)

状态空间设计:给后厨拍一张结构化快照

学徒发现,光记“食材很多、锅很热”这种模糊描述没用,他需要一张标准化的表:菜品是什么类型、食材有多少块、每一块大概多大、灶台是明火还是电磁炉、锅的容量多少。换句话说,不能把厨房的乱象全拍下来,而要抽出影响做菜结果的关键信息。

在算子调优中,状态就是对“当前这个算子和硬件”的结构化描述。它要编码算子的计算图结构:算子类型(卷积、矩阵乘、池化……)、输入张量的形状、循环嵌套的层数与层次关系、当前已选择的部分调度。同时还要融入硬件特征:目标芯片是 CPU 还是 GPU 或 NPU、缓存多大、向量寄存器多宽、核数多少。这些特征共同决定哪个调度方案跑得快。

但原始特征有个麻烦:有的特征数值很大(比如张量长度上万),有的很小(比如向量宽度只有几十)。直接把它们塞给神经网络,大数值会淹没小数值,训练很不稳定。所以要先做归一化,把每个维度的特征压到相近的尺度上,再通过嵌入层把离散结构(比如算子类型)转成稠密向量。

有了这张“结构化快照”,学徒每次进后厨都能一眼看清关键信息。但他还缺一样东西:一份明确的“可以做什么”的清单,不然他可能会把菜扔到天花板上。

s~i=si−μiσi+ϵ\displaystyle \tilde{s}_i = \frac{s_i - \mu_i}{\sigma_i + \epsilon}
归一化公式的含义是:把每个特征减去全体样本的均值、再除以标准差,让所有特征都落在“0 附近、量级相近”的范围内。
  1. 原始特征\(s_i\) 是某个维度的原始值,比如循环层数或向量宽度。
    si∈R\displaystyle s_i \in \mathbb{R}
  2. 中心化减去该维度在所有样本上的均值 \(\mu_i\),让特征围绕 0 分布。
    si−μi\displaystyle s_i - \mu_i
  3. 标准化缩放除以标准差 \(\sigma_i\),把不同量纲统一成“单位尺度”。
    si−μiσi\displaystyle \frac{s_i - \mu_i}{\sigma_i}
  4. 防除零加上极小常数 \(\epsilon\),避免标准差恰好为 0 时算出无穷大。
    σi+ϵ\displaystyle \sigma_i + \epsilon

动作空间设计:合法做菜手法清单

后厨贴着一张规定:学徒只能使用五种手法——切块、焯水、爆炒、摆盘、换锅,而且使用顺序不能颠倒到离谱。比如不能先摆盘再切块。这张清单既保护了菜品的可行性,也大幅缩小了学徒需要尝试的范围。

对应到调优,动作就是“调度变换原语”。AutoTVM/Ansor 里常见的原语有:split(把循环分块)、reorder(交换循环顺序)、vectorize(向量化)、parallel(并行化)、compute_at(把某段计算挪到另一层循环内)等等。一个完整的调度方案,就是这些原语按某种顺序拼成的动作序列。代理的策略,本质上是在为“下一步该用哪个原语、参数选多少”打分。

但并不是所有动作拼接都合法。例如对已经展开的循环再做 split 可能破坏语义,某些变换顺序在不同算子结构下会生成错误代码。假如代理乱试这些非法动作,轻则编译失败浪费试验次数,重则得到错误结果。解决办法是“合法动作掩码”:每一步先把当前状态允许的动作列出来,把非法动作的概率直接置零,再在合法集合上做 softmax。

学徒终于拥有了“合法手法清单”。但他很快发现一个新问题:客人打分必须公平,否则一次手抖就会误导他好几天。

πθ(a∣s)=exp⁡(za)∑a′∈Aexp⁡(za′)\displaystyle \pi_\theta(a \mid s) = \frac{\exp(z_a)}{\sum_{a' \in \mathcal{A}} \exp(z_{a'})}
这是策略网络输出动作概率的公式:先给每个动作打分 \(z_a\),指数化后除以所有合法动作的分数和,得到合法的概率分布。
  1. 动作打分\(z_a\) 是策略网络针对动作 \(a\) 打出的分数,分数越高越倾向选择。
    za=fθ(s,a)\displaystyle z_a = f_\theta(s, a)
  2. 指数化对分数取自然指数,把任意实数变成正数,且保持大小顺序。
    exp⁡(za)\displaystyle \exp(z_a)
  3. 归一化除以合法动作集合 \(\mathcal{A}\) 中所有动作分数之和,变成总和为 1 的概率。
    πθ(a∣s)∈[0,1]\displaystyle \pi_\theta(a \mid s) \in [0, 1]
  4. 合法动作集合\(\mathcal{A}\) 是经过掩码过滤后的合法动作集合,非法动作已被剔除。
    A={合法调度变换}\displaystyle \mathcal{A} = \{ \text{合法调度变换} \}

奖励函数设计:客人的评分标准

学徒一开始让客人随便打分,结果有的客人心情好给满分,有的客人挑剔给零分,他完全学不到规律。后来主厨定了规矩:打分只看一道硬指标——上菜时间。先用最普通的做法做一版,记下耗时作为基准;之后每道菜的分数,就是基准耗时除以这道菜的实际耗时。上菜比基准快一倍,分数就是 2;比基准慢一半,分数就是 0.5。

在调优任务里,这个“客人评分”就是奖励函数。首先用一张默认 schedule 编译并运行算子,测出基线耗时 \(T_{\text{baseline}}\);然后对候选 schedule 同样编译运行,测出 \(T_{\text{candidate}}\)。奖励取两者的比值:

不过比值奖励也有隐患:运行时间受系统噪声影响,同一个方案测两次可能相差 10%。更麻烦的是“奖励稀疏”问题——在组合空间里,绝大多数随机方案性能都差不多,奖励几乎没有区分度,代理像个没有方向的学徒,试了很多次也学不到东西。工程上常用 log 加速比压缩极端值、对同一方案多次测量取中位数、或者先用粗粒度方法筛掉明显差劲的区域,再让 RL 精调。这样才能让客人的评分既稳定又有信息量。

R=TbaselineTcandidate\displaystyle R = \frac{T_{\text{baseline}}}{T_{\text{candidate}}}
奖励大于 1 说明比基线快,小于 1 说明更慢。用比值而非绝对耗时,好处是跨算子、跨硬件时数字尺度稳定,代理学到的经验更容易迁移。
  1. 基线耗时默认调度方案在目标硬件上的实测耗时,作为比较的“原味配方”。
    Tbaseline\displaystyle T_{\text{baseline}}
  2. 候选耗时当前策略选出的新调度方案的实测耗时。
    Tcandidate\displaystyle T_{\text{candidate}}
  3. 比值奖励比值大于 1 表示方案变快,奖励为正反馈。
    R>1⇒加速\displaystyle R > 1 \Rightarrow \text{加速}

学习探索策略:守成与尝新

学徒现在面临一个经典的矛盾:已知“爆炒拿高分”,他应该每道菜都爆炒吗?如果永远只做拿手菜,他就永远发现不了可能更好的“低温慢煮”。但若是每道菜都乱试新花样,客人又会纷纷给差评。成熟的学徒会定下规矩:大部分时候按已知好做法来,留一小部分次数尝试新配方,并且每次试新风波不能太大,免得把学过的经验全搞砸。

这个矛盾在强化学习里叫“探索与利用的权衡”。对应策略包括:\(\epsilon\)-greedy(以 \(\epsilon\) 概率随机探索,否则选当前最优)、在损失函数里加熵正则项鼓励多样化动作、以及把采集到的轨迹放进经验池打乱顺序再学习(经验回放,减少相邻轨迹的关联性)。AutoTVM/Ansor 这类框架接入 RL 时,常用 PPO 优化器来更新策略,因为它有一个特别务实的机制:限制单次更新的幅度。

学徒带着“大部分守成、小部分尝新、每次小步改进”的纪律,手艺稳步上涨。现在该跟其他后厨的效率比一比了。

LCLIP(θ)=Et[min⁡(ρtA^t, clip(ρt,1−ε,1+ε)A^t)]\displaystyle L^{\mathrm{CLIP}}(\theta) = \mathbb{E}_t \left[ \min\left( \rho_t \hat{A}_t,\ \mathrm{clip}(\rho_t, 1-\varepsilon, 1+\varepsilon) \hat{A}_t \right) \right]
PPO 的目标函数像一个“安全带”:允许代理朝好的方向改进,但一旦改进幅度超过阈值(clip 区间),就不再多给鼓励,防止一步迈太大把已有经验破坏掉。
  1. 概率比\(\rho_t\) 是旧策略与当前策略在状态 \(s_t\) 下选择动作 \(a_t\) 的概率之比,衡量策略变化的幅度。
    ρt=πθ(at∣st)πθold(at∣st)\displaystyle \rho_t = \frac{\pi_\theta(a_t \mid s_t)}{\pi_{\theta_{\text{old}}}(a_t \mid s_t)}
  2. 优势函数\(\hat{A}_t = R_t - V(s_t)\) 表示这一步的实际收获比期望好多少。
    A^t=Rt−V(st)\displaystyle \hat{A}_t = R_t - V(s_t)
  3. 截断操作\(\mathrm{clip}(\rho_t, 1-\varepsilon, 1+\varepsilon)\) 把概率比强制限制在安全区间内。
    clip(ρt,1−ε,1+ε)\displaystyle \mathrm{clip}(\rho_t, 1-\varepsilon, 1+\varepsilon)
  4. 保守取小\(\min\) 取原始项与截断项的较小值,保证超范围的部分不再贡献梯度。
    min⁡(ρtA^t, clip(ρt,1−ε,1+ε)A^t)\displaystyle \min(\rho_t \hat{A}_t,\ \mathrm{clip}(\rho_t, 1-\varepsilon, 1+\varepsilon) \hat{A}_t)

效果对比分析:学徒对决掷骰子

餐厅老板想验证学徒的方法值不值得培养,于是请来两组对照组:一组是“掷骰子厨师”,每次做菜完全随机选配方,从不记笔记;另一组是“进化厨师”,同时养着几十个配方,每轮留好的、杂交差的,像自然选择一样迭代。老板给三组同样的预算——各做 200 次试验,看谁先找到满足加速目标的最佳配方。

结果通常很明显:随机搜索完全没有记忆,这次试了高分区域,下次照样可能在低分区域打转,达成目标需要的试验次数非常多。进化算法靠种群并行,能找到不错的解,但每个“个体”都从零开始,缺乏对“一个算子的调度长什么样”的全局理解。RL 代理则把历史试验全部压缩进策略参数里,随着试验次数增加,采样越来越集中在高奖励区域。量化这个差距的指标叫“样本利用率”:

当然,样本效率高不等于永远赢。当搜索空间很小、几十次试验就能穷举完时,随机搜索连训练成本都不用付,反而更干脆;进化算法天然适合并行,多台机器同时跑能摊薄时间。学徒的胜利是有条件的:空间足够大、每次试验成本足够高、并且有足够的历史经验可以积累时,他的“记笔记式学习”才真正划算。

η=NrandomNRL\displaystyle \eta = \frac{N_{\text{random}}}{N_{\text{RL}}}
当 RL 和随机搜索都达到同一目标加速比时,随机搜索所需试验次数与 RL 所需次数的比值,就是 RL 的样本效率倍数。
  1. 随机搜索试错数\(N_{\text{random}}\) 是随机搜索达到目标加速比所需的试验次数。
    Nrandom\displaystyle N_{\text{random}}
  2. RL 试错数\(N_{\text{RL}}\) 是 RL 代理达到同等水平所需的试验次数。
    NRL\displaystyle N_{\text{RL}}
  3. 效率比值\(\eta\) 越大,说明 RL 用越少的试验达到同样效果,样本效率越高。
    η=NrandomNRL\displaystyle \eta = \frac{N_{\text{random}}}{N_{\text{RL}}}

工程选型建议:何时启用学徒制

餐厅老板最后算了一笔账:培养一个学徒有固定成本——要花时间教他记笔记、训练他的判断力。如果餐厅只有一道凉菜,随便让切菜工试几下就行,没必要上学徒制;但如果要应对几十道硬菜、还要长期经营同一间厨房,学徒制就非常值。

调优工作的工程选型也是同样逻辑。先看三个因素:待调算子的数量 \(n_{\text{op}}\)、每个算子需要的平均试验次数 \(N_{\text{trial}}\)、能并行跑试验的设备数 \(p_{\text{device}}\)。三者决定总调优成本:

实际的工程选型还有几条经验法则:搜索空间小(比如旋钮少于四五个)直接用随机或网格搜索;空间中等用进化算法或模拟退火;空间大且硬件会长期服役,才投入 RL。冷启动阶段可以先加载一个预训练策略(从相似算子迁移经验)再微调,避开几百次“从头学起”的浪费。AutoTVM/Ansor 本身也提供了多种搜索器接口,RL 代理并非要替代一切,而是作为可插拔的“精调专家”,与启发式粗筛、进化并行形成混合策略。学徒制的精髓不在于每道菜都从零教,而在于把经验沉淀下来,让下一道菜学得更快。

C=nop×Ntrialpdevice\displaystyle C = \frac{n_{\text{op}} \times N_{\text{trial}}}{p_{\text{device}}}
总机器小时 = 算子数量 × 单算子试验次数 ÷ 并行设备数。成本越高,越值得引入 RL 这种“省试验”的方法。
  1. 算子总数\(n_{\text{op}}\) 是模型里需要调优的算子数量,几十个还是上千个。
    nop\displaystyle n_{\text{op}}
  2. 单算子试验数\(N_{\text{trial}}\) 是每个算子平均要编译运行的试验次数。
    Ntrial\displaystyle N_{\text{trial}}
  3. 并行设备数\(p_{\text{device}}\) 是同时能跑试验的机器数,设备越多调得越快。
    pdevice\displaystyle p_{\text{device}}
  4. 总代价分子越大成本越高,分母越大成本越低,据此判断 RL 是否值得。
    C=nop×Ntrialpdevice\displaystyle C = \frac{n_{\text{op}} \times N_{\text{trial}}}{p_{\text{device}}}

AI 应用场景

新硬件的算子移植加速

阶段:编译优化与算子自动调优

解决的问题:传统方式把算子调度移植到新 NPU 时,需要工程师逐硬件手写优化规则,一个卷积算子往往要调数天甚至数周。强化学习调优把“做菜经验”沉淀为策略网络,新硬件上的算子直接交给 RL 代理探索,省去大量人工。

具体案例:某推理引擎团队在接入一颗新 NPU 时,用 Ansor 为每个卷积算子生成大量 sketch(调度草图),再用 RL 代理在百万级配置空间中搜索。同一个算子,手工调优需要 3 天的任务,RL 加并行采样把时间压缩到 3 小时,最终加速比达到手工方案的 1.2 倍。

动态 shape 模型的在线服务

阶段:推理服务冷启动与动态 shape 适配

解决的问题:线上视频模型的输入分辨率经常变化(720p、1080p、4K),每换一个新 shape 就要重新调优,传统方式会带来分钟级卡顿。用训练好的 RL 策略做 warm start,可以继承相似 shape 的调度经验,把重调优时间压到秒级。

具体案例:一个视频超分服务在分辨率切换时,用历史 RL 策略作为初始参数,只做少量微调就让新 shape 的算子达到 95% 的峰值性能,用户无感切换。

云端批量调优平台

阶段:模型仓库批量调优的基建调度

解决的问题:云上要为整个模型仓库(成百上千个算子)批量调优,试验成本直接决定账单金额。奖励用稳定加速比,策略用 PPO 更新,多台设备共享一个经验池,让所有算子的探索经验互相复用。

具体案例:某云厂商的编译服务把 RL + 并行采样结合,数百个算子的调优任务共享经验回放池,相比各算子独立随机搜索,总机器小时节省约 60%,用户拿到的时间预算从“天”变成“小时”。

Python 演示

python74 行
import numpy as np class TuningEnv:    """模拟一个算子调优环境: 5 个调度旋钮, 存在隐藏最优组合"""    def __init__(self, seed=0):        rng = np.random.default_rng(seed)        self.knob_sizes = [4, 4, 8, 3, 8]        self.optimal = [int(rng.integers(n)) for n in self.knob_sizes]        self.baseline_ms = 10.0     def reward(self, schedule):        # 离最优越远, 耗时越长, 加速比越小        dist = sum(s != o for s, o in zip(schedule, self.optimal))        time_ms = self.baseline_ms * (0.7 + 0.3 * dist)        return self.baseline_ms / max(time_ms, 1e-3)  class PolicyAgent:    """用策略梯度(REINFORCE)学习每个旋钮各档位的选择概率"""    def __init__(self, knob_sizes, lr=0.1):        self.theta = [np.zeros(int(n)) for n in knob_sizes]        self.lr = lr        self.reward_mean = 1.0     def softmax(self, logits):        e = np.exp(logits - logits.max())        return e / e.sum()     def sample(self):        # 按当前策略选一个完整调度, 同时计算对数概率        schedule = []        log_prob = 0.0        for j, logits in enumerate(self.theta):            p = self.softmax(logits)            a = int(np.random.choice(len(p), p=p))            schedule.append(a)            log_prob += np.log(p[a] + 1e-12)        return schedule, log_prob     def update(self, schedule, log_prob, reward):        # 优势 = 奖励 - 平均奖励, 降低方差        advantage = reward - self.reward_mean        self.reward_mean = 0.9 * self.reward_mean + 0.1 * reward        for j, a in enumerate(schedule):            p = self.softmax(self.theta[j])            grad = np.zeros_like(p)            grad[a] += 1.0            grad -= p            self.theta[j] += self.lr * advantage * grad        return advantage  def main():    env = TuningEnv(seed=0)    agent = PolicyAgent(env.knob_sizes, lr=0.1)    print("真实最优旋钮组合:", env.optimal)    best_schedule, best_reward = None, 0.0    rewards_history = []    for step in range(400):        schedule, log_prob = agent.sample()        reward = env.reward(schedule)        if reward > best_reward:            best_reward, best_schedule = reward, schedule        agent.update(schedule, log_prob, reward)        rewards_history.append(reward)        if (step + 1) % 80 == 0:            avg = round(float(np.mean(rewards_history[-80:])), 4)            print("步数:", step + 1, "最近80步平均奖励:", avg)    print("最优发现:", best_schedule, "加速比:", round(float(best_reward), 4))    print("模拟最佳加速比: %.2fx" % best_reward)  if __name__ == "__main__":    main()
目的:用纯 NumPy 实现一个迷你强化学习调优器,让策略代理在模拟的“调度旋钮空间”里通过试错学习最优组合,直观演示 REINFORCE 策略梯度的完整流程。
  1. self.optimal = [int(rng.integers(n)) for n in self.knob_sizes]环境随机生成隐藏的最优旋钮组合,模拟真实硬件上“某个调度最快”这个未知事实。
  2. time_ms = self.baseline_ms * (0.7 + 0.3 * dist)离最优越远的调度耗时越长,把抽象的性能关系简化成可计算的奖励。
  3. def softmax(self, logits)把策略分数变成概率分布,是动作选择的数学基础。
  4. a = int(np.random.choice(len(p), p=p))按当前策略概率采样,高概率动作更常被选中,同时保留探索机会。
  5. log_prob += np.log(p[a] + 1e-12)记录被选动作的对数概率,供策略梯度计算使用。
  6. advantage = reward - self.reward_mean减去历史平均奖励得到优势,让梯度更新更有针对性、方差更小。
  7. grad[a] += 1.0; grad -= p计算 softmax 交叉熵梯度:推高被选动作的概率,压低其他动作。
  8. self.theta[j] += self.lr * advantage * grad策略梯度更新:高优势时加强这次选择,低优势时反向修正。

常见误区

误区我以为是让代理不停随机试配置,试得多了自然能找到好的,本质和随机搜索没区别。

正解随机搜索每次试验相互独立、毫无记忆;RL 代理每次试验后会用奖励更新策略,下一次采样会偏向高分区域。好比学徒会记笔记并调整手法,而掷骰子的人永远靠运气。两者虽然都在“反复试”,但一个在积累经验,一个在重复抛硬币。

为何常见:从外部看,两者都表现为“不断尝试+测量耗时”,表象太像,容易忽略内部是否有“学习”这个关键环节。

误区想当然地认为把算子源码全文、全部硬件参数一股脑塞给代理,信息全了策略自然更准。

正解高维冗余特征会增加策略网络的训练难度,噪声维度还会干扰梯度。关键是提取真正影响性能的核心结构:循环嵌套、数据布局、硬件带宽与缓存。特征要结构化、归一化,而非单纯堆料。

为何常见:人们默认“信息越多决策越准”,却忽略了机器学习中特征工程的重要性,以及高维空间带来的稀疏与过拟合风险。

误区奖励就是实测加速比,数值越大训练越快,不用做任何加工。

正解原始加速比受测量噪声影响大,极端值会拉偏梯度方向。工程上常取 log 压缩极端值、多次测量取中位数、或用相对基线比值得分。奖励追求的是“稳定、有区分度”,而不是单纯的“大”。

为何常见:奖励定义看起来简单,但编译环境的时钟波动、降频、缓存状态都会污染单次测量,初学容易忽视这些工程细节。

误区既然论文里说 RL 样本效率高,那任何场景下都用 RL 就能最快找到最优调度。

正解搜索空间很小或测量噪声很大时,启发式规则、随机搜索、进化算法往往更简单稳定,还没有训练 RL 的额外成本。RL 的优势集中在“空间够大、经验能积累、试验代价高”的场景,选型要看具体约束。

为何常见:把论文中特定条件下的优势,错误推广到所有算子、所有硬件、所有预算。

误区把策略模型当成一个一次性的宝贝,调完一个模型后任何新算子都能直接用。

正解状态中包含算子结构、输入 shape 和硬件特征,模型结构一换、输入分辨率一变、目标芯片一换,状态分布就漂移,旧策略效果大打折扣。需要在新分布上重训或迁移微调,这也正是“冷启动策略”要解决的问题。

为何常见:把离线优化当成一次性工程交付,忽略了部署环境与模型迭代带来的分布变化。

面试题

Q1. 在强化学习调优中,RL 代理最直接利用的“奖励”信号来自哪里?

单选题 · easy

  • A.编译报错次数
  • B.实测运行加速比← 正确答案
  • C.生成代码的行数
  • D.模型参数量大小

答案:B。奖励反映调度方案的好坏,最直接信号就是真实运行的加速比(基线耗时/候选耗时)。编译报错属于约束而非奖励,代码行数和参数量与性能没有直接正比关系。就像客人打分只看上菜快不快,而不是看厨师切了几刀。

Q2. 为什么强化学习调优通常比随机搜索更高效?

单选题 · mid

  • A.它每次都是随机试验
  • B.它用历史奖励指导探索← 正确答案
  • C.它不测量真实运行时间
  • D.它完全依赖手工规则

答案:B。RL 代理每轮试验后会用奖励更新策略,使后续采样偏向高奖励区域,这是“经验积累”;随机搜索没有记忆,每次独立,试验分布不随历史改变。比喻上学徒会记笔记改进手法,掷骰子的人不会。

Q3. 状态特征归一化的主要作用是什么?

单选题 · mid

  • A.让不同特征尺度一致← 正确答案
  • B.直接删除部分特征
  • C.把所有特征全部置零
  • D.减少状态空间的维度

答案:A。归一化把各维度特征缩放到相近尺度,避免大数值特征淹没小数值特征,让策略网络训练更稳定。它不删除特征、不置零,也不负责降维,而是让每个特征在梯度计算中有公平的话语权。

Q4. 动作空间中“合法动作掩码”要解决什么问题?

单选题 · mid

  • A.加快编译器的速度
  • B.屏蔽非法调度变换← 正确答案
  • C.让奖励值变得更大
  • D.缩小状态特征数量

答案:B。某些调度变换在当前算子结构下非法或会破坏语义,掩码把这些动作的概率置零,代理就不会浪费试验次数去试注定失败的方案。好比后厨规定不能先摆盘再切菜,避免无效劳动。

Q5. “奖励稀疏”问题指的是什么?

单选题 · mid

  • A.每次获得的奖励都很大
  • B.大多数动作奖励难区分← 正确答案
  • C.奖励函数总是返回负数
  • D.奖励来自单一测量指标

答案:B。当组合空间极大而大部分随机方案的性能都差不多时,奖励几乎没有区分度,代理难以从反馈中学习。解决思路包括 log 加速比、基线比值、多次测量取中位数和先粗筛后精调,类比客人打分全打 6 分时,学徒根本分不清哪个手法更好。

Q6. PPO 中 clip 操作的核心作用是什么?

单选题 · hard

  • A.将奖励数值整体放大
  • B.限制新旧策略更新幅度← 正确答案
  • C.裁剪掉无用的状态特征
  • D.直接丢弃历史经验轨迹

答案:B。clip 把新旧策略的概率比限制在 \(1 \pm \varepsilon\) 区间内,任何超出该幅度的更新都不再获得额外鼓励,从而防止单次更新破坏已有经验。这是 PPO 相比朴素策略梯度更稳定的关键,类似给学徒的“小步改进纪律”。

Q7. 在“学徒厨师”这个比喻里,AutoTVM/Ansor 中的“算子”最像什么?

单选题 · easy

  • A.一份标准的做菜原料
  • B.一道等待端出的新菜← 正确答案
  • C.一间拥挤的厨房环境
  • D.一位挑剔的试菜客人

答案:B。一个算子是一个待优化目标,对应一道需要做好的新菜;厨房对应编译器+硬件环境,动作是调度变换,奖励是实测加速比。选 B 是因为“给算子找最优调度”就像“把这盘菜做到最好”。

Q8. 以下哪些属于调度变换原语?

多选题 · hard

  • A.分块变换 tile← 正确答案
  • B.向量化 vectorize← 正确答案
  • C.循环重排序 reorder← 正确答案
  • D.随机梯度下降 sgd

答案:ABC。tile、vectorize、reorder 都是 TVM 调度原语,用来调整循环结构和计算方式;sgd 是训练神经网络的优化器,与调度变换无关。原语就像后厨的切块、焯水、摆盘,是构成完整调度动作序列的基本单元。

Q9. 相比进化算法,强化学习调优的潜在优势包括哪些?

多选题 · hard

  • A.能感知调度状态特征← 正确答案
  • B.样本利用率通常更高← 正确答案
  • C.完全不需要真实测量
  • D.可复用相似算子经验← 正确答案

答案:ABD。RL 策略网络以状态为输入,能利用算子结构特征;通过历史奖励更新策略,样本利用率往往更高;相似算子的经验可以通过预训练策略迁移。但 RL 仍然必须做真实运行测量来获得奖励,C 的说法错误。

Q10. 工程选型判断“是否值得用强化学习调优”时,需要考虑哪些因素?

多选题 · mid

  • A.待调算子的数量规模← 正确答案
  • B.可用的算力时间预算← 正确答案
  • C.目标硬件服役周期长短← 正确答案
  • D.算子源码注释的多少

答案:ABC。算子越多、空间越大、硬件服役越久,RL 的学习成本越能被摊薄;算力预算决定能承受多少次试验。注释多少与性能无关,不构成选型依据。类比餐厅只有一道凉菜时不必培养学徒,若长期经营几十道硬菜则很值得。