File size: 19,581 Bytes
31dc8dc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
# Domain / Architecture Refactor Track

This is the **domain/architecture** track β€” making the runtime support multiple
draft algorithms (eagle3 / dflash / domino / future) and a real product surface,
*orthogonal* to the online-disaggregation scale-out work in
[./online-disaggregation.md](./online-disaggregation.md). It builds on the
canonical substrate (SampleRef + FeatureStore + FeatureDataLoader β†’ TrainBatch),
keeps the runtime training seam (`TrainerCore` / `DraftTrainStrategy` /
`TrainingBackend` + `StepContext`) and only *wraps* it with domain objects. The
online target is FROZEN β€” there is **no** weight-sync, no `HiddenStateStream`
source of truth, and `draft_weight_version` survives only as provenance metadata.
Sibling tracks: [./online-disaggregation.md](./online-disaggregation.md),
[./eval-and-breadth.md](./eval-and-breadth.md). Top-level plan:
[../../plan.md](../../plan.md) (the detailed Β§4.2–4.8 sketches this track lands
incrementally are mirrored in [../redesign-draft-legacy.md](../redesign-draft-legacy.md)).

**Dependency order:** A (in review) β†’ B β†’ {C, D}; D β†’ E0 β†’ E (C is parallel β€” not a prerequisite
of E). `E0` is a move-only layout consolidation (zero functional change) that runs at the front of
E so E's composition work lands in the final layout. B's `TargetEngine` extraction also unblocks
the online O1.3 multi-backend producer in [./online-disaggregation.md](./online-disaggregation.md).

---

### A β€” Composable launch Β· size L Β· GPU Β· status: in review (PRs #627/#628/#629, validated)
- **Goal** One strategy-parameterized launch path so adding an algorithm is a
  registry entry, not a new `build_*` family β€” and so a schedule-dependent loss
  (domino) flows through the same `TrainerCore`.
- **Target state** `eagle3`, `dflash`, `domino` all run end-to-end (offline +
  online) through a single set of topology builders that take `strategy=` and
  resolve a `StrategySpec`. `launch.py` grows as `topologies + one spec per
  model`, never `topologies Γ— models`.
- **Implementation** (landed)
  - `training/registry.py` β€” `StrategySpec` dataclass + `register_strategy` /
    `resolve_strategy` / `available_strategies`; one entry each for `eagle3`,
    `dflash`, `domino`. Each spec carries `make_strategy`, `required_features`,
    the offline reader/transform/collate triple + `offline_target_repr`, the
    online collate (`concat_collate`), and `make_adapter` (None β‡’ default
    `SGLangAdapter`).
  - `launch.py` β€” `_assemble_trainer` / `_offline_io` / `_assemble_rollout_workers`
    plus topology builders `build_offline_runtime`, `build_disagg_offline_runtime`,
    `build_online_runtime`, `build_disagg_online_{producer,consumer,runtime}`;
    every one resolves the spec. Legacy `build_*_eagle3_*` names are kept as
    aliases.
  - `training/strategy.py` β€” `DraftTrainStrategy` ABC + `Eagle3TrainStrategy`,
    `DFlashTrainStrategy`, `DominoTrainStrategy`; `StepContext`
    (`global_step` / `total_steps`) is threaded into `forward_loss` so domino's
    `_lambda_base` decay reads the schedule without ad-hoc kwargs on the model
    forward.
  - `inference/dflash_adapter.py` β€” `DFlashAdapter` over `generate_dflash_data`,
    emitting the `{input_ids, hidden_states, loss_mask}` schema (no
    `_project_target` / `t2d`); domino reuses it.
- **Tests / gates** `tests/test_runtime/` incl. `test_dflash_launch.py`,
  `test_dflash_online_launch.py`; full suite **197 tests/test_runtime green on
  H200**.
- **Done when** All three strategies run offline + online via the one parameterized
  path with the suite green (validated, in review).

---

### B β€” Domain abstractions Β· size L Β· GPU Β· status: in review (PRs #631/#632/#633/#635, validated)
- **Goal** De-EAGLE3 the target boundary and give the runtime a domain `Trainer`
  that *wraps* (does not replace) the runtime controller, so the architecture
  reads in product terms while the seam stays intact.
- **Target state** A backend-agnostic `TargetEngine` ABC sits where the
  EAGLE3-named `Eagle3TargetModel` is today; `hf` / `sglang` / `sglang_server` /
  `custom` are interchangeable backends behind it. A domain `Trainer` is the
  caller-facing object; under it the `TrainerController` / `TrainerCore` /
  `DraftTrainStrategy` / `TrainingBackend` seam is byte-for-byte unchanged. The
  `FeatureSource` Protocol in `rollout_worker.py` stays the worker's only
  contract. **No `HiddenStateStream`** β€” `FeatureDataLoader` over
  SampleRef+FeatureStore *is* the stream.
- **Implementation**
  1. Extract `TargetEngine` (ABC) from
     `specforge/modeling/target/eagle3_target_model.py:79` (`Eagle3TargetModel`).
     De-EAGLE3 the names: the ABC's extraction method becomes a generic
     `generate_features(...)`-style capture call with EAGLE3 specifics
     (`generate_eagle3_data`, `aux_hidden_states_layers`,
     `set_aux_hidden_states_layers`) moved into an `Eagle3TargetEngine` subclass;
     `DFlashTargetModel` (`dflash_target_model.py:33`, `set_capture_layers`)
     becomes a sibling subclass. Keep `get_eagle3_target_model(backend=...)`
     working as a thin shim during migration, then rename to a generic factory.
  2. Add an explicit `backend` attribute on each engine (today `SGLangAdapter.health`
     / `DFlashAdapter.health` read `getattr(target_model, "backend", "unknown")`
     β€” make it real), and add the `sglang_server` backend branch to the factory
     (currently only `sglang` / `hf` / `custom`). The *depth* of this `sglang_server`
     branch is informed by the O1.3 capture spike (see
     [online-disaggregation.md](./online-disaggregation.md) Β§O1.3): the de-EAGLE3
     extraction (step 1) and the domain `Trainer` carry no engine risk and can land
     regardless of the spike's outcome β€” only the live-server capture backend does.
  3. Keep the adapters (`SGLangAdapter`, `DFlashAdapter`) as the
     `FeatureSource` implementations over the engine; nothing in
     `rollout_worker.py` changes β€” its `FeatureSource` Protocol
     (`generate_features(tasks, *, capture)`) is already the seam.
  4. Introduce a domain `Trainer` (new `training/` module) that composes
     `resolve_strategy` β†’ `FSDPTrainingBackend` β†’ `TrainerCore` β†’
     `TrainerController` (exactly what `launch._assemble_trainer` wires today)
     behind one object, and calls `.fit()`. The controller/core seam is wrapped,
     not edited.
- **Tests / gates** **Byte-identical batches and loss vs the pre-refactor run**
  for all three strategies (snapshot the first-N `TrainBatch.tensors` digests +
  per-step loss before refactor, assert equality after). `tests/test_runtime/`
  stays green; add a `TargetEngine` backend-parity test (hf vs sglang produce the
  same captured features on a fixed prompt set).
- **Done when** The EAGLE3 name no longer appears in the target ABC or the
  trainer-facing API, `sglang_server` is selectable, and the byte-identical gate
  passes.

---

### C β€” Colocated lightweight path Β· size M Β· CPU Β· status: in review (PR #636, validated)
- **Goal** Make colocated runs (W1/W2) pay nothing for the disagg control plane,
  on the *same* code path β€” not a fork.
- **Target state** One canonical path. For colocated, the control plane is
  opt-in / no-op: `LocalFeatureStore` over `mem://`, no SQLite metadata store, no
  lease / ack / backpressure. Online / offline / disagg differ only in (ref
  source + FeatureStore), shielded from training.
- **Implementation**
  - `control_plane/` β€” make the metadata store (`metadata_store.py`), leasing
    (`controller.py`), and backpressure (`backpressure.py`) selectable as no-op
    implementations rather than required collaborators. Drive the choice from a
    `DeploymentMode` (`contracts.py` already defines
    `local_colocated` / `dataflow_colocated` / `disaggregated`).
  - `launch.py` β€” in the colocated builders, pass the no-op control plane and the
    `mem://` `LocalFeatureStore` (`data_plane/feature_store.py`); the
    `TrainerController.ack_fn` is `None` (already supported β€” the loader is
    assumed to ack) so the durable ack transaction is skipped.
  - Keep the trainer/loader code identical between colocated and disagg.
- **Scope note (as landed, #636)** The no-op axis is the *(metadata store +
  durable-ack transaction)* β€” the pieces with real I/O/txn cost. Prompt leasing
  and the in-process `SampleRefQueue` bookkeeping stay shared across modes
  (single-process lock-guarded dict/deque ops, no I/O; forking them per mode
  would reintroduce the divergence this phase removes), and backpressure was
  already opt-in (`None` default) everywhere. Consequences of a store that
  retains nothing are documented on `NoOpMetadataStore` (`status()` counts read
  0; no `TrainLease` ref reconstruction). `deployment_mode` is selectable on the
  offline builder; the colocated ONLINE builder stays pinned to
  `local_colocated` because its rank-private queue is fed by commit dedup β€” a
  shared durable store there belongs to the disagg online builders.
- **Tests / gates** A **colocated == disagg numerical-equivalence gate**: same
  seed + fixed feature set produce bit-identical loss curves through the one
  builder (`build_offline_runtime(deployment_mode=...)`), no-op colocated plane
  vs full disagg plane over SQLite, with the durable ack marker asserted.
- **Done when** Colocated runs with zero metadata-store/durable-ack overhead
  (leasing/backpressure per the scope note) and the equivalence gate is green.

---

### D β€” Training managers Β· size L Β· GPU Β· status: in review (PR #637, validated)
- **Goal** Bring the training loop up to production parity: real grad
  accumulation, full resume, checkpoint lifecycle, and an evaluator β€” the pieces
  `runtime/training` does not have yet.
- **Target state**
  - **Grad accumulation with `no_sync()`.** Today `runtime/training` has ZERO of
    this: `TrainerCore` honors `accumulation_steps` for the optimizer boundary,
    but `FSDPTrainingBackend.backward` is a bare `loss.backward()`, so FSDP
    all-reduces gradients **every micro-step**. Add `no_sync()` over the
    non-boundary micro-steps so the reduction fires once per optimizer step.
  - **Full resume.** `TrainerController.save_checkpoint` persists only
    `draft_state_dict` + step/epoch; `FSDPTrainingBackend.load_state_dict` only
    half-loads the optimizer. Add optimizer + LR-scheduler + RNG state β€” per
    rank (they are FSDP-shard-local) β€” plus the mid-epoch data position, and a
    seek()-equivalent on the offline stream, so a resumed run continues on the
    same data with the same state. *Continuity precision (as landed, #637):*
    draft weights restore bit-for-bit and the data stream repositions exactly;
    the loss curve is tolerance-continuous, not bit-exact, because
    `BF16Optimizer` rebuilds its fp32 master from the persisted bf16 weights
    (matches the legacy trainer; persisting the master would change the
    optimizer itself and is out of scope here).
  - **CheckpointManager** β€” rotation (keep-last-N), `best` (by eval metric) and
    `latest` symlinks; owns the `output_dir` layout the controller writes today.
  - **Evaluator** β€” `simulated_acc_len` and per-position acceptance, with
    per-position accuracy aggregated **before** the geometric sum (the eagle3
    `acces` / `acc_corrects` / `acc_denoms` already flow through
    `StepOutput.metrics`; `TrainerController.evaluate` currently just means
    scalar metrics, which is wrong for acc-len).
- **Implementation**
  - `training/backend.py` β€” `FSDPTrainingBackend.backward(loss, *, is_boundary)`
    wrapping `self.module.no_sync()` on non-boundary micro-steps; extend
    `state_dict` / `load_state_dict` to round-trip optimizer + scheduler + RNG.
  - `training/trainer.py` β€” `TrainerCore` passes the boundary flag into
    `backward`; `TrainerController` gains scheduler stepping and a real
    `evaluate` that aggregates per-position correct/denom across batches before
    computing acc-len. Replace the inline `save_checkpoint` body with a
    `CheckpointManager`.
  - New `training/checkpoint.py` (`CheckpointManager`) and `eval/evaluator.py`
    (`Evaluator`) β€” see [../../plan.md](../../plan.md) Β§4 (`training/checkpoint.py`,
    `eval/evaluator.py`, `eval/cache.py`).
- **Tests / gates** `tests/test_runtime/test_checkpoint_resume.py` extended to
  assert loss-curve continuity across a save→resume boundary (optimizer + RNG);
  a `no_sync` gate asserting one all-reduce per optimizer step (and identical
  grads to per-step reduction); an evaluator test asserting per-position
  aggregation precedes the geometric sum.
- **Done when** A run can be killed and resumed continuously (weights + data
  position exact; loss within the documented bf16 fp32-master tolerance) at the
  same world size, accumulation reduces once per optimizer step, checkpoints
  rotate with best/latest surviving restarts, and the evaluator reports correct
  `simulated_acc_len` with all metrics DP-reduced.

---

### E0 β€” Layout consolidation (move-only) Β· size M Β· CPU Β· status: in review
- **Goal** Collapse the scattered execution code into **one implementation home per concern** with
  **zero functional change**, so E's composition work is written in the final layout rather than
  re-moved afterward. This is the Β§2.3 target tree in [../../plan.md](../../plan.md): `runtime/` =
  substrate only; top-level `training/` and `inference/` are the single execution homes; `modeling/`
  is model definitions only; no facade package.
- **Target state** `runtime/` contains only `contracts.py` + `control_plane/` + `data_plane/`.
  Every training symbol resolves from top-level `training/`, every rollout/capture symbol from
  top-level `inference/`.
- **Implementation** β€” pure `git mv` + import fixes, one reviewable **rename-only** diff:

  | From (today) | To (consolidated) |
  |---|---|
  | `runtime/training/trainer.py` (`TrainerCore`+`TrainerController`) | `training/controller.py` (kept as one file β€” not split) |
  | `runtime/training/backend.py` | `training/backend.py` |
  | `runtime/training/strategy.py` | `training/strategies/base.py` |
  | `runtime/training/registry.py` | `training/strategies/registry.py` |
  | `specforge/training/trainer.py` (B3 domain `Trainer`) | stays `training/trainer.py` |
  | `modeling/target/base.py` Β· `factory.py` | `inference/target_engine/base.py` Β· `factory.py` |
  | `modeling/target/{eagle3,dflash}_target_model.py` | `inference/target_engine/` (relocated as-is; **collapsed to per-backend engines in E, not here**) |
  | `modeling/target/sglang_backend/capture.py` | `inference/target_engine/sglang_capture_backend.py` |
  | `runtime/inference/rollout_worker.py` Β· `capture.py` | `inference/rollout_worker.py` Β· `capture.py` |
  | `runtime/inference/{sglang,dflash}_adapter.py` | `inference/adapters/{eagle3,dflash}.py` |
  | `runtime/launch.py` | `launch.py` (top-level; topology assembly only) |
  | `modeling/target/{target_head.py, custom_backend/}` | **stays** in `modeling/target/` (model defs) |

  Keep import shims at the old module paths for one release (re-export from the new location) so
  in-flight branches and legacy scripts don't break.
- **Tests / gates** The full `tests/test_runtime` suite stays green **unchanged** (only import paths
  move); the Phase-B byte-identical gate (`test_phase_b_gate.py`) still passes bit-for-bit β€” a
  move-only change must not alter a single tensor or loss value.
- **Done when** `runtime/` is substrate-only (`contracts.py` + `control_plane/` + `data_plane/`);
  every training/inference symbol resolves from its top-level home; suite + B-gate green; no
  functional diff.

---

### E β€” Composition & run surface Β· size L Β· GPU Β· status: later (after E0)
- **Goal** A real product surface, built **on the consolidated layout from `E0`**: a
  draft-architecture registry (separate axis from the strategy registry), the (algorithm Γ— backend)
  target-engine collapse, MLA Eagle3, a typed config + CLI, and exporters.
- **Target state**
  - **`DRAFT_REGISTRY` / `@register_draft`** for draft *architecture* classes
    (`modeling/draft/`), a **distinct axis** from the per-algorithm strategy
    registry. Today there is neither: draft model classes live ungoverned in
    `specforge/modeling/draft/` (`base.py`, `dflash.py`, `llama3_eagle.py`,
    `flex_attention.py`). Add a `modeling/draft/registry.py`; the strategy registry stays in
    top-level `training/strategies/` (relocated in `E0`) so the two registries (architecture vs
    algorithm) are cleanly separate.
  - **Collapse the (algorithm Γ— backend) engine matrix** in `inference/target_engine/`: the
    per-algorithm files relocated by `E0` converge into per-backend generic engines
    (`hf.py` / `sglang.py` / `custom.py`) parameterized by a per-algorithm `CaptureSpec` /
    capture-policy, so adding an algorithm is a spec, not a class-per-backend. NB the SGLang side
    merges cleanly (shared backend; only `wrap_eagle3_logits` + returned fields + shaping differ),
    but the **HF side is a real code difference** (eagle3 = forward hooks on 3 aux layers + concat +
    logits; dflash = `output_hidden_states=True` + layer select, no logits) β€” so it MUST be a policy
    object, not a data table.
  - **MLA Eagle3 draft** β€” an MLA-attention draft architecture registered via
    `@register_draft`. *(The draft itself landed early as PR #640 on the Auto* mapping;
    E re-registers it through `@register_draft`.)*
  - **Pydantic config + `specforge` CLI** β€” a typed run config (Pydantic is
    already a dep in `pyproject.toml`) replacing the argparse-style launch knobs;
    a console-script entry point that builds the config and calls the domain
    `Trainer` (from B).
  - **Exporters** β€” `export/to_sglang` (with a documented MLA weight-name map)
    and `export/to_hf`.
- **Implementation**
  - `modeling/draft/registry.py` (new) β€” `DRAFT_REGISTRY` + `register_draft`
    decorator; register the existing eagle3 / dflash drafts and the new MLA
    eagle3 draft. See [../../plan.md](../../plan.md) Β§4.2.
  - `inference/target_engine/` β€” collapse the `eagle3.py` / `dflash.py` per-algorithm files
    (relocated in `E0`) into `hf.py` / `sglang.py` / `custom.py` + a per-algorithm `CaptureSpec`;
    `sglang_server.py` is filled by online O1.3.
  - `training/strategies/` (relocated in `E0`) β€” finalize the `StrategySpec` registry + the
    per-algorithm specs; keep `register_strategy` / `resolve_strategy` import-compatible.
  - `config/schema.py` (new) β€” Pydantic config; `specforge` CLI entry in
    `pyproject.toml` `[project.scripts]`. See [../../plan.md](../../plan.md)
    Β§4.6 (`config/schema.py`).
  - `export/to_sglang.py`, `export/to_hf.py` (new) β€” exporters + the MLA
    weight-name map. See [../../plan.md](../../plan.md) Β§4 (`export/to_sglang.py`).
  - Detailed sketches for all of the above are in
    [../redesign-draft-legacy.md](../redesign-draft-legacy.md) (legacy redesign
    draft Β§4.2–4.8).
- **Tests / gates** A round-trip export test (train β†’ `to_sglang` β†’ load in
  sglang serving β†’ speculative decode runs); a `to_hf` load test; an MLA-eagle3
  draft training smoke test; a CLI/config test (config parses β†’ builds the same
  `Trainer` the programmatic path does).
- **Done when** A user trains via `specforge <config>`, registers a new draft
  architecture with `@register_draft`, trains an MLA Eagle3 draft, and exports a
  checkpoint that loads in both sglang and HF β€” all from the consolidated layout.