把线上 8B 换成一个"同尺寸新版":离线评测集分数高 3.2 个点,代码没改一行,配置只改了模型名。灰度 20 % 之后,P99 涨了 40 %,少量请求开始答非所问,还有一批请求在切换窗口里返回了空响应。
四个症状,四个不同的根因:P99 是双实例抢显存、答非所问是缓存键里没有模型版本、空响应是旧实例被杀时还有在途请求。它们共用一个特征——都被当成"换文件",而不是"换一套状态"。
换模型至少要一起换四样东西:权重、tokenizer、chat template、采样参数默认值。任何一样没对齐,都会表现为"新模型更差",而实际上是"新模型被旧假设套住了"。
1. 换模型换的是四样东西,不是一个文件
| 组件 | 变了会怎样 | 是否容易漏 |
|---|---|---|
| 权重 | 输出分布变,这是预期内的 | 否 |
| tokenizer | 同一个 prompt 的 token 数变,截断与 max_tokens 全部失准 |
是 |
| chat template | 角色标记、system 位置变,指令遵循度骤降 | 是 |
| 采样参数默认值 | temperature/top_p 默认值不同,风格漂移 | 是 |
第二行的连锁反应最隐蔽:max_input_tokens 是按旧 tokenizer 调的,新 tokenizer 对中文的切分更碎,同一个 prompt 多出 25 % token,于是原本"刚好不截断"的请求开始被截,而告警只会显示"输出质量下降"。
// 一次真实的差异(同一段中文 prompt,两种 tokenizer)
旧: 1,842 tokens → 留 4,000 预算,够
新: 2,310 tokens → 留 4,000 预算,够,但 + 检索 3,000 = 超
// 结论:换 tokenizer 等于改了所有人的预算表,必须重算,不能沿用
2. 双实例共存:灰度期容量按一半算
灰度不是"分流",是两套权重同时驻留。8B 的 FP16 约 16 GB,两张卡各跑一份,或者一张 80G 卡上挤两份——后者意味着 KV 池被切成两半,每份的可用并发只有原来的一半。
| 灰度比例 | 权重驻留 | KV 池 | 有效并发(8k 序列) |
|---|---|---|---|
| 0 %(切换前) | 1 份 | 60 GB | 58 |
| 20 % | 2 份 | 28 GB | 27 |
| 50 % | 2 份 | 28 GB | 27 |
| 100 %(旧下线后) | 1 份 | 60 GB | 58 |
注意第 2、3 行:20 % 与 50 % 的容量完全一样。灰度比例不改变资源占用,只改变流量分配。所以灰度期的容量规划必须按"两份权重"算,而不是按"20 % 的流量"算——这是最容易在容量评审里被漏掉的一行。
// 灰度期的三条硬约束,缺一条就别开
canary:
vram: { old: 16GB, new: 16GB, kv_pool_min: 28GB } // 两份权重 + 池
drain_grace: 180s // ≥ 最长生成时间,见第 3 节
cache_key_versioned: true // 见第 4 节,false 就会串味
rollback_window: 24h // 旧实例保留时长,不是"看着没问题就删"
3. 在途请求:宽限期按"最长生成"算
旧实例不能立刻杀。一个 max_tokens=2000 的请求,在 8B 上跑完要 20–60 秒;如果编排层宽限期只有 30 秒,它会在生成到一半时被 SIGKILL,返回空响应或半截 JSON。
| 参数 | 决定什么 | 常见错配 |
|---|---|---|
max_tokens |
单请求最长生成 | 设 2000,宽限期只给 30 s |
| TPOT(每 token 间隔) | 生成速度 | 高负载时翻倍,宽限期没跟着调 |
drain_grace |
编排层宽限期 | 小于上面两者之积 |
| 客户端超时 | 用户愿意等多久 | 比服务端短,用户先断 |
第四行经常被忽略:客户端 15 秒超时,服务端还在生成 60 秒的答案,用户早就断了,那个请求仍然占着显存跑完——它是纯浪费,还会在灰度期放大双实例的压力。
// 上线前的一次算术核对:宽限期必须覆盖最长生成
$ echo "max_tokens=2000, tpot_p99=35ms → $(python3 -c 'print(2000*0.035)')s"
70.0s
$ grep -r "stop_grace_period\|terminationGracePeriodSeconds" deploy/ | head
// 若小于 70s,先改它,再谈灰度
4. 缓存键里必须有模型版本
前缀缓存、KV 复用、语义缓存,共同的假设是"同样的输入 → 同样的中间状态"。换模型之后这个假设不成立:同样 token 序列在不同权重下对应的 KV 完全不同。
| 缓存类型 | 键里必须含 | 漏了会怎样 |
|---|---|---|
| 前缀缓存(KV) | model_id + 权重哈希 | 命中旧 KV,输出乱码或语义漂移 |
| 语义缓存(答案级) | model_id + 版本号 | 返回旧模型的答案,冒充新版本 |
| tokenizer 缓存 | tokenizer 哈希 | 分词错位,长度统计全错 |
| 模板缓存 | template 哈希 | 角色标记错,指令失效 |
第三行有个具体后果:分词器缓存命中旧版本时,prompt_tokens 统计会偏小,于是限流与预算全部按错的数算——这类错误在监控上表现为"token 用量突然下降",看起来像好事。
// 缓存键的最小组成:四个哈希拼起来,少一个都不算对
def cache_key(prompt, model, tok, tpl):
return "|".join([
model.id, model.weights_hash, // 权重变了 KV 就不通用
tok.hash, // 分词变了 id 序列就变
tpl.hash, // 模板变了前缀就变
sha1(prompt.encode()).hexdigest(),
])
// 上线核对:新模型第一批请求的 cache_hit 应该接近 0,而不是 60%
5. 回滚窗口:留多久才算够
"灰度看着没问题"不是回滚窗口的结束条件。真正的判据是新版本的失效是否已经被观测到,而推理类失效的观测周期天然更长。
| 失效类型 | 典型暴露时间 | 需要的观测 |
|---|---|---|
| 崩溃 / OOM | 分钟级 | 错误率、显存曲线 |
| 输出格式坏 | 小时级 | 解析失败率 |
| 语义漂移 | 天级 | 人工抽检、下游任务指标 |
| 长尾任务变差 | 周级 | 分任务类型的 A/B |
第三、四行是"离线分数高、线上变差"的来源:离线集覆盖不到长尾任务形状,而新模型恰好在那些形状上退化。所以回滚窗口不该按"几天没崩"来定,而该按"下游指标是否覆盖了全部分任务"来定。
// 一个可执行的回滚窗口口径
t0 上线 5% → 观察错误率与格式(小时级)
t0+1d 上线 20% → 开始人工抽检(每天 200 条,按任务类型分层)
t0+3d 上线 50% → 看下游任务指标是否同向
t0+7d 上线 100% → 旧实例仍需保留 48h 且可一键回切
// 任何一步出现"某个任务类型变差",回到上一档,不是"再观察观察"
6. 排错:五种"灰度后变慢/变怪"
| 现象 | 八成原因 | 处理方向 |
|---|---|---|
| P99 涨、QPS 没变 | 双实例把 KV 池切半 | 按两份权重重算容量 |
| 少量请求答非所问 | 缓存键缺模型版本 | 加 weights_hash |
| 切换瞬间空响应 | 宽限期短于最长生成 | 调 drain_grace |
| token 用量下降 | 旧 tokenizer 缓存命中 | 缓存键加 tok.hash |
| 新模型"指令遵循变差" | chat template 没换 | 对齐模板再评 |
第五行特别值得单说:模板没换的时候,新模型收到的 prompt 里角色标记是错的,它会按旧模型的习惯补全,看起来像"变笨了"。这类问题的修复成本极低(改一行模板),但定位成本极高,因为所有指标都指向"模型质量"。
7. 边界:什么情况不该热切
| 场景 | 为什么别热切 | 做法 |
|---|---|---|
| tokenizer 变更 | 所有预算/截断表要重算 | 停机窗口或双轨并行一周 |
| 量化方案变更(FP16→INT8) | 输出分布差异大,A/B 不可比 | 当成新模型,独立评测 |
| 上下文长度变化 | 缓存池与容量规划全变 | 先改容量,再切 |
| 参数规模变化(8B→70B) | 显存与并发是另一套账 | 新建实例组,不做热切 |
| 强 SLA 且无抽检能力 | 失效观测不到 | 灰度 + 人工抽检先行 |
判断口径一句话:当"新旧两版能不能共用同一套预算、缓存与模板"这个问题的答案是"不能"时,就不该热切,该做双轨。因为热切省下的只是运维动作,而它要求的前提恰恰是"四样东西都没变"——那在真实升级里几乎不成立。
8. 自测
- 换模型要一起换的四样东西是什么?哪一样最容易漏、后果最隐蔽?
- 为什么灰度 20 % 与 50 % 的容量需求相同?这对容量评审意味着什么?
- 宽限期该按什么算?客户端超时短于服务端会发生什么?
- 缓存键里为什么必须有权重哈希?漏掉它的两种表现是什么?
- 回滚窗口为什么不能按"几天没崩"来定?
- 什么情况下"热切"从根上就不成立?
9. 一句话总结
换模型是换一套状态,不是换一个文件:权重、tokenizer、模板、采样默认值任一变化都会让旧的预算表、缓存键与宽限期同时失效。三个必算的账——灰度期容量按两份权重算、宽限期按最长生成算、缓存键必须含权重哈希。判据只有一条:新旧两版能否共用同一套预算/缓存/模板;答案是不能,就该双轨而不是热切。