多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

小模型线上部署实战:deepspeed微调与KV Cache推理加速优化

小模型线上部署实战:deepspeed微调与KV Cache推理加速优化 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上手快生态全吞吐低显存管理粗放原型验证、低并发vLLMPagedAttention吞吐高配置稍复杂部分模型适配滞后高并发线上服务TGI部署简单支持量化定制化能力弱快速上线、标准场景llama.cppCPU也能跑量化方案多GPU利用率低边缘设备、低资源环境我最终选了vLLM因为它的PagedAttention对KV Cache的管理最精细并发吞吐比朴素Transformers高好几倍。实测下来同样一张A10Transformers只能跑个位数QPSvLLM能跑到几十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看显存和延迟是否达标不达标再考虑INT8INT8还不够再上INT4。每一步都要重新评估效果不能只看速度。3. 完整实操流程与关键环节实现这一部分我把从模型微调到线上服务的完整流程串起来每一步都给出具体操作和参数说明。你可以直接照着做但要注意根据自己的硬件和业务调整。3.1 环境搭建与依赖安装基础环境是CUDA 12.1加PyTorch 2.1以上。deepspeed和vLLM对版本有要求版本不匹配会报各种奇怪的错误。我一般用conda建一个独立环境避免和系统Python冲突。conda create -n llm_deploy python3.10 conda activate llm_deploy pip install torch2.1.2 torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121 pip install deepspeed0.12.6 pip install vllm0.3.3 pip install transformers4.38.2 pip install accelerate0.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_size4, gradient_accumulation_steps4, learning_rate2e-5, num_train_epochs3, lr_scheduler_typecosine, warmup_ratio0.03, fp16True, logging_steps10, save_strategyepoch, deepspeedds_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 24Gllama3 8B FP16max-model-len 2048并发10时TTFT约200msTPOT约30msQPS约8。换AWQ INT4后TTFT降到120msTPOT降到18msQPS翻倍到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_layers32num_heads32head_dim128dtype_size2FP16。假设batch_size1seq_len20482 * 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约14G13B约26G确认显卡显存是否够。KV Cache预留过多max-model-len设太大显存被预留光。减小到业务实际需要的长度。并发过高同时处理的请求太多KV Cache总和超限。降低并发或启用量化。显存碎片长时间运行后显存碎片化重启服务能缓解。vLLM的PagedAttention对此有改善。其他进程占用确认没有其他进程占着显存nvidia-smi看一下。我遇到过一次OOM排查半天发现是gpu-memory-utilization设了0.95vLLM把显存几乎占满系统其他进程没空间了。改成0.85就好了。留一点余量给系统别把显存吃干。4.3 延迟忽高忽低的波动问题延迟波动通常和批处理策略有关。vLLM默认会做连续批处理新请求会插到正在处理的批次里导致某些请求的延迟被拉长。如果业务对延迟敏感可以关闭连续批处理或者设置最大批大小。另一个原因是输入长度差异大。长输入会拖慢整个批次因为prefill阶段的计算量和输入长度成正比。解决办法是按输入长度分队列长输入走单独的服务实例短输入走另一个。这样互不影响。还有可能是GPU降频。长时间高负载运行GPU温度升高会降频延迟上升。检查散热和功耗设置必要时限制功率或加风扇。4.4 常见问题速查表问题现象可能原因排查方法解决方案输出乱码prompt模板不匹配对比训练和推理模板统一模板保存复用输出重复量化精度损失换FP16测试换AWQ或INT8OOMKV Cache预留过多检查max-model-len减小到实际需要延迟波动连续批处理观察批次大小关闭或限制批大小吞吐低未用量化对比FP16和INT4启用量化首token慢prefill计算量大检查输入长度分队列或chunked prefill4.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要根据业务调。创意类任务温度高一点事实类任务温度低一点。我一般设temperature0.7top_p0.9作为默认值特殊场景再覆盖。这两个参数对输出质量的影响比想象中大别用默认值一把梭。
返回列表