本章目标:吃透 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;
}
这是防御用户传入非法句柄的一道安全网,Graph、GraphExec、UserObject 都用了同样的"静态注册表 + 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 四个关键虚接口:节点的"契约"
Graph 和 GraphExec 在构建与执行时,并不关心某个节点具体是 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 即可。这又是一次"避免重建、就地复用"的空间换时间。
除这四个之外,还有
TopologicalOrder、UpdateEventWaitLists、GetChildGraph、GraphCaptureEnabled、GenerateDOT等虚接口,多数只在特定节点(如子图)才有意义,遇到时再讲。
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; // 克隆出新对象
克隆逻辑分两层协作:
- 图层 :
Graph::clone遍历vertices_,对每个节点调用其node->clone()(2.3 的原型模式),得到节点副本。 - 节点层 :每个节点的
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 本章小结
GraphNode用edges_/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(引用计数生命周期),以及贯穿整个子系统的"静态注册表 + 有效性检查"模式。