RTX Pro 5000 72G 换不换 SGLang,不是看引擎名字听起来更快,而是看同一段 prompt 在两套引擎上跑出来的首 token 延迟和稳态输出 token/s 有没有真实差距。72G 显存意味着大多数模型不需要把层卸载到 CPU,llama.cpp 在单卡上通常也不会被显存卡住,所以「感觉慢」更可能来自量化格式、上下文长度、并发方式或者把权重加载时间误算进了生成时间。先把 llma.cpp 的基线固定下来,再用同样的采样参数在 SGLang 上跑一遍,两组数据对得上再谈长期切换。
值得换的前提是同条件对比后差距依然存在:先用 llama.cpp 固定 prompt 长度、输出长度和采样参数跑一遍并保存日志,再用相同参数起 SGLang 服务跑同一段 prompt。若差距主要来自量化格式不同、上下文长度不一致或 Radix 缓存命中,那不是引擎差异,切换没有意义;若对齐后 SGLang 在首 token 延迟或并发吞吐上仍有稳定优势,且回退路径已经准备好,再考虑长期使用。
确认当前 llama.cpp 慢在哪个环节
llama.cpp 的「慢」至少有两种:一种是启动时加载权重慢,另一种是每次请求生成慢。很多人看到第一次请求等很久,就认定生成慢,其实那可能是文件读取和显存搬运。区分方法是看日志时间线和显存曲线,而不是凭感觉。
启动日志里通常会看到类似 load_tensors: offloaded 65/65 layers to GPU、llama_model_load 相关的行,出现在服务开始监听之前;这类耗时只发生一次,不随请求次数增加。真正的生成耗时体现在每个请求结束后的 llama_perf_context_print,里面会分 prompt eval time(处理输入)和 eval time(逐 token 输出),各自带 token/s。如果 prompt eval 占比很高而 eval 的 token/s 正常,问题在输入长度或 KV cache 分配;如果两者都低,先查层是否真的全在 GPU、有没有别的进程占卡。
显存采样可以配合日志一起看:
nvidia-smi `--query-gpu`=memory.used,utilization.gpu,power.draw `--format`=csv -l 1
nvidia-smi dmon -s mu -d 1
加载阶段显存是阶梯式爬升后稳定;生成阶段显存基本平坦,利用率随 batch 波动。如果生成时利用率一直很低、显存也没吃满,通常是参数没调好或请求串行阻塞,而不是 llama.cpp 本身算不动。-ngl 没给够、-c 上下文开得远大于实际需要,都会让本该一次算完的内容变慢。
用同一段 prompt 建立可比基线
基线的作用是让第二次测量有参照。建议准备一段固定长度的中文 prompt,长度接近你真实业务里的典型输入,先不要随机构造。启动 llama.cpp 服务:
llama-server -m /models/your-model-Q4_K_M.gguf \
-ngl 99 -c 8192 -b 2048 `--host` 0.0.0.0 `--port` 8080
固定三项参数:输入 prompt 内容与长度、输出长度上限(max_tokens)、采样参数(temperature、top_p、top_k、seed)。为了可复现,建议 temperature=0 并固定 seed。发一次请求:
curl -s http://127.0.0.1:8080/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{"model":"local","messages":[{"role":"user","content":"<固定 prompt>"}],"max_tokens":512,"temperature":0,"top_p":1,"seed":1234,"stream":false}'
返回体里的 timings 字段会给出 prompt_ms、prompt_per_second、predicted_ms、predicted_per_second,这些就是你要记的每秒输出 token 和首 token 相关耗时。首 token 延迟如果接口没直接给,就在服务端日志里看 prompt eval 开始到第一个 token 输出之间的时间,或者用 stream:true 在客户端记录首个数据块到达的时刻。同一个请求连跑两遍,第一遍当预热丢弃,第二遍记数。
把模型放到 SGLang 上跑一次
SGLang 需要 HuggingFace 格式的权重目录,和 llama.cpp 用的 GGUF 不是同一种文件。这一点先确认,否则后面比的不是引擎而是量化精度。启动骨架:
python -m sglang.launch_server \
`--model-path` /models/your-model \
`--port` 30000 \
`--tp` 1 \
`--context-length` 8192 \
`--mem-fraction-static` 0.85 \
`--max-running-requests` 8
`--tp`:张量并行份数,单卡就是 1;加卡才动这个值。`--mem-fraction-static`:预留给权重加 KV cache 的显存比例。数值越高,能并发的请求越多,但余量越小,容易在长上下文时 OOM。`--context-length`:要和 llama.cpp 的-c对齐,否则前缀处理量不同。`--max-running-requests`:同时处理的请求上限,直接影响并发场景下的表现。
服务起来后用同一段 prompt 发请求,字段与前面保持一致:
curl -s http://127.0.0.1:30000/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{"model":"your-model","messages":[{"role":"user","content":"<固定 prompt>"}],"max_tokens":512,"temperature":0,"top_p":1,"seed":1234,"stream":false}'
和 llama.cpp 一样跑两遍:第一遍预热,第二遍记录。SGLang 的耗时可以看服务端日志里的 prefill、decode 分段统计,也可以从返回体的 usage 拿到输出 token 数再配合客户端计时。注意别用第一次请求的结果,那时刚加载完权重,数据不代表稳态。
核对两边输出是否一致
速度对比成立的前提是两边在测同一件事。逐项对齐:上下文长度(llama.cpp 的 -c 对 `--context-length`)、采样参数(temperature、top_p、top_k、seed、重复惩罚)、输出长度上限、chat template 与 system 提示、以及量化格式。量化格式不一致是最常见的问题——GGUF 的 Q4_K_M 和 bf16 原版权重本来就不是一个精度,速度差多少都不能算引擎的功劳。
还有一个容易被忽略的点:SGLang 默认启用基于前缀复用的 Radix 缓存。如果你连着跑两次完全相同的 prompt,第二次的首 token 会明显变快,因为前缀已经被缓存。llama.cpp 服务端也有类似的 prompt cache,行为同样受配置影响。所以要么两次对比都换一段等价但不相同的前缀,要么显式清掉缓存再跑,否则测到的是缓存命中率而不是引擎性能。
如果发现两边输出不一致,先别急着下结论:把相关参数改成完全相同的值重跑;重跑后仍不一致,再检查是否 chat template 不同、是否有一边悄悄改写了 prompt。日志里打印实际生效的上下文长度和采样参数,是确认有没有「配置漂移」的稳妥做法。
按数据决定要不要长期用 SGLang
比较维度建议固定成几条,逐条对照而不是只看一个数:单请求首 token 延迟、稳态输出 token/s、并发下的总吞吐、显存峰值与余量、冷启动时间、以及出问题时排查的便利程度。单条长 prompt 测的是延迟,多条并发测的是吞吐,两者结论经常不一样,要按你实际的使用形态选权重。
切换的前置条件可以先列成清单:两边输出在相同参数下一致;并发拉高时不出现 OOM 或明显的请求排队;显存峰值后仍保留合理余量;监控能覆盖服务存活和请求延迟。任何一条没过,就先维持 llama.cpp,把差异记下来再看。
回退记录要提前留好,包括 llama.cpp 的完整启动命令、模型文件路径与量化版本、服务端口、以及当时的采样参数。SGLang 换回来时只需改客户端 base_url 和模型名,前提是这两套配置都写在版本可控的脚本里,而不是记在某个终端的历史记录里。等两边数据在同一条件下反复对得上,再决定长期用哪套。