Repository navigation
MLA decode on Ampere (RTX 3090, sm_86): v2 kernel at 94–99% of memory bandwidth, FP8/INT8 KV cache, skinny GEMM, and a fused single-graph decode layer 2.1–13.3× faster than unfused eager - #233
Open
KernelFlow-ops wants to merge 48 commits into
Conversation
Signed-off-by: zhaop8 <zhaopeng_eng@163.com>
Signed-off-by: zhaopeng <zhaopeng_eng@163.com>
Signed-off-by: zhaopeng <zhaopeng_eng@163.com>
This reverts commit f8dd3c4.
Signed-off-by: zhaopeng <zhaopeng_eng@163.com>
Signed-off-by: zhaopeng <zhaopeng_eng@163.com>
Signed-off-by: zhaopeng <zhaopeng_eng@163.com>
add CmakeLists.txt and Makefile
Merged sm90 and sm80 into a single project.
Signed-off-by: zhaopeng <zhaopeng_eng@163.com>
Signed-off-by: zhaopeng <zhaopeng_eng@163.com>
This reverts commit 2aba6c5c6e69790a7198e35b4c0c6380d484761d.
Signed-off-by: zhaopeng <zhaopeng_eng@163.com>
Signed-off-by: henry zhao <zhaopeng_eng@163.com>
Signed-off-by: henry zhao <zhaopeng_eng@163.com>
Change warp layout from (4,1,1) to (2,4,1) with kNWarps=8. This cuts per-warp acc_o registers from ~128 to ~64, eliminating NVCC miscompilation caused by extreme register spilling (255 regs, 5KB+). Trade-offs for distributing N across 4 warps: - Cross-warp softmax reduction via shared memory - P routed through smem for PV GEMM (replaces convert_layout_acc_Aregs) - Smaller LDSM copy atoms (U32x2/U16x4) for per-warp B fragments Also adds FLASH_MLA_DEBUG build mode.
- Split 8 warps into 4 consumer (QK^T + softmax) and 4 producer (gmem loads) - All 8 warps cooperate on PV GEMM - Double-buffered K/V loads for latency hiding - Raw PTX barriers (bar.sync/bar.arrive) for SM80 compatibility - Exposed via warp_spec=True parameter in flash_mla_with_kvcache - Added bench_compare_sm80.py for correctness and performance comparison - Updated README with warp-specialized kernel description and usage
…ak benchmark - setup.py: FLASH_MLA_ARCH=80/86 builds only sm80/86 sources with -gencode compute_<arch>/sm_<arch>, defines FLASH_MLA_NO_SM90, drops --register-usage-level; FLASH_MLA_LINEINFO=1 adds -lineinfo; CUTLASS submodule is only fetched when missing. - flash_api.cpp: guard sm90 dispatch with FLASH_MLA_NO_SM90. - flash_mla.h: Flash_fwd_mla_params gains workspace, combine_counters, kv_scale_ptr, kv_dtype (default-initialized, unused). - docs/kv_quant_format.md, docs/perf_log.md; benchmarks/phase0_peaks.py, benchmarks/legacy_baseline.py. - NOTE: not yet compiled (no nvcc 11.8 on the server).
…lel metadata, new API Not yet built/tested (waiting for the Phase-0 build setup).
…eline
- setup.py: glob csrc/sm86/*.{cu,cpp} into flash_mla_cuda; csrc/fused/* builds as separate
extension flash_mla_fused_cuda; nvcc args shared between the two extensions
- tools/build.sh: in-place sm_86 build with the flashmla env (nvcc 12.4, torch 2.7.1+cu126)
- legacy sm80 kernel: BlockM=16 / 4-warp fallback when 99 KB smem is the limit (sm_86);
warp-specialized variant raises a clear RuntimeError; interface gains impl="legacy"
- docs: perf_log.md -> perf_log_sys.md with env, peaks, legacy baseline; report skeleton
… decode prologue + quantize-on-write - benchmarks/: common.py, bench_decode.py (eager + CUDA-graph timing, L2 flush, legacy/triton/pkg impls), bench_flashinfer.py (vllm env), bench_one.py (ncu/nsys target), sweep.py (template-parameter sweep driver), summarize.py, baselines/triton_decode_attention.py (vendored vLLM/SGLang, Apache-2.0) - tools/: prof_nsys.sh, prof_ncu.sh (+ metric list, summaries, nsys kernel/gap breakdown) - csrc/fused: fused RoPE + q concat + paged KV write (+ optional RMSNorm, FP8/INT8 quantize-on-write), separate extension flash_mla_fused_cuda; flash_mla/mla_prologue.py, flash_mla/kv_quant.py (torch reference) - tests: test_fused_prologue.py, test_kv_quant.py (CUDA quantizer is bit-exact with the torch reference) - setup.py: FLASH_MLA_DEFINES env var -> -D flags (used by sweep.py)
…accuracy eval, baselines log - flash_mla/mla_decode_layer.py: DeepSeek-V2-Lite decode layer (fused prologue vs unfused PyTorch, pluggable attention, device-side metadata, whole-layer CUDA Graph capture); tests/test_fused_layer.py - benchmarks/bench_layer.py, bench_prologue.py, eval_kv_quant.py (+ end-to-end accuracy gate test in tests/test_kv_quant.py) - bench_decode.py: persistent-workspace path (alloc_mla_workspace) and 3-D uint8 quantized caches - tools/nsys_breakdown.py: NVTX range filter via launch correlation ids - docs: perf_log_sys.md sections 3-6 (baselines, profiling, fusion, quantization), ampere_opt_report.md (SA sections), interface_requests.md - normalize line endings to LF
…combine loads, mbarrier PTX helpers v2b passes the full correctness sweep but does not beat v2a (ties when DRAM bound, slower L2-resident); kept as an option (FLASH_MLA_SM86_CFG=bm,nt,ns,minb,1).
…sed last-arriver combine (optional), macro knobs - metadata: balanced cuts floor(p*T/P) (no empty parts from ceil rounding), active-part cap, seqlen_k of the first segment stored in the metadata (one dependent load less at kernel start); granule fixed at 32 (legacy-compatible) - combine: wide (column-split, <=64 rows) and warp-per-row modes, loads issued before the LSE normalisation - fused combine: last-arriving CTA merges <=FUSE splits, self-resetting counters (off by default: no measurable gain) - compile-time FLASH_MLA_SM86_* macros with #ifndef defaults, env overrides, flash_mla.sm86_config() - tests: fused combine, counters self-reset, graph replay with fusion, legacy cross-check (135 tests pass)
…ined variant, quant tests - mla_decode_sm86_q.cuh: KV tile stays 8-bit in smem; K via ldmatrix + prmt/lop3 conversion with Q dim permutation, V via ldmatrix.trans with even/odd dims in separate n-tiles (4 contiguous output dims per lane); per-token scale applied in fp32 (S = scale*lat + rope, P*scale); cp.async tile + scale ring; bf16 and fp16 - API: uint8 k_cache [nblk, page, 640] + kv_scale + kv_dtype='fp8'|'int8'; get_mla_metadata(..., kv_dtype=None) - v2c: QK(n+1) issued before softmax(n)/PV(n), split-phase mbarrier S exchange (correct, ~equal speed; off by default) - tests/test_flash_mla_sm86_quant.py: matrix over dtype/page/h_q/causal, NaN/garbage tail, long, fused combine, graph replay - h_q=16 fp8 1.67x / int8 1.64x over bf16 at b=128, sk=4096
…rified compute-sanitizer memcheck/racecheck/synccheck/initcheck: 0 errors on v2a, v2c, fused-combine counters and the fp8/int8 kernels. v2b: benign synccheck missing-wait on the empty barriers at kernel exit; racecheck does not model mbarrier.
…tized kernels (fp8 1.72x, int8 1.73x at h_q=16, b=128)
…), zero-length + workspace-reuse tests
… SK interface reply
…layer, e2e layer tests, joint exports
- mla_decode_layer: "v2" is the default attention backend; quantized caches pass kv_scale + kv_dtype
("fp8"|"int8") to flash_mla_with_kvcache (docs/interface_reply_sk.md), kv_dtype also reaches
get_mla_metadata as the scheduler hint; legacy rejects 8-bit caches with a clear error;
softmax_scale stays (128+64)**-0.5.
- flash_mla/__init__.py exports the kernel API (SK) and the quantization / prologue / decode-layer API (SA).
- tests/test_decode_layer_e2e.py: fused-prologue layer + v2 vs the bf16 torch reference layer,
gates cos_diff bf16 < 8e-5, fp8 < 5e-3, int8 < 5e-4; NaN-poisoned tails, graph replay over changing seqlens.
- tests/test_flash_mla_sm80.py: the fork's test is runnable again (warp_spec kwarg is gone), pytest-parametrized
over impl in {legacy, v2}; script mode selects the kernel with FLASH_MLA_TEST_IMPL.
- benchmarks: kv_dtype passed for 8-bit caches, "full" and "spot" presets for the integration matrix.
…rizer, README Ampere section - benchmarks: --cool (wait for the GPU to cool below FLASHMLA_COOL_LO when above COOL_HI; clocks cannot be locked without sudo), "core" preset (h_q 16/64/128), dequantized reference only for layer 0 to fit b=128 s_k=8192 - benchmarks/summarize_matrix.py + docs/bench_matrix.md: v2 / legacy / Triton / FlashInfer matrix, scorecard, fp16, fp8/int8 vs bf16, page 16 spot checks (RTX 3090, GPU1) - tools/run_matrix.sh, tools/run_flashinfer.sh: matrix drivers - flash_mla/kv_quant.py: cache the 0-dim qmax tensor per device so quantize_kv is CUDA-graph capturable - tests/test_decode_layer_e2e.py: whole-layer graph replay also for the unfused layer - README: Ampere (sm_86) build/usage section Re-verified: kv_quant, decode_layer_e2e, fused_prologue, fused_layer, sm86_quant -> 304 passed (GPU3).
CfgQ<16,32,2,2> makes FLASH_MLA_SM86_QCFG=16,2,2,32 launchable: same tile as the default plan, twice the split parts so two CTAs share each SM. Graph mode, h_q=16, fp8 vs bf16 on GPU1: b64 s_k8192 1.55x -> 1.68x, b128 s_k8192 1.54x -> 1.61x; slower for s_k <= 2048 (0.87-1.00x vs the default plan), so the default stays unchanged. Test covers fp8/int8, bf16/fp16, page 16/32/64 and checks the doubled part count.
docs/ampere_opt_report.md replaces the English work-in-progress draft: hardware ceilings, optimization list with measured effects, kernel design, four-way kernel comparison, 8-bit KV (audited speedups, opt-in 2 CTAs/SM regime map, accuracy), whole-layer results with nsys kernel breakdown, ncu evidence, what did not work, scorecard against the plan targets, build and usage, remaining ideas. README: h_q=128 efficiency now quoted as in the scorecard (73-75% eager, 66-72% CUDA Graph, b>=16, s_k>=4096) instead of 71-76%.
…phase 2 B1) csrc/fused/skinny_gemm.cu: batched TN GEMM y[t,g,:] = x[t,g,:] @ w[g]^T for T <= 64 tokens, mma.sync m16n8k16 with swapped operands (weights = A, tokens = B), 48 KB cp.async ring per CTA, split-K over the 4 warps inside the CTA, optional deterministic split-K across CTAs (last arriver, self-resetting counters). Exposed as flash_mla_fused_cuda.skinny_gemm and flash_mla/skinny_gemm.py (plan from the R-051 sweep; cuBLAS fallback for single-matrix projections beyond 32 tokens). MLADecodeLayer(gemm="auto"): qkv_a, W_UK, W_UV and o_proj use it in the fused path (W_UK / W_UV pre-transposed, W_UV writes the o_proj input directly: no reshape copy). Four GEMMs at b=8: 41.5 us vs 61.7 us cuBLAS; 27-layer graph b=8 s_k=4096: 115.4 -> 102.0 us/layer, fp8 100.4 -> 86.1. Also: benchmarks/bench_layer_gemms.py (per-op timing, --sweep), bench_layer.py --gemm and a fix for the summary line when only the fused variant runs, tools/probe_layer_kernels.py (ncu/nsys probe), tests/test_skinny_gemm.py (128 tests: accuracy vs fp32, all tiles/splits, strided views, determinism, CUDA graph replay, fallback, input checks).
… load (phase 2 B2) The quantized decode kernel (BM 16) gets a second set of 4 warps (CfgQ KG = 2): KV group g takes tiles g, g + 2, ... of its part with its own smem ring, S-exchange buffer and named barriers, and the two (m, l, O) states are merged through smem in a fixed order at the end of a segment (KV group 0 writes the output). Two warps per SM sub-partition interleave HMMA bursts with dequant work without doubling the split count (R-048). The second group only pays off on long parts (the per-segment Q load and merge are fixed costs, R-055), and the choice must be the same for every CTA of a launch: a per-segment choice mixes CTAs with one and two tiles in flight and starves the former of DRAM bandwidth (151 vs 122 us at b32 s4096, R-056). The metadata kernel therefore writes the load per part (scheduler units, identical for all parts) into metadata int 6, and the kernel uses two groups iff it is >= kg_min_tiles (FLASH_MLA_SM86_KGMIN, default 64). Default plan of the 8-bit kernels with BM 16 is now kg = 2 (FLASH_MLA_SM86_QKG; QCFG 5th field). R-041 grid, CUDA graph, vs the previous default: fp8 0.993-1.115x (+4..11% at >= 76 units/part, s_k <= 2048 min 0.999), int8 0.992-1.072x; fp8/bf16 at b >= 32, s_k >= 4096 now 1.55-1.70x (R-057, R-058). Eager is 1-3% slower at 75-103 units/part. KG2 instances use 254-255 registers, 0 spills, 1 CTA/SM. Tests: KV-group matrix / edges / b1 long / fused combine / launch threshold / default plan (fp8, int8); compute-sanitizer racecheck, synccheck, memcheck clean.
ampere_opt_report.md: new section 11 (A1-A3 ncu findings, skinny GEMM, two KV groups with the launch-uniform load gate, combine single pass and h128 rescale skip / ping-pong not kept, whole-layer numbers after phase 2); summary, item list, scorecard, usage, remaining ideas and data sources updated. README: 8-bit default and its env overrides, skinny GEMM in the decode layer, test_skinny_gemm in the test list. bench_matrix.md is left as the first-release matrix (noted in both files).
…new README - README.md rewritten in full: baseline -> optimized tables, every optimization of phases 1 and 2, the phase-3 plan, negative results, build, usage, tunables, benchmarks and repository layout - docs/optimization/: results (01), phase 1: v2 decode kernel, scheduler, 8-bit KV cache, fused layer (02), phase 2: skinny GEMM and two KV groups (03), phase 3 plan, not implemented (04), what did not work (05) - docs/optimization/figures/: four hand-drawn-style figures (SVG + PNG) and the scripts that draw them (dependency-free SVG generator, headless-browser render) Documentation only; no code changes. docs/ampere_opt_report.md stays as the original working report.
The library code of phases 1 and 2 was already on this branch. This adds the code behind the remaining numbers in docs/optimization that lived outside the repository. - research/phase3/: the prototypes that measured the phase-3 items on the real 27-layer graph (discard of the partial outputs, GEMM prefetch chain, W_UK + prologue emulation, lean refresh, KV prefetch, per-layer timeline) and the stand-alone GEMM-chain program. Site paths removed; inline extensions build in $FLASHMLA_P3_BUILD. All of them were re-run from this location. - research/phase2_rejected/: the three experiments that were measured and not kept (combine single round, rescale skip, h128 ping-pong) as diffs that apply to this branch. - research/README.md: which script backs which number, how to run, how to read. - README and docs/optimization now point to the research code. No change to the library, tests or benchmarks.
The history of this branch was rewritten (commit messages and author identity only; every tree is unchanged), which changed the commit ids. The report now cites the current ids. Evidence file names keep the old tags; the mapping is given under the table.
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
The Ampere port this branch builds on (
pzhao-eng/FlashMLA@a100_replace, 21 commits, included) does not launch on GeForce Ampere: its kernel needs 113,280 B of shared memory per block and sm_86 allows 101,376 B. The 27 commits added here run MLA decode on an RTX 3090 at the hardware ceiling, then optimize the decode layer around it.What is added
csrc/sm86/): Q in registers, split-K QK inside the CTA, a multi-stagecp.asyncKV ring; balanced scheduler, deterministic combine, graph-safe API; bf16 / fp16, page sizes 16 / 32 / 64.csrc/fused/,flash_mla/mla_decode_layer.py): one prologue kernel (RoPE, concat, cache write, quantize-on-write), a weight-streaming "skinny" GEMM for the four projections, and the whole layer as one CUDA graph: 7 kernels per layer instead of about 44.docs/optimization/).Performance
One RTX 3090; measured peaks 873.8 GB/s (read) and 70.0 TFLOPS (bf16). DeepSeek-V2-Lite layer (h_q = 16), 4096 cached tokens per request, 27-layer CUDA graph, microseconds per layer (lower is better), batch size b:
Reading the table: rows 1–2 are the unfused baselines (attention by the port's kernel), rows 3–5 the fused layer with three attention kernels, the bold rows the current build; the last three divide a baseline by a bold row. The eager layer is launch-bound: over 40 launches cost 820–900 µs at any batch size. The graph removes that cost; fusion removes the small kernels between projections and attention; the skinny GEMM gains 2–15%, most at b=8, where projections are half of the layer. At large batch attention dominates and the 8-bit cache is the lever (b=64: 431.4 → 291.2 µs).
The decode kernel alone:
Correctness
760 tests pass: fp32 reference (cosine distance below 8e-5), NaN padding, variable lengths, graph replay, bit-identical reruns;
compute-sanitizeris clean. 8-bit accuracy on synthetic caches: worst cosine distance 2.4e-3 (FP8), 2.6e-4 (INT8).Notes
main(merge basebcb90f2): 11 files conflict, so it cannot be merged as is and is offered as a reference Ampere branch.docs/optimization/04-phase3-plan.md, harnesses inresearch/; not implemented.🤖 Generated with Claude Code