Skip to content

[Newton] Fix multi-GPU initialization on non-default devices#6322

Merged
hujc7 merged 5 commits into
isaac-sim:developfrom
hujc7:jichuanh/fix-newton-mgpu-capture-device-codex
Jul 7, 2026
Merged

[Newton] Fix multi-GPU initialization on non-default devices#6322
hujc7 merged 5 commits into
isaac-sim:developfrom
hujc7:jichuanh/fix-newton-mgpu-capture-device-codex

Conversation

@hujc7

@hujc7 hujc7 commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

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

  • Regression test fails without device-scoped capture and passes with it
  • Exact three-rank Newton camera training passes on three physical GPUs, including the committed tree merged with current develop
  • Device synchronization, launcher, backend-ordering, and distributed-resolution tests: 28 passed
  • Full Newton manager test file: 71 passed; two unrelated Kamino cases require a newer Newton API than the installed environment provides
  • Changelog fragment gate
  • ./isaaclab.sh -f

Pin Torch and Warp to the simulation device before Newton allocates GPU
state, and bind CUDA graph capture to that same device. This prevents
nonzero distributed ranks from starting capture on Warp's default cuda:0
context.
@github-actions github-actions Bot added bug Something isn't working isaac-lab Related to Isaac Lab team labels Jul 2, 2026
@hujc7
hujc7 marked this pull request as ready for review July 2, 2026 04:03
@greptile-apps

greptile-apps Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes multi-GPU Newton initialization by pinning the Torch and Warp device contexts to PhysicsManager._device before any CUDA allocations, and by passing device=device to wp.ScopedCapture so graph capture is bound to the simulation GPU rather than defaulting to cuda:0.

  • Two device-pinning guards (torch.cuda.set_device + wp.set_device) are inserted at the top of start_simulation and initialize_solver; both are idempotent so they are safe to call redundantly.
  • wp.ScopedCapture is updated to pass device=device, preventing CUDA errors 900/101 on non-default ranks.
  • A new unit test verifies the device argument flows through to wp.ScopedCapture, backed by a FakeScopedCapture mock.

Confidence Score: 4/5

Safe to merge; the change is a targeted, well-commented initialization fix with an accompanying regression test and a full Newton manager test run reported as passing.

The core fix in wp.ScopedCapture(device=device) is straightforward and the device-pinning guards in start_simulation and initialize_solver are idempotent. The one gap is that _capture_or_defer_graph itself has no guard, so if it is called from set_decimation on the Newton-actuator path without a prior pinning call still in effect, allocations inside that recapture window could land on the wrong device. The new unit test validates the capture-device binding but does not cover the torch.cuda.set_device / wp.set_device side-effects directly.

newton_manager.py around _capture_or_defer_graph — verify whether the set_decimation call path guarantees device context is already current before entering this method on Newton-actuator workloads.

Important Files Changed

Filename Overview
source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py Two device-pinning guards added (start_simulation and initialize_solver) plus device parameter passed to wp.ScopedCapture; fixes CUDA context mis-match on non-default GPUs.
source/isaaclab_newton/test/physics/test_newton_manager_abstraction.py New unit test verifies wp.ScopedCapture receives the simulation device (cuda:1) and that _graph is set correctly; device-pinning side-effects in initialize_solver/start_simulation are not covered.
source/isaaclab_newton/changelog.d/jichuanh-fix-newton-mgpu-capture-device-codex.rst Changelog fragment describing the multi-GPU initialization fix; content is accurate and matches the code change.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Rank as Non-zero Rank (cuda:N)
    participant SM as start_simulation()
    participant IS as initialize_solver()
    participant CG as _capture_or_defer_graph()
    participant Torch as torch.cuda
    participant Warp as wp (Warp)

    Note over Rank: Before fix: default context = cuda:0

    Rank->>SM: start_simulation()
    SM->>Torch: set_device("cuda:N")
    SM->>Warp: wp.set_device("cuda:N")
    SM->>IS: initialize_solver()
    IS->>Torch: set_device("cuda:N") [idempotent]
    IS->>Warp: wp.set_device("cuda:N") [idempotent]
    IS->>CG: _capture_or_defer_graph()
    CG->>Warp: "wp.ScopedCapture(device="cuda:N")"
    Note over Warp: Graph capture bound to cuda:N
    Warp-->>CG: capture.graph
    CG-->>IS: _graph set
    IS-->>SM: solver + graph ready
    SM-->>Rank: initialized on cuda:N
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Rank as Non-zero Rank (cuda:N)
    participant SM as start_simulation()
    participant IS as initialize_solver()
    participant CG as _capture_or_defer_graph()
    participant Torch as torch.cuda
    participant Warp as wp (Warp)

    Note over Rank: Before fix: default context = cuda:0

    Rank->>SM: start_simulation()
    SM->>Torch: set_device("cuda:N")
    SM->>Warp: wp.set_device("cuda:N")
    SM->>IS: initialize_solver()
    IS->>Torch: set_device("cuda:N") [idempotent]
    IS->>Warp: wp.set_device("cuda:N") [idempotent]
    IS->>CG: _capture_or_defer_graph()
    CG->>Warp: "wp.ScopedCapture(device="cuda:N")"
    Note over Warp: Graph capture bound to cuda:N
    Warp-->>CG: capture.graph
    CG-->>IS: _graph set
    IS-->>SM: solver + graph ready
    SM-->>Rank: initialized on cuda:N
Loading

Comments Outside Diff (1)

  1. source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py, line 1479-1484 (link)

    P2 _capture_or_defer_graph is also called from set_decimation (Newton-actuator path), which runs after initialization but does not repeat the torch.cuda.set_device / wp.set_device pins. The explicit device=device in wp.ScopedCapture scopes the capture correctly, but any allocations triggered inside that recapture window (e.g. re-initialised collision buffers) would still target whatever the current CUDA context is at that moment. Adding the same guard at the top of _capture_or_defer_graph itself would make the method self-contained and defensive.

Reviews (1): Last reviewed commit: "Fix Newton initialization on non-default..." | Re-trigger Greptile

Comment on lines +429 to +458
def test_cuda_graph_capture_uses_simulation_device(monkeypatch):
"""CUDA graph capture should use the simulation device instead of Warp's default device."""
from isaaclab.physics import PhysicsManager

captured_devices = []
captured_graph = object()

class FakeScopedCapture:
def __init__(self, device=None):
captured_devices.append(device)
self.graph = captured_graph

def __enter__(self):
return self

def __exit__(self, exc_type, exc_value, traceback):
return False

monkeypatch.setattr(PhysicsManager, "_cfg", SimpleNamespace(use_cuda_graph=True), raising=False)
monkeypatch.setattr(PhysicsManager, "_device", "cuda:1", raising=False)
monkeypatch.setattr(NewtonManager, "_usdrt_stage", None, raising=False)
monkeypatch.setattr(NewtonManager, "_solver", None, raising=False)
monkeypatch.setattr(NewtonManager, "_is_all_graphable", classmethod(lambda cls: False))
monkeypatch.setattr(NewtonManager, "_simulate_physics_only", classmethod(lambda cls: None))
monkeypatch.setattr(wp, "ScopedCapture", FakeScopedCapture)

NewtonManager._capture_or_defer_graph()

assert captured_devices == ["cuda:1"]
assert NewtonManager._graph is captured_graph

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.

P2 Test doesn't cover device-pinning side-effects in initialize_solver / start_simulation

The new test verifies that wp.ScopedCapture receives device="cuda:1", but the two torch.cuda.set_device + wp.set_device guards added in start_simulation and initialize_solver are not exercised. If those guards were accidentally removed or made conditional on the wrong predicate, the regression would go undetected — the test would still pass. Consider adding a parallel test that monkeypatches torch.cuda.set_device and wp.set_device, sets PhysicsManager._device = "cuda:1", calls the relevant entry points, and asserts both were invoked with the correct device string.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Select the resolved CUDA device in both runtimes at shared launcher and simulation initialization boundaries. Keep Newton graph capture explicitly bound to the simulation device without backend-specific default-device setup.
hujc7 added 2 commits July 2, 2026 19:06
Synchronize Torch and Warp in the base physics manager before backend-specific setup. Keep the helper internal and cover the launcher and backend initialization boundaries directly.
@hujc7 hujc7 added this to Isaac Lab Jul 6, 2026
@hujc7 hujc7 moved this to In review in Isaac Lab Jul 6, 2026
…n-mgpu-capture-device-codex

# Conflicts:
#	source/isaaclab/test/app/test_kwarg_launch.py
@hujc7
hujc7 merged commit c8477f3 into isaac-sim:develop Jul 7, 2026
36 of 37 checks passed
@github-project-automation github-project-automation Bot moved this from In review to Done in Isaac Lab Jul 7, 2026
hujc7 added a commit to hujc7/IsaacLab that referenced this pull request Jul 7, 2026
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.
hujc7 added a commit to hujc7/IsaacLab that referenced this pull request Jul 8, 2026
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.
hujc7 added a commit that referenced this pull request Jul 13, 2026
…overage (#5823)

## 1. Summary

- **CI:** Adds a dedicated multi-GPU pytest workflow that runs one shard
per non-default `cuda:N`, with a shared atomic work queue, per-file
reports, and end-of-run reconciliation.
- **isaaclab:** Adds composable `DeviceScope` flags and `test_devices()`
for `scope ∩ runtime` parametrization while retaining exact custom
string masks.
- **isaaclab:** Uses `ISAACLAB_TEST_DEVICES` as the single source for
both test parametrization and explicit Kit launch-device selection;
`AppLauncher` has no implicit test-environment override.
- **isaaclab:** Migrates 19 device-parametrized test modules across the
core, Newton, OV PhysX, and PhysX suites while preserving the existing
cpu + cuda:0 single-GPU behavior.

## 2. Device selection

```python
from isaaclab.test.utils import DeviceScope, test_devices

test_devices()                                                  # cpu + every GPU
test_devices(DeviceScope.CUDA)                                  # every GPU
test_devices(DeviceScope.CPU | DeviceScope.NON_DEFAULT_CUDA)    # composed scope
test_devices("101X")                                            # exact custom mask
```

- `ISAACLAB_TEST_DEVICES` limits which devices a run may use; unset
preserves the historical cpu + cuda:0 runtime.
- The multi-GPU lane discovers non-default-capable scopes and narrows
each shard to one concrete device.

## 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:

- #5695 — Cross-platform Part 1 (base pytest markers / shared
scaffolding).
- #5881 — Sim honors the device kwarg over `sim_cfg.device` in
`build_simulation_context`.
- #5933 — Kit renderer defaults to one GPU + adds
`ISAACLAB_FABRIC_USE_GPU_INTEROP`; the multi-GPU lane sets
`ISAACLAB_FABRIC_USE_GPU_INTEROP=0`.
- #6322 — Newton multi-GPU initialization on non-default CUDA devices
(`cuda:1` and higher).

## 4. Test plan

- [x] Device-selection unit suite passes.
- [x] Changed Python modules compile; multi-GPU shell scripts pass `bash
-n`.
- [x] Full documentation build (`./isaaclab.sh -d`).
- [x] Full pre-commit suite (`./isaaclab.sh -f`).
- [x] GitHub Actions green on latest `develop` (one
`rendering-correctness-kitless` flake cleared on rerun; fixed upstream
on `develop` by #6431).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working isaac-lab Related to Isaac Lab team

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants