周三下午,一个做GPU性能分析的工程师在AMD显卡上跑通了原本为NVIDIA写的追踪脚本。没改几行代码,hipLaunchKernel的调用痕迹就出现在了屏幕上。这件事本该更轰动一点——毕竟CUDA和ROCm打了这么多年,底层工具链居然能通用?
但真相是:eBPF本来就这样工作。它不 care 你用的是哪家驱动。
1. 用户态钩子:符号即一切
eBPF的uprobe机制只关心三件事:符号能不能解析、调用约定BPF认不认识、函数调用频不值得挂钩子。libcudart.so和libhip.so在这三点上完全一致。cudaLaunchKernel怎么抓,hipLaunchKernel就怎么抓。代码结构几乎复制粘贴:时间戳、PID、cgroup ID、函数地址、流句柄,打包进ringbuf,用户态再拿/proc/[pid]/maps做符号还原。
厂商差异被抽象层吃掉了。钩子挂在进程地址空间,不是挂在NVIDIA的logo上。
2. 内核态:本来就无差别
真正干活的调度器、内存回收、块设备、TCP栈,对GPU厂商一视同仁。sched:sched_switch不会因为你换了显卡就改变语义,vmscan也不会突然学会识别ROCm。一个卡住的kernel launch,在主机侧表现出来的上下文切换模式,AMD和NVIDIA画出来是同一张图。
这部分追踪代码完全不用动。迁移成本≈0。
3. ROCm暴露了什么,藏了什么
AMD的HIP runtime API是贴着CUDA Runtime API长的:hipMalloc、hipMemcpy、hipLaunchKernel、hipDeviceSynchronize、hipStreamCreate。这些点uprobe都能打,抓到的证据形状和CUDA侧一样——启动延迟、流等待、同步阻塞。
但ROCm没给CUDA Driver API那层的东西。上下文管理在哪?AMD把它拆成了两份:用户态的ROCT-Thunk-Interface开源了,内核态的KFD(Kernel Fusion Driver)接管了剩下的事。对内核追踪者是好消息——更多东西在kernel里,ftrace/bpf都能看见;对纯uprobe方案是坏消息——libhip这层变薄了,有些调用没经过用户态。
4. 抽象层的硬边界
libhip的uprobe能告诉你"某个kernel被发射了",但发射之后的事它管不着。AMD的设备侧计数器,和NVIDIA的DCGM一样,藏在另一套驱动管理接口后面。想追GPU内部的warp调度、显存带宽、L2命中率?得再开一条采集路径。
这不是ROCm的缺陷,是抽象层的固有代价。runtime API设计出来就是为了让程序员不用关心硬件细节,追踪工具想钻进去,就得额外付费。
5. 实际迁移 checklist
如果你有一套CUDA的eBPF追踪方案,切到ROCm需要检查:
- 符号表:hipLaunchKernel在libhip.so里有没有?有,直接复用。
- 调用约定:dim3结构体布局是否一致?实测一致,但建议sizeof验证。
- 流句柄:hipStream_t和cudaStream_t都是指针, Ringbuf里存(u64) stream通用。
- 符号还原:/proc/[pid]/maps的解析逻辑不用改,ELF格式无差别。
- 缺失项:CUDA Driver API的追踪点需要平移到KFD的tracepoint,这部分得重写。
6. 一个未被充分利用的事实
ROCm的用户态驱动开源,意味着你可以编译带debug info的版本,uprobe能打到比CUDA更细的粒度。NVIDIA的libcuda.so是黑箱,符号剥离干净,runtime API已经是能钩的最上层。AMD这边,ROCT-Thunk-Interface的函数命名直接暴露硬件队列、内存池、信号量的操作,理论上可以追踪到hipLaunchKernel往下三层的调用链。
代价是方案不再跨厂商通用。但如果你是纯ROCm环境,这是白捡的可见性。
最后
GPU追踪工具链的"一次编写,到处运行"是个伪命题,但"一次编写,两处小改"是真实的。eBPF的uprobe抽象做得足够薄,薄到厂商差异被压缩在符号名和库路径两个字符串里。剩下的工作量取决于你想挖多深——runtime层免费,driver层另议,设备层各凭本事。
AMD和NVIDIA打了十年,最后在最底层的追踪接口上握手了。不是它们想通,是eBPF根本没给它们选边站的机会。
热门跟贴