Skip to content

Commit 4b9ba22

Browse files
Clear self-collision filter pairs before finalizing shadow Newton model (#7505)
## Summary - The PhysX-backend shadow Newton visualization model never runs collision detection, but USD-authored self-collision filter pairs (`physxArticulation:enabledSelfCollisions`) were still imported into it and replicated across every cloned env. - At real training env counts, this filter-pair set could reach billions of entries, causing `ModelBuilder.finalize()` to run out of memory. - Clears `builder.shape_collision_filter_pairs` before finalizing the shadow model since it has no use for them. ## Test plan - [x] `uv run python -m pytest source/isaaclab/test/sim/test_newton_manager_visualization_state.py` — 23 passed, including new regression test `test_ensure_visualization_model_clears_shape_collision_filter_pairs_before_finalize` - [x] `uv run python -m pytest source/isaaclab/test/sim/test_simulation_context_visualizers.py source/isaaclab_newton/test/physics/test_newton_manager_abstraction.py` — 200 passed - [x] `uv run isaaclab -f` ## Release backport - [x] <!-- backport-active-release --> Backport this pull request to the active release branch after it merges into `develop` --------- Co-authored-by: Octi Zhang <zhengyuz@nvidia.com>
1 parent 64c55de commit 4b9ba22

5 files changed

Lines changed: 66 additions & 1 deletion

File tree

source/isaaclab/changelog.d/mtrepte-shadow-model-collision-filter-oom-test.skip

Whitespace-only changes.

source/isaaclab/test/sim/test_newton_manager_visualization_state.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -826,6 +826,10 @@ def test_clone_visualization_builder_ignores_non_env_deformables_on_world_import
826826
UsdGeom.Xform.Define(stage, "/World/envs/env_1")
827827

828828
fake_builder = _FakeShadowBuilder(body_count=1, cloth_delta=3, track_usd=True)
829+
fake_builder.shape_collision_filter_pairs = []
830+
fake_builder.shape_collision_group = []
831+
fake_builder.shape_count = 0
832+
fake_builder.add_builder = lambda _builder: None
829833
clone_plan = SimpleNamespace(
830834
sources=("/World/envs/env_0",),
831835
destinations=("/World/envs/env_{}",),
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fixed
2+
^^^^^
3+
4+
* Fixed PhysX-backend Newton visualization models replicating unused collision
5+
filters and contact pairs, which could exhaust memory during ``ModelBuilder.finalize()``.

source/isaaclab_newton/isaaclab_newton/physics/visualization_builder.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ def build_visualization_builder_from_stage_envs(
113113
shadow_entities, registry_groups = add_shadow_deformables_to_builder(
114114
builder, stage, env_paths, device=device, entries=deformable_entries, clone_plan=clone_plan
115115
)
116+
builder.shape_collision_filter_pairs = []
117+
builder.shape_collision_group[:] = [0] * builder.shape_count
116118
return builder, (shadow_entities, registry_groups)
117119

118120
if not env_paths:
@@ -147,6 +149,12 @@ def build_visualization_builder_from_stage_envs(
147149
schema_resolvers,
148150
ignore_paths=source_deformable_ignore_paths or None,
149151
)
152+
global_builder = builder
153+
builder = ModelBuilder(up_axis=up_axis) # Preserve Newton's compact empty filter store.
154+
for visual_builder in (global_builder, *source_builders.values()):
155+
visual_builder.shape_collision_filter_pairs = []
156+
visual_builder.shape_collision_group[:] = [0] * visual_builder.shape_count
157+
builder.add_builder(global_builder)
150158
replicate_builder_mapping(builder, sources, mapping, positions, quaternions, source_builders, destinations, env_ids)
151159
shadow_entities, registry_groups = add_shadow_deformables_to_builder(
152160
builder, stage, env_paths, device=device, entries=deformable_entries, clone_plan=clone_plan

source/isaaclab_newton/test/cloner/test_rename_builder_labels.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from isaaclab_newton.physics import visualization_builder as visualization_builder_module
1717
from isaaclab_newton.physics import visualization_deformables as visualization_deformables_module
1818

19-
from pxr import Usd, UsdGeom
19+
from pxr import Sdf, Usd, UsdGeom, UsdPhysics
2020

2121
from isaaclab.cloner import ClonePlan
2222
from isaaclab.scene_data.deformable_discovery import DeformableStageEntry
@@ -40,6 +40,8 @@
4040
class _FakeVisualizationModelBuilder:
4141
def __init__(self, up_axis=None):
4242
self.up_axis = up_axis
43+
self.shape_collision_filter_pairs = []
44+
self.shape_collision_group = []
4345
for attr in _VIS_BUILTIN_LABEL_ATTRS:
4446
setattr(self, attr, [])
4547
setattr(self, attr.replace("_label", "_world"), [])
@@ -80,6 +82,7 @@ def add_usd(self, stage, root_path=None, ignore_paths=None, schema_resolvers=Non
8082
for attr in _VIS_BUILTIN_LABEL_ATTRS:
8183
getattr(self, attr).append(f"{root_path}/{_VIS_LABEL_SUFFIXES[attr]}")
8284
getattr(self, attr.replace("_label", "_world")).append(self._current_world or 0)
85+
self.shape_collision_group.append(1)
8386
self.custom_attributes["mujoco:equality_constraint_label"].values.append(
8487
f"{root_path}/{_VIS_LABEL_SUFFIXES['equality_constraint_label']}"
8588
)
@@ -96,6 +99,7 @@ def add_builder(self, builder, xform=None):
9699
labels = getattr(builder, attr)
97100
getattr(self, attr).extend(labels)
98101
getattr(self, attr.replace("_label", "_world")).extend([self._current_world] * len(labels))
102+
self.shape_collision_group.extend(builder.shape_collision_group)
99103
eq_labels = builder.custom_attributes["mujoco:equality_constraint_label"].values
100104
self.custom_attributes["mujoco:equality_constraint_label"].values.extend(eq_labels)
101105
self.custom_attributes["mujoco:equality_constraint_world"].values.extend([self._current_world] * len(eq_labels))
@@ -363,6 +367,9 @@ def test_visualization_builder_imports_standalone_stage_as_one_world(self):
363367
self._define_xform(stage, "/World")
364368
self._define_xform(stage, "/World/Robot")
365369
builder = mock.Mock()
370+
builder.shape_collision_filter_pairs = []
371+
builder.shape_collision_group = []
372+
builder.shape_count = 0
366373
builder.add_usd.return_value = {"path_shape_map": {}}
367374

368375
with (
@@ -380,6 +387,47 @@ def test_visualization_builder_imports_standalone_stage_as_one_world(self):
380387
self.assertEqual(registry_groups, [])
381388
builder.add_usd.assert_called_once_with(stage, schema_resolvers=["newton", "physx"], ignore_paths=None)
382389

390+
def test_visualization_builder_disables_collision_pairs(self):
391+
stage = Usd.Stage.CreateInMemory()
392+
robot_path = "/World/envs/env_0/Robot"
393+
self._define_xform(stage, "/World")
394+
self._define_xform(stage, "/World/envs")
395+
self._define_xform(stage, "/World/envs/env_0")
396+
self._define_xform(stage, "/World/envs/env_1", (2.0, 0.0, 0.0))
397+
robot = UsdGeom.Xform.Define(stage, robot_path).GetPrim()
398+
UsdPhysics.ArticulationRootAPI.Apply(robot)
399+
robot.CreateAttribute("physxArticulation:enabledSelfCollisions", Sdf.ValueTypeNames.Bool).Set(False)
400+
for name, translation in (("A", 0.0), ("B", 1.0)):
401+
body_path = f"{robot_path}/{name}"
402+
body = UsdGeom.Xform.Define(stage, body_path)
403+
body.AddTranslateOp().Set((translation, 0.0, 0.0))
404+
UsdPhysics.RigidBodyAPI.Apply(body.GetPrim())
405+
collision = UsdGeom.Cube.Define(stage, f"{body_path}/Collision")
406+
collision.CreateSizeAttr(0.2)
407+
UsdPhysics.CollisionAPI.Apply(collision.GetPrim())
408+
joint = UsdPhysics.RevoluteJoint.Define(stage, f"{robot_path}/Joint")
409+
joint.CreateBody0Rel().SetTargets([Sdf.Path(f"{robot_path}/A")])
410+
joint.CreateBody1Rel().SetTargets([Sdf.Path(f"{robot_path}/B")])
411+
412+
clone_plan = ClonePlan(
413+
sources=(robot_path,),
414+
destinations=("/World/envs/env_{}/Robot",),
415+
clone_mask=torch.ones((1, 2), dtype=torch.bool),
416+
env_ids=torch.arange(2),
417+
)
418+
for env_paths, plan, expected_shape_count in (
419+
([], None, 2),
420+
([(0, "/World/envs/env_0"), (1, "/World/envs/env_1")], clone_plan, 4),
421+
):
422+
builder, _shadow_metadata = visualization_builder_module.build_visualization_builder_from_stage_envs(
423+
stage, env_paths, plan
424+
)
425+
model = builder.finalize(device="cpu")
426+
427+
self.assertEqual(model.shape_count, expected_shape_count)
428+
self.assertEqual(len(model.shape_collision_filter_pairs), 0)
429+
self.assertEqual(model.shape_contact_pair_count, 0)
430+
383431
def test_visualization_builder_rejects_clone_plan_without_environment_paths(self):
384432
"""A cloned scene must not be cached as an incomplete single-world model."""
385433
stage = Usd.Stage.CreateInMemory()

0 commit comments

Comments
 (0)