GPU 资源池化的终局推演:统一调度还是按模型分集群
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/优先级: Highbr/配额: 100 GPU] Sub_Queue2[推理队列br/优先级: Mediumbr/配额: 50 GPU] Sub_Queue3[批处理队列br/优先级: Lowbr/配额: 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-schedulerbr/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、DRFDominant 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/(852)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: 20Go 实现的调度策略互补层当 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 张 GPUNVSwitch 域内通信要求 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 库组合不同即使统一调度成功放置 PodPod 启动后仍可能因为二进制不兼容失败。这类失败无法在调度阶段检测调度器只关心资源量不关心二进制兼容性导致调度假成功后需要重新尝试。假成功带来的重试开销可能抵消统一调度带来的利用率提升。分区的确定性是运维的安全网。当集群出现大规模故障时分区模型可以限制故障爆炸半径。如果推理服务因为一个 Bug 占满了 H100 节点统一调度下 H100 上的训练任务也会受影响。在分区模型下故障被限制在推理节点池内。终局推演大型集群不会是纯统一调度或纯按模型分集群而是分层架构GPU 型号层H100、A100、A40 等不同 GPU 型号按物理集群或节点组隔离硬件决定的边界。业务分组层同一 GPU 型号内按训练/推理/批处理做软分区Queue 配额隔离。联邦调度层跨型号的溢出调度仅在稀有场景触发如训练任务紧急需要 GPU 但本型号池满溢出时携带 GPU 型号验证逻辑。五、总结GPU 资源池化的终局不是二选一而是分层收敛物理隔离是底线—— 不同 GPU 型号必须保持在独立的可用域内通过标签和 Node Affinity 确保。软分区通过队列配额—— 同型号 GPU 内用 Volcano/Vela 的队列 weight 和 capability 做多租户隔离。跨型号溢出作为弹性补充—— 仅在单型号池满且任务明确声明兼容性时触发不建议作为常规调度路径。资源利用率不是唯一指标—— 调度确定性、故障隔离、运维复杂度同样重要。99% 的利用率但每次调度都要赌兼容性不如 85% 的利用率但每次调度都确定成功。