您是否曾困惑于哪部分CPU代码触发了特定的GPU内核执行?传统的CPU分析器可以展示主机端的调用堆栈,但当工作移交到GPU后,它们便失去了跟踪能力。反之,GPU分析器能详细记录设备上的活动,却往往无法将这些活动与发起它们的CPU函数联系起来。这造成了一个关键盲区:难以定位究竟是哪行代码导致了缓慢的GPU内核运行。
本教程将指导您构建一个解决此问题的分析器。您将结合eBPF与NVIDIA CUPTI的强大功能,创建一个统一的CPU到GPU火焰图。该工具能在cudaLaunchKernel()被调用时捕获CPU堆栈跟踪,并将其与对应GPU内核的执行数据智能关联,最终生成一个强大的可视化视图,精确揭示哪些主机代码路径触发了哪些GPU内核,从而帮助您在不重新编译应用程序的情况下定位性能瓶颈。这一切的核心在于利用CUPTI提供的关联ID,它充当了连接CPU端API调用与GPU端内核执行的桥梁。
实战示例:分析Qwen3大语言模型推理
为了展示该分析器在实际场景中的应用,让我们以Qwen3 0.6B大语言模型的推理过程为例。下图生成的火焰图可视化了整个操作,将CPU调用堆栈与它们启动的GPU内核融合在了一起。从图中可以立即看出,matmul_kernel(矩阵乘法)是耗时最长的部分,占据了总GPU执行时间的95%。

火焰图的核心洞察:
该可视化清晰地展示了GPU时间的分配情况:
- matmul_kernel:3.1秒(占GPU时间的95%)。这表明矩阵乘法是当前最主要的性能瓶颈。
- multi_head_attention_kernel:105毫秒(3.2%)。注意力机制贡献了少量开销。
- rmsnorm_kernel:44毫秒(1.3%)。归一化是相对轻量的操作。
- 端到端可见性:火焰图展现了从CPU上的
main函数一直到设备上执行的特定[GPU_Kernel]的完整调用链。
核心架构:eBPF与CUPTI注入的协同
我们是如何创建这个统一视图的?整个过程依赖于两项技术的协同:用于CPU端的eBPF和用于GPU端的CUPTI。
- 使用CUPTI注入进行GPU追踪:我们首先创建一个小型自定义CUPTI库。通过设置
CUDA_INJECTION64_PATH环境变量,我们指示CUDA运行时将该库与目标应用程序一同加载。加载后,该库使用CUPTI API记录所有GPU活动(如内核启动、内存拷贝),关键是其会捕获每个事件的时间戳和特殊的关联ID。
- 使用eBPF进行CPU分析:同时,我们使用一个eBPF “uprobe”从外部监控应用程序。该探针附着在CUDA运行时库的
cudaLaunchKernel()函数上。每当应用程序调用此函数启动内核时,我们的eBPF程序便会触发,捕获那一刻的完整CPU调用堆栈。
- 关联数据:应用程序运行结束后,我们获得两组数据:来自CUPTI的GPU事件跟踪和来自eBPF的CPU堆栈跟踪集合。一个最终的合并脚本利用CUPTI的关联ID,将特定的
cudaLaunchKernel API调用链接到GPU上实际执行的内核,并找到eBPF捕获的对应CPU堆栈跟踪(通常通过匹配时间戳),最后将GPU内核名称附加到该堆栈中。
输出结果是一个“折叠的”堆栈文件,可直接用于生成火焰图,其中每一行都代表一条完整的CPU到GPU调用链。
您可以在以下地址找到本教程的完整源代码:GitHub仓库
挑战:为何关联CPU与GPU如此困难?
理解为何需要特殊工具,关键在于把握GPU性能分析的根本挑战。运行CUDA应用程序时,您实际上在处理两个并行且独立的世界:CPU和GPU。
- CPU端:应用程序代码调用CUDA运行时库函数(如
cudaLaunchKernel, cudaMemcpy)。这些调用并不直接执行工作,而是将命令打包发送给GPU驱动。
- GPU端:硬件接收并执行这些命令,包括启动成千上万个线程的内核、移动数据及执行计算。
通常,性能瓶颈恰恰出现在这两个世界的交接处。传统分析器在此处无能为力。CPU分析器(如perf)能显示程序在cudaLaunchKernel中花费了大量时间,但无法告知是哪个内核被启动,或其实际在GPU上的运行时长。而GPU分析器(如NVIDIA Nsight)能提供内核执行的详细指标,却无法追溯到发起它的具体CPU代码行。
这种脱节正是我们要解决的问题。幸运的是,NVIDIA CUDA运行时提供了我们所需的关键:关联ID。每次进行cudaLaunchKernel等API调用时,运行时都会分配一个唯一ID,并随工作传递给GPU。内核执行时也会携带相同的ID。通过在两端捕获此ID,我们就能明确地将CPU调用堆栈与GPU内核执行关联起来。CUPTI在此至关重要,它让我们能够访问这些活动记录。
分析器架构详解:三大支柱
我们的分析器建立在三部分组成的架构上:eBPF分析器、CUPTI注入库和追踪合并器。了解如何利用eBPF进行高效的CPU侧监控是理解现代可观测性工具的关键,更多关于Linux系统和网络层面的深度监控技术可以在云栈社区的网络/系统板块找到相关实践。
- eBPF分析器(CPU端监控):该组件使用eBPF
uprobe附着到CUDA运行时库内的cudaLaunchKernel函数。每当任何进程调用此函数,eBPF程序便会触发,以纳秒级精度捕获完整的CPU调用堆栈,记录发起GPU工作的确切代码路径。
- CUPTI注入库(GPU端追踪):我们编译一个使用CUPTI API的小型共享库。通过设置
CUDA_INJECTION64_PATH环境变量,CUDA运行时会自动将其加载到目标应用程序进程中。该库激活CUPTI的活动追踪功能,记录内核执行和运行时API调用的详细信息,包括时间戳和关键的关联ID。
- 追踪合并器(关联数据):分析结束后,脚本解析来自eBPF的CPU追踪和来自CUPTI的GPU追踪。它通过两步关联过程将CPU堆栈与对应的GPU内核执行进行匹配:首先查找时间戳相近的事件,然后使用关联ID进行确认。匹配成功后,将GPU内核名称附加到CPU堆栈中,生成统一的“折叠堆栈”文件,格式示例如下:
cpu_func1;cpu_func2;cudaLaunchKernel;[GPU_Kernel]kernel_name duration_us。
持续时间加权的重要性
生成最终数据时,我们采用持续时间加权而非简单计数。一个cudaLaunchKernel调用可能启动运行2微秒的内核,也可能启动运行200毫秒的内核。若仅计数,火焰图会错误显示二者同等重要。
通过加权,我们为匹配的堆栈增加GPU内核的实际执行持续时间(微秒)。这使得火焰图中条形的宽度与在GPU上花费的实际时间成正比,从而准确揭示真正的性能热点。
实战步骤:编译与运行分析器
步骤1:构建CUPTI注入库
进入cupti_trace目录并编译:
cd cupti_trace
make
确认生成 libcupti_trace_injection.so。
步骤2:构建Rust eBPF分析器
进入profiler目录并使用Cargo编译:
cd profiler
cargo build --release
生成的可执行文件位于 target/release/profile。
步骤3:构建示例应用程序
先构建简单的模拟LLM应用程序进行测试:
cd mock-test
make
然后构建真实的Qwen3 LLM推理应用(需先下载模型权重):
cd qwen3.cu
make download-model
make runcu
使用ldd runcu | grep cudart确认其动态链接到CUDA运行时库。
步骤4:运行完整分析
使用gpuperf.py脚本协调整个分析过程。以下命令分析Qwen3模型推理:
sudo timeout -s 2 10 python3 gpuperf.py \
-c qwen3_gpu.json \
-p qwen3_cpu.txt \
-m qwen3_merged.folded \
bash -c 'cd qwen3.cu && ./runcu Qwen3-0.6B-FP32.gguf -q "Explain eBPF" -r 1'
此命令将运行分析10秒,并生成CPU、GPU及合并后的追踪文件。
步骤5:生成火焰图
使用提供的脚本从合并文件生成SVG火焰图:
./combined_flamegraph.pl qwen3_merged.folded > qwen3_flamegraph.svg
在浏览器中打开qwen3_flamegraph.svg即可交互式探索性能热点。
深入数据:检查原始追踪文件
分析器生成三个核心文件,从不同维度提供性能洞察:
- CPU端数据 (
qwen3_cpu.txt):eBPF分析器的原始输出,采用扩展折叠格式,包含每次cudaLaunchKernel调用的纳秒级时间戳和完整CPU调用堆栈。
- GPU端数据 (
qwen3_gpu.json):CUPTI库记录的详细GPU活动,格式为Chrome Trace JSON,可加载到chrome://tracing或Perfetto中进行时间线分析,查看内核并发、内存拷贝等。
- 合并数据 (
qwen3_merged.folded):最终的关联输出,包含完整的CPU到GPU调用链及其加权持续时间,是生成火焰图的直接输入。
局限性与未来发展
当前分析器能够揭示哪个CPU代码启动了哪个GPU内核及其运行时长,但无法解释内核内部为何缓慢(如受内存限制还是计算限制)。如需内核内部分析,需借助NVIDIA Nsight Compute等专业工具进行硬件级剖析。
未来的方向包括构建更统一、系统范围的分析器,例如eunomia-bpf/xpu-perf项目所探索的:
- 深入内核内部:集成硬件性能计数器采样,分析指令级停顿原因(如内存延迟)。
- 全面系统视图:合并On-CPU、Off-CPU(等待时间)和GPU跟踪,形成单一系统火焰图。
- 扩展至生产负载:支持多GPU、多CUDA流的复杂工作负载分析。
总结
通过本教程,您构建了一个强大的端到端分析解决方案,成功桥接了CPU与GPU之间的性能分析鸿沟。该方案结合了eBPF的灵活低开销CPU监控与CUPTI的详尽GPU活动追踪,通过关联ID实现了精准的性能归因。最终的统一火焰图为优化GPU加速的应用程序(如AI推理、科学计算)提供了直观且深入的性能洞察。这种基于eBPF和CUPTI注入的方法无需修改目标应用,是进行运维/DevOps和深度性能调优的有效实践。所有工具模块化设计,既可单独使用,也可组合发挥最大效力。