docs(perf): profile CUDA graph coverage and PLE costs on Spark

This commit is contained in:
2026-09-18 01:22:13 +08:00
parent f1a8964072
commit d7e1e745c3
23 changed files with 8384 additions and 1 deletions
+127
View File
@@ -0,0 +1,127 @@
# CUDA Graph 性能剖析:图已生效,主要收益受 GPU 工作占比限制
2026-09-18Asia/Shanghai。方案与脚本见 [profile128k](../experiments/profile128k/plan.md)。
CUDA Graph 确实被重放,并非参数失效。本轮短回答生成速度中位数由 **29.954 → 31.316 tokens/s(约 +4.5%**
与此前约 +4.4% 的复测一致。32K 首次输入首 token 则为 **15.416 → 15.376 秒**,基本不变。
原因有两层:当前小尺寸图没有覆盖实际的长输入预填充分块;同时,GPU 时间线上本来就没有很多空隙可消除。
不能据此认定 SSD、PLE mmap 或 CPU 调度是主要瓶颈,也不能把 CUDA Graph 的小收益解释为 CUDA/GPU 没有发挥作用。
**eager 和 graph 两组都在使用 GPU 执行 CUDA 内核;本次比较的是是否通过 CUDA Graph 提交这些工作。**
## 正常计时与正确性
- 固定 nightly `0bfc7a15`、相同模型 revision、BF16 KV、MTP=2、前缀缓存、两处 Mamba 修正、GPU memory=0.80。
- 实验上下文上限 131072max_num_seqs=4,但测试串行;max_num_batched_tokens=2048。
- 小尺寸图为 breakable PIECEWISEcapture sizes `[1,2,4,8,12]`
- 短回答预算 512,长检索预算 2048,思考与正文共用;temperature=0、seed=42、reasoning_effort=low。
- 两组各 19 次请求:1 次算术预热、三轮各 5 次计时请求、3 次 trace 请求。38 次均通过各自判据。
长检索要求精确答案和正常 stop,短说明仅检查非空及正常 stop,不代表进行了语言质量评估。
- 19 个对应请求的输入 SHA256 全部一致;服务返回 prompt_tokens 与预检逐条一致。
- 每轮首次输入使用不同前缀,避免跨轮缓存命中;重复输入紧随首次输入。
- 下表为三轮中位数,不含预热及 trace 请求。短回答每次均输出 214 tokens;长检索输出约 6274 tokens,有少量差异。
| 指标 | eager | 小尺寸 graph |
|---|---:|---:|
| 短回答解码速度(近似 tokens/s | 29.954 | 31.316 |
| 短回答首 token(秒,含思考) | 0.2627 | 0.2624 |
| 短回答完整耗时(秒) | 7.374 | 7.064 |
| 8K 首次首 token(秒) | 3.788 | 3.770 |
| 8K 复用首 token(秒) | 0.880 | 0.874 |
| 32K 首次首 token(秒) | 15.416 | 15.376 |
| 32K 复用首 token(秒) | 1.368 | 1.327 |
原始请求记录和自动汇总见 [results/profile128k](results/profile128k/benchmark-summary.json)。
计时阶段没有开启 capture,但 Nsight launcher 和 NVTX 包装仍存在;不是完全卸载 profiler 的独立测速。
执行顺序 eager → graph,未随机化、未清 OS 页缓存,小样本不代表统计显著性或长期稳定性。
## 时间线证据
Nsight Systems 2025.3.2CUDA profiler API 控制三个短采样窗口,node 级 graph trace。
NVTX 临时标记 PLE 同步、CPU 去重、CPU gather、lookup,以及图调度描述符。
计时阶段和 trace 阶段分开;下表不用于跨模式的端到端性能排名。
“GPU 活动占比”是 kernel、memcpy、memset 时间区间并集,除以首次至末次 GPU 活动的跨度。
它不是 SM 利用率、内存带宽利用率,也不包含完整 HTTP 请求前后的所有工作。
| 采样请求 | eager 活动占比 | graph 活动占比 | graph 中来自图节点的 kernel 数量占比 |
|---|---:|---:|---:|
| 短回答 | 93.01% | 95.22% | 82.43% |
| 32K 首次输入及回答 | 97.38% | 97.50% | 44.25% |
| 32K 复用及回答 | 95.22% | 95.29% | 77.15% |
### 图生效,但覆盖范围有限且分成多段
- eager 的三个窗口都没有 graph-node kernel。
- graph 短回答窗口记录 **4,664 次 `cudaGraphLaunch`160,160 / 194,303 个 kernel 来自图节点**
- 调度记录显示解码 `actual=3` 被 padding 到 4,模式为 PIECEWISEMTP 路径也存在图重放。
同时保留部分 NONE 路径,不能说整次请求被捕获成一张图。
- 该窗口有 88 次 `actual=3` 的图调度,累计 graph launch 约为其 53 倍。
这是目标模型与辅助路径合计的实测比值,说明提交仍分成多段;不是每生成一个 token 就固定调用 53 次。
- 32K 首次请求中的 **1600 和 742-token 预填充调度均为 NONE**;后续解码才命中图。
- 日志确认 attention/KV block size 为 1600。虽然批处理上限是 2048,实际主要分块是 1600,
因此不能仅凭配置里的 2048 推断捕获或预填充形状。
### PLE 的“等待时间”不是磁盘读表时间
32K 首次输入 trace
| 时间项 | eager | graph |
|---|---:|---:|
| 首次至末次 GPU 活动跨度 | 17.458 s | 17.614 s |
| GPU 活动区间并集 | 17.000 s | 17.173 s |
| PLE CPU gather | 0.275 s | 0.292 s |
| PLE CPU 去重(标记中的 np.unique | 0.019 s | 0.021 s |
| PLE 等待 GPU | 6.389 s | 7.292 s |
| 其中与 GPU 活动重叠 | 6.341 s | 7.233 s |
CPU wait 与 GPU 工作几乎完全重叠,不是额外叠加在 GPU 计算之后的同等开销。
不能把所有 API/NVTX/GPU 时长相加,也不能依据同步函数耗时最长就优先优化同步本身。
图版本 wait 更长不证明 PLE 退化:提交时序和输出长度都不同。
本轮 gather 包括页缓存或实际存储读取,未测物理 NVMe I/O;没有证据说明每次都从 SSD 读取,
也没有证据支持将整张 47.7 GiB 表强制驻留能带来显著收益。
短回答中 CPU gather 约 0.20 秒,相比约 7 秒 GPU 活动跨度亦非主导项。
## 决策与后续方向
本轮**不扩大图捕获尺寸**。长输入 GPU 活动已接近连续,消除提交间隙的空间有限;
扩大图还可能增加 padding、捕获内存和启动成本。当前证据不足以让它成为稳定基线的优先改动。
这不等于证明大图一定没有收益;该候选未实测,不报告虚构加速或稳定性结果。
按方案,只有新候选通过才追加 128K 验证,因此本轮未重跑 128K,也未再次触碰 260K。
稳定基线建议仍是:**eager + 前缀缓存 + MTP=2,实际使用限制在 128K 以内**。
愿意维护实验补丁时,小尺寸图可作为约 4–5% 短回答收益的可选项;目前证据不足以要求默认切换。
下一个有依据的性能工作应聚焦耗时 GPU 内核及其 GB10 后端:32K trace 的主要内核包含
CUTLASS 分组量化 GEMM、稀疏注意力和归一化组合;短回答还有显著 BF16 GEMM 时间。
要判定算力受限还是带宽受限,需要另做针对性内核分析,不能用本轮活动占比替代结论。
## 资源与实验修正
实验采样最低 MemAvailable **15.748 GiB**,主机 `oom_kill` 从 25 到 25,没有新增 OOM。
监控每秒采样,低于 1 GiB 立即停止,持续低于 2 GiB 或主机 OOM 增加则停止。
监控覆盖两组实验,恢复阶段另做最终系统检查;没有为了验证保护阈值主动制造内存压力。
首次诊断启动因临时包装函数缺少 PyTorch schema 所需类型注解而失败,尚未加载完成或执行请求。
自动恢复触发后,修复注解并验证 `torch.library.infer_schema`,随后才运行完整两组。
该失败属于诊断脚本问题,不是模型/vLLM 的新 bug;失败日志保留在 Spark 的 attempt1 目录。
原始 trace 和 SQLite 只保留在 Spark `optimization-results/profile128k/{eager,graph}`,父目录权限 0700。
它们可能包含进程参数,不提交 Git。仓库的 [证据清单](results/profile128k/evidence-manifest.json)
记录文件大小、SHA256、Nsight 版本、配置要点;仅提交 CUDA/NVTX 摘要、请求结果及资源记录。
## 恢复状态
2026-09-18 01:21Asia/Shanghai)已恢复原 eager 镜像,容器 healthy、重启策略 `unless-stopped`
算术、中文、工具调用 smoke test 全部通过;原 Compose 与测试前逐字一致,临时 profiler 参数与挂载均已移除。
可用内存约 18.85 GiB,主机 OOM 计数仍为 25,系统及用户 failed units 均为 0。
恢复证据见 [recovery.json](results/profile128k/recovery.json) 和 [smoke log](results/profile128k/recovery-smoke.log)。
默认配置仍保留原来的 262144 上限,本轮未修改服务容量;该配置值不代表已验证的稳定容量。
实际使用仍建议限制在 128K 以内,接近 260K 的既有 OOM 结论不变。
## 参考
- [vLLM profiling 文档](https://docs.vllm.ai/en/latest/contributing/profiling/)
- [NVIDIA Nsight Systems User Guide](https://docs.nvidia.com/nsight-systems/UserGuide/)
- [NVIDIA CUDA Graph 性能问题说明](https://docs.nvidia.com/dl-cuda-graph/latest/troubleshooting/performance-issues/)