AI 基础设施即代码:用 Terraform 管 GPU 集群不只是炫技
一、GPU 集群的手动管理比普通集群更容易出错
基础设施即代码(IaC)在普通 Kubernetes 集群管理中已经是标准实践——Terraform 定义节点池、VPC 网络、IAM 权限,变更通过 Git 提交触发 CI/CD 应用。但 GPU 集群的 IaC 化率明显低于普通集群,原因不在于技术难度,在于认知偏差。
GPU 集群的运维团队常持有的观点是:GPU 节点数量少(10-50 台 vs 普通节点 100-500 台),手动管理可以应付;GPU 节点配置特殊(驱动版本、CUDA 版本、NCCL 参数),手动调优比模板化更灵活;GPU 集群变更频率低(训练任务周期以天为单位),不需要 IaC 的快速响应能力。
这些观点看似务实,实际上忽略了 GPU 集群管理的三个结构性问题。第一,GPU 节点的配置复杂度比普通节点高 5 倍以上:NVIDIA 驱动版本、CUDA toolkit 版本、容器运行时(nvidia-container-runtime)、设备插件(nvidia-device-plugin)四个组件的版本必须兼容匹配,一个版本不匹配就导致 GPU 不可用。手动管理 20 台 GPU 节点的驱动版本一致性,每次升级都要逐台检查,遗漏一台就会有一个节点的训练任务全部失败。第二,GPU 节点的弹性伸缩依赖精确的资源配置:自动扩缩容时新节点必须携带正确的 GPU 型号、驱动版本和容器运行时配置,手动配置的节点池在扩容时容易出现规格不一致。第三,GPU 集群的故障排查依赖完整的配置记录:节点 GPU 不可用时,需要确认驱动版本、设备插件状态、容器运行时配置、内核模块加载情况,手动管理的节点没有结构化配置记录,排查靠 SSH 到节点上逐个检查。
Terraform 管 GPU 集群的核心价值不是"自动化"(手动也能做),而是"可追溯、可复现、可审计"。GPU 集群的每项配置变更都在 Terraform 状态文件中有记录,变更原因在 Git 提交历史中有追溯,节点故障的配置差异可以通过 Terraform plan 检测发现。基础设施不需要漂亮话,但 GPU 集群的每次故障排查都需要配置基线来托底。
二、GPU 集群 IaC 的配置依赖链路
GPU 集群 IaC 不是简单的"定义节点池",它是一条从底层硬件到上层调度的完整配置依赖链路。
graph TD A[云厂商 GPU 实例规格] --> B[NVIDIA 驱动安装] B --> C[CUDA Toolkit 版本匹配] C --> D[容器运行时: nvidia-container-runtime] D --> E[Kubernetes nvidia-device-plugin] E --> F[GPU 资源在 K8s 中可见] F --> G[训练 Pod 可以调度到 GPU 节点] G --> H[NCCL 网络参数调优] H --> I[多 GPU 分布式训练可用] B --> B1[Terraform: GPU 驱动安装脚本] C --> C1[Terraform: CUDA 版本变量] D --> D1[Terraform: 容器运行时配置] E --> E1[Helm: device-plugin DaemonSet] H --> H1[Terraform: 网络接口配置] style F fill:#9f9,stroke:#333 style I fill:#9f9,stroke:#333依赖链路从云厂商的 GPU 实例规格开始。选择实例规格时已经隐含了 GPU 型号(如 A100、H100),GPU 型号决定了驱动版本的最低要求。驱动版本又决定了 CUDA Toolkit 的兼容版本范围。CUDA 版本影响训练框架(PyTorch、TensorFlow)的版本选择。容器运行时需要配置 nvidia-container-runtime 来把 GPU 设备映射到容器内。Device Plugin 需要匹配驱动和 CUDA 版本。NCCL 参数依赖节点的网络拓扑(InfiniBand 或 RoCE 网卡型号)。
每个环节的配置变更都会影响下游。驱动升级后,CUDA 版本必须同步更新,否则容器内的训练框架找不到 CUDA 库。Device Plugin 版本必须匹配新的驱动 API,否则 GPU 资源在 Kubernetes 中不可见。NCCL 网络参数依赖网卡的固件版本,固件升级后 NCCL 的超时阈值可能需要调整。
手动管理这条链路,每个组件的版本和配置散落在各节点的脚本和文档中,没有统一的版本视图和变更历史。Terraform 把整条链路的配置集中定义,版本匹配关系在变量依赖中显式表达,变更历史在 Git 中完整保留。
三、GPU 集群的 Terraform 配置代码
下面给出 GPU 集群的核心 Terraform 配置,包含节点池、驱动安装、容器运行时和 Device Plugin。
# gpu_cluster_main.tf — GPU 集群 IaC 配置 # 变量定义:GPU 集群的核心版本参数 variable "gpu_driver_version" { description = "NVIDIA 驱动版本,必须与 GPU 型号和 CUDA 版本兼容" type = string default = "535.129.03" # 支持 A100/H100 的稳定版本 } variable "cuda_version" { description = "CUDA Toolkit 版本,必须与驱动版本兼容" type = string default = "12.2.2" # 与驱动 535.x 兼容 } variable "gpu_instance_type" { description = "GPU 实例规格,决定 GPU 型号和数量" type = string default = "n1-standard-8-a100" # 1x A100 80GB } variable "gpu_node_count" { description = "GPU 节点数量" type = number default = 4 } variable "nccl_net_interface" { description = "NCCL 使用的网络接口,影响分布式训练性能" type = string default = "eth0" # RoCE 网络接口名 } # GPU 节点池定义 resource "google_container_node_pool" "gpu_pool" { name = "gpu-pool" cluster = google_container_cluster.primary.name location = var.region node_count = var.gpu_node_count # 自动扩缩容配置 autoscaling { min_node_count = 2 max_node_count = 10 } management { auto_repair = true auto_upgrade = false # GPU 驱动升级需手动触发,不能自动升级 } node_config { machine_type = var.gpu_instance_type image_type = "cos_containerd" # GCP Container-Optimized OS # GPU 驱动预安装:GKE 在节点创建时自动安装指定版本 guest_accelerator { type = "nvidia-tesla-a100" count = 1 } # GPU 驱动安装配置 metadata = { "gpu-driver-version" = var.gpu_driver_version "cuda-version" = var.cuda_version } # 资源请求和限制 kubelet_config { cpu_manager_policy = "static" # GPU 节点 CPU 管理策略 } # 节点标签,用于 Pod 调度 labels = { "gpu-type" = "a100" "gpu-driver-version" = var.gpu_driver_version "cuda-version" = var.cuda_version "workload-type" = "training" } # 节点污点,防止非 GPU Pod 调度到 GPU 节点 taint { key = "nvidia.com/gpu" value = "present" effect = "NO_SCHEDULE" } } } # NCCL 网络参数配置(通过 DaemonSet 在 GPU 节点上设置) resource "kubernetes_daemonset" "nccl_config" { metadata { name = "nccl-network-config" namespace = "kube-system" } spec { selector { match_labels = { "app" = "nccl-network-config" } } template { metadata { labels = { "app" = "nccl-network-config" } } spec { node_selector = { "gpu-type" = "a100" } toleration { key = "nvidia.com/gpu" operator = "Exists" effect = "NoSchedule" } container { name = "nccl-configurator" image = "busybox:1.36" command = [ "sh", "-c", "echo 'NCCL_NET=${NCCL_NET}' > /etc/nccl.conf && " + "echo 'NCCL_NET_IFNAME=${NCCL_NET_IFNAME}' >> /etc/nccl.conf && " + "echo 'NCCL_SOCKET_IFNAME=${NCCL_SOCKET_IFNAME}' >> /etc/nccl.conf && " + "echo 'NCCL_IB_DISABLE=${NCCL_IB_DISABLE}' >> /etc/nccl.conf && " + "sleep infinity" ] env { name = "NCCL_NET" value = "Socket" } env { name = "NCCL_NET_IFNAME" value = var.nccl_net_interface } env { name = "NCCL_SOCKET_IFNAME" value = var.nccl_net_interface } env { name = "NCCL_IB_DISABLE" value = "1" # 不使用 InfiniBand,用 RoCE } volume_mount { name = "nccl-config" mount_path = "/etc/nccl.conf" sub_path = "nccl.conf" } } volume { name = "nccl-config" config_map_ref { name = "nccl-config-map" } } } } } } # NVIDIA Device Plugin Helm 配置 resource "helm_release" "nvidia_device_plugin" { name = "nvidia-device-plugin" repository = "https://nvidia.github.io/k8s-device-plugin" chart = "nvidia-device-plugin" namespace = "kube-system" # Device Plugin 版本必须匹配驱动版本 set { name = "image.tag" value = "v0.14.5" # 匹配驱动 535.x } set { name = "resourceStrategy" value = "time-slicing" # GPU 时间切片,支持多 Pod 共享 GPU } set { name = "timeSlicingConfig.repetitions" value = "2" # 每个 GPU 分成 2 个切片 } set { name = "nodeSelector.gpu-type" value = "a100" } set { name = "tolerations[0].key" value = "nvidia.com/gpu" } set { name = "tolerations[0].operator" value = "Exists" } set { name = "tolerations[0].effect" value = "NoSchedule" } }版本兼容性校验模块:
# gpu_version_compat.tf — GPU 版本兼容性校验 # NVIDIA 驱动和 CUDA 版本兼容矩阵 # 参考:https://docs.nvidia.com/cuda/cuda-toolkit-release-notes/ locals { # 驱动 → CUDA 版本兼容性映射 driver_cuda_compat = { "535.129.03" = ["12.2.0", "12.2.1", "12.2.2"] "535.104.05" = ["12.2.0", "12.2.1", "12.2.2"] "525.147.05" = ["12.0.0", "12.1.0", "12.1.1"] } # GPU 型号 → 驱动版本最低要求 gpu_driver_min = { "a100" = "525.60.13" "h100" = "535.54.14" "l4" = "525.60.13" } # 校验当前配置的版本兼容性 cuda_compatible = contains( lookup(local.driver_cuda_compat, var.gpu_driver_version, []), var.cuda_version ) driver_min_ok = var.gpu_driver_version >= lookup( local.gpu_driver_min, substr(var.gpu_instance_type, -4, 4), # 从实例类型提取 GPU 型号 "0.0.0" ) } # 版本不兼容时阻断 Terraform 应用 resource "null_resource" "version_check" { count = local.cuda_compatible && local.driver_min_ok ? 0 : 1 triggers = { cuda_version = var.cuda_version driver_version = var.gpu_driver_version instance_type = var.gpu_instance_type } provisioner "local-exec" { command = "echo 'ERROR: GPU 版本不兼容!驱动 ${var.gpu_driver_version} 不支持 CUDA ${var.cuda_version}' && exit 1" } }四、GPU 集群 IaC 的适用边界与权衡
GPU 集群用 Terraform 管理,有几个权衡点需要明确。
权衡一:驱动安装方式
云厂商 GKE/GKE Autopilot 在节点创建时自动安装 NVIDIA 鐇动,版本通过 guest_accelerator 配置指定。这是最省事的方式,但驱动版本受云厂商发布节奏限制——新驱动版本可能延迟数周才可用。自建集群(EKS、AKS 或裸金属)需要手动安装驱动,通常用 DaemonSet 或节点初始化脚本。Terraform 可以定义初始化脚本内容,但脚本执行在 Terraform 应用之后,存在短暂的窗口期(节点已创建但驱动未安装)。
权衡二:配置变更的滚动策略
GPU 驱动升级需要节点重启。Terraform 的 node_pool 配置变更会触发节点池滚动更新——逐个替换节点,旧节点上的训练任务会被驱逐。如果在训练任务运行期间触发 Terraform 变更,训练中断。必须配合训练调度系统,在无任务运行时触发变更,或者设定 maintenance_window。这超出了 Terraform 本身的能力范围,需要外部编排。
权衡三:Terraform 状态与集群实际状态的差异
Terraform 状态记录的是配置定义值,集群实际状态可能因手动操作或自动修复而偏离。比如运维人员 SSH 到节点上修改了 NCCL 参数,Terraform 不知道这个变更,下次 apply 时会覆盖手动修改。解决方案是:禁止手动修改 GPU 节点配置(通过 IAM 权限限制 SSH),或者把 Terraform 状态作为唯一配置来源并定期 drift 检测。
权衡四:多环境配置一致性
生产 GPU 集群、开发 GPU 集群、实验 GPU 集群三套环境的驱动版本、CUDA 版本是否必须一致?生产环境用稳定版本(535.129.03 + CUDA 12.2.2),开发环境用最新版本(545.23.08 + CUDA 12.3.0)测试兼容性。Terraform 通过变量区分环境配置,但版本矩阵的维护成本增加——每个环境有自己的兼容校验。
权衡五:GPU 型号异构管理
混合 GPU 集群(A100 + H100 + L4)需要多个节点池,每个节点池有独立的驱动版本、CUDA 版本、Device Plugin 配置。Terraform 的变量矩阵从二维(驱动 × CUDA)变成三维(驱动 × CUDA × GPU 型号),配置复杂度显著增加。管理多型号集群的 Terraform 模块需要精心设计变量结构,避免重复定义。
五、总结
GPU 集群的 IaC 化不是为了自动化炫技,是为了配置可追溯、可复现、可审计。GPU 集群的配置依赖链路(实例规格 → 驱动 → CUDA → 容器运行时 → Device Plugin → NCCL)比普通集群复杂 5 倍以上,手动管理的版本一致性无法保证。Terraform 把整条链路的版本匹配关系在变量依赖中显式表达,版本不兼容时阻断应用。核心权衡包括驱动安装方式(云厂商自动 vs 手动脚本)、变更滚动策略(需配合训练调度)、状态一致性(Terraform 作为唯一配置来源)、多环境版本矩阵维护、多 GPU 型号异构管理。基础设施不需要漂亮话,但 GPU 集群的每次故障排查都需要 Terraform 状态文件作为配置基线来托底。