Skip to content

Commit 71eeeba

Browse files
authored
[Workflow] Expand Newton Warp cache coverage (#7135)
## Summary - Warm the Warp cache with all supported rigid Newton core and multi-agent environments. - Skip empty or unchanged cache snapshots and report byte-accurate growth. ## Validation - uv run --frozen isaaclab -f - Collected 62 selected warm-cache cases.
1 parent 717b7d4 commit 71eeeba

3 files changed

Lines changed: 77 additions & 27 deletions

File tree

.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/workflows/build.yaml

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -896,9 +896,9 @@ jobs:
896896

897897
# Publishes the shared Warp kernel cache the test jobs restore. Needed as its
898898
# own job because a cache written from a PR is readable only by that PR, and
899-
# no solver-heavy test job runs on push. Does not restore before saving:
900-
# appending to the previous run grows the cache without bound, which is what
901-
# exhausted the repository quota last time.
899+
# no solver-heavy test job runs on push. Restores the newest compatible
900+
# snapshot so coverage accumulates, but only publishes when its contents
901+
# change to avoid filling the repository quota with duplicate snapshots.
902902
warm-warp-cache:
903903
name: "warp-cache-warm"
904904
runs-on: [self-hosted, gpu]
@@ -933,20 +933,16 @@ jobs:
933933
isaacsim-base-image: ${{ needs.config.outputs.isaacsim_image_name }}
934934
isaacsim-version: ${{ needs.config.outputs.isaacsim_image_tag }}
935935
filter-pattern: "isaaclab_tasks"
936-
# MJWarp specializes kernels per model config (@cache_kernel in
937-
# mujoco_warp/_src/warp_util.py keys on array sizes; ~250 wp.static uses
938-
# feed Warp's module hash), so these span specialization axes rather than
939-
# maximize task count: Cartpole (primitives, contact-free, + camera),
940-
# Drawer (mesh collision), AnymalD (contact-rich locomotion), Handover
941-
# (dexterous, many contacts). Deformable/MPM kernels are not covered;
942-
# widen if a test job reports large cache growth.
936+
# Warm every supported rigid core environment on Newton. MJWarp
937+
# specializes kernels per model config, so a small representative task
938+
# set leaves most of the cache to be compiled again by each PR test
939+
# shard. The multi-agent suite supplies Handover.
943940
include-files: >-
944-
test_environments_isaacsim_physx.py,
945941
test_environments_newton.py,
946-
test_environments_ovphysx.py
947-
# No Soft/Cloth: the deformable envs depend on optional extras the CI
948-
# image does not install (Soft needs pytetwild), so they only ever fail.
949-
test-k-expr: "Cartpole or Drawer or AnymalD or Handover"
942+
test_multi_agent_environments.py
943+
# Deformable tasks need optional dependencies that are not installed in
944+
# this image.
945+
test-k-expr: "test_environments and not (Soft or Cloth or Cable)"
950946
warp-cache: ${{ env.PUBLISHES_WARP_CACHE == 'true' && 'save' || 'restore' }}
951947
container-name: isaac-lab-warp-cache-warm
952948
omni-github-test-type: warp-cache-warm

0 commit comments

Comments
 (0)