1. 小模型线上部署的整体思路与选型逻辑
把大模型塞进线上环境,最先撞上的不是算法问题,而是成本与延迟的墙。一个70B参数的模型,即便用上A100,单次推理的显存占用和响应时间也很难让业务方满意。所以“llm小模型线上使用”这件事,本质上是在效果、成本、延迟三者之间找一个可落地的平衡点。我自己的经验是,除非业务场景对生成质量有极端要求,否则7B到13B这个量级的模型,经过合理微调和推理优化后,能覆盖绝大多数线上需求,比如意图识别、文本摘要、知识问答、简单对话等。
为什么是llama3系列?因为它的生态最成熟。llama3 8B在开源社区里的微调脚本、量化方案、推理框架适配都最全,踩坑成本最低。deepspeed负责训练侧的显存优化,KV Cache负责推理侧的加速,这两个东西一个管“怎么训出来”,一个管“怎么跑得快”,缺一不可。很多人只关注训练,结果模型上线后发现QPS上不去,单次响应要好几秒,这就是推理侧没做优化的典型表现。
小模型线上使用的核心矛盾在于:模型小了,效果会掉;但如果不做量化和推理优化,小模型也跑不快。所以整个方案的设计思路是——训练阶段用deepspeed做显存优化,让单卡能微调更大参数量的模型;推理阶段用KV Cache加量化,把延迟压到业务可接受的范围。这个组合不是拍脑袋定的,而是经过多轮压测后得出的结论。
1.1 为什么选小模型而不是直接调API
调API看起来省事,但线上业务一旦上量,成本会线性增长。假设每天10万次调用,每次平均500 token,按主流API的定价,一个月下来费用相当可观。而自己部署一个7B模型,用一张消费级显卡就能跑起来,电费加机器折旧,成本是固定的。更重要的是,数据不出域,对于有合规要求的业务场景,这一点是刚需。
小模型的另一个优势是可控性。你可以针对自己的业务数据做微调,让模型更懂你的领域术语和表达习惯。API模型虽然通用能力强,但在垂直场景下往往不如一个精调过的小模型。我试过用llama3 8B在客服问答场景做微调,效果比直接调通用API好不少,尤其是对内部产品名称和流程的理解,精调后的模型几乎不会答偏。
当然,小模型也有短板。复杂推理、多轮长对话、代码生成这些任务,小模型确实力不从心。所以选型时要先明确业务场景的边界,不要指望一个7B模型能搞定所有事。我的做法是,把业务需求拆成多个子任务,简单的用规则或小模型处理,复杂的再走大模型或人工兜底。
1.2 deepspeed在训练侧的角色
deepspeed的核心价值是显存优化。微调一个7B模型,如果用全量参数微调,显存占用轻松超过80G,单卡根本放不下。deepspeed的ZeRO阶段可以把优化器状态、梯度、参数分片到多张卡上,让单卡显存需求大幅下降。实际用下来,ZeRO-2阶段配合梯度累积,单张24G显存的卡就能微调7B模型,ZeRO-3阶段甚至能微调13B。
但deepspeed不是银弹。它的配置比较复杂,不同阶段对通信带宽的要求不同,如果机器间网络不好,多卡训练的加速比会很低。我的经验是,如果只有单卡,用ZeRO-2加CPU offload就够了;如果有多卡且网络是NVLink或高速以太网,ZeRO-3能让你微调更大的模型。配置文件的参数需要根据实际硬件调整,不能直接抄网上的模板。
1.3 KV Cache为什么是推理加速的关键
KV Cache的原理不复杂:自回归生成时,每生成一个新token,都需要计算注意力。如果不缓存,每次都要重新计算前面所有token的Key和Value矩阵,计算量随序列长度平方增长。KV Cache把已经算过的Key和Value存下来,生成新token时直接复用,计算量就降到了线性增长。
为什么不是QKV Cache?因为Query在每个新token生成时都是新的,没法复用。Key和Value只依赖于输入序列,一旦算好就不变了。所以只缓存K和V,不缓存Q。这个细节很多人搞混,面试时也常被问到。
KV Cache的代价是显存。序列越长,缓存的KV矩阵越大。llama3 8B的KV Cache在FP16精度下,每1000 token大约占用几百MB显存。如果并发请求多,显存会迅速吃满。所以线上部署时,KV Cache的管理策略很关键,比如设置最大序列长度、动态批处理、PagedAttention等。vLLM这个框架就是专门做这个的,它把KV Cache分页管理,显存利用率比朴素实现高很多。
注意:KV Cache不是越大越好。设置过大的max_model_len会导致显存预留过多,实际并发数下降。要根据业务的实际输入长度分布来定,比如90%的请求输入都在512 token以内,那max_model_len设1024就够了,没必要设4096。
2. 小模型线上部署的核心细节与实操要点
从训练到上线,中间有一堆细节决定成败。模型微调完了只是第一步,怎么把它变成一个稳定、高效、可维护的线上服务,才是真正考验工程能力的地方。这一块我踩过的坑最多,下面按环节拆开讲。
2.1 模型微调的数据准备与格式对齐
微调数据质量直接决定线上效果。很多人拿开源数据集直接训,结果模型学了一堆无关的说话风格,业务指标反而下降。我的做法是:先用业务日志构造种子数据,再用大模型做数据增强。比如从客服对话里抽1000条真实问答,人工标注成指令格式,然后用大模型对每条种子数据生成5到10个变体,覆盖不同的表达方式。
数据格式要和推理时的prompt模板严格对齐。训练时用的system prompt、user/assistant角色标记,推理时必须一模一样。我见过有人训练时用了某种特殊token,推理时忘了加,结果模型输出完全乱套。llama3的chat模板有固定的特殊token,比如<|start_header_id|>和<|end_header_id|>,这些在训练和推理时都要一致。
数据量方面,7B模型的指令微调,5000到20000条高质量样本通常就够了。再多边际收益递减,而且容易过拟合。关键是多样性,同一个意图要有多种表达方式,让模型学会泛化。我一般会按8:1:1划分训练集、验证集和测试集,验证集用来早停,测试集用来评估最终效果。
2.2 deepspeed配置文件的参数拆解
deepspeed的配置文件看起来参数很多,但核心就几个。下面是一个单卡24G显存微调7B模型的配置示例:
{ "train_batch_size": 16, "gradient_accumulation_steps": 4, "fp16": { "enabled": true }, "zero_optimization": { "stage": 2, "offload_optimizer": { "device": "cpu", "pin_memory": true }, "allgather_partitions": true, "allgather_bucket_size": 2e8, "overlap_comm": true, "reduce_scatter": true, "reduce_bucket_size": 2e8, "contiguous_gradients": true } }train_batch_size是全局批次大小,gradient_accumulation_steps是梯度累积步数。实际单卡批次大小等于train_batch_size / gradient_accumulation_steps,这里就是4。如果显存不够,就减小单卡批次,增大累积步数,效果等价但速度会慢一些。
offload_optimizer把优化器状态放到CPU内存,能省不少显存,但会增加CPU和GPU之间的数据传输,训练速度会下降。如果显存够用,可以先不开offload,速度优先。overlap_comm让通信和计算重叠,多卡训练时能提升效率,单卡无所谓。
实操心得:deepspeed的配置文件不要一次调到位。先跑一个step,看显存占用和loss是否正常,再逐步增大批次。我习惯先用小批次跑通流程,确认数据加载、前向、反向、优化器更新都没问题,再放大批次做正式训练。
2.3 推理框架的选型对比
线上推理框架的选择直接影响延迟和吞吐。我对比过几种主流方案:
| 框架 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| HuggingFace Transformers | 上手快,生态全 | 吞吐低,显存管理粗放 | 原型验证、低并发 |
| vLLM | PagedAttention,吞吐高 | 配置稍复杂,部分模型适配滞后 | 高并发线上服务 |
| TGI | 部署简单,支持量化 | 定制化能力弱 | 快速上线、标准场景 |
| llama.cpp | CPU也能跑,量化方案多 | GPU利用率低 | 边缘设备、低资源环境 |
我最终选了vLLM,因为它的PagedAttention对KV Cache的管理最精细,并发吞吐比朴素Transformers高好几倍。实测下来,同样一张A10,Transformers只能跑个位数QPS,vLLM能跑到几十QPS,差距非常明显。
vLLM的启动命令很简单:
python -m vllm.entrypoints.openai.api_server \ --model /path/to/merged_model \ --tensor-parallel-size 1 \ --max-model-len 2048 \ --gpu-memory-utilization 0.9 \ --dtype halfmax-model-len控制最大序列长度,直接影响KV Cache的显存预留。gpu-memory-utilization控制显存使用比例,0.9表示用90%的显存,留一点给系统。dtype half用FP16推理,比FP32快且显存减半。
2.4 量化方案的取舍
量化是让小模型跑得更快的另一把钥匙。FP16转INT8或INT4,显存占用和计算量都能大幅下降。但量化会带来精度损失,尤其是INT4,某些任务上效果下降明显。
我试过GPTQ和AWQ两种量化方案。GPTQ量化速度快,但精度损失稍大;AWQ对激活值做保护,精度保持更好,但量化过程慢一些。实际用下来,AWQ INT4在7B模型上的效果损失可以接受,显存占用从FP16的约14G降到约4G,一张消费级显卡就能跑,延迟也降了一半左右。
量化不是必须的。如果显存够用,FP16的精度最好。量化的收益主要在显存和延迟,如果业务对这两点不敏感,可以不做。我的建议是:先跑FP16,看显存和延迟是否达标;不达标再考虑INT8;INT8还不够再上INT4。每一步都要重新评估效果,不能只看速度。
3. 完整实操流程与关键环节实现
这一部分我把从模型微调到线上服务的完整流程串起来,每一步都给出具体操作和参数说明。你可以直接照着做,但要注意根据自己的硬件和业务调整。
3.1 环境搭建与依赖安装
基础环境是CUDA 12.1加PyTorch 2.1以上。deepspeed和vLLM对版本有要求,版本不匹配会报各种奇怪的错误。我一般用conda建一个独立环境,避免和系统Python冲突。
conda create -n llm_deploy python=3.10 conda activate llm_deploy pip install torch==2.1.2 torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121 pip install deepspeed==0.12.6 pip install vllm==0.3.3 pip install transformers==4.38.2 pip install accelerate==0.27.2版本号不是随便写的。deepspeed 0.12.x和vLLM 0.3.x是我实测下来比较稳定的组合,再新的版本可能有API变动。transformers的版本要和模型架构匹配,llama3需要4.38以上。
注意:安装vLLM时会自动编译一些CUDA算子,如果机器上没有nvcc或者CUDA版本不对,会编译失败。建议先确认
nvcc --version输出正常,再装vLLM。
3.2 模型微调与合并
微调脚本用HuggingFace的Trainer加deepspeed。核心参数如下:
training_args = TrainingArguments( output_dir="./output", per_device_train_batch_size=4, gradient_accumulation_steps=4, learning_rate=2e-5, num_train_epochs=3, lr_scheduler_type="cosine", warmup_ratio=0.03, fp16=True, logging_steps=10, save_strategy="epoch", deepspeed="ds_config.json", )学习率2e-5是7B模型微调的常用值,太大容易训崩,太小收敛慢。cosine调度加warmup是标配,warmup比例0.03表示前3%的step做线性预热。epoch数看数据量,5000条数据跑3轮通常够,数据多的话2轮也行。
微调完成后,LoRA权重需要合并到基础模型里,才能用于推理:
from peft import PeftModel from transformers import AutoModelForCausalLM base_model = AutoModelForCausalLM.from_pretrained("meta-llama/Meta-Llama-3-8B") model = PeftModel.from_pretrained(base_model, "./output") model = model.merge_and_unload() model.save_pretrained("./merged_model")合并后的模型是一个完整的llama3 8B,可以直接被vLLM加载。如果不合并,vLLM加载LoRA需要额外配置,而且性能不如合并后的模型。
3.3 vLLM服务部署与压测
启动vLLM服务后,用OpenAI兼容的API做压测。我一般用locust或wrk,模拟并发请求,观察QPS、P99延迟和显存占用。
python -m vllm.entrypoints.openai.api_server \ --model ./merged_model \ --served-model-name llama3-8b \ --max-model-len 2048 \ --gpu-memory-utilization 0.85 \ --dtype half \ --port 8000压测时重点关注几个指标:首token延迟(TTFT)和每token延迟(TPOT)。TTFT反映的是prefill阶段的速度,TPOT反映的是decode阶段的速度。KV Cache主要优化的是TPOT,因为decode阶段每步都要读缓存。如果TTFT高,说明prefill计算量大,可以考虑用chunked prefill或者减少输入长度。
实测数据:一张A10 24G,llama3 8B FP16,max-model-len 2048,并发10时,TTFT约200ms,TPOT约30ms,QPS约8。换AWQ INT4后,TTFT降到120ms,TPOT降到18ms,QPS翻倍到16左右。这个提升对线上服务来说非常可观。
3.4 KV Cache的显存计算与调优
KV Cache的显存占用可以用公式估算:
KV Cache大小 = 2 * batch_size * seq_len * num_layers * num_heads * head_dim * dtype_size以llama3 8B为例,num_layers=32,num_heads=32,head_dim=128,dtype_size=2(FP16)。假设batch_size=1,seq_len=2048:
2 * 1 * 2048 * 32 * 32 * 128 * 2 = 约1.07GB这是单条序列的KV Cache。如果并发10,就是10.7GB。加上模型本身的14GB,总共约25GB,超过A10的24G显存。所以要么减并发,要么用量化把模型压到4GB,要么用PagedAttention提高显存利用率。
vLLM的PagedAttention把KV Cache分成固定大小的block,按需分配,碎片少,利用率高。实际用下来,同样显存能支持的并发数比朴素实现高30%到50%。调优时主要调max-model-len和gpu-memory-utilization,前者控制单序列最大长度,后者控制总显存使用上限。
实操心得:不要盲目追求高并发。并发太高,单请求的延迟会上升,用户体验反而下降。我一般会设一个并发上限,超过就排队或拒绝,保证已接入请求的响应时间稳定。
4. 常见问题排查与避坑经验实录
线上环境的问题往往不是单一原因,而是多个因素叠加。下面这些是我实际遇到过的典型问题,按排查思路整理成速查表。
4.1 模型输出乱码或重复
这是最常见的问题,原因通常有三个:prompt模板不匹配、特殊token处理错误、量化精度损失过大。
先检查prompt模板。训练时用的system prompt和推理时是否一致?角色标记是否正确?llama3的chat模板要求严格的特殊token,少一个都会导致输出异常。我习惯把训练时的模板保存下来,推理时直接复用,避免手写出错。
再检查tokenizer。有些微调脚本会添加自定义token,但推理时没加载对应的tokenizer配置,导致token映射错位。合并模型时要把tokenizer一起保存,vLLM加载时指定正确的tokenizer路径。
如果是量化导致的,先换回FP16跑一遍。FP16正常说明是量化问题,可以换AWQ或者降低量化位数。INT4在某些模型上确实会出现重复生成,换INT8通常能解决。
4.2 显存溢出(OOM)的排查路径
OOM的原因很多,按以下顺序排查:
- 模型本身太大:7B FP16约14G,13B约26G,确认显卡显存是否够。
- KV Cache预留过多:
max-model-len设太大,显存被预留光。减小到业务实际需要的长度。 - 并发过高:同时处理的请求太多,KV Cache总和超限。降低并发或启用量化。
- 显存碎片:长时间运行后显存碎片化,重启服务能缓解。vLLM的PagedAttention对此有改善。
- 其他进程占用:确认没有其他进程占着显存,
nvidia-smi看一下。
我遇到过一次OOM,排查半天发现是gpu-memory-utilization设了0.95,vLLM把显存几乎占满,系统其他进程没空间了。改成0.85就好了。留一点余量给系统,别把显存吃干。
4.3 延迟忽高忽低的波动问题
延迟波动通常和批处理策略有关。vLLM默认会做连续批处理,新请求会插到正在处理的批次里,导致某些请求的延迟被拉长。如果业务对延迟敏感,可以关闭连续批处理,或者设置最大批大小。
另一个原因是输入长度差异大。长输入会拖慢整个批次,因为prefill阶段的计算量和输入长度成正比。解决办法是按输入长度分队列,长输入走单独的服务实例,短输入走另一个。这样互不影响。
还有可能是GPU降频。长时间高负载运行,GPU温度升高会降频,延迟上升。检查散热和功耗设置,必要时限制功率或加风扇。
4.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 输出乱码 | prompt模板不匹配 | 对比训练和推理模板 | 统一模板,保存复用 |
| 输出重复 | 量化精度损失 | 换FP16测试 | 换AWQ或INT8 |
| OOM | KV Cache预留过多 | 检查max-model-len | 减小到实际需要 |
| 延迟波动 | 连续批处理 | 观察批次大小 | 关闭或限制批大小 |
| 吞吐低 | 未用量化 | 对比FP16和INT4 | 启用量化 |
| 首token慢 | prefill计算量大 | 检查输入长度 | 分队列或chunked prefill |
4.5 线上监控与告警配置
服务上线后,监控不能少。我一般监控这几个指标:QPS、P99延迟、显存使用率、GPU利用率、错误率。QPS和延迟反映服务质量,显存和GPU利用率反映资源瓶颈,错误率反映稳定性。
告警阈值根据业务定。比如P99延迟超过1秒告警,显存使用率超过90%告警,错误率超过1%告警。告警渠道用企业微信或邮件,确保能及时响应。
日志要记录每个请求的输入长度、输出长度、耗时、是否命中缓存。这些数据对后续调优很有价值。比如发现大部分请求输入都在256 token以内,就可以把max-model-len从2048降到512,省下的显存用来提高并发。
实操心得:线上服务一定要做灰度发布。新模型先接10%的流量,观察一周,确认效果和稳定性没问题再全量。我见过直接全量上线导致效果崩盘的事故,回滚都来不及。
5. 小模型线上使用的扩展思路
小模型上线不是终点,而是一个起点。业务在变,模型也要跟着迭代。我目前的做法是建立一套数据回流加持续微调的机制:线上服务的输入输出日志定期回流,人工抽检标注,积累到一定量后重新微调模型,评估通过后灰度上线。这样模型能持续适应业务变化,效果不会随时间衰减。
另一个方向是多模型路由。简单请求走小模型,复杂请求走大模型,用一个小分类器判断请求难度。这样既能保证效果,又能控制成本。分类器本身也可以是小模型,整个链路都在可控范围内。
KV Cache的优化还有空间。比如prefix caching,把系统prompt的KV缓存复用,多个请求共享同一段前缀的缓存,能省不少显存和计算。vLLM已经支持这个特性,开启后对固定system prompt的场景提升明显。
量化方面,AWQ和GPTQ之外,还有SmoothQuant和FP8等方案。FP8在H100等新卡上有硬件加速,精度损失比INT4小,是未来的趋势。如果硬件支持,值得尝试。
最后再分享一个小技巧:推理时的temperature和top_p要根据业务调。创意类任务温度高一点,事实类任务温度低一点。我一般设temperature=0.7,top_p=0.9作为默认值,特殊场景再覆盖。这两个参数对输出质量的影响比想象中大,别用默认值一把梭。