Skip to content

Add a task discovery API for backend combinations - #7046

Draft
AntoineRichard wants to merge 10 commits into
isaac-sim:developfrom
AntoineRichard:antoiner/task-discovery-api
Draft

Add a task discovery API for backend combinations#7046
AntoineRichard wants to merge 10 commits into
isaac-sim:developfrom
AntoineRichard:antoiner/task-discovery-api

Conversation

@AntoineRichard

@AntoineRichard AntoineRichard commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Description

The Gym registry is currently walked in two places that answer two different questions:

  • tools/environ_docs.py reads what a task declares. That is what the environments.rst tables publish.
  • Nothing reports what actually resolves.

The gap is not theoretical. A combination can be documented while being impossible to run — that is how the AnymalC-Direct rows advertised presets the task does not have (fixed by hand in #6914). Because CI never runs update_environments_rst.py --check, nothing catches that class of drift.

This adds tools/task_discovery.py:

discover_tasks(resolve=False)   # what each task declares - fast
discover_tasks(resolve=True)    # build each combination + run the runtime validator

Resolved mode is the useful part: the cross product is not all legal. OVRTX is kitless and cannot share a process with Kit physics, so isaacsim_physx + ovrtx is declared yet unusable. Discovering that costs one config resolution; discovering it on a GPU costs a whole benchmark run.

Runs, not spellings

Resolving already builds each combination's env config, so combinations can be compared on what they produce rather than on how they were spelled. Fingerprinting the resolved config collapses the ones that produce the same run:

  • physics=physx folds into whichever concrete backend it resolves to — no table of alias names needed, and it stays separate on tasks where it genuinely resolves differently.
  • Passing no tokens folds into whichever preset the config already defaults to. That run is reported separately as default, which the collapse would otherwise hide.

Across the registry that turns 2277 declared combinations into 1180 distinct runs. On Isaac-Cartpole-Camera, 200 → 90.

Collapsing on the resolved config rather than on the backend is deliberate: the four Reach controller presets share a backend and are four different runs.

collapse=False keeps every validated spelling, which is what documentation needs — a preset naming the default of its own axis is still a token a reader can type.

Equivalence with the current docs walk

collect_environment_doc_rows() now delegates its registry walk to discover_tasks(resolve=False). The generated output is unchanged: both environments.rst and environment-browser.js render byte-identical to develop across all 136 tasks.

environ_docs keeps its own display policy — apply_rl_library_overrides, _physics_names_for_docs and the selector filtering stay where they are.

Scope

The module plus the docs-generator wiring. Deliberately not here: feeding resolved data to any published output. That is a separate change with its own cost and CI shape, sketched below.

Follow-up: validated combinations in the interactive environment browser

docs/source/_static/css/environment-browser.js carries a generated taskRows block — 45 core tasks, five comma-joined strings each, [task, rl, physics, renderer, presets], consumed as three independent axis lists.

Because the axes are independent, the picker offers combinations that cannot run. On Isaac-Cartpole-Camera it presents 20 physics×renderer pairs; 2 are rejected by the runtime validator (isaacsim_physx + ovrtx, ovphysx + isaacsim_rtx) and 8 more are duplicate spellings. A user selecting one gets a failure at launch. This matters more than in the RST tables — the browser is a chooser, not prose.

Measured cost of fixing it properly:

45 browser tasks  ->  914 validated combinations   (264s to resolve)
grouped JSON 48 KB      flat JSON 72 KB      current file 18 KB

Two tasks are 45% of that — Isaac-Lift-KukaAllegro-Camera and Isaac-Reorient-KukaAllegro-Camera at 205 modes each (24 camera modality/resolution presets crossed with backends).

It needs three coordinated changes:

  1. Data model — ship tuples instead of three independent lists, from discover_tasks(resolve=True, collapse=True), plus each task's default.
  2. UI — the filter logic has to narrow dependently (pick ovphysx, and isaacsim_rtx must disappear), rather than filtering three axes independently.
  3. CI — 264s cannot live in update_environments_rst.py --check, which gates every PR touching tools/*.py. It needs a scheduled job that regenerates and commits the artifact, with --check still verifying the cheap declared parts.

One constraint to design around: resolved results are environment-dependent. LIVESTREAM=1 in the environment silently rejects every kitless combination, and validation stops before the Isaac Sim availability check, so a kitless machine reports Kit-requiring combinations as runnable. The regenerating job has to be pinned and documented, or an ad-hoc run from a laptop would quietly publish a wrong matrix.

A cheaper first step, if the full version is not worth it: keep the three-list model and only subtract the rejected pairs. No UI change, the page keeps working, it just stops offering combinations that fail.

Also open

  • Home. tools/ matches environ_docs.py, but if this should be a supported API for out-of-tree consumers, source/isaaclab_tasks/isaaclab_tasks/utils/ may be the better place.
  • RL_LIBRARY_OVERRIDES stays in environ_docs, so rl_libraries and the rendered table still disagree for IsaacContrib-Assemble-Trocar-G129-Dex3 (() vs {'rlinf': ['PPO']}). Only the ordering constant is shared.

Type of change

  • New feature (non-breaking change which adds functionality)

Checklist

  • I have run the pre-commit checks with ./isaaclab.sh --format
  • I have added tests that prove my fix is effective or that my feature works
  • No changelog fragment: this changes only tools/, and no source package is touched

@github-actions github-actions Bot added isaac-lab Related to Isaac Lab team infrastructure labels Aug 12, 2026
@AntoineRichard
AntoineRichard force-pushed the antoiner/task-discovery-api branch 4 times, most recently from c557cfe to 0d9227d Compare August 20, 2026 12:39
@AntoineRichard
AntoineRichard force-pushed the antoiner/task-discovery-api branch from 0d9227d to 16dfb9d Compare August 21, 2026 14:52
The Gym registry is walked in two places with two different answers.
tools/environ_docs.py reads what a task declares, which is what the
environment tables publish. Nothing reports what actually resolves, so a
combination can be documented while being impossible to run: that is how
the AnymalC-Direct rows in environments.rst came to advertise presets the
task does not have.

Add tools/task_discovery.py. discover_tasks(resolve=False) reports the
declared view; resolve=True additionally builds each combination and runs
the runtime validator, keeping only combinations that can run.

Automatic selectors are reported separately from concrete backends.
physics=physx resolves to OvPhysX kitless and to Isaac Sim PhysX under
Kit, and renderer=rtx behaves the same way, so a selector and its target
are the same run. Which of the two to drop depends on how the caller
launches, so discovery reports both and lets callers decide. They are
detected by config type rather than by name, so a new selector upstream
needs no edit here.
Two defects in the first cut.

ModuleNotFoundError subclasses ImportError, which was listed as a
structural failure, so one uninstalled extra aborted the whole walk
rather than skipping that task. A task whose config needs teleop or
mimic simply cannot be inspected from a partial install; that is a
property of the environment, not a broken registry.

``declared`` also collapsed two different answers into one. A config
that cannot be loaded is unknown; a config that loads and declares
nothing means the task runs on a fixed backend with no selectable
alternative. 37 of 127 tasks are in the second group, and the
environment tables render the two differently, so ``declared`` is now
``None`` only for the first.

``declared`` reports raw registry names, with BACKEND_MIRROR_NAMES
exported so callers filter to suit what they are reporting.
The environment tables and the discovery API walked the Gym registry
separately, so the same knowledge - which tasks are trainable, which
presets they declare, which names mirror a backend selector - was
maintained twice and had already drifted.

Delegate the walk. The documentation policies stay here, because they
are about how the tables read rather than what the registry contains:
the implicit-PhysX inference, the manual RL-library overrides, and the
render-time preset filtering are unchanged.

Verified by generating environments.rst and environment-browser.js twice
in one environment, once per implementation, in separate processes: both
files are byte-identical. Note that `update_environments_rst.py --check`
is not a valid test for this, because the generator's output depends on
which optional backends are installed - a task shows `ovphysx` only when
ovphysx is importable - so it conflates a code change with environment
drift. The A/B comparison isolates the code change.

Run with teleop and mimic installed so that all 127 tasks load; without
them 31 configs are unloadable and never exercise the comparison.
Resolving a combination already builds its env config, so combinations can be
compared on what they produce rather than on how they were spelled. Fingerprint
the resolved config and collapse the matches: aliases fold into the backend they
resolve to, and the no-token run folds into whichever preset the config already
defaults to. Across the registry that turns 2024 declared combinations into 1045
distinct runs.

Collapsing on the resolved config rather than on the backend is deliberate --
the Reach controller presets share a backend and are four different runs. What a
task does with no tokens is reported separately as ``default``, since the
collapse would otherwise hide it.

This subsumes three name-based filters, all of which were wrong per task:

* ``BACKEND_MIRROR_NAMES`` dropped every DOMAIN name matching a backend. On a
  task that exposes its backends only as ``presets=`` tokens, that was the sole
  handle: ``Isaac-Open-Drawer-Franka`` reported one mode instead of four.
  A DOMAIN name is now a duplicate only when the task also declares it on a
  typed axis.
* ``_SKIP_PHYSICS`` dropped ``newton_mjwarp_vbd_proxy`` as a proxy variant. It
  is a real backend, and on ``Isaac-Lift-Cloth-Franka`` the only one that
  resolves.
* ``_selector_names`` existed to let callers subtract aliases. It also missed
  selectors nested inside a variant, so it reported none for the cabinet tasks.

Also:

* An Isaac Lab API failure now drops the combination it was judging and logs it,
  instead of ending the whole walk; pass ``strict=True`` for the old behaviour.
* ``collapse=False`` keeps every validated spelling, which is what documentation
  needs -- a preset naming the default of its own axis is still a token a reader
  can type.
* ``RL_LIBRARY_PRIORITY`` and ``is_training_task`` are now owned here and
  imported by the docs generator, so the tables and the matrix cannot drift.
  ``rlinf`` was missing from the discovery copy.
* ``Mode.presets`` documented that ``presets=`` composes across config paths and
  that discovery validates each name alone, so ``modes`` under-approximates a
  task with several independent preset axes.
* ``modes`` documented as assuming a headless launch, since a visualizer,
  livestream or explicit experience is a Kit source that narrows the legal set.

The changelog fragment is dropped: no source package changed, and nothing under
tools/ is released.
The module was 49% prose: 240 lines of docstring and comment against 201 of
code, much of it explaining bugs that no longer exist. That history belongs in
the commit log, not in every reader's way -- _domain_presets carried 25 lines
of docstring over 2 lines of code.

Keep the contract and the non-obvious constraints: the headless-launch
assumption, that presets compose across config paths but are validated one at a
time, and why the collapse keys on the resolved config rather than the backend.
Drop the narratives.

Also folds the two single-use helpers into their call sites; the explicitness
score now rides in the dedup map instead of being recomputed.
Sort resolution failures by what they mean instead of by which builtin they
happened to raise. Discovery's own imports -- two of them private sim_launcher
symbols -- now sit in their own try and raise DiscoveryError, so losing one is
reported as the API drift it is rather than escaping as a bare ImportError past
the per-combination handler and aborting the whole walk. That was the exact
failure strict= was added to make survivable, and it was not covered.

A missing extra is now caught as ModuleNotFoundError rather than ImportError,
which makes the ImportError entry in _INFRASTRUCTURE_ERRORS reachable and gives
it a meaning: a module that imports but no longer exports what a config asks for
is drift, not a rejection. Previously that entry was dead and its comment
described the opposite of the behaviour.

A task whose config will not load now reports no modes. It reported declared=None
("nothing is known") beside one all-None mode ("runs on its defaults"), which
contradict each other; a cross product of nothing known is not a cross product of
nothing declared. No task in a complete install hits this, but a partial one does.

Drop the field defaults on DiscoveredTask. They allowed a default-constructed
instance whose declared={} raises KeyError in the docs generator, and the one
construction site passes every field anyway.

Documentation corrections, all of them claims that did not survive checking:
Mode.physics/renderer None means "no token passed", not "declares none"/"headless";
only --visualizer kit is a Kit source, and the headless assumption is not hermetic
because LIVESTREAM is read from the environment and validation stops before the
Isaac Sim availability check; contrib tasks come from isaaclab_tasks, not from the
experimental package; -Eval tasks are separate registrations, not aliases; strict=
drops a combination, not a task; only the RL library ordering is shared with the
docs generator, so rlinf can differ between the two.

Add resolution tests against real configs, kept in their own module so the unit
tests stay runnable on pytest alone, and wire that unit module into tools-tests --
it was never running in CI, having been excluded as needing a full Isaac Lab
install, which is not true of it.
Two fixtures replace the monkeypatch boilerplate that five tests each repeated,
and three merges remove cases that were re-asserting a neighbour: the declared
cross product becomes one parametrized test over its three shapes, the two
uncollapsed cases become one (a rejected token in the same table covers both),
and the fingerprint's stability and discrimination are one property, not two.
The argv guarantee moves into the error-taxonomy test, where it should hold on
every branch rather than on one.

Prose is cut where it restated itself rather than where it carried a fact -- the
launch-baseline caveat keeps both leaks, LIVESTREAM and the missing Isaac Sim
check, in half the words.

Also corrects this file's own docstring, which still claimed selector detection
was exercised by running a tool. Selector detection is gone and there is no
tool; the resolver is covered by the companion module.
The module was 42% prose against 207 lines of code, because I had been putting
reasoning in docstrings that belongs in the commit log and the PR. Every fact a
caller needs to use the API correctly is kept -- the declared None-vs-empty
distinction, the LIVESTREAM and Isaac Sim leaks in the headless assumption, the
one-preset-at-a-time under-approximation, why ModuleNotFoundError is narrower
than ImportError -- and the justifications around them are gone.

143 docstring lines to 107, 39 comment lines to 31.
Docstrings state the contract, not the reasoning behind it. Also fixes a docstring
indent left mangled by an earlier edit, which ruff does not reflow.
The env constructor calls cfg.validate() before it builds anything, and tasks put
their cross-axis rules there: Reach rejects the newton_ik action preset unless
physics is Newton, and Lift rejects camera data types the Warp renderer cannot
produce. Resolution stopped short of that call, so discovery reported those
pairings as runnable and they failed on the first reset instead -- six OSMO jobs
for Reach alone.

Registry-wide this drops 1180 resolved combinations to 1027. Spot-checked: the
Warp renderer keeps rgb and depth and loses albedo and simple_shading, which is
exactly what lift_env_cfg allows, and albedo still resolves on isaacsim_rtx. No
task loses every mode.
@AntoineRichard
AntoineRichard force-pushed the antoiner/task-discovery-api branch from 16dfb9d to 4c74226 Compare August 24, 2026 09:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

infrastructure isaac-lab Related to Isaac Lab team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant