Skip to content

perf(k3): five cuts on the CP8 128k prefill superstep - #1026

Open
xiaguan wants to merge 6 commits into
mainfrom
feat/k3-cp8-anatomy-cuts
Open

perf(k3): five cuts on the CP8 128k prefill superstep#1026
xiaguan wants to merge 6 commits into
mainfrom
feat/k3-cp8-anatomy-cuts

Conversation

@xiaguan

@xiaguan xiaguan commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

What

Five independent cuts on the K3 CP whale lane, one commit each, ordered by the nsys anatomy of a CP8 128k superstep (tray14 deep-position rank, 6,130 ms; archive ~/code/bench_results/2026-09-01-k3-cp8-128k-gap-anatomy/, summary in docs/models/k3/cp-lane-design.md). The anatomy overturned two assumptions: the gaps were not host time (host-starved 0 ms — launch-queue backpressure and doorbell waits), and the FMHA kernel was already at peak — ~1,090 ms of its 2,132 ms was deep-position imbalance from the causal triangle.

# commit cut invariant changed
1 e1fdd881 matmul landing: TileLang 1 elem/thread (2.3 TB/s) → CUDA 8 cols/thread (6.6 TB/s) none — same cast, same order, bit-identical
2 dad55e4b chunk conv reads the f32 partial rows t-3..t in place instead of landing + 3 strided window copies + batched conv + carry copy none — term-for-term arithmetic, bit-identical (CP4 64k: DtoD 25,466 → 4,766 copies)
3 80f12bb8 striped FMHA: pair (q,k) with odd distance computed by the owner, even distance by the key holder; helpers TMA the owner's Q over fabric and return O+LSE through two alternating slabs; owner lse-merges in a fixed order. Deep-rank load 7.5 → 4.5 segment pairs (ideal 4). Fixed per-layer window chain: Upstream + Stripe + ⌊(R−1)/2⌋ return slots; +1.47 GB fabric slab per rank at seg_cap 16896 the per-layer collective window chain (still fixed, still no host waits); CP results are the same up to merge summation order (the existing CP gate contract)
4 003b5b5b lse_merge: 1 elem/thread (1.4 TB/s, 144 × 1.5 ms) → 8 cols/thread none — same per-element expression
5 e82f072d add2 / mul_sigmoid / situ / o_norm_gate: TileLang (1–2 TB/s) → CUDA 8 cols/thread; 40 TileLang instantiations retire none — cutlass bf16 +/*/cast reproduced (__hadd/__hmul/cvt.rn), o_norm_gate's 128-wide xor butterfly reproduced pair for pair; eps becomes a runtime argument

Rejected on the way: landing the GEMM in bf16 via cuBLAS epilogue. It is not bit-identical — cuBLAS picks different kernels for bf16 output, the accumulation order changes, and the 4-layer golden gate flips at its 3-ULP-margin steps (CUBLAS_MATH_DISALLOW_REDUCED_PRECISION_REDUCTION makes no difference). The gate was kept as is; the landing kernel was fixed instead.

Evidence

Fleet CP8 (tray03 + tray14, pruned 224-expert checkpoint, ~/bench-tools/k3-ttft-quick.py, same probe before/after, min-of-2/3):

prompt before (1e3b6f23) after cuts 1–3 after cuts 1–5 Δ
130,232 tok 6,387 ms 5,328 5,021 −21.4%
66,231 tok 2,810 2,394 2,231 −20.6%
16,279 tok 1,150 1,044 946 −17.7%

Superstep wall on the profiled rank 6,130 → 5,071 ms after cut 3 (re-profiled; per-family table in the doc). 24 greedy tokens after the 128k prompt continue the prose verbatim ("Chapter 584: …" following chapter 583). In-process CP4 64k wall 4,171 → 3,639 ms.

Gates per commit (pruned checkpoint, bare host): golden_decode 13/13 step-exact, paged_kv 3/3, spec_verify 6/6, cp_prefill (CP4 vs CP1 at 16,896/16,895 and 65,536/65,535: argmax and boundary tokens equal, rel_l2 within floor — unchanged to four digits across cuts 4–5, consistent with bit-identical kernels), and the kernels crate's windowed_walk_matches_single_causal_call.

Known flake (pre-existing on main, not introduced here)

cp_prefill's two tests run back to back in one process sometimes hit a DeepGEMM grid sync timeout in cp4_prefill_ttft_sweep (60 s barrier, arrival count stuck at 151). Reproduced 2/3 on 80f12bb8 with cut 4 stashed and 1/3 on main (1e3b6f23) built in a clean worktree; each test passes reliably as its own process, which is how the gates above were run. Root cause not yet found (the counter stalls short of the SM count, so some CTAs never become resident within the 60 s barrier — a same-process teardown/residency interaction, not a numerics issue).

Not in this PR

attnres vectorization (~80 ms), doorbell overlap (~144 ms), the residual FMHA imbalance (4.5 vs 4 pairs; helped dense segments over fabric Q run slower than local causal ones) and the mega-MoE wait accounting — the next round, which needs a fresh profile.

🤖 Generated with Claude Code

The CP8 128k prefill anatomy (deep rank 6,130 ms) charged 385 ms to
`k3_land`, the f32-partial -> bf16 landing after every dense projection.
The TileLang `land_batched` family landed one element per thread with
2-byte stores and reached 2.3 TB/s on the 12288-wide chunk landings
(550 us per launch at bucket 16896) — a third of HBM rate.

Replace the family with a hand-written CUDA kernel (`csrc/k3/k3_land.cu`):
eight columns per thread, two float4 loads per segment and one 16-byte
store, grid-stride over rows x n/8, batch a runtime value (no per-bucket
instantiation; rows not 16-byte aligned fall back to a scalar path). The
arithmetic is the retired kernel's spelling — ascending-segment f32 sum
from segment 0, one round-to-nearest-even cast — so at split_k = 1 the
landing is the bare cast and the output is bit-identical. The launcher
keeps its signature; no executor change.

Same-shape A/B (nsys, CP4 64k prefill gate, bucket 16896):
  nt=12288  n=12288  550 us -> 190 us  (1.25 GB, 6.6 TB/s)
  nt=18432  n=18432          287 us  (1.87 GB, 6.5 TB/s)
  nt=67584  n=33792          532 us  (3.4 GB, 6.4 TB/s)

A bf16 GEMM epilogue (cublasGemmEx with a bf16 C) was tried first and
rejected: cuBLAS picks a different kernel for a bf16 destination, so the
accumulation order moves and the 4-layer golden fixture flips at its
3-ULP-margin steps (same deviation distribution against the reference —
median 1 / p90 3 / max 12 bf16 ULP on both — but the gate's exact-match
contract is per kernel selection). The retired TileLang instantiations
(15 spans x 15 buckets) come off the build.

Gates (tray03, pruned 224-expert checkpoint): golden_decode 13/13 with
every step exact, paged_kv 3/3, spec_verify 6/6, cp_prefill 2/2.

Signed-off-by: xiaguan <751080330@qq.com>
The chunked-prefill conv path landed the q/k/v projection to bf16, built
every row's 3-slot window with strided device copies (row t's slot j from
landed row t-3+j, the carried window for the first rows), ran the batched
decode conv over the materialized windows, and copied the last commit row's
successor window out as the carry. The CP8 128k anatomy charged that to
182 ms of conv plus 235 ms of cuMemcpy2DAsync per deep rank — the whole
non-doorbell gap budget.

Replace it with one launch per group (`csrc/k3/k3_conv_silu_chunk.cu`)
that reads the f32 partial rows t-3..t directly, takes the positions before
the segment from the carried window, and writes the carry itself. Each
block owns a column slice and a run of 16 rows: the taps sit in registers
and the window slides through them, so every partial element is read once
and every output written once. The arithmetic is the batched conv kernel's
spelling term for term — `bf16(0 + p)` landing, ascending-tap f32
products, `bf16(ca)` then `sb * (1 / (1 + expf(-sb)))` — under the same
-O3 and no fast-math, so the output is bit-identical. The chunk's separate
conv-input landing and the `conv_window`/`conv_window_next` slabs go away;
the halo path and the decode conv are untouched.

Same-context A/B (nsys, CP4 64k prefill gate, device 0):
  DtoD copies   25,466 / 2,593 ms  ->  4,766 / 102 ms
  conv-input landings                  -385 ms (3 per KDA layer gone)
  conv kernel @ bucket 16896   877 us  ->  476 us per launch

The land kernel also lands onto a zero accumulator now, so a -0 partial
lands as +0 exactly as the retired TileLang kernel did.

Gates (tray03, pruned 224-expert checkpoint): golden_decode 13/13 every
step exact (chunked prefill agrees with the per-token walk), paged_kv 3/3,
spec_verify 6/6 (multi-group verify segments), cp_prefill 2/2.

Signed-off-by: xiaguan <751080330@qq.com>
Under context-parallel prefill every rank attended its own segment
against the whole prefix it had assembled, so rank r paid for r + 1/2
segment-pairs of FMHA while rank 0 paid 1/2: at CP8 128k the deep rank's
2,132 ms of FMHA was ~1,090 ms of pure imbalance (the kernel itself runs
at peak), and the lockstep superstep waits for the deepest rank.

Stripe the triangle instead (`cp::k3_cp_stripe_kept`): off-diagonal pair
(q, k) is attended by the owner q when q - k is odd and by the key holder
k when even — the helper already expanded its own keys for its diagonal,
so only queries and results move. Each rank publishes its queries once
per layer (riding the latent window), attends its kept segments densely
and its own segment causally into the f32 log-sum-exp accumulator, then
runs (cp_size - 1) / 2 return slots: in slot i it attends the i-th owner
it helps — that owner's published queries read in place by the FMHA over
the fabric, output + LSE landing in a return slab — and merges what its
own slot-i helpers returned, again in place. Two return slabs alternate
so slot i+1's FMHA launches inside window i's consume without waiting on
window i's readers; every query read is an FMHA launched before a return
publish the owner waits on, which orders the owner's next overwrite
behind it. Loads go from r + 1/2 to at most (r + 3) / 2 (8 ranks: deep
7.5 -> 4.5, ideal 4). Window kinds gain explicit rank-set bitmasks; the
whale slab grows the query and return regions (~1.47 GB per rank at
seg_cap 16896); the schedule is a pure function of cp_size and unit-tested
for single coverage, mirrored window sets and the leveling bound.

Same-context A/B (in-process CP4, pruned checkpoint, tray03):
  FMHA per rank @64k   1,064 / 1,744 / 2,408 ms  ->  1,735 / 1,068 / 1,733
  CP4 wall @65536      4,171 / 4,151 ms          ->  3,888 / 3,864 ms  (-285)
  deep rank -28% FMHA, exactly the 2.5 / 3.5 the schedule predicts.

Gates (tray03): cp_prefill 2/2 (CP4 vs CP1 argmax and boundary tokens
equal at 65536/65535, logits inside the noise floor), golden_decode
13/13, paged_kv 3/3, spec_verify 6/6, unit tests for the schedule and
the slab layout.

Signed-off-by: xiaguan <751080330@qq.com>
The windowed-context merge folded one bf16 FMHA window into the f32
accumulator with a 128-thread block per (q, head) row and one scalar
element per thread: 1.6M blocks, 2-byte loads, 1.4 TB/s. In the CP8 128k
superstep that was 144 launches x 1.5 ms = 220 ms of a 5,071 ms rank —
and with the striped triangle, every returned peer window pays it again
over fabric.

Now 16 lanes carry a row with one uint4 bf16 load and two float4
accumulator loads/stores each, and a 256-thread block walks 16 rows;
every lane of a row recomputes the two weights from the same lse pair and
lane 0 alone writes the merged lse. The per-element update keeps the
scalar original's expression, so the fold rounds exactly as before. The
finalize leaves the accumulator the same way.

Gates (pruned 224-expert checkpoint): golden_decode 13/13 exact,
paged_kv 3/3, spec_verify 6/6, the kernels crate's windowed walk vs the
single causal call, cp_prefill CP4 vs CP1 at 64k rel_l2 within floor.
CP4 64k in-process wall 3,888 -> 3,785 ms.

Signed-off-by: xiaguan <751080330@qq.com>
add2, mul_sigmoid, situ and o_norm_gate were TileLang batched kernels
walking one element per thread with 2-byte loads and stores — 1–2 TB/s on
the chunked-prefill rows, 220 ms of the CP8 128k deep rank (add2 91,
o_norm_gate 75, situ 36, mul_sigmoid 21) for what is 6 TB/s of traffic.

They are hand-written CUDA now, sixteen bytes per thread per operand and
shape-agnostic, with the retired kernels' arithmetic kept term for term:
TileLang's bfloat16_t is cutlass's, whose `+` is __hadd, `*` is __hmul
and cast is cvt.rn, so the bf16x2 forms round identically; the f32 chains
keep expf/tanhf/rsqrtf and IEEE division under the same -O3 without
fast-math. o_norm_gate's 128-wide xor butterfly (64 and 32 through shared
memory, 16..1 by shuffle) is reproduced pair for pair by sixteen lanes of
eight columns — lane xor 8/4/2/1 then slots j^4/j^2/j^1 — so the norm
scale is the same bits in every lane. eps is a runtime argument
(K3_RMS_EPS at the one call site) instead of a compiled constant.

Forty TileLang instantiations retire with the family (280 -> 240).

Gates (pruned 224-expert checkpoint): golden_decode 13/13 exact — decode
runs the same add2/mul_sigmoid/situ — paged_kv 3/3, spec_verify 6/6,
cp_prefill matches + sweep, CP4 vs CP1 at 64k rel_l2 unchanged to four
digits (8.706e-2 / 6.991e-2). CP4 64k in-process wall 3,785 -> 3,639 ms.

Signed-off-by: xiaguan <751080330@qq.com>
Five cuts on the CP whale lane (vectorized landing, in-place conv window,
striped FMHA triangle, vectorized lse_merge, vectorized bf16 elementwise
family): fleet CP8 128k TTFT 6,387 -> 5,021 ms, 65k 2,810 -> 2,231,
16k 1,150 -> 946. Records the cuBLAS bf16-epilogue rejection (kernel
selection changes accumulation order; the golden gate flips at 3-ULP
steps) and the open cp_prefill same-process grid-sync flake.

Signed-off-by: xiaguan <751080330@qq.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-01T13:06:02.424023Z e3b0c4d PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

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.

1 participant