2.3 GPU/CPU 混合调度与异构资源管理


文档摘要

2.3 GPU/CPU 混合调度与异构资源管理 一个真实的 AI 集群里很少有「清一色同型号 GPU」,更常见的是「A100 训练区 + L20 推理区 + T4 推理区 + CPU 数据处理区」并存。如何让一张调度表管好这片异构丛林,是本节的核心议题。 2.3.1 异构是常态,同构是奢望 理论上,「一台节点池里所有节点配置完全相同」是最容易管理的——所有 Pod 可以随便调度。但现实中的 AI 集群几乎都是异构的,原因有几个: 采购批次不同:今年买的 H100、去年买的 A100、前年买的 V100,混在一个集群。 任务特性不同:训练用高端卡(A100/H100),推理可用中端卡(L20/T4),数据处理只用 CPU。 预算与供应:H100 长期缺货且昂贵,团队只能有什么买什么。

2.3 GPU/CPU 混合调度与异构资源管理

一个真实的 AI 集群里很少有「清一色同型号 GPU」,更常见的是「A100 训练区 + L20 推理区 + T4 推理区 + CPU 数据处理区」并存。如何让一张调度表管好这片异构丛林,是本节的核心议题。

2.3.1 异构是常态,同构是奢望

理论上,「一台节点池里所有节点配置完全相同」是最容易管理的——所有 Pod 可以随便调度。但现实中的 AI 集群几乎都是异构的,原因有几个:

  • 采购批次不同:今年买的 H100、去年买的 A100、前年买的 V100,混在一个集群。
  • 任务特性不同:训练用高端卡(A100/H100),推理可用中端卡(L20/T4),数据处理只用 CPU。
  • 预算与供应:H100 长期缺货且昂贵,团队只能有什么买什么。
  • 云上竞价:混用按需实例与 Spot 实例以降本。

异构带来的调度挑战有两个:

  1. 避免错配:把推理任务调度到 A100 上是浪费(推理用不满),把训练任务调度到 T4 上是失败(T4 显存不够)。
  2. 避免干扰:训练与推理混部在同一节点,可能因为网络/PCIe 带宽争抢而互相拖慢。

解决这两个挑战的核心工具是节点池划分 + 标签 + 亲和/反亲和 + 污点容忍

2.3.2 节点池划分:物理与逻辑的双重隔离

节点池(NodePool)是把一组「配置相似、用途相近」的节点组织起来的基本单元。划分节点池有两个层面:

  • 物理层面:每个节点池对应一类硬件配置(同型号 GPU、同 CPU/内存规格)。
  • 逻辑层面:每个节点池对应一类用途(训练、推理、数据处理)。

一个典型生产集群的节点池布局:

节点池 硬件 用途 规模(示例)
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 节点

💡 判读:节点池划分的本质是「用硬件边界换调度简化」。一旦节点池划好,调度器只需要在「对的池子」里找节点,而不需要在所有节点上做复杂的硬件判断,调度复杂度大幅下降。

2.3.3 标签:把硬件属性变成可调度的字段

节点池划分后,要给每个节点打上标签(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 用途隔离

2.3.4 亲和与反亲和:让 Pod 找到「对的邻居」

节点亲和(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(硬约束)来平衡精度与性能。

2.3.5 污点与容忍:把节点池变成「会员制」

标签 + 亲和解决「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」无法误调度到节点。两者配合才能形成严密的隔离边界。

2.3.6 异构 GPU 型号治理:A100 / H100 / L20 / T4 怎么管

把上述工具组合起来,针对一个多型号 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>

2.3.7 混合调度的进阶:弹性借调与混部

节点池划分是「静态隔离」,但 GPU 太贵,我们希望能「动态共享」——比如夜间推理流量低时,把推理池的 GPU 临时给训练用。这就需要更高级的机制:

  • 优先级与抢占:训练任务设低优先级,推理流量回升时训练被抢占让位。
  • 批处理调度器:用 Kueue/Volcano 做配额与借调(第 3 章)。
  • 弹性训练:训练框架支持动态加入/退出节点(第 3 章)。
  • Descheduler 重平衡:周期性重新调度,优化集群布局(第 3 章)。

这一层进阶能力会在第 3 章和第 8 章展开。本章只需建立「节点池 + 标签 + 污点」这个静态地基。

⚠️ 常见坑:异构 GPU 治理最常见的坑是「标签体系不统一」。如果不同团队给 GPU 型号打了不同的标签名(有人写 gpu-model、有人写 hardware/gpu),调度规则就会失效。建议在集群建设之初就制定统一的标签规范,并强制通过准入控制(Admission Webhook)校验。

2.3.8 一张表收束:异构资源管理工具箱

工具 作用 用法
节点池 物理隔离同类硬件 按型号/用途划分
标签 让 Pod 主动选节点 nodeAffinity
节点亲和 Pod 选节点的硬/软约束 required / preferred
Pod 反亲和 让副本跨节点分散 推理多副本必备
污点 排斥不该来的 Pod 专用池隔离
容忍 允许 Pod 调度到带污点的节点 配合专用池
优先级 决定抢占顺序 训练 vs 推理让位

本节小结

  • 真实 AI 集群几乎都是异构的(多型号 GPU、GPU/CPU 混部),异构治理的核心是节点池 + 标签 + 污点。
  • 节点池用硬件边界换调度简化,每个池对应一类硬件与一类用途。
  • 标签让 Pod 通过节点亲和性主动选节点,关键标签维度包括 gpu-model、gpu-count、network、workload-type。
  • Pod 反亲和让推理副本跨节点分散,是高可用推理的标配。
  • 污点与容忍构成反向隔离,防止不该来的 Pod 误调度到昂贵节点。
  • 静态隔离之上还有动态共享(弹性借调),那是第 3 章的进阶主题。

下一节《2.4 训练作业的 Operator 范式》将讲清为什么分布式训练不能用普通 Job,以及 Operator 如何成为承载训练作业的最佳抽象。


发布者: 作者: 灏天文库 转发
评论区 (0)
U