Repository navigation
Conversation
|
run-ci |
| """Record data at the end of env.step() when all the managers are processed. | ||
| """Record the completed physical transition. | ||
|
|
||
| In RL environments, this runs after rewards and terminations, before automatic reset. |
There was a problem hiding this comment.
too verbose, it's leaking alot of implementation details
| The number of successful episodes. | ||
| """ | ||
| if not hasattr(self, "_exported_successful_episode_count"): | ||
| if not hasattr(self, "_exported_counts"): |
| if cfg.buffer_initial_capacity < 1: | ||
| raise ValueError("buffer_initial_capacity must be positive.") | ||
| self._fields: list[_RecorderField] = [] | ||
| self._env_ids = torch.arange(env.num_envs, device=env.device) |
There was a problem hiding this comment.
I notice alot of torch, should we migrate to warp?
There was a problem hiding this comment.
i'd like to keep this pr small and left that to recorder rewrite
| if isinstance(env_ids, torch.Tensor): | ||
| env_ids = env_ids.tolist() | ||
| env_ids = self._env_ids | ||
| elif isinstance(env_ids, slice): |
There was a problem hiding this comment.
In previous PR we had fixed the id vs slice by allowing a union of the types and then letting torch handle it since torch can handle id and slice, so we don't need to convert, can we use the same here?
| recorder_manager.close() | ||
|
|
||
|
|
||
| def test_batched_episode_indices_and_ownership(monkeypatch): |
There was a problem hiding this comment.
tests can be cleaned up and minimized, don't think we need these many tests
| Operations. | ||
| """ | ||
|
|
||
| def reset(self, env_ids: Sequence[int] | slice | None = None) -> dict[str, torch.Tensor]: |
There was a problem hiding this comment.
can we also add mask support?
There was a problem hiding this comment.
this recorder will be rewriten at some point, I would just leave this pr speed fix minimum
| # update observations for recording if needed | ||
| self.obs_buf = self.observation_manager.compute() | ||
| self.recorder_manager.record_post_step() | ||
| self.recorder_manager.record_post_step() |
There was a problem hiding this comment.
this might be easy to miss. the assumption for pre and post seems different that pre obs is fresh but post is stale. Might prefer to have the behavior aligned to avoid problem. Or a cfg level switch to opt in/out
There was a problem hiding this comment.
same as above, i think recoder manager put a bit too many hooks into managers, a better appoach would be instead of we expose the hook, we just let user compose the step function, and they can add the record in wherever they think its appropriate. this pr will just mostly be speed up
|
run-ci |
[Medium impact] The PR appears safe to merge under its documented breaking API changes. SummarySeparates recorder capture from retained episode storage.
No actionable issues were established. Review checks were static; no tests or benchmarks were run. Acknowledged intentional changes: Diagram%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Recorder callback captures batch] --> B{Return field and values?}
B -->|No| C[Consumer copies published batch]
B -->|Yes| D[Append to batched field storage]
D --> E[Reset selected world indices]
D --> F[get_episode copies owned snapshot]
D --> G{File export enabled?}
G -->|Yes| H[Build snapshots and write dataset]
G -->|No| I[Finalize selected episodes without file writes]
Reviews (1) · Last reviewed commit: "Merge upstream dataset regression with c..." · Reviewed by Greptile |
There was a problem hiding this comment.
Isaac Lab Review Bot
Needs attention: Scope and over-engineering (1), Compatibility and deprecation (3), Style consistency (2), Test quality (1). Other reviewed categories look good.
- PR description: The benchmark targets a9fb50e/a2ba7a126, not this PR’s current revisions; rerun the documented workload against 1ab7bc6/6d112c52b and remove stale integration evidence.
- Design and architecture: Architecture looks good.
- Scope and over-engineering: Minimal implementation: extend RecorderManager, EpisodeData, and both RL step owners; no source modules were added, and unrelated HDF5 rewrites should be reverted; Suggested PR breakdown: 1) batched storage with compatibility bridges and recorder validation; 2) post-step lifecycle migration across both environments with focused observation tests.
- API: API changes look good.
- Compatibility and deprecation: Breaking changes: get_episode semantics, EpisodeData.add/pre_export removal, and post-step observation timing lack functional deprecation bridges, targeted warnings, removal guidance, and transition coverage.
- Implementation: Implementation looks good.
- Style consistency: New recorder methods omit required type annotations and Google-style parameter or return sections while replacing established environment terminology with “world”; restore the existing typed documentation pattern.
- Test quality: The AST-based storage-boundary test asserts imports, absent attributes, and private loop structure rather than observable behavior; delete it and retain the recorder behavior and performance-boundary tests.
Significant concerns. Posted 7 inline findings.
Automated review; human maintainers own approval decisions.
|
|
||
| Args: | ||
| env_id: The environment id. | ||
| def get_episode(self, env_id: int) -> EpisodeData: |
There was a problem hiding this comment.
🔴 Critical · Compatibility — get_episode contract changed without bridge
Callers now get a detached snapshot with tensor leaves, so mutations are silently lost and torch.stack(episode.data[key]) fails. Keep the live contract during deprecation and expose snapshots under a new name.
| """Check if the episode data is empty.""" | ||
| return not self._data | ||
|
|
||
| def add(self, key: str, value: torch.Tensor | dict, clone: bool = True): |
There was a problem hiding this comment.
🔴 Critical · Compatibility — EpisodeData collection API removed without deprecation
add() and pre_export() are public API; removal breaks external episode builders immediately. Keep both delegating to the new storage with a DeprecationWarning and removal guidance for the prescribed window.
| # update observations for recording if needed | ||
| self.obs_buf = self.observation_manager.compute() | ||
| self.recorder_manager.record_post_step() | ||
| self.recorder_manager.record_post_step() |
There was a problem hiding this comment.
🟡 Warning · Compatibility — Post-step recorders now see stale observations
Removing the refresh means existing post-step terms reading env.obs_buf silently record the previous step's observations. Gate the new timing behind an opt-in or deprecation cycle covering both env packages.
| """ | ||
| # Do nothing if no active recorder terms are provided | ||
| if len(self.active_terms) == 0: | ||
| """Set selected worlds' success flags from a tensor of shape (selected worlds,) or (selected worlds, 1).""" |
There was a problem hiding this comment.
🟡 Warning · Style Consistency — Public docstrings drop Args and Returns
set_success_to_episodes and get_episode fold parameters and returns into prose and say "world" where this file and API use environment and env_ids. Restore Google-style sections and existing vocabulary.
| Helper functions. | ||
| """ | ||
|
|
||
| def _reset_buffers(self, env_ids): |
There was a problem hiding this comment.
🟡 Warning · Style Consistency — New helpers omit type annotations
_reset_buffers, _RecorderField.append and _RecorderField.reset leave env_ids unannotated and omit -> None, unlike every sibling recorder method declaring Sequence[int] | slice | None.
| assert not bool(file["data/demo_10"].attrs["success"]) | ||
|
|
||
|
|
||
| def test_recorder_storage_boundary(): |
There was a problem hiding this comment.
🟡 Warning · Test Quality — Source-grep test asserts module structure
test_recorder_storage_boundary parses recorder_manager source to assert absent attributes, allowed imports and no loops in private _RecorderField.append. It guards no observable contract and fails on behavior-preserving refactors; delete it.
|
|
||
| # check if the dataset is created | ||
| assert os.path.exists(os.path.join(temp_dir, dataset_file_name)) | ||
| def test_create_dataset_file(tmp_path, monkeypatch): |
There was a problem hiding this comment.
🟡 Warning · Scope Complexity — Unrelated HDF5 test rewrite
Only create_test_episode needed updating for the removed add()/pre_export(); the tmp_path migration, loop rewrite and merge of test_create_resets_env_args_when_reusing_handler are drive-by churn that enlarge review surface. Revert them.
6d112c5 to
70d4e53
Compare
|
i reproduced a selector regression:
similarly, the removed |
addressed the regression |
|
run-ci |
Separate capture from episode storage in the existing
RecorderManager. Terms can stream batches to consumers without retaining episodes; retained data uses preallocated batched tensors with independent world indices.Capture, retention and export are independent: returning
(None, None)skips retention;EXPORT_NONEonly disables file export. Terms choose the captured fields, while consumers own subsequent processing.What makes insertion faster:
This removes per-world Python bookkeeping from the frequent append path. Snapshot/export work still occurs when requested.
Storage speed — RTX 5090:
These measure steady-state buffer insertion, excluding allocation growth, reset, export and simulation. Sampled stored episodes match byte-for-byte.
Breaking API:
get_episode()returns an owned snapshot. Collect throughadd_to_episodes();EpisodeData.add()/pre_export()and theRecorderTerm.close()hook are removed. RL post-step callbacks capture before reset; observation recording explicitly requests an observation group. Migration guidance is included.Validation: 10 recorder/lifecycle tests, CUDA initial-state capture, formatting/changelog checks, and the warnings-as-errors documentation build pass. Coverage includes reordered/empty tuple IDs, preserved slices, snapshot ownership, export modes, and unchanged policy noise/history/RNG. Four mutation checks confirm the selector regression is caught. HDF5/replay checks passed during the rebase; full simulator integration remains unavailable locally without Isaac Sim.
Benchmark methodology and reproduction
Episode-buffer insertion is 697× faster at 4,096 environments in a fresh comparison of base
0ca25c56and measured headc0931607. These are storage-only timings; the factor does not describe simulation or training throughput.Measured on an NVIDIA GeForce RTX 5090 (driver 580.178.04, PyTorch 2.11.0+cu130, CUDA 13.0) on 2026-10-29. The benchmark uses identical seven-field payloads: four FP32 tensors with widths 37, 37, 19 and 72, and three Boolean fields. Payload size is 169,728 bytes at 256 worlds and 2,715,648 bytes at 4,096 worlds. Each result is the median of five randomized paired trials, with 20 appends per trial after warmup. Wall time is synchronized before and after each trial and includes Python and GPU work. The tested GPU had no competing compute jobs. The CPU host was shared, including a documentation build; other compute jobs used the second GPU.
The batched buffer starts with capacity 32 and reuses its allocation after reset. Timing covers steady-state insertion only: capture, physics, reset, allocation growth, episode materialization and disk export are outside this measurement. Sampled 20-frame episodes from the first, middle and last world match byte-for-byte at both batch sizes.
Where the gain comes from: the previous implementation clones each field's batch, then creates a tensor view and traverses/appends to a Python episode dictionary/list for every world. The new implementation writes all worlds into a preallocated tensor using per-world indices, once per field. Per-world episode materialization moves to
get_episode()/export, and reset reuses storage. Streaming consumers can also skip episode retention, and physical-state recorder terms can avoid an unnecessary observation pass.Use the normal Isaac Lab GPU environment with this PR's head checked out. Save the Python script below as
/tmp/benchmark_recorder.py, then run:The script loads the parent recorder and its original
EpisodeDataalongside the checked-out recorder, uses the same dependencies and payloads for both, synchronizes CUDA, randomizes trial order, checks stored bytes and writes every timing sample. There is no simulation in this storage microbenchmark.Contribution checklist