[Sync][E] delta weight sync (slime #1806/#1946/#1991) - #150
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces Delta Weight Sync to optimize bandwidth by transmitting only the byte-level changes of model weights since the last synchronization, supporting both NCCL and disk-based transports. The review feedback highlights three critical robustness issues: a potential RuntimeError when calling .view() on non-contiguous tensors in _bytewise_diff_mask, and two potential crashes when handling non-floating-point (integer) tensors, specifically when initializing a tensor with NaN in decode_delta_one_param and when calling torch.isnan inside the patched copy and fill operations.
| def _bytewise_diff_mask(current: torch.Tensor, snapshot: torch.Tensor) -> torch.Tensor: | ||
| """ | ||
| Per-element bool mask: True where current and snapshot bytes differ. Dtype-agnostic via view-as-integer. | ||
| """ | ||
| es = current.element_size() | ||
| int_dtype = {1: torch.uint8, 2: torch.int16, 4: torch.int32, 8: torch.int64}.get(es) | ||
| if int_dtype is None: | ||
| raise ValueError(f"unsupported element size {es}") | ||
| return current.view(int_dtype) != snapshot.view(int_dtype) |
There was a problem hiding this comment.
The _bytewise_diff_mask function calls .view() on current and snapshot without ensuring they are contiguous. If either tensor is non-contiguous (which can happen for quantized weights or sliced buffers), PyTorch will raise a RuntimeError. Adding .contiguous() before .view() ensures safety.
| def _bytewise_diff_mask(current: torch.Tensor, snapshot: torch.Tensor) -> torch.Tensor: | |
| """ | |
| Per-element bool mask: True where current and snapshot bytes differ. Dtype-agnostic via view-as-integer. | |
| """ | |
| es = current.element_size() | |
| int_dtype = {1: torch.uint8, 2: torch.int16, 4: torch.int32, 8: torch.int64}.get(es) | |
| if int_dtype is None: | |
| raise ValueError(f"unsupported element size {es}") | |
| return current.view(int_dtype) != snapshot.view(int_dtype) | |
| def _bytewise_diff_mask(current: torch.Tensor, snapshot: torch.Tensor) -> torch.Tensor: | |
| """ | |
| Per-element bool mask: True where current and snapshot bytes differ. Dtype-agnostic via view-as-integer. | |
| """ | |
| es = current.element_size() | |
| int_dtype = {1: torch.uint8, 2: torch.int16, 4: torch.int32, 8: torch.int64}.get(es) | |
| if int_dtype is None: | |
| raise ValueError(f"unsupported element size {es}") | |
| return current.contiguous().view(int_dtype) != snapshot.contiguous().view(int_dtype) |
| param_dtype = p.dtype if isinstance(p.dtype, torch.dtype) else getattr(torch, p.dtype) | ||
| flat = torch.full((numel,), float("nan"), dtype=param_dtype, device=device) |
There was a problem hiding this comment.
decode_delta_one_param attempts to initialize flat with float("nan") for any parameter dtype. If the parameter is an integer tensor (e.g., in quantized models or integer buffers), PyTorch will raise a RuntimeError because NaN cannot be represented in integer types. We should explicitly check and raise a clear error for non-floating-point parameters.
| param_dtype = p.dtype if isinstance(p.dtype, torch.dtype) else getattr(torch, p.dtype) | |
| flat = torch.full((numel,), float("nan"), dtype=param_dtype, device=device) | |
| param_dtype = p.dtype if isinstance(p.dtype, torch.dtype) else getattr(torch, p.dtype) | |
| if not param_dtype.is_floating_point: | |
| raise ValueError( | |
| f"Delta weight sync is only supported for floating-point parameters, " | |
| f"but parameter {p.name} has non-floating-point dtype {p.dtype}." | |
| ) | |
| flat = torch.full((numel,), float("nan"), dtype=param_dtype, device=device) |
| def patched_copy_(self, src, *args, **kwargs): | ||
| if is_param_target(self) is not None: | ||
| src_aligned = ( | ||
| src.to(device=self.device, dtype=self.dtype) if src.dtype != self.dtype else src | ||
| ) | ||
| mask = ~torch.isnan(src_aligned) | ||
| self[mask] = src_aligned[mask] | ||
| return self | ||
| return original_copy_(self, src, *args, **kwargs) | ||
|
|
||
| def patched_fill_(self, value): | ||
| if is_param_target(self) is not None: | ||
| # NaN scalar means "don't change the param" (per-element analog of | ||
| # patched_copy_). Non-NaN scalars write through. | ||
| try: | ||
| if math.isnan(value): | ||
| return self | ||
| except TypeError: | ||
| pass | ||
| return original_fill_(self, value) | ||
| return original_fill_(self, value) |
There was a problem hiding this comment.
patched_copy_ and patched_fill_ intercept copies and fills to model parameters/buffers during the delta apply context. However, if any of these parameters/buffers are non-floating-point (such as integer buffers), calling torch.isnan on them will raise a RuntimeError. Restricting the NaN-masking logic to floating-point tensors only prevents these crashes.
def patched_copy_(self, src, *args, **kwargs):
if is_param_target(self) is not None and torch.is_floating_point(self):
src_aligned = (
src.to(device=self.device, dtype=self.dtype) if src.dtype != self.dtype else src
)
mask = ~torch.isnan(src_aligned)
self[mask] = src_aligned[mask]
return self
return original_copy_(self, src, *args, **kwargs)
def patched_fill_(self, value):
if is_param_target(self) is not None and torch.is_floating_point(self):
# NaN scalar means "don't change the param" (per-element analog of
# patched_copy_). Non-NaN scalars write through.
try:
if math.isnan(value):
return self
except TypeError:
pass
return original_fill_(self, value)
return original_fill_(self, value)8803d18 to
68d40fa
Compare
a446a70 to
e972a40
Compare
73263b6 to
3ec7c48
Compare
…le global batch + group_ids (slime #1926/#1930/#1933/#1941/#1959/#1965/#1969/#1984/#1962)
Port THUDM/slime mega-PR A (rollout data-model + train-split):
- #1926 move micro-batch scheduling train->rollout side (dp_schedule.py, first-fit packing)
- #1930+#1933 variable global batch size + per-token-loss reduction fix (cp_utils
reduce_train_step_metrics / get_sum_of_sample_mean sample_denoms; step_global_batch_size
denominator). CP-invariance + rollout==train equality validated by test_metric_report_dist
/ test_loss_cp_invariance (57/57 multi-proc gloo across CP/DP in {1,2,4}); test_dp_schedule 8/8.
- #1959 forge_load replay (vime/rollout/forge_load.py, --load-forge-rollout-data)
- #1965 group_ids fall back to range(), not sample.index
- #1969 --save-hf for raw mode (hf_checkpoint_saver.py)
- #1941 multi-sample fanout test + helpers (sglang_rollout->vllm_rollout)
- #1984 rename rollout_ids->group_ids (NON-agent files; agent half deferred to PR-D)
- #1962 lint (forge_load)
sglang->vLLM translated per arg-map (--sglang-*->--vllm-*, vllm_speculative_config).
NOTE: test_sample depends on args.vllm_speculative_config existing -> relies on PR-C #1938
getattr guard (stack order F+C+B+A satisfies this). py_compile clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: aoshen02 <aoshen@inferact.ai>
…u CI job
Oracle found A#145 added the data-model numeric tests + hf_checkpoint_saver
impl but did NOT wire them into the cpu CI job (e2e-test-plugin-contracts),
and the hf_checkpoint_saver unit test (slime #1969) was never ported.
- port tests/utils/test_hf_checkpoint_saver.py (slime->vime)
- add dp_schedule/cp_utils/metric_report{,_dist}/loss_cp_invariance/sample
+ utils/test_hf_checkpoint_saver to the cpu job; regen pr-test.yml
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
slime #2013 reverts #1984 (the rollout_id->group_id rename) because it conflicts with internal tooling. A #145 ported #1984, so mirror the revert to stay byte-faithful and realign with main + post-#2013 slime: group_id -> rollout_id, group_ids -> rollout_ids, group_mask_sums -> rollout_mask_sums, num_groups_in_rollout -> num_rollouts_in_rollout, and drop the Sample.rollout_id deprecation guard (__getattribute__/__setattr__ + import warnings) #1984 had added. Per-file churn matches slime #2013 exactly (actor/data/loss/model/rollout/ _fanout_test_helpers/forge_load/dp_schedule/types + 3 tests). The unrelated compute_pass_rate(num_groups=...) kwarg is left as-is (slime keeps it too). NOTE: #2013 also reverts D-side files not present on A (examples/coding_agent_rl/*, vime/agent/trajectory.py, docs .../agent.md, the group_id sections of docs customization.md + multi_agent stamping). Those must get the same revert when D #148 is rebased onto this A.
…no image rebuild Port THUDM/slime delta weight sync to vime's vLLM rollout. Trainer bytewise-diffs weights vs a pinned-CPU snapshot and ships only changed positions+values (nccl broadcast or disk safetensors); receiver overwrites only the changed bytes (lossless, NaN-masked). Bandwidth optimization for large / cross-DC non-colocate. - delta_io.py: DeltaEncoding/DeltaParam/DeltaSpec (local; slime imported from sglang io_struct). - update_weight_from_distributed_delta.py: trainer encoder (subclasses UpdateWeightFromDistributed). - delta_receiver.py: engine-agnostic pure-torch decode + NaN-masked apply (param_storage_index / delta_apply_context monkey-patches torch.Tensor.copy_/fill_ scoped to model param storage, wrapping a normal model.load_weights). - vLLMColocateWorkerExtension: collective_rpc-callable delta receivers (nccl: recv positions/values on model_update_group; disk: read safetensors). NO image rebuild -- receiver is runtime worker-extension/hijack, like the existing IPC hijack. - args: --update-weight-mode/transport/encoding/delta-dir/keep-files/chunk-bytes. - actor.py: delta-mode dispatch (lazy import, #1946 backward-compat) + zero-delta version bump. - docs/zh/advanced/delta-weight-sync.md (+toctree) + e2e test test_delta_weight_update.py (CI registered). SKIPPED: #1993 (sglang EAGLE draft-worker delta forwarding) = N/A, no vime/vLLM analogue. CAVEAT (documented): vLLM does not refresh EAGLE/MTP draft weights on ANY sync (main-model only) -- vLLM-wide limitation, not delta-specific; upstream-track. Based on PR-A (#145) (stacked). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: aoshen02 <aoshen@inferact.ai>
The step-0 actor/ref KL invariant (assert train/kl_loss<1e-8) fails with --entropy-coef 0.01 (kl_loss~6e-3), which the test needs so tied-reward groups still yield a nonzero gradient -> real delta files. A full-mode control run (gb200 8426) fails the SAME gate identically (kl_loss=0.0069), proving the gate is orthogonal to delta correctness. Keep the train_rollout_logprob_abs_diff<=0.1 gate active (0.025) as the real delta-sync correctness guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The delta receivers (apply_delta_from_disk / apply_delta_from_distributed) live on vLLMColocateWorkerExtension, which was only passed via --worker-extension-cls when args.colocate. But delta sync targets the NON-colocate case, so the rollout worker had no extension class and collective_rpc raised 'NotImplementedError: Method apply_delta_from_disk is not implemented' (confirmed on both transports: gb200 8428 disk + 8429 nccl). Wire the extension whenever colocate OR update_weight_mode==delta. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
e972a40 to
9d6f332
Compare
…_metrics) These were introduced by slime PR #1806 (delta weight sync) which maps to vime #150 — not planned for inclusion yet. Revert to pre-#1806 base class interface while keeping the other structural alignments. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: mirror slime structure in update_weight_from_distributed + vllm_rollout Align with slime@44d29ee to reduce structural divergence: update_weight_from_distributed.py: - Add update_weight_metrics + pop_metrics() (actor-side metric drain) - Add _on_chunk() hook (no-op base, override point for subclasses) - Restructure _send_weights() to call _on_chunk before each broadcast - Remove _send_hf_chunk / _update_weights_vllm_packed wrappers - Align _iter_non_expert_chunks buffer accounting to convert-first-then-measure - Remove early-exit guard in _ep_gather_and_convert - Update class docstring to document subclass extension points vllm_rollout.py: - Add docstrings matching slime (generate_rollout, generate_rollout_async, eval_rollout_single_dataset, generate_and_rm_group) - Add inline comments matching slime (generate_and_rm, generate_rollout_async) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: drop delta-sync additions (pop_metrics, _on_chunk, update_weight_metrics) These were introduced by slime PR #1806 (delta weight sync) which maps to vime #150 — not planned for inclusion yet. Revert to pre-#1806 base class interface while keeping the other structural alignments. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: restore slime multi-line formatting (revert ruff line-collapse) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: revert ruff line-collapse in vllm_rollout, keep only comment/docstring additions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: mirror slime structure in update_weight_from_distributed + vllm_rollout Align with slime@44d29ee to reduce structural divergence: update_weight_from_distributed.py: - Add update_weight_metrics + pop_metrics() (actor-side metric drain) - Add _on_chunk() hook (no-op base, override point for subclasses) - Restructure _send_weights() to call _on_chunk before each broadcast - Remove _send_hf_chunk / _update_weights_vllm_packed wrappers - Align _iter_non_expert_chunks buffer accounting to convert-first-then-measure - Remove early-exit guard in _ep_gather_and_convert - Update class docstring to document subclass extension points vllm_rollout.py: - Add docstrings matching slime (generate_rollout, generate_rollout_async, eval_rollout_single_dataset, generate_and_rm_group) - Add inline comments matching slime (generate_and_rm, generate_rollout_async) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: drop delta-sync additions (pop_metrics, _on_chunk, update_weight_metrics) These were introduced by slime PR #1806 (delta weight sync) which maps to vime #150 — not planned for inclusion yet. Revert to pre-#1806 base class interface while keeping the other structural alignments. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: restore slime multi-line formatting (revert ruff line-collapse) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: revert ruff line-collapse in vllm_rollout, keep only comment/docstring additions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PR-E — delta weight sync (RFC #107). Stacked on PR-A (#145) (base
sync/slime-mega-A; touches actor.py which A also touches).Bandwidth-optimized RL weight sync: trainer bytewise-diffs weights vs a pinned-CPU snapshot, ships only changed positions+values (nccl broadcast / disk safetensors); receiver overwrites only the changed bytes (lossless, NaN-masked). For large / cross-DC non-colocate (colocate uses faster CUDA-IPC, doesn't need delta).
Included
DeltaEncoding/DeltaParam/DeltaSpec(local; slime imported these from sglang's io_struct).UpdateWeightFromDistributed).delta_apply_contextmonkey-patchestorch.Tensor.copy_/fill_scoped to model param storage, wrapping a normalmodel.load_weightsso only changed (non-NaN) positions are written;post_load_weights(fp8 scales) uses original copy_.model_update_group; disk: read safetensors).--update-weight-mode/transport/encoding/delta-dir/keep-files/chunk-bytes), actor delta-dispatch (lazy import, #1946 backward-compat) + zero-delta version bump.docs/zh/advanced/delta-weight-sync.md(+toctree) + e2e testtest_delta_weight_update.py(registered ine2e-test-short).🎉 No image rebuild
slime put the receiver in a build-time
sglang.patch. vime injects it at runtime via the worker-extension / the existing_VLLMHijackmechanism (loaded bypip install -e .) — no Dockerfile change, no gb200/h200 rebuild.Skipped / caveat
gpu_model_runner.reload_weightsloads main-modelnamed_parametersonly; drafter loaded once at init). This is a vLLM-wide limitation, not delta-specific (full sync has it too) — to track upstream, not fixed here.CI
test_delta_weight_update.py(4-GPU non-colocate, disk transport, asserts delta safetensors written) registered ine2e-test-short(.j2+.yml). py_compile clean.🤖 Generated with Claude Code
Fidelity ledger (audited 2026-06-04)
apply_delta_from_disk/apply_delta_from_distributed)test_delta_weight_updateEXIT_RC=0 on rebuilt FCBAGDE (gb200 8910)