SGLang GPU 剖析实战:用 gputrc2graph 将 nsys 追踪文件转化为内核级耗时分析
2026/9/11 1:45:46 网站建设 项目流程

SGLang GPU 剖析实战:用 gputrc2graph 将 nsys 追踪文件转化为内核级耗时分析

【免费下载链接】sglangSGLang is a high-performance serving framework for large language models and multimodal models.项目地址: https://gitcode.com/GitHub_Trending/sg/sglang

本篇技术指南围绕 SGLang 仓库 examples/profiler/nsys_profile_tools 目录下的gputrc2graph.py展开,讲解如何把 NVIDIA Nsight Systems(nsys)采集的 CUDA GPU 追踪文件(.nsys-rep)转化为内核级别的耗时统计与可视化,从而量化 GPU 与非 GPU 时间、按内核类别(attention、gemm、moe 等)定位性能瓶颈。读者学完后,将能够独立完成 nsys 采集、单模型分析、多模型横向对比,以及为新的引擎/模型扩展内核分类规则。

工具定位:从 nsys 原始追踪到内核级时间账单

gputrc2graph.py是一个面向 SGLang 服务性能分析的后处理工具。它不负责采集数据,而是消费nsys-t cuda追踪模式下生成的.nsys-rep文件,最终产出两类结果:

  • result.html:内核名称按类别归并后的堆叠柱状图(stacked bar chart),直观展示各类别消耗的 GPU 秒数;
  • result.csv:内核名称到类别的映射明细表,供二次分析使用。

从源码看,脚本的核心工作链在 gputrc2graph.py 的gen_sum_file中完成:它调用nsys stats -r cuda_gpu_trace导出内核追踪 CSV,再通过gen_nonoverlapped_sum_from_gputrace计算非重叠(non-overlapped)GPU 耗时——即对按开始时间排序的内核区间做去重叠处理,只累计真正独占 GPU 的时长,避免多个流(stream)上并行内核被重复累加。这一口径是后续所有分析的基础。

环境准备与依赖

运行脚本前需要满足两个前提(对应原文档 Notes 部分):

  1. pandas:任意版本均可,脚本在GPUTrace2Graph.__init__中惰性导入(见 gputrc2graph.py),因此即使系统未预装 pandas,也只需在分析机上pip install pandas即可。
  2. nsys 命令行工具:需要已安装 NVIDIA Nsight Systems CLI,并保证其版本不低于采集追踪时服务器端使用的 nsys 版本,否则无法解析对应的.nsys-rep文件。若nsys不在系统 PATH 中,用--nsys_cmd显式指定路径(例如--nsys_cmd /usr/bin/nsys)。

关于 nsys 在 SGLang 环境下的安装与基本采集,可参考 docs/docs/developer_guide/benchmark_and_profiling.mdx 中的 "Profile with Nsight" 章节,其中提供了 apt 安装 nsight-systems-cli 的完整命令,以及基于 SGLang Docker 容器的使用方式。

命令行参数详解

脚本通过 argparse 接收四个参数(见 gputrc2graph.py):

参数必填说明
--in_file输入文件及其元数据列表,每项格式为<nsys-rep>,<engine>,<model>,<elapsed_nonprofiled_sec>,多项之间以空格分隔
--out_dir生成的 CSV 与 HTML 输出目录;不指定则输出到当前目录
--titleHTML 图表的标题;不指定时默认使用Model_Engine
--nsys_cmdnsys命令路径,默认nsys(假设已在 PATH 中)

其中--in_file每个元组的四个字段含义如下:

  • nsys-rep.nsys-rep文件路径;
  • engine:引擎名称(例如sglang),必须与引擎目录下的 JSON 文件定义一致;
  • model:模型名称(例如llamagpt-ossds),必须在该引擎下已注册;
  • elapsed_nonprofiled_sec未开启 profiling 时完成整个测试的墙钟运行时间(秒)。此值用于计算 CPU(非 GPU)耗时。若填0,则退化为使用 nsys-rep 文件自身的记录时长——注意这可能虚增非 GPU 时间(因为带 profiling 的实际运行时长往往更长)。

脚本在启动时会列出当前支持的所有engine:[model]组合并写入--help帮助字符串,运行python3 gputrc2graph.py --help即可查看最新支持的引擎与模型清单,这也是原文档推荐的首选排查方式。

第一步:用 nsys 采集 SGLang 服务的 CUDA 追踪

以 llama-3.1-8B 模型 + SGLang 服务为例,完整流程分为四步:

步骤 1:带 profiling 启动服务器

nsys profile -t cuda -o nsys_res -f true --trace-fork-before-exec=true \ --cuda-graph-trace=node --delay <DELAY> --duration <DURATION> \ python3 -m sglang.launch_server --model meta-llama/Llama-3.1-8B ...
  • -t cuda:只追踪 CUDA 活动(脚本仅依赖 cuda 追踪数据);
  • -o nsys_res:输出文件前缀,完成后生成nsys_res.nsys-rep
  • -f true:覆盖同名旧文件;
  • --trace-fork-before-exec=true:追踪 fork/exec 产生的子进程,确保多进程场景不丢失追踪段;
  • --cuda-graph-trace=node:以节点粒度记录 CUDA Graph 内核(SGLang 默认启用 CUDA Graph 加速,必须开启此选项才能看到图内内核);
  • --delay <DELAY>:延迟采集的秒数,给足 SGLang 服务器加载模型、完成预热的时间,避免采集到无意义的启动阶段;
  • --duration <DURATION>:采集持续秒数,必须大于客户端压测的总时长,否则会截断负载阶段的追踪。

若希望在服务器运行期间手动控制结束时机,可以参考 benchmark_and_profiling.mdx 的做法:先nsys sessions list获取会话 ID(形如profile-XXXXX),再nsys stop --session=profile-XXXXX立即停止 profiling 并生成.nsys-rep文件,而无需等待--duration耗尽。

步骤 2:启动客户端负载

服务器起来后,另开终端运行 SGLang 的负载生成命令(例如python3 -m sglang.bench_serving --backend sglang ...)。测试完成后,再过DURATION秒,nsys 会生成nsys_res.nsys-rep并关闭服务器。

步骤 3:无 profiling 重跑一次负载

重新按步骤 1 启动服务器(这次不加nsys profile),再按步骤 2 重跑客户端测试,记录整个测试的完成秒数。这个"未带 profiling 的真实墙钟时间"是后续计算 CPU(非 GPU)耗时的基准。

步骤 4:运行分析脚本

假如步骤 3 记录的总耗时为 132 秒:

python3 gputrc2graph.py \ --in_file run1.nsys-rep,sglang,llama,132

脚本在当前目录生成result.htmlresult.csv两个文件(也可用--out_dir指定输出目录)。

提示:若当前目录下已存在同名的中间 CSV(*_cuda_gpu_trace.csv*_cuda_gpu_kernel_tracesum.csv),脚本会根据修改时间判断是否复用缓存(should_gen_file逻辑,见 gputrc2graph.py),重复分析同一份追踪时能明显提速。

读懂 result.html:堆叠柱状图与数据表

result.html以堆叠柱状图展示各类别(Category)的 GPU 耗时秒数。以文档中的示例结果为例:attention 内核类别耗时约 63 秒,为最大类别,其次为gemm内核。这种归并后的视图能让用户一眼定位"时间花在哪",从而确定性能优化时应优先投入的内核方向。

柱状图下方还附加了一张数据透视表(由make_html中的pivot_table生成,见 gputrc2graph.py):以 Category 为行、Model_Engine为列汇总耗时,并在末尾追加total_elapsed_sec合计行,方便直接复制到 Excel 或其他后处理工具中继续分析。

读懂 result.csv:从类别下钻到具体内核

柱状图回答"哪个类别耗时最多",而result.csv回答"这个类别里到底是哪些内核在消耗时间"。它的每一行包含Model_EngineCategoryInstancesNameElapsed Time (sec)等字段,记录了每个具体内核名称被映射到的类别。

文档给出的场景非常典型:假设用户想优化 triton 内核——它虽然不是最大的耗时来源(例如仅 0.01 秒),但可能从未被充分优化过。此时就可以从result.csv中筛选出Category == "triton_kernel"的所有行,找到具体是哪些 triton 内核、各自耗时多少、被调用多少次,从而精准定位下一步的优化对象。csv 与 html 的映射口径完全一致(同一份Category列),因此可以放心交叉使用。

多 profile 横向对比:一次命令分析多份追踪

当手头有多份不同模型(或不同配置)的 nsys 追踪文件时,可以在一次调用中全部传入,例如同时对比 llama 与 gpt-oss:

python3 gputrc2graph.py \ --in_file run1.nsys-rep,sglang,llama,100 run2.nsys-rep,sglang,gpt-oss,102 \ --out_dir results

处理流程与单文件分析一致,但输出中会出现多根堆叠柱,可以直接比较不同模型/配置在各内核类别上的 GPU 耗时差异。由于所有追踪共用同一套类别定义,跨配置的柱高对比是严格可比的。

对比分析的典型用法是:一旦发现某个类别在某配置下耗时显著偏高,就用对应的result.csv查看该类别映射了哪些内核、哪几个内核耗时最大,从而定位造成整体差异的根因内核。注意图表横轴标签会附加序号(源码中df["Model_Engine"] = f"{model}_{engine}_{file_name}_{idx}",见 gputrc2graph.py),当同一模型有多个副本时会自动区分。

为新的引擎/模型扩展内核分类

脚本的内核分类规则并非写死在代码里,而是以 JSON 文件形式存放在 gputrc2graph.py同目录下。启动时load_engine_model会扫描该目录下所有*.json文件并合并(见 gputrc2graph.py),因此扩展新分类不需要改任何 Python 代码,只需新增或修改 JSON 文件。

仓库自带的引擎/模型定义位于 sglang_engine_model.json,目前包含sglang引擎下的llamads(DeepSeek)、gpt-oss三个模型,覆盖了 gemm、moe_gemm、moe、prepare_next、nccl_and_custom_ar、norm、topk、activation、rope、softmax、attn、elementwise、quantize、reduce、triton_kernel 等十余个类别。

以文档中的例子:假设要为引擎DEF、模型ABC建立分类,且ABC有 4 个需要归类的内核——gemm 类内核名称含*H**I*,attn 类内核名称含*J**K*,则新建一个与gputrc2graph.py同目录的 JSON 文件(例如engine_DEF.json),内容如下:

{ "DEF": { "ABC": { "H|I": "gemm", "J|K": "attn", "CUDA mem": "non-gpu-H_D_memops", ".*": "misc" } } }

每个条目的含义:

  • key:一个正则表达式(regex),用于匹配内核名称;
  • value:匹配到的内核被归入的类别名。

注意规则按 JSON 中声明的顺序依次匹配(源码中anno_gpu_kernname_helper逐条re.search并返回首个命中项,见 gputrc2graph.py),因此越具体的规则应写在越前面

文件末尾的两条规则对所有引擎/模型通用,建议保留:

  • "CUDA mem": "non-gpu-H_D_memops":将 CUDA 内存操作(memcpy/memset 等)归为非 GPU 计算类;
  • ".*": "misc":兜底规则,匹配一切剩余未分类的内核,保证每个内核都能被归类、不会出现空类别。

保存 JSON 后,直接按新引擎/模型运行脚本即可:

--in_file new.nsys-rep,DEF,ABC,<runtime>

如果engine_DEF.json文件已存在,只需在该引擎节点下追加一个新模型节点即可,无需重建文件;脚本会自动把同一引擎下所有模型的定义合并到内存中。

源码要点:非重叠耗时如何计算

理解Elapsed Time (sec)Total Time (sec)的区别,是正确解读结果的前提。两者的差异源自 sum_non_overlapping_intervals:

  • 先把所有内核按Start (ns)升序排序,用current_end维护当前已覆盖区间的最大结束时间;
  • 对每个后续内核,若其开始时间落在已覆盖区间内:
    • 部分重叠:只累加超出current_end的新增时长,并推进current_end
    • 完全重叠:耗时记为 0;
  • 无重叠:完整计入并推进current_end

这样得到的是每个内核在 GPU 上"独占"的有效耗时。CSV 中的Elapsed Time (sec)即基于此口径,而Total Time (sec)是内核自身的原始Duration累加。前者用于堆叠图与类别汇总,后者反映原始调用总量,两者结合可以判断并行度的高低。

此外,非 GPU(CPU)耗时在gen_graph中通过total_sec - gpu_sec计算(见 gputrc2graph.py),并追加为一行Category = "CPU(non-GPU)"的虚拟记录;若传入的elapsed_nonprofiled_sec反而小于 GPU 耗时(数据异常),脚本会告警并将总耗时重置为 GPU 耗时,避免出现负的 CPU 时间。

常见问题与排查

  • 报错'nsys' failed ... Use --nsys_cmd to specify nsys pathnsys不在 PATH 中,或命令执行失败。用--nsys_cmd传入完整路径;同时确认本地 nsys 版本 ≥ 采集追踪时的版本(见 gputrc2graph.py)。
  • 报错engine X unknown/model Y unknown--in_file中填写的引擎/模型名未在 JSON 中注册。运行python3 gputrc2graph.py --help查看支持的engine:[model]列表,并按上一节方式扩展 JSON。
  • CPU(non-GPU) 时间异常偏大:检查elapsed_nonprofiled_sec是否填了带 profiling 的时长或填了 0。文档明确提示:填0会使用 nsys-rep 文件内的记录时长,可能虚增非 GPU 时间;应填写未带 profiling时的真实墙钟运行秒数。
  • 看不到 CUDA Graph 内的内核:采集时务必带上--cuda-graph-trace=node,否则图捕获后的内核不会出现在追踪中,导致类别耗时严重失真。

小结

gputrc2graph.py把 SGLang + nsys 的 GPU 剖析流程收敛为"采集 → 一行命令分析 → 堆叠图 + 映射表"的闭环:result.html回答"时间花在哪个类别",result.csv回答"该类别下具体是哪些内核",JSON 规则文件则让内核分类可以随新引擎/新模型零代码扩展。结合 sglang_engine_model.json 中现成的 llama、ds、gpt-oss 分类定义,SGLang 使用者可以快速将性能优化从"凭经验猜测"推进到"按内核耗时数据决策"。

【免费下载链接】sglangSGLang is a high-performance serving framework for large language models and multimodal models.项目地址: https://gitcode.com/GitHub_Trending/sg/sglang

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询