RTX Pro 5000 72G 跑 27B 慢 / 是卡的锅吗 / 换 SGLang 前先分清显存和解码瓶颈

文章导读
27B 放在 RTX Pro 5000 72G 上跑得慢,先别急着把锅扣在显卡上。按参数量乘位宽估算,fp16 的 27B 权重大概在 54GB 量级,4bit 量化在 15GB 上下,72G 显存通常装得下;真正拖速度的,往往是解码阶段的逐 token 速度,或者上下文拉长后 KV 缓存把显存吃满、并发一开就排队。把显存占用、KV 缓存增长、解码吞吐拆成三个可测点,就能判断该继续调 llama.
📋 目录
  1. 用 nvidia-smi 采样模型加载前后的显存占用
  2. 在 llama.cpp 日志里定位慢发生在哪一步
  3. 只改一个变量:上下文长度或并发请求数
  4. 用同一段请求打到 SGLang 服务上
  5. 按对照结果决定继续调参还是换引擎
A A

27B 放在 RTX Pro 5000 72G 上跑得慢,先别急着把锅扣在显卡上。按参数量乘位宽估算,fp16 的 27B 权重大概在 54GB 量级,4bit 量化在 15GB 上下,72G 显存通常装得下;真正拖速度的,往往是解码阶段的逐 token 速度,或者上下文拉长后 KV 缓存把显存吃满、并发一开就排队。把显存占用、KV 缓存增长、解码吞吐拆成三个可测点,就能判断该继续调 llama.cpp 参数,还是值得换 SGLang。每一步只改一件事,结论以你机器上的实际读数为准。

判断方向:72G 显存跑 27B 多半不是容量不够,慢更可能出在解码吞吐或 KV 缓存增长。先采样显存确认权重完整驻留,再把加载耗时和逐 token 耗时分开看,然后只动上下文长度或并发数观察速度变化;只有同一段请求在 SGLang 上确实更快,换引擎才有意义。验证以日志字段和返回里的用量字段为准,不靠感觉。

用 nvidia-smi 采样模型加载前后的显存占用

先确认权重是不是真的进了显存,而不是被换出到内存或被 mmap 读得慢。开两个终端,一个跑采样,一个跑模型:

# 终端 A:每秒采一次,持续观察
nvidia-smi `--query-gpu`=timestamp,memory.used,memory.total,utilization.gpu \
  `--format`=csv -l 1

# 需要看是哪个进程占的显存(-l 同样支持)
nvidia-smi `--query-compute-apps`=pid,process_name,used_memory `--format`=csv

采样时机建议取四个点:加载前的空载基线、加载完成后静置约 30 秒、首次请求正在生成输出时、生成结束后。加载完成后的读数用于判断权重是否完整驻留;生成中的读数用于判断 KV 缓存和计算缓冲又吃掉多少。

区分“权重占用”和“本进程其他占用”,可以先用参数量乘位宽估算权重体积(例如 27B fp16、4bit 各自的理论量级),再和 `--query-compute-apps` 里对应 pid 的 used_memory 对比。CUDA context 本身还会额外占几百 MB 到 1GB 左右,这部分是固定的。如果进程显存明显小于权重估算值,常见原因是 -ngl 层数没拉满、部分层留在 CPU,或者用的是 mmap 加载、权重按需读入。此时瓶颈在数据搬运,不在显卡算力。

在 llama.cpp 日志里定位慢发生在哪一步

llama.cpp 启动和推理日志里,需要盯住的字段包括:llm_load_tensors: offloaded N/N layers to GPU(判断是否全部 offload)、load time(加载耗时,一次性)、KV self size(KV 缓存占用)、prompt eval time = ... ms / N tokens(预填充)、eval time = ... ms / N tokens ( ... ms per token, ... tokens/s )(逐 token 解码)。加载慢和逐 token 慢是两个问题,前者只影响第一次,后者才是持续体验。

RTX Pro 5000 72G 跑 27B 慢 / 是卡的锅吗 / 换 SGLang 前先分清显存和解码瓶颈

要复现同一次请求,固定变量:把提示词写进文件,固定生成长度、随机种子和采样温度,禁用会让结果漂移的采样项。

./llama-cli -m /models/your-27b.gguf \
  -ngl 999 -c 8192 -n 256 \
  -p "$(cat prompt.txt)" \
  `--temp` 0 -s 42

# 需要走服务时
./llama-server -m /models/your-27b.gguf -ngl 999 \
  -c 8192 -np 1 `--host` 0.0.0.0 `--port` 8080

注意 prompt 缓存:同一条 prompt 连打两次,第二次的 prompt eval 会明显变短,比较时要么每次换一段等长 prompt,要么明确记录缓存命中的情况,否则会把缓存效果误读成引擎变快。

只改一个变量:上下文长度或并发请求数

KV 缓存和上下文长度近似线性相关,可按下面这个关系估算量级:KV ≈ 2 × n_layer × n_kv_head × head_dim × ctx_len × 每元素字节数。其中 2 对应 K 和 V,n_kv_head 在 GQA 模型里比注意力头数小得多,这也是很多 27B 模型上下文能开较大的原因。并发(-np)则是每个请求各自带一份 KV 和计算缓冲,显存按并发数叠加。

做法是从较小上下文起步,每次只改一个值,把下面几项抄成一行记录:

RTX Pro 5000 72G 跑 27B 慢 / 是卡的锅吗 / 换 SGLang 前先分清显存和解码瓶颈
  • 上下文长度 -c
  • 并发请求数 -np
  • 生成过程中显存峰值(nvidia-smi 读数)
  • prompt eval 耗时与每 token 解码耗时
  • 每秒输出 token 数(日志里的 tokens/s)

如果加大上下文后显存接近上限、但逐 token 速度基本不变,说明是容量被占满,属于需要控制长度或换更大卡的问题;如果显存没满而并发一上来速度就掉,瓶颈更可能在调度和批处理方式,而不是显存容量。

用同一段请求打到 SGLang 服务上

这一步的目的不是证明谁更好,而是拿到能和 llama.cpp 直接对齐的数据。服务启动骨架如下,模型路径、上下文长度、显存比例按实际环境替换:

python -m sglang.launch_server \
  `--model-path` /models/your-27b \
  `--host` 0.0.0.0 `--port` 30000 \
  `--context-length` 8192 \
  `--mem-fraction-static` 0.85 \
  `--tp` 1

然后用与前面完全相同的一段 prompt、相同的最大输出长度和温度发一次请求:

RTX Pro 5000 72G 跑 27B 慢 / 是卡的锅吗 / 换 SGLang 前先分清显存和解码瓶颈
curl -s http://127.0.0.1:30000/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "your-27b",
    "messages": [{"role":"user","content":"<粘贴同一段 prompt>"}],
    "max_tokens": 256,
    "temperature": 0,
    "stream": false
  }'

返回体里的用量字段(prompt_tokens、completion_tokens)要拿来核对:它必须和 llama.cpp 那次请求的输入输出长度一致,否则两次对比的不是同一件事。耗时用客户端侧计时,或使用官方附带的测试客户端,保持两边同样的并发数和输出长度。

一个容易踩的坑:两个引擎对量化格式的支持并不完全重合,同一份权重未必两边都能加载。如果只能换用不同精度的权重做对比,结论要打折,最好统一到同一份 fp16/bf16 或双方都支持的量化文件上。另外 SGLang 默认带前缀缓存,同一条 prompt 反复打会偏快,比较时应更换 prompt 或关注缓存命中情况。

按对照结果决定继续调参还是换引擎

把两次结果并排放在一张表里,需要比较的指标是:加载后静态显存、KV 随上下文增长的幅度、单请求每 token 解码耗时、每秒输出 token、以及并发下的总吞吐。判定顺序建议固定下来,避免被单点数据带偏:

  1. 先看显存是否装得下。若权重本身就没完整驻留,先解决 offload 和加载方式,此时换引擎没有意义。
  2. 再看单请求逐 token 速度。两边接近,说明解码本身不是引擎差异造成的,换引擎收益有限。
  3. 然后看并发。若同一批请求下 SGLang 的总吞吐明显更高,而单请求速度相当,说明差距来自批处理和 KV 调度,这才是考虑迁移的合理理由。
  4. 最后看上下文。若长上下文下 llama.cpp 显存先撑不住,而 SGLang 能通过显存比例控制稳住,迁移方向才更明确。

反过来,如果对照下来两边每 token 耗时相差不大、显存也没吃满,那继续在 llama.cpp 上调 offload 层数、上下文长度和线程数,通常比换一套服务栈更省事。换引擎之后参数调度、量化支持、日志格式都会变,这些成本要在决定之前就计入。