diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36531a381..5d6aa6684 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -121,6 +121,10 @@ jobs: uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version-file: ".python-version" + - name: Detect CPU ISA features for cache key + id: cpu-id + shell: bash + run: echo "cpu-hash=$(python3 scripts/ci/cpu_isa_hash.py)" >> "$GITHUB_OUTPUT" - name: Restore Warp kernel cache if: github.event_name != 'merge_group' uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 @@ -129,9 +133,9 @@ jobs: ~/.cache/warp ~/Library/Caches/warp ~\AppData\Local\NVIDIA\warp\Cache - key: warp-kernels-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('uv.lock', 'newton/**/*.py') }} + key: warp-kernels-${{ runner.os }}-${{ runner.arch }}-${{ steps.cpu-id.outputs.cpu-hash }}-${{ hashFiles('uv.lock', 'newton/**/*.py') }} restore-keys: | - warp-kernels-${{ runner.os }}-${{ runner.arch }}- + warp-kernels-${{ runner.os }}-${{ runner.arch }}-${{ steps.cpu-id.outputs.cpu-hash }}- - name: Run Tests run: uv run --extra dev -m newton.tests --no-cache-clear --junit-report-xml rspec.xml --coverage --coverage-xml coverage.xml - name: Test Summary diff --git a/scripts/ci/cpu_isa_hash.py b/scripts/ci/cpu_isa_hash.py new file mode 100644 index 000000000..1d294cd5c --- /dev/null +++ b/scripts/ci/cpu_isa_hash.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 The Newton Developers +# SPDX-License-Identifier: Apache-2.0 + +"""Print a short hash of the host CPU's ISA feature set. + +Warp 1.13+ compiles CPU kernels with ``-march=native``, so cached object +files are only safe to reuse on a CPU with the same instruction set. +This script detects the ISA features and prints a 16-character hex hash +suitable for use in a CI cache key. + +Supported platforms: + - x86_64 Linux / macOS / Windows (via system C compiler) + - AArch64 Linux (via /proc/cpuinfo) + - AArch64 macOS (via sysctl) +""" + +import hashlib +import platform +import subprocess +import sys + +# ISA-related macro keywords emitted by ``cc -march=native -dM -E``. +# These correspond to the instruction set extensions that affect codegen. +_X86_ISA_KEYWORDS = ( + "ADX", + "AES", + "AVX", + "BMI", + "CLFLUSH", + "F16C", + "FMA", + "LZCNT", + "MMX", + "MOVBE", + "PCLMUL", + "POPCNT", + "RDRAND", + "RDSEED", + "SHA", + "SSE", + "VAES", + "VPCLMUL", +) + + +def _x86_features() -> str: + """Query ISA features by asking the system C compiler what -march=native enables.""" + for cc in ("cc", "gcc", "clang"): + try: + out = subprocess.check_output( + [cc, "-march=native", "-dM", "-E", "-x", "c", "-"], + input="", + text=True, + stderr=subprocess.DEVNULL, + ) + except (FileNotFoundError, subprocess.CalledProcessError): + continue + + macros = sorted( + line.split()[1] + for line in out.splitlines() + if line.startswith("#define __") and any(k in line for k in _X86_ISA_KEYWORDS) + ) + if macros: + return " ".join(macros) + + return "" + + +def _aarch64_features() -> str: + """Query ISA features from /proc/cpuinfo (Linux) or sysctl (macOS).""" + # Linux: kernel exposes HWCAP flags in /proc/cpuinfo. + try: + with open("/proc/cpuinfo") as f: + for line in f: + if line.startswith("Features"): + return " ".join(sorted(line.split(":")[1].split())) + except FileNotFoundError: + pass + + # macOS: sysctl exposes CPU features. + try: + out = subprocess.check_output( + ["sysctl", "-n", "machdep.cpu.features"], + text=True, + stderr=subprocess.DEVNULL, + ) + if out.strip(): + return " ".join(sorted(out.strip().split())) + except (FileNotFoundError, subprocess.CalledProcessError): + pass + + return "" + + +def get_features() -> str: + machine = platform.machine().lower() + if machine in ("x86_64", "amd64", "x86", "i686"): + return _x86_features() + if machine in ("aarch64", "arm64"): + return _aarch64_features() + return "" + + +def main() -> None: + features = get_features() or platform.processor() + h = hashlib.sha256(features.encode()).hexdigest()[:16] + print(f"features: {features}", file=sys.stderr) + print(h) + + +if __name__ == "__main__": + main()