Skip to content

Commit f4ff8a2

Browse files
authored
Merge branch 'develop' into feat/weight-loading-natural-order
2 parents 47a3cff + 427d0f5 commit f4ff8a2

221 files changed

Lines changed: 26152 additions & 12119 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Nsys 性能抓取 Skill
2+
3+
## 概述
4+
5+
通用 GPU 推理服务 nsys 性能抓取工具。根据用户的启动脚本自动注入 nsys 命令、向 FastDeploy 代码注入 profiling 埋点、构建带 nsys 的启动脚本,完成完整的 GPU profiling 抓取流程,输出 .nsys-rep 文件。
6+
7+
## 工作流程
8+
9+
完整流程由 SKILL.md 定义,共 5 步:
10+
11+
1. **信息收集** — 从用户启动脚本和描述中提取模型路径、端口、nsys 版本等参数
12+
2. **注入 profiling** — 检查并向 FastDeploy `gpu_model_runner.py` 注入 `nvprof_start`/`nvprof_stop`
13+
3. **生成脚本** — 构建 `start_nsys.sh`(注入 nsys 命令到启动脚本)
14+
4. **用户确认** — 展示生成的脚本和注入的代码,等用户确认
15+
5. **执行抓取** — 启动服务 → 等待就绪 → 发送请求 → 等待文件 → 重命名
16+
17+
## 文件结构
18+
19+
```
20+
~/.claude/skills/nsys-capture/
21+
├── SKILL.md # Skill 定义文件(完整工作流规范)
22+
├── nsys_utils.sh # 辅助执行脚本(timeit 等工具)
23+
├── nsys_capture.sh # 核心函数(wait_service / wait_and_rename_file)
24+
├── nsys_default_client.py # 默认测试请求客户端
25+
└── README.md # 本文档
26+
```
27+
28+
## 环境变量
29+
30+
所有参数均通过 Step 1 信息收集从用户启动脚本中自动提取,无需手动设置环境变量。以下为可选覆盖项:
31+
32+
| 变量 | 默认值 | 说明 |
33+
|------|--------|------|
34+
| `NSYS_OUTPUT_DIR` | `/tmp/nsys_record` | nsys 输出目录 |
35+
36+
## 依赖
37+
38+
- nsys (Nsight Systems)
39+
- bash 4.0+
40+
- curl
41+
- python 3.10+
42+
- openai python 库(默认请求客户端使用)
43+
44+
## 常见问题
45+
46+
**Q: nsys 文件未生成?**
47+
1. 检查埋点:`nvprof_start``nvprof_stop` 各一处,顺序正确
48+
2. 检查请求 token 数是否足够(需 > 埋点触发 iter 数)
49+
3.`nsys sessions list` 查看是否进入过 `RangeCollection` 状态
50+
51+
**Q: 服务启动失败?**
52+
检查 `/tmp/nsys_serve.log` 最后 50 行,常见原因:端口占用、模型路径不存在、GPU 显存不足
53+
54+
**Q: 如何分析 .nsys-rep 文件?**
55+
使用 `nsys-ui <file>.nsys-rep` 打开分析。
Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
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

Comments
 (0)