{
  "@context": "https://schema.org",
  "@id": "https://aggr.aymericbeaumet.com/items/huggingface-co/2026-05-27-shipping-a-trillion-parameters-with-a-hub-bucket-delta/#webpage",
  "@type": [
    "WebPage",
    "ArchiveComponent"
  ],
  "articleBody": "> **TL;DR**, because you have models to train and we respect that:\n>\n> - 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.\n> - 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.\n> - We landed [a TRL PR](https://github.com/huggingface/trl/pull/5417) 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**.\n> - The cherry on top: we ran a full disaggregated training where the **trainer was on one box**, **vLLM lived in a Hugging Face Space**, the **Wordle environment lived in another Space**, and weights flowed through a single Hub bucket. No shared cluster, no RDMA, no VPN.\n>\n> Async RL just got a lot cheaper. Read on.\n\nTwo ways to ship the same weights. Red is wall-clock time during which no tokens are being generated.\n\n* * *\n\n## 1. The One Terabyte Problem\n\nIf you read our previous post on [the landscape of async RL training](https://huggingface.co/blog/async-rl-training-landscape), you already know the punchline. Every async RL library, regardless of how it spells \"actor model\" or which color its NCCL backend is painted, eventually trips over the same root: **weight synchronization**.\n\nThe inference engine speaks the policy of step N. The trainer just finished step N+1. The fresh weights have to get from one side to the other before the inference engine starts drifting hopelessly off-policy. This sits on the critical path whether you are running sync or async: a blocking transfer is *wasted idle compute* of GPUs not generating tokens. With a sparse delta path you collapse that idle time into seconds, and the trainer does not even have to wait for the inference engine to be ready: it just publishes \"weights ready\" and uploads the weights to the shared bucket the moment its optimizer step finishes, while the inference engine fetches on its own time.\n\nFireworks put a very memorable number on this in their post [Frontier RL Is Cheaper Than You Think](https://fireworks.ai/blog/frontier-rl-is-cheaper-than-you-think): for a frontier 1T-parameter checkpoint at fp8 (their setting), a full snapshot is **1024 GiB**, and that is what conventional wisdom says you have to ship every time you update your rollout fleet. That is the kind of number that gets people to start drawing diagrams with mega-clusters, RDMA fabrics, and dedicated cross-region links. Their measured average delta between adjacent checkpoints lands at **20.3 GiB, or 1.98% of the full model**, and \"more than 98% of weights in bf16 format remain bit-equivalent between consecutive checkpoints\".\n\nCursor's [Composer 2 report](https://huggingface.co/papers/2603.24477) tells a parallel story. They run training and inference in different regions and stitch them together with a **shared S3 bucket** (their exact words), into which the trainer uploads compressed weight diffs *every training step*. Each cluster independently downloads and reconstructs from the shared delta chain, \"requiring no direct connectivity to the training cluster\". The two sides never speak to each other about parameters directly. The bucket is the wire.\n\nBoth papers agree on three things, and we want to repeat them slowly, because the rest of this post is essentially a faithful open source translation:\n\n1. Most of the weights have not actually changed between two adjacent RL steps.\n2. If you send only the parts that changed, your bandwidth bill collapses by roughly two orders of magnitude.\n3. If you route those tiny diffs through a shared object store, you no longer need the trainer and the inference cluster to live in the same data center.\n\nThe only thing missing was a version of this story that you can `pip install`. So we wrote one.\n\n## 2. Why bf16 RL Weights Are Almost Always Sparse\n\nBefore we wire anything up, it is worth understanding why this whole game is even winnable. The \"98% of weights do not change\" claim sounds suspiciously like one of those numbers that works in the demo and falls apart in the wild. It is not. It falls out of how bf16 arithmetic works at the learning rates RL uses.\n\nA bf16 number has 7 mantissa bits. Between two consecutive powers of two, there are exactly 27=1282^7 = 128 representable values, so the spacing between adjacent bf16 numbers around ∣w∣|w| is roughly ∣w∣⋅2−7|w| \\\\cdot 2^{-7}. An update gets absorbed by the bf16 cast whenever it sits below *half* of that spacing, i.e., when ∣Δw∣<∣w∣/256|\\\\Delta w| < |w|/256. This is the \"bf16 visibility threshold\" PULSE plots in their Figure 3.\n\nNow look at what Adam does. At an RL learning rate of, say, 3×10−63 \\\\times 10^{-6}, the update to a single weight is: Δw=−η⋅m^v^+ϵ\\\\Delta w = -\\\\eta \\\\cdot \\\\frac{\\\\hat{m}}{\\\\sqrt{\\\\hat{v}} + \\\\epsilon}\n\nThe normalized step m^/(v^+ϵ)\\\\hat{m}/(\\\\sqrt{\\\\hat{v}}+\\\\epsilon) is roughly order one, so ∣Δw∣≈η≈3×10−6|\\\\Delta w| \\\\approx \\\\eta \\\\approx 3 \\\\times 10^{-6}. For most weights, ∣w∣|w| sits somewhere around 10−210^{-2} to 10−110^{-1} (PULSE reports a median of 0.019 for representative LLM weights). The threshold ∣w∣/256|w|/256 at that magnitude is around 4×10−54 \\\\times 10^{-5} to 4×10−44 \\\\times 10^{-4}, which is *bigger* than the update.\n\nIn other words: the optimizer is whispering, and bf16 cannot hear it. The update gets absorbed by rounding, the byte representation of ww does not change, and from the inference engine's perspective, this weight did not move. Multiply that by a few hundred million parameters, and you get the >99% sparsity number, for free, with zero approximation.\n\nThis is exactly the argument made formal in the PULSE paper ([Mihai & Belilovsky, 2026](https://huggingface.co/papers/2602.03839)). They define two thresholds. The **absorption bound** 10η10\\\\eta is the conservative worst case for an Adam update, and the **effective bound** η\\\\eta is the regime you actually live in. The **bf16 visibility threshold** is ∣w∣/256|w|/256. Whenever the update sits below the visibility threshold, it gets absorbed, and the bf16 byte does not change. Their Figure 3 plots both bounds against a cloud of representative LLM weights, and the conclusion is unambiguous: at η=3×10−6\\\\eta = 3 \\\\times 10^{-6}, the absorption bound itself already sits below the visibility threshold for almost every weight in the model. They measure this empirically across Qwen2.5 (0.5B/1.5B/7B), Llama-3.2-3B, and Gemma-3-4B, and consistently find a mean per-step sparsity of **\\~99%, with a standard deviation of 0.2 to 0.4% over 400 training steps**. The worst-case step stays above 98%. So <1% changed is not a lucky measurement; it is what the arithmetic guarantees.\n\nWe do not have to predict this analytically (and indeed, we tried predicting the change mask from Adam's mm and vv statistics, but recall was a sad 30%, more on that later). We just need to **observe which bytes flipped**. That is a tiny boolean tensor per parameter, computed right around the optimizer step.\n\nDrag the learning rate down to RL territory and watch the cast-back-to-bf16 marker snap to the original tick. The 256-element grid on the bottom left is the aggregate effect across a tiny model.\n\n## 3. HF Buckets and the Architecture\n\nHere is where the second piece of the story comes in, and where this post stops being a translation of Fireworks/Cursor and starts being a Hugging Face thing.\n\n### 3.1 What is a Bucket?\n\nA **Bucket** is a repo type on the Hub designed for high-frequency object storage. No commit ceremony, no PR workflow, no LFS quirks. You add files, you list files, you download files. The Python interface is two functions:\n\n```python\nfrom huggingface_hub import batch_bucket_files, download_bucket_files\n\n# Trainer side\nbatch_bucket_files(\"my-org/wordle-deltas\", add=[(buffer, \"deltas/step_000042.safetensors\")])\n\n# Inference side\ndownload_bucket_files(\"my-org/wordle-deltas\", files=[(\"deltas/step_000042.safetensors\", local_path)])\n```\n\nThat is it. Two function calls and your weights are in flight.\n\nUnder the hood, buckets are backed by **Xet**, the Hub's content-defined chunking storage layer. Xet looks at every file you upload, slices it into chunks based on its actual content (not fixed offsets), and deduplicates against everything already in the bucket. The practical upshot, which is delightful in this context, is that even if we were too lazy to write the sparse encoding and just uploaded full anchors every step, Xet would *still* only transfer the changed chunks. Sparse encoding + Xet stack: we pay for what moved, and we pay for it once.\n\nThis is the open source equivalent of the \"shared S3 bucket\" both Fireworks and Cursor reach for, except the storage layer already knows about content hashing, your existing HF token already has permission, and it composes natively with the rest of the stack (Spaces, datasets, models).\n\n### 3.2 The Three Boxes\n\nThe full architecture has exactly three boxes and one shared substrate:\n\n- **Trainer.** Wherever you want. One GPU, eight GPUs, a laptop with a USB-attached H100, we will not judge. Owns the model weights, runs the optimizer, emits sparse deltas.\n- **HF Bucket.** A single repo, two prefixes: `anchors/` for occasional full snapshots and `deltas/` for the sparse patches in between. This is the only thing both sides agree on.\n- **vLLM rollout server.** Wherever you want, and crucially *not necessarily where the trainer is*. Pulls from the bucket, applies the delta, and serves rollouts.\n- **Environment.** Hangs off the rollout server in the usual way (HTTP, function calls, whatever your env speaks).\n\nThe property to internalize, the one Cursor's paper sells hard and that holds verbatim here: **the trainer and the rollout server never talk to each other about weights**. They exchange a tiny POST containing `{\"repo_id\": ..., \"filename\": ...}`, and that is the entire control plane. The actual byte transfer happens between each side and the bucket, in parallel, with no shared network fabric.\n\nWhy that matters in practice:\n\n- The rollout server can be in another region, another cloud, or behind NAT inside a Hugging Face Space. It does not care.\n- N inference replicas can pull the same delta from the same bucket, and Xet deduplicates the bytes across all of them.\n- The trainer never has to know how many inference replicas exist, or where, or whether one of them just crashed.\n\nThe trainer writes. Replicas read. The Hub does the plumbing.\n\n## 4. The Protocol\n\nNow we can open the hood. The protocol has four parts: a wire format, a bucket layout, a 30 line vLLM extension, and a trainer side change detector. It is honestly less code than it sounds.\n\n### 4.1 Safetensors as the Wire Format\n\nWe picked [safetensors](https://github.com/huggingface/safetensors) for the on-disk and on-wire format. It is already the canonical checkpoint format on the Hub, every reasonable framework can read it, and the header carries arbitrary string metadata. That metadata field is where we hide the protocol.\n\nThere are two kinds of files in the bucket.\n\n**Anchors** look like a normal checkpoint: one tensor per parameter, full bf16 weights, written every NN syncs (we default to N=10N=10).\n\n```\nanchors/step_000010.safetensors\n  ├── model.layers.0.self_attn.q_proj.weight   (bf16, full)\n  ├── model.layers.0.self_attn.k_proj.weight   (bf16, full)\n  └── ...\nmetadata:\n  sparse=False, model_version=10, sparsity=0.0\n```\n\n**Deltas** are the interesting bit. For each parameter that actually changed, we store two entries: a flat int32 tensor of element indices, and a bf16 tensor of values at those indices.\n\n```\ndeltas/step_000011.safetensors\n  ├── model.layers.0.self_attn.q_proj.weight.indices   (int32, [num_changed])\n  ├── model.layers.0.self_attn.q_proj.weight.values    (bf16,  [num_changed])\n  ├── model.layers.0.mlp.gate_proj.weight.indices\n  ├── model.layers.0.mlp.gate_proj.weight.values\n  └── ...\nmetadata:\n  sparse=True, model_version=11, sparsity=0.9938, changed_params=[...]\n```\n\nA few nice consequences of this choice:\n\n- A delta is a *file*. You can open it with `safe_open(...)` in Python and inspect every tensor in it. No proprietary framing, no length prefixes, no version handshake.\n- The metadata is self-describing. The receiver reads `sparse=True/False` and branches. There is no separate manifest.\n- It is zero-copy via mmap on the inference side, which matters when you are doing this every few seconds.\n\nThe cadence is straightforward: anchor every Nth step, delta in between. Both end up in the same bucket under `anchors/` and `deltas/` prefixes. Each new inference replica only needs to grab the most recent anchor and then replay the deltas since.\n\nTen training steps. Anchor (full snapshot) on step 1 and step 6, sparse delta on every other step. Files land in the bucket as you watch.\n\n### 4.2 The Trainer Side: a Boolean Mask From an Optimizer Hook\n\nThe trainer needs to know which bf16 elements actually flipped. We do this with a tiny `BF16ChangeDetector` that registers a pre-step and post-step hook on the optimizer:\n\n```python\nclass BF16ChangeDetector:\n    def __init__(self, model, optimizer):\n        self._pre_step_bf16: dict[str, torch.Tensor] = {}\n        self._validated_masks: dict[str, torch.Tensor] = {}\n        optimizer.register_step_pre_hook(self._pre_step_hook)\n        optimizer.register_step_post_hook(self._post_step_hook)\n\n    def _pre_step_hook(self, opt, args, kwargs):\n        for p in self._params:\n            self._pre_step_bf16[name_of(p)] = p.detach().to(torch.bfloat16).cpu().clone()\n\n    def _post_step_hook(self, opt, args, kwargs):\n        for p in self._params:\n            self._validated_masks[name_of(p)] = (\n                p.detach().to(torch.bfloat16).cpu() != self._pre_step_bf16[name_of(p)]\n            )\n```\n\nThe actual code in the PR has a bit more plumbing (matching optimizer param objects to model params via `data_ptr()`, because Accelerate wraps them as different Python objects), but the idea fits on a napkin: snapshot, step, diff.\n\nThis is ground truth. We *tried* the more elegant path of predicting the mask from Adam's mm and vv statistics, using the bf16 ULP threshold directly. It works in principle. In practice, recall was around 30%, which means we would have shipped a delta missing two thirds of the actual updates. Adam's normalization is messy enough that the analytical threshold is not tight. So we just compare bytes. It costs one bf16 CPU snapshot of the model on the trainer side, which we are willing to pay.\n\nThe four phases of the new `_sync_weight` flow are:\n\n1. **Upload while inference keeps running.** The trainer encodes the masked elements into a safetensors buffer and pushes it to the bucket. vLLM is still happily serving the old policy during this whole step.\n2. **Pause vLLM.** A short HTTP call, hundreds of milliseconds.\n3. **Signal `/update_weights`.** Send the bucket coordinates. vLLM downloads, applies, returns.\n4. **Resume.** vLLM is back on the air.\n\nThe log lines tell the story:\n\n```\nDelta: 1234567/200000000 elements changed (sparsity=99.38%)\n[delta_engine] uploaded user/wordle-deltas/deltas/step_000042.safetensors (27.4 MB, ...)\nWeight sync: done. Total 9.4s (inference paused 1.1s)\n```\n\nThe line that matters is the parenthesis. Inference was paused for **1.1 seconds**. The remaining 9.4 seconds were spent uploading, which occurred while the rollout server was still generating tokens. With NCCL, we were paying the full sync time as pause time. Here we are paying for it as background time.\n\nA single sync, end to end. Switch between delta-over-bucket and NCCL broadcast, and try the replica count toggle to see the fan-out story.\n\n### 4.3 The vLLM Side: a 30 Line Extension\n\nvLLM has a clean abstraction for this called `WeightTransferEngine`. We implement a `DeltaWeightTransferEngine` whose `receive_weights` method is, in spirit:\n\n```python\ndef receive_weights(self, update_info, load_weights):\n    download_bucket_files(update_info.repo_id, files=[(update_info.filename, local_path)])\n    with safe_open(local_path, framework=\"pt\", device=\"cpu\") as f:\n        meta = PatchMetadata.from_metadata_dict(f.metadata())\n        if not meta.sparse:\n            # Anchor: feed every tensor and snapshot for future deltas\n            for name in f.keys():\n                tensor = f.get_tensor(name)\n                self._bf16_snapshot[name] = tensor.clone()\n                load_weights([(name, tensor)])\n        else:\n            # Delta: apply (indices, values) to snapshot, hand full tensor to vLLM\n            for name in json.loads(meta.changed_params):\n                indices = f.get_tensor(f\"{name}.indices\").long()\n                values = f.get_tensor(f\"{name}.values\")\n                snap = self._bf16_snapshot[name].flatten()\n                snap[indices] = values\n                self._bf16_snapshot[name] = snap.reshape(self._bf16_snapshot[name].shape)\n                load_weights([(name, self._bf16_snapshot[name])])\n```\n\nWe register it via vLLM's `--worker-extension-cls` flag, which means **no fork of vLLM is required**. You install TRL into the same image as vLLM, point the CLI at our class, and you are done.\n\nWorth mentioning: vLLM itself has an in-flight effort to land sparse weight transfer natively, [vllm-project/vllm#40096](https://github.com/vllm-project/vllm/pull/40096). It adds `receive_sparse_weights()` and `trainer_send_sparse_weights()` directly on the `WeightTransferEngine` base class, with patches encoded as `(indices, values)` and applied in place via `index_copy_()`, removing the GPU/CPU validation roundtrip entirely. The PR reports a transfer of **0.16 MB in 0.40 ms** for a sparse patch on Qwen3-1.7B versus **942 MB in 192 ms** for a full dense send.\n\nOne honest caveat in our implementation on the inference side: we keep a CPU bf16 snapshot of the model so we can reconstruct full tensors from sparse `(indices, values)` patches, because `load_weights` in vLLM today expects full tensors. Once [#40096](https://github.com/vllm-project/vllm/pull/40096) (or its successor) lands and exposes an in-place sparse `load_weights` path, we can apply the indices directly on the GPU and drop the snapshot!\n\n## 5. Standing It Up on Spaces, For Real\n\nThis is the part we are smug about. Everything we have described so far works on your laptop, but the point of routing weights through a Hub bucket is that the trainer and the rollout server do not have to live anywhere near each other. So we ran a fully disaggregated training with three machines, none of which share a network:\n\n- A box with one GPU running the **trainer**.\n- A **Hugging Face Space** (Docker SDK, L4 GPU) running **vLLM** with our extension class.\n- A second **Hugging Face Space** (CPU) running the **Wordle environment** server with 256 concurrent session capacity.\n- A **Hub bucket** in the middle.\n\nSetting this up is genuinely a few `hf` CLI calls. The vLLM Space's `Dockerfile` is essentially the upstream vLLM image plus `pip install trl@...` plus the entrypoint:\n\n```dockerfile\nFROM vllm/vllm-openai:latest\nRUN pip install \"trl @ git+https://github.com/huggingface/trl.git@delta-weight-sync\"\nENV VLLM_SERVER_DEV_MODE=1\nEXPOSE 7860\nENTRYPOINT [\"vllm\", \"serve\", \"Qwen/Qwen3-1.7B\", \\\n    \"--host\", \"0.0.0.0\", \"--port\", \"7860\", \\\n    \"--worker-extension-cls\", \"trl.experimental.async_grpo.delta_engine.DeltaWorkerExtension\", \\\n    \"--weight-transfer-config\", \"{\\\"backend\\\":\\\"nccl\\\"}\", \\\n    \"--max-model-len\", \"32768\", \\\n    \"--gpu-memory-utilization\", \"0.8\"]\n```\n\nDeploy it as a Space:\n\n```bash\nhf repos create $USER/vllm-wordle-inference \\\n    --type space --space-sdk docker --flavor l4x1 \\\n    --secrets HF_TOKEN=$HF_TOKEN\nhf upload $USER/vllm-wordle-inference examples/scripts/openenv/vllm_space/ --type space\n```\n\nAnd kick off training from anywhere on the planet that can talk HTTPS:\n\n```bash\npython examples/scripts/openenv/async_wordle.py \\\n    --vllm-server-url https://$USER-vllm-wordle-inference.hf.space \\\n    --env-url https://openenv-wordle.hf.space \\\n    --delta-sync-repo-id $USER/wordle-deltas \\\n    --model Qwen/Qwen3-1.7B\n```\n\nThe trainer never opens a port. The Space never sees the trainer's IP. The Wordle environment does not know either of them exists. They all talk to the Hub. Training converged on the immediate-EOS sanity check, then on real Wordle rollouts: reward went up, delta payloads stayed in the 20 to 35 MB band, and the inference-paused window per sync stayed around a second. The full run logs are linked in the companion PR.\n\n## 6. So What Does This Actually Unlock?\n\nA few things, and we think they are big.\n\n**Async RL training without a cluster.** If you have one GPU and a Hugging Face account, you can now do real disaggregated training. Your trainer is on the GPU; your rollout fleet lives in Spaces; your environment lives in another Space; weights move through a bucket. This used to require either a colocated setup (with all the throughput compromises that brings) or a real cluster with shared networking. It does not anymore.\n\n**Multi-replica inference, for free.** Stand up two vLLM Spaces, or ten. They all pull from the same bucket. Xet content-addresses storage so consecutive anchors share chunks at rest (which keeps your bucket from blowing up), and the Hub's edge cache makes repeated downloads of the same file cheap to serve. Want a globally distributed rollout fleet? That is now a small DevOps exercise, not a research project.\n\n**A wire format you can debug with your existing tools.** A delta is a safetensors file. You can `safe_open` it from a notebook, list its keys, inspect the indices, compute the sparsity yourself. We have spent enough hours in tcpdump on opaque NCCL streams to appreciate this.\n\n**A path to frontier scale.** The 20 to 35 MB number is for Qwen3-0.6B. The interesting question is what the curve looks like once you turn the dial up. Let us do the napkin math.\n\nTake Llama-3.1-405B. In bf16 that is **810 GB** on disk. PULSE measures ~99% mean per-step sparsity at RL learning rates, so the actual delta sits around 1% of the parameters. Their deployment-measured encoding hits **108 MB on a 7B model**, which is the **\\~130×** reduction PULSE reports. Scaled linearly to 405B, the delta lands at roughly **6 GB per step**.\n\nWhat does that buy you in wall-clock? NCCL is fast inside a cluster, sure. Assume a generous 100 GB/s aggregate broadcast bandwidth (multi-node, RDMA, the works). A full sync is `810 GB / 100 GB/s ≈ 8 seconds` of inference pause, every step. With the delta path, the trainer streams 6 GB to a bucket *in the background* while generation keeps running, and the rollout server's actual paused window is just the apply step, which on this scale lands at a couple of seconds. So even before we leave the cluster, delta cuts the visible pause by 4× and the bytes on the wire by ~130×.\n\nNow leave the cluster. NCCL straight up does not work across clouds. Once you want a rollout fleet in `us-east`, another in `eu-west`, maybe one in a Hugging Face Space, the bucket-based path is the *only* path. At 1 GB/s of usable internet bandwidth, a single full broadcast would take 13 minutes; the delta does it in 6 seconds.\n\nFor a 1 TB-class model in the Fireworks framing, their own measured numbers show **20.3 GiB deltas vs the 1024 GiB full snapshot**, a ~50× reduction. PULSE's tighter, sparse encoding would push that further (extrapolating ~15 GB per delta, closer to ~65×). Either way, you are in a regime where shipping weights through commodity object storage stops being a hack and starts being the only sensible architecture.\n\n## 7. What's Still on Our Plate\n\nWe are not pretending this is finished. Here is the honest list.\n\n- **Two CPU bf16 snapshots, one too many.** The trainer keeps one (for the change detector) and the rollout server keeps one (to reconstruct full tensors for vLLM's `load_weights`). The first one we are stuck with until someone finds a tight analytical mask, which is harder than it looks. The second one goes away when vLLM gains a sparse `load_weights` API. PR forthcoming.\n- **Fixed anchor cadence.** We currently dump a full anchor every NN steps. An adaptive policy (\"anchor when cumulative drift exceeds X\") would cut anchor cost on long runs.\n- **Multi-node FSDP2 trainers.** The `BF16ChangeDetector` is built around per-process optimizer hooks. It should generalize cleanly to FSDP2, but we have not measured it at multi-node scale yet. There is a `TODO` in the PR with our name on it.\n- **Hooking into the optimizer.** Our attempt at predicting the mask from (m,v)(m, v) alone gave low recall, which means the analytical bf16 threshold is doing something more subtle than the textbook formula suggests. We would love to hear from anyone who has cracked this.\n- **Stacking with on-the-wire compression.** Sparse safetensors and per-chunk gzip are orthogonal. We have not tried combining them yet. Although we don't expect huge compression gains.\n\n## 8. Try It\n\n- The PR: [huggingface/trl#5417](https://github.com/huggingface/trl/pull/5417). Branch is `delta-weight-sync`.\n- The full Wordle example: `examples/scripts/openenv/async_wordle.py`.\n- The Spaces Dockerfiles: `examples/scripts/openenv/vllm_space/` and `examples/scripts/openenv/wordle_space/`.\n- Background reading: our [async RL landscape post](https://huggingface.co/blog/async-rl-training-landscape), the [Fireworks 1 TB post](https://fireworks.ai/blog/frontier-rl-is-cheaper-than-you-think), the [Cursor Composer 2 report](https://huggingface.co/papers/2603.24477).\n",
  "authors": [],
  "captured_at": "2026-10-19T01:26:53.998049939+00:00",
  "category": "ai",
  "content_html": "<blockquote>\n<p><strong>TL;DR</strong>, because you have models to train and we respect that:</p>\n<ul>\n<li>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.</li>\n<li>It turns out you do not have to. Between two consecutive RL optimizer steps, <strong>roughly 99% of bf16 weights are bit-identical</strong> (and never less than 98% in the worst case). The actual delta is tiny.</li>\n<li>We landed <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/huggingface/trl/pull/5417\">a TRL PR</a> that encodes just the changed elements as a <strong>sparse safetensors file</strong>, uploads it to a <strong>Hugging Face Bucket</strong>, and tells vLLM to fetch it. On Qwen3-0.6B, the per-step payload drops from 1.2 GB to <strong>20 to 35 MB</strong>.</li>\n<li>The cherry on top: we ran a full disaggregated training where the <strong>trainer was on one box</strong>, <strong>vLLM lived in a Hugging Face Space</strong>, the <strong>Wordle environment lived in another Space</strong>, and weights flowed through a single Hub bucket. No shared cluster, no RDMA, no VPN.</li>\n</ul>\n<p>Async RL just got a lot cheaper. Read on.</p>\n</blockquote>\n<p>Two ways to ship the same weights. Red is wall-clock time during which no tokens are being generated.</p>\n<hr />\n<h2>1. The One Terabyte Problem</h2>\n<p>If you read our previous post on <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/blog/async-rl-training-landscape\">the landscape of async RL training</a>, you already know the punchline. Every async RL library, regardless of how it spells &quot;actor model&quot; or which color its NCCL backend is painted, eventually trips over the same root: <strong>weight synchronization</strong>.</p>\n<p>The inference engine speaks the policy of step N. The trainer just finished step N+1. The fresh weights have to get from one side to the other before the inference engine starts drifting hopelessly off-policy. This sits on the critical path whether you are running sync or async: a blocking transfer is <em>wasted idle compute</em> of GPUs not generating tokens. With a sparse delta path you collapse that idle time into seconds, and the trainer does not even have to wait for the inference engine to be ready: it just publishes &quot;weights ready&quot; and uploads the weights to the shared bucket the moment its optimizer step finishes, while the inference engine fetches on its own time.</p>\n<p>Fireworks put a very memorable number on this in their post <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://fireworks.ai/blog/frontier-rl-is-cheaper-than-you-think\">Frontier RL Is Cheaper Than You Think</a>: for a frontier 1T-parameter checkpoint at fp8 (their setting), a full snapshot is <strong>1024 GiB</strong>, and that is what conventional wisdom says you have to ship every time you update your rollout fleet. That is the kind of number that gets people to start drawing diagrams with mega-clusters, RDMA fabrics, and dedicated cross-region links. Their measured average delta between adjacent checkpoints lands at <strong>20.3 GiB, or 1.98% of the full model</strong>, and &quot;more than 98% of weights in bf16 format remain bit-equivalent between consecutive checkpoints&quot;.</p>\n<p>Cursor's <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/papers/2603.24477\">Composer 2 report</a> tells a parallel story. They run training and inference in different regions and stitch them together with a <strong>shared S3 bucket</strong> (their exact words), into which the trainer uploads compressed weight diffs <em>every training step</em>. Each cluster independently downloads and reconstructs from the shared delta chain, &quot;requiring no direct connectivity to the training cluster&quot;. The two sides never speak to each other about parameters directly. The bucket is the wire.</p>\n<p>Both papers agree on three things, and we want to repeat them slowly, because the rest of this post is essentially a faithful open source translation:</p>\n<ol>\n<li>Most of the weights have not actually changed between two adjacent RL steps.</li>\n<li>If you send only the parts that changed, your bandwidth bill collapses by roughly two orders of magnitude.</li>\n<li>If you route those tiny diffs through a shared object store, you no longer need the trainer and the inference cluster to live in the same data center.</li>\n</ol>\n<p>The only thing missing was a version of this story that you can <code>pip install</code>. So we wrote one.</p>\n<h2>2. Why bf16 RL Weights Are Almost Always Sparse</h2>\n<p>Before we wire anything up, it is worth understanding why this whole game is even winnable. The &quot;98% of weights do not change&quot; claim sounds suspiciously like one of those numbers that works in the demo and falls apart in the wild. It is not. It falls out of how bf16 arithmetic works at the learning rates RL uses.</p>\n<p>A bf16 number has 7 mantissa bits. Between two consecutive powers of two, there are exactly 27=1282^7 = 128 representable values, so the spacing between adjacent bf16 numbers around ∣w∣|w| is roughly ∣w∣⋅2−7|w| \\cdot 2^{-7}. An update gets absorbed by the bf16 cast whenever it sits below <em>half</em> of that spacing, i.e., when ∣Δw∣&lt;∣w∣/256|\\Delta w| &lt; |w|/256. This is the &quot;bf16 visibility threshold&quot; PULSE plots in their Figure 3.</p>\n<p>Now look at what Adam does. At an RL learning rate of, say, 3×10−63 \\times 10^{-6}, the update to a single weight is: Δw=−η⋅m^v^+ϵ\\Delta w = -\\eta \\cdot \\frac{\\hat{m}}{\\sqrt{\\hat{v}} + \\epsilon}</p>\n<p>The normalized step m^/(v^+ϵ)\\hat{m}/(\\sqrt{\\hat{v}}+\\epsilon) is roughly order one, so ∣Δw∣≈η≈3×10−6|\\Delta w| \\approx \\eta \\approx 3 \\times 10^{-6}. For most weights, ∣w∣|w| sits somewhere around 10−210^{-2} to 10−110^{-1} (PULSE reports a median of 0.019 for representative LLM weights). The threshold ∣w∣/256|w|/256 at that magnitude is around 4×10−54 \\times 10^{-5} to 4×10−44 \\times 10^{-4}, which is <em>bigger</em> than the update.</p>\n<p>In other words: the optimizer is whispering, and bf16 cannot hear it. The update gets absorbed by rounding, the byte representation of ww does not change, and from the inference engine's perspective, this weight did not move. Multiply that by a few hundred million parameters, and you get the &gt;99% sparsity number, for free, with zero approximation.</p>\n<p>This is exactly the argument made formal in the PULSE paper (<a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/papers/2602.03839\">Mihai &amp; Belilovsky, 2026</a>). They define two thresholds. The <strong>absorption bound</strong> 10η10\\eta is the conservative worst case for an Adam update, and the <strong>effective bound</strong> η\\eta is the regime you actually live in. The <strong>bf16 visibility threshold</strong> is ∣w∣/256|w|/256. Whenever the update sits below the visibility threshold, it gets absorbed, and the bf16 byte does not change. Their Figure 3 plots both bounds against a cloud of representative LLM weights, and the conclusion is unambiguous: at η=3×10−6\\eta = 3 \\times 10^{-6}, the absorption bound itself already sits below the visibility threshold for almost every weight in the model. They measure this empirically across Qwen2.5 (0.5B/1.5B/7B), Llama-3.2-3B, and Gemma-3-4B, and consistently find a mean per-step sparsity of <strong>~99%, with a standard deviation of 0.2 to 0.4% over 400 training steps</strong>. The worst-case step stays above 98%. So &lt;1% changed is not a lucky measurement; it is what the arithmetic guarantees.</p>\n<p>We do not have to predict this analytically (and indeed, we tried predicting the change mask from Adam's mm and vv statistics, but recall was a sad 30%, more on that later). We just need to <strong>observe which bytes flipped</strong>. That is a tiny boolean tensor per parameter, computed right around the optimizer step.</p>\n<p>Drag the learning rate down to RL territory and watch the cast-back-to-bf16 marker snap to the original tick. The 256-element grid on the bottom left is the aggregate effect across a tiny model.</p>\n<h2>3. HF Buckets and the Architecture</h2>\n<p>Here is where the second piece of the story comes in, and where this post stops being a translation of Fireworks/Cursor and starts being a Hugging Face thing.</p>\n<h3>3.1 What is a Bucket?</h3>\n<p>A <strong>Bucket</strong> is a repo type on the Hub designed for high-frequency object storage. No commit ceremony, no PR workflow, no LFS quirks. You add files, you list files, you download files. The Python interface is two functions:</p>\n<pre><code class=\"language-python\"><span class=\"code-snippet\" data-language=\"Python\"><span class=\"syntax-source syntax-python\"><span class=\"syntax-meta syntax-statement syntax-import syntax-python\"><span class=\"syntax-keyword syntax-control syntax-import syntax-from syntax-python\">from</span></span><span class=\"syntax-meta syntax-statement syntax-import syntax-python\"><span class=\"syntax-meta syntax-import-source syntax-python\"> <span class=\"syntax-meta syntax-import-path syntax-python\"><span class=\"syntax-meta syntax-import-name syntax-python\">huggingface_hub</span></span> <span class=\"syntax-meta syntax-statement syntax-import syntax-python\"><span class=\"syntax-keyword syntax-control syntax-import syntax-python\">import</span></span></span></span><span class=\"syntax-meta syntax-statement syntax-import syntax-python\"></span><span class=\"syntax-meta syntax-statement syntax-import syntax-python\"> <span class=\"syntax-meta syntax-generic-name syntax-python\">batch_bucket_files</span><span class=\"syntax-punctuation syntax-separator syntax-import-list syntax-python\">,</span> <span class=\"syntax-meta syntax-generic-name syntax-python\">download_bucket_files</span></span>\n\n<span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> Trainer side\n</span><span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">batch_bucket_files</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-python\">&quot;</span></span></span><span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\">my-org/wordle-deltas<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-python\">&quot;</span></span></span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span> <span class=\"syntax-variable syntax-parameter syntax-python\">add</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span><span class=\"syntax-meta syntax-structure syntax-list syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-list syntax-begin syntax-python\">[</span><span class=\"syntax-meta syntax-group syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-support syntax-type syntax-python\">buffer</span></span><span class=\"syntax-punctuation syntax-separator syntax-tuple syntax-python\">,</span> <span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-python\">&quot;</span></span></span><span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\">deltas/step_000042.safetensors<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-python\">&quot;</span></span></span><span class=\"syntax-punctuation syntax-section syntax-group syntax-end syntax-python\">)</span></span><span class=\"syntax-punctuation syntax-section syntax-list syntax-end syntax-python\">]</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>\n\n<span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> Inference side\n</span><span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">download_bucket_files</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-python\">&quot;</span></span></span><span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\">my-org/wordle-deltas<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-python\">&quot;</span></span></span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span> <span class=\"syntax-variable syntax-parameter syntax-python\">files</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span><span class=\"syntax-meta syntax-structure syntax-list syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-list syntax-begin syntax-python\">[</span><span class=\"syntax-meta syntax-group syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-python\">&quot;</span></span></span><span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\">deltas/step_000042.safetensors<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-python\">&quot;</span></span></span><span class=\"syntax-punctuation syntax-separator syntax-tuple syntax-python\">,</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">local_path</span></span><span class=\"syntax-punctuation syntax-section syntax-group syntax-end syntax-python\">)</span></span><span class=\"syntax-punctuation syntax-section syntax-list syntax-end syntax-python\">]</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>\n</span></span></code></pre>\n<p>That is it. Two function calls and your weights are in flight.</p>\n<p>Under the hood, buckets are backed by <strong>Xet</strong>, the Hub's content-defined chunking storage layer. Xet looks at every file you upload, slices it into chunks based on its actual content (not fixed offsets), and deduplicates against everything already in the bucket. The practical upshot, which is delightful in this context, is that even if we were too lazy to write the sparse encoding and just uploaded full anchors every step, Xet would <em>still</em> only transfer the changed chunks. Sparse encoding + Xet stack: we pay for what moved, and we pay for it once.</p>\n<p>This is the open source equivalent of the &quot;shared S3 bucket&quot; both Fireworks and Cursor reach for, except the storage layer already knows about content hashing, your existing HF token already has permission, and it composes natively with the rest of the stack (Spaces, datasets, models).</p>\n<h3>3.2 The Three Boxes</h3>\n<p>The full architecture has exactly three boxes and one shared substrate:</p>\n<ul>\n<li><strong>Trainer.</strong> Wherever you want. One GPU, eight GPUs, a laptop with a USB-attached H100, we will not judge. Owns the model weights, runs the optimizer, emits sparse deltas.</li>\n<li><strong>HF Bucket.</strong> A single repo, two prefixes: <code>anchors/</code> for occasional full snapshots and <code>deltas/</code> for the sparse patches in between. This is the only thing both sides agree on.</li>\n<li><strong>vLLM rollout server.</strong> Wherever you want, and crucially <em>not necessarily where the trainer is</em>. Pulls from the bucket, applies the delta, and serves rollouts.</li>\n<li><strong>Environment.</strong> Hangs off the rollout server in the usual way (HTTP, function calls, whatever your env speaks).</li>\n</ul>\n<p>The property to internalize, the one Cursor's paper sells hard and that holds verbatim here: <strong>the trainer and the rollout server never talk to each other about weights</strong>. They exchange a tiny POST containing <code>{&quot;repo_id&quot;: ..., &quot;filename&quot;: ...}</code>, and that is the entire control plane. The actual byte transfer happens between each side and the bucket, in parallel, with no shared network fabric.</p>\n<p>Why that matters in practice:</p>\n<ul>\n<li>The rollout server can be in another region, another cloud, or behind NAT inside a Hugging Face Space. It does not care.</li>\n<li>N inference replicas can pull the same delta from the same bucket, and Xet deduplicates the bytes across all of them.</li>\n<li>The trainer never has to know how many inference replicas exist, or where, or whether one of them just crashed.</li>\n</ul>\n<p>The trainer writes. Replicas read. The Hub does the plumbing.</p>\n<h2>4. The Protocol</h2>\n<p>Now we can open the hood. The protocol has four parts: a wire format, a bucket layout, a 30 line vLLM extension, and a trainer side change detector. It is honestly less code than it sounds.</p>\n<h3>4.1 Safetensors as the Wire Format</h3>\n<p>We picked <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/huggingface/safetensors\">safetensors</a> for the on-disk and on-wire format. It is already the canonical checkpoint format on the Hub, every reasonable framework can read it, and the header carries arbitrary string metadata. That metadata field is where we hide the protocol.</p>\n<p>There are two kinds of files in the bucket.</p>\n<p><strong>Anchors</strong> look like a normal checkpoint: one tensor per parameter, full bf16 weights, written every NN syncs (we default to N=10N=10).</p>\n<pre><code><span class=\"code-snippet\">anchors/step_000010.safetensors\n  ├── model.layers.0.self_attn.q_proj.weight   (bf16, full)\n  ├── model.layers.0.self_attn.k_proj.weight   (bf16, full)\n  └── ...\nmetadata:\n  sparse=False, model_version=10, sparsity=0.0\n</span></code></pre>\n<p><strong>Deltas</strong> are the interesting bit. For each parameter that actually changed, we store two entries: a flat int32 tensor of element indices, and a bf16 tensor of values at those indices.</p>\n<pre><code><span class=\"code-snippet\">deltas/step_000011.safetensors\n  ├── model.layers.0.self_attn.q_proj.weight.indices   (int32, [num_changed])\n  ├── model.layers.0.self_attn.q_proj.weight.values    (bf16,  [num_changed])\n  ├── model.layers.0.mlp.gate_proj.weight.indices\n  ├── model.layers.0.mlp.gate_proj.weight.values\n  └── ...\nmetadata:\n  sparse=True, model_version=11, sparsity=0.9938, changed_params=[...]\n</span></code></pre>\n<p>A few nice consequences of this choice:</p>\n<ul>\n<li>A delta is a <em>file</em>. You can open it with <code>safe_open(...)</code> in Python and inspect every tensor in it. No proprietary framing, no length prefixes, no version handshake.</li>\n<li>The metadata is self-describing. The receiver reads <code>sparse=True/False</code> and branches. There is no separate manifest.</li>\n<li>It is zero-copy via mmap on the inference side, which matters when you are doing this every few seconds.</li>\n</ul>\n<p>The cadence is straightforward: anchor every Nth step, delta in between. Both end up in the same bucket under <code>anchors/</code> and <code>deltas/</code> prefixes. Each new inference replica only needs to grab the most recent anchor and then replay the deltas since.</p>\n<p>Ten training steps. Anchor (full snapshot) on step 1 and step 6, sparse delta on every other step. Files land in the bucket as you watch.</p>\n<h3>4.2 The Trainer Side: a Boolean Mask From an Optimizer Hook</h3>\n<p>The trainer needs to know which bf16 elements actually flipped. We do this with a tiny <code>BF16ChangeDetector</code> that registers a pre-step and post-step hook on the optimizer:</p>\n<pre><code class=\"language-python\"><span class=\"code-snippet\" data-language=\"Python\"><span class=\"syntax-source syntax-python\"><span class=\"syntax-meta syntax-class syntax-python\"><span class=\"syntax-storage syntax-type syntax-class syntax-python\">class</span> <span class=\"syntax-entity syntax-name syntax-class syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">BF16ChangeDetector</span></span><span class=\"syntax-punctuation syntax-section syntax-class syntax-begin syntax-python\">:</span></span>\n<span class=\"syntax-meta syntax-function syntax-python\">    <span class=\"syntax-storage syntax-type syntax-function syntax-python\">def</span> <span class=\"syntax-entity syntax-name syntax-function syntax-python\"><span class=\"syntax-support syntax-function syntax-magic syntax-python\">__init__</span></span></span><span class=\"syntax-meta syntax-function syntax-parameters syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-parameters syntax-begin syntax-python\">(</span></span><span class=\"syntax-meta syntax-function syntax-parameters syntax-python\"><span class=\"syntax-variable syntax-parameter syntax-python\">self</span><span class=\"syntax-punctuation syntax-separator syntax-parameters syntax-python\">,</span> <span class=\"syntax-variable syntax-parameter syntax-python\">model</span><span class=\"syntax-punctuation syntax-separator syntax-parameters syntax-python\">,</span> <span class=\"syntax-variable syntax-parameter syntax-python\">optimizer</span><span class=\"syntax-punctuation syntax-section syntax-parameters syntax-end syntax-python\">)</span></span><span class=\"syntax-meta syntax-function syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-function syntax-begin syntax-python\">:</span></span>\n        <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-language syntax-python\">self</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">_pre_step_bf16</span></span><span class=\"syntax-punctuation syntax-separator syntax-annotation syntax-variable syntax-python\">:</span> <span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-support syntax-type syntax-python\">dict</span></span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-begin syntax-python\">[</span></span><span class=\"syntax-meta syntax-item-access syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-support syntax-type syntax-python\">str</span></span>, <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">torch</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">Tensor</span></span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-end syntax-python\">]</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-structure syntax-dictionary syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-dictionary syntax-begin syntax-python\">{</span><span class=\"syntax-meta syntax-empty-dictionary syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-dictionary syntax-end syntax-python\">}</span></span></span>\n        <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-language syntax-python\">self</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">_validated_masks</span></span><span class=\"syntax-punctuation syntax-separator syntax-annotation syntax-variable syntax-python\">:</span> <span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-support syntax-type syntax-python\">dict</span></span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-begin syntax-python\">[</span></span><span class=\"syntax-meta syntax-item-access syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-support syntax-type syntax-python\">str</span></span>, <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">torch</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">Tensor</span></span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-end syntax-python\">]</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-structure syntax-dictionary syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-dictionary syntax-begin syntax-python\">{</span><span class=\"syntax-meta syntax-empty-dictionary syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-dictionary syntax-end syntax-python\">}</span></span></span>\n        <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">optimizer</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">register_step_pre_hook</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-language syntax-python\">self</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">_pre_step_hook</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>\n        <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">optimizer</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">register_step_post_hook</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-language syntax-python\">self</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">_post_step_hook</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>\n\n<span class=\"syntax-meta syntax-function syntax-python\">    <span class=\"syntax-storage syntax-type syntax-function syntax-python\">def</span> <span class=\"syntax-entity syntax-name syntax-function syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">_pre_step_hook</span></span></span><span class=\"syntax-meta syntax-function syntax-parameters syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-parameters syntax-begin syntax-python\">(</span></span><span class=\"syntax-meta syntax-function syntax-parameters syntax-python\"><span class=\"syntax-variable syntax-parameter syntax-python\">self</span><span class=\"syntax-punctuation syntax-separator syntax-parameters syntax-python\">,</span> <span class=\"syntax-variable syntax-parameter syntax-python\">opt</span><span class=\"syntax-punctuation syntax-separator syntax-parameters syntax-python\">,</span> <span class=\"syntax-variable syntax-parameter syntax-python\">args</span><span class=\"syntax-punctuation syntax-separator syntax-parameters syntax-python\">,</span> <span class=\"syntax-variable syntax-parameter syntax-python\">kwargs</span><span class=\"syntax-punctuation syntax-section syntax-parameters syntax-end syntax-python\">)</span></span><span class=\"syntax-meta syntax-function syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-function syntax-begin syntax-python\">:</span></span>\n        <span class=\"syntax-meta syntax-statement syntax-for syntax-python\"><span class=\"syntax-keyword syntax-control syntax-flow syntax-for syntax-python\">for</span> <span class=\"syntax-meta syntax-generic-name syntax-python\">p</span> <span class=\"syntax-meta syntax-statement syntax-for syntax-python\"><span class=\"syntax-keyword syntax-control syntax-flow syntax-for syntax-in syntax-python\">in</span></span></span><span class=\"syntax-meta syntax-statement syntax-for syntax-python\"> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-language syntax-python\">self</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">_params</span></span><span class=\"syntax-punctuation syntax-section syntax-block syntax-for syntax-python\">:</span></span>\n            <span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-language syntax-python\">self</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">_pre_step_bf16</span></span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-begin syntax-python\">[</span></span><span class=\"syntax-meta syntax-item-access syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">name_of</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">p</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-end syntax-python\">]</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">p</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">detach</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span><span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">to</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">torch</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">bfloat16</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span><span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">cpu</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span><span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">clone</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>\n\n<span class=\"syntax-meta syntax-function syntax-python\">    <span class=\"syntax-storage syntax-type syntax-function syntax-python\">def</span> <span class=\"syntax-entity syntax-name syntax-function syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">_post_step_hook</span></span></span><span class=\"syntax-meta syntax-function syntax-parameters syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-parameters syntax-begin syntax-python\">(</span></span><span class=\"syntax-meta syntax-function syntax-parameters syntax-python\"><span class=\"syntax-variable syntax-parameter syntax-python\">self</span><span class=\"syntax-punctuation syntax-separator syntax-parameters syntax-python\">,</span> <span class=\"syntax-variable syntax-parameter syntax-python\">opt</span><span class=\"syntax-punctuation syntax-separator syntax-parameters syntax-python\">,</span> <span class=\"syntax-variable syntax-parameter syntax-python\">args</span><span class=\"syntax-punctuation syntax-separator syntax-parameters syntax-python\">,</span> <span class=\"syntax-variable syntax-parameter syntax-python\">kwargs</span><span class=\"syntax-punctuation syntax-section syntax-parameters syntax-end syntax-python\">)</span></span><span class=\"syntax-meta syntax-function syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-function syntax-begin syntax-python\">:</span></span>\n        <span class=\"syntax-meta syntax-statement syntax-for syntax-python\"><span class=\"syntax-keyword syntax-control syntax-flow syntax-for syntax-python\">for</span> <span class=\"syntax-meta syntax-generic-name syntax-python\">p</span> <span class=\"syntax-meta syntax-statement syntax-for syntax-python\"><span class=\"syntax-keyword syntax-control syntax-flow syntax-for syntax-in syntax-python\">in</span></span></span><span class=\"syntax-meta syntax-statement syntax-for syntax-python\"> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-language syntax-python\">self</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">_params</span></span><span class=\"syntax-punctuation syntax-section syntax-block syntax-for syntax-python\">:</span></span>\n            <span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-language syntax-python\">self</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">_validated_masks</span></span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-begin syntax-python\">[</span></span><span class=\"syntax-meta syntax-item-access syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">name_of</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">p</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-end syntax-python\">]</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-group syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-begin syntax-python\">(</span>\n                <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">p</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">detach</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span><span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">to</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">torch</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">bfloat16</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span><span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">cpu</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span> <span class=\"syntax-keyword syntax-operator syntax-comparison syntax-python\">!=</span> <span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-language syntax-python\">self</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">_pre_step_bf16</span></span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-begin syntax-python\">[</span></span><span class=\"syntax-meta syntax-item-access syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">name_of</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">p</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-end syntax-python\">]</span></span>\n            <span class=\"syntax-punctuation syntax-section syntax-group syntax-end syntax-python\">)</span></span>\n</span></span></code></pre>\n<p>The actual code in the PR has a bit more plumbing (matching optimizer param objects to model params via <code>data_ptr()</code>, because Accelerate wraps them as different Python objects), but the idea fits on a napkin: snapshot, step, diff.</p>\n<p>This is ground truth. We <em>tried</em> the more elegant path of predicting the mask from Adam's mm and vv statistics, using the bf16 ULP threshold directly. It works in principle. In practice, recall was around 30%, which means we would have shipped a delta missing two thirds of the actual updates. Adam's normalization is messy enough that the analytical threshold is not tight. So we just compare bytes. It costs one bf16 CPU snapshot of the model on the trainer side, which we are willing to pay.</p>\n<p>The four phases of the new <code>_sync_weight</code> flow are:</p>\n<ol>\n<li><strong>Upload while inference keeps running.</strong> The trainer encodes the masked elements into a safetensors buffer and pushes it to the bucket. vLLM is still happily serving the old policy during this whole step.</li>\n<li><strong>Pause vLLM.</strong> A short HTTP call, hundreds of milliseconds.</li>\n<li><strong>Signal <code>/update_weights</code>.</strong> Send the bucket coordinates. vLLM downloads, applies, returns.</li>\n<li><strong>Resume.</strong> vLLM is back on the air.</li>\n</ol>\n<p>The log lines tell the story:</p>\n<pre><code><span class=\"code-snippet\">Delta: 1234567/200000000 elements changed (sparsity=99.38%)\n[delta_engine] uploaded user/wordle-deltas/deltas/step_000042.safetensors (27.4 MB, ...)\nWeight sync: done. Total 9.4s (inference paused 1.1s)\n</span></code></pre>\n<p>The line that matters is the parenthesis. Inference was paused for <strong>1.1 seconds</strong>. The remaining 9.4 seconds were spent uploading, which occurred while the rollout server was still generating tokens. With NCCL, we were paying the full sync time as pause time. Here we are paying for it as background time.</p>\n<p>A single sync, end to end. Switch between delta-over-bucket and NCCL broadcast, and try the replica count toggle to see the fan-out story.</p>\n<h3>4.3 The vLLM Side: a 30 Line Extension</h3>\n<p>vLLM has a clean abstraction for this called <code>WeightTransferEngine</code>. We implement a <code>DeltaWeightTransferEngine</code> whose <code>receive_weights</code> method is, in spirit:</p>\n<pre><code class=\"language-python\"><span class=\"code-snippet\" data-language=\"Python\"><span class=\"syntax-source syntax-python\"><span class=\"syntax-meta syntax-function syntax-python\"><span class=\"syntax-storage syntax-type syntax-function syntax-python\">def</span> <span class=\"syntax-entity syntax-name syntax-function syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">receive_weights</span></span></span><span class=\"syntax-meta syntax-function syntax-parameters syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-parameters syntax-begin syntax-python\">(</span></span><span class=\"syntax-meta syntax-function syntax-parameters syntax-python\"><span class=\"syntax-variable syntax-parameter syntax-python\">self</span><span class=\"syntax-punctuation syntax-separator syntax-parameters syntax-python\">,</span> <span class=\"syntax-variable syntax-parameter syntax-python\">update_info</span><span class=\"syntax-punctuation syntax-separator syntax-parameters syntax-python\">,</span> <span class=\"syntax-variable syntax-parameter syntax-python\">load_weights</span><span class=\"syntax-punctuation syntax-section syntax-parameters syntax-end syntax-python\">)</span></span><span class=\"syntax-meta syntax-function syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-function syntax-begin syntax-python\">:</span></span>\n    <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">download_bucket_files</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">update_info</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">repo_id</span></span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span> <span class=\"syntax-variable syntax-parameter syntax-python\">files</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span><span class=\"syntax-meta syntax-structure syntax-list syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-list syntax-begin syntax-python\">[</span><span class=\"syntax-meta syntax-group syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">update_info</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">filename</span></span><span class=\"syntax-punctuation syntax-separator syntax-tuple syntax-python\">,</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">local_path</span></span><span class=\"syntax-punctuation syntax-section syntax-group syntax-end syntax-python\">)</span></span><span class=\"syntax-punctuation syntax-section syntax-list syntax-end syntax-python\">]</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>\n    <span class=\"syntax-meta syntax-statement syntax-with syntax-python\"><span class=\"syntax-keyword syntax-control syntax-flow syntax-with syntax-python\">with</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">safe_open</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">local_path</span></span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span> <span class=\"syntax-variable syntax-parameter syntax-python\">framework</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span><span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-python\">&quot;</span></span></span><span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\">pt<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-python\">&quot;</span></span></span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span> <span class=\"syntax-variable syntax-parameter syntax-python\">device</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span><span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-python\">&quot;</span></span></span><span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\">cpu<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-python\">&quot;</span></span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span> <span class=\"syntax-meta syntax-statement syntax-with syntax-python\"><span class=\"syntax-keyword syntax-control syntax-flow syntax-with syntax-as syntax-python\">as</span></span></span><span class=\"syntax-meta syntax-statement syntax-with syntax-python\"> <span class=\"syntax-meta syntax-generic-name syntax-python\">f</span><span class=\"syntax-punctuation syntax-section syntax-block syntax-with syntax-python\">:</span></span>\n        <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">meta</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">PatchMetadata</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">from_metadata_dict</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">f</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">metadata</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>\n        <span class=\"syntax-meta syntax-statement syntax-if syntax-python\"><span class=\"syntax-keyword syntax-control syntax-flow syntax-conditional syntax-python\">if</span> <span class=\"syntax-keyword syntax-operator syntax-logical syntax-python\">not</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">meta</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">sparse</span></span><span class=\"syntax-punctuation syntax-section syntax-block syntax-conditional syntax-python\">:</span></span>\n            <span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> Anchor: feed every tensor and snapshot for future deltas\n</span>            <span class=\"syntax-meta syntax-statement syntax-for syntax-python\"><span class=\"syntax-keyword syntax-control syntax-flow syntax-for syntax-python\">for</span> <span class=\"syntax-meta syntax-generic-name syntax-python\">name</span> <span class=\"syntax-meta syntax-statement syntax-for syntax-python\"><span class=\"syntax-keyword syntax-control syntax-flow syntax-for syntax-in syntax-python\">in</span></span></span><span class=\"syntax-meta syntax-statement syntax-for syntax-python\"> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">f</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">keys</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span><span class=\"syntax-punctuation syntax-section syntax-block syntax-for syntax-python\">:</span></span>\n                <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">tensor</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">f</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">get_tensor</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">name</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>\n                <span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-language syntax-python\">self</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">_bf16_snapshot</span></span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-begin syntax-python\">[</span></span><span class=\"syntax-meta syntax-item-access syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">name</span></span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-end syntax-python\">]</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">tensor</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">clone</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>\n                <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">load_weights</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-structure syntax-list syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-list syntax-begin syntax-python\">[</span><span class=\"syntax-meta syntax-group syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">name</span></span><span class=\"syntax-punctuation syntax-separator syntax-tuple syntax-python\">,</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">tensor</span></span><span class=\"syntax-punctuation syntax-section syntax-group syntax-end syntax-python\">)</span></span><span class=\"syntax-punctuation syntax-section syntax-list syntax-end syntax-python\">]</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>\n        <span class=\"syntax-meta syntax-statement syntax-conditional syntax-python\"><span class=\"syntax-keyword syntax-control syntax-flow syntax-conditional syntax-python\">else</span><span class=\"syntax-punctuation syntax-section syntax-block syntax-python\">:</span></span>\n            <span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> Delta: apply (indices, values) to snapshot, hand full tensor to vLLM\n</span>            <span class=\"syntax-meta syntax-statement syntax-for syntax-python\"><span class=\"syntax-keyword syntax-control syntax-flow syntax-for syntax-python\">for</span> <span class=\"syntax-meta syntax-generic-name syntax-python\">name</span> <span class=\"syntax-meta syntax-statement syntax-for syntax-python\"><span class=\"syntax-keyword syntax-control syntax-flow syntax-for syntax-in syntax-python\">in</span></span></span><span class=\"syntax-meta syntax-statement syntax-for syntax-python\"> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">json</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">loads</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">meta</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">changed_params</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span><span class=\"syntax-punctuation syntax-section syntax-block syntax-for syntax-python\">:</span></span>\n                <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">indices</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">f</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">get_tensor</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-storage syntax-type syntax-string syntax-python\">f</span><span class=\"syntax-meta syntax-string syntax-interpolated syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-python\">&quot;</span></span></span><span class=\"syntax-meta syntax-string syntax-interpolated syntax-python\"><span class=\"syntax-meta syntax-interpolation syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-interpolation syntax-begin syntax-python\">{</span><span class=\"syntax-source syntax-python syntax-embedded\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">name</span></span></span></span><span class=\"syntax-meta syntax-interpolation syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-interpolation syntax-end syntax-python\">}</span></span><span class=\"syntax-string syntax-quoted syntax-double syntax-python\">.indices<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-python\">&quot;</span></span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span><span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">long</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>\n                <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">values</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">f</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">get_tensor</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-storage syntax-type syntax-string syntax-python\">f</span><span class=\"syntax-meta syntax-string syntax-interpolated syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-python\">&quot;</span></span></span><span class=\"syntax-meta syntax-string syntax-interpolated syntax-python\"><span class=\"syntax-meta syntax-interpolation syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-interpolation syntax-begin syntax-python\">{</span><span class=\"syntax-source syntax-python syntax-embedded\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">name</span></span></span></span><span class=\"syntax-meta syntax-interpolation syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-interpolation syntax-end syntax-python\">}</span></span><span class=\"syntax-string syntax-quoted syntax-double syntax-python\">.values<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-python\">&quot;</span></span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>\n                <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">snap</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-language syntax-python\">self</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">_bf16_snapshot</span></span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-begin syntax-python\">[</span></span><span class=\"syntax-meta syntax-item-access syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">name</span></span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-end syntax-python\">]</span></span><span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">flatten</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>\n                <span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">snap</span></span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-begin syntax-python\">[</span></span><span class=\"syntax-meta syntax-item-access syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">indices</span></span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-end syntax-python\">]</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">values</span></span>\n                <span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-language syntax-python\">self</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">_bf16_snapshot</span></span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-begin syntax-python\">[</span></span><span class=\"syntax-meta syntax-item-access syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">name</span></span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-end syntax-python\">]</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">snap</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">reshape</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-language syntax-python\">self</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">_bf16_snapshot</span></span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-begin syntax-python\">[</span></span><span class=\"syntax-meta syntax-item-access syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">name</span></span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-end syntax-python\">]</span></span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">shape</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>\n                <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">load_weights</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-structure syntax-list syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-list syntax-begin syntax-python\">[</span><span class=\"syntax-meta syntax-group syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">name</span></span><span class=\"syntax-punctuation syntax-separator syntax-tuple syntax-python\">,</span> <span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-language syntax-python\">self</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">_bf16_snapshot</span></span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-begin syntax-python\">[</span></span><span class=\"syntax-meta syntax-item-access syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">name</span></span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-end syntax-python\">]</span></span><span class=\"syntax-punctuation syntax-section syntax-group syntax-end syntax-python\">)</span></span><span class=\"syntax-punctuation syntax-section syntax-list syntax-end syntax-python\">]</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>\n</span></span></code></pre>\n<p>We register it via vLLM's <code>--worker-extension-cls</code> flag, which means <strong>no fork of vLLM is required</strong>. You install TRL into the same image as vLLM, point the CLI at our class, and you are done.</p>\n<p>Worth mentioning: vLLM itself has an in-flight effort to land sparse weight transfer natively, <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/vllm-project/vllm/pull/40096\">vllm-project/vllm#40096</a>. It adds <code>receive_sparse_weights()</code> and <code>trainer_send_sparse_weights()</code> directly on the <code>WeightTransferEngine</code> base class, with patches encoded as <code>(indices, values)</code> and applied in place via <code>index_copy_()</code>, removing the GPU/CPU validation roundtrip entirely. The PR reports a transfer of <strong>0.16 MB in 0.40 ms</strong> for a sparse patch on Qwen3-1.7B versus <strong>942 MB in 192 ms</strong> for a full dense send.</p>\n<p>One honest caveat in our implementation on the inference side: we keep a CPU bf16 snapshot of the model so we can reconstruct full tensors from sparse <code>(indices, values)</code> patches, because <code>load_weights</code> in vLLM today expects full tensors. Once <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/vllm-project/vllm/pull/40096\">#40096</a> (or its successor) lands and exposes an in-place sparse <code>load_weights</code> path, we can apply the indices directly on the GPU and drop the snapshot!</p>\n<h2>5. Standing It Up on Spaces, For Real</h2>\n<p>This is the part we are smug about. Everything we have described so far works on your laptop, but the point of routing weights through a Hub bucket is that the trainer and the rollout server do not have to live anywhere near each other. So we ran a fully disaggregated training with three machines, none of which share a network:</p>\n<ul>\n<li>A box with one GPU running the <strong>trainer</strong>.</li>\n<li>A <strong>Hugging Face Space</strong> (Docker SDK, L4 GPU) running <strong>vLLM</strong> with our extension class.</li>\n<li>A second <strong>Hugging Face Space</strong> (CPU) running the <strong>Wordle environment</strong> server with 256 concurrent session capacity.</li>\n<li>A <strong>Hub bucket</strong> in the middle.</li>\n</ul>\n<p>Setting this up is genuinely a few <code>hf</code> CLI calls. The vLLM Space's <code>Dockerfile</code> is essentially the upstream vLLM image plus <code>pip install trl@...</code> plus the entrypoint:</p>\n<pre><code class=\"language-dockerfile\"><span class=\"code-snippet\" data-language=\"Dockerfile\">FROM vllm/vllm-openai:latest\nRUN pip install &quot;trl @ git+https://github.com/huggingface/trl.git@delta-weight-sync&quot;\nENV VLLM_SERVER_DEV_MODE=1\nEXPOSE 7860\nENTRYPOINT [&quot;vllm&quot;, &quot;serve&quot;, &quot;Qwen/Qwen3-1.7B&quot;, \\\n    &quot;--host&quot;, &quot;0.0.0.0&quot;, &quot;--port&quot;, &quot;7860&quot;, \\\n    &quot;--worker-extension-cls&quot;, &quot;trl.experimental.async_grpo.delta_engine.DeltaWorkerExtension&quot;, \\\n    &quot;--weight-transfer-config&quot;, &quot;{\\&quot;backend\\&quot;:\\&quot;nccl\\&quot;}&quot;, \\\n    &quot;--max-model-len&quot;, &quot;32768&quot;, \\\n    &quot;--gpu-memory-utilization&quot;, &quot;0.8&quot;]\n</span></code></pre>\n<p>Deploy it as a Space:</p>\n<pre><code class=\"language-bash\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">hf</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> repos create <span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-shell\">USER</span></span>/vllm-wordle-inference <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">    --</span>type</span> space<span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> --</span>space-sdk</span> docker<span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> --</span>flavor</span> l4x1 <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">    --</span>secrets</span> HF_TOKEN=<span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-shell\">HF_TOKEN</span></span></span>\n<span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">hf</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> upload <span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-shell\">USER</span></span>/vllm-wordle-inference examples/scripts/openenv/vllm_space/<span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> --</span>type</span> space</span>\n</span></span></code></pre>\n<p>And kick off training from anywhere on the planet that can talk HTTPS:</p>\n<pre><code class=\"language-bash\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">python</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> examples/scripts/openenv/async_wordle.py <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">    --</span>vllm-server-url</span> https://<span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-shell\">USER</span></span>-vllm-wordle-inference.hf.space <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">    --</span>env-url</span> https://openenv-wordle.hf.space <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">    --</span>delta-sync-repo-id</span> <span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-shell\">USER</span></span>/wordle-deltas <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">    --</span>model</span> Qwen/Qwen3-1.7B</span>\n</span></span></code></pre>\n<p>The trainer never opens a port. The Space never sees the trainer's IP. The Wordle environment does not know either of them exists. They all talk to the Hub. Training converged on the immediate-EOS sanity check, then on real Wordle rollouts: reward went up, delta payloads stayed in the 20 to 35 MB band, and the inference-paused window per sync stayed around a second. The full run logs are linked in the companion PR.</p>\n<h2>6. So What Does This Actually Unlock?</h2>\n<p>A few things, and we think they are big.</p>\n<p><strong>Async RL training without a cluster.</strong> If you have one GPU and a Hugging Face account, you can now do real disaggregated training. Your trainer is on the GPU; your rollout fleet lives in Spaces; your environment lives in another Space; weights move through a bucket. This used to require either a colocated setup (with all the throughput compromises that brings) or a real cluster with shared networking. It does not anymore.</p>\n<p><strong>Multi-replica inference, for free.</strong> Stand up two vLLM Spaces, or ten. They all pull from the same bucket. Xet content-addresses storage so consecutive anchors share chunks at rest (which keeps your bucket from blowing up), and the Hub's edge cache makes repeated downloads of the same file cheap to serve. Want a globally distributed rollout fleet? That is now a small DevOps exercise, not a research project.</p>\n<p><strong>A wire format you can debug with your existing tools.</strong> A delta is a safetensors file. You can <code>safe_open</code> it from a notebook, list its keys, inspect the indices, compute the sparsity yourself. We have spent enough hours in tcpdump on opaque NCCL streams to appreciate this.</p>\n<p><strong>A path to frontier scale.</strong> The 20 to 35 MB number is for Qwen3-0.6B. The interesting question is what the curve looks like once you turn the dial up. Let us do the napkin math.</p>\n<p>Take Llama-3.1-405B. In bf16 that is <strong>810 GB</strong> on disk. PULSE measures ~99% mean per-step sparsity at RL learning rates, so the actual delta sits around 1% of the parameters. Their deployment-measured encoding hits <strong>108 MB on a 7B model</strong>, which is the <strong>~130×</strong> reduction PULSE reports. Scaled linearly to 405B, the delta lands at roughly <strong>6 GB per step</strong>.</p>\n<p>What does that buy you in wall-clock? NCCL is fast inside a cluster, sure. Assume a generous 100 GB/s aggregate broadcast bandwidth (multi-node, RDMA, the works). A full sync is <code>810 GB / 100 GB/s ≈ 8 seconds</code> of inference pause, every step. With the delta path, the trainer streams 6 GB to a bucket <em>in the background</em> while generation keeps running, and the rollout server's actual paused window is just the apply step, which on this scale lands at a couple of seconds. So even before we leave the cluster, delta cuts the visible pause by 4× and the bytes on the wire by ~130×.</p>\n<p>Now leave the cluster. NCCL straight up does not work across clouds. Once you want a rollout fleet in <code>us-east</code>, another in <code>eu-west</code>, maybe one in a Hugging Face Space, the bucket-based path is the <em>only</em> path. At 1 GB/s of usable internet bandwidth, a single full broadcast would take 13 minutes; the delta does it in 6 seconds.</p>\n<p>For a 1 TB-class model in the Fireworks framing, their own measured numbers show <strong>20.3 GiB deltas vs the 1024 GiB full snapshot</strong>, a ~50× reduction. PULSE's tighter, sparse encoding would push that further (extrapolating ~15 GB per delta, closer to ~65×). Either way, you are in a regime where shipping weights through commodity object storage stops being a hack and starts being the only sensible architecture.</p>\n<h2>7. What's Still on Our Plate</h2>\n<p>We are not pretending this is finished. Here is the honest list.</p>\n<ul>\n<li><strong>Two CPU bf16 snapshots, one too many.</strong> The trainer keeps one (for the change detector) and the rollout server keeps one (to reconstruct full tensors for vLLM's <code>load_weights</code>). The first one we are stuck with until someone finds a tight analytical mask, which is harder than it looks. The second one goes away when vLLM gains a sparse <code>load_weights</code> API. PR forthcoming.</li>\n<li><strong>Fixed anchor cadence.</strong> We currently dump a full anchor every NN steps. An adaptive policy (&quot;anchor when cumulative drift exceeds X&quot;) would cut anchor cost on long runs.</li>\n<li><strong>Multi-node FSDP2 trainers.</strong> The <code>BF16ChangeDetector</code> is built around per-process optimizer hooks. It should generalize cleanly to FSDP2, but we have not measured it at multi-node scale yet. There is a <code>TODO</code> in the PR with our name on it.</li>\n<li><strong>Hooking into the optimizer.</strong> Our attempt at predicting the mask from (m,v)(m, v) alone gave low recall, which means the analytical bf16 threshold is doing something more subtle than the textbook formula suggests. We would love to hear from anyone who has cracked this.</li>\n<li><strong>Stacking with on-the-wire compression.</strong> Sparse safetensors and per-chunk gzip are orthogonal. We have not tried combining them yet. Although we don't expect huge compression gains.</li>\n</ul>\n<h2>8. Try It</h2>\n<ul>\n<li>The PR: <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/huggingface/trl/pull/5417\">huggingface/trl#5417</a>. Branch is <code>delta-weight-sync</code>.</li>\n<li>The full Wordle example: <code>examples/scripts/openenv/async_wordle.py</code>.</li>\n<li>The Spaces Dockerfiles: <code>examples/scripts/openenv/vllm_space/</code> and <code>examples/scripts/openenv/wordle_space/</code>.</li>\n<li>Background reading: our <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/blog/async-rl-training-landscape\">async RL landscape post</a>, the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://fireworks.ai/blog/frontier-rl-is-cheaper-than-you-think\">Fireworks 1 TB post</a>, the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/papers/2603.24477\">Cursor Composer 2 report</a>.</li>\n</ul>\n",
  "content_markdown": "> **TL;DR**, because you have models to train and we respect that:\n>\n> - 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.\n> - 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.\n> - We landed [a TRL PR](https://github.com/huggingface/trl/pull/5417) 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**.\n> - The cherry on top: we ran a full disaggregated training where the **trainer was on one box**, **vLLM lived in a Hugging Face Space**, the **Wordle environment lived in another Space**, and weights flowed through a single Hub bucket. No shared cluster, no RDMA, no VPN.\n>\n> Async RL just got a lot cheaper. Read on.\n\nTwo ways to ship the same weights. Red is wall-clock time during which no tokens are being generated.\n\n* * *\n\n## 1. The One Terabyte Problem\n\nIf you read our previous post on [the landscape of async RL training](https://huggingface.co/blog/async-rl-training-landscape), you already know the punchline. Every async RL library, regardless of how it spells \"actor model\" or which color its NCCL backend is painted, eventually trips over the same root: **weight synchronization**.\n\nThe inference engine speaks the policy of step N. The trainer just finished step N+1. The fresh weights have to get from one side to the other before the inference engine starts drifting hopelessly off-policy. This sits on the critical path whether you are running sync or async: a blocking transfer is *wasted idle compute* of GPUs not generating tokens. With a sparse delta path you collapse that idle time into seconds, and the trainer does not even have to wait for the inference engine to be ready: it just publishes \"weights ready\" and uploads the weights to the shared bucket the moment its optimizer step finishes, while the inference engine fetches on its own time.\n\nFireworks put a very memorable number on this in their post [Frontier RL Is Cheaper Than You Think](https://fireworks.ai/blog/frontier-rl-is-cheaper-than-you-think): for a frontier 1T-parameter checkpoint at fp8 (their setting), a full snapshot is **1024 GiB**, and that is what conventional wisdom says you have to ship every time you update your rollout fleet. That is the kind of number that gets people to start drawing diagrams with mega-clusters, RDMA fabrics, and dedicated cross-region links. Their measured average delta between adjacent checkpoints lands at **20.3 GiB, or 1.98% of the full model**, and \"more than 98% of weights in bf16 format remain bit-equivalent between consecutive checkpoints\".\n\nCursor's [Composer 2 report](https://huggingface.co/papers/2603.24477) tells a parallel story. They run training and inference in different regions and stitch them together with a **shared S3 bucket** (their exact words), into which the trainer uploads compressed weight diffs *every training step*. Each cluster independently downloads and reconstructs from the shared delta chain, \"requiring no direct connectivity to the training cluster\". The two sides never speak to each other about parameters directly. The bucket is the wire.\n\nBoth papers agree on three things, and we want to repeat them slowly, because the rest of this post is essentially a faithful open source translation:\n\n1. Most of the weights have not actually changed between two adjacent RL steps.\n2. If you send only the parts that changed, your bandwidth bill collapses by roughly two orders of magnitude.\n3. If you route those tiny diffs through a shared object store, you no longer need the trainer and the inference cluster to live in the same data center.\n\nThe only thing missing was a version of this story that you can `pip install`. So we wrote one.\n\n## 2. Why bf16 RL Weights Are Almost Always Sparse\n\nBefore we wire anything up, it is worth understanding why this whole game is even winnable. The \"98% of weights do not change\" claim sounds suspiciously like one of those numbers that works in the demo and falls apart in the wild. It is not. It falls out of how bf16 arithmetic works at the learning rates RL uses.\n\nA bf16 number has 7 mantissa bits. Between two consecutive powers of two, there are exactly 27=1282^7 = 128 representable values, so the spacing between adjacent bf16 numbers around ∣w∣|w| is roughly ∣w∣⋅2−7|w| \\\\cdot 2^{-7}. An update gets absorbed by the bf16 cast whenever it sits below *half* of that spacing, i.e., when ∣Δw∣<∣w∣/256|\\\\Delta w| < |w|/256. This is the \"bf16 visibility threshold\" PULSE plots in their Figure 3.\n\nNow look at what Adam does. At an RL learning rate of, say, 3×10−63 \\\\times 10^{-6}, the update to a single weight is: Δw=−η⋅m^v^+ϵ\\\\Delta w = -\\\\eta \\\\cdot \\\\frac{\\\\hat{m}}{\\\\sqrt{\\\\hat{v}} + \\\\epsilon}\n\nThe normalized step m^/(v^+ϵ)\\\\hat{m}/(\\\\sqrt{\\\\hat{v}}+\\\\epsilon) is roughly order one, so ∣Δw∣≈η≈3×10−6|\\\\Delta w| \\\\approx \\\\eta \\\\approx 3 \\\\times 10^{-6}. For most weights, ∣w∣|w| sits somewhere around 10−210^{-2} to 10−110^{-1} (PULSE reports a median of 0.019 for representative LLM weights). The threshold ∣w∣/256|w|/256 at that magnitude is around 4×10−54 \\\\times 10^{-5} to 4×10−44 \\\\times 10^{-4}, which is *bigger* than the update.\n\nIn other words: the optimizer is whispering, and bf16 cannot hear it. The update gets absorbed by rounding, the byte representation of ww does not change, and from the inference engine's perspective, this weight did not move. Multiply that by a few hundred million parameters, and you get the >99% sparsity number, for free, with zero approximation.\n\nThis is exactly the argument made formal in the PULSE paper ([Mihai & Belilovsky, 2026](https://huggingface.co/papers/2602.03839)). They define two thresholds. The **absorption bound** 10η10\\\\eta is the conservative worst case for an Adam update, and the **effective bound** η\\\\eta is the regime you actually live in. The **bf16 visibility threshold** is ∣w∣/256|w|/256. Whenever the update sits below the visibility threshold, it gets absorbed, and the bf16 byte does not change. Their Figure 3 plots both bounds against a cloud of representative LLM weights, and the conclusion is unambiguous: at η=3×10−6\\\\eta = 3 \\\\times 10^{-6}, the absorption bound itself already sits below the visibility threshold for almost every weight in the model. They measure this empirically across Qwen2.5 (0.5B/1.5B/7B), Llama-3.2-3B, and Gemma-3-4B, and consistently find a mean per-step sparsity of **\\~99%, with a standard deviation of 0.2 to 0.4% over 400 training steps**. The worst-case step stays above 98%. So <1% changed is not a lucky measurement; it is what the arithmetic guarantees.\n\nWe do not have to predict this analytically (and indeed, we tried predicting the change mask from Adam's mm and vv statistics, but recall was a sad 30%, more on that later). We just need to **observe which bytes flipped**. That is a tiny boolean tensor per parameter, computed right around the optimizer step.\n\nDrag the learning rate down to RL territory and watch the cast-back-to-bf16 marker snap to the original tick. The 256-element grid on the bottom left is the aggregate effect across a tiny model.\n\n## 3. HF Buckets and the Architecture\n\nHere is where the second piece of the story comes in, and where this post stops being a translation of Fireworks/Cursor and starts being a Hugging Face thing.\n\n### 3.1 What is a Bucket?\n\nA **Bucket** is a repo type on the Hub designed for high-frequency object storage. No commit ceremony, no PR workflow, no LFS quirks. You add files, you list files, you download files. The Python interface is two functions:\n\n```python\nfrom huggingface_hub import batch_bucket_files, download_bucket_files\n\n# Trainer side\nbatch_bucket_files(\"my-org/wordle-deltas\", add=[(buffer, \"deltas/step_000042.safetensors\")])\n\n# Inference side\ndownload_bucket_files(\"my-org/wordle-deltas\", files=[(\"deltas/step_000042.safetensors\", local_path)])\n```\n\nThat is it. Two function calls and your weights are in flight.\n\nUnder the hood, buckets are backed by **Xet**, the Hub's content-defined chunking storage layer. Xet looks at every file you upload, slices it into chunks based on its actual content (not fixed offsets), and deduplicates against everything already in the bucket. The practical upshot, which is delightful in this context, is that even if we were too lazy to write the sparse encoding and just uploaded full anchors every step, Xet would *still* only transfer the changed chunks. Sparse encoding + Xet stack: we pay for what moved, and we pay for it once.\n\nThis is the open source equivalent of the \"shared S3 bucket\" both Fireworks and Cursor reach for, except the storage layer already knows about content hashing, your existing HF token already has permission, and it composes natively with the rest of the stack (Spaces, datasets, models).\n\n### 3.2 The Three Boxes\n\nThe full architecture has exactly three boxes and one shared substrate:\n\n- **Trainer.** Wherever you want. One GPU, eight GPUs, a laptop with a USB-attached H100, we will not judge. Owns the model weights, runs the optimizer, emits sparse deltas.\n- **HF Bucket.** A single repo, two prefixes: `anchors/` for occasional full snapshots and `deltas/` for the sparse patches in between. This is the only thing both sides agree on.\n- **vLLM rollout server.** Wherever you want, and crucially *not necessarily where the trainer is*. Pulls from the bucket, applies the delta, and serves rollouts.\n- **Environment.** Hangs off the rollout server in the usual way (HTTP, function calls, whatever your env speaks).\n\nThe property to internalize, the one Cursor's paper sells hard and that holds verbatim here: **the trainer and the rollout server never talk to each other about weights**. They exchange a tiny POST containing `{\"repo_id\": ..., \"filename\": ...}`, and that is the entire control plane. The actual byte transfer happens between each side and the bucket, in parallel, with no shared network fabric.\n\nWhy that matters in practice:\n\n- The rollout server can be in another region, another cloud, or behind NAT inside a Hugging Face Space. It does not care.\n- N inference replicas can pull the same delta from the same bucket, and Xet deduplicates the bytes across all of them.\n- The trainer never has to know how many inference replicas exist, or where, or whether one of them just crashed.\n\nThe trainer writes. Replicas read. The Hub does the plumbing.\n\n## 4. The Protocol\n\nNow we can open the hood. The protocol has four parts: a wire format, a bucket layout, a 30 line vLLM extension, and a trainer side change detector. It is honestly less code than it sounds.\n\n### 4.1 Safetensors as the Wire Format\n\nWe picked [safetensors](https://github.com/huggingface/safetensors) for the on-disk and on-wire format. It is already the canonical checkpoint format on the Hub, every reasonable framework can read it, and the header carries arbitrary string metadata. That metadata field is where we hide the protocol.\n\nThere are two kinds of files in the bucket.\n\n**Anchors** look like a normal checkpoint: one tensor per parameter, full bf16 weights, written every NN syncs (we default to N=10N=10).\n\n```\nanchors/step_000010.safetensors\n  ├── model.layers.0.self_attn.q_proj.weight   (bf16, full)\n  ├── model.layers.0.self_attn.k_proj.weight   (bf16, full)\n  └── ...\nmetadata:\n  sparse=False, model_version=10, sparsity=0.0\n```\n\n**Deltas** are the interesting bit. For each parameter that actually changed, we store two entries: a flat int32 tensor of element indices, and a bf16 tensor of values at those indices.\n\n```\ndeltas/step_000011.safetensors\n  ├── model.layers.0.self_attn.q_proj.weight.indices   (int32, [num_changed])\n  ├── model.layers.0.self_attn.q_proj.weight.values    (bf16,  [num_changed])\n  ├── model.layers.0.mlp.gate_proj.weight.indices\n  ├── model.layers.0.mlp.gate_proj.weight.values\n  └── ...\nmetadata:\n  sparse=True, model_version=11, sparsity=0.9938, changed_params=[...]\n```\n\nA few nice consequences of this choice:\n\n- A delta is a *file*. You can open it with `safe_open(...)` in Python and inspect every tensor in it. No proprietary framing, no length prefixes, no version handshake.\n- The metadata is self-describing. The receiver reads `sparse=True/False` and branches. There is no separate manifest.\n- It is zero-copy via mmap on the inference side, which matters when you are doing this every few seconds.\n\nThe cadence is straightforward: anchor every Nth step, delta in between. Both end up in the same bucket under `anchors/` and `deltas/` prefixes. Each new inference replica only needs to grab the most recent anchor and then replay the deltas since.\n\nTen training steps. Anchor (full snapshot) on step 1 and step 6, sparse delta on every other step. Files land in the bucket as you watch.\n\n### 4.2 The Trainer Side: a Boolean Mask From an Optimizer Hook\n\nThe trainer needs to know which bf16 elements actually flipped. We do this with a tiny `BF16ChangeDetector` that registers a pre-step and post-step hook on the optimizer:\n\n```python\nclass BF16ChangeDetector:\n    def __init__(self, model, optimizer):\n        self._pre_step_bf16: dict[str, torch.Tensor] = {}\n        self._validated_masks: dict[str, torch.Tensor] = {}\n        optimizer.register_step_pre_hook(self._pre_step_hook)\n        optimizer.register_step_post_hook(self._post_step_hook)\n\n    def _pre_step_hook(self, opt, args, kwargs):\n        for p in self._params:\n            self._pre_step_bf16[name_of(p)] = p.detach().to(torch.bfloat16).cpu().clone()\n\n    def _post_step_hook(self, opt, args, kwargs):\n        for p in self._params:\n            self._validated_masks[name_of(p)] = (\n                p.detach().to(torch.bfloat16).cpu() != self._pre_step_bf16[name_of(p)]\n            )\n```\n\nThe actual code in the PR has a bit more plumbing (matching optimizer param objects to model params via `data_ptr()`, because Accelerate wraps them as different Python objects), but the idea fits on a napkin: snapshot, step, diff.\n\nThis is ground truth. We *tried* the more elegant path of predicting the mask from Adam's mm and vv statistics, using the bf16 ULP threshold directly. It works in principle. In practice, recall was around 30%, which means we would have shipped a delta missing two thirds of the actual updates. Adam's normalization is messy enough that the analytical threshold is not tight. So we just compare bytes. It costs one bf16 CPU snapshot of the model on the trainer side, which we are willing to pay.\n\nThe four phases of the new `_sync_weight` flow are:\n\n1. **Upload while inference keeps running.** The trainer encodes the masked elements into a safetensors buffer and pushes it to the bucket. vLLM is still happily serving the old policy during this whole step.\n2. **Pause vLLM.** A short HTTP call, hundreds of milliseconds.\n3. **Signal `/update_weights`.** Send the bucket coordinates. vLLM downloads, applies, returns.\n4. **Resume.** vLLM is back on the air.\n\nThe log lines tell the story:\n\n```\nDelta: 1234567/200000000 elements changed (sparsity=99.38%)\n[delta_engine] uploaded user/wordle-deltas/deltas/step_000042.safetensors (27.4 MB, ...)\nWeight sync: done. Total 9.4s (inference paused 1.1s)\n```\n\nThe line that matters is the parenthesis. Inference was paused for **1.1 seconds**. The remaining 9.4 seconds were spent uploading, which occurred while the rollout server was still generating tokens. With NCCL, we were paying the full sync time as pause time. Here we are paying for it as background time.\n\nA single sync, end to end. Switch between delta-over-bucket and NCCL broadcast, and try the replica count toggle to see the fan-out story.\n\n### 4.3 The vLLM Side: a 30 Line Extension\n\nvLLM has a clean abstraction for this called `WeightTransferEngine`. We implement a `DeltaWeightTransferEngine` whose `receive_weights` method is, in spirit:\n\n```python\ndef receive_weights(self, update_info, load_weights):\n    download_bucket_files(update_info.repo_id, files=[(update_info.filename, local_path)])\n    with safe_open(local_path, framework=\"pt\", device=\"cpu\") as f:\n        meta = PatchMetadata.from_metadata_dict(f.metadata())\n        if not meta.sparse:\n            # Anchor: feed every tensor and snapshot for future deltas\n            for name in f.keys():\n                tensor = f.get_tensor(name)\n                self._bf16_snapshot[name] = tensor.clone()\n                load_weights([(name, tensor)])\n        else:\n            # Delta: apply (indices, values) to snapshot, hand full tensor to vLLM\n            for name in json.loads(meta.changed_params):\n                indices = f.get_tensor(f\"{name}.indices\").long()\n                values = f.get_tensor(f\"{name}.values\")\n                snap = self._bf16_snapshot[name].flatten()\n                snap[indices] = values\n                self._bf16_snapshot[name] = snap.reshape(self._bf16_snapshot[name].shape)\n                load_weights([(name, self._bf16_snapshot[name])])\n```\n\nWe register it via vLLM's `--worker-extension-cls` flag, which means **no fork of vLLM is required**. You install TRL into the same image as vLLM, point the CLI at our class, and you are done.\n\nWorth mentioning: vLLM itself has an in-flight effort to land sparse weight transfer natively, [vllm-project/vllm#40096](https://github.com/vllm-project/vllm/pull/40096). It adds `receive_sparse_weights()` and `trainer_send_sparse_weights()` directly on the `WeightTransferEngine` base class, with patches encoded as `(indices, values)` and applied in place via `index_copy_()`, removing the GPU/CPU validation roundtrip entirely. The PR reports a transfer of **0.16 MB in 0.40 ms** for a sparse patch on Qwen3-1.7B versus **942 MB in 192 ms** for a full dense send.\n\nOne honest caveat in our implementation on the inference side: we keep a CPU bf16 snapshot of the model so we can reconstruct full tensors from sparse `(indices, values)` patches, because `load_weights` in vLLM today expects full tensors. Once [#40096](https://github.com/vllm-project/vllm/pull/40096) (or its successor) lands and exposes an in-place sparse `load_weights` path, we can apply the indices directly on the GPU and drop the snapshot!\n\n## 5. Standing It Up on Spaces, For Real\n\nThis is the part we are smug about. Everything we have described so far works on your laptop, but the point of routing weights through a Hub bucket is that the trainer and the rollout server do not have to live anywhere near each other. So we ran a fully disaggregated training with three machines, none of which share a network:\n\n- A box with one GPU running the **trainer**.\n- A **Hugging Face Space** (Docker SDK, L4 GPU) running **vLLM** with our extension class.\n- A second **Hugging Face Space** (CPU) running the **Wordle environment** server with 256 concurrent session capacity.\n- A **Hub bucket** in the middle.\n\nSetting this up is genuinely a few `hf` CLI calls. The vLLM Space's `Dockerfile` is essentially the upstream vLLM image plus `pip install trl@...` plus the entrypoint:\n\n```dockerfile\nFROM vllm/vllm-openai:latest\nRUN pip install \"trl @ git+https://github.com/huggingface/trl.git@delta-weight-sync\"\nENV VLLM_SERVER_DEV_MODE=1\nEXPOSE 7860\nENTRYPOINT [\"vllm\", \"serve\", \"Qwen/Qwen3-1.7B\", \\\n    \"--host\", \"0.0.0.0\", \"--port\", \"7860\", \\\n    \"--worker-extension-cls\", \"trl.experimental.async_grpo.delta_engine.DeltaWorkerExtension\", \\\n    \"--weight-transfer-config\", \"{\\\"backend\\\":\\\"nccl\\\"}\", \\\n    \"--max-model-len\", \"32768\", \\\n    \"--gpu-memory-utilization\", \"0.8\"]\n```\n\nDeploy it as a Space:\n\n```bash\nhf repos create $USER/vllm-wordle-inference \\\n    --type space --space-sdk docker --flavor l4x1 \\\n    --secrets HF_TOKEN=$HF_TOKEN\nhf upload $USER/vllm-wordle-inference examples/scripts/openenv/vllm_space/ --type space\n```\n\nAnd kick off training from anywhere on the planet that can talk HTTPS:\n\n```bash\npython examples/scripts/openenv/async_wordle.py \\\n    --vllm-server-url https://$USER-vllm-wordle-inference.hf.space \\\n    --env-url https://openenv-wordle.hf.space \\\n    --delta-sync-repo-id $USER/wordle-deltas \\\n    --model Qwen/Qwen3-1.7B\n```\n\nThe trainer never opens a port. The Space never sees the trainer's IP. The Wordle environment does not know either of them exists. They all talk to the Hub. Training converged on the immediate-EOS sanity check, then on real Wordle rollouts: reward went up, delta payloads stayed in the 20 to 35 MB band, and the inference-paused window per sync stayed around a second. The full run logs are linked in the companion PR.\n\n## 6. So What Does This Actually Unlock?\n\nA few things, and we think they are big.\n\n**Async RL training without a cluster.** If you have one GPU and a Hugging Face account, you can now do real disaggregated training. Your trainer is on the GPU; your rollout fleet lives in Spaces; your environment lives in another Space; weights move through a bucket. This used to require either a colocated setup (with all the throughput compromises that brings) or a real cluster with shared networking. It does not anymore.\n\n**Multi-replica inference, for free.** Stand up two vLLM Spaces, or ten. They all pull from the same bucket. Xet content-addresses storage so consecutive anchors share chunks at rest (which keeps your bucket from blowing up), and the Hub's edge cache makes repeated downloads of the same file cheap to serve. Want a globally distributed rollout fleet? That is now a small DevOps exercise, not a research project.\n\n**A wire format you can debug with your existing tools.** A delta is a safetensors file. You can `safe_open` it from a notebook, list its keys, inspect the indices, compute the sparsity yourself. We have spent enough hours in tcpdump on opaque NCCL streams to appreciate this.\n\n**A path to frontier scale.** The 20 to 35 MB number is for Qwen3-0.6B. The interesting question is what the curve looks like once you turn the dial up. Let us do the napkin math.\n\nTake Llama-3.1-405B. In bf16 that is **810 GB** on disk. PULSE measures ~99% mean per-step sparsity at RL learning rates, so the actual delta sits around 1% of the parameters. Their deployment-measured encoding hits **108 MB on a 7B model**, which is the **\\~130×** reduction PULSE reports. Scaled linearly to 405B, the delta lands at roughly **6 GB per step**.\n\nWhat does that buy you in wall-clock? NCCL is fast inside a cluster, sure. Assume a generous 100 GB/s aggregate broadcast bandwidth (multi-node, RDMA, the works). A full sync is `810 GB / 100 GB/s ≈ 8 seconds` of inference pause, every step. With the delta path, the trainer streams 6 GB to a bucket *in the background* while generation keeps running, and the rollout server's actual paused window is just the apply step, which on this scale lands at a couple of seconds. So even before we leave the cluster, delta cuts the visible pause by 4× and the bytes on the wire by ~130×.\n\nNow leave the cluster. NCCL straight up does not work across clouds. Once you want a rollout fleet in `us-east`, another in `eu-west`, maybe one in a Hugging Face Space, the bucket-based path is the *only* path. At 1 GB/s of usable internet bandwidth, a single full broadcast would take 13 minutes; the delta does it in 6 seconds.\n\nFor a 1 TB-class model in the Fireworks framing, their own measured numbers show **20.3 GiB deltas vs the 1024 GiB full snapshot**, a ~50× reduction. PULSE's tighter, sparse encoding would push that further (extrapolating ~15 GB per delta, closer to ~65×). Either way, you are in a regime where shipping weights through commodity object storage stops being a hack and starts being the only sensible architecture.\n\n## 7. What's Still on Our Plate\n\nWe are not pretending this is finished. Here is the honest list.\n\n- **Two CPU bf16 snapshots, one too many.** The trainer keeps one (for the change detector) and the rollout server keeps one (to reconstruct full tensors for vLLM's `load_weights`). The first one we are stuck with until someone finds a tight analytical mask, which is harder than it looks. The second one goes away when vLLM gains a sparse `load_weights` API. PR forthcoming.\n- **Fixed anchor cadence.** We currently dump a full anchor every NN steps. An adaptive policy (\"anchor when cumulative drift exceeds X\") would cut anchor cost on long runs.\n- **Multi-node FSDP2 trainers.** The `BF16ChangeDetector` is built around per-process optimizer hooks. It should generalize cleanly to FSDP2, but we have not measured it at multi-node scale yet. There is a `TODO` in the PR with our name on it.\n- **Hooking into the optimizer.** Our attempt at predicting the mask from (m,v)(m, v) alone gave low recall, which means the analytical bf16 threshold is doing something more subtle than the textbook formula suggests. We would love to hear from anyone who has cracked this.\n- **Stacking with on-the-wire compression.** Sparse safetensors and per-chunk gzip are orthogonal. We have not tried combining them yet. Although we don't expect huge compression gains.\n\n## 8. Try It\n\n- The PR: [huggingface/trl#5417](https://github.com/huggingface/trl/pull/5417). Branch is `delta-weight-sync`.\n- The full Wordle example: `examples/scripts/openenv/async_wordle.py`.\n- The Spaces Dockerfiles: `examples/scripts/openenv/vllm_space/` and `examples/scripts/openenv/wordle_space/`.\n- Background reading: our [async RL landscape post](https://huggingface.co/blog/async-rl-training-landscape), the [Fireworks 1 TB post](https://fireworks.ai/blog/frontier-rl-is-cheaper-than-you-think), the [Cursor Composer 2 report](https://huggingface.co/papers/2603.24477).\n",
  "dateCreated": "2026-10-19T01:26:53.998049939+00:00",
  "date_created": "2026-05-27T00:00:00+00:00",
  "date_modified": "2026-05-27T00:00:00+00:00",
  "date_published": "2026-05-27T00:00:00+00:00",
  "description": "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…",
  "external_url": "https://huggingface.co/blog/delta-weight-sync",
  "git": {
    "commit": "ebb0941c5c06a2ab2377342d607535262a3e969e",
    "path": "items/huggingface-co/2026/05/2026-05-27-shipping-a-trillion-parameters-with-a-hub-bucket-delta.md",
    "permalink": "https://github.com/aymericbeaumet/aggr-instance/blob/ebb0941c5c06a2ab2377342d607535262a3e969e/items/huggingface-co/2026/05/2026-05-27-shipping-a-trillion-parameters-with-a-hub-bucket-delta.md"
  },
  "id": "urn:aggr:item:ead1a5b26a13f7ca51bb445b84001136bdcb5c14",
  "isBasedOn": {
    "@id": "https://huggingface.co/blog/delta-weight-sync"
  },
  "mainEntity": {
    "@id": "https://huggingface.co/blog/delta-weight-sync",
    "@type": "CreativeWork",
    "archivedAt": {
      "@id": "https://aggr.aymericbeaumet.com/items/huggingface-co/2026-05-27-shipping-a-trillion-parameters-with-a-hub-bucket-delta/#webpage"
    },
    "dateModified": "2026-05-27T00:00:00+00:00",
    "datePublished": "2026-05-27T00:00:00+00:00",
    "headline": "Shipping a Trillion Parameters With a Hub Bucket: Delta Weight Sync in TRL",
    "image": {
      "@type": "ImageObject",
      "caption": null,
      "height": 90,
      "url": "https://huggingface.co/blog/assets/delta-weight-sync/thumbnail.png",
      "width": 160
    },
    "timeRequired": "PT18M",
    "url": "https://huggingface.co/blog/delta-weight-sync",
    "wordCount": 3981
  },
  "name": "Archived snapshot: Shipping a Trillion Parameters With a Hub Bucket: Delta Weight Sync in TRL",
  "preview": {
    "alt": null,
    "color": null,
    "height": 90,
    "placeholder": {
      "data_url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=",
      "hash": ""
    },
    "url": "https://huggingface.co/blog/assets/delta-weight-sync/thumbnail.png",
    "width": 160
  },
  "reading_minutes": 18,
  "replicated_at": null,
  "source": "huggingface-co",
  "source_name": "Hugging Face - Blog",
  "summary": null,
  "tags": [],
  "temporalCoverage": "2026-10-19T01:26:53.998049939+00:00",
  "title": "Shipping a Trillion Parameters With a Hub Bucket: Delta Weight Sync in TRL",
  "url": "https://aggr.aymericbeaumet.com/items/huggingface-co/2026-05-27-shipping-a-trillion-parameters-with-a-hub-bucket-delta/",
  "word_count": 3981
}