feat(autotuner): Autotuner v2 (autotune_v2) — managed persistence, deployment-matched measurement, runner contract - #3861
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis PR adds a managed v2 autotune cache backend, wires it into ChangesManaged autotune cache
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces an experimental managed on-disk autotune cache (v2) that stores tuning results per-entry under environment-hashed directories, allowing for automatic, atomic persistence without manual invalidation. The reviewer feedback focuses on two main improvements: first, ensuring that _file_configs and _logged_file_hits are properly saved and restored upon entering and exiting the autotune context to prevent configuration leakage or loss in nested contexts; second, explicitly specifying encoding="utf-8" when reading and writing cache files to guarantee cross-platform consistency.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| use_managed_cache = cache is True | ||
| managed_prev = None | ||
| if use_managed_cache: | ||
| # Managed v2 backend: per-entry files under an environment-hashed | ||
| # directory. Nothing is loaded up front (entries are read lazily | ||
| # on cache miss) and there is no exit-time save (winners are | ||
| # published atomically as soon as they are tuned). | ||
| from .autotune_cache import ManagedAutotuneCache | ||
|
|
||
| with tuner._lock: | ||
| tuner._file_configs.clear() | ||
| tuner._logged_file_hits.clear() | ||
| managed_prev = tuner._managed_cache | ||
| tuner._managed_cache = ManagedAutotuneCache(manifest=_collect_metadata()) | ||
| elif isinstance(cache, str): | ||
| with tuner._lock: | ||
| tuner._file_configs.clear() | ||
| tuner._logged_file_hits.clear() |
There was a problem hiding this comment.
To prevent configuration leaks and loss of previously loaded configs in nested or sequential autotune contexts, we should save the previous state of _file_configs and _logged_file_hits on entry so they can be restored on exit.
use_managed_cache = cache is True
managed_prev = None
file_configs_prev = None
logged_file_hits_prev = None
if use_managed_cache:
# Managed v2 backend: per-entry files under an environment-hashed
# directory. Nothing is loaded up front (entries are read lazily
# on cache miss) and there is no exit-time save (winners are
# published atomically as soon as they are tuned).
from .autotune_cache import ManagedAutotuneCache
with tuner._lock:
file_configs_prev = dict(tuner._file_configs)
logged_file_hits_prev = set(tuner._logged_file_hits)
tuner._file_configs.clear()
tuner._logged_file_hits.clear()
managed_prev = tuner._managed_cache
tuner._managed_cache = ManagedAutotuneCache(manifest=_collect_metadata())
elif isinstance(cache, str):
with tuner._lock:
file_configs_prev = dict(tuner._file_configs)
logged_file_hits_prev = set(tuner._logged_file_hits)
tuner._file_configs.clear()
tuner._logged_file_hits.clear()There was a problem hiding this comment.
This comment targeted an early iteration where v2 rode on autotune(cache=True) and mutated _file_configs. The API has since been forked into a standalone autotune_v2(): autotune() is byte-identical to main, _file_configs/_logged_file_hits are never touched by v2, and the managed store deliberately uses attach semantics (process-lifetime, like load_configs) rather than save/restore scoping — both vLLM and SGLang serve outside any context, so a context-scoped store would silently regress serving to heuristics after warmup. Nested/sequential contexts therefore need no state restoration by design.
| if use_managed_cache: | ||
| with tuner._lock: | ||
| tuner._managed_cache = managed_prev |
There was a problem hiding this comment.
Restore the saved _file_configs and _logged_file_hits in the exception handler to ensure the state is correctly rolled back if context setup fails.
| if use_managed_cache: | |
| with tuner._lock: | |
| tuner._managed_cache = managed_prev | |
| if use_managed_cache or isinstance(cache, str): | |
| with tuner._lock: | |
| if use_managed_cache: | |
| tuner._managed_cache = managed_prev | |
| tuner._file_configs = file_configs_prev | |
| tuner._logged_file_hits = logged_file_hits_prev |
There was a problem hiding this comment.
This comment targeted an early iteration where v2 rode on autotune(cache=True) and mutated _file_configs. The API has since been forked into a standalone autotune_v2(): autotune() is byte-identical to main, _file_configs/_logged_file_hits are never touched by v2, and the managed store deliberately uses attach semantics (process-lifetime, like load_configs) rather than save/restore scoping — both vLLM and SGLang serve outside any context, so a context-scoped store would silently regress serving to heuristics after warmup. Nested/sequential contexts therefore need no state restoration by design.
| if use_managed_cache: | ||
| # Managed entries were already published during tuning; just | ||
| # restore the previously active backend (if any). | ||
| with tuner._lock: | ||
| tuner._managed_cache = managed_prev | ||
| # Save configs on exit when tuning with a legacy cache path, | ||
| # but only if new profiling results were added this session | ||
| # and the cache file was valid (no environment mismatch). | ||
| if cache is not None and cache_valid and tune_mode and tuner._dirty: | ||
| elif isinstance(cache, str) and cache_valid and tune_mode and tuner._dirty: | ||
| tuner.save_configs(cache) |
There was a problem hiding this comment.
Restore the saved _file_configs and _logged_file_hits on normal exit of the context manager to prevent configuration leakage into outer or subsequent contexts.
| if use_managed_cache: | |
| # Managed entries were already published during tuning; just | |
| # restore the previously active backend (if any). | |
| with tuner._lock: | |
| tuner._managed_cache = managed_prev | |
| # Save configs on exit when tuning with a legacy cache path, | |
| # but only if new profiling results were added this session | |
| # and the cache file was valid (no environment mismatch). | |
| if cache is not None and cache_valid and tune_mode and tuner._dirty: | |
| elif isinstance(cache, str) and cache_valid and tune_mode and tuner._dirty: | |
| tuner.save_configs(cache) | |
| if use_managed_cache: | |
| # Managed entries were already published during tuning; just | |
| # restore the previously active backend (if any). | |
| with tuner._lock: | |
| tuner._managed_cache = managed_prev | |
| tuner._file_configs = file_configs_prev | |
| tuner._logged_file_hits = logged_file_hits_prev | |
| # Save configs on exit when tuning with a legacy cache path, | |
| # but only if new profiling results were added this session | |
| # and the cache file was valid (no environment mismatch). | |
| elif isinstance(cache, str) and cache_valid and tune_mode and tuner._dirty: | |
| tuner.save_configs(cache) | |
| with tuner._lock: | |
| tuner._file_configs = file_configs_prev | |
| tuner._logged_file_hits = logged_file_hits_prev | |
| elif isinstance(cache, str): | |
| with tuner._lock: | |
| tuner._file_configs = file_configs_prev | |
| tuner._logged_file_hits = logged_file_hits_prev |
There was a problem hiding this comment.
This comment targeted an early iteration where v2 rode on autotune(cache=True) and mutated _file_configs. The API has since been forked into a standalone autotune_v2(): autotune() is byte-identical to main, _file_configs/_logged_file_hits are never touched by v2, and the managed store deliberately uses attach semantics (process-lifetime, like load_configs) rather than save/restore scoping — both vLLM and SGLang serve outside any context, so a context-scoped store would silently regress serving to heuristics after warmup. Nested/sequential contexts therefore need no state restoration by design.
| """Write JSON to *path* via a same-directory temp file + atomic rename.""" | ||
| fd, tmp_path = tempfile.mkstemp(dir=path.parent, prefix=".autotune_", suffix=".tmp") | ||
| try: | ||
| with os.fdopen(fd, "w") as f: |
There was a problem hiding this comment.
There was a problem hiding this comment.
Fixed in ab85c2c — encoding="utf-8" added to the store's write path.
| return None | ||
| path = self._entry_path(file_key) | ||
| try: | ||
| with open(path, "r") as f: |
There was a problem hiding this comment.
Fixed in ab85c2c — encoding="utf-8" added to the store's read path, matching the writer.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@flashinfer/autotuner.py`:
- Around line 625-639: Managed cache activation is not context-safe because the
shared _managed_cache field can be reused or restored out of order across nested
or overlapping cache contexts, causing legacy cache=str paths to mix with the
managed backend. Update the cache context handling in autotuner.py around the
cache=True and cache=str branches (and the corresponding restore/cleanup sites)
to snapshot and restore the full backend state per context/thread, or maintain a
backend stack so each context gets its own active cache backend. Ensure the
legacy cache path explicitly disables managed cache while it is active, and that
exiting one context cannot resurrect or disable another active context’s
backend.
In `@tests/autotuner/test_autotune_cache_v2.py`:
- Around line 109-114: The test in AutoTuner.get().choose_one assigns an unused
runner variable when unpacking the return value, which should be changed to a
throwaway placeholder to satisfy Ruff. Update the unpacking in the
autotune(False, cache=True) block so only the used tactic value is named,
keeping the assertion focused and avoiding the unused local.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 27c6c5a7-ddd8-4f79-ba0f-cba0f7e5393a
📥 Commits
Reviewing files that changed from the base of the PR and between 5b8da12 and 26abedb6d444f7d809a2d62fba58be4f3f3f081f.
📒 Files selected for processing (3)
flashinfer/autotune_cache.pyflashinfer/autotuner.pytests/autotuner/test_autotune_cache_v2.py
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
flashinfer/autotuner.py (1)
1098-1101: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winStale API reference in comment.
The comment says the managed cache is "set by
autotune(cache=True)", but the activation API is nowpersist=True;cache=TrueraisesTypeError(verified bytest_invalid_persist_and_cache_types_raise). Update to avoid misleading maintainers.📝 Proposed fix
- # 2.5 Managed v2 per-entry cache (experimental, set by - # autotune(cache=True)). One JSON file per entry, read + # 2.5 Managed v2 per-entry cache (experimental, set by + # autotune(persist=True)). One JSON file per entry, read🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@flashinfer/autotuner.py` around lines 1098 - 1101, The managed v2 per-entry cache comment is referring to the old autotune(cache=True) API, which is now obsolete and misleading. Update the wording in the nearby cache documentation/comments around the managed cache path to reference autotune(persist=True) instead, keeping the description of lazy JSON loading and promotion into _file_configs accurate and aligned with the current API.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@flashinfer/autotuner.py`:
- Around line 1098-1101: The managed v2 per-entry cache comment is referring to
the old autotune(cache=True) API, which is now obsolete and misleading. Update
the wording in the nearby cache documentation/comments around the managed cache
path to reference autotune(persist=True) instead, keeping the description of
lazy JSON loading and promotion into _file_configs accurate and aligned with the
current API.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 0a6dc4ca-6261-4295-888c-bf1978407bfe
📥 Commits
Reviewing files that changed from the base of the PR and between 26abedb6d444f7d809a2d62fba58be4f3f3f081f and 31625846f45a4db712215be892e70143761e1b29.
📒 Files selected for processing (3)
flashinfer/autotune_cache.pyflashinfer/autotuner.pytests/autotuner/test_autotune_cache_v2.py
🚧 Files skipped from review as they are similar to previous changes (1)
- flashinfer/autotune_cache.py
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
flashinfer/autotuner.py (1)
1009-1057: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winDo not let managed v2 contexts read legacy
_file_configs.Inside
autotune_v2(),search_cache()still checks_file_configsbefore_managed_cache, so a previously loaded v1 JSON config can override or satisfy a v2 managed lookup. Gate the legacy block behindmanaged_cache is None, and consult only the managed backend while it is active.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@flashinfer/autotuner.py` around lines 1009 - 1057, The search_cache logic in autotuner.py still lets legacy _file_configs satisfy lookups before the managed v2 backend, so autotune_v2() can be overridden by v1 configs. Update the cache resolution in search_cache() to skip the _file_configs block whenever self._managed_cache is active, and only consult self._managed_cache for v2 contexts while preserving the existing logging and runner_id/tactic return flow.
♻️ Duplicate comments (1)
flashinfer/autotuner.py (1)
834-887: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftMake the active managed backend thread/context-local.
_managed_stackis process-global and_managed_cachereturns the top entry, so overlappingautotune_v2(root=...)contexts on different threads can read/write whichever backend was entered most recently. Tokenized removal fixes teardown, but not per-thread active-backend routing.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@flashinfer/autotuner.py` around lines 834 - 887, The active managed backend selection is still process-global because `_managed_stack` and the `_managed_cache` property always use the top entry, so concurrent `autotune_v2(root=...)` contexts can route reads/writes to the wrong backend. Make the active backend thread/context-local in `flashinfer.autotuner` by storing the current managed backend per thread or context (similar to `_override_local` / `_skip_ops_local`) while keeping the existing tokenized stack for safe teardown. Update `_managed_cache` and any push/pop logic in the autotune_v2-managed path so each thread sees its own active backend instead of the last-entered one.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@flashinfer/autotune_cache.py`:
- Around line 186-189: The persistence handling in autotune_v2 treats any truthy
persist value as enabled, so path-like inputs can silently use root=None and
write to the default cache location. Update autotune_v2 to either normalize
non-bool persist values into root when they are path-like, or explicitly reject
them with TypeError before persistence is enabled. Apply the same fix to the
related persistence flow in the later autotune_v2 logic so both paths use
consistent validation.
---
Outside diff comments:
In `@flashinfer/autotuner.py`:
- Around line 1009-1057: The search_cache logic in autotuner.py still lets
legacy _file_configs satisfy lookups before the managed v2 backend, so
autotune_v2() can be overridden by v1 configs. Update the cache resolution in
search_cache() to skip the _file_configs block whenever self._managed_cache is
active, and only consult self._managed_cache for v2 contexts while preserving
the existing logging and runner_id/tactic return flow.
---
Duplicate comments:
In `@flashinfer/autotuner.py`:
- Around line 834-887: The active managed backend selection is still
process-global because `_managed_stack` and the `_managed_cache` property always
use the top entry, so concurrent `autotune_v2(root=...)` contexts can route
reads/writes to the wrong backend. Make the active backend thread/context-local
in `flashinfer.autotuner` by storing the current managed backend per thread or
context (similar to `_override_local` / `_skip_ops_local`) while keeping the
existing tokenized stack for safe teardown. Update `_managed_cache` and any
push/pop logic in the autotune_v2-managed path so each thread sees its own
active backend instead of the last-entered one.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 6ad32f8b-af5b-499e-a4c3-10ff8df4c2e5
📥 Commits
Reviewing files that changed from the base of the PR and between 177e7909c3df30728ab68a7ea15fb43eaa9f19bf and 58b11d1e054830c6ca262ff625d482a2ce900f27.
📒 Files selected for processing (4)
flashinfer/__init__.pyflashinfer/autotune_cache.pyflashinfer/autotuner.pytests/autotuner/test_autotune_cache_v2.py
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
flashinfer/autotuner.py (1)
1031-1057: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winInclude managed-cache hits in the tuned-range warning.
choose_one()only checksprofiling_cacheand_file_configs, so a v2-only process can fall back outside the tuned bucket range without the perf-cliff warning. A small public accessor onManagedAutotuneCachewould lethas_tune_datainclude backend hits without reaching into_hitsdirectly.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@flashinfer/autotuner.py` around lines 1031 - 1057, The tuned-range warning in choose_one() is missing managed-cache coverage, so v2-only hits can bypass the perf-cliff check. Add a small public accessor on ManagedAutotuneCache to expose whether tune data exists for a key, then update has_tune_data in autotuner.py to include managed cache hits alongside profiling_cache and _file_configs without reading _hits directly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@flashinfer/autotune_cache.py`:
- Around line 303-318: The managed cache attachment in autotune_v2 should be
rolled back if entering the delegated autotune context fails, because
_attach_managed_cache can leave the process-wide cache in a modified state.
Update the autotune_v2 context manager so that the call to autotune(...) is
wrapped with cleanup logic that restores the prior AutoTuner managed cache when
__enter__ raises, using the existing tuner = AutoTuner.get() and
_attach_managed_cache path as the rollback point.
---
Outside diff comments:
In `@flashinfer/autotuner.py`:
- Around line 1031-1057: The tuned-range warning in choose_one() is missing
managed-cache coverage, so v2-only hits can bypass the perf-cliff check. Add a
small public accessor on ManagedAutotuneCache to expose whether tune data exists
for a key, then update has_tune_data in autotuner.py to include managed cache
hits alongside profiling_cache and _file_configs without reading _hits directly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 11fae79a-1d86-4671-9b35-cbe9487f9246
📥 Commits
Reviewing files that changed from the base of the PR and between 58b11d1e054830c6ca262ff625d482a2ce900f27 and 38e2bb530ad2f7fb70fcf358864bfae62e070d2c.
📒 Files selected for processing (3)
flashinfer/autotune_cache.pyflashinfer/autotuner.pytests/autotuner/test_autotune_cache_v2.py
|
Update (2 new commits):
Not yet included: CUPTI timing core (next), and the tuner-accuracy quantification harness (regret / top-1 / winner-flip-rate / replay-fidelity vs an oracle sweep — methodology in the proposal doc). |
|
Measurement-policy milestone complete (
|
… capture codex round-6 (graph-compat audit): the autotuner synchronizes and, in graph modes, captures its own private CUDA graph -- both illegal inside an outer stream capture. A tuning context accidentally left open around a framework's model-capture would otherwise surface a cryptic CUDA error. Guard the profiling entry with torch.cuda.is_current_stream_capturing() and raise a clear 'tune before capture, not inside it' message instead. vLLM/SGLang tune-before-capture so this is defensive, but it turns a confusing nested-capture failure into an actionable one. 242 GPU-free tests green; GPU integration sequence (eager + cuda_graph) still clean. AI-assisted (Claude Code). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Consolidates the v2 design into docs/design_docs/, following the
structure of cute_dsl_kernel_cache.md: motivation, store layout,
environment identity, concurrency/crash safety, MeasurementPolicy,
runner contract, distributed story, alternatives, limitations.
Specifics are checked against flashinfer/autotune_cache.py on this
branch (manifest = _collect_metadata() + cache_schema + policy fields,
sha256[:16] env hash / [:24] op hash, {key, runner, tactic} entries).
Two sections go beyond restating RFC flashinfer-ai#3920:
- Relationship to the CuTe-DSL kernel cache (flashinfer-ai#3874): why the two
caches cannot share a payload format -- opposite locking contracts
(single-flight vs last-valid-write-wins, the latter required because
ranks tune inside collectives), reproducible artifacts vs
measurements -- and which mechanics should be shared anyway:
env-record naming (meta.json vs manifest.json), one atomic-write /
invalid-is-a-miss helper, one cache-clearing story.
- Graduation plan: autotune_v2 is a transitional name. At graduation
autotune() becomes the v2 implementation, autotune_v2 becomes a
deprecated alias, and the v1 spellings are retained as forwarding
shims with cache=<path> honored as placement only. Names the four
gates hidden behind "deprecate v1 afterwards" (framework release,
validate_tactic adoption, execution_mode default, regret <= v1 on
>=2 arches) and the major-bump constraint on removal, so the version
number does not become permanent public API surface.
Also records why a separate entry point is needed: not the on-disk
format (autotune caches are already per-version disposable --
flashinfer_version is stamped by _collect_metadata() and hard-rejected
on mismatch, so no v2 process can encounter a live v1 file) but the
call-site signature (cache=<file> vs a placement-only root directory)
and the context-scoped vs process-attach lifetime change.
Flags that docs/autotuning.rst still documents v1 only and must be
updated in the change that swaps the implementation.
AI-assisted: drafted with Claude Code.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…cope The title named only managed persistence, which under-sells the doc: deployment-matched measurement and the runner contract are co-equal parts of the v2 proposal in RFC flashinfer-ai#3920, not sub-topics of persistence. Retitled to name all three, using the RFC's own phrasing. The graduation plan stays a section (§5) rather than a title element -- it is the doc's most contested part but not one of its design pillars. Also adds a "**Scope**:" header line naming the paths the doc governs, so an agent or contributor editing flashinfer/autotune_cache.py or flashinfer/autotuner/ can discover the doc without a central index. AI-assisted: drafted with Claude Code. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… scope A design doc is only read if the code points at it. Adds "Design doc:" pointers following the csrc/fused_moe/monomoe/ convention, deliberately only where the code is v2: - flashinfer/autotune_cache.py -- module docstring; the whole module is v2. - autotuner.py _V2Local -- §2.1 (attach semantics, why v2 does not nest), with an explicit note that the doc governs the autotune_v2 state and hook sites in this file ONLY. - autotuner.py _managed_cache field -- §2.1 (why attach is process-lifetime rather than context-scoped). - autotuner.py managed-store lookup -- §2.4 (invalid entry is a miss, never an error; hits memoized per store identity). - autotuner.py measurement-policy application -- §2.5 (policy is part of the store's environment identity). No pointer at module level in autotuner.py: that file is overwhelmingly v1, and a module-level citation would falsely claim the whole autotuner is governed by a doc that describes only v2. For the same reason the doc's Scope line is narrowed. It previously read "flashinfer/autotuner/", which claimed the v1 API too. It now names autotune_cache.py plus the v2 hook sites, and states explicitly that autotune() / save_configs() / load_configs() are NOT covered -- that code predates this doc and changing it creates no obligation to update it. §5 is where the two converge, and the scope line widens the day v1 is folded into v2. AI-assisted: drafted with Claude Code. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
§3 compared the autotune store to the CuTe-DSL kernel disk cache (flashinfer-ai#3874): why the two cannot share a payload format, and which mechanics they should share. It answered a question that came up in review, but in the doc it reads as a digression into a different subsystem -- a reader arriving at "Autotuner v2" has no reason to care about JitSpec's locking contract, and the section invited more confusion than it resolved. Deleted, keeping the one part that actually explains an autotuner design decision: §2.4's "no locks" bullet now says why single-flight is right for the kernel cache and wrong here -- compiling twice wastes CPU, whereas ranks tune inside collectives, so a cross-rank lock would serialize warmup or deadlock it. That is the sentence a reader needs at the point they wonder why publishes are unsynchronised. The cross-cutting cleanup §3 proposed (one atomic-write / invalid-is-a-miss helper, one name for the environment record, one cache-clearing story) is real but belongs in an issue against the JIT layer, not in this doc. Sections 4-7 renumbered to 3-6; cross-references updated. Code comments cite §2.1/§2.4/§2.5 only, so they are unaffected. AI-assisted: drafted with Claude Code. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Vincent's 5-GPU perf validation (PR flashinfer-ai#3861 graduation criterion #3) found the v2 serving lookup at 2.1x v1: AutoTuner.search_cache rebuilt ProfilingCacheKey.file_key -- a str() of the whole nested key -- on every lookup, at two sites (the user-config probe and the managed-store memo key). A fresh ProfilingCacheKey is constructed per call, so memoizing the string on the key instance can only dedup within a call; it cannot amortize across calls and still leaves v2 above v1. Instead key the warm path on the cheap hashable key_fields tuple (the same runner-identity-free fields file_key str()-ifies) and build the file_key string only on a cold store read, and skip the user-config probe wholesale when no configs were loaded. Warm serving now rebuilds no string. Microbench (bmm_fp8 8192 key, 20k iters): v2 hot path 2.73x v1 -> 1.05x v1 (within noise). str(key_fields) memo op 1.49us -> 0.17us. 258 GPU-free autotuner tests green. AI-assisted.
…local access, cupti docstrings) From @aleozlx's review of flashinfer-ai#3861: - @functools.lru_cache(maxsize=None) -> @functools.cache on the two v2 helpers (_get_cublas_version, _load_cupti); exact equivalent, clearer. - _V2Local already carries class-level defaults (active/store/measure), so the three getattr(..., default) reads are redundant -> direct attribute access. - Document the _CuptiInfraError that _cupti_measure_spans / _profile_single_kernel_cupti can raise, so a future caller handles it instead of crashing (only _profile_single_kernel catches it today). AI-assisted.
…view) From @aleozlx's review of flashinfer-ai#3861 (aligned with @YangXu1990uiuc): - Rename the autotune_v2 'measure' parameter to 'measurement_policy' (it takes a MeasurementPolicy; the noun reads clearer than the verb), move it up next to cache_root, and make the rarely-used tuning knobs keyword-only. Pre-graduation, ~0 external use, so the rename is cheap now. - Rename MeasurementPolicy.cuda_graph -> use_cuda_graph to match TuningConfig.use_cuda_graph. - docs/autotuning.rst: qualify the multi-process race-condition caveat as the v1 file cache and point at the v2 managed store that fixes it; add the managed store as a tier in Config Lookup Priority (the 2.5 hook). - vllm_autotune_v2.patch: note the second autotune call site (flashinfer_sparse_mla_warmup.py) importing the deleted helper, so the draft doesn't ImportError -- both sites migrate together. Deferred to discussion (unchanged): folding v2 into v1 as a mode, PersistentCacheConfig, _V2Local/_cupti_disabled/autotune_v2_reload renames. AI-assisted.
538cc60 took the file_key str() off the v2 serving hot path, which removed ~59% of the v1-vs-v2 lookup gap. The remainder is structural: v2 never populates the winner cache, so every serving call misses source 1 for *each* runner (building one ProfilingCacheKey per runner visited) and only then hits source 2.5. The cost therefore scales with the runner count -- with a single-runner candidate list v2 is already at parity, while the real 3-runner fp8_gemm list costs ~5 us per call, more than the ~9 us kernel it dispatches. Promoting the decoded store hit into the winner cache lets the 2nd+ lookup for a key exit at source 1 on one dict hit, exactly as v1 does. Measured on GB300 (SM103), bmm_fp8 m=64 N=K=8192, 20 000 iterations, full runner list, keeping only trials where both paths resolved the SAME runner (source 1 builds one key per runner visited, so an unmatched trial mixes in a ~3 us per-runner-index cost and can flip the sign of the result): TOT unfixed v2 - v1 = +5.184 us, +4.641 us with this v2 - v1 = -0.032 us, +0.256 us <- parity That meets graduation criterion #3 (hot-path latency v2 <= v1). Safety: "winners" is the partition for this store's (root, env_hash) identity, so a promoted entry can never be served under a different store or measurement policy, and it is never "profiling_cache" -- v1 save_configs still cannot observe v2 entries. The None profile matches what this source already returns. Source 1 re-runs _tactic_still_valid, so revalidation is unchanged, and clear_cache() already clears _winner_partitions. Verified with the cold/hot cache-correctness gate (cold->hot MATCH, bucket boundaries, corrupt-store / env-mismatch / cross-arch negatives): all PASS with this applied, verdicts byte-identical to unfixed TOT. Note this does not cover a store MISS -- a shape outside the tuned buckets still rebuilds keys on every call. That path is unchanged by this commit. AI-assisted (measurement, root-cause and patch drafted with Claude Code). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up to the winner-cache promotion in the previous commit. That change
fixed the *repeat* lookup; the *first* lookup for each shape still pays a
filesystem probe -- open + json.load + validate -- on the serving path, inside
the call the user is timing.
On a shared filesystem that probe is not small. Measured on GB300, synthetic
stores, page cache warm, median of 7:
store on lustre first-touch lookup 389 - 463 us per shape
store on node /tmp first-touch lookup 23 - 42 us per shape
A model that touches a few hundred distinct shapes therefore pays tens to
hundreds of milliseconds of first-call stalls, scattered across the run and
attributed to whichever kernel happened to be dispatched first. That is two
orders of magnitude above the ~5 us repeat-lookup gap the previous commit
closed.
This commit reads the whole store once when it is attached and hydrates the
in-process winner cache, so first touch becomes a dict hit:
stored touched preload init first touch lazy first touch preloaded
1000 50 411 ms 389 us/shape 0.64 us/shape
1000 1000 418 ms 400 us/shape 0.42 us/shape
100 50 42 ms 398 us/shape 0.31 us/shape
(lustre; /tmp is ~17x cheaper throughout)
What this does NOT do is reduce total work -- it increases it. Preloading
decodes and validates every stored entry, including ones this process never
asks for, so a short run that touches a handful of shapes from a large shared
store does strictly more work than before (1000 stored / 7 touched: 441 ms of
init against 3 ms of lazy reads). The trade is deliberate: the cost becomes a
single predictable init cost instead of unpredictable stalls inside timed
serving calls. If that trade is wrong for some deployment, the honest knob is
to make hydration opt-out; this commit does not add one, on the grounds that
the store is per-environment and normally sized to the workload that wrote it.
Serving an entry in bulk must not weaken what serving one at a time enforces,
so preload() re-checks exactly what lookup() checks -- the entry embeds the
canonical key it was stored under, that key agrees with the structural fields,
and the file lives where that key hashes to. Anything that fails is counted as
skipped and left to lookup(), which rejects it as a miss. An early draft of
this patch read the entry JSON directly and trusted it; the cold/hot gate
caught it serving a byte-flipped entry that unpatched TOT correctly rejects.
Persisting key_fields is what makes this possible: the store is keyed by
str(key_fields), which is one-way (torch dtypes do not literal_eval), so the
in-memory key cannot be reconstructed from what v2 stored. Entries now carry a
JSON encoding of the structural fields alongside. Per this module's stated
rule the schema directory is bumped v2 -> v3 rather than versioning entries;
old stores are simply not consulted. autotune_cache.py stays torch-free --
dtypes are detected structurally and torch is imported lazily, only when a
store is actually decoded. Keys carrying anything unencodable degrade to None
and are served lazily, exactly as before.
Verified on GB300 with the cold/hot cache-correctness gate: cold->hot MATCH,
bucket boundaries, and the corrupt-store / env-mismatch / cross-arch negatives
all PASS, with a fresh process reporting "Preloaded 7/7 managed cache entries".
AI-assisted (measurement, root-cause and patch drafted with Claude Code).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…load Two corrections to the preceding commit, both found in review. 1. Schema stays "v2". The rule in this module is to bump the schema directory on an *incompatible* change to the entry format, and adding "key_fields" is not one: an entry without the field is still served by lookup() and is simply skipped by preload(), and a reader that predates the field ignores it. v2 has also not been released, so bumping would have orphaned the stores of everyone testing this branch and forced a needless re-tune for no compatibility benefit. A store may now legitimately hold both formats at once, so that mixture is tested: of 7 entries with 4 downgraded to the pre-key_fields format, preload() reports 3 preloaded / 4 skipped and all 4 skipped entries are still served correctly by lookup(). 2. preload() was defined twice. The second (validating) definition shadowed the first, so the guarded code is what ran and every test passed -- but the file carried 41 lines of dead, unvalidated method that read as if it were live, and any reordering would have silently activated it. Removed; the surviving definition is the one that re-checks what lookup() checks. The duplicate came from re-running the patch script after editing its template: its "already applied" test keyed on the exact replacement text, so an edited template did not match, and the edit was inserted a second time instead of being recognised as already present. The script now keys that test on a short stable marker per edit and refuses to apply over a different version. Verified on GB300 at this state: correctness gate PASS (cold->hot MATCH, bucket boundaries, corrupt-store / env-mismatch / cross-arch negatives all reject) and the mixed-store migration test PASS. AI-assisted (review, measurement and patch drafted with Claude Code). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…vidence
The comment on the winner-cache promotion said it avoids "re-probing the
store". "The store" there is the in-memory _managed_decoded memo, not the
filesystem -- but the wording reads as a claim that v2 touches disk on the hot
path, which is the opposite of what this codebase does and the opposite of what
the design doc says. Reworded to name the real cost: redundant per-runner key
construction.
That claim was checked rather than assumed. An audit hook counting
open/os.open/listdir/scandir/rename plus direct json calls over 500
steady-state calls records ZERO for both v1 and v2 -- raw counts, not just
filtered. The design doc's "these small files are not accessed on the
inference hot path" is correct; the memo in lookup() and _managed_decoded make
it so. The only disk touch is the first lookup per key per process, which the
preceding commit removes.
Two further checks, both on GB300 at this HEAD:
1. Winner-cache growth. Promotion adds a write on the SERVING path, where v1
only writes during tuning, and the key embeds runner_hash -- which is
hash(runner) over the runner's instance attributes, falling back to id() for
unhashable ones. If an op rebuilt runners per call, every call would mint a
new key: source 1 would miss forever and `winners` would grow without bound
during inference. Driving the real op (not a captured argument tuple) for
3000 calls at m=1 and m=64: len(winners) 1 -> 1, distinct runner_hash 1.
No growth.
2. Criterion 3, better sampled. The measurement in the first commit's message
used 2 matched trials. Over 12 trials (6 matched; the rest excluded because
the two paths resolved different runners, which costs ~2 key builds of index
difference and can flip the sign):
v2 - v1 p50: median +0.080 us, range -0.096 .. +0.544, stdev 0.221
So the honest statement is that the gap closed from +5.18 us to
INDISTINGUISHABLE FROM ZERO within run-to-run spread -- not that v2 is
strictly <= v1. The earlier -0.032/+0.256 numbers were not wrong, just
under-sampled; both fall inside this distribution.
The excluded trials are explained rather than merely discarded: the
candidate-list order was stable across all 12 trials, but v1 disagreed with
ITSELF run to run (Cudnn 8/12, Cublas 4/12) for the same shape on the same
machine. These two runners are near-equal here, so noise decides the winner
per run. That is tuning nondeterminism on both sides, not a v1-vs-v2
selection bias, which is what makes excluding those trials legitimate for a
lookup-cost comparison.
AI-assisted (measurement and analysis with Claude Code).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Blank line after docstring + wrapped ternary in _encode/_decode_key_fields (from PR #2 preload). No logic change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The trtllm SM100 MoERunner (_get_trtllm_moe_sm100_module_impl) did not override get_cache_key_extras, so its persisted v2 key was (op, class, profile, ()). runner_hash is dropped from file keys by design, leaving extras the only channel for constructor-fixed config -- so two layers with identical dims/dtypes but different activation (or weight layout, quantization, expert structure) aliased to one stored entry: the second publish() clobbered the first and serving mis-matched tactics under a normal "cache hit" log. In-process tuning was unaffected (runner_hash distinguishes there); only the persistent store collided. Mirror the CUTLASS MoERunner: return the non-shape-derived config as a tuple of int/bool (enums -> int keeps it JSON-round-trippable for preload). flashinfer-ai#4328 fixed only the CUTLASS MoERunner, not this trtllm one. Found by Vincent Tombari during 5-GPU autotuner-v2 validation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
_profile_single_kernel_cupti ran flush_buffer.zero_() as the first op of run_iteration, inside the CUPTI correlation window. zero_() dispatches a fill KERNEL (not a memset), so its ~100 us landed in the measured span -- corrupting the tuner's own timings and biasing tactic selection for the cold-L2 ops. The docstring's "excluded because it is a memset" was the wrong assumption behind it. Hoist the flush into a per-iteration prologue that _cupti_measure_spans runs and drains (via the existing synchronize) BEFORE opening the [t0, t1] window, so its launch record precedes the span and is excluded -- the ordering bench_gpu_time_with_cupti already uses. Cold-L2 semantics are preserved (flush still runs before every measured iteration). Found by Vincent Tombari during 5-GPU autotuner-v2 validation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Persisted autotune keys (ProfilingCacheKey.key_fields / file_key) drop
runner_hash on purpose -- it is runtime identity and is meaningless across
processes -- so get_cache_key_extras is the only channel carrying a runner's
construction-time configuration into the on-disk store. Two runners still
under-report it.
TrtllmGemmRunner (gemm/gemm_base.py) returned only (_use_8x4_sf_layout), but
its own get_valid_tactics selects the tactic list with
op.trtllm_gemm_tactics(m, n, k, self._input_dtype, self._output_dtype,
self._use_8x4_sf_layout)
and _input_dtype additionally doubles k when it is E2m1. m/n/k come from the
shape profile; the dtype pair appears in no tensor shape, so two runners that
differ only in a dtype produced byte-identical file keys. Today's in-tree
constructions (trtllm_fp4_gemm_runner, trtllm_mxfp8_gemm_runner) pin the pair
per class and per custom_op, so this is a latent hazard rather than an
observed live collision -- but the generic trtllm_gemm_runner(input_dtype,
output_dtype, ...) factory is exported and the omitted fields are provably
tactic-selecting in the same method.
CuteDslFusedMoENvfp4Runner (fused_moe/cute_dsl/tuner.py) reported only the
activation parameters, while the sibling CuteDslFusedMoEW4A16Runner in the
same file reports its full configuration. Added, with the reason each is not
shape-derived: output_dtype (picks the GEMM1 epilogue dtype in
get_valid_tactics under per-token activation, and returns [] for anything but
fp16/bf16), use_fused_finalize and enable_pdl (select the kernel path in
forward), num_experts and local_expert_offset (define the expert->token
distribution CuteDslMoEInputsHelper synthesizes while profiling, so they
change how tactics rank).
Deliberately NOT copied from the sibling, because the profile already carries
them for this class: top_k is token_selected_experts.shape[1] (input 2) and
num_local_experts is w1_weight.shape[0] (input 4) -- get_valid_tactics re-reads
the latter straight off the tensor. use_per_token_activation appends
per_token_scale to the input list, so it already changes nearest_profile.
The per-field reasoning above is recorded here and in the PR description
rather than as source comments, so the extras tuples stay readable.
Verified with a fail-before / pass-after check on each runner: two runners
differing only in the newly-reported fields produced byte-identical file
keys before and distinct keys after. Those checks are not included here --
this commit is the fix only. The trtllm-gen SM100 MoERunner half of
this audit is not included: it landed separately as b7ff286. The v2 store
cold/hot correctness gate still reports OVERALL: PASS on GB300.
AI-assisted (audit, measurement and patch drafted with Claude Code).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eload Two audit follow-ups on the v2 store: 1. The environment manifest (and hence the store's env_hash) now includes the CuTe-DSL compiler-stack fingerprint (reusing the JIT cache's _get_cute_dsl_version, which covers the libs variants). DSL runners' tactic spaces are tile configs compiled by that stack, so entries must not survive a nvidia-cutlass-dsl upgrade. Existing v2 store dirs are orphaned by the hash change (by design: new directory, old untouched); old v1 cache files degrade to a soft metadata mismatch (missing key). 2. clear_cache() and autotune_v2_reload() now clear _preloaded_stores, and reload bulk re-hydrates the attached store immediately. Previously the stale hydration marker made every later attach skip its preload, so post-reload serving silently degraded to lazy per-key disk reads -- losing the preload benefit exactly in the rank-finalize flow it was built for. Design doc updated in the same PR (manifest field list, reload semantics). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ernel SKILL.md The branch's rewrite of the Attention routine line predated main adding this entry (2026-09-02), so rebasing silently reverted someone else's line rather than conflicting. Restore main's version; the PR has no intended change to this file. Flagged in review by @aleozlx.
integration_sequence.py, PERF_VALIDATION_GUIDE.md and framework_patches/vllm_autotune_v2.patch were working aids for the perf-validation hand-off (the guide clones a personal fork tag). Their content lives in the flashinfer-ai#3861 / flashinfer-ai#3920 threads; the design doc now refers to the posted migration draft instead of the in-tree file path. Flagged in review by @aleozlx.
082ce20 to
ba40a49
Compare
|
@flashinfer-bot run |
|
/bot run tests/autotuner |
…d cache Use FlashInfer's managed per-entry autotune store (flashinfer-ai/flashinfer#3861) when the installed FlashInfer provides autotune_v2, keeping the legacy file-cache path for older versions. Every rank tunes and publishes into one shared, environment-hashed store; warm restarts skip re-profiling, and autotune_v2_reload() after the post-tuning barrier makes all ranks serve the store's final state. This removes the leader-only read/broadcast/atomic-write/load_configs/ save_configs plumbing from both the generic autotune pass and the SM120 sparse-MLA decode autotune. VLLM_FLASHINFER_AUTOTUNE_CACHE_DIR keeps its meaning as a placement override (cache_root); identity is FlashInfer's. resolve_flashinfer_autotune_file() stays for the PCIe IPC all-reduce tuning file, which derives a sibling path from it. Signed-off-by: Yang Xu <yanxu@nvidia.com>
📌 Description
Autotuner v2 — an evolution of the autotuner exposed through a standalone
autotune_v2(), additive overautotune()(which stays byte-identical). It started as a managed persistent cache and has grown, across review, into five composing pillars:MeasurementPolicy) — tune the way you serve (eager vs CUDA-graph host-cost semantics); part of the store's environment identity.validate_tactic, covering in-memory and on-disk hits) so a stale/invalid tactic is a loud fallback, never a silent wrong kernel or a dead server.autotune_v2_reload()finalize step so homogeneous ranks converge on byte-identical tactics (composes with Addset_autotune_process_groupto synchronize tactic choice across ranks #3187).Summary
FlashInfer owns the persistence and compatibility rules for autotuning results, so frameworks stop constructing cache filenames, parsing FlashInfer's JSON schema, broadcasting raw cache files, and managing invalidation themselves — and, via
MeasurementPolicy, tuning measures the way the deployment actually runs.Exposed as a standalone
autotune_v2()context manager, deliberately disjoint fromautotune(cache=<json path>)(v1) so the design can iterate without inheriting v1 semantics —flashinfer/autotuner/changes are purely additive hooks andautotune()itself is untouched.API
modepersistent_cache"tune""tune""replay""replay"Attach semantics:
persistent_cache=Trueattaches the store for the remainder of the process (like v1'sload_configs) — a survey of vLLM/sglang showed both serve outside any context, so a context-scoped store would silently regress serving to heuristics after warmup exits. The context scopes only when profiling cost may be paid.cache_root(orFLASHINFER_AUTOTUNE_CACHE_DIR) is a placement-only directory; the schema/environment namespaces live below it.MeasurementPolicy — tune the way you deploy
The decisive question is whether per-call host cost counts: under CUDA-graph serving it is paid once at capture (excluding it is correct); under eager serving it is paid every call. Measured on SM100 (
bmm_fp8M=8, kernel ~8µs): the same cuDNN candidate reads 8.0µs host-excluded vs ~330µs host-included — the eager ranking flips to cublas < cutlass ≪ cuDNN, matching real framework behavior.The policy is part of the store's environment identity (manifest → env hash): entries tuned under different policies never overwrite each other. A CUPTI activity-span timer is implemented as a diagnostic-only route (per-iteration GPU spans, median; no delay kernel;
cuptiIsTracingSessionRunningpre-check + graceful events fallback around the legacy single-subscriber limitation) and is reachable only via the private_timer="cupti"override, not throughexecution_mode. It is not used for any shipped policy; see the validation report's CUPTI appendix for why (it cannot rank eagerly-launched multi-kernel ops such asmoe_cute_dsl).Store design
os.replaceas soon as it is tuned — no exit-time read/merge/write, no locks; concurrent all-ranks tuning is safe (redundant work, last valid write wins), so per-rank cache files become unnecessary.save_configsstructurally cannot observe v2 entries (regression-tested).Also included
validate_tactic(inputs, tactic)revalidation on in-memory and on-disk hits (a rejected tactic is a loud fallback, not a blind replay); default-candidate coverage guard.autotune_v2_reload()finalize step for rank convergence.benchmarks/bench_autotuner_accuracy.py: accuracy harness (oracle sweep → regret / top-1 / winner-flip-rate under each policy, scored against both oracles), with an interleaved drift-protected oracle and per-round SM-clock recording. Validated on a production B200 across bmm_fp8 / mm_fp4 / cutlass MoE (thread).Explicitly out of scope (this PR)
execution_mode="auto"to"cuda_graph"(pending forced-capture validation across the op suite)validate_tactic(cuDNN first, via its Yanqinz/autotuner tactic #3707 structured tactics — the hook is here; runners opt in separately)trtllm_fp4_block_scale_moeautotuner can pick slower-than-default tactics and is not EP/DP-aware (MXFP4, SM100) #3537 op-level track; sibling PRs)Relation to existing work
Orthogonal to #3707 (structured
(engine_id, knobs)cuDNN tactics): entries store whatever tactic representation runners produce — compound-tuple round-trip is covered by tests; the environment hash contains index-tactic drift meanwhile.Testing
238 GPU-free tests pass in
tests/autotuner/(the v2 filetest_autotune_cache_v2.pycovering: both real consumer patterns — serve-after-context-exit, hydrate-only; corruption tolerance; env-hash and measurement-policy isolation; bare-serving-uses-ambient-identity and cache_root-switch repopulation; atomicity; v1 coexistence; runner-reorder / extras-collision key completeness; guard win-persist-replay; reload convergence; same-process revalidation fallback — plus the upstream suites incl. the #3187 AST guards). Pre-commit clean.On-GPU validation on SM100 and a production B200: tune → atomic publish → fresh-process serve; a 4-process concurrent tuning storm into one shared store (SGLang all-ranks pattern) with store integrity intact; and the accuracy harness across three ops (bmm_fp8 / mm_fp4 / cutlass MoE) — details, matrices, and framework-migration drafts in the thread. Branch went through several external review rounds plus a four-lens cleanup pass.
🤖 Generated with Claude Code
🔍 Related Issues
RFC #3920 (design) · #3707 (structured tactics, composed) · #3187 (cross-rank sync, composed) · addresses classes from #3186, #3537, #3566, #3622, #3648, #3719 (see the RFC's issue-bucket table)
🚀 Pull Request Checklist
Thank you for contributing to FlashInfer! Before we review your pull request, please make sure the following items are complete.
✅ Pre-commit Checks
pre-commitby runningpip install pre-commit(or used your preferred method).pre-commit install.pre-commit run --all-filesand fixed any reported issues.🧪 Tests
tests/autotuner/test_autotune_cache_v2.py, 28 GPU-free tests; fulltests/autotuner/suite green).Reviewer Notes
Opt-in and additive:
autotune()and the v1 cache path are byte-identical; v2 state lives behindautotune_v2(). Suggested review order:flashinfer/autotune_cache.py(store + API), then the four hook sites inflashinfer/autotuner/autotuner.py(search_cache 2.5, publish, measurement routing, store stack), then the tests.