周六晚上,小饭馆接到一桌十道菜的大单。后厨只有一位大厨时,只能一道菜一道菜慢慢做;但如果来了一支八人厨师团队、八个灶台同时可用,问题就变了:先做哪道菜?哪些菜能同时开火?哪道菜必须等另一道菜出锅才能动手?聪明的厨师长会先扫一眼菜单——哪道汤要炖两小时(关键路径),哪些菜互不干扰可以并行,能不能把焯水和下锅连成一道工序——然后排出一张让整桌菜最快上齐的“厨房排班表”。这就是算子调度:在不破坏依赖的前提下,重新安排计算图中算子的执行顺序,让 GPU 这颗“大芯片后厨”里的计算核心全都忙起来。XLA、PyTorch 2.0 等编译器能大幅加速推理,靠的正是一张好排班表。

岗位关联

掌握算子调度,你就能真正看懂 PyTorch 2.0 与 XLA 的加速原理,这是推理引擎、部署优化、编译器方向 AI 工程师的必修基本功。

目录
  1. 可视化演示
  2. 调度的意义
  3. 依赖关系识别
  4. 关键路径分析
  5. 调度策略方法
  6. 并行度与负载
  7. 算子融合协同
  8. 系统实践生态
  9. 挑战与前沿
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示AI编译器算子调度三维度:调度流程(拓扑依赖分析→就绪集计算前驱完成的算子→资源分配→执行循环推进直到完成)、调度策略对比(并行调度最大化吞吐利用多SM多核/内存感知调度最小化峰值内存避免同时激活过多张量/流水线调度最小化延迟三类非互斥组合使用)、关键路径优化(优化前A→B→C→D→E串行关键路径长度5无并行度/优化后B与C并行D与E并行关键路径长度3加速40%通过拓扑重排暴露并行度),三模式切换理解算子调度核心机制。

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

'+C1+'

调度的意义

周末晚上,小饭馆接到一桌十道菜的大单。后厨只有一位大厨时,只能按菜谱一道接一道做;但如果你请来了一支八人厨师团队、面前摆着八个灶台,问题立刻就变了:先做哪道菜?哪些菜能同时开火?哪道菜必须等另一道菜出锅才能动手?这就是调度的意义——决定每道工序以什么顺序、在哪个灶台、什么时候开始干。

在深度学习里,计算图就是菜谱:每个节点是一个算子(卷积、归一化、激活等),有向边代表张量在算子之间的流动。GPU 有数千个计算核心,相当于一支庞大的后厨团队。如果算子按照图里写的顺序一个个执行,绝大多数核心只能围观,硬件利用率极低。算子调度要做的事情,就是重新安排这些算子的执行顺序和并行方式,让尽可能多的计算核心同时忙起来,从而缩短整张图的执行时间。

串行执行是“一人后厨”的参照系:所有算子排成一队,总耗时等于每个算子耗时之和。

调度的目标,就是在不违反数据依赖的前提下,让总执行时间远远小于这个串行下界。好的厨师长拿到菜谱先扫一眼依赖关系,再排兵布阵,让一桌菜几乎同时出锅——这就是调度的价值。

Tserial=∑v∈Vt(v)T_{\text{serial}} = \sum_{v \in V} t(v)
串行总耗时就是把计算图里所有算子的执行时间全部相加,它代表完全不调度的最慢情况。
  1. 求和符号遍历计算图中所有算子,把它们的耗时逐一累加起来。
    ∑v∈V\sum_{v \in V}
  2. 算子耗时$t(v)$ 表示算子 $v$ 在当前硬件上单独执行的耗时。
    t(v)t(v)
  3. 串行总时间所有算子排成一队依次执行,总耗时等于各项之和。
    Tserial=∑v∈Vt(v)T_{\text{serial}} = \sum_{v \in V} t(v)

依赖关系识别

菜谱里藏着严格的先后约束:肉必须焯水才能下锅爆炒,否则口感发柴;而高汤和蒸鱼互不相干,完全可以同时开火。这些约束不是厨师长随意定的,而是食材特性决定的。在计算图里,算子的依赖关系写在边上——一条边从算子 $v_1$ 指向 $v_2$,意味着 $v_2$ 必须等 $v_1$ 做完才能开始。

依赖关系中最常见的是数据依赖,也叫写后读(RAW):$v_2$ 要读取 $v_1$ 写出的张量,比如卷积的输出喂给归一化层,归一化只能等卷积结束。此外还有读后写(WAR)和写后写(WAW),在并行执行、内存复用的场景下同样会限制重排。控制依赖则来自 if 等分支结构:分支汇合处的算子要等所有分支都执行完。一张计算图本质上是一个有向无环图(DAG),边就是依赖。依赖边规则很简单:一个算子的所有前驱都完成后,它才处于“就绪”状态,可以被调度执行。

数据依赖可以用读写集合精确判定——$v_1$ 写出的变量集合与 $v_2$ 读入的变量集合相交,说明 $v_2$ 必须等 $v_1$。

厨师长看菜谱时,心里默默画着一张“谁等谁”的关系图:能并行的菜分开安排,必须等待的菜排好先后。这张图,就是调度算法眼里的 DAG。

RAW(v1→v2)  ⟺  W(v1)∩R(v2)≠∅\text{RAW}(v_1 \to v_2) \iff \text{W}(v_1) \cap \text{R}(v_2) \neq \varnothing
这个公式是判断“谁必须等谁”的判定式:写集合与读集合有交集,就是写后读依赖。
  1. 写集合$\text{W}(v_1)$ 表示算子 $v_1$ 会往内存中写入哪些变量的集合。
    W(v1)\text{W}(v_1)
  2. 读集合$\text{R}(v_2)$ 表示算子 $v_2$ 执行前需要读取哪些变量的集合。
    R(v2)\text{R}(v_2)
  3. 交集非空两个集合有共同元素,说明 $v_2$ 要读的正是 $v_1$ 写的,产生写后读依赖。
    W(v1)∩R(v2)≠∅\text{W}(v_1) \cap \text{R}(v_2) \neq \varnothing

关键路径分析

一桌菜最快多久能上齐?不是把所有菜各做一遍的时间总和,而是看“最磨蹭”的那条流水线。比如宴席上必须有炖两小时的高汤,那不管其他菜多快,整桌菜至少也要两小时。这条最长的依赖链,就是关键路径。

在计算图中,从入口算子到出口算子的每一条路径,都把路径上所有算子的耗时加起来;其中最大的那个值就是关键路径长度。识别关键路径不必真的枚举所有路径——可以用前向遍历:从入口出发,不断更新每个算子的最早开始时间(等于所有前驱的完成时间最大值),最后取所有算子完成时间的最大值。

关键路径的意义在于它划定了加速极限:哪怕给后厨加再多人手,高汤该炖两小时还是两小时。资源应该优先喂给关键路径上的算子,否则整桌菜的总时间就会被这条“高汤链”卡死。

Tcrit=max⁡P∈P(G)∑v∈Pt(v)T_{\text{crit}} = \max_{P \in \mathcal{P}(G)} \sum_{v \in P} t(v)
关键路径长度是所有依赖链中耗时最长的那条,它决定了无论怎么调度都绕不过去的时间下界。
  1. 路径集合$\mathcal{P}(G)$ 表示计算图 $G$ 中从入口到出口的所有有向路径。
    P(G)\mathcal{P}(G)
  2. 路径总耗时把路径 $P$ 上每个算子的耗时累加,得到这条链的总时长。
    ∑v∈Pt(v)\sum_{v \in P} t(v)
  3. 取最大值在所有路径中找耗时最长的一条,就是关键路径。
    Tcrit=max⁡P∈P(G)∑v∈Pt(v)T_{\text{crit}} = \max_{P \in \mathcal{P}(G)} \sum_{v \in P} t(v)

调度策略方法

厨师长手里有一本排班表。最稳妥的做法是拓扑排序——像“先备菜、再切配、最后掌勺”一样,按依赖关系的先后把算子排成一列,保证顺序合法。更聪明一点的做法是优先级调度:关键路径上的菜是硬菜,给它们插队,让它们尽早开工,因为一旦拖了它们,整桌菜都会延误。贪心调度则信奉“手里有活就干”:只要某个算子已经就绪,立刻把它派到空闲的计算单元上,绝不让灶台空等。动态调度则是应对“客人临时加菜”——运行时根据实测耗时不断调整后续安排。

无论哪种策略,衡量排班表好坏的指标都是完工时间(makespan):在某种调度方案下,所有算子中最晚完成的那个时刻。调度优化的目标就是让这个最晚完成时间尽量小。

一张好排班表不在于花哨,而在于让每个灶台都别闲着、让硬菜别等太久。实际编译器往往把多种策略组合使用:用拓扑序保证正确,用优先级照顾关键路径,用贪心让资源满载。

M(π)=max⁡v∈V(sπ(v)+t(v)),min⁡πM(π)M(\pi) = \max_{v \in V} \bigl( s_{\pi}(v) + t(v) \bigr), \quad \min_{\pi} M(\pi)
完工时间等于所有算子的“开始时刻 + 自身耗时”里的最大值,调度要找到让这个最大值最小的排列方式。
  1. 开始时刻$s_\pi(v)$ 表示在调度方案 $\pi$ 下,算子 $v$ 被批准开工的时间点。
    sπ(v)s_\pi(v)
  2. 完成时刻开始时间加上算子自身耗时,就是它的完成时间。
    sπ(v)+t(v)s_\pi(v) + t(v)
  3. 极小化极大取所有算子完成时间的最大值作为总耗时,再求让它最小的调度方案。
    min⁡πmax⁡v∈V(sπ(v)+t(v))\min_{\pi} \max_{v \in V} \bigl( s_{\pi}(v) + t(v) \bigr)

并行度与负载

八位厨师不能都去切同一根葱。并行度不是越高越好,关键在分工合理:热菜组、凉菜组、汤锅组各司其职,谁忙完了就去帮最忙的组——这就是负载均衡。在 GPU 上,算子并行指的是让互不依赖的算子同时运行;流式并行是借助 CUDA stream 让不同 kernel 在时间上重叠;设备协同则是把不同算子分配到 GPU、CPU 等多类硬件上同时干活。

但 Amdahl 定律给所有并行优化画了一条天花板:程序里总有无法并行的串行部分,这部分时间不会随核心数增加而减少。串行比例 $f$ 越大,加速比的上限越低。这也是为什么调度不能只追求“让算子跑起来”,还要尽量减少串行瓶颈——比如把初始化、同步、内存拷贝这些“必须排队”的环节压缩到最小。

厨师长不会让八个人都去端同一盘菜,而是让每个人都在干活、且干的是当前最需要的活。调度器同样要平衡各计算单元的负载,避免“旱的旱死、涝的涝死”。

S(P)=1(1−f)+fPS(P) = \frac{1}{(1 - f) + \dfrac{f}{P}}
Amdahl 定律说明:有 $f$ 比例的工作无法并行时,即使有无限多个处理器,加速比也超不过 $1/f$。
  1. 串行占比$f$ 表示程序中无法并行的部分占总执行时间的比例。
    f∈[0,1]f \in [0, 1]
  2. 并行后耗时并行部分被 $P$ 个处理单元平分,总耗时为串行部分加平分后的并行部分。
    (1−f)+fP(1 - f) + \frac{f}{P}
  3. 加速比原始串行总时间除以并行后总时间,就是加速倍数。
    S(P)=1(1−f)+f/PS(P) = \frac{1}{(1 - f) + f / P}

算子融合协同

焯水后的肉片,如果先装盘、放凉、收进冰箱,下锅前再拿出来,中间全是浪费的搬运时间。聪明的厨师会把焯水和下锅连成一道工序,省掉中间的装盘与冷藏。算子融合就是这个思路:把计算图上相邻的多个算子合并成一个 kernel 来执行。

融合带来的收益非常直观。原本每个算子都要从显存读一次输入、写一次输出,图里有 $N$ 个算子就是 $2N$ 次访存;融合成一个 kernel 后,中间张量直接留在寄存器和共享内存里,不落回显存,整体只需读一次输入、写一次输出。访存往往是比计算更贵的“搬菜”成本,省掉中间搬运对推理延迟立竿见影。经典的 Conv+BN+ReLU 融合,在 CNN 推理中几乎是标配。融合后图的节点变少,调度问题的规模也随之缩小,但要注意融合过度会让单个 kernel 资源占用过高,反而拖慢速度。

融合就是“连灶”——把几个步骤安排在一个灶台上连续完成,省掉中间洗锅、装盘、冷藏的折腾。它和调度是黄金搭档:融合缩减问题的规模,调度在缩减后的图上继续精打细算。

Memfused=N+1≪2N=Memseparate\text{Mem}_{\text{fused}} = N + 1 \ll 2N = \text{Mem}_{\text{separate}}
融合后访存次数从 2N 降到 N+1,大约省掉一半的“搬菜”时间。
  1. 未融合访存$N$ 个算子各自读写一次中间张量,共 $2N$ 次访存。
    Memseparate=2N\text{Mem}_{\text{separate}} = 2N
  2. 融合后访存合并成一个 kernel 后,只读 $N$ 份输入、写一次最终输出。
    Memfused=N+1\text{Mem}_{\text{fused}} = N + 1
  3. 差距显著融合后的访存次数不到原来的一半,访存瓶颈被大幅缓解。
    Memfused≈12Memseparate\text{Mem}_{\text{fused}} \approx \frac{1}{2} \text{Mem}_{\text{separate}}

系统实践生态

现实世界里的后厨已经用上了高科技。XLA 像一位总厨,拿到整份菜谱后先做全局分析,把计算图编译成自己的中间表示(HLO),完成依赖分析和算子调度后再生成高效代码。PyTorch 2.0 的 torch.compile 则像一个“边看菜谱边排班”的聪明帮厨:先用 Dynamo 捕获计算图,再做图优化与代码生成。cuDNN 的 benchmark 模式像个爱试菜的品控——每次开工前把几种候选做法都试跑一遍,挑最快的那个。Triton 则是发给帮厨们的标准化操作手册,让开发者以接近 Python 的写法描述算子,由编译器负责映射到底层调度。

这些系统在“选择最快的实现”这件事情上高度一致:候选实现不止一种,与其靠理论猜,不如实测一把。

XLA、PyTorch 2.0、cuDNN、Triton 这些工具的本质,都是把“厨师长的经验”变成可自动运行的排班系统,让每一轮推理都能拿到一份不错的调度表。

K⋆=arg⁡min⁡k∈Ktime(k)K^{\star} = \arg\min_{k \in \mathcal{K}} \text{time}(k)
编译器在候选 kernel 实现里逐个实测耗时,选出最快的那一个,这就是 benchmark 调优的本质。
  1. 候选集合$\mathcal{K}$ 是编译器为某个算子准备的所有实现方式集合,如不同 tiling 策略。
    K={k1,k2,…,km}\mathcal{K} = \{k_1, k_2, \dots, k_m\}
  2. 实测耗时$\text{time}(k)$ 表示把实现 $k$ 在实际硬件上跑一遍测出的时间。
    time(k)\text{time}(k)
  3. 取最小值$\arg\min$ 返回让耗时最小的那个实现,作为最终选用的 kernel。
    K⋆=arg⁡min⁡k∈Ktime(k)K^{\star} = \arg\min_{k \in \mathcal{K}} \text{time}(k)

挑战与前沿

厨房越来越大、菜单越来越长、客人还随时改菜——排班这件事正在变得空前困难。动态形状就是“客人临时改菜”:模型输入的长宽、batch大小在运行时才确定,计算图的结构虽然不变,但每个算子的耗时估算随时在变,提前排好的班表可能瞬间失效。

更麻烦的是搜索空间爆炸。调度一个 $n$ 个算子的图,合法的执行顺序本质上是一个拓扑排序,数量最多可以达到 $n!$ 量级;当 $n$ 到几十上百时,穷举所有方案是天文数字。事实上,在资源受限的通用场景下求最优调度是 NP-hard 问题,不存在多项式时间的通用最优解。因此工程上的出路是启发式算法加代价模型——代价模型负责估算每个算子在当前硬件上的耗时,启发式算法则在有限时间内给出“够好”的调度。端侧设备资源更紧张,调度器自身的开销也必须压到极低。

好的厨师长不是能穷举所有排法,而是有一套又快又稳的经验法则,加上对每道菜耗时的准确预估。算子调度的前沿,正是在动态、复杂、资源受限的现实里,用启发式和代价模型逼近那个理想的下界。

order(G)≤n!,最优调度∈NP-hard\text{order}(G) \le n!, \quad \text{最优调度} \in \text{NP-hard}
合法调度方案的数量随算子数阶乘增长,求最优解是 NP-hard,所以实际系统都用启发式搜索。
  1. 排列数上界$n$ 个节点最多有 $n!$ 种拓扑排序,$n$ 稍大就是天文数字。
    order(G)≤n!\text{order}(G) \le n!
  2. 组合爆炸$n!$ 的增长速度远超多项式,枚举所有方案不可行。
    n!≈2πn(ne)nn! \approx \sqrt{2\pi n}\left(\frac{n}{e}\right)^n
  3. 问题复杂度最优调度被证明属于 NP-hard,没有通用快速解法。
    optimal scheduling∈NP-hard\text{optimal scheduling} \in \text{NP-hard}

AI 应用场景

LLM 推理服务吞吐优化

阶段:模型推理加速 / 图优化

解决的问题:在线大语言模型推理中,prefill 和 decode 阶段有大量小算子,若串行执行,GPU 利用率低、响应延迟高。算子调度把 attention、MLP 等模块的若干 kernel 按依赖重排并融合,让多流并行执行。

具体案例:部署 70B 模型时,TensorRT-LLM 将连续 8 个小 kernel 融合为 2 个大 kernel,并把 KV cache 更新与 next token 采样调度到不同 CUDA stream 上并行,端到端吞吐提升约 30%。

自动驾驶芯片算子部署

阶段:边缘部署 / 编译优化

解决的问题:车载芯片包含 CPU、GPU、NPU 等多种异构计算单元,CNN 算子调度不当会导致部分核心闲置、单帧时延超出预算。利用关键路径分析,把检测网络的主干分支与检测头错峰调度,能显著压缩端到端延迟。

具体案例:在 J5 智驾芯片上部署检测模型时,依据关键路径重排算子,使主干网络计算与后处理并行执行,单帧时延从 33ms 降到 22ms,满足了实时性要求。

云端训练通信与计算重叠

阶段:分布式训练优化

解决的问题:多卡训练中 allreduce 通信常常让 GPU 空等,白白浪费算力。调度器把反向传播中的梯度计算与梯度通信算子错峰安排,让通信与计算在时间上重叠,隐藏通信延迟。

具体案例:在 PyTorch DDP 中,将梯度桶的 allreduce 调度到下一层反向计算的同时执行,通信被“藏”在计算背后,4 卡训练实测加速约 1.6 倍。

Python 演示

python67 行
from collections import deque def build_model():    # 每个算子: (耗时, 依赖的算子列表)    ops = {        'Input':  (1, []),        'Conv1':  (1, ['Input']),        'BN':     (1, ['Conv1']),        'Conv2':  (1, ['Conv1']),        'ReLU':   (1, ['Conv2']),        'Add':    (1, ['BN', 'ReLU']),        'Output': (1, ['Add']),    }    return ops def analyze_and_schedule(ops):    names = list(ops.keys())    indeg = {n: len(ops[n][1]) for n in names}    adj = {n: [] for n in names}    for n in names:        for dep in ops[n][1]:            adj[dep].append(n)     # Kahn 拓扑排序,同时计算每个算子的最早开始时间    start = {n: 0 for n in names}    order = []    q = deque([n for n in names if indeg[n] == 0])    while q:        u = q.popleft()        order.append(u)        for v in adj[u]:            start[v] = max(start[v], start[u] + ops[u][0])            indeg[v] -= 1            if indeg[v] == 0:                q.append(v)     finish = {n: start[n] + ops[n][0] for n in names}    critical_len = max(finish.values())     # 贪心就绪调度:每个时间单位执行所有已就绪算子    ready = deque(n for n in names if not ops[n][1])    done = set()    remaining = {n: ops[n][0] for n in names}    time = 0    while ready:        next_ready = []        for _ in range(len(ready)):            u = ready.popleft()            remaining[u] -= 1            if remaining[u] == 0:                done.add(u)                for v in adj[u]:                    if v not in next_ready and all(d in done for d in ops[v][1]):                        next_ready.append(v)        time += 1        for v in next_ready:            ready.append(v)    return order, critical_len, time if __name__ == '__main__':    ops = build_model()    order, critical_len, greedy_time = analyze_and_schedule(ops)    serial_time = sum(v[0] for v in ops.values())    print('拓扑序:', ' -> '.join(order))    print('串行耗时:', serial_time)    print('关键路径长度:', critical_len)    print('贪心就绪调度耗时:', greedy_time)
目的:对一个小型 CNN 计算图做拓扑排序、关键路径分析,并模拟贪心就绪调度,对比串行、关键路径下界与贪心调度的耗时。
  1. indeg = {n: len(ops[n][1]) for n in names}统计每个算子的前驱依赖数量,用于 Kahn 拓扑排序。
  2. q = deque([n for n in names if indeg[n] == 0])没有依赖的算子先入队,相当于后厨里“可以直接开工”的菜。
  3. start[v] = max(start[v], start[u] + ops[u][0])前向传播更新最早开始时间,取所有前驱完成时刻的最大值。
  4. critical_len = max(finish.values())所有算子完成时刻的最大值就是关键路径长度,也是理论上限。
  5. remaining[u] -= 1贪心模拟中每次消耗一个时间单位,代表 CPU 帮算子“走一秒”。
  6. all(d in done for d in ops[v][1])检查一个算子的所有前驱是否都已完工,只有全部完成它才就绪。

常见误区

误区“GPU 核心那么多,把所有算子都丢到不同流上并行执行,利用率一定最高、速度一定最快。”

正解并行度受数据依赖和硬件资源双重限制。无脑并行会让大量 kernel 同时抢占计算单元与显存带宽,反而增加调度和同步开销。调度的目标是“该并行的并行、该串行的串行”,就像不能让八个厨师挤在一个灶台上,正确做法是让每个厨师都有自己的分工。

为何常见:直觉上“人多好干活”,忽略了依赖关系、资源上限和并发调度成本。

误区“关键路径决定总时间,所以关键路径上的每个算子都要尽可能排到所有其他算子前面。”

正解关键路径真正的要求是“尽早开始”——只要它的前驱一完成就立刻执行。盲目把关键算子提前,不仅可能违反依赖,还会抢占资源、拖慢其他路径,导致关键路径反而更晚开始。调度是从全局权衡,不是给单个算子插队。

为何常见:把“重要”简单理解为“排最前”,忽略了就绪条件与全局资源竞争。

误区“融合能省访存,那把全图都融成一个 kernel,性能肯定起飞。”

正解融合过多会让单个 kernel 的寄存器压力暴涨、占用率下降,局部性变差后反而更慢。工程上的做法是“适度融合”,如经典的 Conv+BN+ReLU,并用 benchmark 实测验证收益,而不是盲目追求融合数量。

为何常见:只看到融合减少访存的一面,忽略了 GPU 资源约束和代价模型的复杂性。

误区“PyTorch 2.0 会自动做图优化和调度,任何模型都能白拿加速。”

正解图模式对静态 shape、规则结构效果显著;遇到动态 shape、复杂控制流时,可能退化为 eager 模式甚至更慢。使用 torch.compile 仍需固定 shape、减少 Python 副作用,并配合 benchmark 选择合适的编译配置。

为何常见:把编译器当成万能黑盒,忽视了图优化对问题形态的假设。

误区“每个时间步都执行所有就绪算子,总耗时肯定最短,不可能有更好的方案。”

正解在资源无限时,贪心调度的确能达到关键路径下界;但真实硬件资源有限,就绪算子同时执行会相互争抢。某些情况下,故意延迟非关键算子、优先喂饱关键路径反而更快。最优调度是 NP-hard,贪心只是“够好”的启发式。

为何常见:把“总是忙碌”等同于“总是最优”,忽略了资源竞争这一核心约束。

面试题

Q1. 算子调度优化的核心目标是什么?

单选题 · easy

  • A.减少模型的参数量
  • B.重排算子执行顺序← 正确答案
  • C.压缩权重存储格式
  • D.增加网络的深度

答案:B。算子调度不改变算子本身的内容和参数,而是像厨师长调整做菜顺序一样,重排算子的执行顺序来提升并行度、缩短总耗时。参数量压缩是剪枝量化的事,与调度无关。

Q2. 计算图中算子 v2 需要读取 v1 写出的张量,这属于什么依赖?

单选题 · easy

  • A.数据依赖(写后读)← 正确答案
  • B.控制依赖分支
  • C.反向传播依赖
  • D.参数共享依赖

答案:A。v1 先写、v2 后读,这是最典型的 RAW(写后读)数据依赖,v2 必须等 v1 完成。控制依赖来自 if 分支,反向传播和参数共享都不是这里的依赖类型。

Q3. 关键路径指的是什么?

单选题 · mid

  • A.图中包含算子最多的路径
  • B.图中耗时最长的依赖链← 正确答案
  • C.图中路径最短的链路
  • D.图中最先执行的路径

答案:B。关键路径是执行时间总和最长的那条依赖链。算子最多的路径不一定耗时最长,因为每个算子耗时不同。关键路径决定了整个图执行的时间下界。

Q4. 根据 Amdahl 定律,并行加速的上限主要取决于什么?

单选题 · mid

  • A.并行核心的数量
  • B.程序中串行部分比例← 正确答案
  • C.编译器优化级别
  • D.显存带宽的大小

答案:B。Amdahl 定律指出,即使并行核心无限多,加速比也超不过串行比例的倒数。所以调度不仅要让并行部分跑得快,更要压缩串行瓶颈部分。

Q5. 以下哪些属于常见的调度策略?

多选题 · mid

  • A.拓扑序调度← 正确答案
  • B.优先级调度← 正确答案
  • C.贪心就绪调度← 正确答案
  • D.随机打乱调度

答案:ABC。拓扑序保证依赖合法,优先级调度让关键路径算子优先,贪心调度让就绪算子立刻执行。随机打乱无法保证依赖顺序,也不利于性能,不是工程上使用的策略。

Q6. 算子融合主要减少的开销是什么?

单选题 · mid

  • A.算子本身的浮点计算
  • B.内核启动与中间访存← 正确答案
  • C.模型参数的存储量
  • D.训练数据的读取开销

答案:B。融合把多个算子合成一个 kernel,中间张量不写回显存,省去大量访存和内核启动开销。它并不减少浮点计算量,也不压缩参数。

Q7. 关于贪心就绪调度,下列说法正确的有哪些?

多选题 · hard

  • A.每步执行所有就绪算子← 正确答案
  • B.资源无限时达到关键路径下限← 正确答案
  • C.资源有限时可能不是最优← 正确答案
  • D.复杂度高于最优调度算法

答案:ABC。贪心策略的每一步都执行当前所有就绪算子,资源无限时确实能达到关键路径下界;但资源有限时算子会互相争抢,可能不是全局最优。贪心算法复杂度低,D 说法相反。

Q8. torch.compile 主要属于哪个阶段的优化?

单选题 · easy

  • A.图优化与编译← 正确答案
  • B.数据预处理
  • C.损失函数设计
  • D.超参数搜索

答案:A。torch.compile 通过 Dynamo 捕获计算图,再做图优化、算子融合与代码生成,属于编译阶段的优化。它不涉及数据、损失函数和超参数。

Q9. 算子调度面临的主要挑战有哪些?

多选题 · hard

  • A.动态形状难提前规划← 正确答案
  • B.搜索空间组合爆炸← 正确答案
  • C.代价模型预测不准← 正确答案
  • D.算子数量越多越好

答案:ABC。动态 shape 让耗时难以预估,拓扑序数量随节点数阶乘增长导致搜索爆炸,代价模型不准会让启发式选错方案。算子多并不等于好,调度是在约束下求优,D 错误。

Q10. 拓扑排序在调度中的作用是什么?

单选题 · hard

  • A.保证执行满足依赖顺序← 正确答案
  • B.缩短关键路径的长度
  • C.减少算子的总耗时
  • D.消除计算图中的循环

答案:A。拓扑排序给出一个所有边都从前向后指的合法执行序列,保证任何算子的前驱都先于它执行。它不改变路径长度和算子耗时,DAG 本身也不存在循环。