ROCm HIP Graph 概念与机制分析- 第 2 章 数据结构:节点与图

本章目标:吃透 Graph 子系统的两大数据结构骨架------GraphNode(节点)与 Graph(图)。我们逐字段拆解节点基类、盘点整个节点派生类家族、讲清楚四个关键虚接口的"契约",再看 Graph 如何用邻接表表示 DAG、如何做拓扑排序、增删节点与克隆。


2.1 GraphNode 基类:一个节点身上都挂了什么

第 1 章说过,GraphNode 是所有节点的抽象基类。它的成员可以分成四组来理解:图结构信息执行状态信息调度归属信息Packet 捕获信息

cpp 复制代码
class GraphNode : public hipGraphNodeDOTAttribute {
 protected:
  hip::Stream* stream_ = nullptr;         // 执行时被指派到的实际流
  unsigned int id_;                       // 全局唯一 ID(原子自增)
  hipGraphNodeType type_;                 // 节点类型枚举
  std::vector<amd::Command*> commands_;   // 该节点对应的底层命令
  std::vector<Node> edges_;               // 后继(出边)
  std::vector<Node> dependencies_;        // 前驱(入边)
  bool visited_;                          // 遍历用的临时标记
  int32_t stream_id_ = -1;                // 分配到的虚拟流索引
  int hw_queue_id_ = -1;                  // 硬件队列索引
  int32_t segment_id_ = -1;               // 分段调度中的段 ID
  int32_t launch_id_ = -1;                // 在整图执行序列中的启动序号
  static std::atomic<int> nextID;         // ID 分配器
  Graph* parentGraph_;                    // 所属的图
  static std::unordered_set<GraphNode*> nodeSet_;  // 全局有效节点注册表
  unsigned int isEnabled_;                // 是否启用(可 O(1) 开关)
  bool signal_is_required_ = false;       // 该命令是否需要挂完成信号
  std::vector<uint8_t*> gpuPackets_;      // 预捕获的 GPU AQL packet
  const std::string* capturedKernelName_ = nullptr;
  size_t alignedKernArgSize_ = 256;       // kernel arg 对齐大小
  int dev_id_;                            // 节点所属设备 ID
  // ...
};

分组理解:

分组 字段 作用
图结构 edges_ / dependencies_ / id_ / type_ / parentGraph_ 描述节点在 DAG 中的位置与身份
执行状态 commands_ / stream_ / visited_ / isEnabled_ 运行期真正执行时用到的对象与标记
调度归属 stream_id_ / hw_queue_id_ / segment_id_ / launch_id_ / dev_id_ 实例化阶段调度器写入的"该去哪执行"
Packet 捕获 gpuPackets_ / capturedKernelName_ / alignedKernArgSize_ / signal_is_required_ 支撑"预捕获 packet、重放时零成本"的字段

有几个设计细节值得单独点出:

① 邻接关系存在节点里,而非集中在图里。 edges_dependencies_ 让每个节点自带正反双向邻接表。这样查前驱/后继都是 O(1) 定位,代价是增删边时要两头同时维护------所以才有成对的 AddEdgeDep / RemoveEdgeDep

② 全局唯一 ID 与注册表。 id_ 来自静态原子计数器:

cpp 复制代码
GraphNode(hipGraphNodeType type, ...)
    : type_(type),
      id_(nextID.fetch_add(1, std::memory_order_relaxed)),
      // ...
{
  amd::ScopedLock lock(nodeSetLock_);
  nodeSet_.insert(this);   // 构造即登记
}

每个节点构造时把自己插入静态集合 nodeSet_,析构时移除。配套的静态方法 isNodeValid 就靠它做悬垂指针防护 ------用户 API 传进来一个 hipGraphNode_t,先查它是否还在注册表里:

cpp 复制代码
static bool isNodeValid(GraphNode* pGraphNode) {
  amd::ScopedLock lock(nodeSetLock_);
  if (pGraphNode == nullptr || nodeSet_.find(pGraphNode) == nodeSet_.end()) {
    return false;
  }
  return true;
}

这是防御用户传入非法句柄的一道安全网,GraphGraphExecUserObject 都用了同样的"静态注册表 + isXxxValid"模式,第 3 章会统一讲。

③ 析构时的双向解链。 节点销毁必须把自己从邻居的邻接表里摘干净,否则留下悬垂指针:

cpp 复制代码
virtual ~GraphNode() {
  for (auto node : edges_) {
    node->RemoveDependency(this);   // 让后继忘掉"我"这个前驱
  }
  for (auto node : dependencies_) {
    node->RemoveEdge(this);         // 让前驱忘掉"我"这个后继
  }
  for (auto packet : gpuPackets_) {
    delete[] packet;                // 释放预捕获的 packet
  }
  amd::ScopedLock lock(nodeSetLock_);
  nodeSet_.erase(this);
}

④ 拷贝构造受保护、拷贝赋值被删除。 几乎每个节点类都遵循这个约定:

cpp 复制代码
 protected:
  GraphNode(const GraphNode& node) : hipGraphNodeDOTAttribute(node) { /* ... */ }
 public:
  GraphNode& operator=(const GraphNode&) = delete;

原因是节点持有裸指针(commands_gpuPackets_、kernel 参数内存等),一次浅拷贝就会导致 double-free。把拷贝构造设为 protected,只允许 clone() 这条受控路径复制节点。


2.2 节点派生类家族

GraphNode 本身是抽象的(clone() 是纯虚函数),真正干活的是一大批派生类。按 hipGraphNodeType 分类:
#mermaid-svg-N7PIEhIfdL2jA3iJ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-N7PIEhIfdL2jA3iJ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-N7PIEhIfdL2jA3iJ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-N7PIEhIfdL2jA3iJ .error-icon{fill:#552222;}#mermaid-svg-N7PIEhIfdL2jA3iJ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-N7PIEhIfdL2jA3iJ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-N7PIEhIfdL2jA3iJ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-N7PIEhIfdL2jA3iJ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-N7PIEhIfdL2jA3iJ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-N7PIEhIfdL2jA3iJ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-N7PIEhIfdL2jA3iJ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-N7PIEhIfdL2jA3iJ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-N7PIEhIfdL2jA3iJ .marker.cross{stroke:#333333;}#mermaid-svg-N7PIEhIfdL2jA3iJ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-N7PIEhIfdL2jA3iJ p{margin:0;}#mermaid-svg-N7PIEhIfdL2jA3iJ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-N7PIEhIfdL2jA3iJ .cluster-label text{fill:#333;}#mermaid-svg-N7PIEhIfdL2jA3iJ .cluster-label span{color:#333;}#mermaid-svg-N7PIEhIfdL2jA3iJ .cluster-label span p{background-color:transparent;}#mermaid-svg-N7PIEhIfdL2jA3iJ .label text,#mermaid-svg-N7PIEhIfdL2jA3iJ span{fill:#333;color:#333;}#mermaid-svg-N7PIEhIfdL2jA3iJ .node rect,#mermaid-svg-N7PIEhIfdL2jA3iJ .node circle,#mermaid-svg-N7PIEhIfdL2jA3iJ .node ellipse,#mermaid-svg-N7PIEhIfdL2jA3iJ .node polygon,#mermaid-svg-N7PIEhIfdL2jA3iJ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-N7PIEhIfdL2jA3iJ .rough-node .label text,#mermaid-svg-N7PIEhIfdL2jA3iJ .node .label text,#mermaid-svg-N7PIEhIfdL2jA3iJ .image-shape .label,#mermaid-svg-N7PIEhIfdL2jA3iJ .icon-shape .label{text-anchor:middle;}#mermaid-svg-N7PIEhIfdL2jA3iJ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-N7PIEhIfdL2jA3iJ .rough-node .label,#mermaid-svg-N7PIEhIfdL2jA3iJ .node .label,#mermaid-svg-N7PIEhIfdL2jA3iJ .image-shape .label,#mermaid-svg-N7PIEhIfdL2jA3iJ .icon-shape .label{text-align:center;}#mermaid-svg-N7PIEhIfdL2jA3iJ .node.clickable{cursor:pointer;}#mermaid-svg-N7PIEhIfdL2jA3iJ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-N7PIEhIfdL2jA3iJ .arrowheadPath{fill:#333333;}#mermaid-svg-N7PIEhIfdL2jA3iJ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-N7PIEhIfdL2jA3iJ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-N7PIEhIfdL2jA3iJ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-N7PIEhIfdL2jA3iJ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-N7PIEhIfdL2jA3iJ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-N7PIEhIfdL2jA3iJ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-N7PIEhIfdL2jA3iJ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-N7PIEhIfdL2jA3iJ .cluster text{fill:#333;}#mermaid-svg-N7PIEhIfdL2jA3iJ .cluster span{color:#333;}#mermaid-svg-N7PIEhIfdL2jA3iJ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-N7PIEhIfdL2jA3iJ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-N7PIEhIfdL2jA3iJ rect.text{fill:none;stroke-width:0;}#mermaid-svg-N7PIEhIfdL2jA3iJ .icon-shape,#mermaid-svg-N7PIEhIfdL2jA3iJ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-N7PIEhIfdL2jA3iJ .icon-shape p,#mermaid-svg-N7PIEhIfdL2jA3iJ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-N7PIEhIfdL2jA3iJ .icon-shape .label rect,#mermaid-svg-N7PIEhIfdL2jA3iJ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-N7PIEhIfdL2jA3iJ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-N7PIEhIfdL2jA3iJ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-N7PIEhIfdL2jA3iJ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} GraphNode 抽象基类
GraphKernelNode

kernel 启动
GraphMemcpyNode

3D 拷贝
GraphMemsetNode

填充
GraphHostNode

host 回调
GraphEventRecordNode

记录事件
GraphEventWaitNode

等待事件
GraphEmptyNode

空节点
GraphMemAllocNode

图内存分配
ChildGraphNode

子图
GraphMemcpyNode1D

1D 拷贝
GraphMemcpyNodeFromSymbol
GraphMemcpyNodeToSymbol

各节点类型速览:

节点类型 DOT 形状 职责
GraphKernelNode hipGraphNodeTypeKernel octagon 启动一个 kernel,管理 kernel 参数与属性
GraphMemcpyNode hipGraphNodeTypeMemcpy trapezium 通用 3D 拷贝
GraphMemcpyNode1D hipGraphNodeTypeMemcpy trapezium 1D 线性拷贝(更轻量的常见路径)
GraphMemcpyNodeFromSymbol / ToSymbol hipGraphNodeTypeMemcpy trapezium 与全局 symbol 之间拷贝
GraphMemsetNode hipGraphNodeTypeMemset invtrapezium 内存填充
GraphHostNode hipGraphNodeTypeHost rectangle 在 host 侧执行用户回调
GraphEventRecordNode hipGraphNodeTypeEventRecord rectangle 记录一个事件
GraphEventWaitNode hipGraphNodeTypeWaitEvent rectangle 等待一个事件
GraphEmptyNode hipGraphNodeTypeEmpty rectangle 纯占位,用于组织依赖
GraphMemAllocNode hipGraphNodeTypeMemAlloc rectangle 图内存分配(第 8 章详解)
ChildGraphNode hipGraphNodeTypeGraph rectangle 内嵌一整张子图(第 8 章详解)

这里有两条继承链值得留意:

  • Memcpy 家族的三层继承GraphMemcpyNode(3D)→ GraphMemcpyNode1D(1D)→ GraphMemcpyNodeFromSymbol / ToSymbol。越往下越具体,子类复用父类的命令创建逻辑,只覆盖差异部分。比如 GraphMemcpyNode1D 在 1D 拷贝的基础上,FromSymbol/ToSymbol 只是把一端换成 symbol 解析出来的设备地址。
  • ChildGraphNode 的双重身份 :它既是 GraphNode 又是 GraphExec,是家族里最特殊的一员,留到第 8 章展开。

2.3 四个关键虚接口:节点的"契约"

GraphGraphExec 在构建与执行时,并不关心某个节点具体是 kernel 还是 memcpy------它们只通过基类指针 Node(即 GraphNode*)调用一组虚接口。这组虚接口就是节点必须履行的"契约"。最核心的四个:

clone() ------ 深拷贝自己

cpp 复制代码
virtual GraphNode* clone() const = 0;   // 纯虚,强制每个节点实现

每个派生类的实现都极其一致------new 一个自己的副本:

cpp 复制代码
GraphNode* clone() const override { return new GraphKernelNode(*this); }

它调用的正是 2.1 里那个受保护的拷贝构造函数。Graph::clone() 复制整张图时,就是逐节点调用 clone()。这是**原型模式(Prototype)**的标准用法:基类不知道具体类型,靠虚函数让每个对象"复制自己"。

CreateCommand(stream) ------ 把节点翻译成底层命令

cpp 复制代码
virtual hipError_t CreateCommand(hip::Stream* stream) {
  commands_.clear();
  stream_ = stream;
  return hipSuccess;
}

基类实现只做两件公共事:清空旧命令、记录流。派生类先调用基类版本,再构造自己的 amd::Command。以 GraphKernelNode 为例(节选):

cpp 复制代码
hipError_t CreateCommand(hip::Stream* stream) override {
  hipError_t status = GraphNode::CreateCommand(stream);  // 先做公共部分
  if (status != hipSuccess) return status;
  if (!isEnabled_) return hipSuccess;                    // 禁用则不建命令
  // ... 解析 func、校验参数、构造 launch_params ...
  status = ihipLaunchKernelCommand(command, func, launch_params, stream, ...);
  commands_.emplace_back(command);
  return status;
}

这就是模板方法模式:基类定义骨架(清理 + 记录流),子类填充差异(构造什么命令)。

EnqueueCommands(stream) ------ 把命令真正提交到流

cpp 复制代码
virtual void EnqueueCommands(hip::Stream* stream) {
  // 若节点被禁用(仅 kernel/memcpy/memset 支持),退化为一个 Marker 占位,保持顺序
  if (!isEnabled_ && (type_ == hipGraphNodeTypeKernel || type_ == hipGraphNodeTypeMemcpy ||
                      type_ == hipGraphNodeTypeMemset)) {
    // ... enqueue 一个 amd::Marker ...
    return;
  }
  for (auto& command : commands_) {
    command->enqueue();
    command->release();
  }
}

这里藏着一个重要设计------节点的启用/禁用(enable/disable) 。当一个 kernel/memcpy/memset 节点被禁用时,它不能凭空消失(否则会破坏依赖顺序),而是退化成一个 amd::Marker(屏障标记)占位。这样图的拓扑结构不变,只是那一步"什么都不做"。isEnabled_ 因此可以做到 O(1) 开关,无需重建整张图------这在图更新场景下非常有用(第 8 章)。

SetParams(node) ------ 就地更新参数

cpp 复制代码
virtual hipError_t SetParams(GraphNode* node) { return hipSuccess; }

它接收另一个同类型节点,把对方的参数拷到自己身上。典型实现是先向下转型再取参数:

cpp 复制代码
hipError_t SetParams(GraphNode* node) override {
  const GraphKernelNode* kernelNode = static_cast<GraphKernelNode const*>(node);
  return SetParams(&kernelNode->kernelParams_);
}

SetParams 是**图更新(hipGraphExecUpdate)**的基石:结构不变、只换参数时,无需重新实例化,逐节点 SetParams 即可。这又是一次"避免重建、就地复用"的空间换时间。

除这四个之外,还有 TopologicalOrderUpdateEventWaitListsGetChildGraphGraphCaptureEnabledGenerateDOT 等虚接口,多数只在特定节点(如子图)才有意义,遇到时再讲。


2.4 Graph:用邻接表表示 DAG

看完节点,再看容纳它们的 Graph。它的核心存储很简单:

cpp 复制代码
class Graph {
 private:
  std::vector<Node> vertices_;   // 所有节点(顶点集)
 protected:
  std::vector<Node> topoOrder_;  // 拓扑排序结果
 private:
  hip::MemoryPool* mem_pool_;    // 图关联的内存池
  hip::Device* device_;          // 所属设备
  // ...
};

注意:边不存在 Graph ,而是分散在每个节点的 edges_ / dependencies_ 中(见 2.1)。Graph 只持有顶点集合 vertices_,边是"分布式"存储的邻接表。

Graph 提供了一整套查询 DAG 结构的接口:

cpp 复制代码
std::vector<Node> GetRootNodes() const;   // 所有入度为 0 的节点
std::vector<Node> GetLeafNodes() const;   // 所有出度为 0 的节点
size_t GetNodeCount() const;              // 顶点数
std::vector<std::pair<Node, Node>> GetEdges() const;  // 展开所有边
  • 根节点(roots) :入度为 0,即 dependencies_.empty(),是图执行的起点。
  • 叶节点(leafs) :出度为 0,即 edges_.empty(),是图执行的终点。这两者在并行调度和"叶子同步"里都很关键(第 5、7 章)。

2.5 增删节点:维护顶点集与邻接表

新增一个节点通过 AddNode(声明在头文件,实现在 .cpp)。而典型的"带依赖新增"走的是自由函数 ihipGraphAddNode,或类似 AddExternalEventWaitNode 这种便捷方法,其套路是:先连边,再入集

cpp 复制代码
GraphNode* AddExternalEventWaitNode(hip::GraphNode* pDependencies, size_t numDependencies,
                                    hipEvent_t event) {
  GraphNode* node = new GraphEventWaitNode(event);
  for (size_t i = 0; i < numDependencies; i++) {
    pDependencies[i].AddEdgeDep(node);   // 每个前驱 → 新节点,双向连边
  }
  AddNode(node);                         // 加入顶点集
  return node;
}

AddEdgeDep 一次性维护两头(2.1 讲过),保证 edges_dependencies_ 始终一致。删除节点 RemoveNode 则相反------先解链所有邻接边,再从 vertices_ 移除。得益于节点析构里的双向解链(2.1 的 ~GraphNode),delete 一个节点也不会留下悬垂边。


2.6 拓扑排序:确定执行顺序

DAG 的意义在于"依赖先于被依赖执行"。把 DAG 展平成一个合法的线性执行序列,就是拓扑排序 ,结果存进 topoOrder_

cpp 复制代码
bool TopologicalOrder(std::vector<Node>& TopoOrder);

拓扑排序是后续一切调度的前提:无论是经典的逐节点执行(第 5 章 RunNodes),还是分段/批量调度,都建立在一个合法拓扑序之上。GraphExec 甚至直接把 topoOrder_ 当作自己的节点列表:

cpp 复制代码
std::vector<Node>& GetNodes() { return topoOrder_; }   // GraphExec 版本

注意:topoOrder_ 不包含 子图(ChildGraph)内部的节点------子图作为一个节点参与父图的拓扑排序,其内部另有自己的拓扑序。这种"分层拓扑"是子图机制的一部分(第 8 章)。


2.7 克隆整张图:clone 的两层协作

图的克隆是实例化的关键一步------hipGraphInstantiate 要把用户那张可编辑的 Graph 复制成一份 GraphExec 内部的图。Graph 提供两个重载:

cpp 复制代码
void clone(Graph* newGraph, bool cloneNodes = false) const;  // 克隆进已有对象
Graph* clone() const;                                        // 克隆出新对象

克隆逻辑分两层协作:

  1. 图层Graph::clone 遍历 vertices_,对每个节点调用其 node->clone()(2.3 的原型模式),得到节点副本。
  2. 节点层 :每个节点的 clone() 负责深拷贝自己的参数(kernel 参数、拷贝描述符等)。

克隆时还要重建边------新图里的节点副本之间要按原图的拓扑关系重新连 edges_/dependencies_。为此 Graph 维护了一张旧节点 → 新节点的映射:

cpp 复制代码
std::unordered_map<Node, Node> clonedNodes_;   // 原节点 → 克隆节点

GraphExec 通过 GetClonedNode 就能从原图节点找到实例里对应的副本:

cpp 复制代码
Node GetClonedNode(Node node) {
  if (clonedNodes_.find(node) == clonedNodes_.end()) return nullptr;
  return clonedNodes_[node];
}

这张映射表在图更新(把新参数按原节点对应关系灌到实例节点上)时也会用到。


2.8 内存池:图拥有的内存资源

Graph 还持有一个内存池 mem_pool_,在构造时从设备取得:

cpp 复制代码
Graph(hip::Device* device, const Graph* original = nullptr)
    : /* ... */ device_(device) {
  // ...
  mem_pool_ = device->GetGraphMemoryPool();   // 图内存池
}

它主要服务于 GraphMemAllocNode------图内的内存分配节点会通过 Graph::AllocateMemory / ReserveAddress / FreeMemory 等方法,从这个池子里分配/映射虚拟地址。图内存机制(graph-owned memory)是一个相对独立的话题,本章只需知道"图有一个自己的内存池",细节留到第 8 章。


2.9 本章小结

  • GraphNodeedges_/dependencies_ 把 DAG 的邻接关系分布式 存在每个节点里;id_ + 静态 nodeSet_ 注册表提供唯一标识与悬垂指针防护;持有裸指针使其拷贝受控 (拷贝构造 protected、赋值 delete、只走 clone)。
  • 节点派生类家族按 hipGraphNodeType 分工,其中 Memcpy 家族是三层继承、ChildGraphNode 是双重身份。
  • 四个关键虚接口构成节点契约:clone(原型模式深拷贝)、CreateCommand(模板方法建命令)、EnqueueCommands(提交,含 enable/disable 退化为 Marker)、SetParams(就地更新参数,支撑图更新)。
  • Graph 只存顶点集 vertices_,边分散在节点里;提供根/叶查询、拓扑排序 topoOrder_、增删节点、克隆(图层 + 节点层协作,借 clonedNodes_ 重建映射),以及一个图内存池。

下一章我们看 辅助资源管理GraphKernelArgManager(kernel 参数池)、GraphSignalManager(HW 信号池)、UserObject(引用计数生命周期),以及贯穿整个子系统的"静态注册表 + 有效性检查"模式。