想象你是一家餐厅的后厨总管,手里握着一份写着几百道菜的晚宴订单。菜单上写清楚了每一道菜的食材和最终摆盘——但“先切菜还是先炖汤”“哪个灶台开大火”“哪些调料提前放在手边”,这些真正的效率秘密,全在厨师的调度里。AI编译器也是同样的故事:模型的计算定义是“菜单”,而TensorIR的调度(schedule)原语——reorder(调换步骤)、split(切块)、cache_read(备料碟)——就是后厨里决定出菜速度的关键手艺。同一个模型,调度得好与不好,性能可能差出数倍。本文将带你走进TensorIR的后厨,看清这些原语如何把“能跑”变成“飞跑”。

岗位关联

对AI从业者而言,掌握TensorIR调度原语,就是拿到了让算子逼近硬件性能极限的说明书。这是AI编译性能工程师的核心技能,也是从“会用框架”跨入“会做框架”的钥匙。

目录
  1. 可视化演示
  2. 循环结构基础
  3. reorder重排
  4. split拆分
  5. cache_read缓存
  6. 组合调度策略
  7. 自动调优基础
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。

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

系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T

循环结构基础

想象你是一家高档餐厅的大厨,接到一张写着几百道菜的晚宴订单。食材和最终摆盘都写在菜单上,但“怎么做”完全由你说了算:是先切菜还是先炖汤?哪个灶台开大火?哪些步骤可以并行?餐厅的成功不只靠菜谱,更靠后厨的调度智慧。AI编译器也一样:模型的数学含义是固定的,但翻译成代码后如何组织循环执行,完全由调度策略决定。

在TVM中,TensorIR把“计算定义”和“调度策略”剥离开来。开发者用te.compute描述“计算什么”——菜的原料和成品长什么样;在schedule阶段描述“怎么算”——先做哪一步、后做哪一步。朴素实现里,矩阵乘法就是一个三重循环嵌套:外层遍历输出行i,中间层遍历输出列j,最里层累加求和k。这里的i、j、k被称为IterVar,也就是循环变量。

每个IterVar都有自己的取值范围,合起来构成遍历空间(LoopSpace)。循环嵌套的顺序,直接决定了数据从内存中被取出的节奏:外层变一次,内层要转一整圈。如果内层频繁访问的数据相隔很远,CPU/GPU就会反复从慢速内存搬运数据,就像厨师做菜时每次放盐都要跑到储藏室拿盐罐一样低效。循环结构是一切调度原语表演的舞台。

理解了“菜单”与“后厨工序”的差别,你就拿到了TensorIR的第一把钥匙:循环是舞台,调度是表演。下面我们看第一个简单动作——reorder。

C[i,j]=∑k=0K−1A[i,k]×B[k,j],i∈[0,I), j∈[0,J)\displaystyle C[i, j] = \sum_{k=0}^{K-1} A[i, k] \times B[k, j],\qquad i \in [0, I),\ j \in [0, J)
这个公式是矩阵乘法的骨架:i遍历输出行,j遍历输出列,k把K个乘积累加成一个点。读懂它,才能理解后面的调度为什么能改变性能而不改变结果。
  1. i外层循环:对应输出矩阵的第几行,可以理解为“处理第几桌客人”,变化最慢。
    i∈[0,I)i \in [0, I)
  2. j内层循环:对应第几列,好比在同一桌逐道上菜,每上完一道菜j加一。
    j∈[0,J)j \in [0, J)
  3. k归约轴:把A的第i行和B的第j列逐项相乘再求和,工作量最大,也最值得优化。
    ∑k=0K−1A[i,k]×B[k,j]\sum_{k=0}^{K-1} A[i,k] \times B[k,j]
  4. 遍历空间所有(i,j)组合形成的矩形,是调度原语的作用范围。
    LoopSpace=[0,I)×[0,J)\text{LoopSpace} = [0,I) \times [0,J)

reorder重排

做一道炖牛腩,传统顺序是“焯水→切菜→热锅→炒香料”。如果把顺序换一下,先切菜再焯水,菜的口味不会变,但厨师能一边等水烧开一边切菜,整体节奏顺畅许多。reorder就是厨房里的“换步法”:调换循环嵌套的先后次序,烹饪内容(计算体)一个字都不改。

在TensorIR中,reorder的调用形式极其简洁:s[C].reorder(j, i)表示把原本外层i、内层j的循环交换成外层j、内层i。交换后,循环体内的表达式完全一样,但数据被访问的物理顺序变了。矩阵乘法中,按行优先访问A和按列优先访问B的效果截然不同——reorder能把访问模式扭转到与硬件缓存友好对齐的方向。

但交换不是永远安全。循环之间如果存在数据依赖(例如先写后读),调换顺序可能破坏先后约束。编译器会做依赖分析:只有当依赖方向允许交换时,reorder才被接受。这正是TVM比“手工硬改代码”高明的地方——用抽象原语让工具来保证等价性。

说到底,reorder就像厨师把“烧水”和“切菜”交换位置——结果还是那锅汤,出菜效率却大不一样。下一节看“切块”。

原始: (i,j)↦C[i,j]⟶reorder(j,i)重排后: (j,i)↦C[i,j]\displaystyle \text{原始:}\ (i, j) \mapsto C[i,j] \quad \overset{\text{reorder}(j,i)}{\longrightarrow} \quad \text{重排后:}\ (j, i) \mapsto C[i,j]
reorder只是把循环变量的出场顺序对调:原先i先变化、j后变化;重排后j先变化、i后变化。结果矩阵仍是同一个,但数据的读入节奏被重组。
  1. 原始循环对i在外层、j在内层,按“先逐行后逐列”的顺序扫描矩阵。
    for i 外; for j 内: C[i,j]=⋯\text{for } i \text{ 外; } \text{for } j \text{ 内: } C[i,j] = \cdots
  2. reorder(j, i)调用显式声明把循环对换成j在外、i在内。
    reorder(j,i)\text{reorder}(j, i)
  3. 重排后的循环扫描顺序变成“先逐列后逐行”,内存访问节奏随之改变。
    for j 外; for i 内: C[i,j]=⋯\text{for } j \text{ 外; } \text{for } i \text{ 内: } C[i,j] = \cdots
  4. 合法性条件当数据依赖不形成环时,交换不会改变最终值。
    Dep(i,j)⇒无环即合法\text{Dep}(i, j) \Rightarrow \text{无环即合法}

split拆分

婚宴上一条鱼要蒸给10桌客人分,直接整条端上桌肯定不妥。大厨的做法是先把鱼按人数切成盘:一桌一盘,分批次送出。split就是把一个大循环切成若干小段的“刀工”,它是循环并行、向量化等高级技巧的地基。

split的用法是s[C].split(i, factor=16):把原本从0遍历到N-1的循环i,拆成外层i_outer和里层i_inner两层。外层决定“第几批”,里层决定“这一批的第几个”。拆分后迭代总次数不变,只是结构上多了一个层级,让调度器可以分别处理两层的并行策略。

拆分的收益在哪?内层循环变成了一段连续短循环,正好可以对应CPU的SIMD向量宽度或GPU的一束线程;外层循环又可以拿给多核做任务划分。两个层级各司其职,就像切好的鱼块既能按桌传送,又能装车批量运输。factor的选择影响向量化效率和负载均衡,是自动调优中的关键参数。

切鱼的过程就是split:鱼还是那条鱼,总量没变,可切好之后每条鱼都能被不同灶台同时烹饪。鱼块的分组方式,正是调优要品尝的“刀工”差异。

i=iouter×f+iinner,iouter∈[0,⌈Nf⌉),  iinner∈[0,f)\displaystyle i = i_{\text{outer}} \times f + i_{\text{inner}},\qquad i_{\text{outer}} \in \left[0, \left\lceil \tfrac{N}{f} \right\rceil\right),\; i_{\text{inner}} \in [0, f)
拆分因子f把一个长度为N的循环改造成两个嵌套循环:外循环管“第几大块”,内循环管“块内偏移”。两者组合起来,能还原出原来的下标i,因此计算逻辑完全不变。
  1. f拆分因子(factor):每一块的长度,像“一条鱼切成几份”。
    f∈Z+f \in \mathbb{Z}^{+}
  2. i_outer外循环索引:表示当前在第几块。
    iouter=⌊if⌋i_{\text{outer}} = \left\lfloor \tfrac{i}{f} \right\rfloor
  3. i_inner内循环索引:块内相对位置。
    iinner=i mod fi_{\text{inner}} = i \bmod f
  4. 还原恒等式外索引乘因子加内索引,保证遍历的点集与原来完全一致。
    i=iouter×f+iinneri = i_{\text{outer}} \times f + i_{\text{inner}}

cache_read缓存

专业西餐厨房里有个词叫“备料碟”(mise en place)。正式开火前,大厨把盐、蒜、洋葱、香料各自装进小碗,放在触手可及之地。等油锅热起来,手一伸就能取用,而不是反复跑去储藏柜翻找。cache_read就是代码世界里的备料碟。

cache_read原语的作用是:在计算前,将某个会被反复读取的数据块复制到离计算单元更近的存储层级(例如GPU的共享内存或局部内存),随后循环全部从这个“小碗”里取数。数据本身没变,但取用的距离变了,速度便天差地别。

在TVM中,指令形如s.cache_read(A, "shared", [C])。编译器在lower阶段为A生成一份缓存副本,在循环开头完成拷贝,再让计算循环引用这份副本。对访存密集的算子,这能把数百次慢速全局访问变成少数几次搬运加上大量快速命中,是GPU优化中最常见的必杀技之一。

备料碟不会改变菜的味道,却能让大厨忙乱时依然稳如泰山。cache_read正是这种“有备无患”的智慧,在内存带宽吃紧的深度学习推理场景中尤为可贵。

Bcache=cache_read⁡(B),C[i,j]+=∑kA[i,k]×Bcache[k,j]\displaystyle B_{\text{cache}} = \operatorname{cache\_read}(B),\qquad C[i, j] \mathrel{+}= \sum_{k} A[i, k] \times B_{\text{cache}}[k, j]
cache_read把B的数据就地搬到更近的缓冲区B_cache,之后循环内读取B[k,j]全部改为B_cache[k,j]。数据内容不变,取数路径变短。
  1. cache_read(B)申请一份B的近端副本,像厨师提前装盘备料。
    Buf=cache_read(B)\text{Buf} = \text{cache\_read}(B)
  2. B_cache近端缓冲区:位于共享内存或寄存器级别,读写延迟远比全局内存低。
    Buf[k,j]=B[k,j]\text{Buf}[k, j] = B[k, j]
  3. 读取替换循环体里的B全面替换为Buf,后续每轮循环都在命中缓存。
    C[i,j]+=A[i,k]×Buf[k,j]C[i,j] \mathrel{+}= A[i,k] \times \text{Buf}[k,j]
  4. 复用收益访问全局内存的次数从“每次迭代都去”降为“拷贝一次+缓存命中多次”。
    Traffic:Nglobal→Cload+Ncache\text{Traffic}: N_{\text{global}} \to C_{\text{load}} + N_{\text{cache}}

组合调度策略

真正的后厨大师,不会只用一种技巧:先按桌切好鱼(split),再调换上菜顺序(reorder),同时把酱料装进备料碟(cache_read)。三者组合起来,才构成一条行云流水的流水线。在TensorIR里,这些原语也可以自由叠加,形成任意复杂的调度变换链。

组合的前提是每一步都保持计算等价。reorder不改变计算体、split不改变遍历点集、cache_read不改变数据内容,因此它们可以安全地连续作用。TVM依次应用这些变换,最终lower成目标代码。CPU/GPU上那些令人眼花缭乱的优化,本质上就是这些简单动作的排列组合。

组合的“顺序”很讲究:先split还是先reorder,产物和优化空间都不同;cache_read要挂在哪个循环下面,也直接影响缓存命中率。自动调优器正是把这个组合空间作为搜索对象,用启发式方法寻找最优组合。这也是TensorIR作为调度抽象的核心价值:人工很难穷举的优化方案,机器可以。

组合调度就像“厨艺叠加”:盐和火候单独用都是常识,合起来却能成就一道名菜。调度原语的组合空间,就是AI编译器施展创造力的后厨。

Sall=cache_read∘split∘reorder,Lopt=Sall(L0)\displaystyle S_{\text{all}} = \text{cache\_read} \circ \text{split} \circ \text{reorder},\qquad L_{\text{opt}} = S_{\text{all}}(L_0)
把三个原语串联起来,作用在原始循环L0上,得到优化后的新循环L_opt。变换过程符合数学上函数复合的规则。
  1. L0原始循环:未经过任何调度的朴素三重循环。
    L0:for i,j,k:…L_0: \text{for } i,j,k: \ldots
  2. reorder作用先调整内外层顺序。
    L1=reorder(j,i)(L0)L_1 = \text{reorder}(j,i)(L_0)
  3. split作用再对某个循环进行因数拆分。
    L2=split(j,16)(L1)L_2 = \text{split}(j,16)(L_1)
  4. cache_read作用最后为被重用数据建立近端副本。
    Lopt=cache_read(L2)L_{\text{opt}} = \text{cache\_read}(L_2)

自动调优基础

开连锁餐厅,最有价值的不是某一道菜,而是“如何在每一家分店复制最佳火候”的标准流程。AI编译器也面临同样的问题:每个模型、每种硬件,都有自己最适合的schedule。手动调参如大海捞针,于是TensorIR衍生的AutoScheduler/AutoTVM登场了。

自动调优的核心,是把“调度策略”表示成可搜索的配置空间。比如split的factor取多少?循环要不要reorder?cache_read放在哪层?每个选项都映射为一个候选调度。生成候选后,要么在真实硬件上实测,要么用成本模型(cost model)预测,再一步步逼近性能最优值。

这就是TVM的端到端优化闭环:调优器在搜索空间里探索调度组合,lower生成具体代码,在机器上跑出真实时间,反馈给搜索算法调整策略。一轮调优可能耗时几分钟到几小时,但产出的IR代码往往能媲美甚至超过手工专家。大厨的经验,就这样被沉淀为每一家“后端厨房”的标准操作手册。

从手工切配到标准化流水线,从依赖个人经验到系统化搜索,TensorIR让AI编译器的“后厨”第一次拥有了可复制的大厨智慧。这,正是它成为自动调优基石的根本原因。

S={S1,S2,…,ST},S∗=arg⁡min⁡S∈SCost⁡(lower⁡(S(L0)))\displaystyle \mathcal{S} = \{S_1, S_2, \ldots, S_T\},\qquad S^{*} = \mathop{\arg\min}_{S \in \mathcal{S}} \operatorname{Cost}\bigl(\operatorname{lower}(S(L_0))\bigr)
搜索空间S是所有合法调度的集合;Cost函数评估每个调度生成代码的执行代价;S*是被选中的最优调度,也就是最优“后厨操作手册”。
  1. 候选调度集合由reorder/split/cache_read等原语的参数组合构成。
    S={S1,…,ST}\mathcal{S} = \{ S_1, \dots, S_T \}
  2. lower降低:把高层调度翻译成可执行的底层IR。
    lower⁡(S(L0))\operatorname{lower}(S(L_0))
  3. Cost代价函数:可以是实测时间,也可以是预测成本。
    Cost⁡:IR→R\operatorname{Cost}: \text{IR} \to \mathbb{R}
  4. argmin最优选择:在全体候选中挑出代价最小的那一个。
    S∗=arg⁡min⁡S∈SCost⁡(⋅)S^* = \arg\min_{S \in \mathcal{S}} \operatorname{Cost}(\cdot)

AI 应用场景

AI编译器的算子自动调优

阶段:编译优化与代码生成阶段

解决的问题:深度学习框架中的算子,用CUDA/OpenCL手工优化一个就要数周;TVM通过TensorIR的调度原语自动探索reorder/split/cache_read的组合,以“搜索代替手写”,把同等性能的优化时间压缩到小时级。

具体案例:在ResNet-50的GPU推理部署中,工程师用TVM的AutoScheduler对卷积算子做自动调度搜索,在精度不变的前提下,将推理延迟比TensorFlow默认实现降低了约30%—40%。

端侧NPU推理引擎的访存优化

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

解决的问题:手机、摄像头等端侧芯片的内存带宽很小,访存功耗远高于计算功耗。利用cache_read把权重和激活值放入片上SRAM,再配合reorder调整读取顺序,能显著减少掉DRAM访问次数,延长电池续航。

具体案例:某安防芯片公司在部署YOLOv5时,逐算子构建TensorIR调度,并把权重加载从“每层都读DDR”改为“复用片上缓存”,最终单帧推理功耗下降了约25%。

面向新芯片的算子库自动生成

阶段:芯片适配与算子库构建

解决的问题:新发布的AI加速芯片如果没有成熟算子库,落地就被卡在“写不出高性能算子”上。以TensorIR为中间层,只需为新芯片定义存储层级和执行模型,就能复用现有调度原语自动生成算子库。

具体案例:一家RISC-V AI芯片厂商用TVM的BYOC+TensorIR流程,为自研NPU自动生成全量算子库,避免为上百个算子逐个手工设计,两周内跑通了首个可运行模型。

Python 演示

python33 行
"""TensorIR 三个调度原语的最小演示配方"""import tvmfrom tvm import te  def matmul_schedule_demo():    N = 64    A = te.placeholder((N, N), name="A")    B = te.placeholder((N, N), name="B")    C = te.compute(        (N, N),        lambda i, j: te.sum(A[i, k] * B[k, j], axis=k),        name="C",    )     s = te.create_schedule(C.op)    i, j = C.op.axis     # 1) reorder:把 j 提到外层,i 变成内层    s[C].reorder(j, i)     # 2) split:把外层 j 按 factor=16 拆成 j_outer / j_inner    j_outer, j_inner = s[C].split(j, factor=16)     # 3) cache_read:把 A 复制到本地缓存中复用    A_local = s.cache_read(A, "local", readers=[C])     print("===== 调度后的 TIR 代码(节选) =====")    print(tvm.lower(s, [A, B, C], simple_mode=True))  if __name__ == "__main__":    matmul_schedule_demo()
目的:用最简代码演示TensorIR三个核心调度原语——reorder、split、cache_read——如何焊接到一个矩阵乘法算子上,并输出调度后生成的TIR代码。
  1. s = te.create_schedule(C.op)从计算节点创建调度对象,像大厨拿到今日菜单后开始规划工序。
  2. s[C].reorder(j, i)将外层i与内层j互换,改变循环扫描顺序。
  3. s[C].split(j, factor=16)把j循环按16来拆分,产生外循环j_outer和内循环j_inner。
  4. A_local = s.cache_read(A, "local", readers=[C])为A创建一份本地副本,让后续循环就近读取。
  5. tvm.lower(s, [A, B, C], simple_mode=True)将高层调度降低为接近硬件的IR,方便观察变换结果。

常见误区

误区reorder就是把两个循环的代码位置换一下嘛,怎么高兴怎么换,反正编译器会帮我跑。

正解换步骤是有前提的,就像炖汤时不能先把汤盛出来再放肉。循环之间存在数据依赖时,交换顺序会改变计算结果。编译器必须做依赖分析,确认交换合法后才允许reorder。合法的reorder是“等价变换”,非法的reorder则是“改写了菜谱”。

为何常见:初学时只看代码层面“两行对调”,忽略了背后的依赖约束;而依赖分析恰恰是编译器里看不见的保障。

误区既然split能把大循环拆开,那factor取越大,切出来的内层循环越长,优化空间不就更大?

正解内层循环过长会让向量化寄存器装不下,也可能让硬件线程负载不均;内层循环过短,又会增加外层循环的开销。factor更像是切鱼块的刀工:太小太碎浪费盘子,太大又达不到分批出菜的目的。合理的因子一般与向量宽度或线程束大小对齐。

为何常见:人脑倾向于“越大越强”,而硬件优化讲究“匹配”——因子必须贴合目标硬件的执行宽度,并非越大越优。

误区cache_read就是操作系统或CPU里那个自动缓存,电脑自己会管,不需要我手动操作。

正解cache_read是编译器显式生成的一份数据副本,放进共享内存或本地内存。它不是自动硬件缓存,而是程序员(或调优器)通过TensorIR原语主动安排的“备料碟”。数据放到哪个层级、副本何时更新,都由调度策略决定。

为何常见:日常开发里“cache”一词常指CPU硬件缓存,但编译层的cache_read是软件层面的存储变换,两者工作机制完全不同。

误区reorder、split、cache_read都是对循环动手脚,改变了执行顺序,那结果肯定会有微小误差吧?

正解这些调度原语都是等价变换:reorder不改变计算体,split不改变遍历点集,cache_read不改变数据值。在数学上它们输出完全相同的矩阵。若有差异,只会来自浮点运算的舍入顺序差异,而不是逻辑错误。调度的目的是“同样一道菜,厨房效率更高”。

为何常见:因为人习惯于“过程变了结果总会有变”,却没意识到编译器变换的核心约束就是保持语义不变。

误区既然每个原语都能优化一点,那把reorder、split、cache_read全都用上,越复杂越好。

正解原语组合确实能叠加收益,但也会互相影响:先split再reorder与先reorder再split产生不同的代码形态。调度组合空间呈指数级增大,多数组合的性能是平庸的,甚至不如不调度。自动调优的意义就是在广阔空间里筛出少数几个“黄金组合”。

为何常见:初学者很容易把“优化手段”等同于“尽数使用”,忽视了优化是成本与收益的博弈。

面试题

Q1. reorder原语最核心的作用是什么?

单选题 · easy

  • A.增加循环总迭代次数
  • B.调整循环内外层顺序← 正确答案
  • C.消除循环之间的依赖
  • D.扩大数据访存的规模

答案:B。reorder的本质是交换循环嵌套的先后顺序,就像厨师把“切菜”和“焯水”互换。它不增加迭代总数,也不消灭依赖,只是改变数据访问的节奏,从而改善局部性。

Q2. split(factor=16) 会将一个循环拆成几个嵌套循环?

单选题 · easy

  • A.一个一级循环
  • B.两个嵌套循环← 正确答案
  • C.三个嵌套循环
  • D.四个嵌套循环

答案:B。split把一个循环按因子拆成“外层循环管第几批、内层循环管批内第几个”的两层嵌套结构。这是最核心的拆分结果,后续可再对任一层进一步拆分。

Q3. cache_read的主要作用是什么?

单选题 · mid

  • A.缓存计算结果并复用
  • B.为读取数据建近端副本← 正确答案
  • C.代替操作系统磁盘缓存
  • D.自动扩大硬件缓存容量

答案:B。cache_read把将被多次读取的数据提前复制到更靠近计算单元的存储层级,让后续循环直接命中本地副本。它缓存的是“输入数据”,不缓存计算结果,也不能指挥操作系统或硬件缓存。

Q4. 关于reorder的合法性,正确的说法有哪些?

多选题 · mid

  • A.任意循环均可直接交换
  • B.必须满足数据依赖约束← 正确答案
  • C.交换前后计算结果等价← 正确答案
  • D.性能必然显著提升

答案:BC。reorder是等价变换,交换后结果必须与原来相同,因此必须先做依赖分析;依赖形成环时不能交换。但等价不代表性能一定提升。选项A和D都过于绝对。

Q5. cache_read对访存性能的提升体现在哪些方面?

多选题 · mid

  • A.减少全局内存访问次数← 正确答案
  • B.提升数据读取重用率← 正确答案
  • C.降低算法时间复杂度
  • D.减少循环总迭代次数

答案:AB。cache_read把反复读取的数据放近端,减少了从全局内存重复取数的次数,并让同一份数据被循环多次命中。它改变的是数据搬运路径,不改变算法复杂度,也不减少迭代次数。

Q6. 组合调度原语时,最重要的是什么?

单选题 · mid

  • A.任意顺序都能通过编译
  • B.调度顺序影响最终性能← 正确答案
  • C.每个原语都必须使用一次
  • D.原语之间互不干扰无需分析

答案:B。原语组合时的顺序会改变中间循环结构,进而影响后续优化的空间与效果。比如先split再reorder与先reorder再split,产生的代码形态不同。组合调度讲究“顺序”,也讲究“需要”,不是每道菜都必须用上全部调料。

Q7. 在自动调优搜索中,调度配置主要由什么决定?

单选题 · hard

  • A.计算图结构算子类型数据维度
  • B.split因子reorder开关缓存层级← 正确答案
  • C.编译时间代码行数变量数量
  • D.线程数量锁粒度队列深度

答案:B。自动调优的搜索空间正是由调度原语的参数构成:split的因子取值、reorder是否应用、cache_read放在哪一层。计算图结构属于输入约束,编译时间等不是搜索配置项。

Q8. 关于split对并行和向量化的作用,正确的说法有哪些?

多选题 · hard

  • A.内层循环适合做向量化← 正确答案
  • B.外层循环适合划分线程← 正确答案
  • C.因子必须等于向量宽度
  • D.拆分后循环层级增加← 正确答案

答案:ABD。split之后的内层循环成了一段连续短循环,适合映射到SIMD向量化;外层循环适合分给多线程并行。factor只是通常与向量宽度对齐,并非“必须等于”。拆分后循环从一级变成两级,层级增加是split的直接产物。

Q9. 在TVM中,计算定义与调度策略的关系是什么?

单选题 · easy

  • A.二者完全耦合不可分离
  • B.计算定义在前调度在后← 正确答案
  • C.调度在前计算定义在后
  • D.二者等价可以互相替代

答案:B。TensorIR的设计核心是先描述“计算什么”(计算定义),再描述“怎么算”(调度)。先有菜谱,才有后厨工序。两者可分离,也不可替代。

Q10. 关于reorder提升性能的根本原因,下列最准确的是?

单选题 · hard

  • A.减少了算术计算量
  • B.改善了数据访问的局部性← 正确答案
  • C.减少了循环变量个数
  • D.扩大了数据类型范围

答案:B。reorder不改变算术运算和循环个数,它改变的是数据访问的先后顺序。让访问模式贴近硬件缓存的局部性偏好,从而提高缓存命中率、减少慢速内存访问,这才是性能提升的根本来源。