2609 字
约 8 分钟
12
S23-A27 · 换模型的代价:热切换时的在途请求、缓存与回滚窗口

把线上 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. 自测

  1. 换模型要一起换的四样东西是什么?哪一样最容易漏、后果最隐蔽?
  2. 为什么灰度 20 % 与 50 % 的容量需求相同?这对容量评审意味着什么?
  3. 宽限期该按什么算?客户端超时短于服务端会发生什么?
  4. 缓存键里为什么必须有权重哈希?漏掉它的两种表现是什么?
  5. 回滚窗口为什么不能按"几天没崩"来定?
  6. 什么情况下"热切"从根上就不成立?

9. 一句话总结

换模型是换一套状态,不是换一个文件:权重、tokenizer、模板、采样默认值任一变化都会让旧的预算表、缓存键与宽限期同时失效。三个必算的账——灰度期容量按两份权重算、宽限期按最长生成算、缓存键必须含权重哈希。判据只有一条:新旧两版能否共用同一套预算/缓存/模板;答案是不能,就该双轨而不是热切。

S23-A27 · 换模型的代价:热切换时的在途请求、缓存与回滚窗口
https://www.clxhxhhr.top/posts/6727/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。