2.3 GPU/CPU 混合调度与异构资源管理 一个真实的 AI 集群里很少有「清一色同型号 GPU」,更常见的是「A100 训练区 + L20 推理区 + T4 推理区 + CPU 数据处理区」并存。如何让一张调度表管好这片异构丛林,是本节的核心议题。 2.3.1 异构是常态,同构是奢望 理论上,「一台节点池里所有节点配置完全相同」是最容易管理的——所有 Pod 可以随便调度。但现实中的 AI 集群几乎都是异构的,原因有几个: 采购批次不同:今年买的 H100、去年买的 A100、前年买的 V100,混在一个集群。 任务特性不同:训练用高端卡(A100/H100),推理可用中端卡(L20/T4),数据处理只用 CPU。 预算与供应:H100 长期缺货且昂贵,团队只能有什么买什么。
一个真实的 AI 集群里很少有「清一色同型号 GPU」,更常见的是「A100 训练区 + L20 推理区 + T4 推理区 + CPU 数据处理区」并存。如何让一张调度表管好这片异构丛林,是本节的核心议题。
理论上,「一台节点池里所有节点配置完全相同」是最容易管理的——所有 Pod 可以随便调度。但现实中的 AI 集群几乎都是异构的,原因有几个:
异构带来的调度挑战有两个:
解决这两个挑战的核心工具是节点池划分 + 标签 + 亲和/反亲和 + 污点容忍。
节点池(NodePool)是把一组「配置相似、用途相近」的节点组织起来的基本单元。划分节点池有两个层面:
一个典型生产集群的节点池布局:
| 节点池 | 硬件 | 用途 | 规模(示例) |
|---|---|---|---|
| train-a100 | 8×A100 + 1TB 内存 + IB | 大模型训练 | 20 节点 |
| train-h100 | 8×H100 + 1TB 内存 + IB | 前沿模型训练 | 5 节点 |
| infer-l20 | 2×L20 + 256GB 内存 | LLM 推理 | 50 节点 |
| infer-t4 | 4×T4 + 128GB 内存 | 传统模型推理 | 30 节点 |
| cpu-batch | 64 核 + 256GB | 数据处理、评估 | 40 节点 |
💡 判读:节点池划分的本质是「用硬件边界换调度简化」。一旦节点池划好,调度器只需要在「对的池子」里找节点,而不需要在所有节点上做复杂的硬件判断,调度复杂度大幅下降。
节点池划分后,要给每个节点打上标签(Label),让 Pod 能通过 nodeSelector 或节点亲和性(nodeAffinity)来选节点。一套合理的标签体系是异构资源管理的骨架。
典型标签设计:
# 节点标签示例(伪代码) metadata: labels: node-pool: train-a100 # 节点池名 accelerator: nvidia # 加速器厂商 gpu-model: A100 # GPU 型号 gpu-count: "8" # GPU 数量 gpu-memory: "80Gi" # 单卡显存 network: infiniband # 网络类型 workload-type: training # 用途
Pod 通过节点亲和性选择节点:
spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: gpu-model operator: In values: ["A100", "H100"] # 只调度到 A100/H100 节点
| 标签维度 | 典型取值 | 用途 |
|---|---|---|
node-pool |
train-a100, infer-l20 | 节点池识别 |
gpu-model |
A100, H100, L20, T4 | GPU 型号过滤 |
gpu-count |
1, 2, 4, 8 | 单节点 GPU 数 |
gpu-memory |
16Gi, 80Gi | 显存容量 |
network |
ethernet, infiniband, roce | 网络类型 |
workload-type |
training, inference, batch | 用途隔离 |
节点亲和(nodeAffinity)解决「Pod 想去哪种节点」,而 Pod 间亲和/反亲和(podAffinity / podAntiAffinity) 解决「Pod 想和谁做邻居、不想和谁做邻居」。这两者在 AI 调度里都极其常用。
Pod 反亲和的典型用法:把推理副本分散到不同节点,避免单节点故障打掉多个副本:
spec: affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchLabels: app: llm-inference topologyKey: kubernetes.io/hostname # 不同节点
Pod 亲和的典型用法:分布式训练的 worker 要和它的 rank-0 调度到同一可用区,降低跨区通信:
spec: affinity: podAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchLabels: training.kubeflow.org/role: master topologyKey: topology.kubernetes.io/zone
| 亲和类型 | 解决什么 | AI 典型用法 |
|---|---|---|
| nodeAffinity | Pod 想去哪种节点 | 选 GPU 型号、选节点池 |
| podAffinity | Pod 想和谁同区 | 训练 worker 与 master 同可用区 |
| podAntiAffinity | Pod 不想和谁同节点 | 推理副本跨节点分散 |
⚠️ 注意:Pod 间亲和/反亲和的计算成本随 Pod 数量增长,在大集群(>1000 节点)上可能成为调度器瓶颈。生产中常用
preferredDuringScheduling(软约束)而非requiredDuringScheduling(硬约束)来平衡精度与性能。
标签 + 亲和解决「Pod 主动选节点」,但还有反向问题:怎么防止不该来的 Pod 调度到 expensive 的节点上?比如防止一个普通的 CPU 任务占住 A100 节点(A100 节点 CPU 也强,可能被误调度)。
答案是污点与容忍(Taint / Toleration)。给节点打污点等于挂上「非会员勿入」的牌子,只有声明了对应容忍的 Pod 才能调度上来。
# 给训练节点打污点(命令示意) kubectl taint nodes train-a100-node workload=training:NoSchedule
# 训练 Pod 声明容忍才能调度上来 spec: tolerations: - key: "workload" operator: "Equal" value: "training" effect: "NoSchedule"
污点的三种 effect:
| Effect | 含义 | AI 场景用法 |
|---|---|---|
NoSchedule |
不容忍就不调度 | 专用节点池隔离(最常用) |
PreferNoSchedule |
尽量不调度 | 软隔离 |
NoExecute |
不容忍就驱逐已有 Pod | 紧急腾空节点 |
💡 判读:标签(吸引 Pod)与污点(排斥 Pod)是一对组合拳。标签让「对的 Pod」能主动找到节点,污点让「错的 Pod」无法误调度到节点。两者配合才能形成严密的隔离边界。
把上述工具组合起来,针对一个多型号 GPU 集群,一套典型的治理策略是:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 880 420" font-family="sans-serif" font-size="13"> <!-- 标题 --> <text x="440" y="24" text-anchor="middle" font-size="15" font-weight="bold">异构 GPU 集群的标签 + 污点治理</text> <!-- 节点池 A: H100 训练 --> <rect x="20" y="50" width="200" height="160" rx="8" fill="#fce7f3" stroke="#db2777"/> <text x="120" y="75" text-anchor="middle" font-weight="bold" fill="#9d174d">train-h100 池</text> <text x="120" y="98" text-anchor="middle" font-size="11">label: gpu-model=H100</text> <text x="120" y="118" text-anchor="middle" font-size="11">taint: workload=training</text> <text x="120" y="138" text-anchor="middle" font-size="11">network: IB</text> <text x="120" y="170" text-anchor="middle" font-size="11">用途:前沿大模型训练</text> <text x="120" y="190" text-anchor="middle" font-size="11">容忍者:训练 Operator Pod</text> <!-- 节点池 B: A100 训练 --> <rect x="240" y="50" width="200" height="160" rx="8" fill="#fce7f3" stroke="#db2777"/> <text x="340" y="75" text-anchor="middle" font-weight="bold" fill="#9d174d">train-a100 池</text> <text x="340" y="98" text-anchor="middle" font-size="11">label: gpu-model=A100</text> <text x="340" y="118" text-anchor="middle" font-size="11">taint: workload=training</text> <text x="340" y="138" text-anchor="middle" font-size="11">network: IB</text> <text x="340" y="170" text-anchor="middle" font-size="11">用途:大模型训练</text> <text x="340" y="190" text-anchor="middle" font-size="11">容忍者:训练 Operator Pod</text> <!-- 节点池 C: L20 推理 --> <rect x="460" y="50" width="200" height="160" rx="8" fill="#dbeafe" stroke="#2563eb"/> <text x="560" y="75" text-anchor="middle" font-weight="bold" fill="#1e40af">infer-l20 池</text> <text x="560" y="98" text-anchor="middle" font-size="11">label: gpu-model=L20</text> <text x="560" y="118" text-anchor="middle" font-size="11">taint: workload=inference</text> <text x="560" y="138" text-anchor="middle" font-size="11">network: 以太网</text> <text x="560" y="170" text-anchor="middle" font-size="11">用途:LLM 推理</text> <text x="560" y="190" text-anchor="middle" font-size="11">容忍者:KServe InferenceService</text> <!-- 节点池 D: CPU 批处理 --> <rect x="680" y="50" width="180" height="160" rx="8" fill="#dcfce7" stroke="#16a34a"/> <text x="770" y="75" text-anchor="middle" font-weight="bold" fill="#15803d">cpu-batch 池</text> <text x="770" y="98" text-anchor="middle" font-size="11">label: workload-type=batch</text> <text x="770" y="118" text-anchor="middle" font-size="11">无 taint(开放)</text> <text x="770" y="138" text-anchor="middle" font-size="11">CPU 节点</text> <text x="770" y="170" text-anchor="middle" font-size="11">用途:数据处理/评估</text> <text x="770" y="190" text-anchor="middle" font-size="11">容忍者:任意 CPU 任务</text> <!-- 调度策略说明 --> <rect x="20" y="240" width="840" height="160" rx="8" fill="#fef9c3" stroke="#ca8a04"/> <text x="40" y="265" font-weight="bold" fill="#854d0e">调度策略组合(Pod 声明):</text> <text x="40" y="290" font-size="12" fill="#713f12">· 训练任务:nodeAffinity 选 gpu-model=A100/H100 + toleration 容忍 workload=training</text> <text x="40" y="312" font-size="12" fill="#713f12">· LLM 推理:nodeAffinity 选 gpu-model=L20 + toleration 容忍 workload=inference + podAntiAffinity 跨节点</text> <text x="40" y="334" font-size="12" fill="#713f12">· 数据处理:nodeAffinity 选 workload-type=batch(无需容忍,因为 cpu 池无污点)</text> <text x="40" y="360" font-size="12" fill="#713f12">· 关键效果:训练卡不会被推理任务占住,推理任务不会跑到训练卡上浪费</text> <text x="40" y="382" font-size="12" fill="#713f12">· 进阶:夜间推理低谷时,训练任务可弹性借调推理池(见第 3、8 章)</text> </svg>
节点池划分是「静态隔离」,但 GPU 太贵,我们希望能「动态共享」——比如夜间推理流量低时,把推理池的 GPU 临时给训练用。这就需要更高级的机制:
这一层进阶能力会在第 3 章和第 8 章展开。本章只需建立「节点池 + 标签 + 污点」这个静态地基。
⚠️ 常见坑:异构 GPU 治理最常见的坑是「标签体系不统一」。如果不同团队给 GPU 型号打了不同的标签名(有人写
gpu-model、有人写hardware/gpu),调度规则就会失效。建议在集群建设之初就制定统一的标签规范,并强制通过准入控制(Admission Webhook)校验。
| 工具 | 作用 | 用法 |
|---|---|---|
| 节点池 | 物理隔离同类硬件 | 按型号/用途划分 |
| 标签 | 让 Pod 主动选节点 | nodeAffinity |
| 节点亲和 | Pod 选节点的硬/软约束 | required / preferred |
| Pod 反亲和 | 让副本跨节点分散 | 推理多副本必备 |
| 污点 | 排斥不该来的 Pod | 专用池隔离 |
| 容忍 | 允许 Pod 调度到带污点的节点 | 配合专用池 |
| 优先级 | 决定抢占顺序 | 训练 vs 推理让位 |
下一节《2.4 训练作业的 Operator 范式》将讲清为什么分布式训练不能用普通 Job,以及 Operator 如何成为承载训练作业的最佳抽象。