容器云平台架构设计 —— 基于 Volcano

1. 需求概览

维度 选型
集群架构 单 K8s 集群 + Namespace 隔离
GPU 类型 NVIDIA GPU(A100/H100),NCCL 通信
资源模型 包年包月(预留配额)+ 按量付费(保障配额)+ 抢占式(弹性资源池,可被回收)
资源管控 完全由 Volcano Queue 管控(guarantee/deserved/capability),不使用 K8s ResourceQuota
弹性回收 包年包月和按量队列资源受 deserved 保护,不可被其他租户抢占;抢占式队列由上层平台在新购包年包月/按量需求时 Delete 回收
调度插件 capacity 插件 + enableHierarchy(支持包年包月子队列)
分布式训练 AllReduce(PyTorch DDP/DeepSpeed/Megatron)、PS-Worker、MPI、Spark/Ray
推理服务 完整推理平台(版本管理、A/B 测试、金丝雀发布、弹性伸缩)
上层平台 有 Web 控制台 / API 网关,Volcano 作为底层调度器

2. 整体架构

┌─────────────────────────────────────────────────────────────────┐
│                        Web 控制台 / CLI                         │
└──────────────────────────┬──────────────────────────────────────┘
                           │
┌──────────────────────────▼──────────────────────────────────────┐
│                      API Gateway / BFF                          │
│         认证(OAuth2/OIDC) · 租户路由 · 计费接口                   │
└──────┬──────────┬────────────┬───────────┬──────────────────────┘
       │          │            │           │
┌──────▼───┐ ┌───▼──────┐ ┌──▼────────┐ ┌▼───────────────┐
│ 任务管理  │ │ 资源管理  │ │ 推理服务   │ │ 监控/日志/告警  │
│ Service  │ │ Service  │ │ Service   │ │ Service        │
└──────┬───┘ └───┬──────┘ └──┬────────┘ └┬───────────────┘
       │         │           │           │
┌──────▼─────────▼───────────▼───────────▼────────────────────────┐
│                    Kubernetes Cluster                            │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │                  Volcano Scheduler                        │   │
│  │  capacity 插件 · 包年包月/按量/抢占式                       │   │
│  │  PriorityClass · Preempt · Reclaim · Gang · Binpack       │   │
│  └──────────────────────────────────────────────────────────┘   │
│  ┌─────────────┐ ┌──────────────┐ ┌──────────────────────┐     │
│  │ Volcano Job │ │ vcjob/pytorch│ │ KServe / Triton      │     │
│  │ Controller  │ │ MPI/Spark/Ray│ │ Inference Service     │     │
│  └─────────────┘ └──────────────┘ └──────────────────────┘     │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │  Namespace (per Tenant) · NetworkPolicy · RBAC           │   │
│  └─────────────────────────────────────────────────────────┘   │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │  GPU Nodes: NVIDIA Device Plugin + GPU Feature Discovery │   │
│  │  RDMA/InfiniBand (高速网络)                               │   │
│  │  共享存储: NFS/Lustre/CephFS (数据集 & Checkpoint)        │   │
│  └─────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────┘

3. 租户隔离设计

3.1 Namespace 维度隔离

每个租户对应一个 Namespace,该租户的包年包月、按量付费、抢占式三种任务都在同一个 Namespace 内运行,队列间网络互通。

apiVersion: v1
kind: Namespace
metadata:
  name: tenant-alpha
  labels:
    platform.io/tenant: "alpha"

3.2 RBAC 权限隔离

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: tenant-alpha
  name: tenant-user
rules:
  - apiGroups: ["batch.volcano.sh"]
    resources: ["jobs"]
    verbs: ["get", "list", "create", "delete"]
  - apiGroups: ["serving.kserve.io"]
    resources: ["inferenceservices"]
    verbs: ["get", "list", "create", "update", "delete"]
  - apiGroups: [""]
    resources: ["pods", "pods/log", "events", "configmaps", "secrets"]
    verbs: ["get", "list"]

3.3 网络隔离

不同租户之间网络隔离,同一租户内的所有任务(包年包月、按量、抢占式)互通。

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-cross-tenant
  namespace: tenant-alpha
spec:
  podSelector: {}
  policyTypes: ["Ingress", "Egress"]
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              platform.io/tenant: "alpha"
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              platform.io/tenant: "alpha"
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
    - to:
        - ipBlock:
            cidr: 10.0.0.0/8

4. 队列模型与资源管控

4.1 核心设计原则

  1. 资源配额完全由 Volcano Queue(guarantee/deserved/capability)管控,不使用 K8s ResourceQuota 和 LimitRange
  2. 每个租户拥有三个队列:包年包月队列按量付费队列抢占式队列,三者在 root 下平级
  3. 包年包月队列 deserved = capability(用户已预付费,资源预留,不弹性扩展)
  4. 包年包月队列可创建子队列,映射租户内不同团队;子队列可分别设置 deservedcapability,但资源使用不可超过各自 capability
  5. 按量付费队列为 flat 队列,容量动态调整——租户使用时平台先扩容再创建资源,释放后同步缩容。按量队列和抢占式队列不创建子队列。
  6. 包年包月和按量队列的资源受保护,不可被其他租户抢占;抢占式队列 deserved=0,随时可以被回收。
  7. 抢占式队列的 capability 按租户包年包月购买量的比例分配(大客户获得更多抢占配额),由平台自动计算并更新

4.2 三级资源配置

Volcano 支持三级资源配置,需遵循 guarantee <= deserved <= capability

guarantee:资源预留量
  预留资源只能被该队列使用,其他队列无法使用。
  用于绝对保障的场景(如关键推理服务)。

deserved:资源应得量
  队列的正常份额。超出 deserved 的资源可以被兄弟队列 reclaim。
  是 reclaim 的判断基准——队列当前使用量 > deserved 的部分可被回收。

capability:资源使用上限
  队列能使用的最大资源量,超过 capability 的任务不会被 enqueue。

4.3 队列结构

每个租户三个队列,在 root 下平级。包年包月队列可创建子队列,按量和抢占式为 flat 队列。

                                      root
                                       │
          ┌────────────────────────────┼────────────────────────────┐
          │                            │                            │
 tenant-alpha-subscription   tenant-alpha-payg          tenant-alpha-spot
 (包年包月)                   (按量付费)                   (抢占式)
 d=40, c=40                  d=动态调整                  d=0, c=按比例分配
                              c=动态调整
          │
    ┌─────┼─────┐
    │     │     │
  team-a team-b team-c
  d=20   d=10   d=10
  c=40   c=40   c=40

d = deserved(应得量)
c = capability(硬上限)

包年包月父队列 d=c:用户已预付费,资源预留,不超越购买量使用。
子队列可 d < c:允许子队列借用其他子队列的空闲资源(但不超过各自的 capability)。
按量队列 d、c 动态调整:平台根据租户当前使用量实时调整,d 跟随 c 保持匹配,保障资源不被其他租户抢占。
抢占式队列 d=0,c 按租户包年包月购买量占全部包年包月总量的比例分配集群空闲资源。计算公式:
  c(tenant-i-spot) = cluster_idle_gpu × (subscription_i / total_subscription)
  cluster_idle_gpu = 集群总 GPU - 所有包年包月 deserved 之和 - 所有按量当前使用量之和
  c 在集群资源变化时由平台重新计算并更新到 Queue 的 capability。

为什么包年包月有子队列?
租户内部通常有多个团队(如算法组、工程组、数据组),每个团队需要独立的资源配额和隔离。子队列通过 parent 字段挂在包年包月父队列下,团队间可以在配额范围内互相借用空闲资源(reclaim)。

为什么按量和抢占式没有子队列?
按量付费和抢占式是租户级的弹性资源池,通常由租户统一管理和分配,不需要细分到团队。

三种队列的网络关系:
三种队列的任务在同一个租户 Namespace 内运行,网络完全互通(通过 NetworkPolicy 允许同 Namespace 内所有流量),仅通过 Volcano Queue 隔离资源配额。

4.4 队列间关系

包年包月子队列间(同级团队):
  team-a ↔ team-b ↔ team-c
  同属 tenant-alpha-subscription 父级,是兄弟队列
  空闲时互相借用资源,有新任务时 Volcano reclaim 自动回收
  reclaim 以各子队列 deserved 为基准,不会压到 deserved 以下

同一租户内三种队列之间:
  subscription / payg / spot 在 root 下是平级兄弟
  reclaim 只在兄弟队列间进行——包年包月子队列的 reclaim 不会跨到按量或抢占式
  三种队列在同一个 Namespace 内,网络互通

跨租户:
  包年包月队列 d=c,使用量永不超出 deserved,不存在跨租户 reclaim
  按量队列 d 由平台动态跟随 c 调整,d=c=当前容量,始终受 deserved 保护
  抢占式队列 deserved=0,所有使用量均可被回收

抢占式队列的特殊地位:
  deserved=0,所有使用量都是"超额"→ 始终可被 reclaim 或有新需求时由平台 Delete
  capability 按包年包月购买量比例分配:大客户购买更多包年包月资源 → 获得更大抢占配额 → 可运行更多抢占式任务
  c(tenant-i-spot) = cluster_idle_gpu × (subscription_i / total_subscription),由平台自动计算更新
  抢占式队列的回收由上层平台主动 Delete 驱动,不依赖 Volcano reclaim

### 4.5 Queue YAML 配置

#### 租户包年包月队列(父队列)

包年包月 d=c=40,资源预留给该租户。子队列 YAML 见下文说明。

```yaml
apiVersion: scheduling.volcano.sh/v1beta1
kind: Queue
metadata:
  name: tenant-alpha-fixed
spec:
  reclaimable: true
  parent: root
  deserved:
    nvidia.com/gpu: 40
    cpu: "160"
    memory: "640Gi"
  capability:
    nvidia.com/gpu: 160
    cpu: "640"
    memory: "2560Gi"

包年包月父队列 d=c,资源已预付费,不弹性扩展。
子队列的 capability 可等于父队列 capability(上限),deserved 之和不超过父队列 deserved。
子队列间 reclaim 以各自 deserved 为基准,团队间可借用空闲资源。

租户按量付费队列

按量队列为 flat 队列,不设子队列。队列容量由平台动态管理,不固定。

# (按量队列 YAML 示例,待补充)
# 
# 按量队列的 d/c 由平台动态调整:
#   使用前 → 平台先扩容队列(增加 capability),d 同步跟随 c
#   使用后 → 平台缩容队列(减少 capability),d 同步跟随 c
#   d 始终与 c 一致,保障当前使用量不被其他租户 reclaim
租户抢占式队列

抢占式队列 d=0,capability 由平台按租户包年包月购买量比例自动计算并设置。

apiVersion: scheduling.volcano.sh/v1beta1
kind: Queue
metadata:
  name: tenant-alpha-spot
spec:
  reclaimable: true
  parent: root
  deserved:
    nvidia.com/gpu: 0
  capability:
    nvidia.com/gpu: 68   # 由平台动态计算并更新,非固定值
    cpu: "544"
    memory: "2176Gi"

capability 的动态计算c(tenant-alpha-spot) = cluster_idle_gpu × (subscription_alpha / total_subscription)
cluster_idle_gpu = 集群 GPU 总量 - 所有包年包月 deserved 之和 - 所有按量当前使用量之和
平台在新租户加入、包年包月购买量变更、按量使用量大幅变化时重新计算并更新各租户抢占式队列的 capability。

为什么三种队列都在 root 下?
包年包月、按量、抢占式三种队列在 root 下平级,reclaim 只在兄弟间进行。
包年包月子队列的 reclaim 不会跨到按量或抢占式队列。
抢占式队列 deserved=0,所有使用量都可被回收,由上层平台 Delete 驱动。
三种队列的任务在同一 Namespace 内运行,网络互通。

4.6 资源管控策略

为什么不用 K8s ResourceQuota / LimitRange?
  1. 同一 Namespace 内有包年包月、按量、抢占式三种队列
     ResourceQuota 是 Namespace 级别,无法区分不同队列类型
  2. 包年包月还有子队列(团队级),ResourceQuota 无法表达层级关系
  3. 按量队列 d/c 动态调整,抢占式资源池也动态变化,ResourceQuota 需要频繁修改
  4. Volcano Queue 的 capability 本身就是硬上限:
     超过 capability 的任务 → enqueue 阶段直接拒绝
  5. Volcano Queue 的 guarantee/deserved 提供保障和回收基准
     完全覆盖 ResourceQuota 的能力,且粒度更细(队列级 vs Namespace 级)

5. 优先级体系

5.1 完整优先级定义

优先级值    名称            用途                        可被抢占?   抢占范围
───────────────────────────────────────────────────────────────────────────
P1000     critical-job     最高优先任务                  不可被抢占   P800 及以下
          (在线推理/高等级训练)

══════════ 天花板(提权不可逾越)════════════════════════════════════════

P800      large-job        大规模训练任务(32+ GPU)     可被 P1000   P100 及以下

P100      normal-job       普通任务(可重启/可重试)     可被 P800+   P50 及以下

P50       debug-job        开发调试任务                  可被 P100+   无(Never)

* 大任务通过 P800 在同队列内 preempt P100/P50 任务获取资源
* 提权上限 P800,绝不可能达到 P1000,物理上保证关键任务不被误伤
* 同一优先级体系适用于所有队列(包年包月子队列、按量、抢占式),preempt 在同队列内生效

5.2 PriorityClass 定义

# P1000: 最高优先任务——不可被抢占
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: critical-job
value: 1000
preemptionPolicy: PreemptLowerPriority
description: "最高优先任务(在线推理、高等级训练),任何场景不可被抢占"

---
# P800: 大规模训练任务
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: large-job
value: 800
preemptionPolicy: PreemptLowerPriority
description: "大规模分布式训练任务(32+ GPU),优先抢占 P100/P50 任务"

---
# P100: 普通任务
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: normal-job
value: 100
preemptionPolicy: PreemptLowerPriority
description: "可重启、可重试的普通训练/推理任务"

---
# P50: 开发调试任务
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: debug-job
value: 50
preemptionPolicy: Never
description: "开发调试任务,最低优先级,允许被中断"

---
# P50: 竞价任务
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: spot-job
value: 50
preemptionPolicy: PreemptLowerPriority
description: "抢占式任务,使用弹性资源池,上层平台 Delete 回收"

5.3 每种任务使用哪个队列、哪个优先级

任务类型 提交到哪个队列 PriorityClass 说明
关键推理服务 包年包月子队列 (如 team-a) critical-job (P1000) 最高优先,不可被抢占
关键训练任务 包年包月子队列 (如 team-a) critical-job (P1000) 人工标记为关键
大规模训练 (32+ GPU) 包年包月子队列 或 按量队列 large-job (P800) 同队列内 preempt P100/P50,不超过 P1000 天花板
普通训练 包年包月子队列 或 按量队列 normal-job (P100) 日常训练任务
普通推理 包年包月子队列 normal-job (P100) 普通推理服务
开发调试 包年包月子队列 debug-job (P50) 最低优先级
抢占式任务 抢占式队列 spot-job (P50) 弹性资源,上层平台 Delete 回收

6. 资源抢占与回收机制

6.1 三种资源竞争场景

场景                        触发方           机制
─────────────────────────────────────────────────────────
大任务抢小任务(同队列内)    Volcano          preempt(基于优先级)
包年包月子队列间借用归还      Volcano          reclaim(基于 deserved)
抢占式资源回收              上层平台          Delete 抢占式任务,释放资源
场景一:大任务抢占小任务(Volcano preempt)—— 防饥饿核心机制

同队列内,大规模训练任务(P800)通过 preempt 直接驱逐低优先级任务来获取资源。适用于包年包月子队列和按量队列。

team-a 队列(包年包月子队列)当前使用 30 GPU:
  task-A: normal-job (P100), 8 GPU
  task-B: normal-job (P100), 8 GPU
  task-C: normal-job (P100), 8 GPU
  task-D: debug-job (P50),  4 GPU  ← 开发任务
  task-E: debug-job (P50),  2 GPU  ← 开发任务

大任务提交,需要 32 GPU:
  → 系统当前剩余 0 GPU
  → preempt 选择牺牲者:P50 优先 → task-D + task-E = 6 GPU → 还差 26
  → 继续选 P100 → task-C (后提交先杀) = 8 GPU → 还差 18
  → 继续选 → task-B = 8 GPU → 还差 10
  → 继续选 → task-A = 8 GPU → 还差 2 → 选到刚好满足
  → 总计回收 34 GPU,大任务取走 32,剩余 2 留给其他任务

preempt 仅在同队列内生效。team-a 的 preempt 不会影响 team-b 或按量队列的任务。

场景二:包年包月子队列间借用归还(Volcano reclaim)

同一租户下,不同团队子队列在配额范围内互相借用空闲资源。

team-a (d=20, 当前使用 25) → 借用了 5 GPU
team-b (d=10, 当前使用 5)  → 空闲 5 GPU

team-b 提交新任务,需要 5 GPU:
  → Volcano reclaim 检测到 team-b 使用量 < deserved
  → 在 team-a 中找到超出 deserved 的任务(借用的 5 GPU)
  → 终止超出部分的任务,释放 5 GPU 给 team-b
  → team-a 从 25 降到 20(deserved 底线)

reclaim 只在兄弟队列间进行。team-a 的 reclaim 只会找 team-b/team-c,不会跨到按量或抢占式队列。

场景三:抢占式资源回收(上层平台 Delete)

当新的包年包月或按量需求到来且集群资源不足时,平台主动回收抢占式资源。

新用户购买包年包月配额 → 平台需要为其创建队列
  → 平台 API 检查集群空闲
  ├── 有空闲 → 直接分配,不影响抢占式任务
  └── 无空闲,有抢占式任务占用 →
      → 选择牺牲者(出价低 → 单机 → 运行时间短)
      → 直接 Delete Pod/Job
      → 释放资源,创建新租户的队列
      → 通知抢占式用户"任务已被回收"

6.2 reclaim 牺牲者选择逻辑

reclaim 回收的是正在运行的任务(Pod),不是空闲资源。当某个队列使用量超出 deserved 时,从中选择任务杀掉释放资源。

牺牲者选择顺序:

1. 优先级低的先杀
   P50 (debug-job) → P100 (normal-job) → P800 (large-job) → P1000 不可杀
   优先级越低越先被选为牺牲者

2. 同优先级下,后提交的先杀(LIFO)

3. conformance 保护的 Pod 不可被选
   kube-system namespace、DaemonSet、system-critical 的 Pod 始终跳过

4. 只杀到刚好满足需求为止,不多杀
示例:

team-a (d=20, 当前用 25),正在运行的任务:
  task-A: normal-job (P100), 8 GPU
  task-B: normal-job (P100), 8 GPU
  task-C: normal-job (P100), 8 GPU
  task-D: debug-job (P50),  4 GPU  ← 开发任务
  task-E: debug-job (P50),  2 GPU  ← 开发任务

超出 5 GPU 需要 reclaim 给 team-b(兄弟子队列):

  Step 1: 找优先级最低的 → task-D (P50, 4 GPU), task-E (P50, 2 GPU)
  Step 2: 杀 task-D + task-E → 释放 6 GPU → 超额降到 -1(满足需求)
  
  结果:team-b 获得 5 GPU,team-a 使用回落到 19(< deserved 20)

### 6.3 preempt 与 reclaim 的牺牲者选择对比

| 维度 | preempt(同队列内) | reclaim(兄弟队列间) |
|------|-------------------|-----------------|
| 触发条件 | 高优先级任务(P800)抢低优先级任务(P100/P50) | 队列使用量 < deserved,需要从超用的兄弟队列回收 |
| 适用范围 | 包年包月子队列 / 按量队列内部 | 同级子队列之间(如 team-a ↔ team-b) |
| 牺牲者范围 | 同一队列内优先级更低的任务 | 超出 deserved 的兄弟队列中的任务 |
| 选择顺序 | 优先级低 → LIFO | 优先级低 → LIFO |
| 保护底线 | P1000 不可被抢占(提权上限 P800) | 不能把队列压到 deserved 以下 |
| conformance | 系统 Pod 受保护 | 系统 Pod 受保护 |

### 6.4 大任务防饥饿策略

基于以上 preempt 机制,大任务防饥饿的核心策略分为两层:

#### 6.4.1 优先级抢占(Volcano preempt)

大任务(large-job, P800)提交到包年包月子队列或按量队列后,Volcano 自动触发同队列内 preempt:

大任务提交 → allocate 发现资源不足
→ preempt 选择牺牲者(P50 → P100,不碰 P1000)
→ 驱逐低优先级 Pod → 释放 GPU
→ 大任务获得资源 → 开始训练
→ 被驱逐的任务重新排队,等大任务完成后再调度


抢占的粒度是"刚好够用"——不会多杀 Pod。preempt 仅在**同队列内**生效,不会跨团队或跨队列类型。

#### 6.4.2 提交限流(Submission Throttle)

Volcano preempt 存在一个竞态窗口:被驱逐的 Pod 释放 GPU 后,如果在 Volcano 分配给大任务之前有新的低优先级任务提交进来,可能抢走刚释放的资源。

Volcano 调度周期内的竞态:

周期 N: preempt 驱逐 Pod → GPU 释放

[窗口——新提交可能抢走 GPU]

周期 N+1: 大任务分配 GPU → 但资源已被新任务占走


**解决方案**:平台 API 层在大任务抢占期间暂缓提交低优先级任务。限流按队列独立生效。

大任务提交时的平台逻辑(以包年包月子队列为例):

  1. 用户提交大任务(large-job 类型)→ 平台提交到 Volcano
  2. 平台标记该子队列进入"大任务等候模式"
  3. 等候模式下,同一子队列中新的 normal-job (P100)/debug-job (P50) 在平台层排队
  4. Volcano 调度 → preempt → 释放 GPU → 大任务开始运行
  5. 平台检测到大任务已运行 → 解除等候模式
  6. 排队的低优先级任务提交到 K8s,正常参与调度

| 机制 | 说明 |
|------|------|
| 触发条件 | 大任务提交后初始 allocate 失败(需要 preempt) |
| 限流对象 | 同一队列中优先级低于 P800 的新提交(P100、P50) |
| 解除条件 | 大任务开始运行,或超时(5 分钟未分配到资源则放弃限流) |
| 超时处理 | 大任务 5 分钟仍无法获得资源 → 解除限流,不阻塞小任务 |

### 6.5 抢占式资源回收

抢占式资源回收场景:新的包年包月或按量需求到来,且集群资源不足时

包年包月和按量队列的资源受 deserved 保护,不需要从抢占式队列抢:
→ subscription d=40 / payg d=20,这些 GPU 是预留的或按量保障的
→ 各自在配额内运行

抢占式队列使用未售出的空闲资源(deserved=0):
→ 只有新用户购买包年包月或按量增长时,需要把资源划出去
→ 上层平台主动 Delete 抢占式任务,释放资源


#### 回收流程

  1. 新用户购买 20 GPU 包年包月(或按量需求增长 20 GPU)

  2. 平台检查集群空闲 GPU
    ├── 空闲 >= 20 → 直接创建队列,不需要回收
    └── 空闲 < 20(假设空闲 8 GPU)→ 还差 12 GPU,需要回收抢占式任务

  3. 从所有租户的抢占式队列中查询正在运行的任务

  4. 按牺牲者选择策略排序,选择要回收的任务

  5. 逐个 Delete 选中的任务,直到释放的 GPU >= 12

  6. 创建新租户的 Namespace、队列、RBAC

  7. 通知被回收的抢占式用户(K8s Event + 平台消息)


#### 牺牲者选择策略

牺牲者选择顺序(出价低的先杀):

  1. 抢占式价格低的先杀
    读取 annotation “platform.io/spot-price”
    出价 0.1 的先杀,出价 0.5 的后杀
    → 出价高的用户获得更强的运行保障

  2. 同价格下,单机任务优先杀
    replicas=1 或 GPU 数少的任务先杀
    → 减少对分布式训练的影响(分布式任务杀一个全部失败)

  3. 同价格同规模,运行时间短的先杀
    刚启动的任务先杀
    → 减少用户的计算损失(跑了很久的可能快完成了)

  4. 只杀到刚好满足需求为止,不多杀


示例:

需要回收 12 GPU,当前抢占式任务:
spot-A: 价格 0.1, 4 GPU, 单机, 跑了 10 分钟
spot-B: 价格 0.1, 8 GPU, 分布式, 跑了 2 小时
spot-C: 价格 0.3, 4 GPU, 单机, 跑了 30 分钟
spot-D: 价格 0.5, 8 GPU, 分布式, 跑了 1 小时

Step 1: 价格最低 → spot-A (0.1, 4 GPU) → 杀掉,已释放 4 GPU
Step 2: 继续找价格低 → spot-B (0.1, 8 GPU) → 杀掉,已释放 12 GPU
Step 3: 满足需求(12 >= 12)→ 停止

spot-C (0.3) 和 spot-D (0.5) 未被回收 → 出价高,获得保障


### 6.6 抢占式任务标记

```yaml
apiVersion: batch.volcano.sh/v1alpha1
kind: Job
metadata:
  name: spot-training-job
  namespace: tenant-alpha
  labels:
    platform.io/billing-type: "spot"
  annotations:
    platform.io/spot-price: "0.3"
spec:
  schedulerName: volcano
  queue: tenant-alpha-spot
  priorityClassName: spot-job
  minAvailable: 1
  tasks:
    - replicas: 1
      name: trainer
      template:
        spec:
          containers:
            - name: trainer
              image: registry.example.com/training:v1
              resources:
                requests:
                  nvidia.com/gpu: 4
                limits:
                  nvidia.com/gpu: 4
          restartPolicy: Never

7. Volcano 调度器配置

kind: ConfigMap
apiVersion: v1
metadata:
  name: volcano-scheduler-configmap
  namespace: volcano-system
data:
  volcano-scheduler.conf: |
    actions: "enqueue, allocate, preempt, reclaim, backfill"
    tiers:
    - plugins:
      - name: priority
      - name: gang
        arguments:
          gang.scheduling.io/max-scheduling-delay: "120s"
      - name: conformance
    - plugins:
      - name: drf
      - name: predicates
      - name: capacity
        enableHierarchy: true
      - name: nodeorder
      - name: binpack
        arguments:
          binpack.weight: 10
          binpack.resources: "nvidia.com/gpu"
          binpack.resources.nvidia.com/gpu: 10
          binpack.resources.cpu: 5
          binpack.resources.memory: 2
      - name: network-topology-aware
        arguments:
          weight: 10

关键变更

  • 使用 capacity 插件替代 proportion 插件,通过 deserved 控制队列资源分配。
  • 开启 enableHierarchy: true 支持包年包月队列的子队列(团队级资源隔离)。
  • 使用 network-topology-aware 插件替代旧的 topology 插件,配合 HyperNode CRD 实现网络拓扑感知调度。
  • 去掉了 overcommit 插件(capacity 插件有自己的入队逻辑)。

8. 网络拓扑感知调度

8.1 为什么需要网络拓扑感知

分布式训练(特别是大规模模型并行)中,节点间需要频繁进行大量数据交互(NCCL AllReduce/AllGather)。数据中心网络通常包含多层交换机,两个节点跨的交换机越少,通信延迟越低,吞吐量越高。

不感知拓扑的调度:
  Job 4 个 Pod 被分散到不同机架的节点上
  → AllReduce 跨多层交换机 → 延迟高 → 训练慢 50%+

感知拓扑的调度:
  Job 4 个 Pod 调度到同一 ToR Switch 下的节点
  → AllReduce 只跨 1 层交换机 → 低延迟 → 训练性能最优

8.2 HyperNode —— 统一的网络拓扑 API

Volcano 通过 HyperNode CRD 描述网络拓扑。一个 HyperNode 表示一个网络拓扑性能域(通常映射到一个交换机或 ToR),多个 HyperNode 通过层级连接形成树状结构。

典型的 GPU 集群网络拓扑(IB/RoCE):

         s6 (tier: 3, 核心交换机)
        /                        \
    s4 (tier: 2)              s5 (tier: 2)
    /        \                /        \
  s0          s1           s2          s3
(tier:1)   (tier:1)     (tier:1)    (tier:1)
 │  │       │  │         │  │       │  │
n0  n1     n2  n3       n4  n5     n6  n7

通信效率:
  n0 ↔ n1:同属 s0(tier 1),效率最高
  n1 ↔ n2:跨 s0→s4→s1(tier 2),效率较低
  n0 ↔ n4:跨 s0→s4→s6→s5→s2(tier 3),效率最差

tier 越低 → HyperNode 内节点通信效率越高
HyperNode 关键字段
字段 说明
spec.tier HyperNode 层级,层级越低通信效率越高
spec.members[].type 子节点类型:Node(叶子 HyperNode)或 HyperNode(非叶子)
spec.members[].selector 子节点选择器,支持 exactMatchregexMatchlabelMatch

regexMatch/labelMatch 只能用在叶子 HyperNode(member type 为 Node)中。

HyperNode YAML 示例
# ===== 叶子 HyperNode(挂真实节点) =====

# 方式一:exactMatch(精确匹配)
apiVersion: topology.volcano.sh/v1alpha1
kind: HyperNode
metadata:
  name: s0
spec:
  tier: 1
  members:
    - type: Node
      selector:
        exactMatch:
          name: "gpu-node-0"
    - type: Node
      selector:
        exactMatch:
          name: "gpu-node-1"

---
# 方式二:labelMatch(按标签匹配,推荐)
apiVersion: topology.volcano.sh/v1alpha1
kind: HyperNode
metadata:
  name: s0
spec:
  tier: 1
  members:
    - type: Node
      selector:
        labelMatch:
          matchLabels:
            topology.volcano.sh/tor: "rack-01"

---
# 方式三:regexMatch(正则匹配)
apiVersion: topology.volcano.sh/v1alpha1
kind: HyperNode
metadata:
  name: s0
spec:
  tier: 1
  members:
    - type: Node
      selector:
        regexMatch:
          pattern: "gpu-rack01-node-.*"

---
# ===== 非叶子 HyperNode(挂子 HyperNode) =====

apiVersion: topology.volcano.sh/v1alpha1
kind: HyperNode
metadata:
  name: s4
spec:
  tier: 2
  members:
    - type: HyperNode
      selector:
        exactMatch:
          name: "s0"
    - type: HyperNode
      selector:
        exactMatch:
          name: "s1"

---
apiVersion: topology.volcano.sh/v1alpha1
kind: HyperNode
metadata:
  name: s6
spec:
  tier: 3
  members:
    - type: HyperNode
      selector:
        exactMatch:
          name: "s4"
    - type: HyperNode
      selector:
        exactMatch:
          name: "s5"

8.3 HyperNode 自动发现

手动创建 HyperNode 适合小规模集群。大规模集群推荐使用 HyperNode 自动发现功能,根据网络管理系统自动维护 HyperNode CRs。

# HyperNode 自动发现配置
apiVersion: v1
kind: ConfigMap
metadata:
  name: volcano-controller-configmap
  namespace: volcano-system
data:
  volcano-controller.conf: |
    networkTopologyDiscovery:
      # IB 网络:从 UFM 自动发现拓扑
      - source: ufm
        enabled: true
        interval: 10m
        credentials:
          secretRef:
            name: ufm-credentials
            namespace: volcano-system
        config:
          endpoint: https://ufm-server:8080
          insecureSkipVerify: false
      # 基于节点标签发现拓扑(通用方案)
      - source: label
        enabled: true
        config:
          networkTopologyTypes:
            gpuCluster:
              - nodeLabel: "topology.volcano.sh/tor"
              - nodeLabel: "kubernetes.io/hostname"
发现源 适用场景 说明
UFM InfiniBand 网络 从 UFM API 获取 IB 交换机拓扑,需配置凭据 Secret
label 通用 基于节点标签(如 topology.volcano.sh/tor)构建拓扑,需提前打标签
RoCE RoCE 网络 从 RoCE 管理系统获取拓扑(规划中)

8.4 Job 的拓扑约束

Volcano Job 通过 networkTopology 字段设置拓扑约束:

spec:
  networkTopology:
    mode: hard          # hard 或 soft
    highestTierAllowed: 2  # 仅 hard 模式需要
模式 行为 适用场景
hard 所有 Pod 必须部署在 highestTierAllowed 指定层级(或更低)的同一个 HyperNode 内,否则 Pending 对网络性能有严格要求的大任务
soft 尽量将所有 Pod 调度到同一个 HyperNode,但资源不够时允许跨 HyperNode 希望优化性能但不能因此卡住调度
示例(highestTierAllowed: 2):

         s6 (tier: 3) ← 不允许跨到这层
        /              \
    s4 (tier: 2) ✅   s5 (tier: 2) ✅   ← 允许在此层级内
    /        \         /        \
  s0   ✅  s1  ✅   s2  ✅   s3  ✅     ← 允许在此层级内
  │  │     │  │     │  │     │  │
  n0 n1   n2 n3    n4 n5    n6 n7

Job 4 Pod:
  ✅ 可以调度到 n0,n1,n2,n3(都在 s4 内,tier 2)
  ✅ 可以调度到 n4,n5,n6,n7(都在 s5 内,tier 2)
  ❌ 不能调度到 n0,n1,n4,n5(跨了 s4 和 s5,需要 tier 3 的 s6)

8.5 结合队列设计的拓扑调度策略

大任务(large-job, P800)→ hard 模式
  大规模分布式训练是性能敏感的,必须在同一网络性能域内
  → networkTopology: { mode: hard, highestTierAllowed: 2 }

普通任务(normal-job, P100)→ soft 模式
  普通训练任务也希望拓扑最优,但不能因此调度不上
  → networkTopology: { mode: soft }

开发调试任务(debug-job, P50)→ 不需要拓扑约束
  单节点任务不涉及节点间通信

推理服务(critical-job, P1000)→ 不需要拓扑约束
  推理服务通常是单副本或无状态多副本,副本间不通信

抢占式任务(spot queue)→ soft 模式或不设置
  抢占式任务对性能要求不高,拓扑约束可能导致调度不上

8.6 网络拓扑打分逻辑

network-topology-aware 插件在节点打分时:

  1. HyperNode 的 tier 越低 → 得分越高(通信效率越高)
  2. 同一 HyperNode 内已成功调度的 Pod 数量越多 → 得分越高(更紧凑)

配合 binpack 插件,GPU 任务会优先集中到同一网络性能域内的相同节点,再扩展到同域的其他节点。


9. 任务类型设计

9.1 分布式训练任务(AllReduce / PyTorch DDP)

apiVersion: batch.volcano.sh/v1alpha1
kind: Job
metadata:
  name: ddp-training
  namespace: tenant-alpha
spec:
  schedulerName: volcano
  queue: tenant-alpha-fixed
  priorityClassName: normal-job
  minAvailable: 4
  maxRetry: 3
  networkTopology:
    mode: soft
  plugins:
    env: []
    svc: []
  policies:
    - event: PodEvicted
      action: RestartJob
    - event: PodFailed
      action: RestartJob
  tasks:
    - replicas: 4
      name: worker
      template:
        spec:
          containers:
            - name: trainer
              image: registry.example.com/training:v1
              command:
                - torchrun
                - --nproc_per_node=8
                - --nnodes=4
                - --rdzv_backend=c10d
                - --rdzv_endpoint=$(MASTER_ADDR):29500
                - train.py
              resources:
                requests:
                  nvidia.com/gpu: 8
                  cpu: "32"
                  memory: "128Gi"
                  rdma/hca: 1
                limits:
                  nvidia.com/gpu: 8
              volumeMounts:
                - name: dataset
                  mountPath: /data
                - name: shared-memory
                  mountPath: /dev/shm
          volumes:
            - name: dataset
              persistentVolumeClaim:
                claimName: training-dataset
            - name: shared-memory
              emptyDir:
                medium: Memory
                sizeLimit: "64Gi"
          restartPolicy: OnFailure

9.2 PS-Worker 训练任务

apiVersion: batch.volcano.sh/v1alpha1
kind: Job
metadata:
  name: ps-worker-training
  namespace: tenant-alpha
spec:
  schedulerName: volcano
  queue: tenant-alpha-fixed
  minAvailable: 3
  plugins:
    env: []
    svc: []
  tasks:
    - replicas: 1
      name: ps
      template:
        spec:
          containers:
            - name: ps
              image: registry.example.com/tf-training:v1
              command: ["python", "train.py", "--role=ps"]
              resources:
                requests:
                  cpu: "8"
                  memory: "32Gi"
          restartPolicy: OnFailure
    - replicas: 2
      name: worker
      template:
        spec:
          containers:
            - name: worker
              image: registry.example.com/tf-training:v1
              command: ["python", "train.py", "--role=worker"]
              resources:
                requests:
                  nvidia.com/gpu: 4
                  cpu: "16"
                  memory: "64Gi"
                limits:
                  nvidia.com/gpu: 4
          restartPolicy: OnFailure

9.3 MPI 任务

apiVersion: batch.volcano.sh/v1alpha1
kind: Job
metadata:
  name: mpi-job
  namespace: tenant-alpha
spec:
  schedulerName: volcano
  queue: tenant-alpha-fixed
  minAvailable: 3
  plugins:
    ssh: []
    svc: []
    env: []
  tasks:
    - replicas: 1
      name: mpimaster
      template:
        spec:
          containers:
            - name: mpi
              image: registry.example.com/mpi:v1
              command:
                - mpirun
                - --allow-run-as-root
                - -np 16
                - --hostfile /etc/volcano/mpihost
                - python train.py
              resources:
                requests:
                  cpu: "4"
                  memory: "16Gi"
          restartPolicy: OnFailure
    - replicas: 2
      name: mpiworker
      template:
        spec:
          containers:
            - name: mpi
              image: registry.example.com/mpi:v1
              resources:
                requests:
                  nvidia.com/gpu: 8
                  cpu: "32"
                  memory: "128Gi"
                limits:
                  nvidia.com/gpu: 8
          restartPolicy: OnFailure

9.4 Spark/Ray 任务

apiVersion: batch.volcano.sh/v1alpha1
kind: Job
metadata:
  name: spark-job
  namespace: tenant-alpha
spec:
  schedulerName: volcano
  queue: tenant-alpha-fixed
  minAvailable: 3
  plugins:
    env: []
    svc: []
  tasks:
    - replicas: 1
      name: driver
      template:
        spec:
          containers:
            - name: spark-driver
              image: registry.example.com/spark:3.5
              command: ["spark-submit", "--master", "k8s://...", "app.py"]
              resources:
                requests:
                  cpu: "4"
                  memory: "16Gi"
          restartPolicy: Never
    - replicas: 4
      name: executor
      template:
        spec:
          containers:
            - name: spark-executor
              image: registry.example.com/spark:3.5
              resources:
                requests:
                  cpu: "8"
                  memory: "32Gi"
                  nvidia.com/gpu: 1
                limits:
                  nvidia.com/gpu: 1
          restartPolicy: OnFailure

9.5 单机训练 / 开发任务

apiVersion: batch.volcano.sh/v1alpha1
kind: Job
metadata:
  name: single-gpu-job
  namespace: tenant-alpha
spec:
  schedulerName: volcano
  queue: tenant-alpha-fixed
  priorityClassName: debug-job
  minAvailable: 1
  tasks:
    - replicas: 1
      name: task
      template:
        spec:
          containers:
            - name: trainer
              image: registry.example.com/pytorch:v2
              command: ["python", "train.py"]
              resources:
                requests:
                  nvidia.com/gpu: 1
                  cpu: "8"
                  memory: "32Gi"
                limits:
                  nvidia.com/gpu: 1
              volumeMounts:
                - name: workspace
                  mountPath: /workspace
          volumes:
            - name: workspace
              persistentVolumeClaim:
                claimName: user-workspace
          restartPolicy: OnFailure

10. 推理服务设计

采用 KServe 作为推理平台层,底层支持 Triton Inference Server / vLLM / TGI 等引擎。

10.1 架构

┌─────────────────────────────────────────────────┐
│                 API Gateway                      │
│         流量路由 · A/B测试 · 金丝雀分流            │
└────────────────────┬────────────────────────────┘
                     │
┌────────────────────▼────────────────────────────┐
│              Istio / Knative                     │
│          流量管理 · 版本路由 · 自动伸缩             │
└────────────────────┬────────────────────────────┘
                     │
┌────────────────────▼────────────────────────────┐
│                  KServe                          │
│  InferenceService CRD · Transformer · Explainer  │
└─────┬──────────┬──────────┬─────────────────────┘
      │          │          │
┌─────▼────┐ ┌──▼──────┐ ┌─▼────────┐
│ Triton   │ │ vLLM    │ │ TGI      │
│ Server   │ │ (LLM)   │ │ (LLM)    │
└──────────┘ └─────────┘ └──────────┘

10.2 InferenceService 示例(含金丝雀发布)

apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  name: llm-service
  namespace: tenant-alpha
spec:
  predictor:
    canaryTrafficPercent: 10
    model:
      modelFormat:
        name: vllm
      runtime: kserve-vllm
      storageUri: "s3://models/llama2-70b/v2"
      resources:
        requests:
          nvidia.com/gpu: 4
          cpu: "16"
          memory: "64Gi"
        limits:
          nvidia.com/gpu: 4
    canary:
      model:
        modelFormat:
          name: vllm
        runtime: kserve-vllm
        storageUri: "s3://models/llama2-70b/v3-candidate"
        resources:
          requests:
            nvidia.com/gpu: 4
          limits:
            nvidia.com/gpu: 4
  scaleTarget: 10
  scaleMetric: concurrency
  minReplicas: 1
  maxReplicas: 8

10.3 HPA 伸缩策略

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: llm-service-hpa
  namespace: tenant-alpha
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: llm-service-predictor
  minReplicas: 1
  maxReplicas: 8
  metrics:
    - type: Pods
      pods:
        metric:
          name: DCGM_FI_DEV_GPU_UTIL
        target:
          type: AverageValue
          averageValue: "70"
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 60
      policies:
        - type: Pods
          value: 2
          periodSeconds: 60
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
        - type: Pods
          value: 1
          periodSeconds: 120

11. 监控与可观测性

组件 用途
Prometheus + Grafana 集群/GPU/任务指标监控
DCGM Exporter NVIDIA GPU 指标采集
Loki + Promtail 日志聚合
Alertmanager 告警
Volcano Metrics 队列深度、等待时间、调度延迟

关键监控指标:GPU 利用率/显存/温度、队列排队任务数/等待时间、任务成功失败率、抢占式任务回收次数、推理服务 QPS/延迟、集群资源水位。


12. 存储设计

存储类型 用途 访问模式
Lustre/GPFS 训练数据集 ReadOnlyMany
CephFS/NFS Checkpoint / 模型产物 ReadWriteMany
MinIO/S3 模型仓库(推理拉取) 对象存储
NFS/CephFS 用户工作空间 ReadWriteOnce per user

13. 上层平台需要实现的能力

模块 核心功能
租户管理 租户 CRUD、包年包月/按量/抢占式三种队列 + Namespace/RBAC 自动创建
队列管理 包年包月队列的子队列 CRUD(团队级)、deserved/capability 配置、子队列 deserved 之和校验;按量队列动态扩缩容(平台监听使用量,实时调整 d/c);抢占式队列 capability 按包年包月购买量比例自动计算并更新
任务管理 任务提交(表单→Volcano Job YAML,用户选择队列 + 任务类型,平台自动设置 PriorityClass)、任务列表、日志查看、任务终止
抢占式任务 抢占式提交、价格展示、回收历史
抢占式回收 监听新购包年包月/按量事件 → 检查空闲 → 选择抢占式任务 → Delete → 通知用户
大任务防饥饿 大任务提交时触发 preempt;如资源不足则开启提交限流(Submission Throttle),暂缓低优先级任务提交,大任务运行后解除
网络拓扑管理 HyperNode 自动发现配置(UFM/标签)、拓扑可视化、节点标签管理
镜像管理 基础镜像 + 用户自定义镜像、镜像扫描
数据集管理 数据集挂载、PVC 生命周期管理
模型管理 模型版本、模型上传/下载、与推理服务关联
推理服务 部署/更新/回滚、金丝雀发布、A/B 测试、监控看板
计费 包年包月计费、按量计费(使用时长 × 单价)、抢占式计费(使用时长 × 竞价价格)、账单
监控告警 Grafana 嵌入、告警规则配置、通知渠道

14. 风险与注意事项

  1. 抢占式任务直接 Kill 的影响:用户选择抢占式资源时已明确知道随时可能被终止。上层平台在提交抢占式任务时做强提示。

  2. 层级队列限制:只能在叶子队列提交作业;如果已有任务提交到包年包月父队列则不能创建子队列。上层平台创建/修改子队列时需校验 sum(子队列 deserved) <= 父队列 deserved。

  3. Gang Scheduling 死锁:多个大规模分布式任务同时排队可能互相阻塞。max-scheduling-delay: 120s 可使部分任务回退,给其他任务调度机会。大任务(P800)通过优先级抢占同队列低优先级任务可缓解死锁。

  4. GPU 碎片化:Binpack 策略可减少碎片,但多租户多队列场景下仍需关注。

  5. 网络拓扑感知:分布式训练对网络拓扑敏感,使用 network-topology-aware 插件 + HyperNode CRD。需提前部署 HyperNode(手动或自动发现)。IB 网络需配置 UFM 自动发现;RoCE/以太网需通过节点标签构建拓扑。hard 模式约束过严可能导致大任务 Pending,需确保 HyperNode 内资源充足。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐