大任务防饥饿与重要任务保护方案
1. 问题背景
在 GPU 容器云平台中,大规模分布式训练任务(如需要 32+ GPU 的 DDP/Megatron 任务)面临两个核心调度难题:
问题一:大任务饥饿
大任务需要一次性获得大量资源(Gang Scheduling)
集群中持续有小任务在运行,资源始终被零散占用
大任务每次尝试调度都凑不够 → 反复退回队列 → 永远跑不上
问题二:防饥饿措施可能误伤重要任务
为了让大任务跑上去,提升其优先级去抢占其他任务
但如果抢占了推理服务或关键训练任务 → 生产事故
用户提交模型
用户提交任务时有两个独立的选择维度:
| 维度 | 说明 | 举例 |
|---|---|---|
| 队列 | 选择所属团队队列 | bigdata、data-science、algorithm |
| 任务类型 | 标注任务的类别 | 最高优先 / 大任务 / 正常 / 调试 |
- 队列:由团队管理员创建和维护,用户按团队归属选择
- 任务类型:决定任务的优先级和调度行为,平台自动设置 PriorityClass
任务类型定义
| 任务类型 | 优先级 | 说明 | 抢占行为 |
|---|---|---|---|
| 最高优先任务 | P1000 | 在线推理、高等级训练任务 | 不可被抢占,可以抢占其他 |
| 大任务 | P800 | 大规模分布式训练(32+ GPU) | 可被 P1000 抢占,可抢占 P100 及以下 |
| 正常任务 | P100 | 可重启、可重试的普通任务 | 可被 P800+ 抢占 |
| 调试测试任务 | P50 | 开发调试,允许被中断 | 可被任何优先级抢占 |
2. 队列设计
用户队列由团队管理员自行创建和维护。每个团队一个队列,所有任务类型共享同一个队列,通过优先级调度。
root
│
┌──────────────┼──────────────┐
│ │ │
bigdata-fixed data-science-fixed algo-fixed
d=60, c=120 d=40, c=80 d=20, c=60
调度逻辑很简单:P1000 > P800 > P100 > P50,高优先级任务自然抢占低优先级任务。不需要在队列层面做特殊处理。
3. 防饥饿方案
方案一:优先级抢占(推荐)
利用 Volcano 原生优先级抢占机制。大任务设置高优先级(P800),当资源不足时直接驱逐队列中的低优先级任务来腾出资源。不需要额外的队列结构或预留资源。
为什么主动抢占,而不是等待?
大任务饥饿的根因是"资源永远凑不够"——P100/P50 任务持续提交,释放的资源马上被新的小任务占走。如果大任务只是等待,永远等不到足够资源:
等待模式(无效):
T0: 大任务需要 32 GPU,当前剩余 0
T1: 一个 P100 任务结束,释放 8 GPU → 新来的 P50 立即占走
T2: 又一个 P100 结束,释放 8 GPU → 又被新任务占走
T3: 永远凑不够 → 大任务饿死
主动抢占(有效):
T0: 大任务需要 32 GPU,当前剩余 0
T1: 直接驱逐 4 个 P100 的 Pod,释放 32 GPU
T2: 大任务开始训练
T3: 被驱逐的 P100 任务重新排队,等大任务完成后再跑
抢占策略
| 策略 | 行为 |
|---|---|
| 谁被抢 | 只抢优先级低于 P800 的任务(P100、P50),不碰 P1000 |
| 抢多少 | 刚好够用,用最少的 Pod 凑足需求 |
| 抢的顺序 | 从最低优先级开始抢,优先驱逐 P50 → P100 |
| 抢占后恢复 | 被驱逐的任务重新排队,资源空闲后自动重新调度 |
抢占流程
1. 大任务提交 → 需要 4×8=32 GPU
2. allocate:队列剩余资源不够
3. preempt:同队列内 P800 选择 P100/P50 任务作为牺牲者
4. 驱逐牺牲者 Pod → 释放 GPU
5. 大任务获得 32 GPU → 开始训练
6. 大任务完成 → GPU 空闲
7. 被驱逐的 P100/P50 任务重新调度回来继续运行
提交限流(Submission Throttle)
上述流程有一个隐含风险:被驱逐的 Pod 释放 GPU 后,如果在 Volcano 分配给大任务之前,有新的 P100/P50 任务提交进来,可能抢走刚释放的资源。
Volcano 调度周期内的竞态:
周期 N: preempt 驱逐 4 个 P100 Pod → GPU 释放
↓
[时间窗口——新提交的任务可能在这里抢走 GPU]
↓
周期 N+1: 大任务分配 GPU → 但资源已被新任务占走
解决方案——平台 API 层在大任务抢占期间暂缓提交 P100/P50 任务:
大任务提交时的平台逻辑:
1. 用户提交大任务 → 平台提交到 K8s/Volcano
2. 平台标记该队列进入"大任务等候模式"
3. 队列处于等候模式时,新的 P100/P50 提交在平台层排队(不提交到 K8s)
4. Volcano 调度 → preempt → 释放 GPU → 大任务分配到资源 → 开始运行
5. 平台检测到大任务已运行 → 解除等候模式
6. 排队的 P100/P50 任务提交到 K8s,正常参与调度
| 机制 | 说明 |
|---|---|
| 触发条件 | 大任务提交后初始 allocate 失败(需要 preempt) |
| 限流对象 | 同一队列中优先级低于 P800 的新提交(P100、P50) |
| 解除条件 | 大任务开始运行,或超时(如 5 分钟未分配到资源则放弃限流) |
| 超时处理 | 大任务 5 分钟仍无法获得资源 → 解除限流,不阻塞小任务 |
这种方案的好处:在提交层解决,不需要 Volcano 配置或子队列。
代价:小任务的提交会被短暂延迟(通常秒级到分钟级)。如果大任务始终无法获取足够资源,超时后解除限流,小任务继续正常提交。
资源配置
大任务反饥饿不需要特殊的队列配置。一个标准团队队列即可:
# 团队队列,所有任务共享
apiVersion: scheduling.volcano.sh/v1beta1
kind: Queue
metadata:
name: bigdata
spec:
reclaimable: true
deserved:
nvidia.com/gpu: 60
cpu: "240"
memory: "960Gi"
capability:
nvidia.com/gpu: 120
cpu: "480"
memory: "1920Gi"
任务提交(用户侧)
用户指定所属队列 + 任务类型,平台根据任务类型设置优先级。所有类型都提交到同一个队列:
# 用户提交时指定队列(bigdata)和任务类型(large-job)
apiVersion: batch.volcano.sh/v1alpha1
kind: Job
metadata:
name: large-ddp-training
namespace: bigdata
labels:
platform.io/queue: "bigdata" # 用户选择所属团队队列
platform.io/job-type: "large-job" # 用户选择任务类型
spec:
schedulerName: volcano
queue: bigdata # 所有类型都提交到同一队列
# priorityClassName: 平台根据 job-type 自动设置为 large-job (P800)
minAvailable: 4
networkTopology:
mode: hard
highestTierAllowed: 2
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
limits:
nvidia.com/gpu: 8
restartPolicy: OnFailure
平台逻辑
用户提交任务时:
job_type = request.labels["platform.io/job-type"]
switch job_type:
case "critical":
request.priorityClassName = "critical-job" # P1000
case "large-job":
request.priorityClassName = "large-job" # P800
case "normal":
request.priorityClassName = "normal-job" # P100
case "debug":
request.priorityClassName = "debug-job" # P50
# 队列保持用户指定的不变,不进行特殊路由
request.queue = request.labels["platform.io/queue"]
方案优点
- 用户模型清晰:用户选所属队列 + 选任务类型,不感知优先级数值
- 零额外组件:不需要额外队列、预留资源或 filler controller, Volcano 原生 preempt 即可
- 抢占即时:同队列内 preempt 是调度周期级别的(秒级)
- 按需降级:大任务完成后,被抢占的低优先级任务自动恢复
- 网络拓扑保障:大任务配置
networkTopology: hard,确保所有 Pod 在同一网络性能域内,NCCL 通信性能最优
4. 重要任务保护机制
当大任务通过提权或抢占获得资源时,必须保证重要任务不被误杀。
4.1 优先级分段隔离(核心机制)
设置一条不可逾越的优先级天花板,大任务再怎么提权也不能越过。
优先级分段设计(与用户任务类型对应):
P1000 最高优先任务 (critical-job) ← 在线推理、高等级训练
任何提权都不能到达此级别
════════ 天花板(提权不可逾越)════════════
P800 大任务 (large-job) ← 大规模分布式训练
P100 正常任务 (normal-job) ← 可重启可重试的普通任务
P50 调试测试任务 (debug-job) ← 开发调试,允许被中断
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: 正常任务
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: "开发调试任务,最低优先级,允许被中断"
4.2 上层平台标记"最高优先任务"
用户提交时选择"最高优先任务"类型,平台自动设置 critical-job (P1000) 优先级,物理上不可被抢占。
# 用户提交最高优先任务
apiVersion: batch.volcano.sh/v1alpha1
kind: Job
metadata:
name: important-training
namespace: bigdata
labels:
platform.io/job-type: "critical" # 用户选择"最高优先任务"类型
spec:
schedulerName: volcano
queue: bigdata
# priorityClassName: 平台自动设置为 critical-job (P1000)
# ...
提权逻辑的额外保护(即使优先级数值足够,也跳过受保护任务):
提权时抢占选择逻辑:
func selectVictims(needed_gpu, candidate_jobs):
# 1. 排除所有受保护任务
candidates = filter(candidates, label "platform.io/protected" != "true")
# 2. 排除 P1000 的任务(物理保证,双重保险)
candidates = filter(candidates, priority < 1000)
# 3. 按优先级从低到高选择牺牲者
sort(candidates, by=priority, ascending)
# 4. 选择刚好满足需求的最少任务
victims = []
freed_gpu = 0
for job in candidates:
victims.append(job)
freed_gpu += job.gpu
if freed_gpu >= needed_gpu:
return victims
# 5. 如果排除 protected 后资源仍不够 → 不抢占,继续等待
return None
4.3 Volcano conformance 插件(系统保护)
Volcano 内置的 conformance 插件保护系统级 Pod:
conformance 保护范围:
✓ kube-system namespace 下的所有 Pod
✓ DaemonSet 管理的 Pod
✓ system-node-critical / system-cluster-critical PriorityClass 的 Pod
典型保护对象:
- GPU Device Plugin
- DCGM Exporter
- 网络插件(Calico/Flannel/Cilium)
- CoreDNS
- kube-proxy
4.4 三道防线总结
保护重要任务的三道防线:
第一道:优先级分段隔离
→ 提权上限 P800,最高优先任务 P1000
→ 物理上不可能被抢占(P800 永远抢不了 P1000)
第二道:任务分类控制
→ 只有选择 "critical" 类型的任务才使用 P1000
→ 提权逻辑在选择牺牲者时跳过 P1000 任务
第三道:Volcano conformance
→ 系统 Pod(DaemonSet、kube-system)始终受保护
→ 防止 GPU Device Plugin 等关键组件被驱逐
三道防线独立生效,任何一道都足以保护重要任务。
5. 完整优先级体系总览
优先级值 名称 用途 可被抢占? 能抢占谁?
───────────────────────────────────────────────────────────────────────────
P1000 critical-job 最高优先任务(在线推理/高等级训练) 不可被抢占 P800 以下所有
──────── 天花板(提权不可逾越)────────────────────────────────────────────
P800 large-job 大规模训练任务(32+ GPU) 可被P1000 P100 以下所有
P100 normal-job 普通任务(可重启/重试) 可被P800+ P50 以下所有
P50 debug-job 调试测试任务 可被P100+ 无(Never)
* P1000 不可被抢占:提权上限是 P800,没有合法途径产生 P1000+ 的抢占者
* 用户提交时选择:所属队列(如 bigdata)+ 任务类型(critical/large-job/normal/debug)
* 平台根据任务类型设置 PriorityClass,所有任务提交到同一个团队队列
* 同队列内高优先级自然抢占低优先级,无需额外路由或子队列


被折叠的 条评论
为什么被折叠?



