想象你请装修公司改造一套房子。设计师先画出一张户型草图:客厅、卧室、卫生间的位置都标好了,但瓷砖用多大、水电怎么走、墙砖怎么贴、施工顺序怎么安排,图纸上全是空白。施工队看到草图没法开工,必须由经验丰富的工长在图上补上细节标注:地砖 600×600、墙砖 300×600、先布管再贴砖……这些标注决定了装修成本、工期和最终居住体验。 在 AI 编译器 AutoTVM/Ansor 的调优流程中,也有这样一位“工长”——规则注解(Rule Annotation)。它接收前一阶段生成的候选调度草图,把分块大小、循环展开、向量化等优化参数一一标注上去,形成一份供代价模型评估的完整“施工图”。没有这些细节,编译器就无法判断这个调度好不好,自动调优就成了无米之炊。 本文用“装修施工图”这条主线,带你从零认识规则注解:它从哪里来、怎么工作、如何与代价模型配合,以及在实际 AI 工程中怎么用好它。
理解规则注解,等于掌握了 AI 编译器自动调优的“施工工艺图”。对 AI 从业者而言,这是从“会调模型”走向“能优化推理性能”的关键一步。
可视化演示
展示Ansor搜索流水线中规则注解的关键作用:为草图添加Tile/Unroll/Vectorize等参数标注,并生成完整的搜索空间,供代价模型评估。
本节配有交互动效(进入视口后自动加载,约 3KB)。
流水线:四阶段搜索 草图→规则注解→代价模型→微调 调度=注解(草图,参数) 规则注解为每个候选草图补充优化参数标注。
搜索流程定位
装修公司里,工长拿到户型草图后,不会直接让工人开工,而是先确认自己在整个施工流程中的位置:前面是设计师的方案,后面是业主的验收。只有把这一步想清楚,才知道哪些细节必须由自己补上。规则注解在 Ansor 里也一样,它是搜索流程中承上启下的节点。
Ansor 的自动调优可以拆成四个动作:生成草图、规则注解、代价模型评估、进化搜索。规则注解恰好站在第二棒的位置。它接收候选草图,为草图上的各种循环结构补上具体优化参数,生成真正可评估的候选调度。没有它,草图只是“毛坯房”,代价模型连测量对象都没有。
从数学上看,规则注解产出的候选集合可以写成:
回到装修场景:设计师给出户型,工长补上细节,业主才能验收。规则注解就是那个把所有“待定细节”变成“确定工艺”的关键角色,直接影响后面代价模型能否给出靠谱判断,也最终决定自动调优能不能找到又快又稳的推理方案。
- 候选集合规则注解阶段输出的所有可用调度,统一放入这个集合。
- 草图个体来自草图生成模块的循环骨架,比如一个未指定分块尺寸的矩阵乘循环。
- 合法注解能够作用在这个草图上、不违反硬件约束的参数标注集合。
- 配对组合把草图和注解放到一起,构成代价模型可以评估的完整调度。
候选草图基础
装修时,户型草图上会留下很多空白:瓷砖尺寸没写、水管走向没标、开关数量没填。这些空白不是设计师偷懒,而是因为不同家庭需求不同,必须留出选择空间。Ansor 的候选草图也是这样——它是只包含循环嵌套结构、但没填具体参数的“半成品”。
这些半成品由草图生成模块自动产生。Ansor 会随机或基于进化策略生成多种不同结构的草图,例如“先分块再融合”“先重排序再拆分”。每一种结构都定义了循环之间的嵌套关系,但真正的分块大小、展开因子、向量长度都还是问号。这正是规则注解大展身手的地方。
用元组来描述一个待注解的草图,可以写成:
就像同一种户型可以做出北欧风、中式风,同一个草图结构也可以搭配完全不同的注解参数。草图的价值在于提供多样化的“骨架”,而规则注解负责在骨架上填肉,让代价模型有丰富的候选方案可以比较。
- 循环层序列\(\ell_1,\dots,\ell_k\) 表示草图中的 k 层循环,比如最外层是批次循环,内层是通道循环。
- 注解槽位\(\square_i\) 代表规则注解将来要填的空格,可以是分块大小、展开因子或向量长度。
- 草图元组整体看作“结构 + 留白”的混合体,草图生成只决定结构,参数留给注解器补完。
注解策略类型
工长在施工图上写的标注不是随意的,翻来覆去无非四类:地砖怎么分块、重复工序怎么展开、能不能用大型工具批量施工、先做哪道工序。规则注解也对应四种核心策略:分块(Tile)、展开(Unroll)、向量化(Vectorize)、次序调整(Reorder)。
分块把大循环切成小块,让数据更容易放进缓存;展开把短循环体复制多份,减少循环跳转开销;向量化让一条指令同时处理多个数据,就像用一排电钻同时打一排孔;次序调整则改变循环的执行顺序,让访存更加连续。四者互相配合,构成规则注解的基本工具箱。
一个注解参数组可以形式化地写成:
装修中每一种标注都有适用场景:洗手间必须防水、客厅要防滑,不是越贵越好。同样,分块不是越小越好、向量化不是越长越好,注解策略需要针对目标硬件和算子特征组合使用。这也是为什么规则注解不是简单套模板,而需要搜索和代价模型来把关。
- 分块大小 T决定大循环被切成多小的块,影响缓存命中率和计算并行度。
- 展开因子 U把循环体复制 U 份,减少循环条件判断和跳转指令。
- 向量宽度 V表示一次向量指令处理多少个数据元素,上限由硬件 SIMD 宽度决定。
- 重排序 R循环层级的排列顺序,例如把访存连续的维度移到内层。
注解执行流程
工长写施工标注时,不是每面墙都用同一种砖,而是先看这面墙在哪个位置、承受什么荷载,再决定贴法。规则注解的执行流程也一样:先拿到一批草图,再为每个草图匹配合适的规则,最后把参数填进去,生成一批新调度。
具体来说,Ansor 的规则注解器会遍历草图集合,针对每个循环层尝试不同的候选规则。规则库里预先定义好“可以对哪些循环做分块”“哪些循环适合展开”“向量长度按什么步长枚举”等信息。每次应用一组规则,就产生一个带具体参数的新调度。
这个过程可以写成:
这就像工长把每种铺砖方案都画成小样,交到监理手里评估。规则注解阶段不负责“好不好”,只负责“变出足够多、足够多样的方案”。方案越多,后面的代价模型和进化搜索越能选优,但也意味着搜索空间会变大,因此还需要剪枝和约束来防止“方案爆炸”。
- 应用注解\(\text{Apply}(s, a)\) 表示把注解 a 真正施加到草图 s 上,生成一个完整调度。
- 遍历合法规则\(a \in \mathcal{A}(s)\) 确保我们只尝试当前草图能够接受、且硬件允许的注解。
- 结果并集把所有应用结果合并,得到当前草图拓展出的候选调度集合。
代价模型反馈
装修监理拿到施工图后,不会真的把墙拆了再装一遍来验证效果,而是根据材料单价、工时经验和面积数据快速估个价。Ansor 的代价模型也是这种角色:它不用真的编译运行每个候选调度,而是从调度特征出发,预测这个方案大概能跑多快。
代价模型能否预测得准,很大程度上取决于规则注解提供了什么。一张只写了“贴砖”的图纸没法估价,必须写清楚瓷砖规格、铺贴面积、阴阳角数量;同样,只有被规则注解完整标注过的调度,才能提取出有意义的特征,例如分块后的循环趟数、展开后的指令数、向量化后的访存宽度。
代价模型的核心公式是:
代价模型的预测结果会反馈给搜索循环:进化搜索优先保留预测分数低的候选,生成下一轮草图;新跑出来的真实测量数据又会回来更新 \(\theta\),让下一轮预测更准。这样就形成了一条“注解 → 预测 → 选择 → 再生成”的反馈闭环,如同监理不断校正报价模型,让预算越来越贴近实际。
- 特征提取\(\phi(s, a)\) 把草图结构和注解参数转换成数值特征向量。
- 预测模型\(f_{\theta}\) 是带参数 \(\theta\) 的回归模型,例如 XGBoost 或神经网络。
- 性能预测\(\hat{p}\) 是预测出的性能指标,值越低代表估计运行时间越短。
硬件适配优化
同样是“贴瓷砖”,在老旧小区和新建豪宅里用的工艺完全不同:承重墙能不能开槽、地面平整度够不够、瓷砖规格是否与房间尺寸匹配。硬件就是 AI 编译器的“户型”,规则注解必须考虑 CPU、GPU、NPU 各自的计算与访存特点。
例如 CPU 的 SIMD 指令集有固定向量宽度,GPU 的线程束宽度决定了并行粒度,NPU 可能只支持特定对齐方式。规则注解在配置向量化参数时,必须先查清硬件寄存器能塞下几个 float32 数据,否则填一个超长向量长度,不仅无法加速,反而可能生成非法指令。
于是向量长度通常满足:
硬件适配还体现在分块大小上:GPU 的分块常按线程束大小对齐,NPU 的分块要贴合片上缓冲区行宽。规则注解把这类硬件约束编码进规则库,让自动调优生成的每一个候选都“长在这套房子的结构里”,最终表现为推理延迟更低、吞吐更高。
- 硬件上限\(V_{\max}\) 由目标处理器的 SIMD 宽度决定,是硬约束。
- 块内可向量化元素数\(\mathrm{block\_bytes} / \mathrm{dtype\_bytes}\) 计算一个分块内能装多少个数据元素。
- 取最小值两者取小,保证既不超过硬件能力,也不浪费分块内数据。
工程实践要点
再好的施工标准,也要靠项目经理落地。在 AutoTVM/Ansor 中使用规则注解,同样需要关注工程细节:调优任务参数怎么配、日志怎么看、结果怎么验证。很多开发者把规则注解当成黑盒,结果搜索时间很长却收效甚微。
实践中,首先要设置合理的搜索空间。分块大小太多会让规则注解生成海量候选,代价模型又来不及准确筛选;太少则可能错过最优解。其次要善用调优日志:通过观察每个候选的预估分数与真实测量值的差距,可以判断是代价模型没训好,还是规则库覆盖不够。最后一定要做端到端验证,不能只看单个算子的“纸面加速”。
调优效果的最终检验可以定量描述为:
装修验收看的是入住后的舒适度,而不是施工队搬了多少块砖。规则注解的工程价值同样要用端到端推理性能说话:当你在日志里看到加速比从 1.2 提到 1.8,才真正说明这套“施工图标注”体系没白做。
- 基线耗时\(T_{\text{baseline}}\) 是未使用自动调优时算子的执行时间。
- 调优后耗时\(T_{\text{tuned}}\) 是经过规则注解和搜索优化后的执行时间。
- 加速比两者相除得到最终收益指标,便于在不同硬件和模型间横向比较。
AI 应用场景
云端推理服务自动调优
阶段:模型部署与推理性能优化
解决的问题:传统手工调优依赖资深工程师的经验,面对 Transformer 这类复杂模型,手工编写分块、展开和向量化参数往往耗时数天,且换一个硬件就要重来。规则注解将“凭经验试”变为“自动生成候选 + 代价模型筛选”,让调优过程在几小时内完成。
具体案例:某云服务商部署 GPT 类模型时,用 Ansor 对 attention 中的矩阵乘和 softmax 算子做自动调优。规则注解自动尝试不同分块与向量化组合,在一夜之间找到比手写 CUDA 版本快约 20% 的调度配置,并将调优流程沉淀为可复用流水线。
自研 NPU 编译器算子适配
阶段:AI 芯片编译器后端开发
解决的问题:新 NPU 的指令集与通用编译器之间存在语义鸿沟,通用规则注解产生的调度可能无法利用特殊向量指令或片上缓冲。通过在规则库中定义针对 NPU 的注解约束,可以让自动调优贴合芯片特性。
具体案例:某芯片公司为自研 NPU 编写 TVM 后端时,为卷积算子增加自定义规则注解:强制向量化长度为 16,分块大小与片上缓冲区行宽对齐。规则注解配合代价模型搜索,使核心算子利用率从 42% 提升到 78%。
Python 演示
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))
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}生成一个带具体参数的调度配置,交给后续评分。trips = (n / cfg['tile']) ** 2计算分块后循环趟数,反映分块对计算粒度的影响。loops = trips / (cfg['unroll'] * cfg['vector'])展开和向量化共同减少实际循环开销。self.candidate_count += 1统计候选数量,对应注解阶段的遍历流程。if score < self.best_score:用代价模型选最优,模拟真实调优中的搜索反馈。
常见误区
误区规则注解就是老工程师手工写出分块大小和展开因子,跟填配置文件差不多,哪有那么多算法。
正解规则注解本质是自动化搜索过程:它自动遍历规则空间、生成大量参数化候选,并用代价模型筛选。正如工长不需要亲手搬每块砖,但要指挥系统完成标注;Ansor 的规则注解是“自动施工标注生成器”,不是手工配置表。
为何常见:AutoTVM 时代的模板需要人工编写,容易让人误以为 Ansor 的规则注解仍是人工操作。
误区把所有循环都展开、把分块分到最小、把向量长度拉到最大,性能肯定最好。
正解展开过度会撑爆指令缓存,分块过小会增加访存次数,向量长度超过硬件宽度会生成非法调度。规则注解的目标是在多种约束间寻找平衡,由代价模型预测性能,而不是机械地堆叠优化策略。
为何常见:直觉上认为“优化得越狠越好”,忽略了硬件资源和搜索代价的约束。
误区AutoTVM 的 Compute Template 就是 Ansor 的 Rule Annotation,两者只是名字不同。
正解AutoTVM 依赖人工为每个算子编写代码模板并留下可调参数;Ansor 自动生成草图后用规则注解填充,不再需要人工设计模板。规则注解是模板的“自动版”,这正是 Ansor 减少人工参与的核心突破。
为何常见:两者都出现在 TVM 调优流程中,且都涉及参数配置,容易被当成同一件事。
误区规则注解生成的所有配置都会先真实跑一遍硬件,代价模型只是最后排个名次。
正解真实硬件测量成本很高,代价模型是在搜索过程中持续使用的在线预测器。每个候选在真实运行前先经过它打分,真实数据再反馈回来更新模型参数,形成滚动优化闭环。
为何常见:对代价模型的在线学习方法不了解,误以为它只是静态评分表。
面试题
Q1. 在 Ansor 搜索流程中,规则注解的输入是什么?
- A.完整编译后的可执行文件
- B.训练好的神经网络权重
- C.候选调度草图结构← 正确答案
- D.最终推理结果张量数据
答案:C。规则注解由草图生成模块驱动,它接收的是尚未填写具体优化参数的候选草图。可执行文件、权重和推理结果都是调优流程之外或之后的东西,不是它的输入。
Q2. 以下哪项通常不属于规则注解负责标注的内容?
- A.循环分块大小
- B.循环展开因子
- C.循环执行次序
- D.网络权重数值← 正确答案
答案:D。规则注解关注的是循环结构上的优化参数,例如分块、展开、向量化和重排序。网络权重数值属于模型训练产物,编译器调优不会在规则注解阶段修改权重。
Q3. 公式 \(\mathcal{S} = \{(s,a) \mid s \in \mathcal{K}, a \in \mathcal{A}(s)\}\) 中,\(\mathcal{A}(s)\) 表示什么?
- A.草图 s 的合法注解集合← 正确答案
- B.所有候选张量集合
- C.所有硬件设备集合
- D.损失函数参数集合
答案:A。\(\mathcal{A}(s)\) 表示适用于特定草图 s 的注解参数集合,它是从规则库中匹配出来的合法选项。其他选项与候选调度集合的语义无关。
Q4. 规则注解包含哪些主要策略类型?
- A.分块大小标注← 正确答案
- B.循环展开标注← 正确答案
- C.权重剪枝标注
- D.向量化长度标注← 正确答案
答案:ABD。规则注解的四大策略是分块、展开、向量化和次序调整。权重剪枝属于模型压缩范畴,不是在调度图上做循环变换。
Q5. 为何注解后的调度要交给代价模型而不是直接上硬件跑?
- A.硬件无法执行注解答
- B.真机测量代价太高
- C.代价模型预测更快← 正确答案
- D.规则注解没有输出
答案:C。真实硬件测量一次可能要毫秒甚至更久,候选数量成千上万时开销不可接受。代价模型通过提取调度特征快速预测性能,把待实测的候选压缩到很小范围。其他选项描述都不准确。
Q6. 关于 Ansor 与 AutoTVM 在规则/模板上的区别,下列说法正确的是?
- A.AutoTVM 需人工设计模板← 正确答案
- B.Ansor 自动生成草图填充← 正确答案
- C.两者都不需要人参与
- D.Ansor 规则替代人工模板← 正确答案
答案:ABD。AutoTVM 的模板由专家手工编写,Ansor 通过草图生成加规则注解自动完成模板设计,所以 C 说“两者都不需要人参与”是错误的,A、B、D 均正确。
Q7. 硬件向量宽度为 512 位,数据类型为 float32(32 位),单条向量指令最多可处理几个元素?
- A.四个元素
- B.八个元素
- C.十六个元素← 正确答案
- D.三十二个元素
答案:C。512 位除以 32 位等于 16,所以理论上一条向量指令最多处理 16 个 float32 元素。这对应规则注解中向量化参数的硬件上限。
Q8. 规则注解阶段可能遇到哪些问题?
- A.搜索空间过大← 正确答案
- B.非法参数组合← 正确答案
- C.代价模型不准← 正确答案
- D.草图生成完美
答案:ABC。参数组合过多会造成搜索空间爆炸,部分组合超过硬件约束需要剪枝,代价模型预测误差会误导搜索。但“草图生成完美”不是规则注解阶段的问题,因为草图本身只是候选起点。
Q9. 代价模型在规则注解搜索中的角色是什么?
- A.只记录最终结果
- B.在线预测调度性能← 正确答案
- C.直接生成分块参数
- D.替代编译器代码生成
答案:B。代价模型根据 \(\phi(s,a)\) 提取的特征在线预测候选调度的性能,反馈给进化搜索,同时用真实测量数据更新自身。分块参数由规则注解枚举产生,代码生成由编译后端完成。
Q10. 简化示例代码中,annotate() 如何选出最优配置?
- A.遍历候选代价打分← 正确答案
- B.随机选取首个配置
- C.只按分块大小排序
- D.依赖真实硬件测量
答案:A。annotate() 遍历 gen_configs() 生成的所有候选,用 cost_model() 打分并保留最低分配置。这模拟了规则注解中“生成候选 → 代价模型筛选”的核心思想。