拓冰建站拓冰建站
首页 / 资讯中心 / 正文

AI模型推理容器化实战:镜像瘦身、GPU调度与冷启动优化

先别急着上 vLLM、调 batch size推理服务容器化真正折磨人的地方从来都不是“把代码塞进镜像”这一步。我在实际项目里把训练好的模型推到生产环境时遇到的第一个拦路虎是镜像 12GB、冷启动 3 分钟、GPU 显存被多个副本抢到 OOM。折腾了大半年把“AI 模型推理容器化”从能用做到了好用这篇文章就是这段时间踩坑和调优的完整记录。如果你正打算把模型推理服务布到 Docker 和 Kubernetes 上或者已经被 GPU 调度、镜像体积、优雅下线这些问题搞得焦头烂额那这篇文章应该能帮你省下不少时间。涉及的内容会很杂镜像构建、GPU 切分、推理引擎选型、自动扩缩容、稳定性排查都会覆盖到。我不打算写得像产品文档更像是我在自己的笔记本上做的复盘每一步都告诉你我当时为什么这么选中间踩了什么坑。1. 为什么模型推理容器化和训练容器化完全是两码事训练任务的容器化早就很成熟了PyTorch 官方镜像拉下来挂上数据卷GPU 一指定就能跑。但推理不一样它是常驻在线服务对延迟和稳定性的要求跟训练任务完全不在一个量级。很多团队从训练容器化直接抄作业到推理第一周就会撞得满头包。1.1 你要交付的是一台“一直不能停的机器”而不是一次能重来的实验训练任务跑挂了可以重启大不了从 checkpoint 恢复用户根本感知不到。但推理服务挂在线上每次重启都意味着延迟飙升、请求失败、连接拒绝。更麻烦的是模型服务不像 Web 应用那样改个代码就能秒级重启它启动时要先把模型权重加载进显存一个 7B 参数的模型fp16 权重大概 15GB从冷启动到真正能接流量耗时两三分钟是常态换成更大的几十B模型直接奔着十分钟去了。我对这个问题最直观的感受来自一次生产事故。当时为了更新模型版本我直接滚动更新了 Deployment结果新的 Pod 一直没就绪旧的又还在优雅退出前后 5 分钟里大概有 40% 的请求返回 503。后来查监控发现新 Pod 的模型加载就花了 1 分 40 秒但 readiness 探针我只给了 10 秒的容忍时间K8s 认为它一直不健康就不断重启它形成了重启死循环。那之后我把 readiness 探针换成了 startupProbe初试延迟 120 秒、周期 10 秒、失败阈值 3再没出现过这种问题。所以推理容器化第一条原则就是镜像体积要小、启动链路要快、探针要符合真实启动时间。必须把“模型加载”这个步骤当成一等公民来设计而不是指望 K8s 的探针魔法。1.2 推理服务的生命周期比想象中要短这意味着频繁的冷启动不可避免每天早高峰和晚高峰流量是几倍的差距尤其是 C 端产品白天用户在线多后半夜基本没人用。如果固定跑 10 个副本夜间 GPU 利用率连 5% 都到不了成本全浪费了。所以推理服务不可避免地要频繁扩缩容而每次扩容都是一次冷启动。我后来做了一个粗略的统计在我们平台上一个推理 Pod 从“调度成功”到“就绪可以接流量”时间分布大概是这样的阶段耗时说明镜像拉取30s ~ 3min取决于镜像大小和节点缓存情况容器启动5s ~ 15s主要是 Python 环境和引擎初始化模型加载30s ~ 5min权重从磁盘/网络加载到显存引擎预热10s ~ 30s部分引擎需要预推理一次镜像拉取和模型加载是大头。如果你的镜像每次都是全新的没有利用好节点上的缓存层那每次扩容都会等很久。这就是为什么训练容器化可以容忍镜像体积大推理不行——你的副本数量是动态的每一次滚动更新、每一次流量高峰都是在为镜像体积买单。1.3 推理容器的资源模型比训练复杂得多GPU 是核心但内存和网络也不能忽视训练任务一卡一任务资源模型很清晰。推理服务常见的情况是多个模型副本共享同一块 GPU通过 MIG 或 MPS 切分或者干脆一个容器里起多个推理进程这时 CPU、内存、网络、显存四条资源的账都得算清楚。我们有一个模型是 embedding 类的显存占用不高但每次请求要处理几百个向量计算CPU 直接被打满。当时我把 Pod 的 requests 设成了 2 核结果在高峰期 CPU throttling 严重P99 延迟从 20ms 飙到了 800ms。后来把 requests 提到 4 核加了一个 CPU 上限的 limit问题和延迟都稳下来了。内存方面很多人会忽略共享内存Triton Server 和 vLLM 在批量推理时会用 /dev/shm 做数据传输默认只有 64MB请求一大直接报 “Bus error”。我们的解决方法是给容器挂一个内存类型的 emptyDir 卷sizeLimit 设成 2Gi问题立刻消失。这些都是训练容器化根本不会遇到的问题。2. 镜像构建与模型加载把几百GB模型塞进容器的高效路径镜像构建是一切的起点也是大多数团队犯第一个错的地方。我见过有人在 Dockerfile 里把 15GB 的模型权重 COPY 进去镜像直奔 20GB每次拉取都是一种煎熬。模型文件到底放在哪怎么在不牺牲加载速度的前提下把镜像瘦下来这是推理容器化的第一道分水岭。2.1 模型文件放镜像里是新手最容易犯的错很多人看着原来的脚本觉得模型权重是运行的一部分就直接 COPY 进镜像了。从“本地能跑”的角度没问题但到了线上就是灾难。镜像仓库拉取慢不说每次模型更新都要重新构建镜像版本管理也乱成一团。我们的做法是把模型权重放到独立的存储里挂载到容器路径。方案上有几个选择节点本地磁盘加载最快但每个节点都要提前把模型拉下来更新麻烦节点多了占用空间很大。NFS / NAS 共享存储维护简单模型中心化但网络带宽可能成为瓶颈首次拉取慢。JuiceFS 这类分布式文件系统兼顾了共享和缓存但引入了额外组件要自己维护。我们最终选的是 NFS 为主、节点本地盘做缓存的方式。模型文件在 NFS 上首次加载时算力节点从 NFS 拉取到本地盘之后直接读本地。这个设计让镜像体积从 15GB 降到了 2GB 以下也彻底解决了模型更新要重打镜像的问题。每次有新模型上线只需要在模型仓库里加一个版本记录更新一下 ConfigMap 里的模型路径Deployment 滚动更新就完成了整个过程不涉及任何镜像构建。2.2 多阶段构建与分层缓存镜像从 12GB 降到 2.3GB镜像瘦身这件事我们做了两轮优化。第一轮是用多阶段构建把编译期和生产期的环境分离开。比如有的依赖需要进行本地编译比如 tokenizer 相关的包这些只需要在构建阶段存在运行阶段直接复制产物能省掉 3GB 的体积。第二轮是精简 base 镜像从标准的 python:3.10-slim 开始只装推理真正需要的库。下面是一个优化后的大致 Dockerfile# 构建阶段装依赖、编译必要组件 FROM python:3.10-slim AS builder WORKDIR /app COPY requirements.txt . RUN pip install --prefix/install -r requirements.txt \ python -c import nltk; nltk.download(punkt, download_dir/usr/share/nltk_data) \ python -m spacy download en_core_web_sm # 运行阶段只复制构建产物不放源码构建工具 FROM python:3.10-slim AS runtime WORKDIR /app COPY --frombuilder /install /usr/local COPY --frombuilder /usr/share/nltk_data /usr/share/nltk_data COPY app/ ./app/ ENV PYTHONPATH/app EXPOSE 8000 CMD [python, -m, app.server]这样构建出来的镜像从原来的 12GB 降到了 2.3GB拉取时间从 2 分钟降到了 20 秒左右。同时在 CI 里注意利用 Docker 的 layer 缓存把变化频率最低的依赖层放在最前面模型路径配置放在最后面这样每次构建时大部分层可以直接命中缓存流水线节省了大量时间。2.3 冷启动时的模型加载优化不要让首次请求等 10 分钟镜像瘦下来但模型本身的加载还是躲不掉。模型权重 15GB 如果全部走网络加载第一次启动可能耗时 3~5 分钟这在扩缩容场景下完全不可接受。我在这个环节做了两件事第一是模型文件本地缓存预拉取。通过 DaemonSet 在所有 GPU 节点上跑一个预热脚本在节点空闲时把常用模型从模型仓库拉取到本地盘。Pod 启动时直接把 NFS 路径替换成本地盘路径模型加载耗时稳定在 30 秒以内。这个方案带来的效果非常明显扩容时基本能做到 1 分钟以内接流量。第二是利用了镜像分层和模型分片。有些超大模型是分片存储的推理框架会按需加载部分分片比如 transformers 的 shard 策略。这种场景下冷启动时只加载第一片就能 accept 请求其余分片懒加载让服务“先起来再变快”。这个方法特别适合多租户模型平台能显著提升单 Pod 的启动表现。3. GPU 资源切分与调度显存和算力的精细记账推理服务最重要的资源是 GPU但 GPU 恰恰是 Kubernetes 里调度最粗糙的资源。默认情况下一个 Pod 申请一张整卡跑一个 7B 小模型显存只用了 30%剩下的全部浪费。更尴尬的是显存和算力还不完全是线性关系有的模型显存吃紧但计算量小这时候卡在显存维度上就只能干瞪眼。3.1 显存超卖导致的一系列连环问题一开始我们为了省成本试过让多个推理服务共享一张GPU不划分任何隔离直接把 CUDA_VISIBLE_DEVICES 暴露给多个容器。表面上两个服务都申请了同一张卡K8s 也没拦着直到请求一上来第一个服务 OOM整个 GPU 被 cudaErrorMemoryAllocation 占满所有共享的服务全崩。那时候我们才知道默认的 cup 共享就是裸奔完全没有隔离保障。排查这个问题的完整过程值得记录一下。当时线上不断有推理 Pod 报错错误信息是 “CUDA error: out of memory”但查看 Pod 的监控GPU 内存使用率并不高。我第一反应是显存泄漏用 nvidia-smi 盯了半小时发现显存占用确实在缓慢爬升但没到满。后来在 GPU 节点上用nvidia-smi dmon -s m查看了各进程的显存占用看到两个容器的进程竟然同时跑在同一张卡上这下才反应过来不是显存泄漏是共享导致的资源竞争。再查 K8s 的调度发现两个 Deployment 明明都申请了整卡却因为节点上有两张卡其中一张被两个 Pod 都绑定了。原因是有个同事在 Deployment 里用了nvidia.com/gpu: 2但节点上根本没有两张空闲卡Pod 被调度上去后靠 CUDA 层面共享了显存最终双双 OOM。简单说默认情况下 K8s 不会做显存隔离只有 nvidia-device-plugin 能保证“一个资源单位对应一张卡的独占”想超卖就得自己找方案。3.2 GPU 共享的几种方案MIG、MPS、Triton 并发后端既然整卡独占太浪费共享又容易崩那正规做法是什么我们项目里试过三条路线各有利弊方案隔离性显存限制适用场景NVIDIA MIG强硬件级隔离按实例固定A100/A30/H100 等新卡CUDA MPS中等共享算力但显存不隔离不能限制靠手动分配多进程小模型Triton 并发后端进程内隔离可靠可通过后端配置控制同一模型多实例副本MIG 是最理想的方式等于把一张 A100 物理切开成多个小 GPU 实例显存和算力完全隔离互不干扰。比如一张 80GB 的 H100可以切成 4 个 20GB 的实例每个实例跑一个小模型任何一个实例崩溃都不会影响其他实例。缺点是 MIG 模式开启后GPU 的算力会打点折扣而且不支持所有型号L40S、4090 这类卡就没有 MIG。MPS 则是典型的算力共享方案多个进程可以同时使用一张卡的算力但显存没有隔离。这意味着一个进程的显存分配会把整张卡拖死适合对隔离要求不高的内部环境。我们的另一个模型部署方式是多个模型实例跑在同一个容器里通过 Triton Server 的并发模型实例来共享显存和算力模型间的资源竞争由框架内部管理比裸多容器安全得多。最终我们生产方式上其实是混合的关键模型用 MIG 或整卡独占小模型用 Triton 多实例共享不搞一刀切。3.3 显存预留与 OOM 心态隔离为什么算法说的“够用”不可信显存管理还有一个常见的坑就是依赖算法工程师的“我觉得够用了”。模型推理过程的显存用量受很多因素影响批量大小、KV cache、beam search 宽度都会导致显存峰值大幅波动。我经历过几次上线前说好 20GB 没问题结果一压测直接到 35GB 崩溃的事故。后来我们定了一个原则Pod 里的推理引擎显存上限必须留出 10%~15% 的余量也就是说如果你测出稳定运行需要 20GB那资源配置时至少按 23GB 来申请并且要设置显存保护阈值比如 vLLM 的--gpu-memory-utilization参数设置为 0.85告诉引擎最多使用 85% 的显存超出就排队而不是硬挤。这样既防止了显存 OOM也给监控留了预警空间第一次出现显存水位上升时可以及时扩容而不是等完全打满后崩掉。4. 推理引擎与运行时选择vLLM、TensorRT-LLM、Triton 的容器化差异引擎选型决定了性能上限但很少有人提到它也决定了你容器化的方式。不同引擎的启动方式、依赖形态、资源模型都不一样直接影响镜像内容、参数配置和调度策略。我把三个主流推理框架在容器化场景下的表现和经验整理一下。4.1 引擎选型不只是性能问题也直接决定镜像怎么打vLLM 是目前开源大模型推理最流行的方案吞吐量高部署简单。从容器化角度vLLM 的官方镜像已经做得比较完善自带 CUDA、Python 环境和模型管理你只需要把模型挂载进去指定模型路径和引擎参数就能跑。但 vLLM 对显存很贪婪使用 Apache Arrow 做数据传输/dev/shm空间不够时会报分布式通信错误需要专门设置内存卷。TensorRT-LLM 是 NVIDIA 官方的推理引擎性能上限最高通过将模型编译成 TensorRT engine 来实现极致优化。它的坑在于 engine 文件高度绑定 GPU 型号、CUDA 版本和 TensorRT 版本一张 A100 上编译好的 engine 拿到 H100 上直接跑不了必须重新编译。这导致镜像构建时不能简单拉一个通用镜像而要为每种 GPU 型号打一个专用镜像。Triton Inference Server 则是一个通用推理服务框架支持多种后端可以同时加载不同框架的模型也就是在同一容器里用 Triton 统一管理 vLLM 的模型、PyTorch 的模型和 TensorRT 的模型。它自带 dynamic batching、并发模型实例管理这些功能非常适合模型种类多、框架杂的平台情况。缺点是它本身是一个重量级服务启动和配置复杂度比直接跑 vLLM 高。4.2 vLLM 池化与CPU调度参数最容易忽略的容器内多进程问题vLLM 在生产模式默认会启动 Ray 集群容器内会多出几个进程如果 CPU 资源设置得太小Ray 之间的通信和调度会抢占主推理进程的 CPU 时间片导致推理延迟飙升。我在容器里给 vLLM 分配的是 8 核实际推理进程大约用 4 核另外 4 核被 Ray、tokenizer 和监控进程吃掉了。如果你按“模型只占 4 核”的直觉去设置 CPU limit压测时就会观察到 P99 延迟抖得像心电图。另一个和容器相关的点是 vLLM 的连续批处理机制它的 batch size 是动态的会尽量把并发请求合并到一个 step 里计算。这本身是优点但也意味着在容器层面你看到的 GPU 利用率往往是阶梯式跳动的而不是平滑曲线做监控告警的时候要注意阈值不能因为 GPU 利用率一段时间为 0 就认为服务有问题。4.3 TensorRT-LLM 的图纸不能通用engine 文件与 GPU 型号强绑定TensorRT-LLM 的原理是把模型编译成一种高度优化的 engine 文件包含 GPU 指令和权重。这种“编译一次处处运行”的思路在 CPU 世界是理想状态但在 GPU 世界完全不适用。换个 GPU 架构指令集就不同engine 文件就不能用。我们在内部把这种方法叫作“给每个 GPU 出一套图纸”。这意味着如果平台上有多种 GPU 型号比如一部分 A100 一部分 L40S你要维护两个版本的 engine 文件镜像也要分型号打。实践中我用两个 Dockerfile 分别标记 tag比如inference:tensorrt-llm-a100和inference:tensorrt-llm-l40s部署时通过节点亲和性确保 Pod 跑到对应型号的节点上。虽然维护成本高了一截但换来的是推理性能提升吞吐上比 vLLM 高出不少优势挺明显。4.4 Triton 的 dynamic batching 在容器化下的配置价值Triton 容器化的价值在于它把多模型管理做成了平台能力。你在一个 Pod 里跑多个模型后端每个模型实例指定用多少显存和并发框架会帮你调度。dynamic batching 是 Triton 的杀手锏多个请求到达后不立即执行而是攒到一起作为一个 batch 计算能显著提升 GPU 利用率。配置 dynamic batching 时注意几个参数max_batch_size、preferred_batch_size、max_queue_delay_microseconds。这几个参数本质上是在延迟和吞吐之间做交易。max_queue_delay_microseconds 调得越大攒批时间越长吞吐越高但单请求的等待时间也越长。我在实际项目中把 max_queue_delay_microseconds 设成 3000也就是 3 毫秒batch 大小设成 8P99 延迟基本没变化吞吐提升了 2 倍多。5. 自动扩缩容与冷启动优化让副本数跟着流量走流量潮汐是推理服务固有的特点白天峰值和夜间低谷相差好几倍。固定副本数要么在高峰期扛不住要么在低谷期白白烧钱。基于 HPA 的自动扩缩容是标配思路但在 GPU 推理场景下默认配置几乎不可用必须做定制。5.1 HPA 默认指标在推理场景下基本是废的Kubernetes 默认的 HPA 指标是 CPU 和内存这对 Web 服务有效但对 GPU 推理服务来说没有直接意义。因为 GPU 利用率才是你扩缩容的判断依据而 CPU 它只反映了部分数据预处理的开销。一个模型如果 GPU 利用率已经到 95%CPU 可能才 30%HPA 看到 CPU 不高就继续只跑两个副本结果就是 GPU 打满请求超时。我用的是自定义指标方案让 HPA 基于 DCGM 暴露的 GPU 利用率做扩缩容。DCGM 是 NVIDIA 官方的数据中心 GPU 管理工具可以输出利用率、显存占用、温度这些指标通过 Prometheus 抓取后配合 Prometheus Adapter 暴露给 HPA。以 GPU 利用率 70% 为阈值一个名为gpu_utilization的自定义指标为例HPA 配置大致长这样apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: inference-hpa namespace: ai spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: llm-inference minReplicas: 2 maxReplicas: 8 metrics: - type: External external: metric: name: gpu_utilization target: type: AverageValue averageValue: 70 behavior: scaleDown: stabilizationWindowSeconds: 300 scaleUp: stabilizationWindowSeconds: 60stabilizationWindowSeconds很关键它决定 HPA 在扩容后多久内不会再次扩容。推理服务扩容需要时间如果 HPA 频繁扩缩容会进入抖动状态。我把 scaleUp 的窗口设成 60 秒scaleDown 设成 300 秒弹上来慢一点缩下去稳一点。实践下来扩容延迟大约 2 分钟但不会出现反复横跳。5.2 冷启动时间的三个大头镜像拉取、模型加载、引擎预热自动扩缩容跑起来后最让人头疼的是扩容过程中请求仍然会超时。因为新 Pod 从创建到就绪需要一段时间而高峰期流量不会等人。我把冷启动时间拆解成三块分别做了优化一是镜像拉取。除了前面说的镜像瘦身还可以利用 DaemonSet 在每个节点上做镜像预热。K8s 调度时会优先把 Pod 调度到已经缓存好镜像的节点等于省掉了拉取时间。二是模型加载。模型文件走本地盘缓存后加载时间从 3 分钟降到了 30 秒。但如果你用的是动态下载模型的方式一定要设置好 SST 缓存或者提前把模型拷贝到节点千万别让扩容过程去现场下载。三是引擎预热。有的推理框架在第一次请求时会做一些初始化工作比如 TensorRT engine 反序列化、CUDA context 创建这些都要在探针就绪之前完成。我的做法是在容器启动命令里加一个 warmup 脚本服务起来后先用几条虚拟请求打一次推理把这个初始化过程提前“热”掉然后再把容器标记为就绪这样用户请求进来时体验就不会受初始化影响了。5.3 优雅下线缩容时请求被 499/504 的元凶扩缩容不只是扩容缩容同样有坑。HPA 缩容时直接把 Pod 标记为 Terminating然后 K8s 开始关容器。但这时如果还有请求正在处理中Pod 就被杀掉了客户端只能收到 499 或者 504。这个问题的根因是容器没有实现优雅退出机制。解决方案有几个层次。最基础的是在 Deployment 里设置terminationGracePeriodSeconds默认为 30 秒给容器留出收拾残局的时间。然后在容器主进程里监听 SIGTERM 信号收到信号后先把应用状态切为不接受新请求同时把已接收但未完成的请求跑完再真正退出。vLLM 和 Triton 这两个框架都支持这种优雅退出vLLM 是收到 SIGTERM 后停止接受新请求并等待当前 batch 完成Triton 是停止 model queue 并退出。在 Kubernetes 里还有一个细节Pod 进入 Terminating 后Service 的 Endpoints 会立刻把 Pod 摘掉但正在建立的连接可能还在。为了更平滑可以在 preStop hook 里加sleep 10让 Pod 在完全退出前先等 10 秒把负载均衡器上的残留连接 drain 掉。这个过程我用的配置如下lifecycle: preStop: exec: command: [/bin/sh, -c, sleep 10]这个经验来自一次真实事故高峰期流量下降HPA 缩容 2 个副本当天有用户反馈偶发请求失败。我查了日志发现失败的全部集中在缩容时刻加上 preStop sleep 之后同类事故就再没出现过。6. 推理稳定性排查手册超时、排队、掉卡、显存泄漏前面讲的都是怎么把服务部署好、调好但线上的崩溃不会等你准备好才来。这一章我整理了几次印象深刻的线上问题排查过程把完整的推理链路展示出来你会看到最终答案往往不是表面那一个参数的问题。6.1 现象一P99 飙高而 P50 正常的典型链路有段时间线上模型服务的告警总在半夜响表现是 P99 延迟从 300ms 涨到 2s但 P50 一直正常。我一开始怀疑是网络波动认真查看监控后发现问题只出现在 GPU 利用率达到 90% 以上的时段P50 正常是因为大多数请求还走得很快只有一部分请求在排队等着 GPU 空闲。这个现象背后的逻辑是推理服务的排队机制。vLLM 这类引擎是连续批处理架构一批请求同时算算完一起返回。当 GPU 算力接近极限时新来的请求只能进入队列如果 batch 调度策略激进队头等待会很严重个别请求被拖到 2 秒。这套机制对吞吐很友好但对长尾延迟非常不友好。排查链路我一般是这么走的先看 K8s 的 Pod 事件确认没有 OOM 和重启再看 GPU 利用率和显存确认算力打满了接着看引擎日志确认是排队而不是报错最后用 curl 直接打一个请求观察返回时间。整个过程大概十分钟但能准确定位出瓶颈在哪一层。解决方案通常是扩容、调整并发限制、优化 batch 策略三板斧。6.2 现象二容器被 OOMKilled但 GPU 利用率并不高另一类问题是容器反复进入 CrashLoopBackOffkubectl describe pod显示 OOMKilled但查看 GPU 显存占用才 30%。这说明内存超限而不是显存超限两个是完全不同的资源维度很容易让人误判。这个问题的根源是推理框架的 CPU 内存开销。vLLM 除了显存还需要一部分 CPU 内存存放 tokenizer、调度数据、临时激活值。大批量并发请求时这部分内存会突增如果 Pod 内存 limit 设成固定的 2Gi很可能被打爆。我们当时把 Pod 内存 limit 从 4Gi 调到 16Gi现象立刻消失。另外 /dev/shm 也是一个隐蔽的内存消耗点Apache Arrow 数据传输会用到不设置 emptyDir 内存卷的话默认的 64MB 很容易造成 Bus error。给 Pod 挂一个 sizeLimit 为 2Gi 的 emptyDir 卷就能解决同时这部分内存也会被计入 Pod 的内存限制里记得预留空间。6.3 现象三GPU 型号、驱动、CUDA 版本的三角关系还有一次全网崩溃式的故障是 GPU 节点驱动升级引起的。某个节点从 CUDA 12.0 升到 12.4但镜像里的推理框架还是按 12.0 编译的启动时不报错正常加载模型跑起来后随机出现非法内存访问错误码是 “CUDA error: an illegal memory access was encountered”。这种错误的可怕之处在于它不是必现的毫无规律。排查这类问题不能只靠看容器日志因为容器日志里根本没有 GPU 驱动层的信息。操作顺序是先在节点上执行nvidia-smi确认驱动和 CUDA 版本再跑一个简单的 CUDA 示例程序确认 GPU 本身硬件没坏然后对照镜像里的 CUDA 版本找差异。用docker inspect查看镜像的环境变量看CUDA_VERSION就能定位。这个问题的最佳解法是建立版本兼容矩阵在 CI 里对镜像和 GPU 节点做组合测试确保发布前跑一遍全链路推理。我们后来直接用 NVIDIA 官方推理镜像作为 base只在上面加自己的应用层驱动兼容性问题显著减少遇到问题也能找到官方支持渠道。6.4 现象四显存碎片化与 KV cache 的手动清理最后一个是显存泄漏的排查或者是显存碎片化。长期运行的推理容器显存占用会缓慢上升从 60% 涨到 90%但每次请求返回都是正常的。我一开始也怀疑是代码泄漏后来把显存释放的日志打印出来发现每次请求后显存都能正常释放但空闲块被切得很碎导致大块显存分配不成功即使总量足够也会报 OOM。KV cache 是最主要的碎片来源。Transformer 推理时每个请求都会分配 KV cache 空间请求结束后理论上会释放但分配器可能出现碎片化。不同引擎的处理方式不同vLLM 的 PagedAttention 在设计上就能减少碎片但某些老版本仍有问题。Triton 和 TensorRT-LLM 各自有显存管理机制碎片问题更隐蔽。我采用了一个运维层面的兜底策略监控显存碎片率当可用显存小于某个阈值并且碎片率超过 30% 时主动滚动重启该副本。虽然不优雅但很有效。模型加载和显存预热本来就需要时间所以重启要放在低峰期并配合优雅退出来做避免重启过程丢请求。7. 稳定性和资源利用率的工程辅助手段前面讲到的都是单点技术最后补一点工程层面的东西。推理容器化做久了会意识到光把某个参数调好没用要建立一套围绕推理服务的稳定性和效率体系。7.1 探针设计startupProbe 解决长启动导致的无限重启我前面提到过 startupProbe 的重要性这里展开说一下。K8s 里有三类探针liveness、readiness、startup默认的 liveness/readiness 会从容器启动就开始探测如果服务还在加载模型探针一直失败容器就会被杀掉重启陷入死循环。startupProbe 的逻辑是先让它通过通过之后才启用 liveness 和 readiness正好匹配推理服务的启动模式。我的推荐配置是startupProbe 的 failureThreshold 设成 30periodSeconds 设成 10这样最多容忍 300 秒启动时间覆盖绝大多数模型加载场景。livenessProbe 探活频率 10 秒只要服务进程活着就算健康readinessProbe 判断服务能不能接流量基于一个内部的/health接口来判断是否已经完成预热。这样设计之后模型加载期间 Pod 处于“不被探活打扰”的状态加载完马上进入就绪不会再出现启动即重启的问题。7.2 优雅退出缩容时怎么做到不丢请求优雅退出的实现不只在容器内还涉及负载均衡和 Service 的交互。当一个 Pod 进入 TerminatingK8s 会把它从 Service 的 Endpoints 中移除但已经建立的 TCP 连接不会立刻断负载均衡器切换到其他后端也需要时间。所以最好把优雅退出分成几个阶段收到 SIGTERM停止接收新请求给负载均衡器留出时间把新流量路由到其他副本等待当前正在处理的请求完成设置一个超时上限退出进程。这四步对应的配置是我们前面讲过的 preStop sleep 和 terminationGracePeriodSeconds。有一个注意点terminationGracePeriodSeconds 必须大于 preStop sleep 加上最慢请求的最大时长。如果最慢请求需要 15 秒preStop sleep 10 秒那 terminationGracePeriodSeconds 至少设成 35 秒否则 K8s 直接强制 kill优雅退出白搭。7.3 压测与容量规划不要拍脑袋定副本数最后聊聊容量规划。很多团队的副本数是从“并发用户数”拍出来的然后被突增流量打得措手不及。我的经验是任何推理服务上线前都要做一次全链路压测目的不是看能扛多少 QPS而是为了得到容量规划需要的三个数据单副本的极限吞吐、瓶颈资源类型、扩容耗时的实际值。我们平台的做法是用 Locust 写模拟脚本逐步加压同时记录每个阶段的 GPU 利用率、P99 延迟和排队长度。压测结果告诉我们单副本的极限吞吐是 100 QPS那么生产配置至少留 30% 余量即 70 QPS 触发扩容扩容后每个新副本 1 分半钟就能接流量。这套数据让我们的 HPA 阈值和冷却时间都从“拍脑袋”变成了有据可依缩容时也不会误伤用户体验。最后再分享一个我个人实际操作中的体会。推理容器化这条路一开始我以为最难的会是推理性能比如怎么把 batch 调大、把吞吐拉满。但走了这一圈之后我发现最难的反而是那些“非推理”的部分镜像瘦身、GPU 怎么分、启动怎么快、退出怎么稳。性能和延迟问题现在的推理框架自己解决得已经很好了真正拉开差距的是你把这些框架稳定地跑在集群里的能力。如果你现在刚起步我建议先别急着去做花哨的 GPU 共享和动态调度按这个顺序来第一步把镜像瘦下来第二步把 startupProbe 和优雅退出做对第三步再考虑 MIG 切分和 HPA。基础设施稳了后面的优化才有意义。把这些基础打牢之后你会发现大模型的推理服务也没那么神秘无非是“把东西准备好、让启动变快、让退出变优雅”这三件事反复打磨而已。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门