1. 从训练到落地:为什么微调之后才是真正的硬仗
很多人第一次接触大模型,脑子里想的都是“我要训练一个自己的模型”。折腾了几周数据清洗、租了GPU、跑完LoRA微调,看着loss曲线一路往下掉,心里美滋滋。结果一到实际用的时候傻眼了——模型是训好了,权重文件躺在磁盘里,然后呢?怎么让业务系统调它?怎么扛住并发?怎么控制成本?怎么保证输出稳定?
这就是我常说的:微调只是上半场,工程化落地才是决定项目生死的那一脚。你去看那些真正把大模型跑进生产环境的团队,他们在推理部署和API封装上花的时间,往往比训练本身还多。原因很简单,训练是一次性的活儿,而API服务是7×24小时要扛住的活儿。
这篇文章我想聊的就是从“模型微调完成”到“业务方稳定调用API”这条链路上,到底有哪些坑、哪些关键决策点、哪些能直接抄作业的方案。适合已经跑过至少一次微调、准备把模型推上线的开发者,也适合正在做技术选型、纠结要不要自建推理服务的团队负责人。核心关键词就三个:模型微调、推理部署、API工程化。我会把每一步的“为什么这么选”讲清楚,而不是只丢一个结论给你。
先说一个基本认知:微调产出的模型权重,本质上只是一个“静态文件”。它不会自己变成服务,不会自己处理并发,更不会自己省钱。从权重到API之间,至少隔着推理引擎选型、量化策略、服务框架、并发调度、监控告警这五道关。每一道关都有它的脾气,选错了轻则成本翻倍,重则线上直接雪崩。
我见过太多团队在微调阶段投入了大量精力,结果部署时随便找了个demo脚本套上Flask就上线,QPS一上来直接OOM。也见过有人盲目追求“大而全”的推理框架,配置复杂到自己都维护不动。所以接下来的内容,我会按照真实的落地顺序,一层一层拆开讲。
2. 推理引擎选型:别一上来就上最重的方案
2.1 主流推理引擎的能力边界对比
微调完成后第一件事,是决定用什么推理引擎来加载权重。这一步的选择会直接影响后面所有的工程决策,包括显存占用、吞吐量、支持的量化格式、API封装难度。我先把几个主流方案的核心差异摆出来,这些都是我在实际项目里踩过或者验证过的。
| 推理引擎 | 核心优势 | 主要限制 | 适合场景 |
|---|---|---|---|
| 原生Transformers | 兼容性最好,任何微调权重都能直接加载 | 显存占用大,吞吐低,无连续批处理 | 验证阶段、单次推理、调试 |
| vLLM | PagedAttention显存管理,吞吐极高,支持连续批处理 | 对部分自定义模型结构支持需要适配 | 高并发在线服务 |
| TGI | 部署简单,内置量化,HuggingFace生态无缝 | 定制化能力相对弱 | 快速上线、标准模型 |
| llama.cpp | CPU也能跑,量化极致,资源占用低 | 高并发场景吞吐不如GPU方案 | 边缘设备、低并发、成本敏感 |
| TensorRT-LLM | 极致性能,算子融合深度优化 | 编译流程复杂,模型转换成本高 | 追求极致延迟的大规模服务 |
这张表不是让你背的,而是让你在选型时有个判断依据。我的经验是:如果你还在验证微调效果,就用原生Transformers,别折腾;如果准备上生产且并发量在几十以上,直接上vLLM;如果团队运维能力弱、想快速上线,TGI是稳妥选择;如果预算极紧或者要跑在CPU上,llama.cpp。
2.2 为什么vLLM成了当前在线服务的主流选择
重点说一下vLLM,因为这两年它在在线推理场景里几乎是默认选项。核心原因是它解决了大模型推理里最要命的一个问题:KV Cache的显存浪费。
传统推理方式下,每个请求的KV Cache要预分配一整块连续显存,按最大长度来分。你请求可能只用了200个token,但显存按2048预分配了,剩下的全浪费。并发一上来,显存瞬间被吃光。vLLM的PagedAttention把KV Cache切成固定大小的块,按需分配,类似操作系统的虚拟内存分页。这个改动让显存利用率从原来的可能30%提升到80%以上,吞吐量直接翻几倍。
我实测过一个7B模型,在单张24G显存卡上,原生Transformers大概能扛8到10个并发,换成vLLM之后稳定跑到40以上。这个差距在真实业务里就是“要不要加卡”的区别。
注意:vLLM对模型结构的支持不是100%全覆盖。如果你微调时改了模型结构(比如自定义了attention层),加载时可能报错。这种情况要么改回标准结构,要么用原生方案先跑通再考虑优化。
2.3 量化策略:省显存和保效果之间的平衡术
选完引擎,紧接着要决定量化方案。量化说白了就是用更低的精度存权重和算矩阵,代价是可能损失一点效果。常见的几档:
- FP16/BF16:不量化,效果最好,显存占用最大。7B模型大概要14G权重显存。
- INT8:显存减半,效果损失通常很小,适合大多数场景。
- INT4:显存再减半,7B模型权重只要4G左右,效果损失开始可感知,但很多任务还能接受。
- GPTQ/AWQ:训练后量化方法,INT4下效果比朴素量化好不少,是当前主流选择。
我的建议很直接:如果显存够,优先BF16,别为了省一点显存牺牲效果;如果显存紧张,用AWQ的INT4,它在多数任务上比GPTQ更稳;INT8是折中方案,适合对效果敏感又需要一定压缩的场景。
量化不是免费的午餐。我做过一个文本分类的微调任务,BF16下准确率92%,INT4量化后掉到88%。这4个点在某些业务里是致命的。所以量化后一定要用你的验证集重新跑一遍评估,别想当然。
3. 从权重到服务:API工程化的核心环节
3.1 服务框架的选型逻辑
推理引擎负责“算得快”,服务框架负责“管得好”。这两件事要分开看。很多人把vLLM自带的OpenAI兼容API直接暴露出去就完事了,小规模可以,但生产环境不够。
服务框架要解决的核心问题包括:请求队列管理、超时控制、限流、鉴权、日志、多模型路由、优雅重启。这些用原生推理引擎自带的server很难做全。常见的组合是:
- FastAPI + 推理引擎:灵活,自己写控制逻辑,适合定制需求多的团队。
- Ray Serve:适合多模型、多副本的复杂编排,自动扩缩容。
- Triton Inference Server:工业级,支持多框架多模型,但配置复杂。
- 推理引擎自带server + 网关:最省事,前面加一层网关做限流鉴权。
我个人的偏好是:中小规模用FastAPI自己封装,控制力最强,出问题好排查;规模大了再考虑Ray Serve或者Triton。别一上来就上Triton,那个学习曲线会让你怀疑人生。
3.2 请求队列与并发控制的关键参数
这是最容易出事的环节。大模型推理是计算密集型,一个请求可能要跑几秒。如果没有队列管理,请求一多直接把GPU打满,后面的请求全部超时。
核心要控制的参数有这么几个:
- 最大并发数(max_concurrent_requests):同时处理的请求上限。设太高会OOM,设太低吞吐上不去。经验值是从GPU显存反推,留20%余量。
- 队列长度(queue_size):排队等待的请求上限。超过就快速失败返回,别让用户干等。
- 单请求超时(request_timeout):防止某个长请求拖垮整个服务。
- 批处理窗口(batch_window):vLLM这类引擎会自动做连续批处理,但你可以控制等待新请求加入批次的时间窗口。
我踩过的一个坑:早期没设队列上限,结果流量高峰时请求全堆在内存里,最后OOM把服务搞挂了。后来加了队列长度限制和快速失败机制,虽然会拒绝一部分请求,但至少服务是活的。宁可拒绝,不可雪崩,这是生产环境的基本原则。
3.3 流式输出的实现与坑点
大模型API如果不做流式输出,用户体验会很差。用户等5秒才看到第一个字,体感上比等3秒看到全部还难受。流式输出用SSE(Server-Sent Events)实现,推理引擎逐token返回,服务端逐块推给客户端。
实现上有几个坑:
- 首token延迟:流式的价值在于首token快,如果首token就要等3秒,流式意义不大。这跟推理引擎的prefill速度有关,量化能改善。
- 中断处理:用户可能中途关闭连接,服务端要能感知并停止推理,否则白烧算力。
- 缓冲策略:逐token推太碎,网络开销大;攒一批推延迟高。一般攒几个token或者按时间窗口推。
# FastAPI流式输出核心逻辑示意 from fastapi.responses import StreamingResponse async def stream_generate(prompt): async def event_generator(): async for token in engine.generate_stream(prompt): yield f"data: {token}\n\n" yield "data: [DONE]\n\n" return StreamingResponse(event_generator(), media_type="text/event-stream")这段代码看着简单,但生产环境要加上超时、异常捕获、连接断开检测。别小看这些,少一个都可能在高峰期出问题。
4. 性能调优与成本控制:让每一分算力都花在刀刃上
4.1 批处理大小的计算与调优
批处理是提升吞吐最直接的手段。同样一批请求,逐个跑和攒成一批跑,GPU利用率天差地别。但批处理大小不是越大越好,它受显存限制。
计算逻辑是这样的:显存总量减去模型权重占用,剩下的给KV Cache。每个请求的KV Cache大小跟序列长度、层数、隐藏维度有关。假设模型权重占10G,显存24G,剩14G给KV Cache。如果每个请求平均占0.5G,那理论上能批28个。但实际要留余量,设20个比较稳。
调优方法是:从保守值开始,逐步加压,观察显存和延迟。找到那个“吞吐最高且延迟可接受”的平衡点。我一般会做一个压测脚本,模拟真实请求分布,跑出不同批大小下的QPS和P99延迟曲线,然后选拐点。
提示:批处理大小和延迟是矛盾的。批越大吞吐越高,但单个请求的等待时间也越长。要结合业务对延迟的容忍度来定。实时对话场景批大小要小,离线批量处理可以大。
4.2 显存优化的几个实用手段
除了量化,还有几个手段能省显存:
- KV Cache量化:把KV Cache也用INT8存,能再省一半显存,效果损失很小。
- PagedAttention:前面提过,vLLM默认开启,显存利用率大幅提升。
- 梯度检查点:这是训练用的,推理用不上,别搞混。
- 模型并行:单卡放不下就多卡切分,但会引入通信开销,延迟会上升。
我实际项目里的组合拳是:AWQ INT4量化权重 + KV Cache INT8 + PagedAttention。一套下来,一个13B模型能在单张24G卡上跑得飞起,成本比不优化低了一半以上。
4.3 成本核算:自建和调用的账怎么算
这是很多团队纠结的问题:到底自建推理服务,还是直接调用外部API?我算过一笔账,逻辑是这样的。
自建的成本包括:GPU租用或采购、运维人力、电费、闲置浪费。调用外部API的成本就是按token计费。关键变量是你的请求量和稳定性。
- 请求量小且波动大:调API划算,不用养机器。
- 请求量大且稳定:自建划算,单位成本低。
- 数据敏感:必须自建,没得选。
- 需要深度定制:自建,外部API给不了你微调后的模型。
我见过一个团队,日均请求量不大,但非要自建,结果GPU利用率长期低于10%,纯浪费。也见过请求量巨大的团队一直调API,账单高得吓人。算账要算总拥有成本,不是只看单价。
5. 线上稳定性保障:监控、告警与故障排查
5.1 必须监控的核心指标
服务上线不是终点,是起点。没有监控的线上服务等于裸奔。大模型API服务要盯的指标分几类:
性能类:
- 首token延迟(TTFT):用户感知最直接
- 每token输出延迟(TPOT):影响整体响应时间
- QPS和并发数:容量规划依据
- 批处理利用率:批处理是否跑满
资源类:
- GPU利用率:低于50%说明浪费,长期100%说明要扩容
- 显存占用:接近上限就要警惕OOM
- CPU和内存:服务框架本身的健康度
业务类:
- 请求成功率:低于99%就要查
- 超时率:突增说明有异常
- 队列等待时间:反映容量是否够
这些指标用Prometheus采集,Grafana展示,是标准组合。别自己造轮子。
5.2 常见故障与排查思路
我把实际遇到过的问题整理成一张速查表:
| 现象 | 可能原因 | 排查方向 | 解决手段 |
|---|---|---|---|
| 服务突然OOM | 并发超限、显存泄漏、批太大 | 看显存曲线和并发数 | 降批大小、加队列限制、重启 |
| 延迟突然飙升 | GPU被占满、请求堆积 | 看GPU利用率和队列长度 | 限流、扩容、优化批处理 |
| 输出乱码或截断 | 量化损失、tokenizer问题 | 对比BF16和量化输出 | 换量化方案、检查tokenizer |
| 部分请求超时 | 长请求拖累、调度不均 | 看请求长度分布 | 设单请求超时、长度限制 |
| 服务启动失败 | 权重格式不兼容、显存不足 | 看启动日志 | 换引擎、降量化精度 |
这张表是我踩坑踩出来的,每一条背后都是真实的深夜排查。遇到问题先看监控,别瞎猜。大部分故障在指标上都有预兆。
5.3 灰度发布与回滚机制
模型更新不能一把梭。新版本微调模型上线前,一定要做灰度。流程是:新模型部署成独立副本,切一小部分流量过去,对比新旧版本的效果和性能指标。没问题再逐步放量,有问题立即回滚。
回滚要能做到秒级。这意味着旧版本的权重和服务要保留着,别一上线就把旧的删了。我见过有人更新模型后发现问题,结果旧权重没留,只能干等重新部署,业务停了半小时。
注意:灰度不只是看技术指标,还要看业务指标。新模型可能延迟更低,但输出质量变差了,这种问题只有业务侧能发现。所以灰度期间要拉上业务方一起看。
6. 工程化落地的完整路径复盘
把整条链路串起来,一个标准的落地路径是这样的:
- 微调完成,产出权重文件,用原生Transformers验证效果,确认微调有效。
- 量化转换,根据显存和效果要求选AWQ或GPTQ,转换后重新评估。
- 推理引擎部署,选vLLM或TGI,加载量化权重,压测确定批处理参数。
- 服务框架封装,用FastAPI或Ray Serve包一层,加队列、限流、鉴权、流式输出。
- 监控接入,Prometheus采集指标,Grafana看板,设告警阈值。
- 灰度上线,小流量验证,对比新旧版本,逐步放量。
- 持续调优,根据线上数据调整批大小、量化策略、扩容缩容。
这条路径不是线性的,中间会反复。比如压测发现延迟高,可能要回去换量化方案;灰度发现效果差,可能要重新微调。这很正常,工程化本来就是迭代的过程。
我在实际项目里最大的体会是:别追求一步到位,先跑通再优化。很多团队卡在选型阶段,纠结vLLM还是TGI,纠结INT4还是INT8,结果一个月过去了服务还没上线。正确的做法是先用最稳妥的方案跑通全链路,哪怕性能不是最优,然后再针对性优化。跑通的价值远大于选型完美。
另外一个体会是:文档和配置要版本化。推理引擎的参数、量化配置、服务框架的配置,全部进Git。线上出问题时,能快速定位是哪个版本、哪个配置导致的。我吃过亏,改了个参数没记录,出问题后排查了半天才发现是那个改动。
最后分享一个小技巧:压测要用真实数据分布。很多人压测用固定长度的短请求,结果线上全是长请求,表现完全不一样。压测数据要从真实日志里采样,长度分布、并发模式都要贴近实际。这样压出来的结果才有参考价值。
这条链路我走过好几遍,每次都有新坑,但每次跑通后的那种踏实感是实打实的。模型微调是技术活,工程化落地是手艺活,两者都做好了,大模型才能真正变成业务能用的东西。