从 S3 一次搬到 GPU:Vortex 如何重构机器学习数据加载链路

2026-09-04 28 预计阅读时间: 1 分钟
来源: infoq.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:10 分钟

GPU 训练越来越快,数据加载却仍常沿用一条昂贵的老路径:对象存储下载到本机、写入 NVMe、CPU 解码、转换成训练格式,再复制到 GPU。Onur Satici 介绍的 Vortex 试图缩短这条链路。它是 Linux Foundation 旗下的开源列式文件格式,通过级联轻量编码、基于布局的分段裁剪和零拷贝内存管线,让训练任务可以从 S3 持续向 GPU 输送数据,并在演示场景中达到最高 60 Gbps。

这里真正值得关注的并不只是峰值吞吐量,而是系统边界发生了变化:数据格式不再只是静态存储容器,它开始直接参与查询裁剪、解码调度和设备内存传输。

传统加载链路为什么容易堵塞

典型训练任务可能依次执行以下操作:

  1. 从 S3 下载完整文件或较大的对象范围。
  2. 将数据暂存到本地 NVMe。
  3. 由 CPU 解压、反序列化并转换数据类型。
  4. 把普通主机内存中的批次复制到页锁定内存。
  5. 再通过 PCIe 传入 GPU。

每一步单独看都合理,串联后却会产生重复 I/O 和内存复制。GPU 越快,这些固定成本越明显。增加 DataLoader worker 只能提高并行度,无法消除无效读取、重复转换和中间落盘。

Vortex 的思路可以拆成三个互相配合的机制。

级联轻量编码降低解码成本

列式格式可以针对每一列的数据特征选择编码。Vortex 强调级联轻量编码,即组合多个成本较低的编码步骤,在压缩率和解码速度之间寻找适合高吞吐场景的平衡点。

这与一味追求最小文件不同。训练链路更关心的是端到端时间:如果节省的网络字节需要大量 CPU 周期才能恢复,CPU 仍会成为 GPU 前面的闸门。编码策略因此需要同时观察压缩后大小、CPU 解码吞吐和送入设备的速度。

布局元数据让系统跳过无关分段

如果训练或特征读取只需要部分列、部分样本范围,读取整个对象就是浪费。基于布局的 segment pruning 可以利用文件布局和元数据,在请求 S3 数据前判断哪些分段无关。

这项能力会同时减少网络流量、解码工作量和内存压力。它也意味着数据写入时的排序、分段粒度和列布局会影响之后的训练性能。格式本身可以免除统一的预处理转换,但数据布局仍然需要围绕访问模式设计。

零拷贝管线减少中间缓冲区

“从 S3 到 GPU 一次复制”不应简单理解为网络设备在所有环境中都能直接写入显存。更实用的目标是:避免落盘和多轮用户态复制,让对象存储读取、解码结果和设备传输共享可直接交接的缓冲区。

实际效果取决于云网络、S3 客户端、内存分配器、解码实现、GPU 运行时以及硬件拓扑。60 Gbps 是演示中的上限数据,不等于任意数据集和实例类型都能自动达到这一速度。

可以这样搭建一个可测量的加载原型

下面的示例不调用尚未在摘要中给出的 Vortex API,而是用 PyTorch 构造一个可运行的管线骨架:生产线程代表 S3 范围读取和分段解码,页锁定缓冲区让批次可以异步传入 GPU。接入实际项目时,应把 decode_segment 替换成 Vortex 读取器,并由布局裁剪结果生成 S3 Range 请求。

运行前安装 PyTorch;有 CUDA 时会测量 GPU 传输,没有 CUDA 时也能在 CPU 上验证队列和批处理逻辑。

python -m venv .venv
source .venv/bin/activate
pip install torch
python pipeline_demo.py

pipeline_demo.py

import queue
import threading
import time

import torch

BATCHES = 64
ROWS = 16_384
FEATURES = 256
QUEUE_DEPTH = 4


def decode_segment(index: int) -> torch.Tensor:
    # 实际接入时:按布局元数据读取所需 S3 字节范围并用 Vortex 解码。
    generator = torch.Generator().manual_seed(index)
    return torch.rand((ROWS, FEATURES), generator=generator)


def produce(output: queue.Queue) -> None:
    for index in range(BATCHES):
        batch = decode_segment(index)
        if torch.cuda.is_available():
            batch = batch.pin_memory()
        output.put(batch)
    output.put(None)


def main() -> None:
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    pending = queue.Queue(maxsize=QUEUE_DEPTH)
    worker = threading.Thread(target=produce, args=(pending,), daemon=True)

    started = time.perf_counter()
    transferred = 0
    worker.start()

    while True:
        batch = pending.get()
        if batch is None:
            break
        transferred += batch.numel() * batch.element_size()
        batch = batch.to(device, non_blocking=torch.cuda.is_available())
        _ = batch.square().mean()

    if device.type == "cuda":
        torch.cuda.synchronize()
    elapsed = time.perf_counter() - started
    gbps = transferred * 8 / elapsed / 1_000_000_000
    print(f"device={device}, batches={BATCHES}, elapsed={elapsed:.2f}s, payload={gbps:.2f} Gbps")


if __name__ == "__main__":
    main()

这个原型最重要的部分不是随机数据,而是背压:QUEUE_DEPTH 限制在途批次数,避免生产端持续分配内存。真实实现还应记录每个阶段的耗时,至少包括 S3 等待、下载字节数、裁剪比例、解码时间、主机到设备传输时间和 GPU 空闲时间。

不要只盯着聚合吞吐量

一个加载器显示 60 Gbps,不代表训练一定更快。吞吐测试需要同时回答几个问题:

  • GPU 是否因为等数据而空闲,训练步时间降低了多少?
  • 统计的是压缩字节、解码后字节,还是实际进入 GPU 的张量字节?
  • segment pruning 跳过了多少数据?不同过滤条件下是否稳定?
  • CPU 利用率是否下降,还是瓶颈只是转移到了某个解码线程?
  • 多个训练节点并发读取时,S3、网络接口和请求配额是否仍能支撑?
  • 小对象和随机读取是否产生过多请求费用与尾延迟?

建议用训练 step time 和 GPU 利用率作为主指标,再用网络吞吐解释原因。单独优化 GB/s 很容易得到一个漂亮但与模型训练无关的结果。

采用前的工程检查

Vortex 适合优先在数据量大、重复训练频繁、GPU 等待明显的任务上试点。先选一个真实数据集和固定模型,对比现有链路与新链路的端到端表现,不要只运行格式微基准。

上线前需要确认四件事:布局能否匹配常见列选择和样本过滤;编码是否会把压力重新推给 CPU;缓冲区生命周期是否受到严格控制;对象存储请求成本和失败重试是否纳入测量。还要保留兼容路径,因为格式生态、分析工具和故障排查能力通常比原始吞吐量更慢成熟。

Vortex 展示的核心方向很明确:与其不断扩大本地缓存和 CPU worker 池,不如让文件布局、裁剪、解码与 GPU 传输形成一条连续的数据路径。真正的收益不是省掉某一个函数调用,而是让 GPU 不再等待一连串本可避免的中间步骤。


相关推荐