|
| 1 | +--- |
| 2 | +name: nsys-capture |
| 3 | +description: 通用 GPU 推理服务 nsys 性能抓取工具。根据用户的启动脚本自动注入 nsys 命令、构建带 nsys 的启动脚本,完成完整的 GPU profiling 抓取流程,输出 .nsys-rep 文件。 |
| 4 | +--- |
| 5 | + |
| 6 | +# Nsys 性能抓取 Skill |
| 7 | + |
| 8 | +## 触发条件 |
| 9 | + |
| 10 | +- 用户需要对推理服务进行 nsys GPU profiling / 性能分析 |
| 11 | +- 用户提到 nsys profile、抓 nsys、GPU profiling、性能抓取 |
| 12 | + |
| 13 | +--- |
| 14 | + |
| 15 | +## 总体工作流(5 步) |
| 16 | + |
| 17 | +``` |
| 18 | +Step 1:信息收集 → 从用户脚本和描述中提取关键参数 |
| 19 | +Step 2:注入 profiling → 检查并向 FastDeploy 代码注入 nvprof_start/nvprof_stop |
| 20 | +Step 3:生成脚本 → 构建 start_nsys.sh(注入 nsys)+ 确认请求脚本 |
| 21 | +Step 4:用户确认 → 展示生成的脚本,等用户确认后才执行 |
| 22 | +Step 5:执行抓取 → 启动服务 → 等待就绪 → 发送请求 → 等待文件 → 重命名 |
| 23 | +``` |
| 24 | + |
| 25 | +--- |
| 26 | + |
| 27 | +## Step 1:信息收集 |
| 28 | + |
| 29 | +读取用户提供的启动脚本,提取以下信息: |
| 30 | + |
| 31 | +| 信息 | 提取方式 | 无法提取时 | |
| 32 | +|------|---------|-----------| |
| 33 | +| **模型路径** | `--model` 参数 / `MODEL_PATH` 变量 / 用户描述 | 询问用户,必填 | |
| 34 | +| **服务端口** | `--port` / `--ports` 参数 / 用户描述 | 询问用户,默认 8080 | |
| 35 | +| **Host IP** | 用户描述("本机"→127.0.0.1,远端则询问) | 默认 127.0.0.1 | |
| 36 | +| **nsys 输出目录** | 用户描述 / 脚本中 `OUTPUT_DIR`、`NSYS_PATH` 变量 | 默认 `/tmp/nsys_record` | |
| 37 | +| **nsys 版本** | 用户指定("详细版"/"高版" or "粗略版"/"低版") | 默认粗略版 | |
| 38 | +| **请求脚本** | 用户是否提供了测试请求脚本 | 使用 skill 内置默认脚本 | |
| 39 | +| **nsys_start_step** | 用户指定 nvprof_start 触发的 decode 步数 | 默认 40 | |
| 40 | +| **nsys_stop_step** | 用户指定 nvprof_stop 触发的 decode 步数 | 默认 60 | |
| 41 | + |
| 42 | +信息收集完毕后,进入 Step 2。 |
| 43 | + |
| 44 | +--- |
| 45 | + |
| 46 | +## Step 2:检查并注入 FastDeploy profiling 代码 |
| 47 | + |
| 48 | +nsys 使用 `-c cudaProfilerApi` 模式,要求 FastDeploy 代码中必须包含 `nvprof_start()` / `nvprof_stop()` 调用。本步骤自动检测并注入。 |
| 49 | + |
| 50 | +### 2.1 定位 FastDeploy 代码路径 |
| 51 | + |
| 52 | +按优先级查找目标文件 `fastdeploy/worker/gpu_model_runner.py`: |
| 53 | + |
| 54 | +1. **PYTHONPATH 优先**:检查用户启动脚本中是否通过 `PYTHONPATH` 或 `sys.path` 指定了 FastDeploy 路径(如 `export PYTHONPATH=/path/to/FastDeploy:$PYTHONPATH`),若有,使用该路径下的 `fastdeploy/worker/gpu_model_runner.py` |
| 55 | +2. **pip 安装路径兜底**:执行 `pip3 show fastdeploy-gpu`,取 `Location` 字段,拼接 `fastdeploy/worker/gpu_model_runner.py` |
| 56 | + |
| 57 | +确认文件存在后继续。若文件不存在,提示用户手动指定路径。 |
| 58 | + |
| 59 | +### 2.2 检查是否已有 profiling 代码 |
| 60 | + |
| 61 | +在 `gpu_model_runner.py` 中搜索 `nvprof_start` 和 `nvprof_stop`: |
| 62 | + |
| 63 | +```bash |
| 64 | +grep -n "nvprof_start\|nvprof_stop" <path_to_gpu_model_runner.py> |
| 65 | +``` |
| 66 | + |
| 67 | +- **已存在且未被禁用**(无 `if False` 守卫、代码未注释)→ 跳过注入,提示用户已有 profiling 代码,直接进入 Step 3 |
| 68 | +- **已存在但被禁用**(有 `if False` 守卫或被注释)→ 提示用户发现已有但被禁用的 profiling 代码,询问是否需要启用并修正 step 值 |
| 69 | +- **不存在** → 进入 2.3 执行自动注入 |
| 70 | + |
| 71 | +### 2.3 自动注入 profiling 代码 |
| 72 | + |
| 73 | +**注入前先备份原文件:** |
| 74 | +```bash |
| 75 | +cp <path_to_gpu_model_runner.py> <path_to_gpu_model_runner.py>.bak |
| 76 | +``` |
| 77 | + |
| 78 | +需注入 3 处代码,参考模板来自 `fastdeploy/worker/gpu_model_runner.py` 的已有实现模式: |
| 79 | + |
| 80 | +#### 位置 1:`__init__` 方法中 |
| 81 | + |
| 82 | +在 `self.current_launch_token_num = 0` 之后添加: |
| 83 | + |
| 84 | +```python |
| 85 | +self.nsys_step = 0 |
| 86 | +``` |
| 87 | + |
| 88 | +#### 位置 2:`_execute_model` 方法中,执行调度代码之前 |
| 89 | + |
| 90 | +找到如下执行调度代码块: |
| 91 | +```python |
| 92 | +if not self.enable_overlap_schedule: |
| 93 | + self.execute_model_normal(model_forward_batch, num_running_requests) |
| 94 | +else: |
| 95 | + self.execute_model_overlap(model_forward_batch, num_running_requests) |
| 96 | +``` |
| 97 | + |
| 98 | +在其**之前**插入 nvprof_start: |
| 99 | + |
| 100 | +```python |
| 101 | + if self.nsys_step == <nsys_start_step> and self.forward_meta.ids_remove_padding.shape[0] > 0: |
| 102 | + from paddle.framework import core |
| 103 | + core.nvprof_start() |
| 104 | +``` |
| 105 | + |
| 106 | +> `<nsys_start_step>` 替换为 Step 1 收集到的值,默认 40。 |
| 107 | +
|
| 108 | +#### 位置 3:`_execute_model` 方法中,执行调度代码之后 |
| 109 | + |
| 110 | +在执行调度代码块**之后**插入 nvprof_stop 和步数递增: |
| 111 | + |
| 112 | +```python |
| 113 | + if self.nsys_step == <nsys_stop_step> and self.forward_meta.ids_remove_padding.shape[0] > 0: |
| 114 | + from paddle.framework import core |
| 115 | + core.nvprof_stop() |
| 116 | + if self.forward_meta.ids_remove_padding.shape[0] > 0: |
| 117 | + self.nsys_step += 1 |
| 118 | +``` |
| 119 | + |
| 120 | +> `<nsys_stop_step>` 替换为 Step 1 收集到的值,默认 60。 |
| 121 | +
|
| 122 | +### 2.4 展示 diff 并确认 |
| 123 | + |
| 124 | +注入完成后,向用户展示修改的 diff: |
| 125 | + |
| 126 | +```bash |
| 127 | +diff <path_to_gpu_model_runner.py>.bak <path_to_gpu_model_runner.py> |
| 128 | +``` |
| 129 | + |
| 130 | +说明: |
| 131 | +- 注入了 nsys profiling 代码,nvprof_start 在第 `<nsys_start_step>` 步触发,nvprof_stop 在第 `<nsys_stop_step>` 步触发 |
| 132 | +- 原文件已备份为 `.bak` |
| 133 | + |
| 134 | +**等用户确认后**,进入 Step 3。 |
| 135 | + |
| 136 | +--- |
| 137 | + |
| 138 | +## Step 3:生成 start_nsys.sh |
| 139 | + |
| 140 | +### 3.1 核心注入原则 |
| 141 | + |
| 142 | +**不要** 使用 `nsys profile ... bash start.sh` 的包裹形式。 |
| 143 | +**要** 在脚本内部,找到实际启动服务的可执行命令行(`python` / `python3` / `torchrun` / `deepspeed` 等),在该行**前面**直接插入 `${NSYS_CMD}`,让 nsys 成为该进程的直接父进程。 |
| 144 | + |
| 145 | +```bash |
| 146 | +# 原始脚本中的启动命令: |
| 147 | +python -m some.serving.module \ |
| 148 | + --model /path/to/model \ |
| 149 | + --port 8080 |
| 150 | + |
| 151 | +# 注入后: |
| 152 | +${NSYS_CMD} python -m some.serving.module \ |
| 153 | + --model /path/to/model \ |
| 154 | + --port 8080 |
| 155 | +``` |
| 156 | + |
| 157 | +### 3.2 生成步骤 |
| 158 | + |
| 159 | +1. 完整复制用户原始启动脚本内容 |
| 160 | +2. 在 shebang 行(`#!/bin/bash`)之后、其他内容之前,插入 **nsys 变量定义块**(见 3.3) |
| 161 | +3. 定位脚本中实际启动服务进程的那行 `python` / `python3` 命令,在行首插入 `${NSYS_CMD}` |
| 162 | + - 如果命令跨多行(有 `\` 续行),只在**第一行行首**加 `${NSYS_CMD}`,续行不动 |
| 163 | + - 如果脚本里有多个 python 命令,只注入**最后一个**(实际启动服务的那个,通常是前台阻塞的) |
| 164 | +4. 文件命名:与原脚本同目录,加 `_nsys` 后缀,如 `start.sh` → `start_nsys.sh` |
| 165 | + |
| 166 | +### 3.3 nsys 变量定义块(插入到脚本顶部) |
| 167 | + |
| 168 | +```bash |
| 169 | +# ============================================================ |
| 170 | +# NSYS INJECTION - 由 nsys-capture skill 自动注入 |
| 171 | +# ============================================================ |
| 172 | +NSYS_OUTPUT_DIR="${NSYS_OUTPUT_DIR:-/tmp/nsys_record}" |
| 173 | +mkdir -p "${NSYS_OUTPUT_DIR}" |
| 174 | +NSYS_TIMESTAMP=$(date +%Y%m%d_%H%M%S) |
| 175 | +NSYS_OUTPUT_PATH="${NSYS_OUTPUT_DIR}/${NSYS_TIMESTAMP}_nsys" |
| 176 | +echo "${NSYS_OUTPUT_PATH}" > /tmp/nsys_capture_output_path |
| 177 | +echo "[nsys-capture] nsys 输出路径: ${NSYS_OUTPUT_PATH}" |
| 178 | +``` |
| 179 | + |
| 180 | +然后根据用户选择的版本,追加对应的 NSYS_CMD 定义(两版都写,未选中的注释掉): |
| 181 | + |
| 182 | +**粗略版(默认,日常性能分析,文件约 6-15 MB)**: |
| 183 | +```bash |
| 184 | +# 粗略版(已启用) |
| 185 | +NSYS_CMD="nsys profile -c cudaProfilerApi \ |
| 186 | + -t nvtx,osrt,cuda,cublas-verbose,python-gil \ |
| 187 | + -f true \ |
| 188 | + --cudabacktrace=all \ |
| 189 | + --python-backtrace=cuda \ |
| 190 | + --cuda-memory-usage=true \ |
| 191 | + --output ${NSYS_OUTPUT_PATH}" |
| 192 | +# 详细版(深度调试,含完整 cuda graph trace,文件可达数百 MB) |
| 193 | +# NSYS_CMD="nsys profile -c cudaProfilerApi \ |
| 194 | +# -t nvtx,osrt,cuda,cublas-verbose,python-gil \ |
| 195 | +# -f true \ |
| 196 | +# --cudabacktrace=all \ |
| 197 | +# --python-backtrace=cuda \ |
| 198 | +# --cuda-memory-usage=true \ |
| 199 | +# --cuda-graph-trace=node \ |
| 200 | +# --output ${NSYS_OUTPUT_PATH}" |
| 201 | +``` |
| 202 | + |
| 203 | +**详细版(用户指定时)**:将上面两段注释状态互换即可。 |
| 204 | + |
| 205 | +```bash |
| 206 | +# 粗略版(日常性能分析,文件约 6-15 MB) |
| 207 | +# NSYS_CMD="nsys profile -c cudaProfilerApi \ |
| 208 | +# -t nvtx,osrt,cuda,cublas-verbose,python-gil \ |
| 209 | +# -f true \ |
| 210 | +# --cudabacktrace=all \ |
| 211 | +# --python-backtrace=cuda \ |
| 212 | +# --cuda-memory-usage=true \ |
| 213 | +# --output ${NSYS_OUTPUT_PATH}" |
| 214 | +# 详细版(已启用) |
| 215 | +NSYS_CMD="nsys profile -c cudaProfilerApi \ |
| 216 | + -t nvtx,osrt,cuda,cublas-verbose,python-gil \ |
| 217 | + -f true \ |
| 218 | + --cudabacktrace=all \ |
| 219 | + --python-backtrace=cuda \ |
| 220 | + --cuda-memory-usage=true \ |
| 221 | + --cuda-graph-trace=node \ |
| 222 | + --output ${NSYS_OUTPUT_PATH}" |
| 223 | +``` |
| 224 | + |
| 225 | +> **注意**:`NSYS_OUTPUT_PATH` 在定义 `NSYS_CMD` 时已展开,所以两个块的顺序必须是:先定义 `NSYS_OUTPUT_PATH`,再定义 `NSYS_CMD`。 |
| 226 | +> 如果用户脚本中 `NSYS_OUTPUT_DIR` 已有定义,注入块中的默认值会被覆盖,以用户脚本的为准。 |
| 227 | +
|
| 228 | +### 3.4 用户自定义 nsys 输出目录 |
| 229 | + |
| 230 | +- 如果用户明确提供了输出目录(如 `/data/nsys/`),将注入块中的默认值替换为该路径: |
| 231 | + ```bash |
| 232 | + NSYS_OUTPUT_DIR="/data/nsys/" |
| 233 | + ``` |
| 234 | +- 若用户脚本中已有 `NSYS_OUTPUT_DIR` 或类似变量,注入块放在该变量**之后**,并删除默认值赋值,直接复用。 |
| 235 | + |
| 236 | +--- |
| 237 | + |
| 238 | +## Step 4:确认请求脚本 |
| 239 | + |
| 240 | +向用户展示生成好的 `start_nsys.sh` 关键内容(注入块 + 被注入的 python 命令行),说明: |
| 241 | +- 注入的 nsys 版本(粗略/详细) |
| 242 | +- nsys 输出路径 |
| 243 | +- 使用的请求脚本(默认或用户提供的) |
| 244 | + |
| 245 | +**等用户确认后**,进入 Step 5 执行。 |
| 246 | + |
| 247 | +请求脚本优先级: |
| 248 | +1. 用户明确提供了测试请求脚本 → 使用用户的 |
| 249 | +2. 未提供 → 使用 skill 内置默认脚本: |
| 250 | + ```bash |
| 251 | + python3 ~/.claude/skills/nsys-capture/nsys_default_client.py <HOST> <PORT> |
| 252 | + ``` |
| 253 | + |
| 254 | +--- |
| 255 | + |
| 256 | +## Step 5:执行抓取 |
| 257 | + |
| 258 | +### 5.1 启动服务(后台) |
| 259 | + |
| 260 | +```bash |
| 261 | +rm -f /tmp/nsys_serve.log |
| 262 | +bash <path_to_start_nsys.sh> > /tmp/nsys_serve.log 2>&1 & |
| 263 | +echo "服务启动中,PID=$!" |
| 264 | +``` |
| 265 | + |
| 266 | +### 5.2 等待服务就绪 |
| 267 | + |
| 268 | +轮询策略(每 30s 一次): |
| 269 | +- **就绪标志**:`curl -s http://<HOST>:<PORT>/v1/models` 返回 HTTP 200 |
| 270 | +- **致命错误**:日志出现 `Traceback` / `AssertionError` / `OOM` / `Killed`,且 30s 内日志无新增行 → 停止等待,输出最后 30 行日志供排查 |
| 271 | +- **超时**:超过 20 分钟未就绪 → 终止 |
| 272 | + |
| 273 | +检查日志时,**每次只取最新 50 行**(避免进度条等刷屏内容干扰): |
| 274 | +```bash |
| 275 | +tail -50 /tmp/nsys_serve.log | grep -E "Traceback|AssertionError|OOM|Killed|startup complete" |
| 276 | +``` |
| 277 | + |
| 278 | +### 5.3 发送请求 |
| 279 | + |
| 280 | +```bash |
| 281 | +python3 <request_script> [HOST] [PORT] 2>/dev/null || true |
| 282 | +``` |
| 283 | + |
| 284 | +- 流式连接中断(`RemoteProtocolError`)是正常现象,不报错 |
| 285 | +- 如果 nsys 埋点是在 iter N 触发 stop,请求的 token 数需**足够多**(生成 token > N),保证 stop 被触发 |
| 286 | + |
| 287 | +### 5.4 等待 nsys 文件 & 重命名 |
| 288 | + |
| 289 | +```bash |
| 290 | +NSYS_OUTPUT_BASE=$(cat /tmp/nsys_capture_output_path) |
| 291 | +EXPECTED_FILE="${NSYS_OUTPUT_BASE}.nsys-rep" |
| 292 | +``` |
| 293 | + |
| 294 | +轮询直到文件出现且大小稳定(连续两次 `stat -c%s` 相同,间隔 3s),超时 120s。 |
| 295 | + |
| 296 | +稳定后重命名(带类型标记): |
| 297 | +```bash |
| 298 | +FINAL_NAME="${NSYS_OUTPUT_DIR}/$(date +%Y%m%d_%H%M%S)_nsys_<type>_<level>.nsys-rep" |
| 299 | +# type: text(文生文)/ mm(多模态)/ custom(用户自定义请求) |
| 300 | +# level: low(粗略版)/ high(详细版) |
| 301 | +mv "${EXPECTED_FILE}" "${FINAL_NAME}" |
| 302 | +``` |
| 303 | + |
| 304 | +输出最终路径和文件大小,抓取完成。 |
| 305 | + |
| 306 | +--- |
| 307 | + |
| 308 | +## nsys 两版参数对比 |
| 309 | + |
| 310 | +| 版本 | 额外参数 | 文件大小 | 适用场景 | |
| 311 | +|------|---------|---------|---------| |
| 312 | +| **粗略版(默认)** | 无 `--cuda-graph-trace` | 6-15 MB | 日常性能分析,快速查看算子耗时 | |
| 313 | +| **详细版** | `--cuda-graph-trace=node` | 可达数百 MB | 深度调试,完整 cuda graph 节点信息 | |
| 314 | + |
| 315 | +--- |
| 316 | + |
| 317 | +## 常见问题 |
| 318 | + |
| 319 | +**Q: nsys 文件未生成?** |
| 320 | +1. 检查埋点:`nvprof_start` 和 `nvprof_stop` 各一处,顺序正确 |
| 321 | +2. 检查请求 token 数是否足够(需 > 埋点触发 iter 数) |
| 322 | +3. 用 `nsys sessions list` 查看是否进入过 `RangeCollection` 状态 |
| 323 | + |
| 324 | +**Q: 服务启动失败?** |
| 325 | +- 检查 `/tmp/nsys_serve.log` 最后 50 行 |
| 326 | +- 常见原因:端口占用、模型路径不存在、GPU 显存不足 |
| 327 | + |
| 328 | +**Q: 如何分析 .nsys-rep 文件?** |
| 329 | +```bash |
| 330 | +nsys-ui <file>.nsys-rep |
| 331 | +``` |
| 332 | + |
| 333 | +**Q: 需要抓多次怎么办?** |
| 334 | +每次抓取后服务会自动退出(`nvprof_stop` 触发),下次需重新启动(需重新加载模型)。 |
0 commit comments