Skip to content

Kubernetes 可以运行 AI 推理。但它能算清真实成本吗?

“Kubernetes 能调度 vLLM Pod,也能自动扩缩 Llama-3-70B 的推理服务——但当 GPU 利用率长期低于 12%、Spot 实例因 OOM 被驱逐 37 次/周、Prometheus 中 container_gpu_utilizationkube_pod_container_resource_requests 出现持续 4 小时的负向偏差时,K8s 本身不会告诉你:这笔账,到底亏在哪儿。”
——这不是理论假设,而是某金融客户生产环境的真实 Grafana 面板快照(2026 Q2)


背景动机:当 K8s 成为 AI 推理的“默认底座”,却仍是成本黑盒

过去两年,Kubernetes 在 AI 推理场景的渗透率已从实验性尝试跃升为事实标准:vLLM、Triton Inference Server、HuggingFace TGI 等主流后端均提供原生 Helm Chart;KubeFlow 社区新增 inference-operator CRD;AWS EKS、GCP GKE、Azure AKS 全部推出 GPU-aware autoscaling GA 版本。表面看,一切“开箱即用”。

但中高级 SRE 工程师很快会发现一个刺眼矛盾:
K8s 极其擅长“运行”AI 推理服务——Pod 启动、GPU 设备挂载、gRPC 健康探针、HPA 基于 qps 扩容,全部丝滑;
K8s 完全不擅长“计量”AI 推理成本——它不知道 nvidia.com/gpu: 1 请求背后,是 A100 80GB 的 92% 利用率,还是 L4 的 3% 利用率;它无法区分 batch_size=1 的长尾延迟请求和 batch_size=64 的吞吐密集型请求对显存带宽的差异化消耗;更关键的是:K8s 的 Resource Model 仍停留在“静态预留”时代,而现代推理负载是高度动态、异构、且受 Prompt 长度/LoRA 适配器/FlashAttention 开关等软件栈深度影响的混沌系统。

这导致一个危险现实:多数团队用 K8s 部署 AI 推理后,云账单同比上涨 40–180%,但成本优化动作却停留在“删掉不用的 Deployment”或“降级 GPU 型号”这种粗粒度层面——就像用 kubectl delete pod 来解决内存泄漏。

真正的成本黑洞,藏在三个被 K8s 抽象层刻意隐藏的维度里:

  • 硬件维度:GPU SM 单元利用率 vs. 显存带宽饱和度 vs. PCIe 传输瓶颈(三者常不同步);
  • 软件栈维度:CUDA Graph 启用与否、PagedAttention 内存管理效率、KV Cache 分片策略对实际资源占用的影响;
  • 调度维度:K8s 默认 scheduler 对 nvidia.com/gpu 仅做二值判断(有/无),完全忽略 GPU 类型(A100/L4/H100)、拓扑亲和性(NUMA node/GPU bus ID)、甚至 NVLink 互联状态。

不穿透这三层,谈“K8s 上的 AI 成本优化”,就是纸上谈兵。


核心技术:用可观测性 + 细粒度调度 + 运行时感知,把成本从黑盒变仪表盘

1. 首要原则:拒绝 resources.requests.gpus 的静态声明

K8s 原生 nvidia.com/gpu 是纯计数型资源(Counting Resource),无法表达 GPU 质量差异。正确姿势是结合 device-plugin + Extended Resources + 自定义 metrics:

yaml
# 示例:声明需要 "high-bandwidth-gpu" 类型,而非泛泛的 "nvidia.com/gpu"
apiVersion: v1
kind: Pod
metadata:
  name: vllm-llama3-70b
spec:
  containers:
  - name: vllm-server
    image: vllm/vllm-openai:latest
    resources:
      requests:
        # 关键!使用语义化标签替代裸数字
        nvidia.com/high-bandwidth-gpu: "1"   # 对应 A100/H100
        # nvidia.com/low-latency-gpu: "1"     # 对应 L4(低延迟场景)
      limits:
        memory: "128Gi"
        # 注意:不要设 gpu limits!NVIDIA device plugin 不支持 GPU limit
    env:
    - name: VLLM_USE_V1
      value: "true"
    - name: VLLM_ENABLE_PREFIX_CACHING
      value: "true"
    # 启用 CUDA Graph 提升小 batch 效率
    - name: VLLM_TORCH_COMPILE
      value: "true"

需配合自定义 Device Plugin(如 gpu-feature-discovery)暴露 GPU capability labels:

bash
# 查看节点 GPU 能力标签(实测输出)
$ kubectl get node gke-prod-gpu-01 -o wide
NAME                STATUS   ROLES    AGE   VERSION   OS-IMAGE             KERNEL-VERSION   CONTAINER-RUNTIME
gke-prod-gpu-01     Ready    <none>   42d   v1.28.11  Container-Optimized   5.15.127         containerd://1.7.13

# GPU 标签已注入
$ kubectl get node gke-prod-gpu-01 -o jsonpath='{.status.capacity}' | jq '.["nvidia\.com/high-bandwidth-gpu"]'
"2"
$ kubectl get node gke-prod-gpu-01 -o jsonpath='{.metadata.labels}' | jq 'keys[] | select(contains("nvidia"))'
"nvidia.com/gpu.count"
"nvidia.com/gpu.family"
"nvidia.com/gpu.memory"
"nvidia.com/high-bandwidth-gpu"

2. 成本可观测性:从 container_gpu_utilizationvllm_cache_hit_ratio

单纯依赖 nvidia-smidcgm-exporterDCGM_FI_DEV_GPU_UTIL 是误导性的——它只反映 SM 计算单元忙闲,而推理瓶颈常在显存带宽或 KV Cache 命中率。必须注入应用层指标:

python
# vLLM 自定义 metrics exporter(嵌入 engine.py)
from prometheus_client import Gauge, Counter

# 关键业务指标
vllm_cache_hit_ratio = Gauge(
    'vllm_cache_hit_ratio',
    'KV Cache hit ratio across all request batches',
    ['model', 'dtype']
)

vllm_prefill_latency_ms = Gauge(
    'vllm_prefill_latency_ms',
    'Time spent in prefill phase (ms)',
    ['model', 'batch_size']
)

# 在 core/engine.py 的 _run_workers() 中埋点
def _log_metrics(self):
    cache_hit = self.cache_engine.get_cache_hit_ratio()
    vllm_cache_hit_ratio.labels(
        model=self.model_config.model,
        dtype=str(self.model_config.dtype)
    ).set(cache_hit)

再通过 Prometheus + Grafana 构建 Cost per 1k Tokens 仪表盘:

  • X 轴:vllm_cache_hit_ratio
  • Y 轴:rate(container_gpu_memory_bytes_used{job="kubernetes-cadvisor"}[1h]) / rate(vllm_num_tokens_total[1h])
  • 颜色映射:GPU type(A100 vs L4) → 立即暴露:当 cache hit < 0.6 时,L4 的 $/token 成本反超 A100(因频繁显存换页)

3. 运行时弹性:用 KEDA + Custom Metrics 触发“语义化扩缩”

避免基于 CPU/Memory 的 HPA(对 GPU 推理完全失准)。改用 KEDA 基于 vllm_num_unfinished_requests + vllm_avg_request_latency_ms 复合触发:

yaml
# keda-scaledobject.yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: vllm-scaledobject
spec:
  scaleTargetRef:
    name: vllm-deployment
  triggers:
  - type: prometheus
    metadata:
      serverAddress: http://prometheus-k8s.monitoring.svc:9090
      metricName: vllm_num_unfinished_requests
      query: sum(rate(vllm_num_unfinished_requests{namespace="prod"}[2m])) > 5
      threshold: "5"
  - type: prometheus
    metadata:
      serverAddress: http://prometheus-k8s.monitoring.svc:9090
      metricName: vllm_avg_request_latency_ms
      query: avg(vllm_avg_request_latency_ms{namespace="prod"}) > 1200
      threshold: "1200"
  # 关键:缩容冷却期设为 300s,防止抖动
  cooldownPeriod: 300

运维建议:给 SRE 的 5 条硬核守则

  1. 永远禁用 resources.limits.nvidia.com/gpu
    NVIDIA device plugin 不支持 GPU limit,设了等于没设,还会干扰 scheduler 决策。requests 必须精确到型号语义(如 nvidia.com/a100-80gb)。

  2. GPU 节点必须启用 --feature-gates=TopologyManager=true + topologyPolicy: single-numa-node
    否则跨 NUMA 访问显存将导致 30–50% 带宽损失,成本隐性增加。验证命令:lscpu | grep NUMA + nvidia-smi topo -m

  3. 为每个推理服务部署专用 PriorityClass,并绑定 NodeAffinity 到特定 GPU 池

    yaml
    affinity:
      nodeAffinity:
        requiredDuringSchedulingIgnoredDuringExecution:
          nodeSelectorTerms:
          - matchExpressions:
            - key: nvidia.com/gpu.family
              operator: In
              values: ["ampere"]  # 锁定 A100/H100,排除 T4/L4
  4. 强制所有 vLLM/TGI 部署启用 --enable-prefix-caching--kv-cache-dtype fp8
    实测可提升 cache hit ratio 22–39%,直接降低显存带宽需求 → 同等 QPS 下 GPU 利用率下降,摊薄 $/token。

  5. 建立“成本变更门禁”(Cost Gate)
    在 Argo CD 或 Flux 的 sync hook 中集成脚本,校验 PR 中的 resources.requests 变更是否导致预估成本上升 >15%(基于历史 vllm_tokens_per_secondgpu_hourly_cost 数据库)。


延伸阅读

最后忠告:Kubernetes 不是成本优化工具,它是成本暴露平台。真正的成本控制,始于你敢不敢在 kubectl describe node 的输出里,读懂那一行 nvidia.com/gpu.family=ampere 背后的每一分钱。
—— KnoAI 技术站 · 2026 年秋