TRL 新增 Delta Weight Sync:通过 Hub Bucket 传输权重变化,每步从 1.2 GB 降至 20-35 MB

Hugging Face:Blog(RSS)·2026-05-27 08:00·90天前
AI 导读

异步强化学习中,训练器每步需将完整模型权重(如1T参数checkpoint约1 TB)传输给推理引擎。TRL新增PR利用相邻RL优化步骤间约99%的bf16权重比特相同的特点,仅将变化的权重编码为稀疏safetensors文件,上传至Hugging Face Bucket并通知vLLM获取。在Qwen3-0.6B上,每步传输从1.2 GB降至20–35 MB。实验还展示了完全分离的训练场景:训练器、vLLM和Wordle环境分别位于不同机器和Hugging Face Space中,权重通过单个Hub bucket流动,无需共享集群、RDMA或VPN。

Hugging Face:Blog(RSS)
精选
61AI 编辑部评分,满分 100

TRL 新增 Delta Weight Sync:通过 Hub Bucket 传输权重变化,每步从 1.2 GB 降至 20-35 MB

2026-05-27 08:00· 90天前
AI 导读

异步强化学习中,训练器每步需将完整模型权重(如1T参数checkpoint约1 TB)传输给推理引擎。TRL新增PR利用相邻RL优化步骤间约99%的bf16权重比特相同的特点,仅将变化的权重编码为稀疏safetensors文件,上传至Hugging Face Bucket并通知vLLM获取。在Qwen3-0.6B上,每步传输从1.2 GB降至20–35 MB。实验还展示了完全分离的训练场景:训练器、vLLM和Wordle环境分别位于不同机器和Hugging Face Space中,权重通过单个Hub bucket流动,无需共享集群、RDMA或VPN。

推荐理由

异步RL训练中权重同步的瓶颈被HuggingFace用稀疏增量方案解决了,带宽直接省了两个数量级,还给了可运行的TRL分支,做RL训练的可以直接上手试。

正文 · AI 翻译

长话短说,因为您还有模型要训练,我们理解这一点:

  • 异步强化学习有一个不为人知的秘密:每一步,训练器都必须将整个模型传输到推理引擎。对于一个 7B 参数、bf16 精度的模型,这相当于 14 GB。对于一个前沿的 1T 参数模型检查点,每次传输的数据量大约在 TB 级别。每一步都是如此。
  • 事实证明,你并不需要这样做。在两个连续的强化学习优化器步骤之间,大约 99% 的 bf16 权重是比特完全相同的(在最坏情况下也从未低于 98%)。实际的差异非常微小。
  • 我们提交了一个 TRL 拉取请求,它仅将发生变化的元素编码为一个稀疏的 safetensors 文件,上传到 Hugging Face 存储桶,并通知 vLLM 去获取它。在 Qwen3-0.6B 模型上,每一步的传输负载从 1.2 GB 降到了 20 到 35 MB。
  • 锦上添花的是:我们运行了一个完全分离的训练流程,其中训练器在一台机器上,vLLM 运行在一个 Hugging Face Space 中,Wordle 环境运行在另一个 Space 中,而权重则通过一个单一的 Hub 存储桶流动。无需共享集群,无需 RDMA,无需 VPN。

异步强化学习变得便宜多了。请继续阅读。

传输相同权重的两种方式。红色部分表示没有生成任何 token 的挂钟时间。


1. 一 TB 的问题

如果你读过我们之前关于异步强化学习训练格局的文章,你已经知道关键点了。每一个异步强化学习库,无论它如何拼写“actor 模型”,或者它的 NCCL 后端是什么颜色,最终都会遇到同一个根源问题:权重同步。

推理引擎使用的是第 N 步的策略。训练器刚刚完成了第 N+1 步。新的权重必须在推理引擎开始危险地偏离策略之前,从一端传输到另一端。无论你运行的是同步还是异步模式,这都处于关键路径上:一次阻塞式传输意味着 GPU 在浪费空闲算力,没有生成任何 token。通过稀疏增量路径,你可以将这段空闲时间压缩到几秒钟,而且训练器甚至不需要等待推理引擎准备好:它只需在优化器步骤完成的那一刻发布“权重已就绪”并上传权重到共享存储桶,而推理引擎则可以在自己的时间线内获取。

Fireworks 在其文章《前沿强化学习比你想象的更便宜》中给出了一个令人印象深刻的数字:对于一个 fp8 格式的前沿 1T 参数检查点,完整快照大小为 1024 GiB,而传统观念认为,每次更新你的推理集群时都必须传输这个完整快照。正是这样的数字,让人们开始绘制包含超级集群、RDMA 网络和专用跨区域链路的架构图。他们测量到的相邻检查点之间的平均差值为 20.3 GiB,仅占完整模型的 1.98%,并且“超过 98% 的 bf16 格式权重在连续检查点之间保持比特级一致”。

Cursor 的 Composer 2 报告讲述了一个类似的故事。他们在不同区域运行训练和推理,并通过一个共享的 S3 存储桶(他们的原话)将它们连接起来,训练器会在每个训练步骤将压缩后的权重差异上传到该存储桶。每个集群独立地从共享的差异链中下载并重建模型,“无需与训练集群建立直接连接”。双方从不直接通信参数信息。这个存储桶就是连接它们的“线”。

两篇论文在三点上达成共识,我们想慢慢重复这几点,因为本文的其余部分本质上是对这些共识的忠实开源翻译:

  1. 在相邻的两个强化学习步骤之间,大部分权重实际上并未发生变化。
  2. 如果你只发送发生变化的部分,你的带宽成本将大约降低两个数量级。
  3. 如果你通过一个共享对象存储来路由这些微小的差异数据,那么训练器和推理集群就不再需要位于同一个数据中心。

唯一缺少的是一个可以通过 `pip install` 安装的版本。于是我们编写了一个。

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

在我们连接任何东西之前,有必要理解为什么整个策略是可行的。“98% 的权重不变”这个说法听起来可疑,像是那种在演示中有效、但在实际应用中就会失效的数字。但事实并非如此。这是由 bf16 算术在强化学习所使用的学习率下的工作方式所决定的。

一个 bf16 数有 7 位尾数。在两个相邻的 2 的幂之间,恰好有 $2^{7} = 128$ 个可表示的值,因此在 $\mid w \mid$ 附近相邻 bf16 数之间的间隔大约为 $\mid w \mid \cdot 2^{- 7}$。当一次更新量低于该间隔的一半时,即当 $\mid \Delta w \mid < \mid w \mid / 256$ 时,该更新就会被 bf16 的数值转换所吸收。这就是 PULSE 在其图 3 中绘制的“bf16 可见性阈值”。

现在来看看 Adam 的做法。在强化学习的学习率设为,比如 $3 \times 10^{- 6}$ 时,对单个权重的更新为:$\Delta w = - \eta \cdot \frac{\hat{m}}{\sqrt{\hat{v}} + \epsilon}$

归一化后的步长 $\hat{m} / \left(\right. \sqrt{\hat{v}} + \epsilon \left.\right)$ 大致为 1 的量级,因此 $\mid \Delta w \mid \approx \eta \approx 3 \times 10^{- 6}$。对于大多数权重来说,$\mid w \mid$ 大约在 $10^{- 2}$ 到 $10^{- 1}$ 之间(PULSE 报告称,代表性大语言模型权重的中间值为 0.019)。在该量级下,阈值 $\mid w \mid / 256$ 大约在 $4 \times 10^{- 5}$ 到 $4 \times 10^{- 4}$ 之间,这比更新量要大。

换句话说:优化器在低声细语,而 bf16 听不到。更新被舍入操作所吸收,$w$ 的字节表示没有变化,从推理引擎的角度来看,这个权重根本没有移动。将这种情况乘以数亿个参数,你就得到了大于 99% 的稀疏度数值,无需任何近似,完全免费。

这正是 PULSE 论文(Mihai & Belilovsky, 2026)中正式提出的论点。他们定义了两个阈值。吸收边界 $10 \eta$ 是 Adam 更新的保守最坏情况,而有效边界 $\eta$ 则是实际所处的区间。bf16 可见性阈值是 $\mid w \mid / 256$。每当更新低于可见性阈值时,它就会被吸收,bf16 字节不会发生变化。他们的图 3 将这两个边界与代表性的大语言模型权重云图进行了对比,结论是明确的:在 $\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 的 $m$ 和 $v$ 统计量预测变化掩码,但召回率低至可怜的 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)])

仅此而已。两个函数调用,你的权重就开始传输了。

在底层,存储桶由 Xet 支撑,这是 Hub 基于内容分块的存储层。Xet 会检查你上传的每个文件,根据其实际内容(而非固定偏移量)将其切分成多个块,并与存储桶中已有的所有内容进行去重。实际效果——在此场景下非常令人欣喜——是即使我们懒得编写稀疏编码,每一步都直接上传完整的锚点,Xet 也只会传输发生变化的块。稀疏编码 + Xet 技术栈:我们只为发生变化的内容付费,而且只付一次。

这相当于 Fireworks 和 Cursor 所使用的“共享 S3 存储桶”的开源版本,区别在于该存储层已经了解内容哈希,你现有的 HF token 已具备权限,并且它能与栈中的其他部分(Spaces、数据集、模型)原生组合。

3.2 三个盒子

完整架构恰好包含三个盒子和一个共享基础层:

  • 训练器。放在你想要的任何地方。一块 GPU、八块 GPU、一台通过 USB 连接 H100 的笔记本电脑,我们都不会评判。它拥有模型权重,运行优化器,并输出稀疏增量。
  • HF 存储桶。一个单一的仓库,包含两个前缀:`anchors/` 用于偶尔的完整快照,`deltas/` 用于期间的稀疏补丁。这是双方唯一达成一致的部分。
  • vLLM 部署服务器。放在你想要的任何地方,关键是未必与训练器在同一位置。它从存储桶拉取数据,应用增量,并提供部署服务。
  • 环境。以常规方式(HTTP、函数调用,或你的环境支持的任何方式)挂载在部署服务器上。

需要内化的特性——也是 Cursor 那篇论文极力推崇、在此处完全适用的——是训练器和部署服务器之间从不直接通信权重。它们交换一个微小的 POST 请求,包含 `{"repo_id": ..., "filename": ...}`,这就是整个控制平面。实际的字节传输发生在每一方与存储桶之间,并行进行,无需共享网络结构。

为什么这在实践中很重要:

  • 部署服务器可以位于另一个区域、另一个云,或者 Hugging Face Space 内部的 NAT 之后。它完全不在意。
  • N 个推理副本可以从同一个存储桶拉取同一个增量,而 Xet 会在所有副本之间对字节进行去重。
  • 训练器永远无需知道存在多少个推理副本,它们位于何处,或者其中某个副本是否刚刚崩溃。

训练器负责写入。副本负责读取。Hub 负责管道连接。

4. 协议

现在我们可以打开引擎盖了。该协议包含四个部分:一种线格式、一种存储桶布局、一个 30 行的 vLLM 扩展,以及一个训练器端的变更检测器。老实说,它的代码量比听起来要少。

4.1 Safetensors 作为线格式

我们选择了 safetensors 作为磁盘和网络传输格式。它已经是 Hub 上标准的检查点格式,所有主流的框架都能读取它,并且其头部可以携带任意的字符串元数据。我们正是利用这个元数据字段来隐藏协议。

存储桶中有两种类型的文件。

锚点文件看起来像一个正常的检查点:每个参数对应一个张量,完整的 bf16 权重,每 $N$N 次同步写入一次(我们默认 $N = 10$N=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 步写入一个锚点文件,中间步骤写入增量文件。两者都存放在同一个存储桶中,分别位于 `anchors/` 和 `deltas/` 前缀下。每个新的推理副本只需获取最新的锚点文件,然后重放此后的增量文件即可。

十个训练步骤。在第 1 步和第 6 步写入锚点文件(完整快照),在其他每一步写入稀疏增量文件。你可以实时看到文件落入存储桶中。

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 的 $m$ 和 $v$ 统计量来预测掩码,并直接使用 bf16 ULP 阈值。这种方法在原理上可行,但在实际中召回率仅为 30% 左右,这意味着我们最终交付的增量更新会遗漏三分之二的实际更新。Adam 的归一化过程相当复杂,导致分析阈值并不精确。因此我们直接比较字节。代价是在训练端对模型进行一次 bf16 CPU 快照,这个成本我们愿意承担。

新的 `_sync_weight` 流程包含四个阶段:

  1. 推理持续运行的同时进行上传。训练器将掩码后的元素编码到 safetensors 缓冲区中,并推送到存储桶。在整个步骤中,vLLM 仍在愉快地使用旧策略提供服务。
  2. 暂停 vLLM。一次短暂的 HTTP 调用,耗时数百毫秒。
  3. 发送 `/update_weights` 信号。传递存储桶坐标。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 时,整个同步时间都算作暂停时间。在这里,我们将其转化为后台时间。

一次端到端的同步。在 delta-over-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。你只需将 TRL 安装到与 vLLM 相同的镜像中,将 CLI 指向我们的类即可完成。

值得一提的是:vLLM 自身正在推进一项将稀疏权重传输原生落地的开发工作,即 vllm-project/vllm#40096。该方案在 WeightTransferEngine 基类上直接添加了 receive_sparse_weights() 和 trainer_send_sparse_weights() 方法,补丁以(索引,数值)形式编码,并通过 index_copy_() 原地应用,完全消除了 GPU/CPU 之间的验证往返。该 PR 报告显示,在 Qwen3-1.7B 上传输一个稀疏补丁仅需 0.40 毫秒传输 0.16 MB,而完整稠密传输则需要 192 毫秒传输 942 MB。

我们在推理侧实现中有一个诚实的注意事项:我们保留了一份模型的 CPU bf16 快照,以便能够从稀疏(索引,数值)补丁中重建完整张量,因为当前 vLLM 中的 load_weights 期望接收完整张量。一旦 #40096(或其后续版本)落地并暴露出一个原地稀疏 load_weights 路径,我们就可以直接在 GPU 上应用索引,并丢弃这份快照!

5. 在 Spaces 上真正部署运行

这是让我们颇为得意的一部分。到目前为止我们描述的所有内容都可以在笔记本电脑上运行,但通过 Hub 存储桶路由权重的意义在于,训练器和部署服务器不必位于彼此附近。因此,我们使用三台机器运行了一次完全分离的训练,这三台机器之间不共享网络:

  • 一台配备单 GPU 的机器运行训练器。
  • 一个 Hugging Face Space(Docker SDK,L4 GPU)运行带有我们扩展类的 vLLM。
  • 第二个 Hugging Face Space(CPU)运行 Wordle 环境服务器,支持 256 个并发会话容量。
  • 中间的一个 Hub 存储桶。

设置这一切实际上只需要几个 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 部署上收敛:奖励上升,增量负载保持在 20 到 35 MB 区间,每次同步的推理暂停窗口大约为一秒。完整的运行日志链接在随附的 PR 中。

6. 那么这究竟解锁了哪些能力?

有几项,我们认为意义重大。

无需集群即可进行异步强化学习训练。如果你拥有一块 GPU 和一个 Hugging Face 账号,现在就可以实现真正的解耦训练。你的训练器运行在 GPU 上;你的 rollout 集群部署在 Spaces 中;你的环境运行在另一个 Space 里;模型权重通过一个存储桶传输。过去这需要要么是共置部署(会带来所有吞吐量上的妥协),要么是配备共享网络的真实集群。现在不再需要了。

免费的多副本推理。启动两个 vLLM Space,或者十个。它们都从同一个存储桶拉取数据。Xet 采用内容寻址存储,因此连续的锚点在存储时会共享数据块(这能防止你的存储桶无限膨胀),而 Hub 的边缘缓存使得重复下载同一文件变得成本低廉。想要一个全球分布的 rollout 集群?现在这只是一个小型的 DevOps 任务,而不是一个研究项目。

一种可以用现有工具调试的传输格式。一个 delta 文件就是 safetensors 格式。你可以从 notebook 中通过 safe_open 打开它,列出其键名,检查索引,自行计算稀疏度。我们已经在晦涩的 NCCL 流上用 tcpdump 耗费了足够多的时间,因此深知这一点的价值。

一条通往前沿规模的路径。20 到 35 MB 这个数字是针对 Qwen3-0.6B 的。有趣的问题是,当你把参数调大时,曲线会是什么样子。让我们来做一下粗略估算。

以 Llama-3.1-405B 为例。在 bf16 精度下,它在磁盘上占用 810 GB。PULSE 在强化学习学习率下测得平均每步稀疏度为 99%,因此实际的 delta 大约只占参数的 1%。他们在部署中测得的编码在 7B 模型上达到了 108 MB,这正是 PULSE 报告的 **130 倍** 压缩率。按比例线性放大到 405B,每步的 delta 大约为 6 GB。

这在实际时间上能带来什么好处?NCCL 在集群内部确实很快。假设一个慷慨的 100 GB/s 聚合广播带宽(多节点、RDMA,一应俱全)。一次完整同步需要 810 GB / 100 GB/s ≈ 8 秒的推理暂停,每一步都如此。采用增量路径后,训练器在后台将 6 GB 数据流式传输到存储桶,同时生成过程持续运行,而部署服务器的实际暂停窗口仅在于应用步骤,在此规模下只需几秒钟。因此,即使不离开集群,增量方法也能将可见暂停减少 4 倍,并将网络传输字节减少约 130 倍。

现在离开集群。NCCL 完全无法跨云工作。一旦你希望在美东部署一个推理集群,在欧西部署另一个,甚至可能在一个 Hugging Face Space 中再部署一个,基于存储桶的路径就成了唯一路径。在 1 GB/s 的可用互联网带宽下,一次完整的广播需要 13 分钟;而增量方法只需 6 秒。

对于 Fireworks 框架中 1 TB 级别的模型,他们自己的测量数据显示,增量数据为 20.3 GiB,而完整快照为 1024 GiB,减少了约 50 倍。PULSE 更紧凑的稀疏编码将进一步推动这一优势(推算每个增量约 15 GB,接近 65 倍)。无论哪种方式,你都进入了一个通过通用对象存储传输权重不再是权宜之计,而是唯一合理架构的阶段。

7. 我们仍在处理的问题

我们并不假装这已经完成。以下是诚实的清单。

  • 两份 CPU bf16 快照,多了一份。训练器保留一份(用于变化检测器),部署服务器保留一份(用于为 vLLM 的 load_weights 重建完整张量)。第一份我们暂时无法摆脱,直到有人找到一种紧凑的分析掩码,这比看起来要难。第二份将在 vLLM 获得稀疏 load_weights API 后消失。PR 即将提交。
  • 固定的锚点频率。我们目前每 $N$N 步转储一次完整锚点。自适应策略(“当累积漂移超过 X 时设置锚点”)将在长时间运行中降低锚点成本。
  • 多节点 FSDP2 训练器。BF16ChangeDetector 是围绕每个进程的优化器钩子构建的。它应该能干净地推广到 FSDP2,但我们尚未在多节点规模上进行测量。PR 中有一个 TODO,上面写着我们的名字。
  • 接入优化器。我们仅从 (m, v) 预测掩码的尝试召回率很低,这意味着分析性 bf16 阈值比教科书公式所暗示的要更精妙。我们非常希望听到任何已破解此问题的人的意见。
  • 叠加传输压缩。稀疏 safetensors 和按块 gzip 是正交的。我们尚未尝试将它们结合使用,尽管我们并不期望有巨大的压缩增益。

8. 试试看

  • 该 PR:huggingface/trl#5417。分支名为 delta-weight-sync。
  • 完整的 Wordle 示例:examples/scripts/openenv/async_wordle.py。
  • Spaces 的 Dockerfile:examples/scripts/openenv/vllm_space/ 和 examples/scripts/openenv/wordle_space/。
  • 背景阅读:我们的异步强化学习全景文章、Fireworks 1 TB 文章、Cursor Composer 2 报告。

来源:Hugging Face:Blog(RSS)· huggingface.co