Share one Kit app across test files instead of booting it per file - #6853
Draft
mataylor-nvidia wants to merge 4 commits into
Draft
Share one Kit app across test files instead of booting it per file#6853mataylor-nvidia wants to merge 4 commits into
mataylor-nvidia wants to merge 4 commits into
Conversation
Kit-dependence is currently a property of importing a test file: 156 test modules construct AppLauncher at module scope, so Isaac Sim boots during pytest collection. Because nothing declares that dependency, tools/conftest.py has to run every test file in its own subprocess, paying Kit startup once per file. Introduce the two pieces needed to change that: launch_kit() is an idempotent module-scope replacement for AppLauncher. The first test module in a process boots Kit; later modules receive the running app, so a pytest run covering several files pays startup once. It raises rather than silently returning a mismatched app when a file asks for cameras after a camera-less boot. The kit / kit_cameras / kitless markers let a file declare which launch configuration it needs, so files that can share a process can be grouped without importing them. kit_solo opts a file out of any such grouping. test_kit_marker_contract.py keeps the markers from drifting: it checks by AST that a file's declaration matches what it does at module scope. The checks are AST-based rather than text-based because several kit-free files mention AppLauncher only in a docstring saying they do not use it. Files are not yet required to carry a marker; _ENFORCED_ROOTS is empty and grows per package as files are migrated. No test file changes behaviour: nothing is marked kit or kitless yet, and no file calls launch_kit() yet. The guard found one pre-existing bug on its first run. test_operational_space assigned pytestmark twice, and the second assignment discarded arm_ci, so the file had been excluded from the ARM CI lane. Merged into a single list.
mataylor-nvidia
force-pushed
the
mataylor/kit-test-markers
branch
from
August 2, 2026 20:33
8c337c5 to
57ffdbd
Compare
Replace the module-scope AppLauncher construction in the Kit-dependent files under source/isaaclab/test/sim with launch_kit(), and declare the matching kit or kit_cameras marker on each file. Because launch_kit() is idempotent, a pytest process covering several of these files now boots Kit once instead of once per file. Nothing forces them into one process yet -- tools/conftest.py still runs a subprocess per file -- so this changes how the files launch Kit, not how CI schedules them. 24 files map to `kit` and 4 to `kit_cameras`. The two groups must not share a process in that order: a camera-enabled app can serve tests that do not need cameras, but cameras cannot be enabled after startup, so launch_kit() raises rather than handing back an app that would silently fail to render. The transform is applied by tools/codemods/kit_launch_migration.py, added here because ~125 files in other packages remain to migrate. It edits line ranges in place rather than round-tripping through ast.unparse, which would discard comments and isort directives, and it preserves each launch call's position so the Kit-dependent imports below it still run after Kit starts. The codemod refuses anything it cannot rewrite without changing behaviour, and reports it. In particular it rejects a conditional launch such as `AppLauncher(...).app if _USE_KIT else None`, which test_mjcf_converter.py and test_urdf_converter.py use so they can run kitlessly when the standalone importer wheel is installed; collapsing that ternary would have made the boot unconditional. It also refuses a file that references AppLauncher for anything other than the launch call, since the import is removed.
Whether migrating the remaining ~125 test files off module-scope AppLauncher is worth doing depends on how much Kit startup actually costs, which is not something the current pipeline reports directly. Add two temporary jobs that run the same 30 files from source/isaaclab/test/sim and differ only in how many Kit apps they boot. kit-reuse-probe-per-file keeps the default test-path of "tools", so tools/conftest.py gives each file its own subprocess and Kit boots 30 times. kit-reuse-probe-batched points pytest at the files directly, so they share one process and launch_kit() boots Kit once. The difference between the two job durations is what reuse is worth per 30 files. Both jobs list their files explicitly instead of selecting with `-m kit`, because pytest's marker filtering deselects tests but still imports every collected module, and importing a kit_cameras module calls launch_kit(cameras=True) regardless of whether its tests will run. The batched job lists the four kit_cameras files first: a camera-enabled app can serve tests that do not need cameras, but cameras cannot be enabled after startup, so the opposite order makes launch_kit() raise. Files in TESTS_TO_SKIP are excluded from both sides so the jobs cover the same tests. To let a job bypass the per-file orchestrator, run-package-tests gains a test-path input. It defaults to "tools", the value that was previously hard-coded, so every existing caller is unaffected. Both jobs are continue-on-error and are meant to be deleted once the measurement is recorded.
mataylor-nvidia
force-pushed
the
mataylor/kit-test-markers
branch
from
August 2, 2026 22:19
57ffdbd to
ded9dfd
Compare
The per-file runner grants the first camera-enabled test file an extra 700 s of timeout, because that file compiles RTX shaders (~600 s) on a cold cache. It identified such files by searching their source for the literal string "enable_cameras=True". Migrating a file to launch_kit(cameras=True) removes that literal, so the buffer stopped being applied and the file was killed at the 120 s startup deadline instead. That is what happened to test_simulation_stage_in_memory.py in the kit-reuse-probe-per-file job: it was reported as a startup hang at 120.94 s having run no tests. Match the marker and the launch_kit call as well as the old literal, so the buffer applies both before and after a file is migrated. Also narrow the probe to the 24 `kit` files and drop the four `kit_cameras` ones from both sides. The cold shader compile is roughly thirty times the Kit startup the probe is trying to measure, so including those files tells us about shader caching rather than about app reuse.
8 tasks
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.
Problem
Kit-dependence is currently a property of importing a test file. 156 test modules construct
AppLauncherat module scope, so Isaac Sim boots during pytest collection:Nothing declares that dependency, so
tools/conftest.pycompensates by running every test file in its own subprocess. Kit startup is therefore paid once per file, and there is no way to ask "which tests actually need Kit?" without importing them.Change
launch_kit()(isaaclab/test/launch.py) replaces the module-scopeAppLauncher. It is idempotent: the first module in a process boots Kit, later modules get the running app. It stays at module scope because a test module's own imports (pxr,omni, ...) run at collection, before any fixture could help.kit/kit_cameras/kitlessmarkers let a file declare its launch configuration, so files that can share a process can be grouped without importing them.kit_soloopts a file out.test_kit_marker_contract.pykeeps the markers from drifting by checking, via AST, that each file's declaration matches what it does at module scope. It is AST-based rather than text-based because several kit-free files mentionAppLauncheronly in a docstring saying they do not use it — a grep flags those, an AST walk does not.conftest.pyis unchanged.Scope
source/isaaclab/test/simis migrated as the pilot: 27 files map tokit, 4 tokit_cameras. The other ~125 files across other packages are untouched and keep working exactly as before — an unmarked file is treated as legacy and still gets its own process._ENFORCED_ROOTSin the guard is empty, so no file is yet required to carry a marker; it grows per package as migration proceeds.The transform is applied by
tools/codemods/kit_launch_migration.py, added because those ~125 files remain. It edits line ranges in place rather than round-tripping throughast.unparse, which would discard comments and isort directives, and preserves each launch call's position so Kit-dependent imports below it still run after Kit starts.Ordering constraint
kitandkit_camerasfiles must not share a process in that order. A camera-enabled app can serve tests that do not need cameras, but cameras cannot be enabled after startup, solaunch_kit()raises rather than handing back an app that would silently fail to render.Relatedly,
pytest -mdeselects tests but still imports every collected module, so marker filtering alone cannot keep akit_camerasfile out of akitrun. Both probe jobs list their files explicitly for that reason.Probe (temporary)
kit-reuse-probe-per-fileandkit-reuse-probe-batchedrun the same 30 files fromsource/isaaclab/test/simand differ only in how many Kit apps they boot — 30 versus 1. The gap between the two job durations is what reuse is worth per 30 files, and it decides whether migrating the remaining files is justified. Files inTESTS_TO_SKIPare excluded from both sides so the two jobs cover the same tests.To let a job bypass the per-file orchestrator,
run-package-testsgains atest-pathinput defaulting to"tools"— the previously hard-coded value — so every existing caller is unaffected.Note that migrating files does not by itself speed up CI, because
tools/conftest.pystill gives each file its own process. Teaching the runner to batch same-profile files is separate work, deliberately not in this PR.Incidental fix
The guard found a real bug on its first run:
test_operational_space.pyassignedpytestmarktwice, and the second assignment discardedarm_ci, so that file had been excluded from the ARM CI lane. Merged into a single list.Testing
test_kit_marker_contract.pypasses (~1.7s, no Kit). Verified it has teeth by planting five deliberate violations — wrong marker vs.AppLauncher,kitlessimportingomni,unitcallinglaunch_kit,kit_cameraswithoutcameras=True, two profile markers at once — and confirming each rule fires.uv run isaaclab -fclean.🤖 Generated with Claude Code