Fix AppLauncher exception exit status - #6634
Closed
nblauch wants to merge 2 commits into
Closed
Conversation
Pass a nonzero code to SimulationApp.close() during interpreter shutdown when Python reports an unhandled exception. Add an integration test that reproduces the zero-status regression from isaac-sim#6573.
nblauch
requested review from
AntoineRichard,
kellyguo11,
ooctipus,
pbarejko and
pv-nvidia
as code owners
July 20, 2026 19:05
Contributor
IsaacLab requires Python 3.12, where sys.last_exc is the recommended way to inspect the unhandled exception recorded by the interpreter.
7 tasks
hujc7
added a commit
to hujc7/IsaacLab
that referenced
this pull request
Jul 20, 2026
Extend the re-entrancy fix into a complete teardown-correctness change based on a combined review with the atexit exit-code fix (isaac-sim#6634): - Report killed-by-signal status: the handler previously ran a graceful close whose Kit fast-shutdown path terminated the process with exit code 0, so a SIGTERM-ed worker was recorded as successful and distributed launchers misattributed the failure. The handler now disables fast shutdown so close() performs the full teardown and returns, then re-raises the signal with the default action so the process exits with the conventional 128+signum status. The override is marked WORKAROUND(isaac-sim) for removal once SimulationApp can propagate a nonzero exit status through its fast-shutdown path. - Arm the re-entrancy guard in the atexit close (extracted to the testable _close_app_at_exit method) so a signal arriving during a normal shutdown takes the guarded path instead of starting a nested teardown of a half-closed app. - Stop intercepting SIGSEGV: a Python handler never runs for a synchronous main-thread segfault (the process spins on signal delivery), reports success for worker-thread segfaults, and replaces the carb crash reporter's handler, suppressing minidumps. - Restore Python's default SIGINT handler over SimulationApp's, which exits 0 before user finally blocks or KeyboardInterrupt handlers can run. Marked WORKAROUND(isaac-sim) for removal once the upstream handler preserves exception semantics and a nonzero exit status. - Remove the unregistered, dead _interrupt_signal_handle_callback. Tests cover the single-close-plus-reraise contract, the re-entrant signal path, and the atexit guard arming; all three fail against the previous behavior.
hujc7
added a commit
to hujc7/IsaacLab
that referenced
this pull request
Jul 20, 2026
Gather the startup announcements (CI marker, Kit version diagnostics) and the entire exit-path policy (atexit close, signal handlers, exit codes) into a nested AppLauncher._SimulationAppLifecycle class. The pieces coordinate through one guard flag and exist for one reason -- report the process state truthfully to whatever supervises it -- so a single class with the policy table as its docstring replaces logic previously spread across __init__ and three private methods. Absorb the atexit exit-code fix from PR isaac-sim#6634 (nblauch) into the lifecycle class: the atexit close passes a nonzero exit code when an unhandled exception is pending, so Kit fast shutdown does not replace the failure status with 0. Includes that PR's integration test and a kitless unit test for the exit-code selection. SystemExit is documented as not yet detected. Behavior is otherwise unchanged; all kitless unit tests and both real-Kit integration tests (SIGTERM status, exception exit code) pass.
AntoineRichard
left a comment
Collaborator
There was a problem hiding this comment.
Should we enable this for debug only?
Collaborator
There was a problem hiding this comment.
Do we need this test?
Contributor
Author
There was a problem hiding this comment.
thanks for review @AntoineRichard
re: needing for debug only - why? shouldn't users be correctly informed when their code fails by the exit status of the script?
re: the test, i defer judgment on this to code owners
Contributor
|
superseded by #6636 |
hujc7
added a commit
that referenced
this pull request
Jul 28, 2026
…U NCCL workaround (#6636) # Summary - Consolidates `AppLauncher` process-lifecycle handling into one nested `_SimulationAppLifecycle` class: startup announcements (CI marker, Kit version diagnostics) plus the entire exit-path policy, with the policy table as the class docstring. - Fixes every exit path that misreported failure as success or destroyed its own diagnostics (full failure-case table below). Absorbs the atexit exit-code fix from #6634. - Documents the NCCL cuMem workaround for multi-GPU RTX training on NUMA-spanning GPU allocations (`NCCL_CUMEM_HOST_ENABLE=0` first, `NCCL_CUMEM_ENABLE=0` as fallback). - The upstream `SimulationApp` exit-status fix (public reference: isaac-sim/IsaacSim#717) has been **merged** (`isaacsim.simulation_app` >= 2.18.5): the signal handler now simply passes `close(exit_code=128 + signum)` — full teardown + truthful status on fixed builds, truthful status on older builds. The one remaining `WORKAROUND(isaac-sim)` is the SIGINT re-registration (upstream handler still exits 0 before user code unwinds); it fails loudly if drift disables it. - Fixes #6573 (absorbed atexit exit-code fix, originally #6634 by @nblauch). - Fixes #6530: the SIGTERM handler no longer returns to the interrupted execution path — the worker exits through `close(exit_code=128 + signum)` (full Kit teardown on `isaacsim.simulation_app` >= 2.18.5) or dies by the re-raised signal, so distributed workers terminate with a truthful status instead of surviving and spamming TCPStore `Broken pipe` errors. # Failure cases and how this PR addresses them Root mechanism: Kit fast shutdown terminated the process with exit code 0 from inside `SimulationApp.close()`, so any death funneled through an unqualified `close()` was reported as success; additionally, the abort-signal handler was unguarded against re-entrancy. | How the process ends | Before this PR | After this PR | |---|---|---| | Unhandled Python exception | atexit close overwrote the pending failure with **exit 0** (CI false-green) | exits 1 (`sys.last_exc` detected; absorbed from #6634; `SystemExit` documented as not yet covered) | | Single SIGTERM (torchrun teardown, SLURM preemption, `kill`) | graceful close → **exit 0**; launcher marks the killed rank SUCCEEDED; surviving ranks hang until the NCCL watchdog | `close(exit_code=128 + signum)`: full teardown + truthful status on `isaacsim.simulation_app` >= 2.18.5; truthful status on older builds; dies by the signal if `close()` returns | | Second signal while a close is running (repeated SIGTERM; fault inside the replicator stop/wait) | handler re-entered `close()` → **infinite recursion** → SIGKILL-only shutdown, spurious SIGSEGV, logs flooded (~975 recursion frames/job observed on OSMO pods) | guard: re-entrant signal falls back to `SIG_DFL` | | Signal racing the normal atexit close | nested full second teardown of a half-closed app | atexit arms the same guard | | `kill -ABRT` | graceful close → **exit 0** | same truthful-close path as SIGTERM | | Real SIGSEGV, main thread | Python handler can never run → process **spins forever** at 100% CPU, crash reporter clobbered (no minidump) | SIGSEGV no longer intercepted → default action, minidumps restored | | Real SIGSEGV, worker thread | handler ran on the main thread → **exit 0** for a crashed process | same: default action, truthful signal death | | Ctrl-C | SimulationApp's handler exits **0** before user `finally`/`KeyboardInterrupt` code runs | Python default handler restored: `KeyboardInterrupt` unwinds user code, nonzero exit | Not addressed here (tracked elsewhere): `sys.exit(N)`/`SystemExit` still exits 0 (gap inside the #6634 mechanism, documented at the detection site). # Implementation notes 1. All exit-path logic lives in `AppLauncher._SimulationAppLifecycle`; the class docstring is the policy table, and each decision carries its rationale in place. 2. The signal handler passes the killed-by-signal status through `close(exit_code=128 + signum)`. With the merged upstream fix (`isaacsim.simulation_app` >= 2.18.5) the app performs its full teardown and exits with that status; on older builds the status is preserved without the teardown; if `close()` returns (fast shutdown disabled), the handler re-raises with the default action. A `TypeError` fallback warns loudly if a future `SimulationApp` drops the parameter. 3. Docs: distributed camera training fails deterministically when the allocated GPUs span NUMA nodes and passes on a single-switch set; disabling NCCL cuMem host allocations rescues the failing shape in paired same-node experiments. Added to the multi-GPU NCCL troubleshooting section. **Testing.** Six kitless unit tests (`test_simulation_app_lifecycle.py`: killed-by-signal status, re-entrancy both directions, exit-code selection, drift fallbacks on both paths) plus two real-Kit integration tests (`test_app_launcher_exit_status.py`: SIGTERM → truthful termination status with no handler recursion; unhandled exception → exits 1). The integration tests pass against both the pre-fix and the fixed (>= 2.18.5) `SimulationApp` builds; on develop's original behavior the SIGTERM test observes exit code 0 (the bug). ## Type of change - Bug fix (non-breaking change which fixes an issue) - Documentation update ## Checklist - [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
matthewtrepte
pushed a commit
to matthewtrepte/IsaacLab
that referenced
this pull request
Aug 4, 2026
…U NCCL workaround (isaac-sim#6636) # Summary - Consolidates `AppLauncher` process-lifecycle handling into one nested `_SimulationAppLifecycle` class: startup announcements (CI marker, Kit version diagnostics) plus the entire exit-path policy, with the policy table as the class docstring. - Fixes every exit path that misreported failure as success or destroyed its own diagnostics (full failure-case table below). Absorbs the atexit exit-code fix from isaac-sim#6634. - Documents the NCCL cuMem workaround for multi-GPU RTX training on NUMA-spanning GPU allocations (`NCCL_CUMEM_HOST_ENABLE=0` first, `NCCL_CUMEM_ENABLE=0` as fallback). - The upstream `SimulationApp` exit-status fix (public reference: isaac-sim/IsaacSim#717) has been **merged** (`isaacsim.simulation_app` >= 2.18.5): the signal handler now simply passes `close(exit_code=128 + signum)` — full teardown + truthful status on fixed builds, truthful status on older builds. The one remaining `WORKAROUND(isaac-sim)` is the SIGINT re-registration (upstream handler still exits 0 before user code unwinds); it fails loudly if drift disables it. - Fixes isaac-sim#6573 (absorbed atexit exit-code fix, originally isaac-sim#6634 by @nblauch). - Fixes isaac-sim#6530: the SIGTERM handler no longer returns to the interrupted execution path — the worker exits through `close(exit_code=128 + signum)` (full Kit teardown on `isaacsim.simulation_app` >= 2.18.5) or dies by the re-raised signal, so distributed workers terminate with a truthful status instead of surviving and spamming TCPStore `Broken pipe` errors. # Failure cases and how this PR addresses them Root mechanism: Kit fast shutdown terminated the process with exit code 0 from inside `SimulationApp.close()`, so any death funneled through an unqualified `close()` was reported as success; additionally, the abort-signal handler was unguarded against re-entrancy. | How the process ends | Before this PR | After this PR | |---|---|---| | Unhandled Python exception | atexit close overwrote the pending failure with **exit 0** (CI false-green) | exits 1 (`sys.last_exc` detected; absorbed from isaac-sim#6634; `SystemExit` documented as not yet covered) | | Single SIGTERM (torchrun teardown, SLURM preemption, `kill`) | graceful close → **exit 0**; launcher marks the killed rank SUCCEEDED; surviving ranks hang until the NCCL watchdog | `close(exit_code=128 + signum)`: full teardown + truthful status on `isaacsim.simulation_app` >= 2.18.5; truthful status on older builds; dies by the signal if `close()` returns | | Second signal while a close is running (repeated SIGTERM; fault inside the replicator stop/wait) | handler re-entered `close()` → **infinite recursion** → SIGKILL-only shutdown, spurious SIGSEGV, logs flooded (~975 recursion frames/job observed on OSMO pods) | guard: re-entrant signal falls back to `SIG_DFL` | | Signal racing the normal atexit close | nested full second teardown of a half-closed app | atexit arms the same guard | | `kill -ABRT` | graceful close → **exit 0** | same truthful-close path as SIGTERM | | Real SIGSEGV, main thread | Python handler can never run → process **spins forever** at 100% CPU, crash reporter clobbered (no minidump) | SIGSEGV no longer intercepted → default action, minidumps restored | | Real SIGSEGV, worker thread | handler ran on the main thread → **exit 0** for a crashed process | same: default action, truthful signal death | | Ctrl-C | SimulationApp's handler exits **0** before user `finally`/`KeyboardInterrupt` code runs | Python default handler restored: `KeyboardInterrupt` unwinds user code, nonzero exit | Not addressed here (tracked elsewhere): `sys.exit(N)`/`SystemExit` still exits 0 (gap inside the isaac-sim#6634 mechanism, documented at the detection site). # Implementation notes 1. All exit-path logic lives in `AppLauncher._SimulationAppLifecycle`; the class docstring is the policy table, and each decision carries its rationale in place. 2. The signal handler passes the killed-by-signal status through `close(exit_code=128 + signum)`. With the merged upstream fix (`isaacsim.simulation_app` >= 2.18.5) the app performs its full teardown and exits with that status; on older builds the status is preserved without the teardown; if `close()` returns (fast shutdown disabled), the handler re-raises with the default action. A `TypeError` fallback warns loudly if a future `SimulationApp` drops the parameter. 3. Docs: distributed camera training fails deterministically when the allocated GPUs span NUMA nodes and passes on a single-switch set; disabling NCCL cuMem host allocations rescues the failing shape in paired same-node experiments. Added to the multi-GPU NCCL troubleshooting section. **Testing.** Six kitless unit tests (`test_simulation_app_lifecycle.py`: killed-by-signal status, re-entrancy both directions, exit-code selection, drift fallbacks on both paths) plus two real-Kit integration tests (`test_app_launcher_exit_status.py`: SIGTERM → truthful termination status with no handler recursion; unhandled exception → exits 1). The integration tests pass against both the pre-fix and the fixed (>= 2.18.5) `SimulationApp` builds; on develop's original behavior the SIGTERM test observes exit code 0 (the bug). ## Type of change - Bug fix (non-breaking change which fixes an issue) - Documentation update ## Checklist - [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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
AppLauncherregisters anatexitcallback that closesSimulationApp. WithKit fast shutdown enabled, calling
close()with its default zero exit code canreplace Python's pending nonzero status after an unhandled exception.
This change detects the unhandled exception state exposed by the interpreter
and passes exit code 1 to
SimulationApp.close(). Normal interpreter shutdowncontinues to pass exit code 0.
The new integration test launches a real headless CPU
AppLauncherin a childprocess, raises an unhandled
RuntimeError, and verifies both the traceback andexit status 1.
Fixes #6573
Type of change
Testing
python -m pytest source/isaaclab/test/app/test_app_launcher_exit_code.py source/isaaclab/test/app/test_kwarg_launch.py::test_launch_simulation_preserves_failure_exit_code -q(2 passed)./isaaclab.sh -f(all hooks passed)Screenshots
Not applicable.
Checklist
./isaaclab.sh --format.source/isaaclab/changelog.d/.CONTRIBUTORS.md.