Shipping a Trillion Parameters With a Hub Bucket: Delta Weight Sync in TRL

要点:你还有模型要训练,我们先给出核心结论。

  • 异步强化学习有一个不太显眼的成本:每一步,训练器都必须把整个模型发送到推理引擎。bf16 的7B模型需要14 GB;前沿1T参数模型的检查点则约为一太字节。每一步都要传。
  • 其实不必如此。连续两次强化学习优化器步骤之间,约99%的 bf16 权重在位级别完全相同,最差情况也不会低于98%。真正的增量非常小。
  • 我们提交了 一个 TRL PR,只把变化的元素编码为稀疏 safetensors 文件,上传到 Hugging Face Bucket,再通知 vLLM 获取。在 Qwen3-0.6B 上,每步载荷由1.2 GB降至 20至35 MB。
  • 更进一步,我们完成了一次完全解耦的训练:训练器位于一台机器,vLLM 运行在一个 Hugging Face Space,Wordle 环境运行在另一个 Space,权重通过单个 Hub Bucket 流转。无需共享集群、RDMA 或 VPN。

异步强化学习的成本因此大幅下降。下面详细介绍。

查看原文交互示意图

传输相同权重的两种方式。红色表示没有生成 token 的实际耗时。




1. 一太字节的问题

如果读过我们此前关于异步强化学习训练生态的文章,就已经知道关键点。无论如何定义 actor 模型,或使用怎样的 NCCL 后端,每个异步强化学习库最终都会碰到同一个根本问题:权重同步。

推理引擎正在使用第 N 步的策略,训练器刚完成第 N+1 步。必须把新权重传过去,否则推理引擎就会越来越偏离当前策略。同步与异步训练都绕不开这一关键路径:阻塞式传输会让 GPU 不生成 token,从而浪费空闲算力。稀疏增量路径能把空闲时间压缩到几秒,训练器甚至无需等待推理引擎就绪:优化器步骤结束后,即可发布“权重已就绪”信号并上传到共享 Bucket;推理引擎则按自己的时机获取。

Fireworks 在其文章 《前沿强化学习比你想象的便宜》中给出了一个令人印象深刻的数字:其采用 fp8 的前沿1T参数检查点,完整快照大小为 1024 GiB。传统思路认为,每次更新 rollout 集群都必须发送这些数据。这会让人想到巨型集群、RDMA 网络和专用跨区域链路。然而,他们测得相邻检查点的平均增量只有 20.3 GiB,即完整模型的1.98%,并且连续检查点之间超过98%的 bf16 格式权重在位级别保持相等。

Cursor 的 Composer 2 报告也描述了类似方案:训练与推理分布在不同区域,通过共享 S3 Bucket连接;训练器在每个训练步骤把压缩后的权重差异上传到 Bucket。各集群独立下载并重建共享增量链,不需要直接连接训练集群。双方不会直接交换参数,Bucket 就是传输介质。

两篇报告有三个共同结论。值得逐条强调,因为本文剩余内容本质上是在开源环境中实现同样的思路:

  1. 相邻两次强化学习步骤之间,多数权重其实没有变化。
  2. 只发送变化部分,可以将带宽开销减少约两个数量级。
  3. 通过共享对象存储传输这些小增量,训练器与推理集群就不必位于同一数据中心。

缺少的只是一个能够通过 pip install 安装使用的实现。因此我们写了一个。




2. 为什么 bf16 强化学习权重几乎总是稀疏变化的

在连接各组件之前,先理解这个方案为什么成立。“98%的权重不变化”听起来像只在演示中成立、实际应用就崩溃的数字,但它来自强化学习所用学习率下 bf16 运算的特性。

bf16 数有7位尾数。相邻两个2的幂之间,恰好有 27=1282^7 = 128 个可表示值。因此,在 ∣w∣|w| 附近,相邻 bf16 数的间距大约为 ∣w∣⋅2−7|w| \cdot 2^{-7}。只要更新幅度低于这一间距的一半,也就是满足 ∣Δw∣<∣w∣/256|\Delta w| < |w|/256,该更新就会在转换到 bf16 时被吸收。这就是 PULSE 图3中的“bf16 可见性阈值”。

再看 Adam。在例如 3×10−63 \times 10^{-6} 这样的强化学习学习率下,单个权重的更新为:Δw=−η⋅m^v^+ϵ\Delta w = -\eta \cdot \frac{\hat{m}}{\sqrt{\hat{v}} + \epsilon}

归一化步骤 m^/(v^+ϵ)\hat{m}/(\sqrt{\hat{v}}+\epsilon) 大致为1的量级,因此 ∣Δw∣≈η≈3×10−6|\Delta w| \approx \eta \approx 3 \times 10^{-6}。多数权重的 ∣w∣|w| 大约在 10−210^{-2} 至 10−110^{-1} 之间;PULSE 对代表性 LLM 权重报告的中位数为0.019。在这一幅值下,阈值 ∣w∣/256|w|/256 大约是 4×10−54 \times 10^{-5} 至 4×10−44 \times 10^{-4},比更新幅度更大。

换言之,优化器在轻声耳语,bf16 却听不见。更新被舍入吸收,ww 的字节表示没有改变,因此在推理引擎看来,该权重并未移动。将这一现象乘以几亿个参数,就能得到超过99%的稀疏率,而且没有引入近似。

PULSE 论文(Mihai 与 Belilovsky,2026)正是把这一论证形式化。论文定义了两个阈值:吸收界限 10η10\eta是 Adam 更新的保守最坏情况界限,而有效界限 η\eta对应实际所在的范围。bf16 可见性阈值为 ∣w∣/256|w|/256。更新低于可见性阈值时,会被吸收,bf16 字节不变。图3把两个界限与一组代表性 LLM 权重对比,结论很清楚:在 η=3×10−6\eta = 3 \times 10^{-6} 时,吸收界限本身就已低于模型中几乎所有权重的可见性阈值。论文对 Qwen2.5(0.5B/1.5B/7B)、Llama-3.2-3B 和 Gemma-3-4B 实测,稳定得到约99%的平均每步稀疏率;400个训练步骤中的标准差为0.2%至0.4%。最差步骤仍超过98%。因此,变化低于1%并非幸运测量,而是这些算术条件下的结果。

我们不需要解析预测这一现象;事实上,我们尝试过从 Adam 的 mm 与 vv 统计量预测变化掩码,但召回率仅30%,后文会解释。我们只需要观察哪些字节发生了变化:在优化器步骤前后,为每个参数计算一个小型布尔张量。

查看原文交互示意图

将学习率降低到强化学习范围,可以看到转回 bf16 的标记回到原来的刻度。左下角的256元素网格展示这一效应在小模型中的总体表现。




3. HF Buckets 与架构

现在介绍第二个关键部分。从这里开始,我们不再只是复现 Fireworks/Cursor 的思路,而是用 Hugging Face 的组件构建实现。




3.1 什么是 Bucket?

一个 Bucket 是 Hub 上专为高频对象存储设计的仓库类型。不需要提交仪式、PR 流程或处理 LFS 的特殊行为:添加、列出、下载文件即可。Python 接口由两个函数构成:

from huggingface_hub import batch_bucket_files, download_bucket_files

# Trainer side
batch_bucket_files("my-org/wordle-deltas", add=[(buffer, "deltas/step_000042.safetensors")])

# Inference side
download_bucket_files("my-org/wordle-deltas", files=[("deltas/step_000042.safetensors", local_path)])

仅两次函数调用,权重就开始传输。

底层 Bucket 使用 Xet,即 Hub 基于内容分块的存储层。Xet 根据文件实际内容而非固定偏移进行分块,并与 Bucket 中已有内容去重。因此,即使不实现稀疏编码、每步都上传完整锚点,Xet 仍然只传输变化的块。稀疏编码与 Xet 叠加后,只为发生变化的数据付出一次传输成本。

这相当于 Fireworks 和 Cursor 使用“共享 S3 Bucket”方案的开源版本,但存储层已经支持内容哈希,已有 HF 令牌可以授权访问,并能与 Spaces、数据集和模型等其他组件原生组合。




3.2 三个组件

完整架构有三个组件,以及一个共享基础设施:

  • 训练器。可以放在任意位置:一块 GPU、八块 GPU,甚至通过 USB 连接 H100 的笔记本——我们不会评价你的选择。它拥有模型权重、运行优化器并生成稀疏增量。
  • HF Bucket。一个仓库,两个前缀:anchors/ 存放偶尔写入的完整快照,deltas/ 存放中间的稀疏补丁。这是双方唯一需要约定的共享位置。
  • vLLM rollout 服务器。可以放在任意位置,尤其不必与训练器在同一位置。它从 Bucket 获取数据、应用增量并提供 rollout。
  • 环境。以常规方式挂在 rollout 服务器旁,通过 HTTP、函数调用或环境采用的其他协议交互。

需要理解的关键性质,也是 Cursor 报告强调且在这里同样成立的一点:训练器与 rollout 服务器不会直接交换权重。双方只交换一个包含 {"repo_id": ..., "filename": ...} 的小型 POST 请求;这就是整个控制平面。实际字节传输分别发生在双方与 Bucket 之间,可以并行进行,不需要共享网络结构。

这在实践中意味着:

  • rollout 服务器可以位于另一个区域、另一朵云,或 Hugging Face Space 的 NAT 之后,方案不受影响。
  • N 个推理副本可以从同一个 Bucket 获取同一增量,Xet 在这些副本之间对数据去重。
  • 训练器无需知道有多少推理副本、位于哪里,或某个副本是否刚刚崩溃。

训练器写,副本读,Hub 负责数据流转。




4. 协议

下面看实现内部。协议有四部分:传输格式、Bucket 布局、约30行的 vLLM 扩展,以及训练端变化检测器。实际代码比听起来少。




4.1 使用 safetensors 作为传输格式

我们选择 safetensors 作为磁盘与传输格式。它已经是 Hub 上通行的检查点格式,常见框架均能读取;文件头还支持任意字符串元数据,我们把协议信息放在那里。

Bucket 中有两种文件。

锚点看起来像普通检查点:每个参数对应一个张量,保存完整 bf16 权重,每 NN 次同步写入一次,默认 N=10N=10。

anchors/step_000010.safetensors
  ├── model.layers.0.self_attn.q_proj.weight   (bf16, full)
  ├── model.layers.0.self_attn.k_proj.weight   (bf16, full)
  └── ...
metadata:
  sparse=False, model_version=10, sparsity=0.0

增量才是关键。每个实际发生变化的参数存储两项内容:一个平坦的 int32 元素索引张量,以及这些索引位置上值的 bf16 张量。

deltas/step_000011.safetensors
  ├── model.layers.0.self_attn.q_proj.weight.indices   (int32, [num_changed])
  ├── model.layers.0.self_attn.q_proj.weight.values    (bf16,  [num_changed])
  ├── model.layers.0.mlp.gate_proj.weight.indices
  ├── model.layers.0.mlp.gate_proj.weight.values
  └── ...
metadata:
  sparse=True, model_version=11, sparsity=0.9938, changed_params=[...]

这一选择有几个好处:

  • 增量是一个文件。可以在 Python 中通过 safe_open(...) 打开并检查每个张量。没有专有帧格式、长度前缀或版本握手。
  • 元数据是自描述的。接收方读取 sparse=True/False 后选择对应分支,无需额外清单。
  • 推理端可通过 mmap 实现零拷贝;每隔几秒同步一次时,这一点很重要。

节奏很直接:每第 N 步写入锚点,中间写增量;都放在同一 Bucket 的 anchors/ 和 deltas/ 前缀下。新推理副本只需获取最近的锚点,再重放此后的增量。

查看原文交互示意图

十个训练步骤:第1和第6步写锚点,即完整快照,其他步骤写稀疏增量。演示中可以观察文件进入 Bucket。




4.2 训练端:通过优化器钩子生成布尔掩码

训练器需要知道哪些 bf16 元素真正发生变化。我们用一个小型 BF16ChangeDetector 实现,并在优化器上注册步骤前与步骤后的钩子:

class BF16ChangeDetector:
    def __init__(self, model, optimizer):
        self._pre_step_bf16: dict[str, torch.Tensor] = {}
        self._validated_masks: dict[str, torch.Tensor] = {}
        optimizer.register_step_pre_hook(self._pre_step_hook)
        optimizer.register_step_post_hook(self._post_step_hook)

    def _pre_step_hook(self, opt, args, kwargs):
        for p in self._params:
            self._pre_step_bf16[name_of(p)] = p.detach().to(torch.bfloat16).cpu().clone()

    def _post_step_hook(self, opt, args, kwargs):
        for p in self._params:
            self._validated_masks[name_of(p)] = (
                p.detach().to(torch.bfloat16).cpu() != self._pre_step_bf16[name_of(p)]
            )

PR 的真实代码还有一些处理,例如通过 data_ptr() 将优化器参数对象与模型参数匹配,因为 Accelerate 会把它们包装成不同的 Python 对象。但核心逻辑很简单:快照、执行一步、比较差异。

这反映了实际变化。我们尝试过更优雅的方式:利用 Adam 的 mm 和 vv 统计量及 bf16 ULP 阈值直接预测掩码。理论上可行,实际上召回率约30%,意味着传出的增量会漏掉约三分之二的真实更新。Adam 的归一化足够复杂,使解析阈值并不紧。因此我们直接比较字节,代价是在训练端保留一份模型的 CPU bf16 快照;这个代价可以接受。

新的 _sync_weight 流程分为四个阶段:

  1. 上传期间,推理继续运行。训练器把掩码选中的元素编码到 safetensors 缓冲区并推送到 Bucket。整个过程期间,vLLM 仍使用旧策略提供服务。
  2. 暂停 vLLM。一次短 HTTP 调用,耗时数百毫秒。
  3. 调用 /update_weights。发送 Bucket 坐标;vLLM 下载、应用并返回。
  4. 恢复运行。vLLM 重新提供服务。

日志说明了这一过程:

Delta: 1234567/200000000 elements changed (sparsity=99.38%)
[delta_engine] uploaded user/wordle-deltas/deltas/step_000042.safetensors (27.4 MB, ...)
Weight sync: done. Total 9.4s (inference paused 1.1s)

最重要的是括号中的数字:推理只暂停了 1.1秒。其余9.4秒用于上传,期间 rollout 服务器仍在生成 token。NCCL 路径把完整同步时间变成暂停时间,而这里把它变成后台时间。

查看原文交互示意图

一次同步的完整过程。可在 Bucket 增量与 NCCL 广播间切换,并改变副本数量,观察分发给多个副本的行为。




4.3 vLLM 端:一个约30行的扩展

vLLM 提供了一个清晰的抽象,名为 WeightTransferEngine。我们实现 DeltaWeightTransferEngine,其 receive_weights 方法的核心思路如下:

def receive_weights(self, update_info, load_weights):
    download_bucket_files(update_info.repo_id, files=[(update_info.filename, local_path)])
    with safe_open(local_path, framework="pt", device="cpu") as f:
        meta = PatchMetadata.from_metadata_dict(f.metadata())
        if not meta.sparse:
            # Anchor: feed every tensor and snapshot for future deltas
            for name in f.keys():
                tensor = f.get_tensor(name)
                self._bf16_snapshot[name] = tensor.clone()
                load_weights([(name, tensor)])
        else:
            # Delta: apply (indices, values) to snapshot, hand full tensor to vLLM
            for name in json.loads(meta.changed_params):
                indices = f.get_tensor(f"{name}.indices").long()
                values = f.get_tensor(f"{name}.values")
                snap = self._bf16_snapshot[name].flatten()
                snap[indices] = values
                self._bf16_snapshot[name] = snap.reshape(self._bf16_snapshot[name].shape)
                load_weights([(name, self._bf16_snapshot[name])])

通过 vLLM 的 --worker-extension-cls 标志注册扩展,因此不需要 fork vLLM。只需在 vLLM 的同一镜像中安装 TRL,再让 CLI 指向我们的类。

另外,vLLM 自身也在推进原生稀疏权重传输:vllm-project/vllm#40096。它直接在 receive_sparse_weights() 与 trainer_send_sparse_weights() 所在的 WeightTransferEngine 基类中加入相关接口,将补丁编码为 (indices, values),通过 index_copy_() 原地应用,完全移除 GPU/CPU 验证往返。PR 报告 Qwen3-1.7B 稀疏补丁的传输为0.16 MB,耗时0.40 ms,而完整稠密传输为 942 MB,耗时192 ms。

我们推理端实现有一项限制:要从稀疏 (indices, values) 补丁重建完整张量,必须保存一份模型的 CPU bf16 快照,因为当前 vLLM 的 load_weights 需要完整张量。等 #40096 或后继实现合入并提供原地稀疏 load_weights 路径,就能直接在 GPU 上应用索引,删除这份快照。




5. 真正在 Spaces 上运行

我们对此很满意。前面介绍的方案可以在笔记本上运行,但通过 Hub Bucket 路由权重的意义,是让训练器与 rollout 服务器不必相邻。因此,我们用三台不共享网络的机器完成了完全解耦的训练:

  • 一台单 GPU 机器运行训练器。
  • 一个 Hugging Face Space,采用 Docker SDK、L4 GPU,运行带有我们扩展类的 vLLM。
  • 第二个 Hugging Face Space,使用 CPU,运行支持256个并发会话的 Wordle 环境服务器。
  • 中间使用一个 Hub Bucket。

配置仅需几次 hf CLI 调用。vLLM Space 的 Dockerfile 基本上就是上游 vLLM 镜像,加上 pip install trl@... 和入口命令:

FROM vllm/vllm-openai:latest
RUN pip install "trl @ git+https://github.com/huggingface/trl.git@delta-weight-sync"
ENV VLLM_SERVER_DEV_MODE=1
EXPOSE 7860
ENTRYPOINT ["vllm", "serve", "Qwen/Qwen3-1.7B", \
    "--host", "0.0.0.0", "--port", "7860", \
    "--worker-extension-cls", "trl.experimental.async_grpo.delta_engine.DeltaWorkerExtension", \
    "--weight-transfer-config", "{\"backend\":\"nccl\"}", \
    "--max-model-len", "32768", \
    "--gpu-memory-utilization", "0.8"]

将它部署为 Space:

hf repos create $USER/vllm-wordle-inference \
    --type space --space-sdk docker --flavor l4x1 \
    --secrets HF_TOKEN=$HF_TOKEN
hf upload $USER/vllm-wordle-inference examples/scripts/openenv/vllm_space/ --type space

然后从全球任意能通过 HTTPS 通信的位置启动训练:

python examples/scripts/openenv/async_wordle.py \
    --vllm-server-url https://$USER-vllm-wordle-inference.hf.space \
    --env-url https://openenv-wordle.hf.space \
    --delta-sync-repo-id $USER/wordle-deltas \
    --model Qwen/Qwen3-1.7B

训练器无需开放端口,Space 不会看到训练器的 IP,Wordle 环境也不知道另外两者存在。它们都与 Hub 通信。训练首先通过立即输出 EOS 的基本检查,随后在真实 Wordle rollout 上收敛:奖励上升、增量载荷保持在20至35 MB之间,每次同步中推理暂停窗口约为一秒。完整运行日志位于配套 PR。




6. 这实际带来了什么?

我们认为以下几项能力很有意义。

无需集群的异步强化学习训练。只要有一块 GPU 和 Hugging Face 账号,就能进行真正解耦的训练:训练器运行在 GPU 上,rollout 集群运行在 Spaces 中,环境运行在另一个 Space,权重经由 Bucket 传输。以前需要同机部署并接受吞吐量方面的妥协,或使用有共享网络的真实集群;现在无需如此。

自然支持多副本推理。启动两个或十个 vLLM Space,它们都从同一个 Bucket 拉取。Xet 的内容寻址存储让连续锚点共享静态数据块,避免 Bucket 急剧膨胀;Hub 的边缘缓存也降低重复下载同一文件的服务成本。想部署全球分布式 rollout 集群,现在更像一项小型 DevOps 工作,而不是研究项目。

可以用已有工具调试的传输格式。增量是 safetensors 文件,可以在 notebook 中通过 safe_open 打开,列出键、检查索引并自行计算稀疏率。我们曾花许多小时用 tcpdump 分析不透明的 NCCL 流,因此很珍惜这一点。

通往前沿规模的路径。20至35 MB是 Qwen3-0.6B 的数字。扩大模型后情况如何?可以进行一个粗略估算。

以 Llama-3.1-405B 为例,bf16 磁盘大小为 810 GB。PULSE 在强化学习学习率下测得约99%的平均每步稀疏率,因此实际增量约占参数的1%。其实际部署编码在 7B模型上达到108 MB,对应 PULSE 报告的 约130倍 缩减。线性外推到405B,每步增量约为 6 GB。

这能节省多少实际时间?NCCL 在集群内确实很快。假设多节点 RDMA 等条件下有充裕的100 GB/s总广播带宽,完整同步每步造成 810 GB / 100 GB/s ≈ 8 seconds 的推理暂停。使用增量路径时,训练器将6 GB数据在后台流式上传到 Bucket,生成继续运行;rollout 服务器实际暂停窗口只有应用权重的步骤,在这个规模下约为几秒。因此,即使仍在集群内,增量也能把可见暂停减少约4倍、传输字节减少约130倍。

再考虑跨集群。原文指出,NCCL 不能直接用于跨云连接。当你希望一组 rollout 副本位于 us-east,另一组位于 eu-west,还有一组运行在 Hugging Face Space 时,基于 Bucket 的路径成为唯一选择。在1 GB/s可用互联网带宽下,完整广播约需13分钟,增量约需6秒。

对于 Fireworks 所讨论的1 TB级模型,他们实测为 20.3 GiB增量,对比1024 GiB完整快照,约缩减50倍。PULSE 更紧凑的稀疏编码可能进一步改善,外推约15 GB每次增量,即约65倍。无论哪种情况,都进入了通过普通对象存储传权重不再是权宜之计,而成为合理架构的范围。




7. 尚待解决的问题

我们并不认为工作已经全部完成,下面是目前的限制。

  • 两份 CPU bf16 快照,多了一份。训练器保留一份用于变化检测,rollout 服务器保留一份用于给 vLLM 的 load_weights 重建完整张量。除非有人找到紧致的解析掩码,否则前者仍然需要保留,而这比看起来更难。后者则会在 vLLM 获得稀疏 load_weights API 后移除;相关 PR 将会提交。
  • 固定锚点周期。目前每 NN 步保存一次完整锚点。自适应策略,例如累计偏移超过阈值才写锚点,可以减少长期训练的锚点成本。
  • 多节点 FSDP2 训练器。当前 BF16ChangeDetector 依赖每个进程的优化器钩子。它应当能自然扩展到 FSDP2,但我们尚未在多节点规模测量。PR 中已留下 TODO。
  • 优化器钩子。我们尝试仅根据 (m,v)(m, v) 预测掩码,召回率很低。这意味着解析 bf16 阈值中有比教科书公式更微妙的因素。欢迎已经解决这一问题的人分享经验。
  • 叠加传输压缩。稀疏 safetensors 与逐块 gzip 彼此独立,尚未尝试结合;不过,我们不预期出现非常大的压缩收益。




8. 试用

来源与版权

原文:用 Hub Bucket 传输万亿参数:TRL 增量权重同步;作者:Amine Dirhoussi、Quentin Gallouédec、Kashif Rasul、Lewis Tunstall、Edward Beeching、Albert Villanova del Moral、Leandro von Werra、Sergio Paniego;发表日期:2026-05-27。

原文版权归 Hugging Face 与原作者所有。示例软件与其各自仓库许可证保持一致;性能数字限于原文实验与外推条件。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容