Add opt-in async OVRTX rendering#6484
Draft
pv-nvidia wants to merge 16 commits into
Draft
Conversation
pv-nvidia
force-pushed
the
pv/ovrtx-async-slot-buffers
branch
6 times, most recently
from
July 13, 2026 19:26
83b23b3 to
926879d
Compare
pv-nvidia
force-pushed
the
pv/ovrtx-async-slot-buffers
branch
from
July 13, 2026 21:58
926879d to
01afef6
Compare
pv-nvidia
commented
Jul 15, 2026
pv-nvidia
commented
Jul 15, 2026
pv-nvidia
commented
Jul 15, 2026
pv-nvidia
commented
Jul 15, 2026
pv-nvidia
commented
Jul 15, 2026
pv-nvidia
commented
Jul 15, 2026
pv-nvidia
commented
Jul 15, 2026
pv-nvidia
commented
Jul 15, 2026
pv-nvidia
commented
Jul 15, 2026
pv-nvidia
commented
Jul 15, 2026
pv-nvidia
commented
Jul 15, 2026
pv-nvidia
commented
Jul 15, 2026
pv-nvidia
commented
Jul 15, 2026
pv-nvidia
commented
Jul 15, 2026
pv-nvidia
force-pushed
the
pv/ovrtx-async-slot-buffers
branch
from
July 15, 2026 15:29
2474442 to
5c0b779
Compare
pv-nvidia
commented
Jul 15, 2026
pv-nvidia
commented
Jul 15, 2026
pv-nvidia
commented
Jul 15, 2026
pv-nvidia
commented
Jul 15, 2026
pv-nvidia
commented
Jul 15, 2026
pv-nvidia
commented
Jul 15, 2026
pv-nvidia
force-pushed
the
pv/ovrtx-async-slot-buffers
branch
from
July 17, 2026 11:33
93cfe11 to
318828f
Compare
pv-nvidia
commented
Jul 17, 2026
pv-nvidia
commented
Jul 17, 2026
pv-nvidia
commented
Jul 17, 2026
| @property | ||
| def is_async_rendering_enabled(self) -> bool: | ||
| """Whether asynchronous rendering is enabled (``async_num_buffers >= 1``).""" | ||
| return self._async.enabled |
Contributor
Author
There was a problem hiding this comment.
So this becomes self._async is not None
pv-nvidia
commented
Jul 17, 2026
pv-nvidia
commented
Jul 17, 2026
pv-nvidia
commented
Jul 17, 2026
r-schmitt
requested changes
Jul 20, 2026
| renderer_cfg = env_cfg.tiled_camera.renderer_cfg | ||
| except AttributeError as exc: | ||
| raise ValueError( | ||
| "--ovrtx_render_latency_frames requires a task with env_cfg.tiled_camera.renderer_cfg" |
Collaborator
There was a problem hiding this comment.
do we want to raise a ValueError here? it seems like we could instead log a warning about the argument's compatibility instead and ignore the arg when its not applicable
Add OVRTXRendererCfg.async_render (default False, preserves blocking behavior) and async_num_buffers (default 2). When enabled, render() enqueues step_async into a deque ring and drains the oldest op (wait->fetch->extract) once the ring is full, delivering camera outputs with (async_num_buffers-1) frames of latency. Race safety: - wait->fetch->extract kept atomic per op (buffer fully produced before map()). - Explicit refs to all in-flight Operation/PendingFetch (un-waited __del__ blocks); ring fully drained on cleanup and before any binding write. - Ring drained before update_transforms/update_camera overwrite shared OVRTX AttributeBindings, preventing an in-flight GPU op from reading stale/overwritten binding device buffers. Benchmarks (RGB cartpole, warmup10/steps60): depth-2 gives 1.23-1.48x SPS over sync (best of d1/d2/d3; d3 no better than d2). Correctness: async d2 output equals sync (1-frame aligned) at SSIM ~0.9997, ~0% pixel diff (golden gates: <=1%, SSIM>=0.985). Also: OVRTX_BENCH-gated per-stage timers, OVRTX_FORCE_ASYNC/OVRTX_ASYNC_NUM_BUFFERS test override, scripts/benchmarks/benchmark_ovrtx_render.py harness, and OVRTX_ASYNC_MINDMAP.md design notes. gitignore core.* and bench_out/.
…depth Stage async OVRTX binding writes in per-slot camera/object buffers and decouple the staging-slot count from the render ring depth. Slot buffers: keep persistent AttributeBindings but write camera/object transforms with AttributeBinding.write_async(..., data_access=DataAccess.ASYNC) into slot-owned Warp buffers, waiting only before a slot is reused. This drops update_transforms() from tens of ms to ~0.3 ms. Decoupling: async_num_buffers now controls only render/fetch pipeline depth (and observation latency); a new async_num_slots (default 2, min 2, env override OVRTX_ASYNC_NUM_SLOTS) controls the transform staging buffers. DataAccess.ASYNC writes reference caller-owned slot buffers only until the write op completes (not until the render completes), so two staging slots suffice regardless of ring depth. This lets ring depth grow to absorb render/fetch jitter without adding transform-state latency. Validated: golden RGB Cartpole passes at ring depth 3 with 2 slots. High-rep benchmark (3 reps, warmup 15/steps 120) shows depth 3 vs depth 2 at 2 slots scales with env count: 1.00x (4) / 1.02x (64) / 1.05x (256) / 1.10x (1024).
Remove the OVRTX_BENCH per-stage timers and the standalone benchmark_ovrtx_render.py script. Performance is measured externally (timing env.step on the kitless CartpoleCameraEnv), so the renderer no longer carries measurement code. The async feature itself is unchanged; the OVRTX_ASYNC_NUM_BUFFERS env override is kept for tests.
- Extract async render state (ring + staging slots) into _AsyncRenderState; renderer holds a single self._async instead of scattered _async_* fields. - Type the async ring with _AsyncRenderOp instead of a dict. - Add is_async_rendering_enabled property (replaces _async_render field). - Fix OVRTX_ASYNC_NUM_BUFFERS='0' being ignored (empty-vs-zero check). - update_transforms/update_camera: guard on 'if slot is None' with sync branch first; move the Newton-backend comment above its block. - Stop swallowing exceptions in render(); restore original propagate behavior. Keep best-effort drain only on the teardown path, with a comment.
- AsyncRenderState owns num_envs: reset_slots(num_envs) records the camera count (0 == unbinding); begin_slot_update()/get_update_slot() take no args. Renderer no longer carries a _num_envs field. - Drop write_binding_async's 'if binding is None' guard; both call sites are already under a binding-not-None check, so it was dead. - Remove the write-only _async_last_render_data field entirely.
The ring is now fully private to AsyncRenderState; the renderer never touches it directly. - enqueue_render_op(op): append op, end the current frame's slot, return depth. - try_dequeue_render_op(): wait/fetch/pop the oldest op, return its products (or None if the ring is empty). Owns the entry's lifecycle. - has_pending_ops(): whether anything is still queued. - Renderer: _try_drain_one_async() returns whether it drained; _drain_all_async() loops on it best-effort. Removed .ring property, _drain_one_async, _drain_async_ring, and clear_current_slot.
Keep the changelog focused on user-visible async rendering behavior.
Extract frame execution (transform staging plus step dispatch and consumption) from OVRTXRenderer into a _RenderStrategy hierarchy so the renderer's call sites no longer branch on sync vs async. _SyncRenderStrategy writes transforms straight into OVRTX and consumes each step inline; _AsyncRenderStrategy pipelines steps and double-buffers transform staging. The async object is created only when async is enabled; otherwise the renderer holds the sync strategy. Per review feedback, replace the OVRTXRendererCfg.async_num_buffers integer with an async_rendering on/off boolean. The render pipeline depth is fixed at two (one frame of camera latency); exposing per-frame latency control is deferred to a future Isaac Lab-side setting rather than a renderer-level knob. Rename the OVRTX_ASYNC_NUM_BUFFERS test override to OVRTX_ASYNC_RENDERING. Update the runtime benchmark to match: replace --ovrtx_render_latency_frames with a boolean --ovrtx_async_rendering, and warn-and-ignore instead of raising when the task has no OVRTX renderer config. Restore the run_runtime_loop docstring detail that the warmup change had dropped. Refresh the changelog fragment, docs, and benchmark smoke tests.
pv-nvidia
force-pushed
the
pv/ovrtx-async-slot-buffers
branch
2 times, most recently
from
July 21, 2026 10:08
b0dd468 to
934b3c7
Compare
pv-nvidia
force-pushed
the
pv/ovrtx-async-slot-buffers
branch
from
July 21, 2026 10:13
934b3c7 to
2b03f57
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Adds an opt-in asynchronous render path to the OVRTX renderer. Rendering can overlap with simulation and Python work, improving throughput. It is off by default (
async_rendering=False), so existing behavior is unchanged.What this adds
A single on/off switch,
OVRTXRendererCfg.async_rendering(defaultFalse):False: synchronous rendering (unchanged blocking behavior).True: asynchronous rendering.render()submits the render and returns immediately, so rendering overlaps with simulation and Python work; camera outputs arrive one frame later. The render pipeline depth is fixed at two (one frame of camera latency); exposing per-frame latency control is deferred to a future Isaac Lab-side setting rather than a renderer-level knob.Internally the renderer delegates frame execution to a small
_RenderStrategyhierarchy:_SyncRenderStrategywrites transforms straight into OVRTX and consumes each step inline, while_AsyncRenderStrategypipelines steps and double-buffers transform staging. The async object is created only when async is enabled.For tests, the
OVRTX_ASYNC_RENDERINGenvironment variable overridesasync_rendering(0/false/offdisable, any other non-empty value enables).The runtime benchmark now supports untimed warmup frames and a boolean
--ovrtx_async_renderingflag, so sync/async camera runs can usescripts/benchmarks/runtime.pydirectly.CI support also ensures a per-commit ECR image tag exists when the dependency cache is reused locally, so downstream package-test jobs can pull the image across self-hosted runners.
Correctness
The RGB Cartpole golden-image test passes on the async path (
test_rendering_cartpole_kitless.py::test_rendering_cartpole_kitless[newton-ovrtx-rgb]). Async output matches the sync path (1 frame apart) within the test's SSIM / pixel-difference tolerances.The OVRTX clone-plan unit tests also pass after the async-state refactor (
source/isaaclab_ov/test/test_ovrtx_clone_plan.py, 14 tests), including the backend-free_initialize_from_specpath used for combined-stage dump coverage.Benchmarks
Measured with the official runtime benchmark (
scripts/benchmarks/runtime.py) using kitless OVRTX RGB camera tasks,--warmup_frames 30, and--num_frames 200. SPS = environment steps/sec (num_envs / mean_step_time_s). These are single local runs with schema output, so treat them as throughput signals rather than a multi-repetition benchmark suite. "async" is the one-frame-latency asynchronous path.Benchmark provenance for the runs below:
318828f57e0f9b85c760caeae3630c79b3e604ce0.3.0.312915physics=newton_mjwarp renderer=ovrtx presets=rgbCartpole camera
Task:
Isaac-Cartpole-Camera-DirectCommand used for each row (add
--ovrtx_async_renderingfor the async column,--no-ovrtx_async_renderingfor sync):python scripts/benchmarks/runtime.py \ --task Isaac-Cartpole-Camera-Direct \ --num_envs <64|256|1024> \ --num_frames 200 \ --warmup_frames 30 \ --benchmark_formatter schema \ --output_path ./results/cartpole_<envs>_<sync_or_async> \ --headless \ --ovrtx_async_rendering \ physics=newton_mjwarp renderer=ovrtx presets=rgbShadow Hand camera benchmark
Task:
Isaac-Reorient-Cube-Shadow-Camera-Benchmark-DirectCommand used for each row (add
--ovrtx_async_renderingfor the async column,--no-ovrtx_async_renderingfor sync):python scripts/benchmarks/runtime.py \ --task Isaac-Reorient-Cube-Shadow-Camera-Benchmark-Direct \ --num_envs <64|256|1024> \ --num_frames 200 \ --warmup_frames 30 \ --benchmark_formatter schema \ --output_path ./results/shadow_hand_<envs>_<sync_or_async> \ --headless \ --ovrtx_async_rendering \ physics=newton_mjwarp renderer=ovrtx presets=rgbEnabling async adds one frame of camera latency.
Type of change
Screenshots
Not applicable — performance/API change with no visual UI. Async RGB output is validated against the sync path via the golden correctness test above (SSIM ~0.9997, ~0% pixel diff).
Checklist
pre-commitchecks with./isaaclab.sh --formatsource/<pkg>/changelog.d/for every touched package (do not editCHANGELOG.rstor bumpextension.toml— CI handles that)CONTRIBUTORS.mdor my name already exists there