Rebuild Odin as an OSMO-only benchmark dispatcher - #6786
Draft
AntoineRichard wants to merge 33 commits into
Draft
Conversation
Cuts the Odin harness down to its OSMO half. The fleet stack (asgard, valhalla, hugin, munin, common) stays on antoiner/feat/odin and is not carried over; a layout test asserts those sub-packages never reappear. Keeps the osmo CLI wrapper and trims the task-name sanitiser down to the one function OdinV2 still needs. Everything else is rewritten against Isaac Lab's own benchmark entrypoints.
Validates odin.yaml with key-path error messages. Rejects booleans where integers are required, since YAML would otherwise read 'gpu: true' as a request for one GPU.
Schema 2.0, keyed by row_key rather than a synthesised run_id: upstream's capture.synth_run_id() is timestamp-derived, so the controller cannot pre-compute it. Sizing fields are optional so a first dispatch can omit them and let each task run at its shipped agent-config default; those resolved values are later harvested back out of the emitted bundles. Records an image reference per A/B side, digest-pinned, so a retag cannot change what a comparison compared.
Expands a seed list of (task_id, rl_library, physics) triples across seeds into deterministic rows. Sizing is optional and omitted by default so each task runs at its shipped agent-config default on the first dispatch. Selects uv extras per row instead of using the 'all' aggregate: [tool.uv].conflicts declares isaacsim incompatible with all, test, mimic, teleop, ovphysx, and viser, so a physx row and an ovphysx row can never share a virtualenv. Chunks cheapest-first, with unbudgeted rows last so they cannot size a chunk shared with a known-short task.
Each task runs 'uv run isaaclab benchmark training' directly; the hugin and munin wrappers are gone. Sizing flags are omitted when unset so the task falls back to its shipped agent config. Keeps the one-group-per-task wrapping: OSMO's exec_timeout clock starts at each group's RUNNING transition, so tasks at the workflow root get killed for other tasks' queue time. The entry script captures the benchmark exit code before publishing results and re-raises it afterwards, so a failed run still uploads whatever it produced.
Replaces dataset download and delete with 'osmo data download' against a bucket URI: OSMO datasets are retired. A test asserts the dataset helpers stay gone so they cannot creep back in. Drops the rsync dev path OdinV2 does not use and forwards --priority on submit.
Renames the failure kinds off their Hugin provenance: benchmark_crash, malformed_bundle, timeout, infrastructure. Unrecognised FAILED_* states fall back to infrastructure so OSMO version drift cannot wedge the loop or be misread as a benchmark crash, and a failed status query is skipped for the tick rather than aborting the dispatch.
OSMO datasets are retired and the replacement is not settled. This isolates storage behind publish_command and fetch_results so swapping the mechanism is a one-file change. fetch_results returns the row directory even when the bundle is unreadable, so one bad row is reclassified rather than aborting the dispatch. Ships a bucket-URI implementation over 'osmo data' as the placeholder.
Pins a commit by cloning a git bundle into a CUDA image and running uv sync --frozen against that commit's committed uv.lock, so a commit that changes a dependency is tested with the changed dependency. Clones from a bundle rather than copying a checkout because capture_versions() records git_commit and git_branch; without a real .git every emitted benchmark bundle would lose its provenance. The Dockerfile asserts the checked-out HEAD matches the requested commit. Bundles a single temporary ref rather than --all to keep the build context small, and removes that ref even when bundling fails.
Wires planning, rendering, submission, polling, and fetching into four subcommands. A/B mode submits the same row set against two digest-pinned images, disambiguating row keys and OSMO task names by side so both can share one dispatch. A COMPLETED task whose bundle does not parse is reclassified malformed_bundle rather than counted as a success.
Derives per-task sizing, a wall-clock budget, and a reward baseline from a completed dispatch's bundles, so the sizing table is measured rather than guessed. A first dispatch runs every task at its shipped agent-config default; harvest reads the resolved values back out. plan.apply_metadata overlays the result onto the seed list, with seed-list values winning so a hand-set override survives a later harvest. Crashed runs are excluded: their duration describes the failure, not the task.
Records the build, dispatch, harvest, status, and fetch flows, the on-disk layout, the four failure kinds, and the three known gaps: provisional result retrieval, the interim seed task list, and missing play-side video support upstream.
Enumerating the registry showed the physics presets are physx (44), newton_mjwarp (45), ovphysx (32), newton_kamino (27), isaacsim_physx (10), newton_mjwarp_vbd (2), newton_mjwarp_vbd_proxy (1). isaacsim_physx is a distinct preset, not an alias of physx, and was falling through to the newton profile — which would have built a virtualenv without Isaac Sim and failed at run time. Also normalises the legacy aliases (newton, kamino, ovrtx_renderer, isaacsim_rtx_renderer) before profile selection. The separate 'rtx' renderer preset is left out deliberately: it is not confirmed to require Isaac Sim.
The previous publish path never worked. publish_command() emitted 'osmo data upload', but the image installs only ca-certificates, curl, git and uv — no osmo binary, no credentials. The deliberate '|| echo' that stops a failed upload masking the benchmark exit code would have swallowed 'command not found', so every row of a first real dispatch would have returned malformed_bundle. Publishing is now declarative: each task carries an 'outputs: - url:' block and OSMO uploads the output directory itself, after exec and regardless of exit code. No osmo binary and no storage credentials are needed in the container, a crashed run still returns what it produced, and the benchmark's verdict is structurally unmaskable. Validating a rendered workflow against the live service also caught 'pull_credential' being rejected outright; OSMO's task 'credentials:' block maps a credential name to the registry host. Pool and platform in the shipped config did not exist either. Adds a submit-time preflight — 'osmo data check -a WRITE' plus 'osmo workflow validate' on every chunk — so schema drift and bucket-mode changes fail before any GPU time is spent, rather than after.
A preset token does not determine the physics backend. Measured across every task that declares them: physics=physx resolves to kitless OvPhysX on 39 tasks and to Kit PhysX on 5, while isaacsim_physx resolves to Kit PhysX on all 10. Extras were being chosen from the token, so all three physx rows in the seed list would have been given --extra isaacsim when they actually need ovphysx — and uv refuses to install both. Profiles are now full installs of everything that co-resolves within them, so a row picks a profile rather than reasoning about extras. The two-way split is forced by a dependency clash, not by preference: isaacsim-core pins packaging==26.0 and ovphysx pins packaging<24, so a single virtualenv holding both is unresolvable. Renames the newton profile to kitless, since it covers OvPhysX and OVRTX as well. Makes physics optional: 35 of the 87 usable tasks declare no physics preset and hard-fail on any physics= token, and the template emitted one unconditionally. Rows carry their profile explicitly, defaulting to kitless. Resolving it per task belongs to task discovery.
ovphysx 0.5.9 caps packaging at <24 in its own wheel metadata while isaacsim-core 6.0.0.1 pins ==26.0, which made the two extras impossible to install together and forced [tool.uv].conflicts to declare them incompatible. Both caps are stricter than the code needs. A packaging>=20,<27 override collapses the forked resolution onto 26.0, so the isaacsim/ovphysx conflict entry is no longer required. teleop, viser, mimic, test and the 'all' aggregate still conflict and stay. For OdinV2 this removes a whole class of failure. Its two profiles existed only because of this clash, and picking between them required resolving each row's physics preset to a Kit or kitless runtime — which a preset token does not determine (physics=physx resolves to OvPhysX on 39 tasks and to Kit PhysX on 5). One 'full' profile removes the routing problem entirely. Lockfile regenerated with uv 0.12.0; it also picks up the workspace version bumps the nightly changelog commit left unlocked.
The first real build spent 480s failing apt DNS lookups and reported nothing, because every subprocess ran with capture_output=True. Docker output now streams, so a wedged step looks different from a slow one, and _check reports the exit code when there is no captured stderr. uv sync pulls tens of GB from PyPI in a single layer, so a dropped connection discarded the lot and restarted from zero. It now writes to a BuildKit cache mount, which makes a retry resume from what already landed. That also keeps the wheel cache out of the image: nothing needs it at run time, since the venv built here already matches what tasks request and 'uv run --frozen' cannot re-resolve. Raises UV_HTTP_TIMEOUT to 180s, well above the 30s default, because large wheels behind a VPN stall longer than that while still alive, and builds with --network=host to use the host resolver rather than BuildKit's bridge.
Replaces the hand-typed six-entry seed list with discovery over the Gym registry: 87 dispatchable tasks, 163 rows, 102 core and 61 contrib. The old list covered about 5% of the tree. Discovery is the default source; a hand-written file remains possible via 'dispatch --tasks-yaml'. Rows carry an explicit scope field rather than re-deriving core-vs-contrib from the task-id prefix at each call site. Four named policies with composable post-filters: standard (163 rows), core-only (102), all-libraries (348), cross-backend (75). Two selection rules are worth stating. physx is dropped when ovphysx is also declared, because headless they resolve to the same backend and running both is an exact duplicate. Tasks declaring no physics preset emit one row with the field absent, since they reject any physics= token. Sizing is never invented here -- it is harvested from a real run and applied as an overlay.
AGENTS.md mandates snake_case CLI arguments and upstream follows it (--rl_library, --num_envs, --max_iterations), but sixteen Odin flags used kebab-case, so 'odin' read inconsistently with the 'isaaclab benchmark' command it wraps. --policy is renamed --selection. It names the row-set shape, but 'policy' means the learned agent everywhere else in this codebase, so '--policy standard' read like it selected a network. The RL library axis is --library. Documents the discovery flow, including that physics=physx resolves to kitless OvPhysX on 39 tasks and to Kit PhysX on only 5 -- so sweeping the PhysX family needs all three tokens.
Renderers were discovered and then silently dropped, so a camera task was expanded across physics only and benchmarked headless -- measuring everything except the thing under test. All four renderers on Isaac-Cartpole-Camera-Direct were being discarded. Discovery now returns a task's legal modes directly rather than raw axes, so nothing downstream reconstructs the cross product or reasons about which pairings are rejected. The cross product is not all legal: OVRTX is kitless and cannot share a process with Kit physics, so isaacsim_physx + ovrtx is refused and 15 of 16 pairings survive. Each pairing is checked against the runtime validator, which costs about seven seconds for the whole registry. Row keys gain a renderer segment. Without it the renderer variants of one task collapse onto a single key -- caught by the existing duplicate guard. Headless keys are unchanged. 87 tasks now yield 429 rows, 109 of them carrying a renderer across the five camera-capable tasks.
Domain presets -- depth, rgb, albedo, semantic_segmentation, shading and scene variants -- were a third axis the tool ignored entirely, so depth on cartpole was unreachable. Discovery now expands them and --presets whitelists them, alongside --physics and --renderer. Backend names are filtered out of that axis. 31 tasks list physx or newton_mjwarp under DOMAIN as well, and a per-task check is not enough: a task can list a backend under DOMAIN without declaring it under PHYSICS. Uses the same global mirror-name guard as tools/environ_docs.py. Discovery emits one preset at a time and never combines them, which is a documented limitation rather than a property of the runtime: presets targeting the same field conflict outright, and no rule is available for which pairs compose. The executor imposes no such limit -- a hand-written list may give a token, a comma-separated string, or a YAML list, and it renders as one presets=a,b token. dispatch.json records what ran. Row keys gain a preset segment, without which the variants of one task collide. 87 tasks now yield 1663 rows, so whitelisting is the normal path rather than an optimisation.
The header pointed at --tasks-yaml, which was renamed --tasks_yaml when the CLI moved to snake_case.
PlayBundle.video_path and build_play_bundle(video_path=...) existed, but no play adapter could populate them: the video CLI arguments lived in add_common_train_args and were training-only, and wrap_record_video hardcodes a videos/train folder with a periodic step trigger. Camera tasks were therefore benchmarked headless, measuring everything except the thing under test. Adds --video and --video_length to the RSL-RL, RL-Games, SKRL and SB3 play adapters, which now enable cameras, request an rgb_array render mode, wrap the env before the RL vec wrapper, and report the directory on the bundle. Recording starts at the first step rather than on an interval, because a play run is one bounded rollout with no later interval to catch; --video_interval stays training-only. The shared arguments move to add_video_args so training and play cannot drift apart.
Widening the packaging cap touched only the root pyproject, but the override list is published in two more places that must mirror it: tools/wheel_builder/uv-overrides.txt and the uv_pip install-CI copy. Without them, wheel-based installs would still hit the isaacsim/ovphysx conflict while uv run worked -- a split-brain failure visible only in the wheel path. test_wheel_builder_uv_overrides_match_root_pyproject exists to catch exactly that, and did. Updates test_uv_run_isaacsim_extra_is_conflict_forked, which asserted the isaacsim/ovphysx conflict that was deliberately removed. The pair is now asserted absent, with the reason, so re-adding it fails loudly rather than silently reinstating the split.
parent_dispatch_id and reset_in_flight_to_pending sat in the persisted schema promising a capability nothing implemented. A controller that died mid-poll could only be recovered by hand, and failed rows could not be re-dispatched at all -- painful on a 1663-row sweep. --resume re-attaches to a dispatch whose workflows are already submitted, flipping in-flight rows back to pending and re-entering the poll loop. It never re-submits: the workflow ids are recorded, and submitting again would duplicate every task. It refuses a dispatch that never got past planning rather than silently doing nothing. --retry_failed starts a new dispatch containing only the failed rows of another, linked by parent_dispatch_id, reusing the parent's seeds. Retries stay explicit because a benchmark crash usually repeats. Both accept LATEST. --image and --seeds become conditionally optional, with an explicit error rather than an argparse failure that could not explain which path made them optional.
Goal #2 needs videos and checkpoints for the Core tasks. --play runs the rollout in the same task as training, so it reads the checkpoint just written with no cross-task handoff, no shared storage round trip, and no dependency graph to get wrong. TrainingBundle.checkpoint_path was hardcoded None in the RSL-RL, RL-Games, and SKRL training benchmarks -- the same dead-field shape as video_path -- so there was nothing for a chained step to read. It now reports the checkpoint that actually landed on disk, found by latest_checkpoint_path since the libraries name and place them differently and none return the path. The chained step is skipped when training failed or wrote no checkpoint, and the task exits with the training code: a play failure must not turn a good training run into a red row. Both steps receive the same physics, renderer and preset tokens. Verified beyond rendering: the entry script passes bash -n, the embedded extractor parses and returns the right path on a real bundle shape and nothing on a null field, and the workflow validates against live OSMO.
The chained entry script invoked python3 to read the checkpoint out of the training bundle, but the image is nvidia/cuda plus uv, and uv manages its own Python -- there is no python3 on PATH. The call would have failed with command not found, the substitution would have yielded an empty string, and the row would have reported 'training wrote no checkpoint': a wrong diagnosis for a missing interpreter. Uses the venv interpreter uv sync created instead. Every row now writes odin-steps.json recording the training exit code, whether a checkpoint was found and where, why play did or did not run (not_requested, skipped_training_failed, skipped_no_checkpoint, ran) and its exit code, plus an artifact listing. The bundles say what a run measured; this says what happened, which is what a row returning less than expected needs. Writing it can never fail the task. Verified by executing the rendered script inside the real image on both paths -- training-failed and checkpoint-found-play-ran -- not just with bash -n, which is what missed the interpreter in the first place.
Two bugs found while drafting the data-side design doc.
Checkpoints were never uploaded. RL libraries write them under logs/,
but OSMO collects only {{output}}, and nothing copied them across -- so
the stated goal of producing videos and checkpoints delivered videos and
silently lost every checkpoint, while odin-steps.json reported a path
that died with the container. --keep_checkpoints copies it into the
uploaded output and the step record reports where it landed.
Checkpoint resolution now happens whether or not play is chained:
retaining a policy and rolling it out are separate concerns, and a
training-only sweep should still be able to keep its weights.
harvest grouped by (task, library, physics) alone, so an A/B dispatch
averaged both commits into one baseline, and a depth rollout was pooled
with an RGB one on the same task and backend. It now excludes side B
using dispatch.json and segments by renderer and presets.
Also drops a stale README claim that play video was not yet possible, and
documents that only --output_path survives the container.
Verified in the real image: the checkpoint copy lands and the step record
reads retained_as checkpoint/model_500.pt.
Each of these silently changes the verdict of a real dispatch: - The entry script's `find "$OUT" -type f | head -40` runs under `set -o pipefail`. `head` closes the pipe, `find` takes SIGPIPE, and the pipeline exits 141, ending the script before `exit $rc`. It only triggers once a run produces enough files to fill head's buffer, so small tests pass and production rows fail. - `latest_checkpoint_path` globbed `model_*.pt`, which matches RSL-RL only. SKRL writes `agent_<tag>.pt`, `best_agent.pt` and `<name>_<tag>.pt`, so every SKRL row reported no checkpoint and any chained play step took the `skipped_no_checkpoint` branch. - `JobEntry.physics` is legitimately None for tasks that declare no physics preset, but `_job_from_dict` coerced it with `str()`. Reload produced the string "None", which passes every `is not None` guard and renders `physics=None` into a retry's workflow. - A COMPLETED row whose bundle does not parse is reclassified as failed by the fetch hook. OSMO keeps reporting COMPLETED, so the next tick attempted an illegal failed -> completed transition, and the uncaught ValueError abandoned every still-running row. - `osmo workflow query` takes `-t/--format-type`, not `--output`, so the JSON path had never once run: every query fell through to the table parser, which reports no exit code at all. Worse, the JSON parser read a top-level `tasks` key that OSMO does not emit -- tasks are nested under `groups[].tasks[]` and the workflow is named by `name`. Both are now matched against a real query response, and the table fallback is gone.
Eight places where the code did something narrower or broader than what
it claims:
- A retry collapsed failures to (identities, union-of-seeds) and let
plan_rows re-expand the cross product, so a seed that passed was
re-run. Failures are now grouped by the seed they occurred on.
- A retry plans one row per identity and runs it against --image, so an
A/B dispatch's side-B failures would silently be re-run on side A's
image. Retrying an A/B dispatch is now refused outright.
- apply_metadata keyed on three fields while harvest groups on five, so
a camera row could inherit a headless row's timeout. Both now use the
same key.
- sync_once's query-failure handler had no ceiling, so bad credentials
looped forever. poll_until_terminal now aborts after a bounded run of
ticks in which every query failed, pointing at --resume.
- _mode_resolves caught bare Exception, so an Isaac Lab import
regression marked every mode illegal and the CLI blamed the operator's
filters. Import and API failures now surface as DiscoveryError.
- harvest swallowed an unreadable dispatch.json to {}, which empties
side_b_rows and averages an A/B dispatch's two commits together --
precisely what excluding side B exists to prevent.
- chunk_rows raised only after dispatch.json was written, stranding an
orphan dispatch that LATEST resolves to. chunk_size is now validated
at config load and before the dispatch directory is created.
- The row sort key omitted renderer and presets, so a task's preset
variants tied and their layout followed input order rather than being
identical across reruns.
The workflow YAML and the Dockerfile were each asserted by a few dozen substring checks, which pass while saying nothing about the rest of the artefact. They are now compared whole against committed goldens under tools/odin/tests/golden/, regenerable with ODIN_UPDATE_GOLDEN=1. The invariants a golden diff would show but not explain -- one group per task, the credential-to-host mapping, the exit-code verdict, the venv interpreter -- stay as named tests. Also removed machinery that only ever had one value or no caller: - plan.PROFILE_EXTRAS and image.PROFILES held the same 8-tuple verbatim in two modules, each with a single "full" key. They collapse to one plan.UV_EXTRAS that image.py imports, taking with them uv_extras_for_profile, PlannedRow.profile, --profile, and the Dockerfile's per-profile loop. Tags lose their "-full" suffix. A test now asserts the image and the rows agree on the extras, which nothing checked before. - OsmoClient.logs/_stream_logs/cancel had no callers and no tests; data_check's access argument was only ever "WRITE". - JobEntry.attempts was written and never read. - discover_tasks' validate_modes was never passed. - state's major-version comparison had only ever seen one version. - read_bundle and validate_bundle were the same glob-parse-check written twice; harvest now imports results.read_bundle. Golden files are byte-exact renderer output, so pre-commit's whitespace and license hooks skip that directory.
Comments should say what the code does and why, in the present tense. Removed the ones that instead recount how a bug was found or what the code used to be, the speculative paragraphs about where an API might one day move, and the exact task counts that go stale the next time the registry changes without anything failing to say so. The README also still described the per-profile build that this branch collapsed: --profile, the profile table, and the -full tag suffix are gone, and the retry section now states the per-seed and A/B rules. Added the ODIN_UPDATE_GOLDEN workflow for re-rendering the golden files. Kept the comments that record something the code cannot: the config_file module-name clash, bool subclassing int, BUNDLE_REF's effect on recorded provenance, why pending may go straight to terminal, why each OSMO task gets its own group, and why the image clones a bundle rather than copying a checkout.
tools/odin/config/tasks.yaml is produced by `odin discover` in seconds and goes stale against the registry the moment a task changes. Committing it added 9059 lines -- 55 percent of the branch diff -- of content nobody should read line by line, and every regeneration was a diff. It is now gitignored, and dispatch says how to produce one when it is absent rather than reporting an unreadable path. Dropping the generated_at timestamp from the header removes the remaining source of churn for anyone who does keep a local copy. The CLI tests wrote their own three-row list instead of copying the 1663-row file into every case, which takes the suite from 21s to 1s. Marks the rendered test goldens as generated so they collapse in review.
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
Rebuilds Odin as an OSMO-only benchmark dispatcher on top of Isaac Lab's own
benchmarking infrastructure. The previous harness was 38.5k LOC across an SSH
fleet dispatcher and an OSMO dispatcher, built when Isaac Lab had no benchmark
framework of its own. Most of what it invented is now upstream, so this cuts it
to 4.1k LOC and keeps only the OSMO half.
The fleet stack is preserved untouched on
antoiner/feat/odinand is notcarried over.
What Odin does now
tools/odin/discovers tasks from the Gym registry, pins a commit into acontainer image, submits one OSMO task per row, polls, and pulls results back.
Every task runs
uv run isaaclab benchmark trainingdirectly — Odin owns norunner, no schema, and no metrics code.
Changes outside
tools/odin/Three, each independently reviewable:
1.
packagingcap widened to<27(rootpyproject.toml,uv.lock).ovphysx0.5.9 capspackaging<24in its own wheel metadata whileisaacsim-corepins==26.0, which made the two extras impossible to installtogether and forced a
[tool.uv].conflictsentry. Both caps are stricter thanthe code needs. The override collapses them onto 26.0, so the
isaacsim/ovphysxconflict entry is removed. Validated at runtime, not justat resolution:
import ovphysxsucceeds againstpackaging26.0 withisaacsim6.0.0.1 installed alongside. The two published override copies(
tools/wheel_builder/, install-CI) are synced.2.
--videoon the four play benchmark adapters.PlayBundle.video_pathandbuild_play_bundle(video_path=...)already existed,but no adapter could populate them: the video CLI arguments lived in
add_common_train_argsandwrap_record_videohardcodes avideos/trainfolder with a periodic trigger. Camera tasks were therefore benchmarked
headless. Recording starts at the first step rather than on an interval, since a
play run is one bounded rollout;
--video_intervalstays training-only.3. Shared video helpers in
isaaclab_rl.entrypoints.commonso training andplay cannot drift apart.
Notable findings
physics=physxdoes not mean Kit PhysX. Measured across the registry itresolves to kitless OvPhysX on 39 tasks and to Kit PhysX on 5;
isaacsim_physxis the reliably-Kit one. Selecting uv extras from the presettoken was therefore wrong, which the single
fullprofile now sidesteps.physics=token, sophysicsis optional throughout.outputs: - dataset:is rejected server-sidewith "Bucket isaac mode is read-only". Publishing is declarative via
outputs: - url: swift://…, so OSMO performs the upload after exec regardlessof exit code and no storage credentials enter the container.
Type of change
tools/odin/)Validation
scripts/benchmarks/test+source/isaaclab/test/cli: 256 passed. The 6failures are environmental —
skrl,stable_baselines3andrl_gamesareabsent from the base venv, confirmed by re-running one with
--extra skrl(passes), plus a prebundled-torch check needing a full Isaac Sim install.
(
osmo workflow validate→ succeeded).uv synccompletes with every backend in onevirtualenv.
uv run isaaclab -fclean;tools/changelog/cli.py check developpasses.Not done
faster uplink than the current VPN (~111 KB/s for a 33.8 GB layer).
comparefront-end. Odin produces paired, provenance-tagged bundles;regression methodology belongs with the Perf team.
--resume/--retry_failedare not wired.tools/odin/discover.pyrather thanisaaclab_tasks.utils; promoting it is a follow-up.Checklist
CONTRIBUTORS.md