Skip to content

[TRTLLM-14904][fix] Work around flashinfer 0.6.15 autotuner cache-key hash/eq inconsistency - #17165

Open
brnguyen2 wants to merge 4 commits into
NVIDIA:mainfrom
brnguyen2:fix/TRTLLM-14904-flashinfer-autotuner-key-main
Open

[TRTLLM-14904][fix] Work around flashinfer 0.6.15 autotuner cache-key hash/eq inconsistency#17165
brnguyen2 wants to merge 4 commits into
NVIDIA:mainfrom
brnguyen2:fix/TRTLLM-14904-flashinfer-autotuner-key-main

Conversation

@brnguyen2

@brnguyen2 brnguyen2 commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

Description

flashinfer 0.6.15's DynamicTensorSpec defines a custom __hash__ that skips tensor_initializers (and hashes callables by identity) while keeping the dataclass-generated __eq__, which compares all fields by value. trtllm_batch_decode_with_kv_cache_mla builds a fresh TuningConfig per call (via _build_mla_decode_tuning_config) with fresh initializer closures, so the lru_cache on AutoTuner._find_nearest_profile accumulates hash-equal but eq-unequal keys up to its 16384 cap. Every autotuner cache probe on the MLA decode path then walks the whole collision chain in Python __eq__ — a 17–19 ms host stall per eager MLA generation call. Mixed prefill+decode iterations run eagerly and pay this on every call; decode-only iterations replay CUDA graphs and are unaffected, which made it present as a prefill-side regression. flashinfer 0.6.14 built a fresh bucket mapper per call, so keys hashed differently and cache probes missed in O(1); the 0.6.14→0.6.15 bump (#16530) exposed the inconsistency.

This PR installs a guarded TRT-LLM-side workaround where the fmha flashinfer backend imports flashinfer: flashinfer.mla._core._build_mla_decode_tuning_config is memoized on the scalars that fully determine its output (kv-cache paging geometry, workspace size, runner set, head/rank dims, max_seq_len, device). Repeated decode calls then reuse the same TuningConfig object, so autotuner cache hits resolve through flashinfer's own unmodified hash/eq — the collision-chain growth is fixed at its root (per-call closure churn) with no equality-semantics change anywhere. The scope is exactly the MLA decode path: every other TuningConfig construction site in flashinfer 0.6.15 already reuses its config across calls (module-level constants, instance-level configs, functools.cache-wrapped builders), so this one builder covers the entire pathology.

A behavioral probe applies the workaround only when the hash/eq inconsistency is actually present and the builder still has the expected signature; any failure degrades to not patching, so a future flashinfer release with different internals is unaffected.

This replaces the earlier revision of this PR, which patched DynamicTensorSpec.__eq__ class-wide. Review (see comments) flagged that the class-wide equality change affects every flashinfer consumer in the process and can cross-contaminate AutoTuner._override_config_cache entries whose configs differ only in tensor_initializers; the memoization approach has neither problem. The __eq__ variant that landed on feat/kimi_k3 via #17164 will be aligned in a follow-up.

The bug is upstream in flashinfer; an upstream issue is being filed (proposed upstream fix: cache the builder, as flashinfer's sparse-MLA path already does), and this workaround should be dropped once a fixed release is picked up.

Test Coverage

  • Serving benchmark at high concurrency on a large MoE model with MLA attention: output throughput recovered ~15% (back to par with the pre-regression baseline band, and identical to the previous __eq__-patch revision of this PR); low-concurrency throughput unchanged.
  • CPU microbenchmark of the autotuner cache probe: ~2 µs per call with cache size 1 post-patch, versus ~4–10 ms per call with an 8000-entry collision chain unpatched.
  • Unit-level sanity: same-key calls return the same TuningConfig object; each keyed scalar (paging geometry, workspace size, runners, q_len, heads, rank, max_seq_len) forces a rebuild; DynamicTensorSpec.__eq__ is untouched; install is idempotent; builder signature drift degrades to a no-op.

PR Checklist

  • PR title and description are self-explanatory
  • Commit message includes ticket ID and is signed off (DCO)
  • Change is guarded and degrades safely if flashinfer internals change

Dev Engineer Review

  • Memoizes flashinfer.mla._core._build_mla_decode_tuning_config using the scalar inputs that determine each TuningConfig.
  • Limits the workaround to the MLA decode path.
  • Preserves FlashInfer’s original DynamicTensorSpec hash and equality behavior.
  • Uses behavioral probing and Signature.bind() checks before installation.
  • Prevents duplicate installation and bounds the cache at 256 entries.
  • Handles unavailable or incompatible FlashInfer imports without affecting execution.
  • Removes unsupported exc_info logging arguments from compatibility and exception paths.
  • No public API declarations, configuration files, or test-list files changed.
  • The workaround should be removed when FlashInfer provides a fixed release.

QA Engineer Review

No test changes.

… hash/eq inconsistency

flashinfer 0.6.15's DynamicTensorSpec defines a custom __hash__ that skips
tensor_initializers (and hashes callables by identity) while keeping the
dataclass-generated __eq__, which compares all fields by value. Callers such
as trtllm_batch_decode_with_kv_cache_mla build a fresh TuningConfig per call
with fresh initializer closures, so the lru_cache on
AutoTuner._find_nearest_profile accumulates hash-equal but eq-unequal keys up
to its 16384 cap. Every autotuner cache probe on the MLA decode path then
walks the whole collision chain in Python __eq__ — a 17-19 ms host stall per
eager MLA generation call, which cost ~15% output throughput in a serving
benchmark at high concurrency on a large MoE model (mixed
prefill+decode iterations run eagerly; decode-only iterations replay CUDA
graphs and are unaffected). 0.6.14 built a fresh bucket mapper per call, so
the keys hashed differently and missed in O(1).

Install a guarded TRT-LLM-side workaround where the fmha flashinfer backend
imports flashinfer: replace DynamicTensorSpec.__eq__ with one consistent with
its __hash__ (ignore tensor_initializers, compare callables by identity).
A behavioral probe applies the patch only when the inconsistency is present,
and any failure degrades to not patching, so a future flashinfer release with
different internals is unaffected. The bug is upstream in flashinfer; this
workaround should be dropped once a fixed version is picked up.

Validation: the serving benchmark recovered from ~5200 to ~6500 output
tok/s at concurrency 1024 (par with the pre-regression baseline band), with
low-concurrency throughput unchanged. A CPU microbenchmark of the cache
probe stays at ~2 us per call and cache size 1, versus ~10 ms per call with
an 8000-entry collision chain unpatched.

Signed-off-by: Brian Nguyen <brnguyen@nvidia.com>
@brnguyen2
brnguyen2 requested a review from a team as a code owner August 1, 2026 20:22
@brnguyen2
brnguyen2 marked this pull request as draft August 1, 2026 20:23
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 7ee561d4-4c80-4c17-a6bb-0c5345b44f51

📥 Commits

Reviewing files that changed from the base of the PR and between 632d01a and 3c6987c.

📒 Files selected for processing (1)
  • tensorrt_llm/_torch/attention_backend/fmha/flashinfer_trtllm_gen.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tensorrt_llm/_torch/attention_backend/fmha/flashinfer_trtllm_gen.py

Walkthrough

Changes

FlashInfer MLA decode cache

Layer / File(s) Summary
Detect and guard compatibility
tensorrt_llm/_torch/attention_backend/fmha/flashinfer_trtllm_gen.py
The module probes FlashInfer hash and equality behavior and validates the builder signature before enabling the workaround.
Cache and install the builder
tensorrt_llm/_torch/attention_backend/fmha/flashinfer_trtllm_gen.py
The replacement builder caches configurations by scalar inputs, limits the cache to 256 entries, delegates misses to the original builder, and installs the replacement during initialization.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • NVIDIA/TensorRT-LLM#17175: Updates FlashInfer to version 0.6.16, which is directly related to the MLA decode autotuner behavior.

Suggested reviewers: pengbowang-nv, yunruis

Sequence Diagram(s)

sequenceDiagram
  participant MLARequest
  participant CachedBuilder
  participant OriginalBuilder
  MLARequest->>CachedBuilder: Provide scalar geometry and device inputs
  CachedBuilder->>CachedBuilder: Check bounded cache
  CachedBuilder->>OriginalBuilder: Build configuration on cache miss
  OriginalBuilder-->>CachedBuilder: Return tuning configuration
  CachedBuilder-->>MLARequest: Return cached or new configuration
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the ticket, fix type, affected component, and hash/equality inconsistency being addressed.
Description check ✅ Passed The description explains the cause, scoped workaround, safeguards, performance impact, and test coverage, with only minor checklist omissions.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tensorrt_llm/_torch/attention_backend/fmha/flashinfer_trtllm_gen.py`:
- Around line 134-137: Update the exception handler surrounding the flashinfer
DynamicTensorSpec.__eq__ workaround probe and replacement to catch only expected
compatibility errors, such as ImportError, AttributeError, and TypeError,
instead of Exception. Preserve the existing debug logging and fallback behavior
while allowing unexpected implementation defects to propagate.
- Around line 98-105: Add precise type annotations to the nested function
_probe_spec, including its DynamicTensorSpec return type. Also annotate self,
other, and the return value of _hash_consistent_eq, preserving the existing
comparison behavior; apply these changes at both referenced locations in
flashinfer_trtllm_gen.py (lines 98-105 and 111-129).
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 9ba2af7e-8beb-46f1-aa0a-22306b2f779f

📥 Commits

Reviewing files that changed from the base of the PR and between fdf7bd5 and 44533b9.

📒 Files selected for processing (1)
  • tensorrt_llm/_torch/attention_backend/fmha/flashinfer_trtllm_gen.py

Comment thread tensorrt_llm/_torch/attention_backend/fmha/flashinfer_trtllm_gen.py Outdated
Comment thread tensorrt_llm/_torch/attention_backend/fmha/flashinfer_trtllm_gen.py Outdated
@brnguyen2
brnguyen2 marked this pull request as ready for review August 1, 2026 21:10
@brnguyen2

Copy link
Copy Markdown
Collaborator Author

/bot run

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #63286 [ run ] triggered by Bot. Commit: 44533b9 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #63286 [ run ] completed with state SUCCESS. Commit: 44533b9
/LLM/main/L0_MergeRequest_PR pipeline #51281 completed with status: 'FAILURE'

CI Report

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

CI Agent Failure Analysis

Link to invocation

…lause

Signed-off-by: Brian Nguyen <brnguyen@nvidia.com>
@brnguyen2

Copy link
Copy Markdown
Collaborator Author

/bot run

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #63311 [ run ] triggered by Bot. Commit: bab6ef0 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #63311 [ run ] completed with state SUCCESS. Commit: bab6ef0
/LLM/main/L0_MergeRequest_PR pipeline #51306 completed with status: 'FAILURE'

CI Report

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

CI Agent Failure Analysis

Link to invocation

@brnguyen2

Copy link
Copy Markdown
Collaborator Author

/bot run

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #63331 [ run ] triggered by Bot. Commit: bab6ef0 Link to invocation

@BowenFu

BowenFu commented Aug 3, 2026

Copy link
Copy Markdown

Held rather than approved. One finding on the replacement __eq__, plus a scoping concern.

_override_config_cache is keyed by TuningConfig equality. In flashinfer 0.6.15, AutoTuner._apply_tuning_overrides looks up self._override_config_cache — a WeakKeyDictionary keyed by the TuningConfig — and TuningConfig is @dataclass(kw_only=True, unsafe_hash=True), so its generated __eq__ compares dynamic_tensor_specs element-wise through DynamicTensorSpec.__eq__. Today two configs differing only in their initializer closures are hash-equal but eq-unequal and get separate entries; after this patch they compare equal, so the second one gets back the first one's cached override object, carrying the first config's tensor_initializers. That is not purely a cache-probe speedup — with bucket/round-up overrides active it can change which initializers profiling runs with. A collision needs the same input_idx/dim_idx/bucket tuple and the same map_to_tuning_buckets identity, so it is narrow, but the docstring's "keys equal under this __eq__ produce identical nearest profiles" only covers _find_nearest_profile, not this cache.

Scope. _patch_flashinfer_dynamic_tensor_spec_eq() runs at import of flashinfer_trtllm_gen.py whenever flashinfer is installed and mutates a third-party class process-wide — every flashinfer consumer in the interpreter, not just the MLA decode path it targets. Related: isinstance(other, DynamicTensorSpec) widens equality to subclasses, where the dataclass __eq__ it replaces required the exact class.

The probe establishes that the upstream bug is present, not that the replacement is correct, and nothing here tests the replacement __eq__. I would rather see this bounded — an explicit gate, or narrowed to the MLA decode path — or fixed upstream; either way I do not want to be the first approval on a global monkey-patch of a dependency.

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #63331 [ run ] completed with state SUCCESS. Commit: bab6ef0
/LLM/main/L0_MergeRequest_PR pipeline #51326 completed with status: 'FAILURE'

CI Report

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

CI Agent Failure Analysis

Link to invocation

…config builder

Review flagged two problems with the DynamicTensorSpec.__eq__
replacement: it mutates a third-party class process-wide (every
flashinfer consumer in the interpreter, not just the MLA decode path
the WAR targets), and the widened equality lets AutoTuner's
_override_config_cache return an override built from a different
config's tensor_initializers when two configs differ only in their
initializer closures.

Replace it with a workaround scoped to the pathological path: memoize
flashinfer.mla._core._build_mla_decode_tuning_config, the one function
that rebuilds a TuningConfig with fresh initializer closures on every
decode call, keyed on the scalars that fully determine its output
(kv-cache paging geometry, workspace size, runner set, head/rank dims,
max_seq_len, device). Repeated calls now reuse the same TuningConfig
object, so autotuner cache hits resolve through flashinfer's own
unmodified hash/eq — no equality semantics change anywhere, and the
collision-chain growth is fixed at its root (per-call closure churn).
Every other TuningConfig construction site in flashinfer 0.6.15
already reuses its config across calls (module-level constants,
instance-level configs, functools.cache-wrapped builders), so
memoizing this one builder covers the entire pathology.

Guardrails as before: applied only when a behavioral probe shows the
hash/eq inconsistency and the builder still has the expected
signature; any flashinfer refactor degrades the workaround to a no-op.
Drop once a fixed flashinfer is picked up.

Validation: serving benchmark at high concurrency on a large MoE model
with MLA attention recovers output throughput to the pre-regression
baseline band, identical to the previous workaround (~6500 output
tok/s at concurrency 1024); low-concurrency throughput unchanged. CPU
microbenchmark of the autotuner cache probe: ~2 us per call with cache
size 1, versus ~4-10 ms per call with an 8000-entry collision chain
unpatched. Also verified: same-key calls return the same config
object, each keyed scalar forces a rebuild, DynamicTensorSpec.__eq__
is untouched, install is idempotent, and signature drift no-ops.

Signed-off-by: Brian Nguyen <brnguyen@nvidia.com>
@brnguyen2

Copy link
Copy Markdown
Collaborator Author

@BowenFu Both points were right, and the _override_config_cache one convinced me the __eq__ replacement was the wrong approach entirely, so I withdrew it rather than gating it. New revision: 632d01a.

The workaround no longer touches DynamicTensorSpec at all. Instead it memoizes flashinfer.mla._core._build_mla_decode_tuning_config, the one function that rebuilds a TuningConfig with fresh initializer closures on every decode call, keyed on the scalars that fully determine its output (kv-cache paging geometry, workspace size, runner set, head/rank dims, max_seq_len, device). Repeated calls reuse the same config object, so autotuner cache hits resolve through flashinfer's own unmodified hash/eq. That fixes the collision-chain growth at its root (per-call closure churn), and the override-cache scenario you described can't occur: equality semantics are unchanged, so configs differing in tensor_initializers never alias. The subclass-widening nit is gone with the rest of the eq patch.

On scope: this is bounded to the MLA decode path by construction. I checked the other TuningConfig construction sites in 0.6.15 and they all already reuse their configs across calls (module-level constants in gemm, instance-level config in the cute-dsl MoE tuner, functools.cache-wrapped builders in mla/_sparse_mla_sm120.py), so this builder is the only one exhibiting the pathology; the docstring records that. The install remains probe-gated and now also checks the builder's signature with inspect.signature, so a refactored flashinfer degrades to a no-op.

Re-validated on the new revision: the high-concurrency serving benchmark recovers to the same throughput as the previous revision (par with the pre-regression baseline band), low concurrency unchanged. CPU microbenchmark of the cache probe: ~2 µs per call with cache size 1, versus ~4-10 ms per call with an 8000-entry collision chain unpatched. Also verified at unit level: same-key calls return the same object, each keyed scalar forces a rebuild, DynamicTensorSpec.__eq__ is untouched, install is idempotent, and signature drift no-ops.

The upstream flashinfer issue will propose the same fix (cache the builder, as the sparse-MLA path already does).

@brnguyen2

Copy link
Copy Markdown
Collaborator Author

/bot run

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tensorrt_llm/_torch/attention_backend/fmha/flashinfer_trtllm_gen.py`:
- Around line 197-199: The logger.debug call with the "Skipping flashinfer MLA
decode tuning-config cache workaround." message is passing an unsupported
exc_info keyword argument. Remove the exc_info=True parameter from this
logger.debug call since tensorrt_llm.logger.debug only accepts positional
message arguments. Keep the debug message content unchanged.
- Around line 129-141: Update the signature guard around orig_build before
installing the wrapper to validate keyword-call compatibility, not just
parameter names and order. After confirming the expected parameters in
inspect.signature(orig_build), use Signature.bind with representative keyword
arguments for every wrapped call parameter; return without installing the
wrapper if binding raises TypeError. Keep the existing guard behavior for
refactored signatures.
🪄 Autofix

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 5ed68bcb-ffae-41d2-88ce-395dd4f3d17d

📥 Commits

Reviewing files that changed from the base of the PR and between bab6ef0 and 632d01a.

📒 Files selected for processing (1)
  • tensorrt_llm/_torch/attention_backend/fmha/flashinfer_trtllm_gen.py

Comment thread tensorrt_llm/_torch/attention_backend/fmha/flashinfer_trtllm_gen.py
Comment thread tensorrt_llm/_torch/attention_backend/fmha/flashinfer_trtllm_gen.py Outdated
@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #63759 [ run ] triggered by Bot. Commit: 632d01a Link to invocation

…p unsupported exc_info

Two CodeRabbit findings on the memoized-builder revision:

- tensorrt_llm's logger.debug(*msg) takes no kwargs, so the except
  handler's exc_info=True would itself raise TypeError at import time —
  breaking the attention backend in exactly the incompatible-flashinfer
  case the guard exists for. Drop it.
- The signature guard compared parameter names/order only; a refactor
  keeping the names but making them positional-only would pass the guard
  and then fail the wrapper's keyword call at the first decode. Add a
  Signature.bind() check so that case degrades to a no-op too.

No hot-path change; CPU sanity extended to cover both paths (strict
kwargs-rejecting logger stub, positional-only drift, exception path).

Signed-off-by: Brian Nguyen <brnguyen@nvidia.com>
@brnguyen2

Copy link
Copy Markdown
Collaborator Author

/bot run

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #63770 [ run ] triggered by Bot. Commit: 3c6987c Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #63759 [ run ] completed with state ABORTED. Commit: 632d01a

Link to invocation

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants