Skip to content

Ameyn/fix fp32 mtp pool out indices - #3490

Merged
kahyunnam merged 5 commits into
flashinfer-ai:mainfrom
ameynaik-hub:ameyn/fix-fp32-mtp-pool-out-indices
Jun 26, 2026
Merged

kahyunnam merged 5 commits into
flashinfer-ai:mainfrom
ameynaik-hub:ameyn/fix-fp32-mtp-pool-out-indices

Conversation

@ameynaik-hub

@ameynaik-hub ameynaik-hub commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

📌 Description

This is the fp32 sibling of #3268 — the same 4D-pool support, now for the fp32 MTP decode path (gated_delta_rule_mtp).

It does two things:

  1. In-place 4D pool writeback (correctness fix). Previously a 4D state pool [pool_size, HV, V, K] was reshaped to 3D internally. For a non-contiguous pool (e.g. a strided slice of an oversized backing buffer) .reshape() silently materializes a copy, the kernel updates the copy, and the
    caller's pool is left untouched → state updates lost. The kernel now reads/writes the pool in place via native 4D indexing (use_pool_indexing), so writes land in the caller's tensor with no silent copy.

  2. Separate read/write slots (new capability). New optional output_state_indices arg (shape [B]): where to write the updated state, separate from where it is read (initial_state_indices). Defaults to initial_state_indices (write back to the read slot); negative entries skip the
    writeback
    for that batch slot (matching the read-side -1 padding semantics). This is what speculative-decoding / MTP-verify needs: read prior state from one pool slot, write the verified state to another.

out, state = gated_delta_rule_mtp(                                                                                                                                                                                                                                                                                  
    q, k, v,                                                                                                                                                                                                                                                                                                        
    initial_state=pool,                 # 4D pool, contiguous OR strided                                                                                                                                                                                                                                            
    initial_state_indices=read_idx,                                                                                                                                                                                                                                                                                 
    output_state_indices=write_idx,     # NEW (optional; defaults to read_idx; -1 = skip)                                                                                                                                                                                                                           
    ...                                                                                                                                                                                                                                                                                                             
)                                                                                                                                                                                                                                                                                                                   

The rest of the signature is unchanged; the standard contiguous single-token/MTP path behaves identically.

Rebased on current main

This branch was merged up to current main, which required reconciling the feature with two landed changes:

The non-contiguous 4D pool is passed through from_dlpack without mark_compact_shape_dynamic (which assumes a compact 3D layout); strides are keyed via pool_strides_key and the pool-dim stride is batch-size-independent, so a single compiled kernel is reused across batch sizes — compilation stays
batch-size-agnostic.

🧪 Tests

Added to tests/gdn/test_decode_delta_rule.py:

  • test_mtp_fp32_state_pool — pool read/write across seq lengths and batch sizes, optional separate output indices, intermediate-state caching. Params: batch_size ∈ {1,4,16} × seq_len ∈ {2,4} × use_separate_output_indices ∈ {F,T} × cache_intermediate_states ∈ {F,T} (24 cases).
  • test_mtp_fp32_state_pool_non_contiguous — strided (non-contiguous) pools. Params: batch_size ∈ {1,4} × seq_len ∈ {2,4} × stride_multiplier ∈ {2,3} (8 cases).

This is the fp32 sibling of PR #3268 — same 4D-pool support, now for the fp32 MTP decode path.

Before vs after — wrapper-side

# Before
out, state = gated_delta_rule_mtp( 
    q, k, v,
    initial_state=pool,            # 4D pool
    initial_state_indices=read_idx,
    ...
) 
# Internally: pool is reshaped to 3D. If pool is non-contiguous,
# this silently materializes a copy → kernel updates the COPY,
# original pool is left untouched. Updates lost.

# After
out, state = gated_delta_rule_mtp( 
    q, k, v,
    initial_state=pool,            # 4D pool, contiguous OR strided
    initial_state_indices=read_idx,
    output_state_indices=write_idx,  # NEW (optional, defaults to read_idx)
    ...
)
# Kernel reads/writes pool in place via native 4D indexing.
# No silent copy. Writes land in the caller's tensor.


Before vs aftermemory layout the kernel sees
  
Contiguous pool (unchanged fast path):
  caller's [pool, HV, V, K]  ─reshape view→  kernel's [pool*HV, V, K]
                              (free, no copy)
  
Non-contiguous pool (the new path):  
  Before:  caller's strided [pool, HV, V, K]
           ─.reshape() silent copyscratch [pool*HV, V, K]
           kernel writes scratch ❌ (caller's  pool unchanged)
           
  After:   caller's strided [pool, HV, V, K]
           ─pass through, use_pool_indexing=Truekernel
           kernel writes caller's pool in placeAPI changeone new argument
    
output_state_indices : torch.Tensor, optional (shape [B])
    Where to WRITE the updated state, separate from where you READ.
    Defaults to initial_state_indices (write back to the read slot).
    Negative entries skip the writeback for that batch.

Everything else in the signature behaves identically.
## 🔍 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

* **New Features**
* Multi-token FP32 decoding: supports pool-backed initial states, per-batch configurable output-state writeback indices, and correct handling for non-contiguous pooled layouts. Non-pool single-token decode behavior remains unchanged.

* **Tests**
* New FP32 tests cover pool read/write across sequence lengths and batch sizes, optional separate output indices, intermediate-state caching, non-contiguous pools, and stride variations.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Threads per-batch output_state_indices through MTP decode, updates kernels to derive read/write views and guarded writeback for pooled FP32 state, routes fp32 pooled multi-token decode into the MTP path, and adds tests for separate read/write indices and non-contiguous pools.

Changes

MTP Decode Output State Indexing

Layer / File(s) Summary
Public decode contracts
flashinfer/gdn_decode.py, flashinfer/gdn_kernels/gdn_decode_mtp.py
Adds output_state_indices parameter and docs; documents negative-index skip semantics and intermediate_states_buffer float32 requirement; adds Optional import.
Python routing and host index plumbing
flashinfer/gdn_decode.py, flashinfer/gdn_kernels/gdn_decode_mtp.py
Routes fp32 pretranspose T>1 pooled decode into gated_delta_rule_mtp; validates output_state_indices shape/dtype; prepares contiguous/materialized h0_source and intermediate buffers only when necessary; resolves h0_out_indices on host and forwards via DLPack to kernel launchers; threads use_pool_indexing into compilation.
MTP Kernel read/write views
flashinfer/gdn_kernels/gdn_decode_mtp.py
Adds h0_out_indices kernel parameter for warp and inline kernels; builds h_read_view/h_write_view for pool/flat layouts; computes write_cache_idx/flat_write_idx; guards 8-row/4-row/2-row final-state writeback to skip writes when index < 0.
ILP load/store paths
flashinfer/gdn_kernels/gdn_decode_mtp.py
Switches all ILP h-loading to use h_read_view and replaces ILP final-state writeback sites to write via h_write_view with per-entry guard checks for negative output indices.
Kernel launchers, run_mtp_decode, and cache keys
flashinfer/gdn_kernels/gdn_decode_mtp.py
Extends launcher signatures and run_mtp_decode public API to accept output_state_indices; resolves and forwards h0_out_indices_tensor into compiled kernel invocations; includes use_pool_indexing and pool_strides_key in compilation cache keys.
Tests: FP32 pooled MTP & non-contiguous pools
tests/gdn/test_decode_delta_rule.py
Adds _test_mtp_fp32_state_pool and _test_mtp_fp32_state_pool_non_contiguous helpers and parametrized tests validating outputs, correct per-batch writeback destinations, and exact preservation of non-targeted pool entries for contiguous and strided 4D pools.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

Suggested labels

run-ci

Suggested reviewers

  • yzh119
  • bkryu
  • yongwww
  • kahyunnam

Poem

🐰 I hopped through pools of hidden state with care,
indices in paw to place each update there —
negative slots politely left untouched,
kernels and Python now keep routing much,
tests confirm each slot stayed exactly fair.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% 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 references fixing 'fp32 mtp pool out indices' which directly aligns with the core changes: supporting output_state_indices for FP32 multi-token decode with pool mode.
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.
Description check ✅ Passed The PR description matches the template and clearly explains the change, tests, and reviewer context, with only optional sections left empty.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 support for separate read and write indices (output_state_indices) in the Gated Delta Rule MTP kernels, routes fp32 state with T > 1 through the MTP kernel, and fixes an indexing bug in the BF16 state MTP kernel where intermediate states were incorrectly indexed by the pool slot instead of the batch index. Feedback suggests using h0_out_indices.to(initial_state_indices) to safely align both device and dtype, preventing potential device mismatch issues.

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 on lines +2395 to +2396
if h0_out_indices.dtype != initial_state_indices.dtype:
h0_out_indices = h0_out_indices.to(initial_state_indices.dtype)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Using h0_out_indices.to(initial_state_indices) is safer and more idiomatic than only casting the dtype. This automatically handles both device and dtype alignment, preventing potential device mismatch issues if output_state_indices is on a different device (e.g., CPU).

        h0_out_indices = h0_out_indices.to(initial_state_indices)

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
flashinfer/gdn_decode.py (1)

723-741: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Require a contiguous batch-sized intermediate-state buffer.

The cache is now documented and indexed per batch, but this block still only validates cache_steps. With repeated pool indices, buffer_size can be smaller than B, which makes the kernel write past the end. And if reshape()/.contiguous() materializes a temporary, those writes never reach intermediate_states_buffer.

Suggested validation
     if cache_intermediate_states:
         buffer_size = intermediate_states_buffer.shape[0]
         cache_steps = intermediate_states_buffer.shape[1]
+        assert buffer_size >= B, (
+            f"intermediate_states_buffer first dimension ({buffer_size}) must be >= B={B}"
+        )
         assert cache_steps >= T, (
             f"intermediate_states_buffer second dimension (cache_steps={cache_steps}) must be at least T={T} to prevent out-of-bounds indexing"
         )
         assert intermediate_states_buffer.dtype == torch.float32, (
             f"intermediate_states_buffer must be float32, "
             f"got {intermediate_states_buffer.dtype}"
         )
+        assert intermediate_states_buffer.is_contiguous(), (
+            "intermediate_states_buffer must be contiguous; otherwise kernel writes land in a temporary"
+        )
-
-        intermediate_states = intermediate_states_buffer.reshape(
+        intermediate_states = intermediate_states_buffer.view(
             buffer_size * cache_steps * HV, V, K
         )
-        if not intermediate_states.is_contiguous():
-            intermediate_states = intermediate_states.contiguous()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@flashinfer/gdn_decode.py` around lines 723 - 741, The code only checks
cache_steps and dtype but must also ensure the provided
intermediate_states_buffer is large and contiguous enough for per-batch
indexing: validate that buffer_size (intermediate_states_buffer.shape[0]) is at
least B (the batch size) and that intermediate_states_buffer.is_contiguous() is
true (or call intermediate_states_buffer =
intermediate_states_buffer.contiguous() before reshaping) so writes to the
kernel land in the original buffer; additionally assert the total number of
elements matches or exceeds B * cache_steps * HV * V * K (use
intermediate_states_buffer.numel()) before performing the reshape into
intermediate_states to prevent out-of-bounds or temporary-materialization issues
when calling reshape()/contiguous().
🧹 Nitpick comments (1)
tests/gdn/test_decode_delta_rule.py (1)

1374-1447: ⚡ Quick win

cache_intermediate_states=True is exercised but never verified.

When cache_intermediate_states is True, intermediate_buffer is allocated and passed to the pool-path kernel, but its contents are never asserted against a reference (the reference run uses intermediate_states_buffer=None). The [False, True] parametrization therefore only confirms the kernel doesn't crash with a buffer present — it does not validate that intermediate states are written correctly. Given the PR's focus on pool read/write indexing, this is the most valuable property to check here.

Consider following the pattern in _test_verify_kernel_mtp/_test_gdn_decode_bf16_state_mtp_kernel: gather the per-step reference states and compare against intermediate_buffer after the pool-path run.

🤖 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 `@tests/gdn/test_decode_delta_rule.py` around lines 1374 - 1447, The test
enables cache_intermediate_states and allocates intermediate_buffer but never
verifies its contents; add assertions that intermediate_buffer contains the same
per-step intermediate states produced by a reference run. Run the reference
"gather→direct→scatter" path (the existing gated_delta_rule_mtp call that
returns out_direct/updated_direct) while collecting per-step states (as done in
_test_verify_kernel_mtp / _test_gdn_decode_bf16_state_mtp_kernel), then compare
those per-step reference states to intermediate_buffer from the pool-path run
(pool_under_test/out_pool) using torch.testing.assert_close with the same
atol/rtol; ensure you only perform this check when cache_intermediate_states is
True and reuse existing symbols: intermediate_buffer, cache_intermediate_states,
gated_delta_rule_mtp, pool_under_test, out_pool, out_direct.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@flashinfer/gdn_decode.py`:
- Around line 717-720: The code currently does h0_source =
initial_state.reshape(...), which can create a temporary when initial_state is
non-contiguous so mutations to h0_source are lost; before reshaping, ensure
initial_state is a contiguous tensor (e.g. replace initial_state with
initial_state.contiguous()) so reshape/view produces a real view and mutations
applied by run_mtp_decode() to h0_source persist; update the code around
h0_source / initial_state (and any callers expecting the returned state) to use
the contiguous copy.

In `@flashinfer/gdn_kernels/gdn_decode_bf16_state.py`:
- Around line 1879-1880: The code computes flat_idx using i_n (batch index) when
cache_intermediate_states is true but the BF16 wrapper still reshapes
intermediate_states_buffer assuming its first dimension equals the pool size,
which can be < B; to fix, ensure the buffer is batch-sized or index by
cache_idx: either (preferred) change the BF16 wrapper and any buffer allocation
for intermediate_states_buffer to allocate/reshape its first dimension to at
least B and add an assertion that intermediate_states_buffer.shape[0] >= B
before using i_n, or (alternative) change the indexing in the decode path to use
cache_idx instead of i_n (update flat_idx calculation). Reference symbols:
flat_idx, i_n, cache_idx, cache_intermediate_states, intermediate_states_buffer,
and the BF16 wrapper in gdn_decode_bf16_state.py.

---

Outside diff comments:
In `@flashinfer/gdn_decode.py`:
- Around line 723-741: The code only checks cache_steps and dtype but must also
ensure the provided intermediate_states_buffer is large and contiguous enough
for per-batch indexing: validate that buffer_size
(intermediate_states_buffer.shape[0]) is at least B (the batch size) and that
intermediate_states_buffer.is_contiguous() is true (or call
intermediate_states_buffer = intermediate_states_buffer.contiguous() before
reshaping) so writes to the kernel land in the original buffer; additionally
assert the total number of elements matches or exceeds B * cache_steps * HV * V
* K (use intermediate_states_buffer.numel()) before performing the reshape into
intermediate_states to prevent out-of-bounds or temporary-materialization issues
when calling reshape()/contiguous().

---

Nitpick comments:
In `@tests/gdn/test_decode_delta_rule.py`:
- Around line 1374-1447: The test enables cache_intermediate_states and
allocates intermediate_buffer but never verifies its contents; add assertions
that intermediate_buffer contains the same per-step intermediate states produced
by a reference run. Run the reference "gather→direct→scatter" path (the existing
gated_delta_rule_mtp call that returns out_direct/updated_direct) while
collecting per-step states (as done in _test_verify_kernel_mtp /
_test_gdn_decode_bf16_state_mtp_kernel), then compare those per-step reference
states to intermediate_buffer from the pool-path run (pool_under_test/out_pool)
using torch.testing.assert_close with the same atol/rtol; ensure you only
perform this check when cache_intermediate_states is True and reuse existing
symbols: intermediate_buffer, cache_intermediate_states, gated_delta_rule_mtp,
pool_under_test, out_pool, out_direct.
🪄 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: d462a7c6-b72c-483f-814f-7fc2dd33a8c2

📥 Commits

Reviewing files that changed from the base of the PR and between fc12ef2 and dc528bc5a25fe70876a789e2fdeffd59b5cc3089.

📒 Files selected for processing (4)
  • flashinfer/gdn_decode.py
  • flashinfer/gdn_kernels/gdn_decode_bf16_state.py
  • flashinfer/gdn_kernels/gdn_decode_mtp.py
  • tests/gdn/test_decode_delta_rule.py

Comment thread flashinfer/gdn_decode.py Outdated
Comment on lines +1879 to +1880
if cutlass.const_expr(cache_intermediate_states):
flat_idx = cache_idx * T * HV + i_t * HV + i_hv
flat_idx = i_n * T * HV + i_t * HV + i_hv

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Batch-indexed caching needs a batch-sized buffer.

flat_idx is now keyed by i_n, not cache_idx, but the BF16 wrapper still reshapes intermediate_states_buffer from its first dimension without asserting it is at least B. Pool mode legitimately allows pool_size < B when multiple batch rows share a state slot, so a pool-sized cache buffer will now index past the end here.

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

In `@flashinfer/gdn_kernels/gdn_decode_bf16_state.py` around lines 1879 - 1880,
The code computes flat_idx using i_n (batch index) when
cache_intermediate_states is true but the BF16 wrapper still reshapes
intermediate_states_buffer assuming its first dimension equals the pool size,
which can be < B; to fix, ensure the buffer is batch-sized or index by
cache_idx: either (preferred) change the BF16 wrapper and any buffer allocation
for intermediate_states_buffer to allocate/reshape its first dimension to at
least B and add an assertion that intermediate_states_buffer.shape[0] >= B
before using i_n, or (alternative) change the indexing in the decode path to use
cache_idx instead of i_n (update flat_idx calculation). Reference symbols:
flat_idx, i_n, cache_idx, cache_intermediate_states, intermediate_states_buffer,
and the BF16 wrapper in gdn_decode_bf16_state.py.

@ameynaik-hub
ameynaik-hub force-pushed the ameyn/fix-fp32-mtp-pool-out-indices branch from dc528bc to 665999d Compare June 2, 2026 16:34
@ameynaik-hub

Copy link
Copy Markdown
Contributor Author

/bot run

@flashinfer-bot

Copy link
Copy Markdown
Collaborator

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

@ameynaik-hub
ameynaik-hub force-pushed the ameyn/fix-fp32-mtp-pool-out-indices branch 2 times, most recently from a05ef49 to fe43db2 Compare June 3, 2026 00:34
@flashinfer-bot

Copy link
Copy Markdown
Collaborator

[FAILED] Pipeline #53431684: 11/20 passed

@ameynaik-hub
ameynaik-hub force-pushed the ameyn/fix-fp32-mtp-pool-out-indices branch from fe43db2 to 6340e6b Compare June 4, 2026 00:08
@ameynaik-hub

Copy link
Copy Markdown
Contributor Author

/bot run

@flashinfer-bot

Copy link
Copy Markdown
Collaborator

GitLab MR !747 has been updated with latest changes, and the CI pipeline #53598579 is currently running. I'll report back once the pipeline job completes.

@flashinfer-bot

Copy link
Copy Markdown
Collaborator

[FAILED] Pipeline #53598579: 9/20 passed

…ty with bf16)

This PR fixes the two issues vLLM hit with the fp32 GDN MTP decode path:

  Correctness: the wrapper's `.reshape(pool*HV, V, K)` silently densifies a
               non-contiguous (page-strided) pool. The kernel then writes
               that throwaway copy, dropping updates for vLLM-style pools.
  Perf:        the densification copy runs every call, regardless of whether
               state actually changed.

The fix is in two layers:

1. Native 4D-pool support in both fp32 MTP kernels (gdn_decode_mtp.py):

   - `gdn_verify_kernel_mtp` (warp-spec, B*HV > 128) and
     `gdn_verify_kernel_mtp_inline` (small batch) each gain a
     `use_pool_indexing: cutlass.Constexpr[bool]` switch.
   - Once per CTA the kernel builds a 2D (V, K) view onto the pool slot.
     The constexpr branch is the only site that knows the actual layout:
       * True : 4D `[pool, HV, V, K]` — slice with (cache_idx, i_hv, :, :);
                works for non-contiguous strided pools (vLLM).
       * False: 3D `[pool*HV, V, K]` — slice with (flat_state_idx, :, :);
                free reshape view of a contiguous pool (existing fast path).
   - All ~43 `cute.local_tile(h0_source, (1, 1, vec_size), (flat_*_idx, X,
     lane))` call sites are replaced with the view-based form
     `cute.local_tile(h_*_view, (1, vec_size), (X, lane))`. Same memory
     accesses, same instruction stream for the contiguous fast path.
   - `flat_write_idx` and `write_cache_idx` are pre-declared / clamped to
     satisfy CuTe DSL's "no variable out of control flow" rule. The
     original-sign signal `write_cache_idx_raw` drives the per-site
     write-skip gates so negative output indices still suppress the
     writeback (preserving fp32 padding-skip semantics).
   - Launchers extract `v_dim` / `k_dim` from the correct layout axes
     depending on `use_pool_indexing`.
   - `run_mtp_decode` cache key includes `use_pool_indexing` plus
     `tuple(h0_source.stride())` (only when use_pool_indexing=True) so
     different page-stride patterns each get their own compile and don't
     alias to a stale binary.

2. Wrapper parity with the bf16 MTP path (gdn_decode.py:gated_delta_rule_mtp):

   - Add `output_state_indices` parameter (mirrors the bf16 wrapper).
     Defaults to `initial_state_indices`. Negative write indices skip the
     writeback for that batch slot.
   - Drop redundant `.to(torch.float32)` casts (state was already asserted
     fp32). Validate `intermediate_states_buffer.dtype == float32`.
   - Make `.contiguous()` on the intermediate buffer conditional, matching
     the bf16 wrapper.
   - Dispatch: when `initial_state.is_contiguous()`, take the existing 3D
     fast path (free reshape view, `use_pool_indexing=False`). Else, pass
     the 4D tensor through unchanged with `use_pool_indexing=True` — the
     kernel writes the strided pool in place, no densification, no
     scatter step.
   - `intermediate_states_buffer` is still flat-indexed by batch (i_n), so
     a non-contiguous buffer still triggers a staging copy + scatter back.
     Native 4D for the intermediate buffer is a separate follow-up.

   Additionally, `gated_delta_rule_decode_pretranspose` now routes
   fp32 + T>1 (pool mode) through `gated_delta_rule_mtp` so the dispatcher
   has a single entry point.

Tests (test_decode_delta_rule.py):

  - `test_mtp_fp32_state_pool` (24 parametric variants): non-trivial
    indices, optional separate output_state_indices, optional intermediate
    caching. Verifies gather→direct reference parity, write destination,
    and that non-targeted pool slots are bit-exactly unchanged.
  - `test_mtp_fp32_state_pool_non_contiguous` (8 parametric variants:
    B in {1, 4} x T in {2, 4} x stride_multiplier in {2, 3}). Allocates an
    oversized HV-stride backing tensor and slices every Nth head-slot to
    produce a strided 4D pool. Verifies output parity with a contiguous
    reference, that the strided pool itself receives the updates (the
    exact regression guard), and that interleaved non-selected backing
    slots are bit-exactly unchanged (proves no densification copy).

Validation:

  - Correctness: 149/149 pass across the new non-contig sweep, existing
    contiguous fp32 MTP sweep (B 1..512 x T 2..8), bf16 verify,
    pretranspose pool, negative_indices, and all_padding regressions.
  - Perf (HV=64, B200, --update-state --cache-intermediate-states, 100
    iters / 20 warmup, contiguous-pool path): median delta = 0%, mean
    delta = +0.05% across 72 (BS, T) cells vs the pre-edit baseline. The
    constexpr branch + view indirection compile away on the contiguous
    fast path. Cell-level deltas within +/-5%, within run-to-run noise.
  - Perf (contig vs strided pool, same B200): kernel-level delta is in
    the noise (+/-2% for B >= 16). The strided path's win is in *not*
    doing the per-call densification copy the old code would have done.

Review feedback addressed (CodeRabbit + Gemini):

  - Device+dtype alignment of write indices: use
    `output_state_indices.to(initial_state_indices)` (tensor target,
    not just dtype) so a CPU-side output_state_indices is realigned to
    the kernel's device automatically. No-op if already aligned.

  - Strict intermediate-buffer validation in the wrapper:
      * assert buffer_size >= B (kernel indexes by batch i_n in [0, B);
        a smaller buffer caused silent OOB writes — the bf16 wrapper
        already had this assert via PR flashinfer-ai#3145; fp32 wrapper was missing
        the parallel).
      * assert intermediate_states_buffer.is_contiguous() (caller
        contract, removes the silent staging-copy fallback).
      * Replace .reshape() with .view() so the no-copy contract is
        enforced at runtime — raises if the layout ever doesn't support
        a view.
      * Removed the now-unused post-kernel scatter-back block.

  - Test coverage: test_mtp_fp32_state_pool now passes a parallel
    intermediate buffer to the reference path and verifies the cached
    intermediate states match cell-for-cell when
    cache_intermediate_states=True.

Re-verified: 34/34 spot-check tests pass; HV=64 BS×T perf sweep shows
mean Δ = +0.10%, median 0% vs pre-review-fix (kernel code byte-identical;
review fixes are wrapper-only).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Amey Naik <212485788+ameynaik-hub@users.noreply.github.com>
@ameynaik-hub
ameynaik-hub force-pushed the ameyn/fix-fp32-mtp-pool-out-indices branch from 47135b6 to e253324 Compare June 10, 2026 00:32
@ameynaik-hub

Copy link
Copy Markdown
Contributor Author

/bot run tests/gdn

@flashinfer-bot

Copy link
Copy Markdown
Collaborator

GitLab MR !747 has been updated with latest changes, and the CI pipeline #54260359 is currently running. I'll report back once the pipeline job completes.

@flashinfer-bot

Copy link
Copy Markdown
Collaborator

[FAILED] Pipeline #54260359: 7/20 passed

@ameynaik-hub
ameynaik-hub requested a review from jiahanc as a code owner June 14, 2026 09:55
@ameynaik-hub

Copy link
Copy Markdown
Contributor Author

/bot run tests/gdn

@flashinfer-bot

Copy link
Copy Markdown
Collaborator

GitLab MR !747 has been updated with latest changes, and the CI pipeline #54716955 is currently running. I'll report back once the pipeline job completes.

@flashinfer-bot

Copy link
Copy Markdown
Collaborator

[FAILED] Pipeline #54716955: 9/20 passed

ameynaik-hub and others added 2 commits June 25, 2026 13:32
Resolves conflicts in flashinfer/gdn_decode.py and
flashinfer/gdn_kernels/gdn_decode_mtp.py between this PR's fp32 4D-pool
feature (output_state_indices, use_pool_indexing, in-place 4D writeback)
and main's flashinfer-ai#3649 (batch-size-agnostic compilation) and flashinfer-ai#3502 (BF16
recovery / per-request K, FLA per-token scatter).

Two integration fixes beyond the mechanical conflict resolution:
- Drop B and pool_size from the inline/warp cache keys: flashinfer-ai#3649 made kernel
  compilation batch-size-agnostic and removed them from the compiled
  kernel stubs, so the resolved cache-key tuples must match (was passing
  20 args to an 18-arg stub).
- Make the h0_source dlpack marking conditional on use_pool_indexing:
  main's mark_compact_shape_dynamic(stride_order=(0,1,2)) assumes a
  contiguous 3D flat pool and fails on the PR's 4D strided pool. Use plain
  from_dlpack for the 4D path (strides keyed via pool_strides_key; the
  pool-dim stride is batch-size-independent, so kernel reuse is safe).

Verified on GB200 (sm_100): 32/32 fp32 MTP pool tests pass; benchmark
shows no regression vs main on the standard contiguous path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Amey Naik <212485788+ameynaik-hub@users.noreply.github.com>
@ameynaik-hub

Copy link
Copy Markdown
Contributor Author

/bot run tests/gdn

@flashinfer-bot

Copy link
Copy Markdown
Collaborator

GitLab MR !747 has been updated with latest changes, and the CI pipeline #55856243 is currently running. I'll report back once the pipeline job completes.

@ameynaik-hub

Copy link
Copy Markdown
Contributor Author

/bot run tests/gdn

@flashinfer-bot

Copy link
Copy Markdown
Collaborator

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

@flashinfer-bot

Copy link
Copy Markdown
Collaborator

[FAILED] Pipeline #55856439: 8/20 passed

@kahyunnam
kahyunnam merged commit 43db902 into flashinfer-ai:main Jun 26, 2026
44 of 57 checks passed
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