Reinforcement Learning
Transformers
English
post-training
distillation
agentic-coding
composer-2.5
cursor
kimi-k2
grpo
dapo
diloco
openenv
trl
verl
research
methodology
Instructions to use Codeseys/composer-replication-framework with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use Codeseys/composer-replication-framework with Transformers:
# Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("Codeseys/composer-replication-framework", dtype="auto") - Notebooks
- Google Colab
- Kaggle
| """Spike 008 — DiLoCo outer-loop smoke. | |
| Verifies the framework's DiLoCo wrapper integrates cleanly with | |
| `torchft.local_sgd.DiLoCo`. Tests follow torchft's own test pattern | |
| (`torchft/local_sgd_test.py::DiLoCoTest`) — single-process, mock Manager, | |
| verify that the outer optimizer machinery actually fires, NOT that two | |
| replicas converge in single-process (which they cannot due to the post-hook | |
| sequencing — see below). | |
| Cross-replica convergence test deferred to multi-process integration tests | |
| once we have real torch.distributed in CI (post-replication phase). | |
| Per `docs/adrs/ADR-003-diloco-impl.md`. | |
| """ | |
| from __future__ import annotations | |
| import sys | |
| from pathlib import Path | |
| from unittest.mock import create_autospec | |
| import pytest | |
| import torch | |
| import torch.nn as nn | |
| import torch.optim as optim | |
| HERE = Path(__file__).resolve().parent.parent | |
| sys.path.insert(0, str(HERE)) | |
| from composer_diloco import ( # noqa: E402 | |
| _TORCHFT_AVAILABLE, | |
| DiLoCo, | |
| Manager, | |
| _DummyWork, | |
| ) | |
| pytestmark = pytest.mark.skipif( | |
| not _TORCHFT_AVAILABLE, | |
| reason="torchft not installed (pip install torchft-nightly)", | |
| ) | |
| class TinyMLP(nn.Module): | |
| def __init__(self): | |
| super().__init__() | |
| self.net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 2)) | |
| def forward(self, x): | |
| return self.net(x) | |
| def _make_passthrough_manager(): | |
| """Manager whose allreduce is a no-op pass-through. | |
| Why no-op (and not real-averaging): in single-process, replica A's | |
| `inner_a.step()` post-hook runs prepare_sync + perform_sync to completion | |
| BEFORE replica B's `inner_b.step()` is called. By the time replica B | |
| arrives at allreduce, replica A's outer optimizer has already stepped | |
| using A's local pseudogradient. There is no way to inject a true | |
| cross-replica barrier in single-process without rewriting torchft's | |
| internals — and since we're using upstream code, we don't. | |
| This means single-process tests verify the *machinery* (sync fires, | |
| outer optimizer steps, Nesterov state populates), not cross-replica | |
| convergence. True cross-replica convergence is verified in production | |
| by NCCL. | |
| This is also exactly the pattern torchft uses in their own | |
| `torchft/local_sgd_test.py::DiLoCoTest` — they do not test convergence | |
| in single-process. | |
| """ | |
| mgr = create_autospec(Manager) | |
| mgr._use_async_quorum = False | |
| mgr.errored.return_value = None | |
| mgr.should_commit.return_value = True | |
| mgr.current_step.return_value = 0 | |
| def passthrough(tensor: torch.Tensor, should_quantize: bool = False): | |
| return _DummyWork(tensor) | |
| mgr.allreduce.side_effect = passthrough | |
| return mgr | |
| # --------------------------------------------------------------------- | |
| # Acceptance test 1 — outer loop machinery fires on single replica | |
| # --------------------------------------------------------------------- | |
| def test_diloco_single_replica_machinery_fires(): | |
| """Acceptance: 1 replica × 4 inner steps × 2 outer rounds. | |
| After 2 outer rounds: | |
| - allreduce was called once per parameter per round | |
| - start_quorum was called once per round | |
| - outer optimizer's Nesterov state is populated for every parameter | |
| - parameters moved from the initial state | |
| """ | |
| torch.manual_seed(0) | |
| model = TinyMLP() | |
| initial = {n: p.detach().clone() for n, p in model.named_parameters()} | |
| inner = optim.AdamW(model.parameters(), lr=1e-3) | |
| outer = optim.SGD(model.parameters(), lr=0.7, momentum=0.9, nesterov=True) | |
| mgr = _make_passthrough_manager() | |
| SYNC_EVERY = 4 | |
| OUTER_ROUNDS = 2 | |
| n_params = len(list(model.parameters())) | |
| with DiLoCo(mgr, [model], inner, outer, sync_every=SYNC_EVERY) as dl: | |
| for _outer_round in range(OUTER_ROUNDS): | |
| for _inner_step in range(SYNC_EVERY): | |
| inner.zero_grad() | |
| x = torch.randn(8, 4) | |
| y = torch.randn(8, 2) | |
| ((model(x) - y) ** 2).mean().backward() | |
| inner.step() # outer sync fires automatically inside post-hook | |
| # 1. allreduce was called n_params × OUTER_ROUNDS times | |
| assert mgr.allreduce.call_count == n_params * OUTER_ROUNDS, ( | |
| f"expected {n_params * OUTER_ROUNDS} allreduce calls, got {mgr.allreduce.call_count}" | |
| ) | |
| # 2. start_quorum was called once per outer round | |
| assert mgr.start_quorum.call_count == OUTER_ROUNDS, ( | |
| f"expected {OUTER_ROUNDS} start_quorum calls, got {mgr.start_quorum.call_count}" | |
| ) | |
| # 3. should_commit was called once per outer round | |
| assert mgr.should_commit.call_count == OUTER_ROUNDS | |
| # 4. Outer optimizer holds Nesterov momentum state for every parameter | |
| assert len(outer.state_dict()["state"]) == n_params, ( | |
| f"expected {n_params} momentum buffers, got {len(outer.state_dict()['state'])}" | |
| ) | |
| # 5. Parameters moved from θ_initial (outer optimizer actually applied updates) | |
| any_change = any( | |
| not torch.equal(p, initial[n]) for n, p in model.named_parameters() | |
| ) | |
| assert any_change, "outer optimizer did not move the parameters" | |
| # --------------------------------------------------------------------- | |
| # Acceptance test 2 — torchft sign convention is what we expect | |
| # --------------------------------------------------------------------- | |
| def test_diloco_pseudogradient_sign_convention(): | |
| """Verify torchft computes pseudograd = θ_initial − θ_local + outer SGD math. | |
| Setup: | |
| - inner LR = 0 (so inner steps don't move params; only outer sync moves them) | |
| - manually nudge params so θ_local ≠ θ_initial | |
| - outer LR = 1, momentum = 0 (plain SGD, no Nesterov complications) | |
| - sync_every = 2 | |
| Math: | |
| pseudograd = θ_initial − θ_local = -nudge | |
| restore: p.data ← θ_initial | |
| outer step: p.data ← θ_initial - lr * pseudograd | |
| = θ_initial - 1 * (-nudge) | |
| = θ_initial + nudge | |
| = θ_local_at_sync | |
| merge(alpha=0): p.data unchanged | |
| Expected after 1 outer round: final = θ_local_at_sync | |
| A sign flip in pseudograd would land us at `θ_initial - nudge` (movement | |
| in the wrong direction by 2*nudge total), which this test catches. | |
| """ | |
| torch.manual_seed(0) | |
| model = TinyMLP() | |
| inner = optim.SGD(model.parameters(), lr=0.0) # zero inner LR | |
| outer = optim.SGD(model.parameters(), lr=1.0, momentum=0.0) # plain SGD | |
| mgr = _make_passthrough_manager() | |
| SYNC_EVERY = 2 | |
| NUDGE = 0.5 | |
| initial_param = next(model.parameters()).detach().clone() | |
| with DiLoCo(mgr, [model], inner, outer, sync_every=SYNC_EVERY) as dl: | |
| # Manually nudge AFTER the DiLoCo wrapper saved θ_initial so | |
| # θ_local ≠ θ_initial when prepare_sync runs. | |
| with torch.no_grad(): | |
| for p in model.parameters(): | |
| p.add_(NUDGE) | |
| local_param_after_nudge = next(model.parameters()).detach().clone() | |
| # Run inner steps with zero LR — the post-hook fires the outer sync | |
| # at step `sync_every` but the inner step itself doesn't move params. | |
| for _ in range(SYNC_EVERY): | |
| inner.zero_grad() | |
| x = torch.randn(8, 4) | |
| ((model(x) - torch.randn(8, 2)) ** 2).mean().backward() | |
| inner.step() | |
| final_param = next(model.parameters()).detach().clone() | |
| # Per the math above: final should equal θ_local_at_sync = θ_initial + NUDGE. | |
| expected = local_param_after_nudge | |
| diff = (final_param - expected).abs().max().item() | |
| # And the wrong-sign result would have been θ_initial - NUDGE | |
| wrong_sign = initial_param - NUDGE * torch.ones_like(initial_param) | |
| wrong_sign_diff = (final_param - wrong_sign).abs().max().item() | |
| assert diff < 1e-5, ( | |
| f"sign convention violated. \n" | |
| f" initial[0,0]={initial_param.flatten()[0].item():.6f}\n" | |
| f" local_at_sync[0,0]={local_param_after_nudge.flatten()[0].item():.6f}\n" | |
| f" final[0,0]={final_param.flatten()[0].item():.6f}\n" | |
| f" expected[0,0]={expected.flatten()[0].item():.6f}\n" | |
| f" max-abs-diff={diff:.6e}\n" | |
| f" wrong-sign-diff={wrong_sign_diff:.6e} (≈0 means sign flipped)\n" | |
| ) | |
| # --------------------------------------------------------------------- | |
| # Acceptance test 3 — Spike 005 imports still work alongside torchft | |
| # --------------------------------------------------------------------- | |
| def test_no_regression_in_spike_005_imports(): | |
| """Verify importing torchft + composer_diloco coexists with Spike 005. | |
| This is a lightweight import-side-effects test. The 38-test Spike 005 | |
| suite runs separately and passes there. | |
| """ | |
| spike_005 = HERE.parent / "005-integrated-trainer-skeleton" | |
| sys.path.insert(0, str(spike_005)) | |
| from opsd_loss import generalized_jsd_loss # noqa: F401 | |
| from teacher_replay import extract_dpo_pairs # noqa: F401 | |
| # Construct a fresh DiLoCo and verify it can be entered + exited | |
| model = TinyMLP() | |
| inner = optim.AdamW(model.parameters(), lr=1e-3) | |
| outer = optim.SGD(model.parameters(), lr=0.7, momentum=0.9, nesterov=True) | |
| mgr = _make_passthrough_manager() | |
| with DiLoCo(mgr, [model], inner, outer, sync_every=2) as dl: | |
| assert dl is not None | |
| # --------------------------------------------------------------------- | |
| # Acceptance test 4 — wrapper smoke (make_diloco_outer_loop) | |
| # --------------------------------------------------------------------- | |
| def test_make_diloco_outer_loop_factory(): | |
| """The framework's `make_diloco_outer_loop()` constructs a working DiLoCo.""" | |
| from composer_diloco import make_diloco_outer_loop | |
| model = TinyMLP() | |
| inner = optim.AdamW(model.parameters(), lr=1e-3) | |
| mgr = _make_passthrough_manager() | |
| dl = make_diloco_outer_loop( | |
| manager=mgr, | |
| model_fragments=[model], | |
| inner_optimizer=inner, | |
| outer_lr=0.7, | |
| outer_momentum=0.9, | |
| nesterov=True, | |
| sync_every=4, | |
| ) | |
| # Outer optimizer was constructed with our hyperparams | |
| assert dl._sync_every == 4 | |
| assert dl is not None | |
| # --------------------------------------------------------------------- | |
| # Acceptance test 5 — Streaming DiLoCo config path (deferred to v0.2 but | |
| # importable today) | |
| # --------------------------------------------------------------------- | |
| def test_streaming_diloco_with_two_fragments_constructs(): | |
| """Streaming DiLoCo accepts 2 fragments + nonzero sync delay (config path).""" | |
| torch.manual_seed(0) | |
| model = TinyMLP() | |
| # Two-fragment split (each linear is its own fragment) | |
| fragments = [model.net[0], model.net[2]] | |
| inner = optim.AdamW(model.parameters(), lr=1e-3) | |
| outer = optim.SGD(model.parameters(), lr=0.7, momentum=0.9, nesterov=True) | |
| mgr = _make_passthrough_manager() | |
| # sync_every=4, 2 fragments → effective per-fragment sync_every=2. | |
| # fragment_sync_delay=0 = no delay (still vanilla DiLoCo per-fragment). | |
| with DiLoCo( | |
| mgr, fragments, inner, outer, | |
| sync_every=4, fragment_sync_delay=0, fragment_update_alpha=0.0, | |
| ) as dl: | |
| assert len(dl._fragments) == 2 | |