具身智能训练存储与数据管线:JuiceFS + Lance 深度实践

具身智能的存储问题,难点不在"存得下",而在"读得随机、写得突发、流得动"——一段双臂叠衣 episode 里藏着 3 路 30Hz 相机视频、100Hz 关节状态、未来 50 步的 action chunk,训练时 DataLoader 要从数万条 episode 里随机抽 timestep 并把这几路异频流对齐成同一行。

招商局狮岩 AI Lab 的实测给出了量化答案:用 Lance 把长视频切成 GOP 尺度 blob 后,随机读吞吐相对 LeRobot 提升 1.7–6×、存储降低 42%;而地瓜机器人的集群数据则验证了另一端:JuiceFS 多云 + 分布式缓存把训练任务启动从 20 分钟压到 3 分钟、缓存命中率从 45% 提到 92%。本文把这两套栈拼成具身训练的完整存储底座。


一、具身训练的 I/O 画像:与 LLM/CV 根本不是一回事

先明确负载形态,后面所有选型推理都从这里出发。

具身数据的最小单元是 episode,不是 token,也不是单张图。 一条 episode = 一个完整轨迹:初始状态 → 连续交互 → 任务结束,内部是多路异频传感器流的时序对齐。这带来四个 LLM 训练没有的 I/O 特征:

1. 一次样本构造 = 多路异频并发随机读。 VLA/ACT/Diffusion Policy 训练时,DataLoader 要同时取出:3 个相机在该 timestep 的帧、30+ 维关节位置与速度、夹爪状态、语言指令,并按时间戳对齐。相机 30Hz、控制 100Hz,频率不同就必须做映射。传统方案(LeRobot 把状态放 Parquet、视频放 MP4、元数据放 JSON)导致每次采样都触发"打开视频 → seek → 解码 → 跨文件按时间戳对齐"的链路,文件打开次数随 episode 数线性爆炸。

2. 视频帧随机解码受 GOP 机制惩罚。 视频压缩靠帧间预测:解码 GOP 内任意一个 P 帧,必须从该 GOP 的 I 帧开始逐帧解。GOP 越大压缩越好,但随机取一帧的解码代价越大;HuggingFace 的实测是 GOP=1(纯 I 帧)存储反而比逐帧 JPEG 更大 4%,而 GOP=128 时随机读吞吐跌到 1/12。

3. Checkpoint 是突发大写。 VLA 模型(π0 级别数 B 参数)多机训练时各 rank 并发写分片,单次几十 GB,要求同步写吞吐 ≥15 GB/s 且不阻塞训练 step;同时采集端还在持续回流新 episode,读写双向流。

4. 数据闭环跨多云。 采集车在边缘、研发在本地、训练在云上,数据集需要"同一套目录结构、同一套读写接口"地在云孤岛间增量同步。

对这四个特征,"存储不成为瓶颈"的量化指标就具体了:随机采样 P99 延迟(含解码)< 50ms、缓存命中率 > 90%、Checkpoint 落盘不阻塞 step、跨云同步延迟分钟级。


二、JuiceFS 架构:为这种 I/O 画像定制的分离式设计

2.1 三平面分离与 Chunk/Slice/Block

JuiceFS 把一个文件系统拆成三个可独立扩展的平面:

flowchart TB
    subgraph App["应用平面"]
        FUSE["FUSE POSIX 挂载"]
        SDK["Hadoop SDK / K8s CSI"]
        S3G["S3 Gateway"]
    end
    subgraph Control["控制平面:元数据引擎"]
        Redis["Redis"]
        SQL["MySQL/PG/SQLite"]
        TKV["TiKV/BadgerDB"]
    end
    subgraph Data["数据平面:对象存储"]
        Obj["S3/OSS/COS/MinIO
4MiB Block 对象"] end Client["JuiceFS Client(rich client)
所有 I/O/压缩/compaction 都在这里"] FUSE --> Client SDK --> Client S3G --> Client Client <-->|"元数据事务"| Redis Client <-->|"元数据事务"| SQL Client <-->|"元数据事务"| TKV Client <-->|"数据读写"| Obj

文件数据的三级组织是 JuiceFS 最核心、也最容易被误解的机制:

  • Chunk:文件内的逻辑分段,固定 64 MiB。只要文件总长不变,chunk 划分就保持不变,与文件被改写过多少次无关。chunk 是纯逻辑概念,对象存储里没有 chunk 实体;
  • Slice:chunk 内的一次连续写,变长、不能跨 chunk 边界。顺序写每个 chunk 只产生 1 个 slice;append 写每次触发 flush 就多一个 slice;对同一位置反复覆盖写会产生交叠的 slice,读取时"从上到下"取最新,这是文件碎片化的来源,客户端会异步做 compaction 合并;
  • Block:物理存储与缓存的最小单位,默认 4 MiB,一个 slice flush 时被切成若干 Block 并发上传,每个 Block 对应对象存储里的一个对象,对象 key 格式为 chunks/{slice_id/10^6}/{slice_id/1000}/{slice_id}_{block_id}_{size}。

对具身训练的具体含义:一段 160 MiB 的 episode 视频顺序写入 = 3 个 chunk、每个 chunk 1 个 slice、约 40 个 4 MiB Block 对象;训练时随机读第 100 MiB 处的一帧,客户端只需 Range GET 该位置所在的 Block,不拖整个视频。Block 是 4 MiB 而不是 64 MiB,正是为了随机读延迟与顺序吞吐的平衡——块越小随机读越轻,但对象数和元数据事务越多。

2.2 POSIX 兼容与一致性

JuiceFS 提供接近完整的 POSIX 语义(mmap、flock、原子 rename、硬链接),LeRobot 数据集、rosbag、PyTorch DataLoader 可直接挂载使用无需改代码。一致性是 close-to-open:写端 close() 后读端 open() 保证看到最新数据——这解决了具身采集回流"边采边训"时对象存储最终一致性的脏读问题。元数据内存占用方面,企业版单进程 30 GiB 内存管理 3 亿文件、P99 延迟 ~100µs。

2.3 多级缓存体系:团队正在做的适配重点

完整的缓存层级:

1
2
3
4
5
L1 Kernel Page Cache(读)
L2 Client 内存缓冲 --buffer-size(默认 300MiB,读写共享,超阈值降速/暂停的背压)
L3 本地磁盘缓存 --cache-dir/--cache-size(存 4MiB Block 原始字节)
L4 分布式缓存集群 --cache-group(一致性哈希环 + IORPC,企业版 5.3+ 支持 RDMA)
L5 对象存储(最终回源)

分布式缓存组是具身多机训练的关键:多个客户端用相同 --cache-group 组成一致性哈希环,每个 Block 按 ID 哈希到固定成员节点,读请求直接向持有数据的 peer 拉取,元数据引擎兼任服务发现。故障容忍机制要特别记住:peer 失败只重试一次、连续 31 次失败后从该消费者的成员列表中移除(但不影响其他消费者)、成员变更后等 10 分钟才触发数据迁移以避免抖动。生产调优建议 --remote-timeout 从默认 65 秒降到 1 秒(高内网带宽场景),让请求更快回源而非堆积在拥塞的 peer 上。

Writeback 模式的取舍必须说透:开启后 flush 只写本地 staging 目录即返回,数据异步上传——写吞吐大幅提升,但本地盘损坏或进程异常退出时数据会永久丢失,且未上传期间其他客户端不可见。具身采集端的 episode 如果可重新采集、可容忍丢失,writeback 能极大提升回流吞吐;训练端 Checkpoint 这种不可再生数据严禁开 writeback。商业版 4.9.22+ 的 --fill-group-cache 可让 staging 数据也异步进分布式缓存组,缓解可见性问题,但仍不解决本地盘损坏风险。

2.4 JuiceFS on K8s:Mount Pod vs Sidecar

CSI Driver 两种模式:

维度 Mount Pod 模式(默认) Sidecar 模式
架构 每个 PV 一个独立 Mount Pod,由 CSI Node Service 管理,应用 Pod 通过 CSI 标准协议挂载 mount 进程作为 sidecar 容器直接注入应用 Pod
生命周期 与 PV 一致,应用 Pod 重建不丢 Mount Pod 与应用 Pod 同生共死
缓存持久性 缓存目录可放宿主机 hostPath 或独立卷,应用重建不丢缓存 应用销毁即缓存丢失,除非 --cache-dir 指向 hostPath
资源开销 多个应用 Pod 共享同一 Mount Pod,连接数省 每个 Pod 一份 mount 进程内存
适用场景 具身训练 GPU 集群(推荐)——epoch 重复采样,本地缓存命中率极高 Serverless K8s、强隔离、快速弹性伸缩场景

对具身训练集群的实践结论:用 Mount Pod 模式 + 宿主机 NVMe 盘做缓存目录 + 专用缓存组(应用客户端加 --no-sharing 只取不给,缓存集群用 DaemonSet 部署稳定节点,避免计算 Pod 调度销毁导致缓存组频繁重平衡)。

2.5 为什么具身团队从 CPFS 迁往 JuiceFS

CPFS/GPFS/Lustre 这类并行文件系统的吞吐与购买的容量强耦合——要 100 GB/s 的读吞吐往往要预置 PB 级全闪容量;JuiceFS 把数据面下沉到对象存储后,每 TB 成本是全闪 PFS 的 1/5~1/10,且容量跟随对象存储按量弹性伸缩,而 CPFS 只能扩不能缩。

地瓜机器人在尝试了私有 MinIO、百度云 BOS、阿里云 CPFS 后最终选 JuiceFS,核心动因就是性能-成本-运维的不可能三角被解耦架构解开:对象存储承担容量与成本,独立元数据引擎承担事务性能(亿级文件目录深分页查询从 3.8s 优化到 210ms),分布式缓存承担聚合吞吐。另一个 CPFS 做不到的是多云:JuiceFS 同一文件系统可跨 OSS/COS/S3 部署,juicefs sync 做增量同步,支持具身"采集端(边缘)→ 研发集群 → 多云训练集群"的数据闭环。

诚实的取舍:超低延迟强一致同步写、极高并发随机写这两个维度 GPFS/CPFS 仍占优,但具身负载(读多写少 + 异步可容忍 Checkpoint + 采集回流可异步)恰好避开这个弱点。

2.6 Checkpoint 存储落地

具身 VLA 的 Checkpoint 写入模式:数十亿参数 × 多机 DDP/ZeRO 分片 → 单次几十 GB 的并发顺序大写。JuiceFS 的应对:4 MiB Block + --max-uploads(默认 20)并发上传把大文件切分为海量并发 PUT,充分利用对象存储横向扩展;客户端 --buffer-size 背压防止 OOM。

关键纪律:Checkpoint 场景必须关 writeback,确保 flush 返回 = 数据已在对象存储持久化,配合 --trash-days 保留被覆盖版本做回滚。如果训练框架支持异步 offload(如 FastPersist 类方案),则把 Checkpoint 写移出关键路径,只需保证全局落盘速率不低于约 15–50 GB/s 即可不阻塞训练。


三、存储类型全景:具身场景下的定位对照

维度 OSS/S3 CPFS/GPFS NFS JuiceFS Longhorn
协议 S3/HTTP POSIX 内核态 NFS POSIX via FUSE iSCSI 块
元数据性能 弱(无目录语义、重命名≈重写) 强 弱 强(独立事务引擎) 弱
吞吐模型 随客户端并发线性扩展 容量绑定 单 server 瓶颈 客户端并发+分布式缓存 副本网络限制
单 TB 成本 极低 极高(全闪) 高 ≈对象存储+元数据节点 中
容量弹性 极好 只扩不缩 弱 继承对象存储 K8s 卷级
多云 原生 单可用区 弱 同一 FS 跨云后端 单集群
POSIX 无 有 有 有(含 mmap/flock) 无
具身角色 冷归档/原始 episode 高并发强一致写 小团队过渡 训练主力 K8s 有状态应用

数据湖表格式一层的对比放在 Lance 章节展开,因为 Lance 与 Iceberg/Delta/Hudi 的差异不是增量改进而是设计目标不同。


四、Lance 深度:为具身多模态随机访问而生

4.1 与 Arrow/Parquet/Iceberg 的关系定位

Arrow 是内存列式标准(进程内零拷贝消费);Parquet 是磁盘列式压缩格式(为整行组扫描优化);Lance 是磁盘列式 + 向量 + Blob 格式兼表格式(为 ML 随机访问和迭代更新优化)。Lance 同时覆盖三层:文件格式(类似 Parquet)、表格式(类似 Iceberg,版本化 manifest + fragment + MVCC)、catalog 规范。

4.2 为什么 Parquet 在具身负载下失守:结构编码与页索引的墙

Lance 团队对 Parquet 做过深入测量,结论非常硬核:

  • 把 Parquet 页调小到 8KB、关字典编码和页统计,随机访问能快 60×,全扫描不减速——默认参数就是错的;
  • 但有一堵配置翻不过去的墙:Parquet 在内存里维护页偏移索引(~20 字节/页),页越小页越多。存 10 亿行 3KB embedding:1MB 页 → 300 万页 → 60MB 索引、单行读放大 341×;8KB 页 → 10 亿页 → 20GB 索引、读放大 2.7×。小页救随机读毁内存,大页救内存毁随机读。

Lance 的解法:对大值干脆不维护页索引,同一行的同列数据物理上连续存放,定位退化为一次乘法;配合结构编码(把一个逻辑列如 List<String> 拆成 5 个物理数组,null 位图、偏移、内容分离)——Lance 论文指出几乎所有压缩研究都在做 compressive encoding,但决定单行读成本的是被忽视的 structural encoding。这就是 Lance v2.2 相对 Parquet 压缩率减半、随机 Blob 读快 68 倍的来源。

4.3 Lance 表格式的物理布局

表格式层的核心概念:

  • Manifest(不可变):每个版本一个 manifest,含 schema、fragment 列表、版本号、索引引用;版本演进 = 新 manifest,支持 time travel 和 MVCC;
  • Fragment:行的分区;每个 fragment 可含多个 data file,每个 data file 提供一列或多列——二维存储让加列/回填列只需给已有 fragment 挂新 data file,不用重写全表;
  • Deletion File:删除不物理重写,只追加删除标记;
  • 多 base path:数据文件可在表目录之外(shallow clone、分层存储),企业版特性;
  • 索引是表格式生命周期的一等公民:BTree、倒排(FTS)、向量(IVF/HNSW)由表元数据统一发现和事务协调。

4.4 具身多模态 schema:GOP 尺度 Blob + 行级引用

这是全文最核心的设计,直接给招商狮岩在叠衣任务上验证过的 PyArrow schema(可直接抄):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import pyarrow as pa
schema = pa.schema([
# 身份与时间
pa.field("episode_index", pa.int64()),
pa.field("frame_index", pa.int64()), # 训练样本索引,非原始相机帧号
pa.field("timestamp_ns", pa.int64()),
pa.field("task", pa.string()),
pa.field("split", pa.string()),
# 机器人状态/动作,双臂 14DoF,100Hz 原生频率
pa.field("observation_state", pa.list_(pa.float32(), 14)),
pa.field("action", pa.list_(pa.float32(), 14)),
# 多相机视频:Lance Blob 列 + 行级引用三件套
# 逻辑上 pa.binary(),物理上是 blob 引用;同 GOP 多行经 ref_id 共享字节
pa.field("observation_images_cam_env", pa.binary()),
pa.field("observation_images_cam_env_ref_id", pa.uint64()),
pa.field("observation_images_cam_env_gop_index", pa.int32()),
pa.field("observation_images_cam_env_frame_index_in_gop", pa.int16()),
pa.field("observation_images_cam_left", pa.binary()),
pa.field("observation_images_cam_left_ref_id", pa.uint64()),
pa.field("observation_images_cam_left_gop_index", pa.int32()),
pa.field("observation_images_cam_left_frame_index_in_gop", pa.int16()),
# 语言/标注
pa.field("language_instruction", pa.string()),
pa.field("source", pa.string()), # human / policy / intervention
pa.field("is_intervention", pa.bool_()),
], metadata={
b"camera_fps": b"30",
b"control_hz": b"100", # 行按此频率构建,高频动作零丢失
b"gop_size": b"8",
b"joint_names": b"left_0,...,right_6",
})

这套设计的精髓在三个"多对一":

1. 多行共享一个 GOP blob。 行存 ref_id + gop_index + frame_index_in_gop 三元组,不存帧本身;同 GOP 的多个帧通过 ref_id 共享同一份压缩字节,零冗余。控制 100Hz、相机 30Hz,行按 100Hz 构建使每个动作样本都保留,视频按 GOP 压缩一次、多行引用——打破了"要么丢高频动作、要么重复存视频"的传统 trade-off。

2. 受控 GOP 四不变量(让"取一帧"变成"一次 Range GET + 解一个短 GOP"):

  • 单帧只读一个对象、只发一次连续 Range GET;
  • 解码放大 ≤ MAX_GOP(可调,实测对比 4/8/16);
  • 每个 GOP 从 IDR 帧开始、closed GOP、Annex B 对齐、自包含 SPS/PPS,可独立解码,空 DPB 起解,不依赖前一 GOP 的解码器状态;
  • 帧定位走显式索引,不线性扫描视频文件。

编码侧的配套约束:keyint = min-keyint = GOP 长度、禁 scene cut、每 episode 重启编码器(边界确定)、bframes=0 + zerolatency(显示序=解码序,免重排)。D2E 论文独立验证了同样的发现:默认 x264 的 keyframe 间隔在 1.35s~12.5s 间随机波动导致随机访问性能不可预测,固定 keyint=30、禁 B 帧后得到可预测的 I-P-P-P-…-P-I 结构。

3. 多模态同表对齐。 视频/状态/动作/指令进同一行,替代 LeRobot 的"Parquet 状态 + MP4 视频 + JSON 元数据 + 时间戳隐式对齐"三分离——后者每次采样都要做跨文件 join,标注散在外部 JSON 里,glue code 与对齐 bug 源头。

4.5 实验数据与 GOP=8 的甜蜜点

实验 1(GOP 尺寸 sweep,叠衣数据集 xvla-soft-fold):

GOP 存储节省 单帧随机读吞吐 批量随机读吞吐
1(退化为逐帧) -4%(反而更大) — —
4 36% 683 fps 2650 fps
8 42% 453 fps 2134 fps
16 ~44% 258 fps —
128 49% 37 fps(1/12) 280 fps

解读:GOP 从 8 加到 128(16 倍长)只多省 7 个百分点,随机读却跌到 1/12;GOP=1 纯帧内编码存储不降反升 4%,证明纯 I 帧无意义。GOP≈8 是当前数据集与访问模式下的甜蜜点。作者特别强调:绝对数值依数据集与硬件而变,但"存储可降、随机读能跟上、batch 越大加速越明显"的趋势来自数据结构设计本身。

实验 2(Lance vs LeRobot,同数据同硬件):

场景 访问模式 加速比
A(最保守单点) dataset[i] 逐条读;LeRobot ~81ms/样本(几乎全在视频解码),Lance ~48ms(Range 读 8ms + 解码 40ms,解码本身快 2×) 1.7×
B 批量并行 take_blobs_decoded,I/O 与解码下沉 Rust 并行执行 6.0×
C(视觉-语言训练常见) 取连续 N 帧时间窗,一次 Range 读整窗、同 GOP 帧共享解码 vs LeRobot 逐帧 seek-decode 4.8×

规律:访问模式越接近真实 DataLoader(batch、多相机、时间窗),Lance 相对优势越大。批量读接口的用法(狮岩在 Lance Rust 源码里定制,现已合入主线):

1
2
3
4
5
6
7
# 高性能批量随机读:一次调用完成 Range 读 + Rust 侧并行解码 + 返回张量
frames = ds.take_blobs_decoded(
"observation_images_cam_env", # 一个或多个 blob 列名
indices=batch_indices, # 0-based 行索引
concurrency=32, # 并行解码参数
zero_copy=False, # True 返回复用 Rust 内存的 numpy,须即用即弃
)

zero_copy=True 省一次 PyO3 跨语言拷贝,但返回数组复用 Rust 内存,下游持有会内存泄漏。

4.6 修改成本与单一数据源

具身数据迭代极频繁:标注修正、异常 episode 剔除、数据集组合调整。LeRobot 工作流是"采集端打包 HDF5 → 上传 → 训练端下载解包 → 修改 → 再上传",一个数据集数百 GB 反复搬运。Lance 的答案:append-only + 版本快照——修改 = 追加新 data file + 新 manifest,物理不重写旧数据;数据在 S3 只存一份,采集/处理/训练读写同一份,没有"打包-搬运"循环;lance.dataset(uri, version=42) 可回溯任一版本并追溯"哪个数据集版本训出了哪个模型"。再叠加 JuiceFS 的元数据级 clone,数据集分叉实验做到近零拷贝。

4.7 与 Iceberg/Delta/Hudi/RLDS/LeRobot 全景对比

维度 Parquet Iceberg/Delta/Hudi Lance LeRobot v3 RLDS/TFRecord
设计目标 分析扫描(行组级) 数据湖事务/时间旅行 ML 随机访问+迭代更新 机器人数据交换 TF 生态流式
随机读单行 行组解码放大 依赖底层文件(通常 Parquet) 无页索引,乘法定位 seek+GOP 解码 顺序流
大二进制/视频 无原生 Blob(外存或塞 cell) 无原生 Blob Blob 列+行级引用(共享) MP4 外挂 内嵌
加列 重写 快(metadata-only) 快(挂新 data file 到 fragment) 重导出 重写
删除行 重写 deletion file(Iceberg/Hudi) deletion file 重导出 重写
向量索引 无 无 IVF_PQ/HNSW 原生 无 无
表格式的演进方向 — Iceberg V3 加 VARIANT Blob V2 四存储语义 Streaming from Hub —

Lance 与 Iceberg 系的关系不是替代而是互补——Iceberg 解决数据湖的事务与治理,Lance 解决 AI 负载的随机访问与多模态;Lance Namespace 已支持把 Lance 表挂到 Hive/Glue/Unity Catalog 统一管理。


五、动手实践:搭一套具身训练存储最小闭环

5.1 本地起 JuiceFS 社区版(Redis + MinIO)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 1. 元数据引擎 + 对象存储
docker run -d --name jfs-meta -p 6379:6379 redis:7
docker run -d --name jfs-obj -p 9000:9000 -p 9001:9001 \
-e MINIO_ROOT_USER=minioadmin -e MINIO_ROOT_PASSWORD=minioadmin \
minio/minio server /data --console-address ":9001"
# MinIO 里先建桶 embodied-ds

# 2. 初始化文件系统
juicefs format \
--storage minio \
--bucket http://127.0.0.1:9000/embodied-ds \
--access-key minioadmin --secret-key minioadmin \
redis://127.0.0.1:6379/1 \
embodied-fs

# 3. 挂载(训练读模式:大本地缓存 + 元数据缓存 + 关预取避免稀疏随机读放大)
juicefs mount \
--cache-dir /nvme0/jfsCache \
--cache-size 204800 \
--metacache \
--prefetch=0 \
redis://127.0.0.1:6379/1 \
/mnt/jfs

# 4. POSIX 验证
cd /mnt/jfs && touch t.txt && echo embodied > t.txt && cat t.txt

观察指标用 juicefs stats,重点盯 blockcache.hit/miss(缓存命中)、object.get/put(回源压力)、meta.txn(元数据 QPS)、usage.buf(缓冲水位)。

5.2 用受控 x264 编码 episode + 写入 Lance 多模态数据集

1
2
3
4
5
6
# 受控 GOP 编码:固定 keyint、禁 scene cut、禁 B 帧、zerolatency
# GOP=8, 每个文件一个 episode,SPS/PPS 自包含
ffmpeg -i episode_raw.mp4 \
-c:v libx264 -g 8 -keyint_min 8 -sc_threshold 0 \
-bf 0 -tune zerolatency \
-f h264 /mnt/jfs/episodes/ep_0000.h264
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import lance, pyarrow as pa, numpy as np, subprocess
# 用上面的 schema(§4.4),行按 100Hz 控制 frequency 构建
# 视频列只存 ref_id + gop_index + frame_index_in_gop,blob 由编码后文件写入
rows = []
for ep in range(100):
n_gops = 60 # 假设每 episode 60 个 GOP × 8 帧
for g in range(n_gops):
for f in range(8): # GOP 内帧号
rows.append({
"episode_index": ep,
"frame_index": g*8 + f,
"timestamp_ns": (g*8+f) * 10_000_000, # 100Hz
"observation_state": np.random.randn(14).astype(np.float32),
"action": np.random.randn(14).astype(np.float32),
"observation_images_cam_env": read_gop_bytes(ep, g), # 同 GOP 的 8 行共享同一 blob
"observation_images_cam_env_ref_id": gop_blob_id(ep, g),
"observation_images_cam_env_gop_index": g,
"observation_images_cam_env_frame_index_in_gop": f,
"language_instruction": "fold the shirt",
"source": "human", "is_intervention": False,
})
ds = lance.write_dataset(
pa.Table.from_pylist(rows, schema=schema),
"/mnt/jfs/embodied_ds.lance",
)
# 训练前的批量随机读,走 Rust 并行解码快路径
frames = ds.take_blobs_decoded(
"observation_images_cam_env", indices=[123, 456, 7890], concurrency=32
)

5.3 对比实验:DataLoader 吞吐与 GPU 饥饿

两种方案在同一 JuiceFS 挂载上跑随机抽样(batch=64、num_workers=8、1000 个 batch):

方案 读路径 观察
A. 散文件 + PyAV 逐帧 open(MP4)→ seek 到 keyframe → 逐帧解码 首 batch 延迟高,worker 间重复打开同一文件,D2E 实测朴素并行下每个视频文件平均被打开 ~45 次
B. Lance + take_blobs_decoded 一次 Range GET 整 GOP + Rust 并行解码 吞吐 1.7–6×,batch 越大加速越明显

度量指标建议:samples/s(DataLoader 吞吐)、GPU util(NVIDIA DCGM)、首 batch 延迟、juicefs stats 的 blockcache 命中率。预期结论:方案 B 在命中分布式缓存后第二个 epoch 几乎零回源,GPU 利用率显著抬升。


六、自测问题解答(具身视角)

Q1:为什么团队从 CPFS 迁往 JuiceFS?

成本 + 架构弹性 + 运维解耦。CPFS 吞吐与容量强绑定,为峰值训练吞吐预置 PB 级全闪的成本是 JuiceFS(对象存储底座)的 5–10 倍,且 CPFS 只能扩不能缩、为早期规划失误付出长期代价;具身数据集从 TB 向 PB 演进时,JuiceFS 容量跟随对象存储弹性伸缩、聚合吞吐随客户端数线性扩展,扩训练集群不动存储集群;JuiceFS 把控制面交给托管数据库、数据面交给对象存储,没有存储集群要专业运维,还天然支持跨云数据回流(采集端→训练端)——CPFS 单可用区架构做不到。代价是超低延迟同步写不如 CPFS,但具身负载(读多写少 + 异步可容忍 Checkpoint)恰好避开。

Q2:训练时"存储不成为瓶颈"具体指哪些指标?

对具身训练的量化口径:聚合读吞吐 ≥ DataLoader 需求(batch×分辨率×采样率);随机采样 P99 延迟(含视频解码)< 50ms——其中元数据 lookup 延迟(JuiceFS 元数据引擎可压到 ~100µs,对象存储原生是 10–30ms 级)、Range GET RTT、首帧解码延迟是三个分量;缓存命中率 > 90%(地瓜实测 45%→92%,训练启动 20min→3min);Checkpoint 落盘不阻塞 step,异步落盘速率 ≥ 15 GB/s。

Q3:为什么具身数据(视频、点云、动作序列)适合 Lance 而不是 Parquet?

三个结构性原因:

  1. Parquet 按行组读取、无原生 Blob 语义,取单帧要解码整个行组,大视频块要么外存造成跨文件 join、要么塞 cell 造成读放大;Lance 的 Blob 列 + GOP 尺度受控布局把"取一帧"变成"一次 Range GET + 解一个 ≤8 帧短 GOP";
  2. Parquet 的页偏移索引在内存里随页数爆炸(10 亿行 3KB embedding、8KB 页 = 20GB 索引),小页救随机读毁内存的墙翻不过去;Lance 对大值不建页索引,定位退化为乘法,v2.2 相对 Parquet 压缩减半、随机 Blob 读快 68×;
  3. 具身数据多频率流需要 schema 层对齐(相机 30Hz、控制 100Hz、action chunk 异长),Parquet+MP4+JSON 的物理三分离每次采样都触发跨文件对齐;Lance 把它们放进同一行的不同列,行按最高频控制信号构建、视频经 ref_id 多行共享,高频动作零丢失、视频零冗余。实测随机读 1.7–6×、存储 -42%。

七、收束:具身训练存储的完整拼图

JuiceFS(文件系统层)+ Lance(数据格式层)+ S3 兼容对象存储(物理底座) 三层各司其职:对象存储提供容量与成本的弹性底座;JuiceFS 在其上叠加 POSIX 语义、事务级元数据性能、多级缓存与多云同步,让"亿级小文件 + 多机随机读 + 突发大写"的具身负载不受对象存储原生限制;Lance 在文件系统之上重新组织多模态数据的空间布局,把视频压缩与随机访问这对天然矛盾工程化为 GOP 尺度的受控 trade-off,并让数据集版本管理变成近零成本的 append。

这套组合已在招商狮岩(叠衣任务)、地瓜机器人(具身基座模型)等生产环境验证——当具身数据的 scaling law 开始生效,存储层每一次延迟优化、每一份缓存命中、每一次零拷贝版本快照,都会被乘以 epoch 数和 GPU 小时数,这是数据层税的复利,也是选对存储栈的复利。


延伸资源