[CI] Cross-platform — Part 4: Multi-GPU pytest workflow with cuda:1 coverage#5823
Conversation
There was a problem hiding this comment.
Overall
This is a well-designed PR that introduces a clean, env-driven device parametrization helper for multi-GPU testing. The mask grammar is elegant and the documentation in the module docstring is excellent. The workflow setup and test migrations look correct.
However, there's a critical issue that will prevent the tests from running.
Critical: Missing __init__.py
The new isaaclab.testing package is missing its __init__.py file. Without it, Python won't recognize source/isaaclab/isaaclab/testing/ as a package, and all the imports like:
from isaaclab.testing import cuda_test_deviceswill fail with ModuleNotFoundError.
Fix: Add source/isaaclab/isaaclab/testing/__init__.py with at minimum:
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""Testing utilities for Isaac Lab."""
from .devices import cuda_test_devices
__all__ = ["cuda_test_devices"]Minor Suggestions
1. Workflow: Consider using matrix strategy for future extensibility
The pytest target list is explicit (good for control), but as you add more files in P1/P2, consider whether a matrix or glob pattern might be cleaner. Current approach is fine for now.
2. devices.py line 114: Consider caching available devices
Calling torch.cuda.device_count() on every test parametrization is cheap but redundant. A module-level cache (computed once at import) would be cleaner:
@functools.lru_cache(maxsize=1)
def _list_available_devices() -> tuple[str, ...]:
...(Return tuple for hashability, or just compute at module level.)
3. Workflow line 57: Pin Isaac Sim version handling
The fallback '6.0.0' is hardcoded. If vars.ISAACSIM_BASE_VERSION is the source of truth, consider failing explicitly when unset rather than silently falling back, to avoid version drift.
4. Consider adding a simple unit test for cuda_test_devices itself
A few tests for the helper (mask parsing, strict vs non-strict, edge cases like empty mask, invalid chars) would catch regressions in the helper logic itself.
Summary
- Blocking: Add the missing
__init__.pyforisaaclab.testing - Non-blocking: Minor polish suggestions above
Once the __init__.py is added, this looks good to merge. Nice work on the mask grammar design! 🎯
c0b18ca to
30667b8
Compare
There was a problem hiding this comment.
Overall Assessment
This is a well-designed PR that introduces an elegant, env-driven device parametrization system for multi-GPU testing. The mask grammar is intuitive (e.g., 110 → [cpu, cuda:0], 001 → [cuda:1]), the documentation is excellent, and the backward-compatible default ensures zero impact on existing single-GPU CI runs.
The PR successfully addresses:
- ✅ New
isaaclab.testingmodule withcuda_test_devices()helper - ✅ Auto-discovery workflow that picks up opt-in test files automatically
- ✅ Migration of 19 test files (~280 parametrize sites)
- ✅ Clear P1/P2 scope documentation for follow-ups
Findings
1. Missing Unit Tests for the Helper Function (Medium)
Location: source/isaaclab/isaaclab/testing/devices.py
The cuda_test_devices() function has non-trivial mask parsing logic with multiple edge cases (trailing X, strict vs non-strict, empty masks, invalid characters), but there are no dedicated unit tests for the helper itself.
Recommendation: Add a test file like source/isaaclab/test/testing/test_cuda_test_devices.py with cases covering:
- Default mask resolution
- Explicit mask parsing (
"110","001","00X","X") - Strict mode raising on unavailable devices
- Non-strict mode returning empty list gracefully
- Invalid mask characters raising
ValueError
This protects against regressions in the helper logic itself.
2. Workflow SKIP List Pattern Could Be More Robust (Low) ✅ RESOLVED
Resolved in f66b101 — The new include-files approach is cleaner and avoids the heredoc pattern entirely.
3. Consider Caching Device List (Nit)
Location: source/isaaclab/isaaclab/testing/devices.py line 113
_list_available_devices() is called on every cuda_test_devices() invocation. While torch.cuda.device_count() is cheap, caching the result with @functools.lru_cache would be cleaner and more efficient for test collection.
Summary
This is a solid foundation for multi-GPU testing infrastructure. The design choices are well-reasoned and the migration is clean.
Actionable items:
- (Recommended) Add unit tests for
cuda_test_devices()to catch edge-case regressions (Optional) Minor robustness improvements noted above✅ Resolved
Nice work on the mask grammar design! 🎯
Update (d6b2934): New commit adds explicit pip install pytest step to the workflow — a good defensive fix ensuring runner independence. No new issues. Previous suggestions remain optional improvements for future iterations.
Update (f1fa016): Good improvements in this commit:
- Extended pip install to include
pytest-mockandflakywith clear explanation - Added
is_ok()helper treating pytest exit code 5 (no tests collected) as success — correct for device-gated tests - Extended SKIP list with 4 newton tests and improved documentation
No new issues. Previous optional suggestions (unit tests for helper, robustness improvements) remain relevant but non-blocking.
Update (042c409): This commit expands the skip list with well-documented categories explaining why each test is skipped (FabricFrameView deadlocks, SIGSEGV failures, cuda:1 hangs, etc.). Good practice to document the reasoning. Also adds test_simulation_context.py and physx rigid-object tests to the skip list. No new issues — this is maintenance housekeeping for multi-GPU stability.
Update (1546520): 🎉 Excellent fix! This commit addresses the root cause of the cuda:1 test failures:
app_launcher.py: AddedISAACLAB_SIM_DEVICEenv var support to override the default device when not explicitly passed. Clean implementation with good documentation.- Workflow: Sets
ISAACLAB_SIM_DEVICE=cuda:1to boot Kit withactive_gpu=1from process start. This correctly targets PhysX/Warp to cuda:1 before SimulationApp locks it. - Result: 7 tests removed from the skip list and now run successfully on cuda:1!
This is a proper fix rather than just skipping problematic tests. No new issues introduced.
Update (feb8e21): Skip list maintenance — expanded documentation with categorized skip reasons:
- Upstream Kit limits (FabricFrameView)
- Pre-existing test/API breakage (newton contact_sensor)
- Newton cuda:1 — Warp allocator failure in mujoco_warp collision driver
- PhysX cuda:1 — hangs specific to AWS L40 runner (passes on other hardware)
Added 7 files back to skip list (4 Newton, 3 PhysX asset tests) with clear root-cause explanations. Good documentation practice for future debugging. No new issues.
Update (6eeda64): Added changelog entry (jichuanh-multi-gpu-ci.minor.rst) documenting the new cuda_test_devices() helper and ISAACLAB_SIM_DEVICE env var. Good housekeeping. No new issues.
Update (e3d328b): Major infrastructure improvement — switched from bare-runner installation to Docker-based execution:
- Added
configjob that loads isaacsim image config fromconfig.yamlvia sparse checkout - Added
ecr-build-push-pullaction to pull the same per-commit CI image used by regular CI (dep matrices now match exactly) - Tests run inside container with workspace volume-mounted, removing need for
pip installsteps on runner - Cleaned up SKIP list comments (removed obsolete contact_sensor reference)
- Proper symlink setup (
rm -f _isaac_sim && ln -s /isaac-sim _isaac_sim) inside container
This is a solid approach — reusing the regular CI's Docker image ensures environment consistency and removes runner-specific pip quirks. Good fallback documentation if ECR cache misses. No new issues.
Update (3c5248d): Good documentation and consistency improvements:
- Added clear comments explaining
--entrypoint bash(overrides isaac-sim base image's default ENTRYPOINT that would swallow the script) - Added explanation for
--ignore=tools/conftest.py(matches regular CI's behavior) - Consolidated ignore paths into
ignore_argsvariable and applied to both pure-python and per-file pytest runs - Cleaner shell structure:
--entrypoint bash+-cinstead of inlinebash -c
No new issues. Ready for merge. 🚀
Update (9ff909d): Fixes local image availability issue — when ecr-build-push-pull takes the deps-cache-hit path, it creates an ECR registry-side alias but leaves no image in local Docker. This commit adds an "Ensure image is local" step that:
- Creates isolated DOCKER_CONFIG with
credsStoredisabled (workaround for broken runner credential helper) - Re-authenticates to ECR via EC2 IAM role
- Explicitly pulls and tags the image for subsequent container steps
Good defensive fix with clear comments explaining the edge case. No new issues.
Update (6706690): Good fixes for container permission issues:
- Added
--user "$(id -u):$(id -g)"to run container as host uid:gid — fixes write permission failures on volume-mounted workspace - Created writable $HOME (
/tmp/isaaclab-ci-home) withXDG_CACHE_HOMEfor Warp/numpy/pip caches - Well-commented: explains the issue (image's default
isaaclabuid 1000 ≠ runner'sgithub-runneruser)
Proper solution for the permission mismatch. No new issues.
Update (328878a): Skip list expanded with ovphysx tests. Good documentation explaining the issue: module-level pytest.mark.skipif collects 0 items + 1 skip, which isaaclab.sh CLI wrapper interprets as exit 1 (doesn't distinguish from real failures). Added 3 ovphysx asset tests to skip list. Clean maintenance commit. No new issues.
Update (665f0c3): Added temporary run_docker_tests: 'false' override in build.yaml to free GPU runners during PR iteration. The TEMP comment is well-documented ("revert before final review").
Update (f66b101): 🎉 Excellent refactor! This commit makes major architectural improvements:
Changes:
- New
extra-env-varsinput added to bothrun-testsandrun-package-testsactions — clean mechanism to forward env vars into the container with proper parsing (handles comments, whitespace,KEY=valueformat) - Workflow refactored to use
run-package-testsaction instead of ~150 lines of inline bash. Much cleaner and more maintainable. - Separate
buildjob added to pre-populate the ECR image before test job runs (mirrors the build→test split inbuild.yaml) - Include-list approach replaces the SKIP list pattern — explicitly listing 9 known-working tests is safer than auto-discovery with exclusions
- Path triggers expanded to include action files, so workflow changes properly trigger CI
My previous Finding #2 (SKIP list robustness) is now obsolete — the new include-files approach is cleaner and avoids the heredoc pattern entirely. ✅
No new issues. This is a significant quality improvement that makes the multi-GPU workflow consistent with the rest of the CI infrastructure.
run_docker_tests: 'false' in build.yaml (from 665f0c3).
Update (3c985eb): Minor update for FabricFrameView retest iteration:
Changes in build.yaml:
- Updated TEMP comment: now focused on "FabricFrameView cuda:1 retest" (previously was "Multi-GPU pytest docker conversion")
- Streamlined comment (removed redundant "freeing the single-GPU runner pool" explanation)
run_docker_tests: 'false'remains set — still iterating
Changes in test-multi-gpu-pytest.yaml:
- Added
test_views_xform_prim_fabric.pyto the include-files list — attempting to enable the FabricFrameView test on cuda:1
This is a WIP commit testing whether the FabricFrameView deadlock issue (documented in earlier skip lists) has been resolved. run_docker_tests: 'false' still needs to be reverted before merge.
No new architectural issues. Just experimental test expansion.
Update (bdcb527): ✅ Final Cleanup Complete
The latest commit removes the temporary run_docker_tests: 'false' override that was used during iteration, restoring the proper change-detection logic:
- run_docker_tests: 'false'
+ run_docker_tests: ${{ steps.detect.outputs.run_docker_tests }}This was the expected final step — the temporary bypass was necessary to test the FabricFrameView cuda:1 retest path, and it's now correctly reverted.
Status: All outstanding items addressed. PR is ready for merge. 🚀
There was a problem hiding this comment.
Update (5d29bb0): ✅ Ready for Merge
The final commit reverts the temporary `run_docker_tests: 'false' override — the exact action my previous review flagged as blocking. All outstanding items are now addressed:
✅ Missing __init__.py — Added in earlier commit
✅ **run_docker_tests: 'false' reverted** — This commit ✅ **Workflow refactored** to use run-package-tests` action (f66b101)
✅ Include-list approach replaces SKIP heredoc pattern
✅ Changelog fragment added
Summary
This PR is now complete and ready for merge. The multi-GPU testing infrastructure is solid:
- Clean
cuda_test_devices()helper with elegant mask grammar ISAACLAB_SIM_DEVICEenv var for AppLauncher device override- Proper integration with existing CI via
run-package-testsaction - 9 test files migrated with known-working cuda:1 paths
No new issues introduced. Nice work! 🚀
Update (d6d69c4): ⚠️ Iteration Checkpoint — Not Merge-Ready
This commit re-introduces the temporary run_docker_tests: 'false' override in .github/workflows/build.yaml`. The PR author is iterating and has added a note:
"TEMP (revert before final review / before landing): force run_docker_tests=false while iterating PR #5823"
Status: The PR was previously ready for merge, but is now in an iteration state. This temporary flag must be reverted before merging — the author has clearly marked it as such.
📋 Blocking for merge:
- Revert `run_docker_tests: 'false' override (same fix as commit 5d29bb0)
No functional regressions otherwise — this is expected iteration behavior.
Update (ea919b7): Module Relocation
This commit relocates the testing utilities from isaaclab.testing to isaaclab.test.utils:
Changes:
- Renamed
source/isaaclab/isaaclab/testing/→source/isaaclab/isaaclab/test/utils/ - Updated imports across 16 test files in
isaaclab,isaaclab_newton,isaaclab_ovphysx, andisaaclab_physxpackages - Workflow path trigger updated from
source/isaaclab/isaaclab/testing/**→source/isaaclab/isaaclab/test/utils/** - Changelog fragment updated to reflect new import path
✅ This is a clean refactor that consolidates test utilities under the isaaclab.test namespace. All import statements are consistently updated.
📋 Blocking for merge:
- Revert `run_docker_tests: 'false' override (status unchanged from previous update)
Otherwise, no new issues introduced.
Update (0118ea7): 🎯 Nice Refinements
This commit adds several quality-of-life improvements:
Changes:
1. run-tests/action.yml — Smarter env var parsing
- Now preserves mid-line
#characters (e.g.,IMAGE_TAG=v1.0#nightlysurvives) - Only treats full-line comments (where first non-whitespace is
#) as comments - Strips YAML block indent whitespace properly
2. test-multi-gpu-pytest.yaml — Auto-discovery of opt-in tests
- Replaces hardcoded include-list with
grep -rl 'cuda_test_devices'discovery - Adding a new test to multi-GPU CI now requires no workflow edit — just use the helper
- Clean design: opt-in via code, not via workflow maintenance
3. cuda_test_devices() helper — New skip parameter
- Added
skip: dict[str, str]for per-device skips with reasons - Wraps skipped devices in
pytest.param(..., marks=pytest.mark.skip(reason=...)) - CI shows
SKIPPEDwith reason instead of silent omission - Changed
strictdefault fromTruetoFalse(friendlier for CPU-only dev machines)
4. test_views_xform_prim_fabric.py — Migrated to helper
- Replaced
@pytest.mark.parametrize("device", ["cpu", "cuda:0"])withcuda_test_devices() - Removed manual
ISAACLAB_TEST_MULTI_GPUskipif decorators - Uses
mask="001", strict=Falsefor cuda:1-only tests
✅ All changes are clean refinements that improve maintainability and developer experience.
📋 Blocking for merge:
- Revert
run_docker_tests: 'false'override (status unchanged)
Nice incremental polish! 🔧
Update (101a47a): 🔧 Newton Device-Pinning & Test Tolerance Fixes
This commit addresses cuda:1 context issues in Newton manager and fixes device-specific test tolerances:
Changes:
1. newton_manager.py — Device context pinning (issue #5132)
Added explicit torch.cuda.set_device() + wp.set_device() calls in two critical locations:
start_simulation()— Before Warp/Newton allocations to ensure mujoco_warp collision pipeline allocates on the correct CUDA deviceinitialize_solver()— Mirrors the guard fromstart_simulation()for solver build and collision-pipeline init
This fixes null pointer returns when mujoco_warp allocates against a primary CUDA context that was never made current. The guard is idempotent if already pinned.
2. wp.ScopedCapture(device=device) — Explicit device for CUDA graph capture
Previously, CUDA graph capture didn't specify a device; now explicitly passes device to avoid capturing on wrong GPU.
3. Test tolerance fixes (test_rigid_object.py in both newton & physx packages)
Changed if device == "cuda:0" to if device.startswith("cuda") for tolerance selection. This ensures cuda:1 (and any other CUDA device) uses the relaxed GPU tolerance (1e-2) instead of the strict CPU tolerance (1e-5).
✅ Solid fixes:
- Device-pinning guards reference the related issue (#5132)
- Comments explain why the guards are needed (mujoco_warp context behavior)
- Test tolerance fix is the correct pattern for any CUDA device
- CUDA graph capture now device-aware
📋 Blocking for merge:
- Revert
run_docker_tests: 'false'override (status unchanged)
Otherwise, these are important correctness fixes for multi-GPU setups. 👍
Greptile SummaryThis PR introduces a complete multi-GPU pytest CI lane: a
Confidence Score: 5/5Safe to merge; all findings are edge-case concerns that do not affect the normal single-GPU or multi-GPU test paths. The core logic — mask intersection, atomic work-queue claim, shard fan-out, Newton device pin, and build_simulation_context device-kwarg fix — is well-designed and backed by dedicated unit tests. The three flagged concerns (XR + ISAACLAB_SIM_DEVICE stale dict key, single-quoted discovery gap, theoretical slug collision) all require uncommon conditions to trigger and none affect the intended multi-GPU CI path or the single-GPU invariant. source/isaaclab/isaaclab/app/app_launcher.py (XR + env-device key sync) and .github/workflows/test-multi-gpu-pytest.yaml (discovery grep quote handling) are the two spots that could benefit from a quick follow-up, though neither blocks merging. Important Files Changed
Sequence DiagramsequenceDiagram
participant WF as workflow yaml
participant HL as host_launcher.sh
participant DC as Docker container
participant SR as shard_runner.sh
participant CF as conftest.py
participant Q as Work Queue
WF->>WF: "Discover opt-in test files"
WF->>HL: "bash multi_gpu_host_launcher.sh"
HL->>Q: "Seed queue/ with file slugs"
HL->>DC: "docker run --gpus all"
DC->>SR: "multi_gpu_shard_runner.sh"
SR->>SR: "nvidia-smi DEV_COUNT + torch cross-check"
loop "For each cuda:N (N=1..DEV_COUNT-1)"
SR->>CF: "pytest subprocess (ISAACLAB_SIM_DEVICE + ISAACLAB_TEST_DEVICES)"
CF->>Q: "claim: os.rename queue to inflight"
CF->>CF: "Run per-file pytest with mgpu_shard_select plugin"
CF->>Q: "done: os.rename inflight to done"
end
SR-->>DC: "Aggregate exit codes"
DC-->>HL: "docker exit code"
HL->>HL: "Reconciler: assert queue + inflight empty"
HL-->>WF: "exit (docker_rc or 2 on orphan)"
WF->>WF: "aggregate_test_summary.py to Step Summary"
Reviews (2): Last reviewed commit: "Merge remote-tracking branch 'origin/dev..." | Re-trigger Greptile |
|
|
||
| Args: | ||
| mask: Optional explicit mask string following the grammar in the | ||
| module docstring. When ``None``, the helper reads the mask from | ||
| the ``ISAACLAB_TEST_DEVICES`` environment variable, defaulting | ||
| to ``"110"`` if the variable is unset. | ||
| strict: When ``True`` (the default), raise ``ValueError`` if the | ||
| mask requests devices the host does not have or if the resolved | ||
| list would be empty. When ``False``, silently truncate to what | ||
| the host can satisfy - callers using this in | ||
| ``pytest.mark.parametrize`` get a clean "no tests collected for |
There was a problem hiding this comment.
strict=True default raises at collection time on hosts without cuda:0
With the default mask "110" and strict=True, calling cuda_test_devices() on a host that reports only ["cpu"] (no CUDA) immediately raises ValueError during pytest collection — before any test runs. Before this change, the same tests (parametrized with ["cuda:0", "cpu"]) would collect normally; only the cuda:0 variant would fail at runtime. Tests like test_episode_data.py and test_math.py have no AppLauncher dependency, so they are plausible targets for collection on CPU-only dev machines. Passing strict=False to the default call-sites, or documenting the constraint, would preserve the old graceful-degrade behaviour for contributors without full GPU setups.
| # Caller-supplied extra env vars (one KEY=value per line). | ||
| if [ -n "$extra_env_vars" ]; then | ||
| while IFS= read -r line; do | ||
| line="${line%%#*}" | ||
| line="$(echo "$line" | xargs)" | ||
| [ -z "$line" ] && continue | ||
| key="${line%%=*}" |
There was a problem hiding this comment.
Comment-stripping and
xargs trimming silently corrupt values containing # or single quotes
line="${line%%#*}" strips everything from the first # in a line, so a future caller passing a value like IMAGE_TAG=v1.0#nightly would silently become IMAGE_TAG=v1.0. Similarly, $(echo "$line" | xargs) collapses internal whitespace and fails on values containing unbalanced single quotes. For the two values used today (001 and cuda:1) this is harmless, but it is a latent trap for any future extra-env-vars user. Documenting the # restriction in the input description, or switching to a safer stripping approach, would prevent silent data loss.
There was a problem hiding this comment.
Update (437a6d2): 🔧 Build Job Pool Alignment
New commit reviewed (9d159f3 → 437a6d2):
ECR Cache Locality Fix
# Before:
runs-on: [self-hosted, gpu]
# After:
runs-on: [self-hosted, linux, x64, multi-gpu]Problem solved: The ECR cache repo is resolved per runner pool:
gpupool →gitci-docker-cachemulti-gpupool →multigpu-docker-cache
Building on [self-hosted, gpu] pushed the image to gitci-docker-cache, which the multi-GPU test job cannot see, causing a full rebuild (~27 min) on the scarce multi-GPU runner.
Fix: Align the build job to the same pool (multi-gpu) as the test job so both hit the same cache. This is the correct approach — build and test should share the same ECR namespace.
✅ LGTM for this commit. Clean infrastructure fix with excellent inline documentation explaining the cache topology.
📋 Status Summary:
- Previous reviews covered: runner label fix (
gpuremoval), script extraction, work-queue architecture - No new concerns in this commit
📜 Previous review history
Update (9d159f3): Script Extraction & Runner Targeting Fix
- Removed conflicting
gpulabel (was routing to 1-GPU boxes) - Extracted ~300 lines of inline shell/Python to
multi_gpu_host_launcher.shandaggregate_test_summary.py
Earlier: Container isolation, test_devices() API, Newton device-pinning, lazy pxr imports
| Ordered list of device strings as torch would address them. | ||
| """ | ||
| devices = ["cpu"] | ||
| if torch.cuda.is_available(): |
There was a problem hiding this comment.
Why are we using torch api here? This smells like tech debt. Shouldn't we switch to use warp? i.e.:
>>> import warp
>>> warp.init()
Warp 1.12.0 initialized:
CUDA Toolkit 12.9, Driver 13.0
Devices:
"cpu" : "x86_64"
"cuda:0" : "NVIDIA RTX PRO 6000 Blackwell Workstation Edition" (95 GiB, sm_120, mempool enabled)
Kernel cache:
/home/pbarejko/.cache/warp/1.12.0
>>> warp.get_cuda_devices()
['cuda:0']There was a problem hiding this comment.
Checked. using warp requires initializing runtime which might have side-effect to kit. torch does not have that.
There was a problem hiding this comment.
we would always have warp loaded for our APIs, right? what are the side effects that impact kit?
There was a problem hiding this comment.
Correction: it's the warp behavior on non-default devices such that torch.cuda.set_device must be called first before wp.init. The same issue was discovered by Octi before.
| ], | ||
| ) | ||
| @pytest.mark.parametrize("device", ["cuda:0", "cpu"]) | ||
| @pytest.mark.parametrize("device", cuda_test_devices()) |
There was a problem hiding this comment.
Based on function name cuda_test_devices have we started to exclude cpu device tests. If so - why?
There was a problem hiding this comment.
renamed to test_devices
| so adopting this helper has zero impact on single-GPU runs. | ||
| """ | ||
|
|
||
| _ENV_VAR = "ISAACLAB_TEST_DEVICES" |
There was a problem hiding this comment.
Name of this global variable makes no sense, besides it's used in one place only. This unnecessary indirection makes code harder to read.
| reason="Multi-GPU tests disabled (set ISAACLAB_TEST_MULTI_GPU=1 to enable)", | ||
| ) | ||
| @pytest.mark.parametrize("device", ["cuda:1"]) | ||
| @pytest.mark.parametrize("device", cuda_test_devices(mask="001", strict=False)) |
There was a problem hiding this comment.
I don't understand the premise behind strictness in this function. I read it as maybe run on GPU 1, if unavailable then run on what's available. This introduces testing ambiguity, Just by looking at this line I don't know what you are trying to test.
In my opinion we would be better without cuda_test_devices function and keep list explicit, instead of resolved at runtime. In testing you want to keep constraints explicit.
There was a problem hiding this comment.
Strict is removed.
The goal of this is to dynamically configure the parameterization at test time so we run it where suitable. On regular test, cpu and cuda:0; and cuda:X for multi-gpu. The overall goal is to mark all tests needing device parameterization with this.
Per per-PR minimum-needed analysis: - isaac-sim#5886 (bounded shutdown) is closed (audit verdict nice-to-have; isaac-sim#5933 prevents the hang upstream so the force-exit timer is moot). Reverts SIGHUP handler + ISAACLAB_FORCE_EXIT_TIMEOUT timer in AppLauncher; drops the workflow env var. - isaac-sim#5883 (kitless newton) kept open as a separate PR but left out of this diagnostic bundle to test whether isaac-sim#5933 alone is enough for newton test_articulation (which calls build_simulation_context(sim_cfg=, device=) at line 2427, so still needs isaac-sim#5881 for the cross-device kwarg fix). Reverts the newton test_articulation kitless conversion and the schemas.py _create_fixed_joint_to_world helper. Bundle now contains: isaac-sim#5823 + isaac-sim#5875 base + isaac-sim#5881 + isaac-sim#5933 + the JUnit XML path-collision fix in conftest. If green, confirms only 4 PRs are needed for multi-GPU CI green (with test_articulation un-gated).
There was a problem hiding this comment.
Update (6a0e473): 🐳 Major Refactor — 1-Docker N-Shard Architecture + External Runner Script
(Incremental follow-up to previous review — 45+ commits since a1fa279)
Key Architectural Change: 1-Docker, N-Shard
Before: Each GPU shard ran in its own isolated Docker container
After: One container hosts all pytest shards as parallel subshells
┌─────────────────────────────────────────────────────────┐
│ Single Docker Container (--gpus all) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Shard cuda:1 │ │ Shard cuda:2 │ │ Shard cuda:3 │ │
│ │ (subshell) │ │ (subshell) │ │ (subshell) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ ↓ ↓ ↓ │
│ └────────────────┴────────────────┘ │
│ │ │
│ Shared PYTHONUSERBASE │
│ (junitparser, pytest, etc.) │
└─────────────────────────────────────────────────────────┘
Benefits:
- Eliminates cross-container races on shared workspace mount
- ~30s saved per shard (Docker init overhead removed)
- Shared pip install (single
junitparseretc. install vs N) - Simpler debugging (all shards in one container)
New: tools/multi_gpu_shard_runner.sh
The inside-container logic is now extracted to a standalone script:
Responsibilities:
- Sets up
HOME/PYTHONUSERBASEon tmpfs - Installs pytest deps into shared site-packages
- Auto-discovers GPU count via
nvidia-smi -L - Cross-checks against
torch.cuda.device_count() - Fans out 1 pytest subshell per non-default GPU
- Per-shard isolation: separate
HOME,ISAACLAB_TEST_DEVICES - Wait-all-then-aggregate exit codes
✅ Good extraction — versioned, shellcheck-able, reusable for local probing
Other Notable Changes
1. Kit GPU Pinning (ISAACLAB_PIN_KIT_GPU=1)
- Pins Kit renderer to single GPU when set
- Prevents multi-GPU race conditions in Kit's resource allocation
- Added to
app_launcher.pyand enabled in workflow
2. Directory-based Work Queue
- Uses atomic
os.rename()for claim contention - Reconciler step catches silent-drops
3. MIG Detection
- Handles both discrete GPUs and MIG slices
nvidia-smi -Lis authoritative (torch under-counts MIG)
4. DO-NOT-MERGE Gate
- Docs/install-ci workflows skip when PR title contains
DO-NOT-MERGE
Remaining Blockers
📋 Before merge:
- Revert
run_docker_tests: 'false'override inbuild.yaml— still hardcoded
The PR title contains DO-NOT-MERGE suggesting this is intentional for iteration, but the actual production-readiness blocker is that line in build.yaml.
🏁 Verdict: Looking good for the CI scope. The 1-docker architecture is a solid improvement. Just need to revert the run_docker_tests override before merge.
There was a problem hiding this comment.
Update (e6689f0): 📊 Aggregated Test Summary & Filtered Live Stdout
(Incremental follow-up to previous review)
1. New Aggregated Test Summary Step
A new always()-conditional step parses JUnit XMLs and outputs:
- Per-file results table: file path, shard, status, pass/total, skipped, failed, walltime
- Per-shard aggregate: files, pass/total, skipped, failed, test time
- Grand total row
Dual output:
- Plain text to stdout (for workflow log readability)
- Markdown to
$GITHUB_STEP_SUMMARY(renders at top of run page)
✅ Excellent addition:
- Provides at-a-glance CI results without scrolling through logs
- Per-shard breakdown helps diagnose load imbalance
- Status emojis in markdown (✅ PASS, ❌ FAIL, ⚪ EMPTY) improve scannability
- Clean separation of stdout (tabular text) vs Summary (markdown tables)
2. Filtered Live Stdout
The pytest output pipeline now includes a grep -aE filter that retains only high-signal lines:
Retained:
- 🚀 emoji markers (test boundaries)
- Test result lines (
PASSED,FAILED,ERROR,SKIPPED,XFAIL,XPASS) - Summary stats (
Total:,Passing:,Failing:, etc.) - Error markers (
^E,Traceback,^FAILED) - File references (
^ +File) - Pytest section dividers (
^=+,^~~~~)
Filtered out:
- Kit init chatter, plugin registration
- omni.usd Transfer logs
- Verbose pytest collection output
✅ Good improvement:
- Reduces live log noise while preserving actionable info
- Full output still available in
$shard_log(printed in grouped sections at job end) stdbuf -oLensures line-buffered output (no partial line issues)-aflag on grep handles binary (e.g., emoji bytes) correctly
3. Timeout Raised to 60 Minutes
timeout-minutes: 60 # was 45The diagnostic comment was removed, and timeout raised from 45 → 60 minutes.
ℹ️ Context: The previous 45-min timeout was flagged as diagnostic; 60 min gives more headroom for the parallel shards. Once load-balancing data is collected, this can likely be tightened back down.
4. Debug Comments (Review Notes)
Several #rj: comments were added throughout the workflow:
#rj: need to test using gpu runner. see if ecr is connected.#rj: this is useless here. should be just before running tests#rj: what happens if test file only has a few test_devices()...#rj: is this needed for all docker? or what's the problem here?#rj: does test fail if either queue or inflight is not empty?#rj: this probably should be in action libs?
5. Remaining Blockers
📋 Before merge:
- Revert
run_docker_tests: 'false'override inbuild.yaml— still pending - Remove
#rj:debug comments — review notes, not production docs
🏁 Verdict: Great additions for CI observability. The aggregated summary and filtered stdout significantly improve the developer experience for debugging multi-GPU test failures. Just need to clean up the debug comments and revert the docker test override.
Update (c514199): No changes to the aggregated test summary or filtered stdout logic in this increment. The new work-queue distribution in conftest.py complements these features well — JUnit XML paths now use full-path slugs to avoid collisions between shards. See primary review thread for full analysis.
Most test callers pass both ``sim_cfg=`` and ``device=`` to
:func:`isaaclab.sim.build_simulation_context`, implicitly expecting the
``device`` kwarg to win. The helper previously dropped the kwarg silently
when ``sim_cfg`` was provided, causing warp kernel-launch device
mismatches on non-default GPUs: the test fixture allocated ``env_ids``
on the requested device while the articulation's ``self.device``
resolved from the untouched ``sim_cfg`` default (``cuda:0``), and
``wp.launch(..., device=self.device)`` failed with::
RuntimeError: Error launching kernel 'set_root_link_pose_to_sim_index',
trying to launch on device='cuda:0',
but input array for argument 'env_ids' is on device=cuda:2.
Change ``device``'s default to ``None`` (sentinel) and apply it as an
override after sim_cfg construction in both branches. The one test that
asserted the old "sim_cfg overrides everything" contract is updated to
cover the new override semantics.
Add an ISAACLAB_PIN_KIT_GPU env var to AppLauncher. When truthy, it appends Kit command-line overrides that pin the renderer to a single GPU (renderer.multiGpu.enabled=False, autoEnable=False, maxGpuCount=1) and disable the fabric GPU-interop path (physics.fabricUseGPUInterop= false), so each Kit process touches only its assigned GPU instead of enumerating every visible GPU at startup. Used by the multi-GPU CI workflow to avoid a shared GPU-interop context across concurrent sibling shards, which otherwise surfaces as "Stage X already attached" errors and SimulationApp.close hangs (see isaac-sim#3475). Off by default; single-GPU and user-facing rendering paths are unchanged.
Adds the scope-intersect-runtime device selection helper (test_devices) and its unit test, so unit tests can declare the devices they are valid on and the multi-GPU lane can narrow them via ISAACLAB_TEST_DEVICES.
Pins torch and Warp to the target device before allocations and scopes the CUDA graph capture to it, so Newton runs correctly on cuda:1+ (issue isaac-sim#5132).
…ests actions" This reverts commit 6cef107.
…n_context (#5881) ## Summary - `build_simulation_context(device="cuda:N", sim_cfg=...)` silently dropped the explicit `device` kwarg when a `sim_cfg` was passed, falling back to `sim_cfg.device` (default `cuda:0`). - The multi-GPU CI lane sets `ISAACLAB_SIM_DEVICE=cuda:N` per shard, so tests that pass `device="cuda:N"` got `cuda:0` instead. Downstream Warp kernels then ran on `cuda:0` while the rest of the test believed it was on `cuda:N`: ``` RuntimeError: Error launching kernel 'set_root_link_pose_to_sim_index', trying to launch on device='cuda:0', but input array for argument 'env_ids' is on device=cuda:2. ``` - Fix: make `device`'s default `None` (sentinel) and apply it as an override after `sim_cfg` is resolved, so an explicit kwarg wins whether or not a `sim_cfg` was supplied. ## 1. The bug ```python def build_simulation_context(..., device: str = "cuda:0", sim_cfg=None, ...): if sim_cfg is None: sim_cfg = SimulationCfg(device=device, ...) # ^^ explicit `device` only used in the no-sim_cfg path; otherwise ignored ``` When a caller passed both `sim_cfg=<built with default device>` and `device="cuda:2"`, the kwarg was thrown away. Code that pulled the active device from `sim_cfg.device` saw `cuda:0`; Warp arrays allocated against the cfg device landed on `cuda:0` while torch ops driven by the kwarg ran on `cuda:2` — the cross-device kernel-launch error above. ## 2. Fix ```python def build_simulation_context(..., device: str | None = None, sim_cfg=None, ...): if sim_cfg is None: gravity = (0.0, 0.0, -9.81) if gravity_enabled else (0.0, 0.0, 0.0) sim_cfg = SimulationCfg(dt=dt, gravity=gravity) if device is not None: sim_cfg.device = device # explicit kwarg wins in both branches ``` `device=None` (default) means "use whatever the cfg already has". `device="cuda:N"` is honored even when a cfg is also passed. ## 3. Validation `source/isaaclab/test/sim/test_build_simulation_context_{headless,nonheadless}.py::test_build_simulation_context_cfg` is updated to assert the new override semantics (explicit `device` wins over `sim_cfg.device`). On local multi-GPU/MIG hardware, `build_simulation_context(sim_cfg=cfg, device="cuda:N")` previously hit the kernel-launch assertion; with the fix it runs on the requested device. Consumed by the multi-GPU CI lane (#5823).
# Conflicts: # source/isaaclab_ovphysx/test/assets/test_articulation.py # source/isaaclab_ovphysx/test/assets/test_rigid_object.py # source/isaaclab_ovphysx/test/assets/test_rigid_object_collection.py # tools/conftest.py
| Ordered list of device strings as torch would address them. | ||
| """ | ||
| devices = ["cpu"] | ||
| if torch.cuda.is_available(): |
There was a problem hiding this comment.
we would always have warp loaded for our APIs, right? what are the side effects that impact kit?
| # call site; Kit's active_gpu is process-global and locked after | ||
| # SimulationApp init, so per-test selection cannot retarget it. | ||
| if not device_explicitly_passed: | ||
| env_device = os.environ.get("ISAACLAB_SIM_DEVICE") |
There was a problem hiding this comment.
Could we avoid reading ISAACLAB_SIM_DEVICE directly inside _resolve_device_settings? The behavior makes sense for the multi-GPU workflow, but this makes AppLauncher feel like it is accumulating unrelated CI env vars. I think we are already doing too much of that inside the AppLauncher code, I'd really like to avoid all the CI-specific branching if we can.
| would otherwise skip everything and pass vacuously). A scope that | ||
| merely does not intersect this run's devices skips, it does not raise. | ||
| """ | ||
| requested = os.environ.get(_RUNTIME_DEVICES_ENV_VAR) |
There was a problem hiding this comment.
why do we need to reference CI env variables in the core code?
There was a problem hiding this comment.
This is for configuration the device at CI level. e.g. for multi-gpu instance, it can dispatch the test to one of the non-default gpus.
| i.e. the historical single-GPU device set, so non-default GPUs are opt-in per run.""" | ||
|
|
||
|
|
||
| def test_devices(scope: str = "11X", *, skip: dict[str, str] | None = None) -> list: |
There was a problem hiding this comment.
will this conflict with pytests's test_* search for test cases? could this become more of a test-only module that's not exposed as part of the isaaclab core APIs?
|
|
||
| import os | ||
|
|
||
| import torch |
There was a problem hiding this comment.
is this guaranteed to be imported after launching kit?
There was a problem hiding this comment.
it does not. Was it for the kit crash issue due to openblast? The issue should be resolved. I can move this to function local import.
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| "mask, count, expected", |
There was a problem hiding this comment.
I think the mask idea is clever, but it's a burden on all test developers that I feel might not be very intuitive. If we expect users from the community to implement tests, they'll need to understand the masks and make sure they specify the correct values if their tests cannot support some of the options. perhaps we can have some more user friendly naming for CPU, GPU, and default cuda device options? maybe something like
test_devices(DeviceScope.ALL)
test_devices(DeviceScope.CPU_AND_DEFAULT_CUDA)
test_devices(DeviceScope.NON_DEFAULT_CUDA)
There was a problem hiding this comment.
Yes, I will add enum name support
Make DeviceScope composable while preserving raw mask support. Derive Kit boot and shard reporting from ISAACLAB_TEST_DEVICES so test device selection has one source of truth.
## 1. Summary Fix CUDA initialization on non-default devices by selecting the resolved device in PyTorch before synchronizing Warp at shared launcher and physics-backend initialization boundaries. Newton binds CUDA graph capture explicitly to the simulation device without backend-specific default-device setup. The exact three-rank camera workload failed 2/2 without device-scoped capture and has now passed 6/6 with the fix, including two runs on the final centralized implementation and one after merging current ``develop``. ## 2. Root cause Warp used its default ``cuda:0`` context while nonzero distributed ranks used their assigned devices. Newton's collision pipeline then mixed allocations from different CUDA contexts, producing CUDA errors 900 and 101 during graph capture. ## 3. Design - Select the PyTorch device before synchronizing Warp through a private shared helper. - Synchronize both runtimes after Kit startup in ``AppLauncher``. - Synchronize both runtimes when resolving distributed ranks in ``sim_launcher``. - Reassert the resolved device in ``PhysicsManager.initialize`` before backend-specific setup, covering direct ``SimulationContext`` use for every backend. - Keep ``wp.ScopedCapture(device=device)`` in Newton so graph ownership remains explicit. ## 4. Related work Extracted from #5823 so the product fix can be reviewed and landed independently of the multi-GPU CI infrastructure. ## 5. Test plan - [x] Regression test fails without device-scoped capture and passes with it - [x] Exact three-rank Newton camera training passes on three physical GPUs, including the committed tree merged with current ``develop`` - [x] Device synchronization, launcher, backend-ordering, and distributed-resolution tests: 28 passed - [x] Full Newton manager test file: 71 passed; two unrelated Kamino cases require a newer Newton API than the installed environment provides - [x] Changelog fragment gate - [x] ``./isaaclab.sh -f``
The renderer-pinning env var ISAACLAB_PIN_KIT_GPU was superseded by isaac-sim#5933, which makes single-GPU rendering the Kit default and adds ISAACLAB_FABRIC_USE_GPU_INTEROP for the fabric GPU-interop override. Point the multi-GPU CI launcher/runner at the new env var.
Reconcile against the split-out changes that have since merged: - Drop the renderer-pinning ISAACLAB_PIN_KIT_GPU block and its changelog fragment; superseded by isaac-sim#5933 (default one-GPU + ISAACLAB_FABRIC_USE_GPU_INTEROP). - Drop the inline newton_manager.py torch/Warp device pins and take develop's version; superseded by isaac-sim#6322 (centralized set_cuda_device + ScopedCapture(device=device)). - Resolve test_views_xform_prim_fabric.py: keep develop's isaac-sim#5677/isaac-sim#5380 test bodies, retain the device_scope test_devices() parametrization.
Re-sync onto latest develop (adds isaac-sim#6431 rendering-test timeout bump, isaac-sim#5730, isaac-sim#6363, OVPhysX test enablement). Keep device_scope test_devices() migration in test_rigid_object.py over develop's cpu/cuda:0 list.
The Newton non-default-device init fix moved to its own PR (isaac-sim#6322, merged), so this PR no longer makes a user-facing isaaclab_newton change -- only device_scope test migrations. Convert the fragment to .skip.
Reconcile tools/conftest.py with isaac-sim#5698 (ARM/Spark): adopt the generalized ctx.ci_marker / run_individual_tests(ci_marker) rename while keeping the multi-GPU inject_shard_select plugin hook.
# Conflicts: # source/isaaclab_ovphysx/test/assets/test_articulation.py
# Conflicts: # source/isaaclab_newton/test/assets/test_articulation.py
1. Summary
cuda:N, with a shared atomic work queue, per-file reports, and end-of-run reconciliation.DeviceScopeflags andtest_devices()forscope ∩ runtimeparametrization while retaining exact custom string masks.ISAACLAB_TEST_DEVICESas the single source for both test parametrization and explicit Kit launch-device selection;AppLauncherhas no implicit test-environment override.2. Device selection
ISAACLAB_TEST_DEVICESlimits which devices a run may use; unset preserves the historical cpu + cuda:0 runtime.3. Dependencies
All prerequisite changes have merged into
develop, so this PR no longer carries them — it is now scoped to the device-selection infrastructure and the multi-GPU pytest workflow only:sim_cfg.deviceinbuild_simulation_context.ISAACLAB_FABRIC_USE_GPU_INTEROP; the multi-GPU lane setsISAACLAB_FABRIC_USE_GPU_INTEROP=0.cuda:1and higher).4. Test plan
bash -n../isaaclab.sh -d)../isaaclab.sh -f).develop(onerendering-correctness-kitlessflake cleared on rerun; fixed upstream ondevelopby [Fix] Bump test_rendering_shadow_hand.py timeout to 1500s #6431).