From 4322de0bbce25da7f8e56d1c2ce7b6351782b948 Mon Sep 17 00:00:00 2001 From: GF Date: Thu, 16 Jul 2026 11:04:04 -0400 Subject: [PATCH] Restructure CI and GPU validation --- .github/CODEOWNERS | 2 + .github/workflows/ci.yml | 527 ++++------------- .github/workflows/full-validation.yml | 492 ++++++++++++++++ .github/workflows/gpu-benchmarks.yml | 249 ++++++++ .github/workflows/gpu-validation.yml | 534 +++++++----------- .github/workflows/publish.yml | 6 +- CONTRIBUTING.md | 17 +- Cargo.toml | 5 + crates/j2k-jpeg-metal/benches/compare.rs | 10 +- docs/benchmark-corpora.md | 18 +- docs/benchmark-evidence.md | 3 +- docs/env-vars.md | 11 +- docs/release.md | 10 +- scripts/configure-cuda-bindgen.sh | 38 ++ scripts/github_actions_verify.py | 17 +- scripts/tests/test_github_actions_verify.py | 3 +- xtask/src/benchmark_commands.rs | 159 ++---- xtask/src/benchmark_commands/tests.rs | 59 +- xtask/src/benchmark_registry.rs | 284 ++++++++++ xtask/src/cuda.rs | 177 +++--- xtask/src/cuda/quick.rs | 27 + xtask/src/cuda/test_output.rs | 59 ++ xtask/src/cuda/tests.rs | 46 +- xtask/src/cuda/tests/command_plan.rs | 57 ++ xtask/src/gpu_validation.rs | 44 ++ xtask/src/main.rs | 102 ++-- xtask/src/metal.rs | 64 ++- xtask/src/metal/tests.rs | 43 +- xtask/src/perf_guard.rs | 295 ++++++---- xtask/src/release_commands.rs | 6 +- xtask/src/release_status.rs | 6 +- xtask/src/release_status/tests.rs | 5 +- .../audit_integrity_policy.rs | 2 +- .../documentation_api_evidence.rs | 10 +- .../stable_api_governance.rs | 4 +- .../workflow_coverage_policy.rs | 24 +- .../tests/repo_lint_support/j2k_ml_policy.rs | 53 +- xtask/tests/repo_lint_support/mod.rs | 1 + .../repo_lint_support/public_docs_policy.rs | 45 +- .../repo_lint_support/workflow_policy.rs | 241 +++++--- .../xtask_main_structure_policy.rs | 2 +- 41 files changed, 2423 insertions(+), 1334 deletions(-) create mode 100644 .github/workflows/full-validation.yml create mode 100644 .github/workflows/gpu-benchmarks.yml create mode 100644 scripts/configure-cuda-bindgen.sh create mode 100644 xtask/src/benchmark_registry.rs create mode 100644 xtask/src/cuda/quick.rs create mode 100644 xtask/src/cuda/test_output.rs create mode 100644 xtask/src/cuda/tests/command_plan.rs create mode 100644 xtask/src/gpu_validation.rs diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 43f5bcec..78f72e3a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,6 +1,8 @@ # GPU and accelerator surfaces require maintainer review. .github/CODEOWNERS @frames-sg/j2k-maintainers .github/workflows/ci.yml @frames-sg/j2k-maintainers +.github/workflows/full-validation.yml @frames-sg/j2k-maintainers +.github/workflows/gpu-benchmarks.yml @frames-sg/j2k-maintainers .github/workflows/gpu-validation.yml @frames-sg/j2k-maintainers .github/workflows/publish.yml @frames-sg/j2k-maintainers release-crates.json @frames-sg/j2k-maintainers diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b29bd214..ce51619d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,17 +1,13 @@ -name: CI +name: PR checks on: - push: - branches: [main] pull_request: - schedule: - - cron: "17 8 * * *" permissions: contents: read concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: pr-checks-${{ github.event.pull_request.number }} cancel-in-progress: true env: @@ -20,31 +16,30 @@ env: RUSTDOCFLAGS: "-D warnings" jobs: - gpu-path-policy: + planner: + name: Change planner runs-on: ubuntu-latest - permissions: - actions: read - contents: read - pull-requests: read + outputs: + rust: ${{ steps.plan.outputs.rust }} + docs: ${{ steps.plan.outputs.docs }} + cuda: ${{ steps.plan.outputs.cuda }} + metal: ${{ steps.plan.outputs.metal }} + metal_compile: ${{ steps.plan.outputs.metal_compile }} + expectations: ${{ steps.plan.outputs.expectations }} steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - - name: Non-PR events do not require GPU path policy - if: ${{ github.event_name != 'pull_request' }} - run: echo "GPU path policy only applies to pull requests." - - name: Require GPU validation for GPU path changes - if: ${{ github.event_name == 'pull_request' }} + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + with: + toolchain: "1.96" + - id: plan + name: Classify changed paths and dependency impact env: - GH_TOKEN: ${{ github.token }} - GITHUB_API_URL: ${{ github.api_url }} + BASE_SHA: ${{ github.event.pull_request.base.sha }} HEAD_SHA: ${{ github.event.pull_request.head.sha }} - PR_NUMBER: ${{ github.event.pull_request.number }} - REPOSITORY: ${{ github.repository }} - run: | - python3 scripts/github_actions_verify.py pr-gpu-policy \ - --repository "${REPOSITORY}" \ - --pr-number "${PR_NUMBER}" \ - --head-sha "${HEAD_SHA}" \ - --workflow gpu-validation.yml + run: python3 scripts/ci_plan.py plan --base "${BASE_SHA}" --head "${HEAD_SHA}" github-actions-verifier: name: GitHub Actions verifier tests @@ -53,448 +48,148 @@ jobs: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - run: python3 -m unittest discover -s scripts/tests -p 'test_*.py' -v - fmt: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 - with: - toolchain: "1.96" - components: rustfmt - - run: cargo xtask fmt - diff-check: + name: Diff check runs-on: ubuntu-latest - env: - BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }} steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} - name: Reject whitespace errors in the submitted delta - run: | - if [ -n "${BASE_SHA}" ] \ - && [ "${BASE_SHA}" != "0000000000000000000000000000000000000000" ] \ - && git cat-file -e "${BASE_SHA}^{commit}"; then - git diff --check "${BASE_SHA}...${GITHUB_SHA}" - else - git show --check --format=fuller "${GITHUB_SHA}" >/dev/null - fi + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: git diff --check "${BASE_SHA}...${HEAD_SHA}" secret-scan: name: Secret scan uses: ./.github/workflows/secret-scan.yml - clippy: + rust-quality: + name: Rust quality + needs: [planner] + if: ${{ needs.planner.outputs.rust == 'true' }} runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 - with: - toolchain: stable - components: clippy - - run: cargo xtask clippy - - clippy-strict: - name: Strict Clippy - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 - with: - toolchain: stable - components: clippy - - run: cargo xtask clippy-strict - - panic-surface: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 - with: - toolchain: stable - components: clippy - - run: cargo xtask panic-surface - - clone-audit: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 - with: - toolchain: stable - - run: cargo xtask clone-audit - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 - if: always() - with: - name: j2k-clone-audits - path: | - target/clone-audit/report/jscpd-report.json - target/clone-audit/test-report/jscpd-report.json - if-no-files-found: error - - comparator-parity: - runs-on: ubuntu-latest - env: - J2K_OPENJPEG_BIN: /usr/bin/opj_decompress - J2K_OPENJPEG_DECOMPRESS_BIN: /usr/bin/opj_decompress - J2K_OPENJPEG_COMPRESS_BIN: /usr/bin/opj_compress - J2K_GROK_BIN: /usr/bin/grk_decompress - J2K_GROK_COMPRESS_BIN: /usr/bin/grk_compress - J2K_REQUIRE_OPENJPEG: "1" - J2K_REQUIRE_GROK: "1" - J2K_REQUIRE_LIBJPEG_TURBO: "1" - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 - with: - toolchain: stable - - name: Install comparator tools - run: | - sudo apt-get update - sudo apt-get install -y \ - grokj2k-tools \ - libgrokj2k1-dev \ - libopenjp2-tools \ - libturbojpeg0-dev \ - pkg-config - - name: Verify comparator tools - run: | - command -v opj_compress - command -v opj_decompress - command -v grk_compress - command -v grk_decompress - pkg-config --modversion libgrokj2k - pkg-config --modversion libturbojpeg - - run: cargo xtask j2k-bench-signoff - - semver: - # Published Metal baselines include macOS-gated public API and must be - # rustdoc-built on macOS for cargo-semver-checks. - runs-on: macos-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - with: - fetch-depth: 0 - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 with: toolchain: "1.96" - - run: cargo install cargo-semver-checks --version 0.48.0 --locked - - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 - with: - toolchain: nightly-2026-06-28 - targets: aarch64-apple-darwin - - uses: taiki-e/install-action@7a79fe8c3a13344501c80d99cae481c1c9085912 + components: rustfmt, clippy + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 with: - tool: cargo-public-api@0.52.0 - - run: cargo xtask semver + shared-key: pr-host-${{ runner.os }}-${{ runner.arch }}-1.96 + cache-bin: false + save-if: false + - name: Run fast hosted Rust gates + run: | + cargo xtask fmt + cargo xtask clippy + cargo xtask clippy-strict + cargo xtask test + cargo xtask release-integrity + cargo xtask repo-lint docs: + name: Documentation + needs: [planner] + if: ${{ needs.planner.outputs.docs == 'true' }} runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 with: - toolchain: stable - - run: cargo xtask doc - - stable-api: - runs-on: macos-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 - with: - toolchain: stable - - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 - with: - toolchain: nightly-2026-06-28 - targets: aarch64-apple-darwin - - uses: taiki-e/install-action@7a79fe8c3a13344501c80d99cae481c1c9085912 - with: - tool: cargo-public-api@0.52.0 - - run: cargo public-api --version || cargo install cargo-public-api --version 0.52.0 --locked - - run: cargo xtask stable-api - - codec-math-codegen: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 - with: - toolchain: stable - - run: cargo xtask codec-math-codegen - - public-support-final: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 - with: - toolchain: stable - - run: cargo xtask public-support --final - - machete: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - - uses: taiki-e/install-action@7a79fe8c3a13344501c80d99cae481c1c9085912 - with: - tool: cargo-machete@0.9.2 - - run: cargo xtask machete - - repo-lint: - runs-on: macos-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 - with: - toolchain: stable - - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 - with: - toolchain: nightly-2026-06-28 - targets: aarch64-apple-darwin - - uses: taiki-e/install-action@7a79fe8c3a13344501c80d99cae481c1c9085912 - with: - tool: cargo-public-api@0.52.0 - - run: cargo public-api --version || cargo install cargo-public-api --version 0.52.0 --locked - - name: Run normal and strict repository policy - run: cargo xtask repo-lint --strict - - release-integrity: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 - with: - toolchain: stable - - run: cargo xtask release-integrity - - unsafe-audit: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 - with: - toolchain: stable - - run: cargo xtask unsafe-audit - - typos: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - - uses: crate-ci/typos@d80b8e26878e372a041833cd67163dbdb6a4336e - - test: - strategy: - fail-fast: false - matrix: - include: - - { os: ubuntu-latest, rust: stable, arch: x86_64 } - - { os: ubuntu-latest, rust: beta, arch: x86_64 } - - { os: ubuntu-latest, rust: "1.96", arch: x86_64 } - - { os: ubuntu-24.04-arm, rust: stable, arch: aarch64 } - - { os: ubuntu-24.04-arm, rust: "1.96", arch: aarch64 } - - { os: macos-latest, rust: stable, arch: aarch64 } - - { os: macos-latest, rust: "1.96", arch: aarch64 } - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 - with: - toolchain: ${{ matrix.rust }} - - run: cargo xtask test - - release-cpu: - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + toolchain: "1.96" + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 with: - toolchain: stable - - run: cargo xtask release-cpu + shared-key: pr-docs-${{ runner.os }}-${{ runner.arch }}-1.96 + cache-bin: false + save-if: false + - run: cargo xtask doc metal-compile: name: Metal compile and pure tests + needs: [planner] + if: ${{ needs.planner.outputs.metal_compile == 'true' }} runs-on: macos-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 with: - toolchain: stable + toolchain: "1.96" components: clippy - - run: cargo xtask metal-compile - - no-std: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 with: - toolchain: stable - - run: cargo xtask no-std - - miri: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 - with: - toolchain: nightly - components: miri - - run: cargo xtask miri - - bench-build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 - with: - toolchain: stable - - run: cargo xtask bench-build - - fuzz-build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 - with: - toolchain: stable - - run: cargo xtask fuzz-build - - fuzz-run: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 - with: - toolchain: nightly - - uses: taiki-e/install-action@7a79fe8c3a13344501c80d99cae481c1c9085912 - with: - tool: cargo-fuzz - - run: cargo xtask fuzz-run - env: - J2K_FUZZ_RUNS: "512" - J2K_FUZZ_MAX_TOTAL_TIME_SECONDS: "60" + shared-key: pr-metal-compile-${{ runner.os }}-${{ runner.arch }}-1.96 + cache-bin: false + save-if: false + - run: cargo xtask metal-compile - fuzz-run-long: - if: ${{ github.event_name == 'schedule' }} + gpu-evidence: + name: GPU evidence policy + needs: [planner] + if: ${{ always() && needs.planner.result == 'success' }} runs-on: ubuntu-latest + permissions: + actions: read + contents: read steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 - with: - toolchain: nightly - - uses: taiki-e/install-action@7a79fe8c3a13344501c80d99cae481c1c9085912 - with: - tool: cargo-fuzz - - run: cargo xtask fuzz-run + - name: Require exact-SHA quick GPU validation for affected lanes env: - J2K_FUZZ_RUNS: "20000" - J2K_FUZZ_MAX_TOTAL_TIME_SECONDS: "900" - - package: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 - with: - toolchain: stable - - run: cargo xtask package - - coverage: - runs-on: ubuntu-latest - env: - J2K_COVERAGE_BASE: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event_name == 'push' && github.event.before || 'HEAD^' }} - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - with: - fetch-depth: 0 - - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 - with: - toolchain: stable - - uses: taiki-e/install-action@91534edaf9fd796a162759d80d49cdff574bff2c - with: - tool: cargo-llvm-cov@0.8.7 - - run: cargo xtask coverage host - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 - if: always() - with: - name: j2k-host-coverage - path: | - lcov-host.info - coverage-host-summary.json - coverage-host-regions.json - if-no-files-found: error - - deny: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 - with: - toolchain: stable - - uses: EmbarkStudios/cargo-deny-action@8f84122a46a358a27cb0625d85ad60ab436a1b87 - with: { command: check licenses advisories bans sources } + CUDA_REQUIRED: ${{ needs.planner.outputs.cuda }} + GH_TOKEN: ${{ github.token }} + GITHUB_API_URL: ${{ github.api_url }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + METAL_REQUIRED: ${{ needs.planner.outputs.metal }} + REPOSITORY: ${{ github.repository }} + run: | + if [ "${CUDA_REQUIRED}" = true ] && [ "${METAL_REQUIRED}" = true ]; then + python3 scripts/github_actions_verify.py verify-workflow \ + --repository "${REPOSITORY}" --workflow gpu-validation.yml \ + --sha "${HEAD_SHA}" --event workflow_dispatch \ + --required-job "CUDA quick validation" \ + --required-job "Metal quick validation" + elif [ "${CUDA_REQUIRED}" = true ]; then + python3 scripts/github_actions_verify.py verify-workflow \ + --repository "${REPOSITORY}" --workflow gpu-validation.yml \ + --sha "${HEAD_SHA}" --event workflow_dispatch \ + --required-job "CUDA quick validation" + elif [ "${METAL_REQUIRED}" = true ]; then + python3 scripts/github_actions_verify.py verify-workflow \ + --repository "${REPOSITORY}" --workflow gpu-validation.yml \ + --sha "${HEAD_SHA}" --event workflow_dispatch \ + --required-job "Metal quick validation" + else + echo "No GPU evidence is required for this pull request." + fi - release-candidate: - name: Release candidate aggregate + pr-required-checks: + name: PR required checks if: ${{ always() }} needs: - - gpu-path-policy + - planner - github-actions-verifier - - fmt - diff-check - secret-scan - - clippy - - clippy-strict - - panic-surface - - clone-audit - - comparator-parity - - semver + - rust-quality - docs - - stable-api - - codec-math-codegen - - public-support-final - - machete - - repo-lint - - release-integrity - - unsafe-audit - - typos - - test - - release-cpu - metal-compile - - no-std - - miri - - bench-build - - fuzz-build - - fuzz-run - - package - - coverage - - deny + - gpu-evidence runs-on: ubuntu-latest env: + EXPECTATIONS: ${{ needs.planner.outputs.expectations }} REQUIRED_RESULTS: ${{ toJSON(needs) }} steps: - - name: Require every core and API gate + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - name: Require planned gates and reject unexpected skips run: | - python3 - <<'PY' - import json - import os - import sys - - needs = json.loads(os.environ["REQUIRED_RESULTS"]) - failed = {} - for name, details in needs.items(): - result = details.get("result", "missing") if isinstance(details, dict) else "malformed" - if result != "success": - failed[name] = result - if failed: - for name, result in sorted(failed.items()): - print(f"required gate {name}: {result}", file=sys.stderr) - raise SystemExit(1) - print(f"all {len(needs)} required release-candidate gates succeeded") - PY + python3 scripts/ci_plan.py aggregate \ + --expectations-json "${EXPECTATIONS}" \ + --needs-json "${REQUIRED_RESULTS}" \ + --always-required planner \ + --always-required github-actions-verifier \ + --always-required diff-check \ + --always-required secret-scan \ + --always-required gpu-evidence diff --git a/.github/workflows/full-validation.yml b/.github/workflows/full-validation.yml new file mode 100644 index 00000000..14c03421 --- /dev/null +++ b/.github/workflows/full-validation.yml @@ -0,0 +1,492 @@ +name: Full validation + +on: + push: + branches: [main] + schedule: + - cron: "17 8 * * *" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: full-validation-${{ github.ref }} + cancel-in-progress: ${{ github.event_name != 'workflow_dispatch' }} + +env: + CARGO_TERM_COLOR: always + RUSTFLAGS: "-D warnings" + RUSTDOCFLAGS: "-D warnings" + +jobs: + github-actions-verifier: + name: GitHub Actions verifier tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - run: python3 -m unittest discover -s scripts/tests -p 'test_*.py' -v + + fmt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + with: + toolchain: "1.96" + components: rustfmt + - run: cargo xtask fmt + + diff-check: + runs-on: ubuntu-latest + env: + BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }} + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + fetch-depth: 0 + - name: Reject whitespace errors in the submitted delta + run: | + if [ -n "${BASE_SHA}" ] \ + && [ "${BASE_SHA}" != "0000000000000000000000000000000000000000" ] \ + && git cat-file -e "${BASE_SHA}^{commit}"; then + git diff --check "${BASE_SHA}...${GITHUB_SHA}" + else + git show --check --format=fuller "${GITHUB_SHA}" >/dev/null + fi + + secret-scan: + name: Secret scan + uses: ./.github/workflows/secret-scan.yml + + clippy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + with: + toolchain: stable + components: clippy + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 + with: + shared-key: full-host-${{ runner.os }}-${{ runner.arch }}-stable-clippy + cache-bin: false + save-if: ${{ github.ref == 'refs/heads/main' && github.event_name != 'workflow_dispatch' }} + - run: cargo xtask clippy + + clippy-strict: + name: Strict Clippy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + with: + toolchain: stable + components: clippy + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 + with: + shared-key: full-host-${{ runner.os }}-${{ runner.arch }}-stable-clippy-strict + cache-bin: false + save-if: ${{ github.ref == 'refs/heads/main' && github.event_name != 'workflow_dispatch' }} + - run: cargo xtask clippy-strict + + panic-surface: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + with: + toolchain: stable + components: clippy + - run: cargo xtask panic-surface + + clone-audit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + with: + toolchain: stable + - run: cargo xtask clone-audit + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + if: always() + with: + name: j2k-clone-audits + path: | + target/clone-audit/report/jscpd-report.json + target/clone-audit/test-report/jscpd-report.json + if-no-files-found: error + + comparator-parity: + runs-on: ubuntu-latest + env: + J2K_OPENJPEG_BIN: /usr/bin/opj_decompress + J2K_OPENJPEG_DECOMPRESS_BIN: /usr/bin/opj_decompress + J2K_OPENJPEG_COMPRESS_BIN: /usr/bin/opj_compress + J2K_GROK_BIN: /usr/bin/grk_decompress + J2K_GROK_COMPRESS_BIN: /usr/bin/grk_compress + J2K_REQUIRE_OPENJPEG: "1" + J2K_REQUIRE_GROK: "1" + J2K_REQUIRE_LIBJPEG_TURBO: "1" + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + with: + toolchain: stable + - name: Install comparator tools + run: | + sudo apt-get update + sudo apt-get install -y \ + grokj2k-tools \ + libgrokj2k1-dev \ + libopenjp2-tools \ + libturbojpeg0-dev \ + pkg-config + - name: Verify comparator tools + run: | + command -v opj_compress + command -v opj_decompress + command -v grk_compress + command -v grk_decompress + pkg-config --modversion libgrokj2k + pkg-config --modversion libturbojpeg + - run: cargo xtask j2k-bench-signoff + + semver: + # Published Metal baselines include macOS-gated public API and must be + # rustdoc-built on macOS for cargo-semver-checks. + runs-on: macos-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + fetch-depth: 0 + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + with: + toolchain: "1.96" + - run: cargo install cargo-semver-checks --version 0.48.0 --locked + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + with: + toolchain: nightly-2026-06-28 + targets: aarch64-apple-darwin + - uses: taiki-e/install-action@7a79fe8c3a13344501c80d99cae481c1c9085912 + with: + tool: cargo-public-api@0.52.0 + - run: cargo xtask semver + + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + with: + toolchain: stable + - run: cargo xtask doc + + stable-api: + runs-on: macos-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + with: + toolchain: stable + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + with: + toolchain: nightly-2026-06-28 + targets: aarch64-apple-darwin + - uses: taiki-e/install-action@7a79fe8c3a13344501c80d99cae481c1c9085912 + with: + tool: cargo-public-api@0.52.0 + - run: cargo public-api --version || cargo install cargo-public-api --version 0.52.0 --locked + - run: cargo xtask stable-api + + codec-math-codegen: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + with: + toolchain: stable + - run: cargo xtask codec-math-codegen + + public-support-final: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + with: + toolchain: stable + - run: cargo xtask public-support --final + + machete: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: taiki-e/install-action@7a79fe8c3a13344501c80d99cae481c1c9085912 + with: + tool: cargo-machete@0.9.2 + - run: cargo xtask machete + + repo-lint: + runs-on: macos-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + with: + toolchain: stable + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + with: + toolchain: nightly-2026-06-28 + targets: aarch64-apple-darwin + - uses: taiki-e/install-action@7a79fe8c3a13344501c80d99cae481c1c9085912 + with: + tool: cargo-public-api@0.52.0 + - run: cargo public-api --version || cargo install cargo-public-api --version 0.52.0 --locked + - name: Run normal and strict repository policy + run: cargo xtask repo-lint --strict + + release-integrity: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + with: + toolchain: stable + - run: cargo xtask release-integrity + + unsafe-audit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + with: + toolchain: stable + - run: cargo xtask unsafe-audit + + typos: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: crate-ci/typos@d80b8e26878e372a041833cd67163dbdb6a4336e + + test: + strategy: + fail-fast: false + matrix: + include: + - { os: ubuntu-latest, rust: stable, arch: x86_64 } + - { os: ubuntu-latest, rust: beta, arch: x86_64 } + - { os: ubuntu-latest, rust: "1.96", arch: x86_64 } + - { os: ubuntu-24.04-arm, rust: stable, arch: aarch64 } + - { os: ubuntu-24.04-arm, rust: "1.96", arch: aarch64 } + - { os: macos-latest, rust: stable, arch: aarch64 } + - { os: macos-latest, rust: "1.96", arch: aarch64 } + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + with: + toolchain: ${{ matrix.rust }} + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 + with: + shared-key: full-host-${{ runner.os }}-${{ runner.arch }}-${{ matrix.rust }}-test + cache-bin: false + save-if: ${{ github.ref == 'refs/heads/main' && github.event_name != 'workflow_dispatch' }} + - run: cargo xtask test + + release-cpu: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + with: + toolchain: stable + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 + with: + shared-key: full-host-${{ runner.os }}-${{ runner.arch }}-stable-release-cpu + cache-bin: false + save-if: ${{ github.ref == 'refs/heads/main' && github.event_name != 'workflow_dispatch' }} + - run: cargo xtask release-cpu + + metal-compile: + name: Metal compile and pure tests + runs-on: macos-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + with: + toolchain: stable + components: clippy + - run: cargo xtask metal-compile + + no-std: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + with: + toolchain: stable + - run: cargo xtask no-std + + miri: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + with: + toolchain: nightly + components: miri + - run: cargo xtask miri + + bench-build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + with: + toolchain: stable + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 + with: + shared-key: full-host-${{ runner.os }}-${{ runner.arch }}-stable-bench-build + cache-bin: false + save-if: ${{ github.ref == 'refs/heads/main' && github.event_name != 'workflow_dispatch' }} + - run: cargo xtask bench-build --lane host + + fuzz-build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + with: + toolchain: stable + - run: cargo xtask fuzz-build + + fuzz-run: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + with: + toolchain: nightly + - uses: taiki-e/install-action@7a79fe8c3a13344501c80d99cae481c1c9085912 + with: + tool: cargo-fuzz + - run: cargo xtask fuzz-run + env: + J2K_FUZZ_RUNS: "512" + J2K_FUZZ_MAX_TOTAL_TIME_SECONDS: "60" + + fuzz-run-long: + if: ${{ github.event_name == 'schedule' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + with: + toolchain: nightly + - uses: taiki-e/install-action@7a79fe8c3a13344501c80d99cae481c1c9085912 + with: + tool: cargo-fuzz + - run: cargo xtask fuzz-run + env: + J2K_FUZZ_RUNS: "20000" + J2K_FUZZ_MAX_TOTAL_TIME_SECONDS: "900" + + package: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + with: + toolchain: stable + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 + with: + shared-key: full-host-${{ runner.os }}-${{ runner.arch }}-stable-package + cache-bin: false + save-if: ${{ github.ref == 'refs/heads/main' && github.event_name != 'workflow_dispatch' }} + - run: cargo xtask package + + coverage: + runs-on: ubuntu-latest + env: + J2K_COVERAGE_BASE: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event_name == 'push' && github.event.before || 'HEAD^' }} + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + fetch-depth: 0 + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + with: + toolchain: stable + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 + with: + shared-key: full-host-${{ runner.os }}-${{ runner.arch }}-stable-coverage + cache-bin: false + save-if: ${{ github.ref == 'refs/heads/main' && github.event_name != 'workflow_dispatch' }} + - uses: taiki-e/install-action@91534edaf9fd796a162759d80d49cdff574bff2c + with: + tool: cargo-llvm-cov@0.8.7 + - run: cargo xtask coverage host + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + if: always() + with: + name: j2k-host-coverage + path: | + lcov-host.info + coverage-host-summary.json + coverage-host-regions.json + if-no-files-found: error + + deny: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + with: + toolchain: stable + - uses: EmbarkStudios/cargo-deny-action@8f84122a46a358a27cb0625d85ad60ab436a1b87 + with: { command: check licenses advisories bans sources } + + release-candidate: + name: Release candidate aggregate + if: ${{ always() }} + needs: + - github-actions-verifier + - fmt + - diff-check + - secret-scan + - clippy + - clippy-strict + - panic-surface + - clone-audit + - comparator-parity + - semver + - docs + - stable-api + - codec-math-codegen + - public-support-final + - machete + - repo-lint + - release-integrity + - unsafe-audit + - typos + - test + - release-cpu + - metal-compile + - no-std + - miri + - bench-build + - fuzz-build + - fuzz-run + - package + - coverage + - deny + runs-on: ubuntu-latest + env: + REQUIRED_RESULTS: ${{ toJSON(needs) }} + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - name: Require every core and API gate + run: python3 scripts/ci_plan.py success-only --needs-json "${REQUIRED_RESULTS}" diff --git a/.github/workflows/gpu-benchmarks.yml b/.github/workflows/gpu-benchmarks.yml new file mode 100644 index 00000000..8bc32f73 --- /dev/null +++ b/.github/workflows/gpu-benchmarks.yml @@ -0,0 +1,249 @@ +name: GPU benchmarks + +on: + workflow_dispatch: + inputs: + lane: + description: "Accelerator to benchmark" + type: choice + required: true + options: [cuda, metal] + suite: + description: "Benchmark workload" + type: choice + required: true + default: smoke + options: [smoke, criterion, profile, adoption] + baseline-ref: + description: "Immutable baseline revision for Criterion comparisons" + type: string + required: true + default: "29143c8e1f00bbbe9cf5ab37b3cea882f6d52139" + profile-mode: + description: "Criterion/profile sampling mode" + type: choice + required: true + default: quick + options: [quick, release] + +permissions: + contents: read + +concurrency: + group: gpu-benchmark-${{ inputs.lane }} + cancel-in-progress: false + +env: + CARGO_TERM_COLOR: always + RUSTFLAGS: "-D warnings" + +jobs: + validate-inputs: + name: Validate benchmark request + runs-on: ubuntu-latest + steps: + - name: Require an immutable baseline + env: + BASELINE_REF: ${{ inputs.baseline-ref }} + run: | + if [[ ! "${BASELINE_REF}" =~ ^[0-9a-fA-F]{40}$ ]]; then + echo "baseline must be a full 40-character commit SHA" >&2 + exit 1 + fi + + cuda: + name: CUDA benchmark (${{ inputs.suite }}) + needs: [validate-inputs] + if: ${{ inputs.lane == 'cuda' }} + runs-on: [self-hosted, Linux, X64, cuda] + timeout-minutes: 180 + env: + J2K_REQUIRE_CUDA_BENCH: "1" + J2K_REQUIRE_CUDA_RUNTIME: "1" + J2K_REQUIRE_CUDA_OXIDE_BUILD: "1" + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + fetch-depth: 0 + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + with: + toolchain: "1.96" + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 + with: + shared-key: benchmark-cuda-${{ runner.os }}-${{ runner.arch }}-1.96 + cache-bin: false + save-if: false + - run: bash scripts/configure-cuda-bindgen.sh + - name: Capture stable CUDA device metadata + env: + BASELINE_REF: ${{ inputs.baseline-ref }} + PROFILE_MODE: ${{ inputs.profile-mode }} + SUITE: ${{ inputs.suite }} + run: | + mkdir -p target/gpu-benchmark + nvidia-smi --query-gpu=uuid,name,driver_version --format=csv,noheader \ + | tee target/gpu-benchmark/device-before.txt + nvidia-smi --query-gpu=uuid,pstate,power.draw,power.limit,clocks.sm --format=csv,noheader \ + > target/gpu-benchmark/performance-state-before.txt + uname -a > target/gpu-benchmark/host.txt + rustc -Vv > target/gpu-benchmark/rustc.txt + cargo -V > target/gpu-benchmark/cargo.txt + nvcc --version > target/gpu-benchmark/nvcc.txt + printf 'lane=cuda\nsuite=%s\nbaseline_ref=%s\ncandidate_sha=%s\nprofile_mode=%s\n' \ + "${SUITE}" "${BASELINE_REF}" "${GITHUB_SHA}" "${PROFILE_MODE}" \ + > target/gpu-benchmark/suite-parameters.txt + - name: Compare CUDA Criterion baseline and candidate + if: ${{ inputs.suite == 'smoke' || inputs.suite == 'criterion' }} + env: + BASELINE_REF: ${{ inputs.baseline-ref }} + PROFILE_MODE: ${{ inputs.profile-mode }} + SUITE: ${{ inputs.suite }} + run: | + args=(--lane cuda --baseline-ref "${BASELINE_REF}") + if [ "${SUITE}" = smoke ] || [ "${PROFILE_MODE}" = quick ]; then args+=(--quick); fi + cargo xtask j2k-perf-guard "${args[@]}" + - name: Profile CUDA HTJ2K decode + if: ${{ inputs.suite == 'profile' }} + env: + J2K_CUDA_TRACE: ${{ github.workspace }}/target/gpu-benchmark/cuda-trace.json + J2K_PROFILE_STAGES: summary + PROFILE_MODE: ${{ inputs.profile-mode }} + run: | + args=() + if [ "${PROFILE_MODE}" = quick ]; then args+=(--quick); fi + cargo bench -p j2k-cuda --bench htj2k_decode \ + --features cuda-runtime,cuda-profiling -- "${args[@]}" \ + 2>&1 | tee target/gpu-benchmark/profile.log + - name: Run strict CUDA adoption bundle + if: ${{ inputs.suite == 'adoption' }} + env: + J2K_ADOPTION_FIXTURES: ${{ vars.J2K_ADOPTION_FIXTURES }} + J2K_ADOPTION_MANIFEST: ${{ vars.J2K_ADOPTION_MANIFEST }} + J2K_ADOPTION_ENCODE_FIXTURES: ${{ vars.J2K_ADOPTION_ENCODE_FIXTURES }} + J2K_ADOPTION_ENCODE_MANIFEST: ${{ vars.J2K_ADOPTION_ENCODE_MANIFEST }} + run: | + : "${J2K_ADOPTION_FIXTURES:?Set J2K_ADOPTION_FIXTURES}" + : "${J2K_ADOPTION_MANIFEST:?Set J2K_ADOPTION_MANIFEST}" + : "${J2K_ADOPTION_ENCODE_FIXTURES:?Set J2K_ADOPTION_ENCODE_FIXTURES}" + : "${J2K_ADOPTION_ENCODE_MANIFEST:?Set J2K_ADOPTION_ENCODE_MANIFEST}" + command -v opj_compress + command -v grk_compress + cargo run -p xtask --features adoption -- adoption-benchmark \ + --fixtures "${J2K_ADOPTION_FIXTURES}" \ + --manifest "${J2K_ADOPTION_MANIFEST}" \ + --encode-fixtures "${J2K_ADOPTION_ENCODE_FIXTURES}" \ + --encode-manifest "${J2K_ADOPTION_ENCODE_MANIFEST}" \ + --require-cuda --out-dir target/gpu-benchmark/adoption + - name: Require unchanged CUDA device identity + if: always() + run: | + nvidia-smi --query-gpu=uuid,name,driver_version --format=csv,noheader \ + > target/gpu-benchmark/device-after.txt + nvidia-smi --query-gpu=uuid,pstate,power.draw,power.limit,clocks.sm --format=csv,noheader \ + > target/gpu-benchmark/performance-state-after.txt + cmp target/gpu-benchmark/device-before.txt target/gpu-benchmark/device-after.txt + - name: Upload CUDA benchmark evidence + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: cuda-${{ inputs.suite }}-${{ github.sha }} + path: | + target/gpu-benchmark + target/j2k-perf/cuda + target/criterion + if-no-files-found: warn + + metal: + name: Metal benchmark (${{ inputs.suite }}) + needs: [validate-inputs] + if: ${{ inputs.lane == 'metal' }} + runs-on: [self-hosted, macOS, ARM64, metal] + timeout-minutes: 180 + env: + J2K_REQUIRE_METAL_BENCH: "1" + J2K_REQUIRE_METAL_RUNTIME: "1" + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + fetch-depth: 0 + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + with: + toolchain: "1.96" + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 + with: + shared-key: benchmark-metal-${{ runner.os }}-${{ runner.arch }}-1.96 + cache-bin: false + save-if: false + - name: Capture stable Metal device metadata + env: + BASELINE_REF: ${{ inputs.baseline-ref }} + PROFILE_MODE: ${{ inputs.profile-mode }} + SUITE: ${{ inputs.suite }} + run: | + mkdir -p target/gpu-benchmark + system_profiler SPDisplaysDataType -json \ + | tee target/gpu-benchmark/device-before.json + pmset -g therm > target/gpu-benchmark/performance-state-before.txt || true + uname -a > target/gpu-benchmark/host.txt + sw_vers > target/gpu-benchmark/macos.txt + xcrun metal --version > target/gpu-benchmark/metal-compiler.txt + rustc -Vv > target/gpu-benchmark/rustc.txt + cargo -V > target/gpu-benchmark/cargo.txt + printf 'lane=metal\nsuite=%s\nbaseline_ref=%s\ncandidate_sha=%s\nprofile_mode=%s\n' \ + "${SUITE}" "${BASELINE_REF}" "${GITHUB_SHA}" "${PROFILE_MODE}" \ + > target/gpu-benchmark/suite-parameters.txt + - name: Compare Metal Criterion baseline and candidate + if: ${{ inputs.suite == 'smoke' || inputs.suite == 'criterion' }} + env: + BASELINE_REF: ${{ inputs.baseline-ref }} + PROFILE_MODE: ${{ inputs.profile-mode }} + SUITE: ${{ inputs.suite }} + run: | + args=(--lane metal --baseline-ref "${BASELINE_REF}") + if [ "${SUITE}" = smoke ] || [ "${PROFILE_MODE}" = quick ]; then args+=(--quick); fi + cargo xtask j2k-perf-guard "${args[@]}" + - name: Profile Metal Criterion routes + if: ${{ inputs.suite == 'profile' }} + env: + PROFILE_MODE: ${{ inputs.profile-mode }} + run: | + args=() + if [ "${PROFILE_MODE}" = quick ]; then args+=(--quick); fi + cargo bench -p j2k-jpeg-metal --bench compare -- "${args[@]}" \ + 2>&1 | tee target/gpu-benchmark/profile.log + - name: Run strict Metal adoption bundle + if: ${{ inputs.suite == 'adoption' }} + env: + J2K_ADOPTION_FIXTURES: ${{ vars.J2K_ADOPTION_FIXTURES }} + J2K_ADOPTION_MANIFEST: ${{ vars.J2K_ADOPTION_MANIFEST }} + J2K_ADOPTION_ENCODE_FIXTURES: ${{ vars.J2K_ADOPTION_ENCODE_FIXTURES }} + J2K_ADOPTION_ENCODE_MANIFEST: ${{ vars.J2K_ADOPTION_ENCODE_MANIFEST }} + run: | + : "${J2K_ADOPTION_FIXTURES:?Set J2K_ADOPTION_FIXTURES}" + : "${J2K_ADOPTION_MANIFEST:?Set J2K_ADOPTION_MANIFEST}" + : "${J2K_ADOPTION_ENCODE_FIXTURES:?Set J2K_ADOPTION_ENCODE_FIXTURES}" + : "${J2K_ADOPTION_ENCODE_MANIFEST:?Set J2K_ADOPTION_ENCODE_MANIFEST}" + command -v opj_compress + command -v grk_compress + cargo run -p xtask --features adoption -- adoption-benchmark \ + --fixtures "${J2K_ADOPTION_FIXTURES}" \ + --manifest "${J2K_ADOPTION_MANIFEST}" \ + --encode-fixtures "${J2K_ADOPTION_ENCODE_FIXTURES}" \ + --encode-manifest "${J2K_ADOPTION_ENCODE_MANIFEST}" \ + --require-metal --out-dir target/gpu-benchmark/adoption + - name: Require unchanged Metal device identity + if: always() + run: | + system_profiler SPDisplaysDataType -json > target/gpu-benchmark/device-after.json + pmset -g therm > target/gpu-benchmark/performance-state-after.txt || true + cmp target/gpu-benchmark/device-before.json target/gpu-benchmark/device-after.json + - name: Upload Metal benchmark evidence + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: metal-${{ inputs.suite }}-${{ github.sha }} + path: | + target/gpu-benchmark + target/j2k-perf/metal + target/criterion + if-no-files-found: warn diff --git a/.github/workflows/gpu-validation.yml b/.github/workflows/gpu-validation.yml index 799241ff..a47af49d 100644 --- a/.github/workflows/gpu-validation.yml +++ b/.github/workflows/gpu-validation.yml @@ -3,36 +3,20 @@ name: GPU validation on: workflow_dispatch: inputs: - run-timed-benchmarks: - description: "Run Criterion timing benches on the GPU runners" - type: boolean - required: false - default: false - run-metal-validation: - description: "Run Apple Silicon Metal validation jobs" - type: boolean - required: false - # Default off: with no Metal runner online the job sits queued forever, - # which holds the whole run "in progress" and locks every job's logs - # (including the CUDA gate). Opt in explicitly when a Metal runner is up. - default: false - run-linux-ci: - description: "Run the Linux CI gate on the self-hosted CUDA runner" - type: boolean - required: false - default: false - run-cuda-htj2k-decode-profile: - description: "Collect CUDA HTJ2K decode RCA/profile log, trace, and Criterion artifacts" - type: boolean - required: false - default: false - run-adoption-benchmark: - description: "Run the fixture-backed adoption benchmark bundle on the CUDA runner" - type: boolean - required: false - default: false + target: + description: "Accelerator lane to validate" + type: choice + required: true + default: cuda + options: [cuda, metal, all] + mode: + description: "Quick validates selected lanes; full is release-grade and requires all" + type: choice + required: true + default: quick + options: [quick, full] coverage-base-ref: - description: "Git revision used as the changed-line coverage base" + description: "Git revision used as the changed-line coverage base in full mode" type: string required: false default: "v0.6.2" @@ -40,47 +24,89 @@ on: permissions: contents: read +concurrency: + group: gpu-validation-${{ github.sha }}-${{ inputs.target }}-${{ inputs.mode }} + cancel-in-progress: ${{ inputs.mode == 'quick' }} + env: CARGO_TERM_COLOR: always J2K_COVERAGE_BASE: ${{ inputs.coverage-base-ref }} RUSTFLAGS: "-D warnings" jobs: - linux-ci: - if: ${{ inputs.run-linux-ci }} - name: Linux CI on self-hosted CUDA runner + validate-inputs: + name: Validate GPU request + runs-on: ubuntu-latest + steps: + - name: Reject unsafe or contradictory requests + env: + MODE: ${{ inputs.mode }} + TARGET: ${{ inputs.target }} + run: | + if [ "${MODE}" = full ] && [ "${TARGET}" != all ]; then + echo "full GPU validation requires target=all" >&2 + exit 1 + fi + + cuda-quick: + name: CUDA quick validation + needs: [validate-inputs] + if: ${{ inputs.mode == 'quick' && (inputs.target == 'cuda' || inputs.target == 'all') }} runs-on: [self-hosted, Linux, X64, cuda] + # Quick timing is telemetry until representative warm-cache runs establish a budget. + timeout-minutes: 90 + env: + J2K_REQUIRE_CUDA_RUNTIME: "1" + J2K_REQUIRE_CUDA_OXIDE_BUILD: "1" + J2K_REQUIRE_CUDA_JPEG_HARDWARE_DECODE: "1" + RUST_TEST_THREADS: "1" steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: fetch-depth: 0 - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 with: - toolchain: stable - components: rustfmt, clippy - - name: Show runner diagnostics + toolchain: "1.96" + - name: Fingerprint CUDA cache environment + run: | + { + nvidia-smi --query-gpu=driver_version --format=csv,noheader + nvcc --version + } | sha256sum | awk '{print "J2K_GPU_CACHE_ENV=cuda-" $1}' \ + >> "${GITHUB_ENV}" + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 + with: + shared-key: gpu-cuda-${{ runner.os }}-${{ runner.arch }}-1.96 + key: profiles-gpu-quick-release-v1 + env-vars: "CARGO CC CFLAGS CXX CMAKE RUST J2K_GPU_CACHE_ENV" + cache-bin: false + save-if: false + - name: Show CUDA runner diagnostics run: | uname -a rustc -Vv cargo -V - - run: cargo xtask ci - - run: cargo xtask no-std - - run: cargo xtask bench-build - - run: cargo xtask fuzz-build - - name: Install CI helper tools + nvidia-smi + command -v nvcc + nvcc --version + - name: Configure CUDA Oxide bindgen + run: bash scripts/configure-cuda-bindgen.sh + - name: Run fail-closed quick CUDA validation run: | - cargo install typos-cli --version 1.42.3 --locked - cargo install cargo-deny --locked - - name: Ensure pinned coverage tool - run: scripts/ensure-cargo-llvm-cov.sh - - run: cargo xtask typos - - run: cargo xtask deny - - run: cargo xtask coverage host + started="$(date +%s)" + status=0 + cargo xtask release-cuda --mode quick || status=$? + elapsed="$(( $(date +%s) - started ))" + echo "CUDA quick wall time: ${elapsed}s" | tee -a "${GITHUB_STEP_SUMMARY}" + exit "${status}" - metal-apple-silicon: - if: ${{ inputs.run-metal-validation }} - name: Metal validation on Apple Silicon + metal-quick: + name: Metal quick validation + needs: [validate-inputs] + if: ${{ inputs.mode == 'quick' && (inputs.target == 'metal' || inputs.target == 'all') }} runs-on: [self-hosted, macOS, ARM64, metal] + # Keep the 15-minute target informational until runner variance is measured. + timeout-minutes: 60 env: J2K_REQUIRE_METAL_RUNTIME: "1" RUST_TEST_THREADS: "1" @@ -90,41 +116,44 @@ jobs: fetch-depth: 0 - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 with: - toolchain: stable - - name: Ensure pinned coverage tool - run: scripts/ensure-cargo-llvm-cov.sh - - name: Run fail-closed Metal release validation - run: cargo xtask release-metal - - name: Collect changed-line Metal host coverage - run: cargo xtask coverage metal - - name: Upload Metal coverage evidence - if: always() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + toolchain: "1.96" + components: clippy + - name: Fingerprint Metal cache environment + run: | + { + sw_vers + xcrun metal --version + } | shasum -a 256 | awk '{print "J2K_GPU_CACHE_ENV=metal-" $1}' \ + >> "${GITHUB_ENV}" + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 with: - name: j2k-metal-coverage - path: | - lcov-metal.info - coverage-metal-summary.json - coverage-metal-regions.json - if-no-files-found: error - - run: cargo bench -p j2k-jpeg-metal --no-run - - if: ${{ inputs.run-timed-benchmarks }} - env: - J2K_GPU_BENCH_DIM: "1024" - J2K_GPU_BENCH_BATCH: "64" - J2K_GPU_BENCH_BATCH_DIM: "1024" - run: cargo bench -p j2k-jpeg-metal --bench device_upload -- --noplot --sample-size 10 --warm-up-time 1 --measurement-time 2 - - if: ${{ inputs.run-timed-benchmarks }} - run: cargo bench -p j2k-jpeg-metal --bench compare -- --noplot --sample-size 10 --warm-up-time 1 --measurement-time 2 + shared-key: gpu-metal-${{ runner.os }}-${{ runner.arch }}-1.96 + key: profiles-gpu-quick-release-v1 + env-vars: "CARGO CC CFLAGS CXX CMAKE RUST J2K_GPU_CACHE_ENV" + cache-bin: false + save-if: false + - name: Show Metal runner diagnostics + run: | + uname -a + system_profiler SPDisplaysDataType + rustc -Vv + cargo -V + - name: Run fail-closed quick Metal validation + run: | + started="$(date +%s)" + status=0 + cargo xtask release-metal --mode quick || status=$? + elapsed="$(( $(date +%s) - started ))" + echo "Metal quick wall time: ${elapsed}s (telemetry target: <=900s)" \ + | tee -a "${GITHUB_STEP_SUMMARY}" + exit "${status}" - # cuda-x86_64-compatibility is THE authoritative CUDA parity gate. - # It must be run (via workflow_dispatch) before merging any change to the - # CUDA encode path (crates/j2k-cuda, crates/j2k-cuda-runtime). - # The job is workflow_dispatch-only; do not add push/pull_request triggers - # without a policy decision — these are cost-sensitive self-hosted GPU runners. - cuda-x86_64-compatibility: - name: CUDA API compatibility on x86_64 + cuda-full: + name: CUDA full release validation + needs: [validate-inputs] + if: ${{ inputs.mode == 'full' && inputs.target == 'all' }} runs-on: [self-hosted, Linux, X64, cuda] + timeout-minutes: 90 env: J2K_REQUIRE_CUDA_RUNTIME: "1" J2K_REQUIRE_CUDA_OXIDE_BUILD: "1" @@ -136,284 +165,115 @@ jobs: fetch-depth: 0 - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 with: - toolchain: stable - - name: Ensure pinned coverage tool - run: scripts/ensure-cargo-llvm-cov.sh - - name: Assert CUDA runtime gate is set + toolchain: "1.96" + components: clippy + - name: Fingerprint CUDA cache environment run: | - test -n "${J2K_REQUIRE_CUDA_RUNTIME:-}" || { echo "J2K_REQUIRE_CUDA_RUNTIME not set — parity tests would silently skip"; exit 1; } + { + nvidia-smi --query-gpu=driver_version --format=csv,noheader + nvcc --version + } | sha256sum | awk '{print "J2K_GPU_CACHE_ENV=cuda-" $1}' \ + >> "${GITHUB_ENV}" + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 + with: + shared-key: gpu-cuda-${{ runner.os }}-${{ runner.arch }}-1.96 + key: profiles-gpu-quick-release-v1 + env-vars: "CARGO CC CFLAGS CXX CMAKE RUST J2K_GPU_CACHE_ENV" + cache-bin: false + save-if: ${{ github.ref == 'refs/heads/main' }} - name: Show CUDA runner diagnostics run: | uname -a rustc -Vv cargo -V - if command -v nvidia-smi >/dev/null 2>&1; then - nvidia-smi - else - echo "nvidia-smi not found; CUDA runtime validation requires a working CUDA driver" - fi + nvidia-smi command -v nvcc nvcc --version - name: Configure CUDA Oxide bindgen + run: bash scripts/configure-cuda-bindgen.sh + - name: Run fail-closed full CUDA validation run: | - if [ -z "${LIBCLANG_PATH:-}" ]; then - libclang_dir="" - for dir in /usr/lib/llvm-*/lib "${HOME}"/.local/llvm*/usr/lib/llvm-*/lib; do - if compgen -G "${dir}/libclang*.so*" >/dev/null; then - libclang_dir="${dir}" - fi - done - test -n "${libclang_dir}" || { echo "libclang not found; CUDA Oxide bindgen cannot run"; exit 1; } - echo "LIBCLANG_PATH=${libclang_dir}" >> "${GITHUB_ENV}" - fi - - resource_dir="" - for dir in /usr/lib/llvm-*/lib/clang/[0-9]* "${HOME}"/.local/llvm*/usr/lib/llvm-*/lib/clang/[0-9]*; do - if [ -d "${dir}/include" ]; then - resource_dir="${dir}" - fi - done - - bindgen_args="${BINDGEN_EXTRA_CLANG_ARGS:-}" - if [ -n "${resource_dir}" ]; then - bindgen_args="${bindgen_args:+${bindgen_args} }-resource-dir=${resource_dir}" - fi - gcc_include="$(gcc -print-file-name=include)" - if [ -d "${gcc_include}" ]; then - bindgen_args="${bindgen_args:+${bindgen_args} }-I${gcc_include}" - fi - if [ -d /usr/local/cuda/include ]; then - bindgen_args="${bindgen_args:+${bindgen_args} }-I/usr/local/cuda/include" - fi - echo "BINDGEN_EXTRA_CLANG_ARGS=${bindgen_args}" >> "${GITHUB_ENV}" - - name: Run fail-closed CUDA release validation - run: cargo xtask release-cuda + started="$(date +%s)" + status=0 + cargo xtask release-cuda --mode full || status=$? + elapsed="$(( $(date +%s) - started ))" + echo "CUDA full wall time: ${elapsed}s" | tee -a "${GITHUB_STEP_SUMMARY}" + exit "${status}" + - name: Ensure pinned coverage tool + run: scripts/ensure-cargo-llvm-cov.sh - name: Collect changed-line CUDA host coverage run: cargo xtask coverage cuda + - name: Compile CUDA benchmark lane + run: cargo xtask bench-build --lane cuda - name: Upload CUDA coverage evidence if: always() uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: - name: j2k-cuda-coverage + name: j2k-cuda-full-coverage-${{ github.sha }} path: | lcov-cuda.info coverage-cuda-summary.json coverage-cuda-regions.json if-no-files-found: error - - run: cargo bench -p j2k-jpeg-cuda --bench device_decode --features cuda-runtime --no-run - - run: cargo bench -p j2k-cuda --bench encode_stages --features cuda-runtime --no-run - - run: cargo bench -p j2k-cuda --bench htj2k_decode --features cuda-runtime --no-run - - run: cargo bench -p j2k-cuda --bench htj2k_encode --features cuda-runtime --no-run - - if: ${{ inputs.run-timed-benchmarks }} - env: - J2K_REQUIRE_CUDA_BENCH: "1" - J2K_GPU_BENCH_DIM: "4096" - J2K_GPU_BENCH_BATCH: "64" - J2K_GPU_BENCH_BATCH_DIM: "1024" - run: cargo bench -p j2k-jpeg-cuda --bench device_decode --features cuda-runtime -- --noplot --sample-size 10 --warm-up-time 1 --measurement-time 2 - - if: ${{ inputs.run-timed-benchmarks }} - env: - J2K_REQUIRE_CUDA_BENCH: "1" - run: cargo bench -p j2k-cuda --bench encode_stages --features cuda-runtime -- --noplot --sample-size 10 --warm-up-time 1 --measurement-time 2 - - if: ${{ inputs.run-timed-benchmarks }} - env: - J2K_REQUIRE_CUDA_BENCH: "1" - run: cargo bench -p j2k-cuda --bench htj2k_decode --features cuda-runtime -- --noplot --sample-size 10 --warm-up-time 1 --measurement-time 2 - - name: CUDA HTJ2K decode RCA profile - if: ${{ inputs.run-cuda-htj2k-decode-profile }} - env: - J2K_REQUIRE_CUDA_RUNTIME: "1" - J2K_REQUIRE_CUDA_OXIDE_BUILD: "1" - J2K_REQUIRE_CUDA_BENCH: "1" - J2K_PROFILE_STAGES: summary - J2K_CUDA_TRACE: ${{ github.workspace }}/target/cuda_htj2k_decode_trace.json + + metal-full: + name: Metal full release validation + needs: [validate-inputs] + if: ${{ inputs.mode == 'full' && inputs.target == 'all' }} + runs-on: [self-hosted, macOS, ARM64, metal] + timeout-minutes: 60 + env: + J2K_REQUIRE_METAL_RUNTIME: "1" + RUST_TEST_THREADS: "1" + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + fetch-depth: 0 + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + with: + toolchain: "1.96" + components: clippy + - name: Fingerprint Metal cache environment run: | - set -o pipefail - mkdir -p target - samply_status_file=target/cuda_htj2k_decode_samply_status.txt - bench_cmd=( - cargo bench -p j2k-cuda --bench htj2k_decode --features cuda-runtime,cuda-profiling -- - --noplot --sample-size 10 --warm-up-time 1 --measurement-time 2 - ) - if ! command -v samply >/dev/null 2>&1; then - RUSTFLAGS="" cargo install samply --version 0.13.1 --locked - fi - samply --version - run_samply=1 - if [ -r /proc/sys/kernel/perf_event_paranoid ]; then - current_perf_event_paranoid="$(cat /proc/sys/kernel/perf_event_paranoid)" - echo "perf_event_paranoid=${current_perf_event_paranoid}" - if [ "${current_perf_event_paranoid}" -gt 1 ] 2>/dev/null; then - perf_lowered=0 - if [ -w /proc/sys/kernel/perf_event_paranoid ]; then - if echo 1 > /proc/sys/kernel/perf_event_paranoid; then - perf_lowered=1 - fi - fi - if [ "${perf_lowered}" -ne 1 ] && command -v sudo >/dev/null 2>&1; then - if echo 1 | sudo -n tee /proc/sys/kernel/perf_event_paranoid >/dev/null; then - perf_lowered=1 - else - echo "passwordless sudo could not lower perf_event_paranoid for samply" - fi - fi - if [ "${perf_lowered}" -eq 1 ]; then - echo "perf_event_paranoid=$(cat /proc/sys/kernel/perf_event_paranoid)" - else - run_samply=0 - { - echo "samply_status=blocked" - echo "perf_event_paranoid=${current_perf_event_paranoid}" - echo "reason=passwordless sudo unavailable and /proc/sys/kernel/perf_event_paranoid is not writable" - echo "fallback=cargo bench with J2K_PROFILE_STAGES and J2K_CUDA_TRACE" - } | tee "${samply_status_file}" - fi - fi - fi - if [ "${run_samply}" -eq 1 ]; then - echo "samply_status=attempting" | tee "${samply_status_file}" - samply record --save-only -o target/cuda_htj2k_decode_samply.json.gz -- \ - "${bench_cmd[@]}" \ - 2>&1 | tee target/cuda_htj2k_decode_profile.log - echo "samply_status=recorded" > "${samply_status_file}" - else - "${bench_cmd[@]}" 2>&1 | tee target/cuda_htj2k_decode_profile.log - fi - - name: Upload CUDA HTJ2K decode RCA profile artifacts - if: ${{ inputs.run-cuda-htj2k-decode-profile && always() }} - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + { + sw_vers + xcrun metal --version + } | shasum -a 256 | awk '{print "J2K_GPU_CACHE_ENV=metal-" $1}' \ + >> "${GITHUB_ENV}" + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 with: - name: cuda-htj2k-decode-rca-profile - path: | - target/cuda_htj2k_decode_profile.log - target/cuda_htj2k_decode_trace.json - target/cuda_htj2k_decode_samply.json.gz - target/cuda_htj2k_decode_samply_status.txt - target/criterion - if-no-files-found: warn - - if: ${{ inputs.run-timed-benchmarks }} - env: - J2K_REQUIRE_CUDA_BENCH: "1" - run: cargo bench -p j2k-cuda --bench htj2k_encode --features cuda-runtime -- --noplot --sample-size 10 --warm-up-time 1 --measurement-time 2 - - name: CUDA adoption benchmark bundle - if: ${{ inputs.run-adoption-benchmark }} - env: - J2K_ADOPTION_FIXTURES: ${{ vars.J2K_ADOPTION_FIXTURES }} - J2K_ADOPTION_MANIFEST: ${{ vars.J2K_ADOPTION_MANIFEST }} - J2K_ADOPTION_ENCODE_FIXTURES: ${{ vars.J2K_ADOPTION_ENCODE_FIXTURES }} - J2K_ADOPTION_ENCODE_MANIFEST: ${{ vars.J2K_ADOPTION_ENCODE_MANIFEST }} - J2K_ADOPTION_OUT_DIR: target/j2k-adoption-benchmark/cuda-full + shared-key: gpu-metal-${{ runner.os }}-${{ runner.arch }}-1.96 + key: profiles-gpu-quick-release-v1 + env-vars: "CARGO CC CFLAGS CXX CMAKE RUST J2K_GPU_CACHE_ENV" + cache-bin: false + save-if: ${{ github.ref == 'refs/heads/main' }} + - name: Show Metal runner diagnostics run: | - set -euo pipefail - command -v opj_compress >/dev/null - command -v grk_compress >/dev/null - if [ -n "${J2K_ADOPTION_FIXTURES:-}${J2K_ADOPTION_MANIFEST:-}${J2K_ADOPTION_ENCODE_FIXTURES:-}${J2K_ADOPTION_ENCODE_MANIFEST:-}" ]; then - : "${J2K_ADOPTION_FIXTURES:?Set repository variable J2K_ADOPTION_FIXTURES to the pinned decode fixture directory list}" - : "${J2K_ADOPTION_MANIFEST:?Set repository variable J2K_ADOPTION_MANIFEST to the pinned decode manifest TSV}" - : "${J2K_ADOPTION_ENCODE_FIXTURES:?Set repository variable J2K_ADOPTION_ENCODE_FIXTURES to the pinned staged PNM encode fixture directory list}" - : "${J2K_ADOPTION_ENCODE_MANIFEST:?Set repository variable J2K_ADOPTION_ENCODE_MANIFEST to the pinned encode manifest TSV}" - else - echo "No adoption corpus repository variables set; building the default public starter corpus" - mkdir -p target/j2k-public-corpora/kodak - for n in $(seq -w 1 24); do - kodak="target/j2k-public-corpora/kodak/kodim${n}.png" - if [ ! -f "${kodak}" ]; then - curl -L --fail \ - "https://r0k.us/graphics/kodak/kodak/kodim${n}.png" \ - -o "${kodak}" - fi - done - ( - cd target/j2k-public-corpora/kodak - sha256sum -c - <<'SHA256' - a56e27cbf5f843c048b6af1d6e090760e9c92fadba88b7dee0205918a37523bd kodim01.png - 4f4b74a79237e311d72cad958237b5f7088d8bce1c82305ebefe1a70e3022dfd kodim02.png - e25ca1ff2f0c0cb5fdfd5f9b0a0bb21ac4c3de3c84a67f35b09a85d3306249db kodim03.png - e3b946107c5d3441c022f678d0c3caf1e224d81b1604ba840a4f88e562de61aa kodim04.png - 10349e963c5c813d327852f82c1795fa4148d69fedffc4c589bee458e3ac3d53 kodim05.png - 363510303b715d4cbc384e1ce227e466b613a09e1b71ae985882bf8e7fbd9b18 kodim06.png - b77d3f006f42414bb242222e0482e750c0fb9e5ee8d4bed2f6f11c5605fe54a4 kodim07.png - ba23983c76b4832ee0e8af0592664756841a16779acd69f792e268fb6d13d6e7 kodim08.png - 6a4361c2fc194feb4edaa9f9a4a0620fb9943e460ac7fdf037fb0f6dd6607a7d kodim09.png - 9dfb70f5867c29ff9ed6313683f19b3d867849e40fbc0c4c54a4a89df341cf23 kodim10.png - 7936814b58b5387fce2e4e2488b4ec830dadd95fa9520f358ddb30990b50f2b6 kodim11.png - d78c37c2f04f23761ed2367dd77e2db584ddd4c3950833fecf89f199a8126980 kodim12.png - bc34a3ce58dea09dce1704c997171602de90cb34d0c8503a988b77f473d39b08 kodim13.png - 55a94550ff18f3246c4074fd32b77b0c74447c26b6ad274d564d999c0450ba6e kodim14.png - 7538cbb80cb9103606c48b806eae57d56c885c7f90b9b3be70a41160f9cbb683 kodim15.png - a89c7268ccd4718ba424a99fc4643c572cf692ca6eae887185ceb4e9f11d2e54 kodim16.png - 37afcc89fbdcb76d9518e04b2fc011027e2f4cd14b3b2f83cefd721641a47c5b kodim17.png - 1a9258c365988961d87a0598725b609139c303ad48a5aad6c503c3b1a87849aa kodim18.png - b7450b264b1b0a411390d8931b112c27905a992520fc90569dc4b920aa32bbdc kodim19.png - 3b46c71e3b92a563820ba32936be8330c586c41f938efd94be938386aae4328a kodim20.png - ac958597c82073f6bb65129c68f72b651db5b9efd82e11547d07350214bc268b kodim21.png - 1cee58eb1f2d9c7ebb254d208a03c783ce6cf2c4d8c2cf45e235dd23b4ce1b29 kodim22.png - e3111a2fd4da24af15d6459ef9eacfe54106b38e27b4a21821b75c3f5d2d5baf kodim23.png - 1071c68372cc5a01435c2c225a5cf7d4bb803846ec08bb6b3d6721b156d7cb96 kodim24.png - SHA256 - ) - cargo run -p xtask --features adoption -- adoption-materialize \ - --encode-fixtures target/j2k-public-corpora/kodak \ - --source-command "downloaded-from-r0k-us-kodak-lossless-true-color-sha256-pinned" \ - --license-status redistributable \ - --corpus-name kodak \ - --corpus-category natural-image \ - --out-dir target/j2k-public-corpora/materialized-kodak - readonly J2K_STARTER_OPENJPEG_DATA_COMMIT=39524bd3a601d90ed8e0177559400d23945f96a9 - if [ ! -d target/j2k-public-corpora/openjpeg-data/.git ]; then - mkdir -p target/j2k-public-corpora/openjpeg-data - git -C target/j2k-public-corpora/openjpeg-data init - fi - git -C target/j2k-public-corpora/openjpeg-data remote get-url origin >/dev/null 2>&1 \ - && git -C target/j2k-public-corpora/openjpeg-data remote set-url origin https://github.com/uclouvain/openjpeg-data \ - || git -C target/j2k-public-corpora/openjpeg-data remote add origin https://github.com/uclouvain/openjpeg-data - git -C target/j2k-public-corpora/openjpeg-data fetch --depth 1 origin "${J2K_STARTER_OPENJPEG_DATA_COMMIT}" - git -C target/j2k-public-corpora/openjpeg-data checkout --detach "${J2K_STARTER_OPENJPEG_DATA_COMMIT}" - test "$(git -C target/j2k-public-corpora/openjpeg-data rev-parse HEAD)" = "${J2K_STARTER_OPENJPEG_DATA_COMMIT}" - cargo run -p xtask --features adoption -- adoption-curate \ - --fixtures target/j2k-public-corpora/openjpeg-data/input/conformance \ - --encode-command "source-native-openjpeg-data-conformance-dir@39524bd3a601d90ed8e0177559400d23945f96a9" \ - --license-status permissive-test-fixture \ - --corpus-name openjpeg-data-conformance \ - --corpus-category conformance \ - --out-dir target/j2k-public-corpora/openjpeg-conformance-curated - cargo run -p xtask --features adoption -- adoption-curate \ - --fixtures target/j2k-public-corpora/openjpeg-data/input/nonregression \ - --encode-command "source-native-openjpeg-data-nonregression-dir@39524bd3a601d90ed8e0177559400d23945f96a9" \ - --license-status permissive-test-fixture \ - --corpus-name openjpeg-data \ - --corpus-category interop \ - --max-files 24 \ - --out-dir target/j2k-public-corpora/openjpeg-interop-curated - mkdir -p target/j2k-public-corpora/combined-adoption - awk 'FNR == 1 && NR != 1 { next } { print }' \ - target/j2k-public-corpora/materialized-kodak/fixtures.tsv \ - target/j2k-public-corpora/openjpeg-conformance-curated/fixtures.tsv \ - target/j2k-public-corpora/openjpeg-interop-curated/fixtures.tsv \ - > target/j2k-public-corpora/combined-adoption/fixtures.tsv - cp target/j2k-public-corpora/materialized-kodak/encode-fixtures.tsv \ - target/j2k-public-corpora/combined-adoption/encode-fixtures.tsv - J2K_ADOPTION_FIXTURES="target/j2k-public-corpora/materialized-kodak/decode-fixtures:target/j2k-public-corpora/openjpeg-conformance-curated/fixtures:target/j2k-public-corpora/openjpeg-interop-curated/fixtures" - J2K_ADOPTION_MANIFEST="target/j2k-public-corpora/combined-adoption/fixtures.tsv" - J2K_ADOPTION_ENCODE_FIXTURES="target/j2k-public-corpora/materialized-kodak/staged-pnm" - J2K_ADOPTION_ENCODE_MANIFEST="target/j2k-public-corpora/combined-adoption/encode-fixtures.tsv" - fi - cargo run -p xtask --features adoption -- adoption-benchmark \ - --fixtures "${J2K_ADOPTION_FIXTURES}" \ - --manifest "${J2K_ADOPTION_MANIFEST}" \ - --encode-fixtures "${J2K_ADOPTION_ENCODE_FIXTURES}" \ - --encode-manifest "${J2K_ADOPTION_ENCODE_MANIFEST}" \ - --cuda \ - --require-cuda \ - --cuda-decode-batch-sizes 1,16,256,1024 \ - --out-dir "${J2K_ADOPTION_OUT_DIR}" - cargo run -p xtask --features adoption -- adoption-report \ - --run-dir "${J2K_ADOPTION_OUT_DIR}" \ - --out "${J2K_ADOPTION_OUT_DIR}/adoption-report.md" - - name: Upload CUDA adoption benchmark artifacts - if: ${{ inputs.run-adoption-benchmark && always() }} + uname -a + system_profiler SPDisplaysDataType + rustc -Vv + cargo -V + - name: Run fail-closed full Metal validation + run: | + started="$(date +%s)" + status=0 + cargo xtask release-metal --mode full || status=$? + elapsed="$(( $(date +%s) - started ))" + echo "Metal full wall time: ${elapsed}s" | tee -a "${GITHUB_STEP_SUMMARY}" + exit "${status}" + - name: Ensure pinned coverage tool + run: scripts/ensure-cargo-llvm-cov.sh + - name: Collect changed-line Metal host coverage + run: cargo xtask coverage metal + - name: Compile Metal benchmark lane + run: cargo xtask bench-build --lane metal + - name: Upload Metal coverage evidence + if: always() uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: - name: cuda-adoption-benchmark - path: target/j2k-adoption-benchmark/cuda-full - if-no-files-found: warn + name: j2k-metal-full-coverage-${{ github.sha }} + path: | + lcov-metal.info + coverage-metal-summary.json + coverage-metal-regions.json + if-no-files-found: error diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 949e3e9e..f1de09a8 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -51,12 +51,12 @@ jobs: --server-url "${GITHUB_SERVER_URL}" \ --tag "${RELEASE_TAG}" \ --candidate-sha "${candidate_sha}" \ - --ci-workflow ci.yml \ + --ci-workflow full-validation.yml \ --ci-branch main \ --aggregate-job "Release candidate aggregate" \ --gpu-workflow gpu-validation.yml \ - --cuda-job "CUDA API compatibility on x86_64" \ - --metal-job "Metal validation on Apple Silicon" + --cuda-job "CUDA full release validation" \ + --metal-job "Metal full release validation" - name: Verify release metadata integrity run: cargo xtask release-integrity - name: Verify final publish metadata diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dd71f641..6816d5f4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,16 +41,21 @@ cost-sensitive self-hosted CUDA and Metal runners. Pull requests that touch CUDA, Metal, shared GPU-profile paths, or `.github/workflows/gpu-validation.yml` must record a successful manual `gpu-validation.yml` dispatch for the PR head SHA before merge. The normal CI -`gpu-path-policy` job checks the PR diff, queries `gpu-validation.yml` runs by -head SHA, and fails until the required backend job names have succeeded: +planner checks the PR diff, queries `gpu-validation.yml` runs by head SHA, and +fails until the required quick backend job names have succeeded: -- `CUDA API compatibility on x86_64` for CUDA or shared GPU changes. -- `Metal validation on Apple Silicon` for Metal or shared GPU changes. +- `CUDA quick validation` for CUDA or shared GPU changes. +- `Metal quick validation` for Metal or shared GPU changes. + +Release candidates require one exact-SHA `target=all`, `mode=full` dispatch in +which `CUDA full release validation` and `Metal full release validation` both +succeed. Manual performance and profiling work belongs in the separate +`gpu-benchmarks.yml` workflow so it cannot contend with validation implicitly. Hosted macOS CI runs `cargo xtask metal-compile`; that is a compile and pure-test gate, not Metal hardware evidence. The self-hosted Metal job runs -`cargo xtask release-metal`, which fails on skipped runtime tests or a missing -Metal device. +`cargo xtask release-metal` with either `--mode quick` or `--mode full`. It +fails on skipped runtime tests or a missing Metal device. Do not add `pull_request` or `push` triggers to `gpu-validation.yml` without an explicit policy decision. diff --git a/Cargo.toml b/Cargo.toml index eb93089a..0bb280fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -107,3 +107,8 @@ opt-level = 3 inherits = "release" debug = true strip = "none" + +[profile.gpu-quick] +inherits = "release" +lto = false +codegen-units = 16 diff --git a/crates/j2k-jpeg-metal/benches/compare.rs b/crates/j2k-jpeg-metal/benches/compare.rs index c4e4a46e..a80f375c 100644 --- a/crates/j2k-jpeg-metal/benches/compare.rs +++ b/crates/j2k-jpeg-metal/benches/compare.rs @@ -815,14 +815,20 @@ fn sparse_viewport_workload(workload: &ViewportWorkload) -> Option bool { + let required = std::env::var_os("J2K_REQUIRE_METAL_BENCH").is_some(); #[cfg(target_os = "macos")] { - metal::Device::system_default().is_some() + let available = metal::Device::system_default().is_some(); + assert!( + !required || available, + "J2K_REQUIRE_METAL_BENCH is set but no default Metal device is available" + ); + available } #[cfg(not(target_os = "macos"))] { assert!( - std::env::var_os("J2K_REQUIRE_METAL_BENCH").is_none(), + !required, "J2K_REQUIRE_METAL_BENCH is set but this is not a Metal host" ); false diff --git a/docs/benchmark-corpora.md b/docs/benchmark-corpora.md index 8770cc42..d11b5a11 100644 --- a/docs/benchmark-corpora.md +++ b/docs/benchmark-corpora.md @@ -321,20 +321,14 @@ generated Metal host inputs disabled, no skipped auto rows, no probe errors, and the Metal timing policy. Omit the `--require-*` flag when the hardware rows are diagnostic context rather than part of the adoption claim. -On the self-hosted CUDA GitHub runner, dispatch `GPU validation` with -`run-adoption-benchmark=true`. For the full pinned corpus, set repository variables +On a self-hosted GPU runner, dispatch `GPU benchmarks` with `suite=adoption` +and the required CUDA or Metal lane. Set repository variables `J2K_ADOPTION_FIXTURES`, `J2K_ADOPTION_MANIFEST`, `J2K_ADOPTION_ENCODE_FIXTURES`, and `J2K_ADOPTION_ENCODE_MANIFEST` to the -pinned corpus paths available on that runner. If those variables are absent, -the workflow builds a default public starter corpus from Kodak plus curated -OpenJPEG data under `target/j2k-public-corpora`. The fallback verifies the -Kodak SHA-256 list above and checks out OpenJPEG-data commit -`39524bd3a601d90ed8e0177559400d23945f96a9`. It then runs the same -`adoption-benchmark --cuda --require-cuda` command and uploads the bundle as -the `cuda-adoption-benchmark` artifact. - -The fallback is pinned, not floating: it uses SHA-256-checked Kodak PNGs and a -fixed OpenJPEG-data commit before any adoption benchmark rows are generated. +pinned corpus paths available on that runner. The workflow fails closed when +any variable is absent; it does not synthesize or download a fallback corpus. +It runs `adoption-benchmark` with `--require-cuda` or `--require-metal` and +uploads the lane-specific bundle together with device and suite metadata. Use `--quick --include-generated` only for local smoke checks. A smoke bundle is not publication evidence. Full external `adoption-benchmark` runs fail after diff --git a/docs/benchmark-evidence.md b/docs/benchmark-evidence.md index d5a2639e..ddc9426e 100644 --- a/docs/benchmark-evidence.md +++ b/docs/benchmark-evidence.md @@ -38,7 +38,8 @@ Pinned rerun commands: ```bash cargo xtask bench-build -cargo xtask j2k-perf-guard --baseline-ref 29143c8e --quick +cargo xtask j2k-perf-guard --lane host \ + --baseline-ref 29143c8e1f00bbbe9cf5ab37b3cea882f6d52139 --quick ``` Historical result: both commands passed. The quick guard compared the then-current diff --git a/docs/env-vars.md b/docs/env-vars.md index a75380b9..24a1ec2f 100644 --- a/docs/env-vars.md +++ b/docs/env-vars.md @@ -78,6 +78,7 @@ override it. | `J2K_REQUIRE_METAL_RUNTIME` | Runs runtime-only Metal tests and makes them require a usable Metal runtime instead of default-skipping. | Skip runtime-only Metal paths | Test/CI | | `J2K_REQUIRE_CUDA_BENCH` | Makes CUDA benchmark probes fail instead of skip when CUDA is unavailable or does not dispatch. | Skip unavailable CUDA benchmark paths | Benchmark | | `J2K_REQUIRE_METAL_BENCH` | Makes Metal benchmark probes fail instead of skip when Metal is unavailable or does not dispatch. | Skip unavailable Metal benchmark paths | Benchmark | +| `J2K_GPU_CACHE_ENV` | Internal CI fingerprint of the accelerator driver/runtime and compiler environment, included in the GPU dependency-cache key. The GPU validation workflow computes this value; operators should not set it manually. | Computed by GPU CI | Test/CI | | `J2K_REQUIRE_WSI_ROOT` | Makes external JPEG WSI tests fail if `J2K_WSI_ROOT` is missing or empty. | Skip external WSI tests | Test/CI | | `J2K_REQUIRE_TRANSCODE_WSI_ROOT` | Makes transcode corpus validation require `J2K_TRANSCODE_WSI_ROOT`. | Skip external transcode WSI corpus | Test/CI | | `J2K_REQUIRE_NDPI` | Makes NDPI passthrough tests fail if `J2K_NDPI_PATH` is missing. | Skip NDPI passthrough test | Test/CI | @@ -95,7 +96,6 @@ override it. | `J2K_NDPI_MAX_PAYLOAD_BYTES` | Maximum NDPI tile payload accepted by the passthrough test. | Test default | Test/CI | | `J2K_ISO_CONFORMANCE_DIR` | Path to ISO J2K conformance vectors for ignored/external tests. | Not set | Test/CI | | `J2K_APERIO_TILE_FIXTURE` | Path to an Aperio J2K tile fixture for the ignored lossless encode test. | Not set | Test/CI | -| `J2K_STARTER_OPENJPEG_DATA_COMMIT` | Commit pin used by hosted GPU validation when materializing the default OpenJPEG starter corpus. | Workflow pin | Test/CI | | `J2K_PARITY_CORPUS_MANIFEST` | Manifest path used by `scripts/parity-corpus-fetch.sh`. | `corpus/wsi-samples/manifest.json` or first script argument | Test/CI | | `J2K_PARITY_CORPUS_DIR` | Output directory used by `scripts/parity-corpus-fetch.sh`. | `corpus/wsi-samples` or second script argument | Test/CI | | `J2K_PARITY_CORPUS_MAX_BYTES` | Maximum accepted byte size for each downloaded parity-corpus fixture. | `536870912` | Test/CI | @@ -182,11 +182,10 @@ override it. | `J2K_METAL_ENCODE_MANIFEST` | Optional TSV manifest for Metal encode staged PNM sources. Uses `path` and pinned `input_fnv1a64` from `J2K_ENCODE_COMPARE_MANIFEST`. | Not set | Benchmark | | `J2K_METAL_ENCODE_INCLUDE_GENERATED` | Set to `0`, `false`, `no`, or `off` to omit generated Metal host-input auto-routing rows when external staged PNM sources are provided. Stage microbenchmarks remain generated component rows. | Generated Metal host-input rows included | Benchmark | | `J2K_METAL_ENCODE_RESIDENT_MAX_ESTIMATED_OUTPUT_BYTES` | Maximum raw-byte estimate allowed for a Metal resident host-output encode benchmark row before the row is emitted as a structured memory-budget skip. This protects huge batches from materializing multi-gigabyte host codestream outputs in one benchmark process. | `2147483648` | Benchmark | -| `J2K_ADOPTION_FIXTURES` | Repository variable or environment override for the CUDA adoption workflow decode fixture directory path-list passed to `cargo run -p xtask --features adoption -- adoption-benchmark --fixtures`. | Build default public starter corpus when all adoption corpus variables are unset | Benchmark/CI | -| `J2K_ADOPTION_MANIFEST` | Repository variable or environment override for the CUDA adoption workflow decode fixture manifest passed to `--manifest`. | Build default public starter corpus when all adoption corpus variables are unset | Benchmark/CI | -| `J2K_ADOPTION_ENCODE_FIXTURES` | Repository variable or environment override for the CUDA adoption workflow staged PNM encode fixture directory path-list passed to `--encode-fixtures`. | Build default public starter corpus when all adoption corpus variables are unset | Benchmark/CI | -| `J2K_ADOPTION_ENCODE_MANIFEST` | Repository variable or environment override for the CUDA adoption workflow staged PNM encode manifest passed to `--encode-manifest`. | Build default public starter corpus when all adoption corpus variables are unset | Benchmark/CI | -| `J2K_ADOPTION_OUT_DIR` | Output directory used by the CUDA adoption workflow for generated benchmark artifacts and the rendered report. | `target/j2k-adoption-benchmark/cuda-full` | Benchmark/CI | +| `J2K_ADOPTION_FIXTURES` | Required repository variable or environment override for the manual GPU benchmark workflow decode fixture directory path-list passed to `cargo run -p xtask --features adoption -- adoption-benchmark --fixtures`. | None; adoption runs fail closed when unset | Benchmark/CI | +| `J2K_ADOPTION_MANIFEST` | Required repository variable or environment override for the manual GPU benchmark workflow decode fixture manifest passed to `--manifest`. | None; adoption runs fail closed when unset | Benchmark/CI | +| `J2K_ADOPTION_ENCODE_FIXTURES` | Required repository variable or environment override for the manual GPU benchmark workflow staged PNM encode fixture directory path-list passed to `--encode-fixtures`. | None; adoption runs fail closed when unset | Benchmark/CI | +| `J2K_ADOPTION_ENCODE_MANIFEST` | Required repository variable or environment override for the manual GPU benchmark workflow staged PNM encode manifest passed to `--encode-manifest`. | None; adoption runs fail closed when unset | Benchmark/CI | | `J2K_CUDA_PROFILE_BATCH_SIZE` | Batch size for the CUDA HTJ2K decode profile example. | Example default | Benchmark | | `J2K_CUDA_PROFILE_ITERATIONS` | Iteration count for the CUDA HTJ2K decode profile example. | Example default | Benchmark | diff --git a/docs/release.md b/docs/release.md index c4a511b9..3ff73f00 100644 --- a/docs/release.md +++ b/docs/release.md @@ -65,9 +65,11 @@ date or note change creates a new candidate and requires the exact-SHA gates again. Move the intended protected `origin/main` tip to exactly `RC_SHA` through the -repository's normal reviewed push/merge workflow, then run hosted CI and both -self-hosted GPU workflows for that exact commit. Verify the aggregate only -after those jobs have completed: +repository's normal reviewed push/merge workflow. Let `full-validation.yml` +finish for that push, then dispatch one `gpu-validation.yml` run with +`target=all` and `mode=full` for that exact commit. CUDA and Metal execute in +parallel within that run. Verify the evidence only after all three release jobs +have completed: ```bash test "$(git rev-parse origin/main)" = "$RC_SHA" @@ -252,7 +254,7 @@ Benchmark compilation is a release build-health gate, not a performance regression threshold. A release may claim performance only when the relevant CPU, Metal, or CUDA benchmark artifacts are recorded in [`docs/benchmark-evidence.md`](benchmark-evidence.md) or an attached run -bundle. `cargo xtask j2k-perf-guard` is available for explicit CPU Criterion +bundle. `cargo xtask j2k-perf-guard --lane host` is available for explicit CPU Criterion median regression signoff, but it is not part of the default release gate until the release checklist supplies a baseline ref and artifact retention policy. GPU performance signoff remains hardware-runner evidence, not hosted CI. diff --git a/scripts/configure-cuda-bindgen.sh b/scripts/configure-cuda-bindgen.sh new file mode 100644 index 00000000..476ab0ab --- /dev/null +++ b/scripts/configure-cuda-bindgen.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MIT OR Apache-2.0 + +set -euo pipefail + +if [[ -z "${LIBCLANG_PATH:-}" ]]; then + libclang_dir="" + for dir in /usr/lib/llvm-*/lib "${HOME}"/.local/llvm*/usr/lib/llvm-*/lib; do + if compgen -G "${dir}/libclang*.so*" >/dev/null; then + libclang_dir="${dir}" + fi + done + if [[ -z "${libclang_dir}" ]]; then + echo "libclang not found; CUDA Oxide bindgen cannot run" >&2 + exit 1 + fi + echo "LIBCLANG_PATH=${libclang_dir}" >> "${GITHUB_ENV:?GITHUB_ENV is required}" +fi + +resource_dir="" +for dir in /usr/lib/llvm-*/lib/clang/[0-9]* "${HOME}"/.local/llvm*/usr/lib/llvm-*/lib/clang/[0-9]*; do + if [[ -d "${dir}/include" ]]; then + resource_dir="${dir}" + fi +done + +bindgen_args="${BINDGEN_EXTRA_CLANG_ARGS:-}" +if [[ -n "${resource_dir}" ]]; then + bindgen_args="${bindgen_args:+${bindgen_args} }-resource-dir=${resource_dir}" +fi +gcc_include="$(gcc -print-file-name=include)" +if [[ -d "${gcc_include}" ]]; then + bindgen_args="${bindgen_args:+${bindgen_args} }-I${gcc_include}" +fi +if [[ -d /usr/local/cuda/include ]]; then + bindgen_args="${bindgen_args:+${bindgen_args} }-I/usr/local/cuda/include" +fi +echo "BINDGEN_EXTRA_CLANG_ARGS=${bindgen_args}" >> "${GITHUB_ENV:?GITHUB_ENV is required}" diff --git a/scripts/github_actions_verify.py b/scripts/github_actions_verify.py index cfda0cd0..c940a2d0 100755 --- a/scripts/github_actions_verify.py +++ b/scripts/github_actions_verify.py @@ -39,13 +39,18 @@ { ".github/CODEOWNERS", ".github/workflows/ci.yml", + ".github/workflows/full-validation.yml", + ".github/workflows/gpu-benchmarks.yml", ".github/workflows/gpu-validation.yml", ".github/workflows/publish.yml", + "scripts/ci_plan.py", "scripts/github_actions_verify.py", } ) -CUDA_JOB = "CUDA API compatibility on x86_64" -METAL_JOB = "Metal validation on Apple Silicon" +CUDA_QUICK_JOB = "CUDA quick validation" +METAL_QUICK_JOB = "Metal quick validation" +CUDA_JOB = "CUDA full release validation" +METAL_JOB = "Metal full release validation" RELEASE_CANDIDATE_JOB = "Release candidate aggregate" @@ -234,9 +239,9 @@ def classify_gpu_paths(paths: Iterable[str]) -> GpuPolicyDecision: required: list[str] = [] if cuda_paths or shared_paths: - required.append(CUDA_JOB) + required.append(CUDA_QUICK_JOB) if metal_paths or shared_paths: - required.append(METAL_JOB) + required.append(METAL_QUICK_JOB) return GpuPolicyDecision( tuple(sorted(cuda_paths | metal_paths | shared_paths)), tuple(required) ) @@ -657,7 +662,7 @@ def build_parser() -> argparse.ArgumentParser: release_parser.add_argument("--server-url", required=True) release_parser.add_argument("--tag", required=True) release_parser.add_argument("--candidate-sha", required=True) - release_parser.add_argument("--ci-workflow", default="ci.yml") + release_parser.add_argument("--ci-workflow", default="full-validation.yml") release_parser.add_argument("--aggregate-job", default=RELEASE_CANDIDATE_JOB) release_parser.add_argument("--gpu-workflow", default="gpu-validation.yml") release_parser.add_argument("--cuda-job", default=CUDA_JOB) @@ -670,7 +675,7 @@ def build_parser() -> argparse.ArgumentParser: ) _add_api_arguments(candidate_parser) candidate_parser.add_argument("--candidate-sha", required=True) - candidate_parser.add_argument("--ci-workflow", default="ci.yml") + candidate_parser.add_argument("--ci-workflow", default="full-validation.yml") candidate_parser.add_argument("--aggregate-job", default=RELEASE_CANDIDATE_JOB) candidate_parser.add_argument("--gpu-workflow", default="gpu-validation.yml") candidate_parser.add_argument("--cuda-job", default=CUDA_JOB) diff --git a/scripts/tests/test_github_actions_verify.py b/scripts/tests/test_github_actions_verify.py index a000d0a8..39eb6a00 100644 --- a/scripts/tests/test_github_actions_verify.py +++ b/scripts/tests/test_github_actions_verify.py @@ -129,7 +129,8 @@ def test_pull_request_files_are_paginated_and_renames_use_both_paths(self) -> No decision = verifier.classify_gpu_paths(paths) self.assertEqual( - decision.required_jobs, (verifier.CUDA_JOB, verifier.METAL_JOB) + decision.required_jobs, + (verifier.CUDA_QUICK_JOB, verifier.METAL_QUICK_JOB), ) self.assertEqual( decision.changed_gpu_paths, diff --git a/xtask/src/benchmark_commands.rs b/xtask/src/benchmark_commands.rs index f7e54dd0..f236f242 100644 --- a/xtask/src/benchmark_commands.rs +++ b/xtask/src/benchmark_commands.rs @@ -3,139 +3,52 @@ use std::fmt::Write as _; use std::fs; use std::path::PathBuf; +use crate::benchmark_registry::{BenchmarkLane, CompileBenchmark, COMPILE_BENCHMARKS}; use crate::command_support::{ command_output, command_output_allow_failure, command_output_os, run_cargo, run_cargo_test_with_pass_floor, workspace_version, }; use crate::process::cargo; -pub(super) fn bench_build() -> Result<(), String> { - run_cargo(&["bench", "-p", "j2k", "--bench", "public_api", "--no-run"])?; - run_cargo(&[ - "bench", - "-p", - "j2k-native", - "--bench", - "tier1_bitplane", - "--no-run", - ])?; - run_cargo(&[ - "bench", - "-p", - "j2k-native", - "--bench", - "htj2k_sigprop_phase", - "--no-run", - ])?; - run_cargo(&[ - "bench", - "-p", - "j2k-native", - "--bench", - "direct_cpu", - "--no-run", - ])?; - run_cargo(&[ - "bench", - "-p", - "j2k-jpeg", - "--bench", - "encode_cpu", - "--no-run", - ])?; - run_cargo(&[ - "bench", - "-p", - "j2k-jpeg", - "--features", - "bench-libjpeg-turbo", - "--no-run", - ])?; - bench_build_accelerators()?; - run_cargo(&[ - "bench", - "-p", - "j2k-tilecodec", - "--bench", - "compare", - "--no-run", - ])?; - run_cargo(&[ - "bench", - "-p", - "j2k-transcode", - "--bench", - "dct53", - "--no-run", - ])?; - run_cargo(transcode_metal_bench_args()) +pub(super) fn bench_build(args: impl Iterator) -> Result<(), String> { + let lane = parse_bench_lane(args)?; + for benchmark in COMPILE_BENCHMARKS + .iter() + .filter(|benchmark| lane.selects(benchmark.lane)) + { + run_cargo(&compile_benchmark_args(*benchmark))?; + } + Ok(()) } -fn bench_build_accelerators() -> Result<(), String> { - run_cargo(&["bench", "-p", "j2k-jpeg-metal", "--no-run"])?; - run_cargo(&[ - "bench", - "-p", - "j2k-jpeg-cuda", - "--bench", - "device_decode", - "--features", - "cuda-runtime", - "--no-run", - ])?; - run_cargo(&[ - "bench", - "-p", - "j2k-cuda", - "--bench", - "encode_stages", - "--features", - "cuda-runtime", - "--no-run", - ])?; - run_cargo(&[ - "bench", - "-p", - "j2k-cuda", - "--bench", - "htj2k_decode", - "--features", - "cuda-runtime", - "--no-run", - ])?; - run_cargo(&[ - "bench", - "-p", - "j2k-cuda", - "--bench", - "htj2k_encode", - "--features", - "cuda-runtime", - "--no-run", - ])?; - run_cargo(&[ - "bench", - "-p", - "j2k-ml", - "--bench", - "tensor_decode", - "--features", - "cpu", - "--no-run", - ]) +fn parse_bench_lane(mut args: impl Iterator) -> Result { + let Some(argument) = args.next() else { + return Ok(BenchmarkLane::All); + }; + if argument != "--lane" { + return Err(format!( + "unknown bench-build argument `{argument}`; expected --lane host|cuda|metal|all" + )); + } + let value = args + .next() + .ok_or_else(|| "--lane requires host, cuda, metal, or all".to_string())?; + if let Some(extra) = args.next() { + return Err(format!("unexpected bench-build argument `{extra}`")); + } + BenchmarkLane::parse(&value) } -fn transcode_metal_bench_args() -> &'static [&'static str] { - &[ - "bench", - "-p", - "j2k-transcode-metal", - "--bench", - "dct97", - "--features", - "bench-internals", - "--no-run", - ] +fn compile_benchmark_args(benchmark: CompileBenchmark) -> Vec<&'static str> { + let mut args = vec!["bench", "-p", benchmark.package]; + if let Some(bench) = benchmark.bench { + args.extend_from_slice(&["--bench", bench]); + } + if let Some(features) = benchmark.features { + args.extend_from_slice(&["--features", features]); + } + args.push("--no-run"); + args } pub(super) fn j2k_bench_signoff() -> Result<(), String> { diff --git a/xtask/src/benchmark_commands/tests.rs b/xtask/src/benchmark_commands/tests.rs index f6a3b4ad..5eabf3fd 100644 --- a/xtask/src/benchmark_commands/tests.rs +++ b/xtask/src/benchmark_commands/tests.rs @@ -1,17 +1,23 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 use super::{ - bench_build, bench_report, best_version_line, j2k_bench_signoff, one_line, - render_benchmark_report, split_semicolon_list, transcode_metal_bench_args, BenchmarkReport, + bench_build, bench_report, best_version_line, compile_benchmark_args, j2k_bench_signoff, + one_line, parse_bench_lane, render_benchmark_report, split_semicolon_list, BenchmarkLane, + BenchmarkReport, COMPILE_BENCHMARKS, }; #[cfg(unix)] use crate::{command_support::use_test_cargo_program, test_command::RecordingProgram}; #[test] -fn transcode_metal_bench_enables_its_declared_internal_surface() { +fn shared_registry_declares_transcode_metal_features_and_runtime_gate() { + let benchmark = COMPILE_BENCHMARKS + .iter() + .find(|benchmark| benchmark.package == "j2k-transcode-metal") + .copied() + .expect("transcode Metal benchmark registry entry"); assert_eq!( - transcode_metal_bench_args(), + compile_benchmark_args(benchmark), [ "bench", "-p", @@ -23,6 +29,9 @@ fn transcode_metal_bench_enables_its_declared_internal_surface() { "--no-run", ] ); + assert!(benchmark + .runtime_env + .contains(&("J2K_REQUIRE_METAL_RUNTIME", "1"))); } #[cfg(unix)] @@ -34,7 +43,7 @@ fn benchmark_build_and_signoff_execute_the_complete_fake_cargo_plan() { ); let _cargo = use_test_cargo_program(recording.program().as_os_str().to_owned()); - bench_build().expect("benchmark build plan"); + bench_build(std::iter::empty()).expect("benchmark build plan"); j2k_bench_signoff().expect("benchmark signoff plan"); let log = recording.log(); @@ -48,6 +57,46 @@ fn benchmark_build_and_signoff_execute_the_complete_fake_cargo_plan() { assert_eq!(log.lines().count(), 19); } +#[cfg(unix)] +#[test] +fn benchmark_build_lanes_never_compile_the_other_accelerator() { + for (lane, expected_lines, required, forbidden) in [ + ( + "host", + 9, + "j2k-ml --bench tensor_decode --features cpu", + "j2k-cuda", + ), + ("cuda", 4, "j2k-cuda --bench htj2k_decode", "j2k-metal"), + ("metal", 2, "j2k-jpeg-metal", "j2k-cuda"), + ] { + let recording = RecordingProgram::new("benchmark-lane-test", ""); + let _cargo = use_test_cargo_program(recording.program().as_os_str().to_owned()); + + bench_build(["--lane".to_string(), lane.to_string()].into_iter()) + .expect("lane benchmark plan"); + + let log = recording.log(); + assert_eq!(log.lines().count(), expected_lines, "lane {lane}: {log}"); + assert!(log.contains(required), "lane {lane}: {log}"); + assert!(!log.contains(forbidden), "lane {lane}: {log}"); + } +} + +#[test] +fn benchmark_lane_parser_defaults_all_and_rejects_invalid_input() { + assert_eq!( + parse_bench_lane(std::iter::empty()).unwrap(), + BenchmarkLane::All + ); + assert_eq!( + parse_bench_lane(["--lane".to_string(), "metal".to_string()].into_iter()).unwrap(), + BenchmarkLane::Metal + ); + assert!(parse_bench_lane(["--lane".to_string()].into_iter()).is_err()); + assert!(parse_bench_lane(["--lane".to_string(), "other".to_string()].into_iter()).is_err()); +} + #[test] fn benchmark_report_renderer_preserves_provenance_comparators_and_skips() { let report = BenchmarkReport { diff --git a/xtask/src/benchmark_registry.rs b/xtask/src/benchmark_registry.rs new file mode 100644 index 00000000..4e5888f8 --- /dev/null +++ b/xtask/src/benchmark_registry.rs @@ -0,0 +1,284 @@ +//! Authoritative benchmark packages, executables, lanes, features, and runtime gates. + +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub(crate) enum BenchmarkLane { + Host, + Cuda, + Metal, + All, +} + +impl BenchmarkLane { + pub(crate) fn parse(value: &str) -> Result { + match value { + "host" => Ok(Self::Host), + "cuda" => Ok(Self::Cuda), + "metal" => Ok(Self::Metal), + "all" => Ok(Self::All), + _ => Err(format!( + "unknown benchmark lane `{value}`; expected host, cuda, metal, or all" + )), + } + } + + pub(crate) const fn as_str(self) -> &'static str { + match self { + Self::Host => "host", + Self::Cuda => "cuda", + Self::Metal => "metal", + Self::All => "all", + } + } + + pub(crate) fn selects(self, lane: Self) -> bool { + self == Self::All || self == lane + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct CompileBenchmark { + pub(crate) package: &'static str, + pub(crate) bench: Option<&'static str>, + pub(crate) features: Option<&'static str>, + pub(crate) lane: BenchmarkLane, + pub(crate) runtime_env: &'static [(&'static str, &'static str)], +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct PerformanceBenchmark { + pub(crate) package: &'static str, + pub(crate) bench: &'static str, + pub(crate) filter: Option<&'static str>, + pub(crate) features: Option<&'static str>, + pub(crate) lane: BenchmarkLane, + pub(crate) env: &'static [(&'static str, &'static str)], +} + +pub(crate) const CUDA_BENCH_ENV: &[(&str, &str)] = &[ + ("J2K_REQUIRE_CUDA_BENCH", "1"), + ("J2K_REQUIRE_CUDA_RUNTIME", "1"), +]; +pub(crate) const METAL_BENCH_ENV: &[(&str, &str)] = &[ + ("J2K_REQUIRE_METAL_BENCH", "1"), + ("J2K_REQUIRE_METAL_RUNTIME", "1"), +]; + +const fn compile( + package: &'static str, + bench: Option<&'static str>, + features: Option<&'static str>, + lane: BenchmarkLane, + runtime_env: &'static [(&'static str, &'static str)], +) -> CompileBenchmark { + CompileBenchmark { + package, + bench, + features, + lane, + runtime_env, + } +} + +const fn performance( + package: &'static str, + bench: &'static str, + filter: Option<&'static str>, + features: Option<&'static str>, + lane: BenchmarkLane, + env: &'static [(&'static str, &'static str)], +) -> PerformanceBenchmark { + PerformanceBenchmark { + package, + bench, + filter, + features, + lane, + env, + } +} + +pub(crate) const COMPILE_BENCHMARKS: &[CompileBenchmark] = &[ + compile("j2k", Some("public_api"), None, BenchmarkLane::Host, &[]), + compile( + "j2k-native", + Some("tier1_bitplane"), + None, + BenchmarkLane::Host, + &[], + ), + compile( + "j2k-native", + Some("htj2k_sigprop_phase"), + None, + BenchmarkLane::Host, + &[], + ), + compile( + "j2k-native", + Some("direct_cpu"), + None, + BenchmarkLane::Host, + &[], + ), + compile( + "j2k-jpeg", + Some("encode_cpu"), + None, + BenchmarkLane::Host, + &[], + ), + compile( + "j2k-jpeg", + None, + Some("bench-libjpeg-turbo"), + BenchmarkLane::Host, + &[], + ), + compile( + "j2k-tilecodec", + Some("compare"), + None, + BenchmarkLane::Host, + &[], + ), + compile( + "j2k-transcode", + Some("dct53"), + None, + BenchmarkLane::Host, + &[], + ), + compile( + "j2k-ml", + Some("tensor_decode"), + Some("cpu"), + BenchmarkLane::Host, + &[], + ), + compile( + "j2k-jpeg-cuda", + Some("device_decode"), + Some("cuda-runtime"), + BenchmarkLane::Cuda, + CUDA_BENCH_ENV, + ), + compile( + "j2k-cuda", + Some("encode_stages"), + Some("cuda-runtime"), + BenchmarkLane::Cuda, + CUDA_BENCH_ENV, + ), + compile( + "j2k-cuda", + Some("htj2k_decode"), + Some("cuda-runtime"), + BenchmarkLane::Cuda, + CUDA_BENCH_ENV, + ), + compile( + "j2k-cuda", + Some("htj2k_encode"), + Some("cuda-runtime"), + BenchmarkLane::Cuda, + CUDA_BENCH_ENV, + ), + compile( + "j2k-jpeg-metal", + None, + None, + BenchmarkLane::Metal, + METAL_BENCH_ENV, + ), + compile( + "j2k-transcode-metal", + Some("dct97"), + Some("bench-internals"), + BenchmarkLane::Metal, + METAL_BENCH_ENV, + ), +]; + +pub(crate) const PERFORMANCE_BENCHMARKS: &[PerformanceBenchmark] = &[ + performance("j2k", "public_api", None, None, BenchmarkLane::Host, &[]), + performance( + "j2k-jpeg", + "encode_cpu", + Some("jpeg_cpu_encode_runtime/"), + None, + BenchmarkLane::Host, + &[], + ), + performance( + "j2k-native", + "tier1_bitplane", + Some("htj2k_cleanup_decode/"), + None, + BenchmarkLane::Host, + &[], + ), + performance( + "j2k-native", + "tier1_bitplane", + Some("htj2k_refinement_fixture_decode"), + None, + BenchmarkLane::Host, + &[], + ), + performance( + "j2k-native", + "tier1_bitplane", + Some("htj2k_refinement_block_decode"), + None, + BenchmarkLane::Host, + &[], + ), + performance( + "j2k-native", + "tier1_bitplane", + Some("htj2k_cleanup_encode/"), + None, + BenchmarkLane::Host, + &[], + ), + performance( + "j2k-native", + "tier1_bitplane", + Some("htj2k_cleanup_encode_distribution"), + None, + BenchmarkLane::Host, + &[], + ), + performance( + "j2k-native", + "htj2k_sigprop_phase", + None, + None, + BenchmarkLane::Host, + &[], + ), + performance( + "j2k-cuda", + "htj2k_decode", + Some("j2k_cuda_htj2k_"), + Some("cuda-runtime"), + BenchmarkLane::Cuda, + CUDA_BENCH_ENV, + ), + performance( + "j2k-cuda", + "htj2k_encode", + Some("j2k_cuda_htj2k_"), + Some("cuda-runtime"), + BenchmarkLane::Cuda, + CUDA_BENCH_ENV, + ), + performance( + "j2k-jpeg-metal", + "compare", + None, + None, + BenchmarkLane::Metal, + METAL_BENCH_ENV, + ), +]; diff --git a/xtask/src/cuda.rs b/xtask/src/cuda.rs index 0a1be996..6d822954 100644 --- a/xtask/src/cuda.rs +++ b/xtask/src/cuda.rs @@ -4,8 +4,15 @@ use std::{collections::BTreeSet, env, ffi::OsString}; +use crate::gpu_validation::ValidationMode; use crate::process::{self, cargo, CommandContext}; +mod quick; +mod test_output; + +use quick::quick_runtime_args; +use test_output::{successful_test_summaries, TestSummary}; + const CUDA_RELEASE_ENV: &[(&str, &str)] = &[ ("J2K_REQUIRE_CUDA_RUNTIME", "1"), ("J2K_REQUIRE_CUDA_OXIDE_BUILD", "1"), @@ -167,32 +174,51 @@ const EXACT_CUDA_SUITES: &[ExactCudaSuite] = &[ ]; /// Runs the complete release-mode CUDA validation policy on a real Linux `x86_64` device. -pub(crate) fn release_cuda() -> Result<(), String> { - run_release_cuda(env::consts::OS, env::consts::ARCH) +pub(crate) fn release_cuda(args: impl Iterator) -> Result<(), String> { + run_release_cuda( + env::consts::OS, + env::consts::ARCH, + ValidationMode::parse(args)?, + ) } -fn run_release_cuda(os: &str, arch: &str) -> Result<(), String> { +fn run_release_cuda(os: &str, arch: &str, mode: ValidationMode) -> Result<(), String> { require_cuda_host(os, arch)?; require_cuda_device()?; - for suite in CUDA_CLIPPY_SUITES { - let args = clippy_suite_args(suite); - let label = format!("{} CUDA Clippy", suite.package); - let output = run_cargo_captured(&args, CUDA_RELEASE_ENV, &label)?; - reject_cuda_skip_markers(&output, &label)?; - } - - for suite in CUDA_RUNTIME_SUITES { - let args = runtime_suite_args(suite); - let output = run_cargo_captured(&args, CUDA_RELEASE_ENV, suite.label)?; - validate_complete_test_run(&output, suite.label)?; - } - - for suite in EXACT_CUDA_SUITES { - validate_exact_inventory(suite)?; - let args = exact_suite_args(suite, false); - let output = run_cargo_captured(&args, CUDA_RELEASE_ENV, suite.label)?; - validate_exact_named_run(&output, suite.label, suite.required_tests)?; + match mode { + ValidationMode::Full => { + for suite in CUDA_CLIPPY_SUITES { + let args = clippy_suite_args(suite); + let label = format!("{} CUDA Clippy", suite.package); + let output = run_cargo_captured(&args, CUDA_RELEASE_ENV, &label)?; + reject_cuda_skip_markers(&output, &label)?; + } + + for suite in CUDA_RUNTIME_SUITES { + let args = runtime_suite_args(suite, mode); + let output = run_cargo_captured(&args, CUDA_RELEASE_ENV, suite.label)?; + validate_complete_test_run(&output, suite.label)?; + } + + for suite in EXACT_CUDA_SUITES { + validate_exact_inventory(suite, mode)?; + let args = exact_suite_args(suite, false, mode); + let output = run_cargo_captured(&args, CUDA_RELEASE_ENV, suite.label)?; + validate_exact_named_run(&output, suite.label, suite.required_tests)?; + } + } + ValidationMode::Quick => { + let output = run_cargo_captured( + &quick_runtime_args(), + CUDA_RELEASE_ENV, + "quick CUDA production-feature runtime", + )?; + validate_complete_test_run(&output, "quick CUDA production-feature runtime")?; + for suite in EXACT_CUDA_SUITES { + validate_required_named_tests(&output, suite.label, suite.required_tests)?; + } + } } Ok(()) @@ -268,29 +294,23 @@ fn clippy_suite_args(suite: &CudaClippySuite) -> Vec<&'static str> { ] } -fn runtime_suite_args(suite: &CudaRuntimeSuite) -> Vec<&'static str> { - let mut args = vec![ - "test", - "--release", - "-p", - suite.package, - "--features", - suite.features, - ]; +fn runtime_suite_args(suite: &CudaRuntimeSuite, mode: ValidationMode) -> Vec<&'static str> { + let mut args = vec!["test"]; + args.extend_from_slice(mode.cargo_profile_args()); + args.extend_from_slice(&["-p", suite.package, "--features", suite.features]); args.extend_from_slice(CUDA_RUNTIME_TEST_TARGETS); args.extend_from_slice(&["--", "--show-output"]); args } -fn exact_suite_args(suite: &ExactCudaSuite, list_only: bool) -> Vec<&'static str> { - let mut args = vec![ - "test", - "--release", - "-p", - suite.package, - "--features", - suite.features, - ]; +fn exact_suite_args( + suite: &ExactCudaSuite, + list_only: bool, + mode: ValidationMode, +) -> Vec<&'static str> { + let mut args = vec!["test"]; + args.extend_from_slice(mode.cargo_profile_args()); + args.extend_from_slice(&["-p", suite.package, "--features", suite.features]); for target in suite.test_targets { args.extend_from_slice(&["--test", target]); } @@ -299,8 +319,8 @@ fn exact_suite_args(suite: &ExactCudaSuite, list_only: bool) -> Vec<&'static str args } -fn validate_exact_inventory(suite: &ExactCudaSuite) -> Result<(), String> { - let args = exact_suite_args(suite, true); +fn validate_exact_inventory(suite: &ExactCudaSuite, mode: ValidationMode) -> Result<(), String> { + let args = exact_suite_args(suite, true, mode); let label = format!("list {}", suite.label); let output = run_cargo_captured(&args, CUDA_RELEASE_ENV, &label)?; reject_cuda_skip_markers(&output, &label)?; @@ -368,6 +388,23 @@ fn validate_exact_named_run( Ok(()) } +fn validate_required_named_tests( + output: &str, + label: &str, + required_tests: &[&str], +) -> Result<(), String> { + let actual = passed_rust_tests(output); + let expected = expected_test_set(required_tests); + let missing = expected.difference(&actual).cloned().collect::>(); + if missing.is_empty() { + Ok(()) + } else { + Err(format!( + "{label} required-test mismatch; missing: {missing:?}" + )) + } +} + fn expected_test_set(tests: &[&str]) -> BTreeSet { tests.iter().map(|test| (*test).to_string()).collect() } @@ -451,64 +488,6 @@ fn passed_rust_tests(output: &str) -> BTreeSet { .collect() } -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] -struct TestSummary { - passed: usize, - failed: usize, - ignored: usize, - measured: usize, - filtered_out: usize, -} - -impl TestSummary { - fn parse(line: &str) -> Option { - let counts = line.trim().strip_prefix("test result: ok.")?; - let mut fields = counts.split(';').map(str::trim); - let passed = parse_count(fields.next()?, "passed")?; - let failed = parse_count(fields.next()?, "failed")?; - let ignored = parse_count(fields.next()?, "ignored")?; - let measured = parse_count(fields.next()?, "measured")?; - let filtered_out = parse_count(fields.next()?, "filtered out")?; - if fields - .next() - .is_some_and(|timing| !timing.starts_with("finished in ")) - || fields.next().is_some() - { - return None; - } - Some(Self { - passed, - failed, - ignored, - measured, - filtered_out, - }) - } - - const fn add(self, other: Self) -> Self { - Self { - passed: self.passed + other.passed, - failed: self.failed + other.failed, - ignored: self.ignored + other.ignored, - measured: self.measured + other.measured, - filtered_out: self.filtered_out + other.filtered_out, - } - } -} - -fn parse_count(field: &str, suffix: &str) -> Option { - field - .strip_suffix(suffix)? - .trim() - .trim_end_matches('.') - .parse() - .ok() -} - -fn successful_test_summaries(output: &str) -> Vec { - output.lines().filter_map(TestSummary::parse).collect() -} - #[cfg(all(test, unix))] mod test_support; diff --git a/xtask/src/cuda/quick.rs b/xtask/src/cuda/quick.rs new file mode 100644 index 00000000..be7735b7 --- /dev/null +++ b/xtask/src/cuda/quick.rs @@ -0,0 +1,27 @@ +//! Consolidated production-feature command plan for quick CUDA validation. + +use super::CUDA_RUNTIME_TEST_TARGETS; + +const QUICK_CUDA_FEATURES: &str = "j2k-cuda-runtime/cuda-oxide,j2k-jpeg-cuda/cuda-runtime,j2k-cuda/cuda-runtime,j2k-transcode-cuda/cuda-runtime,j2k-ml/cuda"; +const QUICK_CUDA_PACKAGES: &[&str] = &[ + "j2k-cuda-runtime", + "j2k-jpeg-cuda", + "j2k-cuda", + "j2k-transcode-cuda", + "j2k-ml", +]; + +fn append_packages(args: &mut Vec<&'static str>) { + for package in QUICK_CUDA_PACKAGES { + args.extend_from_slice(&["-p", package]); + } +} + +pub(super) fn quick_runtime_args() -> Vec<&'static str> { + let mut args = vec!["test", "--profile", "gpu-quick"]; + append_packages(&mut args); + args.extend_from_slice(&["--features", QUICK_CUDA_FEATURES]); + args.extend_from_slice(CUDA_RUNTIME_TEST_TARGETS); + args.extend_from_slice(&["--", "--show-output"]); + args +} diff --git a/xtask/src/cuda/test_output.rs b/xtask/src/cuda/test_output.rs new file mode 100644 index 00000000..ae96f015 --- /dev/null +++ b/xtask/src/cuda/test_output.rs @@ -0,0 +1,59 @@ +//! Strict parsing for Rust test summaries emitted by CUDA validation commands. + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub(super) struct TestSummary { + pub(super) passed: usize, + pub(super) failed: usize, + pub(super) ignored: usize, + pub(super) measured: usize, + pub(super) filtered_out: usize, +} + +impl TestSummary { + fn parse(line: &str) -> Option { + let counts = line.trim().strip_prefix("test result: ok.")?; + let mut fields = counts.split(';').map(str::trim); + let passed = parse_count(fields.next()?, "passed")?; + let failed = parse_count(fields.next()?, "failed")?; + let ignored = parse_count(fields.next()?, "ignored")?; + let measured = parse_count(fields.next()?, "measured")?; + let filtered_out = parse_count(fields.next()?, "filtered out")?; + if fields + .next() + .is_some_and(|timing| !timing.starts_with("finished in ")) + || fields.next().is_some() + { + return None; + } + Some(Self { + passed, + failed, + ignored, + measured, + filtered_out, + }) + } + + pub(super) const fn add(self, other: Self) -> Self { + Self { + passed: self.passed + other.passed, + failed: self.failed + other.failed, + ignored: self.ignored + other.ignored, + measured: self.measured + other.measured, + filtered_out: self.filtered_out + other.filtered_out, + } + } +} + +fn parse_count(field: &str, suffix: &str) -> Option { + field + .strip_suffix(suffix)? + .trim() + .trim_end_matches('.') + .parse() + .ok() +} + +pub(super) fn successful_test_summaries(output: &str) -> Vec { + output.lines().filter_map(TestSummary::parse).collect() +} diff --git a/xtask/src/cuda/tests.rs b/xtask/src/cuda/tests.rs index 455cc7c2..f2897fe1 100644 --- a/xtask/src/cuda/tests.rs +++ b/xtask/src/cuda/tests.rs @@ -7,6 +7,9 @@ use super::{ CUDA_CLIPPY_SUITES, CUDA_RUNTIME_SUITES, EXACT_CUDA_SUITES, HTJ2K_ENCODE_PARITY_TESTS, ML_CUDA_TESTS, TRANSCODE_PARITY_TESTS, }; +use crate::gpu_validation::ValidationMode; + +mod command_plan; #[cfg(unix)] use super::{ @@ -41,7 +44,7 @@ fn exact_cuda_inventories_are_unique_and_have_audited_sizes() { #[test] fn release_commands_name_packages_features_and_non_benchmark_test_targets() { for suite in CUDA_RUNTIME_SUITES { - let args = runtime_suite_args(suite); + let args = runtime_suite_args(suite, ValidationMode::Full); assert!(args.windows(2).any(|pair| pair == ["-p", suite.package])); assert!(args .windows(2) @@ -56,8 +59,8 @@ fn release_commands_name_packages_features_and_non_benchmark_test_targets() { } for suite in EXACT_CUDA_SUITES { - let list_args = exact_suite_args(suite, true); - let run_args = exact_suite_args(suite, false); + let list_args = exact_suite_args(suite, true, ValidationMode::Full); + let run_args = exact_suite_args(suite, false, ValidationMode::Full); for target in suite.test_targets { assert!(list_args.windows(2).any(|pair| pair == ["--test", target])); } @@ -205,6 +208,13 @@ fn recording_cuda_cargo() -> RecordingProgram { esac exit 0 ;; +*"test --profile gpu-quick"*) + {ml_passed} + {ht_passed} + {transcode_passed} + printf '%s\n' 'test smoke ... ok' + printf '%s\n' 'test result: ok. 21 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out' + ;; *"test --release"*) printf '%s\n' 'test smoke ... ok' printf '%s\n' 'test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out' @@ -224,12 +234,14 @@ fn cuda_release_executes_the_complete_hermetic_command_plan() { let mut expected_runs = 1; if cfg!(all(target_os = "linux", target_arch = "x86_64")) { - release_cuda().expect("platform wrapper should execute the hermetic CUDA release plan"); + release_cuda(std::iter::empty()) + .expect("platform wrapper should execute the hermetic CUDA release plan"); expected_runs += 1; } else { - assert!(release_cuda().is_err()); + assert!(release_cuda(std::iter::empty()).is_err()); } - run_release_cuda("linux", "x86_64").expect("platform-independent CUDA release plan"); + run_release_cuda("linux", "x86_64", ValidationMode::Full) + .expect("platform-independent CUDA release plan"); let log = cargo.log(); assert_eq!(log.lines().count(), expected_runs * 19); @@ -237,6 +249,25 @@ fn cuda_release_executes_the_complete_hermetic_command_plan() { assert_eq!(device.log().lines().count(), expected_runs); } +#[cfg(unix)] +#[test] +fn cuda_quick_executes_one_shared_runtime_graph() { + let device = RecordingProgram::new("cuda-quick-device-test", "printf '%s\n' '0, GPU-QUICK'"); + let _device = use_test_nvidia_smi_program(device.program().as_os_str().to_owned()); + let cargo = recording_cuda_cargo(); + let _cargo = use_test_cargo_program(cargo.program().as_os_str().to_owned()); + + run_release_cuda("linux", "x86_64", ValidationMode::Quick).expect("quick CUDA command plan"); + + let log = cargo.log(); + assert_eq!(log.lines().count(), 1); + assert!(!log.contains("clippy --profile gpu-quick")); + assert_eq!(log.matches("test --profile gpu-quick").count(), 1); + assert!(!log.contains("--list")); + assert!(!log.contains(" --test ")); + assert!(!log.contains("test --release")); +} + #[cfg(unix)] #[test] fn cuda_device_override_is_nested_transactional_and_fail_closed() { @@ -264,7 +295,8 @@ fn exact_inventory_and_captured_cargo_report_subprocess_failures() { ); { let _cargo = use_test_cargo_program(mismatch.program().as_os_str().to_owned()); - let error = validate_exact_inventory(&EXACT_CUDA_SUITES[0]).unwrap_err(); + let error = + validate_exact_inventory(&EXACT_CUDA_SUITES[0], ValidationMode::Full).unwrap_err(); assert!(error.contains("inventory mismatch")); assert!(error.contains("unexpected_test")); } diff --git a/xtask/src/cuda/tests/command_plan.rs b/xtask/src/cuda/tests/command_plan.rs new file mode 100644 index 00000000..d07a152f --- /dev/null +++ b/xtask/src/cuda/tests/command_plan.rs @@ -0,0 +1,57 @@ +//! Quick/full CUDA command-mode regressions. + +use super::super::{quick_runtime_args, validate_required_named_tests}; +use crate::gpu_validation::ValidationMode; + +#[test] +fn quick_cuda_plan_uses_one_production_feature_runtime_graph_and_fast_profile() { + let tests = quick_runtime_args(); + + assert!(tests + .windows(2) + .any(|pair| pair == ["--profile", "gpu-quick"])); + assert!(!tests.contains(&"--release")); + for package in [ + "j2k-cuda-runtime", + "j2k-jpeg-cuda", + "j2k-cuda", + "j2k-transcode-cuda", + "j2k-ml", + ] { + assert!(tests.windows(2).any(|pair| pair == ["-p", package])); + } + assert!(tests.contains(&"j2k-cuda-runtime/cuda-oxide,j2k-jpeg-cuda/cuda-runtime,j2k-cuda/cuda-runtime,j2k-transcode-cuda/cuda-runtime,j2k-ml/cuda")); + assert!(tests.contains(&"--show-output")); +} + +#[test] +fn required_named_validation_accepts_extra_combined_tests_and_rejects_missing_tests() { + let combined = "\ +test unrelated ... ok +test alpha ... ok +test beta ... ok +test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out +"; + validate_required_named_tests(combined, "CUDA", &["alpha", "beta"]).unwrap(); + + let error = validate_required_named_tests(combined, "CUDA", &["alpha", "missing"]) + .expect_err("a missing required test must fail quick CUDA validation"); + assert!(error.contains("missing")); +} + +#[test] +fn validation_mode_defaults_full_and_rejects_unknown_arguments() { + assert_eq!( + ValidationMode::parse(std::iter::empty()).unwrap(), + ValidationMode::Full + ); + assert_eq!( + ValidationMode::parse(["--mode".to_string(), "quick".to_string()].into_iter()).unwrap(), + ValidationMode::Quick + ); + assert!(ValidationMode::parse(["--mode".to_string()].into_iter()).is_err()); + assert!( + ValidationMode::parse(["--mode".to_string(), "other".to_string()].into_iter()).is_err() + ); + assert!(ValidationMode::parse(["--unknown".to_string()].into_iter()).is_err()); +} diff --git a/xtask/src/gpu_validation.rs b/xtask/src/gpu_validation.rs new file mode 100644 index 00000000..0597e500 --- /dev/null +++ b/xtask/src/gpu_validation.rs @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +//! Shared command-line policy for fail-closed GPU validation modes. + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum ValidationMode { + Quick, + Full, +} + +impl ValidationMode { + pub(crate) fn parse(mut args: impl Iterator) -> Result { + let Some(argument) = args.next() else { + return Ok(Self::Full); + }; + if argument != "--mode" { + return Err(format!( + "unknown GPU validation argument `{argument}`; expected --mode quick|full" + )); + } + let value = args + .next() + .ok_or_else(|| "--mode requires quick or full".to_string())?; + if let Some(extra) = args.next() { + return Err(format!( + "unexpected GPU validation argument `{extra}`; expected only --mode quick|full" + )); + } + match value.as_str() { + "quick" => Ok(Self::Quick), + "full" => Ok(Self::Full), + _ => Err(format!( + "unknown GPU validation mode `{value}`; expected quick or full" + )), + } + } + + pub(crate) const fn cargo_profile_args(self) -> &'static [&'static str] { + match self { + Self::Quick => &["--profile", "gpu-quick"], + Self::Full => &["--release"], + } + } +} diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 48549f63..fe55ede6 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -16,11 +16,13 @@ mod adoption_materialize; #[cfg(feature = "adoption")] mod adoption_report; mod benchmark_commands; +mod benchmark_registry; mod clone_audit; mod codegen_commands; mod command_support; mod coverage; mod cuda; +mod gpu_validation; #[cfg(feature = "adoption")] mod markdown; mod metal; @@ -72,7 +74,7 @@ fn run() -> Result<(), String> { "nextest" => nextest(), "doc" | "docs" => doc(), "typos" => typos(), - "bench-build" => bench_build(), + "bench-build" => bench_build(env::args().skip(2)), "bench-report" => bench_report(env::args().skip(2)), #[cfg(feature = "adoption")] "adoption-benchmark" => adoption_benchmark::adoption_benchmark(env::args().skip(2)), @@ -118,9 +120,9 @@ fn run() -> Result<(), String> { "release-integrity" => release_integrity(env::args().skip(2)), "release-status" => release_status::release_status(env::args().skip(2)), "release-cpu" => release_cpu(), - "release-cuda" => cuda::release_cuda(), + "release-cuda" => cuda::release_cuda(env::args().skip(2)), "metal-compile" => metal::metal_compile(), - "release-metal" => metal::release_metal(), + "release-metal" => metal::release_metal(env::args().skip(2)), "coverage" => coverage::coverage(env::args().skip(2)), "package" => package(), "ci" => ci(), @@ -144,7 +146,7 @@ fn print_help() { nextest run workspace tests with cargo-nextest\n\ doc build workspace docs with warnings denied\n\ typos run typos\n\ - bench-build compile benchmark targets\n\ + bench-build compile benchmark targets [--lane host|cuda|metal|all]\n\ bench-report print or write a benchmark publication report\n\ adoption-benchmark run CPU comparator and optional CUDA/Metal adoption benchmark bundle [--features adoption]\n\ adoption-curate stage inspectable external J2K fixtures and a pinned manifest [--features adoption]\n\ @@ -153,7 +155,7 @@ fn print_help() { adoption-report render a marketing-safe report from an adoption benchmark bundle [--features adoption]\n\ public-support verify the public J2K/HTJ2K support matrix and publication gates [--final]\n\ j2k-bench-signoff run required OpenJPEG/Grok parity and J2K compare bench compile gates\n\ - j2k-perf-guard compare CPU J2K Criterion medians against a baseline git ref\n\ + j2k-perf-guard compare one strict host/CUDA/Metal Criterion lane against a baseline git ref\n\ codec-math-codegen check generated codec-math Rust and Metal fragments\n\ fuzz-build compile fuzz harnesses\n\ fuzz-run run scheduled fuzz targets with J2K_FUZZ_RUNS\n\ @@ -171,9 +173,9 @@ fn print_help() { release-integrity validate offline release metadata; --publish requires final dated/signoff state\n\ release-status verify one frozen SHA's CI aggregate and both GPU jobs [--sha SHA] [--repository owner/name]\n\ release-cpu run release-mode CPU codec tests\n\ - release-cuda run fail-closed release-mode CUDA validation on Linux x86_64\n\ + release-cuda run fail-closed CUDA validation on Linux x86_64 [--mode quick|full]\n\ metal-compile compile all Metal targets and run default/pure tests on hosted macOS\n\ - release-metal run fail-closed release-mode Metal hardware validation on macOS\n\ + release-metal run fail-closed Metal hardware validation on macOS [--mode quick|full]\n\ coverage enforce >=80% host-wide or accelerator critical-path coverage [host|metal|cuda] [--base REV]\n\ package construct all staged packages from a clean worktree and publish-dry-run registry-independent crates" ); @@ -352,56 +354,34 @@ some other output: 12 passed let root = temp_dir("j2k-perf-guard-sync-test"); let source = root.join("source"); let target = root.join("target"); - let jpeg_manifest = "crates/j2k-jpeg/Cargo.toml"; - let cuda_manifest = "crates/j2k-cuda/Cargo.toml"; let public_bench = "crates/j2k/benches/public_api.rs"; let jpeg_encode_bench = "crates/j2k-jpeg/benches/encode_cpu.rs"; let cuda_decode_bench = "crates/j2k-cuda/benches/htj2k_decode.rs"; let cuda_encode_bench = "crates/j2k-cuda/benches/htj2k_encode.rs"; + let metal_bench = "crates/j2k-jpeg-metal/benches/compare.rs"; let native_bench = "crates/j2k-native/benches/tier1_bitplane.rs"; let native_sigprop_bench = "crates/j2k-native/benches/htj2k_sigprop_phase.rs"; let native_fixture = "crates/j2k-native/fixtures/htj2k/openhtj2k_ds0_ht_09_b11.j2k"; - fs::create_dir_all(source.join("crates/j2k/benches")).unwrap(); - fs::create_dir_all(source.join("crates/j2k-jpeg/benches")).unwrap(); - fs::create_dir_all(source.join("crates/j2k-cuda/benches")).unwrap(); - fs::create_dir_all(source.join("crates/j2k-native/benches")).unwrap(); - fs::create_dir_all(source.join("crates/j2k-native/fixtures/htj2k")).unwrap(); - fs::create_dir_all(target.join("crates/j2k-jpeg")).unwrap(); - fs::create_dir_all(target.join("crates/j2k-cuda")).unwrap(); - fs::create_dir_all(target.join("crates/j2k/benches")).unwrap(); - fs::create_dir_all(target.join("crates/j2k-jpeg/benches")).unwrap(); - fs::create_dir_all(target.join("crates/j2k-cuda/benches")).unwrap(); - fs::create_dir_all(target.join("crates/j2k-native/benches")).unwrap(); - fs::create_dir_all(target.join("crates/j2k-native/fixtures/htj2k")).unwrap(); - fs::write( - source.join(jpeg_manifest), - "[package]\nname = \"j2k-jpeg\"\n\n[[bench]]\nname = \"encode_cpu\"\nharness = false\n", - ) - .unwrap(); - fs::write( - target.join(jpeg_manifest), - "[package]\nname = \"j2k-jpeg\"\n", - ) - .unwrap(); - fs::write( - target.join(cuda_manifest), - "[package]\nname = \"j2k-cuda\"\n", - ) - .unwrap(); - fs::write(source.join(public_bench), "current public bench").unwrap(); - fs::write(source.join(jpeg_encode_bench), "current jpeg encode bench").unwrap(); - fs::write(source.join(cuda_decode_bench), "current cuda decode bench").unwrap(); - fs::write(source.join(cuda_encode_bench), "current cuda encode bench").unwrap(); - fs::write(source.join(native_bench), "current native bench").unwrap(); - fs::write(source.join(native_sigprop_bench), "current sigprop bench").unwrap(); - fs::write(source.join(native_fixture), "current fixture").unwrap(); - fs::write(target.join(public_bench), "old public bench").unwrap(); - fs::write(target.join(jpeg_encode_bench), "old jpeg encode bench").unwrap(); - fs::write(target.join(cuda_decode_bench), "old cuda decode bench").unwrap(); - fs::write(target.join(cuda_encode_bench), "old cuda encode bench").unwrap(); - fs::write(target.join(native_bench), "old native bench").unwrap(); - fs::write(target.join(native_sigprop_bench), "old sigprop bench").unwrap(); - fs::write(target.join(native_fixture), "old fixture").unwrap(); + for (path, current) in [ + (public_bench, "current public bench"), + (jpeg_encode_bench, "current jpeg encode bench"), + (cuda_decode_bench, "current cuda decode bench"), + (cuda_encode_bench, "current cuda encode bench"), + (metal_bench, "current metal bench"), + (native_bench, "current native bench"), + (native_sigprop_bench, "current sigprop bench"), + (native_fixture, "current fixture"), + ] { + write_fixture(&source, path, current); + write_fixture(&target, path, "old fixture content"); + } + for (path, package) in [ + ("crates/j2k-jpeg/Cargo.toml", "j2k-jpeg"), + ("crates/j2k-cuda/Cargo.toml", "j2k-cuda"), + ("crates/j2k-jpeg-metal/Cargo.toml", "j2k-jpeg-metal"), + ] { + write_fixture(&target, path, &format!("[package]\nname = \"{package}\"\n")); + } sync_benchmark_sources(&source, &target).unwrap(); @@ -413,7 +393,8 @@ some other output: 12 passed fs::read_to_string(target.join(jpeg_encode_bench)).unwrap(), "current jpeg encode bench" ); - let target_jpeg_manifest = fs::read_to_string(target.join(jpeg_manifest)).unwrap(); + let target_jpeg_manifest = + fs::read_to_string(target.join("crates/j2k-jpeg/Cargo.toml")).unwrap(); assert!( target_jpeg_manifest.contains("name = \"encode_cpu\"") && target_jpeg_manifest.contains("harness = false"), @@ -427,13 +408,24 @@ some other output: 12 passed fs::read_to_string(target.join(cuda_encode_bench)).unwrap(), "current cuda encode bench" ); - let target_cuda_manifest = fs::read_to_string(target.join(cuda_manifest)).unwrap(); + let target_cuda_manifest = + fs::read_to_string(target.join("crates/j2k-cuda/Cargo.toml")).unwrap(); assert!( target_cuda_manifest.contains("name = \"htj2k_decode\"") && target_cuda_manifest.contains("name = \"htj2k_encode\"") && target_cuda_manifest.contains("required-features = [\"cuda-runtime\"]"), "baseline overlay must register CUDA HTJ2K Criterion benches" ); + assert_eq!( + fs::read_to_string(target.join(metal_bench)).unwrap(), + "current metal bench" + ); + assert!( + fs::read_to_string(target.join("crates/j2k-jpeg-metal/Cargo.toml")) + .unwrap() + .contains("name = \"compare\""), + "baseline overlay must register the Metal Criterion bench" + ); assert_eq!( fs::read_to_string(target.join(native_bench)).unwrap(), "current native bench" @@ -449,6 +441,12 @@ some other output: 12 passed let _ = fs::remove_dir_all(root); } + fn write_fixture(root: &std::path::Path, relative: &str, contents: &str) { + let path = root.join(relative); + fs::create_dir_all(path.parent().unwrap()).unwrap(); + fs::write(path, contents).unwrap(); + } + fn temp_dir(name: &str) -> std::path::PathBuf { let mut dir = std::env::temp_dir(); let nanos = SystemTime::now() diff --git a/xtask/src/metal.rs b/xtask/src/metal.rs index c3d38e7e..ac9b7f0a 100644 --- a/xtask/src/metal.rs +++ b/xtask/src/metal.rs @@ -4,6 +4,7 @@ use std::{collections::BTreeSet, env}; +use crate::gpu_validation::ValidationMode; use crate::process::{self, cargo, CommandContext}; const GPU_TEST_SKIP_MARKER: &str = "J2K_GPU_TEST_SKIPPED"; @@ -138,16 +139,24 @@ fn run_metal_compile() -> Result<(), String> { } /// Runs every required Metal runtime suite and rejects all evidence of skipping. -pub(crate) fn release_metal() -> Result<(), String> { +pub(crate) fn release_metal(args: impl Iterator) -> Result<(), String> { require_macos("release-metal")?; - run_release_metal() + run_release_metal(ValidationMode::parse(args)?) } -fn run_release_metal() -> Result<(), String> { - validate_required_ignored_inventory()?; +fn run_release_metal(mode: ValidationMode) -> Result<(), String> { + if mode == ValidationMode::Quick { + let output = run_cargo_captured( + &quick_clippy_args(), + METAL_RUNTIME_ENV, + "quick Metal production-feature Clippy", + )?; + reject_skip_markers(&output, "quick Metal production-feature Clippy")?; + } + validate_required_ignored_inventory(mode)?; for suite in METAL_TEST_SUITES { - let args = runtime_suite_args(suite.package); + let args = runtime_suite_args(suite.package, mode); let output = run_cargo_captured(&args, METAL_RUNTIME_ENV, suite.label)?; validate_test_run( &output, @@ -157,16 +166,16 @@ fn run_release_metal() -> Result<(), String> { )?; } - let mut ignored_args = vec![ - "test", - "--release", + let mut ignored_args = vec!["test"]; + ignored_args.extend_from_slice(mode.cargo_profile_args()); + ignored_args.extend_from_slice(&[ "-p", "j2k-metal", "--lib", "--", "--ignored", "--show-output", - ]; + ]); for optional_test in METAL_OPTIONAL_IGNORED_TESTS { ignored_args.extend_from_slice(&["--skip", optional_test]); } @@ -178,10 +187,26 @@ fn run_release_metal() -> Result<(), String> { validate_exact_ignored_run(&output) } -fn runtime_suite_args(package: &str) -> Vec<&str> { - let mut args = vec!["test", "--release", "--all-features"]; +fn quick_clippy_args() -> Vec<&'static str> { + let mut args = vec!["clippy", "--profile", "gpu-quick", "--all-targets"]; + append_packages(&mut args); + args.extend_from_slice(&["--features", "j2k-ml/metal"]); + args.extend_from_slice(&["--", "-D", "warnings"]); + args +} + +fn runtime_suite_args(package: &str, mode: ValidationMode) -> Vec<&str> { + let mut args = vec!["test"]; + args.extend_from_slice(mode.cargo_profile_args()); + if mode == ValidationMode::Full { + args.push("--all-features"); + } args.extend_from_slice(METAL_RUNTIME_TEST_TARGETS); - args.extend_from_slice(&["-p", package, "--", "--show-output"]); + args.extend_from_slice(&["-p", package]); + if mode == ValidationMode::Quick && package == "j2k-ml" { + args.extend_from_slice(&["--features", "metal"]); + } + args.extend_from_slice(&["--", "--show-output"]); args } @@ -202,17 +227,10 @@ fn append_packages(args: &mut Vec<&str>) { } } -fn validate_required_ignored_inventory() -> Result<(), String> { - let args = [ - "test", - "--release", - "-p", - "j2k-metal", - "--lib", - "--", - "--ignored", - "--list", - ]; +fn validate_required_ignored_inventory(mode: ValidationMode) -> Result<(), String> { + let mut args = vec!["test"]; + args.extend_from_slice(mode.cargo_profile_args()); + args.extend_from_slice(&["-p", "j2k-metal", "--lib", "--", "--ignored", "--list"]); let output = run_cargo_captured( &args, METAL_RUNTIME_ENV, diff --git a/xtask/src/metal/tests.rs b/xtask/src/metal/tests.rs index 6cfac026..a77e7bd7 100644 --- a/xtask/src/metal/tests.rs +++ b/xtask/src/metal/tests.rs @@ -8,6 +8,7 @@ use super::{ run_metal_compile, run_release_metal, runtime_suite_args, validate_exact_ignored_run, J2K_METAL_REQUIRED_IGNORED_TESTS, METAL_OPTIONAL_IGNORED_TESTS, }; +use crate::gpu_validation::ValidationMode; fn recording_metal_cargo() -> RecordingProgram { let listed = J2K_METAL_REQUIRED_IGNORED_TESTS @@ -67,12 +68,12 @@ fn metal_commands_execute_complete_hermetic_compile_and_release_plans() { if cfg!(target_os = "macos") { metal_compile().expect("hermetic Metal compile plan"); - release_metal().expect("hermetic Metal release plan"); + release_metal(std::iter::empty()).expect("hermetic Metal release plan"); } else { run_metal_compile().expect("platform-independent Metal compile plan"); - run_release_metal().expect("platform-independent Metal release plan"); + run_release_metal(ValidationMode::Full).expect("platform-independent Metal release plan"); assert!(metal_compile().is_err()); - assert!(release_metal().is_err()); + assert!(release_metal(std::iter::empty()).is_err()); } let log = recording.log(); @@ -89,6 +90,26 @@ fn metal_commands_execute_complete_hermetic_compile_and_release_plans() { assert!(log.contains("-p j2k-ml")); } +#[test] +fn metal_quick_preserves_every_runtime_and_ignored_inventory_gate() { + let recording = recording_metal_cargo(); + let _cargo = use_test_cargo_program(recording.program().as_os_str().to_owned()); + + run_release_metal(ValidationMode::Quick).expect("quick Metal command plan"); + + let log = recording.log(); + assert_eq!(log.lines().count(), 9); + assert!(log.lines().all(|line| line.contains("--profile gpu-quick"))); + assert_eq!(log.matches("clippy --profile gpu-quick").count(), 1); + assert!(!log.contains("--all-features")); + assert!(!log.contains("cuda")); + assert!(log.contains("--features j2k-ml/metal")); + assert!(log.contains("-p j2k-ml --features metal")); + assert!(log.contains("--ignored --list")); + assert!(log.contains("--ignored --show-output")); + assert!(!log.contains("--release")); +} + #[test] fn parses_listed_and_passed_rust_tests() { let listed = "alpha::works: test\nbeta::works: test\n"; @@ -142,7 +163,7 @@ fn ignored_inventory_is_unique_and_has_expected_size() { #[test] fn runtime_gate_excludes_benchmark_targets() { - let args = runtime_suite_args("j2k-metal"); + let args = runtime_suite_args("j2k-metal", ValidationMode::Full); assert!(args.contains(&"--lib")); assert!(args.contains(&"--bins")); assert!(args.contains(&"--tests")); @@ -150,3 +171,17 @@ fn runtime_gate_excludes_benchmark_targets() { assert!(!args.contains(&"--all-targets")); assert!(!args.contains(&"--benches")); } + +#[test] +fn quick_metal_runtime_uses_fast_profile_without_weakening_target_scope() { + let args = runtime_suite_args("j2k-metal", ValidationMode::Quick); + assert!(args + .windows(2) + .any(|pair| pair == ["--profile", "gpu-quick"])); + assert!(!args.contains(&"--release")); + assert!(!args.contains(&"--all-features")); + assert!(args.contains(&"--lib")); + assert!(args.contains(&"--bins")); + assert!(args.contains(&"--tests")); + assert!(args.contains(&"--examples")); +} diff --git a/xtask/src/perf_guard.rs b/xtask/src/perf_guard.rs index 1eeb1fb9..41acb37b 100644 --- a/xtask/src/perf_guard.rs +++ b/xtask/src/perf_guard.rs @@ -5,6 +5,10 @@ use std::{ path::{Path, PathBuf}, }; +use crate::benchmark_registry::{ + BenchmarkLane as PerfLane, PerformanceBenchmark as BenchCommand, + PERFORMANCE_BENCHMARKS as BENCH_COMMANDS, +}; use crate::process::{self, cargo, CommandContext}; #[derive(Debug, Clone, PartialEq)] @@ -31,6 +35,7 @@ struct PerfGuardOptions { mode: PerfGuardMode, threshold_percent: f64, quick: bool, + lane: PerfLane, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -40,15 +45,6 @@ enum PerfGuardMode { CompareCurrent { name: String }, } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -struct BenchCommand { - package: &'static str, - bench: &'static str, - filter: Option<&'static str>, - features: Option<&'static str>, - env: &'static [(&'static str, &'static str)], -} - #[derive(Debug, Clone, Copy, PartialEq, Eq)] struct BenchManifestStanza { path: &'static str, @@ -67,78 +63,7 @@ const BENCH_PACKAGE_MANIFESTS: &[&str] = &[ "crates/j2k-jpeg/Cargo.toml", "crates/j2k-native/Cargo.toml", "crates/j2k-cuda/Cargo.toml", -]; -const BENCH_COMMANDS: &[BenchCommand] = &[ - BenchCommand { - package: "j2k", - bench: "public_api", - filter: None, - features: None, - env: &[], - }, - BenchCommand { - package: "j2k-jpeg", - bench: "encode_cpu", - filter: Some("jpeg_cpu_encode_runtime/"), - features: None, - env: &[], - }, - BenchCommand { - package: "j2k-native", - bench: "tier1_bitplane", - filter: Some("htj2k_cleanup_decode/"), - features: None, - env: &[], - }, - BenchCommand { - package: "j2k-native", - bench: "tier1_bitplane", - filter: Some("htj2k_refinement_fixture_decode"), - features: None, - env: &[], - }, - BenchCommand { - package: "j2k-native", - bench: "tier1_bitplane", - filter: Some("htj2k_refinement_block_decode"), - features: None, - env: &[], - }, - BenchCommand { - package: "j2k-native", - bench: "tier1_bitplane", - filter: Some("htj2k_cleanup_encode/"), - features: None, - env: &[], - }, - BenchCommand { - package: "j2k-native", - bench: "tier1_bitplane", - filter: Some("htj2k_cleanup_encode_distribution"), - features: None, - env: &[], - }, - BenchCommand { - package: "j2k-native", - bench: "htj2k_sigprop_phase", - filter: None, - features: None, - env: &[], - }, - BenchCommand { - package: "j2k-cuda", - bench: "htj2k_decode", - filter: Some("j2k_cuda_htj2k_"), - features: Some("cuda-runtime"), - env: &[], - }, - BenchCommand { - package: "j2k-cuda", - bench: "htj2k_encode", - filter: Some("j2k_cuda_htj2k_"), - features: Some("cuda-runtime"), - env: &[], - }, + "crates/j2k-jpeg-metal/Cargo.toml", ]; const BENCH_SOURCE_FILES: &[&str] = &[ "crates/j2k/benches/public_api.rs", @@ -148,6 +73,7 @@ const BENCH_SOURCE_FILES: &[&str] = &[ "crates/j2k-native/fixtures/htj2k/openhtj2k_ds0_ht_09_b11.j2k", "crates/j2k-cuda/benches/htj2k_decode.rs", "crates/j2k-cuda/benches/htj2k_encode.rs", + "crates/j2k-jpeg-metal/benches/compare.rs", ]; const BENCH_MANIFEST_STANZAS: &[BenchManifestStanza] = &[ BenchManifestStanza { @@ -165,12 +91,20 @@ const BENCH_MANIFEST_STANZAS: &[BenchManifestStanza] = &[ bench_name: "htj2k_encode", stanza: "[[bench]]\nname = \"htj2k_encode\"\nharness = false\nrequired-features = [\"cuda-runtime\"]\n", }, + BenchManifestStanza { + path: "crates/j2k-jpeg-metal/Cargo.toml", + bench_name: "compare", + stanza: "[[bench]]\nname = \"compare\"\nharness = false\n", + }, ]; pub(crate) fn j2k_perf_guard(args: impl Iterator) -> Result<(), String> { let options = PerfGuardOptions::parse(args)?; let root = repo_root()?; - let perf_root = root.join("target").join("j2k-perf"); + let perf_root = root + .join("target") + .join("j2k-perf") + .join(options.lane.as_str()); fs::create_dir_all(&perf_root) .map_err(|err| format!("failed to create {}: {err}", perf_root.display()))?; @@ -186,8 +120,15 @@ pub(crate) fn j2k_perf_guard(args: impl Iterator) -> Result<(), S reset_dir(&baseline_target)?; reset_dir(¤t_target)?; - run_benches(&baseline_worktree, &baseline_target, options.quick)?; - run_benches(&root, ¤t_target, options.quick)?; + build_benches(&baseline_worktree, &baseline_target, options.lane)?; + build_benches(&root, ¤t_target, options.lane)?; + run_benches( + &baseline_worktree, + &baseline_target, + options.quick, + options.lane, + )?; + run_benches(&root, ¤t_target, options.quick, options.lane)?; let baseline = discover_estimates(&baseline_target.join("criterion"))?; let current = discover_estimates(¤t_target.join("criterion"))?; @@ -196,7 +137,8 @@ pub(crate) fn j2k_perf_guard(args: impl Iterator) -> Result<(), S PerfGuardMode::RecordCurrent { name } => { let target = perf_root.join("current-record-target"); reset_dir(&target)?; - run_benches(&root, &target, options.quick)?; + build_benches(&root, &target, options.lane)?; + run_benches(&root, &target, options.quick, options.lane)?; let estimates = discover_estimates(&target.join("criterion"))?; let snapshot = current_snapshot_path(&perf_root, name)?; write_estimate_snapshot(&snapshot, &estimates)?; @@ -211,7 +153,8 @@ pub(crate) fn j2k_perf_guard(args: impl Iterator) -> Result<(), S let baseline = read_estimate_snapshot(&snapshot)?; let target = perf_root.join("current-compare-target"); reset_dir(&target)?; - run_benches(&root, &target, options.quick)?; + build_benches(&root, &target, options.lane)?; + run_benches(&root, &target, options.quick, options.lane)?; let current = discover_estimates(&target.join("criterion"))?; compare_estimates(&baseline, ¤t, options.threshold_percent)? } @@ -662,8 +605,13 @@ fn emit_report(outcomes: &[RegressionOutcome], threshold_percent: f64) { } } -fn run_benches(workdir: &Path, target_dir: &Path, quick: bool) -> Result<(), String> { - for bench in BENCH_COMMANDS { +fn run_benches( + workdir: &Path, + target_dir: &Path, + quick: bool, + lane: PerfLane, +) -> Result<(), String> { + for bench in BENCH_COMMANDS.iter().filter(|bench| bench.lane == lane) { let args = bench_args(*bench, quick); process::run_command( cargo(), @@ -677,6 +625,30 @@ fn run_benches(workdir: &Path, target_dir: &Path, quick: bool) -> Result<(), Str Ok(()) } +fn build_benches(workdir: &Path, target_dir: &Path, lane: PerfLane) -> Result<(), String> { + for bench in BENCH_COMMANDS.iter().filter(|bench| bench.lane == lane) { + process::run_command( + cargo(), + &bench_build_args(*bench), + CommandContext::new() + .current_dir(workdir) + .target_dir(target_dir) + .envs(bench.env), + )?; + } + Ok(()) +} + +fn bench_build_args(bench: BenchCommand) -> Vec<&'static str> { + let mut args = vec!["bench", "-p", bench.package, "--bench", bench.bench]; + if let Some(features) = bench.features { + args.push("--features"); + args.push(features); + } + args.push("--no-run"); + args +} + fn bench_args(bench: BenchCommand, quick: bool) -> Vec<&'static str> { let mut args = vec!["bench", "-p", bench.package, "--bench", bench.bench]; if let Some(features) = bench.features { @@ -736,19 +708,39 @@ fn repo_root() -> Result { impl PerfGuardOptions { fn parse(mut args: impl Iterator) -> Result { + let mut lane_set = false; let mut options = Self { mode: PerfGuardMode::GitRef { baseline_ref: DEFAULT_BASELINE_REF.to_string(), }, threshold_percent: DEFAULT_THRESHOLD_PERCENT, quick: false, + lane: PerfLane::Host, }; while let Some(arg) = args.next() { match arg.as_str() { + "--lane" => { + if lane_set { + return Err("--lane may be specified only once".to_string()); + } + let value = args + .next() + .ok_or_else(|| "--lane requires host, cuda, or metal".to_string())?; + options.lane = match PerfLane::parse(&value)? { + PerfLane::All => { + return Err( + "performance lane must be exactly host, cuda, or metal".to_string() + ) + } + lane => lane, + }; + lane_set = true; + } "--baseline-ref" => { let baseline_ref = args .next() .ok_or_else(|| "--baseline-ref requires a value".to_string())?; + validate_commit_sha(&baseline_ref)?; options.set_mode(PerfGuardMode::GitRef { baseline_ref })?; } "--record-current" => { @@ -786,6 +778,9 @@ impl PerfGuardOptions { } } } + if !lane_set { + return Err(format!("--lane is required\n{}", help_text())); + } Ok(options) } @@ -803,7 +798,15 @@ impl PerfGuardOptions { } fn help_text() -> String { - "usage: cargo xtask j2k-perf-guard [--baseline-ref REF | --record-current NAME | --compare-current NAME] [--threshold-percent N] [--quick]".to_string() + "usage: cargo xtask j2k-perf-guard --lane host|cuda|metal [--baseline-ref COMMIT_SHA | --record-current NAME | --compare-current NAME] [--threshold-percent N] [--quick]".to_string() +} + +fn validate_commit_sha(value: &str) -> Result<(), String> { + if value.len() == 40 && value.bytes().all(|byte| byte.is_ascii_hexdigit()) { + Ok(()) + } else { + Err("--baseline-ref must be a full 40-character commit SHA".to_string()) + } } fn path_str(path: &Path) -> Result<&str, String> { @@ -814,17 +817,21 @@ fn path_str(path: &Path) -> Result<&str, String> { #[cfg(test)] mod tests { use super::{ - bench_args, compare_estimates, is_enforced_perf_id, read_estimate_snapshot, - validate_baseline_layout, write_estimate_snapshot, BenchCommand, BenchEstimate, - BenchManifestStanza, PerfGuardMode, PerfGuardOptions, BENCH_COMMANDS, - BENCH_PACKAGE_MANIFESTS, DEFAULT_BASELINE_REF, + bench_args, bench_build_args, compare_estimates, is_enforced_perf_id, + read_estimate_snapshot, validate_baseline_layout, write_estimate_snapshot, BenchCommand, + BenchEstimate, BenchManifestStanza, PerfGuardMode, PerfGuardOptions, PerfLane, + BENCH_COMMANDS, BENCH_PACKAGE_MANIFESTS, DEFAULT_BASELINE_REF, }; + use crate::benchmark_registry::CUDA_BENCH_ENV; use std::fs; use std::time::{SystemTime, UNIX_EPOCH}; #[test] - fn perf_guard_defaults_to_documented_immutable_baseline() { - let options = PerfGuardOptions::parse(std::iter::empty()).unwrap(); + fn perf_guard_requires_lane_and_defaults_to_documented_immutable_baseline() { + assert!(PerfGuardOptions::parse(std::iter::empty()).is_err()); + let options = + PerfGuardOptions::parse(["--lane".to_string(), "host".to_string()].into_iter()) + .unwrap(); assert_eq!( options.mode, @@ -832,10 +839,25 @@ mod tests { baseline_ref: "29143c8e1f00bbbe9cf5ab37b3cea882f6d52139".to_string(), } ); + assert_eq!(options.lane, PerfLane::Host); assert_eq!( DEFAULT_BASELINE_REF, "29143c8e1f00bbbe9cf5ab37b3cea882f6d52139" ); + + for mutable_or_abbreviated in ["main", "v0.7.3", "29143c8e"] { + let error = PerfGuardOptions::parse( + [ + "--lane".to_string(), + "host".to_string(), + "--baseline-ref".to_string(), + mutable_or_abbreviated.to_string(), + ] + .into_iter(), + ) + .unwrap_err(); + assert!(error.contains("full 40-character commit SHA")); + } } #[test] @@ -868,9 +890,15 @@ mod tests { #[test] fn perf_guard_parses_current_tree_record_mode() { let options = PerfGuardOptions::parse( - ["--record-current", "htj2k-roi-baseline", "--quick"] - .into_iter() - .map(str::to_string), + [ + "--lane", + "host", + "--record-current", + "htj2k-roi-baseline", + "--quick", + ] + .into_iter() + .map(str::to_string), ) .unwrap(); @@ -887,6 +915,8 @@ mod tests { fn perf_guard_parses_current_tree_compare_mode() { let options = PerfGuardOptions::parse( [ + "--lane", + "cuda", "--compare-current", "htj2k-roi-baseline", "--threshold-percent", @@ -903,15 +933,23 @@ mod tests { name: "htj2k-roi-baseline".to_string() } ); + assert_eq!(options.lane, PerfLane::Cuda); assert!((options.threshold_percent - 7.5).abs() < f64::EPSILON); } #[test] fn perf_guard_rejects_multiple_baseline_modes() { let error = PerfGuardOptions::parse( - ["--record-current", "one", "--compare-current", "two"] - .into_iter() - .map(str::to_string), + [ + "--lane", + "host", + "--record-current", + "one", + "--compare-current", + "two", + ] + .into_iter() + .map(str::to_string), ) .unwrap_err(); @@ -921,6 +959,29 @@ mod tests { ); } + #[test] + fn accelerator_lanes_require_hardware_and_never_share_commands() { + let cuda = BENCH_COMMANDS + .iter() + .filter(|command| command.lane == PerfLane::Cuda) + .collect::>(); + let metal = BENCH_COMMANDS + .iter() + .filter(|command| command.lane == PerfLane::Metal) + .collect::>(); + + assert!(!cuda.is_empty()); + assert!(!metal.is_empty()); + assert!(cuda.iter().all(|command| { + command.env.contains(&("J2K_REQUIRE_CUDA_BENCH", "1")) + && !command.package.contains("metal") + })); + assert!(metal.iter().all(|command| { + command.env.contains(&("J2K_REQUIRE_METAL_BENCH", "1")) + && !command.package.contains("cuda") + })); + } + #[test] fn benchmark_manifest_stanza_accepts_additional_bench_keys() { let root = temp_dir("j2k-perf-bench-stanza-test"); @@ -1045,6 +1106,7 @@ mod tests { bench: "encode_cpu", filter: Some("jpeg_cpu_encode_runtime/"), features: None, + lane: PerfLane::Host, env: &[], }, BenchCommand { @@ -1052,6 +1114,7 @@ mod tests { bench: "tier1_bitplane", filter: Some("htj2k_cleanup_encode/"), features: None, + lane: PerfLane::Host, env: &[], }, BenchCommand { @@ -1059,6 +1122,7 @@ mod tests { bench: "tier1_bitplane", filter: Some("htj2k_cleanup_decode/"), features: None, + lane: PerfLane::Host, env: &[], }, BenchCommand { @@ -1066,6 +1130,7 @@ mod tests { bench: "htj2k_sigprop_phase", filter: None, features: None, + lane: PerfLane::Host, env: &[], }, ]; @@ -1139,6 +1204,7 @@ mod tests { bench: "tier1_bitplane", filter: Some("htj2k_cleanup_encode/"), features: None, + lane: PerfLane::Host, env: &[], }; @@ -1164,6 +1230,7 @@ mod tests { bench: "htj2k_encode", filter: Some("j2k_cuda_htj2k_"), features: Some("cuda-runtime"), + lane: PerfLane::Cuda, env: &[], }; @@ -1184,6 +1251,32 @@ mod tests { ); } + #[test] + fn benchmark_prebuild_never_passes_filters_or_measurement_flags() { + let command = BenchCommand { + package: "j2k-cuda", + bench: "htj2k_decode", + filter: Some("j2k_cuda_htj2k_"), + features: Some("cuda-runtime"), + lane: PerfLane::Cuda, + env: CUDA_BENCH_ENV, + }; + + assert_eq!( + bench_build_args(command), + vec![ + "bench", + "-p", + "j2k-cuda", + "--bench", + "htj2k_decode", + "--features", + "cuda-runtime", + "--no-run", + ] + ); + } + #[test] fn perf_guard_enforces_stable_htj2k_rows_only() { assert!(is_enforced_perf_id( diff --git a/xtask/src/release_commands.rs b/xtask/src/release_commands.rs index dde13c05..057c1e30 100644 --- a/xtask/src/release_commands.rs +++ b/xtask/src/release_commands.rs @@ -435,9 +435,9 @@ fn validate_publish_workflow_source( for required in [ "--origin-url \"${origin_url}\"", "--server-url \"${GITHUB_SERVER_URL}\"", - "--ci-workflow ci.yml", - "--cuda-job \"CUDA API compatibility on x86_64\"", - "--metal-job \"Metal validation on Apple Silicon\"", + "--ci-workflow full-validation.yml", + "--cuda-job \"CUDA full release validation\"", + "--metal-job \"Metal full release validation\"", "cargo xtask release-integrity --publish", "scripts/publish-crate.sh --preflight-all", "python3 scripts/publish_release.py preflight", diff --git a/xtask/src/release_status.rs b/xtask/src/release_status.rs index 78f9112a..645ba231 100644 --- a/xtask/src/release_status.rs +++ b/xtask/src/release_status.rs @@ -6,12 +6,12 @@ use std::path::{Path, PathBuf}; use crate::process::{self, CommandContext}; -const CI_WORKFLOW: &str = "ci.yml"; +const CI_WORKFLOW: &str = "full-validation.yml"; const CI_BRANCH: &str = "main"; const RELEASE_CANDIDATE_JOB: &str = "Release candidate aggregate"; const GPU_WORKFLOW: &str = "gpu-validation.yml"; -const CUDA_JOB: &str = "CUDA API compatibility on x86_64"; -const METAL_JOB: &str = "Metal validation on Apple Silicon"; +const CUDA_JOB: &str = "CUDA full release validation"; +const METAL_JOB: &str = "Metal full release validation"; #[derive(Debug, Eq, PartialEq)] struct Options { diff --git a/xtask/src/release_status/tests.rs b/xtask/src/release_status/tests.rs index 8d2e1e1a..1865dc72 100644 --- a/xtask/src/release_status/tests.rs +++ b/xtask/src/release_status/tests.rs @@ -197,6 +197,7 @@ esac"#, assert!(lines[1] .contains("--candidate-sha aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa --token-env GH_TOKEN")); assert!(lines[1].contains("--aggregate-job Release candidate aggregate")); - assert!(lines[1].contains("--cuda-job CUDA API compatibility on x86_64")); - assert!(lines[1].contains("--metal-job Metal validation on Apple Silicon")); + assert!(lines[1].contains("--ci-workflow full-validation.yml")); + assert!(lines[1].contains("--cuda-job CUDA full release validation")); + assert!(lines[1].contains("--metal-job Metal full release validation")); } diff --git a/xtask/tests/repo_lint_support/audit_integrity_policy.rs b/xtask/tests/repo_lint_support/audit_integrity_policy.rs index 85aef0e6..a43413c2 100644 --- a/xtask/tests/repo_lint_support/audit_integrity_policy.rs +++ b/xtask/tests/repo_lint_support/audit_integrity_policy.rs @@ -80,7 +80,7 @@ fn clone_audit_is_source_aware_pinned_and_ci_required() { let tests = read("xtask/src/source_audit/tests.rs"); let config = read(".jscpd.json"); let test_config = read(".jscpd-tests.json"); - let workflow = read(".github/workflows/ci.yml"); + let workflow = read(".github/workflows/full-validation.yml"); let clone_job = workflow_job(&workflow, "clone-audit"); let aggregate = workflow_job(&workflow, "release-candidate"); let evidence = read("engineering/clone-audit.md"); diff --git a/xtask/tests/repo_lint_support/docs_and_workflows_policy/documentation_api_evidence.rs b/xtask/tests/repo_lint_support/docs_and_workflows_policy/documentation_api_evidence.rs index 8450c93b..2a23c387 100644 --- a/xtask/tests/repo_lint_support/docs_and_workflows_policy/documentation_api_evidence.rs +++ b/xtask/tests/repo_lint_support/docs_and_workflows_policy/documentation_api_evidence.rs @@ -33,10 +33,12 @@ fn ci_workflow_keeps_docs_and_benchmark_compile_gates() { let xtask = xtask_sources(root); assert_file_pattern_checks( root, - &[FilePatternCheck::new(".github/workflows/ci.yml") - .named("CI workflow docs and benchmark compile gates") - .required(&["cargo xtask doc", "cargo xtask bench-build"]) - .forbidden(&["macos-13"])], + &[ + FilePatternCheck::new(".github/workflows/full-validation.yml") + .named("CI workflow docs and benchmark compile gates") + .required(&["cargo xtask doc", "cargo xtask bench-build --lane host"]) + .forbidden(&["macos-13"]), + ], ); assert_pattern_checks(&[ PatternCheck::new("xtask benchmark compile gate", &xtask).required(&[ diff --git a/xtask/tests/repo_lint_support/docs_and_workflows_policy/stable_api_governance.rs b/xtask/tests/repo_lint_support/docs_and_workflows_policy/stable_api_governance.rs index 8a9dd813..7a4912dc 100644 --- a/xtask/tests/repo_lint_support/docs_and_workflows_policy/stable_api_governance.rs +++ b/xtask/tests/repo_lint_support/docs_and_workflows_policy/stable_api_governance.rs @@ -9,8 +9,8 @@ use crate::repo_lint_support::{ #[test] fn ci_stable_api_jobs_pin_inputs_and_never_write() { let root = repo_root(); - let workflow = - fs::read_to_string(root.join(".github/workflows/ci.yml")).expect("read CI workflow"); + let workflow = fs::read_to_string(root.join(".github/workflows/full-validation.yml")) + .expect("read full validation workflow"); let stable_api_job = workflow_job(&workflow, "stable-api"); let semver_job = workflow_job(&workflow, "semver"); let release_candidate_job = workflow_job(&workflow, "release-candidate"); diff --git a/xtask/tests/repo_lint_support/docs_and_workflows_policy/workflow_coverage_policy.rs b/xtask/tests/repo_lint_support/docs_and_workflows_policy/workflow_coverage_policy.rs index 95017f4e..cc0cd97a 100644 --- a/xtask/tests/repo_lint_support/docs_and_workflows_policy/workflow_coverage_policy.rs +++ b/xtask/tests/repo_lint_support/docs_and_workflows_policy/workflow_coverage_policy.rs @@ -96,8 +96,8 @@ fn benchmark_targets_are_not_test_targets() { #[test] fn xtask_exposes_nextest_machete_and_strict_clippy_gates() { let xtask = xtask_sources(repo_root()); - let workflow = - fs::read_to_string(repo_root().join(".github/workflows/ci.yml")).expect("read CI workflow"); + let workflow = fs::read_to_string(repo_root().join(".github/workflows/full-validation.yml")) + .expect("read full validation workflow"); let strict_clippy_job = workflow_job(&workflow, "clippy-strict"); let release_candidate_job = workflow_job(&workflow, "release-candidate"); let help_section = xtask @@ -187,8 +187,8 @@ fn xtask_fuzz_build_checks_every_fuzz_manifest() { fn ci_coverage_job_is_a_required_gate() { const INSTALL_ACTION_SHA: &str = "91534edaf9fd796a162759d80d49cdff574bff2c"; - let workflow = - fs::read_to_string(repo_root().join(".github/workflows/ci.yml")).expect("read CI workflow"); + let workflow = fs::read_to_string(repo_root().join(".github/workflows/full-validation.yml")) + .expect("read full validation workflow"); let coverage_job = workflow_job(&workflow, "coverage"); let install_action = format!("taiki-e/install-action@{INSTALL_ACTION_SHA}"); @@ -250,7 +250,7 @@ fn coverage_measures_accelerator_host_rust_with_narrow_test_backed_exclusions() "coverage-*-summary.json", "coverage-*-regions.json", ]), - FilePatternCheck::new(".github/workflows/ci.yml") + FilePatternCheck::new(".github/workflows/full-validation.yml") .named("host coverage artifacts") .required(&["coverage-host-regions.json"]), FilePatternCheck::new(".github/workflows/gpu-validation.yml") @@ -303,8 +303,8 @@ fn coverage_measures_accelerator_host_rust_with_narrow_test_backed_exclusions() fn self_hosted_accelerator_jobs_publish_distinct_coverage_evidence() { let workflow = fs::read_to_string(repo_root().join(".github/workflows/gpu-validation.yml")) .expect("read GPU validation workflow"); - let metal_job = workflow_job(&workflow, "metal-apple-silicon"); - let cuda_job = workflow_job(&workflow, "cuda-x86_64-compatibility"); + let metal_job = workflow_job(&workflow, "metal-full"); + let cuda_job = workflow_job(&workflow, "cuda-full"); assert_pattern_checks(&[ PatternCheck::new("GPU coverage baseline", &workflow).required(&[ @@ -320,7 +320,7 @@ fn self_hosted_accelerator_jobs_publish_distinct_coverage_evidence() { "fetch-depth: 0", "scripts/ensure-cargo-llvm-cov.sh", "cargo xtask coverage metal", - "name: j2k-metal-coverage", + "name: j2k-metal-full-coverage-${{ github.sha }}", "lcov-metal.info", "coverage-metal-summary.json", "if-no-files-found: error", @@ -331,7 +331,7 @@ fn self_hosted_accelerator_jobs_publish_distinct_coverage_evidence() { "fetch-depth: 0", "scripts/ensure-cargo-llvm-cov.sh", "cargo xtask coverage cuda", - "name: j2k-cuda-coverage", + "name: j2k-cuda-full-coverage-${{ github.sha }}", "lcov-cuda.info", "coverage-cuda-summary.json", "if-no-files-found: error", @@ -348,11 +348,7 @@ fn self_hosted_coverage_tool_bootstrap_is_pinned_and_non_privileged() { let bootstrap = fs::read_to_string(root.join("scripts/ensure-cargo-llvm-cov.sh")) .expect("read self-hosted coverage-tool bootstrap"); - for job_name in [ - "linux-ci", - "metal-apple-silicon", - "cuda-x86_64-compatibility", - ] { + for job_name in ["metal-full", "cuda-full"] { let job = workflow_job(&workflow, job_name); assert_pattern_checks(&[PatternCheck::new("self-hosted coverage bootstrap", job) .required(&["scripts/ensure-cargo-llvm-cov.sh"]) diff --git a/xtask/tests/repo_lint_support/j2k_ml_policy.rs b/xtask/tests/repo_lint_support/j2k_ml_policy.rs index 6c5719b3..fbf42c00 100644 --- a/xtask/tests/repo_lint_support/j2k_ml_policy.rs +++ b/xtask/tests/repo_lint_support/j2k_ml_policy.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -use std::fs; +use std::{fs, process::Command}; use super::{ assert_file_pattern_checks, assert_pattern_checks, repo_root, FilePatternCheck, PatternCheck, @@ -102,6 +102,57 @@ fn j2k_ml_stays_independent_experimental_and_explicitly_feature_gated() { ); } +#[test] +fn j2k_ml_quick_metal_feature_graph_excludes_cuda() { + let output = Command::new("cargo") + .args([ + "tree", + "--locked", + "-p", + "j2k-ml", + "--no-default-features", + "--features", + "metal", + "-e", + "features", + "--prefix", + "none", + ]) + .current_dir(repo_root()) + .output() + .expect("resolve the j2k-ml Metal feature graph"); + assert!( + output.status.success(), + "j2k-ml Metal feature resolution failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + + let graph = String::from_utf8(output.stdout).expect("Cargo feature graph is UTF-8"); + assert!( + graph.starts_with("j2k-ml v"), + "Cargo feature graph must be rooted at j2k-ml:\n{graph}" + ); + for forbidden in ["j2k-cuda", "j2k-cuda-runtime", "burn-cuda"] { + assert!( + !graph.contains(forbidden), + "j2k-ml's Metal-only feature graph unexpectedly contains {forbidden}:\n{graph}" + ); + } + + let manifest = fs::read_to_string(repo_root().join("crates/j2k-ml/Cargo.toml")) + .expect("read j2k-ml manifest"); + let metal_feature = manifest + .split("metal = [") + .nth(1) + .and_then(|rest| rest.split(']').next()) + .expect("j2k-ml metal feature declaration"); + assert!( + !metal_feature.contains("cuda"), + "j2k-ml's Metal feature must not enable CUDA: {metal_feature}" + ); +} + #[test] fn j2k_ml_uses_a_portable_arm_linux_test_backend() { assert_file_pattern_checks( diff --git a/xtask/tests/repo_lint_support/mod.rs b/xtask/tests/repo_lint_support/mod.rs index f4464450..5520a0bd 100644 --- a/xtask/tests/repo_lint_support/mod.rs +++ b/xtask/tests/repo_lint_support/mod.rs @@ -95,6 +95,7 @@ pub(crate) fn sha256_hex(path: &Path) -> String { pub(crate) fn xtask_sources(root: &Path) -> String { const SOURCES: &[&str] = &[ "xtask/src/main.rs", + "xtask/src/benchmark_registry.rs", "xtask/src/benchmark_commands.rs", "xtask/src/codegen_commands.rs", "xtask/src/codegen_commands/transaction.rs", diff --git a/xtask/tests/repo_lint_support/public_docs_policy.rs b/xtask/tests/repo_lint_support/public_docs_policy.rs index 314284bc..d651d096 100644 --- a/xtask/tests/repo_lint_support/public_docs_policy.rs +++ b/xtask/tests/repo_lint_support/public_docs_policy.rs @@ -350,7 +350,8 @@ fn benchmark_docs_define_publication_gate_for_openjpeg_and_grok() { let env_vars = fs::read_to_string(root.join("docs/env-vars.md")).expect("read env var docs"); let benchmark_docs = format!("{benchmark_corpora}\n{benchmark_evidence}\n{env_vars}"); let xtask = xtask_sources(root); - let ci = fs::read_to_string(root.join(".github/workflows/ci.yml")).expect("read CI workflow"); + let ci = fs::read_to_string(root.join(".github/workflows/full-validation.yml")) + .expect("read full validation workflow"); assert_pattern_checks(&[ PatternCheck::new("benchmark publication docs", &benchmark_docs).required(&[ @@ -389,50 +390,46 @@ fn benchmark_docs_define_publication_gate_for_openjpeg_and_grok() { } #[test] -fn adoption_starter_corpus_fallback_is_pinned() { +fn adoption_benchmarks_require_external_corpora_without_fallbacks() { let root = repo_root(); - let workflow = fs::read_to_string(root.join(".github/workflows/gpu-validation.yml")) - .expect("read GPU validation workflow"); + let workflow = fs::read_to_string(root.join(".github/workflows/gpu-benchmarks.yml")) + .expect("read GPU benchmark workflow"); let benchmark_docs = fs::read_to_string(root.join("docs/benchmark-corpora.md")) .expect("read benchmark corpus docs"); - let openjpeg_commit = "39524bd3a601d90ed8e0177559400d23945f96a9"; let workflow_required = [ - "sha256sum -c - <<'SHA256'".to_string(), - "a56e27cbf5f843c048b6af1d6e090760e9c92fadba88b7dee0205918a37523bd kodim01.png".to_string(), - "1071c68372cc5a01435c2c225a5cf7d4bb803846ec08bb6b3d6721b156d7cb96 kodim24.png".to_string(), - "downloaded-from-r0k-us-kodak-lossless-true-color-sha256-pinned".to_string(), - format!("J2K_STARTER_OPENJPEG_DATA_COMMIT={openjpeg_commit}"), - "git -C target/j2k-public-corpora/openjpeg-data fetch --depth 1 origin".to_string(), - "git -C target/j2k-public-corpora/openjpeg-data checkout --detach".to_string(), - format!("source-native-openjpeg-data-conformance-dir@{openjpeg_commit}"), - format!("source-native-openjpeg-data-nonregression-dir@{openjpeg_commit}"), + "suite == 'adoption'".to_string(), + ": \"${J2K_ADOPTION_FIXTURES:?Set J2K_ADOPTION_FIXTURES}\"".to_string(), + ": \"${J2K_ADOPTION_MANIFEST:?Set J2K_ADOPTION_MANIFEST}\"".to_string(), + ": \"${J2K_ADOPTION_ENCODE_FIXTURES:?Set J2K_ADOPTION_ENCODE_FIXTURES}\"".to_string(), + ": \"${J2K_ADOPTION_ENCODE_MANIFEST:?Set J2K_ADOPTION_ENCODE_MANIFEST}\"".to_string(), + "--require-cuda".to_string(), + "--require-metal".to_string(), ]; let workflow_required = workflow_required .iter() .map(String::as_str) .collect::>(); let benchmark_docs_required = [ - "sha256sum -c - <<'SHA256'", - openjpeg_commit, - "downloaded-from-r0k-us-kodak-lossless-true-color-sha256-pinned", - "OpenJPEG-data commit", - "SHA-256-checked Kodak PNGs", - "fixed OpenJPEG-data commit", + "dispatch `GPU benchmarks` with `suite=adoption`", + "fails closed when any variable is absent", + "does not synthesize or download a fallback corpus", + "--require-cuda", + "--require-metal", ]; assert_pattern_checks(&[ PatternCheck::new( - "GPU adoption fallback pinned starter corpus inputs", + "GPU adoption workflow strict external corpus inputs", &workflow, ) .required(&workflow_required) - .forbidden(&["git clone --depth 1 https://github.com/uclouvain/openjpeg-data"]), + .forbidden(&["git clone", "curl ", "wget "]), PatternCheck::new( - "benchmark corpus docs pinned starter corpus inputs", + "benchmark corpus docs strict corpus inputs", &benchmark_docs, ) - .required(&benchmark_docs_required), + .normalized_required(&benchmark_docs_required), ]); } diff --git a/xtask/tests/repo_lint_support/workflow_policy.rs b/xtask/tests/repo_lint_support/workflow_policy.rs index e376e014..9a6e2087 100644 --- a/xtask/tests/repo_lint_support/workflow_policy.rs +++ b/xtask/tests/repo_lint_support/workflow_policy.rs @@ -41,8 +41,8 @@ fn github_workflows_parse_as_yaml() { #[test] fn ci_miri_job_is_a_required_gate() { - let workflow = - fs::read_to_string(repo_root().join(".github/workflows/ci.yml")).expect("read CI workflow"); + let workflow = fs::read_to_string(repo_root().join(".github/workflows/full-validation.yml")) + .expect("read full validation workflow"); let miri_job = workflow_job(&workflow, "miri"); assert_pattern_checks(&[PatternCheck::new("CI miri job", miri_job).required(&[ @@ -56,14 +56,16 @@ fn ci_miri_job_is_a_required_gate() { fn ci_fuzz_run_budgets_are_nontrivial() { assert_file_pattern_checks( repo_root(), - &[FilePatternCheck::new(".github/workflows/ci.yml") - .named("CI fuzz budgets") - .required(&[ - "J2K_FUZZ_RUNS: \"512\"", - "J2K_FUZZ_MAX_TOTAL_TIME_SECONDS: \"60\"", - "J2K_FUZZ_RUNS: \"20000\"", - "J2K_FUZZ_MAX_TOTAL_TIME_SECONDS: \"900\"", - ])], + &[ + FilePatternCheck::new(".github/workflows/full-validation.yml") + .named("CI fuzz budgets") + .required(&[ + "J2K_FUZZ_RUNS: \"512\"", + "J2K_FUZZ_MAX_TOTAL_TIME_SECONDS: \"60\"", + "J2K_FUZZ_RUNS: \"20000\"", + "J2K_FUZZ_MAX_TOTAL_TIME_SECONDS: \"900\"", + ]), + ], ); } @@ -142,11 +144,15 @@ fn unsafe_audit_rows_include_invariants_and_regression_guards() { } #[test] -fn ci_workflow_has_read_only_permissions_and_gpu_path_policy() { +fn ci_workflow_has_read_only_permissions_and_fail_closed_gpu_planning() { let root = repo_root(); let workflow = fs::read_to_string(root.join(".github/workflows/ci.yml")).expect("read CI workflow"); - let gpu_policy = workflow_job(&workflow, "gpu-path-policy"); + let planner = workflow_job(&workflow, "planner"); + let gpu_policy = workflow_job(&workflow, "gpu-evidence"); + let aggregate = workflow_job(&workflow, "pr-required-checks"); + let planner_script = + fs::read_to_string(root.join("scripts/ci_plan.py")).expect("read CI planner"); let verifier = fs::read_to_string(root.join("scripts/github_actions_verify.py")) .expect("read GitHub Actions verifier"); let codeowners = fs::read_to_string(root.join(".github/CODEOWNERS")).expect("read CODEOWNERS"); @@ -154,18 +160,43 @@ fn ci_workflow_has_read_only_permissions_and_gpu_path_policy() { assert_pattern_checks(&[ PatternCheck::new("CI workflow default permissions", &workflow) .normalized_required(&["permissions:\n contents: read"]), - PatternCheck::new("CI GPU path policy job", gpu_policy) + PatternCheck::new("CI dependency-aware planner", planner).required(&[ + "fetch-depth: 0", + "ref: ${{ github.event.pull_request.head.sha }}", + "python3 scripts/ci_plan.py plan", + "expectations: ${{ steps.plan.outputs.expectations }}", + ]), + PatternCheck::new("CI exact-SHA quick GPU evidence job", gpu_policy) .required(&[ - "pull-requests: read", "actions: read", "actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5", - "scripts/github_actions_verify.py pr-gpu-policy", "--repository \"${REPOSITORY}\"", - "--pr-number \"${PR_NUMBER}\"", - "--head-sha \"${HEAD_SHA}\"", "--workflow gpu-validation.yml", + "--sha \"${HEAD_SHA}\"", + "--event workflow_dispatch", + "CUDA quick validation", + "Metal quick validation", ]) .forbidden(&["urllib.request", "python3 <<'PY'"]), + PatternCheck::new("CI validated required-check aggregate", aggregate).required(&[ + "if: ${{ always() }}", + "python3 scripts/ci_plan.py aggregate", + "--expectations-json \"${EXPECTATIONS}\"", + "--always-required planner", + "--always-required secret-scan", + "--always-required gpu-evidence", + ]), + PatternCheck::new("repository-owned CI planner", &planner_script).required(&[ + "git", + "diff", + "--name-status", + "-z", + "cargo", + "metadata", + "validate_aggregate", + "required job", + "optional job", + ]), PatternCheck::new("repository-owned GitHub Actions verifier", &verifier).required(&[ "def fetch_pull_request_paths(", "def classify_gpu_paths(", @@ -177,8 +208,10 @@ fn ci_workflow_has_read_only_permissions_and_gpu_path_policy() { "def require_github_release_absent(", "def verify_candidate_evidence(", "def verify_release_evidence(", - "CUDA API compatibility on x86_64", - "Metal validation on Apple Silicon", + "CUDA quick validation", + "Metal quick validation", + "CUDA full release validation", + "Metal full release validation", "Release candidate aggregate", "workflow run pagination exceeded", "workflow job pagination exceeded", @@ -186,9 +219,13 @@ fn ci_workflow_has_read_only_permissions_and_gpu_path_policy() { ]), PatternCheck::new("CODEOWNERS GPU path coverage", &codeowners).required(&[ ".github/workflows/ci.yml", + ".github/workflows/full-validation.yml", + ".github/workflows/gpu-benchmarks.yml", ".github/workflows/gpu-validation.yml", ".github/workflows/publish.yml", "scripts/github_actions_verify.py", + "scripts/ci_plan.py", + "scripts/publish_release.py", "crates/j2k-cuda-runtime/", "crates/j2k-jpeg-cuda/", "crates/j2k-cuda/", @@ -208,7 +245,8 @@ fn ci_workflow_has_read_only_permissions_and_gpu_path_policy() { )] fn release_candidate_and_publish_evidence_are_fail_closed() { let root = repo_root(); - let ci = fs::read_to_string(root.join(".github/workflows/ci.yml")).expect("read CI workflow"); + let ci = fs::read_to_string(root.join(".github/workflows/full-validation.yml")) + .expect("read full validation workflow"); let secret_scan_workflow = fs::read_to_string(root.join(".github/workflows/secret-scan.yml")) .expect("read secret scan workflow"); let publish = fs::read_to_string(root.join(".github/workflows/publish.yml")) @@ -241,7 +279,6 @@ fn release_candidate_and_publish_evidence_are_fail_closed() { "name: Release candidate aggregate", "if: ${{ always() }}", "github-actions-verifier", - "gpu-path-policy", "fmt", "diff-check", "secret-scan", @@ -331,8 +368,9 @@ fn release_candidate_and_publish_evidence_are_fail_closed() { "GH_TOKEN", "GITHUB_TOKEN", "Release candidate aggregate", - "CUDA API compatibility on x86_64", - "Metal validation on Apple Silicon", + "full-validation.yml", + "CUDA full release validation", + "Metal full release validation", ]) .forbidden(&["verify-release", "--tag"]), PatternCheck::new("candidate private-reporting prerequisite", &verifier).required(&[ @@ -349,11 +387,11 @@ fn release_candidate_and_publish_evidence_are_fail_closed() { "scripts/github_actions_verify.py verify-release", "--origin-url \"${origin_url}\"", "--server-url \"${GITHUB_SERVER_URL}\"", - "--ci-workflow ci.yml", "--ci-branch main", "--aggregate-job \"Release candidate aggregate\"", - "--cuda-job \"CUDA API compatibility on x86_64\"", - "--metal-job \"Metal validation on Apple Silicon\"", + "--ci-workflow full-validation.yml", + "--cuda-job \"CUDA full release validation\"", + "--metal-job \"Metal full release validation\"", ]) .forbidden(&["inputs.dry-run-only"]), PatternCheck::new("publish preflight exact-SHA policy", preflight).required(&[ @@ -417,8 +455,14 @@ fn gpu_validation_workflow_is_self_hosted_and_explicit() { .named("GPU validation workflow") .required(&[ "workflow_dispatch", - "run-timed-benchmarks", - "run-metal-validation", + "target:", + "mode:", + "Validate GPU request", + "full GPU validation requires target=all", + "CUDA quick validation", + "Metal quick validation", + "CUDA full release validation", + "Metal full release validation", "self-hosted", "metal", "cuda", @@ -426,23 +470,42 @@ fn gpu_validation_workflow_is_self_hosted_and_explicit() { "J2K_REQUIRE_METAL_RUNTIME", "cargo xtask release-cuda", "cargo xtask release-metal", - "cargo run -p xtask --features adoption -- adoption-materialize", - "cargo run -p xtask --features adoption -- adoption-curate", - "cargo run -p xtask --features adoption -- adoption-benchmark", - "cargo run -p xtask --features adoption -- adoption-report", + "cargo xtask bench-build --lane cuda", + "cargo xtask bench-build --lane metal", ]) - .forbidden(&["cargo xtask adoption-"]), + .forbidden(&["pull_request:", "push:", "schedule:"]), + FilePatternCheck::new(".github/workflows/gpu-benchmarks.yml") + .named("isolated GPU benchmark workflow") + .required(&[ + "workflow_dispatch", + "Validate benchmark request", + "baseline must be a full 40-character commit SHA", + "group: gpu-benchmark-${{ inputs.lane }}", + "cancel-in-progress: false", + "baseline-ref:", + "profile-mode:", + "cargo xtask j2k-perf-guard", + "J2K_REQUIRE_CUDA_BENCH", + "J2K_REQUIRE_METAL_BENCH", + "adoption-benchmark", + ]) + .forbidden(&[ + "Prebuild selected CUDA lane", + "Prebuild selected Metal lane", + ]), FilePatternCheck::new("CONTRIBUTING.md") .named("contributor GPU validation policy") .required(&[ "The GPU validation workflow is intentionally `workflow_dispatch` only.", - "`CUDA API compatibility on x86_64`", - "`Metal validation on Apple Silicon`", + "`CUDA quick validation`", + "`Metal quick validation`", + "`CUDA full release validation`", + "`Metal full release validation`", ]) .normalized_required(&[ "It does not run automatically on `pull_request` or `push`", "successful manual `gpu-validation.yml` dispatch for the PR head SHA before merge", - "The normal CI `gpu-path-policy` job checks the PR diff", + "The normal CI planner checks the PR diff", "queries `gpu-validation.yml` runs by head SHA", "Hosted macOS CI runs `cargo xtask metal-compile`", "The self-hosted Metal job runs `cargo xtask release-metal`", @@ -451,6 +514,37 @@ fn gpu_validation_workflow_is_self_hosted_and_explicit() { ]), ], ); + + let validation = fs::read_to_string(repo_root().join(".github/workflows/gpu-validation.yml")) + .expect("read GPU validation workflow"); + for lane in ["cuda", "metal"] { + let shared_key = + format!("shared-key: gpu-{lane}-${{{{ runner.os }}}}-${{{{ runner.arch }}}}-1.96"); + assert_eq!( + validation.matches(&shared_key).count(), + 2, + "quick restore-only and trusted full {lane} writers must share one cache namespace" + ); + } + assert!(!validation.contains("-1.96-quick")); + assert!(!validation.contains("-1.96-full")); +} + +#[test] +fn strict_metal_benchmarks_require_a_real_default_device() { + assert_file_pattern_checks( + repo_root(), + &[ + FilePatternCheck::new("crates/j2k-jpeg-metal/benches/compare.rs") + .named("strict Metal benchmark availability") + .normalized_required(&[ + "let required = std::env::var_os(\"J2K_REQUIRE_METAL_BENCH\").is_some();", + "let available = metal::Device::system_default().is_some();", + "!required || available,", + "J2K_REQUIRE_METAL_BENCH is set but no default Metal device is available", + ]), + ], + ); } #[test] @@ -458,7 +552,11 @@ fn cuda_gpu_validation_job_stays_cuda_focused() { let root = repo_root(); let workflow_path = root.join(".github/workflows/gpu-validation.yml"); let workflow = fs::read_to_string(&workflow_path).expect("read GPU validation workflow"); - let cuda_job = workflow_job(&workflow, "cuda-x86_64-compatibility"); + let cuda_job = format!( + "{}\n{}", + workflow_job(&workflow, "cuda-quick"), + workflow_job(&workflow, "cuda-full") + ); let forbidden_j2k_metal_compare_bench = ["cargo bench -p ", "j2k-metal", " --bench compare --no-run"].concat(); @@ -478,30 +576,28 @@ fn cuda_gpu_validation_job_stays_cuda_focused() { "executed-count floor", "passed=$(echo", ]; - assert_pattern_checks(&[PatternCheck::new("CUDA GPU validation job", cuda_job) + assert_pattern_checks(&[PatternCheck::new("CUDA GPU validation jobs", &cuda_job) .required(&[ "runs-on: [self-hosted, Linux, X64, cuda]", "J2K_REQUIRE_CUDA_RUNTIME", "J2K_REQUIRE_CUDA_OXIDE_BUILD", "J2K_REQUIRE_CUDA_JPEG_HARDWARE_DECODE", - "J2K_GPU_BENCH_DIM", - "J2K_GPU_BENCH_BATCH", - "J2K_GPU_BENCH_BATCH_DIM", "uname -a", "rustc -Vv", "cargo -V", "nvidia-smi", - "CUDA runtime validation requires a working CUDA driver", - "Run fail-closed CUDA release validation", - "cargo xtask release-cuda", - "cargo bench -p j2k-jpeg-cuda --bench device_decode --features cuda-runtime --no-run", - "cargo bench -p j2k-jpeg-cuda --bench device_decode --features cuda-runtime -- --noplot", + "Run fail-closed quick CUDA validation", + "Run fail-closed full CUDA validation", + "cargo xtask release-cuda --mode quick", + "cargo xtask release-cuda --mode full", + "cargo xtask bench-build --lane cuda", + "cargo xtask coverage cuda", ]) .forbidden(&forbidden)]); assert_eq!( cuda_job.matches("cargo xtask release-cuda").count(), - 1, - "CUDA GPU validation must delegate exactly once to the repository-owned release gate" + 2, + "quick and full CUDA jobs must each delegate once to the repository-owned release gate" ); } @@ -531,16 +627,23 @@ fn metal_gpu_validation_job_fails_closed_and_stays_metal_focused() { let root = repo_root(); let workflow_path = root.join(".github/workflows/gpu-validation.yml"); let workflow = fs::read_to_string(&workflow_path).expect("read GPU validation workflow"); - let metal_job = workflow_job(&workflow, "metal-apple-silicon"); + let metal_job = format!( + "{}\n{}", + workflow_job(&workflow, "metal-quick"), + workflow_job(&workflow, "metal-full") + ); - assert_pattern_checks(&[PatternCheck::new("Metal GPU validation job", metal_job) + assert_pattern_checks(&[PatternCheck::new("Metal GPU validation jobs", &metal_job) .required(&[ "runs-on: [self-hosted, macOS, ARM64, metal]", "J2K_REQUIRE_METAL_RUNTIME: \"1\"", "RUST_TEST_THREADS: \"1\"", - "Run fail-closed Metal release validation", - "cargo xtask release-metal", - "cargo bench -p j2k-jpeg-metal --no-run", + "Run fail-closed quick Metal validation", + "Run fail-closed full Metal validation", + "cargo xtask release-metal --mode quick", + "cargo xtask release-metal --mode full", + "cargo xtask bench-build --lane metal", + "cargo xtask coverage metal", ]) .forbidden(&[ "nvidia-smi", @@ -577,7 +680,7 @@ fn metal_xtask_owns_complete_compile_and_runtime_policy() { "J2K_METAL_REQUIRED_IGNORED_TESTS", "METAL_OPTIONAL_IGNORED_TESTS", "fn run_metal_compile()", - "fn run_release_metal()", + "fn run_release_metal(mode: ValidationMode)", "validate_required_ignored_inventory", "validate_exact_ignored_run", "passed != J2K_METAL_REQUIRED_IGNORED_TESTS.len()", @@ -762,32 +865,28 @@ fn cuda_oxide_shared_strict_build_gate_is_wired_and_documented() { #[test] fn cuda_decode_profile_workflow_exports_rca_artifacts() { let root = repo_root(); - let workflow_path = root.join(".github/workflows/gpu-validation.yml"); - let workflow = fs::read_to_string(&workflow_path).expect("read GPU validation workflow"); - let cuda_job = workflow_job(&workflow, "cuda-x86_64-compatibility"); + let workflow_path = root.join(".github/workflows/gpu-benchmarks.yml"); + let workflow = fs::read_to_string(&workflow_path).expect("read GPU benchmark workflow"); + let cuda_job = workflow_job(&workflow, "cuda"); assert_pattern_checks(&[PatternCheck::new( "CUDA decode profile workflow/job", &format!("{workflow}\n{cuda_job}"), ) .required(&[ - "run-cuda-htj2k-decode-profile", - "CUDA HTJ2K decode RCA profile", + "Profile CUDA HTJ2K decode", "J2K_REQUIRE_CUDA_BENCH: \"1\"", "J2K_PROFILE_STAGES: summary", - "J2K_CUDA_TRACE: ${{ github.workspace }}/target/cuda_htj2k_decode_trace.json", - "/proc/sys/kernel/perf_event_paranoid", - "cargo install samply --version 0.13.1 --locked", - "samply record --save-only -o target/cuda_htj2k_decode_samply.json.gz", - "target/cuda_htj2k_decode_samply_status.txt", - "samply_status=blocked", - "passwordless sudo", + "J2K_CUDA_TRACE: ${{ github.workspace }}/target/gpu-benchmark/cuda-trace.json", + "group: gpu-benchmark-${{ inputs.lane }}", + "cancel-in-progress: false", + "device-before.txt", + "performance-state-before.txt", + "suite-parameters.txt", "--features cuda-runtime,cuda-profiling", - "2>&1 | tee target/cuda_htj2k_decode_profile.log", - "cuda-htj2k-decode-rca-profile", - "target/cuda_htj2k_decode_profile.log", - "target/cuda_htj2k_decode_trace.json", - "target/cuda_htj2k_decode_samply.json.gz", + "2>&1 | tee target/gpu-benchmark/profile.log", + "cuda-${{ inputs.suite }}-${{ github.sha }}", + "target/gpu-benchmark", "target/criterion", ])]); } diff --git a/xtask/tests/repo_lint_support/xtask_main_structure_policy.rs b/xtask/tests/repo_lint_support/xtask_main_structure_policy.rs index 94930db9..f3a83001 100644 --- a/xtask/tests/repo_lint_support/xtask_main_structure_policy.rs +++ b/xtask/tests/repo_lint_support/xtask_main_structure_policy.rs @@ -210,7 +210,7 @@ fn assert_dispatcher_and_command_ownership(sources: &XtaskSources) { "fn release_integrity(", ]), PatternCheck::new("xtask benchmark command ownership", &sources.benchmark).required(&[ - "pub(super) fn bench_build()", + "pub(super) fn bench_build(args:", "pub(super) fn j2k_bench_signoff()", "pub(super) fn bench_report(", "fn render_benchmark_report(",