Skip to content

Commit bffdce9

Browse files
pv-nvidiaalexmillanepv-nvidiayts-nvkellyguo11
authored
[Backport release/3.0.0-beta2] FrameView local poses (#5677) + view-scoped Fabric selections (#6805) (#7342)
# Description Backports two already-merged FrameView PRs to `release/3.0.0-beta2`, cherry-picked in merge order: - #5677 — GPU-accelerated FrameView local poses + `xform_space_writer` API (upstream `69888c34e471`) - #6805 — scale `FabricFrameView` selections to the view, not the stage (upstream `480370cdc5b0`, nvbug 6535498) Two support commits, because this branch predates some prerequisites: - `Shim needed for Arena compatibility.` (@amillane) adds `split_clone_template`, backported from #6071. Ordered first so no commit imports it before it exists. - `fix: use the OVPhysX tensor binding API…` restores this branch's `physx.create_tensor_binding()` / `binding.read()`. The #5677 pick referenced `OvPhysxView`, which arrives with #6224 and is absent here, so `OvPhysxFrameView` init raised `ModuleNotFoundError`. Conflict resolutions converge each touched file on its post-#6805 state from `develop`, so the backported code is byte-identical to what has been running and tested there. Two consequences of that choice: - `get_scales()` now returns `ProxyArray` in all backends, as on `develop`. On this branch it previously returned `wp.array`, so callers that pass the result straight to Warp APIs need `.warp` (Torch users: `.torch`). This is the one API change the backport carries. - The Fabric-hierarchy import stays unconditional (the optional import is #6499, not backported). The only deviation from `develop`'s state is test-infra: device parametrization uses literal `["cpu", "cuda:0"]` because `test_devices()` does not exist on this branch. ## Type of change - Bug fix (non-breaking change which fixes an issue) - New feature (non-breaking change which adds functionality) — `FrameView.close()`, writer-scope API - Breaking change (relative to this release branch only): `get_scales()` returns `ProxyArray` instead of `wp.array`, matching `develop` ## Tests - `test_views_xform_prim_ovphysx.py` — 49 passed (26 `cpu` + 24 `cuda:0`; needs one process per device, `ovphysx<=0.3.7` binds device mode in C++ on first `PhysX(...)`) - `test_views_xform_prim_newton.py` — 58 passed --------- Co-authored-by: alex <amillane@nvidia.com> Co-authored-by: pv-nvidia <197907000+pv-nvidia@users.noreply.github.com> Co-authored-by: pv-nvidia <peter.verswyvelen@gmail.com> Co-authored-by: yts-nv <yts-nv@users.noreply.github.com> Co-authored-by: Kelly Guo <kellyg@nvidia.com>
1 parent 367e498 commit bffdce9

35 files changed

Lines changed: 3473 additions & 597 deletions

docs/source/api/lab/isaaclab.utils.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,3 +188,16 @@ Warp operations
188188
:members:
189189
:imported-members:
190190
:show-inheritance:
191+
192+
Warp Fabric kernels
193+
^^^^^^^^^^^^^^^^^^^
194+
195+
Warp kernels for reading and writing Fabric ``Matrix4d`` attributes
196+
(``omni:fabric:worldMatrix`` / ``omni:fabric:localMatrix``) via
197+
:class:`wp.fabricarray` and :class:`wp.indexedfabricarray`. Used by
198+
:class:`~isaaclab_physx.sim.views.FabricFrameView` to keep child world and
199+
local matrices consistent without round-tripping through USD.
200+
201+
.. automodule:: isaaclab.utils.warp.fabric
202+
:members:
203+
:show-inheritance:

scripts/benchmarks/benchmark_view_comparison.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,8 @@ def _run_pose_benchmarks(
284284

285285
start_time = time.perf_counter()
286286
for _ in range(num_iterations):
287-
view.set_world_poses(new_positions, orientations)
287+
with view.xform_world_space_writer() as w:
288+
w.set_poses(new_positions, orientations)
288289
timing_results["set_world_poses"] = (time.perf_counter() - start_time) / num_iterations
289290

290291
ret_pos, ret_quat = view.get_world_poses()

scripts/benchmarks/benchmark_xform_prim_view.py

Lines changed: 121 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -138,21 +138,35 @@ def benchmark_frame_view( # noqa: C901
138138

139139
is_newton = api == "isaaclab-newton-site"
140140

141+
# Synchronize around timed regions using Warp directly (rather than torch),
142+
# since all backend kernels here are Warp launches and ``wp.synchronize()``
143+
# covers CPU/CUDA Warp devices consistently. We only need it for GPU runs,
144+
# where kernel launches are asynchronous; guard on device to avoid paying
145+
# it needlessly on CPU.
146+
_needs_sync = str(device).startswith("cuda")
147+
148+
def _sync() -> None:
149+
if _needs_sync:
150+
wp.synchronize()
151+
141152
def to_torch(a):
142-
return wp.to_torch(a) if isinstance(a, wp.array) else a
153+
if isinstance(a, wp.array):
154+
return wp.to_torch(a)
155+
if hasattr(a, "torch"):
156+
return a.torch
157+
return a
143158

144159
try:
145160
# -- Warmup --------------------------------------------------------
146161
xform_view.get_world_poses()
162+
xform_view.get_world_scales()
147163

148164
# -- get_world_poses -----------------------------------------------
149-
if is_newton:
150-
torch.cuda.synchronize()
165+
_sync()
151166
start_time = time.perf_counter()
152167
for _ in range(num_iterations):
153168
positions, orientations = xform_view.get_world_poses()
154-
if is_newton:
155-
torch.cuda.synchronize()
169+
_sync()
156170
timing_results["get_world_poses"] = (time.perf_counter() - start_time) / num_iterations
157171

158172
positions_t = to_torch(positions)
@@ -161,34 +175,36 @@ def to_torch(a):
161175
computed_results["initial_world_orientations"] = orientations_t.clone()
162176

163177
# -- set_world_poses -----------------------------------------------
178+
# ``.warp`` unwraps the ProxyArray returned by ``get_*_poses`` /
179+
# ``get_*_scales`` to the underlying ``wp.array`` that ``wp.clone``
180+
# requires. ProxyArray was introduced in PR #5304 ("ProxyArray and
181+
# Asset/Sensor level property caching") which changed the FrameView
182+
# getter return type. Applies to every ``wp.clone`` call below.
164183
if is_newton:
165-
new_positions = wp.clone(positions)
184+
new_positions = wp.clone(positions.warp)
166185
wp.to_torch(new_positions)[:, 2] += 0.1
167186
else:
168187
new_positions = positions_t.clone()
169188
new_positions[:, 2] += 0.1
170189

171-
if is_newton:
172-
torch.cuda.synchronize()
190+
_sync()
173191
start_time = time.perf_counter()
174192
for _ in range(num_iterations):
175-
xform_view.set_world_poses(new_positions, orientations)
176-
if is_newton:
177-
torch.cuda.synchronize()
193+
with xform_view.xform_world_space_writer() as w:
194+
w.set_poses(new_positions, orientations)
195+
_sync()
178196
timing_results["set_world_poses"] = (time.perf_counter() - start_time) / num_iterations
179197

180198
pa, oa = xform_view.get_world_poses()
181199
computed_results["world_positions_after_set"] = to_torch(pa).clone()
182200
computed_results["world_orientations_after_set"] = to_torch(oa).clone()
183201

184202
# -- get_local_poses -----------------------------------------------
185-
if is_newton:
186-
torch.cuda.synchronize()
203+
_sync()
187204
start_time = time.perf_counter()
188205
for _ in range(num_iterations):
189206
translations, orientations_local = xform_view.get_local_poses()
190-
if is_newton:
191-
torch.cuda.synchronize()
207+
_sync()
192208
timing_results["get_local_poses"] = (time.perf_counter() - start_time) / num_iterations
193209

194210
translations_t = to_torch(translations)
@@ -198,45 +214,99 @@ def to_torch(a):
198214

199215
# -- set_local_poses -----------------------------------------------
200216
if is_newton:
201-
new_translations = wp.clone(translations)
217+
new_translations = wp.clone(translations.warp)
202218
wp.to_torch(new_translations)[:, 2] += 0.1
203219
else:
204220
new_translations = translations_t.clone()
205221
new_translations[:, 2] += 0.1
206222

207-
if is_newton:
208-
torch.cuda.synchronize()
223+
_sync()
209224
start_time = time.perf_counter()
210225
for _ in range(num_iterations):
211-
xform_view.set_local_poses(new_translations, orientations_local)
212-
if is_newton:
213-
torch.cuda.synchronize()
226+
with xform_view.xform_local_space_writer() as w:
227+
w.set_poses(new_translations, orientations_local)
228+
_sync()
214229
timing_results["set_local_poses"] = (time.perf_counter() - start_time) / num_iterations
215230

216231
ta, ola = xform_view.get_local_poses()
217232
computed_results["local_translations_after_set"] = to_torch(ta).clone()
218233
computed_results["local_orientations_after_set"] = to_torch(ola).clone()
219234

220-
# -- get_both (world + local) --------------------------------------
235+
# -- get_world_scales ----------------------------------------------
236+
_sync()
237+
start_time = time.perf_counter()
238+
for _ in range(num_iterations):
239+
world_scales = xform_view.get_world_scales()
240+
_sync()
241+
timing_results["get_world_scales"] = (time.perf_counter() - start_time) / num_iterations
242+
243+
world_scales_t = to_torch(world_scales)
244+
computed_results["initial_world_scales"] = world_scales_t.clone()
245+
246+
# -- set_world_scales ----------------------------------------------
247+
if is_newton:
248+
new_world_scales = wp.clone(world_scales.warp)
249+
wp.to_torch(new_world_scales)[:] = 1.1
250+
else:
251+
new_world_scales = world_scales_t.clone()
252+
new_world_scales[:] = 1.1
253+
254+
_sync()
255+
start_time = time.perf_counter()
256+
for _ in range(num_iterations):
257+
with xform_view.xform_world_space_writer() as w:
258+
w.set_scales(new_world_scales)
259+
_sync()
260+
timing_results["set_world_scales"] = (time.perf_counter() - start_time) / num_iterations
261+
262+
computed_results["world_scales_after_set"] = to_torch(xform_view.get_world_scales()).clone()
263+
264+
# -- get_local_scales ----------------------------------------------
265+
_sync()
266+
start_time = time.perf_counter()
267+
for _ in range(num_iterations):
268+
local_scales = xform_view.get_local_scales()
269+
_sync()
270+
timing_results["get_local_scales"] = (time.perf_counter() - start_time) / num_iterations
271+
272+
local_scales_t = to_torch(local_scales)
273+
computed_results["initial_local_scales"] = local_scales_t.clone()
274+
275+
# -- set_local_scales ----------------------------------------------
221276
if is_newton:
222-
torch.cuda.synchronize()
277+
new_local_scales = wp.clone(local_scales.warp)
278+
wp.to_torch(new_local_scales)[:] = 0.9
279+
else:
280+
new_local_scales = local_scales_t.clone()
281+
new_local_scales[:] = 0.9
282+
283+
_sync()
284+
start_time = time.perf_counter()
285+
for _ in range(num_iterations):
286+
with xform_view.xform_local_space_writer() as w:
287+
w.set_scales(new_local_scales)
288+
_sync()
289+
timing_results["set_local_scales"] = (time.perf_counter() - start_time) / num_iterations
290+
291+
computed_results["local_scales_after_set"] = to_torch(xform_view.get_local_scales()).clone()
292+
293+
# -- get_both (world + local) --------------------------------------
294+
_sync()
223295
start_time = time.perf_counter()
224296
for _ in range(num_iterations):
225297
xform_view.get_world_poses()
226298
xform_view.get_local_poses()
227-
if is_newton:
228-
torch.cuda.synchronize()
299+
_sync()
229300
timing_results["get_both"] = (time.perf_counter() - start_time) / num_iterations
230301

231302
# -- interleaved set -> get ----------------------------------------
232-
if is_newton:
233-
torch.cuda.synchronize()
303+
_sync()
234304
start_time = time.perf_counter()
235305
for _ in range(num_iterations):
236-
xform_view.set_world_poses(new_positions, orientations)
306+
with xform_view.xform_world_space_writer() as w:
307+
w.set_poses(new_positions, orientations)
237308
xform_view.get_world_poses()
238-
if is_newton:
239-
torch.cuda.synchronize()
309+
_sync()
240310
timing_results["interleaved_world_set_get"] = (time.perf_counter() - start_time) / num_iterations
241311

242312
finally:
@@ -267,15 +337,26 @@ def print_results(results_dict: dict[str, dict[str, float]], num_prims: int, num
267337
print(header)
268338
print("-" * 120)
269339

270-
operations = [
271-
("Initialization", "init"),
340+
# ``init`` is the one-time view-construction cost. We display it in the
341+
# per-operation table but EXCLUDE it from the steady-state totals and the
342+
# overall speedup -- otherwise a backend whose construction is dominated
343+
# by stage population (e.g. Newton, where the first call materializes the
344+
# site cache) shows a misleading "0.00x" overall and crushes the rest of
345+
# the table. The overall row is intended to compare per-iteration cost.
346+
init_op = ("Initialization (one-time)", "init")
347+
per_iter_operations = [
272348
("Get World Poses", "get_world_poses"),
273349
("Set World Poses", "set_world_poses"),
274350
("Get Local Poses", "get_local_poses"),
275351
("Set Local Poses", "set_local_poses"),
352+
("Get World Scales", "get_world_scales"),
353+
("Set World Scales", "set_world_scales"),
354+
("Get Local Scales", "get_local_scales"),
355+
("Set Local Scales", "set_local_scales"),
276356
("Get Both (World+Local)", "get_both"),
277357
("Interleaved World Set->Get", "interleaved_world_set_get"),
278358
]
359+
operations = [init_op, *per_iter_operations]
279360

280361
for op_name, op_key in operations:
281362
row = f"{op_name:<28}"
@@ -286,15 +367,16 @@ def print_results(results_dict: dict[str, dict[str, float]], num_prims: int, num
286367

287368
print("=" * 120)
288369

289-
total_row = f"{'Total':<28}"
370+
total_row = f"{'Total (per-iter ops)':<28}"
290371
for name in api_names:
291-
total_row += f" {sum(results_dict[name].values()) * 1000:>{col_width}.4f}"
372+
per_iter_total = sum(results_dict[name].get(k, 0) for _, k in per_iter_operations)
373+
total_row += f" {per_iter_total * 1000:>{col_width}.4f}"
292374
print(f"\n{total_row}")
293375

294376
baseline = "isaaclab-usd"
295377
if baseline in results_dict and len(api_names) > 1:
296378
print("\n" + "=" * 120)
297-
print(f"SPEEDUP vs {baseline.replace('-', ' ').title()}")
379+
print(f"SPEEDUP vs {baseline.replace('-', ' ').title()} (per-iter ops; one-time init excluded)")
298380
print("=" * 120)
299381
header = f"{'Operation':<28}"
300382
for name in api_names:
@@ -304,7 +386,7 @@ def print_results(results_dict: dict[str, dict[str, float]], num_prims: int, num
304386
print("-" * 120)
305387

306388
base = results_dict[baseline]
307-
for op_name, op_key in operations:
389+
for op_name, op_key in per_iter_operations:
308390
row = f"{op_name:<28}"
309391
base_t = base.get(op_key, 0)
310392
for name in api_names:
@@ -317,11 +399,11 @@ def print_results(results_dict: dict[str, dict[str, float]], num_prims: int, num
317399
print(row)
318400

319401
print("=" * 120)
320-
print(f"{'Overall':>28}", end="")
321-
total_base = sum(base.values())
402+
print(f"{'Overall (per-iter ops)':>28}", end="")
403+
total_base = sum(base.get(k, 0) for _, k in per_iter_operations)
322404
for name in api_names:
323405
if name != baseline:
324-
total_impl = sum(results_dict[name].values())
406+
total_impl = sum(results_dict[name].get(k, 0) for _, k in per_iter_operations)
325407
if total_base > 0 and total_impl > 0:
326408
print(f" {total_base / total_impl:>{col_width}.2f}x", end="")
327409
else:
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
Added
2+
^^^^^
3+
4+
* Added explicit local/world scale getters
5+
:meth:`~isaaclab.sim.views.BaseFrameView.get_local_scales` and
6+
:meth:`~isaaclab.sim.views.BaseFrameView.get_world_scales` to the FrameView
7+
API, implemented for :class:`~isaaclab.sim.views.UsdFrameView`. Scale
8+
writes go through the writer scope (see the ``xform-space-writer``
9+
fragment).
10+
11+
* Added :func:`~isaaclab.utils.warp.fabric.decompose_indexed_fabric_transforms`,
12+
:func:`~isaaclab.utils.warp.fabric.compose_indexed_fabric_transforms`,
13+
:func:`~isaaclab.utils.warp.fabric.update_indexed_local_matrix_from_world`, and
14+
:func:`~isaaclab.utils.warp.fabric.update_indexed_world_matrix_from_local`
15+
Warp kernels operating on :class:`wp.indexedfabricarray` for reading and
16+
writing Fabric ``Matrix4d`` attributes (``omni:fabric:worldMatrix`` /
17+
``omni:fabric:localMatrix``).
18+
19+
Notes
20+
^^^^^
21+
22+
* :meth:`~isaaclab.sim.views.BaseFrameView.get_scales` and
23+
:meth:`~isaaclab.sim.views.BaseFrameView.set_scales` remain supported as
24+
convenience helpers (not deprecated). For reads where the space matters,
25+
prefer the explicit ``get_local_scales`` (operates on ``xformOp:scale``) or
26+
``get_world_scales`` (composed world-space scale). For writes that also
27+
update poses, prefer batching inside one scope:
28+
``with view.xform_world_space_writer() as w: w.set_poses(...); w.set_scales(...)``
29+
(or ``xform_local_space_writer``).
30+
:class:`~isaaclab.sim.views.UsdFrameView` preserves prior behavior by
31+
defaulting :meth:`get_scales` / :meth:`set_scales` to local scales.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Fixed
2+
^^^^^
3+
4+
* Fixed :class:`~isaaclab.scene_data.SceneDataProvider` transform mapping stalling
5+
at high rigid-body counts, which delayed setup by minutes in scenes with
6+
thousands of environments.
7+
8+
Added
9+
^^^^^
10+
11+
* Added :meth:`~isaaclab.sim.views.BaseFrameView.close` to release backend state
12+
authored by a frame view. Backends also release best-effort on garbage
13+
collection, but only an explicit close is deterministic.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
Added
2+
^^^^^
3+
4+
* Added :class:`~isaaclab.sim.views.FrameViewSpaceWriterBase`, the new context-managed
5+
write API for ``FrameView``-managed prim transforms. Open with
6+
``view.xform_world_space_writer()`` or ``view.xform_local_space_writer()`` and call
7+
:meth:`~isaaclab.sim.views.FrameViewSpaceWriterBase.set_poses` /
8+
:meth:`~isaaclab.sim.views.FrameViewSpaceWriterBase.set_scales` inside the scope;
9+
the writer's ``__exit__`` derives the opposite-space matrices once and
10+
synchronizes once. Only one writer scope may be active per view at a
11+
time. View-level getters
12+
(:meth:`~isaaclab.sim.views.BaseFrameView.get_world_poses` etc.) raise
13+
:class:`RuntimeError` while a writer scope is active.
14+
15+
* Added the two concrete tag classes
16+
:class:`~isaaclab.sim.views.FrameViewWorldSpaceWriter` and
17+
:class:`~isaaclab.sim.views.FrameViewLocalSpaceWriter` returned by
18+
:meth:`~isaaclab.sim.views.BaseFrameView.xform_world_space_writer` /
19+
:meth:`~isaaclab.sim.views.BaseFrameView.xform_local_space_writer`.
20+
21+
Notes
22+
^^^^^
23+
24+
* :meth:`~isaaclab.sim.views.BaseFrameView.set_world_poses`,
25+
:meth:`~isaaclab.sim.views.BaseFrameView.set_local_poses`, and
26+
:meth:`~isaaclab.sim.views.BaseFrameView.set_scales` remain supported as
27+
convenience helpers (not deprecated). Each opens a single-statement writer
28+
scope internally, so updating poses and scales through separate calls derives
29+
the opposite-space matrices and synchronizes twice. For best performance,
30+
update both inside one scope:
31+
``with view.xform_world_space_writer() as w: w.set_poses(...); w.set_scales(...)``
32+
(or :meth:`~isaaclab.sim.views.BaseFrameView.xform_local_space_writer`).
33+
The bundled examples use the writer scope to get this benefit; callers may
34+
keep the convenience helpers when code simplicity matters more than shaving
35+
a redundant derive/sync.

0 commit comments

Comments
 (0)