Skip to content

perf: cache the bf16 x fp4 runner factories (cute-dsl + cudnn) - #3832

Open
zihaow211 wants to merge 3 commits into
flashinfer-ai:mainfrom
zihaow211:cache-bf16-fp4-runner
Open

zihaow211 wants to merge 3 commits into
flashinfer-ai:mainfrom
zihaow211:cache-bf16-fp4-runner

Conversation

@zihaow211

@zihaow211 zihaow211 commented Jul 4, 2026

Copy link
Copy Markdown

📌 Description

_cute_dsl_bf16_fp4_runner and _cudnn_bf16_fp4_runner define and
instantiate a fresh runner class on every mm_bf16_fp4 call — measured
~8 µs (cute-dsl) and ~10-12 µs (cudnn) per call of pure avoidable work on
the API hot path (timeit, server x86 core).

cute-dsl: the runner is stateless (the closure only captures
enable_pdl), so the factory is cached directly with
@functools.lru_cache(maxsize=1024).

cudnn (per review suggestion): this factory is not safe to cache on
tuning_config — it captures
AutoTuner.get().get_effective_map_to_tuning_buckets(...) at build time,
which reflects the thread-local autotune(tuning_buckets=..., round_up=...)
override active at that moment. A naive cache would freeze a stale
m_bucket_mapper: measured 10/12 probed M values where the stale runner's
internal cache_m bucketing diverges from the autotuner's cache-lookup
bucketing (e.g. M=1000: autotuner bucket 4096 vs stale cache_m 1024) —
exactly the hazard get_effective_map_to_tuning_buckets's docstring warns
about. Instead the factory is split in two: resolve the effective mapper
per call (~0.6 µs, unavoidable anyway), then cache the runner keyed on the
mapper object — stable per override thanks to _apply_tuning_overrides'
own cache. Verified: bucket-consistent inside/outside overrides, cache hits
within and across override scopes, hash(runner) (part of the autotuner
profiling-cache key) stable for the default path and distinct across
override scopes.

Net effect: cute-dsl ~8 µs → 0.03 µs; cudnn ~10-12 µs → ~0.9 µs per call
(factory microbench). End-to-end mm_bf16_fp4(backend="cudnn") host time
at the repo's trace-example shape (M=128, N=2048, K=7168, the
mm_bf16_fp4_cudnn_N2048_K7168_block_size16 benchmark definition):
~165 µs → ~142 µs per call; decode-like M=8 shows the same ~20 µs delta
(A/B on B200, interleaved 5-round medians). The saving exceeds the factory
microbench because timeit disables GC, while the discarded per-call class
objects create reference cycles the real API loop must collect.

Behavior-neutral: the autotuner keys on the runner's class name,
hash(runner), and get_cache_key_extras — all with unchanged values on
every path exercised today.

🔍 Related Issues

N/A

🚀 Pull Request Checklist

✅ Pre-commit Checks

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

🧪 Tests

  • Tests have been added or updated as needed.

  • All tests are passing (unittest, etc.).

  • Original revision (cute-dsl change only): pytest tests/gemm/test_mm_bf16_fp4.py
    green on RTX PRO 6000 Blackwell SE (sm_120a), CUDA 13.1.

  • Current revision (incl. the cudnn runner cache): pytest tests/gemm/test_mm_bf16_fp4.py — 233 passed, 1 skipped on B200 (sm_100a,
    cuDNN backend 9.24, cutlass-dsl 4.5), covering both backends. The 1 skip
    is the pre-existing parametrize gap "cute-dsl requires out_dtype ==
    a.dtype", unrelated to this change.

  • pytest tests/trace/test_mm_bf16_fp4_reference_correctness.py — 4 passed.

  • Runner-cache semantics validated with a standalone harness: override
    bucket consistency (12 M probes × 3 override scopes), instance reuse,
    profiling-cache hash stability.

Reviewer Notes

AI-assisted (Claude); I reviewed the change and ran the validation.

Summary by CodeRabbit

  • Bug Fixes
    • Improved reuse of bf16 x fp4 GEMM runners to avoid rebuilding them repeatedly for the same settings.
    • Fixed cached cuDNN GEMM runner behavior so shape-based results are not incorrectly shared across different bucket-mapping schemes.

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds caching to the bf16/fp4 runner factories in the CUTE DSL and cuDNN paths. The cuDNN runner now keys cached construction off the effective M-bucket mapper derived from AutoTuner, while the CUTE DSL runner is cached by enable_pdl.

Changes

Runner caching

Layer / File(s) Summary
Cache cute DSL runner
flashinfer/gemm/gemm_bf16_fp4_cute_dsl.py
Imports functools and applies @functools.lru_cache(maxsize=1024) to _cute_dsl_bf16_fp4_runner(enable_pdl: bool = True).
Cache cuDNN runner by mapper
flashinfer/gemm/gemm_bf16_fp4_cudnn.py
Adds a cached helper for CudnnBf16Fp4Runner construction keyed by m_bucket_mapper, and updates _cudnn_bf16_fp4_runner(tuning_config) to resolve the effective mapper through AutoTuner before returning the cached runner.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • flashinfer-ai/flashinfer#3192: Also updates cuDNN BF16×FP4 runner selection around AutoTuner.get_effective_map_to_tuning_buckets(...) and cache_m alignment.
  • Suggested reviewers: saltyminty, bkryu
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: caching bf16 x fp4 runner factories for cute-dsl and cuDNN.
Description check ✅ Passed The description covers the change, rationale, related issues, checklist, tests, and reviewer notes per the template.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces caching to the _cute_dsl_bf16_fp4_runner function in flashinfer/gemm/gemm_bf16_fp4_cute_dsl.py using @functools.lru_cache(maxsize=None) to avoid rebuilding the stateless runner class on every API call. The reviewer suggested using @functools.cache instead of @functools.lru_cache(maxsize=None) for cleaner, more modern code and consistency with other parts of the codebase.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread flashinfer/gemm/gemm_bf16_fp4_cute_dsl.py Outdated
_cute_dsl_bf16_fp4_runner defines and instantiates a fresh runner class
on every mm_bf16_fp4 call.  The runner is stateless (no instance
attributes; the closure only captures enable_pdl), so cache the factory
on enable_pdl -- same pattern as the cudnn backend's cached graph
builders.

The per-call class rebuild measures ~5 us (timeit, server x86 core) --
small against the 0.16-0.79 ms kernel at decode shapes, but it is pure
avoidable work on the API hot path.
@zihaow211
zihaow211 force-pushed the cache-bf16-fp4-runner branch from e700562 to 5b57d2d Compare July 4, 2026 04:12

@bkryu bkryu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hi @zihaowang-builder, thanks for opening the PR with this change. Left a few minor comments

While we are at it, can you also check whether _cudnn_bf16_fp4_runner() should get the same treatment?

Comment thread flashinfer/gemm/gemm_bf16_fp4_cute_dsl.py Outdated
Comment thread flashinfer/gemm/gemm_bf16_fp4_cute_dsl.py Outdated
Zihao Wang added 2 commits July 7, 2026 19:56
Switch @functools.cache to @functools.lru_cache(maxsize=1024) to match
the graph-builder caches in gemm_base.py, and drop the caching rationale
paragraph from the docstring.
_cudnn_bf16_fp4_runner rebuilds and re-instantiates the runner class on
every mm_bf16_fp4 call (~10-12 us/call measured with timeit on a server
x86 core; the cudnn graph builds behind it are already lru_cached).

Unlike the cute-dsl factory, this one is not safe to cache on
tuning_config: it captures the effective M-bucket mapper from
AutoTuner.get().get_effective_map_to_tuning_buckets(), which reflects
any thread-local autotune(tuning_buckets=..., round_up=...) override
active at call time.  A cache keyed on tuning_config would freeze a
stale mapper, making the runner's override-shape cache_m bucketing
diverge from the bucketing the autotuner uses for cache lookup (10 of
12 probed M values diverged in a repro, e.g. M=1000: autotuner bucket
4096 vs stale cache_m 1024).

So split the factory: resolve the effective mapper per call (~0.6 us,
required regardless), and lru_cache the runner build keyed on the
mapper object -- stable per override scope because
_apply_tuning_overrides caches the rewritten TuningConfig.  The
autotuner profiling-cache key (hash over runner __dict__) keeps its
current semantics: stable on the default path, distinct across override
scopes.

End-to-end mm_bf16_fp4(backend="cudnn") host time at the repo's
trace-example shape (M=128, N=2048, K=7168) drops ~165 to ~142 us/call
on B200; decode-like M=8 shows the same ~20 us delta.  The saving
exceeds the ~11 us factory microbench because timeit disables GC while
the discarded per-call class objects create reference cycles the real
API loop must collect.

Validated with tests/gemm/test_mm_bf16_fp4.py (233 passed, 1 skipped)
on B200 (sm_100a, cudnn backend 9.24) plus a standalone harness
covering bucket consistency inside/outside overrides, cache hits across
override scopes, and hash stability.
@zihaow211 zihaow211 changed the title perf: cache the cute-dsl bf16 x fp4 runner factory perf: cache the bf16 x fp4 runner factories (cute-dsl + cudnn) Jul 7, 2026
@zihaow211

Copy link
Copy Markdown
Author

@bkryu
Done and good call on _cudnn_bf16_fp4_runner(). It turned out to cost even more per call (~10-12 µs vs ~8 µs for the cute-dsl one), but it isn't safe to cache on tuning_config: it captures the effective M-bucket mapper from AutoTuner.get().get_effective_map_to_tuning_buckets(...), which reflects any thread-local autotune(tuning_buckets=..., round_up=...) override active at call time. A naive cache freezes a stale mapper — I verified the runner's internal cache_m bucketing then diverges from the autotuner's cache-lookup bucketing at 10 of 12 probed M values (e.g. M=1000: autotuner 4096 vs stale 1024), the exact hazard get_effective_map_to_tuning_buckets's docstring warns about.

So I split it: resolve the effective mapper per call (~0.6 µs, needed anyway), and lru_cache the runner keyed on the mapper object, which is stable per override scope thanks to _apply_tuning_overrides' own cache. Verified bucket consistency inside/outside overrides, cache hits within and across scopes, and that hash(runner) in the profiling-cache key stays stable on the default path while override scopes stay distinct.

End-to-end it's worth more than the factory microbench suggests: at the trace-example shape (M=128, N=2048, K=7168) the cudnn-backend host time drops ~165 → ~142 µs/call on B200 (same ~20 µs at decode-like M=8) — the extra beyond the ~11 µs factory cost is GC, since the per-call throwaway class objects create reference cycles that timeit (GC disabled) doesn't charge for.

Also applied your other two comments (lru_cache(maxsize=1024) + docstring trim). pytest tests/gemm/test_mm_bf16_fp4.py: 233 passed, 1 skipped (the pre-existing "cute-dsl requires out_dtype == a.dtype" parametrize gap) on B200 (sm_100a, cuDNN backend 9.24), both backends covered.

@bkryu

bkryu commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Hi @yanqinz2 can you review the cuDNN runner changes in this PR?

@zihaow211

Copy link
Copy Markdown
Author

Hi @yanqinz2 can you help me review the cuDNN runner changes in this PR please?
Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants