RTX Pro 5000 72G 单卡跑 27B 级模型,llama.cpp 的瓶颈先看批处理设置

文章导读
72G 显存单卡跑 27B 级模型,显存没打满但出字慢,通常不是「显存不够」这一件事,而是两个问题被混在一起:批处理/并发调度有没有吃满算力,以及解码阶段单步本身是不是就慢。先把 llama.cpp 的启动参数、显存占用和日志耗时记下来,再拿同一段请求去比另一个引擎,判断才有依据,而不是靠感觉继续调参数。
📋 目录
  1. A 记录当前 llama.cpp 的启动参数与显存占用
  2. B 从 llama.cpp 日志里分辨加载阶段与生成阶段耗时
  3. C 只改批大小和并发数做单变量对照
  4. D 把同一段请求改打到 SGLang 服务上
  5. E 把两边的首 token 延迟和吞吐放在一起比
A A

72G 显存单卡跑 27B 级模型,显存没打满但出字慢,通常不是「显存不够」这一件事,而是两个问题被混在一起:批处理/并发调度有没有吃满算力,以及解码阶段单步本身是不是就慢。先把 llama.cpp 的启动参数、显存占用和日志耗时记下来,再拿同一段请求去比另一个引擎,判断才有依据,而不是靠感觉继续调参数。

适用场景:单卡 72G 跑 27B 级量化模型,显存有余量但每秒 token 偏低。操作动作:记录 llama.cpp 启动参数与显存占用,用日志区分权重加载与逐 token 评估耗时,再用批大小、并发数的单变量对照确认瓶颈位置。验证方式:同一 prompt、同一采样参数下比较两边的首 token 延迟与解码速度。风险边界:若瓶颈在解码单步耗时或采样开销,改批处理收益有限,此时再评估换引擎是否值得。

记录当前 llama.cpp 的启动参数与显存占用

没有基线就无法判断快慢,先把当前真实配置抄下来。llama.cpp 的 server 或 cli 启动时会回显一批参数,重点是上下文长度 -c、批大小 -b、微批 -ub、并行请求数 -np、卸载到 GPU 的层数 -ngl、线程数 -t,以及是否启用了 flash attention(不同版本写作 -fa`--flash-attn`)。把完整启动命令行原样存一份,后面做对照时只改其中一项。

./llama-server -m /models/xxx-27b-Q4_K_M.gguf \
  -c 32768 -b 2048 -ub 512 -np 4 \
  -ngl 99 -t 8 -fa \
  `--host` 0.0.0.0 `--port` 8080

显存占用要在两个时刻各看一次:模型加载完、空闲时,以及正在生成时。前者反映权重与 KV cache 的静态占用,后者能看出长上下文是否把显存顶到接近满。常用命令:

nvidia-smi `--query-gpu`=index,name,memory.used,memory.total,utilization.gpu `--format`=csv
nvidia-smi `--query-compute-apps`=pid,process_name,used_memory `--format`=csv
# 生成过程中另开一个终端采样
nvidia-smi dmon -s mu -d 1

如果 memory.used 离显存上限还有明显余量,就可以先把怀疑方向放在批处理和解码上;如果已经贴边,则上下文长度和并发数本身就会成为限制,调大 -b-np 反而可能触发反向分配或变慢。这一步给出的结论不需要精确,只需要确认「显存是否是当前约束」。

从 llama.cpp 日志里分辨加载阶段与生成阶段耗时

llama.cpp 在退出或每次请求结束后会打印一段性能统计,字段名随版本略有差别,通常在 llama_print_timingsllama_perf_context_print 附近,需要以本机实际日志为准。关注三行就够:

  • load time:权重加载耗时,只发生一次。它大小基本不影响持续出字速度,但如果每次重启都很久,说明模型文件读取是另一个问题。
  • prompt eval time:处理输入 prompt 的耗时,通常还带每 token 毫秒数。这一段慢说明预填充阶段吃 CPU 或批处理没配好。
  • eval time:逐 token 解码耗时,会给出每 token 毫秒和 tokens per second。真正影响体感出字速度的是这一行。

复现方式要固定:同一模型、同一上下文长度、同一段 prompt、输出长度固定、采样固定为贪心(`--temp` 0),跑两次以上看相邻结果是否稳定。

./llama-cli -m /models/xxx-27b-Q4_K_M.gguf \
  -ngl 99 -c 32768 -b 2048 -n 256 `--temp` 0 \
  -p "把下面这段配置说明改写成三条要点:"

判断顺序是:先看 eval time 是否本身就高(说明解码单步慢,与批处理关系不大),再看 prompt eval time 是否异常(说明预填充或调度有问题),最后才怀疑加载阶段。只有分清这两类耗时,后面调批大小才有意义。

只改批大小和并发数做单变量对照

这一步的目的是验证瓶颈是否出在批处理调度。前提是固定其他一切:同一个模型、同一上下文长度 -c、同一输出长度 -n、同一 prompt、同一采样参数。每次只动一个变量,比如先固定 -np 1,把 -b 从较小值往上调一档;再固定 -b,改 -np。每次跑完把统计行抄进同一张表。

RTX Pro 5000 72G 单卡跑 27B 级模型,llama.cpp 的瓶颈先看批处理设置
轮次-c-b-ub-npeval 每 token 毫秒tokens/s生成时显存占用
基线3276820485121
只改 -b3276840965121
只改 -np3276820485124

判读时注意两点:一是若加大 -b 后 tokens/s 没有变化、显存却上升,说明当前请求量下批处理不是瓶颈;二是并发数 -np 只有在同时压多个请求时才有意义,单请求测 -np 通常看不出差别,需要用并发脚本同时发几条请求再对比总吞吐。参数含义和可调范围以本机版本的帮助输出 `--help` 为准。

把同一段请求改打到 SGLang 服务上

在显存确认有余量的前提下,把同一模型以另一种引擎拉起,得到可比表现。SGLang 提供 OpenAI 兼容接口,启动骨架如下,模型路径、显存比例和端口按实际环境替换:

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

`--mem-fraction-static` 控制预留给权重和 KV cache 的显存比例,给太高会挤占其他进程,给太低会在长上下文时报显存不足,建议先取一个偏保守的值、跑通再调。服务起来后用同一段 prompt 发请求,返回结构里通常会带 token 用量字段:

curl -s http://127.0.0.1:30000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "xxx-27b",
    "messages": [{"role":"user","content":"把下面这段配置说明改写成三条要点:"}],
    "max_tokens": 256,
    "temperature": 0,
    "stream": false
  }'

注意 model 字段要与服务实际注册的名字一致(有的版本用路径,有的用 `--served-model-name` 指定)。用 stream: true 时,首 token 到达时间可以直接在终端里观察,这是和 llama.cpp 对比时最直观的一列。两个服务不要同时常驻占满显存,建议先停掉 llama.cpp 再起 SGLang,或者用不同端口分时切换。

把两边的首 token 延迟和吞吐放在一起比

比较维度按以下顺序看,先排除干扰项,再下判断:

  1. 采样参数与模板是否一致:temperature、top_p、top_k、min_p、repeat_penalty、seed、max_tokens、是否流式,以及 chat 模板和 system 提示是否相同。这些不一致会直接造成首 token 延迟和输出长度的假差异,必须先对齐再比。
  2. 输入 token 数是否相同:两个引擎的 tokenizer 和模板拼接方式可能不同,同一段文字算出的 token 数会有出入。用返回里的用量字段核对,差异大时对比意义有限。
  3. 首 token 延迟:反映预填充阶段。差别明显时,先回到批大小和上下文长度检查,而不是急着换引擎。
  4. 解码速度:看每秒 token 或每 token 毫秒。若两边接近,说明瓶颈在解码本身,换引擎未必解决问题;若一边明显更稳,再结合显存余量和并发需求决定是否迁移。

最终判断可以简化成一条:批大小和并发数调过之后,单请求解码速度没变、并发时总吞吐也上不去,而显存又没打满,这类情况更接近解码或调度实现层面的限制,值得评估换引擎;反之若调大并发后吞吐有改善、显存开始吃紧,说明瓶颈还在批处理与 KV cache 的分配上,继续在 llama.cpp 里调参更划算。任何一组对比都建议重复两到三次,并记下当时的进程显存占用,避免把一次波动当成稳定结论。