从输出反向裁剪不参与本次执行的计算节点
TensorFlow 从本次请求的输出和目标操作出发,沿输入边做 BFS,只保留这些目标依赖的计算节点。
工程问题#
一个计算图可能同时包含训练、评估和多个输出。本次只请求部分输出时,保留全部节点会把无关计算带入后续执行图;有副作用的目标操作又不能仅按输出张量判断。
实际实现#
子图重写先接入 feed、fetch,再把 fetch 节点和显式 target 合并为起点。PruneForReverseReachability 使用双端队列沿 in_nodes 扩展,在入队时标记节点;随后删除未访问的普通节点,并重新连接 source/sink 边。
处理流程#
- 重写本次 feed 与 fetch,并收集显式 target 操作
- 将目标集合入队并标记,沿输入边访问所有前驱
- 删除未标记的普通节点,保留内部 source/sink
- 修复 source/sink 控制边,交给后续执行图处理
源码片段#
固定版本 · 原文摘录389 // Add the fetch nodes, also updating "name_index".390 std::vector<Node*> fetch_nodes;391 if (!fetch_rewrites.empty()) {392 TF_RETURN_IF_ERROR(FetchOutputs(g, fetch_rewrites, &name_index,393 &fetch_nodes, &out_metadata->fetch_types));394 }395396 // Prune the graph to only compute what is needed for the fetch nodes and the397 // target nodes.398 if (!fetch_nodes.empty() || !target_node_names.empty()) {399 TF_RETURN_IF_ERROR(400 PruneForTargets(g, name_index, fetch_nodes, target_node_names));401 }先添加 fetch 节点,再按 fetch 与 target 裁剪。本次执行子图的根来自请求,不是随意选择一个计算节点。
199 std::unordered_set<const Node*> targets;200 for (Node* n : fetch_nodes) {201 if (!AddNodeToTargets(n->name(), name_index, &targets)) {202 absl::StrAppend(¬_found, n->name(), " ");203 }204 }205 for (const std::string& s : target_nodes) {206 if (!AddNodeToTargets(s, name_index, &targets)) {207 absl::StrAppend(¬_found, s, " ");208 }209 }210 if (!not_found.empty()) {211 return absl::NotFoundError(absl::StrCat(212 "PruneForTargets: Some target nodes not found: ", not_found));213 }214 PruneForReverseReachability(g, std::move(targets));215216 // Reconnect nodes with no outgoing edges to the sink node217 FixupSourceAndSinkEdges(g);将输出和显式目标合为起点,核对名字有效后调用反向可达裁剪,最后修复特殊边。
224 std::vector<bool> visited(g->num_node_ids());225 for (auto node : start) {226 visited[node->id()] = true;227 }228 std::deque<const Node*> queue(start.begin(), start.end());229 while (!queue.empty()) {230 const Node* n = queue.front();231 queue.pop_front();232 for (const Node* in : n->in_nodes()) {233 if (!visited[in->id()]) {234 visited[in->id()] = true;235 queue.push_back(in);236 VLOG(2) << "Reverse reach : " << n->name() << " from " << in->name();237 }238 }239 }visited 在入队前置位,共享前驱不会重复入队;遍历方向是输入节点,因此求的是目标的依赖闭包。
241 // Make a pass over the graph to remove nodes not in "visited".242 bool any_removed = false;243 for (int i = 0; i < visited.size(); ++i) {244 if (!visited[i]) {245 Node* n = g->FindNodeId(i);246 if (n != nullptr && !n->IsSource() && !n->IsSink()) {247 g->RemoveNode(n);248 any_removed = true;249 }250 }251 }252 return any_removed;253}删除所有未访问的普通节点,但保留内部 source 与 sink;不是只过滤最终输出列表。
片段保持固定提交的连续原文;完整源码与许可证保存在本地。每段说明交代所在分支,省略部分可通过出处查看。
收益与取舍#
工程分析带来的收益
- 保留集合直接对应目标节点的依赖闭包,支持共享前驱和有环图
- 每条可达输入边只需检查一次;在标记数组初始化与全图删除扫描之外,遍历为 O(Vr + Er)
付出的代价
- 标记数组按 num_node_ids 分配,删除扫描也按节点编号空间进行,不能把总成本只算在可达节点上
- 裁剪依据图中的依赖和显式目标;遗漏副作用操作的 target 或控制依赖不会被算法自动补回
可比较的选择
反向 DFS也能求依赖闭包;需要栈而非队列,本题并不需要 BFS 的最短距离性质。
保留完整图省去裁剪分析,但失去按本次请求缩小执行子图的机会。
版本与适用范围#
对应固定提交的 tensorflow/core/graph 子图重写路径。它求反向可达集合,不执行拓扑排序,也不代表 eager 模式或 Grappler 的所有裁剪实现。
参考来源#
源码、官方文档与原始论文tensorflow/core/graph/subgraph.cc · RewriteGraphForExecution / PruneForTargets
tensorflow/core/graph/algorithm.cc · PruneForReverseReachability:BFS
tensorflow/core/common_runtime/graph_execution_state.cc · GraphExecutionState::PruneGraph
相关实现与比较#
4 个相关案例HNSW 从稀疏上层快速定位区域,再在底层扩大候选搜索。
借助计算依赖图与使用者计数,清理既不影响输出也无副作用的节点。
Linux IIO 的 kfifo 缓冲在采样进入内核与用户态读取之间暂存数据,按先进先出顺序交付采样记录。
Linux BPF 验证器的 check_cfg 使用显式指令栈执行非递归 DFS,检查跳转目标、可达性及控制流结构。