Add segment-aware demonstration episodes - #460
Conversation
# Conflicts: # embodichain/lab/scripts/run_env.py # tests/data_pipeline/test_online_data.py # tests/lab/scripts/test_run_env.py
Greptile SummaryThe PR introduces segment-aware expert demonstrations and carries their lifecycle and annotations through execution, online sampling, and dataset persistence.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| embodichain/lab/gym/envs/demo.py | Defines the segment contract and central executor, including per-environment completion, validation, retry, and terminal-state handling. |
| embodichain/lab/gym/envs/embodied_env.py | Adds independent expert-rollout cursors, segment annotations, inactive-row action masking, and transactional recording hooks. |
| embodichain/data_pipeline/engine/data.py | Adds validity-aware segment sampling, bounded rollout generation, and explicit producer lifecycle and error propagation. |
| embodichain/data_pipeline/datasets/online_data.py | Exposes episode, segment, and boundary sampling policies through OnlineDataset. |
| embodichain/lab/gym/envs/managers/datasets.py | Persists per-frame demonstration annotations and episode metadata while making finalization a durability barrier. |
| embodichain/lab/gym/envs/managers/async_datasets.py | Clones segment annotations and metadata before asynchronous FIFO persistence and reports background failures during finalization. |
| embodichain/lab/scripts/run_env.py | Migrates offline demonstration rollout to the shared segment-aware executor and explicit commit-or-discard resets. |
Sequence Diagram
sequenceDiagram
participant Task
participant Executor
participant Env
participant Buffer
participant Recorder
Task->>Executor: create_demo_segments()
loop Each segment and action
Executor->>Env: step(masked action)
Env->>Buffer: write valid frame and annotations
Env-->>Executor: terminated, truncated, info
end
alt Complete successful episode
Executor->>Env: "reset(save_data=true)"
Env->>Recorder: commit episode and metadata
else Failed or incomplete attempt
Executor->>Env: "reset(save_data=false)"
Env->>Buffer: discard pending episode
end
Reviews (2): Last reviewed commit: "test(data): tolerate spawned consumer st..." | Re-trigger Greptile
There was a problem hiding this comment.
Pull request overview
Adds a segment-aware expert demonstration contract so a single Gym episode can contain multiple semantic sub-trajectories (“segments”), while keeping legacy create_demo_action_list() tasks compatible. This integrates a shared episode executor across run-env, dataset recorders (LeRobot + sidecars), and the OnlineDataEngine shared-buffer worker, with reset becoming the explicit commit/discard boundary.
Changes:
- Introduces segment-aware demo types (
DemoSegment,DemoEpisodeResult, etc.) and a common executor that supports lazy segment planning, vector-env staggered completion, and per-frame annotations. - Makes offline generation and online shared-buffer filling transactional (commit on
reset(), discard onreset(save_data=False)), adds bounded retry, and propagates durability/flush failures. - Extends recording/sampling schema to include
valid+ segment/terminal annotations, updates LeRobot exports + metadata sidecar, and documents new APIs/sampling modes.
Reviewed changes
Copilot reviewed 27 out of 27 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| tests/lab/scripts/test_run_env.py | Expands CLI runner tests for transactional generation, replay ownership, and cleanup semantics. |
| tests/gym/envs/test_replay.py | Updates replay close behavior expectation (no implicit autosave on close). |
| tests/gym/envs/test_demo.py | New comprehensive tests for segment-aware demo execution and annotation behavior. |
| tests/gym/envs/tasks/test_stay_still_save.py | Adjusts registered time limit to avoid truncation on a 100-step expert plan. |
| tests/gym/envs/managers/test_dataset_manager.py | Adds finalize idempotency/error aggregation and commit/discard camera/trajectory tests. |
| tests/gym/envs/managers/test_dataset_functors.py | Adds LeRobot finalize idempotency, frame annotation, and JSONL sidecar tests. |
| tests/gym/envs/managers/test_async_dataset_functors.py | Extends async recorder tests for cloned annotations/metadata and finalize failure aggregation. |
| tests/data_pipeline/test_online_data.py | Adds engine lifecycle/state/error-channel tests and segment/boundary sampling tests. |
| tests/data_pipeline/depth_video/test_writer.py | Ensures depth encoder close failures surface as durability errors. |
| embodichain/lab/scripts/run_env.py | Switches to segment-aware executor; makes reset the commit boundary; improves replay/cleanup semantics. |
| embodichain/lab/gym/utils/gym_utils.py | Extends rollout buffer schema with validity/segment/terminal annotation fields. |
| embodichain/lab/gym/envs/managers/record.py | Makes camera recorders transactional + idempotent finalize/close; async recorder supports explicit per-env commit queues. |
| embodichain/lab/gym/envs/managers/datasets.py | LeRobotRecorder now persists per-frame annotations + episode JSONL sidecar and has idempotent durability-aware finalize. |
| embodichain/lab/gym/envs/managers/dataset_manager.py | Makes dataset functor finalization idempotent and aggregates failures across functors. |
| embodichain/lab/gym/envs/managers/async_datasets.py | Async LeRobot recorder now clones annotations + metadata and aggregates background failures at finalize barrier. |
| embodichain/lab/gym/envs/embodied_env.py | Adds per-env rollout cursors, segment metadata hooks, transactional discard, and safe masking for staggered vector demos. |
| embodichain/lab/gym/envs/demo.py | New segment-aware demonstration protocol + executor implementation. |
| embodichain/lab/gym/envs/base_env.py | Disables auto-reset during demo execution (similar to replay). |
| embodichain/lab/gym/envs/init.py | Exposes demo protocol symbols from envs package. |
| embodichain/data_pipeline/engine/data.py | Adds explicit lifecycle states, worker error broadcast channel, transactional writes, and segment-aware sampling modes. |
| embodichain/data_pipeline/engine/init.py | Re-exports new engine state/error types. |
| embodichain/data_pipeline/depth_video/writer.py | Treats depth sidecar close/meta failures as raised durability errors. |
| embodichain/data_pipeline/datasets/online_data.py | Forwards segment/boundary sampling modes from OnlineDataset into OnlineDataEngine. |
| embodichain_tasks/embodichain_tasks/special/stay_still_save.py | Updates env time limit rationale for segment-aware executor commit behavior. |
| docs/source/overview/gym/dataset_functors.md | Documents finalize as a durability barrier (no implicit commit) and error surfacing semantics. |
| docs/source/guides/run_env.md | Documents segment API, retry/commit behavior, and annotation outputs. |
| docs/source/features/online_data.md | Documents transactional rows, lifecycle states, validity masks, and segment/boundary sampling. |
Suppressed comments (1)
embodichain/lab/gym/envs/managers/record.py:198
max_env_numis not being enforced here:num_framesusesmax(rgb.shape[0], max_env_num), which will never cap the number of environments rendered whenrgb.shape[0] > max_env_num. This can accidentally record/merge far more env frames than intended (and increases CPU/GPU copy + video size). Usemin(...)so the recorder respectsmax_env_num.
rgb = data["color"]
num_frames = max(rgb.shape[0], max_env_num)
rgb = rgb[:num_frames]
rgb = self._draw_frames_into_one_image(rgb)[..., :3].cpu().numpy()
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 27 out of 27 changed files in this pull request and generated no new comments.
Suppressed comments (5)
tests/data_pipeline/test_online_data.py:134
forkserveris not available on all supported platforms (e.g., Windows), so this can make the test suite fail depending on the runner OS. Preferspawnhere or select the start method conditionally (e.g., fall back tospawnwhenforkserveris unavailable viamp.get_all_start_methods()).
engine._mp_ctx = mp.get_context("forkserver")
embodichain/lab/gym/envs/managers/record.py:139
env_idsis accepted but ignored in the synchronousrecord_camera_dataimplementation, while callers now pass per-resetenv_ids. This is confusing to API consumers and makes it unclear whether partial resets are supported; either (mandatory) implementenv_idssemantics (e.g., per-env frame buffers like the async recorder) or (alternative) remove the parameter and have the caller only invoke this recorder on full resets (or document/enforce thatenv_idsmust beNone/all envs).
def save_and_clear(self, env_ids: Union[torch.Tensor, None] = None) -> None:
embodichain/lab/gym/envs/managers/record.py:157
env_idsis accepted but ignored in the synchronousrecord_camera_dataimplementation, while callers now pass per-resetenv_ids. This is confusing to API consumers and makes it unclear whether partial resets are supported; either (mandatory) implementenv_idssemantics (e.g., per-env frame buffers like the async recorder) or (alternative) remove the parameter and have the caller only invoke this recorder on full resets (or document/enforce thatenv_idsmust beNone/all envs).
def discard_and_clear(self, env_ids: Union[torch.Tensor, None] = None) -> None:
"""Discard recorded frames without creating an episode video."""
self._frames = []
embodichain/data_pipeline/engine/data.py:991
sample_batch()holds the producer window lock while doing relatively expensive tensor ops (unfold, window filtering, and candidate selection) and then cloning the result. This can block the producer from advancing the write window (it acquires the same lock) and may reduce throughput under load. Consider shrinking the critical section by reading(lock_start, lock_end)under the lock, releasing it during window computation, and only re-acquiring to clone (with a re-check that(lock_start, lock_end)is unchanged), or by moving window precomputation to a cheaper structure.
with self._lock_index.get_lock():
lock_start: int = self._lock_index[0]
lock_end: int = self._lock_index[1]
if "valid" in self.shared_buffer.keys():
valid = self.shared_buffer["valid"].bool()
else:
# Schema-v1 buffers are one fully valid segment per row.
valid = torch.ones(
self.buffer_size,
max_steps,
dtype=torch.bool,
device=self.shared_buffer.device,
)
all_rows = torch.arange(self.buffer_size, device=valid.device)
is_locked = (all_rows >= lock_start) & (all_rows < lock_end)
valid_windows = valid.unfold(1, chunk_size, 1).all(dim=-1)
valid_windows[is_locked] = False
segment_ids = self.shared_buffer.get("segment_id", None)
if segment_ids is None:
segment_ids = torch.zeros_like(valid, dtype=torch.int64)
embodichain/data_pipeline/engine/data.py:1032
sample_batch()holds the producer window lock while doing relatively expensive tensor ops (unfold, window filtering, and candidate selection) and then cloning the result. This can block the producer from advancing the write window (it acquires the same lock) and may reduce throughput under load. Consider shrinking the critical section by reading(lock_start, lock_end)under the lock, releasing it during window computation, and only re-acquiring to clone (with a re-check that(lock_start, lock_end)is unchanged), or by moving window precomputation to a cheaper structure.
result = self.shared_buffer[row_indices[:, None], time_indices].clone()
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 27 out of 27 changed files in this pull request and generated 3 comments.
Suppressed comments (2)
tests/data_pipeline/test_online_data.py:134
- The test helper unconditionally requests the
forkserverstart method, which is unavailable on some platforms (notably Windows). Use a supported start method fallback (e.g., preferforkserverwhen available, elsespawn) so the test suite remains portable.
engine._mp_ctx = mp.get_context("forkserver")
embodichain/data_pipeline/engine/data.py:1004
- This computes
valid_windows(and potentiallysegment_windows) viaunfold(...).all(...)across the entire buffer on everysample_batchcall while holding the shared lock. For largebuffer_size/max_episode_steps, this can become a major CPU bottleneck and also delay the producer from advancing the lock window. Consider a more O(buffer_size) approach (e.g., track per-row valid lengths, and for segment/boundary modes precompute per-row boundary indices or segment spans) so sampling remains fast under training load.
all_rows = torch.arange(self.buffer_size, device=valid.device)
is_locked = (all_rows >= lock_start) & (all_rows < lock_end)
valid_windows = valid.unfold(1, chunk_size, 1).all(dim=-1)
valid_windows[is_locked] = False
segment_ids = self.shared_buffer.get("segment_id", None)
if segment_ids is None:
segment_ids = torch.zeros_like(valid, dtype=torch.int64)
if sampling_mode == "segment":
segment_windows = segment_ids.unfold(1, chunk_size, 1)
same_segment = (segment_windows == segment_windows[..., :1]).all(
dim=-1
) & (segment_windows[..., 0] >= 0)
valid_windows &= same_segment
elif sampling_mode == "boundary":
segment_windows = segment_ids.unfold(1, chunk_size, 1)
crosses_boundary = (
segment_windows[..., 1:] != segment_windows[..., :-1]
).any(dim=-1)
valid_windows &= crosses_boundary
| def _normalize_env_ids(self, env_ids: Union[torch.Tensor, None]) -> list[int]: | ||
| """Return recorder-local environment IDs for a transaction boundary.""" | ||
| if env_ids is None: | ||
| return list(range(self._num_envs)) | ||
| if isinstance(env_ids, torch.Tensor): | ||
| values = env_ids.reshape(-1).cpu().tolist() | ||
| else: | ||
| values = list(env_ids) | ||
| return [int(env_id) for env_id in values if int(env_id) < self._num_envs] |
| success_source = ( | ||
| success_fn() | ||
| if success_fn is not None | ||
| else last_info.get("success", True) | ||
| ) |
| if annotations is not None: | ||
| for annotation_key, feature_key in DEMO_FRAME_FEATURES.items(): | ||
| if annotation_key not in annotations: | ||
| continue | ||
| value = torch.as_tensor(annotations[annotation_key]).item() | ||
| frame[feature_key] = torch.tensor([int(value)], dtype=torch.int64) | ||
|
|
||
| return frame |
Description
Adds a segment-aware expert demonstration contract so one Gym episode can contain multiple semantic subtrajectories, such as several pick-and-place operations, while preserving legacy single-action-list tasks.
The change:
DemoSegment, structured episode/segment results, and one shared executor used byrun-envand the ODS simulation worker;meta/embodichain_episodes.jsonl, including cloned metadata in the async recorder;episode,segment, andboundarychunk policies;Existing tasks implementing
create_demo_action_list()remain compatible as one legacy segment. Direct callers that passnum_traj > 1togenerate_functionmust migrate that logic intocreate_demo_segments().No new dependencies are required.
Fixes: N/A
Type of change
generate_function(num_traj > 1)callersScreenshots
N/A — runtime/data-model change with no visual UI.
Validation
black==26.3.1on all changed Python filespytest -q tests/gym/envs tests/gym/utils/test_gym_utils.py tests/data_pipeline/test_online_data.py tests/lab/scripts/test_run_env.py— 291 passed, 3 skippedmake -C docs html— succeededThe repository-wide strict Sphinx build remains blocked by 671 existing warnings; the normal HTML build succeeds and the changed Markdown pages produced no path-specific warnings.
Checklist
black .equivalent on all changed Python files.