LLM 推理服务与传统的无状态 Web 服务差异很大:模型加载需要数秒到数十秒,显存占用通常在十几 GB 以上,而且请求往往是变长序列,计算时间不稳定。容器化能够统一 CUDA 版本、驱动依赖和推理框架,但如果镜像构建不合理、GPU 分配粒度太粗或调度策略只按卡数而非显存计算,上线后就会出现显存占满但计算利用率很低、请求排队严重等问题。要解决这些矛盾,需要从容器运行时、GPU 资源模型和调度器策略三个层面做系统设计。

一、容器化 LLM 服务的基础镜像与运行时选择
构建 LLM 推理镜像的第一步是选对基础镜像。生产环境不建议使用 CUDA devel 镜像,因为它包含编译工具链,体积通常比 runtime 或 base 镜像大 3 到 5 GB,而且会引入不必要的攻击面。推荐从 nvidia/cuda:12.1.0-base-ubuntu22.04 这类镜像开始,只保留运行时库和驱动接口。推理框架方面,vLLM 提供 PagedAttention 和连续批处理,适合高并发在线服务;TensorRT-LLM 在固定 batch 和量化场景下延迟更低,但编译流程更复杂。如果团队对镜像体积敏感,可以使用多阶段构建,在 devel 阶段安装依赖并编译自定义算子,再把产物复制到 base 阶段。
模型权重不应打进镜像。一个 13B 参数的模型,FP16 权重约 26 GB,如果每次更新模型都重建镜像,不仅推拉镜像耗时,还会让 registry 容量快速膨胀。更合理的做法是使用对象存储保存权重,在 Pod 启动时通过 initContainer 或启动脚本下载到本地 NVMe 盘,或者通过 PersistentVolume 挂载已预热好的模型目录。下载过程要设置重试和校验,避免大文件损坏导致服务反复重启。
# 多阶段构建示例:只保留推理运行时 FROM nvidia/cuda:12.1.0-devel-ubuntu22.04 AS builder RUN apt-get update && apt-get install -y python3 python3-pip RUN pip install --no-cache-dir vllm==0.6.3 FROM nvidia/cuda:12.1.0-base-ubuntu22.04 RUN apt-get update && apt-get install -y python3 python3-pip && rm -rf /var/lib/apt/lists/* COPY --from=builder /usr/local/lib/python3.10/dist-packages /usr/local/lib/python3.10/dist-packages COPY --from=builder /usr/local/bin/vllm /usr/local/bin/vllm COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"]
容器运行时也需要关注。Kubernetes 集群通常使用 containerd 或 CRI-O,需要安装 NVIDIA Container Toolkit 并配置默认运行时,否则容器内无法访问 GPU。在节点上安装 nvidia-container-runtime 后,可以在 /etc/docker/daemon.json 或 containerd 配置中注册 runtime。对于较新的 GPU 型号,还要确保驱动版本与 CUDA 版本的兼容矩阵匹配,避免容器启动后出现 CUDA driver version is insufficient 之类错误。
二、GPU 调度粒度:整卡、MPS、MIG 与时间片
GPU 调度策略决定了一张物理卡如何被多个推理服务共享。最简单的方式是整卡独占:Pod 请求 nvidia.com/gpu: 1,调度器把整张卡分配给一个容器。这种方式的隔离性最好,显存和计算互不干扰,但缺点是资源碎片化严重。一个 7B 模型可能只占用 16 GB 显存,而 A100 有 40 GB 或 80 GB,剩余显存无法被其他容器使用,除非手动降低模型并行度或启用多个副本。整卡独占适合高吞吐离线批处理或对延迟抖动敏感的付费客户。
MPS 即 Multi-Process Service,允许多个 CUDA 进程共享同一张 GPU 的计算单元,但不做显存隔离。MPS 可以减少上下文切换开销,适合多个小请求或小模型并发运行,但一个进程的显存溢出可能影响整张卡上的所有服务。MIG 则是从硬件层面把 GPU 切成多个实例,例如 A100 可以切成 7 个 10 GB 的实例,每个实例拥有独立的显存、缓存和计算单元,隔离性接近物理卡。缺点是 MIG 会降低单实例峰值算力,且部分推理框架对 MIG 的支持需要额外配置。时间片共享则通过驱动或第三方调度器在不同 Pod 之间快速切换,延迟不稳定,一般不推荐用于在线 LLM 服务。
| 调度方式 | 隔离性 | 显存利用率 | 适用场景 |
|---|---|---|---|
| 整卡独占 | 强 | 低 | 高吞吐离线、SLA 严格 |
| MPS | 弱 | 中 | 小模型高并发、内部实验 |
| MIG | 强 | 高 | 多租户、中小模型 |
| 时间片共享 | 中 | 高 | 非实时、批处理 |
选择粒度时,可以先测量模型的显存峰值,再结合单卡显存计算能切分几个实例。对于 7B 到 13B 的模型,在 40 GB 卡上使用 MIG 切分成两个 20 GB 实例通常比整卡独占更划算;对于 70B 模型,单卡放不下,必须使用张量并行跨多卡,此时整卡分配反而更简单。还要注意 MIG 需要重启 GPU 或通过 nvidia-smi mig 命令配置,调度器需要感知 MIG 实例而不是整卡数量。
三、Kubernetes 中声明 GPU 资源与扩展调度
Kubernetes 默认的调度器只能根据 nvidia.com/gpu 的数量做 bin packing,无法感知显存、算力利用率和拓扑位置。在安装 NVIDIA device plugin 后,节点会把 GPU 数量上报为可分配资源,Pod 通过在 limits 中请求 nvidia.com/gpu: 1 获得整卡。如果要使用 MIG,可以安装 NVIDIA MIG Manager 或配置 device plugin 的 MIG 策略,将 MIG 实例暴露为独立的资源类型,例如 nvidia.com/mig-1g.10gb。
apiVersion: v1
kind: Pod
metadata:
name: llm-inference-pod
spec:
containers:
- name: vllm
image: registry.ipipp.com/llm/vllm:0.6.3
resources:
limits:
nvidia.com/gpu: 1
memory: 64Gi
cpu: 16
env:
- name: CUDA_VISIBLE_DEVICES
value: "0"
volumeMounts:
- name: model-store
mountPath: /models
volumes:
- name: model-store
persistentVolumeClaim:
claimName: llama3-8b-model
仅靠数量调度会出现一个问题:两个 Pod 各请求一张卡,但其中一个实际需要 35 GB 显存,另一个需要 10 GB,如果节点上只剩一张 40 GB 卡,两个 Pod 都无法调度,即使这张卡能同时装下两个模型。要解决这个问题,可以在节点上运行显存监控导出器,并使用 Volcano 或自定义调度器按显存请求进行调度。Volcano 支持多维度资源建模和公平调度,也支持 gang scheduling,适合需要多卡并行的 LLM 推理任务。另一个轻量方案是使用 Kubernetes 的 nodeSelector 或 affinity 把不同规格的推理服务绑定到不同 GPU 型号节点,避免把大模型调度到小显存卡上。
超卖和优先级也是生产集群必须考虑的。如果业务分为在线和离线两类,可以将在线服务设置为高优先级,离线批处理使用低优先级和可抢占 Pod。当在线流量上涨时,调度器驱逐离线任务释放 GPU,这与 CPU 内存资源的超卖逻辑类似。需要注意的是,GPU 显存超卖无法像 CPU 那样平滑,必须由推理框架主动限制 KV cache 大小,否则多个模型同时启动时会触发 OOM。vLLM 的 --gpu-memory-utilization 参数就是用来控制单个模型可占用显存比例,建议设置为 0.85 到 0.9,留出碎片和输入处理空间。
四、性能调优与监控闭环
容器化和调度策略只解决资源分配问题,推理服务的实际吞吐和延迟还取决于批处理参数和显存管理。以 vLLM 为例,--max-num-seqs 控制一个 batch 中最多容纳的序列数,--max-model-len 限制最大输入加输出长度。如果设置过大,显存占用上升但 batch 内序列稀疏,利用率下降;设置过小,请求会在队列中等待,P99 延迟上升。建议先用压测工具模拟真实请求分布,找到显存占用和吞吐的拐点,再固定参数。PagedAttention 机制通过 KV cache 分页管理减少碎片,也能提高批量处理的稳定性。
监控方面,使用 NVIDIA DCGM 导出 GPU 利用率、显存占用、温度和功耗指标到 Prometheus,并结合推理框架暴露的请求队列长度、批处理大小、token 生成速度等指标做 Grafana 看板。仅看 GPU 利用率可能产生误导:一个小 batch 推理也可能把 SM 利用率打满,但队列已经积压严重。更有效的指标是每张卡上的活跃请求数、KV cache 占用率和 token 生成吞吐。告警规则可以设置为:当显存占用超过 90% 且请求队列长度持续大于 10 时触发扩容或负载迁移。
apiVersion: v1
kind: ConfigMap
metadata:
name: dcgm-exporter-config
data:
dcgm-metrics.csv: |
DCGM_FI_DEV_GPU_UTIL, gauge, gpu_utilization
DCGM_FI_DEV_MEM_COPY_UTIL, gauge, mem_copy_utilization
DCGM_FI_DEV_FB_USED, gauge, framebuffer_used
DCGM_FI_DEV_FB_FREE, gauge, framebuffer_free
自动化伸缩不能只看 GPU 指标。在线推理服务的扩容速度受限于模型加载时间,通常需要提前预留 warm pool。可以在 HPA 中配置自定义指标,例如基于请求队列长度或并发连接数扩展,但扩容步长要结合模型加载耗时和流量增长速度。对于突发流量,可以将部分请求降级到更小的模型或使用投机采样减少大模型调用次数。关闭容器时也要注意优雅退出,先停止接收新请求,等待正在执行的生成任务完成,再释放 GPU 显存,否则会出现请求中断和显存泄漏。
LLM 服务容器化与 GPU 调度不是一次性的配置工作,而是一个持续调整的过程。模型升级、流量变化和 GPU 型号替换都会影响之前的参数选择。建议把镜像构建、调度策略和监控告警都纳入 CI/CD 和 GitOps 流程,让每次变更可以回滚和对比。只有把容器运行时、调度粒度和性能监控结合起来,才能在 GPU 资源有限的情况下稳定支撑大模型推理服务。