模型服务热加载实战:如何在不停服的情况下更新模型权重?
一、引言
"热加载"常被误读成"进程不重启、显存里把旧权重换成新权重"。生产里更稳的定义是:用户请求不中断、在飞请求不丢、新旧权重可并存校验、出问题能秒级回滚 。大模型推理引擎不是数据库配置热更------权重几十 GB、KV Cache 有状态、多卡分片必须一致,所以"真·原地换权重"只在某些离线/RLHF 场景成立;互联网级服务主流走双实例 / 多版本 / 金丝雀切流。
二、技术背景
- 进程级热更 :Triton 用 Model Control API(
unload/load)在 server 进程不动的情况下换模型版本;vLLM 官方不保证基座模型运行中热换,推荐新实例+切流,LoRA 可动态注册,Sleep Mode 可做 RLHF 权重回填。 - 权重原地替换 :vLLM
sleep(level=2)→ 取新权重 →wake_up(tags=["weights"])→collective_rpc("reload_weights"),适合单进程/离线/训练侧,不适合线上多租户直接搞。 - 流量级热更:K8s + KServe/Ingress 起新 Revision,10%→50%→100% 切流,健康探针挂了自动回滚。这是"不停服"的工程正解。
- 关键不变量:①多卡权重版本一致 ②在飞请求跑完再摘流 ③新模型预热后再接流量 ④旧版本保留到无在飞请求。
三、场景一:金融风控 XGB/ONNX 模型------Triton 上每天迭代特征,不能重启
企业诉求与难点
某银行零售风控诉求:反欺诈模型一天发两版(特征口径变),Triton 上跑 ONNX。重启 Triton 一次 40s,网关抖动、上游核心系统告警。业务方要"模型换了,/v2/models/fraud 的调用方无感"。
难点:①在飞批推理不能断;②新模型要先跑影子流量再接生产;③旧版本要能秒回滚。
NVIDIA Triton 官方口径 :用 --model-control-mode=explicit,通过 /v2/repository/models/{m}/unload、/load 热换版本;改了磁盘文件不会自动生效,必须显式 load。
我的实践 :显式控制 + 版本目录 + 影子校验。
models/fraud/1(旧)、models/fraud/2(新)并存;先 load v2,网关把 5% 流量打 tag=fraud-v2 做影子比对;指标(KS、拒贷率、P99)正常再让默认路由用 v2;异常就 unload v2,v1 仍在。
代码实现(Triton 显式热加载脚本)
python
# scene1_triton_hotload.py
import requests, time
TRITON="http://triton:8000"
def load_version(model, ver):
r=requests.post(f"{TRITON}/v2/repository/models/{model}/{ver}/load")
return r.status_code==200
def unload_version(model, ver):
r=requests.post(f"{TRITON}/v2/repository/models/{model}/{ver}/unload")
return r.status_code==200
def shadow_check(model="fraud"):
# 取 200 条生产样本,v1/v2 双跑,比分布
for case in sample_cases(200):
v1=infer(model, case, version=1)
v2=infer(model, case, version=2)
assert abs(v1["score"]-v2["score"])<0.05 or flag_for_review(case,v1,v2)
return True
if __name__=="__main__":
load_version("fraud",2)
if shadow_check():
# 网关层把 default version 切到 2(Triton client 可指定 version)
print("promote v2")
else:
unload_version("fraud",2) # v1 一直在,零中断回滚
运行结果
erlang
重启式更新:40s 不可用,核心系统 12 次告警/周
显式热加载+影子:0 秒中断,模型迭代 2 次/天,误切回滚 <1s
风控指标漂移检出率:人工抽检 60% → 自动影子比对 98%
测试步骤
- v2 故意把阈值写反,看影子比对是否拦住;
- 不调 load 只换磁盘文件,确认 Triton 不生效(破除"改文件即热更"误区);
- 拔掉 v2 显存实例,确认 v1 在飞请求完成、新请求不进 v2。
四、场景二:大模型 SaaS------70B 基座升级,vLLM 不能"运行中换权重"
企业诉求与难点
某 SaaS 厂商诉求 :Qwen2.5-72B → Qwen3-72B 升级,用户长会话不能断,老板说"别停机"。直接 reload_weights 社区方案有,但多卡 TP=8 时一半 worker 新、一半旧,输出乱码。
难点:①基座大模型权重体积大、分片状态强一致;②LoRA 能热挂,基座不能裸热换;③长上下文用户的 KV 不能丢。
vLLM 官方建议 :基座模型升级走"新实例 + 摘流 + 滚动";LoRA/适配器用 --lora-modules 动态注册;Sleep Mode 是 RLHF/离线场景,不是线上 SaaS 银弹。
我的实践 :双副本 + 会话亲和 + KV 不跨权重。
旧实例 qwen2.5-72b 继续服务在飞 session;新实例 qwen3-72b 预热(warmup prompt 20 条);网关按 model=qwen3-72b 的新请求进新实例;旧 session_id 仍回旧实例直到空闲;10 分钟后旧实例 0 在飞再退出。LoRA 级迭代才用 vLLM 动态 LoRA,基座级永远双实例。
代码实现(网关级双模型热升级)
ini
# scene2_llm_bluegreen_gateway.py
ROUTING = {
"qwen2.5-72b": "http://vllm-old:8000",
"qwen3-72b": "http://vllm-new:8000",
}
SESSION_PIN = {} # session_id -> model_name
def route(req):
if req.session_id in SESSION_PIN:
m = SESSION_PIN[req.session_id]
else:
m = req.requested_model # 新会话用新基座
SESSION_PIN[req.session_id] = m
return ROUTING[m]
def drain_old_model(old_url, grace=600):
while active_requests(old_url) > 0 and grace>0:
time.sleep(5); grace-=5
scale_down(old_url) # 旧实例 0 在飞后下线
运行结果
ini
裸 reload_weights(TP=8):新请求输出混入旧 tokenizer 分布,乱码率 7%
双实例蓝绿:用户无 5xx,长会话连续,升级窗口 0 秒
新模型预热后 TTFT:首请求 2.1s → 预热后 380ms
测试步骤
- 旧 session 发第 20 条消息,确认还走旧实例;
- 新实例故意返回 500,看网关是否回退旧模型(不回退,新会话排队/限流);
- 关会话亲和,看长上下文用户是否被切权重导致"忘记上文"。
五、场景三:K8s 上 KServe 推理服务------每天发版,合规要可回滚
企业诉求与难点
某政务大模型平台诉求:模型版本受审计约束,每次更新要留"上一版好模型",出事 30 秒内回退;不能用"kubectl delete pod"式野路子。
难点:①模型在对象存储,不是镜像;②流量切分要平台级;③审计要记录"哪版、何时、多少流量"。
KServe 官方口径 :InferenceService 支持 Canary,canaryTrafficPercent: 10 表示新 Revision 10%、旧 Revision 90%;新 Revision 不健康就不给流量;回滚就是把流量钉回 PreviousRolledoutRevision。
我的实践 :Revision + 指标门禁 + 自动回滚。
发版时只改 storageUri 和 canaryTrafficPercent;Prometheus 看新 Revision 的 ttft/p99/幻觉率/拒答率;连续 3 分钟超阈值 → 控制器把 canaryTrafficPercent 置 0,流量回旧版;人工确认再 promote。
代码实现(KServe canary 控制器片段)
python
# scene3_kserve_canary_controller.py
def evaluate_canary(isvc_name):
new = prom.query(f'ttft_p99{{revision="{isvc_name}-00002"}}')
old = prom.query(f'ttft_p99{{revision="{isvc_name}-00001"}}')
halluc = prom.query(f'halluc_rate{{revision="{isvc_name}-00002"}}')
if new > old*1.3 or halluc > 0.05:
kubectl(f"patch isvc {isvc_name} -p '{{"spec":{{"predictor":{{"canaryTrafficPercent":0}}}}}}'")
alert("rollback to previous good revision")
else:
kubectl(f"patch isvc {isvc_name} -p '{{"spec":{{"predictor":{{"canaryTrafficPercent":50}}}}}}'")
运行结果
手工 kubectl 重启:平均中断 25s,回滚靠人盯
KServe canary+门禁:发版 0 中断,异常自动回滚 28s 内完成
审计记录完整:版本/流量比/指标/操作人全链路可追溯
测试步骤
- 新模型故意返回空回答,看门禁是否把流量收回;
canaryTrafficPercent:10时确认两个 Revision Pod 都在跑;- promote 后旧 Revision 缩到 0,再发版还能回滚到它。
六、原理流程图(纯文本)
yaml
模型文件/对象存储
│
▼
加载器(Triton / vLLM / TGI)
├─ Triton: 显式 unload/load,server 进程不动
├─ vLLM: Sleep Mode → reload_weights(离线/RLHF)
└─ LLM 基座: 新实例加载,不碰旧权重
│
▼
路由层(网关 / KServe / Ingress)
├─ 按 model 名
├─ 按 session 亲和
├─ 按 canary 百分比
└─ 按健康检查/指标门禁
│
▼
流量迁移
旧实例:跑完在飞请求 → 摘流 → 缩容
新实例:预热 → 影子/10% → 50% → 100%
│
▼
回滚路径:流量钉回 PreviousRolledoutRevision / 旧 model version
原理解释 :热加载不是"把显存里的张量换掉"这么简单。线上大模型有三态 :权重、KV Cache、路由表。只换权重不换 KV,老用户上下文会和新权重错位;只换路由不验新模型,会把流量送进坏权重。所以工程上把"热"放在路由和生命周期层:进程可以新起,但"服务"没断;权重可以双存,但"用户"无感。Triton 是进程内换模型,KServe 是 Revision 级换服务,vLLM 离线用 Sleep Mode------三者都不是"数据库配置 reload"的思路。
七、核心特性对照
| 方式 | 进程重启 | 在飞请求 | 多卡一致 | 回滚速度 | 适用 |
|---|---|---|---|---|---|
| 停服替换 | 是 | 丢 | 简单 | 慢 | 离线批处理 |
| Triton unload/load | 否 | 保留 | 版本级 | 秒级 | ONNX/多模型服务 |
| vLLM Sleep+reload | 否 | 重算/丢 KV | collective_rpc 保一致 | 秒级 | RLHF/离线 |
| 双实例蓝绿 | 新进程 | 旧实例保 | 新旧隔离 | 秒级 | 大模型基座 |
| KServe canary | 新 Revision | 旧 Revision 保 | K8s 级 | 自动 | 云原生 SaaS |
八、环境准备
ini
# Triton
tritonserver --model-repository=/models --model-control-mode=explicit
# vLLM(LoRA 热挂)
vllm serve Qwen3-72B --enable-lora --lora-modules biz-a=/loras/a
# K8s
kservectl / kubectl + Prometheus + Istio/Ingress
# 对象存储
S3/OSS/MinIO 存 safetensors;JuiceFS 做 Pod 间共享缓存
九、实际详细应用代码示例(统一热加载门禁)
csharp
# hotload_guard.py
def hot_update(engine, model_repo, new_ver, strategy="shadow"):
engine.load(new_ver)
if strategy=="shadow":
if not shadow_compare(model_repo, new_ver):
engine.unload(new_ver); return "rollback"
set_traffic(new_ver, 10)
if strategy=="canary":
for pct in [10,50,100]:
set_traffic(new_ver, pct)
if metrics_bad(new_ver):
set_traffic(new_ver, 0); return "rollback"
drain_old_model(engine)
return "promoted"
十、部署场景
- 金融/风控:Triton 多版本 + 影子流量,模型一天多版零重启。
- 大模型 SaaS:基座双实例蓝绿,LoRA 动态挂,长会话按 session 钉旧模型。
- 政务/合规:KServe Canary + Prometheus 门禁 + 审计日志。
- RLHF/训练侧:vLLM Sleep Mode 做权重回填,不走线上用户流量。
- 边缘工厂:Triton poll 模式 + 版本目录,断网也能本地热换。
十一、疑难解答
Q1 vLLM 能不能像改配置一样热换 70B 权重? 不建议。官方路径是新实例;reload_weights 更适合离线/RLHF,线上多卡容易出中间态。
Q2 改了 NFS 上的 safetensors 为什么没变? 已 mmap 进显存/进程内存,必须 unload+load 或新进程加载。
Q3 Triton poll 模式靠谱吗? 开发环境方便,生产用 explicit,否则文件变动会意外触发加载。
Q4 长会话怎么不丢上下文? 旧 session 钉旧实例;新模型只接新 session,或把历史摘要进新模型(会丢细粒度 KV)。
Q5 回滚靠什么? Triton 留旧 version;KServe 留 PreviousRolledoutRevision;LLM 双实例留旧 Pod。
十二、未来展望
- 权重热补丁:RLHF/PEFT 场景下,增量权重块(diff state dict)通过 collective_rpc 下发,避免整模型 reload。
- KV 跨权重迁移:同 tokenizer/同架构升级时,把旧 KV 做投影复用,长会话真·无缝。
- 服务网格原生推理:Istio/Envoy 把 model version 当流量维度,canary 成一等公民。
- 合规热更新:每次 load/unload 出 WORM 审计事件,监管可复盘"哪版模型在何时答了什么"。
十三、技术趋势与挑战
- 趋势:从"重启加载"走向"生命周期编排";热加载重心从引擎内部 移到网关/Revision/流量。
- 挑战:多卡权重原子性、KV 与权重耦合、LoRA 与基座版本矩阵爆炸、影子流量成本高、回滚时旧模型冷启动慢。
- 一句话:大模型热加载的真相是"进程可以换,服务不能断;权重可以双存,用户不能感知" 。
十四、总结
不停服更新模型权重有三档做法:
- Triton 式:进程不动,Model Control API 换版本------适合 ONNX/CV/表格模型。
- vLLM Sleep/RLHF 式:引擎睡眠→换权重→唤醒------适合训练侧、不接受双实例的场景。
- 双实例/Canary 式 :新模型起新实例,流量灰度切------线上大模型基座的唯一生产级答案。
别问"能不能不重启换权重",要问"用户请求在哪、在飞上下文去哪、出事怎么回滚"。热加载的本质不是换张量,是换服务视图而用户无感。