TRL でデルタ重み同期を実装:トリリオンパラメータをハブバケットで管理
本文の状態
日本語全文を表示中
詳細モードで約28分の本文を読めます。
同じ出来事の情報源
この情報源を基点に整理
TLDR AI
TRL は非同期強化学習において、変更されたモデルパラメータのみを送信する「デルタ重み同期」手法を導入し、データ転送量をギガバイトからメガバイトに削減した。また、Hugging Face Hub のバケット機能を活用して学習器と推論エンジンの通信を分離し、帯域幅の大幅な節約を実現した。
Source Article
元記事を日本語で読む
本文に関係しない購読案内、埋め込み通知、サイト内プロモーションは除いています。
**
- 1. The One Terabyte Problem
- 2. Why bf16 RL Weights Are Almost Always Sparse
- 3. HF Buckets and the Architecture 3.1 What is a Bucket?
- 3.2 The Three Boxes
- 4. The Protocol 4.1 Safetensors as the Wire Format
- 4.2 The Trainer Side: a Boolean Mask From an Optimizer Hook
- 4.3 The vLLM Side: a 30 Line Extension
- 5. Standing It Up on Spaces, For Real
- 6. So What Does This Actually Unlock?
- 7. What's Still on Our Plate
- 8. Try It
TL;DR**, because you have models to train and we respect that:
- Async RL has a dirty secret: every step, the trainer has to ship the whole model to the inference engine. For a 7B in bf16 that is 14 GB. For a frontier 1T model checkpoint that is on the order of a terabyte. Per step.
- It turns out you do not have to. Between two consecutive RL optimizer steps, roughly 99% of bf16 weights are bit-identical (and never less than 98% in the worst case). The actual delta is tiny.
- We landed a TRL PR that encodes just the changed elements as a sparse safetensors file, uploads it to a Hugging Face Bucket, and tells vLLM to fetch it. On Qwen3-0.6B, the per-step payload drops from 1.2 GB to 20 to 35 MB.
- さらなる追い風:トレーナーを 1 つのサーバーに、vLLM を Hugging Face Space に、Wordle 環境を別の Space に配置し、重み(weights)を単一の Hub バケット経由で流す完全な分散トレーニングを実行しました。共有クラスタも RDMA も VPN も不要です。
非同期 RL は大幅にコスト削減されました。続きをお読みください。
同じ重みを配送する 2 つの方法。赤色はトークン生成が行われていない間の壁時計時間(wall-clock time)を示します。
1. 1 テラバイトの問題
非同期 RL トレーニングの現状 という以前の投稿をお読みであれば、すでに結論をご存知でしょう。どのライブラリであっても、「アクターモデル」という用語をどう綴ろうと、NCCL バックエンドが何色であろうと、すべての非同期 RL ライブラリは最終的に同じ根本的な問題に直面します:重み同期です。
推論エンジン(inference engine)はステップ N の方策(policy)を実行しています。一方、トレーナーはちょうどステップ N+1 を完了したところです。推論エンジンが方針から完全に逸脱し始める前に、新しい重みを片側から他側に移動させる必要があります。これは同期実行でも非同期実行でもクリティカルパス上に存在します。ブロッキング転送(blocking transfer)は、トークンを生成していない GPU の計算リソースが無駄になることを意味します。スパースデルタ経路(sparse delta path)を使用すれば、このアイドル時間を数秒に圧縮でき、トレーナーが推論エンジンの準備完了を待つ必要さえありません。「重み準備完了」を公開し、オプティマイザステップの終了と同時に共有バケットへ重みをアップロードするだけで、推論エンジン側は各自のタイミングで取得します。
Fireworks は、その投稿 Frontier RL Is Cheaper Than You Think において非常に印象的な数値を示しています。彼らの設定におけるフロンティアレベルの 1T パラメータチェックポイント(fp8 フォーマット)の場合、完全なスナップショットは1024 GiBに達します。これが、ロールアウト・フリートを更新するたびに毎回送信しなければならないという従来の常識です。この数値は、人々がメガクラスターや RDMA ファブリック、専用クロスリージョンリンクを備えた図を描き始めるきっかけとなります。しかし、彼らが測定した隣接するチェックポイント間の平均的な差分(デルタ)は20.3 GiBであり、これは完全なモデルの1.98%に過ぎません。また、「bf16 フォーマットにおける重みの 98% 以上が、連続するチェックポイント間でビット単位で等価である」とも報告されています。
Cursor の Composer 2 report も同様の物語を語っています。彼らはトレーニングと推論を異なるリージョンで実行し、共有 S3 バケット(原文の表現そのまま)を用いてこれらを結合しています。このバケットには、トレーナーがすべてのトレーニングステップにおいて圧縮された重みの差分(デルタ)をアップロードします。各クラスターは独立して、共有される差分チェーンからダウンロードし再構築を行います。「トレーニング・クラスターとの直接的な接続は不要です」というものです。両側はパラメータについて直接対話することはありません。バケットがその通信路となるのです。
両方の論文は 3 つの点で合意しており、私たちはこれらをゆっくりと繰り返したいと考えています。なぜなら、この投稿の残りの部分は本質的に、これらの知見を忠実にオープンソース化して翻訳したものであるからです:
- 隣接する RL(強化学習)ステップの間には、実際には重みの大部分が変更されていません。
- 変更された部分のみを送信すれば、帯域幅の使用料はおよそ2桁(100 分の 1)に激減します。
- これらの微小な差分を共有オブジェクトストア経由でルーティングすれば、トレーニング環境と推論クラスターが同じデータセンター内に存在する必要はなくなります。
- この物語の「pip install」可能なバージョンが存在しないことが唯一の欠点でした。そこで私たちはそれを作成しました。
2. なぜ bf16 RL の重みはほぼ常にスパースなのか
何かを接続する前に、なぜこのゲームが勝利可能なのかを理解しておく価値があります。「98% の重みが変化しない」という主張は、デモでは機能しても実世界では崩壊するような数字のように聞こえるかもしれません。しかし、それは誤りです。これは RL が使用する学習率における bf16 演算の仕組みから自然に導き出される結果なのです。
bf16 数値は 7 ビットの仮数部を持ちます。2 つの連続する 2 のべき乗の間には、正確に 2^7 = 128 の表現可能な値が存在します。したがって、|w| 付近の隣接する bf16 数値間の間隔はおよそ |w| ⋅ 2^-7 です。更新量がこの間隔の半分未満である場合、すなわち |Δw| < |w|/256 の場合、その更新は bf16 へのキャストによって吸収されてしまいます。これが PULSE が Figure 3 でプロットしている「bf16 可視性閾値」です。
さて、Adam オプティマイザが何をしているかを見てみましょう。RL の学習率を例えば 3×10^-6 とすると、単一の重みに対する更新量は以下のようになります:
Δw = -η ⋅ (m̂ / (√v̂ + ε))
正規化されたステップ m^/(v^+ϵ) は概ねオーダー 1 であるため、∣Δw∣≈η≈3×10−6 です。ほとんどの重み ∣w∣ は 10−2 から 10−1 の間に位置します(PULSE では代表的な大規模言語モデルの重みの中央値が 0.019 と報告されています)。この大きさにおける閾値 ∣w∣/256 は、およそ 4×10−5 から 4×10−4 の範囲となり、これは更新量よりも *大きい* です。
つまり、オプティマイザが囁いているのに、bf16(半精度浮動小数点形式)ではそれを聞き取れないのです。更新値は丸めによって吸収され、w のバイト表現は変化しません。推論エンジンから見れば、この重みは移動していないことになります。これを数億パラメータに掛け合わせると、近似を一切行わずに、超過 99% というスパース性という結果が無料で得られるのです。
これはまさに PULSE 論文 (Mihai & Belilovsky, 2026) で形式化された議論です。彼らは 2 つの閾値を定義しています。吸収バウンド 10η は Adam の更新における保守的な最悪ケースであり、有効バウンド η は実際にあなたが存在するレジームです。bf16 可視性閾値は ∣w∣/256 です。更新がこの可視性閾値を下回る場合、それは吸収され、bf16 のバイトは変化しません。彼らの Figure 3 では、両方のバウンドを代表的な LLM(大規模言語モデル)の重みの雲に対してプロットしており、結論は明白です:η=3×10−6 のとき、吸収バウンド自体がモデル内のほぼすべての重みにおいて可視性閾値よりも下に位置します。彼らはこれを Qwen2.5 (0.5B/1.5B/7B)、Llama-3.2-3B、Gemma-3-4B に対して経験的に測定し、400 回のトレーニングステップにわたって平均ステップごとのスパース率が約 99%で、標準偏差が 0.2% から 0.4% であることを一貫して発見しました。最悪ケースのステップでも 98% を上回ります。つまり、1% 未満の変化は偶然の測定結果ではなく、算術によって保証される事実なのです。
これを解析的に予測する必要はありません(実際、Adam の mm と vv の統計量から変化マスクを予測しようと試みましたが、その recall は悲しいことに 30% でした。これについては後ほど詳しく述べます)。私たちが行うべきは、どのバイトが反転したかを観察することだけです。これは最適化ステップの直近で計算される、パラメータごとの小さなブール型テンソルです。
学習率を RL(強化学習)の領域まで下げて、bf16 への戻しマーカーが元のティックにスナップする様子を見てください。左下の 256 要素グリッドは、小さなモデル全体にわたる集約効果です。
3. HF バケットとアーキテクチャ
ここから物語の二つ目のピースが始まり、この投稿が Fireworks/Cursor の翻訳から Hugging Face に特化した内容へと変わります。
3.1 バケットとは何か?
バケットは、ハブ上で高頻度のオブジェクトストレージを目的としたリポジトリタイプです。コミット式典も PR ワークフローも LFS の癖もありません。ファイルを追加し、ファイルをリスト表示し、ファイルをダウンロードするだけです。Python インターフェースは 2 つの関数で構成されます:
from huggingface_hub import batch_bucket_files, download_bucket_files
# トレーナー側
batch_bucket_files("my-org/wordle-deltas", add=[(buffer, "deltas/step_000042.safetensors")])
# 推論側
download_bucket_files("my-org/wordle-deltas", files=[("deltas/step_000042.safetensors", local_path)])これだけです。関数呼び出しが 2 つ行われれば、重みは転送中になります。
内部では、バケットは Hub のコンテンツ定義チャンク化ストレージ層であるXetによってバックアップされています。Xet はアップロードするすべてのファイルを確認し、固定オフセットではなく実際のコンテンツに基づいてチャンクに分割し、バケット内に既に存在するすべてのデータと重複排除を行います。この文脈では特に喜ばしい実用的な結果として、たとえスパース符号化を書くのが面倒で全アンカーを各ステップでアップロードしたとしても、Xet は*依然として*変更されたチャンクのみを転送します。スパース符号化と Xet のスタック:移動した分だけ支払い、かつ一度きりの支払いです。
これは Fireworks と Cursor がともに頼る「共有 S3 バケット」のオープンソース版ですが、ストレージ層がすでにコンテンツハッシュングを知っており、既存の HF トークンに権限が付与されており、さらに Spaces、データセット、モデルなどスタックの他の部分とネイティブに連携する点が異なります。
3.2 三つのボックス
完全なアーキテクチャには、ちょうど三つのボックスと一つの共有基盤があります:
- トレーナー。どこでも構いません。1 つの GPU でも、8 つの GPU でも、USB で接続された H100 を搭載したラップトップでも、私たちは一切批判しません。モデル重みを持ち、オプティマイザを実行し、スパースデルタを生成します。
- HF バケット。単一のリポジトリで、2 つのプレフィックス:稀な完全スナップショット用の anchors/ と、その間のスパースパッチ用の deltas/。これが両側が合意する唯一のものです。
- vLLM ロールアウトサーバー。どこでも構いません。重要なのは、必ずしもトレーナーと同じ場所である必要がないことです。バケットからデータを取得し、デルタを適用してロールアウトを提供します。
- 環境。ロールアウトサーバーに通常の通り(HTTP、関数呼び出し、あるいはご自身の環境が使用するプロトコル)で接続されます。
内部化すべき性質、つまり Cursor の論文が強く主張し、ここではそのまま適用される性質はこれです:トレーナーとロールアウトサーバーは重みについて互いに直接通信しません。両者は repo_id と filename を含むごく小さな POST リクエストを交換するだけで、これが制御プレーンにおけるすべてのやり取りです。実際のバイト転送は、各側がバケットに対して並列で行い、共有ネットワークファブリックは使用されません。
これが実務上でなぜ重要なのか:
- ロールアウトサーバーは別のリージョンや別のクラウドに存在しても、あるいは Hugging Face Space 内の NAT の背後にあっても問題ありません。それは気にしません。
- N 個の推論レプリカが同じバケットから同じデルタ(差分)をプルでき、Xet がそれらすべての間でバイトを重複排除します。
- トレーナーは、推論レプリカがいくつ存在するか、どこにあるか、あるいはそのうちの 1 つが直ちにクラッシュしたかどうかを知る必要はありません。
トレーナーが書き込み、レプリカが読み取り、Hub が配管(パイプライン)を担当します。
4. プロトコル
さて、中身を見ていきましょう。プロトコルは 4 つの部分から構成されます:ワイヤーフォーマット、バケットレイアウト、30 行の vLLM 拡張機能、そしてトレーナー側のチェンジ検出器です。正直に言って、聞こえほど多くのコード量はありません。
4.1 ワイヤーフォーマットとしての Safetensors
オンディスクおよびオンワイヤーのフォーマットには safetensors を採用しました。これはすでに HUB 上の標準的なチェックポイント形式であり、あらゆる合理的なフレームワークで読み込みが可能で、ヘッダーには任意の文字列メタデータを格納できます。このメタデータフィールドこそが、私たちがプロトコルを隠す場所です。
バケット内には 2 種類のファイルが存在します。
アンカー(Anchors) は通常のチェックポイントのように見えます:パラメータごとに 1 つのテンソルを持ち、完全な bf16(半精度浮動小数点)重みを含みます。これは NN 回の同期ごと(デフォルトでは 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
デルタ(Deltas) が興味深い部分です。実際に変更があったパラメータごとに、2 つのエントリを保存します:要素インデックスのフラットな 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/ というプレフィックスの下で同じバケットに格納されます。新しい推論レプリカは、最新のアンカーを取得し、それ以降のデルタを再生するだけで済みます。
10 回のトレーニングステップ。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 の mm および vv 統計量からマスクを予測する、よりエレガントなアプローチを試みました。これは bf16 の ULP(単位最終桁)閾値を直接使用する方法です。原理的には機能しますが、実際には再現率は約 30% でした。つまり、実際の更新の 3 分の 2 を見逃したデルタを送信することになっていたのです。Adam の正規化は十分に複雑であり、解析的な閾値では精度が十分ではありません。そのため、私たちは単にバイト列を比較することにしました。これには、トレーニング側でモデルの bf16 CPU スナップショットを 1 つ作成するコストがかかりますが、私たちはそのコストを支払う用意があります。
新しい _sync_weight フローの 4 つのフェーズは以下の通りです:
- 推論実行中にアップロード。トレーナーはマスクされた要素を safetensors バッファにエンコードし、バケットへプッシュします。このステップ全体を通じて、vLLM は引き続き古いポリシーで喜んでサービスを提供しています。
- vLLM の一時停止。短い HTTP コールで、数百ミリ秒です。
- シグナル /update_weights の送信。バケットの座標を送信し、vLLM がダウンロードして適用し、完了を返します。
- 再開。vLLM は再び稼働状態になります。
ログ行がその物語を語ります:
デルタ:1234567/200000000 要素が変更されました(スパース性=99.38%)
[delta_engine] user/wordle-deltas/deltas/step_000042.safetensors をアップロードしました(27.4 MB、...)
重み同期:完了。合計 9.4 秒(推論一時停止 1.1 秒)
重要なのは括弧内の記述です。推論は1.1 秒間一時停止されました。残りの 9.4 秒はアップロードに費やされ、これはロールアウトサーバーがまだトークンを生成している間に発生しました。NCCL を使用した場合、同期時間の全額を一時停止時間として支払う必要がありました。ここでは、それをバックグラウンド時間として支払っています。
単一の同期をエンドツーエンドで実行します。デルタ・オーバー・バケットと 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 フラグを通じてこれを登録するため、vLLM のフォークは不要です。vLLM と同じイメージに TRL をインストールし、CLI で当社のクラスを指すだけで完了します。
言及しておくべき点:vLLM 自体も、スパース重みの転送をネイティブに実装する取り組みを進めており、vllm-project/vllm#40096 がその一例です。これは WeightTransferEngine ベースクラスに receive_sparse_weights() と trainer_send_sparse_weights() を直接追加し、パッチを (インデックス,値) の形式でエンコードして index_copy_() 経由でインプレース適用することで、GPU/CPU 間の検証ラウンドトリップを完全に排除します。この PR では、Qwen3-1.7B におけるスパースパッチの転送が 0.40 ms で 0.16 MB であるのに対し、フルデンス送信では 192 ms で 942 MB となることを報告しています。
推論側の実装における正直な注意点を一つ:vLLM の load_weights は現状フルテンソルを期待するため、スパース (インデックス,値) パッチから完全なテンソルを再構築できるように、モデルの CPU bf16 スナップショットを保持しています。#40096(またはその後継)がマージされ、インプレースでのスパース load_weights 経路が公開されたら、インデックスを直接 GPU 上で適用してスナップショットを不要にできます!
5. Spaces 上での本格的な立ち上げ
これが私たちが自慢している部分です。これまでに説明したすべてはあなたのラップトップでも動作しますが、Hub バケットを介して重みをルーティングする意義は、トレーニング側とロールアウトサーバーが互いに近くにある必要がない点にあります。そこで、ネットワークを共有しない 3 つのマシンを用いた完全に分離されたトレーニングを実行しました。
- トレーナーを実行する GPU を 1 基搭載したマシン。
- Hugging Face Space(Docker SDK、L4 GPU)上で、拡張クラス付きの vLLM を実行するもの。
- 256 の同時セッション容量を備えた、Wordle 環境サーバーを実行する Hugging Face Space(CPU)の第 2 实例。
- その中間に Hub バケットが存在します。
このセットアップは、実際には数回の hf CLI コマンドで完了します。vLLM Space の Dockerfile は、元々の vLLM イメージに trl@... を pip install で追加し、エントリーポイントを設定したものです:
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(End of Sequence)の健全性チェックで収束し、その後実際の Wordle ロールアウトでも収束しました:報酬が増加し、デルタペイロードは 20〜35 MB の範囲内に留まり、同期ごとの推論一時停止ウィンドウは約 1 秒程度でした。完全な実行ログは、関連する PR にリンクされています。
6. これは実際に何を可能にするのか?
いくつかのことが可能になりますが、私たちはそれが大きな意味を持つと考えています。
クラスターを必要としない非同期 RL(強化学習)トレーニング。 GPU が 1 つあり Hugging Face のアカウントがあれば、今や本格的な分散型トレーニングが可能になりました。トレーナーは GPU 上にあり、ロールアウト用ファームウェアは Spaces に存在し、環境は別の Space にあります。重みはバケットを介して移動します。これには以前、スループットに関する妥協を伴う共設置設定か、共有ネットワークを持つ本格的なクラスターが必要でした。しかし、もうそうではありません。
無料のマルチレプリカ推論。 vLLM の Spaces を 2 つ、あるいは 10 個立ち上げてください。すべてが同じバケットからデータを取得します。Xet はコンテンツアドレス型ストレージを採用しているため、連続するアンカーは保存時にチャンクを共有し(これによりバケットの肥大化を防ぎます)、Hub のエッジキャッシュにより、同一ファイルの繰り返しダウンロードも低コストで提供可能です。グローバルに分散されたロールアウトファームウェアが必要ですか?それは今や小規模な DevOps 作業であり、研究プロジェクトではありません。
既存のツールでデバッグ可能なワイヤーフォーマット。デルタは safetensors ファイルです。ノートブックから safe_open してキーをリストし、インデックスを検査し、スパースティを自分で計算できます。不明瞭な NCCL ストリーム上で tcpdump を何時間も費やしてきた経験から、この利点を十分に理解しています。
フロンティア規模への道筋。20〜35 MB という数値は Qwen3-0.6B におけるものです。興味深いのは、パラメータを大きくした際に曲線がどうなるかという点です。ここでは簡易計算を行ってみましょう。
Llama-3.1-405B を考えてみましょう。bf16(bfloat16)形式ではディスク上で810 GBになります。PULSE は RL 学習率においてステップごとの平均スパースティが約99%であると測定しており、実際のデルタはパラメータの約1%に相当します。彼らのデプロイメントで測定されたエンコーディングは、7B モデルで108 MBであり、これは PULSE が報告する約130倍の削減率です。これを 405B に線形スケールすると、ステップあたりデルタはおよそ6 GBになります。
これが実時間(wall-clock)において何をもたらすでしょうか?NCCL はクラスター内では確かに高速です。ただし、広義な条件として、集約ブロードキャスト帯域を 100 GB/s(マルチノード、RDMA、その他すべてを含む)と仮定しましょう。フル同期の場合、810 GB / 100 GB/s ≒ 8 秒の推論停止がステップごとに発生します。一方、デルタ経路では、トレーナーは生成処理を継続しながら背景で 6 GB をバケットにストリーミングし、ロールアウトサーバーの実質的な停止ウィンドウは適用ステップのみとなります。この規模ではその時間は数秒程度です。したがって、クラスター外に出る前であっても、デルタにより可視化される停止時間が 4 倍短縮され、ワイヤー上のデータ量が約130倍削減されます。
クラスターから離れてください。NCCL はクラウド間ではまともに機能しません。us-east にロールアウト用ファームを、eu-west に別のファームを、あるいは Hugging Face Space に 1 つ配置したい場合、バケットベースの経路が *唯一* の選択肢となります。利用可能なインターネット帯域幅が 1 GB/s の場合、完全なブロードキャストには 13 分かかりますが、デルタ方式ならわずか 6 秒で完了します。
Fireworks の枠組みにおける 1 TB クラスのモデルの場合、彼ら自身の測定数値によると、20.3 GiB のデルタ対 1024 GiB の完全スナップショットとなり、約 50 倍の削減となります。PULSE のより厳密でスパースな符号化方式であれば、さらにこれを押し上げられるでしょう(デルタあたり約 15 GB と外挿すると、約 65 倍に近づきます)。いずれにせよ、汎用オブジェクトストレージを通じて重みを配送することがハックから、唯一の合理的なアーキテクチャへと移行する領域にあります。
7. まだ課題として残っていること
これが完了したと偽るつもりはありません。ここに正直なリストを示します。
- CPU の bf16(半精度浮動小数点)スナップショットが 2 つあります。1 つ多すぎます。トレーナーは 1 つ保持し(変更検出用)、ロールアウトサーバーも 1 つ保持します(vLLM の load_weights で完全テンソルを再構築するため)。最初のものは、誰かが厳密な解析的マスクを見つけるまで、どうしようもなく残ります。それは見た目よりも難しいです。2 つ目は、vLLM がスパースな load_weights API を獲得したときに消えます。PR(プルリクエスト)が近々提出されます。
- 固定されたアンカーの頻度。現在、NN ステップごとに完全なアンカーをダンプしています。適応型ポリシー(累積ドリフトが 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 の Dockerfiles: examples/scripts/openenv/vllm_space/ および examples/scripts/openenv/wordle_space/。
- 背景資料:当社の非同期 RL ランドスケープに関する投稿、Fireworks の 1 TB に関する投稿、Cursor Composer 2 のレポート。
関連記事
今日のまとめ
AIデイリーブリーフで今日の重要ニュースをまとめ読み