File size: 32,095 Bytes
13c5606
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
# SpecForge Architecture Plan (Reconciled)

> **Status**: Draft β€” reconciles the original "SpecForge Redesign Plan" (a from-scratch,
> torchrun-native, HTTP-only target architecture) with the **landed DataFlow `runtime/`
> spine** (control/data/inference/training planes, SampleRef + FeatureStore + Mooncake).
>
> **Supersedes** the previous redesign draft. That draft's detailed component sketches
> (CheckpointManager, Evaluator, Pydantic config, MLA draft, SGLang export, train-with-decode)
> remain valid as the **domain-layer target** and are preserved verbatim in
> [`docs/redesign-draft-legacy.md`](docs/redesign-draft-legacy.md); this document
> re-frames *where they sit* now that the DataFlow runtime exists, and corrects the one bet
> the original draft got wrong for our actual workloads (see Β§6).

---

## 0. TL;DR β€” the decision

There were two architecture efforts in flight for the same goal (organize SpecForge so new
draft models / training modes / disaggregation compose cleanly without a `train_XXX.py` per
method):

- **The redesign draft** (this doc's predecessor): clean domain decomposition
  (`models/drafts` registry, `TargetEngine`, `HiddenStateStream`, `DraftTrainStrategy`,
  `Trainer`, config/eval/export/cli) β€” but **explicitly torchrun-native with no
  Ray/Mooncake**; disaggregation was "the target is an HTTP service" (`RemoteStream`).
- **The DataFlow `runtime/` spine** (landed: the `dataflow-up-1..20` stack, M1–M7/O1): a
  metadata-only **control plane** + a tensor-carrying **data plane** (`SampleRef` +
  `FeatureStore` incl. **Mooncake** + `StreamingRefChannel` + durable metadata + backpressure
  + resume), with disaggregated producer/consumer and an interleaved online loop.

**The fact that settles it:** there is a real **multi-node / >100 GB/s / isolated-pool**
requirement (separate rollout and trainer pools, no shared FS / no viable HTTP path for the
hidden-state volume). That **breaks the original draft's central bet** ("HTTP/gRPC to SGLang
is sufficient; Mooncake only matters >100 GB/s which we never hit"). So the runtime's
Mooncake control/data plane is **necessary, not premature**.

**Decision: do not pick one β€” layer them.**

- **Canonical substrate = the DataFlow `runtime/` control + data plane.** `SampleRef` (metadata
  on the control plane) + `FeatureStore` (tensors, Local/SharedDir/Mooncake) + `FeatureDataLoader`
  β†’ `TrainBatch` is the canonical data path. online / offline / disaggregated converge at
  `SampleRef`.
- **Front-end = the redesign draft's domain layer.** `TargetEngine` / `DraftTrainStrategy` /
  `Trainer` / `CheckpointManager` / `Evaluator` / config / eval / export / cli become the
  user-facing surface. The **canonical "stream" is `FeatureDataLoader` over `SampleRef` +
  `FeatureStore`** β€” we do **not** re-introduce a `HiddenStateStream` as a separate source of
  truth (the loader already plays that role). The online/offline/disaggregated variation lives
  in the *ref source* (`OfflineManifestReader` / `RolloutWorker` / `StreamingRefChannel`) + the
  *transport* (`FeatureStore`: Local for colocated, Mooncake/SharedDir for isolated pools), and
  is shielded from training by `FeatureDataLoader β†’ TrainBatch`. (`RemoteStream`-over-HTTP is
  just one more ref source / engine β€” the light cross-node option, not the base abstraction.)

This is, in the predecessor's own terms, pulling **feature #32 ("Mooncake streaming backend")
forward from Phase 7 to now** because the requirement demands it β€” and implementing it with the
already-built `runtime/` work, behind the clean domain interfaces.

**Two scope decisions that bound this plan** (consolidating with the online-disaggregation
roadmap, #618):

1. **Frozen target β€” no weight sync.** "Train-with-decode" means a *frozen* target streams
   hidden states; the draft is never in the generation loop. Weight sync / hot draft-update /
   weight-version registry / on-policy are **out of scope**; `draft_weight_version` is provenance
   only. (This removes the predecessor's W4 weight-lifecycle workload.)
2. **Ray is an open decision, not a non-goal.** It is a *candidate* for the O2 scale-out
   orchestrator (multi-node N-producer/M-trainer) β€” likely necessary, but not committed. Until the
   decision gate fires we keep the home-grown metadata-only control plane and add nothing.

**Per-phase target/implementation detail lives in [`docs/roadmap/`](docs/roadmap/)** (domain,
online-disaggregation [folds in #618], eval & breadth); this document is the architecture; the
roadmap is the build order.

---

## 1. Why each side is right (and where each is incomplete)

Two orthogonal slicings. The runtime is sliced by **plane** (how bytes/metadata move); the
draft is sliced by **domain object** (draft / target / stream / strategy). They compose; they
do not conflict.

| Layer | Verdict | Rationale |
|---|---|---|
| **control plane** (`runtime/control_plane`) | **Keep β€” canonical. No predecessor equivalent.** | Metadata-only (every entrypoint runs `assert_no_tensors`), lease/ack/dedup/reconcile/backpressure behind a `MetadataStore` seam. This is exactly the cross-pool machinery the isolated-pool requirement needs, and the HTTP design has nothing like it. |
| **data plane** (`runtime/data_plane`) | **Keep β€” canonical; it *is* the stream.** | `SampleRef` (metadata) Γ· `FeatureStore` (tensors) Γ· `FeatureDataLoader` (materialize) is a *finer* decomposition than the predecessor's coarse `HiddenStateStream`, and it's the better substrate for backpressure / lease-ack / resume / Mooncake. `FeatureDataLoader` over `SampleRef`+`FeatureStore` already plays the role plan.md gave `HiddenStateStream`, so **no separate `HiddenStateStream` source-of-truth is introduced** β€” `seek()`/prefetch land on the loader/ref-source; `FeatureStore` is the transport swap point. |
| **inference plane** (`runtime/inference`) | **Converge to the predecessor's `TargetEngine`.** | Today it conflates "wrap a target engine" + "capture" inside `SGLangAdapter`, and is bound to `generate_eagle3_data` / EAGLE3 names. The `FeatureSource` Protocol already exists (good); the gap is a real `TargetEngine` abstraction (hf / sglang / **sglang_server** / custom) + de-EAGLE3-ifying. This is the layer that most borrows the draft. |
| **training plane** (`runtime/training`) | **Keep the seam; fill the managers.** | `DraftTrainStrategy` / `TrainerCore` / `TrainingBackend` (+ the `StepContext` added for Domino) is *already* the predecessor's `training/strategies` shape β€” keep it. What's missing are the **managers**, not the seam: `no_sync()` accumulation, full optimizer/scheduler/RNG resume, `CheckpointManager` (rotation/best), `Evaluator`. |

**Landed (merged β€” the spine):**

- `runtime/` planes (M1–M7/O1): control/data/inference/training, `SampleRef`, `FeatureStore`
  (Local/SharedDir/Mooncake), `StreamingRefChannel`, durable `MetadataStore`, disagg
  producer/consumer + interleaved online loop.

**In the stacked PRs #627/#628/#629 (validated, in review β€” not yet merged):**

- **Composable launch** (`StrategySpec` registry + parameterized `launch.py`): adding a model
  is a spec entry, not a `build_*_runtime` family. eagle3 / **dflash** / **domino** all train
  end-to-end through one strategy-parameterized path (validated: 197 `tests/test_runtime` OK on
  H200). Domino added `StepContext{global_step, total_steps}` threaded through `forward_loss` β€”
  the one deliberate contract extension for schedule-dependent loss.

**Explicitly not yet implemented in `runtime/`** (flagged in-source β€” `contracts.py`,
`trainer.py`, `controller.py`, both `DESIGN.md`s, `runtime/README.md`): live frozen-target online
capture from a real SGLang server (O1.3), full optimizer/scheduler resume, and `no_sync()`
accumulation. These are the Β§3 gaps. (The in-source weight-publication NOTEs β€” `WeightVersion` /
`WeightPublisher` / `update_draft_weights` β€” are **descoped**: the target is frozen, see Β§8.)

---

## 2. Target architecture

### 2.1 Canonical data path (unchanged from the runtime, this is the spine)

```
ref source            ─┐
  OfflineManifestReader β”‚  (offline: re-iterable refs)
  RolloutWorker         β”‚  (online: produced into a SampleRefQueue)
  StreamingRefChannel   β”‚  (disaggregated: cross-process/pool stream)
                        β–Ό
DataFlowController  ── metadata only (SampleRef), assert_no_tensors
                        β”‚
FeatureStore  ── tensors only: LocalFeatureStore (mem://, colocated)
                              SharedDirFeatureStore (shared mount)
                              MooncakeFeatureStore (RDMA, isolated pools)
                        β–Ό
FeatureDataLoader (per_sample_transform + collate) ─► TrainBatch
                        β–Ό
DraftTrainStrategy.forward_loss(batch, ctx)  ─► TrainerCore / TrainerController ─► FSDP
```

### 2.2 Domain layer on top (user-facing abstractions over the substrate)

The substrate's source of truth is `SampleRef` + `FeatureStore` + `FeatureDataLoader` (Β§2.1).
The domain layer does **not** re-introduce a `HiddenStateStream` as a parallel source of truth β€”
`FeatureDataLoader` already *is* the stream. The domain layer is what user/training code sees:

```
PRODUCE (inference):
  TargetEngine        ── the engine rollout wraps to produce features; the abstraction that
                         replaces the EAGLE3-bound SGLangAdapter.
     HFTargetEngine / SGLangTargetEngine / CustomTargetEngine (in-process)
     SGLangServerEngine (frozen target as a live SGLang server, cross-node). ONE engine, two
       feature transports (the engine is identical; only WHERE features land differs):
         Β· capture transport β€” engine-side hook writes hidden states INTO a FeatureStore
           (Mooncake/SharedDir). This is W3 / online O1.3.
         Β· inline-HTTP transport β€” features serialized in the HTTP response, no shared store
           (RemoteStream-style). This is the light W3β€² path, the one case features do NOT live
           in a FeatureStore.
  RolloutWorker       ── drives a TargetEngine β†’ writes tensors to FeatureStore β†’ commits
                         SampleRef to the control plane. Stays at the domain↔substrate seam.

CONSUME (training):
  Trainer             ── owns the lifecycle: loop / eval / checkpoint. WRAPS the
                         TrainerCore/TrainerController seam; does NOT replace it. (No weight-sync β€”
                         the target is frozen, Β§8.)
  DraftTrainStrategy  ── per-algorithm forward+loss (eagle3 TTT / dflash block / domino). The
                         per-step seam; lives in training/strategies (relocated from
                         runtime/training in E0) β€” already plan-shaped.
  CheckpointManager / Evaluator / lr_scheduler / fsdp seam  ── the managers (G1).

COMPOSE:
  modeling/draft      ── DRAFT_REGISTRY (@register_draft) for draft *architecture* classes
                         (llama / deepseek-MLA / dflash-qwen3). Separate axis from strategy.
  StrategyRegistry    ── per-algorithm spec (today's StrategySpec, converged here).
  config / cli / export ── first-class run surface (predecessor Β§4.3–4.8, carried forward).
```

The online / offline / disaggregated distinction is **not visible to `Trainer`/strategy** β€” it
is fully absorbed by (ref source + `FeatureStore`) behind `FeatureDataLoader β†’ TrainBatch`.

### 2.3 Module layout (runtime spine + domain layer)

```
specforge/
β”œβ”€β”€ runtime/                         # SUBSTRATE ONLY β€” the canonical DataFlow spine (keep as-is)
β”‚   β”œβ”€β”€ contracts.py                 # SampleRef, TrainBatch, PromptTask, *Strategy literal
β”‚   β”œβ”€β”€ control_plane/               # metadata-only: controller, metadata_store, backpressure
β”‚   └── data_plane/                  # FeatureStore (Local/SharedDir/Mooncake), FeatureDataLoader,
β”‚                                    #   SampleRefQueue, StreamingRefChannel, offline/disagg readers
β”‚
β”œβ”€β”€ inference/                       # TOP-LEVEL β€” the single home for all rollout/capture execution
β”‚   β”œβ”€β”€ rollout_worker.py            #   (from runtime/inference/rollout_worker.py)
β”‚   β”œβ”€β”€ capture.py                   #   CaptureConfig (from runtime/inference/capture.py)
β”‚   β”œβ”€β”€ feature_source.py            #   the FeatureSource Protocol (the worker's only contract)
β”‚   β”œβ”€β”€ target_engine/               #   TargetEngine, EXTRACTED from modeling/target/*TargetModel
β”‚   β”‚   β”œβ”€β”€ base.py  factory.py       #     (adapters then wrap an engine, Β§G2)
β”‚   β”‚   β”œβ”€β”€ hf.py  sglang.py  sglang_server.py  custom.py   # per-backend generic engines
β”‚   β”‚   └── sglang_capture_backend.py                       # sglang version-pinned glue
β”‚   └── adapters/
β”‚       └── eagle3.py  dflash.py     #   (from runtime/inference/{sglang,dflash}_adapter.py)
β”‚
β”œβ”€β”€ training/                        # TOP-LEVEL β€” the single home for all training execution (NO facade)
β”‚   β”œβ”€β”€ trainer.py                   # domain Trainer: owns loop/eval/checkpoint (B3)
β”‚   β”œβ”€β”€ controller.py                # TrainerCore + TrainerController seam (from runtime/training/trainer.py)
β”‚   β”œβ”€β”€ backend.py                   # FSDPTrainingBackend + no_sync (from runtime/training/backend.py)
β”‚   β”œβ”€β”€ checkpoint.py  resume.py  evaluator.py   # NEW managers (Β§3) β€” born here
β”‚   └── strategies/                  # StrategySpec registry + per-algorithm strategies (converges HERE)
β”‚       β”œβ”€β”€ base.py                   #   DraftTrainStrategy ABC + StepContext (from runtime/training/strategy.py)
β”‚       β”œβ”€β”€ registry.py               #   StrategySpec / resolve_strategy (from runtime/training/registry.py)
β”‚       └── eagle3.py  dflash.py  domino.py
β”‚
β”œβ”€β”€ modeling/                        # MODEL DEFINITIONS ONLY β€” no orchestration, no capture factory
β”‚   β”œβ”€β”€ draft/                       # DRAFT_REGISTRY + @register_draft (NEW registry.py β€” predecessor Β§4.2)
β”‚   β”‚   └── base.py  registry.py  llama3_eagle.py  deepseek_eagle3.py(MLA)  dflash.py  flex_attention.py
β”‚   └── target/                      # target model nn.Modules + model-specific glue ONLY
β”‚       └── target_head.py  custom_backend/  sglang_backend/   # (capture orchestration moved to inference/)
β”‚
β”‚                                    # (NO separate data/streams package β€” FeatureDataLoader over
β”‚                                    #  SampleRef+FeatureStore IS the stream. Ref sources
β”‚                                    #  (offline/rollout/streaming) live in runtime/data_plane;
β”‚                                    #  live frozen-target capture is just another ref source)
β”‚
β”œβ”€β”€ launch.py                        # TOP-LEVEL β€” topology assembly only (spec-driven builders; from runtime/launch.py)
β”œβ”€β”€ eval/  export/  config/  cli.py  # NEW β€” carried forward from predecessor Β§4.4–4.8
└── core/  optimizer.py  tracker.py  distributed.py  lr_scheduler.py  # kept verbatim (predecessor Β§1)
```

> **One implementation home per concern; `runtime/` is substrate-only.** The two structural moves
> (both gated by the byte-identical suite, executed as the move-only step `E0` β€” see
> [docs/roadmap/domain-refactor.md](docs/roadmap/domain-refactor.md)): (1) extract `TargetEngine`
> out of the EAGLE3-bound `modeling/target/*TargetModel` into top-level `inference/target_engine/`
> (the `adapters/` then wrap an engine); (2) lift the training seam
> (`TrainerCore`/`TrainerController`/`TrainingBackend`/strategy+registry) up into top-level
> `training/`. `runtime/` shrinks to the substrate (control + data plane + contracts) β€” it is
> **not** a home for domain/algorithm code, and there is **no facade package**. The control + data
> planes stay exactly where they are; the online/offline/disaggregated distinction stays absorbed
> by (ref source + `FeatureStore`) behind `FeatureDataLoader`, invisible to `training/`.

---

## 3. Gaps to close (prioritized) β€” what the domain layer adds on top of the spine

These are the items the landed `runtime/` does **not** have, drawn from the predecessor and
from the in-source `NOTE`s. Each lands behind the canonical spine without re-plumbing it.

### G1 β€” Training managers (highest leverage; the spine works but is bare)
- **`no_sync()` accumulation.** `runtime/training` has **zero** `no_sync()` β€” FSDP all-reduces
  every micro-step, defeating `accumulation_steps`. (Predecessor Β§4.2 #10; verify with one
  profiler check: one all-reduce per `optimizer.step()`.)
- **Full resume.** `save_checkpoint` persists training state only; no optimizer/scheduler/RNG
  restore, no rotation, no best-tracking. Add `CheckpointManager` (predecessor Β§4.3) + a
  `seek()`-equivalent on the colocated streams.
- **`Evaluator` + `EvalCache`.** No `simulated_acc_len` / per-position-acc / best-checkpoint.
  (Predecessor Β§4.4 β€” per-position acc aggregated across batches *before* the geometric sum.)
- **lr WSD + per-strategy `fsdp_wrap_policy()`** (predecessor Β§4.6, Β§4.2).

### G2 β€” `TargetEngine` abstraction (inference convergence)
- Introduce a `TargetEngine` ABC **extracted from the `modeling/target/*TargetModel` classes**
  (`Eagle3TargetModel`, `DFlashTargetModel`); the `runtime/inference` adapters
  (`SGLangAdapter`/`DFlashAdapter`) then **wrap** a `TargetEngine` rather than being the engine,
  and stop binding to `generate_eagle3_data` / EAGLE3 names. Keep the existing `FeatureSource`
  Protocol. Add `SGLangServerEngine` (live SGLang server) as the cross-node engine β€” see Β§2.2 for
  its two feature transports (capture-into-FeatureStore for W3/O1.3, inline-HTTP for the light
  W3β€²).

### G3 β€” Live online capture (frozen target; **no** weight sync)
- Replace the in-process generator with **live SGLang-server hidden-state capture**: a *frozen*
  target streams aux+final hidden states into `MooncakeFeatureStore`; the producer commits
  `SampleRef`s. The cross-process control plane + async loop are in-review; live capture (the
  gating spike) is next; scale-out (Ray = **open**) and hardening follow.
- **Weight sync / hot draft-update / on-policy are explicitly out of scope.** The target is
  frozen, so the streamed data is independent of draft weights β€” there is nothing to re-sync and
  no staleness. `draft_weight_version` is kept **only as provenance**.
- Detailed phases (O1–O3): [`docs/roadmap/online-disaggregation.md`](docs/roadmap/online-disaggregation.md).

### G4 β€” Composition + models (predecessor Phase 1)
- `modeling/draft` `DRAFT_REGISTRY` (`@register_draft`) for draft **architectures** (separate
  axis from strategy); **MLA Eagle3 draft** (deepseek/Kimi); converge `StrategySpec` β†’
  `training/strategies` registry. Note: draft-arch registry and strategy registry are two
  registries, not one.

### G5 β€” Run surface (predecessor Phase 3)
- Pydantic `config/` + `specforge` CLI; `export/to_sglang` (+ the documented MLA weight-name
  map) and `export/to_hf` with vocab pruning.

---

## 4. Topologies & the colocated lightweight path

The workloads differ only in which *ref source* + `FeatureStore` + `TargetEngine` compose. All
consume **one** `FeatureDataLoader β†’ TrainBatch` iterator; the trainer/strategy/backend are
identical regardless (no per-topology stream class). **In every case the target is frozen** β€” the
draft is never in the generation loop, so the streamed features do not depend on draft training
progress (no staleness, nothing to re-sync).

| # | Workload | Ref source (β†’ FeatureDataLoader) | FeatureStore | Control plane |
|---|---|---|---|---|
| W1 | Offline (precomputed) | `OfflineManifestReader` (refs) | Local (`file://`/`mem://`) | **no-op** |
| W2 | In-process online (frozen target) | `RolloutWorker` β†’ `SampleRefQueue` | Local (`mem://`) | **no-op** |
| W3 | Disaggregated online (frozen target; isolated pools, high BW) | `RolloutWorker` (producer pool) β†’ `StreamingRefChannel`, `SGLangServerEngine` *capture transport* | **Mooncake** | **active** (lease/ack/reconcile/backpressure) |
| W3β€² | Disaggregated online (light/cross-node) | `SGLangServerEngine` *inline-HTTP transport* (RemoteStream-style source) | n/a β€” features inline over HTTP, no shared store | minimal |

> **"Train-with-decode" = live *frozen-target* generation** β€” i.e. W2 (colocated) or W3
> (disaggregated), **not** a separate workload. The predecessor's dual-purpose
> serve-and-push-weights "W4" is **out of scope**: a frozen target means there is nothing to push.
> See [`docs/roadmap/online-disaggregation.md`](docs/roadmap/online-disaggregation.md).

**Colocated lightweight path (W1/W2): keep it, but as a *no-op control plane*, not a fork.**
The control-plane machinery (lease/ack/reconcile/backpressure/durable metadata/cross-process
transport) only earns its keep when producer and consumer are decoupled. For W1/W2 (same
process) it is pure overhead. Resolution:

- **Single canonical path** through `SampleRef` + `FeatureStore`, so colocated and disaggregated
  produce **byte-identical batches for free** (today's property β€” "disaggregation changes *where*
  features live, not their values"). We do **not** fork a second trainer/launch path.
- "Lightweight" = for colocated, the controller's lease/ack/metadata-store are **opt-in / no-op**,
  `FeatureStore` is `LocalFeatureStore(mem://)`, and there is **no SQLite, no cross-process, no
  backpressure**. The colocated ref sources (`OfflineManifestReader` / in-process `RolloutWorker`)
  flow through the spine but skip the heavy bits.
- Cost to own honestly: this preserves the free byte-identical guarantee **only if** we keep one
  path. A numerical-equivalence gate (Β§7) locks "colocated stream output == disagg stream output"
  so the no-op path can never silently diverge.

---

## 5. What changed from the predecessor draft (explicit deltas)

1. **"No Mooncake in Phase 1-6" β†’ revised.** The Mooncake control/data plane is now the
   **canonical** disaggregation backend, because the isolated-pool / >100 GB/s requirement is
   real and HTTP cannot serve it. The predecessor's `RemoteStream`-over-HTTP remains as the
   **light** second backend (W3β€²). Mooncake is used as a *transport*; the scale-out
   **orchestration** layer (Ray vs. home-grown) is an **open decision** (see Β§6, Β§8 and
   [`docs/roadmap/online-disaggregation.md`](docs/roadmap/online-disaggregation.md) Β§O2), no longer
   a flat non-goal.
2. **Primary abstraction / `HiddenStateStream` dropped.** Predecessor: a coarse
   `HiddenStateStream` produces `TrainBatch` and is the source of truth. Reconciled:
   `SampleRef` + `FeatureStore` + `FeatureDataLoader` is canonical, and `FeatureDataLoader`
   **already is** the stream β€” so `HiddenStateStream` is **not introduced** as a parallel
   abstraction. Topology variation lives in (ref source + `FeatureStore`); `seek()`/prefetch
   land on the loader/ref-source. This is strictly finer and is what makes
   lease/ack/backpressure/Mooncake clean.
3. **`runtime/` is not "thrown away."** The predecessor's "what we throw away" never mentioned
   `runtime/` (it predated it). The DataFlow spine is the foundation, not debt.
4. **Module layout** gains a top-level `runtime/` (the spine) under the domain layer; `models/`
   (drafts + targets), `training/` (Trainer + managers), `eval/`, `export/`, `config/`, `cli.py`
   are the layer on top. No separate `data/streams/` package β€” the loader is the stream.
5. **W4 weight lifecycle β†’ dropped.** The predecessor's serve-and-push-weights workload + weight
   registry are **out of scope**. "Train-with-decode" is *frozen-target* live generation (W2/W3);
   weight sync / hot draft-update / staleness gate / on-policy are explicitly cut, aligning with
   #618. `draft_weight_version` survives only as provenance metadata.
6. **Ray reframed from non-goal β†’ open.** It is a *candidate* for the O2 scale-out orchestrator
   (multi-node N-producer/M-trainer), not committed and not forbidden; the decision gate lives in
   the roadmap.

Everything else in the predecessor (workloads W1–W3, the domain abstractions, the testing
discipline, the MLA/export/eval/config detail) **carries forward unchanged**.

---

## 6. Tradeoffs (updated)

### Mooncake transport vs HTTP/gRPC β€” *transport bet reversed*
The predecessor bet "HTTP is sufficient, Mooncake is gated behind profiling." For the **data
transport** that bet is now wrong (isolated-pool / >100 GB/s requirement) β€” Mooncake is in,
canonically. The `RemoteStream`-over-HTTP path stays as the light cross-node backend (W3β€²).

### Scale-out orchestration: Ray vs. home-grown β€” *OPEN*
Today's metadata-only control plane handles a single producer-pool ↔ trainer scope. **O2**
(multi-node N-producer/M-trainer scale-out) needs an orchestration layer, and **whether that is
Ray or a home-grown scheduler is undecided** β€” likely necessary, but not committed. It is **not**
a non-goal anymore; the decision gate (when β‰₯1 of: >1 producer pool, cross-node failover,
multi-job pool sharing) lives in
[`docs/roadmap/online-disaggregation.md`](docs/roadmap/online-disaggregation.md) Β§O2. Until then
we keep the home-grown control plane and add nothing.

### One canonical path vs a light colocated fork
Chosen: **one path** (spine everywhere) + control-plane-as-no-op for colocated. Keeps
byte-identical free and avoids two implementations; costs an equivalence gate. (See Β§4.)

### Two registries, not one
`modeling/draft` `DRAFT_REGISTRY` (architecture) and `training/strategies` (algorithm) are
**separate** axes β€” an algorithm (eagle3) runs on multiple draft architectures. Don't merge
them; the current `StrategySpec` is the *strategy* registry and should converge there, not into
`modeling/draft`.

*(The predecessor's other tradeoffs β€” Pydantic vs OmegaConf, MLA cache compressed vs expanded,
registry vs HF AutoModel β€” are unchanged and carry forward.)*

---

## 7. Migration path (incremental, gated)

The spine is already landed, so migration is "grow the domain layer on top + close the gaps,"
not a rewrite. Every phase that touches the training path passes the numerical-equivalence gate
(below) against the prior commit. **Per-phase target/implementation/tests/done-when detail lives
in [`docs/roadmap/`](docs/roadmap/)** β€” the phases below are the index; the online track there
also folds in the former online-disaggregation roadmap (#618).

- **Phase A β€” composable launch (in review).** `StrategySpec` registry + parameterized
  `launch.py`; eagle3/dflash/domino end-to-end in the stacked PRs #627/#628/#629 (validated,
  in review). 197 `tests/test_runtime` OK.
- **Phase B β€” domain abstractions (no behavior change).** Introduce `TargetEngine` (wrap the
  existing adapters; de-EAGLE3 the names) and the domain `Trainer` wrapping the kept
  `runtime/training` core. No `HiddenStateStream` β€” `FeatureDataLoader` stays the stream. Gate:
  byte-identical batches/loss vs the pre-refactor path.
- **Phase C β€” colocated no-op control plane + equivalence gate.** Make lease/ack/metadata
  opt-in; add the colocated≑disagg equivalence test.
- **Phase D β€” training managers (G1).** `no_sync()`, full optimizer/scheduler/RNG resume,
  `CheckpointManager`, `Evaluator`. Gate: loss/eval parity at fixed steps.
- **Phase E0 β€” layout consolidation (move-only).** Relocate the training seam + target engine
  into the top-level `training/` / `inference/` homes; `runtime/` shrinks to substrate. Pure
  `git mv` + import shims; gate: suite + B byte-identical gate unchanged (no functional diff).
- **Phase E β€” `modeling/draft` registry + MLA draft + config/CLI/export (G4/G5).** Adding a
  draft arch = one decorated file; one validated YAML per run; export-loop test.
- **Online track (parallel; G3) β€” live *frozen-target* capture.** O1.1 shared control plane +
  O1.2 async loop (in review) β†’ **O1.3** live SGLang-server hidden-state capture (next; gated by a
  throughput spike) β†’ **O2** scale-out (orchestrator: Ray = open) β†’ **O3** hardening. **No weight
  sync.** Detail: [`docs/roadmap/online-disaggregation.md`](docs/roadmap/online-disaggregation.md).
- **Eval track (parallel) β€” E1** acceptance-length eval harness β†’ **E2** algorithm breadth (new
  algo = a `StrategySpec` + loss). Detail: [`docs/roadmap/eval-and-breadth.md`](docs/roadmap/eval-and-breadth.md).

Doc debt to fix alongside Phase B: revise the predecessor's "No Mooncake / HTTP is sufficient"
statements (now Β§5/Β§6) so the code and the plan stop contradicting each other.

---

## 8. Non-goals (updated)
- **No weight sync / hot draft-update / on-policy training.** The target is **frozen**; the
  draft is never in the generation loop. No `WeightPublisher`, no weight-version registry, no
  staleness gate. `draft_weight_version` is provenance metadata only.
- **No two-stack fork for colocated.** One canonical data path; colocated is the spine with the
  control plane as a no-op, not a parallel implementation.
- **No vLLM target backend** (possible via `TargetEngine`, not prioritized).
- **No multi-engine load balancing / multi-job inference-pool sharing** for now β€” gated behind
  β‰₯5 concurrent jobs sharing one target.

**Open decisions (NOT non-goals):**
- **Ray (or a home-grown scheduler) for O2 scale-out** β€” likely necessary for multi-node
  N-producer/M-trainer; undecided. Decision gate in Β§6 / the online roadmap.

*(Reversed from the predecessor: "No Mooncake" β†’ Mooncake transport is now canonical for W3;
"No Ray" β†’ Ray is now an open scale-out decision, not a flat non-goal.)*

---

## 9. Success criteria
| Area | Criterion |
|---|---|
| Composable launch (in review) | eagle3/dflash/domino train via one strategy-parameterized path; full `test_runtime` green. |
| Abstractions (B) | `TargetEngine` (wrapping existing adapters) + domain `Trainer` produce byte-identical batches/loss vs the direct spine path. |
| Colocated (C) | W1/W2 run with control plane as no-op; colocated≑disagg equivalence gate passes. |
| Managers (D) | resume reproduces the no-resume loss curve; one all-reduce per optimizer step; best-checkpoint tracked. |
| Drafts/MLA (E) | MLA Eagle3 trains + loads in SGLang; new draft arch = one `@register_draft` file. |
| Online (O1.3) | a live **frozen-target** SGLang server feeds training with zero precomputed features; loss/eval matches the offline baseline on the same prompts+seed. (Scale-out O2 / Ray = open.) |

---

## 10. Testing strategy (gating)

Carries the predecessor's gates, plus the reconciliation-specific one.

- **10.1 Numerical-equivalence gate** (per PR touching `core/` / `training/` /
  `runtime/data_plane/` / `modeling/draft`): fixed seed, 3Γ—4 batches; per-step loss
  `atol/rtol=1e-4` at steps 0/1/100/500/1000; per-position eval acc + `simulated_acc_len`
  `atol=1e-3`.
- **10.2 Colocated ≑ disaggregated gate (NEW):** same data through the colocated (no-op control
  plane, Local store) and the disaggregated (control plane, SharedDir/Mooncake) paths must yield
  identical `TrainBatch`es and identical loss. This is what licenses the lightweight path.
- **10.3 Smoke tests:** each draft arch trains 20 steps under TP=1 / TP=2 / TP=2+SP=2;
  checkpoint save+resume matches no-resume (validates `seek`); eval cache miss==hit.
- **10.4 Distributed correctness:** MLA + Yunchang USP with asymmetric head dims; gradient
  accumulation = one all-reduce per `optimizer.step()`.
- **10.5 Export-loop test:** train MLA draft 100 steps β†’ export β†’ load in SGLang β†’ 32 gens,
  acceptance > 0 (catches weight-name-map regressions).
- **10.6 Online-capture parity gate (O1.3):** a live frozen-target capture run produces
  features + loss matching the offline-precomputed baseline on the same prompts/seed (the target
  is frozen, so this must hold exactly up to nondeterminism tolerance). No weight-sync gate β€”
  weight sync is out of scope.