feat(jit): JitSpec ABC + disk cache for JIT-compiled CuTe-DSL kernels - #3874
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds persistent CuTe-DSL kernel caching, refactors the JIT lifecycle into a shared base template, routes NVFP4 CuTe-DSL compilation through the cache wrapper, and updates related documentation and exports. ChangesCuTe-DSL JIT and kernel caching
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant NVFP4 as nvfp4_quantize compile path
participant Cache as build_and_load_cute_dsl_kernel
participant JitSpec as JitSpecCuteDsl
participant Disk as cached_ops
participant Compiler as cute.compile
NVFP4->>Cache: request kernel(name, extra_key_files)
Cache->>JitSpec: build_and_load()
JitSpec->>Disk: check .o and meta.json
alt cache hit
Disk-->>JitSpec: load exported symbol
else cache miss or stale
JitSpec->>Compiler: compile_fn()
Compiler-->>JitSpec: compiled kernel
JitSpec->>Disk: write .o then meta.json
end
JitSpec-->>NVFP4: kernel handle
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces an on-disk caching mechanism for JIT-compiled CuTe-DSL kernels, mirroring the two-level caching used for nvcc-compiled modules. It adds a core module for managing the cache and integrates it with the nvfp4 quantization kernels. The review feedback is highly constructive, focusing on making the cache architecture-aware on mixed multi-GPU systems by passing a device parameter, and adding defensive checks to prevent potential FileNotFoundError or AttributeError when hashing source files.
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.
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
docs/design_docs/cute_dsl_kernel_cache.md (1)
123-129: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winBroken internal cross-reference: "see 3.2" doesn't exist.
Line 127 references section "3.2" for linking
.os into.sos, but "## 3. Alternatives considered" only contains subsection "3.1" (Line 99). This should point to 3.1.📝 Proposed fix
-- **No `JitSpecRegistry` / AOT integration**: cute-dsl kernels are invisible to `flashinfer aot` and `flashinfer-jit-cache` packaging, and do not honor `FLASHINFER_DISABLE_JIT`. AOT support would prebuild the module directories (or link them into `.so`s, see 3.2). +- **No `JitSpecRegistry` / AOT integration**: cute-dsl kernels are invisible to `flashinfer aot` and `flashinfer-jit-cache` packaging, and do not honor `FLASHINFER_DISABLE_JIT`. AOT support would prebuild the module directories (or link them into `.so`s, see §3.1).🤖 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 `@docs/design_docs/cute_dsl_kernel_cache.md` around lines 123 - 129, The cross-reference in the “No `JitSpecRegistry` / AOT integration” bullet points to a non-existent section. Update the mention of linking into `.so`s in the `cute_dsl_kernel_cache.md` design doc so it references the existing “3.1” subsection instead of “3.2”, keeping the wording aligned with the surrounding discussion in the limitations section.
🤖 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 `@docs/design_docs/cute_dsl_kernel_cache.md`:
- Around line 53-63: The fenced code block in the Cute DSL kernel cache docs is
missing a language hint, which triggers markdownlint MD040. Update the fenced
block in the documented cache layout section to use a language tag such as text
so the block remains readable and lint-compliant.
In `@flashinfer/jit/cute_dsl_core.py`:
- Around line 66-73: The _hash_source_files helper in cute_dsl_core.py should
not be cached solely by the tuple of paths, because that makes source_sha256
stale when file contents change during the process lifetime. Remove the
`@functools.cache` on _hash_source_files or change the invalidation strategy so it
reflects file freshness/content changes, and ensure the caller path through
source_sha256 still produces a hash that updates when any extra_key_files
content changes.
- Around line 161-164: The module invalidation check in the CuTe-DSL module
write path should treat any existing module directory as stale when its metadata
is missing or does not match the expected value. Update the logic around the
meta check in the module directory handling code so that _read_meta(meta_path)
is compared against expected_meta even if meta_path does not exist, and delete
the directory whenever the metadata is absent or mismatched before writing new
metadata. Use the existing module_dir, meta_path, _read_meta, expected_meta, and
logger flow to locate and adjust the invalidation behavior.
---
Outside diff comments:
In `@docs/design_docs/cute_dsl_kernel_cache.md`:
- Around line 123-129: The cross-reference in the “No `JitSpecRegistry` / AOT
integration” bullet points to a non-existent section. Update the mention of
linking into `.so`s in the `cute_dsl_kernel_cache.md` design doc so it
references the existing “3.1” subsection instead of “3.2”, keeping the wording
aligned with the surrounding discussion in the limitations section.
🪄 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: b1a048dd-0e0c-43ca-94c2-a340472fa50a
📒 Files selected for processing (4)
CLAUDE.mddocs/design_docs/cute_dsl_kernel_cache.mdflashinfer/jit/cute_dsl_core.pyflashinfer/quantization/kernels/nvfp4_quantize.py
|
Ran a cold/warm check of this branch on SM120 (RTX PRO 6000, CUDA 13.0) with nvfp4_quantize(backend='cute-dsl'), M=640 K=7168 bf16: current main (f2f9646) pays ~0.45s first-call in every fresh process, this branch pays 0.61s once (compile + persist) then 0.11s per fresh process from the cached .o, with outputs bitwise-equal to backend='cuda' in all runs. From the consumer side this is very welcome -- the heavier DSL specializations we lean on for SM120 MoE take far longer than this quantize kernel to compile, and today they pay it on every server start. |
| def load(self) -> Any: ... | ||
|
|
||
| def build_and_load(self) -> Any: | ||
| cached = self.try_load() |
There was a problem hiding this comment.
let's check if the error/exception handling is uniform and document it in the abc
There was a problem hiding this comment.
Good idea. Did two things in the latest commit:
- Added a try-except block in
try_load() - Added docstrings to ABC methods:
try_load,build,loadto state the uniform expected behavior.
@bkryu Cool! thanks! LGTM |
…parallel compilation (#4029) <!-- .github/pull_request_template.md --> ## 📌 Description <!-- What does this PR do? Briefly describe the changes and why they’re needed. --> Follow-up to #3948 (top-N tactic ranking) and built on #3874 (CuTe-DSL disk cache). Autotuned `mm_fp4(backend='cute-dsl')` still: * Recompiles every tactic in-process * Compiles serially, taking 2-3 seconds per kernel This PR: 1. Persists mm_fp4 kernels to the on-disk CuTe-DSL cache. Loading `JITLink` cached artifacts from disk is measured to be done in ~10 ms per kernel. 2. Parallelizes first-time compilation. The autotuner checks for not-yet-cached tactics with a subprocess pool (default 4, RAM-capped) that persists directly into the shared disk cache. * default pool size of 4 was set to be conservative. Empirically measured RAM (RSS) size per compilation was ~1GB per compilation. ### Autotune time improvements Evaluated with `python3 benchmarks/flashinfer_benchmark.py --routine mm_fp4 --m 256 --n 1024 --k 7168 --out_dtype bfloat16 --backends cute-dsl --use_128x4_sf_layout --use_nvfp4 --refcheck --autotune` End-to-end process wall time on B200, 3 runs each: | Scenario | Runs | Median | Speedup | |---|---|---:|---:| | Current main branch main (in-process serial compilation) | 94.9 / 93.5 / 93.2 s | **93.5 s** | 1.0× | | this PR, first run; cold disk cache (parallel compile + persist) | 45.8 / 44.6 / 46.0 s | **45.8 s** | **~2×** | | this PR, every subsequent process; hot disk cache (load from disk) | 4.38 / 4.35 / 4.35 s | **4.35 s** | **~20×** | Autotuned results are unchanged: the same tactics are profiled with bitwise identical kernel and output; just changes in the compilation infra. ## 🔍 Related Issues <!-- Link any related issues here --> ## 🚀 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. > If you are unsure about how to set up `pre-commit`, see [the pre-commit documentation](https://pre-commit.com/). ## 🧪 Tests - [ ] Tests have been added or updated as needed. - [ ] All tests are passing (`unittest`, etc.). ## Reviewer Notes <!-- Optional: anything you'd like reviewers to focus on, concerns, etc. --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Performance** * Improved FP4 GEMM startup by precompiling eligible CuTe-DSL tactics. * Added parallel CuTe-DSL kernel compilation with persistent on-disk caching to reduce repeated build times. * Strengthened device-scoped caching to speed up subsequent launches. * **Reliability** * Disk persistence failures now fall back gracefully, allowing kernels to compile on demand. * Updated FP4/MXF dtype handling and improved deterministic, collision-resistant kernel naming. * **Tests** * Added coverage ensuring FP4 CuTe-DSL kernel names change with all codegen arguments and are filename-safe. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
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>
§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>
## 📌 Description Rolls the on-disk CuTe-DSL kernel cache out to the three sm12x NVFP4 fused-MoE kernels (micro, static, dynamic), addressing #4317. The infrastructure landed in #3874 / #4029; @bkryu noted in #4317 that it still had to reach individual kernels and that the core team lacked bandwidth, so this applies it to b12x MoE. Follows the rollout note in `docs/design_docs/cute_dsl_kernel_cache.md`: each `cute.compile` becomes a closure passed to `build_and_load_cute_dsl_kernel`. The three cache-key tuples become named functions so both cache levels derive from one source of truth, and the artifact name appends a digest of that tuple — the keys hold floats and `None` (`swiglu_*`), and `1.5` / `-1.5` both sanitize to `1_5`, so a formatted name would not be injective. As in the existing adopters, the kernels now compile against `make_fake_stream(use_tvm_ffi_env_stream=True)`, so TVM-FFI supplies the caller's current stream and the two launch sites no longer pass one (compiled signatures: 24 / 24 / 32 parameters). **Not covered:** the direct-micro kernel (`compile_direct_micro_kernel`), which this module started dispatching to recently. It compiles without `--enable-tvm-ffi` and launches through the DSL rather than a TVM-FFI callable, so caching it is a separate change — #4317 is only partly closed by this PR. ### Measured — GB10 (sm_121), DSL 4.6.0, five kernel shapes per process | process | total compile + load | |---|---:| | before, two runs | 17.92 s / 17.95 s | | after, cold first run | 18.91 s | | after, two later runs | **0.130 s / 0.134 s** (~135×) | Warm, per kernel: dynamic 7.7 s → 1 ms; static 3.2 s → 1 ms; micro 2.3–3.2 s → 1 ms (the first warm kernel pays 0.13 s of one-off module setup). The cold run costs a few percent for the export. Artifacts are 161–262 KB each. ## 🔍 Related Issues #4317 (partly — see the scope note above). Builds on the cache infrastructure from #3874 and #4029. ## 🚀 Pull Request Checklist ### ✅ Pre-commit Checks - [x] `pre-commit` installed. - [x] Hooks installed. - [x] `pre-commit run` on the two changed files: all hooks pass, no files modified. (I ran it file-scoped rather than `--all-files`, since the remaining hooks are repo-wide.) ## 🧪 Tests - [x] Tests added. - [x] The new tests pass; see the caveat below for what does not run on my hardware. - New `tests/moe/test_b12x_moe_kernel_cache.py`: 61 naming-contract tests — signature coverage, per-argument perturbation, symbol safety, cross-family collision — replicating `tests/jit/test_cute_dsl_cache.py` as the design doc asks of new adopters (happy to fold them into that file instead if you would rather keep all adopters together). - Also checked on the same host: a corrupt artifact and a read-only cache directory each fall back to compiling with a warning, and editing a key source invalidates the module and recompiles it once. **Not verified — please check on sm120 hardware.** `tests/moe/test_b12x_fused_moe.py`'s numerical tests do not run on my GB10: the nvcc reference ops fail to build (`CUDA compiler and CUDA toolkit headers are incompatible`). The suite gives an identical `142 failed, 15 passed` — the same 142 test ids — on unmodified `main` and on this branch, so I have no accuracy signal either way. All timings above are compile/load time; steady-state kernel performance should be unchanged, since it is the same binary. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added shared on-disk caching for static, micro, and dynamic MoE kernels. * Improved cache invalidation when source dependencies change. * Kernel launches now automatically use the caller’s active CUDA stream. * **Tests** * Added comprehensive coverage for cache-key completeness, naming stability, symbol safety, and uniqueness across kernel types. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Signed-off-by: Han-Yin Chang <nick20350@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Brian K. Ryu <bryu@nvidia.com>
…r mm_bf16_fp4 (#4038) ## 📌 Description #3597 added `mm_bf16_fp4` (bf16 activations x nvfp4 weights) so vLLM can replace Marlin, its default W4A16 backend, with a FlashInfer kernel; DGX Spark is the lead target. On decode shapes the kernel trailed Marlin for two reasons: small-n grids underfill the GPU, and at single-token batches (m=1) too few resident warps per SM hide DRAM latency. This PR addresses both: m=1 decode beats or matches Marlin on every part we measured, and end-to-end serving of Qwen3.6-27B-NVFP4 flips from behind to ahead at batch 1 with no regression elsewhere. **What it does** - Adds split-K tactics (2/4/8 splits) to the autotuner space, offered only when splitting shortens the grid's last wave by at least 25%. Splits write fp32 partials, and a PDL-chained reduce kernel sums them in fixed order, so results are deterministic run to run. - Adds occupancy tactics: 2 or 3 co-resident CTAs per SM trade pipeline depth for latency hiding on weight-bound grids, plus occupancy 2 combined with split-K for narrow-n shapes. - Adds a streaming GEMV kernel (`gemv_bf16_fp4_sm12x.py`) for the bandwidth-bound m=1 case: no shared memory, no tensor cores, weights stream from global memory to registers with latency hidden by warp count. It reads the same packed operands as the MMA kernel, so the autotuner picks between the two per shape. - Sizes GEMV split-K from the device: alongside the power-of-2 splits, the menu carries a split targeting ~20 warps/SM (the measured saturation point). Tactic indices become device-scoped, so the autotuner cache key now carries the SM count. - Routes the no-autotune m=1 fallback onto the GEMV. Serving stacks do not tune every shape (vLLM's warmup never captures the logits GEMM), so the lm_head always takes this path. - Fixes kernel launch to pass no cluster dimensions: the boilerplate `cluster=[1,1,1]` routed launches through the cluster work distributor, whose co-residency cap silently defeated the occupancy tactics on SM12x. No public API changes. The one observable behavior change: untuned m=1 calls on SM12x now run the GEMV, whose output is bitwise different from the MMA heuristic's but equally accurate and still deterministic. ### Performance #### Split-K on the #3597 decode shapes (RTX PRO 6000 / DGX Spark / RTX 5080) Single-token decode GEMMs (m=1). The first table covers serving-class shapes (Llama-8B projection layers plus the #3597 example shape); the second covers #3597's own benchmark grid. Median GPU time over CUDA-graph replays with a cold L2 cache, as in serving. Baseline is vLLM's Marlin on the same GPU; FlashInfer runs with autotuning. Speedup = Marlin time / FlashInfer time. Each cell is a RTX PRO 6000 / DGX Spark / RTX 5080 triple. | n x k | vs Marlin, before this PR | vs Marlin, with this PR | |--:|:--:|:--:| | 2048x7168 | 0.57 / 1.00 / 0.47 | **1.36** / **1.02** / **0.78** | | 4096x4096 | 0.75 / 0.91 / 0.90 | **1.06** / **1.00** / 0.90 | | 4096x14336 | 0.60 / 0.91 / 0.78 | **0.98** / **1.00** / 0.78 | | 14336x4096 | 0.97 / 1.00 / 0.86 | 0.97 / 1.00 / 0.86 | | 10304x2688 | 0.78 / 0.98 / 0.96 | 0.78 / 0.98 / 0.96 | The same comparison over #3597's benchmark grid (4096x4096 appears in the table above): | n x k | vs Marlin, before this PR | vs Marlin, with this PR | |--:|:--:|:--:| | 512x2048 | 2.09 / 0.95 / 0.95 | **4.48** / **1.43** / **2.30** | | 512x4096 | 1.19 / 0.70 / 0.56 | **3.61** / **1.26** / **1.88** | | 1024x2048 | 1.26 / 0.98 / 0.70 | **2.39** / **1.03** / **1.32** | | 1024x4096 | 0.99 / 0.86 / 0.49 | **2.72** / **1.04** / **1.21** | | 2048x512 | 1.86 / 1.25 / 1.19 | 1.86 / 1.25 / 1.19 | | 2048x1024 | 1.36 / 1.16 / 0.90 | **1.40** / 1.16 / **1.02** | | 2048x2048 | 1.17 / 1.11 / 0.70 | **1.72** / **1.02**\* (1.11) / **0.97** | | 2048x4096 | 0.73 / 1.08 / 0.54 | **1.48** / **1.04**\* (1.08) / **0.84** | | 4096x512 | 1.40 / 1.01 / 1.38 | 1.40 / **0.95**\* (1.01) / 1.38 | | 4096x1024 | 1.41 / 0.93 / 1.18 | 1.41 / **0.95** / 1.18 | | 4096x2048 | 0.95 / 0.95 / 1.06 | 0.95 / **1.00** / 1.06 | | 131072x2048 | 0.98 / 0.97 / 0.90 | 0.98 / 0.97 / 0.90 | | 248320x2048 | 0.98 / 1.00 / 0.93 | 0.98 / 1.00 / 0.93 | - Bold marks the cells this PR changes (the tuner picks a new split-K tactic); unbolded picks perform as before. - \* These three cells are tuner mis-picks, not kernel regressions: an accurate pick would keep #3597's pre-existing non-split config, and the value in parentheses is what that config achieves. The Reviewer Notes explain the cause. - On the serving shapes, the RTX 5080 column stays below 1.0 even where this PR helps. Profiling of the larger losses points to activation re-reads through L2, a separate problem from grid fill and out of scope here. #### Qwen3.6-27B-NVFP4 decode GEMMs at m=1 (RTX 5080, DGX Spark) Same methodology as above; speedup = Marlin time / FlashInfer time. | GEMM (n x k) | RTX 5080 | DGX Spark | |--|:--:|:--:| | gate_up 34816x5120 | **1.03** | 1.00 | | down 5120x17408 | **1.03** | 1.00 | | lm_head 248320x5120 | **1.04**\* | **1.04** | \* Marlin's lm_head repack does not fit on the 16 GB RTX 5080, so this cell compares against the best in-tree MMA tactic. Spark ties at its bandwidth floor on the first two shapes. #### End-to-end vLLM serving of Qwen3.6-27B-NVFP4 (RTX PRO 6000) Full serving A/B, FlashInfer leg vs Marlin leg under identical settings, aiperf output-token throughput, ratio = FlashInfer / Marlin: batch-1 decode 1.011x, low-concurrency speculative decode 1.08 to 1.11x ahead, and every other cell at parity within run-to-run noise (0.978 to 1.014x) with no regression beyond it. ## 🔍 Related Issues Follow-up to #3597. ## 🚀 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 - [x] I have installed `pre-commit` by running `pip install pre-commit` (or used your preferred method). - [x] I have installed the hooks with `pre-commit install`. - [x] I have run the hooks manually with `pre-commit run --all-files` and fixed any reported issues. ## 🧪 Tests - [x] Tests have been added or updated as needed. - [x] All tests are passing (`unittest`, etc.). New tests: - Every enumerated tactic (MMA and GEMV) is checked against a reference and for bit-exact run-to-run determinism. - Unit tests pin the fallback selectors' picks; one test drives tactic=-1 through the GEMV fallback end to end. - Full test file passes on RTX 5080, RTX PRO 6000, and GB10. ## Reviewer Notes - Most gains require autotuning, which serving frameworks run at startup. The no-autotune fallback picks match the tuner's choices on every part we measured. - The autotuner times candidates with a warm L2 while decode serving runs cold, so it can over-rank split tactics; the 25% last-wave guard compensates but does not fully close it (the three Spark cells in the grid table). This measurement gap is general and deserves its own issue. - The fallback picks add JIT-compiled kernel variants per decode shape class, cached in-process only; that cost amortizes to once per machine when this module adopts the #3874 CuTe-DSL disk cache, as #4029 did for the sibling `mm_fp4` path. The GEMV's device-derived splits widen this surface, so the follow-up is worth prioritizing. - Other FlashInfer cute-dsl kernels also pass `cluster=[1,1,1]` at launch and inherit the same co-residency cap; they are worth a separate audit. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added split-K support for bf16 × fp4 matrix multiplication to improve performance across varying workloads. - Added a dedicated SM12x GEMV path for efficient single-row operations. - Added automatic tuning for split counts, occupancy, and device-specific execution strategies. - Added support for FP16 GEMV outputs and deterministic partial-result reduction. - **Bug Fixes** - Improved handling of GEMV and split-K fallback selection across supported shapes and GPU configurations. - **Tests** - Added coverage for accuracy, determinism, GEMV correctness, and split-K selection. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Brian K. Ryu <bryu@nvidia.com>
…r mm_bf16_fp4 (flashinfer-ai#4038) ## 📌 Description flashinfer-ai#3597 added `mm_bf16_fp4` (bf16 activations x nvfp4 weights) so vLLM can replace Marlin, its default W4A16 backend, with a FlashInfer kernel; DGX Spark is the lead target. On decode shapes the kernel trailed Marlin for two reasons: small-n grids underfill the GPU, and at single-token batches (m=1) too few resident warps per SM hide DRAM latency. This PR addresses both: m=1 decode beats or matches Marlin on every part we measured, and end-to-end serving of Qwen3.6-27B-NVFP4 flips from behind to ahead at batch 1 with no regression elsewhere. **What it does** - Adds split-K tactics (2/4/8 splits) to the autotuner space, offered only when splitting shortens the grid's last wave by at least 25%. Splits write fp32 partials, and a PDL-chained reduce kernel sums them in fixed order, so results are deterministic run to run. - Adds occupancy tactics: 2 or 3 co-resident CTAs per SM trade pipeline depth for latency hiding on weight-bound grids, plus occupancy 2 combined with split-K for narrow-n shapes. - Adds a streaming GEMV kernel (`gemv_bf16_fp4_sm12x.py`) for the bandwidth-bound m=1 case: no shared memory, no tensor cores, weights stream from global memory to registers with latency hidden by warp count. It reads the same packed operands as the MMA kernel, so the autotuner picks between the two per shape. - Sizes GEMV split-K from the device: alongside the power-of-2 splits, the menu carries a split targeting ~20 warps/SM (the measured saturation point). Tactic indices become device-scoped, so the autotuner cache key now carries the SM count. - Routes the no-autotune m=1 fallback onto the GEMV. Serving stacks do not tune every shape (vLLM's warmup never captures the logits GEMM), so the lm_head always takes this path. - Fixes kernel launch to pass no cluster dimensions: the boilerplate `cluster=[1,1,1]` routed launches through the cluster work distributor, whose co-residency cap silently defeated the occupancy tactics on SM12x. No public API changes. The one observable behavior change: untuned m=1 calls on SM12x now run the GEMV, whose output is bitwise different from the MMA heuristic's but equally accurate and still deterministic. ### Performance #### Split-K on the flashinfer-ai#3597 decode shapes (RTX PRO 6000 / DGX Spark / RTX 5080) Single-token decode GEMMs (m=1). The first table covers serving-class shapes (Llama-8B projection layers plus the flashinfer-ai#3597 example shape); the second covers flashinfer-ai#3597's own benchmark grid. Median GPU time over CUDA-graph replays with a cold L2 cache, as in serving. Baseline is vLLM's Marlin on the same GPU; FlashInfer runs with autotuning. Speedup = Marlin time / FlashInfer time. Each cell is a RTX PRO 6000 / DGX Spark / RTX 5080 triple. | n x k | vs Marlin, before this PR | vs Marlin, with this PR | |--:|:--:|:--:| | 2048x7168 | 0.57 / 1.00 / 0.47 | **1.36** / **1.02** / **0.78** | | 4096x4096 | 0.75 / 0.91 / 0.90 | **1.06** / **1.00** / 0.90 | | 4096x14336 | 0.60 / 0.91 / 0.78 | **0.98** / **1.00** / 0.78 | | 14336x4096 | 0.97 / 1.00 / 0.86 | 0.97 / 1.00 / 0.86 | | 10304x2688 | 0.78 / 0.98 / 0.96 | 0.78 / 0.98 / 0.96 | The same comparison over flashinfer-ai#3597's benchmark grid (4096x4096 appears in the table above): | n x k | vs Marlin, before this PR | vs Marlin, with this PR | |--:|:--:|:--:| | 512x2048 | 2.09 / 0.95 / 0.95 | **4.48** / **1.43** / **2.30** | | 512x4096 | 1.19 / 0.70 / 0.56 | **3.61** / **1.26** / **1.88** | | 1024x2048 | 1.26 / 0.98 / 0.70 | **2.39** / **1.03** / **1.32** | | 1024x4096 | 0.99 / 0.86 / 0.49 | **2.72** / **1.04** / **1.21** | | 2048x512 | 1.86 / 1.25 / 1.19 | 1.86 / 1.25 / 1.19 | | 2048x1024 | 1.36 / 1.16 / 0.90 | **1.40** / 1.16 / **1.02** | | 2048x2048 | 1.17 / 1.11 / 0.70 | **1.72** / **1.02**\* (1.11) / **0.97** | | 2048x4096 | 0.73 / 1.08 / 0.54 | **1.48** / **1.04**\* (1.08) / **0.84** | | 4096x512 | 1.40 / 1.01 / 1.38 | 1.40 / **0.95**\* (1.01) / 1.38 | | 4096x1024 | 1.41 / 0.93 / 1.18 | 1.41 / **0.95** / 1.18 | | 4096x2048 | 0.95 / 0.95 / 1.06 | 0.95 / **1.00** / 1.06 | | 131072x2048 | 0.98 / 0.97 / 0.90 | 0.98 / 0.97 / 0.90 | | 248320x2048 | 0.98 / 1.00 / 0.93 | 0.98 / 1.00 / 0.93 | - Bold marks the cells this PR changes (the tuner picks a new split-K tactic); unbolded picks perform as before. - \* These three cells are tuner mis-picks, not kernel regressions: an accurate pick would keep flashinfer-ai#3597's pre-existing non-split config, and the value in parentheses is what that config achieves. The Reviewer Notes explain the cause. - On the serving shapes, the RTX 5080 column stays below 1.0 even where this PR helps. Profiling of the larger losses points to activation re-reads through L2, a separate problem from grid fill and out of scope here. #### Qwen3.6-27B-NVFP4 decode GEMMs at m=1 (RTX 5080, DGX Spark) Same methodology as above; speedup = Marlin time / FlashInfer time. | GEMM (n x k) | RTX 5080 | DGX Spark | |--|:--:|:--:| | gate_up 34816x5120 | **1.03** | 1.00 | | down 5120x17408 | **1.03** | 1.00 | | lm_head 248320x5120 | **1.04**\* | **1.04** | \* Marlin's lm_head repack does not fit on the 16 GB RTX 5080, so this cell compares against the best in-tree MMA tactic. Spark ties at its bandwidth floor on the first two shapes. #### End-to-end vLLM serving of Qwen3.6-27B-NVFP4 (RTX PRO 6000) Full serving A/B, FlashInfer leg vs Marlin leg under identical settings, aiperf output-token throughput, ratio = FlashInfer / Marlin: batch-1 decode 1.011x, low-concurrency speculative decode 1.08 to 1.11x ahead, and every other cell at parity within run-to-run noise (0.978 to 1.014x) with no regression beyond it. ## 🔍 Related Issues Follow-up to flashinfer-ai#3597. ## 🚀 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 - [x] I have installed `pre-commit` by running `pip install pre-commit` (or used your preferred method). - [x] I have installed the hooks with `pre-commit install`. - [x] I have run the hooks manually with `pre-commit run --all-files` and fixed any reported issues. ## 🧪 Tests - [x] Tests have been added or updated as needed. - [x] All tests are passing (`unittest`, etc.). New tests: - Every enumerated tactic (MMA and GEMV) is checked against a reference and for bit-exact run-to-run determinism. - Unit tests pin the fallback selectors' picks; one test drives tactic=-1 through the GEMV fallback end to end. - Full test file passes on RTX 5080, RTX PRO 6000, and GB10. ## Reviewer Notes - Most gains require autotuning, which serving frameworks run at startup. The no-autotune fallback picks match the tuner's choices on every part we measured. - The autotuner times candidates with a warm L2 while decode serving runs cold, so it can over-rank split tactics; the 25% last-wave guard compensates but does not fully close it (the three Spark cells in the grid table). This measurement gap is general and deserves its own issue. - The fallback picks add JIT-compiled kernel variants per decode shape class, cached in-process only; that cost amortizes to once per machine when this module adopts the flashinfer-ai#3874 CuTe-DSL disk cache, as flashinfer-ai#4029 did for the sibling `mm_fp4` path. The GEMV's device-derived splits widen this surface, so the follow-up is worth prioritizing. - Other FlashInfer cute-dsl kernels also pass `cluster=[1,1,1]` at launch and inherit the same co-residency cap; they are worth a separate audit. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added split-K support for bf16 × fp4 matrix multiplication to improve performance across varying workloads. - Added a dedicated SM12x GEMV path for efficient single-row operations. - Added automatic tuning for split counts, occupancy, and device-specific execution strategies. - Added support for FP16 GEMV outputs and deterministic partial-result reduction. - **Bug Fixes** - Improved handling of GEMV and split-K fallback selection across supported shapes and GPU configurations. - **Tests** - Added coverage for accuracy, determinism, GEMV correctness, and split-K selection. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Brian K. Ryu <bryu@nvidia.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>
§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>
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>
§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>
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>
§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>
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>
§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>
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>
§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>
📌 Description
FlashInfer's nvcc-compiled kernels are cached on disk and load instantly in new processes; CuTe-DSL kernels were memoized only in-process, so every new process recompiled every kernel.
The PR adds the core disk caching functionality and applies it to
nvfp4_quantize(backend='cute-dsl')by:JitSpecbecomes an abstract base class (ABC) with a sharedbuild_and_load()template method (caching, locking,FLASHINFER_DISABLE_JIT); the former dataclass is nowJitSpecNvcc(behavior unchanged), and the newJitSpecCuteDslpersists CuTe-DSL kernels asexport_to_c()object files undercached_ops/, reloaded via JITLink in 3–30 ms. Future DSLs (e.g. cutile) implement the same three methods.nvfp4_quantizeas the first user. Example kernel cache path:docs/design_docs/cute_dsl_kernel_cache.mdcovers the lifecycle, cache layout, invalidation keys, concurrency/crash safety, and alternatives considered (including the measured-and-rejected K-agnostic compilation). Please start there for review.🔍 Related Issues
🚀 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
unittest, etc.).Reviewer Notes
Summary by CodeRabbit
FLASHINFER_CUTE_DSL_DISABLE_CACHE=1to bypass the disk cache and force recompilation.