Volcano异构资源碎片整理
碎片整理(Repack)是Volcano面向异构资源的运行时碎片整理能力。通过不改变工作负载资源请求,调整现有Pod的节点分布,将分散的空闲资源集中到部分节点,释放完整节点容量。本文介绍Repack的功能配置及操作示例。
背景信息
Repack解决的是“集群仍有空闲卡,但资源布局无法满足目标工作负载”这一运行时容量问题。集群长期经历任务结束、扩缩容、滚动更新和故障恢复后,NPU/GPU空闲容量会分散到不同节点;即使设备总量充足,也可能无法满足工作负载所需的完整节点、单Pod卡数或拓扑组合。
能力概述
Repack是Volcano面向异构资源的运行时碎片整理能力。它在不改变工作负载资源请求的前提下调整现有Pod的节点分布,将分散的空闲资源集中到部分节点,释放完整节点容量。规划以节点腾空为目标,以PodGroup识别受影响的工作负载并评估中断影响;执行阶段逐Pod记录驱逐、重建Pod识别和调度结果。
用户通过集群级CRD RepackRun发起一次整理。每个Run面向一种扩展资源,例如昇腾NPU资源 huawei.com/ascend-1980;GPU资源的使用方式相同,并支持两种执行模式:
- DryRun:基于当前集群状态计算方案,不驱逐Pod,用于判断预计释放的完整节点、受影响工作负载和迁移路径。
- Execute:读取集群实时状态并生成可执行方案,通过Kubernetes Eviction API发起迁移,并跟踪重建后的Pod或PodGroup的调度结果。
这两种模式使碎片整理具备明确的目标、驱逐范围、执行预算和结果记录。

能力一览
您可以通过本节判断Repack是否适合当前场景,后续章节再按需查看具体配置和示例:
| 能力 | 说明 |
|---|---|
| 目标与收益 | 指定一种NPU/GPU扩展资源,以碎片率下降和完整节点释放衡量容量改善。 |
| 工作负载驱逐范围 | 按标签或名称选择允许被驱逐的工作负载,并限定可腾空节点。 |
| 调度可行性 | 使用Volcano Scheduler的调度规则验证接收节点,避免仅按资源数量生成迁移方案。 |
| Gang中断影响评估 | 识别VCJob、ModelServing等工作负载的PodGroup,减少受影响工作负载数量,并评估迁移后PodGroup是否仍满足MinAvailable |
| 节点选择与装箱 | 动态选择腾空节点,并按best-fit等策略填充接收节点,抑制二次碎片。 |
| 变更影响范围控制 | 限制单次影响的工作负载数量和目标资源迁移量。 |
| 候选节点建议 | 通过nominatedNodeName提高重建Pod调度到建议节点的概率,不进行强制绑定或资源预留。 |
| 结果记录与验证 | 记录受影响的工作负载、逐Pod驱逐与调度过程,以及最终节点腾空结果。 |
Repack当前适合由平台主动发起的周期性碎片整理、大规格训练任务提交前容量准备,以及节点维护前的目标资源腾空。它不会持续监听Pending作业并自动触发,也不会为后续任务预留已释放容量。
前提条件
- 已创建v1.30及以上版本的集群,详情请参见购买Standard/Turbo集群。
- 已安装Volcano插件,且插件版本在1.22.36及以上,版本详情请参见Volcano调度器。
- 已选择Volcano为默认调度器,详情请参见调度配置。
约束与限制
- Repack仅支持NPU/GPU等异构资源的碎片整理,不支持CPU、Memory等通用资源。通用资源的碎片整理请参见重调度功能。
- 仅支持节点级碎片整理,不支持超节点维度的碎片整理。
- 为兼顾大规模集群下的规划时延与计算开销,Repack采用启发式搜索算法,不承诺全局最优解,仅推荐达到收益门槛的调度方案。
- Repack不锁定资源,也不暂停常规调度。在并发调度或任务删除等动态场景下,实际整理结果可能与计划产生偏差,属于预期行为。
- Repack不会提前规划PDB(Pod Disruption Budget)规避策略。当PDB不允许驱逐时,对应的迁移将失败。
- Repack不感知上层控制器的级联重建成本。当VCJob配置了PodEvicted生命周期策略或ModelServing配置了ServingGroupRecreate时,单个Pod的驱逐可能触发多个Pod的重建,实际中断范围可能大于计划迁移的Pod数量。
- 当前仅支持昇腾A2系列、310P常规机型;暂不支持A3系列及310P高密超节点机型。
- 使用Repack碎片整理能力时,建议开启“启用装箱策略 (Binpack)”开关,并配置对应GPU/NPU扩展资源类型的装箱权重,可达成更好的碎片整理效果。
开启方式
- 登录CCE控制台,单击集群名称进入集群。
- 单击左侧导航栏的“配置中心”,切换至“调度配置”页面,选择Volcano调度器找到对应的“专家模式”,单击“开始使用”。

- 将配置项repack_enable的值修改为true,单击“保存”,以开启Repack重调度能力。

- 单击左侧导航栏的“插件中心”,在右侧找到Volcano调度器,单击“插件详情”,在“实例列表”页签,待volcano-repack-engine“状态”为“运行中”,即可开始碎片整理功能使用。
- (可选)在“配置中心 > 调度配置”页签的“资源利用率优化调度”区域,打开“启用装箱策略 (binpack)”的开关,单击“添加自定义资源类型”,根据集群的异构资源类型添加自定义资源装箱声明,比如:huawei.com/ascend-1980或者nvidia.com/gpu。装箱调度更多信息,请参见装箱调度(Binpack)。

功能详解
Repack会基于当前集群资源快照,在用户授权的工作负载范围和单次迁移预算内,快速寻找一个能够释放完整加速卡节点且中断影响较低的可执行方案。规划采用启发式搜索,因此不承诺枚举所有迁移组合或获得数学意义上的全局最优。系统优先保证调度可行性、影响范围可控和结果可审核;实际执行仍受实时资源竞争、PDB和控制器行为影响,应以Execute的最终结果为准。
以完整节点释放衡量整理收益
对于具有固定单Pod卡数或整节点需求的工作负载,空闲卡能否用于调度,取决于单个节点的剩余容量是否满足其资源请求。Repack保持现有资源请求总量不变,通过减少目标资源的占用节点数即腾空目标资源的整节点来衡量碎片改善。
碎片率计算
对于目标资源R:
当前占用节点数 - 理论最少占用节点数
碎片率(R) = ------------------------------------------------------- × 100%
提供资源R的节点数 - 提供资源的节点数:Allocatable[R] > 0 的节点数。
- 当前占用节点数:正在使用资源R的节点数。
- 理论最少占用节点数:保持当前资源请求不变,紧凑装箱所需的最少节点数。
例如,20个NPU节点中有15个正在使用,理论上最少只需占用12个节点。本次计划再释放2个节点:
- 整理前:(15 - 12) / 20 × 100% = 15%
- 整理后:(13 - 12) / 20 × 100% = 5%
- 改善幅度:10个百分点
minFragImprovementPercent: 10表示碎片率至少下降10个百分点。碎片率按全集群统计,scope.nodes只限制可腾空节点,不改变统计范围。该指标衡量布局紧凑程度,不代表某个未来任务一定可以调度;计划中的每个迁移仍需通过Scheduler可行性验证。
Repack使用goals指定目标资源和最低改善幅度。方案必须至少释放一个目标资源节点,并达到配置的改善阈值。
配置示例
goals:
- resource: huawei.com/ascend-1980
# 碎片率至少下降 10 个百分点,本次整理才具有执行价值。
minFragImprovementPercent: 10 布局已足够紧凑时,Run以NoFragmentation结束;存在碎片但无法达到收益门槛时,以InsufficientImprovement结束。两者都属于正常评估结果。
使用Scope限定驱逐范围
碎片整理会驱逐运行中的工作负载。scope从两个维度限定本次操作范围:
- scope.podGroups:定义哪些工作负载的目标资源Pod允许被驱逐。
- scope.nodes:定义哪些节点允许作为腾空目标。
例如,训练、推理工作负载部署在同一集群时,可以允许低优先级训练和离线推理参与整理,同时排除关键在线推理;节点侧还可以只选择某个节点池作为本次腾空范围。
配置示例
scope:
podGroups:
include:
selector:
matchLabels:
repack.volcano.sh/eligible: "true"
exclude:
selector:
matchLabels:
repack.volcano.sh/protected: "true"
nodes:
include:
selector:
matchLabels:
accelerator-pool: ascend-npu
exclude:
names: [ascend-maintenance-01] VCJob controller会把Job标签复制到其PodGroup,Kthena会把ModelServing标签复制到各ServingGroup对应的PodGroup;通用pg-controller也会继承Deployment、StatefulSet等工作负载Pod模板中的稳定标签。用户只需在工作负载上维护标签,再通过标签选择器配置驱逐范围,无需识别和维护自动生成的PodGroup名称。
同一include或exclude中,名称列表与标签选择器取并集,且exclude的优先级更高。PodGroup名称采用namespace/name,节点直接使用节点名。
scope.nodes限定的是“哪些节点允许被腾空”,并不限定Pod的接收节点。如果工作负载不能离开指定资源池,应通过节点亲和性、污点/容忍或其他原生调度约束表达;Repack会在规划时继承这些约束。
基于完整调度语义验证迁移可行性
Pod请求的设备量小于节点空闲量,只能说明计划在资源总量上成立,并不能证明Pod可以实际调度。Repack在生成计划时复用Volcano Scheduler的调度策略进行节点过滤,对每个待迁移Pod模拟重新调度,验证其原有调度约束和节点剩余资源是否允许迁移。
一个节点能否作为腾空候选,也通过统一的可迁移性判断完成:
- 不申请本次目标资源的DaemonSet、CNI、kube-proxy和普通Pod不参与迁移,也不会阻止目标资源腾空。
- 申请目标资源的Pod必须由Volcano调度、关联可识别的PodGroup,并属于scope.podGroups允许驱逐的工作负载。
- 任一目标资源Pod不可迁移,或者任一待迁移Pod没有可行接收节点,该候选即被淘汰。
模拟过程会累计已经规划的迁移,避免多个局部可行方案组合后超过接收节点容量。它验证的是当前快照下是否存在完整迁移路径,不保证未来集群状态。模拟选出的接收节点不会触发资源预留;Execute会重新规划,并通过nominatedNodeName提供候选节点建议,最终绑定结果仍由Scheduler根据实时状态决定。
以Gang语义评估工作负载中断影响
在分布式训练和模型推理中,整理方案不应将迁移分散到大量工作负载。即使迁移Pod总数相同,从一个工作负载迁移多个Pod通常也比从多个工作负载各迁移一个Pod具有更小的影响范围。Repack因此以PodGroup作为中断影响统计单元,对每个可行腾空方案计算多维成本。
是否低于Gang最小可用成员数
对于一个PodGroup,Repack从Scheduler快照读取Running成员数和MinAvailable,计算不影响Gang最小可用条件的最大迁移Pod数:
可安全迁移的 Pod 数 = max(Running - MinAvailable, 0)
- 计划迁移数不超过该值:PodGroup的可用副本数仍满足MinAvailable,受影响目标资源量按实际迁移量计算。
- 计划迁移数超过该值:PodGroup将低于MinAvailable,低于最小可用成员数的PodGroup数量增加;受影响目标资源量按整个PodGroup的目标资源占用量计算。
例如,一个训练任务当前有8个Running Worker,minAvailable: 6:
- 迁移1~2个Worker 后仍满足minAvailable;
- 迁移第 3个Worker后低于minAvailable,Repack将整个PodGroup的NPU占用计入受影响目标资源量。
多维中断影响评分
每个可行候选都按完整计划计算五个维度,而不是只评价当前节点上的局部Pod:
- 受影响工作负载数,默认权重1.0:尽量减少同时受到迁移影响的训练或推理工作负载。
- 低于 MinAvailable 的 PodGroup 数,默认权重0.8。
- 受影响目标资源量,默认权重0.6:PodGroup仍满足MinAvailable时计算实际迁移卡数,否则计算整个PodGroup的卡数。
- 迁移目标资源量,默认权重0.3:在工作负载影响接近时减少迁移的NPU/GPU数量。
- 迁移Pod数,默认权重0.1:进一步减少驱逐与重建对象数量。
每个维度都在当前一轮候选之间做 Min-Max 归一化,再乘以默认权重求和,总分越低,方案越优先。权重体现的是综合偏好,不是严格的逐级排序;分数也只用于同一规划轮次内的相对选择,不能跨集群或跨Run比较。
例如,三个方案都能释放一个节点并迁移3张卡:方案 A 从同一工作负载迁移3个Pod,且迁移后仍满足MinAvailable;方案B从3个工作负载各迁移1个Pod;方案C只影响1个工作负载,但迁移后低于 MinAvailable。综合评分通常优先方案A;方案B的工作负载影响范围更大,方案C在低于MinAvailable的PodGroup数和受影响目标资源量两个维度上的成本更高。
scope和maxPerRun是硬性限制,中断影响评分只用于在满足限制的候选方案中排序。评分倾向于减少受影响工作负载,并避免使 PodGroup低于MinAvailable,但不会将其作为禁止条件。不可中断的工作负载应通过scope.podGroups.exclude明确排除。

动态选择腾空目标,并抑制二次碎片
Repack的规划不是预先排好一次节点顺序后逐个执行。规划器采用增量方式工作:每选定一个腾空方案,都会把相应迁移计入模拟状态,再重新评估剩余候选。这样可以处理接收容量被前序迁移消耗、PodGroup 影响范围扩大,以及后续节点腾空代价发生变化等情况。
腾空候选的选择
每一轮首先排除不满足可迁移性、执行预算、接收容量或Scheduler可行性的候选,再按中断影响总分从低到高选择。该总分同时考虑受影响工作负载数、低于MinAvailable的PodGroup数、受影响目标资源量、迁移资源量和迁移Pod数。分数相同时,继续比较腾空收益和候选名称,以获得稳定结果。
因此,Repack选择的不是“当前空闲卡最多的节点”,而是“能够腾空、具备完整迁移路径,并且工作负载中断影响相对较低的节点”。

接收节点的填充
把Pod从源节点移走并不等于碎片得到改善。如果接收节点选择不当,迁移可能只是把碎片从一个位置转移到另一个位置。Repack按以下原则组织接收节点:
- 排除正在腾空的节点,并避免重新占用目标资源完全空闲的节点;
- 优先填充因为工作负载不允许被驱逐、范围限制或既有迁移而确定会保持占用的节点;
- 在仍可能被后续整理的节点中,优先使用未来腾空代价更高的节点,保留更容易释放的节点;
- 对同类候选节点,优先选择迁入后目标资源余量更小的节点。
该策略在实现当前节点腾空目标的同时,尽量为后续任务和下一轮整理保留紧凑的资源布局。节点排序只决定模拟顺序,最终候选仍需通过Scheduler的完整调度过滤。

通过执行预算限制单次影响范围
maxPerRun从工作负载数量和目标资源数量两个维度限制单次Run的变更范围:
- podGroups:最多影响多少个工作负载;系统内部按不同PodGroup计数。
- resources.<resource-name>:最多迁移多少单位目标资源。
podGroups是API字段名,用户不需要据此维护 PodGroup。VCJob通常对应一个PodGroup;ModelServing的每个ServingGroup分别对应一个PodGroup,因此应按实际可能受影响的ServingGroup数量设置预算。
配置示例
maxPerRun:
# 本次最多影响两个训练或推理工作负载。
podGroups: 2
resources:
# 即使工作负载规模更大,本次最多迁移8张卡。
huawei.com/ascend-1980: 8 任何超过上限的候选都会在规划阶段被淘汰。首次上线建议从一个工作负载和较小的设备数量开始,结合DryRun、历史恢复时长和运维窗口逐步扩大预算。
Execute在集群内串行运行,并在一次执行完成后进入冷却时间,避免多轮整理相互影响。Pod驱逐通过Kubernetes Eviction API完成,API Server会根据实时状态检查PDB;DryRun不保证Execute时PDB仍允许驱逐。

通过nominatedNodeName提供候选节点建议
规划阶段给出的接收节点基于当时的集群快照。进入Execute 后,资源占用和调度队列仍可能变化,因此Repack不会将计划节点作为强制绑定结果。
原Pod被驱逐后,工作负载控制器创建新的Pod。Volcano Controller Manager内置的Repack controller根据持久化的relocation记录识别重建后的Pod,在确认候选节点仍然可行后,将节点名写入pod.status.nominatedNodeName。Volcano Scheduler优先评估该节点,同时继续应用完整的调度规则。
- 建议节点仍可用时,重建后的Pod优先调度到该节点,提高计划完成概率。
- 建议节点不可用时,Scheduler可以选择其他可行节点,避免过期计划阻塞工作负载恢复。
- Repack不通过cordon、污点或资源预留限制节点使用,调度队列中的其他工作负载仍可参与调度。
- status.relocations[].placement记录规划节点、执行时选择的节点和实际绑定节点,用于识别替代调度及超时。
nominatedNodeName是调度建议,不是节点绑定或资源预留。

从方案评估到结果验证形成闭环
DryRun和Execute使用同一套规划逻辑,但承担不同职责。DryRun用于确认收益门槛、受影响工作负载和迁移路径,相关信息保存在status.plan.summary、status.plan.moves和status.plan.freedNodes中。
审核通过后,应创建一个新的Execute Run,基于实时状态重新规划。Execute将逐Pod的驱逐和调度过程写入status.relocations,将实际腾空节点和最终碎片改善写入status.result。
计划值和实际值不同并不一定表示故障。差异可能来自PDB状态变化、重建后的Pod调度到其他节点,或者执行期间出现新的资源竞争。用户可以同时查看计划、执行过程和实际结果,判断本次整理是否达到容量目标。
工作负载驱逐与重建机制
VCJob的驱逐恢复
VCJob支持用户定义负载级的policy能力,比如VCJob额外配置了event: PodEvicted 与 action: RestartJob、RestartTask等生命周期策略,一次Repack驱逐可能触发整个工作负载或同一任务角色下的全部Pod重启,使实际中断范围远大于计划迁移的Pod。准备纳入Repack的VCJob不建议配置这类策略;确有整体重启语义时,应按所有受影响Pod评估恢复成本,并使用更严格的Scope和执行预算。
ModelServing的驱逐恢复
ModelServing的 spec.replicas 表示创建多个相互独立的服务分组(ServingGroup),而不是只创建对应数量的Pod。每个服务分组包含一个entry Pod和一个worker Pod,并共用一个PodGroup。Repack的 maxPerRun.podGroups 按这些服务分组对应的PodGroup计数。
示例使用 recoveryPolicy: ServingGroupRecreate。在该模式下,一个Pod被Repack驱逐后,Kthena不只补回这个Pod,而会删除同一服务分组内的全部Pod和旧PodGroup,再创建新的PodGroup和全部Pod。因此,最终效果可能是“Repack只驱逐一个Pod,但控制器重建整个ServingGroup”。
- restartGracePeriodSeconds 控制异常Pod触发ServingGroup恢复前的等待时间;它不同于 spec.eviction.gracePeriodSeconds,后者控制Repack Eviction请求的Pod优雅终止时间。
- Repack保留原PodGroup作为计划审计身份,通过相同ModelServing owner识别新一代PodGroup;名称变化时在 relocations[].replacementPodGroupName 中记录新名称,复用原名时该字段可以为空。
- 新PodGroup中的重建Pod经过placement lease和SchedulerGate协调后,由nominatedNodeName获得候选节点建议,最终绑定仍由Volcano Scheduler决定。
这种恢复语义会扩大工作负载中断范围——maxPerRun限制的是Repack计划内的PodGroup和目标资源迁移量,不能替代ModelServing自身的可用性设计。生产推理服务应至少保留其他可用ServingGroup、确保流量摘除和模型预热能够完成,并将一个完整ServingGroup作为中断影响评估单元。不能接受ServingGroup重建的ModelServing应标记为 repack.volcano.sh/protected=true,由Scope明确排除。
操作示例
以下示例以昇腾NPU资源huawei.com/ascend-1980为例。不同设备插件或驱动版本上报的资源名可能不同,请以节点status.allocatable中的实际资源名为准。GPU集群的使用流程相同,只需将目标资源替换为nvidia.com/gpu。
示例中涉及的节点名、PodGroup 名和资源容量需按实际集群调整。所有字段均为按需添加——不需要的能力不必写入 CR。
步骤一:执行DryRun评估
首次使用只需指定mode和目标资源。以下CR会在提供huawei.com/ascend-1980的节点上评估碎片和迁移可行性,不会驱逐任何Pod:
apiVersion: repack.volcano.sh/v1alpha1
kind: RepackRun
metadata:
name: ascend-npu-dryrun
spec:
mode: DryRun
goals:
- resource: huawei.com/ascend-1980 执行提交命令并观察。
kubectl apply -f ascend-npu-dryrun.yaml kubectl get repackrun ascend-npu-dryrun -w kubectl get repackrun ascend-npu-dryrun -o yaml
重点关注status.conditions和status.plan。DryRun下status.phase=Succeeded仅表示评估正常完成,不代表已执行迁移。condition含义:
- RepackRecommended:存在具有正收益的可行计划,可以继续增加安全控制并审核。
- NoFragmentation:当前目标资源无需整理。
- InsufficientImprovement:存在碎片,但当前没有达到要求的可行计划。
未填scope表示不限制工作负载与腾空节点;未填maxPerRun表示不限制单次影响面。此最小配置适合首次评估。进入Execute前,应按需补齐scope与maxPerRun。
从这份基线CR扩展时,只需按实际需求增加对应字段:
- 限定允许驱逐的工作负载或节点池:配置scope字段。
- 过滤收益过低的计划:配置goals[].minFragImprovementPercent字段。
- 限制单次影响的工作负载数和卡数:配置maxPerRun字段。
- 执行迁移:新建RepackRun并将mode改为Execute。
- 调整Pod优雅终止时间或CR保留时间:配置eviction或ttlSecondsAfterFinished字段。
步骤二:(可选)准备工作负载与节点标签
如果你的VCJob、ModelServing已经配置了可供RepackRun scope筛选的稳定标签,可跳过本节,直接进入步骤三:按需增加Scope。
本节仅用于演示如何为示例环境准备标签和示例工作负载。
- 为节点打标签。
kubectl label node ascend-node-01 accelerator-pool=ascend-npu kubectl label node ascend-node-02 accelerator-pool=ascend-npu kubectl label node ascend-node-03 accelerator-pool=ascend-npu
确认节点已正确上报NPU容量。
kubectl describe node ascend-node-01
- 创建示例工作负载。
以下示例创建一个VCJob训练任务和一个Kthena ModelServing推理服务。两类工作负载均显式标记允许参与整理,生产环境中不允许迁移的在线服务应设置repack.volcano.sh/protected=true(此处仅为示例,您可自定义lable),并在后续Scope中排除。
apiVersion: v1 kind: Namespace metadata: name: repack-demo --- apiVersion: batch.volcano.sh/v1alpha1 kind: Job metadata: name: ascend-batch-training namespace: repack-demo labels: workload-type: training repack.volcano.sh/eligible: "true" repack.volcano.sh/protected: "false" spec: schedulerName: volcano queue: default minAvailable: 2 tasks: - name: worker replicas: 2 template: spec: nodeSelector: accelerator-pool: ascend-npu terminationGracePeriodSeconds: 30 containers: - name: trainer image: alpine:3.20 command: ["sh", "-c", "tail -f /dev/null"] resources: requests: huawei.com/ascend-1980: 1 limits: huawei.com/ascend-1980: 1 restartPolicy: Never --- apiVersion: workload.serving.volcano.sh/v1alpha1 kind: ModelServing metadata: name: ascend-online-serving namespace: repack-demo labels: workload-type: inference repack.volcano.sh/eligible: "true" repack.volcano.sh/protected: "false" spec: schedulerName: volcano replicas: 2 recoveryPolicy: ServingGroupRecreate template: restartGracePeriodSeconds: 30 gangPolicy: minRoleReplicas: inference: 1 roles: - name: inference replicas: 1 workerReplicas: 1 entryTemplate: spec: nodeSelector: accelerator-pool: ascend-npu containers: - name: leader image: alpine:3.20 command: ["sh", "-c", "tail -f /dev/null"] resources: requests: huawei.com/ascend-1980: 1 limits: huawei.com/ascend-1980: 1 restartPolicy: Always workerTemplate: spec: nodeSelector: accelerator-pool: ascend-npu containers: - name: worker image: alpine:3.20 command: ["sh", "-c", "tail -f /dev/null"] resources: requests: huawei.com/ascend-1980: 1 limits: huawei.com/ascend-1980: 1 restartPolicy: Always - 执行以下命令。
kubectl apply -f ascend-workloads.yaml kubectl get vcjob -n repack-demo kubectl get modelserving -n repack-demo kubectl get pod -n repack-demo -o wide kubectl get podgroup -n repack-demo --show-labels
VCJob controller为一个Job创建一个PodGroup,并把VCJob metadata.labels复制到PodGroup;Kthena为每个ServingGroup创建一个PodGroup,并把ModelServing metadata.labels复制到这些PodGroup。用户只需在工作负载CR上维护标签,后续即可通过scope.podGroups.selector同时选择训练和推理工作负载。
步骤三:按需增加Scope
最小DryRun会在全局范围评估。如果只允许带repack.volcano.sh/eligible=true标签的工作负载参与整理,并且只允许从accelerator-pool=ascend-npu节点池选择腾空目标,请复制最小CR、使用新的名称,并在spec中增加:
metadata:
name: ascend-scoped-dryrun
spec:
# 保留基线中的 mode 和 goals,在同级增加 scope。
scope:
podGroups:
include:
selector:
matchLabels:
repack.volcano.sh/eligible: "true"
nodes:
include:
selector:
matchLabels:
accelerator-pool: ascend-npu 如果还需要排除关键在线推理等不可中断工作负载,只需在现有scope.podGroups下增加exclude;需要排除维护中的节点时,在scope.nodes下增加exclude:
spec:
scope:
podGroups:
# 保留已有 include。
exclude:
selector:
matchLabels:
repack.volcano.sh/protected: "true"
nodes:
# 保留已有 include。
exclude:
selector:
matchLabels:
maintenance-state: draining 以上代码块是需要合并到基线CR的字段片段,不是两个独立的RepackRun。
kubectl apply -f ascend-scoped-dryrun.yaml
kubectl get repackrun ascend-scoped-dryrun -w
# 确认 selector 实际解析出的 PodGroup 和节点数量。
kubectl get repackrun ascend-scoped-dryrun \
-o jsonpath='podGroups={.status.plan.summary.resolvedScope.podGroupCount}, nodes={.status.plan.summary.resolvedScope.nodeCount}{"\n"}'
# 查看受影响的工作负载标识、计划迁移的 Pod 及源节点/接收节点。
kubectl get repackrun ascend-scoped-dryrun \
-o jsonpath='{range .status.plan.moves[*]}{.owner.kind}{"/"}{.owner.name}{"\t"}{.namespace}{"/"}{.podGroupName}{"\t"}{.cards}{" cards\n"}{range .pods[*]}{" "}{.name}{": "}{.fromNode}{" -> "}{.toNode}{"\n"}{end}{end}'
kubectl get repackrun ascend-scoped-dryrun \
-o jsonpath='{.status.plan.freedNodes}{"\n"}' 检查结果时应确认:
- resolvedScope的数量符合预期。
- plan.moves[].owner只包含Scope允许且未受保护的VCJob或ModelServing。
- plan.freedNodes只包含accelerator-pool=ascend-npu的节点。
步骤四:加入收益门槛和执行预算
在生产Execute前,建议继续按需增加以下字段。
- 需要避免执行收益过低的计划时,在已有goals条目中增加最低碎片改善幅度:
- 需要限制单次影响范围时,增加maxPerRun。两个上限同时生效:
- 需要自动清理已完成的DryRun时,再增加TTL:
以上字段均为可选扩展。将目标资源、Scope、收益门槛和执行预算合并后,推荐的生产DryRun如下:
apiVersion: repack.volcano.sh/v1alpha1
kind: RepackRun
metadata:
name: ascend-guarded-dryrun
spec:
mode: DryRun
scope:
podGroups:
include:
selector:
matchLabels:
repack.volcano.sh/eligible: "true"
exclude:
selector:
matchLabels:
repack.volcano.sh/protected: "true"
nodes:
include:
selector:
matchLabels:
accelerator-pool: ascend-npu
goals:
- resource: huawei.com/ascend-1980
minFragImprovementPercent: 10
maxPerRun:
podGroups: 1
resources:
huawei.com/ascend-1980: 4
ttlSecondsAfterFinished: 86400 执行以下命令:
kubectl apply -f ascend-guarded-dryrun.yaml
kubectl get repackrun ascend-guarded-dryrun -w
# 一次核对预期收益、腾空节点数和迁移卡数。
kubectl get repackrun ascend-guarded-dryrun \
-o jsonpath='fragmentation={.status.plan.summary.fragBeforePercent}% -> {.status.plan.summary.fragAfterPercent}%, freedNodes={.status.plan.summary.freedNodeCount}, movedCards={.status.plan.summary.movedCardCount}{"\n"}' 假设DryRun输出如下,表示迁移批量训练PodGroup的2张卡,预计释放ascend-node-01,碎片率从33%降至0%:
status:
phase: Succeeded
plan:
summary:
fragBeforePercent: 33
fragAfterPercent: 0
freedNodeCount: 1
movedCardCount: 2
resolvedScope:
podGroupCount: 1
nodeCount: 3
moves:
- namespace: repack-demo
podGroupName: ascend-batch-training-2f6c840d-8b14-4e45-a4cf-b43c249acbfd
owner:
apiVersion: batch.volcano.sh/v1alpha1
kind: Job
name: ascend-batch-training
cards: 2
pods:
- name: ascend-batch-training-worker-0
fromNode: ascend-node-01
toNode: ascend-node-02
cards: 1
- name: ascend-batch-training-worker-1
fromNode: ascend-node-01
toNode: ascend-node-03
cards: 1
freedNodes:
- ascend-node-01
conditions:
- type: Complete
status: "True"
reason: RepackRecommended 步骤五:创建独立Execute Run
DryRun和Execute是两种独立执行模式。Execute不会执行旧DryRun中保存的计划,而会基于实时状态重新规划。审核生产DryRun后,复制其完整YAML,保留已经确认的Scope、目标和执行预算,只修改以下字段:
metadata:
# 必须使用新名称创建新的 RepackRun。
name: ascend-execute-001
labels:
change-ticket: change-20260730
spec:
# 将 DryRun 改为 Execute。
mode: Execute
# 可选:覆盖 Eviction API 使用的优雅终止时间。
eviction:
gracePeriodSeconds: 30
# 可选:Execute 结果保留 7 天用于审计。
ttlSecondsAfterFinished: 604800 该代码块是相对于生产DryRun的修改片段。创建Execute文件时,仍需保留原CR中的scope、goals和maxPerRun。
kubectl apply -f ascend-execute-001.yaml kubectl get repackrun ascend-execute-001 -w # 持续观察被驱逐 Pod、重建 Pod 和节点变化。 kubectl get pod -n repack-demo -o wide -w
Execute使用Kubernetes Eviction API,PDB仍然生效。若另一个Execute正在运行或仍处于执行冷却时间,新Run会保持Pending,可通过Conditions中的 AnotherRunActive或ExecuteCooldownActive识别。
步骤六:验证Execute是否生效
- 检查Conditions和最终收益。
kubectl get repackrun ascend-execute-001 -o wide kubectl get repackrun ascend-execute-001 \ -o jsonpath='{range .status.conditions[*]}{.type}{"\t"}{.status}{"\t"}{.reason}{"\t"}{.message}{"\n"}{end}' # 对比计划值和实际值。 kubectl get repackrun ascend-execute-001 \ -o jsonpath='plan: frag {.status.plan.summary.fragBeforePercent}% -> {.status.plan.summary.fragAfterPercent}%, freed {.status.plan.summary.freedNodeCount}, moved {.status.plan.summary.movedCardCount}{"\n"}result: frag {.status.result.fragAfterPercent}%, freed {.status.result.freedNodeCount}, moved {.status.result.movedCardCount}, verified {.status.result.metricsVerified}{"\n"}' - 检查每个被驱逐Pod对应的重建Pod和实际调度节点。
kubectl get repackrun ascend-execute-001 \ -o jsonpath='{range .status.relocations[*]}{.namespace}{"/"}{.victimPodName}{"\teviction="}{.eviction.phase}{"\tplacement="}{.placement.phase}{"\tplanned="}{.plannedNodeName}{"\tselected="}{.placement.selectedNodeName}{"\tactual="}{.placement.actualNodeName}{"\treplacement="}{.placement.replacementPodName}{"\n"}{end}' # 确认 VCJob、ModelServing和Pod恢复正常,并检查计划腾空节点。 kubectl get vcjob -n repack-demo kubectl get modelserving -n repack-demo kubectl get podgroup -n repack-demo kubectl get pod -n repack-demo -o wide kubectl get pod -A --field-selector spec.nodeName=ascend-node-01 -o wide kubectl describe node ascend-node-01涉及字段如下:
字段
说明
plannedNodeName
Execute阶段计算出的目标节点,作为不可变计划记录。
selectedNodeName
Repack根据实时调度快照,为重建Pod写入nominatedNodeName的节点。
actualNodeName
Volcano Scheduler最终绑定的节点。由Repack Controller观察重建Pod的spec.nodeName后写入。
nominatedNodeName不会强制绑定或预留资源,因此actualNodeName可能与前两个字段不同。若调度到其他节点后仍达到腾空目标,Run可以以 ExecutionCompletedWithAlternativePlacement成功结束。
- 特殊场景:ModelServing验证
步骤七:在整理后的节点上提交大型训练任务
Repack仅负责整理节点资源,不会为后续任务强制预留容量。因此,在完成 Execute 并确认节点释放后,应尽快下发大型训练任务,并通过工作负载的节点约束,将任务调度至整理好的昇腾资源池。
- 任务提交示例。该Volcano Job需要2个Worker同时准入,每个Worker占用4张NPU卡,合计8卡——恰好可由一次整理后释放出的整节点承载。
apiVersion: batch.volcano.sh/v1alpha1 kind: Job metadata: name: ascend-large-training namespace: repack-demo labels: workload-type: training spec: schedulerName: volcano queue: default minAvailable: 2 tasks: - name: worker replicas: 2 template: spec: nodeSelector: accelerator-pool: ascend-npu containers: - name: trainer image: alpine:3.20 command: ["sh", "-c", "tail -f /dev/null"] resources: requests: huawei.com/ascend-1980: 4 limits: huawei.com/ascend-1980: 4 restartPolicy: Never - 执行并验证任务。
kubectl apply -f ascend-large-training.yaml kubectl get job.batch.volcano.sh ascend-large-training -n repack-demo kubectl get pod -n repack-demo -l volcano.sh/job-name=ascend-large-training -o wide
请根据节点实际卡数和训练并行策略调整replicas、minAvailable与单Pod NPU申请量。本验证关注的是整节点释放容量对任务的承载性,而非空闲卡的累计数量。
完整RepackRun CR示例
下面的实例覆盖当前全部用户可配置的spec字段。RepackRun是集群级资源,不填写metadata.namespace。status由Repack组件维护,不应写入用户YAML。
YAML示例
apiVersion: repack.volcano.sh/v1alpha1
kind: RepackRun
metadata:
# RepackRun 是集群级资源,不设置 namespace。
name: ascend-complete-execute-001
labels:
# metadata.labels 仅用于检索和审计,不决定整理范围。
accelerator-pool: ascend-npu
change-ticket: change-20260730
spec:
# DryRun 只生成计划;Execute 会重新规划并通过 Eviction API 驱逐 Pod。
mode: Execute
# 限定允许驱逐的工作负载和允许作为腾空目标的节点。
# scope 为空时,这两个维度均不限制范围。
scope:
podGroups:
# 工作负载驱逐范围。selector 匹配继承到 PodGroup 的工作负载标签。
include:
# 同一 include 内,selector 与 names 按并集计算。
selector:
matchLabels:
repack.volcano.sh/eligible: "true"
# PodGroup 名称格式为 namespace/name;自动生成的 PodGroup 推荐使用 selector。
names:
- repack-demo/manually-managed-training-pg
# exclude 优先级高于 include,命中的工作负载不会被驱逐。
exclude:
selector:
matchExpressions:
- key: repack.volcano.sh/protected
operator: In
values:
- "true"
names:
- repack-demo/protected-training-pg
nodes:
# 只限定可作为腾空目标的节点,不限制待迁移 Pod 的接收节点。
include:
# selector 与 names 按并集计算。
selector:
matchLabels:
accelerator-pool: ascend-npu
names:
- ascend-node-03
# 从腾空目标中排除维护中或本次不允许整理的节点。
exclude:
selector:
matchLabels:
maintenance-state: draining
names:
- ascend-node-08
# 指定本次整理的目标扩展资源;当前每个 Run 最多配置一种资源。
goals:
- resource: huawei.com/ascend-1980
# 要求碎片率至少下降 10 个百分点,未达到时不推荐执行。
minFragImprovementPercent: 10
# 限制单次 Execute 的影响范围;任一上限被突破时,候选方案会被过滤。
maxPerRun:
# 最多影响 2 个工作负载,系统内部按不同 PodGroup 计数。
podGroups: 2
resources:
# 本次最多迁移 8 张昇腾 NPU 卡。
huawei.com/ascend-1980: 8
eviction:
# 覆盖 Eviction 请求的优雅终止时间;省略时使用 Pod 自身配置。
gracePeriodSeconds: 30
# Run 进入终态 7 天后自动删除;删除 CR 不会回滚已完成的迁移。
ttlSecondsAfterFinished: 604800 同一include或exclude内的selector与names按并集计算,exclude的优先级高于include。PodGroup名采用namespace/name,节点名直接填写Kubernetes Node名。自动生成的PodGroup 名不稳定,生产配置优先使用从工作负载继承的标签选择。
核心status示例(只读)
以下展示一个Execute成功后的核心状态。status由Repack Engine和Volcano Controller Manager内置的Repack controller协同更新,用户不应手工填写。DryRun同样包含生命周期字段、Conditions和plan,但不会生成result和relocations。
status:
# 简化生命周期:Pending、Running、Succeeded 或 Failed。
# Conditions 是权威状态,phase 由 Conditions 派生。
phase: Succeeded
# 面向操作者的当前结论摘要。
message: "Repack succeeded for huawei.com/ascend-1980: all 1 replacement Pod was scheduled and all 1 planned node was verified free [ascend-node-01]; cluster fragmentation changed from 33% to 0%."
# 首次进入 Running 和首次进入终态的时间。
# completionTime 也是 ttlSecondsAfterFinished 的计时起点。
startTime: "2026-07-30T10:00:00Z"
completionTime: "2026-07-30T10:03:20Z"
# DryRun 和 Execute 都会保留的规划快照。
# Execute 不会用实际结果覆盖 plan。
plan:
summary:
# 计划前与完整计划成功后的预测碎片率,单位为百分比。
fragBeforePercent: 33
fragAfterPercent: 0
# 预计释放的目标资源节点数。
freedNodeCount: 1
# 预计迁移的目标加速卡总数。
movedCardCount: 1
resolvedScope:
# selector 解析后、当前使用目标资源的候选 PodGroup 数。
# 这不表示所有 PodGroup 最终都会被迁移。
podGroupCount: 1
# Scope 内提供目标资源、可作为腾空目标的节点数。
nodeCount: 3
# 按 PodGroup 汇总的计划迁移明细。
moves:
- namespace: repack-demo
podGroupName: ascend-batch-training-2f6c840d
# PodGroup ownerReference 中记录的直接工作负载控制器。
owner:
apiVersion: batch.volcano.sh/v1alpha1
kind: Job
name: ascend-batch-training
# 该 PodGroup 本次计划迁移的目标加速卡总数。
cards: 1
pods:
- name: ascend-batch-training-worker-0
# Pod 的计划源节点和建议接收节点。
fromNode: ascend-node-01
toNode: ascend-node-02
cards: 1
# 完整计划成功后应释放目标资源的节点。
freedNodes:
- ascend-node-01
# Execute 的实际观测结果;DryRun 中不存在。
result:
# 重建 Pod 完成调度后观测到的碎片率。
fragAfterPercent: 0
# 终态快照中实际验证已释放目标资源的节点。
freedNodeCount: 1
freedNodes:
- ascend-node-01
# Eviction API 已接受驱逐的 Pod 所对应的目标加速卡总数。
movedCardCount: 1
# true 表示以上收益指标来自重建 Pod 绑定后的一致 Scheduler 快照。
# false 时不能将收益指标视为已经完成最终验证。
metricsVerified: true
# Execute 的逐 Pod 驱逐和重新调度记录;每个计划迁移的 Pod 对应一条。
relocations:
- namespace: repack-demo
# 原始 PodGroup 名称,是不可变的计划身份。
podGroupName: ascend-batch-training-2f6c840d
# replacementPodGroupName 仅在重建 PodGroup 名称变化时出现,本例省略。
victimPodName: ascend-batch-training-worker-0
# UID 精确标识被驱逐的原 Pod,避免同名重建 Pod 被重复驱逐。
victimPodUID: "11111111-1111-1111-1111-111111111111"
# 规划阶段选择的建议接收节点,不会在执行过程中改写。
plannedNodeName: ascend-node-02
eviction:
# Pending、InProgress、Accepted、IndirectlyRemoved 或 Rejected。
phase: Accepted
placement:
# WaitingForReplacement、WaitingForNodeSelection、Nominated、
# Placed 或 TimedOut。
phase: Placed
# 执行阶段基于实时快照选择并写入 nominatedNodeName 的节点。
selectedNodeName: ascend-node-02
# Repack 识别到的重建 Pod。
replacementPodName: ascend-batch-training-worker-0
replacementPodUID: "22222222-2222-2222-2222-222222222222"
# Volcano Scheduler 最终绑定的节点,可能与 selectedNodeName 不同。
actualNodeName: ascend-node-02
# 本条 relocation 完成重建 Pod 调度协调的截止时间。
expirationTime: "2026-07-30T10:10:30Z"
# Job 风格的权威状态。终态成功时 Progressing=False、Complete=True。
conditions:
- type: Progressing
status: "False"
observedGeneration: 1
lastTransitionTime: "2026-07-30T10:03:20Z"
reason: ExecutionCompleted
message: "Repack succeeded for huawei.com/ascend-1980: all 1 replacement Pod was scheduled and all 1 planned node was verified free [ascend-node-01]; cluster fragmentation changed from 33% to 0%."
- type: Complete
status: "True"
observedGeneration: 1
lastTransitionTime: "2026-07-30T10:03:20Z"
# reason 给出本次 Run 的具体终态结论。
reason: ExecutionCompleted
message: "Repack succeeded for huawei.com/ascend-1980: all 1 replacement Pod was scheduled and all 1 planned node was verified free [ascend-node-01]; cluster fragmentation changed from 33% to 0%." 建议的生产操作顺序是:创建小范围DryRun,核对resolvedScope、计划收益和迁移明细;随后创建独立的小预算Execute;最后检查Conditions、status.result、relocation、工作负载状态和节点目标资源占用情况。
状态说明
status.conditions是权威状态,status.phase是简化生命周期。常见状态如下:
- Pending:AnotherRunActive表示存在运行中的 Execute;ExecuteCooldownActive 表示仍处于冷却时间。
- Running:Planning、Evicting、ReconcilingPlacements分别表示规划、驱逐和重建Pod协调阶段。
- DryRun Succeeded:RepackRecommended表示存在可审核计划;NoFragmentation或InsufficientImprovement表示正常完成但无需执行。
- Execute Succeeded:ExecutionCompleted表示计划完成;ExecutionCompletedWithAlternativePlacement表示部分Pod调度到其他节点,但收益已验证。
- Failed时优先阅读status.message,再结合Conditions、相关 Pod/PDB 事件和引擎日志定位问题。