Skip to content

常见陷阱与反模式:部署翻车现场合集

本页速览 部署翻车现场合集:训练/推理不一致、量化精度崩塌未发现、显存泄漏、无监控裸奔、一次性全量发布……每个坑都给出「症状→原因→解法」,全是血泪经验,附部署前 10 条自检清单。

模型部署的常见陷阱,是一批「症状看起来千奇百怪、根因翻来覆去就那几样」的经典事故模式。 为什么值得一篇专门的合集?因为部署事故有极强的规律性:团队踩的坑 80% 是同样的十来个,而每个坑从「线上出问题」到「定位根因」平均要消耗数小时到数天——提前知道这些模式,等于给排查装上「预置的怀疑列表」。这篇按「症状 → 原因 → 解法」结构收录十个最经典的坑与反模式,每个坑标注关联页面,最后给出部署前 10 条自检清单。配套的完整动手流程见 从零部署一个模型

阅读方式

先对症状,再查根因。 线上出问题时翻到对应条目,按「解法」里的排查路径走。读一遍 + 踩一遍,比背十遍有用。

坑① 训练/推理预处理不一致

  • 症状:离线验证集 AUC 0.92,上线后准确率断崖下跌 10-20 个点;排查代码、环境都「没区别」。
  • 原因:训练侧和推理侧对输入的处理不一致——常见三类:归一化参数不同(训练用 mean=[0.5,0.5,0.5],推理手写 x/255 忘减均值);图像通道顺序(训练 RGB,推理 BGR);文本 tokenizer 版本不同(训练用 tokenizers 旧版,推理换了新版)。模型对输入分布的假设被悄悄破坏。
  • 解法
    1. 预处理代码只写一份:定义在 preprocess.py,训练加载和推理加载同一个函数(见 从零部署一个模型 的做法);
    2. 归一化参数随模型走:导出时写进 meta.json,推理类读取,杜绝「两处硬编码」;
    3. 上线前做端到端一致性测试:同一张图,训练代码的输出 vs 服务接口的输出,np.allclose(atol=1e-4)

为什么「代码看着一样」还是不一样

最隐蔽的变体是顺序不同:训练先归一化再 resize,推理先 resize 再归一化——数值完全不等。比对用「数值级」断言,别用肉眼。

坑② 数值精度差异:引擎 FP16 与训练 FP32

  • 症状:用 TensorRT/Triton 加速后,个别请求结果错误、分数异常,整体精度略降但偶尔「离谱」。
  • 原因:推理引擎为了提速默认用 FP16(半精度),而训练是 FP32。FP16 的动态范围比 FP32 窄(最大 ~65504),归一化分布偏大或数值剧烈变化的模型(尤其 RNN、attention 的 softmax 前 logits)会溢出/掉精度。
  • 解法
    1. 引擎推理输出 vs 原框架输出做数值比对,设置允许误差(FP16 通常允许 1e-2 量级差异,差异到 1e0 就是问题);
    2. 关键层(如 softmax 前的 logits)保留 FP32 计算;
    3. 换引擎必须重新过一遍离线评估,FP16 的精度损失不会报错,只会悄悄劣化。格式与引擎差异见 模型格式与转换

坑③ 量化后精度崩塌未验证

  • 症状:模型「变快了 2 倍」,但线上转化率掉了 15%,回看才发现 INT8 模型从来没跑过验证集。
  • 原因:量化流程跑了,验证环节没跑——或是校准数据用错了(随机噪声/单类别),或是量化了本就敏感的层。INT8 把激活值压到 256 级,分布一错,误差放大成系统性偏差。
  • 解法
    1. 量化后必做三件事:体积确认(≈FP32 的 1/4)、验证集精度对比(掉点 > 5% 先怀疑校准数据)、延迟实测;
    2. 校准数据用真实分布的 200-1000 个样本,覆盖各类别;
    3. 把「量化后评估」固化成脚本,进 CI(完整流程见 模型优化实战)。原理见 量化

坑④ 显存泄漏与 OOM

  • 症状:服务跑 3 天后开始报 OOM、显存不足;重启后正常,几天后复发;P99 随时间缓慢爬升。
  • 原因:四类高发源——① 长连接/请求对象未释放(请求体、图片 bytes 持有引用);② 推理引擎缓存无界增长(TensorRT 的 workspace、ONNX Runtime 的 arena 配置不当);③ 每请求创建会话(InferenceSession 每请求 new 一个);④ 批处理队列无限积压。
  • 解法
    1. 会话全局单例:模型加载一次,所有请求共享(ONNX Runtime 会话线程安全,从零部署一个模型 里就是这么做的);
    2. 压测时长 ≥ 10 分钟并盯内存曲线(内存不下降就是泄漏信号,见 压测与容量规划);
    3. 容器设内存上限 + OOM 自动重启兜底,但要区分「泄漏」和「正常增长」;
    4. tracemalloc/nvidia-smi 定位持有者。

坑⑤ worker 数与模型重复加载

  • 症状:4 worker 的容器,显存/内存是预期的 4 倍;一台 16GB 卡只够跑 2 个 worker;模型加载变慢拖长启动。
  • 原因:进程模型 worker(如 uvicorn --workers 4每个进程独立加载一份模型。模型 3GB 时,4 worker 就是 12GB。新手常误以为 worker 共享模型内存。
  • 解法
    1. 先算账:worker 数 × 模型体积 ≤ 内存/显存的 70%;
    2. 模型小、QPS 低 → workers=2 甚至 1 + 异步足够;
    3. 模型大 → 用 shared memory / 单进程多线程架构,或把模型加载与请求进程分离(如 Triton 的模型并发实例由引擎管理,见 NVIDIA Triton 多模型服务);
    4. 启动时做模型预热(加载后先跑 1-2 个 dummy 请求),否则第一个真实请求延迟爆表。

坑⑥ 无监控裸奔:上线不带指标

  • 症状:上线即裸奔;两周后业务说「效果变差了」,你没有任何数据能回答「从哪天开始、差多少、哪类流量最差」。
  • 原因:上线前只做了功能验证,没埋指标、没接监控。模型系统还有一个特殊放大器:数据漂移——输入分布变了,模型效果自然退化,没有监控就永远发现不了(监控与可观测性 讲了漂移为何必须盯)。
  • 解法
    1. 上线即带黄金指标:RPS、P99、错误率、饱和度(GPU 利用率/排队数);
    2. 指标上线的验收标准是「up == 1 且有人看」,不是「metrics 端点能访问」;
    3. 漂移检测:输入分布监控(均值/方差/类别分布),阈值触发告警;
    4. 完整落地配置见 可观测性落地

坑⑦ 一次性全量发布:无灰度

  • 症状:新模型全量上线的第二天,客服工单爆了;想回滚,发现回滚命令没准备、旧镜像被覆盖了。
  • 原因:把「离线验证通过」当成了「上线安全的充分条件」。模型效果依赖线上数据分布,离线集再大也是抽样;全量发布等于把「验证」甩给了真实用户。
  • 解法默认金丝雀:5%→20%→50%→100% 逐档观察,自动回滚条件提前配置并演练。三层回滚(模型/特征/代码)版本配套。完整流程见 发布策略:灰度与回滚

坑⑧ 把在线推理当批处理用(或反之)

  • 症状(两个方向):
    • 在线方向:把 batch 预测接口当单条用——每次请求都送 batch=256 的输入,GPU 计算 256 条只返回 1 条,延迟和成本爆炸;
    • 批处理方向:用在线服务逐个处理 100 万条离线数据,跑了 3 天,把服务打挂。
  • 原因:混淆了两种工作负载的形态。在线推理(online)是低延迟、高并发、逐条响应;批处理(batch)是高吞吐、可排队、整体完成。两者的资源策略、超时设置、调度方式完全不同。
  • 解法
    1. 在线服务内部用动态批处理合并请求(Triton 的 dynamic batching 是引擎级解法,见 Triton 案例),而不是让调用方传大 batch;
    2. 离线大批量走批处理流水线(Argo/Celery/云批处理),独立于在线集群,见 批处理流水线案例
    3. 在线服务设置超时与限流,防止批处理流量拖垮在线响应(见 压测与容量规划 的容量模型)。

坑⑨ 缓存污染与一致性

  • 症状:A/B 或灰度期间新旧版本效果对比失真;用户反馈「我上一秒看到的结果,刷新后不一样还自相矛盾」。
  • 原因:缓存键没带模型版本。新旧模型共用同一个缓存键时,先请求的版本决定后请求的内容——灰度期间的「效果差异」可能只是缓存命中差异,不是模型差异。
  • 解法
    1. 缓存键纳入 model_versionfeature_version
    2. 灰度期间对易变缓存做隔离(新版本旁路缓存或用独立缓存前缀);
    3. 明确缓存内容与 TTL:结果缓存、特征缓存、tokenizer 缓存各自的失效策略;
    4. 一致性相关的完整讨论见 模型服务 的状态管理。

坑⑩ 压测数据与线上不符:热身不足、缓存虚高

  • 症状:压测说能扛 1000 QPS、P99 50ms,上线第一天真实流量 300 QPS 就 P99 500ms;或被问「压测数据怎么来的」答不上来。
  • 原因:三类典型——① 热身不足:刚启动就压,JIT/缓存未热,数字虚低或虚高;② 缓存虚高:压测请求全命中缓存(重复参数),真实流量零命中;③ 压测机成瓶颈:压测机先打满,测出的不是服务能力而是压测机能力。
  • 解法
    1. 正式记录前预热 30-60 秒;稳态场景压 ≥ 10 分钟;
    2. 压测请求参数随机化,单独测「无缓存命中」的真实路径;
    3. 记录环境四件套(压测机规格、worker 数、并发、时长),数字可复现——规范见 压测与容量规划

部署前 10 条自检清单

  • [ ] ① 预处理代码单份定义,归一化参数随模型走,一致性测试通过;
  • [ ] ② 换引擎后输出已与原框架数值比对,误差在预期范围;
  • [ ] ③ 量化后已验证集评估,掉点在业务阈值内;
  • [ ] ④ 内存/显存曲线 10 分钟压测平稳,无增长趋势;
  • [ ] ⑤ worker 数 × 模型体积算过账,且启动有模型预热;
  • [ ] ⑥ 黄金指标已埋点,up == 1,监控有人看;
  • [ ] ⑦ 发布走灰度,自动回滚条件配置并演练过;
  • [ ] ⑧ 在线/批处理负载分离,在线服务有超时与限流;
  • [ ] ⑨ 缓存键含版本,灰度期缓存隔离;
  • [ ] ⑩ 压测报告含环境、预热时长、参数随机化说明,结论可信。

检查清单

  • [ ] 能说出自己服务最容易踩的三个坑及对应预案;
  • [ ] 事故复盘按「症状→原因→解法」记录,沉淀进团队 wiki;
  • [ ] 每个坑的关联页面已收藏/熟读,需要时 10 分钟内查到解法;
  • [ ] 自检清单挂在发布流程里(PR 模板/发布单勾选项)。

延伸阅读

参考资料