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. 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,
./eval-and-breadth.md. Top-level plan:
../../plan.md (the detailed §4.2–4.8 sketches this track lands
incrementally are mirrored in ../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.
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 sameTrainerCore. - Target state
eagle3,dflash,dominoall run end-to-end (offline + online) through a single set of topology builders that takestrategy=and resolve aStrategySpec.launch.pygrows astopologies + one spec per model, nevertopologies × models. - Implementation (landed)
training/registry.py—StrategySpecdataclass +register_strategy/resolve_strategy/available_strategies; one entry each foreagle3,dflash,domino. Each spec carriesmake_strategy,required_features, the offline reader/transform/collate triple +offline_target_repr, the online collate (concat_collate), andmake_adapter(None ⇒ defaultSGLangAdapter).launch.py—_assemble_trainer/_offline_io/_assemble_rollout_workersplus topology buildersbuild_offline_runtime,build_disagg_offline_runtime,build_online_runtime,build_disagg_online_{producer,consumer,runtime}; every one resolves the spec. Legacybuild_*_eagle3_*names are kept as aliases.training/strategy.py—DraftTrainStrategyABC +Eagle3TrainStrategy,DFlashTrainStrategy,DominoTrainStrategy;StepContext(global_step/total_steps) is threaded intoforward_lossso domino's_lambda_basedecay reads the schedule without ad-hoc kwargs on the model forward.inference/dflash_adapter.py—DFlashAdapterovergenerate_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
Trainerthat wraps (does not replace) the runtime controller, so the architecture reads in product terms while the seam stays intact. - Target state A backend-agnostic
TargetEngineABC sits where the EAGLE3-namedEagle3TargetModelis today;hf/sglang/sglang_server/customare interchangeable backends behind it. A domainTraineris the caller-facing object; under it theTrainerController/TrainerCore/DraftTrainStrategy/TrainingBackendseam is byte-for-byte unchanged. TheFeatureSourceProtocol inrollout_worker.pystays the worker's only contract. NoHiddenStateStream—FeatureDataLoaderover SampleRef+FeatureStore is the stream. - Implementation
- Extract
TargetEngine(ABC) fromspecforge/modeling/target/eagle3_target_model.py:79(Eagle3TargetModel). De-EAGLE3 the names: the ABC's extraction method becomes a genericgenerate_features(...)-style capture call with EAGLE3 specifics (generate_eagle3_data,aux_hidden_states_layers,set_aux_hidden_states_layers) moved into anEagle3TargetEnginesubclass;DFlashTargetModel(dflash_target_model.py:33,set_capture_layers) becomes a sibling subclass. Keepget_eagle3_target_model(backend=...)working as a thin shim during migration, then rename to a generic factory. - Add an explicit
backendattribute on each engine (todaySGLangAdapter.health/DFlashAdapter.healthreadgetattr(target_model, "backend", "unknown")— make it real), and add thesglang_serverbackend branch to the factory (currently onlysglang/hf/custom). The depth of thissglang_serverbranch is informed by the O1.3 capture spike (see online-disaggregation.md §O1.3): the de-EAGLE3 extraction (step 1) and the domainTrainercarry no engine risk and can land regardless of the spike's outcome — only the live-server capture backend does. - Keep the adapters (
SGLangAdapter,DFlashAdapter) as theFeatureSourceimplementations over the engine; nothing inrollout_worker.pychanges — itsFeatureSourceProtocol (generate_features(tasks, *, capture)) is already the seam. - Introduce a domain
Trainer(newtraining/module) that composesresolve_strategy→FSDPTrainingBackend→TrainerCore→TrainerController(exactly whatlaunch._assemble_trainerwires today) behind one object, and calls.fit(). The controller/core seam is wrapped, not edited.
- Extract
- Tests / gates Byte-identical batches and loss vs the pre-refactor run
for all three strategies (snapshot the first-N
TrainBatch.tensorsdigests + per-step loss before refactor, assert equality after).tests/test_runtime/stays green; add aTargetEnginebackend-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_serveris 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:
LocalFeatureStoreovermem://, 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 aDeploymentMode(contracts.pyalready defineslocal_colocated/dataflow_colocated/disaggregated).launch.py— in the colocated builders, pass the no-op control plane and themem://LocalFeatureStore(data_plane/feature_store.py); theTrainerController.ack_fnisNone(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
SampleRefQueuebookkeeping 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 (Nonedefault) everywhere. Consequences of a store that retains nothing are documented onNoOpMetadataStore(status()counts read 0; noTrainLeaseref reconstruction).deployment_modeis selectable on the offline builder; the colocated ONLINE builder stays pinned tolocal_colocatedbecause 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/trainingdoes not have yet. - Target state
- Grad accumulation with
no_sync(). Todayruntime/traininghas ZERO of this:TrainerCorehonorsaccumulation_stepsfor the optimizer boundary, butFSDPTrainingBackend.backwardis a bareloss.backward(), so FSDP all-reduces gradients every micro-step. Addno_sync()over the non-boundary micro-steps so the reduction fires once per optimizer step. - Full resume.
TrainerController.save_checkpointpersists onlydraft_state_dict+ step/epoch;FSDPTrainingBackend.load_state_dictonly 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, becauseBF16Optimizerrebuilds 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) andlatestsymlinks; owns theoutput_dirlayout the controller writes today. - Evaluator —
simulated_acc_lenand per-position acceptance, with per-position accuracy aggregated before the geometric sum (the eagle3acces/acc_corrects/acc_denomsalready flow throughStepOutput.metrics;TrainerController.evaluatecurrently just means scalar metrics, which is wrong for acc-len).
- Grad accumulation with
- Implementation
training/backend.py—FSDPTrainingBackend.backward(loss, *, is_boundary)wrappingself.module.no_sync()on non-boundary micro-steps; extendstate_dict/load_state_dictto round-trip optimizer + scheduler + RNG.training/trainer.py—TrainerCorepasses the boundary flag intobackward;TrainerControllergains scheduler stepping and a realevaluatethat aggregates per-position correct/denom across batches before computing acc-len. Replace the inlinesave_checkpointbody with aCheckpointManager.- New
training/checkpoint.py(CheckpointManager) andeval/evaluator.py(Evaluator) — see ../../plan.md §4 (training/checkpoint.py,eval/evaluator.py,eval/cache.py).
- Tests / gates
tests/test_runtime/test_checkpoint_resume.pyextended to assert loss-curve continuity across a save→resume boundary (optimizer + RNG); ano_syncgate 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_lenwith 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:
runtime/= substrate only; top-leveltraining/andinference/are the single execution homes;modeling/is model definitions only; no facade package.Target state
runtime/contains onlycontracts.py+control_plane/+data_plane/. Every training symbol resolves from top-leveltraining/, every rollout/capture symbol from top-levelinference/.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.pytraining/backend.pyruntime/training/strategy.pytraining/strategies/base.pyruntime/training/registry.pytraining/strategies/registry.pyspecforge/training/trainer.py(B3 domainTrainer)stays training/trainer.pymodeling/target/base.py·factory.pyinference/target_engine/base.py·factory.pymodeling/target/{eagle3,dflash}_target_model.pyinference/target_engine/(relocated as-is; collapsed to per-backend engines in E, not here)modeling/target/sglang_backend/capture.pyinference/target_engine/sglang_capture_backend.pyruntime/inference/rollout_worker.py·capture.pyinference/rollout_worker.py·capture.pyruntime/inference/{sglang,dflash}_adapter.pyinference/adapters/{eagle3,dflash}.pyruntime/launch.pylaunch.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_runtimesuite 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_draftfor draft architecture classes (modeling/draft/), a distinct axis from the per-algorithm strategy registry. Today there is neither: draft model classes live ungoverned inspecforge/modeling/draft/(base.py,dflash.py,llama3_eagle.py,flex_attention.py). Add amodeling/draft/registry.py; the strategy registry stays in top-leveltraining/strategies/(relocated inE0) 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 byE0converge into per-backend generic engines (hf.py/sglang.py/custom.py) parameterized by a per-algorithmCaptureSpec/ capture-policy, so adding an algorithm is a spec, not a class-per-backend. NB the SGLang side merges cleanly (shared backend; onlywrap_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 +
specforgeCLI — a typed run config (Pydantic is already a dep inpyproject.toml) replacing the argparse-style launch knobs; a console-script entry point that builds the config and calls the domainTrainer(from B). - Exporters —
export/to_sglang(with a documented MLA weight-name map) andexport/to_hf.
- Implementation
modeling/draft/registry.py(new) —DRAFT_REGISTRY+register_draftdecorator; register the existing eagle3 / dflash drafts and the new MLA eagle3 draft. See ../../plan.md §4.2.inference/target_engine/— collapse theeagle3.py/dflash.pyper-algorithm files (relocated inE0) intohf.py/sglang.py/custom.py+ a per-algorithmCaptureSpec;sglang_server.pyis filled by online O1.3.training/strategies/(relocated inE0) — finalize theStrategySpecregistry + the per-algorithm specs; keepregister_strategy/resolve_strategyimport-compatible.config/schema.py(new) — Pydantic config;specforgeCLI entry inpyproject.toml[project.scripts]. See ../../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 §4 (export/to_sglang.py).- Detailed sketches for all of the above are in ../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); ato_hfload test; an MLA-eagle3 draft training smoke test; a CLI/config test (config parses → builds the sameTrainerthe 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.