Fix OVPhysX device routing for CPU-resident tensor types - #7563
Conversation
_CPU_ONLY_TYPES omitted eight tensor types that are CPU-resident even on a GPU simulation: the articulation and rigid-body collision-shape contact and rest offsets, both gravity-disable flags, and the DOF drive type and drive model. Because _native_device consults this set, it returned the simulation device for host-resident data. Every access staged a hidden host-to-device copy on a path the view believed was device-native, and _check_device rejected a buffer placed on the host, where the data actually lives, with DeviceMismatch. Residency was measured rather than assumed: ovphysx exposes no residency query, so each type was classified by counting CUDA memcpys around a binding read into a host buffer and into a device buffer, under the DirectGPU configuration isaaclab_ov itself sets. All eight measured CPU-resident across every GPU sample scene in the ovphysx wheel, and all previously classified members measured as declared. The guarding test compared the view's derived set against the canonical set it is derived from, so it could not fail for any contents. It now asserts against an independent inventory of measured residency, mirroring the existing _EXPECTED_READ_ONLY_NAMES pattern, so an incomplete set is caught.
ovphysx documents ARTICULATION_DOF_DRIVE_TYPE as read-only, but the attribute was missing from _READ_ONLY_NAMES, so set_attribute accepted writes to it and forwarded them to the binding instead of raising. An audit of the whole classification against the wheel's type table found this to be the only gap: of the twenty-one types ovphysx documents as read-only, twenty were already classified, and no member was misclassified. This is a behavior change. Writes to articulation_dof_drive_type that were previously accepted now raise OvPhysxView.ReadOnlyAttribute. Drive type is authored through the USD drive schema, not the tensor path. The existing read-only behavior test is parameterized over the expected inventory, so adding the name there also asserts that a write raises and leaves no binding behind.
Greptile SummaryThis PR corrects OVPhysX tensor routing for eight CPU-resident types and classifies articulation DOF drive type as read-only.
Confidence Score: 5/5The PR appears safe to merge with no actionable correctness, security, or maintainability issues identified. The new classifications consistently feed the existing device-routing and read-only checks, are covered by independent inventory assertions, and the intentional behavior change is documented with migration guidance. Important Files Changed
Reviews (1): Last reviewed commit: "Reject writes to the read-only DOF drive..." | Re-trigger Greptile |
There was a problem hiding this comment.
Isaac Lab Review Bot
The change updates the canonical CPU-residency classification for eight OVPhysX tensor types and marks articulation_dof_drive_type read-only, with focused inventory tests and migration guidance. The proposed changelog-version finding is not supported by a trusted rule establishing that this repository requires a major fragment for this contract change.
- Design and architecture: The reviewed design keeps device routing centralized in
_CPU_ONLY_TYPESand derives the view’s name classification from that set, avoiding duplicate production tables. Using private_TTmembers avoids unnecessarily expanding the public tensor-type alias surface. - API: No public symbols are added or removed. Writes to
articulation_dof_drive_typenow deterministically raiseReadOnlyAttribute, while correctly host-resident buffers for the eight added types pass device validation. The behavior change and migration path are explicitly documented. - Implementation: The added classifications propagate through the existing read-only validation and native-device routing paths. The tests replace the prior self-derived CPU-only assertion with an independent measured inventory and extend the read-only inventory. The hard-coded inventory remains coupled to the supported OVPhysX wheel’s available enum members, which is a non-blocking compatibility tradeoff.
No blocking issues. No inline issue met the actionable-evidence threshold; the assessment above records the review feedback.
Automated review; human maintainers own approval decisions.
The eight types added to the CPU-only classification were referenced through the raw ``_TT`` enum, leaving the candidate tuple as a mix of module aliases and raw enum members and giving a future reader no reason why these eight were different. This module is the alias layer over the ovphysx enum, so a type worth classifying is worth naming. Each alias carries the shape, dtype and units taken from the wheel's own type table, which the raw references did not. Naming follows the established convention: the ``ARTICULATION_`` prefix is dropped for articulation types, as with DOF_STIFFNESS and BODY_MASS, and retained for rigid-body ones, as with RIGID_BODY_MASS.
|
run-ci |
|
Backported to |
# Description `_CPU_ONLY_TYPES` in `isaaclab_ov` is the single constant deciding whether an `OvPhysxView` tensor buffer is allocated on, and validated against, host or device memory. It omitted eight tensor types that are CPU-resident even on a GPU simulation: | tensor type | alias | shape | |---|---|---| | `ARTICULATION_CONTACT_OFFSET` | `CONTACT_OFFSET` | `[N, S]` | | `ARTICULATION_REST_OFFSET` | `REST_OFFSET` | `[N, S]` | | `RIGID_BODY_CONTACT_OFFSET` | `RIGID_BODY_CONTACT_OFFSET` | `[N, S]` | | `RIGID_BODY_REST_OFFSET` | `RIGID_BODY_REST_OFFSET` | `[N, S]` | | `ARTICULATION_BODY_DISABLE_GRAVITY` | `BODY_DISABLE_GRAVITY` | `[N, L]` | | `RIGID_BODY_DISABLE_GRAVITY` | `RIGID_BODY_DISABLE_GRAVITY` | `[N]` | | `ARTICULATION_DOF_DRIVE_TYPE` | `DOF_DRIVE_TYPE` | `[N, D]` | | `ARTICULATION_DOF_DRIVE_MODEL` | `DOF_DRIVE_MODEL` | `[N, D, 3]` | Because `_native_device` consults this set, it returned the simulation device for host-resident data. Two consequences: 1. A hidden per-call host-to-device staging copy on every access, on a path the view believed was device-native. Silent, and not attributable to Isaac Lab in a profile. Values are still correct — ovphysx stages the mismatch transparently, so this is not a data-integrity bug. 2. `_check_device` rejected a **correctly** placed buffer: allocating one of these on the host, where the data actually lives, raised `OvPhysxView.DeviceMismatch`, whose message advises moving the buffer yourself — precisely the advice that fails here. No shipped Isaac Lab workflow reads these eight through the tensor path today, so this is latent rather than a live outage. It is not an obscure corner: Isaac Lab already authors these same properties through USD schema writes at scene construction, and the tensor path exists to change such values per-env at runtime. Each of the eight is also exposed as a public alias carrying its shape, dtype and units, taken from the wheel's own type table. This module is the alias layer over the ovphysx enum, so referencing them raw would have left the candidate tuple a mix of aliases and raw enum members with no stated reason why these eight differed. Naming follows the existing convention: the `ARTICULATION_` prefix is dropped for articulation types, as with `DOF_STIFFNESS` and `BODY_MASS`, and retained for rigid-body ones, as with `RIGID_BODY_MASS`. A second, orthogonal gap surfaced during the audit: ovphysx documents `ARTICULATION_DOF_DRIVE_TYPE` as read-only, but it was missing from `_READ_ONLY_NAMES`, so `set_attribute` accepted writes and forwarded them. An audit of the full classification against the wheel's type table found this to be the only such gap — of the 21 types ovphysx documents read-only, 20 were already classified, with no misclassified members. ## Type of change - Bug fix (non-breaking change which fixes an issue) - New feature (non-breaking change which adds functionality) - **Breaking change** — writes to `articulation_dof_drive_type` now raise `OvPhysxView.ReadOnlyAttribute` instead of being silently forwarded. Migration: remove the write; drive type is authored through the USD drive schema, not the tensor path. ## Release backport - [x] <!-- backport-active-release --> Backport this pull request to the active release branch after it merges into `develop` ## Screenshots Not applicable. ## Checklist - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [x] I have added my name to the `CONTRIBUTORS.md` or my name already exists there ## How it was tested ovphysx exposes no residency query (`ovphysx_tensor_spec_t` carries dtype/ndim/shape and no device field), so residency was **measured**, not assumed: CUDA memcpys were counted via CUPTI around each binding read into a host buffer and into a device buffer, under the DirectGPU configuration `isaaclab_ov` itself sets (`"/physics/suppressReadback": True`). A host read showing `DtoH` means GPU-resident; no memcpy on the host read plus `HtoD` on the device read means CPU-resident. Measured across every GPU sample scene in the ovphysx 0.5.11 wheel: | scene | before | after | |---|---|---| | `two_articulations_gpu` | `agree=40 MISMATCH=8` | `agree=48 MISMATCH=0` | | `links_chain_sample_gpu` | `agree=40 MISMATCH=8` | `agree=48 MISMATCH=0` | | `mixed_base_articulations_gpu` | `agree=36 MISMATCH=8` | `agree=44 MISMATCH=0` | | `boxes_falling_on_groundplane_gpu` | `agree=9 MISMATCH=3` | `agree=12 MISMATCH=0` | | `volume_deformable_multi` | `MISMATCH=0` | `MISMATCH=0` | | `surface_deformable_material` | `MISMATCH=0` | `MISMATCH=0` | Every previously classified member measured CPU-resident as declared in all scenes, so the set was incomplete rather than wrong — there were no false positives to correct. The guarding test was a tautology: it compared the view's derived set against the canonical set it is derived from, so it could not fail for any contents. It now asserts against an independent inventory of measured residency, mirroring the existing `_EXPECTED_READ_ONLY_NAMES` pattern. Both new assertions were confirmed to fail before the fix and pass after it; the read-only case fails with `DID NOT RAISE ReadOnlyAttribute` without the fix. - `pytest source/isaaclab_ov/test/sim/test_ovphysx_view.py` — 81 passed - `test_articulation_helpers`, `test_rigid_object_helpers`, `test_deformable_object_helpers`, `test_ovphysx_scene_data_backend`, `test_randomize_rigid_body_material_mdp` — all pass - `uv run isaaclab -f` — passes - `tools/changelog/cli.py check develop` — passes `test_views_xform_prim_ovphysx.py` reports 23 failed / 28 passed both with and without this change; every failure is the process-global device lock covered by the `device_split` marker, which requires per-device re-invocation. Baselined on a clean tree to confirm it is unrelated. Environment: ovphysx 0.5.11, Linux x86_64, Python 3.12, NVIDIA RTX 5000 Ada Generation Laptop GPU. (cherry picked from commit ec58631)
…7569) # Description > **Stacked on #7563.** This branch needs the `REST_OFFSET` / `CONTACT_OFFSET` / `RIGID_BODY_*_OFFSET` aliases and the CPU-only routing for those tensor types that #7563 adds. Review/merge that first; this diff collapses to a single commit once it lands, at which point the base should be retargeted to `develop`. `randomize_rigid_body_collider_offsets` had no OVPhysX implementation. Its backend dispatch tested only for `"newton"` and fell through a bare `else` to `_RandomizeRigidBodyColliderOffsetsPhysx`, which calls `get_rest_offsets` / `get_contact_offsets` / `set_rest_offsets` / `set_contact_offsets` on `asset.root_view`. On OVPhysX that view is an `OvPhysxView`, which defines none of them, so adding the term to a config raised `AttributeError` during `OvPhysxManager.reset()` (via `PHYSICS_READY`) and the environment never built. The error surfaced from inside the PhysX implementation, so it did not name the unsupported backend either. Found by the OVQA agent harness on Isaac-Cartpole (release/3.0.0, ovphysx 0.5.11). No shipped task uses the term today, so this is latent rather than a live regression. **Fix** - New `_RandomizeRigidBodyColliderOffsetsOvPhysx`, mirroring the PhysX variant. OVPhysX runs the PhysX solver, so rest/contact offsets are written directly, per collision shape, through the asset's `OvPhysxView`: `REST_OFFSET` / `CONTACT_OFFSET` for articulations, `RIGID_BODY_REST_OFFSET` / `RIGID_BODY_CONTACT_OFFSET` for rigid objects. Both bindings are CPU-resident `[N, S]` buffers, so the full tensor is read-modify-written on the host with the selected envs as write indices. - Dispatch now matches `randomize_rigid_body_material` in the same file: `ovphysxmanager` first (it contains the substring `physx`), then Newton, then PhysX, and a `ValueError` naming the manager for anything else instead of silently selecting PhysX. - Docstring lists OVPhysX as a supported backend. **Note on write order** PhysX enforces `restOffset < contactOffset` and on OVPhysX a violating `setRestOffset` is only logged, not raised. The term writes rest before contact, same as the PhysX variant, so raising both above a small authored contact offset can silently skip the rest write. Kept for PhysX parity; flagging in case we want to reorder in both backends. **Verification** New `source/isaaclab_ov/test/test_randomize_rigid_body_collider_offsets_mdp.py` drives the public term (stubbed `cfg` / `env` / `asset_cfg`) against a real OVPhysX `RigidObject` and `Articulation`, so the dispatch itself is covered. It reproduced the reported `AttributeError` before the fix. Assertions: selected envs land in the sampled range, unselected envs are untouched, and omitting one distribution leaves that offset alone. Passes on `cuda:0` and `cpu` (separate processes, ovphysx device lock). PhysX and Newton paths are unchanged. Dependencies: #7563. Fixes # (no tracking issue; reported by the OVQA agent harness) ## Type of change - Bug fix (non-breaking change which fixes an issue) ## Release backport - [x] <!-- backport-active-release --> Backport this pull request to the active release branch after it merges into `develop` ## Screenshots Not applicable: no visual change. ## Checklist Docker and GPU tests run on demand. Push the commits you want tested, then comment `run-ci` on the pull request. - [x] I have read and understood the [contribution guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html) - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] I have added a changelog fragment under `source/<pkg>/changelog.d/` for every touched package (do **not** edit `CHANGELOG.rst` or bump `extension.toml` — CI handles that) - [x] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --------- Co-authored-by: Kelly Guo <kellyg@nvidia.com>
…7569) # Description > **Stacked on #7563.** This branch needs the `REST_OFFSET` / `CONTACT_OFFSET` / `RIGID_BODY_*_OFFSET` aliases and the CPU-only routing for those tensor types that #7563 adds. Review/merge that first; this diff collapses to a single commit once it lands, at which point the base should be retargeted to `develop`. `randomize_rigid_body_collider_offsets` had no OVPhysX implementation. Its backend dispatch tested only for `"newton"` and fell through a bare `else` to `_RandomizeRigidBodyColliderOffsetsPhysx`, which calls `get_rest_offsets` / `get_contact_offsets` / `set_rest_offsets` / `set_contact_offsets` on `asset.root_view`. On OVPhysX that view is an `OvPhysxView`, which defines none of them, so adding the term to a config raised `AttributeError` during `OvPhysxManager.reset()` (via `PHYSICS_READY`) and the environment never built. The error surfaced from inside the PhysX implementation, so it did not name the unsupported backend either. Found by the OVQA agent harness on Isaac-Cartpole (release/3.0.0, ovphysx 0.5.11). No shipped task uses the term today, so this is latent rather than a live regression. **Fix** - New `_RandomizeRigidBodyColliderOffsetsOvPhysx`, mirroring the PhysX variant. OVPhysX runs the PhysX solver, so rest/contact offsets are written directly, per collision shape, through the asset's `OvPhysxView`: `REST_OFFSET` / `CONTACT_OFFSET` for articulations, `RIGID_BODY_REST_OFFSET` / `RIGID_BODY_CONTACT_OFFSET` for rigid objects. Both bindings are CPU-resident `[N, S]` buffers, so the full tensor is read-modify-written on the host with the selected envs as write indices. - Dispatch now matches `randomize_rigid_body_material` in the same file: `ovphysxmanager` first (it contains the substring `physx`), then Newton, then PhysX, and a `ValueError` naming the manager for anything else instead of silently selecting PhysX. - Docstring lists OVPhysX as a supported backend. **Note on write order** PhysX enforces `restOffset < contactOffset` and on OVPhysX a violating `setRestOffset` is only logged, not raised. The term writes rest before contact, same as the PhysX variant, so raising both above a small authored contact offset can silently skip the rest write. Kept for PhysX parity; flagging in case we want to reorder in both backends. **Verification** New `source/isaaclab_ov/test/test_randomize_rigid_body_collider_offsets_mdp.py` drives the public term (stubbed `cfg` / `env` / `asset_cfg`) against a real OVPhysX `RigidObject` and `Articulation`, so the dispatch itself is covered. It reproduced the reported `AttributeError` before the fix. Assertions: selected envs land in the sampled range, unselected envs are untouched, and omitting one distribution leaves that offset alone. Passes on `cuda:0` and `cpu` (separate processes, ovphysx device lock). PhysX and Newton paths are unchanged. Dependencies: #7563. Fixes # (no tracking issue; reported by the OVQA agent harness) ## Type of change - Bug fix (non-breaking change which fixes an issue) ## Release backport - [x] <!-- backport-active-release --> Backport this pull request to the active release branch after it merges into `develop` ## Screenshots Not applicable: no visual change. ## Checklist Docker and GPU tests run on demand. Push the commits you want tested, then comment `run-ci` on the pull request. - [x] I have read and understood the [contribution guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html) - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] I have added a changelog fragment under `source/<pkg>/changelog.d/` for every touched package (do **not** edit `CHANGELOG.rst` or bump `extension.toml` — CI handles that) - [x] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --------- Co-authored-by: Kelly Guo <kellyg@nvidia.com> (cherry picked from commit 1b182e7)
Description
_CPU_ONLY_TYPESinisaaclab_ovis the single constant deciding whether anOvPhysxViewtensor buffer is allocated on, and validated against, host or device memory. It omitted eight tensor types that are CPU-resident even on a GPU simulation:ARTICULATION_CONTACT_OFFSETCONTACT_OFFSET[N, S]ARTICULATION_REST_OFFSETREST_OFFSET[N, S]RIGID_BODY_CONTACT_OFFSETRIGID_BODY_CONTACT_OFFSET[N, S]RIGID_BODY_REST_OFFSETRIGID_BODY_REST_OFFSET[N, S]ARTICULATION_BODY_DISABLE_GRAVITYBODY_DISABLE_GRAVITY[N, L]RIGID_BODY_DISABLE_GRAVITYRIGID_BODY_DISABLE_GRAVITY[N]ARTICULATION_DOF_DRIVE_TYPEDOF_DRIVE_TYPE[N, D]ARTICULATION_DOF_DRIVE_MODELDOF_DRIVE_MODEL[N, D, 3]Because
_native_deviceconsults this set, it returned the simulation device for host-resident data. Two consequences:_check_devicerejected a correctly placed buffer: allocating one of these on the host, where the data actually lives, raisedOvPhysxView.DeviceMismatch, whose message advises moving the buffer yourself — precisely the advice that fails here.No shipped Isaac Lab workflow reads these eight through the tensor path today, so this is latent rather than a live outage. It is not an obscure corner: Isaac Lab already authors these same properties through USD schema writes at scene construction, and the tensor path exists to change such values per-env at runtime.
Each of the eight is also exposed as a public alias carrying its shape, dtype and units, taken from the wheel's own type table. This module is the alias layer over the ovphysx enum, so referencing them raw would have left the candidate tuple a mix of aliases and raw enum members with no stated reason why these eight differed. Naming follows the existing convention: the
ARTICULATION_prefix is dropped for articulation types, as withDOF_STIFFNESSandBODY_MASS, and retained for rigid-body ones, as withRIGID_BODY_MASS.A second, orthogonal gap surfaced during the audit: ovphysx documents
ARTICULATION_DOF_DRIVE_TYPEas read-only, but it was missing from_READ_ONLY_NAMES, soset_attributeaccepted writes and forwarded them. An audit of the full classification against the wheel's type table found this to be the only such gap — of the 21 types ovphysx documents read-only, 20 were already classified, with no misclassified members.Type of change
articulation_dof_drive_typenow raiseOvPhysxView.ReadOnlyAttributeinstead of being silently forwarded. Migration: remove the write; drive type is authored through the USD drive schema, not the tensor path.Release backport
developScreenshots
Not applicable.
Checklist
pre-commitchecks with./isaaclab.sh --formatconfig/extension.tomlfileCONTRIBUTORS.mdor my name already exists thereHow it was tested
ovphysx exposes no residency query (
ovphysx_tensor_spec_tcarries dtype/ndim/shape and no device field), so residency was measured, not assumed: CUDA memcpys were counted via CUPTI around each binding read into a host buffer and into a device buffer, under the DirectGPU configurationisaaclab_ovitself sets ("/physics/suppressReadback": True). A host read showingDtoHmeans GPU-resident; no memcpy on the host read plusHtoDon the device read means CPU-resident.Measured across every GPU sample scene in the ovphysx 0.5.11 wheel:
two_articulations_gpuagree=40 MISMATCH=8agree=48 MISMATCH=0links_chain_sample_gpuagree=40 MISMATCH=8agree=48 MISMATCH=0mixed_base_articulations_gpuagree=36 MISMATCH=8agree=44 MISMATCH=0boxes_falling_on_groundplane_gpuagree=9 MISMATCH=3agree=12 MISMATCH=0volume_deformable_multiMISMATCH=0MISMATCH=0surface_deformable_materialMISMATCH=0MISMATCH=0Every previously classified member measured CPU-resident as declared in all scenes, so the set was incomplete rather than wrong — there were no false positives to correct.
The guarding test was a tautology: it compared the view's derived set against the canonical set it is derived from, so it could not fail for any contents. It now asserts against an independent inventory of measured residency, mirroring the existing
_EXPECTED_READ_ONLY_NAMESpattern. Both new assertions were confirmed to fail before the fix and pass after it; the read-only case fails withDID NOT RAISE ReadOnlyAttributewithout the fix.pytest source/isaaclab_ov/test/sim/test_ovphysx_view.py— 81 passedtest_articulation_helpers,test_rigid_object_helpers,test_deformable_object_helpers,test_ovphysx_scene_data_backend,test_randomize_rigid_body_material_mdp— all passuv run isaaclab -f— passestools/changelog/cli.py check develop— passestest_views_xform_prim_ovphysx.pyreports 23 failed / 28 passed both with and without this change; every failure is the process-global device lock covered by thedevice_splitmarker, which requires per-device re-invocation. Baselined on a clean tree to confirm it is unrelated.Environment: ovphysx 0.5.11, Linux x86_64, Python 3.12, NVIDIA RTX 5000 Ada Generation Laptop GPU.