想象你是一家甜品中央厨房的厂长。你接手后做的第一件事,不是急着烤蛋糕,而是在墙上挂一张生产流程图:搅拌机、烤箱、裱花台、打包台依次排开,传送带把面糊、烤胚、奶油从一个工位送到下一个工位。这张图不关心谁在操作,只关心什么料需要先在哪个工位做好、再流向哪里。AI 编译器眼中的模型,和这张图异曲同工:算子就是工位,张量就是半成品,从工位流向工位的数据边就是传送带。这张图被称为数据流图 Dataflow Graph,是 AI 编译器进行并行计算、显存复用和自动优化的总蓝图。本篇文章会带你亲手看懂这张图:它由哪些元素组成、怎么构建、如何判断依赖、如何挖掘并行、如何复用内存、如何做图变换,以及动态图与静态图的关系。
掌握数据流图,就等于拿到了 AI 编译器的地图与仪表盘:它决定了模型能否被高效并行、省显存地在真实硬件上跑起来。
可视化演示
展示数据流图三维度:DAG构建(算子为节点张量为边7节点7边分层布局按拓扑层级排列)、依赖分析与拓扑排序(最早开始时间EST所有前驱完成后最早时刻/关键路径A→B→D→E长度8决定最小执行时间)、并行性分析(无数据依赖算子可并行执行3条lane 7个task总时间8并行后5并行度等于关键路径长度除以总工作量),三模式切换理解编译器图分析基础。
本节配有交互动效(进入视口后自动加载,约 3KB)。
'+C1+'
图的基本结构
假设你是甜品中央厨房的规划师。你面对的不是一台机器,而是一整张工位地图:搅拌机出面糊、烤箱出面胚、裱花台出奶油蛋糕。工位与工位之间由传送带联结,真正在传送带上流动的是半成品。数据流图就是这场生产流程的静态快照,它把“什么东西先生产好、再被谁拿去用”关系完整记录了下来。
在 AI 编译器中,这张图的节点叫算子,比如矩阵乘法、卷积、归一化;边叫张量,它是多维数组的数学抽象。每条有向边都表示一个数据依赖关系:箭头起点算子产出张量,箭头终点算子消费张量。如果卷积要消费输入矩阵,那么输入矩阵必须先被生产出来。正是这种依赖关系,让编译器能在真实硬件上排定计算顺序。
形式化地,一张数据流图可以写作一个有向图 \(G=(V,E)\)。顶点集合 \(V\) 是算子,边集合 \(E\) 是张量;通常我们希望它是有向无环图,即不存在“我等你、你等我”的循环等待。除数据边外,图中还可能出现控制依赖边,用来表达“虽然没有张量流动,但仍然要先做 A 再做 B”的顺序约束。
有了这张图纸,编译器才第一次“看见”整个模型。你不会从中间开始烤蛋糕,编译器也不会在依赖未满足时执行算子:图纸上每一根箭头,都是后续所有分析和优化的证据。
- 图的二元组\(G=(V,E)\) 表示一张图由顶点集合和边集合组成,就像工厂图纸上“工位+传送带”两样东西。
- 顶点集合\(V=\{v_i\}\) 列出所有算子,每个 \(v_i\) 是一个工位。
- 边集合\(E=\{(v_i,v_j)\;|\;\dots\}\) 说明只要算子 \(v_i\) 产出张量、\(v_j\) 消费张量,就连接一条箭头。
图的构建流程
假设你研发了一款新品“焦糖海盐蛋糕”,中央厨房要修改生产流程:先在图纸上添加一个“熬焦糖”工位,再把原先从“烤胚”到“裱花”的传送带剪开,插入新工位。整个动作在编译器里就是数据流图的构建:框架前端每遇到一个算子,就往图上新增一个节点;每传递一个张量,就在生产者和消费者之间新增一条边。
实际的构建过程通常由前端驱动。以 PyTorch 的 torch.fx、TensorFlow 的 Grappler 或编译器前端 Relay 为例,程序模型会被遍历一遍:遇到卷积层,创建 Conv2D 节点;遇到它的输入张量,让输入张量对应的输出节点与 Conv2D 节点建立边关系。边的本质不是“层号顺序”,而是张量标识——每个张量都有唯一的名字或句柄,构建器通过张量标识查找生产者和消费者,从而确定边。
构建数据流图还需要处理一种特殊边:控制依赖。比如两个算子没有数据关系,但都写同一个全局缓存,为了保证顺序,编译器会插入一条虚线边。它对张量计算没有贡献,只表达“必须谁先执行”。
构建流程看似机械,但它决定了后续所有分析的质量。正如一张错漏百出的厨房图纸会让高峰期的流水线崩溃,一张边缺失的图会让编译器误判依赖、错误并行,甚至造成数据竞争。
- 构建动作\(\text{build}(f)\) 表示把一段模型程序 \(f\) 变成数据流图,类似于把新配方变成墙上的流程图。
- 全称条件\(\forall t\) 表示对每一个张量 \(t\) 都要检查一遍,不能漏掉任何半成品。
- 相交判断\(t\in \text{Out}(v_p)\cap \text{In}(v_q)\) 说明张量 \(t\) 既从 \(v_p\) 产出,又被 \(v_q\) 消费。
- 连边结论\((v_p,v_q)\in E\) 表示在图上添加一条数据边,箭头从产出者指向消费者。
依赖分析方法
厨房排产有个铁律:不能随意开工。如果烤胚需要面糊,就必须先搅拌再烘烤。把这种规则形式化,就是拓扑排序:给每个节点一个次序编号,让所有箭头都从小编号流向大编号。只要图是有向无环图,就一定存在至少一个这样的排序。
最常用的实现是“不断找出当前入度为 0 的节点”。入度是“还有多少前置依赖没完成”的计数器。一个工位当所有前置工位都完成后,入度变为 0,就可以排入执行队列。重复这个过程,直到所有节点都被排序。若有节点始终无法排入,说明图里有环,相当于两个工位互相等对方的半成品,永远死锁。
拓扑排序的价值不止是“排个顺序”。它能帮助编译器计算关键路径:从输入到输出的最长依赖链。关键路径上的算子总时间决定了整个模型的最低执行延迟;不在关键路径上的算子也许可以晚点执行,也许可以挪到其他设备上去做。
依赖分析是数据流图的核心引擎。有了拓扑排序与关键路径,编译器才知道“谁先谁后”“哪里能等”“哪里不能等”。这也是为什么 AI 编译器工程师常说:优化看的是依赖,不是看节点数量。
- 源点基准\(\text{rank}(\text{source})=0\) 说明没有前置依赖的算子排在最前面,等级为 0。
- 前驱集合\(\text{pred}(v)\) 表示所有通过边指向 \(v\) 的算子,也就是 \(v\) 等谁。
- 取最大值\(\max\) 表示 \(v\) 必须等最晚完成的前驱,因此取所有前驱中最大的等级。
- 加一递推\(1+\max\) 得到 \(v\) 自己的等级,所有依赖排完后才能执行它。
并行性挖掘
经营厨房时你会安排两个独立工序同时进行:一边烤饼干,一边泡奶茶。它们互不消费对方的半成品,所以可以在两套设备上并行。数据流图把这种直觉变成了精确判据:两个算子之间不存在任何可达路径,就可以并行执行。
这里的“可达”指沿着有向边能从一个算子走到另一个算子。若从 A 能走到 B,说明 B 直接或间接消费 A 的数据,必须等 A;若能同时从 B 走到 A,说明存在循环依赖,图本身非法。只有当 A 走不到 B、B 也走不到 A,两者才是真正独立的。
并行性挖掘不只在单个设备内部。编译器可以把图切分成多个子图,分配给 GPU、CPU、NPU 等多个设备。切图要找到“边权重小、计算量大”的分割位置,因为每一条跨设备边都意味着一次通信,好比在两个厨房之间运送半成品会额外增加包装和运输成本。
并行不是无限堆工位,而是“哪里有独立工序,哪里才能并行”。数据流图把不可见的数据依赖变成了可计算的路径问题,这是 AI 编译器跨设备调度和算子级并行的数学基础。
- 并行记号\(v_a \parallel v_b\) 表示算子 \(v_a\) 与 \(v_b\) 可以并行执行。
- 正向不可达\(v_a \not\prec v_b\) 表示从 \(a\) 出发沿着箭头走不到 \(b\),所以 \(b\) 不依赖 \(a\)。
- 反向不可达\(v_b \not\prec v_a\) 表示从 \(b\) 也走不到 \(a\),所以 \(a\) 不依赖 \(b\)。
- 合取关系\(\wedge\) 表示两个条件必须同时成立,才能断定没有数据依赖。
内存复用
甜品厨房最贵的不是机器,而是盛放半成品的托盘。烤好的胚子占一个托盘,裱好花的蛋糕占一个托盘;一旦某道工序的消费者全部拿到半成品,托盘就可以洗出来装下一批。AI 编译器面临同样问题:GPU 显存昂贵,每个张量都独占一块内存会迅速耗尽。
编译器用活跃分析判断张量何时可以被释放。一个张量的生命周期从它的生产者被计算开始,到最后一个消费者被计算结束。在这段区间内,张量必须留在内存里;一旦越过最后一个消费者,它就不再活跃,那块内存可以被回收。两个张量如果生命周期不重叠,就可以共用同一块缓冲区。
内存复用和原地操作并不完全相同。原地操作指算子的输出直接覆盖输入张量,比如 ReLU(x) 直接改写 x 的内存;这要求修改前不再有其他消费者需要原输入。而内存复用可以选择一个完全不同的张量来继承旧缓冲区,只要活跃区间错开,对计算结果没有影响。编译器中的内存规划器,就是一个“托盘调度员”。
内存复用是大模型训练能跑起来的隐形功臣。没有它,一个 70B 参数模型的中间激活值可能需要数倍于权重大小的显存;有了它,编译器才能在不改变计算结果的前提下,让一个“托盘”服务多个先后出现的张量。
- 活跃定义\(\text{live}(t,i)\) 表示在执行位置 \(i\),张量 \(t\) 仍然占用内存。
- 已经产出\(\text{producer}(t)\le i\) 表示张量已经由生产者计算出来。
- 还没被用完\(i\le \text{last\_consumer}(t)\) 表示最后一个消费者还没执行完。
- 合取判断两个条件同时为真时张量才活跃,否则就可以释放或复用内存。
图优化变换
中央厨房每天的流程并非最优:烤箱和冷却架明明可以合并成一体机;同一个原料称量被不同工位重复做了两遍;还有一道菜从不上菜单,却每天占用工位。数据流图优化的目标,就是通过变换让图“算得更快、占得更少”,同时保持语义不变。
最常见的变换是算子融合:把逻辑上连续的 A 算子与 B 算子合并成一个融合算子。比如卷积、批归一化、ReLU 原本要写三次中间张量到显存,融合后可以一次算完,减少读写和内核启动开销。死代码消除则删除不会影响最终输出的节点:如果一个节点的输出没有路径到达图输出,它就是从不见客的菜品,直接摘掉。公共子表达式消除把两份完全相同的重复计算合并为一份;常量折叠则在编译期就算出常量表达式的结果,运行时不再计算。
这些变换都必须遵守“语义等价”原则:优化后的图对相同输入要产生相同输出。就像你可以改流程,但不能改变蛋糕的味道。算子融合的数学语义可以用复合函数表示:
图优化是 AI 编译器最“聪明”的部分。它没有改变算法本质,却通过调整工厂流程把成本降了下来。数据流图的价值正在于此:节点和边是数据,而变换是施加在这些数据上的自动化工程决策。
- 融合算子\(fuse(A,B)(x)\) 表示把 \(A\) 和 \(B\) 合并后的新算子,输入仍为 \(x\)。
- 内层计算\(A(x)\) 表示先将原始输入 \(x\) 交给算子 \(A\),得到中间结果。
- 外层计算\(B(A(x))\) 表示把中间结果直接交给算子 \(B\),中间不会额外落盘。
静态图执行
中央厨房有两种经营方式:一种是边看菜谱边做,灵活但每道菜都要现场思考;另一种是营业前把所有菜谱和排产计划固化下来,高峰期一开工就机械地跑完。AI 编译器的静态图执行对应第二种方式:图先被完整构建、优化,然后才真正执行。
静态图的生命周期可以分成三个阶段。第一阶段是构建:前端遍历模型,生成数据流图。第二阶段是优化:编译器对图做算子融合、死代码消除、内存复用等变换。第三阶段是执行:调度器按拓扑排序生成可执行序列,运行时引擎把每个节点映射到底层算子或机器指令。因为是先建图后执行,编译器能看见整张图做全局决策。
这种“先定稿再开火”的模式带来巨大收益:只要图不变,优化只需做一次,之后可以反复在大量数据上运行。推理引擎在生产环境中的低延迟,很大程度上来自这种静态图编译。运行时调度器会尽量按关键路径安排计算,同时让独立的子图并行执行,最终输出一段稳定的调度序列。
静态图执行牺牲了一部分“灵活”,换来了可规划、可优化、可重复的高性能路径。对生产环境推理和大型训练任务来说,这种“先画出完整蓝图再动工”的方式,往往是效率的首选。
- 调度结果\(\text{schedule}(G)\) 表示对整张数据流图生成一个可执行序列。
- 序列展开\([v_1,\ldots,v_n]\) 表示图中 \(n\) 个算子依次排列,每个算子正好出现一次。
- 依赖约束\(\forall\,(v_i,v_j)\in E:\ i<j\) 表示每条边都必须满足源点在终点之前。
- 整体含义只要序列满足所有边的方向,它就是合法执行顺序,可以交给运行时引擎。
动态与静态图
如果你曾经用 PyTorch 写过模型,可能遇到过两种感受:默认的 eager 模式像边做菜边写菜谱,每写一行代码立刻执行,哪一步出错马上能看到;而 torch.jit.trace 或 torch.compile 又像先做一遍演示、再把完整流程录成菜谱,后续照单执行。前者是动态图,后者是静态图。
动态图的优点是直觉、好调试、支持任意 Python 控制流。缺点是执行时看不到未来,无法做全局优化。静态图则相反:它先通过追踪或扫描等方式把算子执行轨迹捕获成数据流图,再进行编译优化。追踪只记录实际执行过的路径,如果模型里有依赖数据的 if 分支,追踪得到的图可能不完整;因此还需要 脚本化或专门的循环控制节点来补全动态控制流。
现代框架越来越倾向混合模式:开发阶段用动态图,调试方便;部署和性能关键阶段切换到静态图,获得并行、融合和内存复用。数据流图就是从“动态直觉”走向“静态高效”的桥梁。两者不是谁取代谁,而是不同场景下的权衡取舍。
动态图让人舒服,静态图让机器舒服。数据流图是这两种模式之间的通用语言:无论模型最初怎么写,最终一旦落到图上,AI 编译器就能用同一套依赖分析、并行规划和内存优化工具去压榨硬件性能。
- 动态函数\(f_{\text{eager}}\) 是逐算子立即执行的原始模型函数。
- 追踪捕获\(\text{Trace}(f_{\text{eager}})\) 记录一次实际执行中的算子和张量依赖,生成数据流图。
- 静态优化\(\text{Optimize}\) 对追踪到的图做融合、删除、内存复用等变换。
- 执行结果\(\text{Execute}(\dots, x)\) 使用优化后的图计算输入 \(x\),得到与动态图等价的输出。
AI 应用场景
大模型训练中的显存复用
阶段:内存复用与显存规划
解决的问题:训练大模型时,激活值、梯度和优化器状态会同时占据显存。传统做法是每层分配独立缓冲,极易在反向传播时出现内存峰值。AI 编译器利用数据流图做活跃分析,找出生命周期不相交的张量,让它们共享缓冲区,从而降低峰值显存。
具体案例:训练语言大模型时,编译器会根据数据流图分析每个激活张量的 producer 和 last_consumer,在反向传播中复用前向阶段的激活内存。某 70B 参数模型在采用图级内存规划后,单卡可训练序列长度明显提升,显存占用下降约 30%。
推理引擎中的算子融合
阶段:图优化变换
解决的问题:卷积、批归一化、ReLU 如果作为三个独立内核执行,每个内核都要把中间结果写回显存再读出来,延迟和带宽开销很大。数据流图把这些算子识别成一条连续依赖链,并融合成单个内核。
具体案例:TensorRT、TVM 等推理引擎在编译 ResNet 类模型时,把 Conv2D → BatchNorm → ReLU 子图替换为一个融合算子。对同一个输入,融合后的 CUDA 内核减少两次中间张量读写,端到端推理延迟降低 20%–40%。
多设备并行与图切分
阶段:并行性挖掘与调度
解决的问题:单张显卡无法容纳超大模型时,需要把计算分到多张卡上。数据流图中互相不可达的子图可以并行执行,编译器据此切割图,并在跨设备边界插入通信算子。
具体案例:TensorFlow 与 PyTorch 分布式训练编译器会把数据流图按设备内存和通信开销切分,将不同子图分配到不同的 GPU 或 TPU 上。每条跨设备边变成一次 All-Reduce 或 Send/Recv,整个训练吞吐量显著提升。
Python 演示
class Node: def __init__(self, name, op_type): self.name = name self.op_type = op_type self.inputs = [] self.outputs = [] class DataflowGraph: def __init__(self): self.nodes = [] self.name_map = {} def add_node(self, name, op_type): node = Node(name, op_type) self.nodes.append(node) self.name_map[name] = node return node def add_edge(self, src_name, dst_name): src = self.name_map[src_name] dst = self.name_map[dst_name] src.outputs.append(dst) dst.inputs.append(src) def topo_sort(self): indeg = {n.name: len(n.inputs) for n in self.nodes} queue = [n for n in self.nodes if indeg[n.name] == 0] order = [] while queue: node = queue.pop(0) order.append(node) for out in node.outputs: indeg[out.name] -= 1 if indeg[out.name] == 0: queue.append(out) if len(order) != len(self.nodes): raise ValueError("graph has cycle") return order def can_reach(self, a, b, seen=None): if a is b: return True if seen is None: seen = set() if a.name in seen: return False seen.add(a.name) for out in a.outputs: if self.can_reach(out, b, seen): return True return False def parallel(self, a, b): return not self.can_reach(a, b) and not self.can_reach(b, a) def dead_code_eliminate(self, keep_names): keep = set() def mark(node): if node.name in keep: return keep.add(node.name) for inp in node.inputs: mark(inp) for name in keep_names: mark(self.name_map[name]) self.nodes = [n for n in self.nodes if n.name in keep] def main(): g = DataflowGraph() g.add_node("x", "Input") g.add_node("w", "Const") g.add_node("conv", "Conv2D") g.add_node("bn", "BatchNorm") g.add_node("act", "ReLU") g.add_node("dead", "Dropout") g.add_edge("x", "conv") g.add_edge("w", "conv") g.add_edge("conv", "bn") g.add_edge("bn", "act") print("topo:", [n.name for n in g.topo_sort()]) print("parallel conv bn:", g.parallel(g.name_map["conv"], g.name_map["bn"])) g.dead_code_eliminate(["act"]) print("after DCE:", [n.name for n in g.topo_sort()]) if __name__ == "__main__": main()
class DataflowGraph:把“中央厨房图纸”抽象成一个容器,负责保存节点和连接关系。def add_node(self, name, op_type):在图纸上新增一个算子工位。src.outputs.append(dst)在生产者与消费者之间铺设传送带,即数据边。indeg = {n.name: len(n.inputs) for n in self.nodes}统计每个工位还差多少前置依赖。queue = [n for n in self.nodes if indeg[n.name] == 0]从一开始就没有依赖的节点入队。order.append(node)弹出一个可执行节点,把它排进拓扑顺序。return not self.can_reach(a, b) and not self.can_reach(b, a)互相不可达,才算真正的并行。self.nodes = [n for n in self.nodes if n.name in keep]删除不会影响最终输出的“幽灵工位”。
常见误区
误区初学者常常觉得数据流图就是神经网络结构图,边代表层的先后顺序,不关心中间张量到底是什么。
正解数据流图里每条边都携带一个明确的张量,语义是“生产者的输出被消费者使用”。两个层之间可能有多条边、也可能有跨层连接;边不是“调用顺序”,而是数据依赖关系。中央厨房的关键不是工位编号,而是传送带上真正流动的面糊和奶油。
为何常见:教程中常用“长方体 → 箭头 → 长方体”来画网络结构,容易让人误以为箭头只是模块之间的装饰性连接。
误区既然要按依赖排序,那数据流图的执行顺序应该是唯一的,照着走就行。
正解只要保证每条边都从前往后,多个互相独立的节点可以任意互换位置。拓扑排序的结果不唯一。例如 A 和 B 互不依赖,既可以先 A 后 B,也可以先 B 后 A。调度器会结合关键路径、设备位置和内存代价选择更优的那一个。
为何常见:教材为了展示算法,通常会画一条线性链,让人误以为所有图都只有一条确定顺序。
误区动态图支持 Python 的 `if` 和 `while`,静态图只有一条直路,凡是带控制流的模型都必须用动态图。
正解静态图中可以用控制流算子(如 Switch、Merge、Loop)与控制依赖边表示分支和循环,只是不能直接依赖 Python 解释器。追踪方式只记录实际走到的路径,所以 Python 层的动态控制流需要被显式编译成图中的控制流节点才能完整表示。
为何常见:初学者接触的静态图大多是简单的线性 CNN,没见过包含控制流算子的大模型图。
误区只要把算子写成原地修改输入张量,就能省显存,所以原地操作是内存复用的唯一手段。
正解in-place 是内存优化的一种方式,但有严格前提:输入张量不再被其他消费者使用。真正的显存规划是分析整个图的活跃区间,让生命周期不重叠的任意张量共享缓冲区。即使不使用 in-place,编译器也能通过 buffer 分配实现大幅省显存。
为何常见:看到 inplace=True 能省显存的例子后,容易把局部技巧当成唯一原理。
面试题
Q1. 数据流图中的“边”最准确的语义是什么?
- A.算子之间的调用顺序
- B.张量及其数据依赖← 正确答案
- C.模型参数的梯度路径
- D.程序员编写的源码行
答案:B。数据流图中的边代表一个张量从生产者流向消费者,即数据依赖。调用顺序是拓扑排序的结果,不是边本身;梯度路径是反向图的概念;源码行与图结构没有直接对应关系。
Q2. 一张可被编译器批量调度的数据流图通常被要求是?
- A.完全二叉树
- B.有向无环图← 正确答案
- C.无向连通图
- D.有向强连通图
答案:B。数据流图使用有向边表达依赖,并且不能有循环等待,否则无法做拓扑排序。正因如此,它通常被限制为有向无环图 DAG。
Q3. 拓扑排序输出的是什么?
- A.所有节点的内存地址
- B.满足依赖关系的执行顺序← 正确答案
- C.图中的最短执行路径
- D.算子输出的最大数值
答案:B。拓扑排序把所有节点排成一个序列,使每条有向边都从前往后。它不关注内存地址、也不做数值计算,目的是得到合法执行顺序。
Q4. 两个算子满足什么条件时可以并行执行?
- A.它们使用了相同算子类型
- B.它们在拓扑排序中相邻
- C.它们之间不存在依赖路径← 正确答案
- D.它们拥有相同大小的输出
答案:C。若两个算子互相都不可达,则没有数据依赖,可以并行。类型相同、相邻或输出大小相同都不构成并行条件,反而相邻节点之间可能有一条依赖边。
Q5. 死代码消除 DCE 的删除依据是什么?
- A.节点运行时间最短
- B.节点与图输出之间无有效路径← 正确答案
- C.节点的输入全部是常量
- D.节点只在动态图中出现
答案:B。如果一个节点的输出不可能传递到图输出节点,它就是死代码。输入为常量适合做常量折叠,而不是死代码删除;运行时间与 DCE 无关。
Q6. 内存规划时,两个张量可以共用同一块缓冲区的前提是?
- A.形状完全相同
- B.生命周期不重叠← 正确答案
- C.数据类型都是浮点
- D.来自同一算子
答案:B。只要两个张量的活跃区间不相交,它们就不会同时被使用,因此可以复用缓冲区。形状与数据类型只决定缓冲区大小,不是能否复用的充分条件。
Q7. 算子融合 fuse(A,B)(x)=B(A(x)) 的核心收益是什么?
- A.减少模型参数量
- B.减少中间张量读写与内核启动开销← 正确答案
- C.提高模型精度
- D.让计算图变成并行图
答案:B。融合后中间结果不再写回显存,也少了一次内核启动,因而更省带宽、延迟更低。参数数量和精度通常不变,融合也不直接产生并行。
Q8. 静态图相比动态图的优势有哪些?
- A.可先做全局优化再执行← 正确答案
- B.更容易做并行与内存规划← 正确答案
- C.天然支持任意 Python 语法
- D.部署时推理效率更高← 正确答案
答案:ABD。静态图先构建完整图,再做融合、调度、内存复用等全局优化,因此部署效率高。它并不天然支持任意 Python 语法,Python 级控制流需要追踪或显式转换。
Q9. 关于控制依赖边,以下说法正确的有?
- A.控制依赖边不携带张量← 正确答案
- B.它只表示执行顺序约束← 正确答案
- C.它可以替换所有数据依赖
- D.它常用于副作用或先决条件← 正确答案
答案:ABD。控制依赖表达“必须先做 A 再做 B”,但没有张量流动。它不能替代数据依赖,因为数据依赖负责的是计算结果是否就绪。
Q10. 构建数据流图时,添加一条数据边需要哪些信息?
- A.算子的输入输出张量标识← 正确答案
- B.算子的名称和所属设备
- C.哪个算子产出该张量← 正确答案
- D.哪个算子消费该张量← 正确答案
答案:ACD。连边必须知道张量标识、生产者节点和消费者节点。算子名称与设备属于调度元数据,对确定边的连接关系不是必需信息。