Kubernetes 上的生成式 AI——作业调度优化

0 阅读1小时+

虽然模型训练涵盖了 LLM 的整个生命周期——从预训练(pre-training) ,到对齐(alignment) ,再到定制化(customization) ——但上一章聚焦的是模型定制化,因为这是当前大多数使用 LLM 的组织最常见、也最务实的路径。上一章介绍了多种定制技术,以及像 Kubeflow Trainer 这样的框架,用于在 Kubernetes 上实现分布式定制任务。尤其是,对于平台管理员来说,他们必须应对一组新的运维挑战,而这些挑战已经超出了训练任务本身的基础配置范围。

虽然第 3 章主要讨论的是推理生产工作负载,但在 Kubernetes 中的 GPU 管理方面,两者有大量重叠。
更进一步说,即便只是从 Kubernetes 上长时间运行任务的管理来看,模型定制工作负载与传统 Kubernetes 应用相比,也在几个关键方面表现出明显不同:

  • 它们天然是资源密集型的,需要在多个节点上长时间占用专用硬件(GPU),有时会持续数天甚至数周。
  • 它们的各组件之间具有很强的相互依赖性,而这种依赖在普通 Kubernetes 工作负载中并不常见;例如,一个分布式训练任务中的所有 Pod 都必须被一起调度,这就需要 gang scheduling(组调度)
  • 它们会产生海量需要通过网络共享的数据,因此网络性能会成为关键瓶颈。
  • 它们无论在时间还是资源层面都意味着显著成本,因此可靠且高效的资源利用至关重要。
  • 在大多数集群中,GPU 都是稀缺且昂贵的资源,因此必须使用更精细的配额管理和调度逻辑,既要防止资源闲置,又要确保多个团队和项目之间的公平访问。

所有这些因素叠加在一起,就构成了每一位 Kubernetes 平台管理员都必须解决的一整套挑战。

本章将聚焦这些生产规模下的挑战,讨论在 Kubernetes 上运营一个健壮的模型定制平台所必需的核心优化和配置。
我们会先从 Kubernetes 调度器优化策略讲起,包括通过 bin packing(装箱式调度) 提高 GPU 利用率以降低成本,以及借助 descheduler 在集群状态变化后持续维持优化。
然后,我们将进一步讨论:

  • gang scheduling 方案:确保一个分布式训练任务的所有组件能够一起被调度
  • topology-aware scheduling(拓扑感知调度) :优化 GPU 互联拓扑上的放置
  • quota management(配额管理) :在团队之间公平分配资源
  • network optimizations(网络优化) :降低通信瓶颈
  • security:多用户环境下的安全考量
  • storage strategies(存储策略) :用于处理大规模数据集和模型工件
  • observability patterns(可观测性模式) :为长时间运行的训练工作负载提供可见性

本章的目标,是把我们在上一章中学到的原则,真正落地为一个生产就绪的平台:它既能够支撑企业规模的模型定制工作流,又能满足现代 Kubernetes 环境中对运维标准的要求。

注意(NOTE)

在本章中,我们统一使用 training job(训练任务) 这个术语,来泛指所有形式的 LLM 模型定制工作负载,包括第 6 章提到的 fine-tuning 以及其他技术。
原因在于:从平台需求来看,它们都共享同样的基础设施要求,例如:

  • 为分布式执行提供 gang scheduling
  • 为梯度同步提供高性能网络
  • GPU 资源管理
  • 完整的可观测性

尽管数据科学层面的技术手段各不相同,但对于 LLM 的所有模型定制方法而言,其基础设施挑战和运维模式是高度一致的。

本章之所以专门聚焦 LLM 定制化,是因为传统预测模型(如分类、回归和时间序列预测)通常规模要小得多,往往可以在单块 GPU 甚至 CPU 上高效训练,因此并不需要本章描述的这类专门基础设施。

Kubernetes 调度器优化

Kubernetes 调度器采用的是一种灵活、可插拔的架构,它支持高度可配置的调度逻辑,以便为不同类型工作负载优化 Pod 放置。
GPU 训练平台可以充分利用这种灵活性:例如,通过 bin packing 将工作负载集中起来,以降低成本;再结合动态重调度,在集群状态随时间变化后持续保持优化。
本节将介绍:

  • Kubernetes 调度器的核心机制
  • 面向成本效率的装箱策略
  • 借助 descheduler 持续进行调度优化

Kubernetes 核心调度器

Kubernetes 调度器是以 每个 Pod 独立决策 的方式运行的,它的决策过程分为两个阶段。

首先是 过滤阶段(filtering phase) ,也就是候选节点筛选阶段。
在这一阶段中,调度器会排除那些无法满足 Pod 需求的节点,例如:

  • CPU、内存或 GPU 资源不足
  • 不满足 taints / tolerations
  • 不满足 affinity 规则
    (详见 “Node affinity”)

接下来是 打分阶段(scoring phase) ,也就是节点排序阶段。
调度器会基于加权标准,对剩余候选节点进行评分,例如:

  • 资源平衡
  • Pod 分散程度
  • affinity 偏好

然后选出最优放置位置。
一旦节点被选定,调度器就会执行 binding 操作,把该 Pod 绑定到该节点上。
至此,调度阶段结束;接着运行在该节点上的 Kubelet 会接手,负责真正启动容器。

Kubelet 是运行在每个 Kubernetes 节点上的代理,它根据控制平面提供的规范,负责在该节点上执行容器。

更多细节见图 7-1。

image.png

图 7-1 Kubernetes 调度器

资源 Bin Packing 策略

默认情况下,Kubernetes 调度器会尽量把 Pod 分散到不同节点上,以提高可用性。
但对于 GPU 训练平台来说,很多时候恰恰相反的策略更有利:也就是把 Pod 尽可能紧密地装到更少的节点上,从而最大化资源利用率,并支持更具成本效益的集群自动伸缩。

这一点在 GPU 集群中特别有价值,因为 GPU 节点的成本可能达到 每小时 10–30 美元
如果能把工作负载集中到少数几个被充分利用的节点上,就能腾出“空节点”,使 autoscaler 可以安全地将这些空节点 drain 并移除。

资源 bin packing 策略,通常通过调度器中的 NodeResourcesFit scoring plug-in 来实现紧凑放置。
其中,MostAllocated 策略会对那些已经分配了更多资源的节点打更高分,因此它会偏向于把新工作负载继续放到这些节点上,而不是像默认的 LeastAllocated 那样倾向于扩散(见示例 7-1)。

示例 7-1 用于 bin packing 的调度器配置

apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: binpack-scheduler   
  pluginConfig:
  - name: NodeResourcesFit
    args:
      scoringStrategy:
        type: MostAllocated   
        resources:
        - name: nvidia.com/gpu
          weight: 5   
        - name: cpu
          weight: 1
        - name: memory
          weight: 1

这里:

  • 定义了一个自定义调度器名称,训练任务可以通过引用它来启用 bin packing 行为。
  • MostAllocated 策略会给那些已经有更高资源分配量的节点更高分,因此偏向于资源集中。
  • GPU 资源的权重设为 5,而 CPU 和内存只设为 1,这意味着调度器会优先按照 GPU 进行装箱,反映出 GPU 更昂贵且更稀缺。

不过,平台管理员在使用 bin packing 时,必须在成本效率可用性降低之间做平衡。
因为如果大量任务被集中在少数节点上,那么单个节点故障会影响更多训练任务;同时,即便 GPU 还有空闲,CPU、内存或网络争用也可能成为新的瓶颈。
因此,bin packing 特别适合那些对成本敏感的批处理训练任务,这类任务通常可以通过 checkpoint / resume 机制容忍中断。
而在生产训练平台中,更常见的做法是同时提供多个调度 profile:例如,实验性任务使用 bin packing,以节约成本;关键任务则采用 spreading,以获得更高韧性。

使用 Descheduler 做动态调度优化

前面提到的 bin packing 策略,主要解决的是初始放置优化
但随着时间推移,这种优化会逐渐失效——因为工作负载会结束,新的任务也会以不同速率落到不同节点上,集群状态不断变化。

Kubernetes 提供了一个可单独安装的组件,叫做 descheduler,就是为了解决这种动态优化问题。
它会持续评估 Pod 的当前放置情况,并把那些处于“次优位置”的 Pod 驱逐出去,从而让调度器基于当前集群状态和策略重新调度它们。

与调度器只在新 Pod 创建时响应不同,descheduler 会主动识别那些:

  • 违反放置策略的现有 Pod
  • 或者导致资源碎片化的 Pod

然后通过驱逐它们来触发重调度,从而提升整体集群效率。

descheduler 是作为一个独立组件运行的,通常会以:

  • CronJob 方式部署,用于周期性优化
  • Deployment 方式部署,用于持续监控

它通过一个名为 DeschedulerPolicy 的自定义资源,来定义可配置的策略 plug-in,从而识别哪些 Pod 应当被驱逐。
当 descheduler 决定驱逐一个 Pod 时,它做的事情其实很简单:直接删除该 Pod
随后,这个 Pod 的控制器(例如 ReplicaSet、StatefulSet,或者在训练任务场景中,Kubeflow Trainer)会立即重新创建该 Pod,而调度器则根据当前调度策略和集群状态,把它重新放到合适位置。

这个“驱逐—重调度”循环会遵守 PodDisruptionBudget(PDB) ,因此 descheduler 不会违反可用性约束,也不会让关键工作负载在超出设定容忍范围的情况下被打断。
对于使用 gang scheduling 的训练任务来说,PDB 也因此成为防止任务被过早驱逐的主要保护机制(见示例 7-2)。

示例 7-2 使用 PodDisruptionBudget 保护 gang-scheduled 训练任务

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: llm-training-pdb
spec:
  # Ensures all workers remain running simultaneously (gang scheduling requirement)
  minAvailable: 4
  selector:
    matchLabels:
      # All pods belonging to the same training job
      scheduling.x-k8s.io/pod-group: llm-training-group

descheduler 的不同策略适用于不同优化目标,尤其是在 GPU 训练平台中尤为重要。这些策略通常通过 DeschedulerPolicy 资源中的 profiles[].plugins 配置。

表 7-1 GPU 训练平台中的 descheduler 策略

HighNodeUtilization
行为:从低利用率节点上驱逐 Pod,把工作负载整合到更少节点上
与调度策略的关系:必须和 MostAllocated(bin packing)一起使用,否则会产生驱逐循环
适用场景:面向成本优化的 GPU 集群、启用 autoscaling 的环境、可容忍中断的批训练场景、追求最大化 GPU 利用率密度

LowNodeUtilization
行为:从高利用率节点(超过阈值)上驱逐 Pod,把它们重新整合到低利用率节点(低于阈值)上,从而触发节点缩容
与调度策略的关系:必须和 LeastAllocated(spreading)一起使用,否则会产生驱逐循环
适用场景:结合 autoscaling 做 scale-down;本质上是与 HighNodeUtilization 相反的策略,只是阈值语义不同

RemovePodsViolatingNodeAffinity
行为:驱逐那些当前所在节点已经不再满足其 node affinity 规则的 Pod
与调度策略的关系:可配合任意调度器;其作用是强制执行声明式放置约束
适用场景:GPU 基础设施动态变化的环境,例如 GPU 类型升级、拓扑重配置时,需要确保 GPU 型号需求持续满足

RemovePodsViolatingInterPodAntiAffinity
行为:驱逐那些违反 anti-affinity 规则的 Pod,以恢复预期中的分散放置
与调度策略的关系:可配合任意调度器;其作用是修复初始放置中的次优结果
适用场景:需要通过副本分散来实现容错的训练任务,避免相关 Pod 共置

descheduling 最大的风险,在于它会打断任务
对于运行时间很长的 LLM 训练任务来说,一旦驱逐 Pod,就会触发昂贵的 checkpoint-and-resume 循环,导致训练完成时间延后数小时甚至数天。
因此,平台管理员通常会通过以下方式来降低风险:

  • 控制部署频率:例如只在维护窗口内低频运行 descheduler,而不是持续运行
  • 使用 PDB:保护关键任务不被驱逐
  • 做 namespace 分层:例如只优化实验性 namespace,而不动生产训练 namespace

组织在启用这类优化时,必须验证:
优化带来的收益(例如更好的 bin packing 所节省的节点成本)是否大于任务中断带来的代价(例如 checkpoint / resume 所损失的训练时间)。

警告(WARNING)

descheduler 的策略必须与调度器的放置策略保持一致,否则系统会进入驱逐循环(eviction loop) :descheduler 驱逐了某个 Pod,而调度器又立刻把它放回同一个节点,然后它又被再次驱逐,如此反复。

具体来说:

  • 如果 descheduler 使用的是 HighNodeUtilization,那么调度器必须使用 MostAllocated(bin packing)。
  • 如果 descheduler 使用的是 LowNodeUtilization,那么调度器必须使用 LeastAllocated(spreading)。
  • 绝不能同时启用 HighNodeUtilization 和 LowNodeUtilization,因为它们目标相反,会彼此冲突。

要验证两者是否匹配,可以通过监控来观察:
如果驱逐次数持续增加,但节点整合并没有进展,那么很可能已经出现驱逐循环,此时必须修正策略配置。

Gang Scheduling

前面讲到的调度优化——无论是为了成本效率的 bin packing,还是为了持续再优化的 descheduler——本质上解决的都是:单个 Pod 如何被放到节点上,以及这种放置如何被持续维护
但分布式训练任务引入了一个完全不同层级的问题:如何确保一个多 Pod 任务的所有组件作为一个原子整体被一起调度

Kubernetes 默认的“按 Pod 单独调度”模型,对于容器化应用和微服务来说运行良好;但对于分布式训练任务来说,这种模型从根本上就会失效,因为调度器并不知道多个 Pod 其实属于同一个协同工作负载。
每个 Pod 都是独立调度的。于是就可能出现这样的问题:

例如,一个需要 8 个 worker 的训练任务,调度器成功放下了其中 7 个,但第 8 个因为资源耗尽无法调度。结果就是:

  • 前面 7 个 worker 已经占住了 GPU 资源
  • 整个任务却因为缺少最后一个 worker 而死锁等待

这种现象也被称为 resource fragmentation(资源碎片化)

大规模 LLM 训练任务之所以需要“要么全有、要么全无”的调度方式,是因为像 PyTorch 这类框架在启动时使用了一种 rendezvous(会合)机制:所有 worker 必须先互相发现,并在一个 barrier 上完成同步,训练才能正式开始。
同样地,DeepSpeed 等其他框架也会在每一轮训练迭代中设置通信屏障,用于协调梯度同步。
如果哪怕少了一个 worker,rendezvous barrier 就无法完成,整个任务就会死锁;而那些已经调度到位的 worker 仍会继续占用 GPU 资源。

而一个集群通常又是为多个并发用户共同设计的:大家都在同时提交训练任务,并希望系统能基于某种公平调度策略,保证任务能在一定时间 / SLO 内启动执行。

Gang scheduling,也叫 coscheduling,就是为了解决这个问题而设计的调度策略。
它会把同一个分布式任务中的所有 Pod 当作一个原子调度单元来看待:

  • 要么这些 Pod 全部同时被调度成功
  • 要么一个都不调度

这种调度方式通常会使用一个队列,让这些 Pod 保持在 pending 状态,不预先占用资源,直到调度器能够确认:整个集群中已经存在足够资源,可以一次性满足整个任务的完整需求。

gang scheduling 并不是 Kubernetes 新出现的问题,也不只针对分布式训练任务,但它在训练场景中尤为突出,因为 GPU 既稀缺又昂贵。
Kubernetes 本身是可插拔设计,因此社区里已经有多个项目用于解决这一问题。

PyTorch Rendezvous 与 Gang Scheduling

PyTorch 的分布式训练依赖于一种 rendezvous 机制,它把**节点发现(peer discovery)屏障同步(barrier synchronization)**结合在一起。
当一个分布式训练任务启动时,所有 worker 都会连接到一个 rendezvous backend(通常是一个基于 TCP 的 key-value store,或者 etcd),以便完成以下工作:

  • 发现训练任务中的其他所有 worker
  • 确定完整参与者集合,并为其分配 rank(从 0 到 world_size-1)
  • 在 barrier 上同步——没有任何一个 worker 会继续往下执行,直到所有 worker 都已到齐
  • 交换点对点通信所需的连接信息

这个 rendezvous barrier 是原子且阻塞的
如果调度器只成功放下了 8 个 worker 中的 7 个,而第 8 个由于资源碎片化无法调度,那么前面 7 个 worker 会永远卡在 barrier 上等待。
这 7 块 GPU 会一直被占用,却没有任何训练进展,持续产生资源成本。

Gang scheduling 正是通过保证“8 个 worker 要么一起调度成功,要么全都不调度”,来避免这种在 rendezvous 点死锁的部分部署问题。
当然,PyTorch 的 elastic training(torch.distributed.elastic)可以处理动态 worker 集合;但大多数 LLM 训练仍采用静态配置,即 worker 数量固定,所有 worker 都必须同时存在。

比较不同的 Gang Scheduling 方案

在 Kubernetes 中实现 gang scheduling 有多种方式。
它们工作在不同层级,适用于不同场景。表 7-2 描述了主要选项。理解这些方案之间的差异,有助于平台管理员为训练工作负载选择合适的技术方案。

表 7-2 Gang Scheduling 方案对比

Coscheduling plug-in(PodGroup CRD)
主要目标:在默认 Kubernetes 调度器中引入 gang scheduling 语义
架构层级:调度器扩展(通过 plug-in framework 扩展 kube-scheduler)
项目社区:Kubernetes SIGS
适用场景:需要 all-or-nothing 调度的通用批处理工作负载(训练任务、Spark 等)

Kueue
主要目标:在 job 层做资源管理和准入控制
架构层级:准入控制器 + 队列管理(位于调度层之上)
项目社区:Kubernetes SIGS
适用场景:多租户环境,需要配额管理、优先级队列、资源借用、公平共享调度

NVIDIA KAI Scheduler
主要目标:面向 AI/ML 工作负载的 GPU 优化调度器
架构层级:为 GPU 集群设计的替代调度器
项目生态:NVIDIA 生态(原 run:ai 核心引擎开源版)
适用场景:大规模 GPU 集群、数千节点、动态 GPU 分配、层级队列、面向 AI/ML 团队的公平性

Volcano
主要目标:具备高级作业管理能力的批处理调度系统
架构层级:替代调度器(完全替换或补充 kube-scheduler)
项目社区:CNCF sandbox
适用场景:原本就面向 HPC 和 AI/ML 批处理调度设计的复杂环境,支持 fair-share、binpack 等高级策略

Kubernetes 社区也正在推动原生 gang scheduling 支持,对应提案是 KEP-4671
该提案引入了一个新的核心 Workload 类型,使得 Kubernetes 调度器能够直接支持“要么全部调度、要么全部不调度”的语义,从而把一组 Pod 作为一个整体进行调度。
这个增强的目标,是为像分布式训练任务这样紧耦合的工作负载提供标准化的调度框架,因为这类任务要求所有 worker 同时启动,才能避免在框架同步点上发生死锁。
一旦该 KEP 获批并真正实现,那么像 Volcano 和 KAI Scheduler 这样的替代调度器,也需要更新实现,以支持这个标准化的 Workload API,才能与更广泛的 Kubernetes 生态保持兼容。
虽然目前它仍处于提案阶段,但如果未来原生支持落地,那么对于基础 gang scheduling 用例,就不再需要外部 plug-in 或自定义调度器了。当然,本文前面提到的这些方案,在当前生产环境中依然非常有价值,因为它们还提供了诸如高级队列管理和 GPU 专属优化等附加能力。

Coscheduling plug-in(PodGroup CRD)

Coscheduling plug-in 是在现有 Kubernetes 集群中引入 gang scheduling 的最直接路径,因为它扩展的是默认调度器,而不需要整体替换掉 kube-scheduler。

安装过程通常需要平台管理员完成以下几步:

  1. 安装 scheduler-plug-ins
  2. kube-scheduler 配置中启用 coscheduling plug-in
  3. 创建一个 PodGroup 对象,用于表示一个调度单元
  4. 给属于同一训练任务的所有 Pod 打上
    scheduling.x-k8s.io/pod-group: groupId
    这样的 annotation / label,使调度器把它们视作一个整体(见示例 7-3)

这种做法的好处在于:
对于不需要 gang scheduling 的普通工作负载,默认调度器的行为不会变化;而只有在你显式配置时,才会为某些任务启用 coscheduling,因此落地更容易,对生产集群的影响也更可控。

不过,PodGroup CRD 提供的抽象是相对底层的。
它可以把多个 deployment / pod 组合在一起,但无法支持更高层能力,例如:

  • job 级配额管理
  • 优先级管理
  • 更复杂的高级调度策略

示例 7-3 PodGroup 配置

apiVersion: scheduling.x-k8s.io/v1alpha1
kind: PodGroup
metadata:
  name: llm-training-group
spec:
  minMember: 4   
  scheduleTimeoutSeconds: 300   

apiVersion: v1
kind: Pod
metadata:
  name: llm-training-0   
  labels:
    scheduling.x-k8s.io/pod-group: llm-training-group   
    job-role: leader   
spec:
  ...

这里:

  • minMember 表示调度器必须至少把多少个 Pod 一起调度。分布式训练任务里通常包含 driver 和 workers,因此这个值要把它们都考虑进去。由于每个 Pod 本身仍是单独创建的,所以这个字段实际上是在告诉调度器“这个组的最小完整规模是多少”。
  • scheduleTimeoutSeconds 表示等待所有 Pod 都变得可调度的最大时长。
  • 这里展示的是第一个工作负载 Pod;其他 Pod 只要使用同样的 pod-group label 即可。
  • 所有带有同样 pod-group 值的 Pod,都会被视作一个单一的原子调度单元,但它们依然保留各自独立的 deployment spec。
  • job-role 并不是 PodGroup 设计的一部分,但它是一个很好的最佳实践,用于标明每个 Pod 在任务中的角色。

Kueue

Kueue 所处的抽象层级,要高于 scheduler plug-in。
它提供的是 job 级别的准入控制(admission control) :根据可用配额和队列优先级,决定一个工作负载是否应该被集群接纳执行。
一旦 Kueue 接纳了某个 job,它就会确保集群中确实存在足够资源;而真正保证“原子调度”的低层语义,则通常由底层的 gang scheduling 机制来补上。

Kueue 最大的价值,在于它能够处理多租户资源管理问题。它支持:

  • 层级化资源配额
  • 基于优先级的排队
  • 团队之间的资源借用
  • 防止某个租户独占资源的公平策略

当你要在多个团队之间管理共享训练集群时,平台管理员就应该认真考虑 Kueue,因为它提供的是一层策略层(policy layer) ——也就是“谁什么时候可以获得资源”。
这也正好支撑了“GPU as a Service”这类更通用的能力,后文“Quota Management and Multitenancy: GPU as a Service”会展开。

最后,Kueue 还可以和 Kubeflow TrainerRayJob 等 AI 项目无缝集成,因此它非常适合作为训练任务编排层的一部分。
在与 Kueue 配合的端到端流程中,不同角色的职责是清晰分离的:

  • 平台管理员负责在 ClusterQueue 中配置全局规则,并创建 LocalQueue,供数据科学家使用
  • 数据科学家则通过这些队列访问分配给自己的配额,并提交工作负载(见图 7-2)

当你提交一个集成了 Kueue 的 job(例如使用 kueue.x-k8s.io/queue-name label)时,Kueue 会自动为它创建一个 Workload 类型的自定义资源,用于管理准入控制。
这个 Workload 对象与实际 job(如 TrainJobJob 等)是分开的,它负责记录资源需求和准入状态。

提交之后,你通常需要检查两类对象的状态:

  • 检查 Workload 对象kubectl get workloads -n <namespace> 可以看到 job 是否已被接纳(admitted)还是仍在排队
  • 检查实际 jobkubectl get trainjob <name> -n <namespace> 可以查看任务状态,但在 Kueue 接纳之前,job 会一直保持 suspended
  • 理解整体流程
    job 提交 → Kueue 创建 Workload → Workload 进入队列 → 配额可用 → Workload 被接纳 → job 解除 suspended → 创建实际 Pod

image.png

图 7-2 Kueue 概览:核心概念与角色

Workload 对象的 status.conditions 会显示诸如:

  • QuotaReserved:表示已经成功预留配额
  • InsufficientQuota:表示因为配额不足而继续排队

这会帮助你理解:为什么 job 还没有开始运行(见示例 7-4)。

示例 7-4 Kueue Workload 对象状态示例

status:
  conditions:
  - type: Admitted
    status: "True"
    reason: "QuotaReserved"
    message: "The workload is admitted and quota is reserved"
  admission:
    clusterQueue: "ai-training-cluster-queue"
    podSetAssignments:
    - count: 1
      flavors:
        nvidia.com/gpu: gpu-training-flavor
      name: head
    - count: 1
      flavors:
        nvidia.com/gpu: gpu-training-flavor
      name: worker

NVIDIA KAI Scheduler

NVIDIA KAI Scheduler 是 run:ai 核心调度引擎的开源版本,目前归属于 NVIDIA。
它只支持 NVIDIA 硬件,而且它的设计思路与前面那些方案不同:它通过把管理能力集中到一个 GPU-aware 的调度器中,来提供:

  • GPU 感知优化
  • 分数 GPU 分配(fractional allocation)
  • 时间切片(time slicing)
  • 带公平策略的层级队列管理

这些能力再与 gang schedulingGPU 拓扑感知放置(例如基于 NVLink 连接关系)结合起来,以便为分布式训练任务提供优化过的共置调度。

KAI Scheduler 通常不会像 PodGroup 那样去“聚合独立 Pod”,而更倾向于通过和某些聚合式部署 API 的显式集成来完成 gang scheduling(见“Kubeflow Trainer”)。
它是一个替代型 Kubernetes 调度器,核心目标是借助对 Kubeflow 的内建支持,最小化 GPU 空闲带来的成本
示例 7-5 展示了使用 PyTorchJob 资源进行配置的方式;而如果你使用的是 TrainJob,则这些设置(annotation 和 schedulerName)通常会配置在 ClusterTrainingRuntime 模板中。

示例 7-5 使用 KAI Scheduler 进行 gang scheduling(PyTorchJob)

# project with GPU quota
apiVersion: kai.run.ai/v1
kind: Project
metadata:
  name: ml-team-a
spec:
  gpuQuota: 8   

---
# Distributed training job with gang scheduling
apiVersion: kubeflow.org/v1
kind: PyTorchJob
metadata:
  name: llm-training
  namespace: ml-team-a
  annotations:
    kai.run.ai/project: ml-team-a   
spec:
  pytorchReplicaSpecs:
    master:
      replicas: 1
      template:
        spec:
          schedulerName: kai-scheduler   
  ...

这里:

  • Project 定义了项目级 GPU 配额:总共 8 块 GPU,用于 fair-share 调度。
  • annotation 把当前 job 与前面定义的 project 绑定起来,从而纳入配额统计。
  • spec 中显式指定 schedulerName: kai-scheduler,这样才能启用 KAI Scheduler 提供的 GPU 感知放置与 gang scheduling。

注意(NOTE)

本章中的示例会使用来自 Kubeflow Training OperatorPyTorchJob 资源(kubeflow.org/v1 API),以便在 Kubernetes 资源层面演示调度与网络配置。

而上一章中介绍的是 Kubeflow Trainer 更高层的 TrainJob 抽象(trainer.kubeflow.org/v1alpha1 API)。
这里讲到的所有调度概念,对这两种方式其实都是一样适用的。
TrainJob 在底层会创建 JobSet 工作负载,而像调度器选择、Kueue label、网络 annotation、NCCL 环境变量等这些配置,通常是定义在 ClusterTrainingRuntimeTrainingRuntime 模板中的。

这种分层设计的好处是:平台管理员可以在 runtime 模板中预先配置好这些生产级优化,而数据科学家则只需通过 SDK 关注训练逻辑本身。

Volcano

Volcano 是一个 CNCF sandbox 项目,最初由华为发起。
它提供的是一个完整的批处理调度系统,适用于 HPC、AI/ML 和大数据工作负载(例如 Apache Spark),并且它会整体替换 kube-scheduler,形成一个一体化方案。
这个方案把:

  • 队列管理
  • gang scheduling
  • topology-aware placement

全部集成在一个统一调度系统中。

Volcano 引入了 QueuePodGroupJob 等 CRD,用于描述并处理高级调度算法,以及用于抢占低优先级任务的 reclaim policy,因此它适合用于运行复杂批处理工作流的生产环境,并支持 TensorFlow、PyTorch、Apache Spark 等主流框架。
不过,采用 Volcano 的代价也很明显:你必须完全替换默认 Kubernetes 调度器
因此,相比“在 kube-scheduler 之上叠加 Kueue + coscheduling plug-in”这种方式,它的侵入性更强,也就未必适用于已经在运行传统工作负载的现有生产集群。

如何做出正确选择

本节中,我们介绍了在 Kubernetes 上为分布式训练任务实现 gang scheduling 的多种技术路径,以及它们各自的优缺点。
不过,在真实生产集群中,通常并不是“只选一个”,而是多种方案组合使用

一种很常见、也很推荐的架构是:

  • Kueue 负责准入控制和配额管理
  • 再结合面向 AI 的专用部署 API,例如 PyTorchJob(见“Kubeflow Trainer”)或 LeaderWorkerSet

例如,NVIDIA KAI Scheduler 可以替换默认调度器,以提供 GPU 感知优化;同时,仍然继续利用 Kueue 提供的 admission 管理层。

注意(NOTE)

LeaderWorkerSet(LWS) 也可以和专用调度器一起使用,因为它解决的是另一个不同层次的问题:
它关注的是如何管理那些天然具有 leader-worker 拓扑结构的工作负载,而不仅仅是确保原子调度。

虽然 LWS 本身也假设存在 gang scheduling 语义(也就是一组 Pod 要么全部一起调度,要么一个都不调度),但它真正的价值在于:
它提供了一个理解 leader-worker 模式的工作负载 API,而这种模式在 AI/ML 推理场景中非常常见——leader Pod 负责协调,把工作分配给 worker Pod,而两者通常必须共置,或者至少拥有高效网络连接。
这使得 LWS 相比更通用的 PodGroup API,更加专门化于 AI/ML 场景。

如果说 PyTorchJob 更偏向分布式训练,那么 LWS 的主要目标则是分布式推理,尤其是多主机推理场景:此时 LLM 会被切分后运行在多个节点、多个设备上。
而这种场景,在调度层面和分布式训练任务一样,也面临同样的 gang scheduling 挑战。

前面讨论的这些 gang scheduling 方案,主要解决的是如何让分布式训练任务获得完整的资源分配;但它们并不关心这些资源在集群物理基础设施中究竟位于哪里
而不同节点之间 GPU 互联拓扑的差异,会极大影响训练性能,因此接下来我们要讨论的,就是另一个关键问题:拓扑感知调度

拓扑感知调度(Topology-Aware Scheduling)

Gang scheduling 解决的是:一个分布式训练任务的所有 Pod 能不能一起被调度下来。
但它并不保证:这些 Pod 最终会落在拥有最优硬件拓扑的节点上,以支持高效的 GPU 间通信。

如果缺少 gang scheduling,任务可能只被部分部署,从而浪费昂贵的 GPU 资源;
而如果缺少 topology awareness,那么另一个问题就会出现:任务的 worker 可能被分散到多个节点上,被迫走低效的节点间连接来进行 GPU 通信,结果就是通信速度会比最优路径慢一个数量级,任务执行时间也会急剧增加。

这里的 拓扑感知(topology-aware) ,主要指的是 GPU 之间的物理互联架构
它包括:

  • 同一节点内,GPU 是否通过 NVLinkPCIe 互连
  • 跨节点之间,是否通过 InfiniBandRoCE(RDMA over Converged Ethernet) 等高速网络互连
  • 还是只能通过普通 Ethernet

这些互联方式在带宽延迟上的差异,会直接影响分布式训练性能:

  • NVLink:在同节点 GPU 之间提供最高 900 GBps 的双向带宽(H100 上的 NVLink 4.0)
  • InfiniBand:跨节点可达到 200–400 GBps,且延迟小于微秒
  • 标准 Ethernet:通常只有 10–100 GBps,且延迟更高

关于不同 GPU 互联带宽和相关技术,前文在推理场景的 “Single-Node Versus Multinode Inference” 中也提到过类似问题。

提示(TIP)

在网络术语中,fabric(网络织构 / fabric) 指的是底层网络基础设施,它为多个节点或设备之间提供互联通信路径。
与传统分层网络架构不同,fabric 更接近一种 mesh 式拓扑:端点之间存在多条可用路径,从而支持高带宽和低延迟通信。

在 GPU 计算场景中,像 InfiniBandNVSwitch 这样的 fabric,就是为高性能互联而设计的底层基础设施。
它可以用于:

  • 单机内:如 NVSwitch 把 8–16 块 GPU 组成全互联
  • 多机之间:如 InfiniBand 把数百个节点连成高性能集群

fabric 把底层复杂的交换结构隐藏起来,对分布式训练工作负载呈现出一个统一的高性能通信层。

Topology-aware scheduling 可以看作是 gang scheduling 的进一步扩展:
它在做放置决策时,还会考虑硬件拓扑约束,从而确保分布式训练任务被调度到那些拥有最优互联结构的节点上,以匹配其通信模式。

举个例子:一个需要 8 块 GPU 的训练任务,如果这 8 块 GPU 都位于一台单节点 8-GPU 机器上,且彼此通过 NVLink 相连,那么它的性能通常会远好于把 8 块 GPU 分散在 8 台单 GPU 节点上,再通过 Ethernet 通信。

虽然所有分布式训练策略都能从高质量互联中受益,但它们对互联的敏感程度差异很大:

  • Data Parallel 主要依赖高效梯度同步

  • Tensor ParallelismFSDP(Fully Sharded Data Parallelism) 对低延迟、高带宽互联的要求则要严苛得多,因为它们在每一次 forward / backward pass 中都要执行更细粒度的通信

    • Tensor parallelism 需要在 GPU 间频繁传递 activations
    • FSDP 则会持续执行 parameter gathering 和 gradient reduction

这些操作的开销都随着通信延迟增加而上升,并随着带宽提升而下降,因此 GPU 拓扑与互联质量 对训练性能至关重要,尤其是在超越简单 data parallel 的高级并行策略中更是如此。

分布式训练中的通信模式

不同的并行策略,会生成不同的通信模式,因此对网络的要求也不同。

Data Parallelism
会在每个 GPU 上复制完整模型,并在每个训练 step 结束后同步梯度。
梯度表示模型为了改进预测而应如何调整参数:在每轮训练中,模型先做预测,再和正确答案比较,进而计算每个参数(权重)应该如何调整,以减少误差。
每个 worker 会在自己的数据分片上计算梯度,然后所有 worker 必须同步这些梯度(通常是求平均),以保证模型更新一致。
对于一个拥有数十亿参数的 LLM,每一次迭代的梯度同步都可能涉及数十 GB 的数据传输

Tensor Parallelism
会把单层模型切分到多个 GPU 上,因此在 forward 与 backward 过程中都需要传递 activation。
例如,一个矩阵乘法可能被拆分到 4 块 GPU 上执行,而 activation tensor 会在这些 GPU 之间来回传递。
因此,Tensor Parallelism 对网络延迟尤其敏感。

FSDP
是比 Data Parallel 更节省显存的一种方式。
它不会在每个 GPU 上复制完整模型,而是把参数、梯度和 optimizer state 都分片到所有 GPU 上。
每块 GPU 只会在计算前“按需”从其他 GPU 收集自己所需的参数分片,计算后再释放。
这种频繁的 all-gather 与 reduce-scatter,使得 FSDP 对带宽延迟都非常敏感。

关于这些并行策略的详细解释,可参考 “Model Parallelism”。

正因为这些性能差异非常明显,平台管理员在设计拓扑感知调度时,必须在“尽可能利用所有资源”和“避免严重性能瓶颈”之间做平衡。
有时候,尽管集群中总 GPU 数量是够的,但如果这些 GPU 没有以理想拓扑分布,那么任务仍可能继续排队不启动。
这种“宁可排队,也不接受一个存在巨大性能瓶颈的放置结果”的策略,在很多场景中是更合理的。

比较不同的拓扑感知调度方案

前面介绍的某些支持 gang scheduling 的调度方案,同时也具备不同程度的 topology awareness,用于优化 GPU 放置和互联利用。
理解它们各自如何实现 topology-aware scheduling,有助于平台管理员根据自己集群的拓扑复杂度和性能需求做技术选型。

表 7-3 拓扑感知调度方案对比

Coscheduling plug-in(PodGroup CRD)
拓扑感知能力:无原生支持
实现方式:依赖默认 Kubernetes node label 和 pod affinity / anti-affinity,提供基础放置提示
适用场景:拓扑简单、人工打 label 与 affinity 规则就足够的环境;复杂拓扑下需要搭配其他方案

Kueue
拓扑感知能力:基于 ResourceFlavor 的拓扑感知
实现方式:用 node label 和 toleration 定义不同“资源风味”,区分 NVLink、InfiniBand、机架位置等拓扑属性
适用场景:多层级 GPU 拓扑环境,需要把工作负载路由到特定互联类型或故障域

NVIDIA KAI Scheduler
拓扑感知能力:内建 GPU 拓扑感知,与 gang scheduling 深度集成
实现方式:原生理解 NVLink、NVSwitch、InfiniBand 等拓扑关系,并做最优放置
适用场景:GPU 密集型集群,GPU 互联拓扑会直接决定训练性能

Volcano
拓扑感知能力:内置拓扑 plug-in
实现方式:拓扑感知调度插件理解 GPU 和网络拓扑,并根据任务需求自动优化放置
适用场景:复杂 HPC 风格拓扑,例如 NVLink + InfiniBand + Ethernet 混合环境

Coscheduling plug-in(PodGroup CRD)

Coscheduling plug-in 本身除了 Kubernetes 原生调度语义之外,并没有额外提供拓扑感知能力。
平台管理员如果想弥补这一不足,通常只能通过手工方式给节点打上拓扑信息标签,例如:

  • rack-id
  • nvlink-enabled
  • infiniband-connected

然后再在工作负载 spec 中配置 pod affinity / anti-affinity 规则,以影响放置结果。

需要注意的是,这种 affinity / anti-affinity 的使用,与 Coscheduling plug-in 本身无关,它完全是一种人工配置过程
这种方法只适合拓扑较简单、GPU 互联类型不多的环境;一旦拓扑复杂度上来,每个训练任务都需要写大量手工配置,管理成本就会迅速失控。

Kueue

Kueue 通过 ResourceFlavor 机制来实现 topology-aware scheduling。
它允许平台管理员把 GPU 资源划分成多个“风味(flavor)”,每一种 flavor 对应一种拓扑属性组合。

这些 flavor 的定义依赖于 node label,既可以由平台管理员手工打标,也可以借助像 NVIDIA GPU Feature Discovery 这样的工具自动生成(见 “GPU Feature Discovery”)。
一个 ResourceFlavor 会通过 nodeLabels(例如 gpu-interconnect: nvlinknetwork-fabric: infinibandrack: rack-1)和 toleration 选择节点,从而形成逻辑上的资源池。
然后,工作负载就可以通过 queue 配置,去请求这些特定 flavor 的资源。
当某个 ClusterQueue 引用了多个具有不同拓扑属性的 ResourceFlavor 时,Kueue 的 admission controller 就能基于 flavor 约束,在接纳 workload 时确保只有满足相应拓扑的资源可用,任务才会被放行。

这种方式的优点是:它能够实现较复杂的 topology awareness,而无需修改应用本身
工作负载只需要指定 queue 名称,Kueue 就会通过 flavor 机制和底层调度器协同完成拓扑感知放置。

第一步,是先定义好 ResourceFlavor,例如示例 7-6。

示例 7-6 Kueue ResourceFlavor 定义

# ResourceFlavor for premium GPU nodes with NVLink and InfiniBand
apiVersion: kueue.x-k8s.io/v1beta1
kind: ResourceFlavor
metadata:
  name: gpu-nvlink-infiniband
spec:
  nodeLabels:
    # Select nodes where GPUs are connected via NVLink
    gpu-interconnect: nvlink
    # Select nodes where inter-node communication happens via Infiniband
    network-fabric: infiniband
    gpu-type: nvidia-h100
  ...

# ResourceFlavor for standard GPU nodes with Ethernet
apiVersion: kueue.x-k8s.io/v1beta1
kind: ResourceFlavor
metadata:
  name: gpu-standard-ethernet
spec:
  nodeLabels:
    # Nodes that use PCIe for GPU interconnect and Ethernet for networking
    gpu-interconnect: pcie
    network-fabric: ethernet
    gpu-type: nvidia-h100
  ...

定义好 ResourceFlavor 之后,就可以把它们用到工作队列的资源需求中,如示例 7-7 所示。

示例 7-7 创建 Kueue ClusterQueue 与 LocalQueue

# ClusterQueue with topology-aware resource flavors
apiVersion: kueue.x-k8s.io/v1beta1
kind: ClusterQueue
metadata:
  name: topology-aware-cluster-queue
spec:
  namespaceSelector: {}
  resourceGroups:
  - coveredResources: ["cpu", "memory", "nvidia.com/gpu"]
    flavors:
    # The premium flavor listed first so Kueue tries this flavor first
    - name: gpu-nvlink-infiniband
      resources:
      - name: nvidia.com/gpu
        nominalQuota: 16
      ...
    # When premium quota is exhausted, this second flavor is used
    - name: gpu-standard-ethernet
      resources:
      - name: nvidia.com/gpu
        nominalQuota: 32
      ...
  flavorFungibility:
    # This option allows fallback to next flavor when the first is exhausted
    whenCanBorrow: TryNextFlavor
    whenCanPreempt: Preempt


# LocalQueue for team access
apiVersion: kueue.x-k8s.io/v1beta1
kind: LocalQueue
metadata:
  name: team-queue
spec:
  clusterQueue: topology-aware-cluster-queue

最后,就可以让 job 通过 LocalQueue 运行,如示例 7-8。

示例 7-8 集成 Kueue 的 PyTorchJob

# PyTorchJob that will use topology-aware flavor selection
# (for TrainJob, apply this label to the TrainJob metadata)
apiVersion: kubeflow.org/v1
kind: PyTorchJob
metadata:
  name: llm-training
  labels:
    # This label triggers the Kueue admission controller and
    # binds the job to a specific queue
    kueue.x-k8s.io/queue-name: team-queue
spec:
  ...

NVIDIA KAI Scheduler

KAI Scheduler 提供的是最强的 GPU 拓扑感知能力。
它原生理解 NVIDIA 的 GPU 互联技术,包括:

  • NVLink
  • NVSwitch
  • NVLink bridge
  • 以及它们与 InfiniBand 等网络织构之间的关系

它会与 NVIDIA GPU Operator(见 “NVIDIA GPU Operator”)深度集成,以获取详细 GPU 信息,并据此做出 topology-aware 的调度决策,因此平台管理员对节点打手工标签的需求会明显降低。

在为分布式训练任务做放置时,KAI Scheduler 会主动分析 GPU 拓扑,并自动把 worker 共置到网络配置最优的节点上。
例如,一个需要 8 块 GPU 的训练任务,如果存在一台 8-GPU 机器,且这 8 块 GPU 之间有 NVSwitch 全连接,那么它显然优于把这些 GPU 分散在多台服务器上,再通过 InfiniBand 通信。

Kueue 的 topology awareness 主要发生在准入层,通过 ResourceFlavor 去选择某种“硬件层级”;
而 KAI Scheduler 则把 topology optimization 直接集成在调度决策里,并同时结合:

  • gang scheduling
  • fair-share 调度
  • GPU 感知能力

从而保证:拓扑最优的放置结果不会破坏配额策略,也不会制造额外资源碎片。

也正因为它和 NVIDIA 硬件拓扑集成极深,所以对于大规模 NVIDIA GPU 集群来说,KAI Scheduler 往往是很自然的选择,尤其是在还需要结合:

  • MIG(Multi-Instance GPU) 切分
  • fractional GPU allocation

这些 NVIDIA 专属能力时(见 “Sub-GPU Allocation”)。

Volcano

Volcano 调度器内置了 topology-aware scheduling plug-in,它们会基于节点标签中提供的硬件拓扑信息,优化 Pod 放置。
和 Kueue 的 ResourceFlavor 类似,平台管理员仍然需要先给节点打上拓扑标签,例如:

  • gpu-interconnect: nvlink
  • network-fabric: infiniband
  • rack-id: rack-1

然后 Volcano 的 topology plug-in 会基于这些标签来对节点打分:如果某节点能够为任务的 GPU 通信需求提供更高带宽,就会获得更高分数。

但 Kueue 和 Volcano 的区别在于:

  • Kueue:通过 ResourceFlavor准入阶段做抽象,真正的放置仍然交给底层调度器
  • Volcano:把拓扑评分直接集成进调度决策本身

因此,Volcano 可以在不需要显式 affinity 规则的情况下,自动把分布式训练 worker 放到同机架节点、或具备直接 NVLink 连接的节点上。

如何做出正确选择

对于需要高级拓扑感知能力的组织来说,通常会把多种方案叠加使用,形成一个完整的“拓扑感知 + gang scheduling”体系。
一种常见架构是:

  • Kueue 管理准入控制
  • 再搭配像 VolcanoKAI Scheduler 这样的 topology-aware 调度器

这样一来,Kueue 负责在 job 层通过 ResourceFlavor 做拓扑感知选择;而底层调度器则负责在节点层做更细粒度的 topology-aware Pod placement。
这种分层方式实现了很清晰的职责分离:

  • 在 admission 层执行拓扑策略:例如,请求 premium interconnect 的任务必须有相应配额
  • 在调度层执行放置优化:由具备深度拓扑理解的专用调度器决定具体节点

为了让这一切工作起来,平台管理员需要先准备好拓扑信息:给节点打上诸如机架 ID、网络 fabric 类型、GPU 互联能力等标签,并在调度器中配置相应的 topology-aware 策略。
不同 GPU 厂商都提供了一些工具来简化这一打标工作;尤其是 NVIDIA 的工具链在这方面已经相当成熟和完整。

前面讲的调度器,关注的是跨节点的 GPU 互联拓扑;
但拓扑问题其实也存在于单个节点内部:例如 CPU 与设备本地性(device locality)同样会影响性能。
Kubernetes 为此引入了 Topology Manager 组件,下面这个侧栏会进一步解释。

Kubernetes Topology Manager

Kubernetes Topology Manager 是运行在每个节点上的一个 Kubelet 组件。
它与调度器层面的 topology policy 互补:调度器负责跨节点的放置,而 Topology Manager 则在单个节点内部协调资源分配,以保证单个 Pod 的硬件本地性尽可能最优。

在现代多路服务器中,一个最关键的硬件本地性问题就是 NUMA(Non-Uniform Memory Access) 架构:每个 CPU socket 都有自己的本地内存,而访问另一个 socket 上的内存会带来更高延迟。
这对 GPU 密集型工作负载尤为关键,因为跨 NUMA 内存访问会显著拖慢性能。

在 Topology Manager 出现之前,Kubernetes 中的 CPU ManagerDevice Manager 是各自独立做决策的。
这就可能出现:某个 Pod 分配到的 CPU 来自一个 NUMA node,而 GPU 却来自另一个 NUMA node,导致大量跨 NUMA 内存流量(见图 7-3)。

Topology Manager 解决这个问题的方式,是让 CPU 和设备(包括 GPU)的分配都具备 topology awareness,从而保证分配给某个 Pod 的 CPU 与 GPU 尽可能处于同一 NUMA 本地范围内,以最小化内存访问延迟。

平台管理员可以为 Topology Manager 配置不同策略:

  • best-effort:尽量实现 NUMA 对齐,但不强制
  • restricted:只有当资源能被正确对齐时才接纳 Pod
  • single-numa-node:要求该 Pod 的所有资源都必须来自同一个 NUMA node

其中,single-numa-node 是最严格的策略,它能保证最佳本地性,但在资源紧张的集群中也会牺牲一定调度灵活性。

image.png

图 7-3 单机多路服务器中的 NUMA 架构:本地访问与跨 NUMA 访问延迟对比

虽然 topology-aware scheduling 能为单个 job 提供最优性能放置,但它仍然解决不了另一个根本问题:
在多个团队竞争稀缺 GPU 资源的共享集群里,怎样公平地分配这些 GPU

配额管理与多租户:GPU as a Service

Gang scheduling 解决的是:分布式任务中的所有 Pod 能否一起调度。
Topology-aware scheduling 解决的是:这些 Pod 是否能被放到拥有最优 GPU 互联的硬件上。
但这两者都只是在解决“单个 job 如何执行”这个问题。
它们并没有回答:当多个团队同时竞争集群容量时,谁能获得稀缺的 GPU 资源?

如果要把一个共享 GPU 集群运营成一个真正的多租户平台,就必须有一套精细的配额管理机制,既能确保团队之间的资源分配公平,又能最大化整体 GPU 利用率。
而这恰恰是标准 Kubernetes ResourceQuota 无法很好解决的问题,尤其是对 AI/ML 工作负载而言更是如此。

传统 Kubernetes 配额是按 namespace 设硬性上限的:一旦额度耗尽,新的工作负载就直接启动不了。
这种方式对于固定且稀缺的 GPU 资源来说,在实践中往往会导致利用率很差。
例如:

  • A 团队虽然有预留配额,但当前没有数据科学家在跑实验,于是 GPU 闲着
  • B 团队的任务却一直在排队等待,即使它们有紧急截止时间
  • 最终结果是:昂贵 GPU 资源空着,但由于硬配额边界存在,任务又无法动态共享这些资源

GPU as a Service(GPUaaS) 架构,就是为了解决这一问题。
它通过引入:

  • 层级化配额管理
  • 资源借用(borrowing)
  • 抢占(preemption)
  • 公平策略(fairness policies)

来让团队在集群空闲时可以超出自身保证额度“借用”资源;而当集群资源紧张时,又能确保没有任何一个团队能够长期垄断资源。
其核心目标是:

  • 以机会主义方式尽可能提高 GPU 利用率
  • 同时又在团队真正需要时,保证原本设计好的配额能够被收回和兑现

比较配额管理与多租户方案

在 GPU 资源配额管理方面,不同调度方案提供的能力和思路并不相同。
它们在:

  • 资源分配
  • 公平策略
  • 多租户支持方式

上都有差异。表 7-4 对主要方案做了比较。理解这些方案是如何执行配额约束和资源借用的,有助于平台管理员选择最符合本组织需求的技术栈。

表 7-4 配额管理与多租户方案对比

Kueue
配额能力:层级化配额,支持 borrowing 与 preemption
多租户方式:namespace 级 LocalQueue 映射到集群级 ClusterQueue,支持 cohort 级共享与 priority class
实现方式:admission controller + ClusterQueue / LocalQueue 模型 + cohort 资源借用 + 基于优先级的抢占
适用场景:需要灵活配额共享、团队间资源借用、且不希望替换调度器的多租户环境

NVIDIA KAI Scheduler
配额能力:项目级 GPU 配额与公平调度算法
多租户方式:通过 Project CRD 把团队隔离成独立项目,并通过层级队列组织部门与团队,使用 fair-share 避免资源被垄断
实现方式:把 quota 分配、fair-share 调度和 GPU 专属优化集成进单一调度器
适用场景:GPU 密集型集群,需要把配额策略与 GPU-aware 调度(例如 fractional GPU、MIG)紧密结合

Volcano
配额能力:基于 Queue 的资源限额与比例分配
多租户方式:多个 queue 各自拥有独立资源上限,通过 queue 级 priority 和 namespace 到 queue 的映射实现团队隔离
实现方式:Queue CRD + 资源限额 + 按权重比例分配 + reclaim / preemption 策略
适用场景:需要单一组件中同时集成队列管理与调度,并按比例切分资源的批处理环境

Kueue

Kueue 提供了一套面向 batch workload 的完整配额管理体系,非常适合训练任务。
它采用的是一种两层架构

  • 在集群层面,由 ClusterQueue 负责全局资源治理
  • 在团队层面,由 LocalQueue 提供面向用户的队列入口

在集群层,ClusterQueue 会定义资源池,并为 CPU、内存和 GPU 设置配额上限,以及当多个团队争抢有限资源时采用何种分配策略。
平台管理员通常会创建多个不同资源层级的 ClusterQueue,例如:

  • 一个 gpu-training ClusterQueue,包含 32 张 NVIDIA H100,用于生产训练任务
  • 一个 gpu-development ClusterQueue,包含 8 张 GPU,用于实验
  • 一个 gpu-spot ClusterQueue,基于云上的 spot 实例,用于成本敏感型工作负载

每个 ClusterQueue 都会定义:

  • nominal quota:团队有权保证获得的资源额度
  • borrowing limit:当其他队列空闲时,该队列最多可以临时借用多少资源

这种设计使容量共享变得更灵活:既提升利用率,又不牺牲公平性。
LocalQueue 则是团队真正用来提交工作负载的入口,它会映射到某个特定 ClusterQueue,并自动继承配额约束。

Kueue 中的 cohort 机制,则进一步实现了资源借用。
它会把多个 ClusterQueue 组织到一个共享组中,从而启用更复杂的多租户共享策略。
每个 ClusterQueue 把自己的 reserved quota 贡献给 cohort,总容量就是这些 reserved quota 的总和。
于是就可能出现这样的场景:

  • Team A 原本只有 10 张 GPU 配额,但当前已经用满
  • Team B 和 Team C 当前有空闲 quota
  • 如果 Team A 还有排队任务,Kueue 可以允许它借用 Team B / Team C 的空闲 GPU,直到达到 cohort 允许的总上限

Kueue 会自动提高 Team A 的“有效可用额度”,从而利用这些闲置 GPU;
但一旦 Team B 或 Team C 提交了新任务、需要拿回自己的保证配额,Kueue 又会立即通过 preemption 把借出的资源收回。

相比传统的硬配额边界,这种动态共享极大提高了集群利用率:

  • 只要还有任务在排队,GPU 就不会闲着
  • 同时又能保证:每个团队始终有权拿回自己的 nominal quota

此外,Kueue 还支持层级队列,可以映射组织内部真实的部门结构、项目团队和资源池边界。

示例 7-9 展示了 cohort、nominal quota、borrowing limit 和 priority class 这些能力是如何组合使用的。

示例 7-9 Kueue 中的 cohort 与 priority class 配额管理

apiVersion: kueue.x-k8s.io/v1beta1
kind: ClusterQueue
metadata:
  name: team-a-queue
spec:
  cohort: shared-gpu-cohort   
  resourceGroups:
  - coveredResources: ["cpu", "memory", "nvidia.com/gpu"]
    flavors:
    # Using a built-in ResourceFlavor that matches all nodes.
    - name: default-flavor
      resources:
      - name: nvidia.com/gpu
        nominalQuota: 10
        borrowingLimit: 22
  queueingStrategy: BestEffortFIFO   
  preemption:
    reclaimWithinCohort: Any   
    withinClusterQueue: LowerPriority   
---
# High-priority class for production workloads
apiVersion: kueue.x-k8s.io/v1beta1
kind: WorkloadPriorityClass
metadata:
  name: production-priority
spec:
  # Specifying a high value makes this class higher priority
  value: 10000
  description: "High priority for production training jobs"
---
# Production PyTorchJob from Team A (high priority)
apiVersion: kubeflow.org/v1
kind: PyTorchJob
metadata:
  name: production-llm-training
  namespace: team-a
  labels:
    # The definition of the LocalQueue is skipped here for brevity
    kueue.x-k8s.io/queue-name: team-a-training-queue
    kueue.x-k8s.io/priority-class: production-priority   
  ...

其中:

  • 多个 ClusterQueue 属于同一个 shared-gpu-cohort,从而允许团队之间资源借用。cohort 的总容量就是这些 ClusterQueue 的名义配额总和。
  • BestEffortFIFO 队列策略表示:在配额可用时,按照先来先服务方式处理 workload,同时尽量高效打包资源。
  • reclaimWithinCohort: Any 表示:同一 cohort 中的任意团队,都可以抢回自己借出去的资源,以恢复 nominal quota。
  • withinClusterQueue: LowerPriority 表示:在同一个 ClusterQueue 内,高优先级 workload 可以在 quota 已满时抢占低优先级 workload。
  • 这个生产任务使用了 production-priority,意味着 Kueue 会优先让它被接纳;必要时,它甚至可以抢占实验性任务。

提示(TIP)

Kueue 的 fair-share 算法会综合考虑:

  • 每个团队的历史资源消耗
  • 当前队列深度

从而优先让那些最近消耗资源更少、或者已经等待更久的团队先获得资源。
它的排队策略在公平性和效率之间做平衡:既保证每个团队最终都能拿到资源,又尽量优先接纳那些能高效利用资源的大任务。

priority class 则让组织能够实现一种 SLA 化的资源策略:例如,让某些生产训练任务在高峰期可以抢占低优先级实验任务;而在资源宽松时,大家又能共享空闲容量。

NVIDIA KAI Scheduler

KAI Scheduler 提供的是面向 GPU 的专用配额管理机制,并通过层级队列为大规模 GPU 集群服务。
它能在团队之间实现 fair-share 调度,并且其配额分配决策中已经集成了 GPU 拓扑感知能力。

与 Kueue 作为 admission controller 层运行、可与任何底层调度器配合使用不同,KAI Scheduler 本身就是一个完整调度器替代方案:它把配额执行和 Pod 放置决策都集成在同一个组件中。
这带来的好处是:它可以把 quota policy 和 GPU 专属调度优化(例如分数 GPU、MIG)更紧密地耦合起来。
但这也意味着:如果你要采用 KAI Scheduler,就必须整体替换默认 Kubernetes 调度器。

Kueue 则更加灵活:
它既可以直接配合默认 kube-scheduler 使用,也可以叠加在像 KAI Scheduler 这样的专用调度器之上。
于是你就可以形成一种分层架构:

  • Kueue 管 admission 与配额
  • KAI Scheduler 管 GPU-aware placement

Volcano

Volcano 调度器有自己的一套 queue 抽象,并通过 Queue CRD 来实现配额管理,这与 Kueue 的 ClusterQueue / LocalQueue 模型是不同路线。
Volcano 的 queue 可以定义资源上限和优先级,还可以通过 reclaim policy,在高优先级 workload 到来时抢占低优先级任务。
它还支持按权重比例分配资源,从而在多个 queue 之间进行比例切分。

与 Kueue 把 admission control 与 scheduling mechanics 分离、并运行在调度器之上的思路不同,Volcano 把:

  • queue 管理
  • gang scheduling
  • 调度本身

全部集成在一个单体调度器中。
这种方式的优点是:quota policy 和 scheduling 决策之间整合得更紧。
但代价也同样明显:你必须整体替换 kube-scheduler

如何做出正确选择

选择哪种配额管理方案,取决于组织需求和现有基础设施。

  • Kueue 非常适合 cloud-native 环境,尤其是当你希望保留默认 Kubernetes 调度器时。它提供了成熟的 admission control,且与 GPU 厂商无关。它与 Kubeflow Trainer 以及其他 Kubernetes-native 工具集成顺畅,并把“政策层”(配额管理)与“机制层”(调度)分得很清楚。
  • NVIDIA KAI Scheduler 更适合 GPU 密集型部署,尤其是当 GPU 专属优化(例如分数 GPU 分配和 topology-aware placement)带来的收益已经足以支撑替换调度器的成本。对于拥有数千 GPU 的组织来说,这类优化通常会有非常实际的效率收益。
  • Volcano 则适合那些愿意用一个完整调度系统去整体替换 kube-scheduler 的组织,因为它能够在单一组件中同时提供 gang scheduling 与 quota management。

因此,平台管理员通常需要在两种架构哲学之间做选择:

  • 是在现有调度器之上叠加 Kueue,以保持灵活性
  • 还是采用自带 quota 管理的一体化专用调度器,以减少架构分层复杂度

与此同时,也要认识到:
Kueue 并不是专用 GPU 调度器的竞争替代者,它也可以成为这些调度器的补充层。
例如,你完全可以让 Kueue 负责 admission control,而让 KAI Scheduler 负责 GPU placement。

理解 Kueue 中 ResourceFlavorpriority class 的交互方式,对于配置有效的 quota policy 非常关键,下面这个侧栏会进一步解释。

KUEUE PRIORITY CLASSES 与 RESOURCEFLAVOR

Kueue 会按照 ClusterQueueflavors 列表的顺序,依次评估 ResourceFlavor

1. 第一次尝试
先尝试列表中的第一个 flavor(例如前例中的 gpu-nvlink-infiniband)。如果该 flavor 下的 nominal quota 足够,就把 workload 接纳到这一 flavor。

2. 自动回退
如果第一个 flavor 的 quota 已经耗尽,Kueue 会自动尝试下一个 flavor(例如 gpu-standard-ethernet)。

3. Borrowing 行为
flavorFungibility.whenCanBorrow 控制的是:当 nominal quota 已耗尽、但在当前 flavor 下可以通过 borrowing 获得资源时,系统应该怎么做。

  • MayStopSearch(默认)
    如果当前 flavor 可以借到资源,就直接使用这个 flavor,不再继续搜索其他 flavor。
  • TryNextFlavor
    即便当前 flavor 可以借到资源,也继续尝试列表中的下一个 flavor,以便尽量优先使用 nominal quota,而不是依赖借用资源。

4. 注入 node selector
当 Kueue 选定一个 flavor 后,它会把这个 flavor 的 nodeLabels 自动注入为 node selector,从而确保 Pod 只会被调度到满足拓扑要求的节点上。

换句话说,flavor 列表中的顺序,本身就定义了“偏好顺序”——premium topology 在前,standard topology 在后,后者是前者不可用时的 fallback。

那么这与 priority class 的关系是什么?

Kueue 中的 priority class(WorkloadPriorityClass CRD) ,与 ResourceFlavor 解决的是不同问题

  • ResourceFlavor selection
    决定一个 workload 最终获得哪种硬件和拓扑(由 flavor 顺序与资源可用性控制)
  • Priority class
    决定多个等待中的 workload 中,谁会先被接纳(由 priority value 控制)

当多个训练任务同时排队等待资源时,Kueue 会先根据 priority 决定谁先 admission:

  • 高优先级 workload:在 quota 可用时先被接纳
  • 抢占:高优先级 workload 可以驱逐正在运行的低优先级 workload,以回收 quota
  • 相同优先级:采用 FIFO 顺序
  • 公平共享:通过历史资源使用情况来避免资源垄断,优先让长期低占用的队列获得机会,从而让资源使用较少的团队即使在繁忙集群里也能持续推进任务

到这里为止,gang scheduling 解决了完整资源分配,topology-aware placement 优化了 GPU 互联,quota management 则实现了多租户公平访问。
但即使这些调度基础设施都已经完善,分布式训练任务仍然会面对一个非常关键的性能瓶颈:worker 之间在梯度同步时的网络通信

分布式训练的网络优化

在前面关于 topology-aware scheduling 的章节中,我们已经多次提到像 NVLinkInfiniBandRoCE 这样的网络互联技术,因为它们会直接影响调度器的放置决策。
而本节则更进一步,把焦点放在:网络通信本身是分布式深度学习中最关键的性能瓶颈之一

这是因为,不同并行策略——例如:

  • data parallelism
  • tensor parallelism
  • pipeline parallelism

都会产生不同的通信模式,而这些模式在性能特征和网络要求上差异明显。
关于不同模型并行策略的细节,可参考 “Model Parallelism”。

PyTorch FSDPDeepSpeed 这样的分布式训练框架,会在每个训练迭代中执行一系列集体通信操作(collective communication) ,例如:

  • all-reduce
  • all-gather
  • reduce-scatter
  • 点对点传输(point-to-point transfers)

这使得它们产生的网络流量模式,与传统应用工作负载有本质不同。
理解这些通信模式及其网络要求,能够帮助平台管理员做出匹配工作负载特征的基础设施决策。

集体通信操作(Collective Communication Operations)

分布式训练依赖三种最基础的集体通信模式:

All-reduce
每个 worker 先在本地计算梯度,然后所有 worker 把梯度组合起来(通常是求和或求平均),再把结果分发回所有 worker。
这是 data-parallel 训练中最常见的操作,它确保所有 worker 在执行 optimizer step 之前,都拿到相同的梯度更新。

All-gather
每个 worker 提供自己的数据,而所有 worker 最终都会收到由所有参与者拼接而成的完整数据集。
这种模式通常用于 model-parallel 训练,在那里不同 worker 各自持有不同模型分片,需要交换 activation tensor 或局部结果。

Broadcast
由一个 worker(通常是 rank-0)向所有其他 worker 发送相同的数据。
这常用于分发初始模型权重、超参数,或者把 checkpoint 从 rank-0 节点发给所有 worker。

这些操作通常都是同步执行的,也就是说:它们会让所有 worker 停下来一起等待。
因此,网络延迟和带宽会直接成为训练吞吐量的关键瓶颈。

一个典型的 LLM 训练任务,如果部署在 8 个节点、共 64 块 GPU 上,那么它可能每隔几秒就需要同步数十亿参数,并持续产生数百 Gbps级别的网络流量。此时,只要通信延迟上升,GPU 就会因为等待同步而空转,训练吞吐量会直接下降。

硬件厂商通常会直接销售专用的“AI 节点”,例如:

  • NVIDIA DGX
  • Dell PowerEdge XE
  • HPE Cray EX

这类节点往往会把高端 GPU(通常单节点最多 8 块)、CPU、内存以及经过优化的单节点内互联(例如 NVLink)集成在一起。
通过这种一体化硬件设计,它们能在单节点内提供数百 GBps 的 GPU 互联带宽,而不需要平台管理员手动去优化 GPU-to-GPU 拓扑。
然而,一旦训练规模扩展到单节点之外,问题就会迅速复杂化。

标准 Kubernetes 网络本来是为微服务设计的:面向有限的东—西向流量,以及北—南向 API 请求。
它并不具备分布式训练所需的性能特征。
更糟的是,像 OVN-Kubernetes 这样的默认 CNI 插件,还会引入额外网络虚拟化开销,使高带宽训练流量的性能进一步恶化。
传统 TCP/IP 网络栈还会引入:

  • kernel context switch
  • buffer copy
  • 以及标准 Ethernet 带宽上限

这些都会成为集体通信操作的最终瓶颈。

东—西向与北—南向网络流量

在数据中心网络术语中,流量通常按方向来分类:

北—南向流量(north-south traffic)
指的是数据中心外部客户端与内部服务之间的通信,例如:用户访问 Web 应用,或者外部 API 调用。
这类流量会“进入”并“离开”数据中心,因此会穿越边界防火墙。

东—西向流量(east-west traffic)
指的是数据中心内部服务之间的通信,例如:微服务之间调用,或应用服务器访问数据库。
这类流量是在内部网络中横向流动的,不会离开数据中心。

传统 Kubernetes 网络主要是为微服务优化的,典型特征是:

  • 北—南向流量占主导(服务外部请求)
  • 东—西向流量适中(服务间调用)

而分布式训练工作负载正好相反:
它会产生极其庞大的东—西向流量,用于 worker 之间的梯度同步,而对北—南向连接需求很小。

因此,平台管理员如果要在 Kubernetes 上构建生产级训练平台,就必须部署专门的网络配置,绕开标准内核网络栈,并利用那些最初为 HPC 场景设计的高性能互联技术。
事实上,如今 AI 训练基础设施的很多网络优化经验,正是直接借鉴了科学计算和超级计算机集群中几十年来积累的 HPC 经验,因为分布式训练在通信模式上与 HPC 场景高度相似。

虽然本书大部分内容都集中在软件层面的配置与工具,但这一节必须讨论:在集群节点选型阶段,究竟应该考虑哪些硬件网络方案。

比较不同 GPU 通信网络技术

理解不同网络技术的性能特征,对于选择合适基础设施、以及为分布式训练配置最优通信路径来说至关重要。

表 7-5 用于 GPU 通信的网络技术

NVLink / AMD Infinity Fabric
范围:单节点内 GPU-to-GPU 点对点互联
带宽:每个 GPU 聚合带宽约 900 GBps–1.8 TBps(NVLink 4.0–5.0);MI300X 可达约 896 GBps
延迟:微秒级
适用场景:单节点内直接 GPU 通信,尤其是 2–4 GPU 配置

NVSwitch
范围:单节点内 GPU 交换式互联
带宽:每个 GPU 约 600–900 GBps(全互联)
延迟:微秒级
适用场景:8–16 GPU 服务器,需要所有 GPU 全互联,例如 DGX

InfiniBand
范围:跨节点 RDMA 网络织构
带宽:每端口 200–400 GBps
延迟:亚微秒级
适用场景:大规模 HPC 训练集群、跨几十个节点训练超大模型、追求极致性能

RoCE(RDMA over Converged Ethernet)
范围:基于 Ethernet 的跨节点 RDMA
带宽:每端口 100–400 GBps
延迟:低微秒级
适用场景:希望获得高性能训练能力,但又不想为 InfiniBand 单独建设专用网络基础设施;适合多种流量共网

标准 Ethernet
范围:跨节点 TCP/IP 网络
带宽:典型为 10–25 GBps(高端可到 100 GBps)
延迟:几十到几百微秒
适用场景:小规模训练任务、通信压力较低的工作负载、没有专用高性能网络基础设施的组织

GPUDirect RDMA
范围:作为 InfiniBand / RoCE 的增强能力
效果:相较传统路径可减少 40–60% 延迟
适用场景:通信受限型训练场景,需要 GPU 与 NIC 之间直接传输,避免 CPU 参与

NVLink 与 AMD Infinity Fabric

高速点对点 GPU 互联为 GPU-to-GPU 通信提供了最高带宽、最低延迟,能够绕过传统 GPU 通信依赖的 PCIe(Peripheral Component Interconnect Express) 限制。

NVLink 是 NVIDIA 的专有高速互联技术,它通过点对点链路直接在 GPU 与 GPU、GPU 与 CPU 之间建立通信通道。
现代 NVIDIA 数据中心 GPU(例如 H100)支持 NVLink 4.0,每块 GPU 通过 18 条高速链路提供最高 900 GBps 的双向聚合带宽,使得原本以毫秒计的梯度同步操作,可以缩短到微秒级。
NVLink 通常部署在 2–4 GPU 的配置中,此时 GPU 之间通过直接点对点连接,就足以满足小规模训练工作负载的最佳性能需求。

AMD Infinity Fabric 则是 AMD 对应的 GPU 互联技术,适用于其 Instinct MI 系列 GPU(如 MI250X、MI300X)。
Infinity Fabric 提供的 GPU-to-GPU、GPU-to-CPU 通信能力,与 NVLink 在性能特征上大体相当。例如,在 8-GPU 的 MI300X 平台上,它可以提供接近 900 GBps 的双向带宽。
和 NVIDIA 一样,AMD 的方案也是通过优化过的点对点链路,实现小规模多 GPU 服务器中的直接内存访问。

无论是 NVLink 还是 Infinity Fabric,它们通常都局限于特定服务器配置,因为 GPU 必须通过专有线缆或集成背板物理连接。
因此,它们主要还是单节点内的通信技术,用于直接 GPU-to-GPU 互联。
当然也有少量例外,例如 NVIDIA 的 DGX SuperPOD 架构,以及 AMD 基于 OAM(Open Accelerator Module) 的系统,它们可以在较小规模的多节点训练中延伸这类互联能力。

NVSwitch

当服务器中的 GPU 数量增加到 8 块甚至更多时,仅靠点对点链路就不再足够,此时就需要像 NVSwitch 这样的交换式 fabric,在单机内部为所有 GPU 构建无阻塞通信路径。

NVSwitch 的作用,是在一台包含 8–16 块 GPU 的服务器中,提供全互联的 GPU-to-GPU 通信结构。
每块 GPU 都可以通过连接到交换 fabric 的 NVLink 链路,获得最高约 900 GBps 的聚合带宽。
与直接把两块 GPU 用点对点 NVLink 连起来不同,NVSwitch 更像一个中心交换 fabric:NVLink 负责作为物理链路,把各块 GPU 连到交换机,而交换机则为任意一对 GPU 提供无阻塞路径。

这种架构常见于 NVIDIA DGX 之类的高端 AI 训练服务器中。
对于那些需要在很多 GPU 之间同时做梯度同步的大规模分布式训练任务来说,NVSwitch 能够支撑所需的 all-to-all 通信模式,并避免形成通信热点。
因为在全互联结构下,无论是 all-reduce 还是 all-gather,任意参与 GPU 都能以接近一致的带宽通信,而不会出现多块 GPU 同时争抢单一路径的瓶颈。

InfiniBand

在多节点 GPU 通信领域,InfiniBand 一直被视为高性能计算和大规模 AI 训练环境中的“黄金标准”。
它通过 RDMA(Remote Direct Memory Access) 提供亚微秒级延迟,并可把带宽扩展到每端口 400 GBps

InfiniBand 是一种专门为 HPC 集群设计的高速网络 fabric,它允许 GPU 和 CPU 直接读写远程节点上的内存,而不需要操作系统内核介入,从而消除了 TCP/IP 网络中的 context switch 和 buffer copy 开销。

InfiniBand fabric 可以通过交换机扩展到成千上万个节点,并提供全二分带宽(full bisection bandwidth) ,确保网络中任意两个节点之间都能获得满速通信,而不受拓扑结构影响。
这对大规模训练任务尤为关键,因为 all-reduce 往往需要同时在几十甚至上百块 GPU 之间聚合梯度。

当然,InfiniBand 也有代价:
它需要一套独立于标准 Ethernet 数据中心网络之外的专用网络基础设施,因此资本成本和运维复杂度都更高。
因此,它最适合那些正在运行大规模训练平台、且其性能收益足以覆盖这些额外基础设施投入的组织。

RoCE

RoCE(RDMA over Converged Ethernet) 把 RDMA 能力带到了标准 Ethernet 网络之上,因此它在性能与成本之间提供了一种折中:
既能部分接近 InfiniBand,又保留了 Ethernet 的普适性和成本优势。

RoCE 使用与 InfiniBand 相同的 RDMA 编程接口,只不过把 RDMA 数据包封装在 Ethernet 帧之中,因此组织可以复用现有的 Ethernet 交换网络基础设施,同时依然获得 kernel-bypass 和 zero-copy 的优势。

当前主流标准是 RoCEv2(截至 2026 年初),它把 RDMA 流量封装进 UDP/IP 包中,因此具备路由能力,而这是二层通信的 InfiniBand 所不具备的。不过,这也意味着它的延迟会略高于原生 InfiniBand。

现代支持 RoCE 的 Ethernet 适配器,每个端口可提供 100–400 GBps 的带宽,延迟则处于低微秒级。
对于很多工作负载而言,它已经非常接近 InfiniBand,同时又能运行在承载常规 TCP/IP 流量的融合网络上。

但由于它是基于 UDP/IP 来规避 TCP/IP 开销,因此会有丢包风险。一旦丢包,就会触发昂贵的应用层重传逻辑。
为降低这一问题,通常需要采用一些更严格的 Ethernet 配置,例如:

  • Priority Flow Control
  • Enhanced Transmission Selection

标准 Ethernet

基于 TCP/IP 的标准 Ethernet,仍然是最容易获得、门槛最低的分布式训练网络方案。
对于规模较小的训练任务,或者没有专用高性能网络基础设施的组织来说,它依然可以提供足够的可用性能。

Kubernetes 的默认网络(通过 Calico、Cilium、Flannel 等 CNI 插件实现)通常就运行在标准 Ethernet 之上。
其带宽范围一般在 10–100 GBps,具体取决于网卡和交换机能力;而延迟则通常在几十到几百微秒之间,也会受网络拓扑与拥塞影响。

虽然它显著慢于 InfiniBand 或 RoCE,但在较小规模下,标准 Ethernet 仍然是可行方案。
例如,对于 data-parallel 训练,只要通信开销占单次 step 总时间的比例低于 15%,那么 2–8 节点的标准 Ethernet 方案往往仍能接受。
实践中,最好的办法是通过 profiling 去测量:

  • all-reduce 时间
  • 相对于纯计算时间的占比

如果通信开销超过 20–25% ,那就意味着你大概率需要上 RDMA 网络了。
当然,这个阈值也会随着模型大小和并行策略不同而变化:例如,pipeline parallelism 的网络流量通常小于 tensor parallelism;而 gradient accumulation 则可以减少同步频率,但会带来收敛上的权衡。

标准 Ethernet 的最大优点,在于它简单:

  • 无需专用硬件(普通网卡即可)
  • 无需复杂 fabric 配置
  • 与 Kubernetes 默认网络模型开箱即用

GPUDirect RDMA

GPUDirect RDMA 是一种进一步的增强能力,它允许 GPU 与网络适配器之间直接做内存访问,从而去掉传统通信路径中的 CPU 参与和中间内存拷贝,这能显著降低延迟。

前面那些技术已经优化了 GPU-to-GPU 通信,但跨节点通信通常仍然要经过内核与内存拷贝。
GPUDirect RDMA 通过让网卡直接从 GPU 显存中读写数据,绕过这些中间步骤,大幅提升分布式训练时的节点间通信效率。

它既可以和 InfiniBand 搭配,也可以和 RoCE 搭配,通常能把通信延迟相较传统路径降低 40–60%
不过,它也需要:

  • 支持 RDMA 的专用网卡(例如 NVIDIA Mellanox ConnectX)
  • 已启用 GPUDirect 的 GPU 驱动
  • 在训练框架中正确配置 NCCL

因此,它的配置复杂度会更高。

如何做出正确选择

图 7-4 展示了完整 GPU 网络栈:节点内通过 NVLink / NVSwitch 连接 GPU;节点间则通过 InfiniBand、RoCE 或标准 Ethernet 做通信。

image.png

图 7-4 GPU 网络栈:单节点内 NVLink / NVSwitch 与跨节点 InfiniBand / RoCE / Ethernet

平台管理员在选择网络技术时,必须在:

  • 性能需求
  • 基础设施成本
  • 运维复杂度

之间做平衡,而并行策略(见 “Model Parallelism”)正是决策中的关键因素。

对于 data parallelism 工作负载

这是分布式训练中最常见的策略。
如果组织要在几十个节点上训练大模型,那么应该认真考虑 InfiniBand + GPUDirect RDMA,因为它在梯度 all-reduce 上的带宽优势非常明显。
如果组织本身已经拥有高性能 Ethernet 网络,也可以使用 RoCE + GPUDirect RDMA,在不彻底替换网络基础设施的前提下逼近 InfiniBand 性能。
而对于规模较小的 data-parallel 任务(2–8 节点),使用 100 GBps 以太网适配器 并做好 Kubernetes 网络优化,通常就能带来显著提升,而不必承担专用网络的复杂度。

对于 tensor parallelism 工作负载

NVLink / NVSwitch 几乎是必需品,而不是可选项。
因为 tensor parallelism 对每层级别的 all-gather / reduce-scatter 延迟极其敏感:
这类工作负载在标准 Ethernet 上几乎不可行,即便是跨节点的 InfiniBand 也会非常有挑战。
所以 tensor parallelism 通常会限制在:

  • 单节点内(8–16 GPU,通过 NVSwitch)
  • 或极少量节点(2–4 节点,通过 InfiniBand)

只有在这些环境下,亚微秒级延迟要求才有可能满足。

对于 pipeline parallelism 工作负载

pipeline parallelism 的阶段间通信是顺序型的,因此它更依赖于拓扑感知调度(例如把相邻 pipeline stage 尽量共置,减少 hop),而不是极限带宽本身。
因此,RoCE,甚至优化过的 Ethernet,都可能已经足够。
因为 pipeline parallelism 的点对点通信带宽需求只是中等水平,而且它对延迟的容忍度明显高于 tensor parallelism。

对于混合并行(hybrid parallelism)

如果把 data + tensor + pipeline 多种策略混合起来,那么 InfiniBand + GPUDirect RDMA 基本就成了最现实的选择。
因为这类工作负载同时要求:

  • 高带宽(用于 data parallel 的梯度同步)

  • 低延迟(用于 tensor parallel 的层间通信)

  • 以及仔细设计的 topology-aware scheduling

    • 把 tensor parallel 的 GPU 尽量放在同一个 NVSwitch 域中
    • 而把 data-parallel 的 replica 分布在 InfiniBand fabric 之上

无论最终选择的是 InfiniBand、RoCE 还是优化过的 Ethernet,要在 Kubernetes 中真正使用这些高性能 fabric,都需要在集群默认的 CNI 网络之外,额外配置第二网络接口

在 Kubernetes 中使用第二网络接口

Kubernetes 最初的网络模型,是围绕“每个 Pod 只有一个网络接口”来设计的:Pod 的网络连通性由集群的主 CNI 插件负责。
但分布式训练工作负载需要的不只是这个默认接口,它们往往还需要额外的专用网络接口,以接入像 InfiniBand 或 RoCE 这样的高性能 fabric。

Multus CNI 正是为了解决这个限制而设计的。
它允许 Pod 同时挂接多个网络接口:

  • 主接口继续使用集群默认 CNI
  • 第二接口则提供训练框架所需的专用高性能网络路径

提示(TIP)

CNI(Container Network Interface) 是 Cloud Native Computing Foundation 制定的一项规范,它定义了容器运行时与网络插件之间的标准接口,用于为 Linux 容器配置网络接口。

CNI 插件通过实现 Kubernetes 网络模型,为 Pod 提供 Pod-to-Pod 和 Pod-to-External 的通信能力。每个插件都负责:

  • 创建网络接口
  • 分配 IP 地址
  • 建立连通性

Kubernetes 之所以可以使用不同的网络实现(如 Calico、Cilium、Flannel),正是因为 CNI 提供了这种可插拔架构。
这些插件都能提供基础网络功能,同时也可能额外带来网络策略、加密、数据路径优化等增强能力。

Multus CNI 本身是一个 meta-plug-in,它并不直接创建网络接口,而是根据 NetworkAttachmentDefinition(NAD) 自定义资源,把这项工作委托给其他 CNI 插件去完成。
NAD 负责描述:某个 second interface 应该如何被配置。

例如,在 InfiniBand 场景下,NAD 可以定义一个挂载到每个节点 ib0 设备上的 second interface,同时分配 IP 地址,并通过 rdmaIsolation: false 之类的设置来启用 GPUDirect 所需的直接 RDMA 访问(见示例 7-10)。

示例 7-10 配置基于 IPoIB 的 NetworkAttachmentDefinition

apiVersion: k8s.cni.cncf.io/v1
kind: NetworkAttachmentDefinition
metadata:
  name: ib-network
spec:
  config: '{
    "cniVersion": "0.3.1",
    "type": "ipam",   
    "master": "ib0",
    "ipam": {
      "type": "whereabouts",   
      "range": "10.0.0.0/24",
      "exclude": [ "10.0.0.1/32" ]
    }
  }'

这里:

  • 使用 IPAM 插件来处理 IP 地址分配
  • 这里具体使用的是 whereabouts 插件作为 IPAM

平台管理员通常会把 Multus 以 DaemonSet 形式部署到所有节点上,然后创建 NAD 资源来描述可供 Pod 挂接的第二网络。
当训练 Pod 通过 k8s.v1.cni.cncf.io/networks annotation 请求挂接某个 secondary network 时,Multus 就会按照相应 NAD 配置,在 Pod 的 network namespace 内创建并配置额外网络接口。

如果你使用的是 RDMA,事情会更复杂一些:
因为训练框架必须能够访问 RDMA 设备(通常会以 /dev/infiniband/* 这类设备文件形式暴露),才能实现 kernel-bypass 通信。
此时通常还需要一个专门的 RDMA CNI plug-in 与 Multus 配合,用于配置 RDMA 设备权限,并确保 Pod 能访问与其 second interface 对应的 RDMA 设备。
然后,再通过 RDMA device plug-in,把这些 RDMA 设备暴露成 Kubernetes scheduler 可见的资源,这样你就可以在 deployment spec 中显式请求它们,例如:

rdma/hca: 1

表示请求一个 RDMA host channel adapter。

即便集群网络配置本身已经完成,也还需要确保训练框架所依赖的通信库(最主要的是 NVIDIA 在 PyTorch 工作负载中使用的 NCCL)真的能发现并使用这些高性能网络接口,而不是退回到 Kubernetes 默认主网络上。

NCCL 会自动检测网络接口,并在可用时优先选择那些支持 RDMA 的接口。
不过,为了获得更确定的行为,通常还是建议通过环境变量显式配置它,例如:

  • NCCL_IB_HCA
  • NCCL_SOCKET_IFNAME
  • NCCL_NET_GDR_LEVEL

这样可以明确指定 NCCL 在 collective communication 中应走哪条网络路径(见示例 7-11)。

非常重要的一点是:平台管理员必须协调网络接口命名规则,确保 secondary interface 在所有节点上都有一致且可预测的名字。
只有这样,训练任务配置才能统一引用这些接口,而不需要为每个节点单独定制。

示例 7-11 使用第二网络接口配置 PyTorchJob

# For TrainJob, configure these settings in the ClusterTrainingRuntime template
apiVersion: kubeflow.org/v1
kind: PyTorchJob
metadata:
  name: llm-training-ib
spec:
  pytorchReplicaSpecs:
    master:
      ...
      template:
        metadata:
          annotations:
            k8s.v1.cni.cncf.io/networks: ib-network   
        spec:
          containers:
          - name: pytorch
            ...
            env:
            # Enable this option to get detailed information about
            # detected network interfaces and more
            - name: NCCL_DEBUG
              value: "INFO"
            # Specifies which InfiniBand adapters NCCL should use
            - name: NCCL_IB_HCA
              value: "mlx5_0,mlx5_1"
            # Selects the Global Identifier (GID) index for
            # InfiniBand communication (3=RoCEv2 mode on Ethernet)
            - name: NCCL_IB_GID_INDEX
              value: "3"
            # Enables maximum GPUDirect RDMA optimization
            # allowing direct GPU-to-NIC transfers
            - name: NCCL_NET_GDR_LEVEL
              value: "5"
            # Directs NCCL to use the secondary network interface
            # for communication instead of the default eth0
            - name: NCCL_SOCKET_IFNAME
              value: "net1"
            resources:
              requests:
                ...
                # Require two RDMA host channel adapters per pod
                rdma/hca: 2
              ...
            securityContext:
              capabilities:
                add: ["IPC_LOCK"]   
    ...

这里:

  • annotation k8s.v1.cni.cncf.io/networks: ib-network 会让 Multus 为 Pod 附加由 ib-network NAD 定义的 second interface。
  • IPC_LOCK capability 允许容器锁定内存页,防止被 swap 到磁盘。这对于 RDMA 非常关键,因为 RDMA 要求使用 pinned memory buffer。

第二网络优化与排障

在完成基础配置之后,还有一些优化和排障手段,可以显著提升 second network 在分布式训练中的表现:

NCCL 拓扑感知
通过配置 NCCL_TOPO_FILE,可以给 NCCL 提供 GPU 与网卡拓扑的详细信息,从而让它选择最优通信路径。
NCCL 的自动探测对标准环境通常已经够用,但在复杂的多 GPU、多 NIC 节点中,它未必总能找到最快路径。
可以在每个节点上通过 nvidia-smi topo -m 生成 topology file,再通过 ConfigMap 提供给训练 Pod。

网卡调优
现代 RDMA 网卡通常暴露了很多可调参数,这些参数会直接影响性能。
例如对 Mellanox ConnectX 网卡,可以考虑在 subnet manager 中启用 adaptive routing(--set_adaptive_routing),以在 InfiniBand fabric 中平衡多条路径的流量;
同时也可以配置合适的 MTU(例如 InfiniBand 常见为 4096),以减少包头开销。

NUMA 感知
在多路节点中,必须尽量让训练 Pod 被 pin 到与其所用 GPU 和网卡本地对应的 CPU 上,以减少跨 socket 内存流量带来的延迟。
Kubernetes 的 Topology Manager 可以帮助实现 NUMA 感知放置,而 NCCL 也会在决定通信模式时参考 CPU affinity。

网络隔离
应尽量把训练工作负载部署在专用 VLAN 或 InfiniBand partition 中,与其他集群流量隔离,防止无关流量竞争带宽,造成延迟抖动。
Kubernetes NetworkPolicy 可以做应用层隔离,但只有物理网络隔离才能真正为高带宽训练通信提供有保障的带宽。

验证与排障
第二网络的配置并不简单,因此必须验证 NCCL 是否真的发现并使用了你期望的网络。示例 7-12 提供了一些常见排障命令。

平台管理员还应当做有针对性的 benchmark,对比“优化网络配置前后”的训练表现,以量化收益。
只有使用代表性模型架构和实际分布式训练配置做出来的 benchmark,才能为基础设施投资提供真正有说服力的数据:这些优化后的 second network 和 RDMA 配置,究竟能不能带来足够性能收益,值得它们额外的运维复杂度。

示例 7-12 第二网络排障命令

# Check that a secondary network interface was created on the primary node
kubectl exec -n %NAMESPACE% llm-training-ib-master-0 -- ip addr show net1

# Verify RDMA devices are accessible
kubectl exec -n %NAMESPACE% llm-training-ib-master-0 -- ls /dev/infiniband/

# Examine NCCL debug output to confirm InfiniBand usage
# "Using network IB" and "NET/IB/GDRDMA" indicates usage of InfiniBand
# with GPUDirect RDMA
kubectl logs -n %NAMESPACE% llm-training-ib-master-0 | grep "NCCL INFO"

连接 HPC 与 Kubernetes:Slurm 与 Slinky

虽然 Kubernetes 已经成为 cloud-native 工作负载的主流平台,但传统 HPC(High-Performance Computing) 环境在管理大规模科学计算工作负载方面,早已积累了数十年的经验。
Slurm(Simple Linux Utility for Resource Management) 这样的专用 workload manager,一直是 HPC 世界的主力。
而随着 AI 训练工作负载越来越像 HPC 批处理任务——同样需要 gang scheduling、多节点协调、GPU 资源管理、topology-aware placement——业界也越来越关注:能否把 HPC 的调度经验引入 Kubernetes 环境。

Slurm 在全球 HPC 环境中占据主导地位,它支撑着世界上最大的超级计算中心,并且在许多能力上比今天的 Kubernetes 更成熟,例如:

  • 原生 gang scheduling,确保 all-or-nothing 的资源分配
  • network topology-aware scheduling,把 job 放到 GPU 互联带宽最优的节点上
  • 成熟的 accounting 系统,可以记录 GPU-hours,用于 charge-back 和 fair-share
  • 可插拔架构,支持复杂的资源选择策略

HPC 社区用 Slurm 管理大规模 AI 模型训练所积累的经验,对 Kubernetes 训练平台也具有很高参考价值。

Slinky 是 SchedMD 推出的一组创新项目,目的是打通 Slurm 与 Kubernetes 两大生态,使组织既可以在 Kubernetes 基础设施中运行由 Slurm 管理的工作负载,也可以把 Slurm 的调度能力与 Kubernetes 编排能力结合起来。
Slinky 包含:

  • Slurm Operator:把 Slurm 集群管理成 Kubernetes 自定义资源,并支持动态扩缩
  • REST Client:用于把 Slurm 接入 Kubernetes 控制器与 webhook
  • Prometheus Exporter:提供跨 Slurm 与 Kubernetes 的统一监控

对于那些已经拥有 HPC 基础设施、或者工作负载确实需要 Slurm 高级调度能力(例如复杂 GPU 拓扑、成熟的 fair-share 策略和精细 accounting)的组织来说,Slinky 可以是一条务实的迁移路径。
而对 cloud-native 团队而言,也应认识到:Kubernetes 其实正在不断吸收 HPC 的这些模式——通过 gang scheduling plug-in、GPU device plug-in、topology-aware scheduling proposal 等手段,逐渐具备类似能力。

HPC 与 Kubernetes 的这种融合,正代表着 AI 训练基础设施的演进方向:两大生态都在从彼此的优势中学习。

当调度、拓扑感知、配额管理和高性能网络都配置好之后,训练平台还需要一个可靠的存储系统,来支撑训练任务完整生命周期,尤其是 checkpoint 管理,以及在被抢占之后的恢复。

训练存储(Storage for Training)

对于分布式训练工作负载来说,可靠的持久化存储至关重要。
这一点在 GPU as a Service 场景中尤为明显,因为在那种环境下,配额管理和抢占策略会带来动态资源共享。
例如在“Quota Management and Multitenancy: GPU as a Service”中提到的资源借用和优先级抢占:低优先级训练任务可能会在执行到一半时被暂停,以便把 GPU 让给更高优先级工作负载,之后再在资源重新可用时继续恢复。
如果没有稳健的 checkpoint 存储,被抢占的任务就会失去全部训练进度,导致昂贵的重新计算。

因此,平台管理员必须提供一种存储基础设施,它能够:

  • 支撑频繁的 checkpoint 操作
  • 支持在抢占或故障后恢复
  • 允许多个并发训练任务共享访问训练数据集

针对分布式训练的存储需求,已经有多种技术可选,它们在性能、可扩展性和运维复杂度上各有权衡。

表 7-6 分布式训练存储方案

Network File Systems(NFS)
访问模式:RWX / RWO / ROX
性能:顺序读较好,但高并发或随机 I/O 下性能会下降
运维复杂度:低(容易接入现有企业 NFS 基础设施)
适用场景:本地部署环境中共享数据集与 checkpoint,且企业本身已有 NFS

分布式文件系统(Ceph / CephFS、GlusterFS、OpenShift Data Foundation)
访问模式:RWX / RWO / ROX
性能:可横向扩展吞吐,并具备节点故障容忍能力
运维复杂度:高(需要专门存储节点、容量规划和分布式系统运维经验)
适用场景:大规模训练平台,有专门基础设施团队,且同时运行多个并发任务

云托管文件存储(Amazon EFS、Google Filestore、Azure Files)
访问模式:RWX / RWO / ROX
性能:性能稳定,支持自动扩缩
运维复杂度:低(全托管,无需自维护)
适用场景:云原生平台、优先追求运维简洁而不是极致成本优化、团队没有存储专长

对象存储(S3、GCS、MinIO、Ceph RGW)
访问模式:API 模式(不是 POSIX mount)
性能:可扩展性最强,适合多 worker 并行下载,避免共享文件系统瓶颈
运维复杂度:中(需要通过 S3 API 与应用集成,不能直接挂载为文件系统)
适用场景:超大规模数据集(TB 级以上),配合 PyTorch DataLoader、TensorFlow tf.data 等流式加载器,且对成本敏感

本地 NVMe 存储
访问模式:RWO(节点本地)
性能:微秒级延迟,多 GBps 吞吐
运维复杂度:高(需要数据预热、checkpoint 再复制到远端持久存储,且 Pod 重调度后本地数据会丢)
适用场景:需要极致 I/O 性能的数据预热场景,且作业能够接受重复预热,同时已有可靠远端 checkpoint 策略

在生产环境中,一个成熟的训练存储架构通常会把多种方案组合起来使用:

  • 对象存储:用于承载大规模、不可变的数据集,并通过流式 API 访问
  • 分布式文件系统或云托管文件存储:用于共享 checkpoint,因为它们需要跨 worker、跨节点的 RWX 访问
  • 本地 NVMe(可选) :用于数据预热,以最大化 GPU 利用率

其中最关键的要求之一,就是 checkpoint 和模型工件必须放在支持 RWX 的共享存储上。
这样多个 worker Pod 才能跨节点访问共享状态,也才能支持任务在被抢占后的恢复。

在为训练工作负载配置存储时,平台管理员必须注意:容量规划不能只看最终模型大小,还必须把训练过程中所有中间 checkpoint 都算进去。
一个实用经验公式是:

2 × base_model_size + checkpoint_overhead

也就是至少要为:

  • base model
  • 中间 checkpoint(训练框架通常每 N 个 step 保存一次)
  • 最终产物

预留足够空间。
例如:

  • 训练一个带 LoRA adapter 的 Llama 3.1 8B 模型,并频繁做 checkpoint,通常需要约 100 GB 存储
  • 而对一个 70B 模型做 full fine-tuning,则可能需要 500 GB 甚至更多,具体取决于 checkpoint 频率和保留策略

有了支持 checkpoint 和恢复的存储基础设施之后,平台管理员还必须解决另一个容易被忽视的问题:
由于分布式训练框架本身是为性能优先设计的,它们在多租户环境中会带来额外的安全挑战

训练任务安全(Training Job Security)

RayPyTorch Distributed 这样的分布式训练框架,会引入一些独特的安全问题,这些问题已经超出了传统 Kubernetes 工作负载安全的范畴。
平台管理员如果要在多租户训练环境中使用它们,就必须先理解这些框架的架构特点,并且在基础设施层面默认启用适当安全配置。
这些问题的根源,主要来自于它们的一个核心设计取向:优先性能,而不是优先内建安全隔离

这类框架通常默认自己运行在一个可信网络环境中,也就是说它们预设:
系统已经在更底层完成了参与者认证,因此它们本身不需要在应用层再去做复杂的安全机制。
这种设计哲学对于最初面向的研究环境或单租户环境是合理的,但它并不适合传统的、多租户的生产 Kubernetes 集群。

无论是 Ray 还是 PyTorch,在分布式组件之间的通信上都缺乏足够强的认证与授权机制。
只要某个进程能够建立到 Ray 集群或 PyTorch 训练任务的网络连接,它通常就可以在应用权限范围内执行任意代码。
默认情况下,这些框架:

  • 通信不加密
  • 接受来自任意网络源的连接
  • 不对工作负载执行额外安全检查

也就是说,它们把“网络可达性”直接等价成了“已获授权”。

再加上它们默认配置中通常缺少加密,而像 cloudpickle 这样的序列化机制本身又被广泛认为是不安全的(因为它可以执行任意 Python 代码),整个攻击面就被进一步扩大了。

因此,平台管理员必须认识到:
这些安全限制并不是配置疏忽,而是这些框架有意作出的设计选择——为了训练性能,它们牺牲了内建安全隔离。
如果强行在框架内部补齐严格安全边界,通常会引入很大的性能损耗,从而直接削弱这些框架本来的价值。

所以,要安全地部署分布式训练框架,就必须依赖基础设施层面的控制手段
其中最核心的是通过 Kubernetes NetworkPolicy网络隔离,把每个训练任务包裹在一个“可信 enclave”中,只允许同一训练任务内部的授权 Pod 相互通信。
在此基础上,还可以叠加一些“纵深防御”手段,例如:

  • 对 Ray 加启 TLS
  • 对 PyTorch 所在网络使用加密型 CNI 插件

但必须接受:这些额外防御也会带来性能代价。
换句话说,分布式训练框架本身应被视作“天然不安全的组件”;它们只有在被正确的网络边界和 namespace 边界包裹起来之后,才真正变得可用于生产。

Ray 的安全指南

Ray 是一个广泛用于强化学习、超参数调优和分布式训练的分布式计算框架。
它的安全模型延续了其“性能优先”的设计哲学:默认假设运行环境是可信网络、执行代码也是可信代码,因此并不提供内建访问控制或代码隔离机制
这一设计原则其实适用于 Ray 整个生态,因此,只要客户端在网络上可以访问 Ray 服务,它就可以通过:

  • Ray jobs
  • Ray Client API
  • Dashboard REST endpoint

执行任意代码。

在这种情况下,首先值得做的事情,是按照 Ray 官方指南为 gRPC 通道启用 TLS 认证
通常是通过在 RayCluster 自定义资源中配置 rayStartParams,并把 TLS 证书作为 Kubernetes secret 挂载进去。
这可以缩小攻击面,但它不能代替网络隔离

平台管理员应当把每个 Ray 集群都部署到独立的 Kubernetes namespace 中,并通过基础设施层的安全控制,保证 Ray 在生产环境中的安全使用。
其中最核心的手段,仍然是 Kubernetes NetworkPolicy
一个设计合理的 NetworkPolicy,通常应做到:

  • 默认拒绝所有进入 Ray head 和 worker Pod 的流量

  • 再按需放开必要通信,例如:

    • worker 到 head 的内部端口访问(6379:GCS,8265:dashboard,10001:Ray client server)
    • 同一个 Ray cluster 内部 pod-to-pod 通信(供 object store 使用)
    • 通过认证代理,受控地暴露 Ray Client 或 Jobs API
      (见示例 7-13)

示例 7-13 用于隔离 Ray 集群的 NetworkPolicy

# Deny all ingress traffic by default for the Ray namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: ray-default-deny
  namespace: ray-cluster-team-a
spec:
  # Empty selector applies to all pods in the namespace,
  # denying all ingress by default
  podSelector: {}
  policyTypes:
  - Ingress
---
# Allow worker-to-head communication on Ray internal ports
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: ray-worker-to-head
  namespace: ray-cluster-team-a
spec:
  podSelector:
    matchLabels:
      # Select Ray head node pods to receive traffic from workers
      ray.io/node-type: head
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          # Allow ingress only from pods with the same cluster label,
          # ensuring isolation between Ray clusters
          ray.io/cluster: ray-cluster-team-a
    ports:
    # Port 6379 for Ray GCS server
    - protocol: TCP
      port: 6379
    # Port 8265 for Ray dashboard
    - protocol: TCP
      port: 8265
    # Port 10001 for Ray client server
    - protocol: TCP
      port: 10001
---
# Allow pod-to-pod communication within same Ray cluster
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: ray-intra-cluster
  namespace: ray-cluster-team-a
spec:
  podSelector:
    matchLabels:
      ray.io/cluster: ray-cluster-team-a
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          # Object store requires cluster-internal access
          ray.io/cluster: ray-cluster-team-a
---
# Allow controlled access to Ray Client/Jobs API through auth proxy
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: ray-api-access
  namespace: ray-cluster-team-a
spec:
  podSelector:
    matchLabels:
      ray.io/node-type: head
  policyTypes:
  - Ingress
  ingress:
  - from:
    # Only allow access from authentication proxy in a separate namespace
    - namespaceSelector:
        matchLabels:
          name: auth-proxy-namespace
      podSelector:
        matchLabels:
          app: oauth2-proxy
    ports:
    # Dashboard access through authentication proxy only
    - protocol: TCP
      port: 8265

PyTorch 的安全指南

PyTorch Distributed 是目前最广泛使用的分布式深度学习框架之一,支撑着全球大量大规模训练任务。
其中,PyTorch 的 Distributed Data Parallel(DDP) 会把模型复制到多个进程中,并借助集体通信后端同步梯度:

  • GPU-to-GPU 通信一般使用 NCCL
  • CPU 场景下则常用 Gloo

在安全问题上,PyTorch Distributed 与 Ray 一样,存在一个核心限制:
其官方安全声明明确指出,分布式功能“仅适用于内部通信”,并“不适合用于不可信环境”。
它不提供内建授权机制,通信默认也不加密,而且接受来自任意网络源的连接。
只要能接入其网络,理论上就可以在应用权限下执行任意代码。
与 Ray 不同的是,PyTorch Distributed 甚至没有内建 TLS 能力,因此,网络隔离几乎就是唯一有效的安全手段。

要在 Kubernetes 中安全地运行 PyTorch Distributed,必须使用 NetworkPolicy 创建一组与框架安全假设一致的隔离边界。
平台管理员通常会借助 label selector 来精确限定一个 job 的 Pod,例如使用:

pytorch-job-name=my-training-job

这样就能允许“同一训练任务内部通信”,同时阻断外部流量。
一个合理设计的策略,应遵循与 Ray 类似的模式:

  • 默认拒绝所有 ingress

  • 然后只放开必要通信

    • 同一 PyTorchJob 内部 pod-to-pod(因为 NCCL 可能使用动态端口范围,因此通常需要允许 job 内所有 TCP 流量)
    • 以及从 Kubeflow Trainer 所在 namespace 到 rank-0 worker 的特定访问

训练任务可观测性(Observability of Training Jobs)

在 Kubernetes 上为分布式训练任务做可观测性,会面临很多传统应用监控中没有的挑战。
平台管理员需要为这样一种系统建立监控能力:它可能由几十甚至几百个短生命周期 Pod 组成,这些 Pod 协同执行一个长时间运行的训练任务,因此既要跟踪训练进度,也要跟踪资源利用率与任务健康状态。

与无状态微服务不同,监控单个实例并不足以了解整体情况。
分布式训练要求的是跨全部 worker Pod 的关联观测
例如:

  • 一个 worker 变慢,整个任务都会变慢
  • 某个节点上的 GPU 利用率可能很低,而其他节点上的 GPU 正在高效运行
  • 某些梯度同步瓶颈,只有在观察整个 worker 集合的通信模式时才能发现

此外,训练任务往往会运行很长时间(从数小时到数周),因此可观测性必须同时覆盖两种维度:

  • 实时运维指标:用于发现当前立即需要处理的问题
  • 历史训练指标:用于分析收敛行为、排查失败实验、优化超参数,并对比多轮训练效果

因此,平台管理员需要构建一套覆盖三个层面的可观测性体系:

  • 应用层训练指标:跟踪模型性能和收敛过程
  • 基础设施指标:跟踪资源利用率(尤其是 GPU)和任务健康状态
  • 分布式系统指标:跟踪通信模式和协调开销

分布式训练中的指标采集

我们已经在 “GPU Usage Monitoring” 一节中讲过如何监控 GPU 指标,这里重点补充其他部分。

训练指标(Training metrics)

训练指标直接反映模型的学习进展和性能表现。
它们帮助你判断:

  • 训练是否在朝着目标精度收敛
  • 是否因为超参数配置不当而开始发散

现代训练框架通常会集成实验追踪系统,用来记录诸如:

  • training loss
  • validation loss
  • accuracy
  • learning rate schedule
  • 数据科学家自定义的业务指标

虽然 TensorBoard 最初来自 TensorFlow 生态,但如今它已经成为 PyTorch 领域中可视化训练指标的事实标准。
在代码层面,训练程序可以通过:

  • torch.utils.tensorboard.SummaryWriter
  • tf.summary

把指标写到日志中;然后 TensorBoard server 会读取这些日志文件,并提供 Web dashboard,把训练 step / epoch 上的指标变化可视化出来。

在 Kubernetes 部署中,TensorBoard 通常作为一个单独的 Deployment / Pod 运行,并挂载与训练任务相同的持久卷,这样数据科学家在 job 运行期间就可以实时观察训练趋势。
不过,TensorBoard 更擅长看单次训练 run 内部的曲线;如果你要对比多个 run,通常还需要借助 MLflowWeights & Biases 之类工具。

Job 级指标(Job-level metrics)

如果你使用的是 Kubeflow Trainer Operator,那么对整个 job 的跟踪会容易很多。
它会自动跟踪:

  • worker Pod 状态
  • replica 数量
  • job 的状态条件,例如 CreatedRunningSucceededFailed

一个 job 实际上由多个 Pod 组成,而这些 Pod 本身仍会产出普通指标,可以导入 Prometheus。
同时,训练 job 生命周期中的关键事件,也会以 Kubernetes Event 的形式暴露出来。

跨分布式 worker 的日志管理

分布式训练任务的日志管理会更复杂,因为真正有意义的日志分析,通常要求你把多个并行 worker Pod 的日志关联在一起看。
这些 worker 很可能在相近时间产生几乎相同的日志信息,因此如果不加整理,就会非常混乱。

最直接的方法,是使用一个集中式日志系统,把所有 Pod 日志统一收集到 Elasticsearch、Loki 或 CloudWatch Logs 之类的平台中,并用:

  • job name
  • worker rank
  • pod name

等字段作为标签,以便过滤和关联分析。

在 PyTorch 分布式训练里,一个常见实践是:
只让 rank-0 输出详细训练日志,例如:

  • epoch 进度
  • loss 值
  • checkpoint 操作

而其他 worker 要么完全 suppress 输出,要么只在出错时打日志,这样能显著降低日志量并避免重复。
这种方式通常已经足够让你理解训练内部过程。
但在实际排查分布式训练失败时,往往仍然需要查看所有 worker 的日志,以确定究竟是哪一个 rank 出现问题。
因此,最佳实践通常是:

  • 所有 worker 的日志都收集起来
  • 但默认视图只展示 rank-0
  • 真正排障时再切换去看其他 rank

在规模变大之后,日志量本身就会成为主要挑战。
对多任务的大量原始日志直接做全文搜索,是不可持续的。
因此,结构化日志变得非常关键:训练代码最好输出 JSON 格式日志,并包含统一字段,例如:

  • job_name
  • worker_rank
  • step_number
  • epoch
  • loss_value

然后 Kubernetes 的日志系统(如 Fluent Bit、Fluentd)就可以解析这些结构化日志,并结合 Pod label / annotation 自动补充:

  • namespace
  • node name
  • GPU device ID

等元数据,从而形成可查询的完整日志记录。

分布式训练操作的 tracing

在某些情况下,分布式 tracing 对于找出与协同或通信模式相关的瓶颈是很有帮助的。

PyTorch 自带 PyTorch Profiler(torch.profiler ,可以对训练代码进行性能分析,捕获:

  • CPU 操作
  • GPU kernel 执行
  • 内存分配
  • 以及最关键的:分布式训练中的 collective communication 操作(如 all-reduce / all-gather)

Profiler 的结果可以通过 TensorBoard 的 profiling plug-in 来可视化。
它会提供:

  • GPU 利用率时间线
  • 带调用栈的性能热点
  • 跨不同 rank 的分布式通信视图

这能帮助数据科学家优化诸如:

  • batch size
  • gradient accumulation 策略
  • 网络瓶颈

等问题。

如果需要进一步从 CUDA kernel 级别分析 GPU 工作负载,还可以使用 NVIDIA Nsight Systems
不过,这类分析通常属于特定性能优化工作,而不是日常监控,因为:

  • trace 文件很容易达到 GB 级
  • profiling 本身会对训练性能产生额外开销

经验总结(Lessons Learned)

本章中,我们探讨了在 Kubernetes 上运行生产级 AI 训练工作负载所需的运维基础:从调度、网络,到存储和安全。

要运营一个生产级 AI 训练平台,必须采用与传统无状态应用部署完全不同的思路。
网络要求必须在集群规划阶段就被纳入考虑,而且网络方案通常和 GPU 型号、互联拓扑紧密相关。

Gang schedulingtopology-aware scheduling 已经不是“锦上添花”的优化,而是刚需。
因为默认调度器按 Pod 独立调度的模型,会在分布式训练中制造资源碎片,使任务只能获得部分资源,从而浪费昂贵 GPU。

安全与存储 也是两项无法在部署完成后“补上去”的基础能力。
存储通常需要分层设计:

  • 对象存储用于数据集
  • 分布式文件系统用于支持 RWX 的共享 checkpoint
  • 可选地,本地 NVMe 用于数据预热

平台管理员应当以“基础设施即产品”的方式来看待训练平台设计。
衡量平台成功与否的标准,不应只是基础设施 uptime,而应是:

  • 训练任务成功率
  • time-to-result

本章讨论的调度、安全、存储和可观测性选择,最终共同定义了数据科学家的开发体验。
应把他们视作平台的“客户”:他们工作流的效率,会直接影响组织迭代模型、并把 AI 能力交付到生产中的速度。

至此,第三部分结束。
现在,随着模型调优基础设施已经就位,你已经可以在生产规模上定制 foundation model,并运行训练工作负载。
下一部分将把视角从“模型”转向“应用”:我们将讨论,如何构建完整的 AI 驱动系统,把 LLM、向量数据库、工具调用以及 agentic workflow 一起编排起来。