Skip to content

Add opt-in async OVRTX rendering#6484

Draft
pv-nvidia wants to merge 16 commits into
isaac-sim:developfrom
pv-nvidia:pv/ovrtx-async-slot-buffers
Draft

Add opt-in async OVRTX rendering#6484
pv-nvidia wants to merge 16 commits into
isaac-sim:developfrom
pv-nvidia:pv/ovrtx-async-slot-buffers

Conversation

@pv-nvidia

@pv-nvidia pv-nvidia commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

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 (default False):

  • 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 _RenderStrategy hierarchy: _SyncRenderStrategy writes transforms straight into OVRTX and consumes each step inline, while _AsyncRenderStrategy pipelines steps and double-buffers transform staging. The async object is created only when async is enabled.

For tests, the OVRTX_ASYNC_RENDERING environment variable overrides async_rendering (0/false/off disable, any other non-empty value enables).

The runtime benchmark now supports untimed warmup frames and a boolean --ovrtx_async_rendering flag, so sync/async camera runs can use scripts/benchmarks/runtime.py directly.

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_spec path 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:

  • Git commit: 318828f57e0f9b85c760caeae3630c79b3e604ce
  • OVRTX: 0.3.0.312915
  • Selectors: physics=newton_mjwarp renderer=ovrtx presets=rgb

Cartpole camera

Task: Isaac-Cartpole-Camera-Direct

envs sync mean ms sync SPS async mean ms async SPS speedup
64 14.281 4,481.4 9.761 6,556.7 1.46x
256 18.733 13,665.6 13.279 19,277.9 1.41x
1024 31.460 32,549.0 21.972 46,605.3 1.43x

Command used for each row (add --ovrtx_async_rendering for the async column, --no-ovrtx_async_rendering for 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=rgb

Shadow Hand camera benchmark

Task: Isaac-Reorient-Cube-Shadow-Camera-Benchmark-Direct

envs sync mean ms sync SPS async mean ms async SPS speedup
64 22.601 2,831.7 19.294 3,317.1 1.17x
256 30.151 8,490.5 24.017 10,659.3 1.26x
1024 53.297 19,212.9 36.996 27,679.0 1.44x

Command used for each row (add --ovrtx_async_rendering for the async column, --no-ovrtx_async_rendering for 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=rgb

Enabling async adds one frame of camera latency.

Type of change

  • New feature (non-breaking change which adds functionality)

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

  • I have read and understood the contribution guidelines
  • I have run the pre-commit checks with ./isaaclab.sh --format
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • I have added a changelog fragment under source/<pkg>/changelog.d/ for every touched package (do not edit CHANGELOG.rst or bump extension.toml — CI handles that)
  • I have added my name to the CONTRIBUTORS.md or my name already exists there

@github-actions github-actions Bot added the isaac-lab Related to Isaac Lab team label Jul 13, 2026
@pv-nvidia
pv-nvidia force-pushed the pv/ovrtx-async-slot-buffers branch 6 times, most recently from 83b23b3 to 926879d Compare July 13, 2026 19:26
@pv-nvidia pv-nvidia changed the title Pv/ovrtx async slot buffers Add opt-in async OVRTX rendering Jul 13, 2026
@pv-nvidia
pv-nvidia force-pushed the pv/ovrtx-async-slot-buffers branch from 926879d to 01afef6 Compare July 13, 2026 21:58
Comment thread source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py Outdated
Comment thread source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py Outdated
Comment thread source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py Outdated
Comment thread source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py Outdated
Comment thread source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py Outdated
Comment thread source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py Outdated
Comment thread source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py Outdated
Comment thread source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py Outdated
Comment thread source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py Outdated
Comment thread source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py Outdated
Comment thread source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py Outdated
Comment thread source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py Outdated
Comment thread source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py Outdated
Comment thread source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py Outdated
@pv-nvidia
pv-nvidia force-pushed the pv/ovrtx-async-slot-buffers branch from 2474442 to 5c0b779 Compare July 15, 2026 15:29
Comment thread source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py Outdated
Comment thread source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py Outdated
Comment thread source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py Outdated
Comment thread source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py Outdated
Comment thread source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py Outdated
Comment thread source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py Outdated
@pv-nvidia
pv-nvidia force-pushed the pv/ovrtx-async-slot-buffers branch from 93cfe11 to 318828f Compare July 17, 2026 11:33
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Jul 17, 2026
Comment thread source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py
Comment thread source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py Outdated
@property
def is_async_rendering_enabled(self) -> bool:
"""Whether asynchronous rendering is enabled (``async_num_buffers >= 1``)."""
return self._async.enabled

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

So this becomes self._async is not None

Comment thread source/isaaclab/isaaclab/test/benchmark/stepping.py
Comment thread source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py Outdated
Comment thread source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py Outdated
Comment thread scripts/benchmarks/runtime.py Outdated
Comment thread scripts/benchmarks/runtime.py Outdated
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"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Comment thread scripts/benchmarks/runtime.py Outdated
Horde and others added 15 commits July 21, 2026 08:46
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
pv-nvidia force-pushed the pv/ovrtx-async-slot-buffers branch 2 times, most recently from b0dd468 to 934b3c7 Compare July 21, 2026 10:08
@pv-nvidia
pv-nvidia force-pushed the pv/ovrtx-async-slot-buffers branch from 934b3c7 to 2b03f57 Compare July 21, 2026 10:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation infrastructure isaac-lab Related to Isaac Lab team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants