释放超级算力:深入解析NVIDIA IMEX在Kubernetes上的实现
随着GB200等超级芯片的问世,单节点内的计算和内存能力达到了前所未有的高度。然而,如何让云原生应用、特别是AI训练任务,充分利用这种“Scale-Up”架构的全部潜力?本文将探讨NVIDIA的IMEX(In-Memory Execution)技术,以及它如何通过Kubernetes最新的动态资源分配(DRA)框架和ComputeDomain这一核心抽象,在容器化环境中得以实现。
1. 背景
1.1 从多GPU到超级节点(Supernode)
为了适应越来越大的模型,我们通过在服务器上集成多块GPU卡(“Scale-Up”)或连接多个GPU服务器(“Scale-Out”)来扩展计算能力。NVIDIA的Grace Blackwell (GB200)和Grace Hopper (GH200)架构正是Scale-Up路线的体现,它们不再是简单地将多个GPU放置在同一主板上,而是通过超高速的NVLink-C2C (Chip-to-Chip) 互联技术,将多个GPU和CPU(在GH200中为Grace CPU)紧密耦合,形成一个拥有统一、高带宽、缓存一致性内存空间的“超级节点”(Supernode)。
在这个架构中,所有GPU共享一个庞大的内存池。对于运行在其中的应用程序来说,这块巨大的内存就像是本地显存一样,可以被直接、高速地访问。这种设计从物理上消除了传统多卡系统中因数据在不同GPU显存间移动而产生的性能开销,为更大规模的模型训练和推理铺平了道路。
1.2 性能瓶颈与IMEX
传统的GPU计算模式通常遵循“数据拷贝 -> 计算 -> 结果拷贝”的流程。应用程序首先需要将数据从CPU主存(RAM)拷贝到GPU显存(VRAM),GPU完成计算后,再将结果拷回主存。当数据量巨大时,这个拷贝过程会消耗大量时间,形成所谓的“数据重力”或“内存墙”问题。
从更深层次的计算机体系结构来看,这种模式遵循的是**消息语义 (Message Semantics)**。每个GPU都像一个独立的计算岛屿,拥有自己的私有内存。当需要与其他GPU协作时,程序员必须编写代码,像发送网络数据包一样,显式地将数据从一个GPU的内存“打包”发送到另一个GPU的内存。这种方式不仅带来了巨大的性能开销,更让多卡编程变得异常复杂,开发者需要耗费大量精力来管理数据同步、通信和一致性。
为了从根本上解决这个问题,我们需要一种更自然的编程模型。这就是**内存语义 (Memory Semantics)**。在这种模型下,所有GPU共享一个统一的、可共同寻址的内存空间。硬件本身通过高速互联(如NVLink)和缓存一致性协议,确保任何一个GPU对内存的修改,都能被其他GPU“看到”,就如同所有人都在使用同一块无限大的本地内存。这极大地简化了编程,让开发者可以专注于算法本身,而非繁琐的数据搬运。
IMEX (In-Memory Execution) 正是建立在由GB200等硬件提供的强大内存语义之上的技术。它允许应用程序的数据和计算任务始终在统一的GPU内存池中执行。数据从加载、预处理到模型计算的全过程,都无需在不同的内存空间之间进行显式拷贝。
IMEX的主要特点为:
- 极致性能:通过消除基于“消息语义”的数据拷贝,最大化利用NVLink的高带宽,显著提升计算效率。
- 超大容量:应用程序可用的内存不再受限于单张GPU卡的显存大小,而是整个超级节点提供的庞大内存池,使得运行万亿参数级别的巨型模型成为可能。
- 简化编程:开发者从繁琐的数据同步管理中解放出来,可以像在单机上一样,采用更自然、更高效的“内存语义”模型进行编程。
我们如何在一个动态、弹性的云原生环境中,智能、高效地调度和管理这种新型的计算资源?答案,就在于Kubernetes的DRA。
1.3 Kubernetes的演进:从Device Plugin到DRA
Kubernetes最初通过Device Plugin框架来支持GPU等硬件设备,但Device Plugin具有一定的限制:
- 它只能上报设备数量(
nvidia.com/gpu: 8),无法描述设备间的拓扑关系。 - 请求方式单一,无法表达“我需要两组通过NVLink相连的GPU”这样复杂的约束。
对于GB200这样的超级节点,Device Plugin显然已力不从心。我们需要一种更强大、更具表现力的资源管理框架。于是,DRA (Dynamic Resource Allocation) 应运而生。
DRA是Kubernetes为应对下一代复杂硬件而设计的全新框架。根据其最新的设计(KEP-4381),它通过将核心调度逻辑深度整合进Kubernetes,并定义清晰的驱动插件接口,完美地解决了Device Plugin的局限性。
- 全新的和谐:Kubernetes控制平面与Kubelet插件的协同
在最新的DRA设计中,核心的分配和调度逻辑由Kubernetes的核心组件自身承担,而非一个独立的驱动控制器(DRA的Alpha版本中,DRA Controller需要负责协助调度)。这种架构的协同关系体现在:- Kubernetes控制平面:
kube-controller-manager和kube-scheduler中新增的内置逻辑,共同负责ResourceClaim的创建、分配和与Pod的绑定。它们拥有全局视角,能够进行智能的调度决策。 - DRA Kubelet插件:由硬件厂商(如NVIDIA)提供,运行在每个节点上。它负责与本节点的硬件直接交互,向上报告资源拓扑(
ResourceSlice),并向下响应Kubelet的指令,执行具体的资源准备和清理工作。
- Kubernetes控制平面:
- 结构化参数:让资源请求更富有表现力
DRA引入了一系列新的API对象:-
ResourceClass: 定义资源的“类型”,例如"nvidia-imex-computedomain"。 -
ResourceClaim: Pod对资源的具体请求。其spec中可以直接包含结构化的参数,或者通过CEL表达式来筛选具有特定属性的设备。这取代了早期设计中的ClaimParameters对象。 -
ResourceClaimTemplate: 在Pod模板中定义,可以为每个Pod实例自动创建专属的ResourceClaim。
-
- 新版DRA工作流概览
- DRA Kubelet插件在每个节点上启动,以
ResourceSlice的形式将可分配的资源上报给API Server。 - DRA Kubelet插件或集群管理员创建
ResourceClass与ResourceClaimTemplate,定义一些特定的资源。 - 用户创建一个Pod,其
spec中包含一个ResourceClaimTemplate。 -
kube-controller-manager监听到后,会根据模板为这个Pod创建一个对应的ResourceClaim对象。 -
kube-scheduler在调度Pod时,其内置的DRA插件会分析Pod的需求,并从可用的ResourceSlice中寻找能满足ResourceClaim的资源。 - 一旦找到,调度器就做出分配决策,并将包含具体设备名、节点名等信息的
AllocationResult写入ResourceClaim的status中,然后将Pod调度到该节点。 - 目标节点上的
kubelet看到Pod已绑定了具体的资源分配,于是调用本地的DRA Kubelet插件的NodePrepareResources接口。 - DRA Kubelet插件插件读取
ResourceClaim中的分配结果,生成CDI描述文件,完成GPU的最终准备。 -
kubelet使用CDI文件中的信息,创建并启动容器。
- DRA Kubelet插件在每个节点上启动,以
在此基础上,NVIDIA DRA通过抽象出ComputeDomain,实现了GB200超节点中IMEX的使用。
2.ComputeDomain的设计
在理解了DRA框架如何为我们提供管理复杂资源的“能力”之后,我们必须深入探讨NVIDIA DRA驱动中的ComputeDomain,它是多节点、紧耦合的一组硬件(如GB200超级节点)在Kubernetes中的建模。
ComputeDomain是一个自定义资源定义(CRD),它为集群管理员和用户提供了一个声明和管理多节点计算域的接口,其API定义位于api/nvidia.com/resource/v1beta1/computedomain.go。
2.1 ComputeDomain的定义
首先,ComputeDomain CRD的核心是ComputeDomainSpec和ComputeDomainStatus。
// ComputeDomain prepares a set of nodes to run a multi-node workload in.
type ComputeDomain struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec ComputeDomainSpec `json:"spec,omitempty"`
Status ComputeDomainStatus `json:"status,omitempty"`
}
// ComputeDomainSpec provides the spec for a ComputeDomain.
type ComputeDomainSpec struct {
NumNodes int `json:"numNodes"`
Channel *ComputeDomainChannelSpec `json:"channel"`
}
// ComputeDomainChannelSpec provides the spec for a channel used to run a workload inside a ComputeDomain.
type ComputeDomainChannelSpec struct {
ResourceClaimTemplate ComputeDomainResourceClaimTemplate `json:"resourceClaimTemplate"`
// Allows for requesting all IMEX channels (the maximum per IMEX domain) or
// precisely one.
AllocationMode string `json:"allocationMode"`
}从Spec的定义中我们可以看到两个关键点:
-
NumNodes: 这个字段直接揭示了ComputeDomain的本质——它是一个跨节点的(Multi-Node)抽象。用户通过这个字段声明”我需要一个由N个节点组成的计算域”。这与传统Device Plugin只能在单节点内分配资源的做法形成了鲜明对比。 -
Channel: 这个字段指向ComputeDomainChannelSpec,它内部定义了用于从此ComputeDomain中申请资源的ResourceClaimTemplate的名称。这说明ComputeDomain本身并不直接被Pod消费,而是作为一个”资源池”被创建和准备。当这个资源池状态就绪后,用户需要通过特定的资源声明模板(ResourceClaimTemplate)来从中获取具体的计算资源。这是一种”准备-使用”分离的清晰设计。
2.2 ComputeDomain 状态与节点管理
ComputeDomainStatus 描述了当前域的状态和节点信息:
// ComputeDomainStatus represents the observed state of a ComputeDomain.
type ComputeDomainStatus struct {
Nodes []*ComputeDomainNode `json:"nodes"`
// Status indicates the status of the ComputeDomain. It can be "Ready" or "NotReady".
Status string `json:"status"`
}
// ComputeDomainNode represents a node in a ComputeDomain.
type ComputeDomainNode struct {
Name string `json:"name"`
Index int `json:"index"`
IPAddress string `json:"ipAddress"`
CliqueID string `json:"cliqueID"`
}每个节点都包含 CliqueID 字段,这个标识符对于理解 ComputeDomain 的拓扑一致性至关重要。
3. ComputeDomain 工作流程详解
基于对 NVIDIA DRA Driver 代码的深入分析,我们可以详细追踪 ComputeDomain 的完整工作流程:
3.1 用户创建 ComputeDomain
管理员首先创建 ComputeDomain CRD:
apiVersion: resource.nvidia.com/v1beta1
kind: ComputeDomain
metadata:
name: my-compute-domain
spec:
numNodes: 4
channel:
allocationMode: "shared"3.2 控制器处理与资源模板创建
compute-domain-controller 监听到 ComputeDomain 创建事件后,立即启动资源准备流程:
控制器会创建两种关键的 ResourceClaimTemplate:
- DaemonSet ResourceClaimTemplate:
apiVersion: resource.k8s.io/v1
kind: ResourceClaimTemplate
metadata:
generateName: my-compute-domain-daemon-claim-template-
labels:
resource.nvidia.com/computeDomain: "<compute-domain-uid>"
resource.nvidia.com/computeDomainTarget: "Daemon"
spec:
spec:
devices:
requests:
- name: daemon
exactly:
deviceClassName: compute-domain-daemon.nvidia.com
config:
- requests: ["daemon"]
opaque:
driver: compute-domain.nvidia.com
parameters:
apiVersion: resource.nvidia.com/v1beta1
kind: ComputeDomainDaemonConfig
domainID: "<compute-domain-uid>"- Workload ResourceClaimTemplate:
apiVersion: resource.k8s.io/v1
kind: ResourceClaimTemplate
metadata:
name: my-compute-domain-workload-claim-template
labels:
resource.nvidia.com/computeDomain: "<compute-domain-uid>"
resource.nvidia.com/computeDomainTarget: "Workload"
spec:
spec:
devices:
requests:
- name: channel
exactly:
deviceClassName: compute-domain-default-channel.nvidia.com
config:
- requests: ["channel"]
opaque:
driver: compute-domain.nvidia.com
parameters:
apiVersion: resource.nvidia.com/v1beta1
kind: ComputeDomainChannelConfig
domainID: "<compute-domain-uid>"
allocationMode: "shared"DaemonSet 创建
控制器接着创建用于管理 ComputeDomain 的 DaemonSet:
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: compute-domain-imex-daemon-<uid>
spec:
selector:
matchLabels:
resource.nvidia.com/computeDomain: "<compute-domain-uid>"
template:
spec:
nodeSelector:
resource.nvidia.com/computeDomain: "<compute-domain-uid>"
containers:
- name: imex-daemon
image: nvcr.io/nvidia/k8s-dra-driver-gpu:v25.8.0-dev
command: ["compute-domain-daemon"]关键设计点:DaemonSet 使用 nodeSelector 确保只运行在具有特定 ComputeDomain 标签的节点上。
具体的,在compute-domain-controller的代码层面,包括:
控制器事件处理机制
控制器的核心事件处理逻辑在 cmd/compute-domain-controller/computedomain.go:229-286:
func (m *ComputeDomainManager) onAddOrUpdate(ctx context.Context, obj any) {
cd, ok := obj.(*nvapi.ComputeDomain)
if !ok {
return
}
if cd.ObjectMeta.DeletionTimestamp != nil {
klog.Infof("ComputeDomain %s is being deleted, skipping reconcile", cd.Name)
return
}
if cd.Status.Status == nvapi.ComputeDomainStatusReady {
klog.Infof("ComputeDomain %s is already Ready, skipping reconcile", cd.Name)
return
}
// 创建 DaemonSet ResourceClaimTemplate
rct, err := m.daemonSetManager.Create(ctx, cd)
if err != nil {
klog.Errorf("error creating DaemonSet: %w", err)
return
}
// 创建 Workload ResourceClaimTemplate
rct, err = m.workloadResourceClaimTemplateManager.Create(ctx, cd.Namespace, cd.Name, cd)
if err != nil {
klog.Errorf("error creating Workload ResourceClaimTemplate: %w", err)
return
}
}这个处理函数实现了几个关键机制:
- 防重复处理:检查 DeletionTimestamp 和 Ready 状态避免重复操作
- 顺序创建:先创建 DaemonSet 相关资源,再创建工作负载相关资源
- 错误隔离:两种 ResourceClaimTemplate 创建失败不会相互影响
DaemonSet 管理器的创建流程
DaemonSetManager.Create 方法在 cmd/compute-domain-controller/daemonset.go:434-496 实现了完整的 DaemonSet 创建流程:
func (m *DaemonSetManager) Create(ctx context.Context, cd *nvapi.ComputeDomain) (*appsv1.DaemonSet, error) {
// 1. 检查 DaemonSet 是否已存在
ds, err := getByComputeDomainUID[*appsv1.DaemonSet](ctx, m.mutationCache, string(cd.UID))
if err != nil {
return nil, fmt.Errorf("error retrieving DaemonSet: %w", err)
}
if len(ds) == 1 {
return ds[0], nil // 已存在,直接返回
}
// 2. 创建 DaemonSet ResourceClaimTemplate
rct, err := m.resourceClaimTemplateManager.Create(ctx, cd)
if err != nil {
return nil, fmt.Errorf("error creating ResourceClaimTemplate: %w", err)
}
// 3. 构建 DaemonSet 模板数据
templateData := DaemonSetTemplateData{
Namespace: m.config.driverNamespace,
GenerateName: fmt.Sprintf("%s-", cd.Name),
Finalizer: computeDomainFinalizer,
ComputeDomainLabelKey: computeDomainLabelKey, // "resource.nvidia.com/computeDomain"
ComputeDomainLabelValue: cd.UID, // 使用 ComputeDomain UID 作为标签值
ResourceClaimTemplateName: rct.Name,
ImageName: m.config.imageName,
MaxNodesPerIMEXDomain: m.config.maxNodesPerIMEXDomain,
FeatureGates: featuregates.ToMap(),
}
// 4. 渲染 YAML 模板并创建 DaemonSet
tmpl, err := template.ParseFiles(DaemonSetTemplatePath)
if err != nil {
return nil, fmt.Errorf("failed to parse template file: %w", err)
}
var daemonSetYaml bytes.Buffer
if err := tmpl.Execute(&daemonSetYaml, templateData); err != nil {
return nil, fmt.Errorf("failed to execute template: %w", err)
}
// 5. YAML 到结构化对象的转换
var unstructuredObj unstructured.Unstructured
err = yaml.Unmarshal(daemonSetYaml.Bytes(), &unstructuredObj)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal yaml: %w", err)
}
var daemonSet appsv1.DaemonSet
err = runtime.DefaultUnstructuredConverter.FromUnstructured(unstructuredObj.UnstructuredContent(), &daemonSet)
if err != nil {
return nil, fmt.Errorf("failed to convert unstructured data to typed object: %w", err)
}
// 6. 创建 DaemonSet 并更新缓存
d, err := m.config.clientsets.Core.AppsV1().DaemonSets(daemonSet.Namespace).Create(ctx, &daemonSet, metav1.CreateOptions{})
if err != nil {
return nil, fmt.Errorf("error creating DaemonSet: %w", err)
}
// 7. 立即更新本地缓存以避免重复创建
m.mutationCache.Mutation(d)
return d, nil
}ResourceClaimTemplate 管理器的创建逻辑
DaemonSet ResourceClaimTemplate 的创建在 cmd/compute-domain-controller/resourceclaimtemplate.go:502-536:
func (m *DaemonSetResourceClaimTemplateManager) Create(ctx context.Context, cd *nvapi.ComputeDomain) (*resourceapi.ResourceClaimTemplate, error) {
// 1. 检查是否已存在
rcts, err := getByComputeDomainUID[*resourceapi.ResourceClaimTemplate](ctx, m.mutationCache, string(cd.UID))
if err != nil {
return nil, fmt.Errorf("error retrieving ResourceClaimTemplate: %w", err)
}
if len(rcts) > 1 {
return nil, fmt.Errorf("more than one ResourceClaimTemplate found with same ComputeDomain UID")
}
if len(rcts) == 1 {
return rcts[0], nil // 已存在
}
// 2. 构建配置参数
daemonConfig := nvapi.DefaultComputeDomainDaemonConfig()
daemonConfig.DomainID = string(cd.UID)
templateData := ResourceClaimTemplateTemplateData{
Namespace: m.config.driverNamespace,
GenerateName: fmt.Sprintf("%s-daemon-claim-template-", cd.Name),
Finalizer: computeDomainFinalizer,
ComputeDomainLabelKey: computeDomainLabelKey,
ComputeDomainLabelValue: cd.UID,
TargetLabelKey: computeDomainResourceClaimTemplateTargetLabelKey,
TargetLabelValue: computeDomainResourceClaimTemplateTargetDaemon,
DeviceClassName: computeDomainDaemonDeviceClass,
DriverName: DriverName,
DaemonConfig: daemonConfig,
}
// 3. 调用基类创建方法
rct, err := m.BaseResourceClaimTemplateManager.Create(ctx, DaemonSetResourceClaimTemplateTemplatePath, &templateData)
if err != nil {
return nil, fmt.Errorf("error creating ResourceClaimTemplate from base: %w", err)
}
return rct, nil
}Workload ResourceClaimTemplate 的创建在 cmd/compute-domain-controller/resourceclaimtemplate.go:591-626:
func (m *WorkloadResourceClaimTemplateManager) Create(ctx context.Context, namespace, name string, cd *nvapi.ComputeDomain) (*resourceapi.ResourceClaimTemplate, error) {
// 1. 检查是否已存在(与 DaemonSet 版本相同的逻辑)
rcts, err := getByComputeDomainUID[*resourceapi.ResourceClaimTemplate](ctx, m.mutationCache, string(cd.UID))
if err != nil {
return nil, fmt.Errorf("error retrieving ResourceClaimTemplate: %w", err)
}
if len(rcts) > 1 {
return nil, fmt.Errorf("more than one ResourceClaimTemplate found with same ComputeDomain UID")
}
if len(rcts) == 1 {
return rcts[0], nil // 已存在
}
// 2. 构建通道配置
channelConfig := nvapi.DefaultComputeDomainChannelConfig()
channelConfig.DomainID = string(cd.UID)
channelConfig.AllocationMode = cd.Spec.Channel.AllocationMode // 关键:继承 ComputeDomain 的分配模式
templateData := ResourceClaimTemplateTemplateData{
Namespace: namespace, // 用户指定的命名空间
Name: name, // 用户指定的名称
Finalizer: computeDomainFinalizer,
ComputeDomainLabelKey: computeDomainLabelKey,
ComputeDomainLabelValue: cd.UID,
TargetLabelKey: computeDomainResourceClaimTemplateTargetLabelKey,
TargetLabelValue: computeDomainResourceClaimTemplateTargetWorkload,
DeviceClassName: computeDomainDefaultChannelDeviceClass,
DriverName: DriverName,
ChannelConfig: channelConfig,
}
// 3. 调用基类创建方法
rct, err := m.BaseResourceClaimTemplateManager.Create(ctx, WorkloadResourceClaimTemplateTemplatePath, &templateData)
if err != nil {
return nil, fmt.Errorf("error creating ResourceClaimTemplate from base: %w", err)
}
return rct, nil
}模板渲染机制
控制器使用 Go 模板引擎来生成 YAML 配置。以 DaemonSet 模板为例,templates/compute-domain-daemon.tmpl.yaml 中的关键部分:
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: compute-domain-imex-daemon-{{ .ComputeDomainLabelValue }}
namespace: {{ .Namespace }}
spec:
selector:
matchLabels:
{{ .ComputeDomainLabelKey }}: {{ .ComputeDomainLabelValue }}
template:
metadata:
labels:
{{ .ComputeDomainLabelKey }}: {{ .ComputeDomainLabelValue }}
spec:
nodeSelector:
{{ .ComputeDomainLabelKey }}: {{ .ComputeDomainLabelValue }} # 关键:节点选择器
containers:
- name: compute-domain-daemon
image: {{ .ImageName }}
command: ["compute-domain-daemon", "run"]
env:
- name: MAX_NODES_PER_IMEX_DOMAIN
value: "{{ .MaxNodesPerIMEXDomain }}"
resources:
claims:
- name: compute-domain-daemon这个模板渲染机制实现了几个关键设计:
- 动态标签匹配:使用 ComputeDomain UID 作为标签值,确保精确匹配
- 节点选择器约束:DaemonSet 只调度到具有正确标签的节点
- 配置传递:通过环境变量传递关键配置参数
3.3 工作负载调度与节点标签机制
当用户创建需要 ComputeDomain 资源的工作负载时:
apiVersion: v1
kind: Pod
metadata:
name: gpu-workload
spec:
resourceClaims:
- name: imex-channel
source:
resourceClaimTemplateName: my-compute-domain-workload-claim-template
containers:
- name: gpu-container
resources:
claims:
- name: imex-channel调度过程的关键步骤:
kube-scheduler 调度:Pod 被调度到具有足够资源的节点(注意:调度器不知道 CliqueID)
kubelet 调用 DRA 插件:在目标节点上,kubelet 调用 compute-domain-kubelet-plugin 的
NodePrepareResources接口(在插件内部对应nodePrepareResource函数)添加节点标签:DRA 插件执行关键操作(
cmd/compute-domain-kubelet-plugin/device_state.go:429):if err := s.computeDomainManager.AddNodeLabel(ctx, config.DomainID); err != nil { return nil, fmt.Errorf("error adding Node label for ComputeDomain: %w", err) }
这会为节点添加标签:
metadata:
labels:
resource.nvidia.com/computeDomain: "<compute-domain-uid>"- DaemonSet 调度触发:节点标签添加后,DaemonSet 的 nodeSelector 匹配成功,compute-domain-daemon Pod 被调度到该节点
3.4 IMEX 守护进程与域管理
compute-domain-daemon 启动后执行以下关键任务:
节点注册与状态同步
守护进程通过 Kubernetes Informer 监控 ComputeDomain 状态,并将本节点信息注册到 ComputeDomain 的 Status 中:
// 在 cmd/compute-domain-daemon/computedomain.go 中
nodeInfo = &nvapi.ComputeDomainNode{
Name: m.config.nodeName,
CliqueID: m.config.cliqueID, // 关键:包含本节点的 CliqueID
Index: nextIndex,
}DNS 名称管理
守护进程为 ComputeDomain 中的每个节点生成标准的 DNS 名称,用于 IMEX 通信:
// 在 cmd/compute-domain-daemon/dnsnames.go 中
func (m *DNSNameManager) GetComputeDomainDNSNames(cd *nvapi.ComputeDomain) ([]string, error) {
var cliqueNodes []*nvapi.ComputeDomainNode
for _, node := range cd.Status.Nodes {
if node.CliqueID == m.cliqueID {
cliqueNodes = append(cliqueNodes, node) // 只处理相同 CliqueID 的节点
}
}
// 生成 DNS 名称...
}配置文件生成
守护进程生成 IMEX 配置文件,只包含相同 CliqueID 的节点信息:
// 在 cmd/compute-domain-daemon/main.go 中
for _, node := range cd.Status.Nodes {
if node.CliqueID == cliqueID {
// 将节点配置写入 IMEX 配置文件
}
}3.5 ComputeDomain 就绪状态判断
ComputeDomain 变为 Ready 的条件在 cmd/compute-domain-controller/daemonset.go:381:
if int(d.Status.NumberReady) != cd.Spec.NumNodes {
return nil // 节点数量不匹配,继续等待
}
newCD := cd.DeepCopy()
newCD.Status.Status = nvapi.ComputeDomainStatusReady关键发现:Ready 状态只检查 DaemonSet Pod 数量,不验证 CliqueID 一致性。
3.6 工作负载资源分配与 IMEX 通道注入
当 ComputeDomain Ready 后,工作负载的 DRA 分配继续进行:
在 cmd/compute-domain-kubelet-plugin/device_state.go:406-447 中:
func (s *DeviceState) applyComputeDomainChannelConfig(ctx context.Context, config *configapi.ComputeDomainChannelConfig, claim *resourceapi.ResourceClaim, results []*resourceapi.DeviceRequestAllocationResult) (*DeviceConfigState, error) {
// 1. 检查 ComputeDomain 命名空间
if err := s.computeDomainManager.AssertComputeDomainNamespace(ctx, claim.Namespace, config.DomainID); err != nil {
return nil, permanentError{fmt.Errorf("error asserting ComputeDomain's namespace: %w", err)}
}
// 2. 添加节点标签(如果尚未添加)
if err := s.computeDomainManager.AddNodeLabel(ctx, config.DomainID); err != nil {
return nil, fmt.Errorf("error adding Node label for ComputeDomain: %w", err)
}
// 3. 验证 ComputeDomain 就绪状态
if err := s.computeDomainManager.AssertComputeDomainReady(ctx, config.DomainID); err != nil {
return nil, fmt.Errorf("error asserting ComputeDomain Ready: %w", err)
}
// 4. 注入 IMEX 通道设备
if s.computeDomainManager.cliqueID == "" {
// 如果没有 CliqueID,不注入 IMEX 通道
return &configState, nil
}
for _, info := range s.nvdevlib.nvCapImexChanDevInfos[:chancount] {
configState.containerEdits = configState.containerEdits.Append(s.computeDomainManager.GetComputeDomainChannelContainerEdits(s.cdi.devRoot, info))
}
return &configState, nil
}4. CliqueID:拓扑一致性的挑战与解决方案
4.1 CliqueID 的定义与生成
CliqueID 是 NVIDIA GPU 硬件拓扑的唯一标识符,格式为 <ClusterUUID>.<CliqueId>,用于标识通过高速 NVLink 物理连接的节点组。
CliqueID 的生成逻辑在 cmd/compute-domain-kubelet-plugin/nvlib.go:178:
func (l deviceLib) getCliqueID() (string, error) {
if err := l.init(); err != nil {
return "", fmt.Errorf("error initializing deviceLib: %w", err)
}
defer l.alwaysShutdown()
uniqueClusterUUIDs := make(map[string]struct{})
uniqueCliqueIDs := make(map[string]struct{})
err := l.VisitDevices(func(i int, d nvdev.Device) error {
isFabricAttached, err := d.IsFabricAttached()
if err != nil {
return fmt.Errorf("error checking if device is fabric attached: %w", err)
}
if !isFabricAttached {
return nil
}
info, ret := d.GetGpuFabricInfo()
if ret != nvml.SUCCESS {
return fmt.Errorf("failed to get GPU fabric info: %w", ret)
}
clusterUUID, err := uuid.FromBytes(info.ClusterUuid[:])
if err != nil {
return fmt.Errorf("invalid cluster UUID: %w", err)
}
cliqueID := fmt.Sprintf("%d", info.CliqueId)
uniqueClusterUUIDs[clusterUUID.String()] = struct{}{}
uniqueCliqueIDs[cliqueID] = struct{}{}
return nil
})
if err != nil {
return "", fmt.Errorf("error getting fabric information from one or more devices: %w", err)
}
if len(uniqueClusterUUIDs) == 0 && len(uniqueCliqueIDs) == 0 {
return "", nil
}
if len(uniqueClusterUUIDs) != 1 {
return "", fmt.Errorf("unexpected number of unique ClusterUUIDs found on devices")
}
if len(uniqueCliqueIDs) != 1 {
return "", fmt.Errorf("unexpected number of unique CliqueIDs found on devices")
}
for clusterUUID := range uniqueClusterUUIDs {
for cliqueID := range uniqueCliqueIDs {
return fmt.Sprintf("%s.%s", clusterUUID, cliqueID), nil
}
}
return "", fmt.Errorf("unexpected return")
}4.2 当前实现的架构挑战
通过深入的代码分析,我们发现了一个重要的架构设计问题:
调度器盲区问题
kube-scheduler 在调度 Pod 时完全不知道 CliqueID,这导致了以下问题:
- 盲调度:Pod 可能被调度到具有不同 CliqueID 的节点上
- 被动组合:ComputeDomain 的节点组成依赖 Pod 调度结果,而非拓扑规划
- 状态误导:ComputeDomain “Ready” 状态仅反映 DaemonSet Pod 数量,不保证拓扑一致性
实际的节点选择机制
从代码分析可以看出,选择哪些节点加入 ComputeDomain 确实是由 k8s 调度器决定的:
用户创建 Pod → kube-scheduler 盲调度 → kubelet 调用 DRA 插件 → 添加节点标签 → DaemonSet 调度这种机制对于 GB200 这样的关键设备系统存在可靠性隐患。
4.3 系统的容错机制
尽管存在设计挑战,系统实现了多层容错机制:
运行时过滤
在 cmd/compute-domain-daemon/dnsnames.go:71 中,守护进程只与相同 CliqueID 的节点通信:
if node.CliqueID == m.cliqueID {
cliqueNodes = append(cliqueNodes, node)
}配置隔离
在 cmd/compute-domain-daemon/main.go:388 中,只配置相同 CliqueID 的节点:
if node.CliqueID == cliqueID {
// 将节点写入 IMEX 配置文件
}失败回退
如果 ComputeDomain 中的节点 CliqueID 不一致,IMEX 通信会在运行时失败,但不会影响系统稳定性。
4.4 推荐的部署模式
对于 GB200 系统,建议采用以下部署模式:
基础设施预规划
节点预标签:管理员为具有相同 CliqueID 的节点添加统一标签:
kubectl label node gb200-node-1 nvidia.com/gpu.clique=cluster1-clique0 kubectl label node gb200-node-2 nvidia.com/gpu.clique=cluster1-clique0工作负载亲和性约束:
apiVersion: v1 kind: Pod metadata: name: gb200-workload spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: nvidia.com/gpu.clique operator: In values: ["cluster1-clique0"] containers: - name: workload resources: claims: - name: compute-domain-channel
ComputeDomain 对应 GB200 超级节点
理想情况下,一个 ComputeDomain 应该对应一个预先规划的 GB200 超级节点,而不是依赖动态的节点发现。
5. 总结与展望
NVIDIA DRA ComputeDomain 为在 Kubernetes 中管理 GB200 超级节点提供了强大的基础设施。通过 DRA 框架,它实现了跨节点资源的统一管理和动态分配。
然而,当前的实现也存在一些架构挑战,特别是在 CliqueID 拓扑一致性保证方面。对于生产环境中的 GB200 部署,建议:
- 采用基础设施预规划模式,通过节点标签和亲和性约束确保拓扑一致性
- 实施监控和验证机制,在运行时检查 ComputeDomain 的拓扑一致性
- 关注项目演进,NVIDIA 可能会在未来版本中改进调度器的拓扑感知能力