推理延迟诊断指南,利用 rocprof 追踪 GPU 内核执行

从“感觉卡”到“数据实锤”:用 rocprof 揪出推理延迟真凶

做推理服务优化最头疼的不是模型跑不起来,而是那种“明明资源没满,响应却慢得离谱”的玄学问题。很多时候,我们习惯性地调整 batch size 或者换量化精度,但这往往是在盲猜。在 AMD Instinct GPU 上,想要真正解决高延迟,必须把黑盒打开,看清 GPU 内核到底在忙什么。今天不聊虚的理论,直接分享一套我常用的实战流程:利用 ROCm 自带的rocprof工具捕获执行轨迹,精准定位耗时算子和数据拷贝瓶颈。

为什么你的延迟降不下来?

在排查延迟问题时,最常见的误区是只盯着 GPU 利用率看。有时候 SM 利用率很高,但请求依然排队,这通常是因为内核启动开销大,或者存在大量的 Host-to-Device (H2D) 数据拷贝阻塞了计算流水线。特别是在多卡张量并行场景下,卡间通信如果走了低速链路,延迟会成倍增加。

要解决这些问题,不能靠猜,得靠 trace。rocprof就是 ROCm 生态里的“显微镜”,它能记录下每一个 kernel 的启动时间、执行时长以及内存拷贝细节。拿到这些数据,你才能知道是模型结构里的某个算子在拖后腿,还是数据预处理环节成了瓶颈。

手把手捕获 GPU 内核执行轨迹

首先,确保你的环境已经安装了 ROCm 工具链(通常包含在rocm-dev包中)。假设你已经有一个基于 PyTorch 或 vLLM 运行的推理服务进程,我们需要在它运行时注入探针。

最简单的方式是通过环境变量启动你的推理脚本。比如,如果你正在运行一个测试脚本infer.py,可以使用以下命令:

ROCP_TRACING=1ROCP_OUTPUT_DIR=./trace_data python infer.py

这里ROCP_TRACING=1开启追踪,ROCP_OUTPUT_DIR指定输出目录。运行结束后,你会在./trace_data目录下看到生成的.csv.rof文件。这些文件记录了从程序启动到结束的所有 GPU 活动。

如果你希望更精细地控制,比如只追踪特定时间段或特定内核,可以使用rocprof命令行工具直接包裹应用:

rocprof--output./trace_data/result.rof--timestampon python infer.py

生成的.rof文件可以用 Chrome 的about:tracing加载(需转换为 JSON 格式),或者直接解析 CSV 进行定量分析。对于自动化脚本来说,CSV 格式更友好。

编写脚本定位耗时算子与 H2D 瓶颈

拿到原始的 trace 数据后,面对成千上万行记录,人眼很难看出门道。我写了一个简单的 Python 脚本来辅助分析,主要做两件事:统计耗时最长的 Top 10 内核,以及计算 Host-to-Device 拷贝占总时间的比例。

importcsvimportosdefanalyze_trace(csv_path):kernel_times=[]h2d_copies=[]withopen(csv_path,'r')asf:reader=csv.DictReader(f)forrowinreader:# 筛选有效的内核执行记录ifrow['Name']androw['DurationNs']:duration_us=int(row['DurationNs'])/1000.0kernel_times.append({'name':row['Name'],'duration':duration_us})# 识别 H2D 拷贝 (通常包含 memcpy H2D 或类似标识)if'memcpy'inrow['Name'].lower()and'h2d'inrow['Name'].lower():h2d_copies.append(duration_us)# 按耗时排序kernel_times.sort(key=lambdax:x['duration'],reverse=True)print("=== Top 10 耗时算子 ===")total_kernel_time=sum(k['duration']forkinkernel_times)fori,kinenumerate(kernel_times[:10]):percent=(k['duration']/total_kernel_time*100)iftotal_kernel_time>0else0print(f"{i+1}.{k['name']}:{k['duration']:.2f}us ({percent:.2f}%)")ifh2d_copies:total_h2d=sum(h2d_copies)print(f"\n=== 数据拷贝瓶颈分析 ===")print(f"H2D 拷贝总耗时:{total_h2d:.2f}us")print(f"占内核总耗时比例:{(total_h2d/total_kernel_time*100):.2f}%")iftotal_h2d/total_kernel_time>0.15:print("⚠️ 警告:H2D 拷贝占比过高,建议检查数据加载流水线或启用 pinned memory。")# usage: python analyze_trace.py ./trace_data/kernel_trace.csvif__name__=="__main__":importsysiflen(sys.argv)>1:analyze_trace(sys.argv[1])else:print("请提供 CSV 文件路径")

这段脚本能帮你快速抓住重点。如果发现某个自定义算子(比如特定的 Attention 实现)占据了 30% 以上的时间,那就值得去查查有没有更优化的 HIP 内核版本,或者是否可以通过融合算子来减少启动次数。如果 H2D 拷贝占比超过 15%,说明你的数据预处理可能没跟上 GPU 的速度,或者没有正确使用异步拷贝。

平衡延迟与吞吐量的实战策略

找到瓶颈后,怎么调优?最直接的手段就是调整 batch size。很多开发者为了追求高吞吐,会把 batch size 设得很大,但这会导致首字延迟(TTFT)急剧上升,用户体验变差。

通过rocprof的数据,你可以观察不同 batch size 下内核执行时间的变化曲线。通常情况下,随着 batch size 增加,单个 token 的平均计算时间会下降(因为并行度提高了),但队列等待时间会增加。

我的经验是采用自适应批处理策略。不要固定一个死板的 batch size,而是根据当前的请求队列长度和 GPU 显存剩余量动态调整。例如,当检测到 H2D 拷贝频繁且 GPU 有空闲时,适当增大 batch;一旦发现某个内核执行时间突增(可能是显存带宽饱和),立即减小 batch 并优先处理积压请求。

在 vLLM 等框架中,可以通过调整--max-num-seqs--gpu-memory-utilization参数来模拟这种效果。结合rocprof的实时监控(可以编写脚本定期采样),你能找到一个“甜点”:既保证了吞吐量不至于太低,又让 P99 延迟控制在可接受范围内。

最后,别忘了检查多卡环境下的通信。如果在 trace 中看到大量的rccl相关内核耗时异常,可能需要检查 PCIe 拓扑或 Infinity Fabric 连接状态,确保卡间通信没有绕路。优化是一个迭代过程,每一次调整都用数据说话,才能让推理服务真正快起来。

200小时GPU算力已就位,快来领取:https://marketing.csdn.net/questions/Q2604140858304426315?utm_source=AIpaper