Skip to content

Feature/nvblox next datagen slim#883

Draft
david-tingdahl-nvidia wants to merge 4 commits into
mainfrom
feature/nvblox_next_datagen_slim
Draft

Feature/nvblox next datagen slim#883
david-tingdahl-nvidia wants to merge 4 commits into
mainfrom
feature/nvblox_next_datagen_slim

Conversation

@david-tingdahl-nvidia

Copy link
Copy Markdown
Collaborator

Summary

Short description of the change (max 50 chars)

Detailed description

  • What was the reason for the change?
  • What has been changed?
  • What is the impact of this change?

# leaves camera buffers stale on the first frame of every episode after the
# first, so the previous episode's final rendered frame leaks in (corrupting
# RGB/depth/flow during datagen). See IsaacLab-Arena #339.
num_rerenders_on_reset: int = 5

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Warning — This flips the reset behavior for every Arena environment, not just datagen. IsaacLabArenaManagerBasedRLEnvCfg is the shared base for all RL/eval envs, so every reset now triggers 5 extra RTX rerenders for everyone — a non-trivial per-reset cost on any camera-enabled run — even though the comment scopes the motivation entirely to datagen ("corrupting RGB/depth/flow during datagen").

The blast-radius/conservative-defaults invariant is that new behavior built for a narrow use case should be opt-in rather than silently changing the default path. Could this instead be applied only on the datagen cfg (e.g. inside prepare_env_cfg_for_datagen, which already mutates the cfg for exactly this purpose) rather than the global base?

Also, the comment says "Force at least one RTX sensor refresh" but the value is 5 — is 5 needed, or would a smaller number (1?) suffice? Worth a NOTE explaining why 5.

@arena-review-bot

Copy link
Copy Markdown
Contributor

🤖 Isaac Lab-Arena Review Bot

Summary

This PR wires optional datagen collection into the eval runner via a duck-typed collector_provider argument, keeping the core decoupled from the (now externally-vendored) datagen package. It disables the env's in-step() auto-reset for datagen so the rollout loop drives clean episode boundaries and resets (avoiding stale first-frame renders), classifies each episode outcome, and threads a per-episode frame cap. The decoupling design is clean and the new outcome logic is unit-tested. One concern below.

Design, Boundaries & Scope

🟡 num_rerenders_on_reset = 5 on the shared base cfg changes the default path for everyone. It's added to IsaacLabArenaManagerBasedRLEnvCfg (the base for all RL/eval envs), so every reset on every camera-enabled run now pays 5 extra RTX rerenders — yet the motivation is entirely datagen. Per the conservative-defaults invariant, narrow-use behavior should be opt-in; consider applying it inside prepare_env_cfg_for_datagen (which already mutates the datagen cfg) instead of globally. Also worth clarifying why 5 when the comment says "at least one". (Inline on the changed line.)

Findings

🟡 Warning: environments/isaaclab_arena_manager_based_env_cfg.py:88 — global num_rerenders_on_reset = 5 default; see above.

Test Coverage

episode_outcome.classify_outcome gets a focused sim-free unit test (test_episode_outcome.py) covering all three branches — good. The larger datagen rollout path (_run_datagen_rollout, prepare_env_cfg_for_datagen) is not directly tested here, but it requires a live sim and the concrete collector lives in the external nvblox_next package, so that's reasonable for this slice. test_eval_runner.py is correctly updated for load_env's new tuple return.

Verdict

Minor fixes needed — the datagen wiring is well-structured; please just confirm the global num_rerenders_on_reset default is intended for all envs (or scope it to datagen).

@greptile-apps

greptile-apps Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR wires a datagen collection pipeline into the eval runner, allowing dedicated camera-based HDF5 recording during policy rollouts. It disables the environment's auto-reset during datagen to prevent stale frames leaking across episodes, adds a num_rerenders_on_reset default of 5, and introduces per-job and top-level datagen config blocks.

  • Datagen rollout loop (_run_datagen_rollout): disables env auto-reset, manually evaluates stashed termination terms, and drives explicit env.reset() between episodes so the first frame of each episode is clean.
  • prepare_env_cfg_for_datagen: strips termination/metrics/recorder terms from the env cfg before build, stashing non-timeout terms for manual evaluation.
  • episode_outcome.py: new helper mapping termination term names to \"success\" / \"timeout\" / \"failure\" outcome labels, fully unit-tested.

Confidence Score: 3/5

The rollout and cleanup paths are well-structured, but two gaps in the datagen lifecycle could silently corrupt or drop collected data.

The start_run()/finish_run() hooks are gated on datagen_defaults is not None, so any deployment that places datagen config only in per-job blocks bypasses both — leaving run-level initialization unexecuted and the run never finalized. Separately, in num_steps mode the datagen loop breaks out before calling collector.end_episode(), so the last in-flight episode's frames are recorded but the episode is never closed.

eval_runner.py (lifecycle hook conditions) and policy_runner.py (_run_datagen_rollout step-limit exit path).

Important Files Changed

Filename Overview
isaaclab_arena/evaluation/eval_runner.py Adds datagen pipeline integration: load_env now returns (env, reset_terms), main() accepts a collector_provider, and jobs wire up per-job collectors. Two issues: start_run()/finish_run() lifecycle hooks are guarded by datagen_defaults is not None but per-job-only datagen configs skip them; recorder filename is set then immediately nulled out in the datagen path.
isaaclab_arena/evaluation/policy_runner.py Adds prepare_env_cfg_for_datagen, _manual_episode_done, and _run_datagen_rollout. When num_steps mode breaks out mid-episode, collector.end_episode() is never called, leaving the last partial episode un-terminated.
isaaclab_arena/evaluation/episode_outcome.py New helper mapping termination term names to outcome labels (success/timeout/failure); simple and well-tested.
isaaclab_arena/evaluation/eval_runner_cli.py Adds --datagen-description CLI argument for recording dataset provenance; straightforward addition.
isaaclab_arena/evaluation/job_manager.py Adds optional datagen dict field to Job; correctly read from JSON config and initialized to None by default.
isaaclab_arena/environments/isaaclab_arena_manager_based_env_cfg.py Sets num_rerenders_on_reset = 5 to flush camera buffers after reset, preventing stale-frame leakage across episodes.
isaaclab_arena/tests/test_episode_outcome.py New unit tests for classify_outcome; covers all three outcome branches.
isaaclab_arena/tests/test_eval_runner.py Updates test call site for the new (env, reset_terms) return signature from load_env.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant M as main()
    participant CP as collector_provider
    participant LE as load_env()
    participant RP as rollout_policy()
    participant DR as _run_datagen_rollout()
    participant C as collector

    M->>CP: "start_run() [only if datagen_defaults != None]"
    loop Per Job
        M->>LE: "load_env(disable_auto_reset=is_datagen)"
        LE->>LE: prepare_env_cfg_for_datagen() stash reset_terms
        LE-->>M: (env, datagen_reset_terms)
        M->>CP: create(job_name, job_datagen, env) collector
        M->>RP: rollout_policy(collector, datagen_reset_terms)
        RP->>DR: _run_datagen_rollout(...)
        loop Per Step
            DR->>DR: env.step()
            DR->>C: on_step()
            alt Episode end / cap reached
                DR->>C: end_episode(outcome)
                DR->>C: resample_cameras()
                DR->>DR: env.reset()
            end
        end
        DR-->>RP: return
        RP->>C: finalize(env)
        M->>C: close(env)
        M->>CP: on_job_finished()
    end
    M->>CP: "finish_run() [only if datagen_defaults != None]"
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant M as main()
    participant CP as collector_provider
    participant LE as load_env()
    participant RP as rollout_policy()
    participant DR as _run_datagen_rollout()
    participant C as collector

    M->>CP: "start_run() [only if datagen_defaults != None]"
    loop Per Job
        M->>LE: "load_env(disable_auto_reset=is_datagen)"
        LE->>LE: prepare_env_cfg_for_datagen() stash reset_terms
        LE-->>M: (env, datagen_reset_terms)
        M->>CP: create(job_name, job_datagen, env) collector
        M->>RP: rollout_policy(collector, datagen_reset_terms)
        RP->>DR: _run_datagen_rollout(...)
        loop Per Step
            DR->>DR: env.step()
            DR->>C: on_step()
            alt Episode end / cap reached
                DR->>C: end_episode(outcome)
                DR->>C: resample_cameras()
                DR->>DR: env.reset()
            end
        end
        DR-->>RP: return
        RP->>C: finalize(env)
        M->>C: close(env)
        M->>CP: on_job_finished()
    end
    M->>CP: "finish_run() [only if datagen_defaults != None]"
Loading

Comments Outside Diff (1)

  1. isaaclab_arena/evaluation/eval_runner.py, line 51-57 (link)

    P2 Recorder filename set then immediately discarded

    env_cfg.recorders.dataset_filename is assigned at line 53, then prepare_env_cfg_for_datagen sets env_cfg.recorders = None at line 57 (when disable_auto_reset is True), so the filename assignment is always wasted in the datagen path. This is harmless today but suggests the ordering could be reversed for clarity.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Reviews (1): Last reviewed commit: "refactor(eval): remove vendored isaaclab..." | Re-trigger Greptile

Comment on lines +267 to +268
if collector_provider is not None and datagen_defaults is not None:
collector_provider.start_run(eval_jobs_config, args_cli.datagen_description, args_cli.device)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 start_run() / finish_run() skipped for per-job-only datagen configs

Both lifecycle hooks are guarded by datagen_defaults is not None, but is_datagen (line 309) can be True when only a per-job datagen block exists and the top-level datagen key is absent. In that scenario collector_provider.create() and collector_provider.on_job_finished() run, but start_run() is never called — skipping any run-level initialization (output directories, manifest creation, etc.) — and finish_run() is never called, leaving the run incomplete.

Comment on lines +140 to +153
if num_steps is not None:
pbar.update(1)
if num_steps_completed >= num_steps:
break

ended_by = _manual_episode_done(env, reset_terms)
hit_cap = steps_in_episode >= max_episode_length
if ended_by is not None or hit_cap:
collector.end_episode(env, outcome=classify_outcome(ended_by))
num_episodes_completed += 1
if num_episodes is not None:
pbar.update(1)
if num_episodes_completed >= num_episodes:
break

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Partial episode not terminated when num_steps limit is reached

In _run_datagen_rollout, the num_steps check (line 142-143) breaks out of the loop before the episode-end check (line 145). If the step limit is reached mid-episode, collector.on_step() has been called for each frame of the current episode but collector.end_episode() is never called — only collector.finalize(env) runs later via rollout_policy(). Unless finalize internally closes the in-flight episode, the last partial episode's data is silently dropped or left in an inconsistent state.

@david-tingdahl-nvidia
david-tingdahl-nvidia marked this pull request as draft July 10, 2026 07:08
Comment thread isaaclab_arena/evaluation/eval_runner.py Outdated
Comment thread isaaclab_arena/evaluation/eval_runner.py Outdated
Comment thread isaaclab_arena/evaluation/episode_outcome.py Outdated
@david-tingdahl-nvidia
david-tingdahl-nvidia force-pushed the feature/nvblox_next_datagen_slim branch 2 times, most recently from 498d3bf to 89c379a Compare July 10, 2026 10:02
Inject nvblox_next datagen collection into eval_runner and policy_runner via an
optional collector_provider argument. The provider builds a per-job collector from
a job's datagen config block; rollouts then record per-step data, per-episode
outcomes, and a run-level provenance manifest. Adds episode outcome
classification, a --datagen-description CLI flag, and a max_recorded_frames cap.

Signed-off-by: David Tingdahl <dtingdahl@nvidia.com>
@david-tingdahl-nvidia
david-tingdahl-nvidia force-pushed the feature/nvblox_next_datagen_slim branch from 89c379a to 5afe2b1 Compare July 10, 2026 11:20
Define the collector interface the evaluation runners drive (on_step,
on_episode_end, finalize, close) as an ABC in
evaluation/datagen_collector.py, and type rollout_policy's collector
argument against it. Implementations live in the collecting package;
the runners no longer rely on duck typing.

on_episode_end doubles as the pre-reset hook for preparing the next
episode's cameras, since the runner calls it before the explicit
env.reset() whose RTX rerenders flush new camera poses.

Signed-off-by: David Tingdahl <dtingdahl@nvidia.com>
Comment thread isaaclab_arena/evaluation/datagen_collector.py Outdated
Comment thread isaaclab_arena/evaluation/policy_runner.py Outdated
Comment thread isaaclab_arena/evaluation/policy_runner.py Outdated
eval_runner now takes a datagen_run_manager typed against the new
DatagenRunManagerBase ABC. Injecting the manager enables collection for
every job (start_run asserts the config configures it), so the per-job
is_enabled hook is gone. The manager owns the collector it creates per
job: on_job_finished(job, env) closes and records it, so the runner no
longer manages collector lifetime or passes the collector back. The
per-episode recording cap moves onto the collector interface as
cap_episode_length with a no-cap default, and episode outcomes are typed
as the EpisodeOutcome literal that classify_outcome produces.

Signed-off-by: David Tingdahl <dtingdahl@nvidia.com>
Comment thread isaaclab_arena/evaluation/eval_runner.py Outdated
Comment thread isaaclab_arena/evaluation/eval_runner.py Outdated
Comment thread isaaclab_arena/evaluation/datagen_collector.py Outdated
Comment thread isaaclab_arena/evaluation/eval_runner.py Outdated
Comment thread isaaclab_arena/evaluation/datagen_collector.py
Comment thread isaaclab_arena/evaluation/eval_runner.py Outdated
Comment thread isaaclab_arena/evaluation/eval_runner.py Outdated
Comment thread isaaclab_arena/evaluation/eval_runner.py Outdated
Comment thread isaaclab_arena/evaluation/policy_runner.py
Comment thread isaaclab_arena/evaluation/policy_runner.py Outdated
Comment thread isaaclab_arena/evaluation/policy_runner.py Outdated
Comment thread isaaclab_arena/evaluation/policy_runner.py Outdated
- Collapse the datagen rollout onto the standard auto-reset loop; drop
  _run_datagen_rollout, _manual_episode_done, and the termination stashing. On a
  done step the finished episode is closed before the post-reset obs is recorded,
  so the leaked reset frame lands in the next episode where the collector's
  skip_initial_frames drops it.
- Add NoOpDatagenCollector / NoOpDatagenRunManager and default to them, so the
  runners no longer special-case the no-collection path.
- Move env-cfg prep (drop metric recorders) and the camera requirement onto the
  run manager (prepare_env_cfg, needs_cameras); remove the unused
  cap_episode_length hook.
- Delete episode_outcome (classify_outcome unused after the rewrite).

Signed-off-by: David Tingdahl <dtingdahl@nvidia.com>
@david-tingdahl-nvidia
david-tingdahl-nvidia force-pushed the feature/nvblox_next_datagen_slim branch from 49029f8 to 282e47d Compare July 19, 2026 15:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant