[API] Accept caller-provided FP8 output buffers in the fused sparse attention API - #23
Merged
Merged
Conversation
…on API Add an optional `out=(out_fp8, out_sf)` pair to `fused_norm_rope_attn_rope_cast.prefill` / `.decode` (schema: `Tensor(d!)? out_fp8=None, Tensor(e!)? out_sf=None`). When given, the kernel writes the quantized activation and packed ue8m0 scales into the caller's tensors instead of allocating; omitting it keeps the old behaviour. The checks are exactly what the kernel needs, not what DeepGEMM needs: out_fp8 must be a contiguous, 32-byte-aligned e4m3 [s_q, n_wv_group, wv_group_size*d_v] tensor (a token range of a larger contiguous buffer qualifies, since the kernel hard-codes the token stride); out_sf must be int32 [s_q, n_wv_group, wv_group_size*d_v/128] with stride 1 along the token dim, and its group / head-dim strides must not let token ranges overlap. The head-dim stride is deliberately NOT required to equal ceil4(s_q), so the prefill and decode segments of one mixed step can write disjoint token ranges of a single buffer pair sized for the total N, which one deep_gemm.fp8_einsum call consumes without a concat copy. (DeepGEMM's check_sf_layout requires sf.stride(-1) == align(mn, 4) exactly, so that single einsum must run over all N tokens.) tests/test_fused_optional_out.py is self-contained (torch + flash_mla only) and checks bit-identical results between provided and allocated buffers for prefill and decode (V4 / V4.1 caches, h_q 64 / 128, s_q down to 1, with and without Q norm), the shared-buffer segment scenario with sentinels on the untouched range and padding columns, and rejection of invalid buffers. 31 cases pass on GB200. Co-Authored-By: foraxe <ningyunxiao.nyx@antgroup.com> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Add an optional
out=(out_fp8, out_sf)argument toflash_mla.fused_norm_rope_attn_rope_cast.prefilland.decode. When given, the fused norm + RoPE + attn + RoPE + cast kernel writes the FP8 activation and the packed ue8m0 scales into the caller's tensors instead of allocating them. Omitting it keeps the current allocating behaviour. Stable-ABI schema:Tensor(d!)? out_fp8=None, Tensor(e!)? out_sf=Noneappended to both operators, so existingtorch.ops._flashmla_Ccallers are unaffected.Motivation
In a mixed prefill + decode step the two fused launches (non-paged bf16 KV for prefill, paged FP8/FP4 cache for decode) can now write disjoint token ranges of one shared buffer pair sized for the total
Ntokens, and a singledeep_gemm.fp8_einsum("bhr,hdr->bhd", ...)consumes the whole step. Without this, the two outputs have to be concatenated (abouth_q * 512bytes per token) before theWvprojection. Caller-owned outputs are also what a CUDA-graph capture of the downstream einsum needs.Interface and checks
Both tensors must be given together. The checks are exactly what the kernel needs, deliberately not what DeepGEMM needs:
out_fp8:[s_q, n_wv_group, wv_group_size*d_v],float8_e4m3fn, contiguous, 32-byte aligned (256-bit stores). A token range of a larger contiguous buffer qualifies, since the kernel hard-codes the token stride.out_sf:[s_q, n_wv_group, wv_group_size*d_v/128],int32,stride(0) == 1(MN-major, skipped fors_q == 1because PyTorch normalizes size-1 strides). The group and head-dim strides are passed through to the kernel and only have to keep token ranges from overlapping (stride(2) >= s_q,stride(1) >= size(2)*stride(2)).The head-dim stride is not required to equal
ceil4(s_q). DeepGEMM'scheck_sf_layoutrequiressf.stride(-1) == get_tma_aligned_size(mn, 4)as an exact equality for themnit is finally called with, so a shared buffer must be allocated for the totalNand consumed by one einsum over allNtokens. Per-segment einsums on views of a shared scale buffer are rejected by DeepGEMM, which is why the fused API leaves that constraint to the caller:Tests
tests/test_fused_optional_out.pyis self-contained (torch + flash_mla only, no DeepGEMM / TileKernels / kernelkit) and checks:out_fp8/out_sf/lse/max_logitsbetween provided and allocated outputs, for prefill and decode, V4 and V4.1 fp8 caches,h_q64 and 128,s_q1 / 5 / 7 / 128 / 200, with and without Q norm, including a query with no valid KV tokenN, with sentinels proving neither launch touches the other's range or the scale padding columns, for boundaries that are not multiples of 4out_fp8, wrong dtypes, wrong shape, non-contiguous activation, misaligned activation, non-unit token stride, and overlapping head-dim columnsOn GB200 (Torch 2.13 / CUDA 13, extension built for sm_100a + sm_103a):
Not run here: the upstream suites (
tests/test_fused_norm_rope_attn_rope_cast.py,tests/test_flash_mla_sparse_decoding.py) needkernelkit/deep_gemm/tile_kernels, which are not installed on this machine. Those code paths are untouched by this change.Relation to #22
#22 by @foraxe proposes the same capability with separate keyword arguments. This PR takes the single
out=(out_fp8, out_sf)pair, adds the shared-buffer segment scenario and the DeepGEMM stride analysis above; @foraxe is credited as co-author on the commit.Files
csrc/api/api.cpp: schema for both fused operatorscsrc/api/interfaces.h,csrc/api/fused_norm_rope_attn_rope_cast_fwd.cpp:resolve_fp8_outputsvalidate-or-allocate helper, used by both entry pointsflash_mla/fused_norm_rope_attn_rope_cast.py:outparameter and docstests/test_fused_optional_out.py: new🤖 Generated with Claude Code