想象你是一家中央厨房的总调度师。厨房里有很多厨师(硬件线程)、很多灶台(计算单元)、很多食材(数据)。顾客点的是一道需要反复烹饪的菜——比如一百份红烧肉。如果让一位厨师从头到尾一锅一锅地炖,效率低得惊人。聪明的总调度师会怎么做?他会先分块:把一百份肉分成十组;再并行:让十位厨师同时开火;再把调料预先备好放近处——这就是调度。 TVMScript就是这份厨房操作手册的“速记语言”。它让你用熟悉的Python语法,指挥TVM编译器底层那些“厨师”如何安排工序。而元调度原语,就是手册里的高级指令:循环分块、向量化、循环融合、缓存安排、线程协作……你可以像搭积木一样自由组合它们,把同一个算子调度出截然不同的执行效率。 本文将通过“中央厨房”这个比喻,带你从零看懂TVMScript元调度原语组合编程:先认识编程模型,再逐个学习高级调度原语,最后学会组合使用并解决实际问题。

岗位关联

掌握TVMScript元调度原语,等于拿到了AI编译器性能优化的“总钥匙”。无论你做算子开发、推理加速还是框架适配,都能用这套组合拳把硬件性能榨干,是AI编译器工程师的核心竞争力。

目录
  1. 可视化演示
  2. 编程模型
  3. 常用原语
  4. 组合策略
  5. 缓存优化
  6. 并行协作
  7. 性能调优
  8. 实战落地
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示TVMScript五大调度原语分类、原语组合优化流水线(blockize→tensorize→compute_at)、以及元调度自动搜索空间探索,切换模式查看不同调度层面。

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

调度原语:五大类优化操作 循环绑定(LB)、布局变换(LT)、计算位置(CL) 线程绑定(TB)、张量化(TZ)映射硬件指令 5 Primitives → Combinatorial Space 原语分类覆盖循环/布局/计算/线程/张量五大维度。

编程模型

回到我们的中央厨房。总调度师手上有一本厚厚的标准菜谱,也就是 TVM 的 TIR(Tensor Intermediate Representation)——一份用循环、读写、计算写成的“底层工序表”。传统做法是让厨师逐行背诵菜谱,而 TVMScript 给了你一支魔法笔:直接用 Python 语法,在这份工序表上做批注、划线、重组。你写的不是新菜谱,而是一张“怎么执行菜谱”的便签。

TVMScript 的调度脚本长什么样?你创建一个调度器对象 S = Schedule(mod),它就像一个贴身助理,把你对循环结构的每一次修改都记录在案。通过 S.get_block("compute") 选中某个算子块,再通过 S.split(...)、S.vectorize(...) 等原语发出指令。整个过程完全在 Python 层面完成,不需要手写底层 IR,也不需要重写算法——你只改变“执行方式”,不改变“计算结果”。

用数学语言表达,调度器是在原计算图 $\mathcal{G}$ 上做循环结构映射:

这节告诉我们:调度原语组合的威力再大,也不能改变计算结果。正如总调度师可以随意安排灶台与厨师,但端上桌的必须还是那道红烧肉。

S:Ginit→Gsched,s.t. Semantics(Ginit)=Semantics(Gsched)\mathcal{S}: \mathcal{G}_{\text{init}} \rightarrow \mathcal{G}_{\text{sched}},\quad \text{s.t. Semantics}(\mathcal{G}_{\text{init}}) = \text{Semantics}(\mathcal{G}_{\text{sched}})
这个公式的意思是:调度器把一个初始的“工序图”改写成新的“工序图”,但严格要求前后计算出的结果完全一致。
  1. 初始计算图$\mathcal{G}_{\text{init}}$ 表示 TVM 默认生成的未优化计算流程,如同一份未拆分的菜谱。
    Ginit:for i in range(N): compute(A[i])\mathcal{G}_{\text{init}}: \text{for } i \text{ in range}(N): \text{ compute}(A[i])
  2. 调度后计算图$\mathcal{G}_{\text{sched}}$ 是经过 split、vectorize 等原语改写后的新流程,如同把大工序拆成并行小组。
    Gsched:for i in range(N/4): vectorize(A[i∗4:i∗4+4])\mathcal{G}_{\text{sched}}: \text{for } i \text{ in range}(N/4): \text{ vectorize}(A[i*4:i*4+4])
  3. 语义等价约束$\text{Semantics}(\cdot)$ 表示“最终炒出的菜不变”,这是所有调度的底线。
    Semantics(Ginit)=Semantics(Gsched)\text{Semantics}(\mathcal{G}_{\text{init}}) = \text{Semantics}(\mathcal{G}_{\text{sched}})

常用原语

厨房里的总调度师有哪些“常用指令”?第一个是循环分块(split):把“一百份肉一锅炖”拆成“每十份一组,分十批处理”。第二个是向量化(vectorize):让一个厨师同时拿四个锅铲,一次翻四份肉——对应 CPU/GPU 的 SIMD 指令。第三个是循环融合(fuse):把相邻两道工序放在同一段流水线上,避免厨师来回跑动。第四个是重排序(reorder):调整多个循环的先后顺序,让数据访问更顺路。

在 TVMScript 中,这些指令一行一个。例如对一个深度学习算子(如矩阵乘加偏置):

这就像在厨房操作手册上写下:“第 4 组到第 7 组肉,用四锅连炒,先摆盘后开火。”每一步原语都对应一个数学变换——把一段循环轴拆成两个嵌套轴:

分块、向量化、融合这些原语,本质上都是“重新编排循环结构”。总调度师不必换厨具、不必换食材,只需在流水线上调整节奏,就能让效率天差地别。

loop: i∈[0,N)→split(io,ii)∈[0,N/B)×[0,B)\text{loop: } i \in [0, N) \quad \xrightarrow{\text{split}} \quad (i_o, i_i) \in [0, N/B) \times [0, B)
split 把一条长度为 $N$ 的循环轴,拆成外层长度为 $N/B$、内层长度为 $B$ 的两条嵌套轴。
  1. 原始循环轴$i$ 从 0 遍历到 $N-1$,代表一长串串行工作。
    i∈[0,N)i \in [0, N)
  2. 拆分因子 B确定每个批次处理多少份(如 B=4),拆出外轴 $i_o$ 和内轴 $i_i$。
    (io,ii)∈[0,N/B)×[0,B)(i_o, i_i) \in [0, N/B) \times [0, B)
  3. 等价关系内层轴连续取 B 个值,与外层轴拼起来正好等价于原来的 $i$。
    i=io×B+iii = i_o \times B + i_i
python6 行
sch = tvm.tir.Schedule(mod)block = sch.get_block("compute")i, j = sch.get_loops(block)i_outer, i_inner = sch.split(i, factors=[None, 4])sch.vectorize(i_inner)sch.reorder(j, i_outer, i_inner)

组合策略

单个原语只是“一招鲜”,真正厉害的是组合。中央厨房的效率巅峰,来自把 split、vectorize、fuse 和 reorder 嵌套使用。比如一个复杂算子,有行循环和列循环两层。你可以先把外层行循环 split 成大块,再把内层列循环 vectorize,最后把大块与某个相邻算子 fuse 起来,减少一次全量数据搬运。

组合策略的关键经验是:先切后融、多级嵌套、语义保持。先切是为了暴露并行度;融合是为了减少中间结果的落地;到每一步都要用 TVM 的 T.parallel、T.vectorized 等标注来表达你期望的硬件执行方式。看一个典型的组合示例:

这个组合在数学上对应一个多维循环空间的分块与重排。设原始迭代空间为 $I \times J$,经过两级 split 后变成四层嵌套:

组合策略的诀窍在心中:只有像搭积木一样理解每个原语的“变形效果”,才能面对新算子时信手拈来。总调度师不是记死菜谱,而是根据食材形状灵活调整切法。

(I×J)  ↦  (Io×Ii)×(Jo×Ji)(I \times J) \;\mapsto\; (I_o \times I_i) \times (J_o \times J_i)
二维迭代空间被切成一个个大小为 $I_i \times J_i$ 的小瓷砖(tile),这正是 Tiling 优化的数学本质。
  1. 原始二维空间$(I \times J)$ 表示行、列两层循环构成的迭代空间,如同一整块大面团。
    (I×J)(I \times J)
  2. 行轴拆分行轴 $I$ 拆成外块 $I_o$ 与内块 $I_i$,列轴同理。
    I→Io×Ii,J→Jo×JiI \to I_o \times I_i,\quad J \to J_o \times J_i
  3. 重排嵌套reorder 把四层轴按“块外、块内”交错排列,使数据访问在缓存中更紧凑。
    (Io×Jo)×(Ii×Ji)(I_o \times J_o) \times (I_i \times J_i)
python5 行
i_outer, i_inner = sch.split(i, factors=[None, 8])j_outer, j_inner = sch.split(j, factors=[None, 8])sch.reorder(i_outer, j_outer, i_inner, j_inner)sch.vectorize(j_inner)sch.parallel(i_outer)

缓存优化

为什么最熟练的厨师也会把常用调料放在手边?因为数据搬运比计算更贵。在 AI 算子里,从主存(冰箱)取数往往比计算慢一个数量级。TVMScript 提供了 cache_read 和 cache_write 两组原语,相当于在灶台旁临时搭一个“调料台”(共享内存/寄存器),把反复使用的数据先搬近一点。

cache_read(block, 0, "shared") 的意思是:把计算块的第 0 个输入读到共享内存缓存中。cache_write(block, 0, "local") 则是先写到本地寄存器、攒够一批再统一写回主存,减少“每次一小勺、跑一趟冰箱”的尴尬。多级缓存原语的组合使用,让数据在寄存器→共享内存→全局内存之间形成高效流水线。

用公式表达,设一个计算块总共需要访存 $M$ 次,缓存命中后只有 $M_h$ 次走快速路径,那么访存代价比为:

总调度师最高明的操作,往往不是炒得更快,而是少跑几趟冰箱。缓存优化的本质是时间换空间(数据搬运的减少),这也是元调度原语中性价比最高的一类。

Ceffective=Cfast×Mh+Cslow×(M−Mh)C_{\text{effective}} = C_{\text{fast}} \times M_h + C_{\text{slow}} \times (M - M_h)
有效访存代价 = 缓存命中部分走快路径的代价 + 未命中部分走慢路径的代价。命中率越高,整体越快。
  1. 总访存次数$M$ 表示整个算子执行期间对主存发起的数据访问次数。
    M=Mh+(M−Mh)M = M_h + (M - M_h)
  2. 命中部分$M_h$ 表示能从缓存直接拿到的次数,代价是快速路径 $C_{\text{fast}}$。
    Cfast×MhC_{\text{fast}} \times M_h
  3. 未命中部分$M - M_h$ 次必须访问主存,代价是慢速路径 $C_{\text{slow}}$。
    Cslow×(M−Mh)C_{\text{slow}} \times (M - M_h)

并行协作

中央厨房不可能只有一位厨师。一块 GPU 上有数百个“厨师”,分属不同的“班组”(线程块)。TVMScript 的 bind 原语,就是把这些循环层绑定到具体的线程或块上:sch.bind(loop, "blockIdx.x") 表示外层循环交给不同班组并行跑,sch.bind(inner_loop, "threadIdx.x") 表示内层循环由班组内多位厨师协作完成。

并行协作还需要同步原语——厨师们做完自己的菜不能直接上桌,要等所有同事都报数完成再统一出餐,否则顾客会拿到半熟的菜。在 CUDA 里对应 __syncthreads(),在 TVMScript 中通过 T.sync() 标注。计算每个线程负责的迭代份数,是并行绑定的核心数学:

并行编组做得好,一百份菜能同时出锅;做得不好,一半厨师在等,一半厨师在挤。TVMScript 的 bind 原语,就是帮你把“谁干什么”写到操作手册上。

Niter, per thread=NtotalNblock×Nthread per blockN_{\text{iter, per thread}} = \frac{N_{\text{total}}}{N_{\text{block}} \times N_{\text{thread per block}}}
每个线程分到的迭代次数 = 总迭代量 ÷(线程块数 × 每块线程数)。这就是并行度分配的“分餐公式”。
  1. 总迭代量$N_{\text{total}}$ 表示整个循环要处理的元素总数,如一百份菜。
    NtotalN_{\text{total}}
  2. 总并行度$N_{\text{block}} \times N_{\text{thread per block}}$ 表示所有能同时干活的厨师总数。
    Nblock×Nthread per blockN_{\text{block}} \times N_{\text{thread per block}}
  3. 人均分配两者相除,得到平均每位厨师要处理多少份。
    Niter, per thread=NtotalNblock×Nthread per blockN_{\text{iter, per thread}} = \frac{N_{\text{total}}}{N_{\text{block}} \times N_{\text{thread per block}}}

性能调优

调度原语组合得再好,最终都要用数据说话。性能调优的第一件事是建立指标:吞吐率(每秒处理多少元素)、耗时(毫秒)、带宽利用率。TVMScript 配合 TVM 的 autotuner,可以在一个参数搜索空间内自动尝试不同的原语组合。

瓶颈分析的核心是判断一个算子到底“卡在计算上”还是“卡在访存上”。为此我们引入经典的 Roofline 模型:以算术强度(每字节访存对应多少次计算)为横轴,可达性能为纵轴,形成一条“屋顶曲线”。当算子的算术强度低时,性能等于访存带宽 $\beta$ 乘以强度 $I$;当强度超过拐点时,受限于峰值计算能力 $\pi$:

调优不是玄学,而是“找到短板、用原语补长”。总调度师盯着下单速度与上菜速度的差值,就知道该加灶台还是该改传菜路线——TVMScript 的调度原语就是那支可以动态调整的笔。

P=min⁡(π,  β×I)P = \min(\pi,\; \beta \times I)
Roofline 模型说:算子能跑多快,取决于“计算峰值”和“访存带宽”哪个先到顶——就像厨房效率取决于“灶台火力”和“传菜速度”哪个是短板。
  1. 算术强度 I每个字节的数据搬运能支撑多少次浮点运算,单位是 FLOP/Byte。
    I=FLOPsBytesI = \frac{\text{FLOPs}}{\text{Bytes}}
  2. 访存上限 β×I带宽 β 乘以强度 I,给出访存不拖后腿时能爬到的性能上限。
    β×I\beta \times I
  3. 取最小值最终性能受限于两者中较小的那个瓶颈。
    P=min⁡(π,  β×I)P = \min(\pi,\; \beta \times I)

实战落地

有了前面的武器,我们看一个真实案例:优化一个矩阵乘法算子 C = A @ B。传统写法是三层朴素循环,性能平平。用 TVMScript 元调度原语组合改造,大致分四步:

这段脚本等于在厨房手册上写下:“把矩阵切片成 16×16 的小瓷砖,8 个 k 值一次算完,行与行并行翻炒。”当这些原语组合起来,矩阵乘的缓存命中率和并行度都会大幅提升,最终收益可对比:

实战落地时,TVMScript 的价值在于:把“调优经验”沉淀为可读的 Python 脚本,而不是散落的编译器黑魔法。总调度师把一次成功的大赛经验写进手册,以后每道菜都能复用这套打法——这就是元调度原语组合编程的魅力。

Speedup=TbeforeTafter≈N3 朴素循环耗时分块并行后耗时\text{Speedup} = \frac{T_{\text{before}}}{T_{\text{after}}} \approx \frac{N^3 \text{ 朴素循环耗时}}{\text{分块并行后耗时}}
加速比 = 优化前总耗时 ÷ 优化后总耗时。实测中,分块+向量化+并行组合常常带来数倍到数十倍的提升。
  1. 优化前耗时$T_{\text{before}}$ 指朴素三层循环的总耗时,往往缓存命中率低、并行度为零。
    TbeforeT_{\text{before}}
  2. 优化后耗时$T_{\text{after}}$ 指经过 split、reorder、vectorize、parallel 组合后的耗时。
    TafterT_{\text{after}}
  3. 加速比两者相除,得到“快了多少倍”的硬指标。
    Speedup=TbeforeTafter\text{Speedup} = \frac{T_{\text{before}}}{T_{\text{after}}}
python10 行
mod = tvm.script.from_source(matmul_tir)sch = tvm.tir.Schedule(mod)block = sch.get_block("C")i, j, k = sch.get_loops(block)i0, i1 = sch.split(i, factors=[None, 16])j0, j1 = sch.split(j, factors=[None, 16])k0, k1 = sch.split(k, factors=[None, 8])sch.reorder(i0, j0, i1, j1, k1)sch.vectorize(j1)sch.parallel(i0)

AI 应用场景

AI编译器算子自动调优

阶段:算子编译优化 / 后端代码生成

解决的问题:深度学习模型中有大量矩阵乘、卷积、归一化等算子。传统手写 CUDA 内核需要数周工作量,且每换一种硬件(不同 GPU 型号)就要重写一遍。TVMScript 元调度原语组合将调优逻辑写成 Python 脚本,配合 TVM AutoTVM/Ansor 自动搜索 split 块大小、向量宽度与并行度组合,把调优周期从数周压缩到数小时。

具体案例:某推理引擎团队优化 ResNet-50 的 3×3 卷积算子。他们用 TVMScript 写出包含 split(分块)、vectorize(向量化)、cache_read(缓存)的调度模板,再让自动调优器在同一模板下搜索 200 组参数组合。最终在 T4 GPU 上获得了 1.8 倍推理加速,全程无需手写底层 CUDA。

异构硬件的算子移植

阶段:模型部署 / 硬件适配

解决的问题:同一套模型要在 CPU、GPU、NPU 上运行,每个硬件的缓存大小、线程模型、SIMD 宽度都不同。TVMScript 调度脚本与硬件无关,只需更换原语的绑定参数(如 bind(loop, "threadIdx.x")),就能复用同一套组合逻辑完成迁移。

具体案例:某 AI 芯片公司适配 YOLOv5 的检测头算子时,将 GPU 上写好的 TVMScript 调度脚本,仅修改了 bind 目标的线程层级和 split 因子,便在自研 NPU 上达到 85% 的硬件峰值算力利用率。

Python 演示

python60 行
# -*- coding: utf-8 -*-# 本示例展示了 TVMScript 元调度原语组合的基本流程# 注意: 无真实 TVM 环境时也可运行, 核心 API 调用以中文伪注释说明 import math class LoopInfo:    def __init__(self, name, extent):        self.name = name        self.extent = extent        self.bound = None        self.child = None class ScheduleDemo:    def __init__(self, loop):        self.root = loop        self.calls = []     def get_loops(self):        loops = []        node = self.root        while node is not None:            loops.append(node)            node = node.child        return loops     def split(self, loop, factors):        outer = int(loop.extent // factors[1])        inner = int(factors[1])        self.calls.append("split({0}, {1}) -> outer {2} x inner {3}".format(loop.name, factors, outer, inner))        return LoopInfo(loop.name + "_o", outer), LoopInfo(loop.name + "_i", inner)     def vectorize(self, loop):        self.calls.append("vectorize({0}, width={1})".format(loop.name, loop.extent))     def parallel(self, loop):        self.calls.append("parallel({0})".format(loop.name))     def run(self):        print("=== 初始循环结构 ===")        i = self.get_loops()[0]        print("for {0} in range({1}): # 串行执行".format(i.name, i.extent))        io, ii = self.split(i, [None, 8])        self.vectorize(ii)        self.parallel(io)        print("=== 调度原语执行记录 ===")        for c in self.calls:            print("  " + c)        total = i.extent        per = ii.extent        groups = math.ceil(total / per)        print("=== 优化效果 ===")        print("串行分组数: {0} 组, 每组 {1} 份, 预计加速 {2}x".format(groups, per, groups)) if __name__ == "__main__":    inner_loop = LoopInfo("j", 16)    outer_loop = LoopInfo("i", 64)    outer_loop.child = inner_loop    demo = ScheduleDemo(outer_loop)    demo.run()
目的:模拟 TVMScript 调度器对两层循环执行 split → vectorize → parallel 的组合调度过程,并打印每一步原语记录与最终加速收益,帮助你建立“调度原语组合”的心智模型。
  1. class ScheduleDemo:用 Python 类模拟 TVMScript 的 Schedule 调度器对象,无需安装 TVM 也能理解调度流程。
  2. self.calls = []记录每次调度原语的调用痕迹,类似 TVM 中 sch.split(...) 的日志。
  3. def split(self, loop, factors)实现 split 循环分块原语,将 64 份迭代拆成 8 组 × 8 份。
  4. def vectorize(self, loop)模拟向量化原语,表示内层 8 份迭代用 SIMD 一次算完。
  5. def parallel(self, loop)模拟并行原语,把外层 8 组分配给不同线程并行执行。
  6. groups = math.ceil(total / per)计算分组数,这是加速比的理论上限依据。
  7. demo.run()组合触发整套调度流程,输出原语记录与优化效果。

常见误区

误区“我用了 split 和 reorder,计算顺序变了,结果肯定跟原来不一样,是不是要重新验证正确性?”不少初学者把调度原语当成“算法改写”,总担心结果会漂移。

正解TVMScript 的调度原语只改变“循环怎么跑”,不改变“每个元素算出来的数值”。就像中央厨房把一百份菜从“一位厨师做一百遍”改成“十位厨师各做十遍”,每份菜的配方和克重完全不变。只要浮点累加顺序没有变化,数学结果就是严格等价的。正确验证方式是跑 TVM 的 tvm.testing.assert_allclose 对比优化前后输出,而不是怀疑调度器的语义保持能力。

为何常见:很多人习惯了手写 CUDA 时需要同步修改算法逻辑,形成了“改动执行方式=改动算法”的思维定式,忽略了编译器调度器的语义等价保证。

误区“反正调度原语是免费的,我干脆把 split、fuse、cache_read、bind 全部堆上去,一定能把算子调到最快。”初学者容易把调度原语当成“增益 Buff 越叠越强”。

正解每个原语都有代价。盲目 split 会让循环层数过多、调度开销增大;盲目 cache_read 会占满共享内存甚至导致 bank conflict;盲目 bind 会让线程利用率下降。正确做法是用 Roofline 模型定位瓶颈:访存瓶颈优先 cache 和 reorder,计算瓶颈优先 vectorize 和 parallel,然后只组合 2-4 个必要的原语。

为何常见:初学阶段大家总想“多试几个参数”,缺乏“先测瓶颈再选原语”的工程方法论,把调优当成了叠加 Buff 的堆料游戏。

误区“vectorize 就是让循环每次处理更多数据,比如把 range(64) 改成 range(32),我只要能写出 32 次循环就是向量化。”初学者混淆了“手动展开”与“向量化指令生成”。

正解向量化的本质是让编译器生成 SIMD(单指令多数据)指令,CPU 一个时钟周期内对多个数据执行同一条运算。TVMScript 的 vectorize 原语是告诉调度器的后端“这一段请用 SIMD 指令实现”,而不是简单地把循环步长加倍。硬件向量宽度(如 AVX2 的 8 个 float)才是真正决定每批处理多少份的约束,超出宽度反而会被拆分。

为何常见:很多人把“循环内一次算多个元素”和“向量化”画等号,忽略了 SIMD 指令宽度与寄存器大小这些硬件约束,导致写出“假向量化”代码。

面试题

Q1. TVMScript 在 TVM 编译流程中主要扮演什么角色?

单选题 · easy

  • A.编写高性能算子的计算逻辑← 正确答案
  • B.用 Python 风格描述循环调度策略
  • C.替代 CUDA 成为唯一编程语言
  • D.负责前端模型的格式转换
  • E.外层因子自动取剩余值
  • F.表示该层不拆分
  • G.内层因子自动取 0
  • H.表示拆分后丢弃外层

答案:A。在 split 原语中,None 表示“该因子由调度器自动推断”。当内层因子指定为 4 时,外层因子自动等于 原长度 ÷ 4。B 混淆了 split 与 no-op,C、D 在语义上都不合法。就像把一百份菜按每 4 份一组,自然分成 25 组。

Q2. 以下哪些是 TVMScript 的高级调度原语?

多选题 · mid

  • A.split(循环分块)← 正确答案
  • B.vectorize(向量化)← 正确答案
  • C.backward(反向传播)
  • D.fuse(循环融合)← 正确答案

答案:ABD。split、vectorize、fuse 都是 TVMScript 中常见的循环级调度原语,用于改变执行结构。backward 是深度学习训练中的梯度传播概念,与循环调度无关。本知识点对应思维导图“常用原语”分支的四个子节点。

Q3. 关于 cache_read 原语,下列说法正确的是?

单选题 · hard

  • A.它减少了计算量
  • B.它把数据搬到更近的存储层级← 正确答案
  • C.它等价于向量化指令
  • D.它会改变算子的数学结果

答案:B。cache_read 的语义是在计算块与全局内存之间插入一个缓存层级(寄存器/共享内存),让反复使用的数据少跑远路。它不改变计算量(A 错)、不等于 SIMD 向量化(C 错)、不改变数学结果(D 错)。正如把调料从大冰箱搬到手边灶台,菜味不变,但做菜更快。

Q4. 下列哪项描述符合“调度原语组合”的核心约束?

单选题 · easy

  • A.必须只用一种原语
  • B.组合前后语义必须等价← 正确答案
  • C.组合越多性能一定越好
  • D.组合只用在 CPU 上

答案:B。语义等价是调度组合的底线,无论 split、fuse 还是 bind,都只能改变执行方式不能改变计算结果。A 太绝对,C 是常见误区,D 毫无根据。总调度师可以换方法做菜,但不能把红烧肉做成清蒸鱼。

Q5. 在调优 TVMScript 调度时,哪些做法是合理的?

多选题 · hard

  • A.先用 Roofline 判断访存还是计算瓶颈← 正确答案
  • B.盲目堆砌所有原语不加测量
  • C.用 autotuner 搜索分块大小← 正确答案
  • D.不考虑缓存层级直接绑线程

答案:AC。正确的方法论是先定位瓶颈(Roofline),再用自动调优器搜索参数组合。B 是“原语越多越好”的误区,D 忽略了缓存优化与并行绑定的协同关系。这对应思维导图“性能调优”分支的瓶颈分析与参数搜索。

Q6. 下列哪项是使用 bind 原语的主要目的?

单选题 · mid

  • A.将循环层映射到线程或块← 正确答案
  • B.让循环自动向量化
  • C.将两个循环合并为一层
  • D.改变数据类型的精度

答案:A。bind 原语是把循环轴绑定到硬件执行单元(threadIdx、blockIdx 等),实现并行协作。B 是 vectorize 的职责,C 是 fuse 的职责,D 是精度转换的职责。就像给厨师排班表,指定谁负责哪几口锅。