Skip to content

[Kernel][FA] Support paged FP8 Flash attention with asymmetric K/V head dim on gfx950 - #1066

Open
sammysun0711 wants to merge 27 commits into
ROCm:mainfrom
sammysun0711:upstream/fa-paged-fp8-asymmetric-v
Open

[Kernel][FA] Support paged FP8 Flash attention with asymmetric K/V head dim on gfx950#1066
sammysun0711 wants to merge 27 commits into
ROCm:mainfrom
sammysun0711:upstream/fa-paged-fp8-asymmetric-v

Conversation

@sammysun0711

@sammysun0711 sammysun0711 commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Motivation

Based on #1065.
FlyDSL FA cached prefill currently cannot consume MiMO-V2.5-Pro physical FP8 Q/K192-V128 pages through the public FlyDSL flash-attention API. Padding V to 192 wastes cache memory and bandwidth.

This PR aims to add gfx950 causal packed-varlen FP8 flash attention for page-64 vectorized-5D KV with Q/K D192 and independent V/output D128 or D192.

Technical Details

  • Add a dedicated model-neutral paged FP8 software pipeline for gfx950.
  • Extend flydsl_flash_attn_func using the existing 2D block_table ABI.
  • Infer the physical value width and allocate/store native-width BF16 output.
  • Use page-local descriptors with 64-bit physical-page rebasing.
  • Preserve the caller/current stream and CUDA-graph-compatible preallocated output operation.
  • Keep the optimized dense FP8 D128 implementation byte-for-byte unchanged.

Initial scope is causal packed-varlen attention, OCP E4M3 Q/K/V, page size 64,
vectorized-5D KV, Q/K D192, V/O D128 or D192, one KV split, and scalar descales.

Performance (MI350X/gfx950)

Performance evaluation result with random per-tensor OCP E4M3 inputs, preallocated output, 10 warmups, 30 iters. Dense comparison is available only for D128/V128 because the upstream dense kernel does not support Q/K D192.

B=1, Hq=16, Hkv=1, Page size=64

Q / KV D128/V128 paged Dense D128 Paged/dense D192/V128 paged D192/V192 paged
4K / 8K 0.1725 ms / 1,195.2 TFLOP/s 0.1639 ms / 1,257.9 TFLOP/s 95.01% 0.2610 ms / 987.4 TFLOP/s 0.4346 ms / 711.6 TFLOP/s
16K / 32K 2.0737 ms / 1,590.7 TFLOP/s 1.9925 ms / 1,655.5 TFLOP/s 96.08% 3.0717 ms / 1,342.3 TFLOP/s 4.9083 ms / 1,008.1 TFLOP/s
32K / 64K 8.2959 ms / 1,590.5 TFLOP/s 7.9126 ms / 1,667.5 TFLOP/s 95.38% 12.0487 ms / 1,368.8 TFLOP/s 18.7560 ms / 1,055.2 TFLOP/s
64K / 128K 33.5786 ms / 1,571.7 TFLOP/s 32.1621 ms / 1,641.0 TFLOP/s 95.78% 48.1408 ms / 1,370.4 TFLOP/s 76.1674 ms / 1,039.4 TFLOP/s

B=1-32, Hq=16, Hkv=1, Page size=64, Q/KV=4K/8K

B D128/V128 paged D128 paged/dense D192/V128 paged D192/V192 paged V192/V128 throughput
1 0.1719 ms / 1,199.1 TFLOP/s 93.85% 0.2598 ms / 992.0 TFLOP/s 0.4351 ms / 710.9 TFLOP/s 71.66%
2 0.2892 ms / 1,426.1 TFLOP/s 93.92% 0.4106 ms / 1,255.2 TFLOP/s 0.6566 ms / 942.0 TFLOP/s 75.04%
4 0.5620 ms / 1,467.5 TFLOP/s 94.72% 0.7822 ms / 1,317.9 TFLOP/s 1.2588 ms / 982.7 TFLOP/s 74.57%
8 1.1049 ms / 1,492.8 TFLOP/s 95.99% 1.5853 ms / 1,300.6 TFLOP/s 2.5066 ms / 987.0 TFLOP/s 75.89%
16 2.2233 ms / 1,483.7 TFLOP/s 95.92% 3.2047 ms / 1,286.7 TFLOP/s 5.0288 ms / 984.0 TFLOP/s 76.47%
32 4.4484 ms / 1,483.2 TFLOP/s 96.08% 6.4670 ms / 1,275.3 TFLOP/s 9.9310 ms / 996.5 TFLOP/s 78.14%
  • Q: number of query tokens processed in this prefill chunk.
  • KV: total key/value context length attended by those query tokens, including the cached prefix and current query chunk.
  • B: Batch Size
  • Hq: Query heads
  • Hkv: KV heads

Test Plan

python -m pytest -q tests/kernels/test_flash_attn_fwd.py

Test Result

============================================================= test session starts ==============================================================
platform linux -- Python 3.10.12, pytest-9.0.3, pluggy-1.6.0
rootdir: /root/workspace/mimo-opt/flydsl-upstream-prs/pr3-fa-paged-fp8/tests
configfile: pytest.ini
plugins: hypothesis-6.150.2, anyio-4.14.2
collected 179 items                                                                                                                            

tests/kernels/test_flash_attn_fwd.py ..................................................................................................s [ 55%]
s.......ss......................................................................                                                         [100%]

=============================================================== warnings summary ===============================================================
kernels/test_flash_attn_fwd.py::test_paged_fp8_asymmetric_value_matches_torch[ragged-default-d128-v128]
kernels/test_flash_attn_fwd.py::test_paged_fp8_asymmetric_value_matches_torch[ragged-default-d128-v128]
  /opt/venv/lib/python3.10/site-packages/Cython/Distutils/old_build_ext.py:15: DeprecationWarning: dep_util is Deprecated. Use functions from setuptools instead.
    from distutils.dep_util import newer, newer_group

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
============================================ 175 passed, 4 skipped, 2 warnings in 122.29s (0:02:02) ============================================

Accuracy verification with pytorch reference:

Configuration Maximum absolute error Mean absolute error Minimum row cosine
D128/V128 0.003052 0.000376 0.999339
D192/V128 0.002563 0.000329 0.999536
D192/V192 0.002686 0.000328 0.999527

V128 error is the expected direct-FP8 P×V tradeoff, Dense D128 has the same 0.002319 maximum error against the PyTorch reference, while D192/V192 retains BF16 P×V.

Submission Checklist

Copilot AI lite review requested due to automatic review settings August 24, 2026 10:19

Copilot AI 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.

Pull request overview

Adds gfx950-only support for paged FP8 flash-attention with asymmetric K/V head dimensions (Q/K=192, V/O=128 or 192) through the existing flydsl_flash_attn_func paged-KV API, avoiding padding V to 192 and reducing cache/bandwidth waste.

Changes:

  • Extend paged flash-attn dispatch/validation to allow FP8 packed-varlen vectorized-KV with independent value_head_dim, producing native-width BF16 output.
  • Generalize gfx950 dualwave FP8 traits/context/utilities to handle paged+vectorized KV and asymmetric output strides.
  • Add correctness and addressing-robustness tests for the new paged FP8 asymmetric-V path (including a >4GiB cache-offset case).

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
tests/kernels/test_flash_attn_fwd.py Adds gfx950 paged-FP8 asymmetric-V correctness test and a large-shape >4GiB cache-offset validation.
kernels/attention/flash_attn_utils.py Extends gfx950 dualwave FP8 trait/context/helpers for paged vectorized KV and asymmetric output width/stride.
kernels/attention/flash_attn_interface.py Hooks paged FP8 path into the public API, validates shapes/scales, and allocates native-width BF16 output.
kernels/attention/flash_attn_fp8_paged_gfx950.py New gfx950 paged FP8 packed-varlen kernel module specialized for Q/K=192 and V/O=128/192.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +4410 to +4413
key_page_bytes = page_size * head_dim
value_page_bytes = page_size * value_head_dim
high_page = math.ceil(2**32 / min(key_page_bytes, value_page_bytes))
num_pages = high_page + 1

@sammysun0711 sammysun0711 Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 32dac38. The test_flash_attn_fwd.py now calculates the K/V cache allocation size, releases unused cached allocations, and checks available VRAM before allocating.
It skips with a clear message unless the approximately 10 GiB cache requirement plus 2 GiB headroom is available.

@sammysun0711
sammysun0711 force-pushed the upstream/fa-paged-fp8-asymmetric-v branch 2 times, most recently from b22846f to 32dac38 Compare August 26, 2026 15:08
@coderfeli
coderfeli requested review from jhinpan and yanguahe August 27, 2026 09:31

@jhinpan jhinpan 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.

COMMENT — no blocking defect found in what I can verify offline, but I cannot run the device suite this change depends on, so I am not approving around that gap.

Prior review items

  • Copilot, tests/kernels/test_flash_attn_fwd.py:4413 (≈10 GiB K/V cache can OOM under large_shape) — fixed. At this head, test_paged_fp8_d192_cache_offsets_above_4gib computes cache_bytes = num_pages * (key_page_bytes + value_page_bytes), calls torch.cuda.empty_cache(), reads torch.cuda.mem_get_info() and skips unless cache_bytes + 2 GiB is free. I checked the arithmetic: high_page = ceil(2**32 / 8192) = 524288, num_pages = 524289, cache_bytes = 524289 * 20480 ≈ 10.7 GB, so the guard threshold is ≈12.7 GB — consistent with the reply. The in-test assert high_page * key_page_bytes >= 2**32 / assert high_page * value_page_bytes >= 2**32 still hold exactly at that page count, so the >4 GiB property the test exists to prove is not weakened by the guard.

No other reviewer items are open.

What I verified at this commit

Environment: no build-fly/, no torch, and rocminfo reports no GPU agents — so pytest tests/kernels/test_flash_attn_fwd.py cannot run here at all.

What I did run / check:

  • python3 -m py_compile on all four changed files — clean.
  • python3 scripts/check_repo.pyagent-docs check OK (871 symbols, 16 skills). The typed-arithmetic check aborts with git merge-base HEAD origin/main returning 128 because this is a single-commit shallow checkout; that is an environment limitation, not a finding. I substituted a grep of the added lines for the spellings it rejects (maximumf/minimumf/maxsi/minsi/ceildivsi/…) — none present; the new code uses fx.maxnumf.
  • No added line exceeds 120 columns, so the black gate should be clean (black/ruff are not installed here, so this is a proxy, not the real gate).
  • "Dense FP8 D128 unchanged" claim — checked by hand rather than taken on trust. store_final_o now calls global_idx_o instead of global_idx_q; that is equivalent on the dense path because DualwaveFp8KernelContext.__init__ defaults stride_o_n to stride_q_n (flash_attn_utils.py:4426), kernels/attention/flash_attn_fp8_gfx950.py:137 does not pass stride_o_n, and V_HEAD_DIM defaults to head_dim in _make_dualwave_swp_fp8_traits. Same for o_nrec_bytes. smem_d_rpt = ceil(head_dim / d_128b_size) is identical to the old head_dim // d_128b_size at D128, d_chunks/sdrpt_bf are unchanged when value_head_dim is None, and fp8_pv/fp8_pv_direct only lose their value when paged=True. Every other new branch is behind const_expr(PAGED) / const_expr(KV_VECTORIZED). The only dense-path side effect is a changed cache_tag, i.e. a one-time JIT cache miss.
  • LDS budget for the new config — with varlen=True, bn128 is False so num_prefetch_k = 2: kv = 2 * (821040 + 821088) = 68096 B, vt = 2 * 8 * sdrpt_bf * 544 * 2 B = 34816 B (V128) / 52224 B (V192). Totals 102 KB / 118 KB, inside the gfx950 160 KB budget.
  • New LDS index arithmetic — walked the bounds of _stage_vt_dequant_fp8_vectorized and the vectorized load_v: max written element is 13023 and max read is 13024 against VT_BF16_ELEMS = 13056 at V192 (8671/8672 vs 8704 at V128), and the vectorized K reads stay under 12288 B inside the 16640 B K region. No overrun that I can see.
  • Interface validation vs the existing fixtures — the new expected_v_tail = (Hkv, page_size // kvs, value_head_dim, kvs) matches what _vectorize_paged_kv (tests/kernels/test_flash_attn_fwd.py:405-410) produces, and the new seqlen_k / block_table / out assertions are satisfied by the existing paged fixtures (seqlen_k is int32 with numel() == B; o_t = torch.zeros_like(q_t)). So the in-repo BF16 paged tests should not regress on validation grounds.
  • New reference test math — the K permute (0,3,1,2,4) and V permute (0,2,4,1,3) in test_paged_fp8_d192_asymmetric_value_matches_torch correctly invert the aiter 5D layouts, and the bottom-right causal mask matches cross_seqlen=True.

The gap (why COMMENT, not APPROVE)

Everything above is static. The actual correctness of the new 962-line gfx950 schedule — the sigma-permuted V dequantization, the folded logit scale in qk, _lazy_rescale_o_paged, and the page-rebased 64-bit descriptors — is only demonstrated by tests/kernels/test_flash_attn_fwd.py on real gfx950 hardware. I have no GPU here and no built flydsl, so I am relying entirely on the 103-passed run in the PR description. There are also zero check-runs on this head commit (gh api .../check-runs returns total_count: 0), so CI has not covered it either.

Also worth flagging as state, not as a code defect: this PR is mergeable_state: dirty (conflicts with main) and is stacked on the still-open #1065.

Non-blocking notes

Inline below. None of these are correctness bugs as far as I can tell; the first one is the one I would most like to see removed before merge because it reads as an unfinished edit.

Comment thread kernels/attention/flash_attn_utils.py Outdated

def floor_masked_max(self, row_max):
if const_expr(self.traits.PAGED):
return fx.maxnumf(row_max, self.c_neg_floor)

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.

These three const_expr(self.traits.PAGED) branches are exact no-ops — the guarded return and the fall-through return are character-for-character identical:

  • floor_masked_max (5386-5388): both return fx.maxnumf(row_max, self.c_neg_floor)
  • reduce_sum (5402-5404): both return l_row + self.tile_sum(v_p)
  • apply_l_rescale (5445-5446): both return l_row * rescale

The neighbouring sub_m / rescale_from_tile_max / lazy_rescale_o branches do differ (they drop the c_logit_scale factor, which is correct since the paged qk now folds it in), so these three read like an unfinished edit rather than an intentional pattern. I convinced myself they are harmless — c_neg_floor is a unit-agnostic -3.0e38 clamp, and both rescale forms are plain multiplies — but a future reader will burn time re-deriving that. Please just delete the three if blocks.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 2b2cbe9f. I removed the identical PAGED branches from floor_masked_max, reduce_sum, and apply_l_rescale; the behaviorally distinct paged softmax/rescale branches remain unchanged.

Comment thread kernels/attention/flash_attn_utils.py Outdated
src_div = self.make_page_view(self.k_base_iter, page_id)
# Each wave owns two 64-lane octet rows at D192. The physical K
# page has only 12 d-groups; the final four groups address beyond
# the page descriptor and the gfx950 buffer load supplies zeros.

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.

This comment is only accurate for the last KV head. The physical K page is [Hkv, D/16, PageSize, 16], so a head's region is 12 * 64 * 16 = 12288 bytes, but NUM_DMA_K = SMEM_D_RPT = 2 makes each wave issue octets for dg in [0, 15]. For kv_head_idx < Hkv - 1, dg 12..15 lands inside the next head's data, not "beyond the page descriptor", so it does not read zeros.

As far as I can tell this is benign: _read_vec_key below only ever addresses dg <= 11 (ws runs over HEAD_DIM // 64 = 3, dg0 = ws*4 + lane_div_32*2, max dg0 + 1 = 11), so those LDS slots are written and never consumed, and on the final head the buffer descriptor clamps the read to zero without faulting. So this is a comment-accuracy issue plus ~25% wasted K DMA bandwidth, not a correctness bug. Worth rewording, since both new tests use num_kv_heads=1 and would not expose the difference. The same claim appears in the smem_d_rpt comment in _make_dualwave_swp_fp8_traits.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 2b2cbe9f. Both comments now describe the unused D256 LDS padding without claiming that every extra load is descriptor-clamped. The DMA schedule is unchanged.
An additional Hkv=2 gfx950 probe matches PyTorch with max absolute error 4.883e-4 and cosine 0.9999964.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 2b2cbe9f. The V staging permutation now calls _sigma_k_tile_n, making the required K/V permutation relationship explicit.

Comment thread kernels/attention/flash_attn_utils.py Outdated
)
ni = oct_idx % traits.BLOCK_N
dg = oct_idx // traits.BLOCK_N
src_oct = dg * traits.BLOCK_N + _sigma_k_tile_n(ni)

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.

This inlines exactly what _vec_k_src_elem (line 807) already computes — the oct_idx expression above matches _vec_k_dma_oct_idx (line 797) verbatim, and these two lines match lines 810-815. _vec_k_src_elem is already used at line 4067 for the bf16 vectorized path. Reusing it here would keep the two vectorized-K address derivations from drifting apart.

@sammysun0711 sammysun0711 Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 2b2cbe9f. The paged FP8 K loader now calls _vec_k_src_elem instead of duplicating the vectorized address calculation.

Comment thread kernels/attention/flash_attn_utils.py Outdated
# sigma(n). Apply the same (involutive) permutation to V;
# otherwise selective attention pairs K[sigma(n)] with V[n]
# even though near-uniform low-amplitude tests appear valid.
bf = [bf[(i & 3) | ((i & 8) >> 1) | ((i & 4) << 1)] for i in range_constexpr(16)]

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.

This bit-shuffle is _sigma_k_tile_n(i) (line 802) for i < 16 — the (ni & ~15) term is zero over that range. Calling the named helper would make the "same (involutive) permutation as K" claim in the comment above self-evident instead of something the reader has to verify by hand.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 2b2cbe9f. The V staging permutation now calls _sigma_k_tile_n, making the required K/V permutation relationship explicit.

f"flydsl_flash_attn_func: block_table must be 2D on {q.device}, "
f"got shape={tuple(block_table.shape)} device={block_table.device}"
)
if seqlen_k.dtype != torch.int32 or seqlen_k.device != q.device or seqlen_k.numel() != B:

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.

This tightens the existing BF16/F16 paged ABI, not just the new FP8 one: seqlen_k must now be exactly int32 with numel() == B. Previously it was only used host-side (seqlen_k.reshape(-1).tolist(), and only when max_seqlen_kv is None), so an int64 tensor or an over-allocated buffer worked. The in-repo fixtures all pass int32 [B] so nothing here regresses, but external callers on the already-shipped paged path could start raising.

Same question for the block_table.dim() != 2 check just above. If the tightening is deliberate, it would be worth a line in the PR description — right now the description says the dense FP8 path is unchanged but says nothing about the paged BF16 ABI getting stricter.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 02ecfe12. The exact device-side int32 [B] requirement is now scoped to paged FP8. Existing BF16/F16 paged calls continue to accept host-only int64 or over-allocated seqlen_k;
the new regression covers both dtypes. I retained the 2D/same-device block-table validation because that is the documented layout consumed directly by the kernel.

q_flat = q.contiguous()
out_dtype = torch.bfloat16 if paged_fp8 else q.dtype
out = torch.empty(expected_out_shape, dtype=out_dtype, device=q.device)
elif out.shape != expected_out_shape or not out.is_contiguous():

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.

Same class of change as the seqlen_k check: a caller-supplied out must now be contiguous with an exact shape match, on every paged path including the pre-existing BF16 one. This is strictly better than the old o_flat = out.contiguous(), which silently wrote results into a throwaway copy for a non-contiguous out, so I read it as a genuine fix rather than a regression — just noting it is a user-visible behaviour change beyond the PR's stated scope.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed that this is caller-visible. I retained the exact-shape and contiguous-output checks because the prior out.contiguous() behavior could write into a temporary and return the caller's untouched tensor.
Commit 68f0587d also validates that out is on the Q/K/V device.

@jhinpan jhinpan 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.

REQUEST_CHANGES after three independent passes (host/API boundaries, kernel/addressing/numerics, and tests/CI). The new paged-FP8 math itself held up on a real MI355X/gfx950: the full attention file passed 103/103, the >4 GiB page-offset test passed, and extra partial-page/GQA/fully-masked/peaked-logit/V128/V192 probes matched an independent PyTorch oracle (worst max abs error 7.8125e-3). Repo and Python-style checks also pass.

Two host-side correctness defects remain: flattened Q/O can overflow the signed-int32 C ABI, and internal contiguous/cast copies are not ordered before an explicit side-stream launch. Both are reproduced inline below. The PR is also still conflicted with current main, where #1020 added the analogous dense-FP8 overflow handling; rebasing alone will not cover this paged early-return path.

# Keep serving-sized physical K/V caches rank-5 because flattening their
# dynamic memref shape can exceed signed int32. The FP8 schedule consumes
# Q/O as flat token-major buffers, matching its explicit runtime strides.
q_flat = q.contiguous().view(-1) if paged_fp8 else q.contiguous()

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.

Blocking — internal copies race an explicit stream. q.contiguous() here (likewise K/V and the block_table.to(...).contiguous() above) executes on the ambient current stream, while exe(..., stream=launch_stream) immediately consumes the result on the caller-supplied stream. The new side-stream test only uses already-contiguous inputs, so every copy is a no-op and cannot catch this.

I reproduced this on MI355X/gfx950 with a stride-2 Q view. With no pending default-stream work, the side-stream result was bit-identical to the contiguous reference. After enqueueing torch.cuda._sleep(3_000_000_000) on the default stream, side.synchronize() returned in 0.043 s (so it did not wait for that copy) and the same call was no longer bit-identical (max_abs_err=0.0045166015625). Running the call inside with torch.cuda.stream(side): restored bit equality, confirming the missing ordering edge.

Please enqueue all wrapper-owned conversions/copies on launch_stream (including the block-table cast), or reject layouts that require a copy, and add a delayed non-current-stream regression with a non-contiguous input.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 1eacd9d0. All wrapper-owned casts, contiguous copies, output allocation, and workspace allocation now run inside torch.cuda.stream(launch_stream).
The regression uses non-contiguous Q/K/V, an int64 block table, and a blocked default stream; the side-stream result is bit-identical to the contiguous reference without waiting for the default stream.

k_flat = k.contiguous()
v_flat = v.contiguous()
o_flat = out.contiguous()
o_flat = out.contiguous().view(-1) if paged_fp8 else out.contiguous()

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.

Blocking — the flattened paged-FP8 ABI has no signed-int32 dimension guard. These 1-D Q/O views put numel() into the dynamic memref dimension, which the C ABI packs as int32. On the real gfx950 I used a valid packed Q shape [699051, 16, 192] (2,147,484,672 elements, just 1,024 above 2**31) with a one-page KV cache; the call reached <flydsl-cabi-fill> and failed with struct.error: 'i' format requires -2147483648 <= number <= 2147483647.

Current main's #1020 added _FP8_MAX_FLAT_ELEMS for the same dense-FP8 failure, but its guard explicitly excludes paged KV and the paged dispatch returns before the dense path, so rebasing does not fix this. Please reject every flattened Q/O dimension >= 2**31 before build/launch (or keep a non-flattened ABI) and add a lowered-limit regression so CI does not need multi-GiB allocations.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 2f03af8b. Paged FP8 now checks flattened Q and output sizes before build/launch and raises a descriptive NotImplementedError at 2**31.
A lowered-limit V128/V192 regression covers the guard, and the original [699051, 16, 192] reproducer now fails at the host boundary instead of inside C-ABI packing.

Signed-off-by: Xiake Sun <xiake.sun@amd.com>
- Calculate the paged K/V cache footprint and require 2 GiB of additional
  headroom before running the >4 GiB addressing regression.
- Skip with a clear diagnostic when the selected GPU has insufficient free memory.

Signed-off-by: Xiake Sun <xiake.sun@amd.com>
- Run wrapper-owned casts, contiguous copies, output allocation, and workspace allocation on the selected stream.
- Reuse validated caller output directly without creating a throwaway contiguous copy.
- Add delayed side-stream coverage for non-contiguous Q/K/V and block-table conversion.

Signed-off-by: Xiake Sun <xiake.sun@amd.com>
- Reject flattened Q or output dimensions at the signed-int32 C ABI boundary.
- Raise a descriptive error before kernel build or launch.
- Add lowered-limit V128 and V192 regression coverage.

Signed-off-by: Xiake Sun <xiake.sun@amd.com>
- Remove redundant paged conditionals.
- Reuse shared K-address and sigma-permutation helpers.
- Correct the D192 padding comments without changing the schedule.

Signed-off-by: Xiake Sun <xiake.sun@amd.com>
- Apply the strict int32 [B] seqlen_k contract only to paged FP8.
- Preserve existing BF16/F16 acceptance of host-only length metadata.
- Retain common block-table and output-buffer safety validation.

Signed-off-by: Xiake Sun <xiake.sun@amd.com>
@sammysun0711
sammysun0711 force-pushed the upstream/fa-paged-fp8-asymmetric-v branch from 32dac38 to 68f0587 Compare August 31, 2026 13:28
- Reject caller-provided paged outputs that are not colocated with Q/K/V.
- Prevent cross-device output buffers from reaching the kernel launch.
- Add host-side regression coverage for a CPU output tensor.

Signed-off-by: Xiake Sun <xiake.sun@amd.com>
jhinpan
jhinpan previously approved these changes Aug 31, 2026

@jhinpan jhinpan 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.

APPROVE. Both blocking items from my CHANGES_REQUESTED review at 32dac38 are fixed at 68f0587, the five non-blocking items are fixed as well, and the BF16 paged ABI regression I flagged as an unstated side effect has been deliberately narrowed. One verification gap is stated at the bottom.

Prior items, re-checked at this head

1. Blocking — internal copies race an explicit stream (comment) — fixed. Every wrapper-owned conversion now runs inside with torch.cuda.stream(launch_stream): (kernels/attention/flash_attn_interface.py:702-722): the block_table.to(torch.int32).contiguous().reshape(-1) cast, the torch.empty output allocation, and q/k/v.contiguous(). Nothing device-side is left on the ambient stream between the guards and exe(..., stream=launch_stream) — the only remaining pre-launch host interaction is seqlen_k.reshape(-1).tolist(), which is a blocking D2H and is order-safe by construction. The regression exists and is the right shape: test_paged_fp8_d192_asymmetric_value_matches_torch[side-copies] (tests/kernels/test_flash_attn_fwd.py) builds stride-2 non-contiguous Q/K/V plus an int64 block_table (so all four copies are real), enqueues torch.cuda._sleep(3_000_000_000) on the default stream, asserts not default_done.query() (the side stream did not wait for it), and then asserts rtol=0, atol=0 against a contiguous reference. Under the old code the copies would sit behind that sleep and the kernel would read uninitialized memory, so the test discriminates.

2. Blocking — no signed-int32 dimension guard on the flattened paged-FP8 ABI (comment) — fixed. flash_attn_interface.py:628-636 computes q_flat_elems = q.numel() and out_flat_elems = q_flat_elems // D * value_head_dim and raises NotImplementedError when either is >= _FP8_MAX_FLAT_ELEMS (2**31, line 47), before any build or launch. The shape I reproduced with, [699051, 16, 192] = 2,147,484,672 elements, is 1,024 above the bound and is rejected. Because paged FP8 requires packed varlen (q.dim() == 3, line 494) the numel() // D * value_head_dim derivation is exact for both the allocated and the caller-supplied out, and the guard sits before the out shape check so the derived value is the one that will actually be flattened. K/V stay rank-5 and the comment at lines 715-718 now says why. test_paged_fp8_d192_rejects_flattened_int32_overflow monkeypatches _FP8_MAX_FLAT_ELEMS down to the tiny fixture's size, so CI gets the coverage without a multi-GiB allocation.

3. Three no-op const_expr(PAGED) branches (comment) — fixed. floor_masked_max (flash_attn_utils.py:5389), reduce_sum (5419) and apply_l_rescale (5460) are now single unconditional returns. The branches that genuinely differ (sub_m 5392, rescale_from_tile_max 5453, lazy_rescale_o 5487) are untouched.

4. Inaccurate "reads zeros beyond the page descriptor" comment (comment) — fixed. Both sites now state the layout fact rather than a false zero-fill claim: flash_attn_utils.py:4984-4985 ("Each wave owns two 64-lane octet rows for the D256-shaped LDS tile. The QK reader ignores the four groups beyond physical D192.") and the smem_d_rpt comment at 1955-1957.

5. Inlined duplicate of _vec_k_src_elem (comment) — fixed. flash_attn_utils.py:4990-4996 calls _vec_k_src_elem(traits, d, self.wave_id_uni, self.lane_in_warp, self.kv_head_idx). I diffed the removed inline block against the helper body (lines 814-824): the oct_idx/ni/dg/src_oct/src_elem chain is term-for-term identical, so the refactor is behaviour-preserving.

6. Open-coded sigma bit-shuffle (comment) — fixed. flash_attn_utils.py:5112 is now bf = [bf[_sigma_k_tile_n(i)] for i in range_constexpr(16)]. The helper's extra (ni & ~15) term is zero over i < 16, so the substitution is exact.

7. seqlen_k / out ABI tightening on the pre-existing BF16 paged path (1, 2) — addressed. The strict int32 [B] requirement is now gated behind paged_fp8 (flash_attn_interface.py:615), so an int64 or over-allocated host-only seqlen_k still works on BF16/F16, with test_paged_legacy_dtype_accepts_host_only_seqlen_metadata pinning it. The out contiguity/shape/device checks do still apply to every paged path; as I said last round, that replaces a silent write-into-a-throwaway-copy, so I read it as a fix and am not blocking on it.

What I ran here

  • python3 scripts/check_repo.pyagent-docs passes. typed-arithmetic cannot resolve origin/main in this pinned single-commit checkout, so I reproduced it by hand: parsed the base…head patch for kernels/**, fed the 1,498 added lines to check_typed_arithmetic_usage.scan_source, zero violations.
  • python3 -m py_compile on all four changed files — clean.
  • Structural diff of flash_attn_utils.py / flash_attn_interface.py / flash_attn_fp8_paged_gfx950.py between 32dac38 and this head, to confirm the only semantic changes since my last review are the six items above plus the rebase onto main's rescale_threshold / _P_HEADROOM_LOG2 work. The PR-owned part of that rebase is rescale_threshold=8.0 for the paged FP8 module; paged PV is HIPREC (bf16 P), so the e4m3 headroom reasoning that motivates the dense 6.0/4.0 split does not apply there and 8.0 is consistent with the unscaled m_diff comparison in _lazy_rescale_o_paged.
  • GitHub check runs at this commit: Check Python Code Style and Check C++ Code Style both green.

Verification gap

This environment has no torch and no pytest, so I could not execute the new gfx950 tests, and unlike my last round I could not re-run the two GPU repros. Both fixes are closed above by code inspection plus a reading of the new regressions, not by execution. The test job at this commit is still in_progress; please confirm it goes green before merging — in particular test_paged_fp8_d192_asymmetric_value_matches_torch[side-copies-*], which is the one whose assert not default_done.query() could in principle be flaky if a runner is slow enough to drain a 3e9-cycle sleep during the side-stream launch.

@coderfeli

Copy link
Copy Markdown
Collaborator

@sammysun0711 seems large perf gap . 30-50% loss. have you validated this? not make sense to regression too much

- Load V192 LDS fragments in two smaller groups.
- Overlap tail V reads with softmax computation.
- Enable post-RA scheduling and disable machine sinking.
- Tune D192 setprio and long-context V192 staggering.
- Preserve D192/V128 correctness and performance.

Signed-off-by: Xiake Sun <xiake.sun@amd.com>
- Accept vectorized page-64 D128/V128 inputs on gfx950.
- Use native FP8 P*V for D128 paged attention.
- Apply V descale during final output normalization.
- Convert vectorized V into the FP8 MFMA operand layout.
- Cover ragged batches and explicit-stream execution.

Signed-off-by: Xiake Sun <xiake.sun@amd.com>
- Process adjacent page-64 tiles through a BN128 softmax iteration.
- Reuse paired 64-bit block-table loads for K and V.
- Stage V through an 80-byte bank-padded D-major LDS layout.
- Use DPP lane exchange instead of LDS cross-lane shuffles.
- Validate direct BN128 launch assumptions.
- Cover even-page fast dispatch, odd-page fallback, partial tails, and graph replay.

Signed-off-by: Xiake Sun <xiake.sun@amd.com>
- Keep post-P*V accumulators in SSA for the paired-page path.
- Reduce wait, MFMA, and packed-add stalls without increasing resources.
- Improve D128 throughput across the 4K, 16K, and 64K targets.

Signed-off-by: Xiake Sun <xiake.sun@amd.com>
- Load each 32-byte V fragment with two aligned 128-bit LDS operations.
- Split vector loads into the four i64 FP8 MFMA K-steps.
- Reduce V-fragment address and wait overhead without increasing resources.
- Improve D128 throughput across the 4K, 16K, and 64K targets.

Signed-off-by: Xiake Sun <xiake.sun@amd.com>
- Map each 16-lane row to one vectorized V token group.
- Replace four DPP exchanges and selects with two permlane16 swaps.
- Require a full EXEC mask through the static D128/V128 tile shape.
- Improve D128 throughput across the 4K, 16K, and 64K targets.

Signed-off-by: Xiake Sun <xiake.sun@amd.com>
- Store permlane16 swap results without a staging-register reshuffle.
- Apply the matching fixed word permutation to packed P.
- Preserve the FP8 contraction mapping and full-EXEC requirement.
- Improve D128 throughput at the 16K and 64K targets.

Signed-off-by: Xiake Sun <xiake.sun@amd.com>
- Split vectorized V loads from permutation and LDS staging.
- Hide V4 latency behind K prefetch and V5 latency behind QK MFMAs.
- Keep one prefetched V vector live to preserve register and scratch usage.
- Improve random-input D128 latency across the 4K, 16K, and 64K targets.

Signed-off-by: Xiake Sun <xiake.sun@amd.com>
- Keep paged BN128 QK accumulators unscaled through row-max reduction.
- Fold the runtime logit scale into score subtraction and max correction.
- Remove eager per-score multiplies and reduce VGPR use from 246 to 218.
- Reach 92.57% of dense throughput at the 64K/128K target.

Signed-off-by: Xiake Sun <xiake.sun@amd.com>
- Pair each future V load with an independent QK contraction.
- Keep only one prefetched V vector live at a time.
- Preserve 218-VGPR, zero-scratch kernel resource usage.
- Improve the 16K and 64K random-input attention targets.

Signed-off-by: Xiake Sun <xiake.sun@amd.com>
- Keep generic paged QK accumulators in their raw domain.
- Fold logit scaling into score subtraction and max correction.
- Compare lazy-rescale thresholds in scaled log2 units.
- Improve D192 steady-state latency and reduce V192 scratch usage.

Signed-off-by: Xiake Sun <xiake.sun@amd.com>
- Add an explicit bank-padded FP8 V-layout trait.
- Reuse vectorized V staging for D192/V128 and generic D128 fallback.
- Pack softmax probabilities in the matching MFMA word order.
- Improve D192/V128 latency while preserving accuracy and zero scratch.

Signed-off-by: Xiake Sun <xiake.sun@amd.com>
  - Reuse the compiled paged launcher instead of resolving the FlyDSL JIT cache key on every invocation.
  - Cache per-device architecture detection and reuse it during paged dispatch.
  - Avoid redundant current-stream context setup while preserving explicit-stream ordering.
  - Reduce 4K host submission overhead to reach over 90% of D128 throughput compare with dense attention

Signed-off-by: Xiake Sun <xiake.sun@amd.com>
@sammysun0711

sammysun0711 commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

@sammysun0711 seems large perf gap . 30-50% loss. have you validated this? not make sense to regression too much

Thanks for your feedback. I understand the original performance gap refer to fp8 paged attention vs fp8 dense attention kernel with D128/V128 head dim, I can reproduced it on MI350X/gfx950 with random FP8 inputs.

  • For D128/V128 head dim, I optimize the FP8 paged attention pipeline and the host launch path for Q/KV head. The final native D128/V128 paged kernel reaches 90.76%, 92.13%, and 93.23% of the matched dense kernel at Q/KV 4K/8K, 16K/32K, and 64K/128K.

  • For the PR's D192 target, V128 latency improve from 3.81/58.75 ms to 3.14/48.35 ms at 16K/64K, while V192 improves from 9.56/141.56 ms to 5.80/88.12 ms by resolve register spill.

Here are the final performance evaluation results on MI350X (gfx950) with random per-tensor OCP E4M3 inputs, preallocated output, 10 warmups, 30 iters. Dense comparison is available only for D128/V128 because the upstream dense kernel does not support Q/K D192.

D128

Q / KV Configuration Median latency TFLOP/s Paged/dense TFLOPs
4K / 8K D128/V128 paged 0.1825 ms 1,129.7 90.76%
4K / 8K D128 dense 0.1656 ms 1,244.7 reference
16K / 32K D128/V128 paged 2.1998 ms 1,499.5 92.13%
16K / 32K D128 dense 2.0266 ms 1,627.7 reference
64K / 128K D128/V128 paged 34.1832 ms 1,543.9 93.23%
64K / 128K D128 dense 31.8693 ms 1,656.0 reference

D192

Q / KV Configuration Median latency TFLOP/s Paged V128 vs V192 TFLOPs
4K / 8K D192/V128 paged 0.2605 ms 989.4 150.9%
4K / 8K D192/V192 paged 0.4718 ms 655.5 n/a
16K / 32K D192/V128 paged 3.1439 ms 1,311.5 153.7%
16K / 32K D192/V192 paged 5.7995 ms 853.2 n/a
64K / 128K D192/V128 paged 48.3453 ms 1,364.6 151.9%
64K / 128K D192/V192 paged 88.1212 ms 898.4 n/a

- Load and consume each BF16 V192 P*V fragment immediately to reduce register pressure and scratch usage.
- Enable the schedule for paged FP8 D192/V192 at KV lengths of 64K and above.
- Share the long-context threshold with stagger selection and cover its dispatch boundary.
- Validate random pages, partial tails, explicit streams, and graph replay.

Signed-off-by: Xiake Sun <xiake.sun@amd.com>

import flydsl.compiler as flyc
import flydsl.expr as fx
from flydsl.compiler.kernel_function import CompilationContext

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.

why import this?

from flydsl.expr import const_expr, range_constexpr, rocdl
from flydsl.expr.typing import T
from flydsl.expr.utils.arith import ArithValue
from flydsl.expr.utils.arith import _to_raw as _raw

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.

clean

inv_l_rcp = rocdl.rcp(T.f32, _raw(l_row))
inv_l = ArithValue(fx.Float32(l_row) > ctx.c_zero_f).select(inv_l_rcp, ctx.c_zero_f)
if const_expr(traits.FP8_PV):
inv_l = ArithValue(inv_l) * ctx.vd_fp8

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.

clean ArithValue

Comment thread kernels/attention/flash_attn_utils.py Outdated
result.append(self.anchor_scalar_f32(m_new))
return result

result = scf_if_dispatch(

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.

use if else

@coderfeli

Copy link
Copy Markdown
Collaborator

much better perf now. still need more clean. use flydsl kernel code cleanup to do a deeper clean.

- Factor value dimensions into generic H1/H2 segments and avoid padded D256 P*V work.
- Reuse full-CTA H1 staging and stage H2 with complete active waves in the bank-padded FP8 layout.
- Distribute FP8 P*V chunks across the existing pipeline and disable the BF16-specific wave stagger.
- Preserve dense FP8 and other paged dimensions while covering factorization, dispatch, streams, tails, and graph replay.

Signed-off-by: Xiake Sun <xiake.sun@amd.com>
- Carry two future V fragments across BN128 loop iterations.
- Materialize prefetched fragments into bank-padded LDS after QK work.
- Keep paired page-table indexing in the int32 kernel ABI.
- Preserve D192 paths and the generic ragged/odd-page fallback.

Signed-off-by: Xiake Sun <xiake.sun@amd.com>
- Decouple BN128 tile selection from packed-varlen metadata.
- Load per-sequence Q/KV offsets and causal lengths for B>1.
- Preserve active q-block-aware longest-first scheduling.
- Maintain B=1-relative performance through batch size 32.
- Cover ragged multi-q-block batches with distinct causal offsets.

Signed-off-by: Xiake Sun <xiake.sun@amd.com>
- Interleave up to eight MiMo batches on the head-fast grid axis.
- Issue active causal q-blocks longest-first within each batch group.
- Preserve the cache-locality-favored V192 mapping above batch size 16.
- Keep divisor-safe fallbacks for arbitrary batch counts.
- Cover dispatch policy and ragged multi-q-block D192 accuracy.

Signed-off-by: Xiake Sun <xiake.sun@amd.com>
- Reuse the shared safe reciprocal helper for output normalization.
- Remove private arithmetic conversion imports from the paged kernel.
- Express paged lazy rescaling with typed FlyDSL if/else state.
- Preserve generated ISA, resources, numerics, and performance.

Signed-off-by: Xiake Sun <xiake.sun@amd.com>
@sammysun0711
sammysun0711 force-pushed the upstream/fa-paged-fp8-asymmetric-v branch from 5130cbc to 840d793 Compare September 3, 2026 23:21
- Delete split-K combine imports, kernel, and launch branches.
- Remove dead workspace and partial-output plumbing.
- Keep the existing num_kv_splits=1 validation contract.
- Preserve generated ISA, resources, numerics, and performance.

Signed-off-by: Xiake Sun <xiake.sun@amd.com>
@sammysun0711

Copy link
Copy Markdown
Contributor Author

I've further optimized D192/V192 and D128/V128 performance and extend B>1 support as follow:

  • Optimized D192/V192 with segmented H1/H2 native-FP8 P×V inspired by Non-power-of-two (NPOT) head dimension optimization aotriton#66, improving performance by 6.3–13.5%.
  • Pipelined D128/V128 V-cache prefetch through registers, reaching 95.0–96.1% of matched dense FP8 throughput
  • Extended the paired-page D128 BN128 kernel to packed-varlen batches, reducing B=2–32 latency by 25–39% over the generic fallback.
  • Added grouped, longest-first D192 batch scheduling, improving D192/V128 by up to 10.9% and D192/V192 by up to 17.6%.
  • Refactor paged-FP8 control flow and arithmetic helpers, removed unreachable split-K code.

Here are performance evaluation results on MI350X (gfx950)
B=1, Hq=16, Hkv=1, Page size=64

Q / KV D128/V128 paged Dense D128 Paged/dense D192/V128 paged D192/V192 paged
4K / 8K 0.1725 ms / 1,195.2 TFLOP/s 0.1639 ms / 1,257.9 TFLOP/s 95.01% 0.2610 ms / 987.4 TFLOP/s 0.4346 ms / 711.6 TFLOP/s
16K / 32K 2.0737 ms / 1,590.7 TFLOP/s 1.9925 ms / 1,655.5 TFLOP/s 96.08% 3.0717 ms / 1,342.3 TFLOP/s 4.9083 ms / 1,008.1 TFLOP/s
32K / 64K 8.2959 ms / 1,590.5 TFLOP/s 7.9126 ms / 1,667.5 TFLOP/s 95.38% 12.0487 ms / 1,368.8 TFLOP/s 18.7560 ms / 1,055.2 TFLOP/s
64K / 128K 33.5786 ms / 1,571.7 TFLOP/s 32.1621 ms / 1,641.0 TFLOP/s 95.78% 48.1408 ms / 1,370.4 TFLOP/s 76.1674 ms / 1,039.4 TFLOP/s

B=1-32, Hq=16, Hkv=1, Page size=64, Q/KV=4K/8K

B D128/V128 paged D128 paged/dense D192/V128 paged D192/V192 paged V192/V128 throughput
1 0.1719 ms / 1,199.1 TFLOP/s 93.85% 0.2598 ms / 992.0 TFLOP/s 0.4351 ms / 710.9 TFLOP/s 71.66%
2 0.2892 ms / 1,426.1 TFLOP/s 93.92% 0.4106 ms / 1,255.2 TFLOP/s 0.6566 ms / 942.0 TFLOP/s 75.04%
4 0.5620 ms / 1,467.5 TFLOP/s 94.72% 0.7822 ms / 1,317.9 TFLOP/s 1.2588 ms / 982.7 TFLOP/s 74.57%
8 1.1049 ms / 1,492.8 TFLOP/s 95.99% 1.5853 ms / 1,300.6 TFLOP/s 2.5066 ms / 987.0 TFLOP/s 75.89%
16 2.2233 ms / 1,483.7 TFLOP/s 95.92% 3.2047 ms / 1,286.7 TFLOP/s 5.0288 ms / 984.0 TFLOP/s 76.47%
32 4.4484 ms / 1,483.2 TFLOP/s 96.08% 6.4670 ms / 1,275.3 TFLOP/s 9.9310 ms / 996.5 TFLOP/s 78.14%

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.

4 participants