想象你请装修公司改造一套房子。设计师先画出一张户型草图:客厅、卧室、卫生间的位置都标好了,但瓷砖用多大、水电怎么走、墙砖怎么贴、施工顺序怎么安排,图纸上全是空白。施工队看到草图没法开工,必须由经验丰富的工长在图上补上细节标注:地砖 600×600、墙砖 300×600、先布管再贴砖……这些标注决定了装修成本、工期和最终居住体验。 在 AI 编译器 AutoTVM/Ansor 的调优流程中,也有这样一位“工长”——规则注解(Rule Annotation)。它接收前一阶段生成的候选调度草图,把分块大小、循环展开、向量化等优化参数一一标注上去,形成一份供代价模型评估的完整“施工图”。没有这些细节,编译器就无法判断这个调度好不好,自动调优就成了无米之炊。 本文用“装修施工图”这条主线,带你从零认识规则注解:它从哪里来、怎么工作、如何与代价模型配合,以及在实际 AI 工程中怎么用好它。

岗位关联

理解规则注解,等于掌握了 AI 编译器自动调优的“施工工艺图”。对 AI 从业者而言,这是从“会调模型”走向“能优化推理性能”的关键一步。

目录
  1. 可视化演示
  2. 搜索流程定位
  3. 候选草图基础
  4. 注解策略类型
  5. 注解执行流程
  6. 代价模型反馈
  7. 硬件适配优化
  8. 工程实践要点
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示Ansor搜索流水线中规则注解的关键作用:为草图添加Tile/Unroll/Vectorize等参数标注,并生成完整的搜索空间,供代价模型评估。

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

流水线:四阶段搜索 草图→规则注解→代价模型→微调 调度=注解(草图,参数) 规则注解为每个候选草图补充优化参数标注。

搜索流程定位

装修公司里,工长拿到户型草图后,不会直接让工人开工,而是先确认自己在整个施工流程中的位置:前面是设计师的方案,后面是业主的验收。只有把这一步想清楚,才知道哪些细节必须由自己补上。规则注解在 Ansor 里也一样,它是搜索流程中承上启下的节点。

Ansor 的自动调优可以拆成四个动作:生成草图、规则注解、代价模型评估、进化搜索。规则注解恰好站在第二棒的位置。它接收候选草图,为草图上的各种循环结构补上具体优化参数,生成真正可评估的候选调度。没有它,草图只是“毛坯房”,代价模型连测量对象都没有。

从数学上看,规则注解产出的候选集合可以写成:

回到装修场景:设计师给出户型,工长补上细节,业主才能验收。规则注解就是那个把所有“待定细节”变成“确定工艺”的关键角色,直接影响后面代价模型能否给出靠谱判断,也最终决定自动调优能不能找到又快又稳的推理方案。

S={ (s, a)∣s∈K,  a∈A(s) }\displaystyle \mathcal{S} = \{\, (s,\, a) \mid s \in \mathcal{K},\; a \in \mathcal{A}(s)\,\}
通俗理解:候选集合由“草图”和“适用于该草图的注解”共同组成,缺一不可。
  1. 候选集合规则注解阶段输出的所有可用调度,统一放入这个集合。
    S\mathcal{S}
  2. 草图个体来自草图生成模块的循环骨架,比如一个未指定分块尺寸的矩阵乘循环。
    s∈Ks \in \mathcal{K}
  3. 合法注解能够作用在这个草图上、不违反硬件约束的参数标注集合。
    a∈A(s)a \in \mathcal{A}(s)
  4. 配对组合把草图和注解放到一起,构成代价模型可以评估的完整调度。
    (s,a)(s, a)

候选草图基础

装修时,户型草图上会留下很多空白:瓷砖尺寸没写、水管走向没标、开关数量没填。这些空白不是设计师偷懒,而是因为不同家庭需求不同,必须留出选择空间。Ansor 的候选草图也是这样——它是只包含循环嵌套结构、但没填具体参数的“半成品”。

这些半成品由草图生成模块自动产生。Ansor 会随机或基于进化策略生成多种不同结构的草图,例如“先分块再融合”“先重排序再拆分”。每一种结构都定义了循环之间的嵌套关系,但真正的分块大小、展开因子、向量长度都还是问号。这正是规则注解大展身手的地方。

用元组来描述一个待注解的草图,可以写成:

就像同一种户型可以做出北欧风、中式风,同一个草图结构也可以搭配完全不同的注解参数。草图的价值在于提供多样化的“骨架”,而规则注解负责在骨架上填肉,让代价模型有丰富的候选方案可以比较。

s=⟨ℓ1,ℓ2,…,ℓk, □1,□2,…,□m⟩\displaystyle s = \langle \ell_1, \ell_2, \dots, \ell_k,\ \square_1, \square_2, \dots, \square_m \rangle
通俗理解:草图 = 一组确定的循环结构 + 一组等待填充的注解槽位。
  1. 循环层序列\(\ell_1,\dots,\ell_k\) 表示草图中的 k 层循环,比如最外层是批次循环,内层是通道循环。
    ℓ1,ℓ2,…,ℓk\ell_1, \ell_2, \dots, \ell_k
  2. 注解槽位\(\square_i\) 代表规则注解将来要填的空格,可以是分块大小、展开因子或向量长度。
    □1,□2,…,□m\square_1, \square_2, \dots, \square_m
  3. 草图元组整体看作“结构 + 留白”的混合体,草图生成只决定结构,参数留给注解器补完。
    s=⟨ℓ1,ℓ2,…,ℓk, □1,□2,…,□m⟩s = \langle \ell_1, \ell_2, \dots, \ell_k,\ \square_1, \square_2, \dots, \square_m \rangle

注解策略类型

工长在施工图上写的标注不是随意的,翻来覆去无非四类:地砖怎么分块、重复工序怎么展开、能不能用大型工具批量施工、先做哪道工序。规则注解也对应四种核心策略:分块(Tile)、展开(Unroll)、向量化(Vectorize)、次序调整(Reorder)。

分块把大循环切成小块,让数据更容易放进缓存;展开把短循环体复制多份,减少循环跳转开销;向量化让一条指令同时处理多个数据,就像用一排电钻同时打一排孔;次序调整则改变循环的执行顺序,让访存更加连续。四者互相配合,构成规则注解的基本工具箱。

一个注解参数组可以形式化地写成:

装修中每一种标注都有适用场景:洗手间必须防水、客厅要防滑,不是越贵越好。同样,分块不是越小越好、向量化不是越长越好,注解策略需要针对目标硬件和算子特征组合使用。这也是为什么规则注解不是简单套模板,而需要搜索和代价模型来把关。

A(s)={ (T,U,V,R)∣T,U,V∈Z+, R∈Perm(ℓ) }\displaystyle \mathcal{A}(s) = \{\, (T, U, V, R) \mid T,U,V \in \mathbb{Z}^{+},\ R \in \text{Perm}(\ell) \,\}
通俗理解:任何一个合法注解,都包含分块大小、展开因子、向量长度和循环次序这四类信息。
  1. 分块大小 T决定大循环被切成多小的块,影响缓存命中率和计算并行度。
    T∈Z+T \in \mathbb{Z}^{+}
  2. 展开因子 U把循环体复制 U 份,减少循环条件判断和跳转指令。
    U∈Z+U \in \mathbb{Z}^{+}
  3. 向量宽度 V表示一次向量指令处理多少个数据元素,上限由硬件 SIMD 宽度决定。
    V∈Z+V \in \mathbb{Z}^{+}
  4. 重排序 R循环层级的排列顺序,例如把访存连续的维度移到内层。
    R∈Perm(ℓ)R \in \text{Perm}(\ell)

注解执行流程

工长写施工标注时,不是每面墙都用同一种砖,而是先看这面墙在哪个位置、承受什么荷载,再决定贴法。规则注解的执行流程也一样:先拿到一批草图,再为每个草图匹配合适的规则,最后把参数填进去,生成一批新调度。

具体来说,Ansor 的规则注解器会遍历草图集合,针对每个循环层尝试不同的候选规则。规则库里预先定义好“可以对哪些循环做分块”“哪些循环适合展开”“向量长度按什么步长枚举”等信息。每次应用一组规则,就产生一个带具体参数的新调度。

这个过程可以写成:

这就像工长把每种铺砖方案都画成小样,交到监理手里评估。规则注解阶段不负责“好不好”,只负责“变出足够多、足够多样的方案”。方案越多,后面的代价模型和进化搜索越能选优,但也意味着搜索空间会变大,因此还需要剪枝和约束来防止“方案爆炸”。

C(s)=⋃a∈A(s)Apply(s,a)\displaystyle \mathcal{C}(s) = \bigcup_{a \in \mathcal{A}(s)} \text{Apply}(s, a)
通俗理解:对同一个草图应用所有合法注解,把得到的新调度全部收集起来。
  1. 应用注解\(\text{Apply}(s, a)\) 表示把注解 a 真正施加到草图 s 上,生成一个完整调度。
    Apply(s,a)\text{Apply}(s, a)
  2. 遍历合法规则\(a \in \mathcal{A}(s)\) 确保我们只尝试当前草图能够接受、且硬件允许的注解。
    a∈A(s)a \in \mathcal{A}(s)
  3. 结果并集把所有应用结果合并,得到当前草图拓展出的候选调度集合。
    C(s)=⋃a∈A(s)Apply(s,a)\mathcal{C}(s) = \bigcup_{a \in \mathcal{A}(s)} \text{Apply}(s, a)

代价模型反馈

装修监理拿到施工图后,不会真的把墙拆了再装一遍来验证效果,而是根据材料单价、工时经验和面积数据快速估个价。Ansor 的代价模型也是这种角色:它不用真的编译运行每个候选调度,而是从调度特征出发,预测这个方案大概能跑多快。

代价模型能否预测得准,很大程度上取决于规则注解提供了什么。一张只写了“贴砖”的图纸没法估价,必须写清楚瓷砖规格、铺贴面积、阴阳角数量;同样,只有被规则注解完整标注过的调度,才能提取出有意义的特征,例如分块后的循环趟数、展开后的指令数、向量化后的访存宽度。

代价模型的核心公式是:

代价模型的预测结果会反馈给搜索循环:进化搜索优先保留预测分数低的候选,生成下一轮草图;新跑出来的真实测量数据又会回来更新 \(\theta\),让下一轮预测更准。这样就形成了一条“注解 → 预测 → 选择 → 再生成”的反馈闭环,如同监理不断校正报价模型,让预算越来越贴近实际。

p^=fθ(ϕ(s,a))\displaystyle \hat{p} = f_{\theta}\bigl(\phi(s, a)\bigr)
通俗理解:代价模型先抽取调度特征,再用机器学习模型预测性能分数。
  1. 特征提取\(\phi(s, a)\) 把草图结构和注解参数转换成数值特征向量。
    ϕ(s,a)\phi(s, a)
  2. 预测模型\(f_{\theta}\) 是带参数 \(\theta\) 的回归模型,例如 XGBoost 或神经网络。
    fθf_{\theta}
  3. 性能预测\(\hat{p}\) 是预测出的性能指标,值越低代表估计运行时间越短。
    p^\hat{p}

硬件适配优化

同样是“贴瓷砖”,在老旧小区和新建豪宅里用的工艺完全不同:承重墙能不能开槽、地面平整度够不够、瓷砖规格是否与房间尺寸匹配。硬件就是 AI 编译器的“户型”,规则注解必须考虑 CPU、GPU、NPU 各自的计算与访存特点。

例如 CPU 的 SIMD 指令集有固定向量宽度,GPU 的线程束宽度决定了并行粒度,NPU 可能只支持特定对齐方式。规则注解在配置向量化参数时,必须先查清硬件寄存器能塞下几个 float32 数据,否则填一个超长向量长度,不仅无法加速,反而可能生成非法指令。

于是向量长度通常满足:

硬件适配还体现在分块大小上:GPU 的分块常按线程束大小对齐,NPU 的分块要贴合片上缓冲区行宽。规则注解把这类硬件约束编码进规则库,让自动调优生成的每一个候选都“长在这套房子的结构里”,最终表现为推理延迟更低、吞吐更高。

V∗=min⁡(Vmax⁡, ⌊block_bytesdtype_bytes⌋)\displaystyle V^* = \min\Bigl(V_{\max},\ \left\lfloor \frac{\mathrm{block\_bytes}}{\mathrm{dtype\_bytes}} \right\rfloor\Bigr)
通俗理解:实际向量长度 = 硬件能接受的最大宽度与分块内元素数的较小值。
  1. 硬件上限\(V_{\max}\) 由目标处理器的 SIMD 宽度决定,是硬约束。
    Vmax⁡V_{\max}
  2. 块内可向量化元素数\(\mathrm{block\_bytes} / \mathrm{dtype\_bytes}\) 计算一个分块内能装多少个数据元素。
    ⌊block_bytesdtype_bytes⌋\left\lfloor \frac{\mathrm{block\_bytes}}{\mathrm{dtype\_bytes}} \right\rfloor
  3. 取最小值两者取小,保证既不超过硬件能力,也不浪费分块内数据。
    V∗=min⁡(…)V^* = \min(\ldots)

工程实践要点

再好的施工标准,也要靠项目经理落地。在 AutoTVM/Ansor 中使用规则注解,同样需要关注工程细节:调优任务参数怎么配、日志怎么看、结果怎么验证。很多开发者把规则注解当成黑盒,结果搜索时间很长却收效甚微。

实践中,首先要设置合理的搜索空间。分块大小太多会让规则注解生成海量候选,代价模型又来不及准确筛选;太少则可能错过最优解。其次要善用调优日志:通过观察每个候选的预估分数与真实测量值的差距,可以判断是代价模型没训好,还是规则库覆盖不够。最后一定要做端到端验证,不能只看单个算子的“纸面加速”。

调优效果的最终检验可以定量描述为:

装修验收看的是入住后的舒适度,而不是施工队搬了多少块砖。规则注解的工程价值同样要用端到端推理性能说话:当你在日志里看到加速比从 1.2 提到 1.8,才真正说明这套“施工图标注”体系没白做。

Speedup=TbaselineTtuned\displaystyle \text{Speedup} = \frac{T_{\text{baseline}}}{T_{\text{tuned}}}
通俗理解:加速比 = 原始版本运行时间 ÷ 调优后运行时间,数值越大收益越明显。
  1. 基线耗时\(T_{\text{baseline}}\) 是未使用自动调优时算子的执行时间。
    TbaselineT_{\text{baseline}}
  2. 调优后耗时\(T_{\text{tuned}}\) 是经过规则注解和搜索优化后的执行时间。
    TtunedT_{\text{tuned}}
  3. 加速比两者相除得到最终收益指标,便于在不同硬件和模型间横向比较。
    Speedup=TbaselineTtuned\text{Speedup} = \frac{T_{\text{baseline}}}{T_{\text{tuned}}}

AI 应用场景

云端推理服务自动调优

阶段:模型部署与推理性能优化

解决的问题:传统手工调优依赖资深工程师的经验,面对 Transformer 这类复杂模型,手工编写分块、展开和向量化参数往往耗时数天,且换一个硬件就要重来。规则注解将“凭经验试”变为“自动生成候选 + 代价模型筛选”,让调优过程在几小时内完成。

具体案例:某云服务商部署 GPT 类模型时,用 Ansor 对 attention 中的矩阵乘和 softmax 算子做自动调优。规则注解自动尝试不同分块与向量化组合,在一夜之间找到比手写 CUDA 版本快约 20% 的调度配置,并将调优流程沉淀为可复用流水线。

自研 NPU 编译器算子适配

阶段:AI 芯片编译器后端开发

解决的问题:新 NPU 的指令集与通用编译器之间存在语义鸿沟,通用规则注解产生的调度可能无法利用特殊向量指令或片上缓冲。通过在规则库中定义针对 NPU 的注解约束,可以让自动调优贴合芯片特性。

具体案例:某芯片公司为自研 NPU 编写 TVM 后端时,为卷积算子增加自定义规则注解:强制向量化长度为 16,分块大小与片上缓冲区行宽对齐。规则注解配合代价模型搜索,使核心算子利用率从 42% 提升到 78%。

Python 演示

python47 行
import itertools  class LoopAnnotator:    """简化版规则注解器:为循环草图生成参数并打分"""     def __init__(self, loop_size=128):        self.loop_size = loop_size        self.best_config = None        self.best_score = float('inf')        self.candidate_count = 0     def gen_configs(self):        # 枚举分块、展开、向量化参数组合        tiles = [16, 32, 64]        unrolls = [1, 2, 4]        vecs = [1, 4, 8]        for t, u, v in itertools.product(tiles, unrolls, vecs):            if t * u * v > self.loop_size * 2:                continue            yield {'tile': t, 'unroll': u, 'vector': v}     def cost_model(self, cfg):        # 简单代价模型:循环趟数少则开销小        n = self.loop_size        trips = (n / cfg['tile']) ** 2        mem = (n ** 2) / cfg['tile']        loops = trips / (cfg['unroll'] * cfg['vector'])        return loops * 0.4 + mem * 0.6     def annotate(self):        # 遍历所有候选并用代价模型打分        for cfg in self.gen_configs():            score = self.cost_model(cfg)            self.candidate_count += 1            if score < self.best_score:                self.best_score = score                self.best_config = cfg        return self.best_config, self.best_score  if __name__ == '__main__':    annotator = LoopAnnotator(loop_size=128)    best_cfg, score = annotator.annotate()    print('candidates:', annotator.candidate_count)    print('best config:', best_cfg)    print('predicted cost:', round(score, 4))
目的:模拟 Ansor 规则注解的核心逻辑——枚举分块、展开、向量化组合,用简单代价模型筛选最优配置。
  1. for t, u, v in itertools.product(tiles, unrolls, vecs):枚举所有规则注解组合,相当于施工队把所有铺砖方案都列出来。
  2. if t * u * v > self.loop_size * 2: continue剪枝非法候选,防止注解组合造成循环爆炸。
  3. yield {'tile': t, 'unroll': u, 'vector': v}生成一个带具体参数的调度配置,交给后续评分。
  4. trips = (n / cfg['tile']) ** 2计算分块后循环趟数,反映分块对计算粒度的影响。
  5. loops = trips / (cfg['unroll'] * cfg['vector'])展开和向量化共同减少实际循环开销。
  6. self.candidate_count += 1统计候选数量,对应注解阶段的遍历流程。
  7. if score < self.best_score:用代价模型选最优,模拟真实调优中的搜索反馈。

常见误区

误区规则注解就是老工程师手工写出分块大小和展开因子,跟填配置文件差不多,哪有那么多算法。

正解规则注解本质是自动化搜索过程:它自动遍历规则空间、生成大量参数化候选,并用代价模型筛选。正如工长不需要亲手搬每块砖,但要指挥系统完成标注;Ansor 的规则注解是“自动施工标注生成器”,不是手工配置表。

为何常见:AutoTVM 时代的模板需要人工编写,容易让人误以为 Ansor 的规则注解仍是人工操作。

误区把所有循环都展开、把分块分到最小、把向量长度拉到最大,性能肯定最好。

正解展开过度会撑爆指令缓存,分块过小会增加访存次数,向量长度超过硬件宽度会生成非法调度。规则注解的目标是在多种约束间寻找平衡,由代价模型预测性能,而不是机械地堆叠优化策略。

为何常见:直觉上认为“优化得越狠越好”,忽略了硬件资源和搜索代价的约束。

误区AutoTVM 的 Compute Template 就是 Ansor 的 Rule Annotation,两者只是名字不同。

正解AutoTVM 依赖人工为每个算子编写代码模板并留下可调参数;Ansor 自动生成草图后用规则注解填充,不再需要人工设计模板。规则注解是模板的“自动版”,这正是 Ansor 减少人工参与的核心突破。

为何常见:两者都出现在 TVM 调优流程中,且都涉及参数配置,容易被当成同一件事。

误区规则注解生成的所有配置都会先真实跑一遍硬件,代价模型只是最后排个名次。

正解真实硬件测量成本很高,代价模型是在搜索过程中持续使用的在线预测器。每个候选在真实运行前先经过它打分,真实数据再反馈回来更新模型参数,形成滚动优化闭环。

为何常见:对代价模型的在线学习方法不了解,误以为它只是静态评分表。

面试题

Q1. 在 Ansor 搜索流程中,规则注解的输入是什么?

单选题 · easy

  • A.完整编译后的可执行文件
  • B.训练好的神经网络权重
  • C.候选调度草图结构← 正确答案
  • D.最终推理结果张量数据

答案:C。规则注解由草图生成模块驱动,它接收的是尚未填写具体优化参数的候选草图。可执行文件、权重和推理结果都是调优流程之外或之后的东西,不是它的输入。

Q2. 以下哪项通常不属于规则注解负责标注的内容?

单选题 · easy

  • A.循环分块大小
  • B.循环展开因子
  • C.循环执行次序
  • D.网络权重数值← 正确答案

答案:D。规则注解关注的是循环结构上的优化参数,例如分块、展开、向量化和重排序。网络权重数值属于模型训练产物,编译器调优不会在规则注解阶段修改权重。

Q3. 公式 \(\mathcal{S} = \{(s,a) \mid s \in \mathcal{K}, a \in \mathcal{A}(s)\}\) 中,\(\mathcal{A}(s)\) 表示什么?

单选题 · mid

  • A.草图 s 的合法注解集合← 正确答案
  • B.所有候选张量集合
  • C.所有硬件设备集合
  • D.损失函数参数集合

答案:A。\(\mathcal{A}(s)\) 表示适用于特定草图 s 的注解参数集合,它是从规则库中匹配出来的合法选项。其他选项与候选调度集合的语义无关。

Q4. 规则注解包含哪些主要策略类型?

多选题 · mid

  • A.分块大小标注← 正确答案
  • B.循环展开标注← 正确答案
  • C.权重剪枝标注
  • D.向量化长度标注← 正确答案

答案:ABD。规则注解的四大策略是分块、展开、向量化和次序调整。权重剪枝属于模型压缩范畴,不是在调度图上做循环变换。

Q5. 为何注解后的调度要交给代价模型而不是直接上硬件跑?

单选题 · mid

  • A.硬件无法执行注解答
  • B.真机测量代价太高
  • C.代价模型预测更快← 正确答案
  • D.规则注解没有输出

答案:C。真实硬件测量一次可能要毫秒甚至更久,候选数量成千上万时开销不可接受。代价模型通过提取调度特征快速预测性能,把待实测的候选压缩到很小范围。其他选项描述都不准确。

Q6. 关于 Ansor 与 AutoTVM 在规则/模板上的区别,下列说法正确的是?

多选题 · hard

  • A.AutoTVM 需人工设计模板← 正确答案
  • B.Ansor 自动生成草图填充← 正确答案
  • C.两者都不需要人参与
  • D.Ansor 规则替代人工模板← 正确答案

答案:ABD。AutoTVM 的模板由专家手工编写,Ansor 通过草图生成加规则注解自动完成模板设计,所以 C 说“两者都不需要人参与”是错误的,A、B、D 均正确。

Q7. 硬件向量宽度为 512 位,数据类型为 float32(32 位),单条向量指令最多可处理几个元素?

单选题 · mid

  • A.四个元素
  • B.八个元素
  • C.十六个元素← 正确答案
  • D.三十二个元素

答案:C。512 位除以 32 位等于 16,所以理论上一条向量指令最多处理 16 个 float32 元素。这对应规则注解中向量化参数的硬件上限。

Q8. 规则注解阶段可能遇到哪些问题?

多选题 · mid

  • A.搜索空间过大← 正确答案
  • B.非法参数组合← 正确答案
  • C.代价模型不准← 正确答案
  • D.草图生成完美

答案:ABC。参数组合过多会造成搜索空间爆炸,部分组合超过硬件约束需要剪枝,代价模型预测误差会误导搜索。但“草图生成完美”不是规则注解阶段的问题,因为草图本身只是候选起点。

Q9. 代价模型在规则注解搜索中的角色是什么?

单选题 · hard

  • A.只记录最终结果
  • B.在线预测调度性能← 正确答案
  • C.直接生成分块参数
  • D.替代编译器代码生成

答案:B。代价模型根据 \(\phi(s,a)\) 提取的特征在线预测候选调度的性能,反馈给进化搜索,同时用真实测量数据更新自身。分块参数由规则注解枚举产生,代码生成由编译后端完成。

Q10. 简化示例代码中,annotate() 如何选出最优配置?

单选题 · easy

  • A.遍历候选代价打分← 正确答案
  • B.随机选取首个配置
  • C.只按分块大小排序
  • D.依赖真实硬件测量

答案:A。annotate() 遍历 gen_configs() 生成的所有候选,用 cost_model() 打分并保留最低分配置。这模拟了规则注解中“生成候选 → 代价模型筛选”的核心思想。