feat: disk-delta weight sync for non-colocated rollout engines - #1235
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces Delta Weight Sync, a sparse, non-colocated weight synchronization mode that sends only changed weight positions and values instead of broadcasting full parameters at each step. It supports both NCCL and disk-based transports, along with multiple encoding strategies (indices, deltas, and zstd compression). The code review identified three critical or high-severity issues in the delta sync implementation: a runtime AttributeError due to the use of a non-existent torch.hash_tensor API, a potential distributed deadlock in _finalize_sync caused by conditionally calling the collective _publish_batch operation, and a potential RuntimeError when calling .view() on non-contiguous tensors in _bytewise_diff_mask.
|
qq, is this compatible with |
Add --update-weight-transfer-mode disk-delta: source ranks diff each gathered HF tensor against a CPU snapshot of the previous sync and publish only the changed bytes (xor or overwrite encoding, zstd, per- tensor checksums) as a canonical HF checkpoint dir under --update-weight-disk-dir. Each engine's /pull_weights (companion sglang PR) applies the deltas into a host-local checkpoint on every host the engine spans, and the engine reloads it via the ordinary update_weights_from_disk path. Migrated from slime PRs THUDM/slime#1806, radixark#2089 and radixark#2181 (final design: disk-level delta + engine-side pull), rebuilt on miles primitives: the delta updater subclasses DistBucketedWeightUpdateMixin and feeds its callback-based TP/EP gather into the pinned-buffer diff/compress pipeline.
daa2ac0 to
ddda316
Compare
Megatron replicates the word embedding onto the last PP stage when it hosts an MTP block (and for tied embeddings), so one source rank per stage gathers and diffs the same HF tensor. The XOR apply is an involution, so a tensor present in two shard files is applied twice and reverts — caught by the per-tensor checksum on GLM-4.7-Flash (PP2 + --mtp-num-layers 1). Dedup at publish time: gather the per-rank name->checksum maps, raise on divergent duplicates (replicas are gradient-synced; divergence means broken sync), lowest rank keeps the tensor. Broadcast/P2P are unaffected (duplicate loads are idempotent).
49cfa17 to
32581c9
Compare
Not yet. Integrity is covered differently here: every sync, each host verifies a per-tensor xxh3-128 checksum of the applied bytes against the trainer's export and fails loud on mismatch. If we want CI compatibility: when the checker is on, make the baseline also full-reload the pulled v0 checkpoint via |
|
Note for future maintenance: (1) unify the checksum mechanism in miles repo (2) when refactor the weight update logic, unify some logic here (e.g. lifecycle, pop metrics...) |
…i-weight-update The weights checker (--check-weight-update-equal, armed by --ci-test) resets engine tensors at startup and compares after the first sync, expecting it to rewrite every tensor — but the disk-delta baseline publishes nothing. When the checker is armed, the baseline now also reloads the just-pulled v0 checkpoint via update_weights_from_disk, restoring engine state and setting the weight version the CI equality check expects. Adds tests/e2e/megatron/test_qwen3_4B_disk_delta.py (mirrors the p2p weight-update test: 4 actor + 4 rollout GPUs, --ci-test), registered under labels ["megatron", "weight-update"]; disabled until the CI image ships sglang /pull_weights (sgl-project/sglang#30366).
|
Update: |
…ge ships them The GPU CI image predates the requirements.txt additions and everything is installed --no-deps, so the disk-delta test failed with ModuleNotFoundError: zstandard in the trainer actor. Same pattern as polars: install alongside it until the next image rebuild.
…int-url) An elastic fleet behind one opaque HTTP endpoint exposes no per-engine handles, so the disk-delta updater can't push pull_weights/update RPCs or gather per-engine success. In publish-only mode each sync publishes the version dir, then advances an atomic `latest` pointer (running the post-write hook on the version dir first, and on the pointer after, so a non-POSIX shared store surfaces the data before the pointer that names it). The fleet pulls on its own schedule; per-request weight-version gating on the endpoint is the consumer-side guarantee. The trainer-side updater still connects every sync (actor.py guard: zero engines means has_new_engines never fires and the disk-delta class defines no is_rollout_engines_fresh), and validation forces rollout_num_gpus=0 so no rollout GPUs are reserved in the placement group. Ported from the nvfp4-disagg-v2 fork (1210351) onto the radixark#1235 delta architecture, where it is simpler: pause/flush live inside _reload_engines, so publish-only just takes the _announce_version branch, and _capture_baseline needs no guard (empty engine lists are already no-ops). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ucT9vHENjhVatxSvsT1Mq
ci-sglang-pr: #30366
Adds
--update-weight-transfer-mode disk-delta: weight sync for non-colocated rollout engines that ships only the bytes changed between syncs — for cross-cluster/datacenter disaggregation where moving the full model every sync dominates.Engine-side
/pull_weights: sgl-project/sglang#30366 (sglang-miles), sgl-project/sglang#30367 (main). Refs: THUDM/slime#1806, THUDM/slime#2089, THUDM/slime#2181.Design
update_weights()publishes nothing: it seeds a CPU snapshot from--hf-checkpoint's safetensors bytes (= the engines' actual base, robust to non-byte-exact megatron→HF round-trips) and issuespull_weights(0)so each host warms its local checkpoint.weight_v{N:06d}/under--update-weight-disk-diras a canonical HF checkpoint dir — compressed-diff shards + an index carrying version/encoding/checksum metadata, coordinated over gloo rather than the filesystem.pull_weights(N)before pausing (disk-only, overlaps rollout): per-host flock, in-place apply, per-tensor checksum verify, fail-loud. Then pause → flush → vanillaupdate_weights_from_disk(local_dir, weight_version=N)→ continue.DistBucketedWeightUpdateMixin's callback gather; no NCCL groups or engine lock; density/wire metrics drain viapop_metrics()into the step log.Args
Read-side counterpart:
--sglang-custom-pull-weights-pre-read-hook. New deps:xxhash,zstandard,blake3.Validation
GLM-4.7-Flash GRPO on 4× H200: 2-node actor (TP2/PP2/CP2/EP8, MTP on → exercises the dedup) + one tp16/dp16 engine spanning 2 rollout nodes (exercises the multi-host pull fan-out). Disk-delta publishes to an object-store-backed shared volume via the post-write/pre-read hooks (
xor+xxh3-128); the baseline is the same config with the default--update-weight-transfer-mode broadcast(full-checkpoint NCCL each sync).One sync per training step:
vNis the model after optimizer step N, published and applied before rollout N+1. "baseline" is the startupupdate_weights()before the first rollout: disk-delta ships nothing (the trainer captures its diff-base snapshot while each host copies the engine's base checkpoint to local disk); broadcast pushes the full weights.disk-delta vs full-checkpoint NCCL broadcast
update_weightstimeupdate_weights_from_diskreload; every other phase runs while the engine servesBroadcast wins on raw sync latency inside one datacenter; disk-delta moves ~80× fewer bytes with a comparable generation-paused window and runs where NCCL can't (cross-cluster/datacenter, object-store-only links).
disk-delta per-sync detail
update_weightstimeSteady-state breakdown of the 23.2 s (from engine request timestamps):
update_weights_from_disk: local disk → GPUThe only phase that needs the engine stopped is the final
update_weights_from_diskreload — in a fully async rollout loop the engine keeps serving through diff/publish/pull, so the effective generation pause per sync is just the 3–5 s reload. All pulls checksum-verified on both engine hosts; the index holds each tensor exactly once.Caveats
--check-weight-update-equal(when the checker is armed, the baseline also reloads the pulled v0 checkpoint — the checker resets engine tensors and expects the first sync to rewrite everything).tests/e2e/megatron/test_qwen3_4B_disk_delta.pyis registered underrun-ci-weight-update, disabled until the CI image ships sglang/pull_weights([RL] Add /pull_weights: engine-side pull of published weights into a host-local checkpoint (sglang-miles) sgl-project/sglang#30366).