ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

大模型私有化交付:显存估算与压测的三个致命陷阱

大模型私有化交付:显存估算与压测的三个致命陷阱

项目地址

本文配套的完整交付手册《大模型私有化交付操作手册》已开源,涵盖 8 章正文 + 3 篇附录 + 3 张交付模板,覆盖 vLLM / SGLang / TGI / TensorRT-LLM / lmdeploy / ollama 等主流推理引擎。

项目地址:https://gitee.com/wang-guangshuooo/llm-private-deploy-handbook

TL;DR(太长不看版)

  • 显存估算必须包含KV Cache,否则并发能力错 8 倍。
  • TTFT统一口径(从发送请求算),推理模型单独统计思考段。
  • 容量规划看饱和点与尾延迟,不能只看吞吐均值。

先说结论

私有化交付大模型,翻车最多的不是"装不上",而是"算错了"。装不上会报错,你能立刻发现。而算错了不报错——它安静地给你一个错误的数字,然后你在方案里写下这个数字,客户按它配了机器,验收那天才发现不对。

下面三个陷阱,是我认为代价最高的三个,共同特点是:代码不会报错,错误要到很晚才暴露。

陷阱症状代价
一显存估算只算权重,忽略 KV Cache,容量高估数倍买错卡 / 并发只有预估的 1/5
二TTFT 口径不统一,同一个词两个人量出差十倍验收现场扯皮
三只看吞吐均值,漏掉饱和点与尾延迟容量规划拍脑袋

陷阱一:显存估算只算权重,KV Cache 被漏掉

这是交付翻车的第一大原因。很多人算显存是这么算的:72B FP16 → 72.7 × 2 = 145.4 GB → 8 张 80G 卡够,收工。

问题在于,显存是三项之和,不是一项:

总显存需求 = 权重显存 + KV Cache 显存 + 激活/框架开销

权重只是"装得下"的门槛,KV Cache 才是决定"能服务多少并发"的那一项。而并发能力才是客户真正要买的东西。

1.1 权重显存:一行乘法,但别用约数

权重显存 = 参数量 × 每参数字节数

精度每参数字节说明
FP16 / BF162.0推理默认
FP81.0需 H100/L40S 等支持
INT8 (W8A8)1.0 + 少量 scale精度损失小
INT4 (AWQ/GPTQ)≈0.55含 scale/zero point,实际约 0.5~0.6

常用结果速查(GB):

模型FP16INT8INT4
7B(7.6B)15.27.64.2
14B(14.7B)29.414.78.1
32B(32.5B)65.032.517.9
72B(72.7B)145.472.740.0

注意用真实参数量(7.6B / 72.7B),不要用"7B / 72B"这种约数。以模型目录下 config.json 的字段为准,或者直接量 safetensors 文件总大小。

1.2 KV Cache:最容易被漏掉的一项

单 token KV 字节 = 2 × 层数 × KV 头数 × head_dim × 字节数(2 = Key 和 Value 各一份)

各模型单 token KV 占用(FP16):

模型层数KV 头数head_dim单 token8K 上下文
Qwen2.5-7B28412856 KiB448 MiB
Qwen2.5-14B488128192 KiB1.5 GiB
Qwen2.5-32B648128256 KiB2.0 GiB
Qwen2.5-72B808128320 KiB2.5 GiB

这里就是那个坑:GQA 的 8:1 分组。Qwen2.5-72B 有 64 个注意力头,但只有 8 个 KV 头(分组查询注意力,8:1)。如果你按 64 个头去算 KV:

2 × 80 层 × 64 头 × 128 × 2 字节 = 2.5 MiB / token ← 错,是实际值的 8 倍

2 × 80 层 × 8 头 × 128 × 2 字节 = 320 KiB / token ← 对

一个字段看错,单 token KV 就错 8 倍,最终并发能力估算也跟着错 8 倍——往哪个方向错取决于你是保守还是激进,但两种都是错的。

✅ 算之前先打开 config.json,确认 num_key_value_heads。这个字段名和 num_attention_heads 长得很像,但它们不是一回事。

1.3 三个完整算例,把并发算出来

算例 1:Qwen2.5-7B FP16,单卡 A100 40G,8K 上下文

项计算结果
权重7.6B × 215.2 GB
可用显存40 GB × 0.90(gpu_memory_utilization)36.0 GB
减激活/框架取 1.5 GB34.5 GB
KV Cache 池36.0 − 15.2 − 1.519.3 GB
单序列 8K 占用56 KiB × 8192448 MiB
可支持并发19.3 × 1024 / 448≈ 44 路

算例 2:Qwen2.5-72B FP16,8 × A100 80G,32K 上下文

项计算结果
权重72.7B × 2145.4 GB
可用显存640 GB × 0.90576 GB
减激活/框架/通信4 GB × 832 GB
KV Cache 池576 − 145.4 − 32398.6 GB
单序列 32K 占用320 KiB × 3276810 GiB
可支持并发398.6 / 10≈ 39 路

8 张 80G 卡,只能同时服务约 39 路 32K 上下文请求。这个数字往往低于客户的预期——很多方案只报"能跑起来",但并发能力被 KV Cache 死死限制住。

算例 3:Qwen2.5-72B AWQ INT4,8 × A100 40G,8K 上下文

项计算结果
权重72.7B × 0.5540.0 GB
可用显存320 GB × 0.90288 GB
减激活/框架3 GB × 824 GB
KV Cache 池288 − 40 − 24224 GB
单序列 8K 占用320 KiB × 81922.5 GiB
可支持并发224 / 2.5≈ 89 路

量化把并发从"约 39 路(32K)"提到"约 89 路(8K)",代价是权重从 145 GB 降到 40 GB,并接受量化精度损失。这就是"精度 vs 容量"的量化决策依据。

1.4 估完之后,一定要反向校验

方案定稿后,实际跑起来用命令核对:

# vLLM 启动日志里会打印: # "GPU KV cache size: XXX tokens" # "Maximum concurrency for XXX tokens per request: Y.Yx" 实时观察显存 nvidia-smi --query-gpu=index,memory.used,memory.total,utilization.gpu --format=csv -l 5 通过 Prometheus 看 KV Cache 使用率 curl -s localhost:8000/metrics | grep gpu_cache_usage_perc

一条实用判据:正常负载下 gpu_cache_usage_perc 应在 20% ~ 70% 之间。

  • 长期 > 90% → 并发接近上限,请求开始排队,TTFT 会陡增
  • 长期 < 10% → 显存浪费,可以调大 --max-num-seqs 或减少卡数

陷阱二:TTFT 这个词,两个人能量出十倍差

TTFT(Time To First Token,首包延迟)是交付现场唯一有意义的体感指标——用户不关心"整个回答花了几秒",他只在意"多久开始出字"。但这个词谁都在用,定义却不一样。同一个服务、同一次压测,两个人给出的 TTFT 可以差十倍。交付现场 90% 的"性能争议",本质是双方对 TTFT 的定义不一样。

有三个口径陷阱:

陷阱 2.1:从哪一刻开始算?

口径含义什么时候用
从发送请求算含 HTTP 建连 + TLS 握手 + 排队 + 预填充✅ 推荐,最接近用户真实感受
从收到 HTTP 200 算不含建连,含排队 + 预填充排除网络噪声,对比引擎性能
从预填充开始算只含预填充计算纯引擎能力对比,不是用户体验

交付压测用第一种,但必须在报告里写明是哪种,并注明"客户端与服务端同机 / 同机房",否则网络 RTT 会被算进去。

顺便说,用流式请求时常见的写法是:

resp = client.chat.completions.create(..., stream=True) ttft = time.perf_counter() - start # 错:这是 HTTP 200 的时刻 for chunk in resp: # 真正的首 token 在这里才到 ...

HTTP 200 返回的时候,模型一个字都还没出。

陷阱 2.2:排队时间算不算?

必须算。用户按下回车就开始等了,服务在排队,这段时间他是真实感受到了的。这正是"并发一高 TTFT 就爆炸"的原因——不是模型变慢了,而是请求在队列里等。这恰恰是压测最重要的发现之一,把它排除掉等于把最关键的结论藏起来。

陷阱 2.3:推理模型的"思考段"算不算?(最隐蔽)

像 DeepSeek-R1、QwQ 这类推理模型,会先输出一大段思考内容。在 OpenAI 兼容的 SSE 流式接口里,这段内容往往走不同的字段(如 delta.reasoning_content),而不是 delta.content。

实测逐帧解析的结果:

帧 1 ~ 帧 128 : delta 里只有 reasoning_content(思考段) 帧 129 : 第一个 delta.content 出现 ← 真正的 TTFT 应该在这里 帧 129 ~ 帧 211 : 正常输出内容
  • 把第一个任意分片当 TTFT → 首帧在第 1 帧就来了,TTFT 被严重低估
  • 把 reasoning 也算作输出 token → TPOT 被摊薄,虚低
  • 取 content → TTFT 里包含了几十秒思考,数字大得没法跟普通模型对比

三条路都不对,因为这个指标对推理模型本身就不成立。

✅ 正确做法:

  1. TTFT 定义为第一个 content 字段出现的时刻,思考段单独统计为"思考耗时",或一并计入端到端但明确标注;
  2. 报告里必须写清目标模型是推理模型还是普通模型;
  3. 做配置对比时不要混用推理模型和普通模型,指标不可比。

结论:推理模型不适合作为 TTFT 口径对比的基准模型。做压测前先解析一次 SSE 帧结构,确认字段名。

陷阱三:只看吞吐均值,不看饱和点和尾延迟

压测做完,报一个"吞吐 XXX tok/s",这几乎等于没报。因为单看吞吐,你无法回答客户唯一关心的问题:我能服务多少人?

3.1 先看一组真机数据

真机实测三条曲线。环境标注(很重要):RTX 4060 Laptop 8.0 GiB | Windows 11 | ollama | qwen2.5:7b Q4_K_M | 并发 1/2/4/8/16/32 | 每请求 256 tokens | 预热 2 次 | 温度 0 | 流式 | 63 个请求、0 失败、总耗时 222.9 s。

并发吞吐 (tok/s)加速比TTFT 均值 (ms)TTFT P99 (ms)TPOT (ms)端到端 P99 (ms)
126.91.00×168.81168.8121.642159.52
227.31.02×1335.322436.1723.934607.57
430.41.13×3636.496869.4723.448871.39
830.81.14×7804.7815156.9222.9617178.87
1630.01.11×15979.1230537.7921.5632414.35
3229.31.09×30839.1060821.1421.6962798.84

显存峰值 5.6 GiB / 8.0 GiB(70.3%),平均 GPU 利用率 71.9%,63 请求 0 失败。

从这组数据可以读出三个关键信息:

第一,饱和点出现在并发 2 附近。并发从 1 提到 2,吞吐从 26.9 微升到 27.3 tok/s,加速比只有 1.02×;之后并发 4/8/16/32 一路走平甚至回落,到并发 32 时吞吐降到 29.3,加速比只剩 1.09×。也就是说,这张 8G 显存的笔记本卡,在并发 2 时吞吐就已经到顶,继续加并发不仅没有收益,还会因为排队和调度开销让整体吞吐下滑。饱和点就是 2。

第二,尾延迟在并发 2 之后开始失控。TTFT 均值从并发 1 的 168.81 ms 一路涨到并发 32 的 30839.10 ms,涨了约 182 倍;TTFT P99 在并发 2 时已经到 2436.17 ms,并发 8 时冲到 15156.92 ms,并发 32 时更是到 60821.14 ms。TPOT 全程稳定在 21.56~23.93 ms 之间,基本不动,说明单 token 生成速度并没有变慢——瓶颈不在算力,而在调度。尾延迟的恶化远早于吞吐见顶,这才是容量规划真正要盯的指标。

第三,据此确定最大并发数。如果只看吞吐,并发 2 之后基本没有收益;结合尾延迟,并发 2 时 TTFT P99 已经超过 2.4 秒,并发 4 时超过 6.8 秒,体感明显变差。所以这张卡的实际可用并发建议取 2,最多不超过 4——在 2 并发下吞吐仍有 27.3 tok/s,接近峰值,而 TTFT P99 还能压在 2.5 秒以内。容量规划的正确姿势是:先找吞吐饱和点,再往前退一档到尾延迟还能接受的位置,那个并发值才是能写进方案的数字。由于 TPOT 全程恒定,加算力(换更好的卡)无用,要换调度更好的引擎或加副本才能提升并发能力。

参考资料

以下 5 条官方文档与本文三个陷阱直接相关,建议在方案定稿前通读一遍:

  1. vLLM 官方文档(KV Cache 与 gpu_memory_utilization):Automatic Prefix Caching - vLLM。为什么值得看:本文 1.3 节所有算例里的 gpu_memory_utilization 和 KV Cache 池估算,都能在这里找到官方口径,避免凭经验拍脑袋。
  2. HuggingFace 关于 GQA(分组查询注意力)的技术博客:https://huggingface.co/blog/gqa。为什么值得看:GQA 的 8:1 分组是 1.2 节最容易算错 8 倍的地方,这篇博客把 KV 头与注意力头的关系讲清楚了,能帮你少踩一个字段看错的坑。
  3. OpenAI 关于流式响应与 SSE 字段定义的 API 文档:https://platform.openai.com/docs/api-reference/chat。为什么值得看:陷阱 2.1 和 2.3 都依赖对 SSE 流式帧结构的准确理解,这份文档是 OpenAI 兼容接口字段定义的权威来源。
  4. DeepSeek-R1 技术报告中关于 reasoning_content 输出的说明:https://github.com/deepseek-ai/DeepSeek-R1。为什么值得看:陷阱 2.3 里推理模型思考段与 content 字段的区分,在 DeepSeek-R1 官方仓库里有最直接的说明,是压测前解析 SSE 帧结构的必读材料。
  5. ollama 官方性能调优指南:https://github.com/ollama/ollama/blob/main/docs/faq.md。为什么值得看:本文 3.1 节真机压测用的是 ollama + qwen2.5:7b,这份 FAQ 覆盖了并发、上下文长度和 KV Cache 相关的常见调优问题,能帮你复现并验证压测结论。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进