Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions source/isaaclab/changelog.d/pk-xr-multigpu-device-pinning.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Fixed
^^^^^

* Fixed the Kit renderer not being restricted to a single GPU under ``--xr``. The
``--/renderer/multiGpu/activeCudaGpus`` setting was only applied when ``multi_gpu`` was
``False``, which is set for distributed runs alone, so an XR session left the renderer
spanning every visible GPU while physics ran on the selected device. XR streams a single
stereo swapchain that the CloudXR compositor imports, so the renderer is now pinned to the
simulation device whenever XR is enabled.
5 changes: 4 additions & 1 deletion source/isaaclab/isaaclab/app/app_launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -1299,7 +1299,10 @@ def _resolve_kit_args(self, launcher_args: dict):
self._kit_args.append(argument)

# Select the renderer by CUDA index; the trailing comma keeps the setting string-typed.
if launcher_args.get("multi_gpu") is False:
# XR streams a single stereo swapchain that the CloudXR compositor imports, so the
# renderer has to stay on one known device there too -- otherwise the compositor and
# the renderer can end up on different GPUs and the headset receives noise.
if launcher_args.get("multi_gpu") is False or self._xr:
Comment thread
rwiltz marked this conversation as resolved.
Outdated
argument = f"--/renderer/multiGpu/activeCudaGpus={self.device_id},"
setting = argument.partition("=")[0]
if not any(arg.partition("=")[0] == setting for arg in sys.argv + self._kit_args):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Fixed
^^^^^

* Fixed the XR headset receiving noise instead of the rendered scene on multi-GPU hosts.
The auto-launched CloudXR runtime selected its own device, and because Vulkan's physical
device enumeration is unrelated to the CUDA ordering Isaac Lab picks the simulation and
renderer devices with, the compositor could end up on a different GPU than the one holding
the rendered swapchain. The runtime is now pinned to the renderer's CUDA device via
``NV_CXR_GPU_INDEX_CUDA``; an index already set in the environment or in the
``--cloudxr_env`` profile is left untouched.
59 changes: 59 additions & 0 deletions source/isaaclab_teleop/isaaclab_teleop/session_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,35 @@
from .isaac_teleop_cfg import IsaacTeleopCfg
from .teleop_message_processor import TeleopMessageProcessor

# The CloudXR runtime accepts at most one of these; setting both is rejected outright.
_CXR_GPU_INDEX_ENV_VARS = ("NV_CXR_GPU_INDEX_CUDA", "NV_CXR_GPU_INDEX_VULKAN")


def _env_file_pins_gpu_index(env_file: str | None) -> bool:
"""Whether a CloudXR ``.env`` profile already selects a GPU index."""
if not env_file:
return False
try:
with open(env_file, encoding="utf-8") as handle:
lines = handle.readlines()
except OSError:
return False
return any(line.strip().split("=", 1)[0].strip() in _CXR_GPU_INDEX_ENV_VARS for line in lines)


def _renderer_cuda_index() -> int | None:
"""CUDA index the Kit renderer is pinned to, or ``None`` when it is not pinned."""
try:
import carb
except ImportError:
return None
setting = carb.settings.get_settings().get("/renderer/multiGpu/activeCudaGpus")
if not setting:
return None
first = str(setting).split(",")[0].strip()
return int(first) if first.isdigit() else None


if TYPE_CHECKING:
from .haptic_feedback import HapticFeedbackCfg

Expand Down Expand Up @@ -1310,13 +1339,43 @@ def _ensure_cloudxr_runtime(self) -> None:

from isaacteleop.cloudxr import CloudXRLauncher as _CloudXRLauncher

self._pin_cloudxr_to_render_device()

self._cloudxr_launcher = _CloudXRLauncher(
install_dir=str(Path.home() / ".cloudxr"),
env_config=self._cloudxr_env_file,
accept_eula=False,
)
logger.info("CloudXR runtime auto-launched")

def _pin_cloudxr_to_render_device(self) -> None:
"""Point the CloudXR runtime at the GPU the frames are rendered on.

Left to itself the runtime takes the first Vulkan physical device. That
enumeration is unrelated to the CUDA ordering Isaac Lab selects the
simulation and renderer devices with, so on a multi-GPU host the
compositor routinely lands on a different card than the one holding the
rendered swapchain. Nothing reports an error -- the client connects, the
session starts and the encoder logs normal frame timings -- but the
headset only shows noise.
Comment on lines +1354 to +1360

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unsure if we need the context, but the reason it picks the first Vulkan device is that most (all) windowed applications will pick the first Vulkan device and then ignore what the runtime asks the app to use, becuse the first Vulkan device is the GPU that the monitor is plugged into (breaks down in "unusual" setups).


An explicit choice, in the process environment or in the
``--cloudxr_env`` profile, is left untouched.
"""
if any(name in os.environ for name in _CXR_GPU_INDEX_ENV_VARS):
return
if _env_file_pins_gpu_index(self._cloudxr_env_file):
return
Comment on lines +1365 to +1368

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Maybe add warnings here if they don't match, and ask the user to check the configs?


index = _renderer_cuda_index()
if index is None and self._device.type == "cuda":
index = self._device.index
if index is None:
return

os.environ["NV_CXR_GPU_INDEX_CUDA"] = str(index)
logger.info("Pinned the CloudXR runtime to CUDA device %d", index)

# ------------------------------------------------------------------
# OpenXR handle acquisition
# ------------------------------------------------------------------
Expand Down
Loading