AI/ML 基础设施编排:GPU 节点池与推理服务

用 Terraform 编排 AI/ML 基础设施:GPU 节点池与自动伸缩、Kubernetes 与推理服务部署、模型与数据集存储、弹性排队与 GPU 成本优化。

1. AI/ML 基础设施的编排挑战

一句话总结: AI/ML 基础设施的难点不在「能不能起集群」,而在异构算力、秒级弹性、昂贵资源与快速迭代这四者之间长期存在的张力。

传统 Web 基础设施的编排假设是:实例同质、负载平稳、扩缩容以分钟计。AI/ML 场景几乎把这三个假设全部推翻。训练任务要的是「短时间拿到几百张卡、跑完立刻释放」,推理服务要的是「常驻低延迟、按 QPS 弹性」,而两者共用同一套集群时又互相抢资源。Terraform 在这里的角色不是替代 Kubernetes,而是把「集群骨架、节点池分层、存储与网络边界」这些变化频率低但影响面大的部分固化下来,让 K8s 之上的调度器专心处理高频变更。

1.1 四类核心张力

一句话总结: 异构、弹性、昂贵、迭代快——四个约束互相牵制,编排方案必须显式地在它们之间做取舍而不是假装都能满足。

  • 异构算力:A100/H100/L4/T4 的显存、互联(NVLink/NVSwitch)、驱动与 CUDA 版本各不相同,节点池必须按机型分层,镜像也要按 GPU 型号区分。
  • 弹性与冷启动:GPU 节点从创建到可调度通常需要 3~8 分钟(镜像拉取、驱动初始化、CNI 就绪),而请求峰值可能只有 30 秒,缓冲池与预热策略是刚需。
  • 单位成本高:单卡月成本可达数千元,闲置一小时就是实打实的浪费,利用率指标必须进入编排的一等公民。
  • 迭代速度快:模型版本、推理框架、批大小几乎每周都在变,基础设施要允许「不重建集群就能换模型」。

1.2 分层参考架构

一句话总结: 把基础设施切成「集群骨架 / 节点池 / 工作负载 / 数据」四层,各层用不同工具与变更频率管理,是避免 Terraform 状态爆炸的关键。

[第 4 层] 数据:对象存储桶 / 共享文件系统 / 特征库
[第 3 层] 工作负载:Deployment / Job / Kueue 队列 / HPA
[第 2 层] 节点池:GPU 机型分层 + taint + 自动伸缩
[第 1 层] 集群骨架:VPC / 子网 / IAM / EKS 或 GKE 控制面

第 1、2 层用 Terraform 管理,变更频率低、需要强一致;第 3 层主体交给 GitOps 与 K8s 控制器;第 4 层用 Terraform 建桶与策略,用应用侧做对象生命周期。这条分界线一旦画错,最常见的结果就是「每次改个副本数都要 plan 三分钟」。

2. GPU 节点池与自动伸缩

一句话总结: GPU 节点池的设计核心是「按机型分层 + 用 taint 隔离 + 让自动伸缩器看懂 GPU 资源」,三者缺一不可。

节点池不是「一个大池子放所有卡」。实践中至少要分三类:交互式开发池(单卡或双卡,允许抢占,成本优先)、训练池(8 卡 NVLink 整机,禁止碎片化,稳定性优先)、推理池(中低端卡,常驻,弹性优先)。分层之后,调度与计费都能按池维度做策略。

2.1 EKS 托管节点组定义

一句话总结: 托管节点组把驱动安装与节点生命周期交给 EKS,Terraform 只需声明机型、容量类型与标签,重点是把 GPU 相关的标签打全。

resource "aws_eks_node_group" "gpu_inference" {
  cluster_name    = aws_eks_cluster.main.name
  node_group_name = "gpu-inference"
  node_role_arn   = aws_iam_role.node.arn
  subnet_ids      = var.private_subnet_ids

  ami_type       = "AL2_x86_64_GPU"
  instance_types = ["g5.xlarge", "g5.2xlarge"]
  capacity_type  = "ON_DEMAND"

  scaling_config {
    desired_size = 2
    min_size     = 1
    max_size     = 20
  }

  labels = {
    "node-type"                  = "gpu-inference"
    "nvidia.com/gpu.product"     = "NVIDIA-A10G"
    "karpenter.sh/capacity-type" = "on-demand"
  }

  taint {
    key    = "nvidia.com/gpu"
    value  = "present"
    effect = "NO_SCHEDULE"
  }
}

ami_type = AL2_x86_64_GPU 会预装 NVIDIA 驱动与 device plugin,省掉自建 AMI 的维护成本。训练池则改用 capacity_type = "SPOT" 与更大的 instance_types,并显式声明 max_size 上限防止账单失控。

2.2 Taint 与自动伸缩

一句话总结: taint 保证只有声明了容忍的工作负载才落到 GPU 节点,而 Cluster Autoscaler 或 Karpenter 负责在「有 Pod 因 GPU 不足 Pending」时扩池。

taint 的写法要成对出现:节点侧 nvidia.com/gpu=present:NoSchedule,工作负载侧写对应的 tolerations。这样普通 CPU 服务不会被误调度到每小时几十元的机器上。

tolerations:
  - key: nvidia.com/gpu
    operator: Equal
    value: present
    effect: NoSchedule
nodeSelector:
  node-type: gpu-inference

自动伸缩方面,Cluster Autoscaler 依赖节点组的 min/max 与 --expander 策略,Karpenter 则直接读取 Pod 的 resources.requests 与 nodeSelector 做即时供给,冷启动更快。两种方案都要求 Pod 必须显式声明 nvidia.com/gpu: 1,否则调度器与伸缩器都看不到需求。

3. Kubernetes 与推理服务部署

一句话总结: 推理服务部署的分工是「Helm/kubernetes provider 建骨架,Deployment 与 HPA 管副本,模型服务框架管吞吐」。

集群与节点池就绪后,推理服务的编排分两段:Terraform 负责命名空间、ServiceAccount、ConfigMap、Ingress 与初始 Deployment;之后的副本调整交给 HPA,避免把高频变更塞进 Terraform 状态。

3.1 用 Helm 发布推理服务

一句话总结: 用 helm_release 发布推理服务可以复用社区 Chart 的成熟默认值,Terraform 只覆写关键参数。

resource "helm_release" "vllm" {
  name       = "vllm-qwen"
  namespace  = kubernetes_namespace.ml.metadata[0].name
  chart      = "vllm"
  repository = "https://vllm-project.github.io/production-stack"
  version    = "0.1.4"

  set {
    name  = "model"
    value = "Qwen/Qwen2.5-7B-Instruct"
  }
  set {
    name  = "resources.limits.nvidia\\.com/gpu"
    value = "1"
  }

  values = [file("${path.module}/values/vllm.yaml")]
}

若不想引入 Chart 依赖,也可以直接用 kubernetes_deployment_v1 写原生资源,代价是要自己维护探针、卷挂载与资源声明。

3.2 Deployment 与 HPA 片段

一句话总结: 推理 Deployment 的关键是就绪探针要等模型真正加载完,HPA 要基于 GPU 利用率或队列深度而不是 CPU。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: vllm-qwen
spec:
  replicas: 2
  selector:
    matchLabels:
      app: vllm-qwen
  template:
    metadata:
      labels:
        app: vllm-qwen
    spec:
      containers:
        - name: server
          image: vllm/vllm-openai:v0.6.3
          args: ["--model", "Qwen/Qwen2.5-7B-Instruct", "--max-model-len", "8192"]
          resources:
            limits:
              nvidia.com/gpu: 1
          readinessProbe:
            httpGet:
              path: /health
              port: 8000
            initialDelaySeconds: 60
            periodSeconds: 10

initialDelaySeconds 给到 60 秒以上是常态:7B 模型加载加显存预热往往就要 30~90 秒,探针过早会把刚起的 Pod 判死并触发重启循环。HPA 则建议用 autoscaling/v2 配合自定义指标(GPU 利用率或 vLLM 的 num_requests_waiting)。

4. 模型与数据集存储

一句话总结: 模型与数据存储要解决三件事:桶策略与加密、共享文件系统的多读并发、以及「训练数据尽量不跨区读」。

存储是最容易被低估的一环。一个 70B 模型权重约 140 GB,数据集动辄 TB 级,如果每个 Pod 启动都从公网拉一遍,网络成本与冷启动时间都会失控。

4.1 对象存储桶与生命周期

一句话总结: 用 Terraform 建桶时把版本控制、加密、生命周期与访问策略一次配齐,避免事后补丁式加固。

resource "aws_s3_bucket" "models" {
  bucket = "acme-ml-models-prod"
}

resource "aws_s3_bucket_versioning" "models" {
  bucket = aws_s3_bucket.models.id
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_lifecycle_configuration" "models" {
  bucket = aws_s3_bucket.models.id

  rule {
    id     = "expire-old-checkpoints"
    status = "Enabled"

    filter {
      prefix = "checkpoints/"
    }

    expiration {
      days = 30
    }
  }
}

训练检查点(checkpoint)是最容易堆积的垃圾:一次长训练可能产出上百个 GB 的中间态。给它单独设 30 天过期规则,比事后人工清理可靠得多。推理用的正式权重则放在 models/ 前缀下,开启版本控制并禁止生命周期删除。

4.2 共享文件系统与就近读取

一句话总结: 多 Pod 需要同时只读挂载同一份权重时用 EFS/Filestore,但要注意吞吐模式与同可用区约束。

resource "aws_efs_file_system" "shared" {
  creation_token  = "ml-shared"
  encrypted       = true
  throughput_mode = "elastic"

  lifecycle_policy {
    transition_to_ia = "AFTER_30_DAYS"
  }
}

EFS 的吞吐模式选 elastic 可以避免预置吞吐的容量绑定;挂载目标必须建在 GPU 节点所在的每个可用区,否则跨区访问会引入毫秒级延迟与额外流量费。GKE 侧的对应方案是 Filestore,Terraform 资源为 google_filestore_instance,注意 tier 决定了最小容量与带宽。

5. 弹性、排队与批处理

一句话总结: 弹性的本质是「排队 + 优先级 + 抢占」三件套:谁先拿到卡、谁可以被踢掉、谁的额度不能超。

训练与推理混部时,如果不做排队,结果是所有任务一起变慢。Kueue 通过 ClusterQueue 与 LocalQueue 把配额显式化,Pod 在准入阶段就排队,而不是等调度器反复重试。

5.1 Spot 与抢占式实例

一句话总结: Spot 能把 GPU 成本降 60% 以上,代价是两分钟中断通知,因此只适合可断点续训的任务。

resource "aws_eks_node_group" "gpu_spot" {
  cluster_name    = aws_eks_cluster.main.name
  node_group_name = "gpu-spot-training"
  node_role_arn   = aws_iam_role.node.arn
  subnet_ids      = var.private_subnet_ids

  ami_type       = "AL2_x86_64_GPU"
  instance_types = ["p4d.24xlarge", "p4de.24xlarge", "p5.48xlarge"]
  capacity_type  = "SPOT"

  scaling_config {
    desired_size = 0
    min_size     = 0
    max_size     = 16
  }
}

配合 AWS Node Termination Handler 或 Karpenter 的中断处理,节点在收到回收通知时先 cordon 再驱逐,训练脚本监听 SIGTERM 保存检查点后退出。多机型混排(instance_types 给三个以上)能显著降低「全部机型同时无容量」的概率。

5.2 Kueue 队列与配额

一句话总结: Kueue 把「谁能用多少卡」写成声明式配额,团队之间不再靠口头约定。

apiVersion: kueue.x-k8s.io/v1beta1
kind: ClusterQueue
metadata:
  name: gpu-cluster-queue
spec:
  namespaceSelector: {}
  resourceGroups:
    - coveredResources: ["cpu", "memory", "nvidia.com/gpu"]
      flavors:
        - name: on-demand-gpu
          resources:
            - name: nvidia.com/gpu
              nominalQuota: 32
        - name: spot-gpu
          resources:
            - name: nvidia.com/gpu
              nominalQuota: 64

nominalQuota 是硬上限,flavors 让同一队列可以先用按需再溢出到 Spot。配合 preemption 策略,高优先级任务可以抢占低优先级任务的配额,这正是「交互式调试优先、离线训练让路」的落地方式。

6. 成本与可观测

一句话总结: GPU 成本治理的前提是能按团队、按任务、按节点池算清账,而算账的前提是标签体系从第一天就打全。

没有标签的 GPU 集群等于没有账单明细。Terraform 应该在节点组、命名空间、甚至 Pod 模板上都注入一致的标签键,让成本分摊工具(Kubecost、云厂商成本探针)能把费用归到具体团队。

6.1 标签与成本分摊

一句话总结: 至少要有 cost-center、team、workload-type 三个维度,并在节点组与命名空间两侧同时打标。

locals {
  common_tags = {
    "cost-center"   = "ml-platform"
    "team"          = var.team
    "workload-type" = "gpu-inference"
    "managed-by"    = "terraform"
  }
}

resource "aws_eks_node_group" "gpu_inference" {
  # ... 省略集群与角色配置
  tags   = merge(local.common_tags, { Name = "gpu-inference" })
  labels = { "cost-center" = "ml-platform", "workload-type" = "gpu-inference" }
}

节点标签与 K8s 标签成对出现,Kubecost 才能把节点费用按 Pod 请求量分摊下去。若只打云标签,K8s 侧的 Pod 归属仍然是一笔糊涂账。

6.2 利用率指标与闲置回收

一句话总结: 关注 DCGM 的 GPU 利用率与显存占用,把「连续 30 分钟利用率低于 5%」的节点纳入自动回收。

# 查询最近 24 小时 GPU 平均利用率低于阈值的节点
kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1/namespaces/ml/pods/*/DCGM_FI_DEV_GPU_UTIL" \
  | jq -r '.items[] | select(.value < "5") | .metadata.name'

更实际的做法是让监控系统(Prometheus + DCGM Exporter)持续采集,对空闲节点先打 idle=true 标签并 cordon,观察一个窗口期后再由伸缩器缩容。开发池尤其适合这种策略:白天满负荷、夜间归零。

7. 安全与多租户隔离

一句话总结: 多租户隔离要同时管住四件事:命名空间配额、镜像来源、密钥访问、网络与数据边界。

共享 GPU 集群通常服务多个团队,隔离不做好就会出现「一个团队的任务把整集群卡住」或者「镜像里带走了别人的凭据」。

7.1 命名空间、配额与网络策略

一句话总结: ResourceQuota 限制总量、LimitRange 限制单 Pod、NetworkPolicy 限制横向流量,三者配合才形成有效边界。

apiVersion: v1
kind: ResourceQuota
metadata:
  name: gpu-quota
  namespace: team-alpha
spec:
  hard:
    requests.nvidia.com/gpu: "8"
    limits.nvidia.com/gpu: "8"
    requests.cpu: "64"
    requests.memory: 256Gi
    persistentvolumeclaims: "10"

配额之外还要配 NetworkPolicy:默认拒绝所有入站,只放行同一命名空间与 Ingress 控制器的流量。GPU 训练任务常需要节点间高速通信(NCCL),因此要额外放行同节点池内的全部端口。

7.2 镜像、密钥与数据访问边界

一句话总结: 镜像只允许来自受信仓库、密钥用 External Secrets 或 CSI 驱动注入、数据访问用 IAM 角色而非长期 AK。

resource "kubernetes_namespace_v1" "ml" {
  metadata {
    name = "ml"

    labels = {
      "pod-security.kubernetes.io/enforce" = "restricted"
    }
  }
}

pod-security.kubernetes.io/enforce: restricted 会禁止特权容器与宿主机路径挂载,这对共享集群是必要的默认值。数据侧则用 IRSA(IAM Roles for Service Accounts)把桶访问权限绑到 ServiceAccount,避免把 AK/SK 写进 Secret 再挂进 Pod。

8. 总结

环节要点
集群骨架VPC、子网、IAM 与控制面用 Terraform 固化,变更频率低、需强一致
节点池分层开发、训练、推理三类池分开,机型与容量类型按需组合
GPU 隔离taint 与容忍成对出现,Pod 必须显式声明 nvidia.com/gpu
推理部署Helm 或原生资源建骨架,就绪探针等模型加载完,HPA 看 GPU 指标
存储对象存储管版本与生命周期,共享文件系统注意同区与吞吐模式
弹性排队Kueue 配额加优先级抢占,Spot 降本但只用于可续训任务
成本与可观测标签三维度打全,DCGM 利用率驱动闲置回收
多租户隔离ResourceQuota 加 NetworkPolicy 加 IRSA,密钥不进 Secret

AI/ML 基础设施的编排,说到底是在「成本」与「响应速度」之间找一个团队能长期承受的平衡点。Terraform 负责把集群骨架、节点池分层与安全边界这些低频高影响的部分固化下来,Kubernetes 之上的调度器负责高频的副本与队列变化。下一篇将讨论多云网络与互联,看看当训练集群与数据源分处不同云厂商时,网络层该怎么编排。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「terraform」更多文章

  1. Helm Provider 与应用发布:值注入与回滚
  2. 模块注册表与分发:版本、文档与测试
  3. DNS 与证书编排:托管区域与自动验证