1. GPU 在等数据时,问题发生在哪里
机器人训练需要读取相机画面、机械臂状态和动作序列。数据量增加后,训练任务可能已经占到 GPU,DataLoader 却持续等待文件。
这时需要把三个问题分开:
任务在哪运行? Kubernetes / Kueue
文件怎样到达节点? JuiceFS / CSI / Object Storage
Episode 怎样被读取? Lance / LeRobot Dataset三个层次可以一起使用,也可以按场景选择其他实现。把它们都叫“数据存储”,会掩盖真正的故障位置。
2. 从 Episode 到模型产物的完整链路
一个匿名的多摄像头机器人采集场景可以表示为:
Camera / Robot
→ Episode
→ Dataset Version
→ Lance / LeRobot Dataset
→ JuiceFS 或 Object Storage
→ Kubernetes Training Job
→ GPU Worker
→ Checkpoint
→ Model Version平台至少要保留:
| 对象 | 关键内容 |
|---|---|
| Episode | 任务、时间范围、设备、相机和状态 |
| DatasetVersion | 包含哪些 Episode、筛选条件、数据格式 |
| TrainingJob | 数据版本、代码、参数和资源需求 |
| TrainingAttempt | Kubernetes Job、节点、开始结束时间 |
| Checkpoint | 步数、路径、校验值和兼容信息 |
| ModelVersion | 来源任务、产物、评估和发布状态 |
数据文件在存储系统里,关系与状态在平台元数据库里。只保存目录路径,无法稳定回答一个模型使用了哪些 Episode。
3. Kubernetes 和 Kueue负责计算资源
Kubernetes 通过 Job 管理一次运行到完成的任务,通过设备插件暴露 GPU。训练平台把资源需求写入 Pod 模板,调度器选择可用节点。
多个团队共享 GPU 时,还需要队列、配额和优先级。Kueue 在 Kubernetes 工作负载之上提供 LocalQueue、ClusterQueue、ResourceFlavor 和 Workload 等对象,用来管理准入与资源配额。
team namespace
→ LocalQueue
→ ClusterQueue
├─ cpu / memory quota
├─ GPU flavor A
└─ GPU flavor BKueue 决定任务何时获得配额,kube-scheduler 决定 Pod 放到哪个具体节点。平台页面应该区分“等待队列准入”和“已经准入但没有可调度节点”。
4. JuiceFS 负责把共享文件带到 Pod
许多训练程序希望像读取本地目录一样读取数据。JuiceFS 把元数据服务、对象存储和客户端组合成文件系统,CSI Driver 可以通过 PV/PVC 把它挂载到 Kubernetes Pod。
volumes:
- name: dataset
persistentVolumeClaim:
claimName: robot-dataset
containers:
- name: trainer
volumeMounts:
- name: dataset
mountPath: /datasets
readOnly: truePVC 名称和路径是说明示例。训练数据只读挂载可以减少误写风险,检查点和输出使用独立目录。
JuiceFS 客户端支持本地数据缓存、预读和预热。缓存只有在数据会重复读取,且访问模式能够命中时才可能带来收益。随机稀疏读取还可能产生读放大,需要用真实训练访问模式测量。
节点本地缓存也要设置容量和剩余空间边界。Pod 换节点后可能重新建立缓存,不能把缓存目录当成唯一数据副本。
5. Lance 负责多模态数据组织与访问
JuiceFS 提供文件接口,Lance 是一套面向多模态的湖格式,分为文件、表、索引和目录四层规范。
Lance 的物理组织不是“一个分区目录一个文件”。表按两个维度划分:
表
├─ Fragment(行分组,每个 Fragment 覆盖一段行区间)
│ └─ Data File(列文件,同属一个 Fragment 的多个文件各持有部分列)
├─ 索引(Zone Map / BTree / Bitmap / 向量索引等,独立于数据文件存在)
└─ Manifest(记录版本与提交)这样组织有三个直接含义:
- 随机行访问:训练集筛选只读需要的行区间,不必为了取少量目标数据下载并解压整块文件。
- 列裁剪:每个 Data File 独立存储,读取器只打开用到的列。
- 独立索引:Zone Map(每列维护 min/max)做粗剪枝,BTree / Bitmap 等二级索引把查询精确到行;索引与数据物理分离,可以按需构建、不重写数据。
Lance 是多模态列式格式,二进制内容(图片、视频)也存在表里。视频可以存为 blob 列,读取时只取命中的字节区间,而不是把整段视频拉进节点。
LeRobot Dataset v3 的官方文档把 Lance 列为可选实现。lerobot-lancedb 提供把 LeRobot v3 数据集转换为 Lance 布局(帧、视频、元数据三张表)的工具和训练加载器,支持直接从对象存储读数据,一个 batch 只取它所需要的那部分字节,而不必先下载整个数据集。
训练样本可能只读取:
observation.images.front
observation.state
action
timestamp
episode_index列式组织让读取器选择需要的字段。随机采样能力也更贴近训练 DataLoader 的访问方式。
格式转换仍然需要校验:Episode 边界、时间对齐、图像质量、动作维度和训练样本是否一致。官方仓库中的基准属于外部结果,本文不把它们当成本机性能证明。
6. JuiceFS 与 Lance 怎样组合
可以有多种路径:
方案 A:LeRobot Parquet/MP4 → JuiceFS → Training Pod
方案 B:Lance Dataset → Object Storage → Training Pod
方案 C:Lance Dataset → JuiceFS 挂载目录 → Training Pod选择取决于现有读取器、存储接口、随机访问需求、缓存和运维能力。Lance 数据集可以直接使用对象存储 URI;如果训练框架需要 POSIX 路径,JuiceFS 提供另一种接入方式。
这两层不会替平台做业务层 DatasetVersion 管理,但 Lance 本身提供版本语义来固定数据。每次提交数据集都会产生一个新版本,可以用 Tag 指向“训练就绪”的版本,让训练期间读到的数据保持一致。平台需要把 存储位置 + 数据集版本/Tag 记进业务记录,而不是只存一个目录路径;格式版本、源数据修订和校验信息仍然由平台维护。
7. 四类常见失败
任务获得 GPU,但数据卷没有挂载
Pod 可能停在创建阶段。检查 PVC、CSI 组件、Mount Pod 和节点事件,而不是继续调整训练参数。
缓存盘被占满
数据读取和其他 Pod 都可能受到影响。需要容量限制、剩余空间阈值、清理和节点监控。
数据版本发生变化
训练过程中读取可变目录,会让同一任务前后看到不同数据。提交时应固定数据集版本或用 Tag 锁定“训练就绪”版本,把版本标识写进任务记录,而不是只存路径。
Pod 重建后从头训练
Kubernetes 能重新创建 Pod,训练恢复仍依赖可用检查点、代码和参数兼容。两类恢复状态要分别记录。
8. Java 控制面负责连接这些对象
Spring Boot 平台可以接收训练请求、校验项目权限和数据版本,创建业务任务,再由集群适配器提交 Kubernetes Job。
Java Control Plane
├─ DatasetVersion / Episode manifest
├─ TrainingJob / Attempt
├─ Queue / resource request
├─ Checkpoint / ModelVersion metadata
└─ Kubernetes status synchronizationJava 服务不参与张量计算,也不实现文件系统和列式读取器。它把用户、数据、任务、资源和产物连接成可查询的生命周期。
训练“读数据慢”可能来自调度、挂载、远端存储、缓存、数据格式、解码或 DataLoader。只有先区分停在哪个层次,才知道该检查哪个系统;把数据、节点、任务和产物拆成可查询的对象,性能问题才能真正定位到组件。
相关阅读
- AI 数据平台部署前,先看懂一台 Kubernetes 节点
- 为什么训练任务不应该直接塞进 Spring Boot 请求里
- 机械臂训练之前,数据到底长什么样?
- 模型推理服务怎样运行在 Kubernetes:版本、KServe、灰度与回滚
参考资料
- Kueue:ClusterQueue
- JuiceFS CSI:架构
- JuiceFS:缓存
- LeRobot Dataset v3
- LeRobot Lance 数据集实现
- Lance 格式规范
- Lance 能力概览(LanceDB)