Skip to content

K3: paged latent MLA KV + absorbed decode; EP slot default 64 - #869

Merged
xiaguan merged 4 commits into
mainfrom
feat/k3-paged-kv
Aug 14, 2026
Merged

K3: paged latent MLA KV + absorbed decode; EP slot default 64#869
xiaguan merged 4 commits into
mainfrom
feat/k3-paged-kv

Conversation

@xiaguan

@xiaguan xiaguan commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

What

Replaces K3's fully-expanded, statically-sized MLA decode cache with a paged latent KV and an absorbed-MLA decode kernel, and raises the EP slot default now that the chain-era cap is gone.

Three commits:

  1. EP slot default 16 → 64 — the old ceiling came from the retired NCCL-chain constraint (ep × batch ≤ masked_cap 128). The mega protocol admits up to 384 rows/rank; the practical bound is now the KDA state slab (~929 MB/slot), which fits 64 slots next to the weights on GB300. 128 becomes reachable once the FP8 backbone lands.

  2. Paged MLA latent KV pool + write path (M1) — decode stores the 576-wide latent rows (KV-LoRA 512 + RoPE 64, bf16) in 64-token pages, [page][layer][token][576] page-first slab, per-slot block tables (host mirror + device i32) backed by a free-list allocator. K3ExecutorConfig gains kv_pages (0 = full coverage derived from max_ctx), and max_ctx becomes a runtime knob (default 4096, PEGAINFER_K3_MAX_CTX, ceiling 1M) instead of a compile-time constant. 27.6 KB/token across the 24 MLA layers, vs ~1.47 MB/token for the expanded cache this replaces.

  3. Absorbed-MLA decode over the paged KV (M2) — hand CUDA kernel (k3_mla_paged_attn.cu): per step, q_abs = W_UKᵀ · q_nope is precomputed and attention runs as MQA directly over the latent rows via a 3-sweep online softmax with no O(ctx) scratch and a runtime page-walk (no compile-time ctx instantiation — TileLang can't express the runtime-length page loop, hence hand CUDA). The expanded cache, K3MlaState, the kv_b expansion, and the mla_attn_batched TileLang family are deleted in lockstep.

Verification

  • Absorbed vs. expanded reference: median 1.0 / max 3.0 bf16 ULP (noise floor).
  • Golden decode transcript: 39/40 steps unmoved; the single miss is a step-11 0-ULP coin flip.
  • Paged vs. permuted-pages: bitwise identical over 200 decode steps.
  • ctx-2048 × 1100 steps, CUDA graphs vs. eager: bitwise identical.
  • EP4 oracle: bitwise identical.
  • 34 lib tests green; gates live in pegainfer-k3/tests/paged_kv.rs.

Follow-ups (out of scope here)

Chunked prefill (removes the token loop and the second state pool), kv-store BlockPool integration for content addressing/reuse, EP4 CUDA graphs, and a fusion pass over the 3-sweep kernel.

🤖 Generated with Claude Code

xiaguan and others added 3 commits August 14, 2026 05:55
The 16-slot default enforced the retired stepwise chain's
ep_size x max_batch <= masked_cap constraint. The fused transport's
protocol maximum is 384 rows per rank; the binding constraint today is
the KDA state slab (~929 MB/slot: f32 recurrent x2 parity + conv
windows across 69 layers), which puts 64 slots (~58 GiB) next to the
224-expert rank's weights with room left for the paged MLA pool. The
compiled 128-row bucket ceiling becomes reachable once the backbone
goes FP8.

Signed-off-by: xiaguan <751080330@qq.com>
Add the paged latent cache the K3 MLA layers will decode from: one
page-first slab per state pool ([page][mla_layer][64 tokens][576] bf16 —
the post-norm kv latent and the shared rope half, the exact quantity
kv_b expands K and V from; NoPE, so the row is position-independent),
a per-slot block table with a host mirror, and a plain free-list page
allocator (claim on 64-boundary crossing, release-all on slot reset,
whole-page copy on prefill adoption). Pool size comes from the new
K3ExecutorConfig::kv_pages (0 derives full coverage: every slot can
reach max_ctx).

The decode step now appends every MLA layer's new-token latent into the
current page slice (two indexed writes through the per-layer row shift)
alongside the existing expanded K/V append. This is a transitional dual
write: attention still reads the expanded cache, so behavior is
unchanged and every gate stays green; M2 lands the absorbed-MLA kernel
over the pages and deletes the expanded cache and its kernel family.

The shared indexed row write widens its element index to size_t — the
K3 paged slab is the first destination past 2^31 elements.

Gates: cargo test -p pegainfer-k3 --lib (34 passed) and cargo check
--tests green; GPU gates unaffected by construction (attention path
untouched).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: xiaguan <751080330@qq.com>
Decode now attends over the paged latent cache M1 introduced, and the
expanded slot-indexed MLA cache is gone. The step lands the 576-wide
[kv_lora 512 | rope 64] row into the mapped page and runs one absorbed
kernel per MLA layer: score(t) = (W_UK[h]^T q_nope)^T c_t + q_rope^T
k_rope_t, o[h] = W_UV[h] (sum p_t c_t), with W_UK/W_UV read straight out
of the checkpoint's w_kv_b — no expansion GEMM, no per-head K/V rows,
27.6 KB/token instead of 1.47 MB/token, and no compile-time context cap
(max_ctx is a runtime number now: default 4096, PEGAINFER_K3_MAX_CTX
override, ceiling K3_MAX_CONTEXT).

The kernel is hand CUDA (csrc/k3/k3_mla_paged_attn.cu), not TileLang:
the design goals are a runtime-length block-table walk and no O(ctx)
storage (3-sweep recomputed softmax), and TileLang 0.1.12 expresses
neither without a static capacity — which is the thing being removed.
The certified rounding chain is preserved landing for landing and
documented step-by-step in the kernel header: q absorption lands bf16;
f32 score dot over 576 ascending; dot lands bf16 and multiplies the
bf16 scale in bf16; f32 fixed-order max and sum(exp); probabilities
land bf16 after normalizing; f32 latent accumulation over ascending t
landing bf16; f32 W_UV expansion landing bf16. Physical page ids never
enter the arithmetic and a -1 table entry reads as zero latent, so page
permutation is bitwise invisible and padding rows behave exactly like
the old zeroed cache.

The mla_attn_batched TileLang family is deleted in lockstep: vendored
factory, generator plan + MAX_CTX list, FFI decl, ops wrapper +
K3_MAX_CTX, build.rs stub launcher, KERNELS.md and kernels/README rows.
Eleven batched families remain, none of them attention.

Gates (224-expert checkpoint, PEGAINFER_K3_LAYERS via fixture, GB300):

- Absorbed-vs-expanded certification (tests/paged_kv.rs logit dump on
  the M1 revision vs this one, same forced feed): argmax identical on
  all 40 steps; deviation over the fixture-published top-5 logits
  median 1.0 / max 3.0 bf16 ULP — inside the measured <=2-ULP
  structural noise floor; whole-row max deviation at the top-logit
  magnitude median 1.75 / max 4.0 ULP. Bit equality is not expected:
  the absorbed kernel associates the score dot as (W_UK^T q)^T c
  rather than q^T (W_UK c).
- golden_decode: all 8 gates green; 39/40 exact under the fused kernel
  (miss only at step 11, a 0.00-ULP coin flip), 38/40 under the masked
  chain (steps 25 and 33, the documented coin flips).
- Paged-vs-permuted (tests/paged_kv.rs): 200 steps across 4 pages,
  scrambled free list claims disjoint pages in opposite order — every
  logit of every step bit-identical.
- Long context (tests/paged_kv.rs): 1100 steps at max_ctx 2048, finite
  logits throughout; eager rerun and graphs-vs-eager both bit-identical
  (tokens every step, logits every 128th and the last).
- ep_mega_oracle (4 GPUs): EP4 rank 0 vs single-rank mega bitwise (all
  40 tokens, all 163840 final logits), peer-traffic invariance bitwise.
- cargo test -p pegainfer-k3 --lib: 34 passed. fmt + clippy clean.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fc551f894c

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +337 to +338
/// The paged MLA latent cache all MLA layers share.
pub(crate) kv: K3PagedKv,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Report usage of the new KV page pool

When K3 serves requests, this newly owned pool allocates and releases pages, but K3Scheduler::load in pegainfer-k3/src/scheduler/mod.rs still hard-codes kv_used_blocks: 0, while model_line.rs supplies kv_capacity: None. Consequently the load snapshot and Prometheus/load-aware consumers always see zero K3 KV capacity and usage even as this pool fills; expose the pool's total and allocated page counts through the scheduler.

Useful? React with 👍 / 👎.

Pure code motion: the paged latent cache (struct, page allocator, block
tables, append/adopt paths) and its two geometry constants move from
executor/buffers.rs to executor/paged_kv.rs; copy_rows stays in buffers
as pub(super). No behavior change.

Signed-off-by: xiaguan <751080330@qq.com>
@xiaguan
xiaguan merged commit e9b0bb9 into main Aug 14, 2026
13 checks passed
@xiaguan
xiaguan deleted the feat/k3-paged-kv branch August 14, 2026 07:13
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