Online Disaggregation Roadmap
Live disaggregated training: a rollout pool runs the frozen target model autoregressively
over a fixed prompt set and streams its hidden states across the network into a separate trainer
pool. This folds PR #618's online roadmap into the consolidated tree and applies the shared
decisions: train-with-decode only (the online target is frozen, so the streamed data is
independent of draft weights β there is no staleness and nothing to re-sync, and the whole
weight-sync / hot-update / on-policy axis is dropped here, kept only as the explicit out-of-scope
note below); and Ray is OPEN/undecided for the O2 scale-out layer β presented as a candidate
with a decision gate against a lighter torchrun/rcli alternative, not as a commitment.
Substrate is canonical and shared with the sibling docs: SampleRef (control plane, tensor-free)
FeatureStore(tensors: Local / SharedDir / Mooncake) +FeatureDataLoaderβTrainBatch. There is no separate hidden-state stream source of truth βFeatureDataLoaderoverSampleRef+FeatureStoreis the stream; online/offline/disagg vary only in the ref source + theFeatureStorebackend, shielded from training. See domain-refactor for the domainTrainer/managers that wrap the runtime training seam and for Phase B'sTargetEnginethat O1.3 depends on, eval-and-breadth for the orthogonal eval/algorithm track, and the top-level plan.
What we keep (do not rebuild)
The data plane is competitive β arguably ahead of the comparables on rigor (three backends behind one contract, contract-tested, with a durability seam). The gap is the orchestration + live-engine + async control loop. Reuse, do not rewrite:
- Data plane β
MooncakeFeatureStorezero-copy (data_plane/mooncake_store.py) with hard-pin replication and per-generation key sets;LocalFeatureStore/SharedDirFeatureStorebehind the sameFeatureStorecontract (data_plane/feature_store.py). - Control-plane vocabulary β
SampleRef(tensor-free;assert_no_tensorsincontracts.py);SampleRefQueuewith lease/ack and the reserved partition seam (data_plane/sample_ref_queue.py);DataFlowControllercommit/lease/ack +reconcile_on_restart(control_plane/controller.py); theMetadataStoreABC withInMemoryMetadataStore/SQLiteMetadataStore(control_plane/metadata_store.py). - Streaming seam β
StreamingRefChannel/StreamingRefQueue(data_plane/streaming_ref_channel.py) already carry committed refs cross-process with in-flight accounting and a close-and-drain protocol. - Trainer seam β
TrainerCore/DraftTrainStrategy/TrainingBackend+StepContext(training/), wrapped (not replaced) by the domainTrainer.
Comparables (from #618)
| Axis | SpecForge (today) | TorchSpec | DeepSpec |
|---|---|---|---|
| Data plane (Mooncake zero-copy, tensor-free metadata) | At parity / cleaner β 3 backends, one contract, contract-tested, durability seam | EagleMooncakeStore (per-tensor keys) |
disk cache, no streaming |
| Orchestration / scale-out | GAP β library + composable builders, controller in-process | Ray actors + placement groups + async controllers + EnginePool |
single 8-GPU node, no Ray |
| Live generation (train-with-decode) | GAP β in-process generator stub today; no server | live autoregressive rollout streaming | offline regen-to-disk |
| Cross-pool backpressure | policy exists, off by default | pool-byte feedback + capacity await | n/a |
| Online weight sync / staleness | n/a β out of scope (frozen target) | also absent (not needed) | absent |
| Fault tolerance | reconcile_on_restart + checkpoint resume |
checkpoint resume (β parity) | n/a |
Read: the data plane is at or ahead of parity; the competitive gap is orchestration + the live engine + the async control loop. DeepSpec's separate edge (breadth + eval) is orthogonal and lives in eval-and-breadth.
O1.1 β Shared cross-process control plane Β· size M Β· CPU Β· status: in-review
- Goal Producer and consumer attach to one durable control plane instead of each building
its own
InMemoryMetadataStore, so commit/lease/ack/dedup and restart-reconcile work across processes (~PR #624). - Target state A separate producer and consumer share commit/lease/ack via a durable store;
the "single-host in-process index" caveat is gone for the shared-store path;
reconcile_on_restartis wired into the launcher andresume=Truere-streams the committed-but-unacked tail. - Implementation
control_plane/metadata_store.pyβSQLiteMetadataStoreis the end-to-end dev/single-host durable tier behind the existingMetadataStoreABC; a thinRedisMetadataStoreis a later subclass behind the same interface (chosen at O2 if multi-node demands it).data_plane/sample_ref_queue.pyβSampleRefQueueoperates over the shared store rather than purely in-process.control_plane/controller.pyβDataFlowController(metadata_store=...); the producer'scommit_samplesand the consumer'slease_train_refs/ack_train_refsland in the same durable store;reconcile_on_restartresolves committed-but-unacked refs on attach.launch.pyβbuild_disagg_online_producer/build_disagg_online_consumeralready acceptmetadata_store/metadata_db_path(O1.1 wiring);_resolve_metadata_storepoints both halves at the same durable path, and the consumer'sresume=Truereconciles before training.
- Tests / gates Two controllers over one store: producer commits, consumer leases/acks across
processes; restart reconcile hands already-trained ids back as
skip_idsand re-streams the tail; at-least-once dedup.tests/test_runtime/test_disagg_online_launch.py,tests/test_runtime/test_disagg_online.py. - Done when A separate producer and consumer share commit/lease/ack via the durable store, and the single-host-in-process-index caveat is removed for the shared-store path.
O1.2 β Async streaming loop + one-process builder Β· size M Β· CPU / small-GPU Β· status: in-review
- Goal Replace synchronous drain-then-fit with an async streaming loop, using the existing in-process generator as a stub so this milestone carries no engine risk (~PR #625).
- Target state End-to-end live 1+1 run with in-process generation: producer pulls prompts β
generates β
put+ commit ref; consumer leases βgetβ trains β acks at the optimizer step; a bounded sample pool throttles via the channel's in-flight watermark. - Implementation
launch.pyβ the composable builders already exist today:build_disagg_online_producerreturns(workers, drive_producer);build_disagg_online_consumerreturns(trainer, loader);run_disagg_online_interleavedruns the producer on a background thread whiletrainer.fitconsumes on the main thread, with symmetric hang-free shutdown (trainer-done setsshould_stop; producer-done closes the channel; producer-raise closes the channel then re-raises).build_disagg_online_runtime(aliasedbuild_disagg_online_eagle3_runtime) composes the two halves and dispatches torun_disagg_online_interleaved.- The producer's
drive_producer(should_stop=...)applies channel backpressure (pauses whilechannel.in_flight_remote()exceedsin_flight_high_watermark) and closes the channel on exit. - In-process generator stub:
SGLangAdapter.generate_featuresover the SpecForgeEagle3TargetModel.generate_eagle3_datapath (inference/sglang_adapter.py), driven byRolloutWorker(inference/rollout_worker.py).
- Tests / gates Tiny-model 1+1 online run: trainer consumes a streamed (not precomputed)
ref set; metrics align with the offline baseline on the same prompts/seed. Interleaved
shutdown is hang-free in all three orderings.
tests/test_runtime/test_disagg_online_launch.py,tests/test_runtime/test_streaming_ref_channel.py. - Done when An end-to-end live 1+1 run drives training from an in-process generator over the streaming channel, with no precomputed features.
O1.3 β Live SGLang-server hidden-state capture Β· size L Β· GPU + engine Β· status: in review (PR #641, reforward transport; spike findings + upstream proposal in ./o13-capture-spike.md)
π΄ GATING RISK β run the capture spike first. The unknown is how far
Eagle3TargetModel.generate_eagle3_datais from server-backed capture. The spike is the difference between O1.3 being days vs. a quarter; SpecForge is SGLang's own project so reuse is plausible (TorchSpec needed an SGLang codebase patch). Run it before/with O1.1.
- Goal Replace the in-process generator with a real SGLang server emitting aux + final
hidden states straight into
MooncakeFeatureStore. - Target state A live SGLang server feeds training with zero precomputed features: server generates β hidden states land in the store β consumer trains; acceptance metrics match the offline baseline.
- Implementation
- Depends on domain-refactor Phase B's
TargetEngineabstraction (SGLangServerEngine) β O1.3 is the online consumer of that engine. inference/sglang_adapter.pyβ a server-backedFeatureSource(launch/attach an SGLang server) alongside the existing in-processSGLangAdapter; the capture hook is engine-side, writing aux + final hidden states to theFeatureStore. Reuse theCaptureConfig/verify_capturecontract (inference/capture.py) so an aux-layer-id / width / target-dim mismatch fails loudly. Seeinference/sglang_patch_inventory.mdfor the patch surface.inference/rollout_worker.pyβ the producer uses the server-backed source; committedSampleRefs flow through the sameStreamingRefChannel.
- Depends on domain-refactor Phase B's
- Tests / gates (gated GPU) Server generates β hidden states resident in the store β consumer
trains; capture verification passes; acceptance vs. offline.
tests/test_runtime/test_mooncake_store.pyfor the store path. - Done when A live SGLang server feeds training with zero precomputed features and acceptance metrics match the in-process / offline baseline.
O2 β Scale-out orchestration (Ray = OPEN) Β· size L Β· infra Β· status: later
- Goal Move from 1+1 to N producers + M trainers with per-DP-rank leasing, cross-pool backpressure, and a managed streaming-pool lifetime β and decide the orchestration layer.
- Target state Independent N producers and M trainers scale without re-leasing or dropping
samples; a slow trainer throttles the producer (bounded memory, not OOM); the online
MooncakeFeatureStorebugs that become reachable at scale are closed; the orchestration layer is chosen against an explicit gate.
Orchestration: Ray candidate vs. lighter alternative (decision gate)
Ray is a candidate for the scale-out orchestration layer, not a commitment and not a non-goal.
It is likely necessary for multi-node N-producer/M-trainer scale-out and high-bandwidth
isolated-pool runs (its actor/placement model maps cleanly onto independent rollout and trainer
pools with separate GPU counts/sharding, matching the comparables' shape). The lighter alternative
is torchrun + the existing rcli multi-node launch driving the already-composable
build_disagg_online_producer / build_disagg_online_consumer builders across nodes, with the
shared durable MetadataStore (O1.1) as the only cross-process coordination point β no actor
framework.
- Trade-off Ray buys dynamic placement, actor restart/health, and a uniform multi-pool control
surface, at the cost of a heavy runtime dependency, a new failure surface, and packaging/operability
overhead on the GPU boxes. torchrun +
rclikeeps the dependency surface minimal and reuses the existing launch path, but leaves placement, fan-out, and restart as bespoke launcher logic. - Decision gate β adopt Ray only if the N+M / multi-node runs demonstrably need dynamic
placement or per-actor restart that the static torchrun +
rcli+ shared-store path cannot deliver (e.g. heterogeneous pool sizes that must reshard at runtime, or actor-level fault recovery that bash-level relaunch cannot meet the availability target for). If a static launch over the shared store carries N+M with acceptable restart behaviour, stay on the lighter path. Re-evaluate at the first true multi-node scale-out run.
dp_partition leasing
- Implementation
data_plane/sample_ref_queue.pyβ the partition seam exists today:dp_partition(sample_id, num_partitions)computes a stable DP-rank assignment, andlease(..., partition=(index, num_partitions))restricts a lease to a DP shard via_lease_partition_locked. The reservedpartition_keyproducer-side routing hint is accepted-and-ignored today; turn on per-DP-rank leasing so every sample is leased exactly once across a reshard.control_plane/controller.pygains multi-trainer lease dispatch. - Tests / gates N+M run; every sample leased exactly once across a reshard; no double-train;
changing
num_partitionsre-distributes the same committed pool without re-leasing or dropping.
Cross-pool backpressure
- Implementation
control_plane/backpressure.pyexists and is off by default:BackpressureControllerwithshould_pause_prompts/cap_prompt_grant/cap_train_leaseand a high/low watermarkBackpressureConfig. Wire the controller into the dispatch loop so it returns pool-size feedback and blocks granting when the pool is full; the producer throttles instead of OOM-ing the store.control_plane/controller.pyreports capacity. - Tests / gates A slow trainer makes the producer throttle (bounded store memory) rather than OOM.
Streaming-pool lifetime + known online MooncakeFeatureStore bugs
- Implementation
data_plane/mooncake_store.pyβ add consume-then-GC / TTL lifetime for the streaming pool, and close the two online-only bugs that become reachable at scale, both of which the shared metadata index (O1.1) unblocks:- release-pending re-put gc-clobber β a re-put bumps the per-sample generation and removes the
prior key set; a parked failed-free in
_release_pendingplusgc()can clobber the fresh blob. Needs a tombstone-then-free protocol tied to the shared index. - cross-process abort tombstone β under a lease-deferred remove,
producer.abortmarks only its own_freed, so the bytes linger and a separate consumer (empty_freed) still resolves the aborted ref β stale. This is theexpectedFailuretest_cross_process_abort_under_lease_defer_is_known_gapintests/test_runtime/test_mooncake_store.py:445; flip it to a hard assertion once the shared index lands.
- release-pending re-put gc-clobber β a re-put bumps the per-sample generation and removes the
prior key set; a parked failed-free in
- Tests / gates Re-put-while-pending no longer deletes the fresh blob; cross-process abort
blocks a separate consumer (the
expectedFailurebecomes a real assertion); consume-once streaming is correct cross-process. - Done when N producers and M trainers scale without re-leasing/dropping; trainer-behind-rollout is a bounded-memory backpressure signal; the streaming-pool bugs are closed; the orchestration decision gate has been evaluated against a real N+M run.
O3 β Hardening Β· size L Β· infra Β· status: later
- Goal Make the live disaggregated path production-operable.
- Target state RDMA path runs without per-op buffer churn; actor/process restart and health are handled; the run is observable.
- Implementation
- RDMA buffer-pool registration in
data_plane/mooncake_store.pyβ replace the per-opregister_buffer/unregister_bufferaround_store_put_tensor/_store_get_tensorwith a registered pool before flippingDEFAULTprotocolfromtcptordma. - Actor / process restart + health: restart the rollout/trainer/controller processes (under
whichever orchestration O2 selects) with
reconcile_on_restartdriving recovery from the shared store; health endpoints onRolloutWorker.health/SGLangAdapter.health. - Observability: pool depth, in-flight, lease/ack rates, backpressure snapshots
(
BackpressureController.snapshot), capture-verification counts.
- RDMA buffer-pool registration in
- Tests / gates Kill-and-restore a producer/trainer mid-run and confirm no lost or double-trained samples; RDMA path registers buffers once; metrics emitted.
- Done when The live disaggregated run survives a process restart with no data loss and runs the RDMA path with one-time buffer registration.
Provenance, not weight sync
draft_weight_version is retained only as provenance metadata on each committed sample β it is
recorded by RolloutWorker._put_metadata / _offline_metadata (inference/rollout_worker.py),
threaded through contracts.py, the metadata store, the controller, and every FeatureStore
backend's metadata round-trip. It exists for debugging/audit. There is no staleness gate, no
version registry, and no consumer-side check keyed on it. Because the online target is frozen, the
streamed hidden states are independent of draft weights.
Dependency graph
capture spike (gates O1.3)
β
O1.1 βββΆ O1.2 βββΆ O1.3 βββΆ O2 βββΆ O3
β β
β ββ depends on domain Phase B TargetEngine (SGLangServerEngine)
β
ββ shared MetadataStore unblocks the O2 online-store bug fixes (re-put gc-clobber,
cross-process abort tombstone)
eval-and-breadth track ββ independent (orthogonal to disaggregation)
Explicitly out of scope
- Draft-weight sync, weight-version registry, hot draft-update, staleness gate,
WeightPublisher/WeightVersion/ serving accept-length gate /ServingTrafficStream/ serving-traffic capture β not needed for train-with-decode. The online target is frozen; the draft is not in the generation loop; the data is independent of draft weights, so there is no staleness and nothing to re-sync.draft_weight_versionsurvives as provenance only (above). - True on-policy / draft-coupled training (draft proposes β target verifies β train on realized acceptance, requiring live draft-weight sync) β a separate research bet, not addressed here. No public framework ships it.
- Elastic / dynamic engine scaling at runtime β engines are fixed at init; the comparables do the same.