把大模型从单机Demo推到生产环境,难度完全不是同一个量级。我见过不少团队在跑通一个7B模型的时候信心满满,结果一上服务就翻车:显存爆了、并发一高就超时、多卡通信直接卡死。这篇文章就围绕“大规模部署LLM”这件事,把我实际踩过的坑、测过的参数、最后沉淀下来的对策,完整梳理一遍。内容聚焦在推理服务层的部署与调优,适合正在做模型落地、或者准备把本地实验环境升级成正式服务的同学参考。
1. 部署需求拆解:先算清楚账,再动手
1.1 你的业务到底需要什么样的LLM服务
很多人在部署LLM之前只想着“把模型跑起来”,却忽略了“服务”和“跑起来”完全是两码事。生产环境需要明确的性能指标,否则后面做的所有调优都没有目标。
我建议在部署前先问自己三个问题:
- 业务方预期的并发量是多少?峰值QPS大概多少?
- 单次请求允许的最长响应时间是多少?
- 模型上下文窗口要设多大?是8K、32K还是更长?
这些问题直接决定了你选什么推理引擎、配置多少显存、用不用量化。举个例子,一个客服场景可能要求首token延迟在2秒以内,生成速度每秒不低于10个token,并发50路。而一个离线批量处理的场景,对延迟完全不敏感,但要求吞吐量越高越好。两者的部署策略截然不同:前者要调低并发、预留更多算力保证响应速度;后者可以尽量把batch推到极限。
我建议把SLA量化成几个核心指标再拆解:
- TTFT(Time to First Token):从请求发出到返回第一个token的耗时,反映服务响应的即时性。
- TPOT(Time Per Output Token):每个输出token的生成耗时,决定用户感受到的生成速度。
- 吞吐量(Tokens/s):单位时间内整体系统能生成的token数,决定服务能支撑多少并发。
这三个指标没办法同时全部拉满,互相对抗。TTFT需要推理引擎尽快拿到第一批输出,就要预填充阶段算得快,减少排队;但高吞吐量需要尽量把批量请求合在一起做连续批处理,这又会让先来的请求等后续请求凑批,拉高TTFT。记住这个矛盾,后面调参就有方向了。
1.2 量化方案与推理引擎选型
引擎选型是整个部署的基石。当前主流推理框架无外乎vLLM、TensorRT-LLM、SGLang这些,我最终用的是vLLM,原因是它的PagedAttention机制能大幅减少显存碎片,连续批处理(continuous batching)带来的吞吐提升非常明显,而且它做到了OpenAI接口兼容,接入业务代码几乎零成本,社区活跃度也最高,遇到问题容易找到解决方案。
- vLLM:生态好,部署快,吞吐高,支持模型广,最适合第一版上线快速落地。
- TensorRT-LLM:NVIDIA全家桶,推理性能极致,但模型编译和构建流程复杂,适合追求极限性能的团队。
- SGLang:新秀,RadixAttention做前缀缓存很强,适合长上下文或大量同前缀请求场景。
- Transformers原生:出一个Demo可以,生产环境不要碰,速度慢且缺乏并发管理。
谈到硬件成本,量化是一个绕不开的话题。我只推荐两种方案:AWQ和GPTQ(4bit)。他们在推理质量上损失很小,普遍在小语种、逻辑推理等场景下会有一点掉点,但显存占用能下降接近一半到三分之二。曾经有一个服务原来跑13B模型需要接近24GB显存,换成4bit量化后卡在16GB显卡上也能稳定支撑业务。
量化格式对比:
| 格式 | 精度 | 权重显存(以7B为例) | 推理速度 | 推荐场景 |
|---|---|---|---|---|
| FP16/BF16 | 16bit | 约14GB | 最快 | 有充足A100/H100,不差钱 |
| INT8 | 8bit | 约7GB | 较快 | 显存紧张但不愿降质量 |
| AWQ/GPTQ 4bit | 4bit | 约4GB | 中等 | 服务型场景,追求低成本部署 |
注意量化只是把权重压下来了,KV Cache和中间激活值仍然是全精度计算,所以显存估算别只按量化后的权重大小来算,要留出至少30%的冗余。
1.3 显存规划:估算公式与服务器选型
显存规划是大规模部署中最先要算清楚的一笔账,我见过太多人模型还没加载就因为显存估算失误导致OOM崩溃。别只盯着模型权重,显存消耗大头其实有三块:模型权重、KV Cache、激活值。
权重大小较好估算,一个7B模型FP16权重就是7B × 2字节 = 14GB,4bit量化则约3.5GB。KV Cache按上下文长度和并发数动态增长,公式大致为(单位字节):2 × 层数 × KV头数 × 头维度 × 序列长度 × batch数 × 精度字节数。以常见7B规模的32层模型为例,上下文4K,单并发时KV Cache约0.5GB左右,但这个数会随并发线性增长。激活值则取决于batch大小和序列长度,通常建议预留总显存的10%到20%。
我的选型建议很直接:单张24GB显卡可以支撑7B模型的量化部署,但并发只能控制在20到50路之间;如果要把13B模型跑FP16并获得较高并发,至少需要两张24GB卡做张量并行。服务器选型时优先考虑A800、H800这类大显存卡,不要只图便宜选16GB的卡然后被显存问题折磨。
2. 环境准备与基础依赖部署
2.1 CUDA/PyTorch/vLLM版本匹配
编程环境版本匹配是部署LLM中第一个劝退点,很多人在这上面浪费了一整天时间。从我的实战经验看,版本不匹配造成的“诡异问题”远比代码逻辑错误多得多,比如CUDA driver版本过低导致无法加载算子,或者PyTorch与vLLM版本相差过大导致引擎初始化失败。
我当时部署时采用的稳定组合是:CUDA 12.1 driver、PyTorch 2.1.x、vLLM 0.4.x。这个组合经历了生产环境多次验证,稳定性很好。如果要用更新的vLLM版本,建议同步升级PyTorch并检查驱动是否满足要求。
部署前检查环境的命令:
nvidia-smi # 查看驱动版本和显存 python -c "import torch; print(torch.__version__, torch.version.cuda)" python -c "import vllm; print(vllm.__version__)"版本匹配决定了很多隐性问题是否存在。torch和CUDA toolkit版本不匹配时,某些自定义算子会静默失败或直接抛错。目前社区有个常见的经验法则:vLLM对CUDA版本的要求比较严格,低版本CUDA驱动下很多算子无法编译,而高版本驱动反而容易导致显存分配行为变了。因此装完环境后第一件事不是直接跑模型,而是先跑一下官方预置的示例,确认GPU算子和通信库都工作正常再往下走。
2.2 模型文件下载与格式处理
模型下载是大规模部署中极容易忽略的环节,特别是国内服务器直接访问HuggingFace经常断流,导致模型文件下载到一半就中断,之后加载时随机报错特别难排查。
建议下载时使用huggingface-cli并开启断点续传,或者先用wget配合代理下载,再通过huggingface-cli导入到指定缓存目录。另一个更稳妥的办法是将模型文件通过SCP到服务器本地目录,然后直接用本地路径加载,完全不依赖HuggingFace服务。
模型文件格式也要注意。官方现在普遍默认用safetensors格式,它比旧式的bin格式更安全更快,加载时不需要反序列化Python pickle对象,安全性高很多。如果下载的模型同时包含两种格式,优先加载safetensors,vLLM会自动识别。还要记住不同格式的分片逻辑可能不一样,比如某些模型会把权重拆成多个文件,文件名中的数字序号跟文件大小必须完整,缺少任何一片模型都无法加载。下载完成后检查文件数量、大小是否与仓库描述一致,最好计算一下SHA256校验值。
2.3 容器部署与基础配置
生产环境我用容器部署。这里有一个新手容易踩的硬坑:/dev/shm 空间默认只有64MB,而vLLM在加载模型时会在共享内存中写入大量中间数据,空间不足会直接报错退出。启动容器时务必加上--shm-size=10g之类的参数。
容器启动参数示例:
docker run --gpus all \ --shm-size=10g \ -p 8000:8000 \ -v /data/models:/models \ -e HF_HOME=/models/cache \ your-registry/vllm-image:latest \ --model /models/llama-7b \ --served-model-name my-llm多卡场景下还要额外配置NCCL环境变量,比如NCCL_DEBUG=INFO用于排查通信问题,NCCL_IB_DISABLE=1可以在没有InfiniBand的时候强制走TCP,避免启动时卡在通信初始化。这些坑在文档里基本不会写,但实际部署时几乎每次都会遇到。
3. 核心服务启动与关键参数配置
3.1 启动vLLM服务:聚焦几个生死攸关的参数
vLLM虽然看起来是启动一个服务那么简单,但内部参数很多。实际生产环境中影响服务生死的就是几个关键参数:--gpu-memory-utilization、--max-model-len、--max-num-seqs、--tensor-parallel-size。
一个典型的启动命令(vLLM较新版本):
python -m vllm.entrypoints.openai.api_server \ --model /data/models/llama-7b-chat \ --served-model-name chat-7b \ --gpu-memory-utilization 0.90 \ --max-model-len 8192 \ --max-num-seqs 64 \ --tensor-parallel-size 1 \ --dtype auto这几个参数之间不是独立的,而是相互制约的关系。--max-model-len设置得越大,KV Cache预留就越多;--max-num-seqs越大,batch上限越高,吞吐量越好,但显存压力也越大;--gpu-memory-utilization则是所有这些参数的“总预算”。如果把这些参数都设到极大值,服务会直接启动失败,因为显存装不下。
3.2 显存利用率、最大序列长度、并发数与KV Cache的关系
先解释--gpu-memory-utilization。它指的是模型权重和KV Cache占显存的上限比例,剩余部分留给激活值和临时计算。设为0.9已经是个比较激进的数值,如果你同时还有别的GPU任务跑在同一张卡上,建议降到0.8甚至更低。这个参数设置过高时,某些模型会在长上下文的极端情况下触发显存溢出。
--max-model-len指的是模型允许的最大上下文长度。很多人有个误解,认为模型本身能支持32K就一定要设成32K。实际不是这样,你设置的是显存预算的一部分,每拉大一倍都会显著增加KV Cache开销。如果业务正常对话平均只有2K到3K上下文,但你把上限设到32K,意味着你为一个几乎不会发生的场景预留大量显存,白白浪费资源,就会导致并发下降。
--max-num-seqs是引擎所允许的最大同时推理序列数。它跟外部并发密切相关,假设你设64,那么即便客户端发了200个并发请求,服务端也只会同时处理64个,其余请求排队。这个值调大吞吐量会上升,但每个请求的延迟也会相应增加。一般7B模型单卡设为32到64是平衡区间,超过之后收益变得很小,排队延迟反而让人难以接受。
3.3 多卡部署与tensor parallelism
单卡扛不住大模型时就需要多卡并行。vLLM中最常用的方式是--tensor-parallel-size,数值代表用几张GPU分担同一个模型的计算。例如13B模型在两张24GB卡上做张量并行,权重被切分到两张卡上,每张卡只需要负责一半的矩阵运算。
张量并行能解决单卡显存放不下大模型的问题,但它并非没有代价。每轮计算的过程中,多张卡之间要通过NCCL同步中间结果,通信耗时随着卡数量增多会变得明显。对于7B以内的模型,我实际上不推荐做2卡并行,单卡的性能往往更好,因为通信开销有时反而让推理变慢。只有当模型单卡完全放不下时,或者你需要同时服务的上下文特别长,才考虑张量并行。
另外,部署时还要区分张量并行(TP)和流水线并行(PP)。TP是切分单层计算,PP是按层切分,vLLM对PP支持不如TP成熟。生产环境的稳妥做法是优先TP,别碰PP。多卡部署还有一个容易被忽视的问题:TP规模必须在启动时固定,运行中不能动态调整,如果业务需要弹性扩缩容,就要在部署层重新拉起新进程,而负载均衡则交给服务网关处理。
4. 性能评估与调优:让模型跑得更快更稳
4.1 性能基线怎么测:不要凭感觉判断快慢
部署完成后,第一件事不是让业务接入,而是先打出一个性能基线。我在调试过程中每次都先用一个小脚本模拟请求,记录TTFT、TPOT和总吞吐量。没有基线的调优都是盲人摸象。
一个简单的压测思路是:先单线程发一个请求,连续发10次取平均值,确定单请求的延迟天花板。然后逐渐增加并发线程数,观察吞吐量和延迟的变化曲线。一般规律是并发从1增加到16时总吞吐量显著上升,延迟上升不大;继续增加并发到64以上时,吞吐量增长变缓,延迟开始快速攀升,这说明已经接近系统瓶颈。
模型实测中的一组典型数据(量化与未量化的对比):
| 配置 | TTFT(首token延迟) | TPOT(每token耗时) | 吞吐量 | 显存占用 |
|---|---|---|---|---|
| 7B FP16 | 约680ms | 约35ms | 约28 tokens/s | 约16GB |
| 7B AWQ 4bit | 约760ms | 约43ms | 约23 tokens/s | 约6GB |
| 13B FP16 双卡 | 约920ms | 约52ms | 约19 tokens/s | 约30GB |
| 13B AWQ 4bit | 约1050ms | 约47ms | 约21 tokens/s | 约8GB |
从数据能看出一个容易被误解的点:FP16单看TPOT更快,但实际吞吐量不一定比量化高多少。4bit量化虽然权重精度低了,但显存占用大幅下降,同样的显存下可以开更大的批处理,把连续批处理的效果发挥出来,从而在总吞吐上扳回一城。如果你的业务场景对单请求延迟极其敏感,用FP16划算;如果你追求单位时间处理最多请求,量化后调大并发往往收益更高。
4.2 continuous batching与并发调优
连续批处理是vLLM对比传统批处理的核心优势。传统方案是把一批请求对齐到同一波次,整批结束时统一交换结果,一旦某个请求生成长文本,其他已完成请求的GPU算力就空等了。连续批处理则不同,任何序列生成结束后立刻从待处理队列拉新请求填充,GPU始终处于忙碌状态。
这种机制对吞吐量的提升非常明显,尤其是当请求的生成长度差异很大时,传统批处理的算力浪费比例会达到40%以上,连续批处理能让利用率保持在80%以上。
为了让吞吐跑起来,还要调整客户端和服务端的配合方式。客户端不能发一个请求等一个结果再发下一个,那样无论服务端多强都撑不起并发。应该用线程池或异步IO同时发送多个请求。一个简单的Python压测脚本示意:
import concurrent.futures import requests url = "http://127.0.0.1:8000/v1/chat/completions" def send_one(prompt): payload = { "model": "chat-7b", "messages": [{"role": "user", "content": prompt}], "max_tokens": 512, "temperature": 0.7 } resp = requests.post(url, json=payload, timeout=120) return len(resp.json()["choices"][0]["message"]["content"]) prompts = ["介绍一下人工智能"] * 100 with concurrent.futures.ThreadPoolExecutor(max_workers=32) as pool: results = list(pool.map(send_one, prompts)) print(len(results), sum(results))实测下来,当并发从1提升到32时,总吞吐通常能翻5到10倍。但并发继续加到128,延迟会急剧升高,TTFT可能从不到1秒飙升到5秒以上。这就回到了第一节说的矛盾,高吞吐和低延迟需要动态平衡。
4.3 长上下文的性能陷阱与PagedAttention
长上下文是大规模部署中最隐形的性能杀手。很多人最初只看模型参数支持多少K,没有意识到长上下文会对显存造成巨大压力。当多个请求同时持有很长上下文时,KV Cache占用会指数级增长,触发OOM没有任何先兆。
vLLM的PagedAttention把KV Cache按固定大小的块来分配,解决了碎片化问题。每个“页”可以被多个序列引用,这就使得前缀相同的请求能共享KV Cache,节省大量显存。这一特性对系统提示词很长、用户问题有明显共同前缀的场景特别有价值,显存占用可能减少50%以上。
但要注意,PagedAttention并不会减少长上下文的绝对消耗。假设每个请求固定10K上下文,100个并发请求的KV Cache预留会达到非常可观的数值。这时候就需要动态权衡:设置--max-model-len为业务实际所需值,不要盲目追求长文本;另外主动控制请求的最大输出长度,否则模型生成到一半显存耗尽,服务直接崩溃,没有任何缓冲余地。我在一些项目中甚至会在API网关层限制业务侧请求的max_tokens,避免业务方把输出长度调得过大导致后端不稳定。
5. 高频故障排查与稳定性保障
5.1 显存相关问题的排查与解决
显存溢出是部署中最常见的问题,但表现形式五花八门。一种情况是启动时直接显存不足,日志会报CUDA out of memory。另一种情况更隐蔽:启动时明明显存足够,跑着跑着突然OOM,这通常是长上下文请求导致的。第三种情况是显存溢出没有报错,只是推理速度突然变得极慢,这是显存碎片化导致额外内存拷贝增多。
排查显存问题的实用命令:
nvidia-smi # 查看进程占用 nvidia-smi --query-gpu=memory.total,memory.used,memory.free --format=csv遇到启动OOM,优先降低--gpu-memory-utilization或降低--max-model-len。遇到运行期OOM,先检查是否有异常长上下文的请求进来,再检查并发峰值是否超过--max-num-seqs设定。还有一个小技巧:vLLM支持--enforce-eager模式,它关闭了CUDA Graph的预编译优化,虽然性能会有损失,但能显著降低启动时显存占用,排查问题时可以先用它验证模型的正确性。
5.2 并发下的性能劣化与超时处理
一旦并发压上去,最容易出现的问题是客户端超时。表现出来就是服务端日志显示请求正在排队,而客户端已经报Connect timed out或者Read timed out。这种情况下很多人第一反应是加机器,但有时候不需要,先看服务端是否还有余量。
vLLM的请求队列有个特点,并发请求数超过--max-num-seqs时,新的请求会进入内部队列等待。如果队列过长,先进入的请求TTFT会不断推迟。此时需要做限流,合理设置--max-num-seqs的值,控制每次实际处理的数量,同时让真正进入服务端的请求都能在可接受时间内返回。
生产环境我建议在业务侧做两层超时保护:第一层,业务后端调用LLM时设置连接超时和读超时;第二层,网关层设置请求总超时,超时直接返回降级文案,不要傻等。不然一个慢请求把线程占住,后面所有请求全部遭殃。另外,vLLM的队列本身是内存态的,一旦进程崩溃,所有排队的请求都会消失,所以网关层的超时和降级策略一定要有。
5.3 服务稳定性保障与监控
稳定性保障有一个核心原则:一切可能挂的东西都会挂,要提前做预案。LLM服务是长驻服务,不像普通Web服务那样进程退出了重启就完事,它重启一次光模型加载就要几十秒到几分钟。因此一定要有健康检查和自动拉起机制。
健康检查不要只检查进程是否存活,最好是发一个真实的最小LLM请求,比如“你好”,看它能否在30秒内返回结果。只检查端口能连通是不可靠的,我之前遇到过进程活着但CUDA上下文已经损坏,模型推理全部卡死,TCP端口却正常响应的情况。
监控指标至少要覆盖这几个维度:
- 请求数、平均TTFT、平均TPOT、生成总token数
- 活跃序列数、排队序列数
- GPU利用率、显存占用率、温度
- 进程异常退出次数和原因
我习惯把这些指标通过Prometheus格式暴露出来,配一个定时拉取的监控平台,再对关键指标设置阈值告警。显存占用率超过设定值就提前报警,不要在OOM之后才收到通知。还有一点,服务进程要注意用--served-model-name显式命名模型,这样在OpenAI接口调用时,产品名与模型真实名称解耦,后续模型迭代升级直接改映射即可,不需要开发改代码。
5.4 故障排查速查表
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 启动时CUDA OOM | GPU显存不足或--gpu-memory-utilization过高 | 调低该值,降低--max-model-len |
| 运行一段时间后OOM | 长上下文请求或并发峰值超预算 | 限制输入长度,降低--max-num-seqs |
| 多卡推理卡死 | NCCL通信初始化异常 | 设NCCL_IB_DISABLE=1,查看NCCL_DEBUG日志 |
| 服务端口通但推理超时 | 内部排队过长或GPU算力打满 | 客户端做限流,服务端开新的副本 |
| 模型加载报格式错误 | 模型文件不完整或格式不支持 | 校验文件完整性,优先使用safetensors |
| 输出效果和本地测试不一致 | 量化精度损失或temperature设置不同 | 对比配置文件,必要时切换到BF16精度 |
这个部署链路走完一遍,你会发现真正难的不是把模型加载起来,而是让它在真实负载下保持稳定、可控、可观测。我在部署几个项目之后最大的体会是,一定要先把性能基线和SLA指标定好,再往里调参,否则每天都在救火。如果在做生产部署时能把显存规划、引擎选型、参数匹配、监控告警这四件事提前想清楚,大概可以避开七八成以上的故障,希望你部署时也顺利。