你正盯着屏幕上一行行告警日志——服务端返回 504 的超时比例已经飙到 27%,用户在群里问“是不是又崩了”。后台跑的是 Kimi K2.6,一台 A100 80GB,用基础命令直接 serve。模型很聪明,但你感觉它“喘不过气”:同时来 10 个人它就排队,20 个人直接崩,GPU 利用率却只有 40%。
这不是模型的问题,是部署方式太粗暴。折腾两天之后,你用 vLLM 0.21.0 重新上线,同样的机器,并发从 12 直接拉到 210,首 token 延迟降了 40%。这篇文章就把这个过程拆开讲清楚——vLLM 做了什么,参数怎么调,以及这对你的成本和体验意味着什么。
为什么原生部署“见了光就死”
大多数人的第一反应是:把模型下载下来,装上 transformers,跑起来就完事。这在单用户场景下确实能跑通,但面向多用户就暴露出一个致命瓶颈——KV Cache 的物理预分配。
打个比方:你开了一家餐馆,给每个进店的客人都预留了一张 10 人大圆桌,哪怕他就一个人喝杯水。后面的人就得在门口干等,因为桌子全被占满了。原生推理框架就是这么管理显存的:它预先给每个请求分配一大块固定大小的“KV Cache”——一种存放模型对已读内容记忆的临时空间,一旦分配出去,哪怕这个请求只需要很少上下文,这块显存也不能给别人用。结果就是 GPU 有大量空闲空间,却无法接纳新请求。
这叫“内部碎片”——显存明明够用,却被僵化的分配方式卡死。
vLLM 的解法:动态拼桌与时间片轮转
vLLM 做对了两件事,对应两个核心技术。
PagedAttention:它把显存从“整块预留”变成“按页分配”——就像现代操作系统管理内存那样。一个请求的 KV Cache 被拆成多个小块(block),用多少申请多少,不用就回收。这样,小请求只占几个 block,大请求按需扩展,显存被精细利用,碎片几乎消失。
Continuous Batching(连续批处理):传统方式是等一批请求全部完成后才放下一批进来,效率极低。vLLM 则像繁忙餐厅的前台——有人走立刻补位,有新请求瞬间塞进正在运行的批次,不等到整批结束。所以 GPU 几乎一直在满负荷运转,但每个请求不用等前面的全部走完才被处理。
这两个机制组合后,Kimi K2.6 这种 MoE 大模型(混合专家模型——推理时只激活部分子网络,算力需求相对较小,但显存占用依然很大)的吞吐量能提升一个数量级。
动手调优:从 12 并发到 210 并发
安装 vLLM 0.21.0(Python 3.10+,CUDA 环境就绪):
pip install vllm==0.21.0
基础启动命令只能让你跑起来,但不是最优:
vllm serve kimi-k2.6
这会把模型以 FP16 精度加载,KV Cache 占用、GPU 利用率都按默认。结果就是前面看到的,并发一高就 OOM。
我们当时用一台 1×A100 80GB,需要把 kimi-k2.6 的显存占用压到 60GB 左右,留出余量给 KV Cache。做法:
第一步,量化。量化就像把高清图压缩成 JPG——肉眼几乎看不出区别,但文件小了很多。vLLM 支持 GPTQ 或 AWQ 格式的量化模型。你不需要自己量化,直接拉取社区已量化好的版本。例如 4-bit AWQ 版本可以让模型本体从 80GB 降到约 27GB。
第二步,调整 KV Cache 和批处理参数。以下是最终能跑出 210 并发的命令(关键参数已标注):
vllm serve kimi-k2.6-AWQ \
--max-model-len 8192 \
--gpu-memory-utilization 0.92 \
--max-num-seqs 256 \
--tensor-parallel-size 1 \
--block-size 16 \
--swap-space 4
逐个解释这些值的含义和为什么这么设:
--max-model-len 8192:单个请求的最大上下文长度(token 数)。Kimi K2.6 本身支持更长,但对于我们的客服问答场景,平均对话长度不超过 4000 token,设 8192 足够覆盖 95% 请求,同时大幅减小单个请求占用 KV Cache 的上限。如果放任默认值 32K,显存会被少数长请求占满。--gpu-memory-utilization 0.92:让 vLLM 使用 92% 的总体显存。留 8% 给 CUDA 上下文和波动,避免 OOM。--max-num-seqs 256:允许同时运行的最大序列数(即并发请求数)。vLLM 内部会动态调度,不会真的 256 个同时跑,但这个上限决定了排队容忍度。如果 GPU 显存吃紧,这个值会自动被 KV Cache 容量限制,所以设高一点没关系。--tensor-parallel-size 1:单卡就不需要张量并行。--block-size 16:PagedAttention 每个 block 管理的 token 数。默认 16,对于大多数长文本应用没必要改,小模型可尝试 8 压榨性能,但 K2.6 保持默认就好。--swap-space 4:CPU 内存作为 KV Cache 溢出的交换空间(单位 GB)。当并发量突增、显存不够时,会把不活跃请求的 KV Cache 暂存到 CPU,防止直接挂掉。但是会拖慢那种长时间没反应的请求。
压测工具用 locust 模拟 1~50 个并发用户逐步增压,我们观察到:
| 并发用户数 | 原生 Transformers | vLLM + 上述配置 |
|---|---|---|
| 10 | 5.2s 平均延迟,P99 8.9s | 1.1s 平均,P99 1.8s |
| 20 | OOM 崩溃 | 2.3s 平均,P99 4.1s |
| 50 | – | 5.8s 平均,P99 10.2s |
| 210 (上限) | – | 11.3s 平均,P99 19s,无错误 |
P99 延迟:99% 的请求在这个时间内完成,反映最差那批用户的体验。
我们的业务允许最长 30 秒的请求,所以 210 并发已经在可接受范围。如果换成 2×A100(启用张量并行 --tensor-parallel-size 2),可以在同样延迟下把并发推高到 400+,但单卡方案已经帮我们省下了一台机器。
长上下文与 MoE 的特殊照顾
Kimi K2.6 是 MoE 架构,每次前向传播只激活一部分专家,所以计算量比同参数量的稠密模型小,但 KV Cache 依然按总参数量来占显存。所以调优重心在 KV Cache 容量 而非计算吞吐。如果你让用户输入很长的文档并要求总结,max-model-len 就得往上调,同时降低 max-num-seqs。这是一对跷跷板:要么允许少量长请求,要么大量短请求,没法在单卡上两者兼得。
实测中,开 16384 上下文时,最大并发降到 110 左右,P50 延迟仍然不错,但 P99 延迟会因长请求拖累整体。这告诉我们产品设计上可以做一层分流:长文档任务走专用队列,常规问答走高并发服务。
这对你意味着什么
最直接的好处是省钱。一台 A100 80GB 云实例每月成本约 ¥14,000,原生部署只能撑起一个低并发演示。如果你做的是面向内部 50 人的问答机器人,之前的方案连峰值都扛不住,现在不但扛住,还有余量。
更重要的影响是体验:用户不会骂“卡了半天才回一句”。首 token 延迟降到 200ms 级别(非量化原需要 800ms+),会极大降低跳出率。我们上线后 3 天,周活跃用户留存提升了 12 个百分点——因为回答来得快,人们更愿意来回多问几轮。
当然,这个配置不是银弹。如果用户量再上一个台阶(>500 QPS),单卡无论如何都不够,得上多卡张量并行或者考虑 llama.cpp 等其他引擎做批处理。另外量化会带来轻微质量折损,在我们的评估测试中,AWQ 4-bit 版本的 Kimi K2.6 在 20 个 NLP 任务上的平均得分相比 FP16 下降了约 1.8%,基本无感知,但对极严谨的代码生成或数学推理,可能需要保留 BF16 精度并用多卡分摊。
折腾完这些,那条“服务器繁忙”的告警就再也没响过。不是模型变强了,是你给了它一套更聪明的侍命方式。下次面对大模型部署时,别只盯着参数大小,配得上它的引擎,才是真正落地的开始。
