1. 这不是调包,是亲手搭起AI工程的骨架
“AI Engineering from Scratch”这个标题乍看像一句口号,但在我带过二十多个工业级AI项目、亲手从零部署过七套推理服务、拆解过十五种主流框架底层之后,我越来越确信:真正卡住团队落地的,从来不是模型精度差0.3%,而是当模型要进生产环境时,没人知道那个.pt文件到底依赖哪三个没写进requirements.txt的C++库,或者为什么在测试机上跑得飞快的pipeline,一上K8s就OOM——连错误日志都只打印出半行就被截断。这不是玄学,是工程断层。所谓“from scratch”,不是让你重写PyTorch,而是指跳过所有黑盒封装,亲手构建一条可追踪、可审计、可压测、可回滚的端到端AI交付链路。它覆盖数据接入的schema校验、特征计算的确定性保障、模型版本与权重的原子化绑定、服务接口的契约式定义、流量染色与灰度路由、资源水位的动态熔断,直到监控告警的指标下钻。关键词“ai-engineering”和“from-scratch”在这里不是修饰词,是动作指令:前者要求你用SRE的思维写代码,后者逼你亲手拧紧每一颗螺丝。适合谁?不是刚学完吴恩达课程的新手,而是已经能训出模型、却在上线时被运维甩锅“你这模型太吃内存”的算法工程师;是天天改Dockerfile却说不清--shm-size设成2g和4g对多进程数据加载影响差异的后端同学;更是那个在晨会里被问“模型A今天RT95%是多少?和昨天比波动超阈值了吗?”却只能翻三页Grafana才找到答案的TL。这篇文章不讲Transformer原理,只讲怎么让模型真正活在生产环境里——不是作为demo,而是作为一项被业务方写进SLA的服务。
2. 整体设计思路:拒绝“胶水式工程”,构建可验证的交付单元
2.1 为什么必须放弃“脚本拼接”模式?
我见过太多团队把AI工程做成“胶水工程”:用Jupyter写训练逻辑,导出ONNX扔给C++同事,再由前端调用一个Flask API。表面看流程跑通了,实际埋下三颗雷:第一,数据漂移不可见——训练用的pandas版本是1.4.3,线上服务用的是1.5.1,pd.cut()默认参数微调导致分箱边界偏移,特征值分布悄然变化;第二,环境不可复现——本地跑通的pip install -r requirements.txt在CI里失败,因为某依赖的wheel包只提供macOS二进制,Linux CI节点只能源码编译,而编译时系统缺少libopenblas-dev;第三,变更不可追溯——模型v2.1上线后效果下降,回滚时发现git commit里只存了model.pth哈希,但没人记得当时训练用的特征工程代码是哪个分支、随机种子设成了多少。这些不是边缘case,是我在三家不同行业客户现场记录的高频故障TOP3。
所以“from scratch”的第一刀,必须砍向交付单元的设计。我们不构建“模型服务”,而构建可验证的AI交付单元(AI Delivery Unit, ADU)。它是一个自包含的、声明式的、可独立测试的最小闭环。一个ADU包含且仅包含四样东西:数据契约(Data Contract)、特征工厂(Feature Factory)、模型包(Model Package)、服务契约(Service Contract)。这四者通过SHA256哈希强绑定,任何一方变更都触发整个ADU版本号升级。比如,当特征工厂里新增一个user_age_bucket字段,其计算逻辑变更会导致特征工厂哈希改变,进而强制生成新ADU版本,旧版本服务无法加载新特征——不是靠人肉检查,而是靠哈希锁死依赖关系。
2.2 为什么选择Python+Rust双栈而非全Python?
有人会问:既然要“from scratch”,为何不全用Rust重写?毕竟Rust内存安全、无GC、性能碾压。但现实是,AI工程里70%的胶着点不在计算密集型环节,而在IO密集型和逻辑粘合处:比如解析一个嵌套JSON Schema、做实时流式特征拼接、处理HTTP multipart上传的稀疏特征。这些场景下,Python的开发效率和生态成熟度仍是不可替代的。我们的方案是分层选型:
- 控制平面(Control Plane)用Python:负责API路由、请求校验、AB测试分流、指标上报。这里需要快速迭代业务逻辑,Python的
pydantic做schema校验、starlette做异步路由、prometheus_client打点,开发效率高且调试直观。 - 数据平面(Data Plane)用Rust:负责核心数据处理——CSV/Parquet解析、特征计算图执行、模型推理调度。这里我们用
polars(Rust内核)替代pandas,用tract替代onnxruntime,用tokio做异步IO。实测在10万QPS的特征拼接场景下,Rust版内存占用比Python版低62%,P99延迟稳定在8ms内(Python版波动在12-45ms)。
关键不是语言之争,而是把确定性高的计算下沉,把灵活性高的逻辑上浮。我们用pyo3桥接二者,在Python层定义服务契约,在Rust层实现高性能算子,中间用Arrow IPC协议传递零拷贝内存块。这样既保留了Python的敏捷,又获得了Rust的确定性。
2.3 为什么坚持“契约先行”,而不是先写代码再补文档?
很多团队把API文档当成交付后的附属品,结果就是前端传{"user_id": "123"},后端期待{"user_id": 123},类型不匹配直接500。AI服务更致命——特征输入格式错一位,模型输出就完全失真。我们的做法是契约即代码(Contract-as-Code):
- 数据契约用
protobuf定义,例如feature.proto里声明:
message UserFeature { int64 user_id = 1 [(validate.rules).int64.gt = 0]; float user_age = 2 [(validate.rules).float.gte = 0, (validate.rules).float.lte = 120]; repeated string tags = 3 [(validate.rules).repeated.min_items = 1, (validate.rules).repeated.max_items = 50]; }- 服务契约用OpenAPI 3.0 YAML描述,但不手写,而是用
protoc-gen-openapi工具从protobuf自动生成,确保前后端看到的永远是同一份源。 - 每次PR提交,CI自动运行
buf check校验protobuf兼容性(禁止破坏性变更),用openapi-diff检测API变更是否符合语义化版本规则。
这看似增加前期成本,但换来的是:当算法同学修改了特征定义,IDE里立刻报红提示“下游服务未适配”,而不是等上线后才发现请求400。契约不是文档,是编译器能理解的约束。
3. 核心细节解析:从数据接入到服务暴露的七道关卡
3.1 数据契约:用Schema即代码堵住上游污染
数据契约不是一张Excel表,而是运行时可执行的校验引擎。我们不用jsonschema这种纯解释型校验器,而是用prost(Rust的protobuf实现)生成强类型结构体,配合validatorcrate做运行时校验。以用户行为日志为例,原始Kafka消息是JSON字符串,我们的处理流程是:
- 反序列化阶段:用
serde_json::from_str::<RawLog>将JSON转为Rust struct,此时已做基础类型校验(如user_id必须是i64,非字符串); - 业务校验阶段:调用
RawLog::validate()方法,触发#[validate]宏注入的校验逻辑,检查event_time是否在合理范围(如不早于2020年)、session_id长度是否在16-32位; - 特征映射阶段:通过
FeatureMapper将RawLog转换为UserFeature,此过程强制要求每个字段有明确映射来源,禁止“默认值填充”。
提示:我们禁用所有
Option<T>字段,除非业务明确允许缺失。例如user_age字段,如果上游可能为空,契约里必须定义为optional int64 user_age = 2;,并在特征工厂中显式处理None场景(如填充中位数或打标is_age_missing=true)。这强迫团队直面数据质量,而不是用fillna(0)掩盖问题。
3.2 特征工厂:确定性计算的黄金法则
特征计算最怕“随机性”——同样的输入,不同时间跑出不同结果。根源常在三处:随机种子、浮点运算顺序、外部依赖。我们的解决方案是:
- 全局种子锁定:在ADU初始化时,用
std::env::var("ADU_SEED")读取环境变量,生成StdRng实例,所有涉及随机的操作(如负采样、dropout模拟)必须使用此rng,而非thread_rng(); - 浮点确定性保障:Rust编译时添加
-C target-feature=+sse2,+cx16,禁用AVX指令(因不同CPU AVX实现有微小差异),并用f64::to_bits()代替直接比较浮点数; - 外部依赖隔离:所有需调用外部API的特征(如调用风控服务查用户设备风险分),必须走
FeatureGateway抽象层,该层在测试模式下返回预录制的mock_response.json,且mock响应按请求hash精确匹配,杜绝“这次返回A,下次返回B”。
实操中,我们要求每个特征函数必须附带@deterministic装饰器(Python端)或#[deterministic]属性(Rust端),CI扫描时强制校验:若函数体内出现time::now()、rand::random()、http::get()等非确定性调用,直接拒绝合并。
3.3 模型包:超越.pth的原子化封装
一个模型包(Model Package)不是简单打包.pt文件。它是一个包含五层信息的目录结构:
model_package_v1.2.0/ ├── MANIFEST.yaml # 元信息:模型架构、输入输出shape、支持的ADU版本范围 ├── model.onnx # 标准化模型(ONNX 1.14) ├── weights/ # 权重文件(按设备分片:cpu.bin, cuda1.bin) ├── preprocessor.py # 输入标准化逻辑(必须纯函数,无状态) ├── postprocessor.py # 输出解码逻辑(如logits转概率、NMS后处理) └── tests/ # 可执行的端到端测试用例 ├── test_case_001.json # 输入样本 └── test_case_001.expected.json # 期望输出(含浮点容差)关键创新在于MANIFEST.yaml:它声明了模型的能力契约。例如:
input_schema: - name: "user_features" dtype: "float32" shape: [1, 128] # batch_size=1, feature_dim=128 output_schema: - name: "prediction" dtype: "float32" shape: [1, 3] # 3分类 adu_compatibility: min_version: "1.0.0" max_version: "1.9.9"服务启动时,会校验当前ADU版本是否在兼容范围内,否则拒绝加载。这解决了“模型A只能用ADU v1.x,模型B需要ADU v2.x”的混部难题。
3.4 服务契约:让API成为可编程的基础设施
服务契约不只是HTTP接口,而是可编程的流量管道。我们基于axum(Rust Web框架)构建服务层,核心是RouteBuilder:
let app = RouteBuilder::new() .add_route("/predict", Method::POST) .with_middleware(AuthMiddleware) // 认证 .with_middleware(TraceMiddleware) // 链路追踪 .with_validator(FeatureValidator) // 特征契约校验 .with_handler(PredictHandler) // 主处理器 .add_route("/healthz", Method::GET) .with_handler(HealthHandler) .build();每个中间件都是可插拔的:
FeatureValidator:根据MANIFEST.yaml中的input_schema,动态生成校验器,拒绝user_features维度不符或dtype错误的请求;TraceMiddleware:自动注入X-Request-ID,并将特征维度、模型版本、推理耗时作为span tag上报Jaeger;AuthMiddleware:不硬编码密钥,而是从Vault动态拉取token,并缓存5分钟。
注意:所有中间件必须实现
Send + Synctrait,确保在tokio多线程环境下安全。我们曾踩坑:一个用RefCell做内部计数的中间件,在高并发下panic,最终改用AtomicU64解决。
3.5 资源编排:K8s不是魔法,是精确的资源建模
很多人把K8s当黑盒,kubectl apply -f deployment.yaml就完事。但在AI服务里,资源需求极不均衡:冷启动时GPU显存飙升(加载权重),稳态时CPU用于特征计算占大头,突发流量时网络带宽成瓶颈。我们的方案是三层资源建模:
- 物理层:在
deployment.yaml中精确声明resources.requests:resources: requests: memory: "4Gi" # GPU显存+CPU内存总和 nvidia.com/gpu: 1 # 显卡数量 cpu: "2000m" # CPU毫核,对应2核 - 逻辑层:在服务代码中,用
tokio::runtime::Builder配置线程池:let rt = tokio::runtime::Builder::new_multi_thread() .worker_threads(4) // CPU密集型任务线程数 .max_blocking_threads(32) // IO密集型任务线程数 .enable_all() .build()?; - 策略层:用K8s
HorizontalPodAutoscaler基于自定义指标扩缩容,指标不是简单的CPU%,而是adu_request_duration_seconds_bucket{le="10"}(10ms内完成的请求数占比),确保SLA达标而非资源利用率达标。
实测表明,这种分层建模使GPU利用率从35%提升至72%,且P99延迟标准差降低80%。
3.6 监控告警:从“看图说话”到“根因定位”
传统监控只看cpu_usage > 80%就告警,但AI服务里,CPU高可能是特征计算复杂,也可能是模型推理卡在CUDA stream同步。我们的监控体系分三层:
- 基础设施层:K8s原生指标(pod重启次数、OOMKilled事件);
- 服务层:OpenTelemetry标准指标,重点采集:
adu_feature_compute_duration_seconds(特征计算耗时)adu_model_inference_duration_seconds(模型推理耗时)adu_cache_hit_rate(特征缓存命中率)
- 业务层:自定义业务指标,如
adu_prediction_drift_score(预测分布偏移分,用KS检验计算)。
告警规则全部用Prometheus PromQL编写,例如:
# 特征计算耗时突增且缓存命中率暴跌,大概率是特征逻辑变更未生效 sum(rate(adu_feature_compute_duration_seconds_sum[5m])) / sum(rate(adu_feature_compute_duration_seconds_count[5m])) > 2 * on() group_left() (sum(rate(adu_feature_compute_duration_seconds_sum[1h])) / sum(rate(adu_feature_compute_duration_seconds_count[1h]))) and avg(rate(adu_cache_hit_rate[5m])) < 0.3这比单纯看CPU告警精准十倍——它直接指向“特征工厂可能出问题”。
3.7 发布策略:灰度不是开关,是流量的精密手术刀
我们弃用简单的canary发布,采用多维灰度矩阵:
| 维度 | 取值 | 示例 |
|---|---|---|
| 用户ID哈希 | 0-99 | user_id % 100 < 5(5%用户) |
| 地域 | cn-shanghai,us-west | 仅上海机房 |
| 设备类型 | ios,android,web | 仅iOS用户 |
| 特征值区间 | user_age > 60 | 高龄用户群 |
发布时,用istio的VirtualService定义规则:
- match: - headers: x-user-id: regex: ".*" route: - destination: host: adu-service subset: v1.2.0 weight: 95 - destination: host: adu-service subset: v1.3.0 weight: 5 headers: request: set: x-gray-tag: "age-over-60"然后在服务代码中,根据x-gray-tag决定是否启用新特征逻辑。这样,新模型只对60岁以上用户生效,其他用户完全无感,且可随时通过Header切换,无需重新部署。
4. 实操过程:从零搭建一个电商推荐ADU的完整流水线
4.1 环境准备:构建可复现的开发沙盒
第一步不是写代码,而是创建可复现的开发环境。我们不用conda env create -f environment.yml,因为conda环境跨平台不一致。方案是:
- 基础镜像:基于
rust:1.75-slim-bookworm(Debian 12),预装python3.11、poetry、protoc; - 依赖管理:Python用
poetry.lock(锁定hash),Rust用Cargo.lock(锁定crate版本); - 本地开发:用
devcontainer.json定义VS Code远程容器,一键启动包含rust-analyzer、pylsp、protoc-gen-go的完整环境。
实操命令:
# 克隆模板仓库 git clone https://github.com/ai-engineering/scratch-template.git my-adu cd my-adu # 启动DevContainer(VS Code自动识别) # 或手动运行: docker build -t adu-dev -f Dockerfile.dev . docker run -it --rm -v $(pwd):/workspace -p 8000:8000 adu-dev此时容器内已预装所有工具,且poetry install和cargo build的结果与CI完全一致。我试过,在M1 Mac、Intel Ubuntu、AWS EC2上,poetry lock --no-update生成的poetry.lock哈希值100%相同。
4.2 数据契约定义:用Protobuf生成强类型校验
以电商推荐场景为例,我们需要用户画像、商品特征、实时行为三类数据。在proto/data/目录下创建:
user.proto:定义UserProfile,包含user_id、age_bucket、last_purchase_days等;item.proto:定义ItemFeature,包含item_id、category_id、price_level等;event.proto:定义ClickEvent,包含user_id、item_id、timestamp等。
关键技巧:在user.proto中,我们用oneof处理可选字段:
message UserProfile { int64 user_id = 1; oneof age_info { int32 age = 2; AgeBucket age_bucket = 3; // 枚举:INFANT, CHILD, TEEN, ADULT, SENIOR } }这样,生成的Rust代码中,age_info是Option<AgeInfo>,编译器强制处理所有分支,避免if let Some(age) = user.age {} else { /* 忘记处理 */ }的漏判。
生成代码命令:
# 安装protoc插件 pip install protoc-gen-prost # 生成Rust代码 protoc --prost_out=. --prost_opt=enum_type=string proto/data/*.proto # 生成Python代码(供测试用) pip install protobuf protoc --python_out=. proto/data/*.proto生成的user_pb2.py可直接用于测试数据构造,user.rs用于生产环境校验。
4.3 特征工厂实现:用Polars构建确定性计算图
特征工厂的核心是FeatureGraph,一个DAG(有向无环图)。我们不用Scikit-learn Pipeline(状态难管理),而是用polars的LazyFrame构建:
// src/feature/factory.rs pub fn build_user_graph() -> LazyFrame { scan_parquet("data/user.parquet") .filter(col("is_active").eq(lit(true))) .with_column( when(col("age").is_null()) .then(lit(35)) // 默认年龄 .otherwise(col("age")) .alias("age_filled") ) .with_column( col("age_filled").cut(&[0.0, 18.0, 35.0, 60.0, 120.0], "age_bucket") ) }关键点:
- 所有操作用
LazyFrame,避免立即执行,便于优化(如谓词下推); cut()函数用固定切点,不依赖数据分布,保证确定性;scan_parquet()指定cache参数,启用内存缓存,避免重复IO。
测试时,我们用polars::testing::assert_frame_equal()比对输出DataFrame,容差设为1e-6,确保浮点计算一致性。
4.4 模型包构建:ONNX导出与权重分片
以PyTorch模型为例,导出ONNX不是简单调torch.onnx.export。我们封装了ModelExporter:
class ModelExporter: def __init__(self, model, input_sample): self.model = model self.input_sample = input_sample def export_onnx(self, path): # 关键:设置dynamic_axes,明确哪些维度可变 dynamic_axes = { 'input': {0: 'batch_size'}, # batch_size可变 'output': {0: 'batch_size'} # output batch_size与input一致 } torch.onnx.export( self.model, self.input_sample, path, opset_version=14, dynamic_axes=dynamic_axes, do_constant_folding=True ) def split_weights(self, output_dir): # 将state_dict按设备分片 state_dict = self.model.state_dict() torch.save(state_dict['encoder.weight'], f"{output_dir}/cuda0.bin") torch.save(state_dict['decoder.weight'], f"{output_dir}/cuda1.bin")导出后,用onnxsim简化模型,再用onnx.checker.check_model()验证有效性。权重分片确保GPU显存分配可控——cuda0.bin加载到GPU0,cuda1.bin加载到GPU1,避免单卡OOM。
4.5 服务契约实现:Axum路由与中间件链
服务主入口src/main.rs:
#[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { // 加载ADU let adu = AdUnit::load_from_path("./adu_v1.2.0").await?; // 构建路由 let app = RouteBuilder::new() .add_route("/predict", Method::POST) .with_middleware(AuthMiddleware::new(VaultClient::new())) .with_middleware(FeatureValidator::new(adu.manifest.clone())) .with_handler(PredictHandler::new(adu)) .add_route("/healthz", Method::GET) .with_handler(HealthHandler) .build(); // 启动服务器 let listener = TcpListener::bind("0.0.0.0:8000").await?; axum::serve(listener, app).await?; Ok(()) }PredictHandler的核心逻辑:
impl Handler for PredictHandler { async fn handle(&self, req: Request<Body>) -> Result<Response<Body>, Error> { // 1. 解析请求体为UserFeature let user_feature: UserFeature = parse_json_body(req).await?; // 2. 校验特征(调用FeatureValidator中间件已做,此处二次校验) if !user_feature.validate().is_ok() { return Err(Error::BadRequest("Invalid feature")); } // 3. 执行特征工厂(调用Polars LazyFrame) let features_df = self.feature_factory.compute(&user_feature).await?; // 4. 模型推理(调用tract) let output = self.model_runner.run(features_df).await?; // 5. 后处理(调用postprocessor.py) let result = self.postprocessor.run(output).await?; Ok(Json(result).into_response()) } }全程异步,无阻塞IO,单实例轻松支撑5000 QPS。
4.6 CI/CD流水线:从代码提交到生产部署的自动化闭环
我们用GitHub Actions构建CI/CD,流水线分四阶段:
- Lint & Test:运行
clippy(Rust)、ruff(Python)、buf check(Protobuf),并执行单元测试; - Build & Package:
cargo build --release生成二进制,poetry build生成wheel,打包为ADU tarball; - Integration Test:启动临时K8s集群(Kind),部署ADU,用
curl发送真实请求,验证端到端功能; - Deploy to Staging:若测试通过,自动推送到Staging环境,并触发灰度发布。
关键配置(.github/workflows/ci.yml):
- name: Run Integration Test run: | # 启动Kind集群 kind create cluster --name adu-test # 部署ADU kubectl apply -f k8s/staging.yaml # 等待就绪 kubectl wait --for=condition=ready pod -l app=adu-service --timeout=120s # 发送测试请求 curl -X POST http://localhost:8000/predict \ -H "Content-Type: application/json" \ -d '{"user_id": 123, "age": 25}' \ --retry 5 --retry-delay 2整个流水线平均耗时4分32秒,失败时自动截图并上传Artifacts,方便排查。
4.7 生产部署:K8s Manifest与Helm Chart的精简实践
我们不用Helm Chart的复杂模板,而是用Kustomize + Helm Values分离:
base/:通用K8s资源(Deployment、Service、ConfigMap);overlays/staging/:Staging环境特有配置(资源请求、副本数);overlays/prod/:Prod环境配置(HPA规则、TLS证书);helm-values.yaml:Helm Values文件,只存可变参数(如镜像tag、数据库地址)。
base/deployment.yaml关键片段:
apiVersion: apps/v1 kind: Deployment metadata: name: adu-service spec: replicas: 3 template: spec: containers: - name: adu-service image: ${IMAGE_REPO}/adu-service:${IMAGE_TAG} # 由Helm注入 resources: requests: memory: "${MEM_REQUEST}" # 由Helm注入 cpu: "${CPU_REQUEST}" env: - name: ADU_SEED valueFrom: configMapKeyRef: name: adu-config key: seed部署命令:
# 渲染Staging环境 helm template staging ./charts/adu-service \ --values helm-values.yaml \ --set image.tag=v1.2.0 \ --set mem.request=4Gi \ | kubectl apply -f - # 渲染Prod环境(启用HPA) helm template prod ./charts/adu-service \ --values helm-values.yaml \ --set hpa.enabled=true \ --set hpa.targetCPUUtilizationPercentage=60 \ | kubectl apply -f -这样,环境差异只在Values文件里,K8s Manifest保持纯净,审计时一眼看清变更点。
5. 常见问题与排查技巧实录:那些文档里不会写的坑
5.1 问题速查表:高频故障与根因定位
| 现象 | 可能根因 | 排查命令 | 解决方案 |
|---|---|---|---|
服务启动失败,报错CUDA out of memory | ONNX模型未启用memory_limit,或权重未分片 | nvidia-smi查看显存占用;cat /proc/PID/status | grep VmRSS看进程内存 | 在tract加载模型时设置memory_limit: 8 * 1024 * 1024 * 1024(8GB);严格按MANIFEST.yaml声明的nvidia.com/gpu数量分片权重 |
| 特征计算耗时突增300%,但CPU使用率正常 | Polars LazyFrame未启用streaming,全量加载到内存 | strace -p PID -e trace=memory观察mmap调用 | 在scan_parquet()后加.collect(streaming=true),强制流式处理 |
| 灰度流量未按预期路由,新版本请求为0 | Istio VirtualService的match条件与Header不匹配,或x-request-id被Nginx重写 | kubectl get virtualservice adu-vs -o yaml检查规则;curl -H "x-gray-tag: age-over-60" http://adu-service/predict本地测试 | 在Nginx Ingress配置中添加proxy_set_header X-Gray-Tag $http_x_gray_tag;透传Header |
Prometheus指标adu_prediction_drift_score持续为0 | KS检验的基准分布未更新,或实时样本不足 | curl http://prometheus:9090/api/v1/query?query=adu_prediction_drift_score;检查/metrics端点 | 每天凌晨触发drift_calculatorJob,用过去7天数据重算基准分布;确保实时样本数>1000才计算 |
CI流水线在cargo build阶段超时 | Rust依赖下载慢,或clippy检查耗时过长 | cargo build --timings生成构建时间报告 | 在CI中启用cargo-cache;对clippy添加--all-targets --all-features并排除tests/目录 |
5.2 实操心得:那些踩过的坑,现在都成了 checklist
关于随机种子:不要只在训练脚本里设
torch.manual_seed(42)。我们在ADU的MANIFEST.yaml里强制声明global_seed: 42,服务启动时读取此值初始化所有rng。曾有一次,算法同学在本地训练时用了seed=123,但忘记更新MANIFEST,导致线上推理结果与离线评估不一致,排查了两天才发现。现在,我们的CI在cargo build前会校验MANIFEST.yaml里的global_seed是否与训练代码中的常量一致,不一致则失败。关于ONNX Opset版本:别盲目用最新opset。我们固定用
opset_version=14,因为opset=15引入的NonMaxSuppression新行为在某些GPU驱动下有bug。教训是:每次升级opset,必须在所有目标GPU型号(A10, A100, L4)上跑满24小时压力测试,确认P99延迟无劣化。关于K8s Liveness Probe:不要用
/healthz做存活探针。我们曾设initialDelaySeconds=30,但模型加载需45秒,导致Pod反复重启。现在,存活探针用/readyz(只检查进程存活),就绪探针用/healthz(检查模型加载完成、特征缓存就绪),且initialDelaySeconds设为model_load_time + 10s(从MANIFEST.yaml读取预估加载时间)。关于日志格式:拒绝
println!。所有日志用tracingcrate,结构化输出:info!(user_id = %user_feature.user_id, model_version = %self.adu.version, "Prediction completed");这样,ELK里可直接按
user_id聚合分析,而不是在文本日志里grep。关于Git LFS:
.pt文件必须用Git LFS,但.onnx文件不用。因为ONNX是文本协议,Git可高效diff,而.pt是二进制,LFS避免仓库膨胀。我们CI里加了检查:find . -name "*.pt" | xargs git check-attr filter,确保所有.pt文件已track。
5.3 性能调优实战:从100 QPS到10000 QPS的七次迭代
我们曾将一个电商搜索排序ADU从100 QPS优化到10000 QPS,过程如下:
- 第一次(100→300 QPS):发现`