GPU 资源池化的终局推演:统一调度还是按模型分集群

一、百卡集群已经管不过来了,千卡之后呢

当前大多数 AI 平台的 GPU 管理方式仍然是静态分区——按业务线、按团队、按模型划分 GPU 节点池,池之间资源不互通。训练团队用 H100 节点组,推理团队用 A100 节点组,偶尔借调但流程繁琐。这种模式在百卡以下勉强运作,但当集群规模达到千卡时,静态分区的资源碎片会成为系统性的低效。

静态分区的问题在于:各池的峰值时间不同。训练任务集中在工作日白天,推理服务的高峰在晚间。如果两个池不能动态共享空闲 GPU,总利用率会被拉低到一个尴尬的数字。更糟的是,当一个池资源耗尽而另一个池大量闲置时,扩容只能等采购新硬件,而非复用已有资源。

GPU 资源池化(Resource Pooling)试图解决这个问题:将所有 GPU 纳入统一调度,任务不再关心物理 GPU 在哪,只声明需求(型号、数量、显存、带宽),调度系统按需分配。听起来是理想架构,但工程的魔鬼在细节。基础设施不需要漂亮话,统一调度的代价是你失去了"知道 GPU 在哪"的确定性。

二、两种池化架构的对比:集中调度 vs 分区自治

graph TB
    subgraph "方案 A: 统一调度池"
        US_Scheduler["统一调度器<br/>(Volcano/Yunikorn/Kueue)"]
        
        US_Pool["全局 GPU 资源池<br/>H100 + A100 + A40 混合"]
        
        Sub_Queue1["训练队列<br/>优先级: High<br/>配额: 100 GPU"]
        Sub_Queue2["推理队列<br/>优先级: Medium<br/>配额: 50 GPU"]
        Sub_Queue3["批处理队列<br/>优先级: Low<br/>配额: 30 GPU"]
        
        US_Scheduler --> US_Pool
        US_Pool --> Sub_Queue1
        US_Pool --> Sub_Queue2
        US_Pool --> Sub_Queue3
        
        US_Pro["优势: 全局最优分配<br/>劣势: 调度延迟较高<br/>跨型号调度复杂度 O(n²)"]
    end

    subgraph "方案 B: 分区自治 + 联邦"
        Fed["联邦调度层<br/>负载感知路由 + 跨区迁移"]
        
        Zone1["Zone A: H100 集群<br/>本地 Volcano 调度<br/>8 节点 × 8 GPU"]
        Zone2["Zone B: A100 推理集群<br/>本地 default-scheduler<br/>16 节点 × 8 GPU"]
        Zone3["Zone C: A40 开发集群<br/>轻量调度<br/>4 节点 × 8 GPU"]
        
        Fed --> Zone1
        Fed --> Zone2
        Fed --> Zone3
        
        Zone1 -.->|"溢出请求"| Zone2
        
        Zone2_Pro["优势: 故障隔离、调度快速<br/>劣势: 资源碎片<br/>集群间模型/镜像同步"]
    end

    style US_Scheduler fill:#4caf50,color:#fff
    style Fed fill:#2196f3,color:#fff
    style Zone1 fill:#c8e6c9,color:#333
    style Zone2 fill:#fff9c4,color:#333
    style Zone3 fill:#ffcdd2,color:#333

方案 A:统一调度

将所有 GPU 节点纳入单一调度器(如 Kubernetes + Volcano),通过队列优先级(Priority)、公平份额(Fair-Share)、资源配额(ResourceQuota)实现多租户的资源分配。核心优势是全局资源视图——调度器能看到所有节点的实时使用情况,规避"池 A 满池 B 空"的静态分区问题。

实现链路:GPU 节点统一注册到 K8s 集群 → Volcano 接管 PodGroup 调度 → Queue 配置 weight 和 capability → 支持 Gang Scheduling、DRF(Dominant Resource Fairness)等公平策略 → 可抢占(Preemption)确保高优任务获取资源。

方案 B:分区自治 + 联邦

保持按 GPU 型号或业务线的物理分区,各分区内独立调度,分区之间通过联邦层做负载感知的路由和溢出调度。这种方式保留了分区的确定性(H100 节点不会因为调度策略被分配给推理任务),同时通过溢出机制解决资源复用问题。

实现链路:H100 集群独立 Volcano 调度 → A100 集群独立调度 → 联邦层(如 Karmada/OpenCluster)监听各集群空余 GPU → 训练任务优先路由到 H100 集群 → H100 满载后溢出到 A100 → 溢出任务携带 GPU 型号检查,避免不兼容。

三、统一调度的生产级队列配置

以 Volcano 为例,统一调度池的队列配置需要体现优先级、公平份额和弹性借用的三层策略。

# volcano-queue.yaml — Volcano 队列配置
apiVersion: scheduling.volcano.sh/v1beta1
kind: Queue
metadata:
  name: gpu-training
spec:
  reclaimable: true   # 允许被更高优先级队列抢占
  weight: 8           # 权重 8,占 8/(8+5+2)=53% 的资源
  capability:
    cpu: "200"        # CPU 硬上限
    memory: "2000Gi"
    nvidia.com/gpu: "100"  # GPU 硬上限
---
apiVersion: scheduling.volcano.sh/v1beta1
kind: Queue
metadata:
  name: gpu-inference
spec:
  reclaimable: false  # 推理服务不允许被抢占(保证在线服务稳定性)
  weight: 5
  capability:
    cpu: "100"
    memory: "500Gi"
    nvidia.com/gpu: "50"
---
apiVersion: scheduling.volcano.sh/v1beta1
kind: Queue
metadata:
  name: gpu-batch
spec:
  reclaimable: true
  weight: 2
  capability:
    cpu: "50"
    memory: "200Gi"
    nvidia.com/gpu: "20"

Go 实现的调度策略互补层:当 Volcano 无法满足特定约束(如需要同机架放置)时,通过自定义 Webhook 做二次筛选。

// gpu_pool_router.go — GPU 池化路由层
// 职责:在 K8s 调度器之外做 GPU 节点选择的二次校验
// 场景:训练任务要求同机架 NVSwitch 互联,普通调度器无法感知拓扑

package main

import (
    "context"
    "fmt"

    corev1 "k8s.io/api/core/v1"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/apimachinery/pkg/labels"
    "k8s.io/client-go/kubernetes"
)

// TopologyRequirement 节点拓扑约束
type TopologyRequirement struct {
    RackAffinity string // 同机架要求
    NvlinkCount  int    // 最小 NVLink 链路数
    GPUModel     string // 要求的 GPU 型号
}

// PoolRouter 池化路由器
type PoolRouter struct {
    client kubernetes.Interface
}

// SelectNodes 根据拓扑约束筛选可用节点
func (r *PoolRouter) SelectNodes(ctx context.Context, req TopologyRequirement) ([]corev1.Node, error) {
    // 步骤 1:按 GPU 型号和 NVLink 数量做标签选择
    labelSelector := labels.Set{
        "nvidia.com/gpu.product":    req.GPUModel,
        "feature.node.kubernetes.io/pci-10de.present": "true", // NVIDIA GPU 存在
    }.AsSelector()

    nodes, err := r.client.CoreV1().Nodes().List(ctx, metav1.ListOptions{
        LabelSelector: labelSelector.String(),
    })
    if err != nil {
        return nil, fmt.Errorf("查询 GPU 节点失败: %w", err)
    }

    // 步骤 2:按可用 GPU 数量过滤
    var candidates []corev1.Node
    for _, node := range nodes.Items {
        gpuCap, ok := node.Status.Capacity["nvidia.com/gpu"]
        if !ok {
            continue
        }
        gpuAvailable := gpuCap.Value()
        
        // 获取已分配 GPU 数量
        gpuAllocated := int64(0)
        if val, ok := node.Status.Allocatable["nvidia.com/gpu"]; ok {
            gpuAllocated = gpuCap.Value() - val.Value()
        }

        // 至少需要 NvlinkCount 张 GPU(NVSwitch 域内通信要求)
        if gpuAvailable-gpuAllocated >= int64(req.NvlinkCount) {
            candidates = append(candidates, node)
        }
    }

    // 步骤 3:按机架亲和性分组
    if req.RackAffinity != "" {
        rackNodes := make(map[string][]corev1.Node)
        for _, node := range candidates {
            rack := node.Labels["topology.kubernetes.io/zone"]
            if rack != "" {
                rackNodes[rack] = append(rackNodes[rack], node)
            }
        }

        // 返回指定机架的节点
        if nodes, ok := rackNodes[req.RackAffinity]; ok {
            return nodes, nil
        }
        return nil, fmt.Errorf("机架 %s 没有可用 GPU 节点", req.RackAffinity)
    }

    return candidates, nil
}

四、统一调度 vs 按模型分集群的终局判断

统一调度的最大吸引力是"理论上的全局最优",但它面临三个工程难题:

调度延迟与调度质量不可兼得。全局最优需要全量扫描所有候选节点,在千卡集群中扫描节点需要 O(n) 时间复杂度。当每秒有数十个调度请求时,调度器的瓶颈从内存扩展到 CPU。Volcano 的 Gang Scheduling 在百卡以上场景中,P99 调度延迟可能达到数秒——对推理服务的滚动更新来说不可接受。

模型-硬件的绑定关系不可消除。不同 GPU 型号的镜像、驱动、CUDA 库组合不同,即使统一调度成功放置 Pod,Pod 启动后仍可能因为二进制不兼容失败。这类失败无法在调度阶段检测(调度器只关心资源量,不关心二进制兼容性),导致调度"假成功"后需要重新尝试。"假成功"带来的重试开销可能抵消统一调度带来的利用率提升。

分区的确定性是运维的安全网。当集群出现大规模故障时,分区模型可以限制故障爆炸半径。如果推理服务因为一个 Bug 占满了 H100 节点,统一调度下 H100 上的训练任务也会受影响。在分区模型下,故障被限制在推理节点池内。

终局推演:大型集群不会是纯"统一调度"或纯"按模型分集群",而是分层架构:

  1. GPU 型号层:H100、A100、A40 等不同 GPU 型号按物理集群或节点组隔离(硬件决定的边界)。
  2. 业务分组层:同一 GPU 型号内按训练/推理/批处理做软分区(Queue 配额隔离)。
  3. 联邦调度层:跨型号的溢出调度仅在稀有场景触发(如训练任务紧急需要 GPU 但本型号池满),溢出时携带 GPU 型号验证逻辑。

五、总结

GPU 资源池化的终局不是二选一,而是分层收敛:

  1. 物理隔离是底线 —— 不同 GPU 型号必须保持在独立的可用域内,通过标签和 Node Affinity 确保。
  2. 软分区通过队列配额 —— 同型号 GPU 内用 Volcano/Vela 的队列 weight 和 capability 做多租户隔离。
  3. 跨型号溢出作为弹性补充 —— 仅在单型号池满且任务明确声明兼容性时触发,不建议作为常规调度路径。
  4. 资源利用率不是唯一指标 —— 调度确定性、故障隔离、运维复杂度同样重要。99% 的利用率但每次调度都要赌兼容性,不如 85% 的利用率但每次调度都确定成功。
Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐