Add torch support to collection reshape helper#5897
Conversation
There was a problem hiding this comment.
🤖 Isaac Lab Review Bot
Summary
This PR adds torch.Tensor input support to RigidObjectCollection.reshape_data_to_view_3d() in both PhysX and OVPhysX backends, fixing #5893. The implementation dispatches on isinstance(data, torch.Tensor), performs the instance-major → body-major transpose, and honors the target device. Tests cover dtype preservation, device transfer, and Warp/torch output equivalence.
Design Assessment
Architecture: The approach is sound — type dispatch via isinstance before the existing Warp path preserves backward compatibility cleanly. Implementing in both backend classes mirrors the existing pattern.
Reshape correctness: The reshape logic data.transpose(0, 1).reshape(N*M, D).to(device).contiguous() correctly converts from (num_instances, num_bodies, data_dim) to (num_bodies * num_instances, data_dim) in body-major order.
API consistency: The type annotations (wp.array | torch.Tensor) and return type annotations are correctly updated. The docstrings are informative.
Findings
🟡 Minor Issues
-
.to(device).contiguous()ordering —source/isaaclab_physx/.../rigid_object_collection.py:L1147andsource/isaaclab_ovphysx/.../rigid_object_collection.py:L1234After
.reshape()on a transposed tensor, the tensor is likely non-contiguous. Calling.to(device)on a non-contiguous tensor may trigger an implicit copy anyway, but calling.contiguous()after.to()means the contiguity enforcement happens on the target device. This is actually fine for correctness (and may even be preferable for avoiding a double copy), but it is worth noting that ifdevicediffers from the source, there could be a brief non-contiguous tensor on the target device before the final.contiguous()call. The current order is acceptable. -
PhysX
devicedefault changed semantics —source/isaaclab_physx/.../rigid_object_collection.py:L1122The PhysX signature changes from
device: str = "cpu"todevice: str | None = "cpu". This correctly allowsNonefor torch inputs (defaulting todata.device), but for the Warp path further down,deviceis still used as a string directly (noNonecheck before passing towp.clone). This is safe only because the Warp path is reached exclusively whendatais NOT a torch tensor, and in that casedevicedefaults to"cpu"(the callers would never passNoneexplicitly for Warp data). However, if a user explicitly passesdevice=Nonewith a Warp array, the code would passNonetowp.clone, which may raise. Consider adding a guard:if device is None: device = str(data.device)
at the top of the Warp path (similar to what OVPhysX already does).
-
Duplicate logic across backends — Both PhysX and OVPhysX implement the identical torch path. This is a minor DRY concern but consistent with the existing codebase pattern where each backend has its own implementation. Not blocking.
🟢 No Issues
- Type dispatch (
isinstance(data, torch.Tensor)) is correct and robust. - Device placement is honored for torch tensors.
- Dtype preservation is verified by tests.
- The Warp code path is untouched and unaffected.
- Changelog fragments follow project conventions.
Test Coverage
Strong. The PR adds 14 test cases (parametrized across backends, devices, and dtypes):
- ✅
test_reshape_data_to_view_3d_accepts_torch_tensor— validates shape, dtype, device, contiguity, and correctness of the reorder againstpermute(1,0,2).reshape() - ✅
test_reshape_data_to_view_3d_moves_torch_tensor_to_requested_device— cross-device (CUDA→CPU) transfer - ✅
test_reshape_data_to_view_3d_keeps_warp_array_behavior— regression test ensuring Warp path unchanged and outputs match torch
Suggestions:
- Consider adding a test that explicitly passes
device=Nonewith a torch tensor to verify the default-device fallback path. - A test with a 1D
data_dim=1edge case would strengthen coverage.
Verdict
Minor fixes needed — The core implementation is correct and well-tested. The only actionable concern is the potential device=None passthrough to wp.clone in the PhysX backend if a user explicitly sets device=None with a Warp array (unlikely but possible). A one-line guard would close this gap.
Update (139d07d): New commit adds the Newton backend implementation of reshape_data_to_view_3d with torch support, a changelog fragment, and expands test parametrization to include the Newton backend.
- ✅ The Newton implementation correctly includes the
device=Noneguard for the Warp path (if device is None: device = str(data.device)). - The PhysX backend still lacks this guard on its Warp path (default
"cpu"mitigates practical risk, but an explicitNonecould still propagate). Not re-posting since it's minor and unlikely to be hit in practice. - Overall: LGTM. Clean implementation consistent across all three backends.
Greptile SummaryThis PR extends
Confidence Score: 4/5Safe to merge; the torch reshape logic is correct and well-tested, with only a minor type annotation inaccuracy. The reshape arithmetic (transpose(0,1).reshape(B*N, D)) produces the correct body-major layout and is validated against the Warp path in tests. The one concrete inaccuracy is that device = data.device assigns a torch.device object to a variable declared str | None, which will surface as a type-checker warning in both backends but has no runtime impact. Both backend rigid_object_collection.py files share the same device type annotation gap in the new torch branch. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["reshape_data_to_view_3d(data, data_dim, device)"] --> B{isinstance data torch.Tensor?}
B -- Yes --> C{device is None?}
C -- Yes --> D["device = data.device"]
C -- No --> E["use supplied device"]
D --> F["data.transpose(0,1)\n.reshape(num_bodies*num_instances, data_dim)\n.to(device).contiguous()"]
E --> F
F --> G["return torch.Tensor\n(body-major, contiguous)"]
B -- No --> H{device is None?}
H -- Yes --> I["device = str(data.device)"]
H -- No --> J["use supplied device (str)"]
I --> K["wp.array strided view\n(B, N, data_dim)"]
J --> K
K --> L["wp.clone → reshape\n(num_bodies*num_instances, data_dim)"]
L --> M["return wp.array\n(body-major, contiguous)"]
Reviews (1): Last reviewed commit: "Add torch support to collection reshape ..." | Re-trigger Greptile |
| if isinstance(data, torch.Tensor): | ||
| if device is None: | ||
| device = data.device | ||
| return data.transpose(0, 1).reshape(self.num_bodies * self.num_instances, data_dim).to(device).contiguous() |
There was a problem hiding this comment.
The
device parameter is annotated as str | None, but when device=None and a torch.Tensor is passed, the local variable is immediately reassigned to data.device, which is a torch.device object — not a str. Static type checkers (mypy/pyright) will flag the re-assignment as a type error. Using str(data.device) keeps the annotation correct and is consistent with how the warp path normalises the device string below.
| if isinstance(data, torch.Tensor): | |
| if device is None: | |
| device = data.device | |
| return data.transpose(0, 1).reshape(self.num_bodies * self.num_instances, data_dim).to(device).contiguous() | |
| if isinstance(data, torch.Tensor): | |
| if device is None: | |
| device = str(data.device) | |
| return data.transpose(0, 1).reshape(self.num_bodies * self.num_instances, data_dim).to(device).contiguous() |
There was a problem hiding this comment.
seems like a valid point. there's some inconsistency between the check on line 1235 and 1239
There was a problem hiding this comment.
Thanks for confirming! The author addressed this in the Newton backend commit — adding a device is not None guard before the Warp wp.clone(..., device=device) call. The OVPhysX backend here could benefit from the same treatment for consistency.
There was a problem hiding this comment.
Thanks for confirming! The fix is straightforward—changing line 1235 from:
device = data.deviceto:
device = str(data.device)This keeps the type annotation (str | None) accurate and maintains consistency with the Warp path below. The .to(device) call on torch tensors accepts both str and torch.device, so this change is safe.
| if isinstance(data, torch.Tensor): | ||
| if device is None: | ||
| device = data.device | ||
| return data.transpose(0, 1).reshape(self.num_bodies * self.num_instances, data_dim).to(device).contiguous() |
There was a problem hiding this comment.
Same type annotation issue as in the ovphysx backend:
device is typed as str | None but is reassigned to data.device (torch.device) when it is None and the input is a torch tensor. Use str(data.device) to stay within the declared type.
| if isinstance(data, torch.Tensor): | |
| if device is None: | |
| device = data.device | |
| return data.transpose(0, 1).reshape(self.num_bodies * self.num_instances, data_dim).to(device).contiguous() | |
| if isinstance(data, torch.Tensor): | |
| if device is None: | |
| device = str(data.device) | |
| return data.transpose(0, 1).reshape(self.num_bodies * self.num_instances, data_dim).to(device).contiguous() |
Description
Adds
torch.Tensorinput support toRigidObjectCollection.reshape_data_to_view_3d()for the PhysX, Newton, and OVPhysX backends. Torch inputs now reshape from instance-major(num_instances, num_bodies, data_dim)to body-major(num_bodies * num_instances, data_dim), preserve dtype, return a contiguous torch tensor, and honor the requested output device.The existing Warp array paths remain unchanged after the torch type check where the helper already existed. Newton now exposes the same helper for backend API consistency.
Fixes #5893
Type of change
Screenshots
N/A
Test Plan
PYTHONPATH=/home/antoiner/Documents/IsaacLab_Newton/.worktrees/fix-issue-5893/source/isaaclab_newton:/home/antoiner/Documents/IsaacLab_Newton/.worktrees/fix-issue-5893/source/isaaclab_ovphysx:/home/antoiner/Documents/IsaacLab_Newton/.worktrees/fix-issue-5893/source/isaaclab_physx:/home/antoiner/Documents/IsaacLab_Newton/.worktrees/fix-issue-5893/source/isaaclab:$PYTHONPATH ./isaaclab.sh -p -m pytest source/isaaclab/test/assets/test_rigid_object_collection_iface.py::TestCollectionViewReshape -qgit diff --check./isaaclab.sh --formatcheck-git-lfs-pointersfailed becausegit-lfsis not installed in this local environment.Checklist
pre-commitchecks with./isaaclab.sh --format(ran the exact command; blocked only by missing localgit-lfs)source/<pkg>/changelog.d/for every touched package (do not editCHANGELOG.rstor bumpextension.toml— CI handles that)CONTRIBUTORS.mdor my name already exists there