how-to Intermediate

Caching Compiled Artifacts in CI

Skip the compile step on every repeated CI build: the verified pyvorin-native 1.0.9 cache locations, cache keys that actually hit, and the trap in cache clear.

Published Apr 13, 2026

Pyvorin recompiles a function only when its source, its options or the toolchain change — everything else is a cache lookup. In CI, where every job starts from a clean workspace and a fresh install, that lookup misses by default and each build pays the full compile cost, a few hundred milliseconds per function, again and again. Restoring the compile cache between runs turns that one-off cost into a rare one. This page gives you the verified cache locations for pyvorin-native 1.0.9, the cache keys that actually hit, and one trap in the built-in cache commands that will waste an afternoon if you do not know it in advance. Everything quoted was run or read against the installed build.

The older version of this page pointed CI at ~/.pyvorin/cache, a directory the product never creates. The real locations are different, and they matter, so we start there.

The two caches, precisely

The compile artefact cache lives inside the install tree, not the home directory:

<site-packages>/.pyvorin_cache/disk_compile/

On our machine that directory held 159 compiled shared objects named by SHA256 hash, plus a cache_index.json mapping each key to its artefact path and the optimisations applied. The cache is an LRU capped at 500 entries, and each entry is checksum-validated when loaded — a corrupted or tampered artefact is detected and discarded rather than executed. The key itself hashes the function's source, the function name, the compile options and the runtime and compiler modification times, which has a direct consequence for CI: reinstall or upgrade the package and every key changes, because the toolchain's mtimes changed. Your cache key must include the pyvorin version, or you will restore a cache that can never hit.

The second location appears the moment pyvorin is imported, before any licence activity:

~/.pyvorin_cache/columnar_kernels/

That one is created unconditionally at import time and is not the compile cache. Caching it buys you nothing on a cache key; leave it out.

How much the cache holds, and when it evicts

The compile cache is a bounded LRU: 500 entries, oldest evicted first when a compile pushes it past capacity. At our observed artefact sizes — on the order of kilobytes to low megabytes per entry — that is comfortably hundreds of functions deep, so eviction matters mainly on long-lived shared runners compiling many unrelated projects. For a single project's pipeline, the practical questions are hit rate and validity, both of which your CI cache key controls, not the LRU. Note also that the index is self-contained: each entry is independently checksummed, so a partially restored cache degrades to a few recompiles rather than to wrong code. That property is what makes archiving and restoring the directory wholesale a safe operation.

The trap: cache status manages the wrong cache

The product CLI has cache commands, and they work — on the other directory. Verified during the writing of this page:

python -m pyvorin cache status
Cache directory: /root/.pyvorin_cache
Total files: 172
Total size: 1.35 MB

And python -m pyvorin cache clear empties that home directory — we ran it, and it removed the guard-runtime, JIT and optimisation subcaches under ~/.pyvorin_cache while leaving all 159 artefacts in disk_compile exactly where they were. Neither command touches the compile artefact cache. If you are debugging a suspected corrupt compile artefact in CI, do not reach for cache clear; it will confidently clean the wrong thing. Exclude the compile cache directory from your cache archive and let a fresh compile repopulate it, or wipe the job's cache volume directly.

Finding the directory in a pipeline

CI runners do not hardcode site-packages paths, and neither should you. One command locates the cache directory wherever the install landed:

python -c "import pyvorin, pathlib; print(pathlib.Path(pyvorin.__file__).parent / '.pyvorin_cache' / 'disk_compile')"

The package metadata path is the only anchor you need: the cache directory sits inside the pyvorin package directory, one level below it. Capture that path into an environment variable at job start and every later step — cache save, cache restore, debugging — refers to the variable.

Cache keys that hit

The artefact key already covers source and options, so the CI cache key only needs to cover what a job cannot detect: the platform, the Python version and the pyvorin version. A workable key, in order of preference:

  1. Operating system and architecture. Compiled machine code is platform-specific; a Linux x86_64 artefact is useless on an ARM runner.
  2. Python version. The shipped wheel is built per CPython version — 1.0.9 ships cp312 — and the ABI differs across versions.
  3. pyvorin-native version, pinned in requirements. A requirements-hash component covers this automatically and is the strongest signal, because the toolchain mtimes are hashed into every artefact key.

Hashing your own source files into the key, the pattern the old version of this page recommended, is redundant: change the source and the artefact key changes, so the old entry simply stops being used while the LRU evicts it — a 500-entry LRU absorbs routine churn without any key gymnastics. Judgement call, stated plainly: we would rather accept a slowly growing cache directory of dead entries than key on source hashes and forfeit every hit the moment any file in the repository changes. Compilation of a changed function is one cache write; a blown cache key is a full recompile of everything.

GitHub Actions

A template to adapt — the paths and keys above, wired into the standard cache action:

- name: Locate Pyvorin compile cache
  id: pv-cache
  run: |
    echo "dir=$(python -c "import pyvorin, pathlib; print(pathlib.Path(pyvorin.__file__).parent / '.pyvorin_cache' / 'disk_compile')")" >> "$GITHUB_OUTPUT"

- uses: actions/cache@v4
  with:
    path: ${{ steps.pv-cache.outputs.dir }}
    key: pyvorin-${{ runner.os }}-${{ runner.arch }}-py${{ env.PYTHON_VERSION }}-${{ hashFiles('requirements.txt') }}

Pin pyvorin-native in requirements.txt and the requirements hash doubles as the package version. Restore happens before your test or benchmark step; save happens automatically at job end if the key missed. On a cache hit, a benchmark step runs with warm artefacts and the compile time line in its output drops toward zero.

GitLab CI

benchmark:
  cache:
    key:
      files:
        - requirements.txt
      prefix: pyvorin-$CI_RUNNER_EXECUTABLE_ARCH-py$PYTHON_VERSION
    paths:
      - .pyvorin_cache/
  script:
    - export PV_CACHE=$(python -c "import pyvorin, pathlib; print(pathlib.Path(pyvorin.__file__).parent / '.pyvorin_cache' / 'disk_compile')")
    - ln -sfn "$PV_CACHE" .pyvorin_cache
    - python -m pyvorin bench my_module.py --function entry --runs 7 --warmup 2 --json > bench.json

GitLab caches paths inside the project workspace, and the compile cache lives outside it, inside the installed package. The symlink bridges the two. It is one extra line and it keeps the cache policy where GitLab can see it.

What not to cache

Two things look cacheable and are not worth it. The home cache at ~/.pyvorin_cache — the guard-runtime, JIT and optimisation subcaches — is per-machine working state; restoring it across runners buys little and complicates your key story. And the columnar_kernels directory is recreated at import time regardless, so archiving it is effort spent on files the package regenerates by itself. Cache the compile artefacts; let everything else be rebuilt. Pipelines are easier to reason about when the cached surface is small enough to name in one sentence.

Prove the cache is working

A cache you have not verified is a hope. Make the proof part of the pipeline. The --json benchmark output exposes metrics.compile_time_ms; on a warm cache that figure collapses — in our own repeated runs a cached compile reported 0.000 ms against a cold-cache 238 ms for the same function. Gate on the fields that matter rather than the fields that shine:

"correct": true        # never relax this one
"fallback_count": 0     # a restored cache that silently fell back is a failed cache
"deopt_count": 0

A drop in compile time with a rise in fallback count is the signature of a cache that restored garbage — or of source that changed while the key did not. Either way the gate should fail and a human should look. If it does fail, the diagnosis path is how to handle compilation failure; the command reference for every flag used here is on the CLI reference page.

Where to go next

Last reviewed 13 April 2026 against pyvorin-native 1.0.9 installed at /root/pvfinal. The cache locations, the 159-artefact cache index, the LRU and key composition were read from installed files; cache status and cache clear were executed and their exact scope verified. The CI snippets are templates adapting those verified facts, not captured pipeline runs.