从“上线就崩”到稳如老狗:我的机器学习部署优化实战手记

大模型修路人
2025-12-19 00:49
阅读 2263

上周五晚上十一点半,我正瘫在工位上啃着冷掉的黄焖鸡,突然收到一条钉钉报警:“模型服务 P99 延迟飙到 2.3 秒”。当时心里一咯噔——这可是我们新上的推荐算法服务,明天就是运营大促,要是挂了,产品经理怕不是要提着保温杯来我工位泡枸杞。

我是阿哲,在这家公司干了三年多,去年刚升技术组长。说实话,带团队之后才发现,“能跑就行”的代码和“能扛住流量”的服务完全是两码事。以前写个 Flask 接口丢给运维就完事,现在得考虑 GPU 利用率、批处理吞吐、冷启动延迟……甚至还得跟运营同学解释为什么他们的 A/B 测试数据波动是因为特征 pipeline 没对齐。

最近半年,团队接了个活:把原来离线跑的推荐模型改成实时在线服务。目标很朴素:响应时间 < 200ms,99 分位 < 500ms,支持每秒 1000+ QPS。听起来不难?但等你真上手,就会发现 ML 部署是个深坑,比相亲对象的前任还多。

起因:别再拿 Jupyter Notebook 当生产环境了!

事情起源于去年双11。运营团队搞了个“猜你喜欢”活动,算法同事用 PyTorch 训了个双塔模型,本地测试 AUC 0.89,效果炸裂。结果一上线,直接 OOM,连带着整个 Kubernetes Pod 被 kill。运维老哥一脸懵:“你们这模型加载就要 4GB 内存?我们线上容器才配了 2G!”

更离谱的是,他们居然直接把 .ipynb 文件里的 model.predict() 封装成 REST API。没有 batch inference,没有模型缓存,每次请求都重新 load 模型……我当时看到代码真的想砸键盘。

后来复盘会上,CTO 直接点名:“ML 不是玩具,部署必须走工程化流程。”于是这个“脏活”就落到了我头上。毕竟我平时爱扒开源项目源码(比如 Triton Inference Server 和 BentoML),领导觉得我“看起来比较靠谱”。

踩坑实录:从模型格式到服务框架

第一坑:模型格式选型

一开始我们直接用 PyTorch 的 .pt 文件部署。结果发现,Python GIL 限制下,多线程根本跑不满 CPU。换成 TorchScript 吧,序列化倒是快了,但某些自定义算子不支持,报错:

RuntimeError: Exporting the operator xxx to ONNX is not supported.

最后咬牙转成 ONNX。虽然转换过程像在玩拼图(特别是带 Attention 的结构),但好处是推理引擎选择多了:TensorRT、ONNX Runtime、甚至能塞进 WebAssembly(虽然没用上)。

小贴士:如果你用 HuggingFace Transformers,记得加 torchscript=True 导出,不然动态 shape 会炸。

第二坑:服务框架乱斗

我们试过三种方案:

方案 QPS (P99<500ms) 内存占用 开发成本 备注
Flask + Gunicorn ~120 3.2 GB Python GIL 瓶颈明显
FastAPI + Uvicorn ~280 2.8 GB 异步好但模型加载仍是阻塞
Triton Inference Server ~1100 1.9 GB 需要 Docker + GPU 驱动

最终选了 Triton。虽然前期折腾 CUDA 版本和驱动兼容性差点让我秃头,但它的动态批处理(dynamic batching)和模型并发(model concurrency)功能简直是性能救星。配置一段 config.pbtxt 就能自动合并请求:

dynamic_batching {
  max_queue_delay_microseconds: 1000
  preferred_batch_size: [4, 8, 16]
}

意思是:最多等 1ms,如果队列里有 4/8/16 个请求就立刻打包推理。实测 QPS 提升 3 倍,P99 从 1.8s 降到 320ms。

区块链?别笑,它真能帮上忙

说到这儿你可能疑惑:标题里的“区块链”是凑数的?还真不是。

我们有个场景:运营团队需要审计模型决策过程。比如用户 A 为什么被推荐商品 X?传统做法是 log 特征 + 模型版本,但日志可能被篡改或丢失。

于是我们搞了个骚操作:把每次推理的关键信息(用户 ID、特征哈希、模型版本、输出概率)写入私有区块链(Hyperledger Fabric)。虽然性能有损耗(每笔交易多 15ms),但换来的是不可篡改的审计追踪。

# 伪代码:推理后上链
def predict_and_log(user_id, features):
    output = model.infer(features)
    tx = {
        "user": user_id,
        "feature_hash": hashlib.sha256(features).hexdigest(),
        "model_version": "v2.3",
        "score": output[0]
    }
    blockchain_client.submit(tx)  # 异步提交,不影响主路径
    return output

运营同学现在拿着链上数据做合规报告,老板看了直呼“高大上”。虽然我知道这玩意儿本质是分布式数据库,但谁让“区块链”三个字能打动投资人呢?(手动狗头)

性能优化三板斧

1. 特征预计算 + 缓存

模型推理快了,但特征拉取成了新瓶颈。用户特征来自 5 个微服务,串行调用要 300ms。我们做了两件事:

  • 离线预计算:夜间 ETL 把用户画像打成宽表,存 Redis。
  • 在线兜底:若缓存 miss,走异步补全,返回默认策略。

结果:特征获取 P99 从 310ms → 28ms。

2. 模型瘦身

原模型 128MB,加载慢。我们用了知识蒸馏(Knowledge Distillation),用大模型教小模型:

# 教师模型(复杂)
teacher = BERTLarge()
# 学生模型(轻量)
student = TinyBERT()

# 蒸馏损失 = 交叉熵 + KL散度(学生logits, 教师logits/温度)
loss = ce_loss + alpha * kl_div(student_logits / T, teacher_logits / T)

最终模型体积压到 23MB,CPU 推理速度提升 4 倍,AUC 只降了 0.015——运营完全无感。

3. 监控闭环

部署不是终点。我们接入了 Prometheus + Grafana,监控:

  • 模型输入分布偏移(用 KS 检验)
  • 推理延迟分位数
  • 错误率 & fallback 触发次数

一旦特征分布突变(比如某天突然全是老年用户),自动告警。上周就靠这个抓到一个数据管道 bug:运营同学误删了年轻用户标签。

心得:ML 工程师 ≠ 算法工程师

干了这摊子事后,我彻底明白:部署才是算法落地的最后一公里。再牛的 AUC,上线崩了等于零。

现在招人面试,我必问:“你上次部署模型时,怎么测 P99 延迟的?” 如果回答“本地跑了几百次取平均”,基本就 pass 了。

顺便说一句,我最近在看外面机会。倒不是对公司不满(团队氛围其实挺好,除了产品经理总在周五提需求),主要是想接触更大规模的 ML Infra。毕竟,谁不想亲手调优一个亿级用户的推荐系统呢?


后记:文章写完,运维老哥发来消息:“你那 Triton 服务昨晚扛住了 1500 QPS,P99 412ms。” 我回了个 😎,然后默默续了一杯咖啡——明天还得改 feature store 的 schema,运营说要加“用户最近浏览区块链新闻”的特征……

(全文完,共 2278 字。代码和配置均来自真实项目脱敏,如有雷同,纯属你也被坑过。)

评论 0

最热最新
暂无评论
大模型修路人Lv.1
0
影响力
0
文章
0
粉丝