MUSA Graphs
图是由一系列操作(如内核启动、数据传输等)通过依赖关系连接形成的工作流程,其定义阶段与执行阶段相互分离。
概述
什么是 MUSA Graphs
MUSA Graphs 是 MUSA 的一种工作提交模式。图是由一系列操作(如内核启动、数据传输等)通过依赖关系连接形成的工作流程,其定义阶段与执行阶段相互分离。使用该机制,图只需定义一次,之后可重复启动执行。
将图的定义与执行解耦,可实现以下优化:
- 降低 CPU 启动开销:相比于流(stream)方式,大部分设置工作已提前完成
- 支持全局优化:将完整工作流程一次性提交给 MUSA,使一些在流的逐段提交模式下难以实现的优化成为可能
优化原理
当将内核放入流中时,主机驱动程序会执行一系列操作,为 GPU 上的内核执行做准备。这些内核设置和启动所需的操作,构成了每次内核启动的固定开销。对于执行时间较短的内核而言,这部分开销可能占总执行时间的很大比例。
通过创建包含多次重复执行的工作流程的 MUSA Graph,这些开销仅在图实例化时产生一次,之后图便可反复启动,且每次启动的开销极低。
使用场景
| 场景 | 传统流方式 | 使用 Graph |
|---|---|---|
| 神经网络推理 | 多次内核启动,CPU 开销大 | 单次图执行,降低启动开销 |
| 图像 处理管道 | 串行操作,依赖管理复杂 | 流水线图,自动依赖管理 |
| 迭代算法 | 迭代间同步开销大 | 图重复启动,开销极低 |
| 多租户环境 | 资源竞争,性能不稳定 | 固定图结构,性能可预测 |
| 低延迟服务 | CPU 提交延迟占比高 | 一次实例化,多次低开销启动 |
核心优势
| 特性 | 传统模式 | Graph 模式 |
|---|---|---|
| CPU 开销 | 每次启动都有开销 | 捕获后重复执行,开销极低 |
| 执行确定性 | 依赖驱动调度 | 图结构固定,执行顺序确定 |
| 多流协调 | 需手动同步 | 图内依赖自动管理 |
| 全局优化 | 局部优化 | 完整工作流一次提交,支持全局优化 |
与流式提交的对比
| 维度 | 流式提交 | 图提交 |
|---|---|---|
| CPU 开销 | 每次启动都需设置 | 一次设置,多次启动 |
| 优化机会 | 局部优化 | 全局优化 |
| 灵活性 | 高,动态工作负载 | 中,适合重复工作负载 |
| 适用场景 | 动态、不可预测的工作 | 固定 、重复的工作流程 |
快速上手示例
使用流捕获创建一个简单的图:
#include <musa_runtime.h>
__global__ void myKernel(float* data, int n) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < n) {
data[idx] = data[idx] * 2.0f;
}
}
int main() {
float *d_data;
musaMalloc(&d_data, 1024 * sizeof(float));
musaStream_t stream;
musaStreamCreate(&stream);
// 1. 开始捕获
musaStreamBeginCapture(stream, musaStreamCaptureModeGlobal);
// 2. 提交工作(将被捕获到图中)
myKernel<<<4, 256, 0, stream>>>(d_data, 1024);
// 3. 结束捕获,获取图
musaGraph_t graph;
musaStreamEndCapture(stream, &graph);
// 4. 实例化图
musaGraphExec_t graphExec;
musaGraphInstantiate(&graphExec, graph, 0);
// 5. 执行图(可重复执行)
musaGraphLaunch(graphExec, stream);
musaStreamSynchronize(stream);
// 6. 清理
musaGraphExecDestroy(graphExec);
musaGraphDestroy(graph);
musaStreamDestroy(stream);
musaFree(d_data);
return 0;
}
图结构
节点与边
在 Graph 中,每个操作构成一个节点,操作之间的依赖关系则形成边。依赖关系限定了操作的执行顺序。
当某个节点依赖的所有前置节点执行完毕后,该节点对应的操作即可被调度执行。具体调度工作由 MUSA 系统自动管理。
节点类型
MUSA Graph 支持以下节点类型:
| 节点类型 | 常量 | 描述 |
|---|---|---|
| Kernel | musaGraphNodeTypeKernel | GPU 内核执行 |
| Memcpy | musaGraphNodeTypeMemcpy | 内存拷贝 |
| Memset | musaGraphNodeTypeMemset | 内存填充 |
| Host | musaGraphNodeTypeHost | CPU 函数调用 |
| Empty | musaGraphNodeTypeEmpty | 空节点(同步点) |
| Event Record | musaGraphNodeTypeEventRecord | 记录 MUSA 事件 |
| Event Wait | musaGraphNodeTypeEventWait | 等待 MUSA 事件 |
| External Semaphore Signal | musaGraphNodeTypeExternalSemaphoreSignal | 信号量通知 |
| External Semaphore Wait | musaGraphNodeTypeExternalSemaphoreWait | 等待外部信号量 |
| Child Graph | musaGraphNodeTypeChildGraph | 子图节点 |
| MemAlloc | musaGraphNodeTypeMemAlloc | 内存分配 |
| MemFree | musaGraphNodeTypeMemFree | 内存释放 |
| Conditional | musaGraphNodeTypeConditional | 条件节点 |
对应 API:
- Kernel 节点:
musaGraphAddKernelNode() - Memcpy 节点:
musaGraphAddMemcpyNode() - Memset 节点:
musaGraphAddMemsetNode() - Host 节点:
musaGraphAddHostNode() - Empty 节点:
musaGraphAddEmptyNode() - Event Record 节点:
musaGraphAddEventRecordNode() - Event Wait 节点:
musaGraphAddEventWaitNode() - External Semaphore Signal 节点:
musaGraphAddExternalSemaphoresSignalNode() - External Semaphore Wait 节点:
musaGraphAddExternalSemaphoresWaitNode() - Child Graph 节点:
musaGraphAddChildGraphNode() - MemAlloc 节点:
musaGraphAddMemAllocNode() - MemFree 节点:
musaGraphAddMemFreeNode() - Conditional 节点:
musaGraphAddNode()
依赖关系
- 上游节点 → 下游节点:依赖关系决定执行顺序
- 根节点:无依赖的节点,可立即执行
- 叶节点:无后续依赖的节点,图执行的结束点
边数据(Edge Data)
MUSA 5.1.0 版本目前仅支持默认边。
MUSA 5.1.0 版本引入了对 MUSA Graphs 边数据的支持。边数据用于调整一条边所定义的依赖关系,包含三个部分:
| 组成部分 | 说明 | 默认值 |
|---|---|---|
| 输出端口 | 决定关联的边何时触发 | 0(等待整个任务完成) |
| 输入端口 | 指定节点中依赖该边的具体部分 | 0(阻塞整个任务) |
| 类型 | 用于调整端点间的关系 | 0(完整依赖) |
在所有情况下,零初始化的边数据代表默认行为。
图的工作流程
使用 MUSA Graph 进行任务提交分为四个阶段:
| 阶段 | API | 说明 |
|---|---|---|
| 阶段 1:创建/定义 | musaGraphCreate() | 描述图中的操作及其依赖 |
| 阶段 2:实例化 | musaGraphInstantiate() | 对图模板进行快照、验证和初始化 |
| 阶段 3:执行 | musaGraphLaunch() | 将可执行图启动到流中 |
| 阶段 4:销毁 | musaGraphExecDestroy(), musaGraphDestroy() | 释放图资源 |
阶段 1:创建
musaGraph_t graph;
musaGraphCreate(&graph, 0);
// 添加节点和依赖
musaGraphNode_t kernelNode;
musaKernelNodeParams kernelParams = {0};
kernelParams.func = (void*)myKernel;
kernelParams.gridDim = dim3(256, 1, 1);
kernelParams.blockDim = dim3(256, 1, 1);
kernelParams.sharedMemBytes = 0;
kernelParams.kernelParams = args;
musaGraphAddKernelNode(&kernelNode, graph, NULL, 0, &kernelParams);
阶段 2:实例化
musaGraphExec_t graphExec;
musaGraphInstantiate(&graphExec, graph, 0);
阶段 3:执行
musaStream_t stream;
musaStreamCreate(&stream);
musaGraphLaunch(graphExec, stream);
musaStreamSynchronize(stream);
阶段 4:销毁
musaGraphExecDestroy(graphExec);
musaGraphDestroy(graph);
musaStreamDestroy(stream);
阶段 1:图创建
图可通过两种机制创建:使用显式图 API 或 通过流捕获。
方法 1:显式图 API
步骤 1:创建空图
/**
* @brief 创建一个空的 MUSA 图
* @param graph[out] 返回的图句柄
* @param flags[in] 创建标志(保留供未来使用,传 0)
* @return musaSuccess, musaErrorInvalidValue
*/
musaError_t musaGraphCreate(musaGraph_t* graph, unsigned int flags);
步骤 2:添加内核节点
/**
* @brief 向图中添加一个内核节点
* @param pGraphNode[out] 返回的节点句柄
* @param graph[in] 目标图
* @param pDependencies[in] 依赖节点数组
* @param numDependencies[in] 依赖节点数量
* @param pNodeParams[in] 内核节点参数
*/
musaError_t musaGraphAddKernelNode(
musaGraphNode_t *pGraphNode,
musaGraph_t graph,
const musaGraphNode_t *pDependencies,
size_t numDependencies,
const struct musaKernelNodeParams *pNodeParams
);
内核节点参数结构:
struct musaKernelNodeParams {
void* func; // 内核函数指针
dim3 gridDim; // 网格维度
dim3 blockDim; // 块维度
unsigned int sharedMemBytes; // 共享内存大小
void **kernelParams; // 内核参数数组
void **extra; // 额外参数
};
示例:
__global__ void vectorAdd(float *a, float *b, float *c, int n) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < n) c[i] = a[i] + b[i];
}
// 创建图
musaGraph_t graph;
musaGraphCreate(&graph, 0);
// 准备内核参数
float *d_a, *d_b, *d_c;
int n = 1024;
void *args[] = {&d_a, &d_b, &d_c, &n};
musaKernelNodeParams kernelParams = {0};
kernelParams.func = (void*)vectorAdd;
kernelParams.gridDim = dim3((n + 255) / 256, 1, 1);
kernelParams.blockDim = dim3(256, 1, 1);
kernelParams.sharedMemBytes = 0;
kernelParams.kernelParams = args;
// 添加内核节点
musaGraphNode_t kernelNode;
musaGraphAddKernelNode(&kernelNode, graph, NULL, 0, &kernelParams);
步骤 3:添加内存拷贝节点
musaError_t musaGraphAddMemcpyNode(
musaGraphNode_t *pGraphNode,
musaGraph_t graph,
const musaGraphNode_t *pDependencies,
size_t numDependencies,
const struct musaMemcpy3DParms *pCopyParams
);
内存拷贝参数结构:
struct musaMemcpy3DParms {
musaPitchedPtr srcPtr;
musaPos srcPos;
musaPitchedPtr dstPtr;
musaPos dstPos;
musaExtent extent;
enum musaMemcpyKind kind;
};
示例:
// Host 到 Device 拷贝
musaMemcpy3DParms copyParams = {0};
copyParams.srcPtr = make_musaPitchedPtr(h_src, width * sizeof(float), width, 1);
copyParams.dstPtr = make_musaPitchedPtr(d_dst, width * sizeof(float), width, 1);
copyParams.extent = make_musaExtent(width * sizeof(float), 1, 1);
copyParams.kind = musaMemcpyHostToDevice;
musaGraphNode_t memcpyNode;
musaGraphAddMemcpyNode(&memcpyNode, graph, NULL, 0, ©Params);
步骤 4:添加内存填充节点
musaError_t musaGraphAddMemsetNode(
musaGraphNode_t *pGraphNode,
musaGraph_t graph,
const musaGraphNode_t *pDependencies,
size_t numDependencies,
const struct musaMemsetParams *pMemsetParams
);
内存填充参数结构:
/**
* @brief 内存填充节点参数结构
* @note 元素大小必须为 1、2 或 4 字节
*/
struct musaMemsetParams {
void *dst; // 目标内存地址
unsigned int value; // 填充值
size_t pitch; // 跨距(为 0 表示 1D)
size_t elementSize; // 元素大小(1、2 或 4 字节)
size_t width; // 宽度(元素个数)
size_t height; // 高度
};
步骤 5:添加空节点
musaError_t musaGraphAddEmptyNode(
musaGraphNode_t *pGraphNode,
musaGraph_t graph,
const musaGraphNode_t *pDependencies,
size_t numDependencies
);
空节点不执行任何操作,常用于:
- 作为同步点
- 作为依赖关系的汇合点
步骤 6:添加事件节点
// 添加事件记录节点
musaError_t musaGraphAddEventRecordNode(
musaGraphNode_t *pGraphNode,
musaGraph_t graph,
const musaGraphNode_t *pDependencies,
size_t numDependencies,
musaEvent_t event
);
// 添加事件等待节点
musaError_t musaGraphAddEventWaitNode(
musaGraphNode_t *pGraphNode,
musaGraph_t graph,
const musaGraphNode_t *pDependencies,
size_t numDependencies,
musaEvent_t event
);
步骤 7:添加外部信号量节点
版本要求: __MUSART_API_VERSION >= 10200
/**
* @brief 向图中添加外部信号量通知节点
* @param pGraphNode[out] 返回的节点句柄
* @param graph[in] 目标图
* @param pDependencies[in] 依赖节点数组
* @param numDependencies[in] 依赖数量
* @param nodeParams[in] 信号量通知参数
* @note 此 API 需要 MUSA Runtime API 版本 >= 10200
*/
musaError_t musaGraphAddExternalSemaphoresSignalNode(
musaGraphNode_t *pGraphNode,
musaGraph_t graph,
const musaGraphNode_t *pDependencies,
size_t numDependencies,
const struct musaExternalSemaphoreSignalNodeParams *nodeParams
);
/**
* @brief 向图中添加外部信号量等待节点
* @param pGraphNode[out] 返回的节点句柄
* @param graph[in] 目标图
* @param pDependencies[in] 依赖节点数组
* @param numDependencies[in] 依赖数量
* @param nodeParams[in] 信号量等待参数
* @note 此 API 需要 MUSA Runtime API 版本 >= 10200
*/
musaError_t musaGraphAddExternalSemaphoresWaitNode(
musaGraphNode_t *pGraphNode,
musaGraph_t graph,
const musaGraphNode_t *pDependencies,
size_t numDependencies,
const struct musaExternalSemaphoreWaitNodeParams *nodeParams
);
步骤 8:添加依赖关系
musaError_t musaGraphAddDependencies(
musaGraph_t graph,
const musaGraphNode_t *from,
const musaGraphNode_t *to,
size_t numDependencies
);
方法 2:流捕获
流捕获提供了一种通过现有流 API 创建图的机制。任何向流提交工作的代码段,都可通过捕获 API 生成图。