ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

AI应用架构设计:从模型到生产系统的七节点落地方法

AI应用架构设计:从模型到生产系统的七节点落地方法

1. 这不是画PPT,是给AI系统搭骨架

“图解AI应用架构设计”这六个字,最近在技术社区里出现频率高得有点反常——不是因为大家突然爱上了画图,而是越来越多的项目卡在了“模型跑通了,但上线就崩”这个坎上。我去年帮某高校实验室做图像识别落地支持时,对方导师拿着一份PyTorch训练脚本兴冲冲来找我:“模型准确率98.5%,能不能直接部署到产线摄像头里?”我扫了一眼代码,没碰GPU,没看日志,第一句就问:“你打算用什么方式接收视频流?预处理是在边缘端做,还是传到服务器再做?推理结果怎么回传给PLC?报警阈值谁来配置?模型版本更新时,旧设备要不要停机?”他愣了三秒,说:“……我们只写了infer.py。”

这就是典型把“AI应用”等同于“模型推理”的认知偏差。真正的AI应用架构,从来不是一张孤立的模型结构图,而是一张覆盖数据入口、计算调度、服务编排、状态管理、可观测性、灰度策略的立体网络。它要回答的不是“能不能算”,而是“能不能稳、能不能扩、能不能查、能不能换、能不能退”。图解,不是为了好看,是为了让每个参与方——算法工程师、后端开发、运维同学、甚至产品经理——都能在同一张图上找到自己的责任边界和协作接口。

你手头可能正面临这些具体问题:

  • 训练好的模型,在测试环境跑得好好的,一上生产环境就OOM或延迟飙升;
  • 多个业务线共用一个大模型服务,A部门调用量突增,B部门接口直接超时;
  • 模型迭代了三个版本,但线上还在跑v1.2,没人知道哪个服务绑的是哪个权重文件;
  • 运维反馈“服务CPU常年95%,但GPU利用率不到10%”,你却找不到瓶颈在哪一层;
  • 客户提了个新需求:“能不能让模型对模糊图像也打个置信度提示?”,你翻了一圈代码,发现输入预处理模块和后处理逻辑全耦合在同一个函数里,改一处就得全测一遍。

这些问题,没有一个能靠调参解决。它们暴露的是架构层的缺失。而“图解”,就是把隐性的依赖关系、显性的数据流向、容易被忽略的容错路径,全部摊开在一张图上,用节点和箭头说话。这不是设计文档的装饰,是系统可维护性的底线。接下来我会带你从零开始,拆解一张真正能落地的AI应用架构图该怎么画、为什么这么画、哪些地方最容易画错,以及画完之后,怎么用这张图去驱动开发、排查问题、说服团队——毕竟,一张没人看、看不懂、看了也不执行的架构图,不如不画。

2. 架构图不是示意图,是带约束的工程协议

很多人画AI架构图,习惯从模型框开始:左边一个“ResNet-50”,中间一个“推理引擎”,右边一个“API服务”,再加几个箭头,配点云朵和数据库图标,就算交差。这种图的问题在于——它既不能指导开发,也无法用于评审,更没法做容量规划。它描述的是“理想态”,而不是“运行态”。真正的AI应用架构图,必须承载四类硬约束:数据约束、计算约束、状态约束、演进约束。缺一不可。

2.1 数据约束:流量不是均匀的,它是带形状的脉冲

AI系统最常被低估的,是数据的“形状”。训练数据是静态快照,但生产数据是动态流。一张架构图如果只标“输入图像”,那就等于没标。你需要明确标注:

  • 数据源类型与协议:是RTSP流(带时间戳、帧率、GOP结构)?是HTTP multipart/form-data上传(单次小文件,但并发高)?还是Kafka Topic里的base64编码二进制(有分区、有offset、有rebalance)?不同来源,决定了你前端需要什么样的接入网关——RTSP需要解码缓冲和关键帧对齐,HTTP上传需要分片校验和防重放,Kafka则要求消费者组管理和offset提交策略。

  • 数据速率与峰谷比:某安防项目实测,工作日早8点到9点,300路摄像头同时触发人形检测,峰值QPS达2400,是均值的4.7倍。如果架构图只写“支持1000 QPS”,那上线第一天就会雪崩。必须在图上标出基线流量(如500 QPS)、峰值流量(2400 QPS)、持续时长(60分钟),并注明该峰值是否可接受丢帧(如允许丢弃非关键帧)。

  • 数据质量契约:模型训练时假设输入是256×256 RGB归一化图像,但产线摄像头实际输出可能是1920×1080 YUV420,自动白平衡失效,低照度下噪点密集。架构图中必须在“预处理”节点旁标注输入契约(如“YUV420, 1920×1080, 无畸变校正”)和输出契约(如“RGB, 256×256, 像素值[0,1]”),并明确谁负责契约转换——是边缘设备固件?是接入网关?还是模型服务内部?契约不清晰,就是甩锅温床。

提示:我在某工业质检项目里吃过亏。算法团队说“模型只要JPG”,现场工程师就直接把相机原始BMP转JPG扔给服务。结果JPG有压缩伪影,模型把伪影当缺陷误判。后来我们在架构图“数据入口”节点强制加了一条约束:“禁止JPEG有损压缩;若源为BMP,须由接入网关转为PNG无损格式,并校验MD5”。一句话,省了两周返工。

2.2 计算约束:GPU不是万能插座,它是带脾气的精密仪器

架构图里画个“GPU推理集群”,就像装修图里只写“电器区”一样危险。GPU的约束体现在三个维度:

  • 显存带宽瓶颈:A100的显存带宽是2TB/s,但PCIe 4.0 x16只有64GB/s。如果你的模型加载权重后占32GB显存,每次推理前都要从CPU内存拷贝输入tensor,那PCIe就成了木桶最短板。架构图中,凡是有“CPU→GPU”或“GPU→CPU”数据搬运的路径,必须标注预计带宽占用(如“每次推理搬运128MB,理论耗时2ms”),并评估是否可优化为零拷贝(如使用CUDA Unified Memory)。

  • 计算单元饱和度陷阱:一个A100有108个SM,但你的模型如果只用到其中20个SM,其余空转,那显存再大也没用。这时候加机器不如改模型——把串行推理改成batch inference。架构图中,“推理服务”节点必须注明预期batch size(如bs=8)、对应GPU利用率目标(如70%~85%),并说明batch size如何动态调整(是固定值?还是根据队列长度自适应?)。

  • 异构计算协同成本:很多CV模型,前处理(resize、normalize)和后处理(NMS、bbox clip)在CPU上更快,而核心卷积在GPU上。但频繁CPU↔GPU切换代价巨大。架构图中,如果存在“CPU预处理→GPU推理→CPU后处理”链路,必须评估是否可将整个pipeline移至GPU(如用Triton的ensemble模型),或至少将预/后处理kernel写成CUDA算子。我在一个实时车牌识别项目里,把OpenCV的resize和normalize换成CUDA kernel,端到端延迟从83ms降到41ms,GPU利用率反而从45%升到78%——因为消除了三次内存拷贝。

2.3 状态约束:AI服务不是无状态的,它有记忆、有偏好、有生命周期

传统Web服务强调“无状态”,但AI应用天然带状态。架构图若回避这点,就是埋雷。

  • 模型状态:权重文件是只读的,但模型服务本身有状态——比如TensorRT引擎的context、ONNX Runtime的session、PyTorch的CUDA stream。一个服务实例加载多个模型时,这些context会竞争GPU资源。架构图中,“模型服务”节点必须标注支持的模型数量(如“单实例最多加载3个不同模型”)、模型热加载机制(是进程重启?还是runtime hot-swap?)、以及模型卸载策略(如“闲置5分钟自动unload”)。

  • 会话状态:某些场景需要上下文,比如语音助手要记住用户刚说的“把空调调到26度”,下一句“再降两度”才能正确解析。这时架构图里就不能只有“ASR→NLU→TTS”单向流,必须加入“会话状态存储”节点(如Redis),并标注TTL(如“会话ID有效期30分钟”)、一致性要求(如“强一致,写成功才返回”)。

  • 外部依赖状态:模型可能依赖外部知识库(如实体链接需查百科)、实时特征(如用户当前地理位置)、或第三方API(如天气服务)。架构图中,所有对外部系统的调用,必须标注超时时间(如“天气API超时800ms”)、降级策略(如“超时则用缓存值+告警”)、以及熔断阈值(如“连续5次失败,熔断30秒”)。某推荐系统曾因未标注天气API的降级策略,一次天气服务宕机导致整个推荐流中断22分钟。

2.4 演进约束:架构图必须回答“怎么换模型”这个终极问题

一个AI应用的生命周期里,模型迭代次数远超服务代码迭代。架构图如果没定义好“模型替换路径”,就等于没设计。

  • 版本隔离机制:是按URL path区分(/v1/predict, /v2/predict)?还是按请求header(X-Model-Version: v2)?或是按流量标签(canary: true)?架构图中,“API网关”节点必须明确路由策略,并标注各版本的灰度比例(如“v2初始1%,每小时+5%”)。

  • AB测试能力:要对比v1和v2效果,不能只靠离线指标。架构图中,需增加“效果收集”节点,连接到“在线日志系统”,并标注采集字段(如request_id, model_version, latency_ms, prediction_result, user_feedback)。某广告点击率模型,就是靠这张图里定义的feedback字段,发现v2在夜间时段CTR提升但转化率下降,及时叫停灰度。

  • 回滚保障:v2上线后发现问题,怎么秒级切回v1?是DNS切流?是K8s Service selector改label?还是API网关配置热重载?架构图中,必须有一条虚线箭头,从“监控告警”指向“回滚执行器”,并注明SLA(如“RTO < 30秒”)。我们曾在一个金融风控项目里,把回滚操作封装成一条curl命令,写在架构图边注里:“curl -X POST http://gateway/rollback?service=risk&to=v1.3”,运维同学至今还在用。

3. 一张合格的架构图,必须包含这七个核心节点

抛开花哨风格,一张能指导落地的AI应用架构图,必须稳定包含以下七个节点。少一个,就可能在某个环节掉链子。我把它叫做“AI应用七巧板”——每一块形状固定,组合方式灵活,但缺一不可。

3.1 数据接入网关:不是转发器,是数据整形师

这是整个系统的“门卫+翻译官”。它不只做协议转换(RTSP→HTTP),更要承担数据整形职责。常见误区是把它做成透明代理,结果上游数据格式一变,下游全崩。

  • 核心功能:

    • 协议适配:支持RTSP/HLS/WebSocket/HTTP/GRPC/Kafka等主流协议;
    • 流控限速:按IP、token、client_id维度限流,防刷防爆;
    • 数据校验:检查图像尺寸、编码格式、时间戳连续性;
    • 预处理卸载:将CPU密集型操作(如YUV转RGB、缩放)前置到这里,减轻模型服务压力。
  • 实操要点:
    我们在某智慧园区项目中,用Nginx+Lua做轻量级接入网关。针对RTSP流,编写Lua脚本实现:

    -- 检查GOP关键帧间隔,超过2s则丢弃该帧(防卡顿) if frame.gop_interval > 2000 then return end -- 将YUV420P转为RGB,用FFmpeg wasm版,避免进程fork开销 local rgb_data = ffmpeg_wasm.yuv2rgb(yuv_data, width, height)

    这样,模型服务收到的永远是标准RGB tensor,不用再写一堆兼容代码。

  • 避坑经验:

    注意:别在网关里做模型级预处理(如归一化、mean/std减法)。这些操作应由模型服务自己完成,因为不同模型的归一化参数不同。网关只做“格式统一”,不做“语义变换”。

3.2 特征工程管道:不是ETL,是特征契约的守门人

很多团队把特征工程全堆在模型训练脚本里,上线时才发现:训练用的pandas.DataFrame,生产用的是Spark DataFrame,字段名大小写不一致,缺失值填充策略不同,结果线上预测全错。

  • 核心功能:

    • 特征注册:定义特征名、数据类型、取值范围、更新频率(如“用户近7天登录次数:int32, [0,100], 每小时更新”);
    • 在线特征服务:提供低延迟(P99 < 10ms)、高并发(10K+ QPS)的特征查询API;
    • 特征一致性保障:确保训练离线特征与线上实时特征计算逻辑100%一致(即“feature parity”)。
  • 实操要点:
    采用Feast作为特征存储,但关键在“特征定义文件”(feature_view.py):

    # user_activity_fv.py user_activity_fv = FeatureView( name="user_activity", entities=["user_id"], ttl=timedelta(hours=1), schema=[ Field(name="login_count_7d", dtype=Int32), Field(name="avg_session_duration_sec", dtype=Float32), ], online=True, source=user_activity_source, # 指向Kafka或Delta Lake )

    这份文件既是代码,也是契约。算法、数据、后端三方都基于它开发,谁也不能擅自改字段含义。

  • 避坑经验:

    注意:特征管道必须有“影子模式”(shadow mode)。上线新特征时,先让模型同时读取新旧两套特征,记录差异,确认无异常后再切流。我们曾发现新特征管道里,一个时间窗口计算逻辑漏了时区转换,导致所有夜间特征值偏移8小时,靠影子模式提前3天捕获。

3.3 模型服务网格:不是单体服务,是带弹性的计算单元

这是架构图的心脏。但它不该是一个大盒子,而应是一组可编排、可伸缩、可观测的微服务单元。

  • 核心功能:

    • 多框架支持:TensorRT、ONNX Runtime、Triton Inference Server、TorchServe;
    • 自动扩缩容:基于GPU显存使用率(而非CPU)触发HPA;
    • 模型热更新:不重启进程,动态加载新权重;
    • 请求队列管理:支持优先级队列(如VIP用户请求插队)、超时丢弃、背压控制。
  • 实操要点:
    Triton是目前最成熟的方案,但要用好,得理解它的配置哲学。config.pbtxt文件是关键:

    name: "resnet50" platform: "onnxruntime_onnx" max_batch_size: 8 input [ { name: "input" data_type: TYPE_FP32 dims: [3, 224, 224] reshape: { shape: [3, 224, 224] } } ] output [ { name: "output" data_type: TYPE_FP32 dims: [1000] } ] dynamic_batching [ # 启用动态批处理 max_queue_delay_microseconds: 10000 # 最大排队10ms ]

    这段配置决定了:模型能吃多大batch、输入输出shape、是否开启动态批处理、最大容忍排队时长。漏配一项,性能就打五折。

  • 避坑经验:

    注意:Triton的dynamic_batching默认关闭。很多团队开了但没调max_queue_delay_microseconds,结果请求永远等不满batch size,延迟飙升。实测下来,这个值设为10ms~50ms最稳——既能攒到batch,又不明显增加延迟。

3.4 业务逻辑编排器:不是胶水代码,是决策中枢

模型输出的是概率,业务需要的是动作。这个节点负责把“模型结果”翻译成“业务指令”。

  • 核心功能:

    • 规则引擎集成:将专家规则(如“置信度<0.7且图像模糊度>0.8,则标记为‘需人工复核’”)与模型输出结合;
    • 多模型融合:对同一输入,调用多个模型(如OCR+NER+关系抽取),做结果融合;
    • 动作路由:根据结果,调用不同下游系统(如“检测到火灾→触发消防报警API;检测到陌生人→推送企业微信”)。
  • 实操要点:
    我们用Camunda BPMN做可视化编排。画一个流程图:

    [Start] → [Call OCR Model] → [Parse Text] → [Call NER Model] → [Extract Entities] → [Rule Engine: Check Policy] → [Route Action]

    每个节点都是可配置的:OCR节点指定模型URL、超时、重试次数;Rule Engine节点挂载Drools规则文件。业务方改策略,不用动代码,只改BPMN图和Drools文件。

  • 避坑经验:

    注意:编排器必须有“降级开关”。当某个模型服务不可用时,能自动跳过该节点,走兜底逻辑(如“OCR失败,则用图像哈希匹配历史图库”)。这个开关要能在配置中心实时生效,不能重启服务。

3.5 状态与配置中心:不是配置文件,是运行时的神经系统

AI应用的状态比传统服务复杂得多:模型版本、特征开关、业务规则、灰度比例、告警阈值……全得集中管理。

  • 核心功能:

    • 实时配置推送:支持监听配置变更事件(如“model_version changed to v2.1”);
    • 配置灰度:按用户ID哈希、设备ID前缀、地域等维度下发不同配置;
    • 配置审计:记录谁在什么时候改了哪项配置,便于事后追溯。
  • 实操要点:
    用Apollo配置中心,但关键在“命名空间”设计。我们划分了四个namespace:

    • model-config: 存模型服务地址、超时、重试;
    • feature-switch: 存各特征开关(如enable_face_recognition=true);
    • business-rule: 存Drools规则的JSON序列化字符串;
    • monitor-threshold: 存告警阈值(如latency_p95_ms=200)。
      每个服务只订阅自己需要的namespace,解耦彻底。
  • 避坑经验:

    注意:配置中心必须有“配置快照”功能。每次发布新配置,自动保存diff。某次线上事故,就是因为一个运维误删了monitor-threshold里的error_rate_alert,靠快照3分钟内恢复。

3.6 可观测性平台:不是监控大盘,是故障的CT扫描仪

AI系统故障往往隐蔽:模型精度缓慢下降、特征漂移、GPU显存泄漏……传统监控(CPU、内存、HTTP状态码)完全抓不住。

  • 核心功能:

    • 模型性能监控:预测延迟P95、吞吐量QPS、GPU显存使用率;
    • 数据质量监控:输入图像分辨率分布、像素值范围、缺失率;
    • 模型健康度:预测结果分布(如分类结果各label占比)、置信度分布、概念漂移检测(KS检验);
    • 关联追踪:从HTTP请求ID,穿透到模型推理trace,再到GPU kernel执行耗时。
  • 实操要点:
    用Prometheus+Grafana,但指标要自定义。在模型服务里埋点:

    # 每次推理后上报 MODEL_LATENCY_SECONDS.observe(latency_ms / 1000.0) MODEL_CONFIDENCE_HISTOGRAM.observe(max(pred_probs)) INPUT_IMAGE_SIZE_BYTES.observe(len(image_bytes)) # 每分钟聚合上报特征统计 FEATURE_DRIFT_KS_SCORE.set(compute_ks_score(current_dist, baseline_dist))

    Grafana看板分三层:基础设施层(GPU温度、显存)、服务层(QPS、延迟)、模型层(置信度分布、漂移分数)。故障时,先看模型层,再看服务层,最后看基础设施层——顺序错了,排查效率归零。

  • 避坑经验:

    注意:不要只看平均值!必须看分位数(P50/P90/P99)和分布直方图。我们曾发现P50延迟15ms很稳,但P99高达1200ms,查出来是某台GPU显存泄漏,每处理1000次请求就OOM一次,重启后恢复——平均值完全掩盖了这个问题。

3.7 模型生命周期管理器:不是仓库,是模型的HR系统

模型不是部署完就完事了。它有入职(注册)、试用(灰度)、转正(全量)、晋升(v2.0)、离职(下线)、档案(元数据)。

  • 核心功能:

    • 模型注册:存模型文件、版本号、训练数据集、评估报告、负责人;
    • 元数据管理:记录模型框架、输入输出schema、许可证、安全扫描结果;
    • 生命周期工作流:定义“开发→测试→灰度→生产→归档”各阶段准入条件(如“灰度前,必须通过A/B测试p-value<0.05”);
    • 自动化流水线:对接CI/CD,模型PR合并后,自动触发测试、打包、推送到镜像仓库。
  • 实操要点:
    用MLflow做模型注册,但重点在“阶段流转”。我们定义了四个stage:

    • Staging: 新模型,仅内部测试;
    • Archived: 已下线,但保留元数据供审计;
    • Production: 当前线上主力;
    • Challenger: 竞争模型,用于A/B测试。
      每次调用mlflow.register_model(),必须指定stage。API网关根据stage路由请求,Production走主流量,Challenger走1%灰度。
  • 避坑经验:

    注意:模型注册必须强制关联数据集版本。我们曾因没绑定,v1.2模型用v1.0数据集重新训练,结果线上效果暴跌。现在流程卡点:不填dataset_version=20240501,注册失败。

4. 从图到代码:架构图落地的四步验证法

画完架构图,只是万里长征第一步。真正考验在落地。我总结了一套“四步验证法”,每一步都用真实代码或配置验证,图不对,代码就跑不通。

4.1 第一步:协议握手验证——确保数据能进门

目标:证明数据接入网关能正确解析并转发请求,不丢帧、不错码。

  • 验证方法:
    写一个最小客户端,模拟真实数据源:

    # 模拟RTSP流(用ffmpeg推一个测试流) ffmpeg -re -i test.mp4 -f rtsp rtsp://localhost:8554/test # 模拟HTTP上传(用curl发一张图) curl -X POST http://gateway/upload \ -F "image=@test.jpg" \ -H "X-Device-ID: cam-001"
  • 验证点:

    • 网关日志是否打印“[INFO] RTSP connected: cam-001, fps=25”?
    • 是否有“[WARN] Frame drop: cam-001, reason=gop_too_long”?
    • 模型服务日志是否收到{"device_id":"cam-001","tensor_shape":[3,224,224]}?
  • 失败案例:
    某次,客户端发的JPG是CMYK色彩空间,网关没做色彩空间校验,直接转发,模型服务报错ValueError: expected 3 channels, got 4。修复:在网关加一行OpenCV校验:

    if len(img.shape) == 3 and img.shape[2] == 4: img = cv2.cvtColor(img, cv2.COLOR_CMYK2RGB)

4.2 第二步:服务连通验证——确保模型能算出来

目标:证明模型服务能正常加载、响应,且输出符合契约。

  • 验证方法:
    直接调用模型服务gRPC接口(以Triton为例):

    import tritonhttpclient client = tritonhttpclient.InferenceServerClient(url="localhost:8000") inputs = [tritonhttpclient.InferInput("input", [1,3,224,224], "FP32")] inputs[0].set_data_from_numpy(np.random.rand(1,3,224,224).astype(np.float32)) outputs = [tritonhttpclient.InferRequestedOutput("output")] result = client.infer(model_name="resnet50", inputs=inputs, outputs=outputs) print(result.as_numpy("output").shape) # 应输出 (1, 1000)
  • 验证点:

    • 返回shape是否为(1,1000)?
    • result.get_response()["model_name"]是否为"resnet50"?
    • result.get_response()["parameters"]["content-type"]是否为"application/octet-stream"?
  • 失败案例:
    某次,config.pbtxt里dims: [3,224,224]写成了dims: [224,224,3],客户端发[1,3,224,224],Triton报错invalid shape。修复:严格按ONNX规范,channel必须在前。

4.3 第三步:链路贯通验证——确保业务逻辑能串起来

目标:证明从数据进门,到模型计算,再到业务动作,整条链路畅通,且结果正确。

  • 验证方法:
    写一个端到端测试脚本,模拟完整业务流:

    # 1. 上传图片 resp1 = requests.post("http://gateway/upload", files={"image": open("fire.jpg","rb")}) task_id = resp1.json()["task_id"] # 2. 轮询结果 for _ in range(10): resp2 = requests.get(f"http://orchestrator/result/{task_id}") if resp2.json()["status"] == "done": assert resp2.json()["action"] == "fire_alarm_triggered" break time.sleep(1)
  • 验证点:

    • action字段是否为预期值(如"fire_alarm_triggered")?
    • 整个链路耗时是否在SLA内(如<1500ms)?
    • 编排器日志是否打印[INFO] Rule matched: fire_confidence>0.9 → trigger_alarm?
  • 失败案例:
    某次,编排器里规则写的是fire_confidence > 0.9,但模型输出的key是fire_prob,导致永远不匹配。修复:在架构图“模型服务”节点旁,用红色字体标注output_keys: ["fire_prob", "smoke_prob"],强制契约。

4.4 第四步:压力与混沌验证——确保系统能扛住真实世界

目标:证明在高并发、部分故障、网络抖动下,系统仍能按预期行为。

  • 验证方法:
    用k6做压测,同时注入混沌:

    // k6 script import http from 'k6/http'; import { sleep, check } from 'k6'; export default function () { const res = http.post('http://gateway/upload', { image: open('./test.jpg', 'b'), }); check(res, { 'status is 200': (r) => r.status === 200, 'latency < 1500ms': (r) => r.timings.duration < 1500, }); sleep(0.1); }

    同时,用Chaos Mesh随机kill一个模型服务pod,或给网关加200ms网络延迟。

  • 验证点:

    • P95延迟是否仍<1500ms?
    • 错误率是否<0.1%?
    • 监控大盘是否自动告警,并触发自动扩容?
    • 故障期间,降级逻辑是否生效(如“模型不可用时,返回缓存结果”)?
  • 失败案例:
    某次压测,QPS到800时,模型服务开始503。查发现HPA只看CPU,没看GPU显存。修复:在Triton metrics里暴露nv_gpu_memory_used_bytes,K8s HPA新增指标:

    - type: Pods pods: metric: name: nv_gpu_memory_used_bytes target: type: AverageValue averageValue: 8Gi

5. 常见问题与实战排查技巧实录

画图容易,落地难。以下是我在十几个AI项目中,高频遇到的真问题及独家排查技巧。没有理论,全是血泪。

5.1 问题:模型在本地GPU跑得飞快,上K8s就慢三倍,GPU利用率还不到30%

  • 表象:nvidia-smi显示GPU-Util 25%,但watch -n1 'kubectl top pods'看到pod CPU用满,延迟飙升。
  • 根因分析:
    K8s默认不启用GPU共享,每个pod独占整卡。但你的模型很小,只用10%显存,却霸占了整张A100。其他pod只能排队等卡,CPU在等GPU,形成“CPU-GPU锁死”。
  • 排查技巧:
    1. kubectl describe pod <pod-name>,看Events里有没有FailedScheduling: 0/5 nodes are available: 5 Insufficient nvidia.com/gpu;
    2. kubectl exec -it <pod> -- nvidia-smi -q -d MEMORY | grep "Used",确认显存实际使用率;
    3. kubectl exec -it <pod> -- ss -tuln | grep :8000,看Triton监听的是0.0.0.0:8000还是127.0.0.1:8000(后者会导致K8s service无法转发)。
  • 解决方案:
    启用NVIDIA Device Plugin的MIG(Multi-Instance GPU)或vGPU。我们选了vGPU,用nvidia-container-toolkit配置:
    // /etc/nvidia-container-runtime/config.toml [nvidia-container-cli] no-cgroups = true [nvidia-container-runtime] debug = "/tmp/nvidia-container-runtime.log"
    然后在pod spec里指定:
    resources: limits: nvidia.com/gpu: 1 # 改为 0.5

5.2 问题:模型服务启动就OOM,但显存明明够

  • 表象:nvidia-smi显示显存空闲15GB,但Triton报错cudaErrorMemoryAllocation。
  • 根因分析:
    不是显存不够,是CUDA context初始化失败。常见于:
    • 容器里没装对应版本的NVIDIA driver(如容器用CUDA 11.8,宿主机driver是470,不兼容);
    • 容器没加--gpus all,或K8s没配置nvidia.com/gpu: 1;
    • Triton config里max_batch_size设得太大,预分配显存超限。
  • 排查技巧:
    1. 进容器执行nvidia-smi,如果报错NVIDIA-SMI has failed because it couldn't communicate with the NVIDIA driver,就是driver问题;
    2. cat /proc/driver/nvidia/version,看宿主机driver版本;
    3. docker run --rm --gpus all nvidia/cuda:11.8.0-devel nvidia-smi,测试基础环境。
  • 解决方案:
    统一宿主机driver和容器CUDA版本。我们锁定driver 525.60.13 + CUDA 11.8.0,所有镜像基于nvidia/cuda:11.8.0-devel-ubuntu20.04构建。

5.3 问题:A/B测试结果打架,v2模型离线A

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进