Skip to content

feat(autotuner): Autotuner v2 (autotune_v2) — managed persistence, deployment-matched measurement, runner contract - #3861

Merged
aleozlx merged 30 commits into
flashinfer-ai:mainfrom
YangXu1990uiuc:yanxu/autotune-cache-v2-mvp
Sep 5, 2026
Merged

aleozlx merged 30 commits into
flashinfer-ai:mainfrom
YangXu1990uiuc:yanxu/autotune-cache-v2-mvp

Conversation

@YangXu1990uiuc

@YangXu1990uiuc YangXu1990uiuc commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

📌 Description

Autotuner v2 — an evolution of the autotuner exposed through a standalone autotune_v2(), additive over autotune() (which stays byte-identical). It started as a managed persistent cache and has grown, across review, into five composing pillars:

  1. Managed persistence — FlashInfer owns cache keys, schema, environment isolation, and invalidation; frameworks stop hashing filenames, parsing the JSON schema, broadcasting raw cache files, and managing staleness. Per-entry atomic store, safe for concurrent all-ranks tuning.
  2. Deployment-matched measurement (MeasurementPolicy) — tune the way you serve (eager vs CUDA-graph host-cost semantics); part of the store's environment identity.
  3. Runner contract — self-describing tactics + runtime revalidation (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.
  4. Distributed consistency — a shared store plus autotune_v2_reload() finalize step so homogeneous ranks converge on byte-identical tactics (composes with Add set_autotune_process_group to synchronize tactic choice across ranks #3187).
  5. Selection quality — a default-candidate coverage guard (a tuned result cannot lose to not-tuning at the probe inputs) and an accuracy harness that quantifies regret against a deployment-faithful oracle.

RFC (design + evidence, for public/customer review): #3920. Detailed rationale, the review-round fixes, framework-migration drafts, and the production-B200 validation are in the thread below.

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 from autotune(cache=<json path>) (v1) so the design can iterate without inheriting v1 semantics — flashinfer/autotuner/ changes are purely additive hooks and autotune() itself is untouched.

API

with flashinfer.autotune_v2(measure=MeasurementPolicy(execution_mode="cuda_graph")):
    model(dummy_inputs)          # startup warmup: tune + publish atomically
model(inputs)                    # serving: reuses entries, no context needed

# fresh process, same environment: hydrate + serve
with flashinfer.autotune_v2(mode="replay", measure=policy):
    pass
mode persistent_cache meaning
"tune" True (default) tune misses, publish to disk
"tune" False tune in-memory only (no disk I/O)
"replay" True serve from the on-disk cache (no profiling)
"replay" False no-op unless already hydrated

Attach semantics: persistent_cache=True attaches the store for the remainder of the process (like v1's load_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 (or FLASHINFER_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_fp8 M=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.

MeasurementPolicy(execution_mode="cuda_graph")  # profile under real capture+replay
MeasurementPolicy(execution_mode="eager")       # host cost included (no delay kernel)
# default "auto" = today's legacy behavior; cold_l2 composes. The timer is derived
# from execution_mode and is always CUDA events: "auto"/"cuda_graph" use the
# events+delay-kernel window, "eager" uses events without the delay kernel.
# CUPTI activity-span timing is NOT selected by any public mode; it exists only
# as a private diagnostic override (_timer="cupti") used by the accuracy harness.

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; cuptiIsTracingSessionRunning pre-check + graceful events fallback around the legacy single-subscriber limitation) and is reachable only via the private _timer="cupti" override, not through execution_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 as moe_cute_dsl).

Store design

<root>/v2/<environment_hash>/          # sha256 of canonical manifest (flashinfer/CUDA/
  manifest.json                        #   cuBLAS/cuDNN backend+frontend/GPU [+ policy])
  entries/<operation_hash>.json        # one atomic file per tuned operation
  • One entry per tuned op, published via tempfile + atomic os.replace as 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.
  • Missing/malformed/key-mismatched entries are cache misses, never errors; each entry embeds its canonical key.
  • Hit/miss memoized (decoded once); the serving hot path re-reads neither the filesystem nor JSON.
  • Incompatible environments hash to different directories: nothing to invalidate manually, downgrade-safe.
  • v1 and v2 never share in-memory state — a v1 save_configs structurally cannot observe v2 entries (regression-tested).

Also included

  • Runner contract: 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.
  • Distributed: 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).
  • Observability: loud INFO at store attach, once-per-op hit provenance logs.

Explicitly out of scope (this PR)

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 file test_autotune_cache_v2.py covering: 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

  • I have installed pre-commit by running pip install pre-commit (or used your preferred method).
  • I have installed the hooks with pre-commit install.
  • I have run the hooks manually with pre-commit run --all-files and fixed any reported issues.

🧪 Tests

  • Tests have been added or updated as needed (tests/autotuner/test_autotune_cache_v2.py, 28 GPU-free tests; full tests/autotuner/ suite green).
  • All tests are passing.

Reviewer Notes

Opt-in and additive: autotune() and the v1 cache path are byte-identical; v2 state lives behind autotune_v2(). Suggested review order: flashinfer/autotune_cache.py (store + API), then the four hook sites in flashinfer/autotuner/autotuner.py (search_cache 2.5, publish, measurement routing, store stack), then the tests.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds a managed v2 autotune cache backend, wires it into autotune_v2() and AutoTuner, and adds tests covering persistence, reuse, isolation, compatibility, and lifecycle behavior.

Changes

Managed autotune cache

Layer / File(s) Summary
Cache root and atomic write helpers
flashinfer/autotune_cache.py
Defines the v2 schema version, cache root selection, canonical JSON serialization, and atomic JSON file writing helpers.
ManagedAutotuneCache implementation
flashinfer/autotune_cache.py
Implements environment-hashed entry storage, manifest initialization, per-key lookup and publish operations, and per-process memoization for hits and misses.
autotune_v2 context manager
flashinfer/autotune_cache.py, flashinfer/__init__.py
Adds the managed autotune context manager and exposes it from the package top level.
AutoTuner managed cache wiring
flashinfer/autotuner.py
Adds managed backend state, managed-cache lookup in search_cache(), eager publish during profiling, and memo clearing on cache reset.
Persistence and lookup tests
tests/autotuner/test_autotune_cache_v2.py
Adds scaffolding and coverage for publishing entries, lookup reuse, corruption handling, environment isolation, tuple round-tripping, and publish failure tolerance.
Compatibility and lifecycle tests
tests/autotuner/test_autotune_cache_v2.py
Adds coverage for v1 and PathLike compatibility, root relocation, persistent-cache disablement, disk separation, attach idempotence, hydrate-only reuse, and context-entry failure cleanup.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies Autotuner v2 and managed persistence, which are central changes. Its additional references to measurement and runner contracts remain related to the stated feature scope.
Description check ✅ Passed The description includes the required description, related issues, testing, checklist, and reviewer notes sections. It provides detailed scope, API, implementation, testing, and out-of-scope informati…
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread flashinfer/autotuner.py Outdated
Comment on lines 625 to 642
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()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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()

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread flashinfer/autotuner.py Outdated
Comment on lines +686 to +688
if use_managed_cache:
with tuner._lock:
tuner._managed_cache = managed_prev

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

Suggested change
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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread flashinfer/autotuner.py Outdated
Comment on lines 708 to 717
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

Suggested change
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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread flashinfer/autotune_cache.py Outdated
"""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:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Specify encoding="utf-8" in os.fdopen to ensure that the JSON file is written consistently across different platforms (especially on Windows where the default encoding is often not UTF-8).

Suggested change
with os.fdopen(fd, "w") as f:
with os.fdopen(fd, "w", encoding="utf-8") as f:

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ab85c2cencoding="utf-8" added to the store's write path.

Comment thread flashinfer/autotune_cache.py Outdated
return None
path = self._entry_path(file_key)
try:
with open(path, "r") as f:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Specify encoding="utf-8" when opening the cache file to ensure consistent reading across different platforms, matching the encoding used when writing.

Suggested change
with open(path, "r") as f:
with open(path, "r", encoding="utf-8") as f:

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ab85c2cencoding="utf-8" added to the store's read path, matching the writer.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.py
  • flashinfer/autotuner.py
  • tests/autotuner/test_autotune_cache_v2.py

Comment thread flashinfer/autotuner.py Outdated
Comment thread tests/autotuner/test_autotune_cache_v2.py Outdated
@YangXu1990uiuc YangXu1990uiuc changed the title [RFC] feat(autotuner): FlashInfer-managed per-entry autotune cache (v2 MVP, autotune(cache=True)) [RFC] feat(autotuner): FlashInfer-managed per-entry autotune cache (v2 MVP, autotune(persist=True)) Jul 7, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Stale API reference in comment.

The comment says the managed cache is "set by autotune(cache=True)", but the activation API is now persist=True; cache=True raises TypeError (verified by test_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.py
  • flashinfer/autotuner.py
  • tests/autotuner/test_autotune_cache_v2.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • flashinfer/autotune_cache.py

@YangXu1990uiuc YangXu1990uiuc changed the title [RFC] feat(autotuner): FlashInfer-managed per-entry autotune cache (v2 MVP, autotune(persist=True)) [RFC] feat(autotuner): FlashInfer-managed per-entry autotune cache (v2 MVP, autotune_v2()) Jul 7, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Do not let managed v2 contexts read legacy _file_configs.

Inside autotune_v2(), search_cache() still checks _file_configs before _managed_cache, so a previously loaded v1 JSON config can override or satisfy a v2 managed lookup. Gate the legacy block behind managed_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 lift

Make the active managed backend thread/context-local.

_managed_stack is process-global and _managed_cache returns the top entry, so overlapping autotune_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__.py
  • flashinfer/autotune_cache.py
  • flashinfer/autotuner.py
  • tests/autotuner/test_autotune_cache_v2.py

Comment thread flashinfer/autotune_cache.py Outdated
@YangXu1990uiuc YangXu1990uiuc changed the title [RFC] feat(autotuner): FlashInfer-managed per-entry autotune cache (v2 MVP, autotune_v2()) [RFC] feat(autotuner): FlashInfer-managed per-entry autotune cache (v2 MVP, autotune_v2) Jul 7, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Include managed-cache hits in the tuned-range warning. choose_one() only checks profiling_cache and _file_configs, so a v2-only process can fall back outside the tuned bucket range without the perf-cliff warning. A small public accessor on ManagedAutotuneCache would let has_tune_data include backend hits without reaching into _hits directly.

🤖 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.py
  • flashinfer/autotuner.py
  • tests/autotuner/test_autotune_cache_v2.py

Comment thread flashinfer/autotune_cache.py Outdated
@YangXu1990uiuc

Copy link
Copy Markdown
Collaborator Author

Update (2 new commits):

  • 1a247605 — observability for attach semantics: loud INFO log at store attach (repr: env_dir + memo counts), complementing the once-per-op source=managed cache hit log.
  • e83f0201MeasurePolicy (first half of the measurement-policy milestone): autotune_v2(measure=MeasurePolicy(cuda_graph=..., cold_l2=...)) forces profiling to match the deployment (CUDA-graph replay vs eager; cold vs hot L2), promoting the previously op-author-only TuningConfig knobs to a user-selectable per-context policy (None fields inherit). Winners depend on how they were measured, so the policy is part of the store's environment identity (manifest → env_hash): entries tuned under different policies never overwrite each other, and consumers attach with the same policy. Inert when unused; v1 untouched. 22 GPU-free tests now.

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).

@YangXu1990uiuc

Copy link
Copy Markdown
Collaborator Author

Measurement-policy milestone complete (e83f0201 + b77ccea8):

MeasurePolicy(cuda_graph=..., cold_l2=..., timer="cupti") — profile the way the deployment runs. The CUPTI timer measures per-iteration GPU kernel spans from activity records (mirrors bench_gpu_time_with_cupti): no delay kernel needed — GPU-side timestamps structurally exclude host launch latency. On-GPU validation: identical winners and identical 2x tactic ratios under all five policy combinations, while absolute magnitudes legitimately diverge (67.7µs events vs 82.1µs cupti for the same matmul: steady-state throughput vs isolated-call latency semantics) — which is exactly why the timer is part of the policy and of the store's environment identity. cupti-python>=13 detected once, graceful events fallback. 24 GPU-free tests; 147 total in the autotuner suites.

YangXu1990uiuc and others added 19 commits September 4, 2026 14:48
… 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.
@YangXu1990uiuc
YangXu1990uiuc force-pushed the yanxu/autotune-cache-v2-mvp branch from 082ce20 to ba40a49 Compare September 4, 2026 22:10
@YangXu1990uiuc

Copy link
Copy Markdown
Collaborator Author

@flashinfer-bot run

@Vinnie6167

Copy link
Copy Markdown
Contributor

/bot run tests/autotuner

@flashinfer-bot

Copy link
Copy Markdown
Collaborator

GitLab MR !1421 has been created, and the CI pipeline #66310023 is currently running. I'll report back once the pipeline job completes.

@aleozlx
aleozlx enabled auto-merge (squash) September 4, 2026 23:32
@aleozlx
aleozlx merged commit 3a75b4e into flashinfer-ai:main Sep 5, 2026
24 of 25 checks passed
YangXu1990uiuc added a commit to YangXu1990uiuc/vllm that referenced this pull request Sep 11, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants