释放超级算力:深入解析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-managerkube-scheduler中新增的内置逻辑,共同负责ResourceClaim的创建、分配和与Pod的绑定。它们拥有全局视角,能够进行智能的调度决策。
    • DRA Kubelet插件:由硬件厂商(如NVIDIA)提供,运行在每个节点上。它负责与本节点的硬件直接交互,向上报告资源拓扑(ResourceSlice),并向下响应Kubelet的指令,执行具体的资源准备和清理工作。
  • 结构化参数:让资源请求更富有表现力
    DRA引入了一系列新的API对象:
    • ResourceClass: 定义资源的“类型”,例如"nvidia-imex-computedomain"
    • ResourceClaim: Pod对资源的具体请求。其spec中可以直接包含结构化的参数,或者通过CEL表达式来筛选具有特定属性的设备。这取代了早期设计中的ClaimParameters对象。
    • ResourceClaimTemplate: 在Pod模板中定义,可以为每个Pod实例自动创建专属的ResourceClaim
  • 新版DRA工作流概览
    1. DRA Kubelet插件在每个节点上启动,以ResourceSlice的形式将可分配的资源上报给API Server。
    2. DRA Kubelet插件或集群管理员创建ResourceClassResourceClaimTemplate,定义一些特定的资源。
    3. 用户创建一个Pod,其spec中包含一个ResourceClaimTemplate
    4. kube-controller-manager监听到后,会根据模板为这个Pod创建一个对应的ResourceClaim对象。
    5. kube-scheduler在调度Pod时,其内置的DRA插件会分析Pod的需求,并从可用的ResourceSlice中寻找能满足ResourceClaim的资源。
    6. 一旦找到,调度器就做出分配决策,并将包含具体设备名、节点名等信息的AllocationResult写入ResourceClaimstatus中,然后将Pod调度到该节点。
    7. 目标节点上的kubelet看到Pod已绑定了具体的资源分配,于是调用本地的DRA Kubelet插件的NodePrepareResources接口。
    8. DRA Kubelet插件插件读取ResourceClaim中的分配结果,生成CDI描述文件,完成GPU的最终准备。
    9. kubelet使用CDI文件中的信息,创建并启动容器。

​ 在此基础上,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的核心是ComputeDomainSpecComputeDomainStatus

// 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的定义中我们可以看到两个关键点:

  1. NumNodes: 这个字段直接揭示了ComputeDomain的本质——它是一个跨节点的(Multi-Node)抽象。用户通过这个字段声明”我需要一个由N个节点组成的计算域”。这与传统Device Plugin只能在单节点内分配资源的做法形成了鲜明对比。
  2. 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:

  1. 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>"
  1. 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

调度过程的关键步骤

  1. kube-scheduler 调度:Pod 被调度到具有足够资源的节点(注意:调度器不知道 CliqueID

  2. kubelet 调用 DRA 插件:在目标节点上,kubelet 调用 compute-domain-kubelet-plugin 的 NodePrepareResources 接口(在插件内部对应 nodePrepareResource 函数)

  3. 添加节点标签: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>"
  1. 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,这导致了以下问题:

  1. 盲调度:Pod 可能被调度到具有不同 CliqueID 的节点上
  2. 被动组合:ComputeDomain 的节点组成依赖 Pod 调度结果,而非拓扑规划
  3. 状态误导: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 系统,建议采用以下部署模式:

基础设施预规划
  1. 节点预标签:管理员为具有相同 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
  2. 工作负载亲和性约束

    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 部署,建议:

  1. 采用基础设施预规划模式,通过节点标签和亲和性约束确保拓扑一致性
  2. 实施监控和验证机制,在运行时检查 ComputeDomain 的拓扑一致性
  3. 关注项目演进,NVIDIA 可能会在未来版本中改进调度器的拓扑感知能力