Skip to content

Commit 8abbc62

Browse files
authored
perf: Order OVRTX render-var reads on the consuming stream, and block the host on Linux (#7074)
## Description Reading an OVRTX render var has to be ordered against render completion. On Linux, blocking the calling thread on the render-completion event measures faster end to end than a GPU-side wait, so that is now what Linux does. Measured across three camera tasks at 256/1024/4096 envs, this is +14% to +73% throughput (mean +36%), and restores parity with ovrtx 0.3 on every case measured. Other platforms order the read on the consuming Warp stream. `ISAAC_LAB_OVRTX_DISABLE_LINUX_CUDA_CPU_SYNC=1` puts Linux on that ordering too — an escape hatch for when the trade-off changes; it is not currently faster. All mapping sites now go through one helper, `OVRTXRenderer._map_render_var_to_dlpack()`. Camera outputs are unchanged — only the read ordering differs. Reported in https://nvbugspro.nvidia.com/bug/6566453 ## Type of change - Bug fix (non-breaking change which fixes an issue) - Performance improvement ## Screenshots None — no visual change. ## Checklist - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [x] I have added my name to the `CONTRIBUTORS.md` or my name already exists there Co-authored-by: pv-nvidia <197907000+pv-nvidia@users.noreply.github.com>
1 parent cd7ea42 commit 8abbc62

3 files changed

Lines changed: 167 additions & 14 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Fixed
2+
^^^^^
3+
4+
* Improved OVRTX camera-output throughput on Linux. A render var has to be read in an order that
5+
respects render completion, and on Linux blocking the calling thread on the render-completion
6+
event measures faster than a GPU-side wait. Camera outputs are now read that way on Linux, worth
7+
15-70% more end-to-end throughput depending on task and environment count. Other platforms order
8+
the read on the consuming Warp stream, which Linux can also be switched to by setting
9+
``ISAAC_LAB_OVRTX_DISABLE_LINUX_CUDA_CPU_SYNC=1``. Camera outputs themselves are unchanged.

source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py

Lines changed: 67 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import math
2525
import os
2626
import re
27+
import sys
28+
from collections.abc import Iterator
2729
from pathlib import Path
2830
from typing import TYPE_CHECKING, Any, NoReturn, cast
2931

@@ -125,6 +127,11 @@
125127
_USE_OVSTAGE_ENV = "ISAAC_LAB_OVRTX_USE_OVSTAGE"
126128

127129

130+
# Opts Linux out of the host wait, onto the same GPU-side ordering every other platform uses.
131+
# See :meth:`OVRTXRenderer._map_render_var_to_dlpack`.
132+
_DISABLE_LINUX_CUDA_CPU_SYNC_ENV = "ISAAC_LAB_OVRTX_DISABLE_LINUX_CUDA_CPU_SYNC"
133+
134+
128135
if _OVSTAGE_AVAILABLE:
129136
# DLDataType for a 4×4 double matrix (omni:xform column). ovstage stores omni:xform
130137
# as one 16-lane float64 element per prim; wp.mat44d maps to the same layout via __dlpack__.
@@ -203,6 +210,25 @@ def _read_gpu_transforms_enabled() -> bool:
203210
return value == "1"
204211

205212

213+
def _gpu_side_render_var_sync_enabled() -> bool:
214+
"""Return whether a render-var mapping is ordered by a GPU-side wait rather than a host wait.
215+
216+
See :meth:`OVRTXRenderer._map_render_var_to_dlpack` for why Linux is the exception, and
217+
:data:`_DISABLE_LINUX_CUDA_CPU_SYNC_ENV` for opting out of it.
218+
219+
Raises:
220+
ValueError: If the environment variable is set to anything other than ``0`` or ``1``.
221+
"""
222+
if not sys.platform.startswith("linux"):
223+
return True
224+
value = os.environ.get(_DISABLE_LINUX_CUDA_CPU_SYNC_ENV, "0").strip()
225+
if value not in {"0", "1"}:
226+
raise ValueError(
227+
f"Invalid value for environment variable `{_DISABLE_LINUX_CUDA_CPU_SYNC_ENV}`: {value}. Expected 0 or 1."
228+
)
229+
return value == "1"
230+
231+
206232
def _resolve_rtx_minimal_mode(data_types: list[str]) -> int | None:
207233
"""Resolve the RTX minimal mode from data types.
208234
@@ -1016,6 +1042,40 @@ def _generate_random_colors_from_ids(self, input_ids: wp.array, output_colors: w
10161042
)
10171043
return output_colors
10181044

1045+
@contextlib.contextmanager
1046+
def _map_render_var_to_dlpack(self, render_var: Any) -> Iterator[wp.array]:
1047+
"""Map ``render_var`` for CUDA reads and yield it as a Warp array.
1048+
1049+
The render is still in flight when the mapping returns, so reading it has to be ordered
1050+
against render completion. Normally that is a ``cudaStreamWaitEvent`` on the Warp stream the
1051+
consuming kernels run on, which is the ordering the OVRTX API is designed around.
1052+
1053+
On Linux that GPU-side wait measures substantially slower end to end, so the mapping is
1054+
instead requested with no GPU-side barrier and the calling thread blocks on the
1055+
render-completion event. Setting :data:`_DISABLE_LINUX_CUDA_CPU_SYNC_ENV` to ``1`` puts
1056+
Linux back on the GPU-side wait; it is an escape hatch for platforms where that trade-off
1057+
no longer holds, and is worth re-measuring before being relied on.
1058+
1059+
Note that ``sync_stream=0`` is OVRTX's "no sync" sentinel, *not* the NULL CUDA stream: the
1060+
field encodes ``0=no sync, 1=default stream, >1=specific stream``, so omitting the argument
1061+
entirely means ``1``, not ``0``.
1062+
1063+
The yielded array is a zero-copy view of the mapped memory and is only valid inside the
1064+
``with`` block -- the mapping is released on exit.
1065+
1066+
Args:
1067+
render_var: OVRTX ``RenderVarOutput`` to map (``frame.render_vars[name]``).
1068+
1069+
Yields:
1070+
The render var's contents as a Warp array, valid for the duration of the context.
1071+
"""
1072+
gpu_side_sync = _gpu_side_render_var_sync_enabled()
1073+
sync_stream = wp.get_stream(self._device).cuda_stream if gpu_side_sync else 0
1074+
with render_var.map(device=Device.CUDA, sync_stream=sync_stream) as mapping:
1075+
if not gpu_side_sync:
1076+
mapping.wait()
1077+
yield wp.from_dlpack(mapping)
1078+
10191079
def _process_id_segmentation_render_var(
10201080
self,
10211081
render_data: OVRTXRenderData,
@@ -1042,8 +1102,7 @@ def _process_id_segmentation_render_var(
10421102
if render_var_name not in frame.render_vars or buffer_key not in output_buffers:
10431103
return
10441104

1045-
with frame.render_vars[render_var_name].map(device=Device.CUDA) as mapping:
1046-
tiled_data = wp.from_dlpack(mapping)
1105+
with self._map_render_var_to_dlpack(frame.render_vars[render_var_name]) as tiled_data:
10471106
if tiled_data.dtype != wp.uint32:
10481107
return
10491108

@@ -1255,15 +1314,13 @@ def _process_render_frame(self, render_data: OVRTXRenderData, frame, output_buff
12551314
break
12561315

12571316
if buffer_key is not None:
1258-
with frame.render_vars["LdrColor"].map(device=Device.CUDA) as mapping:
1259-
tiled_data = wp.from_dlpack(mapping)
1317+
with self._map_render_var_to_dlpack(frame.render_vars["LdrColor"]) as tiled_data:
12601318
self._extract_rgba_tiles(render_data, tiled_data, output_buffers, buffer_key)
12611319

12621320
for depth_var in ["DistanceToCameraSD", "DistanceToImagePlaneSD", "DepthSD"]:
12631321
if depth_var not in frame.render_vars:
12641322
continue
1265-
with frame.render_vars[depth_var].map(device=Device.CUDA) as mapping:
1266-
tiled_depth_data = wp.from_dlpack(mapping)
1323+
with self._map_render_var_to_dlpack(frame.render_vars[depth_var]) as tiled_depth_data:
12671324
if tiled_depth_data.dtype == wp.uint32:
12681325
tiled_depth_data = wp.from_torch(
12691326
wp.to_torch(tiled_depth_data).view(torch.float32), dtype=wp.float32
@@ -1272,13 +1329,11 @@ def _process_render_frame(self, render_data: OVRTXRenderData, frame, output_buff
12721329
break
12731330

12741331
if "DiffuseAlbedoSD" in frame.render_vars and "albedo" in output_buffers:
1275-
with frame.render_vars["DiffuseAlbedoSD"].map(device=Device.CUDA) as mapping:
1276-
tiled_albedo_data = wp.from_dlpack(mapping)
1332+
with self._map_render_var_to_dlpack(frame.render_vars["DiffuseAlbedoSD"]) as tiled_albedo_data:
12771333
self._extract_rgba_tiles(render_data, tiled_albedo_data, output_buffers, "albedo", suffix="albedo")
12781334

12791335
if "HdrColor" in frame.render_vars and "rgb_hdr" in output_buffers:
1280-
with frame.render_vars["HdrColor"].map(device=Device.CUDA) as mapping:
1281-
tiled_hdr_data = wp.from_dlpack(mapping)
1336+
with self._map_render_var_to_dlpack(frame.render_vars["HdrColor"]) as tiled_hdr_data:
12821337
tiled_hdr_data = self._prepare_ppisp_hdr_source(render_data, tiled_hdr_data, output_buffers)
12831338
self._extract_hdr_color_tiles(render_data, tiled_hdr_data, output_buffers)
12841339

@@ -1308,16 +1363,14 @@ def _process_render_frame(self, render_data: OVRTXRenderData, frame, output_buff
13081363
self._process_instance_segmentation_maps(render_data, frame)
13091364

13101365
if "NormalSD" in frame.render_vars and "normals" in output_buffers:
1311-
with frame.render_vars["NormalSD"].map(device=Device.CUDA) as mapping:
1312-
tiled_normals_data = wp.from_dlpack(mapping)
1366+
with self._map_render_var_to_dlpack(frame.render_vars["NormalSD"]) as tiled_normals_data:
13131367
self._launch_extract_all_tiles(render_data, tiled_normals_data, output_buffers["normals"])
13141368

13151369
# For motion vectors, extract only the first two (u, v) channels from the tiled buffer.
13161370
# Note: mirrors the Isaac RTX renderer's handling of the "TargetMotionSD" AOV
13171371
# (check: https://github.com/isaac-sim/IsaacLab/issues/2003).
13181372
if "TargetMotionSD" in frame.render_vars and "motion_vectors" in output_buffers:
1319-
with frame.render_vars["TargetMotionSD"].map(device=Device.CUDA) as mapping:
1320-
tiled_motion_vectors_data = wp.from_dlpack(mapping)
1373+
with self._map_render_var_to_dlpack(frame.render_vars["TargetMotionSD"]) as tiled_motion_vectors_data:
13211374
self._launch_extract_all_tiles(render_data, tiled_motion_vectors_data, output_buffers["motion_vectors"])
13221375

13231376
def _render_legacy(self, render_data: OVRTXRenderData) -> None:

source/isaaclab_ov/test/test_ovrtx_renderer_contract.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55

66
"""Tests for the OVRTX renderer output contract."""
77

8+
import contextlib
89
import importlib.util
10+
import sys
11+
import types
912

1013
import pytest
1114
import torch
@@ -30,8 +33,10 @@
3033
from isaaclab_ov.renderers import OVRTXRendererCfg # noqa: E402
3134
from isaaclab_ov.renderers import ovrtx_renderer as ovrtx_renderer_module # noqa: E402
3235
from isaaclab_ov.renderers.ovrtx_renderer import ( # noqa: E402
36+
_DISABLE_LINUX_CUDA_CPU_SYNC_ENV,
3337
OVRTXRenderData,
3438
OVRTXRenderer,
39+
_gpu_side_render_var_sync_enabled,
3540
ovrtx_use_ovstage_enabled,
3641
)
3742
else:
@@ -40,6 +45,8 @@
4045
OVRTXRendererCfg = None
4146
ovrtx_renderer_module = None
4247
ovrtx_use_ovstage_enabled = None
48+
_DISABLE_LINUX_CUDA_CPU_SYNC_ENV = None
49+
_gpu_side_render_var_sync_enabled = None
4350

4451
_SPAWN = PinholeCameraCfg(
4552
focal_length=24.0,
@@ -366,6 +373,90 @@ def test_ovrtx_use_ovstage_rejects_non_boolean_values(monkeypatch):
366373
ovrtx_use_ovstage_enabled()
367374

368375

376+
@pytest.mark.parametrize("platform", ["win32", "darwin"])
377+
def test_ovrtx_render_var_sync_is_gpu_side_off_linux(monkeypatch, platform):
378+
"""Everywhere but Linux the mapping is ordered by a GPU-side wait on the Warp stream."""
379+
monkeypatch.setattr(sys, "platform", platform)
380+
monkeypatch.delenv(_DISABLE_LINUX_CUDA_CPU_SYNC_ENV, raising=False)
381+
assert _gpu_side_render_var_sync_enabled() is True
382+
383+
384+
def test_ovrtx_render_var_sync_waits_on_host_on_linux(monkeypatch):
385+
"""Linux blocks the calling thread instead, which measures faster there."""
386+
monkeypatch.setattr(sys, "platform", "linux")
387+
monkeypatch.delenv(_DISABLE_LINUX_CUDA_CPU_SYNC_ENV, raising=False)
388+
assert _gpu_side_render_var_sync_enabled() is False
389+
390+
391+
def test_ovrtx_render_var_sync_is_gpu_side_on_linux_when_disabled(monkeypatch):
392+
"""Opting out of the host wait puts Linux on the same GPU-side wait as every other platform."""
393+
monkeypatch.setattr(sys, "platform", "linux")
394+
monkeypatch.setenv(_DISABLE_LINUX_CUDA_CPU_SYNC_ENV, "1")
395+
assert _gpu_side_render_var_sync_enabled() is True
396+
397+
398+
def test_ovrtx_render_var_sync_keeps_host_wait_when_explicitly_enabled(monkeypatch):
399+
"""``0`` is the default, so setting it explicitly must not change anything."""
400+
monkeypatch.setattr(sys, "platform", "linux")
401+
monkeypatch.setenv(_DISABLE_LINUX_CUDA_CPU_SYNC_ENV, "0")
402+
assert _gpu_side_render_var_sync_enabled() is False
403+
404+
405+
@pytest.mark.parametrize("value", ["", "true", "yes", "2"])
406+
def test_ovrtx_render_var_sync_rejects_non_boolean_values(monkeypatch, value):
407+
"""Values other than 0/1 are a configuration error, not a silent fallback to the host wait."""
408+
monkeypatch.setattr(sys, "platform", "linux")
409+
monkeypatch.setenv(_DISABLE_LINUX_CUDA_CPU_SYNC_ENV, value)
410+
with pytest.raises(ValueError, match="Expected 0 or 1"):
411+
_gpu_side_render_var_sync_enabled()
412+
413+
414+
class _RecordingRenderVar:
415+
"""Stand-in for an OVRTX ``RenderVarOutput`` that records how the read was ordered.
416+
417+
Any of OVRTX's ordering mechanisms counts, so the test stays about *whether* the read is
418+
ordered rather than which call carries it.
419+
"""
420+
421+
def __init__(self):
422+
self.ordering: list[str] = []
423+
424+
def map(self, *, device, sync_stream):
425+
if sync_stream:
426+
self.ordering.append("gpu")
427+
recorder = self
428+
429+
class _Mapping:
430+
def wait(self):
431+
recorder.ordering.append("host")
432+
433+
def wait_on(self, stream):
434+
recorder.ordering.append("gpu")
435+
436+
return contextlib.nullcontext(_Mapping())
437+
438+
439+
@pytest.mark.parametrize(("gpu_side", "expected"), [(True, "gpu"), (False, "host")])
440+
def test_ovrtx_map_render_var_orders_the_read_against_render_completion(monkeypatch, gpu_side, expected):
441+
"""The read is ordered exactly once -- by a GPU-side barrier or a host block, never by neither.
442+
443+
Ordering by neither is a silent race on half-written render output rather than a failure, so
444+
this asserts which mechanism ran and not which API call carries it.
445+
"""
446+
sentinel = object()
447+
render_var = _RecordingRenderVar()
448+
monkeypatch.setattr(ovrtx_renderer_module, "_gpu_side_render_var_sync_enabled", lambda: gpu_side)
449+
monkeypatch.setattr(ovrtx_renderer_module.wp, "get_stream", lambda device: types.SimpleNamespace(cuda_stream=99))
450+
monkeypatch.setattr(ovrtx_renderer_module.wp, "from_dlpack", lambda mapping: sentinel)
451+
452+
renderer = _make_ovrtx_renderer_without_backend()
453+
renderer._device = "cuda:0"
454+
with renderer._map_render_var_to_dlpack(render_var) as array:
455+
assert array is sentinel
456+
457+
assert render_var.ordering == [expected]
458+
459+
369460
def test_ovrtx_cleanup_releases_only_the_given_render_data():
370461
"""``cleanup`` releases the render data's own buffers and leaves the renderer usable.
371462

0 commit comments

Comments
 (0)