返回文章

AI-Infra

训练任务平台真正难的是什么:从提交作业到交付结果

训练平台不是一个提交作业的界面,而是一套管理资源、环境、时间与失败的不确定性系统。本文从生命周期、调度、可观测性和容错四个方面,讨论训练任务平台真正困难的工程问题。

很多训练平台都从一个表单开始:选择镜像、填写启动命令、申请几张 GPU,然后点击“提交”。

如果任务成功跑起来,这套系统看上去已经完成了大半。但只要把目标从“创建一组容器”改成“稳定地交付一次训练结果”,真正的问题才刚刚开始。

用户关心的从来不是 Pod 是否被创建,而是:

  • 我的任务为什么还在排队?
  • 为什么昨天能跑,今天却启动失败?
  • 训练中断以后,能不能从最近的 checkpoint 恢复?
  • 这次实验到底消耗了多少算力,慢在哪里?
  • 如果平台自身发生故障,任务状态还能不能被还原?

先说明这篇文章的边界:它不是一篇我亲历建设训练平台的项目复盘,而是基于 Kubernetes、Kueue、Volcano、PyTorch Elastic 等公开资料,以及我对系统工程问题的学习和整理。文中的建设顺序和判断,是一种分析框架,不代表某个真实平台的完整实践。

基于这些问题,可以把训练任务平台理解为一套不确定性管理系统。它管理的不只是 GPU,还包括环境、数据、时间和失败;它的目标也不只是提高利用率,而是让用户以可预期的方式得到结果。

一、先设计任务生命周期,而不是提交页面

训练任务不是一个 Kubernetes 对象,而是一段可能持续几小时、几天甚至更久的过程。一个相对完整的生命周期至少包含下面这些状态:

SUBMITTED
    │
    ├──> VALIDATING ──> REJECTED
    │
    └──> QUEUED ──> ADMITTED ──> PREPARING ──> RUNNING
                                                │
                            ┌───────────────────┼───────────────┐
                            ↓                   ↓               ↓
                       SUCCEEDED            FAILED         CANCELLED
                                                │
                                                └──> RETRYING

这张状态图看起来普通,却决定了平台后面的大部分复杂度。

例如,一个任务显示为 QUEUED 时,它究竟是在等待团队配额、等待某种 GPU,还是在等待多机资源同时满足?一个任务进入 FAILED 后,是用户代码确定性失败,还是节点、网络、存储等基础设施的瞬时故障?如果控制器重启,平台能否根据持久化状态和集群事实恢复到同一个结论?

这里有三个基础问题必须尽早回答:

  1. 谁拥有状态。 数据库、Kubernetes 对象和训练进程都可能报告状态,但平台必须定义最终的状态所有者。
  2. 什么是事实,什么是推导。 “Pod 已创建”是事实,“任务正在准备环境”是平台根据多个事实推导出的用户状态。
  3. 每一步能否幂等。 控制器重复执行、事件重复投递或者网络超时,都不应该创建两份任务或重复扣减配额。

如果状态机含糊,后面再强的调度算法和监控系统,也只是在一个不可靠的地基上增加复杂度。

二、GPU 调度远不止“还剩几张卡”

CPU 和内存通常可以按数量切分,GPU 却很难被视为完全同质的资源。即使数量相同,不同 GPU 型号、显存容量、驱动版本、节点拓扑和互联方式,也可能让训练效率产生明显差异。

Kubernetes 通过 Device Plugin 把 GPU 暴露为可调度的扩展资源,这是训练平台的基础能力,但不是完整的资源模型。官方文档也明确区分了 GPU 类型和节点标签等约束:Schedule GPUs。在平台层,资源请求往往至少需要表达:

  • GPU 数量、型号和显存要求;
  • 单机多卡还是多机多卡;
  • 节点、机架或网络拓扑偏好;
  • 镜像与驱动、CUDA 版本的兼容关系;
  • 是否接受其他型号、抢占资源或弹性规模。

分布式任务需要“一起开始”

对于紧耦合的分布式训练,只调度出一部分 worker 往往没有意义。已经拿到资源的进程无法开始训练,却会继续占用 GPU,剩余 worker 又可能永远等不到资源,最终形成“资源已经分配,但有效计算没有发生”的局面。

这也是 Gang Scheduling 试图解决的问题:只有当满足最小运行规模时,才整体执行调度,否则不让部分任务提前占住资源。Volcano 的 Gang 调度说明把它概括为一种 “All or nothing” 的约束。

配额、公平与利用率必须一起考虑

多团队共享集群时,调度还需要同时处理三个互相拉扯的目标:

  • 保障: 每个团队应获得的基础资源不能长期被挤占;
  • 弹性: 某个团队暂时不用的资源不应该闲置;
  • 公平: 大任务、低优任务和后提交任务不能无限期饥饿。

硬配额容易理解,但通常会造成空闲;完全共享利用率高,却可能让资源失去确定性。更实用的方式往往是“名义配额 + 借用 + 回收 + 优先级”的组合。Kubernetes Kueue 也把 ClusterQueueResourceFlavor、Cohort、借用和抢占作为不同层次的概念,说明调度决策不仅是节点选择,更包含准入和配额管理:Kueue Concepts

好的调度系统不只要做出决定,还必须解释决定。如果平台不能回答“这个任务为什么没有运行”,用户就会不断重试、放大资源请求,或者绕过平台规则,最终让调度问题变得更糟。

三、真正昂贵的常常不是训练,而是训练之前

一个任务从拿到 GPU 到第一步训练开始,中间可能还要完成:

  • 拉取大体积镜像;
  • 挂载数据集和 checkpoint 存储;
  • 下载模型权重或依赖;
  • 初始化通信网络;
  • 构建数据索引和本地缓存;
  • 生成运行配置与凭据。

这些步骤都发生在“资源已经分配、有效训练尚未开始”的阶段。如果只观察任务是否处于 Running,平台很容易把准备时间误认为训练时间,把 GPU 空转误认为资源已被充分利用。

因此,一个比“Pod 启动耗时”更有意义的指标是:

Time to Useful Work:从任务提交到第一次有效训练计算之间的时间。

它可以进一步拆成排队、准入、资源绑定、镜像准备、数据准备和进程初始化。只有把这些阶段分开,平台才知道应该优化镜像分发、缓存命中、存储带宽,还是调度策略。

与此同时,任务必须具有可复现性。至少需要保存代码版本、镜像摘要、数据集版本、启动参数、GPU 型号和关键运行环境。用户再次提交同一个任务时,平台应该创建一份新的不可变运行快照,而不是悄悄读取已经变化的“最新配置”。

四、可观测性应该围绕问题,而不是围绕组件

训练平台通常不缺指标。Kubernetes、节点、GPU、容器、网络和存储都有自己的监控,但组件指标丰富,并不代表用户的问题能被回答。

从任务生命周期出发,平台可观测性至少需要服务三类人:

对训练用户

  • 任务当前处于哪个阶段?
  • 为什么还没有开始?
  • 最近一次进展和异常是什么?
  • 失败后应该修改代码,还是直接重试?

对平台运营者

  • 哪种任务、镜像或资源规格最容易失败?
  • 资源碎片主要发生在哪些 GPU 池?
  • 排队时间增长是容量不足,还是调度策略导致?
  • 有多少 GPU 时间消耗在准备、重试和无效运行上?

对平台开发者

  • 一次状态变化由哪个事件触发?
  • 控制器的每次决策是否可以追溯?
  • 跨调度、存储和训练进程的事件能否通过同一个任务 ID 关联?

这意味着日志、指标和事件不能只是按组件分散展示,而要围绕任务生命周期组织。对每个任务,平台最好能重建一条完整时间线:什么时候提交、什么时候进入队列、为什么被准入、在哪个节点启动、什么时候产生第一个 checkpoint,以及最终为什么结束。

可观测性的终点不是“有图表”,而是降低一次异常从发现到得出结论的时间

五、失败不是异常分支,而是训练流程的一部分

长时间训练一定会遇到失败。节点故障、GPU Xid、网络抖动、存储超时、镜像拉取失败、进程 OOM 和用户代码错误,都会表现为“任务没有成功完成”,但它们需要完全不同的处理方式。

一种实用的分类方式是:

  • 确定性用户错误: 配置错误、代码异常、稳定复现的 OOM;
  • 瞬时基础设施错误: 节点掉线、网络短暂中断、存储超时;
  • 资源与环境错误: 驱动不兼容、镜像不可用、磁盘空间不足;
  • 编排系统错误: 状态不同步、控制器异常、重复操作。

平台首先应该识别失败类别,然后再决定是否重试。无差别自动重试看似提升成功率,实际上可能让确定性错误持续消耗昂贵资源,还会掩盖真正的基础设施问题。

即使允许重试,也需要明确:

  • 最大重试次数和退避策略;
  • 哪些错误允许自动重试;
  • 从头启动还是从 checkpoint 恢复;
  • 恢复后是否仍满足数据和训练语义;
  • 重试消耗如何计入用户配额和平台成本。

PyTorch Elastic 同样使用 max-restarts 限制故障或伸缩导致的重启次数:Torch Distributed Elastic Quickstart。这个限制很重要,因为“能够重启”不等于“能够无限重启”。

真正可靠的训练任务,不是从不失败,而是失败以后状态清楚、损失可控、恢复路径明确。

六、GPU 利用率不是平台的最终指标

提高 GPU 利用率当然重要,但一个利用率很高的集群,仍然可能交付结果很慢。

常见的算力浪费至少包括:

  1. 排队浪费: 有可用资源,但因为配额、拓扑或碎片无法满足任务;
  2. 准备浪费: GPU 已经分配,任务仍在拉镜像、加载数据或初始化;
  3. 恢复浪费: checkpoint 间隔过长,失败后重复了大量计算;
  4. 训练浪费: 数据管道、通信或代码瓶颈让 GPU 长期等待。

Bin Packing 可以减少资源碎片,为集群扩缩容留出空间;Kubernetes 调度器也提供了面向扩展资源的装箱评分策略:Resource Bin Packing。但越紧凑的装箱并不总是越好:分布式训练还会受到网络拓扑、故障域和热点的影响。

因此,平台优化不应该只追求一条 GPU 利用率曲线,而应该关注一组更接近结果的指标:

  • 任务排队时间的分位数;
  • 从准入到第一次有效训练的时间;
  • 成功任务占用的 GPU 小时;
  • 因准备、失败和恢复损失的 GPU 小时;
  • 每次成功实验或有效训练步的成本。

利用率描述“机器有没有忙”,这些指标才更接近“用户有没有得到结果”。

七、平台抽象必须允许用户看见现实

训练平台需要提供简单入口,但不能假装底层复杂度不存在。

过度暴露 Kubernetes 配置,会让每个用户都重复处理镜像、网络、调度和存储细节;过度封装又会让复杂任务没有表达空间,出了问题也无从定位。

更合理的方式是提供分层抽象:

  • 默认路径只暴露少量稳定概念,覆盖大多数标准任务;
  • 高级配置允许表达拓扑、容错和数据准备等需求;
  • 必要时保留受控的原生配置扩展能力;
  • 平台自动生成的最终运行规格对用户可见、可导出、可审计。

好平台不是把复杂度藏起来,而是把复杂度放在合适的位置,并为常见问题提供经过验证的默认答案。

八、从系统工程视角看,更稳妥的建设顺序

训练平台很容易从“先把功能做出来”开始,最后变成大量控制器、页面和策略的集合。如果按照前面的风险重新排序,可以优先建设下面五件事:

  1. 定义清楚任务状态机。 每个状态、事件、超时和终态都能被解释与恢复。
  2. 建立可解释的准入和排队机制。 先回答为什么运行、为什么不运行,再逐步增加复杂调度策略。
  3. 打通任务级可观测链路。 让一次任务能够跨组件还原完整时间线。
  4. 建立失败分类与恢复语义。 明确哪些错误重试、从哪里恢复、最多付出多少成本。
  5. 最后优化利用率。 在状态和成本可观测的前提下,装箱、借用、抢占和弹性才有可靠反馈。

这个顺序看起来没有把“资源利用率”放在最前面,但它能避免平台在不可解释的状态下做激进优化。无法解释的调度越复杂,排查成本就越高;无法分类的重试越自动,资源浪费就越隐蔽。

九、一份训练平台自查清单

如果要快速判断一个训练平台是否正在走向成熟,可以问下面这些问题:

  • 任意任务的状态能否通过持久化事件重新构建?
  • 每个排队任务是否都有明确、稳定的等待原因?
  • 一次运行是否绑定了不可变的代码、镜像、数据和配置版本?
  • 平台能否区分用户错误、基础设施错误和编排错误?
  • 自动重试是否有条件、次数和成本上限?
  • 能否计算从提交到第一次有效训练的时间?
  • 能否统计失败与恢复浪费的 GPU 小时?
  • 调度是否理解 GPU 类型、拓扑和分布式任务的整体性?
  • 抢占发生后,被抢占任务是否有清楚的恢复语义?
  • 平台控制面故障后,是否可以根据事实恢复,而不是依赖内存状态?

这些问题未必需要一次全部解决,但它们比“支持多少种任务模板”更能反映平台的真实能力。

写在最后

训练平台表面上管理的是任务,底层管理的是算力,本质上管理的却是从一个实验意图到一个可用结果之间的不确定性

状态机让过程可恢复,调度让资源可分配,可观测性让问题可解释,容错让失败可控制。只有这几部分形成闭环,平台才不再是 Kubernetes 上的一层提交入口,而成为真正能够承载研发效率和算力效率的基础设施。

所以,衡量训练平台时,我更关心的不是“今天提交了多少任务”,也不只是“GPU 利用率有多高”,而是:

用户能否以可预期的时间、成本和失败边界,得到一次可信的训练结果。

这可能才是训练任务平台最应该交付的东西。