Skip to content

Commit 21a23e8

Browse files
authored
Merge branch 'develop' into antoiner/deprecate-io-descriptors
2 parents 1864ead + c4a2759 commit 21a23e8

1,244 files changed

Lines changed: 56180 additions & 18109 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitattributes

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,15 @@
77
*.obj filter=lfs diff=lfs merge=lfs -text
88
*.gif filter=lfs diff=lfs merge=lfs -text
99
*.mp4 filter=lfs diff=lfs merge=lfs -text
10+
*.webp filter=lfs diff=lfs merge=lfs -text
1011
*.pt filter=lfs diff=lfs merge=lfs -text
1112
*.jit filter=lfs diff=lfs merge=lfs -text
1213
*.hdf5 filter=lfs diff=lfs merge=lfs -text
1314

1415
source/isaaclab_tasks/test/golden_images/**/*.png filter=lfs diff=lfs merge=lfs -text
1516

17+
# Generated actuator plots are reviewed as rendered images rather than XML.
18+
docs/source/_static/actuators/*.png binary linguist-generated
19+
1620
*.bat text eol=crlf
1721
*.sh text eol=lf

.github/actions/_lib/compute-deps-hash/action.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ runs:
5454
isaaclab.sh
5555
environment.yml
5656
source/isaaclab/isaaclab/cli
57+
tools/wheel_builder/uv-overrides.txt
58+
# Pins the CI pytest deps layered onto the image after build, so a
59+
# change to that list must invalidate the deps cache.
60+
.github/actions/docker-build/action.yml
5761
)
5862
deps_manifest_pattern='(setup\.py|pyproject\.toml|setup\.cfg|extension\.toml|requirements[^/]*\.txt|uv\.lock)$'
5963
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/usr/bin/env bash
2+
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
3+
# All rights reserved.
4+
#
5+
# SPDX-License-Identifier: BSD-3-Clause
6+
7+
set -u
8+
9+
if [ "$#" -eq 0 ]; then
10+
echo "Usage: $0 <pip-or-uv-command> [args...]" >&2
11+
exit 2
12+
fi
13+
14+
readonly command_attempts=3
15+
readonly package_index_retries=3
16+
readonly retry_delay_seconds=3
17+
18+
# Bound the combined command- and request-level retry budget to avoid excessively long CI stalls.
19+
export PIP_RETRIES="${PIP_RETRIES:-$package_index_retries}"
20+
export UV_HTTP_RETRIES="${UV_HTTP_RETRIES:-$package_index_retries}"
21+
22+
for ((attempt = 1; attempt <= command_attempts; attempt++)); do
23+
if "$@"; then
24+
exit 0
25+
else
26+
exit_code=$?
27+
fi
28+
29+
if ((attempt == command_attempts)); then
30+
exit "$exit_code"
31+
fi
32+
33+
echo "Package install attempt $attempt/$command_attempts failed; retrying in $retry_delay_seconds seconds..." >&2
34+
sleep "$retry_delay_seconds"
35+
done

.github/actions/docker-build/action.yml

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,33 @@ runs:
175175
docker buildx build --load "${BUILD_ARGS[@]}" "${{ inputs.context-path }}"
176176
echo "was-built=true" >> "$GITHUB_OUTPUT"
177177
178-
##### 6: Tag built image with local deps-tag #####
178+
##### 6: Layer the CI pytest harness onto the built image #####
179+
180+
# Kept out of the tracked Dockerfiles, which build local dev containers and
181+
# ship via publish-images.yaml. Only on a real build; cache hits have it.
182+
183+
- name: Layer CI test dependencies
184+
if: steps.build.outputs.was-built == 'true'
185+
shell: bash
186+
run: |
187+
set -euo pipefail
188+
# pip needs root; restore the image's own default user afterwards.
189+
image_user="$(docker image inspect --format '{{.Config.User}}' "${{ inputs.image-tag }}")"
190+
# Dockerfile on stdin (no context to stage). Quoted heredoc so the
191+
# shell leaves ISAACLAB_PATH and IMAGE_USER for Docker to expand; the
192+
# image tag is an action input, substituted before bash ever runs.
193+
docker build --platform "${{ inputs.platform }}" \
194+
--build-arg "IMAGE_USER=${image_user:-root}" \
195+
-t "${{ inputs.image-tag }}" - <<'DOCKERFILE'
196+
FROM ${{ inputs.image-tag }}
197+
ARG IMAGE_USER
198+
USER root
199+
RUN ${ISAACLAB_PATH}/isaaclab.sh -p -m pip install \
200+
pytest pytest-mock junitparser flaky "coverage>=7.6.1"
201+
USER ${IMAGE_USER}
202+
DOCKERFILE
203+
204+
##### 7: Tag built image with local deps-tag #####
179205

180206
# Runs only when a real build happened (not on cache hits). Populates the
181207
# deps-tag so the next build with identical deps short-circuits at step 4.
@@ -191,7 +217,7 @@ runs:
191217
echo "🟠 LOCAL_DEPS_TAG not set, skipping local deps-cache tag"
192218
fi
193219
194-
##### 7: Evict stale local deps-cache tags (>14d) — opt-in #####
220+
##### 8: Evict stale local deps-cache tags (>14d) — opt-in #####
195221

196222
- name: Evict stale local deps-cache tags (>14d)
197223
if: always() && inputs.evict-stale-cache == 'true'
@@ -214,7 +240,7 @@ runs:
214240
--format '{{.CreatedAt}}|{{.Repository}}:{{.Tag}}' 2>/dev/null)
215241
echo "🔵 Evicted ${evicted} deps tag(s) older than ${TTL_DAYS}d"
216242
217-
##### 8: Host disk snapshot (post) #####
243+
##### 9: Host disk snapshot (post) #####
218244

219245
- name: Host disk snapshot (post)
220246
if: always()

.github/actions/install-ci-collect/action.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ runs:
3030
venv="$RUNNER_TEMP/collect-venv"
3131
collected="$RUNNER_TEMP/install-ci-collected.txt"
3232
python3 -m venv "$venv"
33-
"$venv/bin/pip" install --quiet pytest pytest-timeout
33+
bash "$GITHUB_WORKSPACE/.github/actions/_lib/with-python-package-retries.sh" \
34+
"$venv/bin/pip" install --quiet pytest pytest-timeout
3435
# -m "not skip" hides tests with @pytest.mark.skip; they would just
3536
# report SKIPPED at run time and clutter the collected inventory.
3637
args=( --collect-only -q -m "not skip" source/isaaclab/test/install_ci )

.github/actions/multi-gpu/multi_gpu_shard_runner.sh

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,37 +20,33 @@
2020
#
2121
# Behavior:
2222
# 1. Materializes HOME + PYTHONUSERBASE dirs (tmpfs, world-writable)
23-
# 2. Installs pytest deps (junitparser et al.) into the shared PYTHONUSERBASE
24-
# 3. Derives shard count from nvidia-smi -L (authoritative; torch.cuda.device_count
23+
# 2. Derives shard count from nvidia-smi -L (authoritative; torch.cuda.device_count
2524
# under-counts MIG-on-same-parent unless CUDA_VISIBLE_DEVICES enumerates each)
26-
# 4. Cross-checks torch against the nvidia-smi count and caps shards to what torch
25+
# 3. Cross-checks torch against the nvidia-smi count and caps shards to what torch
2726
# can address (guards against CUDA_VISIBLE_DEVICES misconfig on a MIG host)
28-
# 5. Fans out 1 pytest subshell per non-default cuda:N with per-shard HOME +
27+
# 4. Fans out 1 pytest subshell per non-default cuda:N with per-shard HOME +
2928
# ISAACLAB_TEST_DEVICES; each shard tees its stdout to
3029
# /shard-logs/cuda-N.log for the host's grouped re-print after the run
31-
# 6. Waits on every shard before aggregating exit codes — a fast failure doesn't
30+
# 5. Waits on every shard before aggregating exit codes — a fast failure doesn't
3231
# tear down still-running siblings
32+
#
33+
# The pytest deps (pytest, junitparser et al.) are baked into the image by
34+
# .github/actions/docker-build, so this script no longer installs them.
3335

3436
set +e # keep going on errors; per-shard exit codes are aggregated at the end
3537
cd /workspace/isaaclab
3638
unset DISPLAY # clear the var that would force Kit into headed (X11) mode
3739

38-
# Container-level HOME + PYTHONUSERBASE for pip --user installs. The image
39-
# runs as --user $host_uid:$host_gid with no matching /etc/passwd entry, so
40-
# HOME defaults to /root which the user cannot write. /tmp/* is on tmpfs
40+
# Container-level HOME + PYTHONUSERBASE. The image runs as
41+
# --user $host_uid:$host_gid with no matching /etc/passwd entry, so HOME
42+
# defaults to /root which the user cannot write. /tmp/* is on tmpfs
4143
# (1777, world-writable).
4244
#
43-
# PYTHONUSERBASE is the key for the 1-docker shape: pip --user writes to
44-
# ${PYTHONUSERBASE}/lib/python3.12/site-packages, and every Python invocation
45-
# that sees the same env var imports from there. Per-shard subshells below
46-
# override HOME (so .cache / .nvidia-omniverse are isolated) but inherit
47-
# PYTHONUSERBASE so junitparser et al. resolve everywhere.
45+
# Both must exist before any shard starts: per-shard subshells below override
46+
# HOME (so .cache / .nvidia-omniverse are isolated) but inherit
47+
# PYTHONUSERBASE, so anything writing to the user site shares one directory.
4848
mkdir -p /tmp/mgpu-base-home /tmp/mgpu-pyuserbase
4949

50-
# Pytest deps (same as run-tests action). junitparser is imported at
51-
# tools/conftest.py load time, so it must be present first.
52-
./isaaclab.sh -p -m pip install pytest pytest-mock junitparser flaky "coverage>=7.6.1"
53-
5450
# Shard count from nvidia-smi -L (truth; torch under-counts MIG).
5551
MIG_COUNT=$(nvidia-smi -L | grep -c "^ MIG ") # grep -c = count of matching lines (MIG slices)
5652
GPU_COUNT=$(nvidia-smi -L | grep -c "^GPU ") # count of whole GPUs

.github/actions/run-package-tests/action.yml

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ runs:
211211
HOST_DIR: ${{ steps.warp-cache-key.outputs.host-dir }}
212212
MATCHED_KEY: ${{ steps.warp-cache-restore.outputs.cache-matched-key }}
213213
COLLECTION: ${{ steps.warp-cache-key.outputs.collection }}
214+
CACHE_MODE: ${{ inputs.warp-cache }}
214215
run: |
215216
set -euo pipefail
216217
mkdir -p "$HOST_DIR"
@@ -228,9 +229,13 @@ runs:
228229
echo "Warp cache hit: ${MATCHED_KEY}"
229230
echo "WARP_CACHE_HIT=${MATCHED_KEY}" >> "$GITHUB_ENV"
230231
fi
231-
echo "WARP_CACHE_MB_BEFORE=$(du -sm "$HOST_DIR" | cut -f1)" >> "$GITHUB_ENV"
232+
echo "WARP_CACHE_BYTES_BEFORE=$(du -sb "$HOST_DIR" | cut -f1)" >> "$GITHUB_ENV"
232233
233234
python3 .github/actions/run-package-tests/warp_cache_inventory.py "$HOST_DIR" "Warp cache restored"
235+
if [ "$CACHE_MODE" = "save" ]; then
236+
fingerprint="$(python3 .github/actions/run-package-tests/warp_cache_inventory.py "$HOST_DIR" --fingerprint)"
237+
echo "WARP_CACHE_FINGERPRINT_BEFORE=$fingerprint" >> "$GITHUB_ENV"
238+
fi
234239
235240
- name: Setup wheelhouse registry authentication
236241
if: inputs.wheelhouse-image != ''
@@ -333,15 +338,16 @@ runs:
333338
exit 1
334339
fi
335340
336-
# Growth is the coverage signal: it is exactly the kernels this job needed
337-
# that the collection did not already hold. Reporting only - no thresholds,
338-
# since we have no measured basis for one yet.
341+
# Net growth is a useful coverage signal for missing kernels. The writer
342+
# also fingerprints the tree because recompilation can replace an existing
343+
# artifact without changing the total size.
339344
- name: Report Warp cache growth
340345
if: always() && inputs.warp-cache != ''
341346
id: warp-cache-growth
342347
shell: bash
343348
env:
344349
HOST_DIR: ${{ steps.warp-cache-key.outputs.host-dir }}
350+
CACHE_MODE: ${{ inputs.warp-cache }}
345351
run: |
346352
set -euo pipefail
347353
@@ -351,31 +357,51 @@ runs:
351357
exit 0
352358
fi
353359
354-
before="${WARP_CACHE_MB_BEFORE:-0}"
355-
after="$(du -sm "$HOST_DIR" | cut -f1)"
360+
before="${WARP_CACHE_BYTES_BEFORE:-0}"
361+
after="$(du -sb "$HOST_DIR" | cut -f1)"
356362
grew=$(( after - before ))
363+
before_mb=$(( before / 1000000 ))
364+
after_mb=$(( after / 1000000 ))
365+
grew_mb=$(( grew / 1000000 ))
357366
358367
if [ "${WARP_CACHE_HIT:-miss}" = "miss" ]; then
359368
verdict="cold run, nothing restored"
360369
elif [ "$grew" -le 0 ]; then
361-
verdict="fully covered"
370+
verdict="no net growth"
362371
else
363372
verdict="compiled $(( grew * 100 / (before > 0 ? before : 1) ))% beyond the restored cache"
364373
fi
365374
366-
echo "Warp cache: restored ${before} MB, ended at ${after} MB (+${grew} MB) - ${verdict}"
367-
echo "🔵 Warp cache: ${before} -> ${after} MB (+${grew}) - ${verdict}" >> "$GITHUB_STEP_SUMMARY"
375+
echo "Warp cache: restored ${before_mb} MB, ended at ${after_mb} MB (+${grew_mb} MB) - ${verdict}"
376+
echo "🔵 Warp cache: ${before_mb} -> ${after_mb} MB (+${grew_mb}) - ${verdict}" >> "$GITHUB_STEP_SUMMARY"
368377
python3 .github/actions/run-package-tests/warp_cache_inventory.py "$HOST_DIR" "Warp cache final"
369378
370379
# Publishing an empty tree would replace a good snapshot with nothing.
371-
[ "$after" -lt 1 ] && echo "empty=true" >> "$GITHUB_OUTPUT" || echo "empty=false" >> "$GITHUB_OUTPUT"
380+
if [ -z "$(find "$HOST_DIR" -type f -print -quit)" ]; then
381+
echo "::warning::Warp cache contains no files; skipping publish"
382+
echo "empty=true" >> "$GITHUB_OUTPUT"
383+
echo "changed=false" >> "$GITHUB_OUTPUT"
384+
else
385+
echo "empty=false" >> "$GITHUB_OUTPUT"
386+
if [ "$CACHE_MODE" = "save" ] && [ "${WARP_CACHE_HIT:-miss}" != "miss" ]; then
387+
fingerprint="$(python3 .github/actions/run-package-tests/warp_cache_inventory.py "$HOST_DIR" --fingerprint)"
388+
if [ "$fingerprint" = "${WARP_CACHE_FINGERPRINT_BEFORE:-}" ]; then
389+
echo "Warp cache contents are unchanged; skipping duplicate snapshot"
390+
echo "changed=false" >> "$GITHUB_OUTPUT"
391+
else
392+
echo "changed=true" >> "$GITHUB_OUTPUT"
393+
fi
394+
else
395+
echo "changed=true" >> "$GITHUB_OUTPUT"
396+
fi
397+
fi
372398
373399
# Not success(): the product is compiled kernels, and those stay valid when a
374400
# test assertion fails. Gating on success() let one failing env silently
375401
# discard the whole cache. Cancellation still skips, so a tree Warp was
376402
# mid-write on is never published.
377403
- name: Save Warp kernel cache
378-
if: '!cancelled() && inputs.warp-cache == ''save'' && steps.warp-cache-growth.outputs.empty != ''true'''
404+
if: '!cancelled() && inputs.warp-cache == ''save'' && steps.warp-cache-growth.outputs.empty != ''true'' && steps.warp-cache-growth.outputs.changed == ''true'''
379405
uses: actions/cache/save@v4
380406
with:
381407
path: ${{ steps.warp-cache-key.outputs.host-dir }}

.github/actions/run-package-tests/warp_cache_inventory.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@
1010
cache size and retention with measurements - module counts, artifact mix, and
1111
the spread of GPU targets a shared collection has accumulated.
1212
13-
Usage: warp_cache_inventory.py <cache-dir> [label]
13+
Usage:
14+
warp_cache_inventory.py <cache-dir> [label]
15+
warp_cache_inventory.py <cache-dir> --fingerprint
1416
"""
1517

1618
import collections
19+
import hashlib
1720
import os
1821
import pathlib
1922
import re
@@ -22,8 +25,33 @@
2225
SM_PATTERN = re.compile(r"\.(sm\d+[a-z]?)\.")
2326

2427

28+
def fingerprint(root: pathlib.Path) -> str:
29+
"""Return a stable digest of every cache file's relative path and contents.
30+
31+
This lets the writer avoid publishing another immutable GitHub cache entry
32+
when a warm run only read the previously restored files.
33+
"""
34+
digest = hashlib.sha256()
35+
for path in sorted(path for path in root.rglob("*") if path.is_file()):
36+
digest.update(path.relative_to(root).as_posix().encode())
37+
digest.update(b"\0")
38+
file_digest = hashlib.sha256()
39+
try:
40+
with path.open("rb") as stream:
41+
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
42+
file_digest.update(chunk)
43+
except OSError:
44+
continue
45+
digest.update(file_digest.digest())
46+
return digest.hexdigest()
47+
48+
2549
def main() -> int:
2650
root = pathlib.Path(sys.argv[1])
51+
if len(sys.argv) > 2 and sys.argv[2] == "--fingerprint":
52+
print(fingerprint(root))
53+
return 0
54+
2755
label = sys.argv[2] if len(sys.argv) > 2 else "Warp cache"
2856

2957
if not root.is_dir():

.github/actions/run-tests/run_tests.sh

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ run_tests() {
305305
docker run -d --name $container_name \
306306
--init --stop-timeout 5 \
307307
--entrypoint bash --gpus all --network=host \
308+
-v "$PWD/.github/actions/_lib/with-python-package-retries.sh:/with-python-package-retries.sh:ro" \
308309
--security-opt=no-new-privileges:true \
309310
--memory=$(echo "$(free -m | awk '/^Mem:/{print $2}') * 0.9 / 1" | bc)m \
310311
--cpus=$(echo "$(nproc) * 0.9" | bc) \
@@ -321,7 +322,6 @@ run_tests() {
321322
mkdir -p tests
322323
rm _isaac_sim || true
323324
ln -s /isaac-sim _isaac_sim
324-
./isaaclab.sh -p -m pip install pytest pytest-mock junitparser flaky \"coverage>=7.6.1\"
325325
if [ -n \"\${WARP_CACHE_PATH:-}\" ]; then
326326
./isaaclab.sh -p tools/verify_warp_cache.py
327327
fi
@@ -347,7 +347,7 @@ run_tests() {
347347
fi
348348
if [ -n \"\${TEST_EXTRA_PIP_PACKAGES:-}\" ]; then
349349
echo \"Installing extra pip packages: \${TEST_EXTRA_PIP_PACKAGES}\"
350-
./isaaclab.sh -p -m pip install \${TEST_EXTRA_PIP_PACKAGES}
350+
bash /with-python-package-retries.sh ./isaaclab.sh -p -m pip install \${TEST_EXTRA_PIP_PACKAGES}
351351
case \" \${TEST_EXTRA_PIP_PACKAGES} \" in
352352
*\" leapp\"*)
353353
echo \"Resolved LEAPP package:\"
@@ -363,14 +363,14 @@ run_tests() {
363363
isaac_user_site=\"\$(./isaaclab.sh -p -c 'import site; print(site.getusersitepackages())' | tail -n 1)\"
364364
uv_executable=\"\$(./isaaclab.sh -p -c 'import pathlib, site; print(pathlib.Path(site.getuserbase()) / \"bin\" / \"uv\")' | tail -n 1)\"
365365
if [ ! -x \"\${uv_executable}\" ]; then
366-
./isaaclab.sh -p -m pip install uv
366+
bash /with-python-package-retries.sh ./isaaclab.sh -p -m pip install uv
367367
fi
368-
\"\${uv_executable}\" pip install --python \"\${isaac_python}\" --target \"\${isaac_user_site}\" \${TEST_EXTRA_UV_PACKAGES}
368+
bash /with-python-package-retries.sh \"\${uv_executable}\" pip install --python \"\${isaac_python}\" --target \"\${isaac_user_site}\" \${TEST_EXTRA_UV_PACKAGES}
369369
# Isaac Sim puts bundled packages ahead of the user site. Overlay only
370370
# the explicitly requested packages so compatible direct pins win
371371
# without shadowing bundled transitive dependencies such as NumPy.
372372
isaac_uv_overlay=\"\$(mktemp -d)\"
373-
\"\${uv_executable}\" pip install --python \"\${isaac_python}\" --target \"\${isaac_uv_overlay}\" --no-deps \${TEST_EXTRA_UV_PACKAGES}
373+
bash /with-python-package-retries.sh \"\${uv_executable}\" pip install --python \"\${isaac_python}\" --target \"\${isaac_uv_overlay}\" --no-deps \${TEST_EXTRA_UV_PACKAGES}
374374
export PYTHONPATH=\"\${isaac_uv_overlay}\${PYTHONPATH:+:\${PYTHONPATH}}\"
375375
fi
376376
echo 'Starting pytest with path: $test_path'

.github/actions/upload-omni-github-test-results/action.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@ runs:
9696
# Validating with a local pinned copy of the schemas is recommended by the omni-github team.
9797
schema_venv="${RUNNER_TEMP}/omni-github-jsonschema-venv"
9898
python3 -m venv "$schema_venv"
99-
"$schema_venv/bin/python" -m pip install -q jsonschema
99+
bash "$GITHUB_WORKSPACE/.github/actions/_lib/with-python-package-retries.sh" \
100+
"$schema_venv/bin/python" -m pip install -q jsonschema
100101
if ! "$schema_venv/bin/python" -m jsonschema \
101102
"$GITHUB_ACTION_PATH/result-json.schema.json" \
102103
--instance "$result_json"; then

0 commit comments

Comments
 (0)