1. 项目概述:这不是一次模型训练,而是一场工程交付
“From Notebook to Production: Running ML in the Real World (Part 4)”——这个标题里藏着一个被太多人轻描淡写、却让无数团队在临门一脚时彻底卡死的真相:Notebook 是思考的草稿纸,Production 是交付的合同书。它不讲怎么调参、不教怎么画 loss 曲线,它直指那个没人愿意多说但每天都在吞噬工程师时间的核心问题:当你在 Jupyter 里跑通了 accuracy 92.3% 的模型,下一步该把这串代码交给谁?用什么方式交?交过去之后,它会不会在凌晨三点因为一条脏数据崩掉,而你手机没响、告警没触发、业务方已经打电话来问“为什么推荐页全黑了”?
我做过 7 个从零到上线的机器学习服务,其中 4 个在模型准确率达标后,花了比训练周期长 2.3 倍的时间才真正稳定跑进生产环境。Part 4 这个编号很关键——它不是入门篇,不是原理篇,而是压轴的“交付实战篇”。它默认你已掌握模型开发(Part 1)、特征工程落地(Part 2)、模型监控基线(Part 3),现在要解决的是:如何让一个“能跑”的模型,变成一个“敢签 SLA”的服务。
核心关键词“Notebook to Production”背后,实际覆盖三个不可妥协的硬性要求:可复现性(Reproducibility)——今天在你本地跑的结果,和三个月后运维同事在 k8s 集群里拉起的镜像结果必须完全一致;可观测性(Observability)——不是只看 CPU 和内存,而是要实时知道特征分布是否漂移、预测置信度是否集体下滑、某类样本的延迟是否异常升高;可演进性(Maintainability)——当业务方下周突然要求增加“用户最近 30 分钟行为加权”,你能不能在不重启服务、不影响线上流量的前提下完成热更新?这三个词,就是 Part 4 的全部分量。它适合两类人:一类是刚把模型跑通、正对着部署文档发愁的算法工程师;另一类是被算法同学反复喊“再给我两天就能上线”、但已经等了三周的后端或 SRE 同事。这篇文章,就是给你们共同写的交接清单。
2. 整体设计思路:为什么放弃“一键部署”,选择“分层解耦”
很多团队在 Part 4 阶段会本能地走向两个极端:要么用 MLflow 或 Kubeflow 搞一套“全自动流水线”,结果半年过去 pipeline 跑得比模型还复杂,出了问题连日志都找不到在哪;要么干脆手写 Flask API + Gunicorn,模型 load 一次、全局变量存着,美其名曰“轻量”,实则成了线上最脆弱的单点故障。这两种方案,本质上都错在试图用“一个工具”解决“三层矛盾”:开发态与运行态的矛盾、模型逻辑与基础设施的矛盾、快速迭代与系统稳定的矛盾。
我们最终采用的方案是“三层解耦架构”,它不追求炫技,只确保每层职责清晰、替换成本可控、故障边界明确。第一层叫Model Serving Layer(模型服务层),它的唯一任务是:接收标准化输入(JSON/Protobuf)、执行模型推理、返回结构化输出。这里我们不用 Flask,而选了Triton Inference Server——不是因为它最新,而是因为它原生支持 PyTorch/TensorFlow/ONNX 多框架共存,且内置了动态批处理(dynamic batching)和模型版本热加载。举个真实例子:我们有个点击率预估模型,QPS 从 200 突增到 1800,Triton 自动将 batch size 从 1 提升到 32,GPU 利用率从 35% 拉到 89%,而整个过程对上游完全透明。第二层叫Feature Serving Layer(特征服务层),它和模型服务层物理隔离。我们用Feast构建,但做了关键改造:所有离线特征(如用户历史平均停留时长)走 Hive 批计算,所有实时特征(如用户当前 session 的点击序列)走 Flink 实时流,两者通过统一的 feature store ID 关联。这样做的好处是,当某天业务方说“把实时特征延迟从 1 秒放宽到 5 秒”,我们只需调整 Flink 的 watermark,模型服务层完全无感。第三层叫Orchestration & Observability Layer(编排与观测层),它不碰模型、不碰特征,只做三件事:流量路由(A/B 测试、灰度发布)、指标采集(延迟、错误率、特征统计)、异常告警(基于 Prometheus + Grafana + Alertmanager)。这一层我们用Argo CD + Kustomize管理 Kubernetes manifests,用OpenTelemetry统一埋点,拒绝任何“自研监控 SDK”。
为什么放弃“一键部署”?因为真正的生产环境没有“一键”。它有的是:凌晨两点,你发现模型延迟突增,需要快速回滚到上一版本;是新模型上线前,必须用 5% 流量验证效果,而不是全量切流;是某次特征更新导致线上 AUC 下降 0.8%,你需要 10 分钟内定位到是哪个特征桶的分布偏移了。这些场景,“一键”给不了你控制权,而分层解耦给你的,是每一层都能独立升级、独立压测、独立熔断的能力。就像汽车的发动机、变速箱、底盘,你可以单独更换涡轮、调校齿比、加固悬挂,而不必把整辆车送回工厂重造。
3. 核心细节解析:从 Notebook 到可部署模型的 5 个致命改造点
把 Jupyter 里的.ipynb文件直接扔进生产环境,就像把实验室的烧杯直接拿到化工厂反应釜里用——看着都是玻璃器皿,但承压、耐温、密封性、安全冗余,全都不在一个量级。我们在 Part 4 中强制执行了 5 项模型代码改造,缺一不可。这些不是“最佳实践”,而是我们踩过坑后定下的“红线”。
3.1 输入/输出接口必须脱离 Pandas,拥抱 Schema-first 设计
你在 Notebook 里写df = pd.read_csv("data.csv")很自然,但在生产里,这行代码会成为性能黑洞和兼容性雷区。Pandas DataFrame 在跨进程、跨语言、跨网络传输时,序列化开销大、版本兼容差(pandas 1.5 和 2.0 的 dtype 行为差异曾让我们线上服务连续报错 47 分钟)。我们的改造是:所有模型必须定义明确的 input schema 和 output schema,用 Pydantic v2 实现强类型校验。例如,一个用户画像模型的输入不再是pd.DataFrame,而是:
from pydantic import BaseModel, Field from typing import List, Optional class UserInput(BaseModel): user_id: str = Field(..., description="用户唯一标识,长度 16-32 字符") device_type: str = Field(..., pattern="^(ios|android|web)$", description="设备类型") recent_clicks: List[str] = Field(default_factory=list, max_length=50, description="最近点击商品 ID 列表") class ModelOutput(BaseModel): score: float = Field(..., ge=0.0, le=1.0, description="预测得分,0~1 区间") risk_level: str = Field(..., pattern="^(low|medium|high)$", description="风险等级")模型加载时,Triton 会自动将 JSON 请求反序列化为UserInput实例,并在传入模型前完成字段校验、类型转换、长度限制。一旦recent_clicks超过 50 个,请求直接 422 返回,不进模型、不占 GPU 显存。这个改动带来的收益是:API 响应时间 P95 从 128ms 降到 43ms,无效请求拦截率 100%,且前端同学再也不用猜“user_id 是字符串还是数字”。
3.2 模型加载必须剥离 I/O,实现“冷启动即热服务”
Notebook 里常见的model = torch.load("model.pth")在生产中是定时炸弹。它意味着每次服务启动都要从磁盘读取几百 MB 模型文件,如果磁盘 IO 不稳(比如云厂商的共享存储抖动),服务可能卡在 loading 状态长达 2 分钟,而 Kubernetes 的 liveness probe 已经连续失败 3 次,开始杀进程重启。我们的方案是:所有模型权重必须打包进容器镜像,且加载逻辑必须在__init__中完成,而非首次请求时懒加载。具体操作分三步:
- 在 Dockerfile 中,将
model.pth复制到/app/models/目录下; - 在 Triton 的 model repository 结构中,为每个模型版本创建
config.pbtxt,明确指定platform: "pytorch_libtorch"和max_batch_size: 32; - 编写
model.py,其initialize()方法在容器启动时即执行torch.load()并缓存到self.model属性。
提示:Triton 启动日志里会显示
Loading model 'user_score_v2' version 1,紧接着是Successfully loaded model 'user_score_v2'。这两行日志出现之间的时间差,就是你的冷启动耗时。我们要求这个时间必须 ≤ 800ms,超时则视为镜像构建失败,自动阻断 CI 流水线。
3.3 特征预处理必须与模型解耦,禁止嵌入模型代码
这是最隐蔽也最危险的坑。很多算法同学会把StandardScaler的fit_transform()直接写在模型forward()里,理由是“保证训练和推理一致”。但问题在于:fit_transform()需要访问全局统计量(均值、方差),而这些统计量在训练时是离线计算的,在线上推理时必须作为常量注入。如果混在模型里,你就无法单独更新 scaler 参数——比如某天发现用户年龄分布整体右移了 5 岁,你需要更新 age 的均值,但模型代码没改,你只能重新训练整个模型,哪怕只是换了个数字。我们的规范是:所有特征变换(归一化、One-Hot、分桶)必须由 Feature Serving Layer 完成,模型只接收已变换的数值向量。Triton 的config.pbtxt中,我们显式声明输入 tensor 的 shape 和 dtype:
input [ { name: "user_features" data_type: TYPE_FP32 dims: [128] # 固定 128 维特征向量 } ]这意味着,上游特征服务必须保证每次返回的user_features都是长度 128、dtype=float32 的 numpy array。模型代码里不再有任何scaler.transform(),只有纯粹的self.model(x)。这个改动让特征参数更新周期从“按模型迭代”缩短到“按小时级”,且完全不触发模型服务重启。
3.4 错误处理必须覆盖业务语义,拒绝裸抛异常
Notebook 里except Exception as e: print(e)能帮你 debug,但在生产里,它会让你失去所有上下文。当模型因输入user_id为空字符串而崩溃,Triton 默认返回500 Internal Server Error和一串 PyTorch 底层 traceback,运维同学看到的只有RuntimeError: expected scalar type Float but found Double,根本不知道是哪条请求、哪个字段、哪个业务方造成的。我们的做法是:在 Triton 的model.py中,用 try-catch 包裹整个execute()方法,并将业务语义错误映射为标准 HTTP 状态码:
def execute(self, requests): responses = [] for request in requests: try: # 解析输入 user_input = UserInput(**json.loads(request.get_input("INPUT0").as_numpy()[0])) # 业务校验 if not user_input.user_id.strip(): raise ValueError("user_id cannot be empty or whitespace") # 执行推理 features = self.feature_service.get(user_input.user_id) score = self.model(torch.tensor(features, dtype=torch.float32)) # 构建响应 output = ModelOutput(score=float(score), risk_level=self._map_risk(score)) responses.append([np.array(output.json(), dtype=object)]) except ValueError as ve: # 业务错误 -> 400 Bad Request responses.append([np.array(json.dumps({"error": "Invalid input", "detail": str(ve)}), dtype=object)]) except torch.cuda.OutOfMemoryError: # 系统错误 -> 503 Service Unavailable responses.append([np.array(json.dumps({"error": "GPU memory exhausted"}), dtype=object)]) except Exception as e: # 未知错误 -> 500,但附带 trace_id trace_id = str(uuid.uuid4()) logger.error(f"Unexpected error in model execute, trace_id={trace_id}", exc_info=True) responses.append([np.array(json.dumps({"error": "Internal error", "trace_id": trace_id}), dtype=object)]) return responses这样,前端收到 400 时知道是自己传参错了,收到 503 时立刻扩容 GPU,收到 500 时拿着trace_id直接查日志。错误不再模糊,责任不再推诿。
3.5 日志必须结构化、带上下文、可关联追踪
Notebook 里的print("Start inference for user:", user_id)在生产里毫无价值。它无法被 ELK 收集,无法按user_id过滤,更无法和上游请求、下游数据库操作串联成完整链路。我们的日志规范强制三点:
- 格式统一:全部使用 JSON 格式,字段包括
timestamp,level,service,trace_id,span_id,user_id,model_version,latency_ms,input_hash(输入数据的 SHA256 前 8 位); - 上下文注入:每个请求进入时,由网关生成
trace_id,并透传到 Triton;Triton 在execute()开头就记录"event": "inference_start",结尾记录"event": "inference_end",中间所有日志自动携带该trace_id; - 关键字段脱敏:
user_id在日志中显示为u_abc123...(保留前缀+哈希后缀),原始值仅存于加密日志库中,满足 GDPR 基础要求。
注意:我们禁用所有
logging.basicConfig()全局配置,每个模块必须用logger = logging.getLogger(__name__)获取命名 logger,并通过structlog绑定上下文。实测下来,当线上出现偶发性高延迟时,运维同学能在 Grafana 里点击某个 P99 延迟毛刺,直接跳转到对应trace_id的全链路日志,平均定位时间从 22 分钟缩短到 90 秒。
4. 实操过程:从本地验证到灰度发布的 7 步上线流程
Part 4 的价值,不在于告诉你“应该做什么”,而在于给你一份可逐字照抄、每一步都有检查点的《上线核对清单》。我们把整个流程拆成 7 个原子步骤,每个步骤都有明确的准入条件、执行动作、验收标准和回滚预案。这不是理论流程,而是我们过去 18 个月里,23 次模型上线的真实操作记录。
4.1 Step 1:本地沙箱验证(准入条件:Notebook 代码已提交 Git,feature branch 名为feat/prod-ready-v3)
这一步的目标是:在完全隔离的环境中,验证模型代码能否脱离 Notebook 环境独立运行。我们不用本地 conda 环境,而是用docker build --target dev-sandbox -t ml-model-sandbox .构建一个最小化镜像(基础镜像为nvidia/cuda:11.8.0-devel-ubuntu22.04,只装 PyTorch 2.0 和必要依赖)。镜像构建完成后,执行:
docker run --rm -v $(pwd)/test_data:/data -it ml-model-sandbox \ python /app/scripts/validate_local.py --input-path /data/sample_request.json --model-path /app/models/user_score_v3.pthvalidate_local.py脚本会:
- 加载
sample_request.json(一个符合UserInputschema 的真实样例); - 调用模型
forward()方法,捕获输出; - 对比输出是否符合
ModelOutputschema,且score在 [0,1] 区间; - 计算本次推理的
latency_ms,要求 ≤ 50ms(P95)。
验收标准:脚本返回exit code 0,且 stdout 输出✅ Local validation passed. Latency: 32.7ms。如果失败,必须修复代码,不得进入下一步。
4.2 Step 2:Triton 模型仓库集成(准入条件:Step 1 通过,且config.pbtxt已按规范编写)
这一步是模型服务化的起点。我们创建标准 Triton model repository 结构:
models/ ├── user_score_v3/ │ ├── 1/ │ │ └── model.py # Triton Python backend 代码 │ ├── config.pbtxt # 必须包含 platform, max_batch_size, input/output 定义 │ └── requirements.txt # 仅列出 Triton 运行时依赖,禁止放 torch/tf关键检查点有三个:
config.pbtxt中version_policy必须设为latest { num_versions: 1 },确保只加载最新版本;model.py的execute()方法必须返回List[np.ndarray],且每个 ndarray 的dtype和shape必须与config.pbtxt中声明的完全一致;requirements.txt中不能出现torch==2.0.1这类精确版本,只能写numpy>=1.21.0,<2.0.0,避免与 Triton 基础镜像冲突。
验证方式:启动本地 Triton 服务tritonserver --model-repository=./models --strict-model-config=false,然后用curl发送测试请求:
curl -d '{"inputs":[{"name":"INPUT0","shape":[1],"datatype":"BYTES","data":["{\"user_id\":\"u_abc123\",\"device_type\":\"ios\"}"]}]}' \ -X POST http://localhost:8000/v2/models/user_score_v3/infer验收标准:返回 HTTP 200,且响应 JSON 中outputs[0].data是合法的ModelOutput字符串。
4.3 Step 3:特征服务联调(准入条件:Step 2 通过,且 Feast feature repo 已同步最新特征定义)
这一步验证模型与特征服务的契约是否对齐。我们写一个feature_integration_test.py,它不调用 Triton,而是直接调用 Feast 的get_online_features()方法,获取与 Triton 输入完全一致的user_features向量,然后用本地加载的模型进行推理,对比结果是否与 Triton 一致。重点检查:
- 当
user_id不存在时,Feast 是否返回全零向量(而非报错); - 当某特征(如
age_bucket)值域超出训练时范围,Feast 是否按约定填充默认值(如 -1); - 特征向量长度是否严格等于
config.pbtxt中声明的dims。
我们曾在这里发现一个严重问题:Feast 的online_store配置中,redis_ttl设置为 3600 秒,但某天 Redis 内存满,部分 key 被 LRU 清除,导致特征返回空值,模型推理出 NaN。解决方案是:在 Feast 的get_online_features()调用后,强制添加np.nan_to_num(features, nan=0.0)。这个修复被写入团队 Wiki,成为所有新模型的强制检查项。
4.4 Step 4:压力测试与容量规划(准入条件:Step 3 通过,且 Prometheus 监控已接入 Triton metrics)
这一步决定你到底要买几块 GPU。我们不用 ab 或 wrk,而是用Locust编写真实业务场景的负载脚本:
# locustfile.py from locust import HttpUser, task, between import json import random class ModelUser(HttpUser): wait_time = between(0.1, 0.5) # 模拟真实请求间隔 @task def infer(self): # 随机选取 100 个真实 user_id(来自线上采样) user_id = random.choice(self.user_ids) payload = { "inputs": [{ "name": "INPUT0", "shape": [1], "datatype": "BYTES", "data": [json.dumps({"user_id": user_id, "device_type": random.choice(["ios","android"])})] }] } self.client.post("/v2/models/user_score_v3/infer", json=payload)测试分三轮:
- Baseline:50 QPS,持续 5 分钟,目标 P95 延迟 ≤ 60ms,错误率 0%;
- Peak:300 QPS(模拟大促峰值),持续 2 分钟,目标 GPU 利用率 ≤ 85%,无 OOM;
- Stress:500 QPS,持续 30 秒,观察是否触发自动扩缩容(KEDA + Triton HPA)。
验收标准:三轮测试全部通过,且 Prometheus 中nv_gpu_duty_cycle指标在 Peak 轮中未超过 90%。如果超标,则必须调整 Triton 的max_batch_size或增加 GPU 数量,重新测试。
4.5 Step 5:CI/CD 流水线打通(准入条件:Step 4 通过,且 GitLab CI 配置已就绪)
这一步把前面所有验证自动化。我们的.gitlab-ci.yml关键 stage 如下:
stages: - validate - build - test - deploy validate-local: stage: validate image: docker:20.10.16 script: - docker build --target dev-sandbox -t $CI_REGISTRY_IMAGE:sandbox . - docker run --rm $CI_REGISTRY_IMAGE:sandbox python /app/scripts/validate_local.py ... build-model-image: stage: build image: docker:20.10.16 script: - docker build --target triton-model -t $CI_REGISTRY_IMAGE:triton-$CI_COMMIT_TAG . - docker push $CI_REGISTRY_IMAGE:triton-$CI_COMMIT_TAG test-integration: stage: test image: python:3.10 script: - pip install feast tritonclient - python scripts/feature_integration_test.py --model-tag $CI_COMMIT_TAG deploy-to-staging: stage: deploy image: bitnami/kubectl:1.25 script: - kubectl set image deployment/ml-model-deployment model=$CI_REGISTRY_IMAGE:triton-$CI_COMMIT_TAG -n staging - kubectl rollout status deployment/ml-model-deployment -n staging --timeout=120s environment: staging only: - tags关键设计:所有测试必须在only: tags下触发,即只有打v3.1.0这样的语义化版本 tag 时,才执行完整流水线。日常开发用git push只触发单元测试,不构建镜像。这避免了“开发分支天天构建几百个镜像,占满 Harbor 存储”的悲剧。
4.6 Step 6:金丝雀发布(准入条件:Step 5 通过,且 staging 环境已验证 24 小时)
这一步是上线前的最后一道保险。我们不直接切 100% 流量,而是用Istio VirtualService实现 5% → 20% → 50% → 100% 的四阶段灰度:
apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: ml-model-vs spec: hosts: - ml-api.example.com http: - route: - destination: host: ml-model-service subset: v3-1-0 weight: 5 # 第一阶段:5% 流量 - destination: host: ml-model-service subset: v2-9-0 # 当前稳定版本 weight: 95每个阶段持续 30 分钟,期间 SRE 同学紧盯 Grafana 看板:
triton_inference_requests_total{model="user_score_v3"}是否与总流量同比例增长;triton_inference_errors_total{model="user_score_v3"}是否出现尖峰;feature_store_latency_seconds_bucket{feature="user_age_mean"}的 P99 是否稳定。
如果任一指标异常(如错误率 > 0.1% 或延迟 P95 > 100ms),立即执行kubectl patch virtualservice ml-model-vs -p '{"spec":{"http":[{"route":[{"weight":0},{"weight":100}]}]}}',100% 切回旧版本。整个过程,业务方无感知。
4.7 Step 7:生产环境全量与监控固化(准入条件:Step 6 四阶段全部平稳通过)
这一步不是结束,而是开始。全量发布后,我们执行三项固化动作:
- 更新文档:在 Confluence 的“模型服务手册”中,新增
user_score_v3条目,包含config.pbtxt全文、输入/输出 schema、SLA 承诺(P95 < 60ms, 99.95% 可用性)、负责人(@zhangsan); - 创建专属告警:在 Alertmanager 中,为
user_score_v3新建规则:- alert: MLModelLatencyHigh expr: histogram_quantile(0.95, sum(rate(triton_inference_request_duration_us_bucket{model="user_score_v3"}[1h])) by (le)) > 60000000 for: 5m labels: severity: warning annotations: summary: "User Score v3 latency > 60ms (P95)" - 归档训练 artifacts:将本次上线对应的
model.pth、feast_feature_repo_commit_hash、triton_config.pbtxt打包为user_score_v3-release-bundle.tar.gz,上传至 MinIO 的ml-artifacts/releases/目录,并生成 SHA256 校验码存入 Git。
至此,Part 4 的使命完成。模型不再是 notebook 里的一个变量,而是一个有身份证(版本号)、有户口(文档)、有社保(监控告警)、有紧急联系人(负责人)的正式服务成员。
5. 常见问题与排查技巧实录:那些没写在文档里的血泪经验
在 Part 4 的实践中,有些问题不会出现在官方文档里,但它们高频、致命、且往往让你在深夜三点对着日志抓狂。我把过去两年遇到的 7 个典型问题,连同排查路径、根因分析、永久解决方案,整理成这张速查表。这不是“可能遇到”,而是“你一定会遇到”。
| 问题现象 | 排查路径 | 根因分析 | 永久解决方案 | 我的实操心得 |
|---|---|---|---|---|
Triton 启动成功,但curl测试返回400 Bad Request,日志无有效信息 | 1.kubectl logs <triton-pod>查看启动日志;2.kubectl exec -it <triton-pod> -- ls /models/确认模型目录结构;3.kubectl exec -it <triton-pod> -- cat /models/user_score_v3/config.pbtxt检查语法 | config.pbtxt中input或output的name字段,与model.py中execute()方法里request.get_input("INPUT0")的字符串不匹配。Triton 不报错,但找不到对应 input,返回 400。 | 所有config.pbtxt必须用yamllint校验,且name字段必须与代码中硬编码字符串完全一致(大小写、下划线)。我们已在 CI 中加入grep -r "get_input" . | grep -o "INPUT[0-9]\+" | sort -u自动提取所有 input name,与 config.pbtxt 比对。 | 我第一次栽在这儿,花了 3 小时。后来写了个小脚本check_triton_config.py,输入模型目录,自动比对 config 和代码中的 input/output name,现在是每个 PR 的必检项。 |
| 压力测试时,GPU 利用率始终 ≤ 40%,QPS 上不去 | 1.nvidia-smi dmon -s u -d 1实时查看 GPU utilization;2.kubectl top pods查看 pod CPU/Mem;3.curl http://<triton-ip>:8002/v2/models/user_score_v3/stats查看 Triton 内部队列状态 | Triton 的dynamic_batching未生效。原因通常是config.pbtxt中max_batch_size设得太小(如 1),或preferred_batch_size未设置,导致 Triton 不敢合并请求。 | config.pbtxt中必须显式配置:dynamic_batching [ preferred_batch_size: [8, 16, 32] max_queue_delay_microseconds: 1000 ]且 max_batch_size至少为 32。我们通过locust脚本模拟不同 batch size 的请求,找到 P95 延迟最优的preferred_batch_size组合。 | 别迷信文档里的默认值。我们实测发现,对于 128 维特征的模型,preferred_batch_size: [16, 32]比[8, 16, 32]的 GPU 利用率高 12%,因为 8 这个档位太小,合并收益低。 |
线上服务偶发CUDA out of memory,但nvidia-smi显示显存充足 | 1.kubectl logs <triton-pod> | grep -i "out of memory";2.kubectl describe pod <triton-pod>查看 events;3.kubectl exec -it <triton-pod> -- nvidia-smi -q -d MEMORY查看显存详细分配 | Triton 的 Python backend 使用torch.jit.script加载模型时,会为每个模型实例预留显存池。当多个模型版本(v3.0, v3.1)同时加载,或同一模型有多个 instance(instance_group配置),显存被碎片化占用。 | 1. 在config.pbtxt中,为每个模型设置instance_group [ kind: KIND_CPU ](CPU 推理)或严格限制count: 1;2.永远不要在同一 Triton server 中混布 CPU 和 GPU 模型;3. 升级 Triton 到 23.06+,启用--cuda-memory-pool-byte-size=1073741824(1GB)显存池管理。 | 这个坑让我重启了 7 次服务。后来我们规定:GPU 模型独占一个 Triton Pod,CPU 模型用另一个 Pod,物理隔离。虽然多花点资源,但换来的是稳定性。 |
| 特征服务返回的向量,模型推理结果全是 NaN | 1.kubectl exec -it <triton-pod> -- python -c "import torch; print(torch.__version__)";2.kubectl exec -it <triton-pod> -- python -c "import numpy; print(numpy.__version__)";3. 用feature_integration_test.py打印特征向量的np.isnan(features).any() | Feast 的get_online_features()返回的 numpy array dtype 是float64,而 Triton 的 PyTorch backend 期望float32。类型不匹配导致 PyTorch 内部计算溢出,产生 NaN。 | 在model.py的execute()方法中,在torch.tensor(features)前,强制转换:features = features.astype(np.float32)。并在 Feast 的feature_view定义中,显式指定dtype=np.float32。 | 类型问题永远是第一怀疑对象。我们现在的feature_integration_test.py第一行就是assert features.dtype == np.float32,不通过直接 fail。 |
| 灰度发布后,新版本流量的 P95 延迟比旧版本高 200ms,但单机压测无差异 | 1.kubectl get endpoints ml-model-service -n prod查看 endpoints 列表;2.kubectl get pod -l app=ml-model -n prod -o wide查看 pod 分布;3.kubectl top nodes查看节点负载 | 新版本模型镜像体积比旧版大 2.3GB,导致 Kubernetes 在调度时,将新 pod 调度到了一台磁盘 IO 较差的老旧节点上。kubectl describe node <old-node>显示DiskPressure为 True。 | 1. 所有模型镜像构建后,必须docker history <image>检查层数和大小,禁止引入apt-get install等非必要包;2. 在deployment.yaml中,为 Triton Pod 添加nodeSelector,限定只调度到gpu-class: high-io的节点组;3. 为 |