Skip to content

Commit 57d7600

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 57d7600

4 files changed

Lines changed: 101 additions & 57 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: 59 additions & 53 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
@@ -1161,7 +1160,7 @@ def clear(cls):
11611160
NewtonManager._graph = None
11621161
NewtonManager._graph_capture_pending = False
11631162
NewtonManager._sensor_tasks = {}
1164-
NewtonManager._invalidate_sensor_graph()
1163+
NewtonManager._invalidate_sensor_graphs()
11651164
NewtonManager._sensor_state = None
11661165
NewtonManager._sensor_state_dirty = True
11671166
NewtonManager._sensor_graph_capture_failed = False
@@ -2698,13 +2697,13 @@ def _register_sensor_task(cls, name: str, update_fn: Callable[[], None]) -> None
26982697
cls._sensor_tasks[name] = update_fn
26992698
cls._sensor_state = state
27002699
cls._sensor_state_dirty = True
2701-
cls._invalidate_sensor_graph()
2700+
cls._invalidate_sensor_graphs()
27022701

27032702
@classmethod
27042703
def _unregister_sensor_task(cls, name: str) -> None:
27052704
"""Remove a scene-query task, ignoring unknown names."""
27062705
if cls._sensor_tasks.pop(name, None) is not None:
2707-
cls._invalidate_sensor_graph()
2706+
cls._invalidate_sensor_graphs()
27082707

27092708
@classmethod
27102709
def _update_sensor_tasks(cls, *names: str) -> None:
@@ -2718,29 +2717,24 @@ def _update_sensor_tasks(cls, *names: str) -> None:
27182717
if state is not cls._sensor_state:
27192718
cls._sensor_state = state
27202719
cls._sensor_state_dirty = True
2721-
cls._invalidate_sensor_graph()
2720+
cls._invalidate_sensor_graphs()
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:
2725-
cls._capture_sensor_graph()
2726-
if cls._sensor_graph is None:
2723+
if use_cuda_graph and cls._sensor_refit_graph is None and not cls._sensor_graph_capture_failed:
2724+
cls._capture_sensor_graphs()
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:
@@ -2757,7 +2751,7 @@ def _mark_sensor_state_dirty(cls) -> None:
27572751
return
27582752
if NewtonManager._state_0 is not NewtonManager._sensor_state:
27592753
NewtonManager._sensor_state = NewtonManager._state_0
2760-
NewtonManager._invalidate_sensor_graph()
2754+
NewtonManager._invalidate_sensor_graphs()
27612755
NewtonManager._sensor_state_dirty = True
27622756

27632757
@classmethod
@@ -2781,49 +2775,61 @@ def _refit_sensor_bvh(cls) -> None:
27812775
cls._model.bvh_refit_particles(cls._sensor_state)
27822776

27832777
@classmethod
2784-
def _invalidate_sensor_graph(cls) -> None:
2785-
"""Discard captured scene-query graph resources."""
2786-
cls._sensor_graph = None
2787-
cls._sensor_flags = None
2788-
cls._sensor_flags_host = None
2778+
def _invalidate_sensor_graphs(cls) -> None:
2779+
"""Discard the captured scene-query graphs."""
2780+
cls._sensor_refit_graph = None
2781+
cls._sensor_task_graphs = {}
27892782
cls._sensor_graph_capture_failed = False
27902783

27912784
@classmethod
2792-
def _capture_sensor_graph(cls) -> None:
2793-
"""Capture BVH refit and scene-query tasks into a conditional graph."""
2794-
with wp.ScopedDevice(PhysicsManager._device):
2785+
def _capture_sensor_graphs(cls) -> None:
2786+
"""Capture the BVH refit and each scene-query task into its own graph.
2787+
2788+
Each one 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_graph(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_graph(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_graphs()
2812+
# Latch after invalidating: _invalidate_sensor_graphs() 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_graph(cls, device: str, capture_target: Callable[[], None]) -> wp.Graph | None:
2823+
"""Capture one scene-query callable 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)