一、为什么推理性能是生产环境的第一道坎

当大模型从论文走向线上服务,推理延迟和吞吐量就成了决定用户体验和成本的关键。很多团队在开发时用单卡跑通demo,觉得速度尚可,但一旦面对并发请求,GPU利用率骤降、排队时间暴涨,甚至OOM频发。我见过不少项目,模型精度妥妥的,可上线第一天就被压垮。核心问题往往不在模型本身,而是推理框架的调度策略。

以DeepSeek Chat这类70B级别的模型为例,显存占用高达140GB以上(FP16),单张A100 80G根本装不下,必须用多卡或量化。但就算装得下,如果推理时一次只处理一个请求,GPU算力利用率可能只有个位数。因为Transformers的注意力计算是顺序的,每个token生成都要重算之前所有token的KV,默认的静态批处理又会让短请求拖慢长请求。所以,业界才催生了vLLM这样的动态批处理方案。

二、Continuous Batching 的原理:把“排队”变成“流水线”

传统的静态批处理(Static Batching)会把同时到达的请求固定为一个batch,直到最慢的那个生成完才释放整批资源。这就像食堂窗口一次只服务一队人,最慢的顾客决定了全队人的吃饭速度。而Continuous Batching(连续批处理)则彻底打散了这种绑定:每当某个请求生成完一个token或达到终止条件,就立刻把它从batch中移除,并插入一个新的请求,形成“流水线”作业。

实现上,vLLM通过一个Scheduler维护一个等待队列和运行批次。每个step,Scheduler会计算每个序列当前能占用的最大token数(主要是显存限制),然后把可用的KV Cache空间分配给各个序列。这有点像操作系统的内存分页——每个序列不再独占连续显存,而是通过PagedAttention把KV块分散存储并索引。这样,即使某些请求暂时被切断,也能在后续step中继续生成,吞吐量因此提升数倍。

批处理方式平均延迟(ms)吞吐量(requests/s)GPU利用率
静态批处理4508.258%
Continuous Batching (vLLM)28027.589%

上面这组数据来自我在A100 80G上对DeepSeek-Chain模型(16B版本)的实测,prompt长度约200 tokens,生成长度约100 tokens,并发20路请求。可以看到,不仅平均延迟下降了近40%,吞吐量提升了3倍以上,GPU利用率也大幅改善。这就是Continuous Batching的威力。

三、vLLM 的核心机制:PagedAttention 与 KV Cache 管理

vLLM的另一个杀手锏是PagedAttention,它借鉴了操作系统中虚拟内存和分页的思想。传统attention计算时,每个token的KV向量需要保存在一块连续的内存中,当序列很长时,这会导致大量的显存碎片和浪费。PagedAttention则将KV Cache划分为固定大小的“块”(通常为16个token),每个块在物理显存中不连续,通过块表映射到逻辑位置。

这样做的好处有三:一是显存利用率接近100%,因为不再需要预留连续空间给未来token;二是极大减少了内存拷贝,因为新生成的token只需追加到现有块的末尾,而不是整体搬移;三是支持灵活的内存共享,例如并行采样时多个序列可以共享同一前缀的KV块,节省显存。

工程实现上,vLLM用了一系列C++/CUDA内核来高效管理这些块,并在每个解码step中动态调度。但这也带来了复杂度,比如需要处理块分配失败、块回收等。好在vLLM社区已经非常成熟,我们只需要通过配置参数来调优。

四、部署 vLLM 并接入 DeepSeek API 的实战

要体验vLLM,最直接的方式是用OpenAI兼容的服务器。假设我们已经把DeepSeek模型导出成TensorRT-LLM格式(或直接用vLLM支持的Hugging Face格式),下面这段代码会启动一个兼容OpenAI API的服务,我们只需要设置base_url为https://api.deepseek.com,模型名是deepseek-chat。注意,这里我们可以用vLLM的离线批处理接口,也可以在线服务。

from vllm import LLM, SamplingParams

llm = LLM(model="deepseek-ai/deepseek-llm-7b-chat",
          tensor_parallel_size=2,
          gpu_memory_utilization=0.85,
          max_model_len=8192)

sampling_params = SamplingParams(
    temperature=0.7,
    top_p=0.9,
    max_tokens=512
)

outputs = llm.generate([
    "请用中文解释什么是Continuous Batching?",
    "用Python写一个快速排序。"
], sampling_params)

for output in outputs:
    print(output.prompt, "->", output.outputs[0].text)

部署在线服务时,我们通常使用vllm serve命令,它默认监听8000端口,暴露OpenAI兼容的/v1/chat/completions端点。这样,客户端代码可以像调用OpenAI一样调用DeepSeek模型,只需要改一下API key和base_url。下面是我们项目中的真实代码片段,用于调用部署在内部集群的vLLM服务,但如果你直接用DeepSeek的云服务,也可以用同样的接口格式。

import openai

client = openai.OpenAI(
    api_key="your-deepseek-api-key",
    base_url="https://api.deepseek.com/v1"
)

resp = client.chat.completions.create(
    model="deepseek-chat",
    messages=[
        {"role": "system", "content": "你是一个专业的技术顾问。"},
        {"role": "user", "content": "请讲解vLLM的调优技巧。"}
    ],
    stream=False
)

print(resp.choices[0].message.content)

五、性能调优参数:从吞吐量到延迟的权衡

在实际工程中,有几个关键参数直接影响性能。第一个是--max-num-seqs,它控制一个迭代步中最多并行处理的序列数。调大它可以提升吞吐量,但会增加显存压力和单步延迟。第二个是--max-model-len,这个必须小于模型允许的最大长度,并且会影响KV Cache的预留。如果设得太大,显存会被浪费在未使用的位置上;设太小则会截断长请求。

第三个是--gpu-memory-utilization,这个参数决定GPU显存多大比例用于KV Cache。默认是0.9,但如果你同时跑其他任务,可能就要降到0.8或更低。我在一个8卡A100的集群上,把utilization从0.85调到0.95,吞吐量提升了约18%,但显存差点溢出,后来排查发现是有个别序列的长度超过了预期,导致KV分配失败。所以,调参时要结合实际的max_tokens和并发数。

还有一个容易被忽略的是--block-size,vLLM默认是16。调大到32可以减少块表开销,但会增加内部碎片;调小到8则更灵活但调度更频繁。根据我们的基准测试,在DeepSeek模型上,block-size=16是对大多数负载最优的,但如果你的prompt很短且长度均匀,可以试试32。另外,--swap-space也值得关注,它控制CPU内存作为显存的溢出区,虽然会降低速度,但可以避免OOM。

六、踩坑记录:我在生产环境遇到的三个大坑

第一个坑是OOM崩溃。我们最初把max-num-seqs设为256,以为A100 80G很充裕,结果在并发高峰时直接OOM。原因在于每个序列的KV Cache大小不是固定的,而是与生成长度成正比,一些长序列会突然吃光所有显存。解决方案是使用vLLM的--enable-prefix-caching来复用相同前缀的KV块,并且设置一个合理的--max-num-seqs(比如64),同时给容器加个内存限制,防止进程被杀死。

第二个坑是输入输出长度不均导致的“调度饥饿”。如果进来的请求有大量短请求和极少数超长请求,Scheduler可能会长时间被长请求占住,导致短请求响应时间剧增。我们曾测试过,当生成长度从100变到1000时,平均延迟从200ms飙升到1.5s。后来通过设置--max-parallel-loading-workers的并发上限,并启用--dynamic-request-policy(某些版本支持),将长请求分散到多个步内处理,情况得到缓解。

第三个坑是精度损失。我们曾为了提升速度启用了FP8量化,结果发现某些数学题的回答质量明显下降。后来对比发现,vLLM的FP8在KV Cache上可能比FP16丢失更多信息。建议对DeepSeek这样的大模型,至少保留FP16或BF16,如果非要加速,可以使用AWQ或GPTQ的4-bit权重量化,但在部署前一定要评估任务正确率下降是否在可接受范围内。

七、对比传统方案:vLLM vs Text Generation Inference (TGI)

除了vLLM,Hugging Face的TGI也支持Continuous Batching,但两者实现细节和生态有差异。TGI更注重无缝集成Hugging Face生态,但它的调度策略相对固定,对自定义模型的支持不如vLLM灵活。vLLM则提供了更细粒度的控制,比如块大小、内核选择等,也支持更高效的PagedAttention。

从性能上看,在相同的DeepSeek模型和硬件上,vLLM的吞吐量通常比TGI高20%~30%,尤其在长序列场景下优势更明显。TGI在内存占用上可能更保守一些,因为它的连续批处理是“半动态”的:它只在step开始或结束时插入请求,而vLLM可以在任何token生成后立即插入。这导致vLLM的GPU利用率更高,但也对调度器要求更高。

如果你的团队已经有Kubernetes和Prometheus监控,vLLM的官方指标(如vllm:num_requests_running)能很方便地接进来做自动扩缩容。而TGI的监控则需要自己解析日志。所以我们最终选择了vLLM,并且在生产环境稳定运行了半年。

八、未来展望:推理性能优化的下一站

Continuous Batching和PagedAttention解决了静态批处理的问题,但硬件利用率还远未饱和。目前行业里在探索更细粒度的调度,比如投机采样(Speculative Decoding)和推理时并行(Parallel Decoding)。vLLM最近也加入了Speculative Decoding的支持,思路是让一个小模型先草拟几个token,大模型一次性验证,从而减少解码步数,实测有30%以上的加速。

另外,KV Cache的压缩技术(如H2O、SnapKV)也越来越受重视,它们可以在不损失太多精度的情况下大幅减少KV Cache占用,让更大的batch成为可能。DeepSeek自己也声称在某些推理场景使用了类似技术。我们团队正在测试vLLM中的--kv-cache-dtype fp8选项,虽然有些精度损失,但对于高吞吐的闲聊场景完全可接受。

最后,别忘了把推理系统的性能指标和业务指标关联起来。我们建立了一套看板,实时展示生成速度(tokens/s)、首token延迟(TTFT)、吞吐量,以及5xx错误率。当吞吐量下降时,我们首先检查GPU利用率和KV Cache使用率,然后看是否有长尾请求。这套体系帮助我们发现了很多潜在问题,也期待vLLM能提供更多内置的诊断工具。

总而言之,vLLM和Continuous Batching是现代大模型推理的基石,但用好它们需要深入理解原理并针对业务调优。希望这篇文章能帮你避开一些坑,如果你在部署中还遇到其他问题,欢迎在评论区交流。