想象一座大型快递分拣中心:包裹在传送带上川流不息,每个工位负责贴单、称重、扫码,加工完就传给下一个。这条流水线运转得很顺畅,直到有一天你发现,包裹不能只走直线——发往华东的要进 A 分拣口,发往华北的进 B 分拣口;被机器压坏的包裹要送回返修间重新处理。于是分拣中心多出了两样东西:会看标签的闸口,和绕回原点的循环传送带。AI 计算图也面临同样的升级。数据像包裹一样沿着边流动,节点像工位一样做计算,这种“数据流”模型表达不了“如果去 A 否则去 B”和“绕三圈再走”。要在计算图里表达条件分支与循环,就需要一套全新的控制流机制——这正是本文要讲的故事。

岗位关联

理解控制流表达,你就能看懂 TF/PyTorch 处理 RNN、动态网络和部署优化的底层逻辑,是设计 AI 编译器与调优训练推理的关键基本功。

目录
  1. 可视化演示
  2. 控制流概念
  3. 图级控制原语
  4. Switch开关算子
  5. Merge与循环模式
  6. 动态图执行
  7. 编译优化权衡
  8. 选型与演进
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示词嵌入向量空间分布、正弦位置编码热力图及词间余弦相似度矩阵,交互切换查看嵌入与位置编码核心概念。

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

嵌入空间:词向量的语义分布 语义相近的词在向量空间中距离更近 不同词类形成独立聚类簇 sim(w1,w2)=cos(v1,v2) 词嵌入捕获语义关系

控制流概念

分拣中心最初的设计只考虑一件事:包裹从 A 送到 B。传送带直线铺开,每个工位加工完就传给下一个,所有包裹都被当成同一种。可现实很快给出难题——发往华东和华北的包裹要进不同分拣口,被压坏的包裹要送回返修间重新处理。如果只铺直线传送带,设计师根本没法表达“看情况走哪条路”和“绕回来再处理一次”。

计算图就像这套传送带系统:节点是工位(算子),边是传送带(数据依赖),数据张量是包裹。数据流图有一个先天局限:它只描述“数据从哪来、到哪去”,却不描述“要不要去、去几次”。另外还有一类控制依赖边,专门表达执行先后顺序,比如“必须先算出循环条件,再决定是否进入循环体”。一旦模型需要根据输入决定走向(比如只对包含行人的图片走检测分支),或需要反复处理(比如 RNN 逐词读句子),纯数据流就失灵了。这些“非数据流”的逻辑统称为控制流。

控制流表达在计算图里有两大形态:条件分支与循环。条件分支是“如果谓词 p 成立就执行 f,否则执行 g”;循环是“把某个状态反复送进循环体,直到满足退出条件”。RNN 的按时间步展开、动态网络按输入特征走不同子网、beam search 的终止判断,背后都是这两种形态。

所以分拣中心的蓝图里必须增加两种设备:会看标签的闸口(条件分支),以及把包裹送回加工区的循环传送带(循环)。有了它们,传送带系统才从“一条直线”升级成“四通八达的管网”,这正是计算图获得控制流能力后发生的变化。

y={f(x),p(x)=trueg(x),p(x)=false\displaystyle y = \begin{cases} f(x), & p(x) = \text{true}\\[4pt] g(x), & p(x) = \text{false} \end{cases}
一个输入数据 x,根据条件 p(x) 的结果,二选一进入不同的处理函数,就像分拣闸口二选一放行。
  1. 输出 y分支的最终输出,就像分拣完成后包裹被送到出口流水线的最终结果。
    yy
  2. 谓词 p(x)条件判断开关,像包裹标签上的分拣码,决定包裹走哪条传送带。
    p(x)=true或p(x)=falsep(x) = \text{true} \quad \text{或} \quad p(x) = \text{false}
  3. 两个分支 f(x) 与 g(x)真分支和假分支的两个处理流程,可以是完全不同的工位组合。
    f(x),g(x)f(x), \quad g(x)
  4. cases 分段结构数据只能走其中一条路,这正是控制流区别于纯数据流的本质。
    {f(x),p(x)=trueg(x),p(x)=false\begin{cases} f(x), & p(x) = \text{true}\\[4pt] g(x), & p(x) = \text{false} \end{cases}

图级控制原语

分拣中心的设计师很快发现,不能为每一条新线路定制一台特殊机器,否则维护成本会爆炸。正确做法是设计几种标准化设备:闸口负责分流、合流器负责汇合、循环轨道负责回流。任意复杂的分拣流程,都是这些标准设备拼出来的。

计算图同理,AI 编译器需要提炼“图级控制流原语”——图中承担决策、循环职能的最小标准节点。原语有两个设计目标:既要保持数据依赖清晰(算子之间谁等谁),又要让控制信息显式暴露(哪条路径可能不执行、哪段会重复),这样编译器和运行时才能读懂并优化。

核心原语有三类。条件执行:Switch 与 Merge 的组合,表达“二选一”。循环机制:Enter、NextIteration、Exit,表达“重复 K 次直到条件满足”。状态转移:循环中每一轮把旧状态变成新状态,形成环状数据依赖。所有控制流最终都要能被编译成这些原语的组合,例如 RNN 的一次时间步就是一个带循环状态的转移。

有了标准化设备,工程师画分拣流程图时只需关心“闸口、合流、回路怎么接”,而不用重复设计细节;编译器也只需要针对这几种原语做专门优化。这就是图级原语的力量:把无穷的控制逻辑,收敛为有限的几种模式。

sk+1=ϕ(sk),k=0,1,…,N−1\displaystyle s_{k+1} = \phi(s_k), \quad k = 0, 1, \dots, N-1
循环的每一轮,状态 s 都经过循环体 φ 变换一次,N 轮之后得到最终状态。
  1. 循环状态 s_k第 k 轮迭代时的循环状态,像包裹在第 k 圈返修后的当前加工状态。
    sks_k
  2. 循环体 φ把一轮的加工动作(若干算子组合)打包成一个整体。
    ϕ(⋅)\phi(\cdot)
  3. 迭代轮次 k由循环条件控制的总圈数,从 0 到 N−1 共 N 轮。
    k=0,1,…,N−1k = 0, 1, \dots, N-1
  4. 状态转移关系下一轮状态由上一轮推导,形成数据上的环,这是静态图表示循环的基础。
    sk+1=ϕ(sk)s_{k+1} = \phi(s_k)

Switch开关算子

分拣中心里最显眼的设备就是闸口:一个包裹滑过来,闸口看它的分拣码,抬杆放它进左道,或者拨盘送它进右道。一次只走一边,另一个方向的传送带安安静静,没有任何包裹经过。

TensorFlow 静态图里承担这个职责的是 Switch 算子。它接收两个输入:一个数据 x 和一个谓词 p(标量条件值),输出两个分支端口。执行时只有满足条件的那个分支端口有数据,另一个输出为空——就像闸口只放行一个方向。

Switch 的语义是:真端口输出 x(当 p>0),假端口输出空 ∅。这个“空”极其重要:它告诉编译器“未激活分支没有数据流”,于是编译器可以做死路径消除——把永远不会被数据触及的分支子图整体剪掉,既省内存又省计算。TF 的 while_loop 内部判断“继续循环还是退出”,最终也是靠 Switch 完成的。

闸口的精妙之处在于它不关心分支里是什么工位,只负责“放行哪一边”。Switch 也一样,它把“往哪走”的决策从计算中剥离出来,让编译器一眼看出哪些传送带永远不会有包裹经过,直接把那段拆掉。

Switch(p,x)=(xtrue,  xfalse),xtrue={x,p>0∅,p≤0\displaystyle \text{Switch}(p, x) = \left( x_{\text{true}}, \; x_{\text{false}} \right), \qquad x_{\text{true}} = \begin{cases} x, & p > 0\\ \varnothing, & p \le 0 \end{cases}
Switch 把同一份数据 x 按谓词 p 只路由到一条分支,未选中的分支输出空 ∅。
  1. 输入 (p, x)x 是待分发的数据,p 是控制条件对应的标量值。
    (p,x)(p, x)
  2. 双输出端口Switch 有两个输出,分别对应真分支与假分支,像闸口的两条通道。
    (xtrue,  xfalse)\left( x_{\text{true}}, \; x_{\text{false}} \right)
  3. 真分支输出p 大于 0 时真端口输出 x,同时假端口被置为空。
    xtrue=x,xfalse=∅,(p>0)x_{\text{true}} = x, \quad x_{\text{false}} = \varnothing, \quad (p > 0)
  4. 空集 ∅表示该分支没有被激活,这是编译器做死路径消除的依据。
    ∅\varnothing

Merge与循环模式

闸口把包裹一分为二,可包裹总要在某个出口重新汇入主流水线,否则分拣中心会变成一堆断头路。于是设计师加了合流器:两条传送带汇成一条,谁有包裹就放谁过去。

图里对应的是 Merge 算子:从两个输入端口取数据,输出其中“非空的那一路”。Switch 解决“怎么分”,Merge 解决“怎么合”,两者总是一起出现,形成完整的分支结构。真正的循环还需要第三组原语:Enter 把初始状态“装进”循环,NextIteration 把一轮加工的结果送回循环体入口,Exit 在循环条件不满足时放行最终状态。

TF 的 while_loop 正是这样实现的:循环变量经 Enter 初始化,进入循环体;每轮末尾用 Switch 判断条件,满足则走 NextIteration 回到循环体,不满足则走 Exit 离开。梯度计算时,因为每一轮迭代都是图中显式的节点,反向传播可以沿循环展开链逐轮回传,也可以按需重算中间值。

循环传送带是分拣中心处理返修的秘诀:包裹绕一圈,工位再加工一次,绕够了就放行。图的循环原语同样让“重复”在静态结构里显式可见,编译器由此知道哪里会反复执行,并针对它做循环优化或不变量提升。

x0=Enter(xinit),xi+1=NextIteration(ϕ(xi))\displaystyle x^{0} = \text{Enter}(x_{\text{init}}), \qquad x^{i+1} = \text{NextIteration}\big( \phi(x^{i}) \big)
Enter 开启循环,第 i+1 轮的状态由第 i 轮经循环体 φ 变换后,通过 NextIteration 传回循环入口。
  1. Enter 初始化把循环初始值放进循环结构,相当于包裹第一次被送上循环传送带。
    x0=Enter(xinit)x^{0} = \text{Enter}(x_{\text{init}})
  2. 循环体加工第 i 轮状态经过循环体 φ 变换后的中间结果。
    ϕ(xi)\phi(x^{i})
  3. NextIteration 进位把本轮结果“进位”成下一轮输入,数据在图上绕了一圈。
    xi+1=NextIteration(⋅)x^{i+1} = \text{NextIteration}(\cdot)
  4. 环状依赖整体形成一条环状数据依赖,循环轮数由 Switch 的退出条件决定。
    xi+1=NextIteration(ϕ(xi))x^{i+1} = \text{NextIteration}\big( \phi(x^{i}) \big)

动态图执行

另一种分拣中心完全不用蓝图:一位经验老到的分拣员站在传送带旁,包裹来了,他看一眼标签,手一指:“这个送 A 口,那个送返修间,转三圈。”现场判断、现场指挥,流程随着包裹变化而灵活调整。

PyTorch 的动态执行就是这位分拣员。它不预先构建整张图,而是逐行解释执行 Python 代码。你在模型 forward 里写 if/for,Python 本身就是控制流引擎——完全不需要 Switch/Merge 这些原语。变长序列、动态维度的中间结果、甚至每个样本走不同网络结构,都能直接表达。

动态的优点也是它的代价:每次前向都要重新“现场指挥”一遍。为了部署,PyTorch 提供了追踪式固化:torch.jit.trace 用一份真实输入把计算过程“拍张照片”,把执行过的算子序列存成静态图;但照片里的循环已被展开成固定步数的直线路径,换一个循环次数就失效。TorchScript 的 script 模式才能保留真正的循环结构。

分拣员的灵活性令人羡慕,代价是每次包裹来都要重新想一遍;而且如果分拣员退休了(进程重启),他的经验就没了。动态图同样难以把运行信息沉淀下来供编译器优化,这正是下一节要讲的权衡。

y=fθ(x),执行路径由运行时确定的 N(x) 决定\displaystyle y = f_{\theta}(x), \qquad \text{执行路径由运行时确定的 } N(x) \text{ 决定}
动态执行中,同一份模型代码面对不同输入 x,可能走出完全不同的算子序列。
  1. 模型函数 f_θ参数 θ 固定,但内部流程不是预先画死的图。
    y=fθ(x)y = f_{\theta}(x)
  2. 动态决策量 N(x)运行时根据输入确定的循环次数或分支选择。
    N(x)N(x)
  3. 执行路径表达式路径是执行过程中“路过”出来的,而非提前建好的。
    执行路径=Path(fθ,x)\text{执行路径} = \text{Path}\big( f_{\theta}, x \big)
  4. 与静态图的对照静态图在建图瞬间就固定所有路径,动态图直到运行时才展开路径。
    y=fθ(x),N(x) 运行时才确定y = f_{\theta}(x), \quad N(x) \text{ 运行时才确定}

编译优化权衡

蓝图为主的分拣中心有一个隐藏优势:所有传送带都画在纸上之后,总设计师可以全局巡视——“这条支路永远不会被用到?拆掉!这两个工位挨着?合并成一个复合工位!”这些优化不需要运行任何包裹,光看图纸就能完成。

静态计算图天然拥有这种全局视野。编译器可以消除死路径(Switch 未激活分支)、做算子融合(把相邻小算子合成 kernel)、做常量折叠与公共子表达式消除,还能对循环做不变量提升。而在动态执行模式下,编译器每次看到的都是“刚刚跑过的路径片段”,无法建立全局视图;循环每绕一圈都要重复解释和调度,开销随迭代轮数线性放大。

更难的是图捕获:把动态代码转成静态图时,如果代码里既有动态分支又有循环,每次输入不同,捕获到的子图结构就不同,导致缓存失效、反复重编译。经典做法是“先试静态:能捕获就捕获;遇到动态控制流就回退解释执行”,这正是 tf.function 的 Autograph 机制向后端编译的妥协。

蓝图优化的魔力在循环多的模型(RNN、扩散模型的多步采样)上体现得淋漓尽致:图纸上拆掉一条死支路、合并两个工位,比现场分拣员每圈重复思考要省太多。前提是——你得愿意先画图纸,也就是接受静态图的开发约束。

Tstatic≈E,Tdynamic≈E×Rˉ\displaystyle T_{\text{static}} \approx E, \qquad T_{\text{dynamic}} \approx E \times \bar{R}
E 是单轮路径上的算子数,R̄ 是平均循环轮数。静态图一次性规划路径,动态图每轮都要现场决策。
  1. 算子数 E单次穿过计算图需要执行的有效算子数,像一趟分拣经过的工位总数。
    EE
  2. 静态执行时间接近一条最优路径的耗时,图优化后往往能进一步缩小。
    Tstatic≈ET_{\text{static}} \approx E
  3. 动态执行时间路径耗时乘以平均循环轮数,循环越深开销放大越明显。
    Tdynamic≈E×RˉT_{\text{dynamic}} \approx E \times \bar{R}
  4. 平均循环轮数 R̄衡量“绕圈”带来的重复解释成本,是动态方案的主要瓶颈。
    Rˉ\bar{R}

选型与演进

分拣中心到底用“蓝图施工”还是“能人现场指挥”?答案取决于旺季还是淡季:淡季包裹形态复杂多变,人工分拣更灵活,不怕流程改动;旺季包裹洪峰固定,必须蓝图加自动化流水线才能扛住吞吐。

对应到 AI 工程:训练阶段追求快速试错,结构频繁改动,天然适合动态执行;部署阶段追求低延迟高吞吐,结构固定,一定要静态图加编译优化。现代框架普遍采用混合方案——前端保持 Python 动态体验,后端用 tf.function、torch.compile、JAX jit 把热路径编译成静态图,既享受动态的灵活,又拿到静态的性能。

演进趋势是“编译器接管更多控制流”:TorchScript 和 ONNX 导出时自动把 if/for 翻译成图原语;XLA 在静态子图上做循环融合;AI 编译器甚至能把动态 shape 的循环也静态化。选型核心就一句话:结构稳定、循环多的路径走静态;结构多变、交互性强的地方保持动态。

聪明的高级分拣中心早已不是二选一:平时让能人现场应变,旺季按蓝图上自动化线。AI 编译器也一样,在灵活性、性能与工程成本之间,用混合方案走出第三条路——这,就是控制流表达的完整图景。

Speedup=TdynamicTstatic>1  ⇒  选择静态图方案\displaystyle \text{Speedup} = \frac{T_{\text{dynamic}}}{T_{\text{static}}} > 1 \;\Rightarrow\; \text{选择静态图方案}
当静态图相对动态执行的加速比大于 1 时,值得为这条路径做静态化改造。
  1. 耗时比值同一模型两种执行方式耗时的比值,代表静态化的收益。
    TdynamicTstatic\frac{T_{\text{dynamic}}}{T_{\text{static}}}
  2. 加速比条件比值大于 1 说明静态版本更快,静态化收益明显。
    Speedup>1\text{Speedup} > 1
  3. 决策规则收益大就静态化,收益小或结构多变就保持动态,这是工程选型的基本准则。
    Speedup=TdynamicTstatic>1\text{Speedup} = \frac{T_{\text{dynamic}}}{T_{\text{static}}} > 1

AI 应用场景

RNN 与变长序列训练

阶段:模型训练(动态执行)

解决的问题:传统静态图要求序列定长,变长句子要 pad 到统一长度,白白浪费算力;动态执行让循环由 Python 的 for 直接驱动,每个 batch 都可用不同长度,无需 padding。

具体案例:训练情感分类 LSTM,输入句子词数从 5 到 50 不等。PyTorch 中直接写 for t in range(len(inputs)): h = lstm_cell(h, inputs[t]),循环轮数随句子长短变化,框架按实际长度计算,开发体验和计算效率都得到保障。

推理部署中的静态图控制流

阶段:模型部署(静态编译)

解决的问题:在线推理要求稳定低延迟,动态解释加每轮调度在负载高时抖动明显;把控制流编译成 Switch/Merge 的静态图后,编译器能预分配内存、消除死分支、减少 kernel 启动次数。

具体案例:TF Serving 部署一个“置信度不够就循环精修”的检测模型,用 tf.while_loop 表达精修迭代,循环轮数由置信度动态决定;编译成静态图后,GPU kernel 启动次数下降约 40%,P99 延迟显著改善。

动态转静态的导出陷阱

阶段:模型交付(trace 导图)

解决的问题:把 PyTorch 模型导成 ONNX/TorchScript 时,直接 trace 会把 for 循环展开成固定长度的直线图,输入长度一变就出错;理解控制流的表达方式才能避开这个坑。

具体案例:导出 beam search 解码器,用 torch.jit.trace 后,max_len=20 的循环被写成 20 层展开,换 max_len=30 直接越界;改用 torch.jit.script 保留 while 结构后才正确导出。

Python 演示

python77 行
"""迷你静态计算图 - 演示 Switch/Merge 控制流表达"""  class MiniGraph:    def __init__(self):        self.nodes = []     def add(self, name, op, a=None, b=None, cond=None):        self.nodes.append({'name': name, 'op': op, 'a': a, 'b': b, 'cond': cond})     def run(self, feeds):        v = dict(feeds)        for n in self.nodes:            op = n['op']            name = n['name']            if op == 'input':                continue            elif op == 'const':                v[name] = n['b']            elif op == 'switch':                # 条件成立只激活一个输出端口                x = v[n['a']]                p = v[n['cond']]                v[name + '_true'] = x if p > 0 else None                v[name + '_false'] = x if p <= 0 else None            elif op == 'merge':                # 从两个分支中取非空的那一路                t = v.get(n['a'])                f = v.get(n['b'])                v[name] = t if t is not None else f            elif op == 'add':                a, b = v.get(n['a']), v.get(n['b'])                v[name] = None if a is None or b is None else a + b            elif op == 'mul':                # 死路径上的算子不产生有效值, 类似死路径消除                a, b = v.get(n['a']), v.get(n['b'])                v[name] = None if a is None or b is None else a * b        return v  def build_abs_graph():    # y = (x > 0) ? 2*x : x + 100  ->  Switch + Merge 图    g = MiniGraph()    g.add('x', 'input')    g.add('two', 'const', b=2)    g.add('hundred', 'const', b=100)    g.add('sw', 'switch', a='x', cond='x')    g.add('mul', 'mul', a='sw_true', b='two')    g.add('add', 'add', a='sw_false', b='hundred')    g.add('out', 'merge', a='mul', b='add')    return g  def while_loop(init, cond_fn, body_fn, max_iters=8):    # 图级循环的表达模型: Enter -> 迭代 -> Exit    state = init    for _ in range(max_iters):        if not cond_fn(state):            break        state = body_fn(state)    return state  if __name__ == '__main__':    g = build_abs_graph()    for x in (-3, 0, 5):        y = g.run({'x': x})['out']        print('x =', x, '-> y =', y)     def cond(s):        return s < 8.0     def body(s):        return s * 0.9 + 1.0     final = while_loop(0.0, cond, body)    print('while_loop final =', round(final, 4))
目的:用约六十行代码模拟静态计算图中 Switch/Merge 原语与图级循环的执行语义,验证条件分支与循环控制流的数据流动。
  1. v[name + '_true'] = x if p > 0 else NoneSwitch 真分支:条件成立才输出数据,否则为空
  2. v[name] = t if t is not None else fMerge 汇合:取两个分支中非空的那一路
  3. v[name] = None if a is None or b is None else a * b死路径上的算子不产生有效值,模拟死路径消除
  4. g.add('mul', 'mul', a='sw_true', b='two')把 Switch 的真分支端口接到乘法算子的输入端
  5. g.add('out', 'merge', a='mul', b='add')用 Merge 把真分支和假分支的结果合并成最终输出
  6. g.run({'x': x})['out']喂入输入后按节点顺序执行,得到分支结构的计算结果
  7. for _ in range(max_iters): if not cond_fn(state): break模拟 Enter→迭代→Exit 的图级循环控制语义

常见误区

误区控制流不就是 if 嘛,我在搭建计算图的时候直接把 if 语句写进去,图就能按条件执行了吧?

正解计算图本质是数据流图,节点只有“输入到输出”的变换,没有“语句依次执行”的概念。if 是程序语言的控制结构,不是图结构。必须用显式的图原语——Switch 分流加 Merge 汇合——把分支画成两条数据路径,编译器才看得懂、优化得了。分拣中心里“如果”是通过闸口实现的,不是写在墙上的标语。

为何常见:用 PyTorch 写模型时 if 用起来天经地义,很容易把“源代码的控制流”和“图里的控制流”混为一谈,忽略了静态图没有执行顺序的事实。

误区Switch 有两个输出,是不是像广播一样把输入复制两份,两个分支都执行,最后二选一取结果?

正解恰恰相反。Switch 的意义在于只有一个分支被激活,未激活端口输出空 ∅,编译器据此做死路径消除,把没有数据的分支整段删掉。如果两个分支都算一遍,Switch 就失去了优化价值,和普通的多路计算没区别了。

为何常见:看着 Switch 有两个输出端口,容易联想到并行计算,忽略了“条件路由”的本质是一次只放行一条传送带。

误区PyTorch 动态执行又慢又不能编译优化,那训练阶段也应该全部换成静态图。

正解动态执行牺牲全局优化,换来的是开发速度和灵活性:断点调试、逐行打印、任意 Python 控制流,这些对研究和快速迭代不可或缺。训练阶段的瓶颈常常不在解释开销而在 GPU 计算;况且 torch.compile、tf.function 已能对热路径做静态化。静态与动态是互补,不是替代。

为何常见:把“部署期静态图快”的结论错误推广到所有阶段,忽略了训练和部署的度量标准完全不同。

误区我用 torch.jit.trace 导出模型,里面 for 循环被捕获到图里,应该会自动变成一个 while 循环节点。

正解trace 的本质是“用一份输入实际跑一遍,记录执行过的算子序列”,循环在记录时已经按当时的轮数展开成直线路径了,换一个循环次数就失效。要保留循环结构,必须用 torch.jit.script 或显式图原语(如 tf.while_loop),让编译器看到“循环”这个结构本身。

为何常见:trace 的“记录执行轨迹”语义对新手不直观,容易误以为它理解了代码语义,其实它只是拍了一张执行路径的照片。

面试题

Q1. 计算图中“控制流表达”主要解决什么问题?

单选题 · easy

  • A.描述数据在节点间的流动路径
  • B.表达条件分支和循环执行逻辑← 正确答案
  • C.加快大型矩阵的乘法运算速度
  • D.减少模型参数量和存储开销

答案:B。数据流图已经负责“数据在节点间流动”(A),矩阵加速(C)和减参数(D)是其他话题。控制流要表达的是数据流之外的“往哪走、走几圈”,即条件分支与循环,对应分拣中心的闸口与循环传送带。

Q2. TensorFlow 静态图中 Switch 算子的核心作用是什么?

单选题 · mid

  • A.把两个分支的数据合并成一路
  • B.按谓词把输入路由到两个分支← 正确答案
  • C.完成两个矩阵的逐元素乘法
  • D.对网络输出执行梯度裁剪

答案:B。Switch 就是分拣闸口,按谓词 p 把数据 x 路由到真/假两个分支端口,未激活端口输出空。合并(A)是 Merge 的职责,乘法(C)和裁剪(D)与条件路由无关。

Q3. Merge 算子与 Switch 配对时承担什么职责?

单选题 · mid

  • A.把两个分支的数据汇合成一路← 正确答案
  • B.按条件复制输入到两个分支
  • C.将一条数据流拆分成两条支路
  • D.保存循环状态的每次迭代进位

答案:A。Switch 负责“分”,Merge 负责“合”——把真/假分支的数据取非空的一路汇成单一输出,就像合流器把两条传送带并成一条。B 和 C 是 Switch 的语义,D 是 NextIteration 的职责。

Q4. 静态计算图控制流原语能带来哪些编译优化机会?

多选题 · mid

  • A.可以消除永不激活的死路径分支← 正确答案
  • B.更容易做跨节点的算子融合优化← 正确答案
  • C.天然支持 Python 原生动态语法
  • D.每次运行都会重建整张计算图

答案:AB。静态图有全局视野,能看到未激活分支并剪掉(A),也能把相邻算子融合成 kernel(B)。C 是动态图特性,D 是动态执行的缺点,都不是静态图控制流的优化机会。

Q5. Enter / NextIteration / Exit 三个算子的组合用于实现什么?

单选题 · hard

  • A.条件分支的路由、汇合与剪枝
  • B.图级循环的进入、迭代与退出← 正确答案
  • C.卷积层的输入、卷积与输出
  • D.归一化层的均值、方差与缩放

答案:B。Enter 把初始状态放进循环带,NextIteration 把本轮结果送回循环体入口,Exit 在条件不满足时放行最终状态——三者合起来构成图级循环,对应分拣中心的循环传送带。A 是 Switch/Merge 的职责,C、D 是具体算子。

Q6. PyTorch 动态执行模式下编写 if/for 有什么特点?

单选题 · mid

  • A.必须显式调用 Switch 算子来分流
  • B.可以直接使用 Python 原生语法← 正确答案
  • C.需要提前把循环完全展开成线
  • D.无法根据输入动态改变执行路径

答案:B。动态执行是“现场指挥的分拣员”,Python 解释器本身就是控制流引擎,if/for 直接可用(B)。A 是静态图的做法,C 是 trace 的副作用,D 与动态执行“按输入决定路径”的本质相反。

Q7. 静态图中表示循环后,梯度是如何计算的?

多选题 · hard

  • A.反向传播时按迭代轮次逐层展开← 正确答案
  • B.存储每轮迭代中间状态用于回传← 正确答案
  • C.循环体无法参与到梯度计算之中
  • D.必须截断梯度跳过整个循环结构

答案:AB。TF while_loop 的循环体本身可微,反向时沿线展开的迭代链逐轮回传梯度(A),并存储或重算每轮中间激活(B)。C 错在循环体是可微函数组合当然参与梯度;D 只有显式 truncate 时才截断。

Q8. “死路径消除”指的是下列哪项优化?

单选题 · easy

  • A.删除训练中用不到的权重参数
  • B.未激活的分支整体不参与执行← 正确答案
  • C.把常量表达式直接折叠成结果
  • D.将相邻算子融合成单个复合算子

答案:B。Switch 未激活分支输出空 ∅,编译器据此把整段分支子图剪掉,如同拆掉永远不会有包裹的传送带。A 是剪枝,C 是常量折叠,D 是算子融合,都与控制流的死路径无关。

Q9. 以下哪些属于图级控制流原语?

多选题 · easy

  • A.Switch 条件路由算子← 正确答案
  • B.Merge 分支汇合算子← 正确答案
  • C.Enter 循环进入算子← 正确答案
  • D.ReLU 激活函数算子

答案:ABC。Switch 负责分流、Merge 负责汇合、Enter 负责开启循环,都是编译器专门为控制流设计的图原语。ReLU 是普通激活算子,只做数据变换,不承担任何决策或循环职能。

Q10. 动态执行模式下,控制流给运行时带来的主要开销是什么?

单选题 · extreme

  • A.每次迭代都要重新解释执行路径← 正确答案
  • B.静态优化无法跨循环展开进行
  • C.数据必须频繁在设备之间进行拷贝
  • D.参数更新需要额外计算梯度步骤

答案:A。动态图每次前向由解释器逐个执行算子,循环每绕一圈都重新调度和解释(A),轮数越多开销线性放大。B 是缺少优化机会的劣势而非运行时开销本身,C 是数据搬运问题,D 是参数更新问题,都不是控制流解释的核心成本。