Skip to content

perf: reuse Triton's autotune result across Cherimoya calls (2.7x on every prediction after the first) - #165

Open
jmschrei wants to merge 1 commit into
pinellolab:mainfrom
jmschrei:perf-cherimoya-autotune-cache
Open

perf: reuse Triton's autotune result across Cherimoya calls (2.7x on every prediction after the first)#165
jmschrei wants to merge 1 commit into
pinellolab:mainfrom
jmschrei:perf-cherimoya-autotune-cache

Conversation

@jmschrei

@jmschrei jmschrei commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Cherimoya's inference kernels are wrapped in triton.autotune, which benchmarks its candidate configs the first time a kernel is launched for a given shape. That's the right call for training, where a process compiles once and then runs for hours. It's badly mismatched to Chorus, where run_code_in_environment spawns a fresh subprocess per call — so the benchmark is re-run every single time, to serve one forward pass.

On CATv1's native 2114 bp geometry the first forward costs ~7.6 s against ~0.96 ms steady-state. A one-shot prediction was spending ~99.99% of its GPU time deciding how to run the kernel rather than running it.

Triton 3.6+ can persist the selected config to disk, keyed on the same tuple autotune keys on. It's off by default, so this turns it on.

This is a cache, not a guess. The config used is the one autotune itself benchmarked and picked for that exact key; a shape Chorus hasn't seen falls back to benchmarking. Predictions are bit-identical.

Measurements

1× H200, CATv1 DNASE:K562, through Chorus's own API with use_environment=True, medians of 3. Baseline measured on this same base by reverting only these files:

before after speedup
_predict 11.53 s 4.27 s 2.70×
predict_variant_effect 22.95 s 8.41 s 2.73×
first forward, in-process 7.6 s 0.40 s 19×
steady-state forward 0.960 ms 0.956 ms unchanged

Raw: before _predict 11.42 / 11.55 / 11.53, variant 22.95 / 22.90 / 22.95. After _predict 4.32 / 4.21 / 4.27, variant 8.41 / 8.41 / 8.40.

Prediction checksum is identical before and after: 341.3908. I confirmed the baseline writes zero autotune cache files, so the two arms are genuinely independent.

What this does not do

The first call on a machine is not faster — it still pays the full ~11.5 s and populates the cache. Only later processes benefit. Since Chorus's normal mode is many short-lived subprocess calls, that's the right trade, but a single-call benchmark would show no improvement at all.

The cache is keyed on shape, so a new batch size pays one benchmark before being cached (the background builder at batch 64/256 has its own entry). A cleared ~/.triton, or a Triton / GPU / cherimoya version bump, resets it — all three are part of Triton's cache key.

Cherimoya itself is unmodified.

Implementation notes

The knob has no backing environment variable (knobs.autotuning.cache has env=None), so it can't go in EnvironmentRunner._prepare_envAutotuner.__init__ reads it when the decorators are evaluated, which happens at import cherimoya. It therefore has to be set from Python before that import, on each of the three load paths.

  • chorus/oracles/cherimoya_source/_triton_autotune.py — new; canonical helper and the full rationale
  • chorus/oracles/cherimoya.py_load_direct, which also covers scripts/build_backgrounds_cherimoya.py and the in-process tests
  • both templates — inlined rather than importing the helper, because importing chorus inside the cherimoya env costs ~0.6 s and would eat much of the saving; each carries a pointer to the helper's explanation
  • guarded with try/except (ImportError, AttributeError) so CPU-only and macOS installs (no Triton) are unaffected — a False return is a missed optimization, never a functional difference

Tests

tests/test_cherimoya.py: 50 passed (46 existing + 4 new). Integration: 9 passed, 1 skipped — unchanged.

Three of the new tests assert the knob precedes the cherimoya import on each load path. That ordering is load-bearing and fails silently — set it after the import and every call quietly goes back to re-benchmarking, with no error to notice.

🤖 Generated with Claude Code

Cherimoya's inference kernels are wrapped in triton.autotune, which
benchmarks its candidates on the first launch for a given shape. That is
right for training, but Chorus spawns a fresh subprocess per call, so the
benchmark is re-run every time to serve a single forward pass -- ~7.6s of
tuning against ~0.96ms of steady-state compute.

Enable Triton's on-disk autotune cache (knobs.autotuning.cache) so a later
process reuses the config autotune already selected. This is a cache, not a
guess: the key is the same shape/dtype tuple autotune keys on, and an unseen
shape falls back to benchmarking. Predictions are bit-identical.

Measured on 1x H200, CATv1 DNASE:K562, medians of 3 at this commit's base:

                            before    after   speedup
  _predict                  11.53s    4.27s     2.70x
  predict_variant_effect    22.95s    8.41s     2.73x

The first call on a machine still pays the full ~11.5s to populate the
cache; only later processes benefit. Prediction checksum identical before
and after (341.3908). tests/test_cherimoya.py 50 passed; integration 9
passed, 1 skipped.

The knob has no backing environment variable, so it must be set from Python
before the decorators are evaluated at `import cherimoya`. Tests assert that
ordering on all three load paths, since getting it wrong is silent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant