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/>优先级: 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 上的训练任务也会受影响。在分区模型下,故障被限制在推理节点池内。
终局推演:大型集群不会是纯"统一调度"或纯"按模型分集群",而是分层架构:
- GPU 型号层:H100、A100、A40 等不同 GPU 型号按物理集群或节点组隔离(硬件决定的边界)。
- 业务分组层:同一 GPU 型号内按训练/推理/批处理做软分区(Queue 配额隔离)。
- 联邦调度层:跨型号的溢出调度仅在稀有场景触发(如训练任务紧急需要 GPU 但本型号池满),溢出时携带 GPU 型号验证逻辑。
五、总结
GPU 资源池化的终局不是二选一,而是分层收敛:
- 物理隔离是底线 —— 不同 GPU 型号必须保持在独立的可用域内,通过标签和 Node Affinity 确保。
- 软分区通过队列配额 —— 同型号 GPU 内用 Volcano/Vela 的队列 weight 和 capability 做多租户隔离。
- 跨型号溢出作为弹性补充 —— 仅在单型号池满且任务明确声明兼容性时触发,不建议作为常规调度路径。
- 资源利用率不是唯一指标 —— 调度确定性、故障隔离、运维复杂度同样重要。99% 的利用率但每次调度都要赌兼容性,不如 85% 的利用率但每次调度都确定成功。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐

所有评论(0)