从“模型跑得动”到“线上稳如狗”:一个奶爸程序员的机器学习部署实战复盘

CDN迷路人
2026-02-24 15:01
阅读 1428

上周五晚上十点半,两个娃终于睡了。我蹑手蹑脚关上儿童房门,打开 MacBook,准备继续捣鼓那个拖了三周的推荐模型上线任务。老婆在客厅小声提醒:“别熬太晚,明天还要送老大上学。”我苦笑点头——这大概就是当代打工人最真实的“深夜 coding”场景。

我在一家中型电商公司干了三年多后端开发,最近团队开始搞 AI 赋能,领导拍板要给首页商品推荐加上实时个性化能力。作为组里唯一啃过《分布式系统原理》还顺手跑过几个 Kaggle 模型的人,这活儿自然而然落到了我头上。一开始我以为就是“训练个模型 + Flask 部署”的老套路,结果踩了一路坑,差点把生产环境干崩。

更尴尬的是,最近正琢磨跳槽。简历刚投出去几份,就有猎头甩来一堆“机器学习部署经验”的面试题,比如:“你们怎么保证模型线上推理延迟低于 50ms?”、“如何做 A/B 测试和灰度发布?”——我当时心里一凉:纸上谈兵谁不会,真要讲清楚线上落地细节,还真得靠这次实战攒点干货。

今天这篇就聊聊,我是怎么把一个本地跑得欢的 PyTorch 模型,变成高可用、可监控、能扛住双11流量的线上服务的。全程用 VSCode(插件装了二十多个,光 Remote-SSH 就救我三次命),没上 Kubernetes(公司还在 Docker Swarm 时代),但思路绝对通用。


别再只关心准确率了!部署才是真正的炼狱

很多教程到 model.predict() 就戛然而止,仿佛世界就此和平。但现实是:模型上线 ≠ 能跑就行

我们最初直接用 Flask 包了个接口:

from flask import Flask, request
import torch

app = Flask(__name__)
model = torch.load('rec_model.pt')
model.eval()

@app.route('/predict', methods=['POST'])
def predict():
    data = request.json
    input_tensor = preprocess(data['user_id'], data['history'])
    with torch.no_grad():
        output = model(input_tensor)
    return {'recommendations': decode(output)}

本地测试秒出结果,美滋滋。结果一压测——QPS 超过 20,CPU 直接飙到 90%,响应时间从 30ms 蹿到 800ms。运维大哥在群里@我:“兄弟,你这是要干掉 Redis 吗?”

问题出在哪?三个致命伤:

  1. 每次请求都做预处理:用户行为序列转 embedding 的逻辑写在接口里,重复计算开销大;
  2. PyTorch 默认开启梯度计算:虽然用了 torch.no_grad(),但模型本身没做优化;
  3. 单进程单线程:Flask 开发服务器根本扛不住并发。

当时真的想砸电脑。但想到明天还要改简历、刷 LeetCode,只能深吸一口气:部署的本质,是工程化


第一步:模型瘦身 + 推理加速,让 Gemini 都夸你会优化

我们用的是自研的双塔召回模型,输入是用户行为序列和商品特征。训练时用的是完整 PyTorch,但线上推理完全不需要反向传播、动态图这些花里胡哨的东西。

于是祭出两大神器:TorchScriptONNX

先试试 TorchScript:

# trace 模式(适合无控制流的模型)
example_input = torch.randn(1, 50, 128)  # batch=1, seq_len=50, dim=128
traced_model = torch.jit.trace(model, example_input)
traced_model.save("rec_model_traced.pt")

加载时直接用 torch.jit.load,速度提升约 30%。但如果模型里有 if 分支(比如根据用户等级走不同逻辑),就得用 script 模式,手动加 @torch.jit.script 装饰器——这点特别坑,文档写得含糊不清。

后来干脆转 ONNX,通用性更强,还能塞进 Triton Inference Server(后面会说):

torch.onnx.export(
    model,
    example_input,
    "rec_model.onnx",
    input_names=["user_seq"],
    output_names=["scores"],
    dynamic_axes={"user_seq": {0: "batch", 1: "seq_len"}}  # 支持变长序列!
)

💡 小技巧:导出 ONNX 前务必用 torch.set_grad_enabled(False),否则可能残留梯度节点。

实测下来,ONNX Runtime(CPU 版)比原生 PyTorch 快了近 2 倍,内存占用降了 40%。关键是——它支持多线程并行推理!

顺便吐槽一句:现在有些大厂面试题直接问“ONNX 和 TorchScript 有什么区别”,答不上来的简历直接进回收站。所以我连夜补了这块,Gemini(Google 的那个大模型)还帮我对比了性能数据,确实靠谱。


第二步:别再裸奔!用 Triton 把服务武装到牙齿

Flask 太弱鸡,自己写 gunicorn + uvicorn 又容易翻车。最后我们上了 NVIDIA 的 Triton Inference Server——虽然是 GPU 方案,但它也完美支持 CPU,而且自带批处理、动态加载、指标监控。

部署流程如下:

  1. 把 ONNX 模型放进 models/rec/1/model.onnx
  2. 写个 config.pbtxt
name: "rec"
platform: "onnxruntime_onnx"
max_batch_size: 64
input [
  {
    name: "user_seq"
    data_type: TYPE_FP32
    dims: [ -1, 128 ]  # -1 表示动态维度
  }
]
output [
  {
    name: "scores"
    data_type: TYPE_FP32
    dims: [ 1000 ]  # 返回 top1000 商品得分
  }
]
dynamic_batching {
  preferred_batch_size: [ 8, 16, 32, 64 ]
  max_queue_delay_microseconds: 1000  # 最多等 1ms 凑批
}
  1. 启动服务:
docker run --rm -p 8000:8000 -p 8001:8001 -v $(pwd)/models:/models nvcr.io/nvidia/tritonserver:23.08-py3 tritonserver --model-repository=/models

客户端用官方 Python SDK 调用:

from tritonclient.utils import *
import tritonhttpclient

client = tritonhttpclient.InferenceServerClient(url="localhost:8000")
inputs = [tritonhttpclient.InferInput("user_seq", user_tensor.shape, "FP32")]
inputs[0].set_data_from_numpy(user_tensor)
outputs = [tritonhttpclient.InferRequestedOutput("scores")]
result = client.infer(model_name="rec", inputs=inputs, outputs=outputs)
scores = result.as_numpy("scores")

效果立竿见影:

方案 P99 延迟 (ms) QPS (4核) 内存 (MB)
Flask + PyTorch 780 18 1200
Flask + ONNX Runtime 320 45 750
Triton + ONNX 48 210 680

P99 压到 50ms 以内!运维大哥终于不再在群里艾特我了。


第三步:线上不是终点,可观测性才是护城河

模型跑得快还不够。上周上线后,产品突然找我说:“为什么张三看到的推荐全是奶粉?”——好家伙,数据漂移了!

所以必须加上全链路监控

  • 输入日志:记录原始请求(脱敏后),用于回溯问题;
  • 特征一致性检查:线上预处理逻辑必须和离线训练对齐;
  • 指标上报:QPS、延迟、错误率接入 Prometheus;
  • A/B 测试:通过网关分流,新旧模型并行跑一周。

我们在 Nginx 层做了流量切分:

upstream rec_v1 { server 10.0.0.10:8000; }
upstream rec_v2 { server 10.0.0.11:8000; }

map $cookie_abtest $backend {
    default rec_v1;
    "new"  rec_v2;
}

location /predict {
    proxy_pass http://$backend;
}

配合埋点,每天看 CTR、转化率变化。结果发现新模型虽然准确率高,但长尾商品曝光不足——赶紧加了个多样性约束,这才稳住。


给想跳槽的朋友:这些坑,简历上别写“熟悉部署”

最近面了几家,发现大厂对“机器学习工程化”要求极高。光写“使用 XGBoost 完成用户分群”已经不够看了,必须体现:

  • 模型版本管理:我们用 MLflow 记录每次训练的参数、指标、模型文件;
  • CI/CD 流水线:GitHub Actions 自动测试模型 + 构建 Triton 镜像;
  • 资源隔离:模型服务和核心交易系统分开部署,避免互相影响;
  • 降级策略:当模型超时,自动返回热门商品兜底。

有次面试官直接问:“如果线上模型预测结果突然全为 0,你怎么排查?”
我答:先看输入是否异常(比如用户 ID 为空),再查特征 pipeline 是否中断,最后看模型文件是否加载错误——他点点头:“看来真干过。”


结语:带娃的夜晚,代码也能闪闪发光

现在这个推荐服务已经稳稳跑了两个月,双11当天扛住 300 QPS 没抖一下。虽然过程曲折,但每次看到线上指标上涨,就觉得熬夜值得。

作为一个奶爸程序员,我没那么多时间追新框架、刷顶会论文。但我坚信:能把技术扎实落地的人,在哪儿都稀缺

如果你也在准备跳槽,别光堆砌“精通 TensorFlow”这种虚词。拿出你解决过的真实问题——哪怕只是把 Flask 换成 Triton,只要讲清楚 why 和 how,面试官眼睛都会亮。

最后,Gemini 帮我润色简历时说:“你的部署经验很有差异化。” 我笑了笑,心想:哪有什么天赋异禀,不过是娃睡了之后,多敲了几行代码罢了。

附:常用工具清单(奶爸亲测有效)

  • 模型格式转换:torch.onnx.export, tf2onnx
  • 推理引擎:ONNX Runtime, Triton Inference Server, TorchServe
  • 监控:Prometheus + Grafana(Triton 自带 metrics endpoint)
  • 版本管理:MLflow, DVC
  • 压测:wrk, locust

愿你的模型不仅 accuracy 高,P99 也低;愿你的简历不仅关键词多,故事也动人。
毕竟,这个世界需要的不是“会跑模型的人”,而是“能让模型在线上活下去的人”。

评论 0

最热最新
暂无评论
CDN迷路人Lv.1
0
影响力
0
文章
0
粉丝