
本章目标吃透 Graph 子系统的两大数据结构骨架——GraphNode节点与Graph图。我们逐字段拆解节点基类、盘点整个节点派生类家族、讲清楚四个关键虚接口的契约再看Graph如何用邻接表表示 DAG、如何做拓扑排序、增删节点与克隆。2.1 GraphNode 基类一个节点身上都挂了什么第 1 章说过GraphNode是所有节点的抽象基类。它的成员可以分成四组来理解图结构信息、执行状态信息、调度归属信息、Packet 捕获信息。classGraphNode:publichipGraphNodeDOTAttribute{protected:hip::Stream*stream_nullptr;// 执行时被指派到的实际流unsignedintid_;// 全局唯一 ID原子自增hipGraphNodeType type_;// 节点类型枚举std::vectoramd::Command*commands_;// 该节点对应的底层命令std::vectorNodeedges_;// 后继出边std::vectorNodedependencies_;// 前驱入边boolvisited_;// 遍历用的临时标记int32_tstream_id_-1;// 分配到的虚拟流索引inthw_queue_id_-1;// 硬件队列索引int32_tsegment_id_-1;// 分段调度中的段 IDint32_tlaunch_id_-1;// 在整图执行序列中的启动序号staticstd::atomicintnextID;// ID 分配器Graph*parentGraph_;// 所属的图staticstd::unordered_setGraphNode*nodeSet_;// 全局有效节点注册表unsignedintisEnabled_;// 是否启用可 O(1) 开关boolsignal_is_required_false;// 该命令是否需要挂完成信号std::vectoruint8_t*gpuPackets_;// 预捕获的 GPU AQL packetconststd::string*capturedKernelName_nullptr;size_t alignedKernArgSize_256;// kernel arg 对齐大小intdev_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_来自静态原子计数器GraphNode(hipGraphNodeType type,...):type_(type),id_(nextID.fetch_add(1,std::memory_order_relaxed)),// ...{amd::ScopedLocklock(nodeSetLock_);nodeSet_.insert(this);// 构造即登记}每个节点构造时把自己插入静态集合nodeSet_析构时移除。配套的静态方法isNodeValid就靠它做悬垂指针防护——用户 API 传进来一个hipGraphNode_t先查它是否还在注册表里staticboolisNodeValid(GraphNode*pGraphNode){amd::ScopedLocklock(nodeSetLock_);if(pGraphNodenullptr||nodeSet_.find(pGraphNode)nodeSet_.end()){returnfalse;}returntrue;}这是防御用户传入非法句柄的一道安全网Graph、GraphExec、UserObject都用了同样的静态注册表 isXxxValid模式第 3 章会统一讲。③ 析构时的双向解链。节点销毁必须把自己从邻居的邻接表里摘干净否则留下悬垂指针virtual~GraphNode(){for(autonode:edges_){node-RemoveDependency(this);// 让后继忘掉我这个前驱}for(autonode:dependencies_){node-RemoveEdge(this);// 让前驱忘掉我这个后继}for(autopacket:gpuPackets_){delete[]packet;// 释放预捕获的 packet}amd::ScopedLocklock(nodeSetLock_);nodeSet_.erase(this);}④ 拷贝构造受保护、拷贝赋值被删除。几乎每个节点类都遵循这个约定protected:GraphNode(constGraphNodenode):hipGraphNodeDOTAttribute(node){/* ... */}public:GraphNodeoperator(constGraphNode)delete;原因是节点持有裸指针commands_、gpuPackets_、kernel 参数内存等一次浅拷贝就会导致 double-free。把拷贝构造设为protected只允许clone()这条受控路径复制节点。2.2 节点派生类家族GraphNode本身是抽象的clone()是纯虚函数真正干活的是一大批派生类。按hipGraphNodeType分类GraphNode 抽象基类GraphKernelNodekernel 启动GraphMemcpyNode3D 拷贝GraphMemsetNode填充GraphHostNodehost 回调GraphEventRecordNode记录事件GraphEventWaitNode等待事件GraphEmptyNode空节点GraphMemAllocNode图内存分配ChildGraphNode子图GraphMemcpyNode1D1D 拷贝GraphMemcpyNodeFromSymbolGraphMemcpyNodeToSymbol各节点类型速览类节点类型DOT 形状职责GraphKernelNodehipGraphNodeTypeKerneloctagon启动一个 kernel管理 kernel 参数与属性GraphMemcpyNodehipGraphNodeTypeMemcpytrapezium通用 3D 拷贝GraphMemcpyNode1DhipGraphNodeTypeMemcpytrapezium1D 线性拷贝更轻量的常见路径GraphMemcpyNodeFromSymbol/ToSymbolhipGraphNodeTypeMemcpytrapezium与全局 symbol 之间拷贝GraphMemsetNodehipGraphNodeTypeMemsetinvtrapezium内存填充GraphHostNodehipGraphNodeTypeHostrectangle在 host 侧执行用户回调GraphEventRecordNodehipGraphNodeTypeEventRecordrectangle记录一个事件GraphEventWaitNodehipGraphNodeTypeWaitEventrectangle等待一个事件GraphEmptyNodehipGraphNodeTypeEmptyrectangle纯占位用于组织依赖GraphMemAllocNodehipGraphNodeTypeMemAllocrectangle图内存分配第 8 章详解ChildGraphNodehipGraphNodeTypeGraphrectangle内嵌一整张子图第 8 章详解这里有两条继承链值得留意Memcpy 家族的三层继承GraphMemcpyNode3D→GraphMemcpyNode1D1D→GraphMemcpyNodeFromSymbol/ToSymbol。越往下越具体子类复用父类的命令创建逻辑只覆盖差异部分。比如GraphMemcpyNode1D在 1D 拷贝的基础上FromSymbol/ToSymbol只是把一端换成 symbol 解析出来的设备地址。ChildGraphNode的双重身份它既是GraphNode又是GraphExec是家族里最特殊的一员留到第 8 章展开。2.3 四个关键虚接口节点的契约Graph和GraphExec在构建与执行时并不关心某个节点具体是 kernel 还是 memcpy——它们只通过基类指针Node即GraphNode*调用一组虚接口。这组虚接口就是节点必须履行的契约。最核心的四个①clone()—— 深拷贝自己virtualGraphNode*clone()const0;// 纯虚强制每个节点实现每个派生类的实现都极其一致——new一个自己的副本GraphNode*clone()constoverride{returnnewGraphKernelNode(*this);}它调用的正是 2.1 里那个受保护的拷贝构造函数。Graph::clone()复制整张图时就是逐节点调用clone()。这是**原型模式Prototype**的标准用法基类不知道具体类型靠虚函数让每个对象复制自己。②CreateCommand(stream)—— 把节点翻译成底层命令virtualhipError_tCreateCommand(hip::Stream*stream){commands_.clear();stream_stream;returnhipSuccess;}基类实现只做两件公共事清空旧命令、记录流。派生类先调用基类版本再构造自己的amd::Command。以GraphKernelNode为例节选hipError_tCreateCommand(hip::Stream*stream)override{hipError_t statusGraphNode::CreateCommand(stream);// 先做公共部分if(status!hipSuccess)returnstatus;if(!isEnabled_)returnhipSuccess;// 禁用则不建命令// ... 解析 func、校验参数、构造 launch_params ...statusihipLaunchKernelCommand(command,func,launch_params,stream,...);commands_.emplace_back(command);returnstatus;}这就是模板方法模式基类定义骨架清理 记录流子类填充差异构造什么命令。③EnqueueCommands(stream)—— 把命令真正提交到流virtualvoidEnqueueCommands(hip::Stream*stream){// 若节点被禁用仅 kernel/memcpy/memset 支持退化为一个 Marker 占位保持顺序if(!isEnabled_(type_hipGraphNodeTypeKernel||type_hipGraphNodeTypeMemcpy||type_hipGraphNodeTypeMemset)){// ... enqueue 一个 amd::Marker ...return;}for(autocommand:commands_){command-enqueue();command-release();}}这里藏着一个重要设计——节点的启用/禁用enable/disable。当一个 kernel/memcpy/memset 节点被禁用时它不能凭空消失否则会破坏依赖顺序而是退化成一个amd::Marker屏障标记占位。这样图的拓扑结构不变只是那一步什么都不做。isEnabled_因此可以做到O(1) 开关无需重建整张图——这在图更新场景下非常有用第 8 章。④SetParams(node)—— 就地更新参数virtualhipError_tSetParams(GraphNode*node){returnhipSuccess;}它接收另一个同类型节点把对方的参数拷到自己身上。典型实现是先向下转型再取参数hipError_tSetParams(GraphNode*node)override{constGraphKernelNode*kernelNodestatic_castGraphKernelNodeconst*(node);returnSetParams(kernelNode-kernelParams_);}SetParams是**图更新hipGraphExecUpdate**的基石结构不变、只换参数时无需重新实例化逐节点SetParams即可。这又是一次避免重建、就地复用的空间换时间。除这四个之外还有TopologicalOrder、UpdateEventWaitLists、GetChildGraph、GraphCaptureEnabled、GenerateDOT等虚接口多数只在特定节点如子图才有意义遇到时再讲。2.4 Graph用邻接表表示 DAG看完节点再看容纳它们的Graph。它的核心存储很简单classGraph{private:std::vectorNodevertices_;// 所有节点顶点集protected:std::vectorNodetopoOrder_;// 拓扑排序结果private:hip::MemoryPool*mem_pool_;// 图关联的内存池hip::Device*device_;// 所属设备// ...};注意边不存在Graph里而是分散在每个节点的edges_/dependencies_中见 2.1。Graph只持有顶点集合vertices_边是分布式存储的邻接表。Graph提供了一整套查询 DAG 结构的接口std::vectorNodeGetRootNodes()const;// 所有入度为 0 的节点std::vectorNodeGetLeafNodes()const;// 所有出度为 0 的节点size_tGetNodeCount()const;// 顶点数std::vectorstd::pairNode,NodeGetEdges()const;// 展开所有边根节点roots入度为 0即dependencies_.empty()是图执行的起点。叶节点leafs出度为 0即edges_.empty()是图执行的终点。这两者在并行调度和叶子同步里都很关键第 5、7 章。2.5 增删节点维护顶点集与邻接表新增一个节点通过AddNode声明在头文件实现在 .cpp。而典型的带依赖新增走的是自由函数ihipGraphAddNode或类似AddExternalEventWaitNode这种便捷方法其套路是先连边再入集。GraphNode*AddExternalEventWaitNode(hip::GraphNode*pDependencies,size_t numDependencies,hipEvent_t event){GraphNode*nodenewGraphEventWaitNode(event);for(size_t i0;inumDependencies;i){pDependencies[i].AddEdgeDep(node);// 每个前驱 → 新节点双向连边}AddNode(node);// 加入顶点集returnnode;}AddEdgeDep一次性维护两头2.1 讲过保证edges_和dependencies_始终一致。删除节点RemoveNode则相反——先解链所有邻接边再从vertices_移除。得益于节点析构里的双向解链2.1 的~GraphNodedelete一个节点也不会留下悬垂边。2.6 拓扑排序确定执行顺序DAG 的意义在于依赖先于被依赖执行。把 DAG 展平成一个合法的线性执行序列就是拓扑排序结果存进topoOrder_boolTopologicalOrder(std::vectorNodeTopoOrder);拓扑排序是后续一切调度的前提无论是经典的逐节点执行第 5 章RunNodes还是分段/批量调度都建立在一个合法拓扑序之上。GraphExec甚至直接把topoOrder_当作自己的节点列表std::vectorNodeGetNodes(){returntopoOrder_;}// GraphExec 版本注意topoOrder_不包含子图ChildGraph内部的节点——子图作为一个节点参与父图的拓扑排序其内部另有自己的拓扑序。这种分层拓扑是子图机制的一部分第 8 章。2.7 克隆整张图clone 的两层协作图的克隆是实例化的关键一步——hipGraphInstantiate要把用户那张可编辑的Graph复制成一份GraphExec内部的图。Graph提供两个重载voidclone(Graph*newGraph,boolcloneNodesfalse)const;// 克隆进已有对象Graph*clone()const;// 克隆出新对象克隆逻辑分两层协作图层Graph::clone遍历vertices_对每个节点调用其node-clone()2.3 的原型模式得到节点副本。节点层每个节点的clone()负责深拷贝自己的参数kernel 参数、拷贝描述符等。克隆时还要重建边——新图里的节点副本之间要按原图的拓扑关系重新连edges_/dependencies_。为此Graph维护了一张旧节点 → 新节点的映射std::unordered_mapNode,NodeclonedNodes_;// 原节点 → 克隆节点GraphExec通过GetClonedNode就能从原图节点找到实例里对应的副本NodeGetClonedNode(Node node){if(clonedNodes_.find(node)clonedNodes_.end())returnnullptr;returnclonedNodes_[node];}这张映射表在图更新把新参数按原节点对应关系灌到实例节点上时也会用到。2.8 内存池图拥有的内存资源Graph还持有一个内存池mem_pool_在构造时从设备取得Graph(hip::Device*device,constGraph*originalnullptr):/* ... */device_(device){// ...mem_pool_device-GetGraphMemoryPool();// 图内存池}它主要服务于GraphMemAllocNode——图内的内存分配节点会通过Graph::AllocateMemory/ReserveAddress/FreeMemory等方法从这个池子里分配/映射虚拟地址。图内存机制graph-owned memory是一个相对独立的话题本章只需知道图有一个自己的内存池细节留到第 8 章。2.9 本章小结GraphNode用edges_/dependencies_把 DAG 的邻接关系分布式存在每个节点里id_ 静态nodeSet_注册表提供唯一标识与悬垂指针防护持有裸指针使其拷贝受控拷贝构造 protected、赋值 delete、只走clone。节点派生类家族按hipGraphNodeType分工其中 Memcpy 家族是三层继承、ChildGraphNode是双重身份。四个关键虚接口构成节点契约clone原型模式深拷贝、CreateCommand模板方法建命令、EnqueueCommands提交含 enable/disable 退化为 Marker、SetParams就地更新参数支撑图更新。Graph只存顶点集vertices_边分散在节点里提供根/叶查询、拓扑排序topoOrder_、增删节点、克隆图层 节点层协作借clonedNodes_重建映射以及一个图内存池。下一章我们看辅助资源管理GraphKernelArgManagerkernel 参数池、GraphSignalManagerHW 信号池、UserObject引用计数生命周期以及贯穿整个子系统的静态注册表 有效性检查模式。