Skip to content

Commit 463ddce

Browse files
committed
Capture Newton scene-query tasks into per-task CUDA graphs
Every registered scene-query task ran inside a wp.capture_if conditional body. CUDA forbids memory-allocation nodes there, and wp.Mesh.refit() allocates scratch on every call, so tasks with deformable geometry failed to capture and all sensor work fell back to eager execution. Capture the shared BVH refit and each task into their own top-level graph, where allocation nodes are legal. This preserves the previous per-task selection semantics and drops the device flag array and its per-call host-to-device copy.
1 parent 64c55de commit 463ddce

4 files changed

Lines changed: 92 additions & 48 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Fixed
2+
^^^^^
3+
4+
* Fixed Newton sensor CUDA graph capture failing with ``RuntimeError: Conditional body graph contains an
5+
unsupported operation (memory allocation)`` on tasks with deformable geometry, such as
6+
``Isaac-Lift-Cloth-Franka-Camera``. Scene-query tasks are now captured into one graph each, because Warp
7+
forbids memory allocation inside the conditional body that previously held them.

source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py

Lines changed: 50 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -477,9 +477,8 @@ class NewtonManager(PhysicsManager):
477477

478478
# Newton scene-query scheduling and graph execution.
479479
_sensor_tasks: dict[str, Callable[[], None]] = {}
480-
_sensor_graph: wp.Graph | None = None
481-
_sensor_flags: wp.array | None = None
482-
_sensor_flags_host: np.ndarray | None = None
480+
_sensor_refit_graph: wp.Graph | None = None
481+
_sensor_task_graphs: dict[str, wp.Graph] = {}
483482
_sensor_state: State | None = None
484483
_sensor_state_dirty: bool = True
485484
_sensor_graph_capture_failed: bool = False
@@ -2721,26 +2720,21 @@ def _update_sensor_tasks(cls, *names: str) -> None:
27212720
cls._invalidate_sensor_graph()
27222721
cfg = PhysicsManager._cfg
27232722
use_cuda_graph = bool(getattr(cfg, "use_cuda_graph", False)) and "cuda" in str(PhysicsManager._device)
2724-
if use_cuda_graph and cls._sensor_graph is None and not cls._sensor_graph_capture_failed:
2723+
if use_cuda_graph and cls._sensor_refit_graph is None and not cls._sensor_graph_capture_failed:
27252724
cls._capture_sensor_graph()
2726-
if cls._sensor_graph is None:
2725+
if cls._sensor_refit_graph is None:
27272726
if cls._sensor_state_dirty:
27282727
cls._refit_sensor_bvh()
27292728
cls._sensor_state_dirty = False
27302729
for name in names:
27312730
cls._sensor_tasks[name]()
27322731
return
27332732

2734-
assert cls._sensor_flags_host is not None
2735-
assert cls._sensor_flags is not None
2736-
cls._sensor_flags_host.fill(0)
2737-
cls._sensor_flags_host[0] = int(cls._sensor_state_dirty)
2738-
task_names = tuple(cls._sensor_tasks)
2733+
if cls._sensor_state_dirty:
2734+
wp.capture_launch(cls._sensor_refit_graph)
2735+
cls._sensor_state_dirty = False
27392736
for name in names:
2740-
cls._sensor_flags_host[1 + task_names.index(name)] = 1
2741-
cls._sensor_flags.assign(cls._sensor_flags_host)
2742-
wp.capture_launch(cls._sensor_graph)
2743-
cls._sensor_state_dirty = False
2737+
wp.capture_launch(cls._sensor_task_graphs[name])
27442738

27452739
@classmethod
27462740
def _mark_sensor_state_dirty(cls) -> None:
@@ -2783,47 +2777,59 @@ def _refit_sensor_bvh(cls) -> None:
27832777
@classmethod
27842778
def _invalidate_sensor_graph(cls) -> None:
27852779
"""Discard captured scene-query graph resources."""
2786-
cls._sensor_graph = None
2787-
cls._sensor_flags = None
2788-
cls._sensor_flags_host = None
2780+
cls._sensor_refit_graph = None
2781+
cls._sensor_task_graphs = {}
27892782
cls._sensor_graph_capture_failed = False
27902783

27912784
@classmethod
27922785
def _capture_sensor_graph(cls) -> None:
2793-
"""Capture BVH refit and scene-query tasks into a conditional graph."""
2794-
with wp.ScopedDevice(PhysicsManager._device):
2786+
"""Capture the BVH refit and each scene-query task into its own graph.
2787+
2788+
Each step gets a standalone top-level graph rather than a ``wp.capture_if``
2789+
conditional body of one shared graph: Warp rejects memory allocation inside a
2790+
conditional body, and ``wp.Mesh.refit`` allocates scratch on every call, so
2791+
deformable geometry in the tiled-camera render path would fail to capture.
2792+
"""
2793+
device = PhysicsManager._device
2794+
with wp.ScopedDevice(device):
27952795
cls._refit_sensor_bvh()
27962796
for update_fn in cls._sensor_tasks.values():
27972797
update_fn()
27982798

2799-
cls._sensor_flags = wp.zeros(1 + len(cls._sensor_tasks), dtype=wp.int32, device=PhysicsManager._device)
2800-
cls._sensor_flags_host = np.zeros(1 + len(cls._sensor_tasks), dtype=np.int32)
2801-
update_fns = tuple(cls._sensor_tasks.values())
2799+
refit_graph = cls._capture_sensor_step(device, cls._refit_sensor_bvh)
2800+
failed = None if refit_graph is not None else "bvh refit"
2801+
task_graphs: dict[str, wp.Graph] = {}
2802+
if failed is None:
2803+
for name, update_fn in cls._sensor_tasks.items():
2804+
graph = cls._capture_sensor_step(device, update_fn)
2805+
if graph is None:
2806+
failed = name
2807+
break
2808+
task_graphs[name] = graph
2809+
2810+
if failed is not None:
2811+
cls._invalidate_sensor_graph()
2812+
# Latch after invalidating: _invalidate_sensor_graph() clears the flag.
2813+
cls._sensor_graph_capture_failed = True
2814+
logger.warning("Newton sensor graph capture failed for '%s'; falling back to eager execution.", failed)
2815+
return
28022816

2803-
def pipeline() -> None:
2804-
assert cls._sensor_flags is not None
2805-
wp.capture_if(cls._sensor_flags[0:1], cls._refit_sensor_bvh)
2806-
for index, update_fn in enumerate(update_fns):
2807-
wp.capture_if(cls._sensor_flags[index + 1 : index + 2], update_fn)
2817+
cls._sensor_refit_graph = refit_graph
2818+
cls._sensor_task_graphs = task_graphs
2819+
logger.info("Captured Newton sensor graphs for %d task(s).", len(task_graphs))
28082820

2809-
device = PhysicsManager._device
2821+
@classmethod
2822+
def _capture_sensor_step(cls, device: str, capture_target: Callable[[], None]) -> wp.Graph | None:
2823+
"""Capture one scene-query step into a standalone graph, or ``None`` on failure."""
28102824
if cls._usdrt_stage is not None:
2811-
cls._sensor_graph = cls._capture_relaxed_graph(device, capture_target=pipeline)
2812-
else:
2813-
try:
2814-
with wp.ScopedCapture(device=device) as capture:
2815-
pipeline()
2816-
cls._sensor_graph = capture.graph
2817-
except Exception:
2818-
logger.exception("[NewtonManager] sensor CUDA graph capture failed")
2819-
cls._sensor_graph = None
2820-
if cls._sensor_graph is None:
2821-
cls._sensor_flags = None
2822-
cls._sensor_flags_host = None
2823-
cls._sensor_graph_capture_failed = True
2824-
logger.warning("Newton sensor graph capture failed; falling back to eager execution.")
2825-
else:
2826-
logger.info("Captured Newton sensor graph with %d task(s).", len(cls._sensor_tasks))
2825+
return cls._capture_relaxed_graph(device, capture_target=capture_target)
2826+
try:
2827+
with _paused_gc(), wp.ScopedCapture(device=device) as capture:
2828+
capture_target()
2829+
return capture.graph
2830+
except Exception:
2831+
logger.exception("[NewtonManager] sensor CUDA graph capture failed")
2832+
return None
28272833

28282834
@classmethod
28292835
def get_num_envs(cls) -> int:

source/isaaclab_newton/test/physics/test_newton_manager_abstraction.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -338,9 +338,8 @@ def get_state(cls):
338338
monkeypatch.setattr(NewtonManager, "_sensor_tasks", {}, raising=False)
339339
monkeypatch.setattr(NewtonManager, "_sensor_state", None, raising=False)
340340
monkeypatch.setattr(NewtonManager, "_sensor_state_dirty", True, raising=False)
341-
monkeypatch.setattr(NewtonManager, "_sensor_graph", None, raising=False)
342-
monkeypatch.setattr(NewtonManager, "_sensor_flags", None, raising=False)
343-
monkeypatch.setattr(NewtonManager, "_sensor_flags_host", None, raising=False)
341+
monkeypatch.setattr(NewtonManager, "_sensor_refit_graph", None, raising=False)
342+
monkeypatch.setattr(NewtonManager, "_sensor_task_graphs", {}, raising=False)
344343
monkeypatch.setattr(NewtonManager, "_sensor_graph_capture_failed", False, raising=False)
345344
monkeypatch.setattr(PhysicsManager, "_cfg", SimpleNamespace(use_cuda_graph=False), raising=False)
346345

@@ -350,6 +349,38 @@ def get_state(cls):
350349
assert status["rendered"]
351350

352351

352+
@pytest.mark.skipif(not wp.get_cuda_device_count(), reason="CUDA is unavailable")
353+
def test_sensor_graph_captures_allocating_task(monkeypatch):
354+
"""A sensor task that allocates scratch is still graph-capturable.
355+
356+
``wp.Mesh.refit`` allocates scratch natively on every call, which Warp rejects
357+
inside a ``wp.capture_if`` conditional body. Deformable geometry reaches it through
358+
the tiled-camera render task, so each task must be captured as its own graph.
359+
"""
360+
361+
points = wp.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]], dtype=wp.vec3f, device="cuda:0")
362+
mesh = wp.Mesh(points, wp.array([0, 1, 2], dtype=wp.int32, device="cuda:0"))
363+
364+
state = object()
365+
monkeypatch.setattr(NewtonManager, "get_state", classmethod(lambda cls: state))
366+
monkeypatch.setattr(NewtonManager, "_model", None, raising=False)
367+
monkeypatch.setattr(NewtonManager, "_sensor_tasks", {"mesh_refit": mesh.refit}, raising=False)
368+
monkeypatch.setattr(NewtonManager, "_sensor_state", state, raising=False)
369+
monkeypatch.setattr(NewtonManager, "_sensor_state_dirty", True, raising=False)
370+
monkeypatch.setattr(NewtonManager, "_sensor_refit_graph", None, raising=False)
371+
monkeypatch.setattr(NewtonManager, "_sensor_task_graphs", {}, raising=False)
372+
monkeypatch.setattr(NewtonManager, "_sensor_graph_capture_failed", False, raising=False)
373+
monkeypatch.setattr(NewtonManager, "_usdrt_stage", None, raising=False)
374+
monkeypatch.setattr(PhysicsManager, "_device", "cuda:0", raising=False)
375+
monkeypatch.setattr(PhysicsManager, "_cfg", SimpleNamespace(use_cuda_graph=True), raising=False)
376+
377+
NewtonManager._update_sensor_tasks("mesh_refit")
378+
379+
assert not NewtonManager._sensor_graph_capture_failed
380+
assert NewtonManager._sensor_refit_graph is not None
381+
assert "mesh_refit" in NewtonManager._sensor_task_graphs
382+
383+
353384
def test_sensor_bvh_shape_flags_are_fixed_before_builder_creation(monkeypatch):
354385
"""Builder finalization includes collision-only shapes without a later BVH rebuild."""
355386
import newton

source/isaaclab_newton/test/sensors/test_newton_raycast_sensor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,4 +252,4 @@ def test_renderer_and_raycast_share_newton_manager_graph(sim):
252252
task_names = sorted(NewtonManager._sensor_tasks)
253253
assert any(name.startswith("newton_raycast:") for name in task_names)
254254
assert any(name.startswith("newton_warp_render:") for name in task_names)
255-
assert (NewtonManager._sensor_graph is not None) == sim.cfg.physics.use_cuda_graph
255+
assert bool(NewtonManager._sensor_task_graphs) == sim.cfg.physics.use_cuda_graph

0 commit comments

Comments
 (0)