Skip to content

[Fix] Stop OvPhysX writing its collider cache into the Python interpreter directory - #6889

Open
hujc7 wants to merge 15 commits into
isaac-sim:developfrom
hujc7:jichuanh/ovphysx-derived-data-cache-dir
Open

[Fix] Stop OvPhysX writing its collider cache into the Python interpreter directory#6889
hujc7 wants to merge 15 commits into
isaac-sim:developfrom
hujc7:jichuanh/ovphysx-derived-data-cache-dir

Conversation

@hujc7

@hujc7 hujc7 commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

1. Summary

  • Kitless physics=ovphysx runs wrote their UJITSO derived-data cache into the directory holding the Python interpreter. The location is now a config field, defaulting to a per-user directory under the system temp directory.
  • Fixes nvbug 6537980: where that directory is not writable (uv reusing a system /usr/bin/python3.12), every run logged two [Error] [omni.datastore] lines.
  • Where it was writable there was no error at all, and the cache was silently written inside the Python installation — shared by every project using that interpreter, and discarded on an interpreter upgrade.
  • Side effect: cooked colliders now persist across runs, which they previously did not.

2. Root cause

The ovphysx wheel bundles its own Carbonite runtime, which resolves the UJITSO cache from realpath("/proc/self/exe") + /cache/DerivedDataCache when no path is configured.

That assumption — the executable's directory belongs to me — holds for Kit, whose kit binary lives in its own application package next to a cache/ sibling (on a local Isaac Sim source build, _isaac_sim/kit/cache/DerivedDataCache). It breaks for every embedder, where the "executable" is a shared Python interpreter whose directory the program does not own.

3. Fix

OvPhysxCfg.cooked_collider_cache_dir selects the directory, and _create_physx_instance passes it to ovphysx.PhysXConfig(cooked_collider_cache_dir=...). This mirrors the sibling kitless runtime: OVRTXRendererCfg.log_file_path declares a temp-rooted default and is threaded into RendererConfig.

Routing the same key through the generic carbonite_overrides dict is rejected by the wheel at runtime — conflicts with typed field 'cooked_collider_cache_dir'. Use the typed field instead. — so the typed field is the only accepted route.

The default is per-user, <temp>/ovphysx_derived_data_cache_<uid>. A fixed name would be created 0755 by whichever user runs first, leaving a second user on the same host unable to write it and hitting the very failure above. Windows has no getuid and already gives each user a private temp root, so the suffix is applied only where it exists. Sites wanting one shared cache point the config field at a directory they provision.

4. Validation

uv run isaaclab train --rl_library rsl_rl --task Isaac-Cartpole-Camera-Direct \
    physics=ovphysx renderer=ovrtx presets=rgb --num_envs 32 --max_iterations 2

Run three ways, varying only the fix and whether the interpreter directory is writable:

Arm Fix Interpreter dir omni.datastore errors Cache written to
A0 reverted writable 0 interpreter's bin/cache/DerivedDataCache
A1 reverted read-only 2 not created
B applied read-only 0 the configured directory

Arm A1 reproduces both reported lines verbatim. Arm A0 is the one showing this is a wrong-location bug rather than a permissions bug: no error, cache still in the wrong place. All three arms exit 0 with training completing, matching the report that functionality is unaffected.

5. Test

The cache assertion and a strict PhysXConfig fake fold into the existing parametrized test_manager_supports_pinned_runtime_api, which already covers CPU and GPU; both parametrizations fail when the keyword is removed. test_ovphysx_manager_lifecycle covers construction with no config, where the module default applies. Both fakes carry cooked_collider_cache_dir so they mirror the real 0.5.9 signature rather than diverging from it.

6. Notes

  • Earlier revisions also configured a pre-0.5.9 legacy runtime path. Develop removed that code in Remove obsolete OVPhysX bootstrap hacks #6074, so this branch targets the pinned runtime only and is correspondingly smaller.
  • The default is per-user but predictable, so it does not carry tempfile.mkdtemp's unpredictability. Hardening against a pre-created directory was judged out of scope; the same exposure exists for GIT_ASSET_CACHE_DIR.
  • ovrtx exposes no equivalent knob (RendererConfig's string keys are a closed enum: log path, log level, CUDA devices). Only one error appeared in the report and it came from the ovphysx bring-up, so this is expected to close it, but an ovrtx-side store would need a wheel-side change.
  • The underlying default is arguably wrong in the wheels themselves; every embedder of these kitless runtimes inherits it.

hujc7 added 2 commits August 4, 2026 01:41
The kitless OvPhysX runtime bundles its own Carbonite, which resolves
the UJITSO derived-data cache location from ${omni_cache}. That token
is registered by omni.kit.app, so a run that never starts Kit cannot
resolve it, and Carbonite falls back to the directory holding the
Python interpreter.

Where that directory is writable the cache is silently written inside
the Python installation, shared across every project using that
interpreter and discarded on an interpreter upgrade. Where it is not
writable, startup logs "Failed to acquire exclusive lock to data
store" followed by "Failed to create local file data store".

Pass the location Kit itself uses so the cache lands in the Omniverse
user cache directory on both runtime branches.
@github-actions github-actions Bot added the isaac-lab Related to Isaac Lab team label Aug 4, 2026
@hujc7
hujc7 marked this pull request as ready for review August 4, 2026 09:17
@hujc7
hujc7 requested a review from a team August 4, 2026 09:17
@greptile-apps

greptile-apps Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR redirects kitless OvPhysX cooked-collider caches from the Python installation to the standard per-user Omniverse cache and updates the legacy-runtime fix so every supported constructor and device path receives an explicit cache configuration.

  • Adds platform-aware derivation of the Omniverse cache directory, including a temporary-directory fallback when no absolute home-based path can be resolved.
  • Uses the typed cache field for the current runtime and the compatible Carbonite override for legacy runtimes.
  • Adds regression coverage across current and legacy constructor variants on CPU and GPU.
  • Documents the corrected cache behavior in the changelog.

Confidence Score: 5/5

The PR appears safe to merge.

The previously reported legacy-path omission is resolved because the current code constructs and passes a cache-configured PhysXConfig for every supported legacy constructor and device combination, and no blocking failure remains.

Important Files Changed

Filename Overview
source/isaaclab_ovphysx/isaaclab_ovphysx/physics/ovphysx_manager.py Adds canonical cache-path resolution and passes it through version-appropriate configuration routes for every current and legacy runtime branch; the previously reported omission is fixed.
source/isaaclab_ovphysx/test/physics/test_ovphysx_scene_data_backend.py Adds regression coverage for both devices across current, legacy CUDA-list, and legacy GPU-index constructor variants.
source/isaaclab_ovphysx/changelog.d/jichuanh-ovphysx-derived-data-cache-dir.rst Accurately documents that the collider cache now uses the standard user cache and persists across runs.

Reviews (3): Last reviewed commit: "Merge remote-tracking branch 'upstream/d..." | Re-trigger Greptile

Comment on lines 1029 to 1031
if "active_cuda_gpus" in physx_parameters and ovphysx_device == "gpu":
physx_kwargs["active_cuda_gpus"] = str(gpu_index)
physx_kwargs["config"] = ovphysx.PhysXConfig(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Legacy paths omit cache config

When the legacy runtime uses CPU physics or exposes the gpu_index constructor, this condition constructs PhysX without a PhysXConfig, leaving Carbonite's interpreter-relative cache fallback active and causing cache writes beside Python or omni.datastore errors when that directory is read-only.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in 48e4935.

The legacy branch now builds PhysXConfig unconditionally, so legacy CPU runs and the gpu_index constructor both receive the cache setting. The GPU-only /physics/suppressReadback and /physics/suppressFabricUpdate keys stay conditional, since those are correctly GPU-gated.

One correction worth recording: on that path the typed field was not merely omitted, it was unsafe. The declared runtime's PhysXConfigpredatescooked_collider_cache_dir(verified against an olderovphysxwheel:carbonite_overridespresent, the typed field absent), socooked_collider_cache_dir=would have raisedTypeErroron legacy GPU startup. The declared runtime now goes through the generic/UJITSO/datastore/localCachePath` override instead, which it accepts and which 0.5.9 rejects as conflicting with its typed field. The two routes are mutually exclusive by wheel version.

Test coverage went from 2 to 6 combinations (two legacy constructors x two devices, plus the current runtime x two devices), and each stubbed PhysXConfig now mirrors its runtime's real signature, so a keyword the real runtime would reject fails the test instead of being absorbed by a permissive lambda **kwargs`.

@isaaclab-review-bot isaaclab-review-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isaac Lab Review Bot

The PR correctly routes the OVPhysX cooked-collider cache through the typed PhysXConfig field and adds a changelog fragment and regression coverage, but the legacy runtime path still omits this configuration for CPU runs and constructors using gpu_index.

  • Design and architecture: Containing cache-path resolution within OvPhysxManager is appropriate, and the platform-specific user-cache mapping avoids writing beside the interpreter. However, the legacy-runtime integration applies the cache configuration only within one GPU-specific constructor branch rather than as a runtime-wide setting.
  • API: No public Isaac Lab API symbols are added, removed, or renamed. The private helper and internal construction changes do not require deprecation or documentation exports, and the package changelog fragment follows the repository format.
  • Implementation: The current runtime path always passes cooked_collider_cache_dir, but the legacy path constructs PhysXConfig only when active_cuda_gpus is exposed and GPU physics is selected. Legacy CPU runs and the gpu_index constructor variant therefore retain the interpreter-relative fallback. Construct the cache-bearing configuration unconditionally for the legacy path while keeping GPU selectors and GPU-only overrides conditional; extend the regression test to cover those paths.

Minor fixes needed. Posted 1 actionable finding inline.

Automated review; human maintainers own approval decisions.

if "active_cuda_gpus" in physx_parameters and ovphysx_device == "gpu":
physx_kwargs["active_cuda_gpus"] = str(gpu_index)
physx_kwargs["config"] = ovphysx.PhysXConfig(
cooked_collider_cache_dir=OvPhysxManager._cooked_collider_cache_dir(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Suggestion · Implementation — Legacy runtime gets cache dir only on GPU

On the legacy runtime path, PhysXConfig is constructed only when the constructor exposes active_cuda_gpus and the device is "gpu". For legacy CPU runs, or constructors taking gpu_index, ovphysx.PhysX is created without any config, so Carbonite keeps its interpreter-relative cache fallback and the reported behavior persists. Build the cache-bearing config unconditionally in this branch and keep the GPU-specific overrides and selector conditional.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in 48e4935, along the lines you suggested.

The cache-bearing config is now built unconditionally on the legacy path, with the GPU selectors (active_cuda_gpus / gpu_index) and the GPU-only /physics/suppress* overrides kept conditional:

legacy_overrides = {"/UJITSO/datastore/localCachePath": OvPhysxManager._cooked_collider_cache_dir()}
if "active_cuda_gpus" in physx_parameters and ovphysx_device == "gpu":
    physx_kwargs["active_cuda_gpus"] = str(gpu_index)
    legacy_overrides["/physics/suppressReadback"] = True
    legacy_overrides["/physics/suppressFabricUpdate"] = True
elif "gpu_index" in physx_parameters:
    physx_kwargs["gpu_index"] = gpu_index
physx_kwargs["config"] = ovphysx.PhysXConfig(carbonite_overrides=legacy_overrides)

One deviation from the suggestion: the declared runtime cannot take the typed field at all. Its PhysXConfig predates cooked_collider_cache_dir (verified against an older ovphysx wheel), so the typed keyword would raise TypeError there rather than merely being skipped. It therefore uses the generic /UJITSO/datastore/localCachePath override, while the current runtime keeps the typed field, which is the only route it accepts (0.5.9 rejects the generic key as conflicting with the typed field).

The regression test was extended as you asked, to two legacy constructors x two devices plus the current runtime x two devices, and its stubs now mimic each runtime's real PhysXConfig` signature so this class of mistake fails the test.

hujc7 added 5 commits August 4, 2026 10:07
The declared runtime's PhysXConfig predates cooked_collider_cache_dir,
so passing that keyword raised TypeError there. The config was also
built only for GPU runs exposing active_cuda_gpus, leaving legacy CPU
runs and the gpu_index constructor on the interpreter-relative
fallback the previous commit set out to remove.

Route the declared runtime through the generic
/UJITSO/datastore/localCachePath override, which it accepts and the
current runtime rejects as conflicting with its typed field, and build
the config unconditionally so both devices and both constructors
receive it.

Fall back to the temporary directory when the home directory cannot be
resolved: expanduser leaves "~" unchanged, and a relative path would
put the cache under the working directory.

Give each stubbed PhysXConfig the signature of the runtime it stands
for, so a keyword the real runtime would reject fails the test instead
of being absorbed.
Both construction sites spelled out which keyword carries the cache
location, so the rule for choosing between the typed field and the
generic override lived in two places and had to be kept in step.

Move it into a single helper that inspects the runtime's PhysXConfig
signature and writes whichever keyword that signature accepts, merging
into any overrides the caller already set. Selecting on the config
signature rather than on which PhysX API is in use also covers a
runtime whose config predates the typed field while its PhysX class
does not, which the previous branch-based choice could not express.
Having the helper decorate a caller-supplied dict meant both call sites
had to name and hold that dict purely to hand it over and splat it
back, so each grew a variable describing nothing the reader needs.

Build and return the PhysXConfig instead. The construction sites become
expressions again and the two intermediate dicts go away, leaving only
the carbonite overrides the legacy branch genuinely assembles.
@hujc7
hujc7 marked this pull request as draft August 4, 2026 20:52
hujc7 added 2 commits August 4, 2026 15:31
The helper that chose between the typed field and the generic override
guarded a runtime whose PhysXConfig and PhysX class disagree about the
typed field. No published wheel does: in every version available, the
field and PhysX.set_cpu_mode appear together or not at all. The branch
already tells the two runtimes apart, so each can name the keyword its
own config accepts.

Drop the helper and construct PhysXConfig directly at both sites, and
name the path helper for what it returns rather than for what one of
its two callers does with it.
@hujc7
hujc7 marked this pull request as ready for review August 4, 2026 22:59
@hujc7

hujc7 commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator Author

@greptileai review

Comment on lines +970 to +990
@staticmethod
def _derived_data_cache_dir() -> str:
"""Return the canonical Omniverse derived-data cache directory.

Mirrors the location Kit resolves from its ``${omni_cache}`` token. The kitless
OVPhysX runtime cannot resolve that token, and its Carbonite fallback resolves
relative to the Python interpreter binary, which is not writable on installs that
reuse a system interpreter.
"""
if sys.platform == "win32":
base = os.environ.get("LOCALAPPDATA") or os.path.join(os.path.expanduser("~"), "AppData", "Local")
cache_dir = os.path.join(base, "ov", "cache", "DerivedDataCache")
else:
base = os.environ.get("XDG_CACHE_HOME") or os.path.join(os.path.expanduser("~"), ".cache")
cache_dir = os.path.join(base, "ov", "DerivedDataCache")
if not os.path.isabs(cache_dir):
# ``expanduser`` leaves "~" unchanged when the home directory cannot be resolved
# (e.g. an arbitrary-UID container with no passwd entry); a relative path would
# put the cache under the working directory.
cache_dir = os.path.join(tempfile.gettempdir(), "ov", "DerivedDataCache")
return cache_dir

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use this, if IsaacSim is not installed does this even exists?

# ``expanduser`` leaves "~" unchanged when the home directory cannot be resolved
# (e.g. an arbitrary-UID container with no passwd entry); a relative path would
# put the cache under the working directory.
cache_dir = os.path.join(tempfile.gettempdir(), "ov", "DerivedDataCache")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 AI-generated review: This fallback uses the same predictable <temp>/ov/DerivedDataCache directory for every user. On a shared Linux host, the first user to create /tmp/ov will normally own it, so another arbitrary-UID process may be unable to write the cache and hit the same datastore failure this branch is intended to avoid; another user can also pre-populate that cache. Could we use a securely created temporary directory (for example, tempfile.mkdtemp(...)) or another safely per-user location here?



@pytest.mark.skipif(sys.platform == "win32", reason="cache root is LOCALAPPDATA-derived on Windows")
def test_manager_passes_explicit_derived_data_cache_dir(monkeypatch, tmp_path):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 AI-generated review: The regression is worth covering: the neighboring runtime-API tests do not assert the cache setting, and a focused test would have caught the earlier legacy-CPU omission. The full 3 × 2 matrix seems larger than the distinct behavior, though, while the decorator skips the only platform-specific branch added by this PR. Could we fold strict PhysXConfig fakes and cache assertions into the existing current/legacy API tests, retain focused cases for legacy CPU and gpu_index, and make the expectation conditional on LOCALAPPDATA versus XDG_CACHE_HOME instead of skipping Windows? That preserves the useful regression coverage with less duplicated fixture code.

hujc7 added 5 commits August 10, 2026 17:04
Develop removed the pre-0.5.9 OvPhysX compatibility code in 2404010
(Remove obsolete OVPhysX bootstrap hacks, isaac-sim#6074), so the legacy half of
this branch no longer has a runtime to configure. Resolve in develop's
favour and keep only the cooked-collider cache setting:

- drop the legacy PhysX branch and its /UJITSO/datastore/localCachePath
  override; the pinned runtime takes the typed cooked_collider_cache_dir
- restore the "import os" that the auto-merge dropped along with develop's
  removal of os._exit, since _derived_data_cache_dir still needs it
- fold the cache assertion and a strict PhysXConfig fake into develop's
  parametrized test_manager_supports_pinned_runtime_api and delete the
  standalone runtime matrix, whose legacy stubs no longer match any code
The fallback fires when no absolute home-based path resolves, and it used a
fixed <temp>/ov directory. On a shared POSIX host the temp root is
world-writable, so the first user to create it owns it and every other UID
either cannot write the cache or reads collider data that user placed there.
Windows already gives each user a private temp directory, so the UID suffix
is applied only where os.getuid exists.
Follow the pattern the sibling kitless runtime already uses: OVRTXRendererCfg
declares log_file_path with a temp-rooted default and threads it into
RendererConfig. The cache directory now lives on OvPhysxCfg the same way,
replacing the bespoke XDG_CACHE_HOME/LOCALAPPDATA resolution, which was the
only such code in source/.

The default is per-user (<temp>/ovphysx_derived_data_cache_<uid>) because the
POSIX temp root is shared: a fixed name is created 0755 by whoever runs first,
so a second user cannot write it and hits the omni.datastore failure this
branch removes. Windows has no getuid and already gives each user a private
temp root, so the suffix is applied only where it exists.

_create_physx_instance takes the directory as an argument, and the caller
falls back to the module default because the runtime is also constructed
outside a configured simulation. Both PhysXConfig fakes gain the field so they
mirror the real 0.5.9 signature instead of diverging from it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

isaac-lab Related to Isaac Lab team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants