Skip to content

feat(kv): rk2v4-e8 compressed-KV (E8-root, 208 B/head-token) on the paged-KV engine - #173

Open
danielfparkernz wants to merge 7 commits into
Neroued:devfrom
danielfparkernz:feat/rk-compressed-kv-on-6e2786c5
Open

feat(kv): rk2v4-e8 compressed-KV (E8-root, 208 B/head-token) on the paged-KV engine#173
danielfparkernz wants to merge 7 commits into
Neroued:devfrom
danielfparkernz:feat/rk-compressed-kv-on-6e2786c5

Conversation

@danielfparkernz

Copy link
Copy Markdown

What

Re-port of the rk* compressed-KV storage format — specifically rk2v4-e8 (E8-root Conway-Sloane lattice Key codes, 2-bit Key / 4-bit Value, 208 B per head-token) — onto the current paged-KV dev engine, as a first-class KvCacheStorage profile selectable via --kv-dtype rk2v4-e8.

This is the successor to PR #35, which was written against the pre-paged-KV gqa_attention_* kernels and closed when the engine moved to the KVPageGeometry / causal_softmax_attention plane model. This port re-derives the same density win on the current kernel architecture, using the per-format kernel-file pattern (mirroring the K8V4 family) rather than the old KvCacheCodec template fold.

Why it matters on 24 GB cards

NInfer targets a single RTX 5090. The 24 GB laptop 5090 (sm_120a, 95 W) is the class where KV-cache headroom is binding, because dense 27B models leave only ~3.4 GiB free after weights. The existing compressed-KV profile, k8v4 (402 B/head-token), is coarser than rk2v4-e8 (208 B/head-token, 1.93× denser). All figures below were measured on a 24 GB RTX 5090 Laptop, CUDA 13.1.80, sm_120a — not projections.

Boot: KV headroom at 256K (Qwen3.6-35B-A3B, 19.6 GiB weights)

profile --kv-capacity 262144 @ 256K
k8v4 FATALs in auto mode; explicit mode boots with 574.6 MiB free
rk2v4-e8 boots with 1.56 GiB free (2.7× the margin)

Qwen3.8-27B NVFP4 (19.0 GiB weights) — 256K does not fit either

k8v4-class KV wants ~3.64 GiB at 256K but only 3.39 GiB is free after weights, so a dense 27B NVFP4 cannot reach 256K at k8v4's density. rk2v4-e8's 208 B/head-token brings the 256K reservation down enough to boot at 237,568 tokens, and to run 160K with MTP speculative decoding (--spec mtp --draft-tokens 3 --lm-head-draft), whose draft head needs ~1 GiB.

Fidelity (needle-in-haystack, case-insensitive, 3 needles at 10/50/90% depth)

result
E8 codec oracle (tools/test_kv, 1M tokens) 5/5 needle retrieval, exact
rk2v4-e8 @ A3B, 100K / 150K / 200K 3/3 each, no OOM
rk2v4-e8 @ Qwen3.8-27B groupwise, 219K 3/3
decode regression none — 21.8 tok/s baseline matches the groupwise reference; 47.1 tok/s @100k with MTP

How

  • include/ninfer/types.h: add KvCacheStorage::Rk2v4E8.
  • src/core/paged_kv_storage.h paged_kv_storage_layout(): Rk2v4E8 geometry — K = {I8, 64, FP16, 4}, V = {U8, 128, FP16, 4} = 208 B/head/token (head_dim 256). plan_cache() derives the 4 planes automatically.
  • src/ops/kernel/{e8_lattice.cuh,e8_root_codec.cuh}: self-contained E8 Conway-Sloane codec cores (port from the 4090 fork; oracle-verified).
  • src/ops/kv_cache/{rk2v4e8_codec.cuh,append/rk2v4e8_kernel.cuh,append/rk2v4e8_launch.cu}: codec + append kernel family (mirrors k8v4).
  • src/ops/softmax_attention/dense/causal_cache/{small_t_rk2v4e8,prompt_rk2v4e8}.{cuh,cu}: split-KV small-T and causal prompt attention families (mirror k8v4).
  • Dispatch: KvCacheStorage::Rk2v4E8 branches wherever Fp8KeyNvfp4Value (k8v4) is routed — append, small-T (both cached and uncached), prompt, fp32_acc list, split-capacity bucket.
  • src/serve/serve_options.cpp: parse --kv-dtype rk2v4-e8; capacity/request logs name the profile.
  • tools/test_kv/: standalone oracle + 1M retrieval verifier (raw-nvcc buildable, no engine deps).

The V inverse-rotation is required (the PV output lands in rotated-V space); gated on cache.storage == Rk2v4E8 at the output launch.

Verified on Beast (RTX 5090 Laptop, 24 GB, sm_120a, CUDA 13.1.80)

  • Rebased onto origin/dev a140e7ae — clean rebase, 6 commits, ~4K insertions, build 321/321 exit 0.
  • A3B 256K rk2v4-e8: runtime 1.20 GiB | free 1.59 GiB, needle 3/3 @100k.
  • Qwen3.8-27B (groupwise + NVFP4): 256K-class context + MTP as above.

Test plan

  1. cmake --build build --target ninfer_kv_e8_verify && ./build/tools/test_kv/ninfer_kv_e8_verify 1000000 → 5/5.
  2. Boot a 27B or A3B at --kv-dtype rk2v4-e8 --max-context 262144 --kv-capacity 262144; confirm the capacity log line names rk2v4-e8 and reports the expected pages/runtime.
  3. Needle-in-haystack at 100K (harness in this PR's description / tools/test_kv/) → 3/3.

Supersedes PR #35 (which targeted the pre-paged-KV engine).

Daniel Parker and others added 6 commits September 4, 2026 13:37
…-KV base

Add KvCacheStorage::Rk2v4E8 (E8-root cylinder K: I8/64 + FP16/4 scale, V: U8/128
packed i4 + FP16/4 scale = 208 B/head/token) to the closed-profile layout
resolver, serve --kv-dtype rk2v4-e8, and drop the self-contained E8 lattice/codec
kernels + codec oracle from the original port (branch feat/rk-compressed-kv-on-paged).
Adds the rk2v4-e8 persistent codec on top of the paged-KV geometry registered in
d465fd63: K takes the fixed normalized D256 Hadamard rotation and is E8-root
("cylinder") encoded into a quarter-width 64-byte plane (one code pair per 8
dims); V takes a per-64-group Hadamard rotation and is packed to int4 at half
width. Both planes carry a per-64-group FP16 scale (208 B/token/head).

The single-row encoder is shared between the standalone append kernel and the
fused append inside the attention kernels so both paths emit identical codes and
each of the four K and four V group scales is written exactly once. The E8
encoder is warp-collective over 8-lane subgroups, so a warp owns a complete D256
row and every lane reaches every encode call.

Routes both the direct and the batch append entry points on KvCacheStorage::Rk2v4E8.
QK keeps the G64 INT8 m16n8k32 contraction: the E8-root code pairs expand back to
eight int8 codes that share the per-64-group scale, so the int32 MMA output is
still rescaled by qs[row,g]*ks[key,g]. V is unpacked from int4 to full-width int8
and dequantized to the existing bf16 PV tile. Neither plane can use the int8
kernel's 16-byte cp_async (both are narrower than the int8 cache and a 16-byte
copy would run off the end of the plane), so the code tiles are expanded
synchronously into the staging arena.

PV accumulates in the rotated-V basis. The family owns its own cross-split reduce
kernel, which keeps FP32 partial numerators and re-applies the per-64-group
Hadamard - its own inverse - before narrowing to bf16, so the output leaves in
original V coordinates. Rk2v4E8 therefore joins the fp32 partial-accumulator list.
Mirrors the INT8 prompt kernel with the compressed planes expanded into the same
full-width staging tiles, so the QK/PV schedule, shared-memory arena and warp
split are unchanged. The prompt kernel writes `out` directly, so the inverse
per-64-group rotation runs as a separate pass over the finished tile once the
attention kernel returns.

Routes both the fill+attend and the attend-only prompt entry points.
Both switches fell through to "unknown", so the capacity line reported the
storage as unknown for --kv-dtype rk2v4-e8.

@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: fe62250575

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

if (value == "fp8") { return KvCacheStorage::Fp8E4M3Row256; }
if (value == "nvfp4") { return KvCacheStorage::Nvfp4Group16; }
if (value == "k8v4") { return KvCacheStorage::Fp8KeyNvfp4Value; }
if (value == "rk2v4-e8") { return KvCacheStorage::Rk2v4E8; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Wire RK2V4E8 through every product parser

When users select this profile outside ninfer-serve, the new mode is unusable: apps/cli/options.cpp:55-61, apps/perplexity/main.cpp:104-117, and bench/targets/qwen3_6_27b/ninfer_bench_support.cpp:51-57 still reject rk2v4-e8. This leaves the required CLI, perplexity, and measurement Engine routes unable to exercise the supposedly first-class storage mode, so their parsers, names, help text, and relevant tests need the same addition.

AGENTS.md reference: AGENTS.md:L111-L113

Useful? React with 👍 / 👎.

Comment on lines +25 to +27
add_executable(ninfer_kv_e8_verify
test_e8_codec.cu
verify_1m_retrieval.cu)

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 Test the production codec rather than separate formats

Whenever the production RK2V4E8 codec or attention path is wrong, this verifier can still pass because the target builds only the self-contained implementations in tools/test_kv: they use unrelated E8Packed2BitTile/E8Packed4BitTile layouts and never include or invoke src/ops/kv_cache/rk2v4e8_codec.cuh, the append kernels, or either attention kernel. Consequently the advertised 5/5 result checks neither the 208-byte production representation nor its V rotation, paging, or final output; compare the actual production route directly against an independent oracle.

AGENTS.md reference: AGENTS.md:L244-L257

Useful? React with 👍 / 👎.

Comment thread tools/test_kv/CMakeLists.txt Outdated
Comment on lines +7 to +9
# 1. From the NInfer top level with CUDA already configured
# cmake -S . -B build -GNinja -DCMAKE_CUDA_ARCHITECTURES=120a
# cmake --build build --target ninfer_kv_e8_verify

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 Register the documented verification target

Following this newly documented top-level build command cannot work: the repository's root CMakeLists.txt never calls add_subdirectory(tools/test_kv), so a normal top-level configuration has no ninfer_kv_e8_verify target or CTest entry. The same file acknowledges that omission on lines 10-12, while tools/test_kv/README.md repeats the invalid command; either register the target under the appropriate build option or document the standalone source directory instead.

AGENTS.md reference: AGENTS.md:L286-L295

Useful? React with 👍 / 👎.

Address Codex P1 on Neroued#173: the --kv-dtype rk2v4-e8 profile was only parsed
in ninfer-serve. apps/cli/options.cpp, apps/perplexity/main.cpp, and
bench/.../ninfer_bench_support.cpp rejected it, leaving the required CLI,
perplexity, and measurement Engine routes unable to select the mode. Add
the parse, the enum->name mapping, the usage text, and the error string to
each, mirroring the serve parser (canonical name "rk2v4-e8").

Address P2 (register the verification target): wire tools/test_kv into the
root CMakeLists under BUILD_TESTING so the documented top-level build and
`ctest -R ninfer_kv_e8_verify` actually work, and correct the CMakeLists/README
to state the oracle's true scope — it is an independent reimplementation of
the E8 Conway-Sloane math, not a test of the production 208 B layout /
V-rotation / paging (which the end-to-end boot + needle verification covers).

Tests: rk2v4-e8 parse + report-name assertions added to test_cli_options and
test_ninfer_bench_support. Verified: both pass, perplexity binary accepts
--kv-dtype rk2v4-e8 (advances past dtype check), CLI/bench help list it, and
ninfer_kv_e8_verify now builds + runs as a registered target.
@danielfparkernz

Copy link
Copy Markdown
Author

Addressed all three in daf60f36:

P1 — wire rk2v4-e8 through every product parser (fixed). The profile was only parsed in ninfer-serve; apps/cli/options.cpp, apps/perplexity/main.cpp, and bench/.../ninfer_bench_support.cpp rejected it. Added the parse, the enum→name mapping, the usage text, and the error string to each, mirroring the serve parser (canonical name rk2v4-e8). Added rk2v4-e8 parse + report-name assertions to test_cli_options and test_ninfer_bench_support; verified both pass, the perplexity binary now accepts --kv-dtype rk2v4-e8 (advances past the dtype check to the corpus error), and CLI/bench --help list the profile.

P2 — register the documented verification target (fixed). tools/test_kv is now wired into the root CMakeLists.txt under BUILD_TESTING, so the documented top-level build and ctest -R ninfer_kv_e8_verify work. ninfer_kv_e8_verify now builds and runs as a real target (1M-token needle retrieval passes 5/5).

P2 — "tests separate formats, not the production codec" (scoping corrected, not a code change). You're right that the oracle does not exercise the production route: test_kv/test_e8_codec.cuh is a deliberately self-contained reimplementation of the E8 Conway-Sloane lattice / 240-root math — it does not #include src/ops/kernel/e8_*.cuh. So the 5/5 validates the mathematics, not the production rk2v4e8_codec.cuh 208 B plane layout, the V Hadamard rotation/inverse, or paged addressing. I corrected the CMakeLists.txt / README.md to state that scope explicitly (and why the oracle is kept independent: a self-including test would be circular). The production route — 208 B representation, V rotation, paging, and final attention output — is validated by the end-to-end boot + needle-in-haystack verification recorded in PORT-RK2V4E8.md. Happy to add a direct production-codec unit test (e.g. encode a known K/V plane, compare against a reference reconstruction) if you'd like that folded in too — flagging that as the one genuinely useful follow-up here.

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