Skip to content
Open
Show file tree
Hide file tree
Changes from 74 commits
Commits
Show all changes
97 commits
Select commit Hold shift + click to select a range
830de9b
feat(weights): sparse XOR deltas for the filesystem weight broadcast
faresobeid Oct 14, 2026
41836bb
feat(weights): engine workers prefetch only the sparse delta entries …
faresobeid Oct 14, 2026
8766951
fix(weights): allow checkpoint tensors the engine casts on load (e.g.…
faresobeid Oct 14, 2026
52c4d7f
feat(weights): fp8-domain sparse deltas for engines that quantize onl…
faresobeid Oct 14, 2026
ab7f572
feat(weights): write periodic sparse-broadcast anchors off the traine…
faresobeid Oct 14, 2026
23123e9
perf(weights): keep the sparse snapshot in pinned host memory and reu…
faresobeid Oct 14, 2026
0f7fad6
feat(weights): verify every sparse delta against trainer checksums; l…
faresobeid Oct 14, 2026
66917af
perf(weights): drop sparse deltas and anchors from the engine host's …
faresobeid Oct 14, 2026
5bcb2b0
perf(weights): pin the anchor host buffer on a thread ahead of each a…
faresobeid Oct 14, 2026
1615bbe
perf(weights): checksum held params with one cumsum per param; filter…
faresobeid Oct 14, 2026
8d4d5e3
fix(weights): engines off the delta chain replay from the latest comp…
faresobeid Oct 14, 2026
a5b9734
feat(weights): boot weights as the first sparse step, background delt…
faresobeid Oct 14, 2026
2b49427
perf(weights): register exact-size pinned buffers; drop written spars…
faresobeid Oct 15, 2026
faab858
fix(weights): decode and apply sparse deltas in chunks of whole tenso…
faresobeid Oct 15, 2026
af4dbea
perf(weights): background sparse writes release the GIL; fewer compre…
faresobeid Oct 15, 2026
7d08f09
chore(weights): log the sparse snapshot initialization time
faresobeid Oct 15, 2026
7554b18
feat(weights): write sparse anchors from the trainer snapshot; page-l…
faresobeid Oct 15, 2026
8a48879
fix(weights): page-lock the snapshot only after it is filled
faresobeid Oct 15, 2026
939c14e
fix(weights): page-lock the snapshot with one registration
faresobeid Oct 15, 2026
e076262
fix(weights): load anchors layer by layer
faresobeid Oct 15, 2026
3c6f97a
feat(weights): encode sparse deltas in the background, overlapped wit…
faresobeid Oct 15, 2026
3ef6205
fix(weights): diff fp8-held tensors on the training thread; pin the s…
faresobeid Oct 15, 2026
3289d97
perf(weights): skip the pre-broadcast drain and cache release for spa…
faresobeid Oct 15, 2026
b37392d
feat(weights): store sparse deltas as zigzag raw-bit differences, one…
faresobeid Oct 15, 2026
fa14059
docs: sparse weight broadcast snapshot placement and background encode
faresobeid Oct 15, 2026
9afadaa
feat(weights): stream sparse deltas in parts; record the v0 snapshot …
faresobeid Oct 15, 2026
d3a9e95
perf(weights): copy sparse delta parts to the host through a pinned s…
faresobeid Oct 15, 2026
253f1a5
perf(weights): diff fp8-held tensors on the encode thread with an ahe…
faresobeid Oct 15, 2026
fd11f15
fix(weights): engine fp8 layout only from checkpoint weights; positio…
faresobeid Oct 16, 2026
c87e75e
refactor(weights): simplify the sparse weight broadcast
faresobeid Oct 16, 2026
dbe267b
refactor(weights): sparse sender as a FileSystemWeightSender subclass…
faresobeid Oct 16, 2026
1d731e3
fix(weights): finish the encode's weight reads before the final backw…
faresobeid Oct 16, 2026
31ca464
fix(weights): wait for every anchor write before the host buffer is r…
faresobeid Oct 16, 2026
54e241a
perf(weights): position-weighted checksums with one reduction per run…
faresobeid Oct 16, 2026
44b0c83
fix(weights): engines recognize a sparse run's full checkpoint by its…
faresobeid Oct 16, 2026
f2bea83
fix(weights): revalidate NFS dirs for anchor markers and engine layou…
faresobeid Oct 16, 2026
8501f91
refactor(weights): record the v0 snapshot inside the first broadcast …
faresobeid Oct 16, 2026
2ecd0c9
perf(weights): no host snapshot buffer for a GPU snapshot: v1 is a fu…
faresobeid Oct 16, 2026
427b749
docs(weights): snapshot placement at v1 and the host anchor buffer
faresobeid Oct 16, 2026
4b9e0d9
refactor(weights): drop the engine reader's NUMA binding (no gain in …
faresobeid Oct 16, 2026
1a42971
perf(weights): v1 is a delta against the boot checkpoint read back fr…
faresobeid Oct 16, 2026
515cfc2
fix(weights): order the gathered fp8 tensors v0 read after the snapsh…
faresobeid Oct 16, 2026
54ad1b4
feat(weights): fp8 block scales in the delta; slice-local full checkp…
faresobeid Oct 17, 2026
255b0d0
docs(weights): fp8 scales in the delta, slice-local anchor reloads
faresobeid Oct 17, 2026
0e90805
perf(weights): checksum same-shape writes into a param in one batch (…
faresobeid Oct 17, 2026
48de997
fix(weights): fault the sparse host buffer in before training; skip a…
faresobeid Oct 17, 2026
5cc0591
fix(weights): pin the engine's delta read buffer at twice the delta, …
faresobeid Oct 17, 2026
f39c69e
feat(weights): sparse deltas from the full-offload optimizer step, wi…
faresobeid Oct 17, 2026
df71b4e
perf(weights): write the optimizer step delta parts during the step
faresobeid Oct 17, 2026
738b37e
fix(weights): map a non-empty sparse host buffer on ranks without shards
faresobeid Oct 17, 2026
cc5beb6
chore(weights): trim sparse broadcast comments, drop dead attributes …
faresobeid Oct 17, 2026
8e87fe9
docs(weights): concise sparse broadcast docs and config descriptions
faresobeid Oct 17, 2026
129b225
fix(weights): fail the trainer when the optimizer-step diff fails; ta…
faresobeid Oct 17, 2026
d445d80
feat(weights): sparse deltas from the optimizer step for every optimi…
faresobeid Oct 18, 2026
aa3c695
fix(weights): write the anchor slices of optimizer tensors the step d…
faresobeid Oct 18, 2026
d5ea649
feat(weights): anchor every 50 steps by default
faresobeid Oct 18, 2026
bc69bd5
fix(weights): verify every tensor of anchors and full checkpoints on …
faresobeid Oct 18, 2026
0ad0147
fix(weights): diff packed qkv in the optimizer step instead of a copy…
faresobeid Oct 18, 2026
3140787
perf(weights): diff bf16 updates in place with sync-free kernels, wit…
faresobeid Oct 18, 2026
6ce9377
perf(weights): write full-offload anchors from the live weights after…
faresobeid Oct 18, 2026
81064f4
fix(weights): release the diff slot and regions before writing the an…
faresobeid Oct 18, 2026
4bc9e2d
fix(weights): reject Muon and LoRA with the sparse weight broadcast i…
faresobeid Oct 19, 2026
36e0de7
fix(weights): fail engines reading a sparse delta whose trainer encod…
faresobeid Oct 19, 2026
303d438
Merge remote-tracking branch 'origin/main' into feat/sparse-filesyste…
faresobeid Oct 26, 2026
a980ff0
Merge remote-tracking branch 'origin/main' into feat/sparse-filesyste…
faresobeid Oct 27, 2026
2e99ce3
fix(weights): engine layouts from every world rank, so pipeline stage…
faresobeid Oct 27, 2026
2299e35
fix(weights): engines report their layout at every full broadcast, an…
faresobeid Oct 27, 2026
681231e
Merge remote-tracking branch 'origin/main' into feat/sparse-filesyste…
faresobeid Oct 28, 2026
b5a4f3c
feat(weights): sparse weight broadcast with Muon
faresobeid Oct 28, 2026
b8ecec1
refactor(weights): SparseFileSystemWeightBroadcastConfig, 1:1 with Sp…
faresobeid Oct 28, 2026
0cd553e
fix(weights): fail sparse commits on failed ranks, fail every rank on…
faresobeid Oct 28, 2026
14bd1bd
test(optim): chunked optimizer steps match a plain step and advance p…
faresobeid Oct 28, 2026
2ad7455
refactor(weights): sparse deltas as `[weight_broadcast.delta]` on the…
faresobeid Oct 28, 2026
2616dfa
fix(weights): a tied weight's other names carry no sparse deltas; `de…
faresobeid Oct 28, 2026
bbfe2f1
perf(weights): read the sparse delta before the dispatcher stops for …
faresobeid Oct 28, 2026
2fcb620
Merge remote-tracking branch 'origin/main' into feat/sparse-filesyste…
faresobeid Oct 28, 2026
97be305
fix(weights): anchors and full checkpoints keep every name of a tied …
faresobeid Oct 28, 2026
adbf95d
fix(weights): anchors and full checkpoints load tensors the engine ca…
faresobeid Oct 28, 2026
10d931e
Merge remote-tracking branch 'origin/main' into feat/sparse-filesyste…
faresobeid Oct 30, 2026
5286d0a
chore: bump vLLM to 0.33.0 (torch 2.14.0)
faresobeid Nov 2, 2026
3307dc2
refactor(inference): drop the LoRA load_inplace reset vLLM 0.33 does …
faresobeid Nov 2, 2026
95e9768
chore: pin prime-kernels, deep-ep, deep-gemm, deep-select built for t…
faresobeid Nov 2, 2026
27ce941
fix(inference): keep KV offload reuse with routed experts on vLLM 0.33
faresobeid Nov 2, 2026
7eb55c6
Merge commit '27ce941cf' into feat/sparse-filesystem-weight-broadcast
faresobeid Nov 2, 2026
0ea767d
fix(weights): one update plan per TP group; replica-aware, incrementa…
faresobeid Nov 3, 2026
ca9cfa6
perf(weights): apply sparse deltas with one fused Triton kernel
faresobeid Nov 3, 2026
f1652fe
feat(weights): offset tables for writes into permuted kernel layouts
faresobeid Nov 3, 2026
7d35cf4
perf(weights): keep the static half of the TP checksum check across u…
faresobeid Nov 3, 2026
ef15448
perf(weights): one compiled sparse-apply variant per bits dtype
faresobeid Nov 3, 2026
45f1541
feat(weights): log where a sparse delta's apply time goes
faresobeid Nov 3, 2026
05d6f00
fix(trainer): per-token fp8 casts multiply by the reciprocal, like vL…
faresobeid Nov 3, 2026
7763f04
Merge remote-tracking branch 'origin/main' into worktree-vllm-0.33
faresobeid Nov 3, 2026
5312977
Merge remote-tracking branch 'origin/chore/vllm-0.33' into sparse-core
faresobeid Nov 3, 2026
9280e96
fix(weights): revalidate before probing a delta; check loaded anchors…
faresobeid Nov 3, 2026
1de91f1
fix(weights): match the index map to the engine's boot layout
faresobeid Nov 4, 2026
c70aa3c
fix(weights): skip tied names a checkpoint omits when reading rows
faresobeid Nov 9, 2026
515e70f
Merge remote-tracking branch 'origin/main' into feat/sparse-filesyste…
faresobeid Nov 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions docs/scaling.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,23 @@ ModelExpress exchanges peer metadata during startup. Weight updates reuse prepar

By default, the trainer and inference worker each allocate one transfer arena. Set `weight_broadcast.overlap_transfer_and_replay = true` to allocate two arenas on both sides and replay one weight group while receiving the next. The additional arena is the size of the largest transfer group per GPU; allocation errors are reported instead of silently disabling overlap.

### Sparse filesystem weight broadcast

With `[weight_broadcast.delta]` on the filesystem broadcast, the trainer publishes only the weight elements that changed since the previous broadcast, in the engine's weight format (bf16, or fp8 for engines that quantize online to `fp8_per_block`). Engines read and decode each delta while they keep serving, patch their weights in place during the pause, and verify every update against trainer checksums.

```toml
[weight_broadcast]
type = "filesystem"

[weight_broadcast.delta]
interval = 50 # full checkpoint (anchor) every N steps
```

- RL: deltas come from the optimizer step, with every optimizer and offload mode. Right before the step updates a chunk of parameters, their shards still hold the previous broadcast's weights; the trainer diffs the old and new values of each chunk as the step goes and writes the delta out. No copy of the weights is kept, except of the few tensors that are not optimizer parameters (buffers, frozen parameters, fp8 tensors whose 128-row blocks straddle trainer ranks). The trainer checks that no weight changed outside the optimizer step.
- A broadcast that does not follow every optimizer step (the startup broadcast of a resumed run, SFT online evals) publishes a full checkpoint.
- Anchors (full checkpoints) are written every `interval` steps, from the optimizer step. They slow that step down, so prefer a larger `interval`. An engine that joins, restarts or fails an update loads the latest complete anchor, reading only the rows it holds, and replays the deltas since.
- Not supported: LoRA, fp8 tensors whose columns are not a multiple of 128, and engines that transform weights after loading other than online `fp8_per_block` (e.g. FlashInfer TRTLLM/CUTLASS MoE, Marlin). fp8 deltas need the same vLLM and torch versions on the trainer and the engines.

### Custom Templates

For unusual partitions, module loads, or environment setup, supply your own Jinja2 template:
Expand Down
13 changes: 13 additions & 0 deletions packages/prime-rl-configs/src/prime_rl/configs/shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,22 @@ class BaseWeightBroadcastConfig(BaseConfig):
offered version in time. Raise it for large models on slow shared filesystems."""


class DeltaWeightBroadcastConfig(BaseConfig):
"""Between anchors, publish only the weight elements that changed since the previous broadcast. Engines patch
their weights in place and verify every update. Not supported with LoRA."""

interval: int = Field(50, ge=1)
"""Publish a full checkpoint (anchor) every this many steps; the steps in between publish their delta
against the previous step. Engines that join or fall off the delta chain load the last anchor and replay
the deltas since."""


class FileSystemWeightBroadcastConfig(BaseWeightBroadcastConfig):
type: Literal["filesystem"] = "filesystem"

delta: DeltaWeightBroadcastConfig | None = None
"""Publish sparse deltas instead of full checkpoints."""


class InMemoryWeightBroadcastConfig(BaseWeightBroadcastConfig):
host: str = "localhost"
Expand Down
2 changes: 2 additions & 0 deletions packages/prime-rl-configs/src/prime_rl/configs/trainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,8 @@ def validate_lora_broadcast(self):
"LoRA requires weight_broadcast.type = 'filesystem': vLLM loads adapters only from a "
"PEFT-shaped directory on disk - in-memory transports have no disk artifact to load from."
)
if self.model.lora is not None and self.weight_broadcast.type == "filesystem" and self.weight_broadcast.delta:
raise ValueError("weight_broadcast.delta does not support LoRA")
return self

@model_validator(mode="after")
Expand Down
12 changes: 12 additions & 0 deletions src/prime_rl/inference/vllm/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,18 @@ async def update_weights(request: Request):
return {"status": "ok"}


@router.post("/prefetch_weights")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we not just make the update weight route non-blocking?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked into this and would keep the route blocking. Here is what /update_weights waits for on a sparse step. All of it runs inside the pause the orchestrator holds:

  1. The TP group's plan. TP rank 0 decides between applying the delta and reloading an anchor and replaying, then broadcasts the plan.
  2. The apply. The fused kernel patches the params in place. The delta is already in pinned host memory, because /prefetch_weights read it.
  3. The verify. It updates the running checksums and runs one TP all-reduce. If any tensor is off, the update fails on every TP worker.
  4. update_weight_version, so tokens sampled after it carry the new version.

Steps 2 and 3 take 0.12-0.83 s in the H200 table. The slow part that needs no pause is reading and decompressing the delta (0.1-5 s). /prefetch_weights already takes that out and is the non-blocking half: its RPC only submits the read to a worker thread, and the route polls prefetch_weights_done. The orchestrator calls it in WeightReceiver.prepare, before it stops dispatch.

Returning from /update_weights before the apply finishes would gain almost nothing and would cost correctness:

  • No throughput gain. The apply runs as a collective_rpc on the engine core, and the core schedules no batches while it runs. The engine generates nothing during the apply either way. A non-blocking route could only save the pause and resume RPCs (0.03-0.08 s together in the table).
  • Versions would move too early. The orchestrator resumes and advances policy.version when the call returns. If it returned early, both the version label and policy.version would move before the weights are verified. Rollouts from a failed or half-applied update would then be labeled v{n+1}. Avoiding that means polling for completion before resuming, which is the blocking call again.
  • Errors would need another channel. Today a failed verify comes back as an error on this call. The engine then marks its step unknown (weight_step = -1), and the next update reloads an anchor and replays. A fire-and-forget route would have to report that failure some other way.
  • The pause would leave the orchestrator. One route that reads and then pauses its own engine would move the pause out of the orchestrator. The orchestrator orders pauses across engines (prefill before decode under P/D) and runs on_version_pending in between, which aborts stale rollouts before the pause.

This does not interact with #4427's overlapped step. The trainer drains it (wait_for_optimizer_step) before weight_sender.broadcast, so the manifest and part files that prepare waits for appear only after the step is complete, and the engine routes key off those files. The "return early, fence later" pattern pays off in #4427/#4428 because the trainer has the next forward to overlap before the fence. Here the fence would be the resume, and nothing useful runs between the update and the resume.

So I'd keep the two routes: /prefetch_weights runs without blocking while the engine serves, and /update_weights blocks for the apply and verify inside the pause. If the naming is the problem, I can rename them (e.g. /stage_weights + /update_weights).

async def prefetch_weights(request: Request):
"""Start decoding a sparse weight delta in the workers and wait for it without
blocking generation: the workers decode on a background thread."""
data = await request.json()
client = engine_client(request)
await client.collective_rpc("prefetch_weights_from_path", args=(data.get("weight_dir"),))
while not all(await client.collective_rpc("prefetch_weights_done")):
await asyncio.sleep(0.05)
return {"status": "ok"}


@router.post("/load_lora_adapter")
async def load_lora_adapter(lora_request: LoadLoRAAdapterRequest, raw_request: Request, weight_version: str):
"""Wrapper around vLLM's /v1/load_lora_adapter.
Expand Down
Loading
Loading