Skip to content

Commit 3fb3563

Browse files
authored
ci: move prek to fingerprinted uv-cache artifacts (#560)
- Replace prebuilt-image bootstrap with uv-cache release-asset workflow and fingerprint gating. - Use compute/build helpers (`compute_uv_fingerprint.py`, `build_and_push_uv_cache.sh`) and document cache refresh in CONTRIBUTING. - Store full uv cache as fingerprinted chunked release assets (`.tar.zst.part-###`) so CI reuses wheel/build payloads without GH single-asset size limits. - Download cache parts with parallelism 8 in CI restore path, then reassemble in deterministic order. - Keep immutable cache retention policy (latest 4 fingerprints).
1 parent 35c0c6e commit 3fb3563

12 files changed

Lines changed: 3349 additions & 2185 deletions

.github/workflows/build-prek-cache-image.yml

Lines changed: 0 additions & 180 deletions
This file was deleted.

.github/workflows/prek.yml

Lines changed: 107 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,129 @@ on:
55
push:
66
branches: [main]
77

8+
permissions:
9+
contents: read
10+
811
jobs:
912
quality-checks:
10-
runs-on: ubuntu-latest
13+
runs-on: art-large-runner
14+
container:
15+
image: pytorch/pytorch:2.9.0-cuda12.8-cudnn9-devel
16+
env:
17+
CI_BASE_IMAGE: "pytorch/pytorch:2.9.0-cuda12.8-cudnn9-devel"
18+
CI_PYTHON_MM: "3.11"
19+
CI_UV_CACHE_RELEASE_TAG: "prek-uv-cache"
20+
CI_UV_CACHE_ASSET_PREFIX: "prek-uv-cache"
21+
UV_CACHE_DIR: "/root/.cache/uv"
22+
UV_LINK_MODE: "copy"
23+
TORCH_CUDA_ARCH_LIST: "8.0"
1124

1225
steps:
26+
- name: Install CI dependencies
27+
run: |
28+
apt-get update
29+
apt-get install -y --no-install-recommends ca-certificates curl git zstd
30+
rm -rf /var/lib/apt/lists/*
31+
curl -LsSf https://astral.sh/uv/install.sh | sh
32+
echo "/root/.local/bin" >> "${GITHUB_PATH}"
33+
1334
- name: Checkout code
1435
uses: actions/checkout@v4
1536

16-
- name: Set up Python
17-
uses: actions/setup-python@v5
18-
with:
19-
python-version: "3.11"
37+
- name: Mark workspace as a safe git directory
38+
run: |
39+
git config --global --add safe.directory "${GITHUB_WORKSPACE}"
2040
21-
- name: Install uv
41+
- name: Compute expected uv cache fingerprint
42+
id: expected-uv-fingerprint
2243
run: |
23-
curl -LsSf https://astral.sh/uv/install.sh | sh
24-
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
44+
fp="$(python3 scripts/ci/compute_uv_fingerprint.py \
45+
--pyproject pyproject.toml \
46+
--uv-lock uv.lock \
47+
--base-image "${CI_BASE_IMAGE}" \
48+
--python-mm "${CI_PYTHON_MM}")"
49+
echo "fingerprint=${fp}" >> "${GITHUB_OUTPUT}"
50+
echo "Expected uv cache fingerprint: ${fp}"
51+
52+
- name: Restore prebuilt uv cache
53+
env:
54+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
55+
run: |
56+
release_api="https://api.github.com/repos/${GITHUB_REPOSITORY}/releases/tags/${CI_UV_CACHE_RELEASE_TAG}"
57+
fingerprint="${{ steps.expected-uv-fingerprint.outputs.fingerprint }}"
58+
part_prefix="${CI_UV_CACHE_ASSET_PREFIX}-${fingerprint}.tar.zst.part-"
59+
60+
release_json="$(curl -fsSL \
61+
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
62+
-H "Accept: application/vnd.github+json" \
63+
"${release_api}" || true)"
64+
65+
if [ -z "${release_json}" ]; then
66+
echo "::error::Missing cache release '${CI_UV_CACHE_RELEASE_TAG}'."
67+
echo "::error::Build and upload cache with: bash scripts/ci/build_and_push_uv_cache.sh"
68+
exit 1
69+
fi
70+
71+
part_selection_file="/tmp/uv-cache-part-selection.txt"
72+
if ! RELEASE_JSON="${release_json}" PART_PREFIX="${part_prefix}" python3 -c "import json, os, re, sys; payload=json.loads(os.environ['RELEASE_JSON']); part_prefix=os.environ['PART_PREFIX']; pattern=re.compile(r'^' + re.escape(part_prefix) + r'(\\d{3})$'); parts=[]; [parts.append((int(m.group(1)), int(a.get('id')), a.get('name'))) for a in payload.get('assets', []) for m in [pattern.match(a.get('name', ''))] if m and a.get('id') is not None]; parts.sort(key=lambda x: x[0]); indices=[p[0] for p in parts]; expected=list(range(len(parts))); print('\\n'.join(f'{asset_id} {name}' for _, asset_id, name in parts)) if parts and indices == expected else (_ for _ in ()).throw(SystemExit(2 if not parts else 3))" > "${part_selection_file}"; then
73+
echo "::error::No complete uv cache part set found for prefix '${part_prefix}'."
74+
echo "::error::Build and upload cache with: bash scripts/ci/build_and_push_uv_cache.sh"
75+
exit 1
76+
fi
77+
78+
part_count="$(wc -l < "${part_selection_file}" | tr -d ' ')"
79+
echo "Using uv cache part set '${part_prefix}*' (${part_count} parts)."
80+
81+
parts_dir="/tmp/uv-cache-parts"
82+
part_paths_file="/tmp/uv-cache-part-paths.txt"
83+
rm -rf "${parts_dir}"
84+
mkdir -p "${parts_dir}"
85+
awk -v d="${parts_dir}" '{print d "/" $2}' "${part_selection_file}" > "${part_paths_file}"
86+
87+
PARTS_DIR="${parts_dir}" GITHUB_TOKEN="${GITHUB_TOKEN}" GITHUB_REPOSITORY="${GITHUB_REPOSITORY}" \
88+
xargs -n 2 -P 8 sh -c '
89+
asset_id="$1"
90+
asset_name="$2"
91+
part_path="${PARTS_DIR}/${asset_name}"
92+
curl -fsSL -L \
93+
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
94+
-H "Accept: application/octet-stream" \
95+
"https://api.github.com/repos/${GITHUB_REPOSITORY}/releases/assets/${asset_id}" \
96+
-o "${part_path}"
97+
' sh < "${part_selection_file}"
98+
99+
while IFS= read -r part_path; do
100+
[ -s "${part_path}" ] || {
101+
echo "::error::Missing or empty cache part: ${part_path}"
102+
exit 1
103+
}
104+
done < "${part_paths_file}"
105+
106+
rm -rf "${UV_CACHE_DIR}"
107+
mkdir -p "${UV_CACHE_DIR}"
108+
while IFS= read -r part_path; do
109+
cat "${part_path}"
110+
done < "${part_paths_file}" | zstd -d -c | tar -xf - -C "${UV_CACHE_DIR}"
111+
du -sh "${UV_CACHE_DIR}"
25112
26113
- name: Install dependencies (with all optional extras for complete type checking)
27114
run: |
28-
uv sync --all-extras
115+
py_mm="$(python -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')"
116+
cudnn_path="${GITHUB_WORKSPACE}/.venv/lib/python${py_mm}/site-packages/nvidia/cudnn"
117+
export CUDNN_PATH="${cudnn_path}"
118+
export CUDNN_HOME="${cudnn_path}"
119+
export CUDNN_INCLUDE_PATH="${cudnn_path}/include"
120+
export CUDNN_LIBRARY_PATH="${cudnn_path}/lib"
121+
export CPLUS_INCLUDE_PATH="${CUDNN_INCLUDE_PATH}${CPLUS_INCLUDE_PATH:+:${CPLUS_INCLUDE_PATH}}"
122+
export LIBRARY_PATH="${CUDNN_LIBRARY_PATH}${LIBRARY_PATH:+:${LIBRARY_PATH}}"
123+
export LD_LIBRARY_PATH="${CUDNN_LIBRARY_PATH}${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}"
124+
uv --version
125+
uv sync --all-extras --group dev --frozen
29126
30127
- name: Run prek hooks (lint, format, typecheck, uv.lock, tests)
31128
run: |
32129
uv run prek run --all-files
33130
34131
- name: Run unit tests (via prek)
35132
run: |
36-
uv run prek run pytest
133+
uv run prek run pytest

CONTRIBUTING.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,36 @@ uv run prek run pytest
3737

3838
These checks are automatically run in CI for all pull requests. If your PR fails these checks, re-run the corresponding `prek` hook locally and commit any fixes.
3939

40+
### CI uv Cache Refresh
41+
42+
The PR `prek` workflow uses a prebuilt full `uv` cache (stored as a GitHub release asset) to avoid rebuilding heavy dependencies on every run.
43+
44+
To refresh the cache after dependency changes, run:
45+
46+
```bash
47+
bash scripts/ci/build_and_push_uv_cache.sh
48+
```
49+
50+
This command builds a full cache archive locally (using `uv sync --frozen --all-extras --group dev --no-install-project`) and uploads a fingerprinted part set:
51+
52+
- `prek-uv-cache-<fingerprint>.tar.zst.part-000`
53+
- `prek-uv-cache-<fingerprint>.tar.zst.part-001`
54+
- ...
55+
56+
The script also prunes old immutable cache assets (keeps newest 4 by default).
57+
It requires GitHub CLI authentication (`gh auth login`) and should be run in an environment compatible with CI (same base CUDA image/toolchain).
58+
59+
You can override native-build parallelism while preparing cache:
60+
61+
```bash
62+
bash scripts/ci/build_and_push_uv_cache.sh --build-jobs 2
63+
```
64+
65+
By default, `--build-jobs auto` is used and resolves from available CPU and memory.
66+
By default, cache parts are split at `1900 MiB`; override with `--part-size-mb <n>` if needed.
67+
68+
CI computes the expected cache fingerprint from `pyproject.toml`, `uv.lock`, base image, Python version, and cache asset layout contract. If no matching cache part set exists, CI fails fast and tells you to refresh cache with the script above.
69+
4070
### Release Process
4171

4272
To create a new release:

docker/ci-prek-cache.Dockerfile

Lines changed: 0 additions & 35 deletions
This file was deleted.

0 commit comments

Comments
 (0)