nemo_automodel.components.speculative.streaming
nemo_automodel.components.speculative.streaming
Tensor-free control-plane contracts for the speculative-training stream.
The streaming pipeline (per the EAGLE-3 / DFlash / DSpark train-inference
disaggregation RFC, issue #3062 PR 1) splits every transferred sample into a
refs (control-plane, no tensors) and a store (data-plane, holds
the supervision tensors). The queue carries refs only; tensors live in the
store and are referenced by nemo_automodel.components.speculative.streaming.refs.SampleRef.feature_keys.
This package owns:
SampleRefandFeatureSpec— the frozen, tensor-free reference carried on every control-plane hop.assert_no_tensors— the guard that enforces the no-tensor invariant on the control plane.FeatureStore— the pluggable data-plane transport (local dict, shared POSIX mount, NCCL, …);LocalFeatureStore— the in-process implementation land-tested by PR 1.SampleRefQueue— the metadata-only lease/ack/fail queue between producers and consumers with visibility-timeout reclaim and watermark-based backpressure.FeatureProducerandFeatureDataLoader— the EAGLE-3 produce and consume sides that turn a target forward into refs and refs back intoEagle3TargetBatchinstances.AsyncFeaturePipeline— runs the producer in a background thread so target forward and draft backward overlap;SharedDirFeatureStoreadds a POSIX shared-mount data plane for cross-process producer/consumer.
Subpackages
Submodules
nemo_automodel.components.speculative.streaming.async_pipelinenemo_automodel.components.speculative.streaming.eagle3nemo_automodel.components.speculative.streaming.loadernemo_automodel.components.speculative.streaming.producernemo_automodel.components.speculative.streaming.queuenemo_automodel.components.speculative.streaming.refsnemo_automodel.components.speculative.streaming.store
Package Contents
Classes
Functions
API
Run a FeatureProducer in a background thread, draining a prompt source.
Lifecycle
Construct, then start (or use the context manager).
The background thread is daemon; stop joins it within
join_timeout seconds. Outstanding leases are the trainer’s
responsibility — the pipeline does not drop them on close.
Parameters:
The FeatureProducer to invoke on each prompt.
It carries the wrapped target backend, the store, and the
per-call metadata.
The SampleRefQueue to push the resulting
SampleRef onto. The queue’s HWM/LWM hysteresis
paces the producer against the consumer.
A PromptSource — either a zero-arg
callable or an Iterator. The callable is invoked
from the background thread; it must be thread-safe (e.g.
a torch.utils.data.DataLoader iterator is).
Seconds between prompt_source invocations
after exhaustion when stop_on_exhausted is False.
Defaults to 100ms — cheap, lets the producer resume
quickly if more data lands.
When True (default), the background
thread exits as soon as prompt_source returns None
and close drains. When False, the thread keeps
polling so a streaming dataset can refill.
Spawn the background producer thread.
Idempotent while the thread is alive: a second call is a no-op. The
thread is named streaming-async-producer so test failures and
runtime traces show which thread is hung.
The pipeline is single-use. When the producer thread exits — prompt
source exhausted, stop, or an error — the bound queue is
closed in _run’s finally so the consumer drains and stops. A
closed queue cannot reopen, so restarting is not supported; construct
a fresh pipeline (and queue) to stream again. Restart is rejected here
rather than failing later with an opaque “queue is closed” from a
blocking put.
Signal the background thread to exit and join it.
Outstanding leases are the trainer’s; this method does not ack them. Idempotent. If the background thread raises, the exception is re-raised here so the trainer sees the failure.
Bases: enum.Enum
Which speculative-decoding draft family produced this sample.
The on-wire string value (eagle3 / dflash / dspark) is the
stable schema key the consumer matches against. New algorithms land by
adding a member here plus a schema row in the RFC’s “Feature schema per
algorithm” table; both moves are part of the same change.
Iterator over Eagle3TargetBatch materialized from a streaming queue.
Lifecycle
Each iterator pull yields an Eagle3TargetBatch whose
tensors come from a fresh store.get(). The previous
batch’s lease is ack’d and its store handle released on the
NEXT pull — so the trainer can hold one batch across one
forward pass without it being freed mid-forward. A consumer
that wants eager reclamation (e.g. to free memory before
pulling the next batch) calls consume_now after
computing its loss. Iteration ends on close or when
the queue drains.
Parameters:
The metadata-only queue the producer puts SampleRef
onto. queue.close() at any time cuts the iterator short.
The FeatureStore each lease will be materialized
through. Must match ref.store_uri for the leased refs.
FeatureAlgorithm the loader runs the
per-algorithm schema check for; defaults to EAGLE-3.
Ack the most recent lease / release its store handle and shut the queue.
After close the iterator raises StopIteration on
the next pull. Idempotent so a trainer can use with safely.
Release the most recently yielded batch eagerly.
Useful for trainer hooks that want to free memory before
pulling the next batch (e.g. immediately after backward()).
Idempotent.
Run a target backend once and put the supervision into a FeatureStore.
Thread safety: produce is not reentrant with itself on the same
producer instance; one thread should drive produce at a time.
Multiple producers over the same store are fine; the store’s lock
serializes the puts.
Parameters:
an object exposing generate_batch(input_ids, attention_mask, loss_mask, ...) -> Eagle3TargetBatch plus
the get_input_embeddings and set_vocab_mapping
accessors from
Eagle3TargetBackend.
The FeatureStore every produce call writes
into.
Stable across the whole training run; mirrored onto every
SampleRef.
Forced FeatureAlgorithm; defaults to picking
by the backend’s runtime type.
Monotonically increasing identifier of the
target-model weights; surfaced on each SampleRef.
Same idea for the draft model’s weights.
Callable that produces the per-call sample id;
defaults to "sample-{run_id}-{n}". Wrap with a hash of
the input batch if you want stable ids across runs.
Release any backend resources; the producer itself holds none.
Evict a produced sample whose ref will not be enqueued.
Used when a produced ref is dropped before it reaches the queue: a
spec-mismatch abort in produce, or the async pipeline shutting
down while a blocking put is in flight. get + release drops the
last handle so the store frees the sample; without it the sample’s bytes
(or its shared-dir file) stay resident with no consumer able to release
them.
Parameters:
The reference whose backing sample should be evicted.
Expose the wrapped backend’s input-embedding module.
Mirrors Eagle3TargetBackend.get_input_embeddings so the
draft can seed its input embeddings from the target before the
first streamed batch arrives.
Run the wrapped backend once and stash its supervision into the store.
Parameters:
Tensor of shape [batch, sequence], torch.long.
Tensor of shape [batch, sequence], torch.long.
Tensor of shape [batch, sequence], torch.long.
Per-document position ids when packing is on;
None for non-packed. Carried through unchanged to
the Eagle3TargetBatch so the trainer’s loss
path stays bit-identical to the colocated path.
Per-document sequence lengths when packing is on;
None for non-packed.
[batch, sequence] long tensor gating
cross-document TTT supervision when packing is on;
None otherwise.
Returns: SampleRef
class:SampleRef carrying a tensor-free description of
Raises:
MemoryError: forwarded from the store when the put would exceedmax_samplesormax_bytes. Producers can wait for the store to drain through the queue’s backpressure path (put_blocks_until_below).
Thread the draft-vocab mapping through to the wrapped backend.
The colocated backend ignores the mapping and projects the
full-vocab logits trainer-side; a future draft-vocab
encoding producer supplies the mapping here so the target can
precompute target_probs + position_mask and the wire
never carries a full-vocab tensor.
Shape + dtype metadata for one named feature in a SampleRef.
Mirrors the {dtype_code, shape} pair the EAGLE-3 remote protocol
encodes in nemo_automodel.components.speculative.eagle.remote.protocol.encode_nccl_metadata
so the client can preallocate the NCCL receive buffer. Consumers here use
it to preallocate the receive tensor before calling
FeatureStore.get.
The torch.dtype the producer will store under
SampleRef.feature_keys[name]. Validation in
assert_no_tensors rejects stray tensors in the ref but
a torch.dtype object on this field is allowed.
Logical shape with the same axis order the producer used.
shape[0] is conventionally batch and shape[-1] the
feature dimension; algorithms with packed layouts (THD, BHSD)
document their conventions in the algorithm row of the
RFC’s “Feature schema per algorithm” table.
Abstract transport every feature-store backend implements.
Implementations must be safe to call from one thread per operation;
cross-thread concurrency is the queue’s responsibility and outside this
contract. The SampleRefQueue
uses health for backpressure; everything else is the
produce/consume pair plus release / gc for lifecycle.
Release every resource owned by the store (file handles, NCCL groups, …).
After close, all subsequent calls raise RuntimeError.
Idempotent with respect to an already-closed store.
Sweep stale entries (e.g. failed releases from a crashed consumer).
Returns the number of entries reclaimed. Called opportunistically by
the SampleRefQueue between leases and unconditionally by
close.
Materialize ref’s tensors on device and hand back a StoreHandle.
The returned tensors are detached copies (clone for cuda
tensors, plain detach for CPU views) so prefetch cannot observe
aliasing through release. Returns one tensor per key in
SampleRef.feature_keys, in the same insertion order, so the
consumer can wire them straight into the per-algorithm batch.
Return a ints-only StoreHealth snapshot for backpressure.
Must not block on tensor I/O (no .cpu(), no .to()); in-memory
counters are sufficient. Backends that do background I/O MUST serve
health from cached counters, not from the in-flight I/O
thread.
Stash tensors for sample_id and return a tensor-free SampleRef.
The producer-side metadata (run_id, algorithm, schema_version,
target_model_version, draft_weight_version, num_tokens) is
the data the SampleRef carries on the control plane, so the
store must accept it here even though it does not inspect the values
beyond building the ref. Consumers see the ref unchanged.
Implementations must validate that the tensors match what they
declare (dtype, shape per feature, numel * element_size
summed across features == ref.estimated_bytes) and reject the
put with a specific exception when they do not, before any partial
write is observable.
Free the resources backing handle; idempotent.
After this returns, the sample is no longer present in the store and
a second get on the same SampleRef raises
KeyError. gc retries releases that a previous call
rejected (e.g. transient I/O), so the queue can rely on
gc + release being idempotent + retriable.
Handle to a leased SampleRef.
Each SampleRefQueue.acquire call mints a fresh Lease
with a unique lease_id. The queue’s ack and
fail verify the lease identity before mutating internal
state, so a late ACK for a stale (reclaimed) lease cannot pop a
newer active lease for the same sample_id.
Monotonic-clock timestamp at which this lease is
considered orphaned. Used by
SampleRefQueue.reclaim_expired to redeliver the ref.
Per-acquire unique identifier. The queue uses it as
the key in _outstanding and verifies it before any
ack/fail mutation.
Number of times this ref has been leased and re-leased (used for retry telemetry). Starts at 0.
The leased reference — the only sanctioned way to materialize
its tensors is FeatureStore.get, which returns a
StoreHandle
the consumer must hand to FeatureStore.release once it is
done with them.
The VisibilityTimeout that produced
this lease, kept here so the consumer can introspect it.
Bases: FeatureStore
In-process FeatureStore implementation.
Thread safety: every public method holds a single threading.Lock,
so concurrent puts and gets from the same Python process are safe. Async
/ cross-process safety is the queue’s responsibility and is out of scope
for PR 1.
Parameters:
Hard cap on simultaneously-stored samples. None means
unbounded sample count (still bounded by max_bytes).
Hard cap on resident bytes. None means unbounded
(still bounded by max_samples). At least one of
max_samples / max_bytes must be set, otherwise a
misconfigured store silently behaves as unbounded.
Threshold above which StoreHealth.high_watermark_hit
is True. The producer pauses here.
Threshold below which StoreHealth.low_watermark_hit
is True. The producer resumes here. Must be
strictly less than high_watermark_bytes; a hysteresis band
of zero flaps the producer on every step.
Materialize ref’s features on device and hand back a StoreHandle.
Parameters:
The reference returned by put (typically via a
queue lease). ref.store_uri MUST equal this store’s
store_uri; a mismatch raises KeyError so a
consumer cannot accidentally materialize a foreign ref.
Optional target device. None returns each feature
on the device it was put on; a non-None value
materializes every feature on that device via
Tensor.to(device) (a no-op when already in place).
Returns: dict[str, torch.Tensor]
A (tensors, handle) pair. tensors is a
Raises:
KeyError: whenref.store_uridoes not match this store, or whenref.sample_idis no longer present (released or never put).RuntimeError: when the stored tensor’s shape or dtype differs from what the ref claims, or the store has been closed.
Store tensors under sample_id and return a tensor-free SampleRef.
Parameters:
Stable identifier within run_id. Must be unique
in this store at put time; duplicates raise ValueError.
Feature-name to tensor mapping. The store detaches,
clones, and makes each tensor contiguous before stashing it,
so the producer may keep mutating its source tensors after
the put returns without disturbing what a later
get hands out. The shape and dtype of each tensor
are captured into the returned SampleRef’s
feature_specs; the consumer uses those specs to
preallocate the receive buffer at get time, so
changing tensors[name].shape or dtype between put
and get without updating the ref will surface as a
RuntimeError on materialization.
Same value on every ref of one run; surfaces on the
SampleRef.run_id so producers and consumers can
verify they are talking about the same run.
Which draft family produced this sample; gates the
SampleRef required-features check.
Bumped whenever the producer’s feature set or
layout for algorithm changes incompatibly.
Monotonically increasing identifier of the target-model weights.
Same idea for the draft model’s weights.
Sum of attended tokens; used by the consumer for empty / short loss-mask neutralization.
Returns: SampleRef
A tensor-free SampleRef carrying the per-feature
Raises:
MemoryError: if the put would exceedmax_samplesormax_bytes. The producer is expected to retry after the store drains below the low watermark (seehealth).RuntimeError: if the store has been closed.ValueError: on bad input (empty sample id, empty tensors map, duplicate sample id).
Zero-argument callable that supplies the next prompt batch.
Return the next prompt batch or None when the source is exhausted.
Returns: _PromptBatch | _PackedPromptBatch | None
A three-tuple (input_ids, attention_mask, loss_mask) where
Tensor-free reference to one produced sample in a feature store.
Which draft family produced this sample; the consumer
matches it against a registered FeatureSpec registry
to validate the ref before materializing.
Same idea for the draft model’s weights, so a
consumer can refuse to train against a ref produced before its
own weight snapshot. Starts at "0"; PR 4 wires the resync.
Sum of every feature’s numel() * dtype_bytes.
The SampleRefQueue reads this against
StoreHealth.resident_bytes for watermark hysteresis.
Named features this sample contributes, mapped to
per-store keys (e.g. filenames for SharedDirFeatureStore in
PR 3, dict keys for LocalFeatureStore).
Per-feature FeatureSpec so the consumer can
allocate the receive buffer before calling FeatureStore.get.
Sum of attended tokens; used by the consumer for empty / short loss-mask neutralization and by the queue for backpressure.
Stable identifier across the whole training run. Same value on every ref of one run so producers and consumers can verify they are talking about the same run before materializing.
Stable identifier within run_id. The consumer uses it
to partition the stream across DP ranks (RFC §“Consumer-side DP
resharding”) and to ACK / FAIL the corresponding lease.
Bumped whenever the producer’s feature set or tensor
layout for algorithm changes incompatibly. The consumer uses
it as a hard gate on the ref.
scheme://location identifying the FeatureStore
back-end (e.g. mem://local for
LocalFeatureStore).
The store URI plus feature_keys form the lookup the consumer
makes; the store object itself is discovered through the URI at
materialization time, not stored on the ref.
Monotonically increasing identifier of the target-model weights that produced this sample. Used to reject refs from a stale target (relevant once train-with-decode / weight resync lands in PR 4).
Return the feature names in insertion order.
The order is significant because some algorithms size the draft’s
fc projection by the order of the aux hidden states, not just
their count; the producer chooses the order once, the ref freezes it,
and the consumer preserves it through FeatureStore.get.
Lease / ack / fail queue over SampleRef.
Thread safety: a single threading.Lock protects every list /
counter, so a multi-producer / multi-consumer deployment works as long
as only one thread at a time calls any one of the methods.
Parameters:
The data-plane store the consumers will materialize against.
The queue reads FeatureStore.health for backpressure.
How long a leased-but-not-acked ref can live before reclaim. Defaults to 30s; production deployments normally key this off the recipe’s per-step budget.
Optional resident-byte threshold for
pausing. When None (default), the queue defers to
StoreHealth.high_watermark_hit (i.e. the store’s own
configured threshold). When set, the queue pauses whenever
StoreHealth.resident_bytes >= high_watermark_bytes.
Optional resident-byte threshold for
resuming. When None (default), the queue defers to
StoreHealth.low_watermark_hit. When set, the queue
resumes only after StoreHealth.resident_bytes <= low_watermark_bytes. Must be strictly less than
high_watermark_bytes so the hysteresis band is
non-empty.
Optional callbacks fired when the queue transitions high-watermark-paused -> resumed and back.
Whether close has been called on this queue.
Consumers that pull acquire and receive None use
this to disambiguate “drained, stop” (is_closed is True)
from “transient empty poll, retry” (is_closed is False).
Mirrors the Python queue.Queue separation between
empty() and the lifecycle-shutdown signal.
Whether the producer should pause against health.
When the queue ctor was given an explicit
high_watermark_bytes, that threshold wins; otherwise the
decision defers to StoreHealth.high_watermark_hit
(i.e. the store’s own configured threshold).
Whether the producer should resume against health.
When the queue ctor was given an explicit
low_watermark_bytes, that threshold wins; otherwise the
decision defers to StoreHealth.low_watermark_hit.
Hysteresis is preserved either way: resume crosses the low
threshold, pause crosses the high threshold.
Mark a leased ref as successfully consumed and free its queue slot.
Verifies Lease.lease_id matches the live outstanding
entry for lease.ref.sample_id: a stale ACK for a lease
that has been reclaimed and re-leased is rejected (logged,
ignored) so the new consumer’s live lease is not popped by
accident.
Does NOT touch the store — the consumer’s FeatureStore.get
return value carries a StoreHandle
that the consumer must hand to FeatureStore.release to drop
the tensors. The queue’s responsibility ends at “lease no longer held”.
Lease the next ref; returns None when nothing is ready.
None is returned in two situations, which consumers
disambiguate with is_closed:
is_closed is True: the queue has been shut down and is drained. The consumer should stop iterating.is_closed is False: a transient empty poll (the producer is briefly behind). The consumer should retry.
The returned Lease is the only sanctioned way to access
the ref’s tensors — FeatureStore.get requires a SampleRef,
and that ref must come from a lease. The consumer MUST hand back
the lease via ack (on success) or fail (on error)
so the queue can reclaim the slot and the store can drop the
sample.
Mark the queue closed; acquire drains what remains, then returns None.
Closing does not discard already-enqueued refs: acquire keeps
handing out pending refs until they are all leased, and only returns
None once the queue is closed and both pending and outstanding are
empty. is_closed therefore reports the closed flag, not that the
queue is already drained; FeatureDataLoader polls it to know
when a None from acquire is terminal.
Outstanding leases are left intact: their consumer still owns the
tensors, and a leaked FeatureStore.release would push the
store’s residency counter below zero. The store’s own close
is the canonical place to drop residency.
Return a leased ref to the pending queue, without dropping its tensors.
Verifies the lease identity before re-enqueuing: a stale
fail for a lease that has been reclaimed is a no-op. The
ref will be leased again (its Lease.redelivery_count
increments). Re-delivery is what makes the pipeline fault-tolerant
to a transient consumer error — a permanently bad ref is the
consumer’s problem (drop it after a bounded retry budget).
Enqueue ref for a future acquire.
Does not block on backpressure; producers that care should call
put_blocks_until_below instead, which honors the high/low
watermark hysteresis from FeatureStore.health.
Enqueue ref, blocking the producer while the store is over its high watermark.
The producer is paused when _should_pause returns True
(resident crossed the high threshold) and only resumed when
_should_resume returns True (resident dropped back
below the low threshold). In the band between the two
thresholds the producer’s existing paused / unpaused state is
preserved — that hysteresis is what prevents flapping when the
producer is sitting near the high watermark.
Parameters:
The reference to enqueue.
Seconds between backpressure checks when paused.
Defaults to 50ms — well below typical step times, well above
the cost of a Python-level FeatureStore.health call.
Optional callable checked on each loop iteration.
When it returns True, the put aborts with
RuntimeError so a shutdown signal can unblock a
producer waiting on backpressure without closing the queue
first.
Raises:
RuntimeError: if the queue is closed while the producer is blocked, or ifabort_whenreturnsTrue.
Reclaim leases whose Lease.deadline has passed.
Each reclaimed lease is re-enqueued; acquire returns it on
a future call with an incremented Lease.redelivery_count.
Returns the number of leases reclaimed — a queue that is healthy
returns 0 most of the time.
Bases: FeatureStore
FeatureStore backed by one <sample_id>.safetensors per sample.
Thread safety: every public method holds a single
threading.Lock. Cross-process / cross-node is supported
as long as distinct processes use distinct sample_id values;
the lock does not extend across processes.
Parameters:
Filesystem path used as the rendezvous. Created if it
does not exist. Concurrent producers and consumers in
separate processes / ranks coordinate via unique
sample_id values — collision is the caller’s problem.
Same residency contract as LocalFeatureStore; the
queue’s HWM/LWM hysteresis reads them off
health.
Opaque token the consumer must return to FeatureStore.release.
Each FeatureStore.get mints a fresh handle with a unique
handle_id. Two get calls on the same sample return two
distinct handles; FeatureStore.release matches against
handle_id so releasing one handle twice (or releasing a stale
handle after a sibling has been acquired) cannot decrement a
sibling’s outstanding count.
Holds the producing store, the sample id, the originating
SampleRef, and the per-get handle identity.
Integers-only snapshot of store residency for backpressure decisions.
Configured hard cap from
LocalFeatureStore
(PR 3’s shared-dir / PR 4’s NCCL store report the analogous cap).
True iff resident_bytes >= high_watermark_bytes
on the last health call. The queue pauses a producer that
sees this transition.
True iff resident_bytes <= low_watermark_bytes
on the last health call. The queue resumes a producer
that has been paused and now sees this transition. The
hysteresis band between the two is what prevents flapping.
Bytes currently held in the store across un-acked
samples. Compared against capacity_bytes for the high/low
watermark hysteresis.
Number of un-acked samples currently held. Used for the second cap (sample count) the RFC §“Open questions” Q2 keeps alongside the byte backstop.
How long an unacked Lease is allowed to live before reclaim.
Any positive value is accepted (sub-second values are useful in tests). Production deployments typically pick something an order of magnitude larger than the recipe’s per-step budget so a slow but healthy consumer does not see its leases reclaimed out from under it.
Recursively validate that obj carries no tensors.
Walks dataclasses (any nested dataclass included), dict with str
keys, and list / tuple. Anything else must be a primitive
(str/int/float/bool/None/bytes) — a tensor, numpy
array, or duck-typed tensor-like (has data_ptr + is_cuda + numel) at
any depth raises ValueError.
The check is structural, not nominal: a third-party tensor type that quacks like one is rejected. PR 2’s registry / schema validators layer on top of this primitive guard, not instead of it.