Skip to content

feat(kda/sm90): FlashKDA prefill port + intracard-CP prefill#103

Closed
cherhh wants to merge 45 commits into
inclusionAI:mainfrom
cherhh:flashkda-cp
Closed

feat(kda/sm90): FlashKDA prefill port + intracard-CP prefill#103
cherhh wants to merge 45 commits into
inclusionAI:mainfrom
cherhh:flashkda-cp

Conversation

@cherhh

@cherhh cherhh commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Stacked on # (refactor(kda): reorganize KDA backends into arch-first layout and add lazy imports) — please review only the commits on top of it.

📌 Description

This PR adds the SM90 (Hopper) KDA prefill backend to cuLA, ported from MoonshotAI's [FlashKDA](https://github.com/MoonshotAI/FlashKDA) CUTLASS C++ kernels to CuTeDSL, and extends it with an intracard context-parallel (CP) mode that is not present upstream.

Huge thanks to the MoonshotAI FlashKDA authors — the kernel design and its numerics are theirs; this port follows the original kernels closely.

What's included:

  • FlashKDA CuTeDSL port: cula_kda_prefill, a FLA-compatible API. Each call launches two kernels; workspaces are reused across calls.
  • Intracard CP prefill (new): splits long sequences into segments and runs their recurrences in parallel (K1 once → pre_scan → merge → segment-K2). Output is bit-identical to the serial path (torch.equal).
  • Auto dispatch: use_intracard_cp="auto" | True | False, default off. In auto mode the planner turns CP on only when it predicts a real win, from tile counts and the device SM count. Its two tuned constants are chain-cost ratios (fitted on H100); both are env-overridable (cula/ops/kda/sm90/cp/plan.py).

🔍 Related Issues

N/A

🚀 Pull Request Checklist

Thank you for contributing to cuLA! 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.

🧪 Tests

  • Tests have been added or updated as needed.
  • All tests are passing.
CUDA_VISIBLE_DEVICES=0 python -m pytest tests/test_kda_sm90_prefill_vs_fla.py tests/test_kda_sm90_intracard_cp.py -q
31 passed, 3 skipped in 102.20s
Full pytest output
tests/test_kda_sm90_prefill_vs_fla.py::test_prefill_dense_matches_fla[B1-T63-H1-tail] PASSED
tests/test_kda_sm90_prefill_vs_fla.py::test_prefill_dense_matches_fla[B1-T256-H2-aligned] PASSED
tests/test_kda_sm90_prefill_vs_fla.py::test_prefill_dense_matches_fla[B1-T500-H2-tail] PASSED
tests/test_kda_sm90_prefill_vs_fla.py::test_prefill_dense_matches_fla[B1-T512-H2-init_state] PASSED
tests/test_kda_sm90_prefill_vs_fla.py::test_prefill_dense_matches_fla[B2-T512-H2-stride] PASSED
tests/test_kda_sm90_prefill_vs_fla.py::test_prefill_dense_matches_fla[B1-T1024-H4-init_state] PASSED
tests/test_kda_sm90_prefill_vs_fla.py::test_prefill_dense_matches_fla[B2-T1024-H4] SKIPPED
tests/test_kda_sm90_prefill_vs_fla.py::test_prefill_varlen_matches_fla[cu[0,15]-H2] PASSED
tests/test_kda_sm90_prefill_vs_fla.py::test_prefill_varlen_matches_fla[cu[0,256,500,1000]-H4] SKIPPED
tests/test_kda_sm90_prefill_vs_fla.py::test_prefill_varlen_matches_fla[cu[0,15,100,300,1200]-H4-init] SKIPPED
tests/test_kda_sm90_prefill_vs_fla.py::test_prefill_workspace_reuse_across_shapes PASSED
tests/test_kda_sm90_intracard_cp.py::test_cp_matches_serial_fixed[1] PASSED
tests/test_kda_sm90_intracard_cp.py::test_cp_matches_serial_fixed[2] PASSED
tests/test_kda_sm90_intracard_cp.py::test_cp_matches_serial_fixed[4] PASSED
tests/test_kda_sm90_intracard_cp.py::test_cp_matches_serial_fixed[7] PASSED
tests/test_kda_sm90_intracard_cp.py::test_cp_matches_serial_fixed_b2_with_state PASSED
tests/test_kda_sm90_intracard_cp.py::test_cp_matches_serial_varlen PASSED
tests/test_kda_sm90_intracard_cp.py::test_cp_matches_serial_state_transposed PASSED
tests/test_kda_sm90_intracard_cp.py::test_cp_no_final_state PASSED
tests/test_kda_sm90_intracard_cp.py::test_cp_matches_serial_varlen_nonaligned[lens0] PASSED
tests/test_kda_sm90_intracard_cp.py::test_cp_matches_serial_varlen_nonaligned[lens1] PASSED
tests/test_kda_sm90_intracard_cp.py::test_cp_matches_serial_varlen_nonaligned[lens2] PASSED
tests/test_kda_sm90_intracard_cp.py::test_cp_matches_serial_varlen_nonaligned[lens3] PASSED
tests/test_kda_sm90_intracard_cp.py::test_cp_matches_serial_dense_nonaligned[100] PASSED
tests/test_kda_sm90_intracard_cp.py::test_cp_matches_serial_dense_nonaligned[4100] PASSED
tests/test_kda_sm90_intracard_cp.py::test_cp_matches_serial_dense_nonaligned[8197] PASSED
tests/test_kda_sm90_intracard_cp.py::test_cp_vs_fla_dense[8192] PASSED
tests/test_kda_sm90_intracard_cp.py::test_cp_vs_fla_dense[16384] PASSED
tests/test_kda_sm90_intracard_cp.py::test_cp_vs_fla_dense_with_state PASSED
tests/test_kda_sm90_intracard_cp.py::test_cp_vs_fla_varlen_with_state PASSED
tests/test_kda_sm90_intracard_cp.py::test_cp_determinism PASSED
tests/test_kda_sm90_intracard_cp.py::test_cp_determinism_varlen PASSED
tests/test_kda_sm90_intracard_cp.py::test_cp_matches_serial_ragged_auto_plan[skewed-1long-16short] PASSED
tests/test_kda_sm90_intracard_cp.py::test_cp_matches_serial_ragged_auto_plan[mixed-lengths] PASSED

============ 31 passed, 3 skipped, 54 warnings in 102.20s (0:01:42) ============
  • test_kda_sm90_prefill_vs_fla.py — serial prefill accuracy against FLA's Triton chunk_kda (rel err asserted at 5e-3, bf16 tolerance), dense + varlen + initial/final state, plus a workspace-arena reuse guard across shape changes.
  • test_kda_sm90_intracard_cp.py — CP vs serial bit-exactness (torch.equal, incl. unaligned varlen and ragged auto-planned batches), CP determinism, and forced-CP accuracy against FLA through the public API.

⚡ Performance

Intracard CP vs the serial path, same kernel binary, auto dispatch (benchmarks/bench_kda_sm90_cp.py, H100 80GB SXM, median of 100 iters):

  [H=4]                                            [H=8]
  config        T   CP_off(ms) CP_on(ms) Speedup   CP_off(ms) CP_on(ms) Speedup
  T=4K       4096       0.4544    0.2121   2.14x       0.4723    0.2300   2.05x
  T=8K       8192       0.8833    0.2595   3.40x       0.9110    0.2962   3.08x
  T=16K     16384       1.7197    0.3620   4.75x       1.8313    0.4967   3.69x
  T=32K     32768       3.4424    0.5503   6.26x       3.5553    0.8625   4.12x
  T=64K     65536       6.7930    0.9299   7.30x       7.0130    1.6200   4.33x
  2x16K     32768       1.8313    0.6093   3.01x       1.9323    0.8402   2.30x
  32K+4K    36864       3.4785    0.6970   4.99x       3.5585    1.1836   3.01x
  64K+1K    66560       6.7955    1.1826   5.75x       7.0221    2.1282   3.30x
  64K+5x1K  70656       6.8061    1.2292   5.54x       7.0617    2.2694   3.11x

  All configs: geo-mean 3.86x, best 7.30x, worst 2.05x

The serial path is unaffected: CP is off by default and, in auto mode, the planner declines shapes where splitting cannot pay for its re-run cost (short sequences, machines already saturated by batch x heads).

Reviewer Notes

  • Dispatch policy lives entirely in cula/ops/kda/sm90/cp/plan.py (plan_prefill → trivial plan = serial path); the executor (cp/driver.py) runs a given plan and contains no decisions.
  • Current limits: forward only; varlen CP requires packed B=1; num_kv_heads == num_qk_heads (GVA is a follow-up).

@cherhh
cherhh requested review from fkuner and icavan July 6, 2026 17:36

@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 reorganizes the repository layout by moving KDA public APIs to cula/kda/ and grouping backend kernels by architecture under cula/ops/kda/sm90/ and cula/ops/kda/sm100/. It also introduces a new SM90 (Hopper) two-kernel FlashKDA prefill backend with intracard context-parallel (CP) support, and consolidates the CUDA extensions into a single cula.cudac module.

Feedback on these changes highlights three key areas for improvement: a critical race condition in the SM90 CP driver's scratch buffer cache (_get_scratch) due to a lack of stream-safety, an installation failure on Hopper-only systems because Blackwell build environment validation is called unconditionally in setup.py, and an excessively slow determinism test suite that runs 10,000 iterations instead of a more reasonable number like 100.

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 +49 to +57
def _get_scratch(key_name: str, shape: tuple, dtype, device) -> torch.Tensor:
key = (key_name, shape, dtype, str(device))
cached = _SCRATCH_CACHE.get(key)
if cached is None:
if len(_SCRATCH_CACHE) >= _SCRATCH_CACHE_MAXSIZE:
_SCRATCH_CACHE.pop(next(iter(_SCRATCH_CACHE)))
cached = torch.empty(shape, dtype=dtype, device=device)
_SCRATCH_CACHE[key] = cached
return cached

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.

critical

The _get_scratch function uses a cache key that only includes the device ID, ignoring the current CUDA stream. In concurrent multi-stream environments (such as multi-stream inference servers or pipeline parallel setups), different streams executing concurrently on the same device with the same shape will share the same physical scratch tensors (b_seg, m_seg, carries, seg_final). This leads to a critical race condition and silent data corruption as the asynchronous kernels overwrite each other's intermediate states.\n\nIncluding the current stream pointer in the cache key ensures stream safety.

def _get_scratch(key_name: str, shape: tuple, dtype, device) -> torch.Tensor:\n    stream_ptr = int(torch.cuda.current_stream(device).cuda_stream)\n    key = (key_name, shape, dtype, str(device), stream_ptr)\n    cached = _SCRATCH_CACHE.get(key)\n    if cached is None:\n        if len(_SCRATCH_CACHE) >= _SCRATCH_CACHE_MAXSIZE:\n            _SCRATCH_CACHE.pop(next(iter(_SCRATCH_CACHE)))\n        cached = torch.empty(shape, dtype=dtype, device=device)\n        _SCRATCH_CACHE[key] = cached\n    return cached

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.

@cherhh Actually it is fine since the caller always try to call k1 & k2 in the same cuda stream.

Adding stream_ptr as a part of the key is a good suggestion. We shall take this suggestion.

Comment thread setup.py Outdated
Comment on lines +118 to +119
# Validate Blackwell build environment
assert_blackwell_build_env()

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.

high

The Blackwell build environment validation (assert_blackwell_build_env()) is called unconditionally inside get_arch_flags(). If a user is installing cuLA on a Hopper-only system with an older CUDA toolchain (e.g., CUDA 12.2/12.3) and explicitly disables Blackwell builds via CULA_DISABLE_SM100=1 and CULA_DISABLE_SM103=1, the installation will still fail during setup.\n\nWrapping the validation in a conditional check ensures that the Blackwell toolchain requirements are only enforced when actually building for Blackwell architectures.

Suggested change
# Validate Blackwell build environment
assert_blackwell_build_env()
# Validate Blackwell build environment if building for Blackwell\n if not DISABLE_SM100 or not DISABLE_SM103:\n assert_blackwell_build_env()

Comment thread tests/test_kda_sm90_intracard_cp.py
cheheng.ch added 25 commits July 16, 2026 13:20
K1 prepares decayed q/k, beta and the WY inverse; K2 runs the
inter-chunk recurrence. CHUNK=16, D=128, forward only.
Split long sequences into segments and run the recurrence per segment:
K1 once -> pre_scan -> merge -> segment-K2. Bit-identical to serial.
The shape binds wrong whenever only one of the two states is present.
27us -> 3us per launch; torch tensors pass straight through.
allocate_workspace/clear_workspace_cache lost their last caller when the
arena landed; banners and restating comments go per the new comment rules.
@cherhh cherhh closed this Jul 19, 2026
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.

2 participants