Online Data Streaming (ODS)#
ODS supplies owned, causally aligned training batches from continuously
generated trajectories through a bounded CPU shared cache. The producer and
learner can run independently; explicit provenance and optional per-trajectory
delivery budgets make data use auditable. Shared APIs live in
embodichain.data_pipeline and do not depend on a particular learner or task.
Component |
Responsibility |
|---|---|
|
Own the simulation producer, shared storage, publication, locked selection/copy and optional live sampling ledger. |
|
Present live, resident tensor and numeric LeRobot data under one explicit schema/context contract. |
|
Iterate causal contexts from a source; the caller owns production, loading, retry and refill. |
|
Preserve item/batch chunk iteration and dynamic chunk lengths. |
|
Calculate bounded reuse, cache and pacing/refill targets from measurements and a versioned profile; the host applies the plan. |
|
Apply an optional measured plan to a caller-managed manual engine: explicit refill, bounded waits, lifecycle draining and batch-start pacing. |
Guides and ownership#
Architecture and data flow: process structure, module ownership and online/offline routes.
Throughput configuration: rate units, reuse profiles, cache/rate limits, consumer responsibilities and runtime verification.
Dataset/source architecture: public schemas, source ownership and implementation boundaries.
Simple BC demo: one task/model binding, runnable commands and retained experiment results.
These guides own reusable ODS behavior. Task-specific parameters, checkpoint results and historical validation remain with their examples.
OnlineDataEngine#
The engine owns live production and publication. The training process chooses when to consume data, request manual refill and stop production. The engine does not own an optimizer or infer when a policy has learned the task.
Configuration and recorded-action meaning#
The engine preserves expert-controller recording by default. Policy requests have a separate schema and may have a different width from both robot state and expert commands.
Setting |
Meaning |
|---|---|
|
Stored robot proprioception width. |
|
Optional stored action width; omitted values preserve the historical |
|
|
|
Ordered ActionManager contract IDs; required with an explicit |
|
|
|
Optional per-source delivered-chunk budget and its configuration ceiling; supply both positive values or omit both. |
|
Add numeric frame identity and publication timestamps to sampled tensors. |
|
Optional bounded publication/eviction log; the owner must drain an enabled log during production and waits. |
|
Finite, positive graceful-close wait in seconds; defaults to |
For an environment with six relative EEF dimensions followed by one normalized parallel-gripper dimension, select the request schema explicitly:
from embodichain.data_pipeline.engine import OnlineDataEngineCfg
cfg = OnlineDataEngineCfg(
gym_config=your_gym_cfg,
state_dim=9,
action_dim=7,
action_semantics="policy_request",
action_contracts=("eef_pose.relative@1", "gripper.parallel@1"),
refill_policy="manual",
include_provenance=True,
)
Before generating transitions, the worker checks the live rollout semantics,
proprioception/action widths, ordered contracts, contiguous term slices and
controlled-joint order. It selects the environment’s
rollout_action_semantics accordingly. Explicit policy recording preserves raw
requests through ActionManager processing, requires finite floating values
within the action-space bounds, and works without a DatasetManager. A direct
ControllerAction cannot be stored as a policy request. Expert-controller
recording continues to use ExpertActionSpec.
With manual refill, request_refill() returns a target generation and coalesces
an outstanding request; wait_for_generation() waits for complete publication
with a timeout.
With include_provenance=True, each sample carries ods_run_id,
ods_episode_id, ods_generation, ods_frame_index, ods_accepted_at_ns and
ods_published_at_monotonic_ns.
Frame indices are zero-based positions in the stored episode. The timestamp
uses Unix nanoseconds when a successful episode is accepted for publication;
publication time separately uses host monotonic nanoseconds. These clocks must
not be subtracted without an explicit alignment. The worker stamps provenance
under the publication lock and samplers copy it with the corresponding payload.
Invalid frames retain -1 for every provenance field. Engine-local IDs identify
live samples; validated offline corpus manifests provide separate content
identities.
Statistics and accounting#
Interface |
Information and limits |
|---|---|
|
Require READY and surface worker failures without sampling or requesting refill. |
|
Compact READY-only generation/accepted-episode counters and optional unlocked/lifetime delivery totals. Counters use separate lock snapshots; quota is a coarse scheduling bound, not proof that a specific context/filter can be sampled. |
|
Production counts/time, refill generations and sampled chunks. Readable-frame counts exclude the active write window. Sampled chunks count deliveries, not completed optimizer work. |
|
With quotas enabled, retained identities and assigned/delivered/remaining allowances, plus lifetime delivered chunks. Unlocked quota is a scheduling hint; context and generation filters can further restrict eligibility. |
|
Optional publication/eviction records and monotonic timestamps. Enable |
The application records completed optimizer work, retired-source exposure, latency distributions and resource peaks. These interfaces do not generate a training dashboard or an evaluation report. See Verify the plan for measurement checks.
Causal context and padding#
sample_batch_with_context(batch_size, history_length, prediction_horizon)
copies a fixed causal history and future supervision around each valid anchor
under the producer lock. The current frame is at history_length - 1.
Episode ends, validity gaps and continuity changes bound the context; numeric
padding is zero, while language/provenance padding is -1. valid selects real
frames and loss_mask selects real current/future targets. Decode language only
for valid frames, as resolve_language() rejects padding IDs.
Module: embodichain/data_pipeline/engine/data.py
OnlineDataEngine manages an in-memory, shared buffer for streaming trajectory data. A typical usage pattern is:
Build and start the engine with
OnlineDataEngineCfg.Run simulation workers that continually push new experience into the engine.
Train by sampling trajectory chunks from the engine via
OnlineDataset.
Key ideas:
Shared buffer: the simulation producer and training consumers can read/write concurrently.
CPU shared memory: the supported process-shared buffer uses CPU storage; sampled tensors can then be transferred to the learner’s device.
Chunked sampling: training samples fixed-length or dynamically sized chunks.
Transactional rows: the current write window is locked until a complete, successful episode replaces it. Failed generation attempts never become sampleable.
Variable lengths: every frame has a
validflag. Sampling constructs windows only from real frames and never reads zero padding or a stale tail.Episode-uniform sampling: an eligible episode row is selected uniformly, then a valid start offset is selected within that row. Longer episodes do not receive extra probability merely because they contain more windows.
Explicit lifecycle: the engine progresses through
CREATED,STARTING,READY, and a terminalFAILEDorSTOPPEDstate. Sampling is allowed only inREADY, and a stopped or failed instance cannot be restarted.Fail-fast errors: initial-fill timeouts, rollout failures, hard worker exits, and shutdown/recorder failures are raised to the owner. The same stable error snapshot is visible to every forked or spawned data consumer.
Each row stores one complete task episode. A row may contain multiple semantic segments. In addition to observations, actions, and rewards, the shared buffer contains:
valid, episode_step, segment_id, segment_step,
segment_start, segment_end, segment_accepted, segment_attempt_id,
continuity_id, terminated, truncated, task_index, subtask_index
Tasks use the same create_demo_segments() protocol as run-task; legacy
create_demo_action_list() tasks are treated as one segment. The worker checks
termination after every action and retries a failed episode up to
max_generation_attempts before reporting an error.
start() blocks until the initial fill is complete and is bounded by
initialization_timeout. If the worker exits or generation fails, start()
raises the worker error instead of waiting indefinitely. A later worker failure
is reported on the next sample_batch() call rather than silently serving a
permanently stale buffer.
Minimal setup#
from embodichain.data_pipeline.engine import OnlineDataEngine, OnlineDataEngineCfg
cfg = OnlineDataEngineCfg(
buffer_size=2, # trajectories kept in the shared buffer
state_dim=6, # example state dimension
gym_config=your_gym_cfg, # parsed gym config for the task
initialization_timeout=300, # maximum seconds for the initial fill
)
engine = OnlineDataEngine(cfg)
engine.start()
Shutdown#
Prefer a context manager so cleanup runs on both success and failure:
with OnlineDataEngine(cfg) as engine:
batch = engine.sample_batch(batch_size=32, chunk_size=64)
train_step(batch)
For an explicitly managed lifecycle, call stop() in the process that created
the engine:
engine = OnlineDataEngine(cfg)
engine.start()
try:
train(engine)
finally:
engine.stop()
stop() is idempotent after a successful cleanup. It waits up to
shutdown_timeout seconds for graceful worker close and raises any failure reported while the worker closes its environment or
flushes committed data. If graceful shutdown times out and the worker requires
terminate() or kill(), durability cannot be confirmed: the engine remains
FAILED and stop() raises instead of reporting a false STOPPED state. When
a with body and cleanup both fail, the body exception remains primary and the
cleanup failure is attached as a note.
Start the engine before constructing multiprocessing DataLoader workers.
Forked and spawned copies may call sample_batch(), but only the original
owner process may call start() or stop(); consumer destructors never signal
the shared producer.
Per-trajectory delivery protection#
Set both episode_sample_budget and episode_sample_hard_limit on
OnlineDataEngineCfg to positive integers with budget <= hard limit. Omit both
for unrestricted historical behavior. Strict process-shared budgets use CPU
storage and identify trajectories by immutable engine run/episode IDs, rather
than reusable cache slots.
One returned batch row/context consumes one selection, independent of chunk length. Within-batch duplicate draws count separately. Each sampling call selects, copies and debits its complete requested batch under the shared lock. A failed copy spends no quota; downstream transforms, optimizer failures and prefetch do not refund delivered data. A new Dataset iterator does not reset its source’s allowance. Live replacement retires unused credits.
get_sampling_budget_snapshot() exposes retained identities, assigned/delivered/
remaining allowances and lifetime delivered chunks. Its unlocked total requires
at least one valid continuity-qualified anchor, but does not apply every
requested length, segment or generation filter. It is a scheduling hint, not
proof that a specific batch can be sampled. Retired-source exposure and completed
optimizer counts require consumer accounting.
Outcome |
Required handling |
|---|---|
Invalid schema/arguments |
Fix the configuration; do not treat it as a data wait. |
|
No eligible unlocked window/anchor exists. A live caller may wait with a deadline if production can change that condition. Fixed data cannot repair it by waiting. |
|
Eligible data exists but cannot fund the requested full batch. Stop or have the owner schedule further supply within its declared production budget. No partial batch is silently debited. |
Worker/read/shutdown failure |
Propagate the failure; do not continue indefinitely with old data. |
OnlineDataBudgetExhaustedError is the engine-facing alias of common
DataBudgetExhaustedError. Eligibility is checked before quota sufficiency;
errors are immediate and Dataset wrappers do not silently retry. Worker health
errors take precedence. Quota depletion does not automatically request refill.
Use the configuration guide to connect strict sampling
protection to an explicit host pacing/refill policy.
For a caller-managed manual engine, the optional
ODSFlowController provides reusable
bounded retries, refill and pacing. It does not change Source or Dataset error
semantics, own the optimizer, or start/stop the engine.
OnlineDataset#
Module: embodichain/data_pipeline/datasets/online_data.py
OnlineDataset wraps a live OnlineDataEngine and exposes a PyTorch IterableDataset. It supports two modes:
Item mode (default)#
Create the dataset with
batch_size=None(default).Each iteration yields a single
TensorDictof shape[chunk_size, ...].Use
DataLoader(dataset, batch_size=B)to let the DataLoader stack items into batches.
from torch.utils.data import DataLoader
from embodichain.data_pipeline.datasets import OnlineDataset
dataset = OnlineDataset(engine, chunk_size=64)
loader = DataLoader(
dataset,
batch_size=32,
collate_fn=OnlineDataset.collate_fn,
)
for batch in loader:
# batch shape: [32, 64, ...]
train_step(batch)
Batch mode#
Create the dataset with
batch_size=N.Each iteration yields a pre-batched
TensorDictof shape[N, chunk_size, ...].Use
DataLoader(dataset, batch_size=None)to bypass auto-collation.
dataset = OnlineDataset(engine, chunk_size=64, batch_size=32)
loader = DataLoader(
dataset,
batch_size=None,
collate_fn=OnlineDataset.passthrough_collate_fn,
)
for batch in loader:
# batch shape: [32, 64, ...]
train_step(batch)
Dynamic chunk sizes#
Pass a ChunkSizeSampler instead of an int to chunk_size to sample a new length each iteration step.
from embodichain.data_pipeline.datasets.sampler import UniformChunkSampler
sampler = UniformChunkSampler(low=16, high=64)
dataset = OnlineDataset(engine, chunk_size=sampler)
In batch mode, the sampler is called once per step so all trajectories in the batch share the same chunk length.
Segment-aware sampling#
Set sampling_mode according to the training objective:
# Chunks may span adjacent subtasks (default).
episode_dataset = OnlineDataset(engine, chunk_size=64, sampling_mode="episode")
# Every chunk stays inside one pick/place segment.
segment_dataset = OnlineDataset(engine, chunk_size=32, sampling_mode="segment")
# Every chunk contains an internal transition between two segments.
boundary_dataset = OnlineDataset(engine, chunk_size=32, sampling_mode="boundary")
All three modes still require every sampled frame to be valid. boundary
requires a chunk size of at least two and raises a clear error when no internal
boundary can satisfy the requested length.
ChunkSizeSampler#
Module: embodichain/data_pipeline/datasets/sampler.py
ChunkSizeSampler is a small interface that returns a positive integer chunk size each time it is called.
Built-in samplers:
UniformChunkSampler(low, high): discrete uniform over[low, high].GMMChunkSampler(means, stds, weights, low, high): Gaussian mixture with optional bounds.
Example (GMM):
from embodichain.data_pipeline.datasets.sampler import GMMChunkSampler
sampler = GMMChunkSampler(
means=[16.0, 64.0],
stds=[4.0, 8.0],
weights=[0.6, 0.4],
low=8,
high=96,
)
Runnable examples#
A runnable example that wires everything together is provided in:
examples/data_pipeline/online_dataset_demo.py
The local Franka BC demo adds a complete learner, expert, checkpoint/evaluator and fixed-data controls:
python -m examples.ods.simple_bc_demo.train --smoke --no-viewer --no-video
Its architecture bindings, task-specific throughput packet, artifacts and learning results belong to the example. The generic chunk example shows item mode, batch mode and dynamic chunk sizes. Run it with:
python examples/data_pipeline/online_dataset_demo.py
See Also#
RL Architecture — RL training pipeline
Expert Data Expansion Tutorial — Generating offline datasets