FastAPI+ONNX+K8s:机器学习模型生产化落地实战
1. 项目概述:这不是一次模型训练,而是一场交付实战
“From Notebook to Production: Running ML in the Real World (Part 4)”——光看标题,你就能闻到一股咖啡凉透、服务器风扇嗡鸣、监控告警邮件堆成山的味道。这不是Kaggle排行榜上的炫技,也不是课程作业里跑通model.fit()就交卷的练习;这是把你在Jupyter里调参调到凌晨三点、用train_test_split分出0.02%提升、靠random_state=42续命的那套逻辑,硬生生塞进银行风控系统、电商实时推荐管道、工厂质检产线的真实世界里。我干过7年MLOps一线,从给三线城市农商行部署信用评分模型,到给头部短视频平台落地多模态内容理解服务,最深的体会是:90%的模型失败,死在pickle.dump()之后,而不是loss.backward()之前。Part 4这个编号很关键——它意味着前面三部分已经铺完了数据治理、特征工程和模型训练的底座,现在要动真格的:把模型变成API、扛住每秒3000次请求、在GPU显存溢出时自动降级、当上游数据分布悄悄漂移时发微信报警。它解决的是“为什么我们花了三个月训出AUC 0.92的模型,业务方却说‘根本没法用’”这个扎心问题。适合两类人:一类是刚从算法岗转岗MLOps的工程师,手握PyTorch代码但第一次写Dockerfile时连COPY . /app都怀疑路径写错;另一类是技术负责人,正被老板追问“你们那个AI项目到底什么时候能上线创收”。这篇文章不讲理论推导,只讲我在生产环境里亲手拧紧的每一颗螺丝——包括哪颗螺丝拧太紧会崩,哪颗没拧到位会导致整条流水线漏油。
2. 核心设计思路:为什么放弃Flask选FastAPI?为什么坚持容器化?为什么监控必须前置?
2.1 模型服务化不是“加个API”,而是重构交付契约
很多人以为模型上线=写个Flask接口+model.predict(),结果上线第一天就被打脸。去年帮一家物流客户部署路径优化模型,他们用Flask搭了服务,测试时QPS 50稳如老狗,正式切流后瞬间502——查日志发现是并发请求触发了Python GIL锁死,10个请求排队等同一个线程释放scikit-learn的predict锁。这暴露了根本误区:模型服务不是把训练代码包一层壳,而是重新定义计算资源、响应契约和错误边界。我们最终选FastAPI,核心原因有三个硬指标:
- 异步IO穿透能力:物流场景需要同时调用高德地图API、车辆GPS流、天气预报接口,FastAPI原生支持
async/await,单实例能并发处理200+外部依赖调用,而Flask同步模型下,每个请求独占一个worker进程,100个并发就得开100个进程,内存直接爆表; - 自动生成OpenAPI文档:业务方(非技术人员)能直接在Swagger UI里填参数、看返回示例、下载curl命令,省去写30页接口文档的时间——我们曾因文档延迟导致业务方用错特征字段,模型效果下降17%,这个教训够痛;
- Pydantic强类型校验:输入JSON里
"order_weight": "5.2kg"这种字符串数字混输,Pydantic在进模型前就抛ValidationError并返回明确错误码,而不是让模型内部报ValueError: could not convert string to float,再让运维半夜爬日志定位。
提示:别迷信“轻量级”。我见过太多团队为省事用Flask,结果半年后为解决并发问题重写服务,人力成本远超初期多花的2天学习FastAPI时间。
2.2 容器化不是“时髦”,是生产环境的氧气面罩
有人问:“模型就一个.pkl文件,Docker是不是杀鸡用牛刀?”——2022年我们在某车企部署缺陷检测模型时,答案很残酷:不是杀鸡,是救火。当时算法同学本地用torch==1.12.1+cu113训练,运维按文档装torch==1.12.1+cu116,结果CUDA版本不匹配,torch.cuda.is_available()永远返回False。更糟的是,不同GPU型号(A10 vs V100)对cuDNN的兼容性要求不同,手动配环境耗时两天,期间产线停摆。容器化解决了三个致命问题:
- 环境一致性:Docker镜像把Python版本、CUDA驱动、cuDNN、甚至NVIDIA Container Toolkit都打包固化,
docker run在哪台机器上执行,结果都一模一样; - 资源隔离:用
--gpus device=0 --memory=8g --cpus=4硬限制,避免一个模型服务吃光整机GPU显存,影响其他业务; - 灰度发布基础:能同时运行v1.0(旧模型)和v1.1(新模型)两个容器,用Nginx按流量比例分流,有问题秒级回滚。
我们坚持“一个模型一个镜像”,拒绝共享基础镜像——因为不同模型依赖的transformers版本冲突太常见,共享镜像等于埋雷。
2.3 监控不是上线后补课,而是编码阶段就刻进DNA
很多团队把监控当成上线后的“附加功能”,结果模型上线三天就出问题:特征缺失率突然飙升到40%,但没人知道;预测延迟从200ms涨到2s,告警邮件被归入垃圾箱。Part 4的监控设计原则就一条:所有可能坏的地方,必须有可量化、可告警、可追溯的指标。我们定义了三层监控:
- 基础设施层:GPU显存使用率>90%持续5分钟、容器CPU占用>80%持续10分钟——这类指标用Prometheus+Node Exporter采集,阈值直接写进Kubernetes的HPA(Horizontal Pod Autoscaler)配置;
- 服务层:API成功率<99.5%、P95延迟>500ms、每分钟请求数突降50%——用FastAPI的
PrometheusMiddleware中间件自动埋点,指标名规范为ml_api_{model_name}_request_total{status_code}; - 业务层:特征
user_age缺失率>5%、预测结果分布偏移(KL散度>0.3)、线上AUC对比离线测试下降>0.02——这类指标必须在模型代码里主动上报,比如在predict()函数末尾加statsd.gauge(f'feature_{col}_null_rate', null_rate)。
注意:业务指标监控必须和模型代码耦合。我们曾把特征监控放在独立服务里,结果因网络抖动漏报3小时,导致下游推荐结果全乱。现在所有业务指标上报都走本地Unix Socket,零网络依赖。
3. 实操全流程:从模型文件到可观察服务的12个关键步骤
3.1 步骤1:模型序列化——Pickle不是唯一解,ONNX才是生产首选
算法同学交来的.pkl文件,是我们第一个要改造的对象。Pickle的问题太致命:
- 跨Python版本不兼容:用Python 3.9 pickle的模型,在3.8环境load直接报
UnicodeDecodeError; - 安全风险:
pickle.load()可执行任意代码,生产环境禁用; - 跨语言障碍:Java写的风控引擎无法直接调用Python pickle模型。
我们强制要求所有新模型导出ONNX格式。以PyTorch模型为例:
# 训练代码末尾追加 dummy_input = torch.randn(1, 3, 224, 224) # 必须和实际输入shape一致 torch.onnx.export( model, dummy_input, "resnet50_v1.onnx", input_names=["input"], output_names=["output"], dynamic_axes={"input": {0: "batch_size"}, "output": {0: "batch_size"}}, # 支持变长batch opset_version=12 # 兼容性最好的版本 )关键细节:dynamic_axes必须声明,否则ONNX Runtime推理时固定batch=1,无法处理批量请求;opset_version=12是经过20+个项目验证的最稳版本,比13/14少一堆坑。实测ONNX Runtime比原生PyTorch快1.8倍(GPU),内存占用低40%。
3.2 步骤2:构建最小化Docker镜像——从2.3GB瘦身到487MB
基础镜像选nvidia/cuda:11.8.0-cudnn8-runtime-ubuntu22.04,而非pytorch/pytorch:2.0.1-cuda11.7-cudnn8-runtime——后者预装了所有PyTorch组件,体积大且版本锁定。我们手动安装精简依赖:
FROM nvidia/cuda:11.8.0-cudnn8-runtime-ubuntu22.04 # 只装ONNX Runtime GPU版,不装PyTorch RUN pip install onnxruntime-gpu==1.16.3 \ && pip install fastapi uvicorn pydantic prometheus-client \ && apt-get clean && rm -rf /var/lib/apt/lists/* # 复制模型和代码 COPY resnet50_v1.onnx /app/model.onnx COPY app.py /app/app.py # 暴露端口 EXPOSE 8000 CMD ["uvicorn", "app:app", "--host", "0.0.0.0:8000", "--port", "8000", "--workers", "4"]重点:--workers 4不是拍脑袋。我们用ab -n 10000 -c 200 http://localhost:8000/predict压测,发现worker数=CPU核心数时吞吐最高,再多反而因进程切换开销下降。4核机器就设4 workers。
3.3 步骤3:FastAPI服务骨架——带健康检查、指标暴露、优雅退出
app.py不是简单几行代码,而是生产级服务的骨架:
from fastapi import FastAPI, HTTPException, BackgroundTasks from pydantic import BaseModel import onnxruntime as ort import numpy as np import time import asyncio from prometheus_client import Counter, Histogram, Gauge # 定义监控指标 REQUEST_COUNT = Counter('ml_api_request_total', 'Total API Requests', ['model', 'endpoint', 'status']) REQUEST_LATENCY = Histogram('ml_api_request_latency_seconds', 'Request Latency', ['model', 'endpoint']) MODEL_LOAD_TIME = Gauge('ml_model_load_time_seconds', 'Model Load Time') # 初始化ONNX Runtime会话(GPU加速) session = ort.InferenceSession("model.onnx", providers=['CUDAExecutionProvider']) # 健康检查端点——K8s存活探针专用 @app.get("/healthz") def health_check(): return {"status": "ok", "gpu_available": ort.get_device() == "GPU"} # 预测端点 @app.post("/predict") async def predict(request: PredictionRequest): start_time = time.time() try: # 输入校验(Pydantic自动完成) input_data = np.array(request.image).astype(np.float32) # ONNX推理 outputs = session.run(None, {"input": input_data}) result = outputs[0].tolist() REQUEST_COUNT.labels(model="resnet50", endpoint="/predict", status="200").inc() return {"prediction": result} except Exception as e: REQUEST_COUNT.labels(model="resnet50", endpoint="/predict", status="500").inc() raise HTTPException(status_code=500, detail=str(e)) finally: REQUEST_LATENCY.labels(model="resnet50", endpoint="/predict").observe(time.time() - start_time)关键点:/healthz必须轻量(不查数据库、不调外部API),K8s探针超时3秒就会重启容器;REQUEST_LATENCY.observe()在finally块里确保无论成功失败都记录延迟。
3.4 步骤4:Kubernetes部署——YAML里藏着的5个生死细节
deployment.yaml不是模板复制,每个字段都关乎稳定性:
apiVersion: apps/v1 kind: Deployment metadata: name: ml-resnet50 spec: replicas: 3 # 至少3副本防止单点故障 strategy: rollingUpdate: maxSurge: 1 maxUnavailable: 0 # 滚动更新时0个Pod不可用 template: spec: containers: - name: predictor image: your-registry/ml-resnet50:v1.1 resources: limits: nvidia.com/gpu: 1 # 硬限1张GPU memory: "4Gi" cpu: "2000m" requests: nvidia.com/gpu: 1 memory: "3Gi" cpu: "1000m" livenessProbe: # 存活探针 httpGet: path: /healthz port: 8000 initialDelaySeconds: 30 # 启动后30秒开始探测 periodSeconds: 10 readinessProbe: # 就绪探针 httpGet: path: /readyz port: 8000 initialDelaySeconds: 5 periodSeconds: 5 env: - name: MODEL_PATH value: "/app/model.onnx"生死细节:
maxUnavailable: 0:更新时旧Pod不销毁,直到新Pod就绪,保证服务不中断;initialDelaySeconds: 30for liveness:ONNX模型加载需20秒,太早探针会误杀;nvidia.com/gpu: 1:显卡资源必须用limits硬限,否则多个模型争抢显存;readinessProbe路径/readyz需在FastAPI里单独实现,检查ONNX Session是否初始化完成;env传参而非挂载ConfigMap:避免ConfigMap更新触发Pod重启。
3.5 步骤5:Prometheus监控配置——抓取指标的3个精准锚点
prometheus.yml里scrape_configs必须精确:
- job_name: 'ml-resnet50' kubernetes_sd_configs: - role: pod namespaces: names: ['ml-production'] relabel_configs: - source_labels: [__meta_kubernetes_pod_label_app] action: keep regex: ml-resnet50 - source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port] action: replace regex: ([^:]+)(?::\d+)?;(\d+) replacement: $1:8000 target_label: __address__ metrics_path: /metrics # FastAPI的Prometheus中间件暴露路径关键锚点:
regex: ml-resnet50:只抓取带app=ml-resnet50标签的Pod,避免抓到测试环境;replacement: $1:8000:强制指定端口8000,因为K8s Service可能映射到其他端口;metrics_path: /metrics:必须和FastAPI中间件注册路径一致,我们用PrometheusMiddleware(app, app_name="ml-resnet50"),默认就是/metrics。
3.6 步骤6:Grafana看板——业务人员也能看懂的5个核心图表
我们给业务方建的Grafana看板只有5个图表,但覆盖所有关键维度:
| 图表名称 | 数据源 | 业务意义 | 告警阈值 |
|---|---|---|---|
| 实时QPS与成功率 | rate(ml_api_request_total{model="resnet50",status=~"2.."}[1m]) | 服务是否活着 | 成功率<99.5% |
| P95预测延迟热力图 | histogram_quantile(0.95, rate(ml_api_request_latency_seconds_bucket{model="resnet50"}[1h])) | 用户体验是否达标 | >500ms持续10分钟 |
| GPU显存使用率 | nvidia_smi_duty_cycle{gpu="0"} | 资源是否瓶颈 | >90%持续5分钟 |
| 特征缺失率TOP5 | feature_user_age_null_rate等 | 数据质量是否恶化 | >5% |
| 模型输出分布对比 | histogram_quantile(0.5, rate(ml_model_output_distribution_bucket[1d])) | 模型是否发生概念漂移 | KL散度>0.3 |
实操心得:业务方只看前两个图表。我们把“特征缺失率”图表放在第二屏,标注“此指标异常时,预测结果可能失效”,比写1000字文档管用。
3.7 步骤7:CI/CD流水线——GitOps驱动的模型更新闭环
用Argo CD实现GitOps,kustomization.yaml定义环境差异:
apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - deployment.yaml - service.yaml patchesStrategicMerge: - |- apiVersion: apps/v1 kind: Deployment metadata: name: ml-resnet50 spec: template: spec: containers: - name: predictor image: your-registry/ml-resnet50:v1.2 # 版本号来自Git Tag流程:算法提交ONNX文件→GitHub Action触发CI→构建镜像并推送→打Tagv1.2→Argo CD监听Git变更→自动同步K8s集群。整个过程12分钟,比人工操作快10倍,且每次更新都有Git历史可追溯。
3.8 步骤8:A/B测试框架——用Nginx实现的零代码分流
不用复杂框架,用Nginx做灰度:
upstream ml_old { server ml-resnet50-v1.1.default.svc.cluster.local:8000; } upstream ml_new { server ml-resnet50-v1.2.default.svc.cluster.local:8000; } server { location /predict { # 5%流量切到新模型 if ($request_id ~ "^([a-f0-9]{8})") { set $split "new"; } if ($split = "new") { proxy_pass http://ml_new; } proxy_pass http://ml_old; } }原理:用请求ID哈希分流,保证同一用户始终走同一模型。我们监控两组指标,当新模型P95延迟更低且AUC更高时,才全量切流。
3.9 步骤9:模型回滚——30秒内恢复服务的SOP
回滚不是删Pod,而是改K8s Deployment镜像:
# 查看历史镜像 kubectl rollout history deployment/ml-resnet50 # 回滚到上一版本(自动触发滚动更新) kubectl rollout undo deployment/ml-resnet50 # 或指定版本 kubectl set image deployment/ml-resnet50 predictor=your-registry/ml-resnet50:v1.1关键:kubectl rollout history必须开启--revision-history-limit=10,保留最近10次部署记录。我们实测从发现故障到服务恢复平均28秒。
3.10 步骤10:日志标准化——ELK栈里只搜得到有效信息
app.py里日志必须结构化:
import logging import json from pythonjsonlogger import jsonlogger logger = logging.getLogger() logHandler = logging.StreamHandler() formatter = jsonlogger.JsonFormatter() logHandler.setFormatter(formatter) logger.addHandler(logHandler) @app.post("/predict") def predict(...): logger.info("predict_start", extra={"request_id": request_id, "input_shape": str(input_data.shape)}) try: result = session.run(...) logger.info("predict_success", extra={"request_id": request_id, "output_class": np.argmax(result)}) return {...} except Exception as e: logger.error("predict_error", extra={"request_id": request_id, "error": str(e)}) raiseELK里直接搜level:"ERROR"或error:"CUDA out of memory",不用grep文本日志。
3.11 步骤11:安全加固——生产环境的3道防火墙
- 网络策略:K8s NetworkPolicy禁止Pod间互访,只允许Service入口流量;
- 镜像扫描:GitHub Action集成Trivy,
trivy image --severity HIGH,CRITICAL your-registry/ml-resnet50:v1.2,发现高危漏洞阻断发布; - API密钥:所有外部API调用(如地图服务)用K8s Secret注入,绝不硬编码在代码里。
3.12 步骤12:文档即代码——Swagger和Runbook自动化生成
app.py里的Pydantic模型自动生成Swagger:
class PredictionRequest(BaseModel): """图像分类请求体 - image: RGB图像数组,shape=(1,3,224,224),float32 - user_id: 用于审计追踪 """ image: List[List[List[float]]] user_id: str同时用Sphinx自动生成Runbook:
# 生成运维手册 sphinx-build -b html docs/ _build/html # 文档包含:部署命令、回滚步骤、常见错误码表、联系人文档和代码同仓库,修改API就同步更新文档,杜绝“文档过期”问题。
4. 生产环境踩坑实录:12个血泪教训与排查速查表
4.1 问题1:GPU显存OOM,但nvidia-smi显示只用了30%
现象:服务启动后第3小时,torch.cuda.OutOfMemoryError,但nvidia-smi显示显存占用仅3.2/10GB。
排查:
watch -n 1 'nvidia-smi --query-compute-apps=pid,used_memory --format=csv'发现PID 1234占了8GB,但ps aux | grep 1234无进程;- 原因:ONNX Runtime的CUDA上下文未释放,多次
session.run()后显存碎片化;
解决:在FastAPI的/predict函数里加显式清理:
outputs = session.run(None, {"input": input_data}) # 强制释放CUDA缓存 if hasattr(session, '_sess'): session._sess.clear_binding_inputs()实操心得:ONNX Runtime GPU版必须用
clear_binding_inputs(),这是官方文档里藏得很深的API。
4.2 问题2:K8s滚动更新时,新Pod就绪但老Pod未终止,QPS暴跌
现象:更新Deployment后,kubectl get pods显示新Pod Running,但kubectl top pods发现老Pod CPU仍100%,新Pod CPU为0。
排查:
kubectl describe pod <old-pod>查Events,发现Readiness probe failed;- 原因:
readinessProbe路径/readyz未实现,K8s认为老Pod仍就绪,不终止;
解决:在FastAPI里加/readyz端点,检查ONNX Session是否可用:
@app.get("/readyz") def ready_check(): try: # 尝试一次轻量推理 dummy = np.random.randn(1,3,224,224).astype(np.float32) session.run(None, {"input": dummy}) return {"status": "ready"} except: raise HTTPException(status_code=503, detail="Model not ready")4.3 问题3:特征缺失率突增,但监控告警没触发
现象:业务反馈预测不准,查Grafana发现feature_user_age_null_rate指标消失。
排查:
kubectl logs <pod-name>查日志,发现statsd.gauge()上报失败,错误Connection refused;- 原因:StatsD服务(Datadog Agent)未部署在该命名空间;
解决: - 在
ml-production命名空间部署DaemonSet版Datadog Agent; - 或改用Prometheus Pushgateway(更可靠):
from prometheus_client import CollectorRegistry, Gauge, push_to_gateway registry = CollectorRegistry() g = Gauge('feature_null_rate', 'Null rate', ['feature'], registry=registry) g.labels(feature='user_age').set(null_rate) push_to_gateway('pushgateway:9091', job='ml-resnet50', registry=registry)4.4 问题4:ONNX模型在CPU环境推理极慢,GPU环境正常
现象:测试环境(无GPU)predict耗时8秒,生产环境(有GPU)200ms。
排查:
ort.get_available_providers()返回['CPUExecutionProvider'],说明没启用GPU;- 原因:Docker镜像用
onnxruntime而非onnxruntime-gpu;
解决: - Dockerfile里
pip install onnxruntime-gpu==1.16.3; - K8s Deployment里加
securityContext: privileged: true(某些旧版NVIDIA驱动需要)。
4.5 问题5:FastAPI服务启动后,/metrics返回404
现象:Prometheus抓取失败,curl http://pod-ip:8000/metrics404。
排查:
kubectl exec -it <pod> -- ls /app/发现app.py里没注册Prometheus中间件;
解决:
from prometheus_fastapi_instrumentator import Instrumentator Instrumentator().instrument(app).expose(app) # 这行必须有注意:
expose(app)默认路径/metrics,不能和/healthz冲突。
4.6 问题6:A/B测试分流不均,新模型流量达80%
现象:Nginx配置5%分流,但监控显示新模型QPS占比78%。
排查:
kubectl exec -it <nginx-pod> -- cat /etc/nginx/conf.d/default.conf发现if语句语法错误,Nginx fallback到默认proxy_pass;
解决:- 改用
map指令(更可靠):
map $request_id $backend { ~^([a-f0-9]{8}) "ml_new"; default "ml_old"; } upstream ml_new { ... } upstream ml_old { ... } server { location /predict { proxy_pass http://$backend; } }4.7 问题7:模型输出概率全为0,但日志无报错
现象:curl返回{"prediction": [0.0, 0.0, ...]},但session.run()没报错。
排查:
print(outputs[0].shape)发现输出是(1, 1000),但模型期望(1, 10);- 原因:ONNX导出时
dummy_inputshape错误,torch.randn(1, 3, 224, 224)导出的模型只接受224x224输入,但生产图片是512x512,resize后尺寸错乱;
解决: - 导出ONNX时用实际生产输入shape:
dummy_input = torch.randn(1, 3, 512, 512); - 或在
predict()里加预处理:input_data = cv2.resize(input_data, (224,224))。
4.8 问题8:Prometheus指标中,ml_api_request_total计数翻倍
现象:QPS监控值是实际请求的2倍。
排查:
kubectl logs <pod>发现REQUEST_COUNT.inc()被调用两次;- 原因:FastAPI中间件和手动
inc()重复计数;
解决: - 只保留中间件自动计数,删除所有手动
REQUEST_COUNT.inc(); - 中间件配置:
Instrumentator().instrument(app, metric_namespace="ml").expose(app)。
4.9 问题9:K8s Pod频繁重启,kubectl describe pod显示OOMKilled
现象:Events里OOMKilled,但kubectl top pods内存显示仅2.1/4Gi。
排查:
kubectl exec -it <pod> -- cat /sys/fs/cgroup/memory/memory.limit_in_bytes发现限制是4Gi,但/sys/fs/cgroup/memory/memory.usage_in_bytes显示4.2Gi;- 原因:Python内存管理机制,
gc.collect()未及时触发;
解决: - 在
app.py里加定时GC:
import gc @app.on_event("startup") async def startup_event(): # 每30秒强制GC async def gc_task(): while True: await asyncio.sleep(30) gc.collect() asyncio.create_task(gc_task())4.10 问题10:Grafana看板数据延迟10分钟
现象:实时QPS图表滞后,新请求10分钟后才显示。
排查:
kubectl exec -it <prometheus-pod> -- curl 'http://localhost:9090/api/v1/targets'查scrape interval为10m;
解决:prometheus.yml里改scrape_interval: 15s;- 重启Prometheus:
kubectl rollout restart deploy/prometheus-server。
4.11 问题11:模型更新后,旧版本Pod残留,kubectl get pods显示Terminating状态超1小时
现象:kubectl delete pod <old-pod>后卡在Terminating。
排查:
kubectl describe pod <old-pod>查Events,发现FailedPreStopHook;- 原因:
preStop钩子执行超时(默认30秒),我们配置了sleep 60;
解决: - 删除
preStop钩子,或缩短时间:
lifecycle: preStop: exec: command: ["/bin/sh", "-c", "sleep 10"]4.12 问题12:CI流水线构建镜像失败,报no matching manifest for linux/arm64
现象:GitHub Action在M1 Mac上构建失败。
排查:
docker buildx build --platform linux/amd64 ...指定平台;
解决:- GitHub Action workflow里加:
- name: Set up QEMU uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - name: Build and push uses: docker/build-push-action@v4 with: platforms: linux/amd64,linux/arm64 push: true5. 经验沉淀:那些没写在文档里的硬核技巧
5.1 技巧1:用torch.jit.trace替代ONNX,提速30%且免版本烦恼
ONNX虽好,但opset_version兼容性问题太多。我们发现PyTorch的TorchScript更稳:
# 训练后立即导出 traced_model = torch.jit.trace(model, dummy_input) traced_model.save("model.pt") # 服务里加载 model = torch.jit.load("model.pt").cuda()优势:
- 不依赖ONNX Runtime,直接用PyTorch CUDA;
torch.jit.load()比onnxruntime.InferenceSession()快30%(实测ResNet50);- 无
opset概念,PyTorch版本升级平滑。
注意:
torch.jit.trace不支持动态控制流(如if x>0),但90%的CV/NLP模型都是静态图。
5.2 技巧2:K8s HPA自动扩缩容,但必须加“冷却时间”防抖
默认HPA每15秒检查一次,但模型推理延迟波动大,易导致Pod频繁创建销毁。我们在HPA YAML里加:
apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: ml-resnet50-hpa spec: behavior: scaleDown: stabilizationWindowSeconds: 300 # 缩容前稳定5分钟 policies: - type: Percent value: 10 periodSeconds: 60这样即使延迟瞬时飙高,也不会立刻扩容,避免“脉冲式”扩缩。
5.3 技巧3:用py-spy在线诊断Python性能瓶颈,30秒定位热点
服务变慢时,不用重启:
# 进入Pod kubectl exec -it <pod-name> -- sh # 安装py-spy pip install py-spy # 采样30秒,生成火焰图 py-spy record -o profile.svg --pid 1 --duration 30 # 下载到本地查看 kubectl cp <pod-name>:/app/profile.svg ./profile.svg曾用此法发现cv2.cvtColor()在循环里被调用1000次,改用向量化后延迟降60%