feat(autotuner): reach both persistence backends through autotune() - #1
feat(autotuner): reach both persistence backends through autotune()#1aleozlx wants to merge 22 commits into
Conversation
…totuner package Port of the autotune-cache-v2 branch onto current main (104 commits, including the autotuner.py -> flashinfer/autotuner/ package split): - search_cache branch 2.5 (managed store) rewritten in the per-runner-key style of flashinfer-ai#4004: hits validate runner_class_name, ProfilingCacheKey.file_key replaces the hand-rolled projection helper. - choose_one publishes via cache_key.file_key / .runner_class_name. - The CUPTI measurement route now lives INSIDE flashinfer-ai#3187's cardinality-invariant try block, so a rank using timer='cupti' still reaches the cross-rank all-reduce on success and on failure; the events_no_delay delay-kernel skip composes with the CC globaltimer backend. - Review fixes: utf-8 encodings on store I/O (gemini), attach/policy-push moved after successful context entry so a failed autotune() entry leaves no side effects (coderabbit), TypeError guard for path-like persistent_cache (coderabbit), unused unpack (coderabbit). - New fix found while dogfooding the port on SM100: re-attaching a different store (e.g. new measurement policy) now clears the decode memo so store A's winners are never served under store B's identity; regression test added. 226 autotuner tests green (24 v2 + upstream incl. flashinfer-ai#3187 AST guards); GPU-validated on SM100: tune -> publish -> fresh-process serve from store. AI-assisted (Claude Code). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase-separated per-candidate oracle measurement manufactured phantom regret under thermal drift (58% on an ES board whose SM clock settles 1965->~660MHz within seconds). The oracle now measures all candidates back-to-back per round with a rotating start offset, records the SM clock per round (pynvml), and scores against two explicit deployment oracles — cupti (host-excluded span) and execution_mode='eager' (host-included) — instead of relying on delay-budget overflow for the eager measurand. AI-assisted (Claude Code). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…re identity Review hardening (external design review of 8338400): - P1: partition the in-memory winner cache by measurement identity (policy manifest fields). Winners tuned under one policy no longer short-circuit tuning under another in the same process; the on-disk isolation contract now holds in memory too. Default flows stay on the plain profiling_cache (v1 byte-identical; save_configs sees exactly what it used to). - P1: context-scoped store targeting via a per-thread store stack. Lookups/publishes inside autotune_v2 target THAT context's store; persistent_cache=False now truly forbids disk for the context even when an ambient store is attached; nested contexts with different identities never publish into each other's manifest. The ambient store still serves context-free lookups after exit. - P2: CUPTI partial-init failure now disables whatever activity kinds were enabled (and flush failure cannot skip disable / mask the original error). - P2: rotating input batches are only allocated on the event path, halving transient tuning memory under cold_l2 with the cupti timer. - harness: add the v2_eager selection policy (completes the policy x deployment matrix) and point the methodology reference at RFC flashinfer-ai#3920. 3 new regression tests (policy-switch reprofile, persistent_cache=False disk-freeze, nested-context publish isolation); 229 GPU-free tests green; GPU-validated: same-process policy sweep now yields 3 env dirs / 12 entries (was 1 dir with silent in-memory reuse). AI-assisted (Claude Code). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sion guard, tactic revalidation Closes the three gaps an issue-sweep review identified as prerequisites for vLLM/SGLang deleting their own cache logic (persistence makes a bad or divergent winner long-lived, so these cannot wait until after migration): - autotune_v2_reload(): rank-consistency finalize step. Call on every rank after a post-tuning barrier: drops in-process winners and store memos so all homogeneous ranks re-read the store's canonical entries (the flashinfer-ai#3186 divergence class; composes with flashinfer-ai#3187's in-session all-reduce). - Regression guard: inside autotune_v2 contexts, (runners[0], -1) always races as a candidate, so a tuned-and-persisted selection can structurally never lose to the default path (the slower-than-default class of flashinfer-ai#3537/flashinfer-ai#3622/flashinfer-ai#3409). Plain autotune() selection stays byte-identical (guard scoped to v2). - Runner-contract revalidation: on-disk hits consult an optional runner hook validate_tactic(inputs, tactic); a rejected tactic is a loud once-logged cache miss (retune or fallback), never a blind replay (the flashinfer-ai#3566 plan-rejected-at-execute class). Plus contract regression tests: runner-list reorder replay, extras collision (flashinfer-ai#3363/vllm#43119 class), guard win/persist/replay, v1-unchanged, reload convergence. 235 GPU-free tests green; GPU-revalidated (3-policy sweep + serve). AI-assisted (Claude Code). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…in-memory hits Round-3 review fixes: - Winner partitions now key on the active/ambient STORE identity (root, env_hash) when a store is targeted — policy fields only for store-less policy tuning, legacy profiling_cache only for pure v1 flows. Bare serving after a policy context exits therefore serves the ambient identity's winner (never a stale legacy-memory winner tuned under a different identity), and switching cache_root repopulates the new root instead of silently reusing the old one. Corollary: winners tuned in a v1 context nested inside a v2 context belong to the v2 identity (they were measured under its policy) and are no longer written into the v1 JSON file; test updated to the corrected semantics. - validate_tactic revalidation now also covers in-memory winners, so a runtime shape the runner rejects becomes a clean fallback in the same serving process (the no-restart flashinfer-ai#3566 case); test added. - The default-candidate guard comment now states its honest scope: no-regression AT THE PROBE inputs; unrepresentative probes (flashinfer-ai#3622/flashinfer-ai#3537 class) are op-level work it does not cover. Framework patch drafts pass an explicit MeasurementPolicy so the deployment-mode choice (flashinfer-ai#3719) is visible at the migration call site. 238 GPU-free tests green (3 new); GPU-revalidated. AI-assisted (Claude Code). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…th clock guard - Op registry + multi-op capture: a composite API (MoE) that tunes several internal ops per call is swept per-op; input builders lifted from the corresponding tests so probes are valid (quant/scale layouts, routing). - --max-candidates subsampling for large sets (MoE ~324), logged as PARTIAL. - Validated on production B200: bmm_fp8 reproduces the deployment-match matrix; mm_fp4 is a clean negative control (<=2%); cutlass MoE tactic selection is ~0% at balanced routing on stable clocks. - PERF_VALIDATION_GUIDE: added the clock caveat — an ES SM100 board threw 43-96% phantom MoE regret by collapsing 1965->120 MHz mid-sweep; check min/max SM-clock ratio before trusting a row. AI-assisted (Claude Code). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…le_tuning bool At a serving call site 'enable_tuning=False' read like 'discard the tuning results', when it actually means 'replay tuned winners, skip profiling'. Replace the negated boolean with a positive mode: autotune_v2(mode='tune') # warmup: profile misses + publish (default) autotune_v2(mode='replay') # serving: replay winners, no profiling persistent_cache stays as the orthogonal disk toggle. Invalid mode raises a clear ValueError. v2 is unmerged/opt-in so no back-compat shim is needed. AI-assisted (Claude Code). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ocess ambient The ambient store (what bare, context-free serving calls read) is a process-wide default set at warmup. Entering a NESTED autotune_v2 context used to also rebind that ambient as a side effect, so a scoped execute-time override (e.g. an eager-region context inside a graph-serving process) leaked past its 'with' block and changed what later bare calls selected -- a scope guard that fails to restore what it touched. Now only a TOP-LEVEL context (store stack empty on entry: warmup or an explicit re-attach) sets the ambient; a nested context resolves + pushes its store for the region and pops on exit, leaving the ambient default intact. Sequential top-level re-attach stays last-wins. Splits _attach_managed_cache into _resolve_managed_store(set_ambient=). Two regression tests: nested override does not clobber ambient; sequential top-level is last-wins. 241 GPU-free tests green; tune/serve revalidated on SM100. AI-assisted (Claude Code). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…o on switch codex round-4 P1(runtime)+P2: store registry keyed by (root, manifest) reuses store objects across identity switches; re-attach no longer clears the decode memo (keyed by (root,env,file_key), so identities coexist and post-hydrate lookups stay pure-memory; alternating graph/eager no longer re-reads disk). clear_cache clears every store's memo. replay+persistent_cache=False documented as memory-only replay. Regression test A->B->A serves from memory after disk delete. Open (codex P1 design): explicit scope= vs nesting-inferred lifetime. 241 GPU-free tests green. AI-assisted (Claude Code). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t record codex round-5 (converged): the store stack existed only to support nested / scoped-override autotune_v2, which no real consumer does -- verified across vLLM (single warmup context before capture, bare serving) and SGLang (one context per warmed-once forward; speculative path sequential per-capture, guarded by tuned_phases). The stack was implicit state for a phantom use case; scope=/top-level-vs-nested inference was its fragile surface. Replace the per-thread store stack + measure stack with a single-slot per-thread record (_V2Local: active/store/measure). A persistent context always attaches its store as the process ambient (last-wins); bare serving reads the ambient; a context's own lookups read its slot (concurrent threads in different identities stay isolated). autotune_v2 no longer nests: v2-in-v2 fails fast (nesting a plain v1 autotune() is still fine). No scope= param. Kept (the real isolation): store registry, identity-partitioned winner cache, identity-keyed decode memo, validate_tactic on hits. 241 GPU-free tests green; tune/serve revalidated on SM100. AI-assisted (Claude Code). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… flow Reproduces the exact consumer sequence -- warmup context over a shape grid -> autotune_v2_reload() -> bare (context-free) serving -- against a real bmm_fp8 op, short of a full model server. Asserts warmup profiles + publishes, and bare serving hits the store with ZERO re-profiling. Runs BOTH measurement policies, including execution_mode='cuda_graph' (per-candidate CUDA-graph capture profiling), which had no prior on-GPU coverage. Both pass on SM100: eager 7 entries, cuda_graph 7 entries (separate env dir), bare serve 0 profile calls / 4 store hits each. vLLM patch comment updated: cuda_graph is the mode-sensitive-correct policy and is now validated-working for bmm_fp8; flip once the full op suite's capture-safety is confirmed. NOTE: this validates the API SEQUENCE + cuda_graph profiling, NOT a full vLLM/SGLang server e2e (no real model / scheduler / multi-rank NCCL) -- that remains the open validation gap. AI-assisted (Claude Code). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… 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>
Ships v2 under the existing top-level API instead of a second context
manager. `autotune()` gains an explicit `managed_cache` argument that
selects the persistence backend; `autotune_v2()` becomes a thin alias
that only spells the arguments differently.
autotune(True) # legacy: memory only
autotune(True, cache="cfg.json") # legacy: JSON file cache
autotune(True, managed_cache=True) # managed store
autotune(False, managed_cache=True) # replay, no profiling
autotune(True, managed_cache=False) # forbid disk for this context
`managed_cache` defaults to None, so absent the argument behaviour is
byte-identical to before it existed. This is not the "subtle behaviour
change" a merged API risks: the backend is selected by an explicit
argument, and resolved ONCE on context entry before any state is
touched. Within a context the two cannot interleave -- the legacy file
path runs iff managed_cache is None, and the managed store is consulted
iff a context record was pushed. Mixing is prevented by construction,
which is the property the separate-function design was reaching for.
The two objections to merging are answered rather than accepted:
- Call-site signature: `cache=<path>` is NOT redefined. It keeps its
exact v1 meaning; the store is placed with a distinct `cache_root`,
and combining the two raises instead of guessing.
- Lifetime: process-lifetime attach happens only on the branch a caller
explicitly opted into. `autotune(True)` and `autotune(True, cache=...)`
keep block-scoped semantics.
Beyond naming, one front door is a structural brake on divergence. Two
context managers are free to drift -- separate arguments, lifetimes, and
mental models -- until "migrate to v2" means relearning the API rather
than passing a flag. One signature makes each divergence explicit and
reviewable where it is introduced, and makes eventual v1 removal a
deleted branch rather than a caller migration.
Dependency inverted: autotune_v2() previously delegated to autotune()
and then attached; now every behaviour lives in autotune() and the alias
just forwards. `_attach_managed_store()` builds the manifest (v1 env
metadata + non-default MeasurementPolicy fields) on the managed branch.
Design doc updated in the same change: §4 previously argued FOR a
separate entry point and now explains why the two share one; §5.1
records the merge as done and keeps the remaining graduation steps.
Tests: tests/autotuner/test_autotune_unified_api.py, 18 GPU-free cases
covering argument validation and rollback, backend isolation in both
directions, attach-survives-exit, replay, measure-without-persistence,
and alias/unified equivalence. NOT RUN LOCALLY -- no torch/pytest in
this environment; they need CI or a GPU box.
AI-assisted: drafted with Claude Code.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Renames the selector from managed_cache to v2_opt_in, and moves the "no disk this context" case onto its own persistent_cache argument. The earlier tri-state (managed_cache None/True/False) overloaded one name: False meant "use v2 machinery but forbid disk", which is not "opt out of v2". Splitting the two makes each argument mean one thing and mirrors the alias's existing shape. The name is deliberately a version rather than a behaviour, because a version-named flag announces its own expiry and retires along a path a behaviour name (or a separate function) cannot offer: flip the default -> ignore the argument internally once no caller passes it -> delete it with callers working at every step, each step independently verifiable and revertible. A separate autotune_v2() function cannot be "ignored" -- importing it either resolves or raises -- so its removal is one breaking edit for every caller. A behaviour name like managed_cache= would read as permanent and invite the cargo-culting that keeps a transitional switch alive forever. The durable knobs (persistent_cache, cache_root, measure) keep behaviour names because they outlive v1. Design doc updated in the same change: - §2.1 no longer claims v2 is "a standalone context manager, deliberately disjoint from autotune()" -- that became false when the entry points merged. - §5.1 gains the option-space comparison as a table, including the effect on FUTURE EDITS: with two entry points a change to shared machinery must be plumbed twice and drifts by omission, and a v1-only fix (e.g. a ported TRT-LLM change) is invisible to v2 unless someone remembers to mirror it. With one signature both land in view of the same reviewer. Divergence cost is paid per-change as a review question instead of later, all at once, as a migration. - §5 opens with an explicit CRITICAL ASSUMPTION callout: the entire graduation plan assumes v1 is eventually removed, which is not decided, has no owner and no date. Lists what must become true (frameworks stay migrated; TRT-LLM work lands against the runner contract rather than v1's cache; a major version is available), names the honest alternative outcome (indefinite coexistence with a preferred default), and separates the unconditional part -- one entry point -- from the conditional part -- removal. - Scope line now says the v2 *branch* of autotuner.py rather than the whole file; fixed a stale §4 cross-reference that meant §3. Tests renamed to match. Still NOT RUN LOCALLY -- no torch/pytest here. AI-assisted: drafted with Claude Code. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Folding v2's logic INTO autotune() made one function hold both
implementations' state, which is exactly where a later edit can wire up
a subtle cross-path behaviour without anyone noticing. Restructured so
the entry point only chooses:
if v2_opt_in:
_reject_v1_only_args(cache)
with autotune_v2(...): # v2's implementation, unchanged, in
yield # autotune_cache.py
return
_reject_v2_only_args(cache_root, persistent_cache, measure)
# ---- v1 implementation ----
v2's body moves back to autotune_v2() where it was. The v1 body no
longer contains a single line of v2 state. Interleaving is not merely
prevented, it is unrepresentable: the two never share a function scope.
No recursion -- autotune_v2 calls autotune() without v2_opt_in for the
shared machinery (bucketing, skip_ops, tuning-mode refcount).
Also switches validation to positive selection. `if not v2_opt_in:` was
an unbounded condition: it means "everything that is not v2" and would
silently absorb a third implementation the day one exists. Each branch
is now keyed on the implementation it names and rejects only the
arguments the OTHER one owns, via two small named helpers
(_reject_v1_only_args / _reject_v2_only_args), so adding an
implementation means adding a case rather than widening an existing
one. The earlier dict-and-loop version of this validation was dropped:
it was compact but not scannable, and unscannable validation is the
same hazard in a different place.
Behaviour change: measure= now requires v2_opt_in=True. It is a v2
concept and the legacy implementation has no measurement policy; under
the dispatcher there is no longer a path that could honour it on the v1
branch. Previously it was accepted without the opt-in.
Design doc §2.1 and §5.1 updated to describe dispatch rather than a
merged body.
Tests updated: measure-without-opt-in now asserts the raise, and a new
case covers a policy under v2 with persistent_cache=False.
Still NOT RUN LOCALLY -- no torch/pytest in this environment.
AI-assisted: drafted with Claude Code.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Six of the eighteen cases were policing a boundary that separate function bodies now make unrepresentable, or duplicating coverage that already exists in test_autotune_cache_v2.py: test_default_is_byte_identical_legacy -> test_legacy_v1_path_does_not_create_v2_dirs test_v2_writes_no_v1_file -> test_v2_disk_entries_cannot_leak_into_v1_file test_persistent_cache_false_forbids_disk -> test_persist_false_disables_disk test_attach_survives_context_exit -> test_serving_after_context_exit_reuses_entries test_replay_mode_does_not_profile -> test_hydrate_only_context_enables_reuse test_measure_policy_without_persistence -> test_persistent_false_context_never_touches_disk They were written when v2's logic lived inside autotune() and a stray edit really could have made one path observe the other's state. Once the entry point became a dispatcher, v2's behaviour is reached through the same autotune_v2() the existing suite already exercises, so the duplicates only added runtime and a second place to update. The alias/dispatch equivalence parametrisation drops from three cases to two: dispatch now *calls* autotune_v2(), so equivalence is by construction and the test only needs to pin the argument mapping. What remains (12) tests the dispatcher, which is the actually-new code: argument ownership and its error messages, the nesting guard, rollback on rejected arguments, validation ordering, one smoke test that dispatch reaches v2 at all, and the alias's argument translation. AI-assisted: drafted with Claude Code. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
docs/autotuning.rst described the legacy implementation exclusively, and two of its sections were not merely incomplete but wrong once v2 exists: - The Config Caching note warns that multi-process use is "best-effort: concurrent writes to a shared cache file may result in lost updates from race conditions" -- exactly what v2's per-entry atomic publish fixes. A reader hitting that warning had no way to learn the fix shipped in the same release. Now qualified as describing the legacy file cache, pointing at v2. - Config Lookup Priority omitted the managed store, which is a real tier in the resolution order. Added, and the list renumbered. Adds an "Autotuner v2 -- Managed Persistence (Experimental)" section: opt-in example, warmup/serving and hydrate-only flows, a legacy-vs-v2 comparison table (cache identity, unit of write, concurrent ranks, crash mid-tuning, wrong environment), cache location and layout, the non-persistence of entries across versions, measurement policy, and the distributed story including autotune_v2_reload(). Adds API-reference entries for v2_opt_in, persistent_cache, cache_root and measure on autotune(), plus MeasurementPolicy, autotune_v2_reload and autotune_v2 -- three symbols exported from the top-level package that had no user-facing documentation anywhere. The v2 section leads with the transitional nature of v2_opt_in and points at the design doc for what is not yet decided about v1's future, so a reader does not mistake an opt-in flag for a stable configuration surface. Design doc §5.4 updated from "documentation debt" to what now exists, keeping the two obligations that belong to later graduation steps. AI-assisted: drafted with Claude Code. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Investigation: how vLLM and SGLang call the autotuner todayNew in this comment
Corrections to the first revision of this comment
Re-verified, unchanged: every claim in flashinfer-ai#3920 Evidence 1 still holds at HEAD 2026-08-04, including the zero-external-use feature list. Read against Most of this was already established in flashinfer-ai#3920 and in
Confirmed, already documented
The only thing re-verification adds is currency: all of it still holds three weeks later. Where the code lives (for the migration PRs)vLLM — SGLang — Full used surface across both projects: New — not found in flashinfer-ai#3920 or the flashinfer-ai#3861 thread1. vLLM has a second autotune call site, and the migration draft misses it.
and from vllm.model_executor.warmup.flashinfer_autotune_cache import (
resolve_flashinfer_autotune_file,
write_flashinfer_autotune_cache,
)So applying the draft as written is an Why it was missed — worth recording, because the obvious explanations do not hold:
The finding also illustrates §2.2's filename-as-identity argument concretely: two independent tuning passes were never really distinct cache identities, and only v1's read/merge/write hid it. 2. SGLang can only detect "am I inside autotune?" by sniffing the Python stack.
traces = traceback.format_stack()
# Skip autotune stack traces
if any("_flashinfer_autotune" in trace for trace in traces):
returnA symmetric-memory debug check suppresses warnings during tuning by matching a function name in the stack — it breaks the day that function is renamed. There is no supported predicate; 3. vLLM omits SGLang appends 4. A wording tension in flashinfer-ai#3920's Evidence 1. The vLLM bullet reads "rank 0 runs Divergence between the two consumers
Effort estimate: us sending the migration PRsFraming: neither project will prioritise migrating someone else's caching layer, and the patch draft in flashinfer-ai#3861 already implies we send both. So this is our cost plus two external review queues. Diffs are small, mostly deletionvLLM ≈ 150 lines removed / 30 added — SGLang ≈ 60 removed / 10 added — Coding: ~half a day (vLLM), a few hours (SGLang). That is not the cost. What actually costs
vLLM's multi-GPU path is a behaviour change, not a refactor. Persistence is currently off there, so migrating turns caching on in the configuration everyone runs. Needs a rank-consistency and perf gate, not a green test suite. The hard sell is key completeness, and it is the one thing still unguarded. vLLM disabled this because a key omitted a field. Re-enabling requires showing the class is structurally fixed — contract rule 5 plus the debug-mode completeness check, which flashinfer-ai#3920 lists as "proposed", not in flashinfer-ai#3861. The thing that got the feature disabled is the thing not yet mechanically guarded. Build the check before opening the vLLM PR, or the review stalls on exactly the point that killed it last time. Pre-empt one conceptual objection in the PR body. vLLM's cache identity is model-level; ours contains no model identity at all, separation coming from the op key. Correct, but it looks wrong at a glance and is better answered in the description than in a review round. Estimate
Implication for §5.2 gate 1"Frameworks migrated and released" unpacks into four sequential steps, the first two ours and blocking:
Realistically multi-quarter. That does not change what this PR does — one entry point is right either way — but it should calibrate how the graduation timeline is written, and it reinforces the §5 assumption callout: worth making coexistence good rather than treating removal as imminent. Line numbers read at HEAD 2026-08-04; both projects move fast. |
§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>
…o feat/autotune-unified-api # Conflicts: # docs/design_docs/autotuner_v2.md
c701529 to
c2f48b7
Compare
…nfer-ai#4219) ## Issue flashinfer-ai#4214 Addresses GDN-C1 / GDN-C2 / GDN-C3 / GDN-H1 from the GDN CuTe-DSL cache audit (PR #1 of the suggested sequence). ## Summary - Documented FP16 `q/k/v/a/b` on the BF16-state and FP32-state MTP decode paths were silently reinterpreted as BF16 because the kernels hard-code `cutlass.BFloat16` fragments. Convert those operands to BF16 at the kernel boundary (and stage non-BF16 caller `output=` on MTP). - Add polymorphic operand dtypes (`A_log`, `dt_bias`, slot indices) to the compile-cache identities so mixed-dtype sequences no longer collide. - Stop returning a cached per-batch default `output` buffer from BF16-state paths when `output=None`. - Apply the same FP16 conversion on the WY output-only kernel; convert slot indices to int32 when needed. - Follow-up: non-BF16 MTP `output=` staging uses `output.to(bfloat16)` (not `empty_like`) so negative-index padding rows keep caller-initialized values. - Assert documented `dt_bias` (bf16/fp32) and `initial_state_indices` (int32/int64) dtypes at the public API. ## Test plan - [x] New regressions in `tests/gdn/test_decode_delta_rule.py` (8 parametrizations): FP16 conversion, dtype / `dt_bias` interleaving, default-output non-aliasing, non-BF16 `output=`, padding-slot preservation, WY FP16 - [x] Those regressions fail on unmodified `main` (most on main) and pass on this branch - [x] Full `tests/gdn/test_decode_delta_rule.py`: **838 passed** on H100 NVL (`CUDA_VISIBLE_DEVICES=1`, `-x -vv`, ~38 min) - [ ] GPU CI: `@flashinfer-bot run` ## Review Independent re-review at `024e7c4f`: **approve-with-nits** (padding critical fixed). Follow-up commits add comment trim + API dtype asserts. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved validation for tensor data types, shapes, and index values. * Fixed mixed-precision decode workflows, including proper BF16 conversion and preservation of requested output types. * Prevented stale output reuse and preserved padding in partially filled output buffers. * Improved compilation behavior when switching between supported input data types. * **Tests** * Added coverage for FP16/BF16 conversion, output handling, cache isolation, index validation, and mixed-precision correctness. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
f50a940 to
f7760f2
Compare
📌 Description
Targets
yanxu/autotune-cache-v2-mvp, notmain— a proposal for flashinfer-ai#3861.Reach v2 through the existing
autotune()instead of a second public context manager. An explicitv2_opt_inargument selects the implementation, andautotune()dispatches toautotune_v2()rather than absorbing it:
Not the "subtle behaviour change" flashinfer-ai#3920 warned about.
v2_opt_indefaults toFalse, so absentthe argument behaviour is byte-identical. The v2 branch hands off the whole context and returns, so
the v1 body never runs for it — the two implementations never share a function scope, and
interleaving is not merely prevented but unrepresentable. No recursion:
autotune_v2()callsautotune()without the opt-in for the shared machinery (bucketing,skip_ops, tuning-moderefcount).
Branch selection is positive.
if not v2_opt_in:asserts "everything that is not v2", anunbounded set that would silently absorb a third implementation; each branch now names its own and
rejects only the arguments the other owns.
The RFC's two objections are answered rather than accepted:
cache=<file>would be silently redefined as a root dircachekeeps its exact v1 meaning; the store uses a separatecache_root, and combining them raises.autotune_v2(), reached only via the opt-in.autotune(True)andautotune(True, cache=...)keep block scope.Why one entry point. A shared front door is a structural brake on divergence, and it changes
what happens to future edits: with two entry points a change to shared machinery must be plumbed
twice and drifts by omission, and a v1-only fix (a ported TRT-LLM change, say) is invisible to v2
unless someone remembers to mirror it. With one signature both land in front of the same reviewer —
divergence is paid per-change as a review question rather than later, all at once, as a migration.
Why
v2_opt_inand not a behaviour name. A version-named flag announces its own expiry andretires along a path a function name cannot offer: flip the default → ignore the argument internally
once no caller passes it → delete it, with callers working at every step, each step independently
verifiable and revertible.
autotune_v2()cannot be "ignored" — an import either resolves or raises— so removing it is one breaking edit for every caller. The durable knobs (
persistent_cache,cache_root,measure) keep behaviour names because they outlive v1.User-facing docs, the gap flagged on flashinfer-ai#3861.
docs/autotuning.rstdescribed the legacyimplementation only, and two sections were wrong once v2 exists: the Config Caching note warned
that concurrent writes "may result in lost updates" — precisely what v2's atomic per-entry publish
fixes — and Config Lookup Priority omitted the managed store. Both corrected, plus an
Autotuner v2 (Experimental)section and API-reference entries forv2_opt_in,persistent_cache,cache_root,measure,MeasurementPolicy,autotune_v2_reloadandautotune_v2— three ofwhich are exported from the top-level package and had no user-facing documentation anywhere.
Design doc, updated in the same change: §2.1 describes dispatch (it previously claimed v2 was
"deliberately disjoint from
autotune()"); §4 explains why the two share an entry point; §5 openswith the assumption callout; §5.1 carries the option-space and future-edits comparison; §5.4 records
the docs as written rather than owed; the Scope line narrows to the v2 branch of
autotuner.py.🔍 Related Issues
RFC flashinfer-ai#3920 · MVP flashinfer-ai#3861 · design-doc policy flashinfer-ai#4334
🚀 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_unified_api.py, 12 GPU-free cases coveringthe dispatcher, which is the new code: argument ownership and its error messages, the nesting
guard, rollback on a rejected argument, validation ordering, one smoke test that dispatch reaches
v2, and the alias's argument translation. Deliberately thin on v2 behaviour — that is unchanged
and already covered by
test_autotune_cache_v2.py; six cases written against the earliermerged-body design were deleted once dispatch made the boundary structural.
and the existing
tests/autotuner/suite are unrun. Needs CI or a GPU box.Reviewer Notes
measure=now requiresv2_opt_in=True(behaviour change from an earlier revision of thisPR). It is a v2 concept and the dispatcher has no path that could honour it on the v1 branch.
Say the word if you would rather it stayed permissive.
persistent_cachewithoutv2_opt_in=Trueraises — deliberate, but it is a new error for anargument name
autotune_v2()already accepts.is absorbing upstream changes, that weakens once those changes must satisfy the runner contract
anyway. Worth checking against a real recent TRT-LLM port — it decides whether §5's timeline is
conservative or simply never.