Skip to content

[draft] feat(rdna2): LeapQuant config flag, validator, numerics tests and opt-in page packing (follow-up to #168) - #170

Draft
BlivionIaG wants to merge 13 commits into
rdna_extrasfrom
feat/gdn-leapquant-pack-layers
Draft

BlivionIaG wants to merge 13 commits into
rdna_extrasfrom
feat/gdn-leapquant-pack-layers

Conversation

@BlivionIaG

@BlivionIaG BlivionIaG commented Nov 14, 2026 •

Copy link
Copy Markdown

Summary

Draft. Follow-up to #168 (merged). Nothing here has run on a GPU, and the HIP change is not built.

Four parts, each its own commits:

  1. Config flag. --replayssm-state-dtype {auto,leapquant} (CacheConfig.replayssm_state_dtype) replaces the env variable VLLM_RDNA_GDN_LEAPQUANT. There is no alias. The launcher key is REPLAYSSM_STATE_DTYPE, and the launcher exits if the old variable is still set.
  2. One validator and a state format id. verify_leapquant_gdn rejects every unsupported setup at config time, per layer and at connector creation. MambaSpec.state_format identifies the format.
  3. Numerics. A fold guard for exactly low-rank and huge-scale states (oracle and the HIP mirror), plus GPU tests for determinism, stale-state isolation and the flush race.
  4. Page packing (opt-in, VLLM_RDNA_GDN_LEAPQUANT_PACK=1). It gains nothing at the shipped 512-token recipe.

Flag off, the KV-cache config and the config hashes are unchanged (observed on CPU).

flowchart LR
    F["--replayssm-state-dtype leapquant"] --> V["verify_leapquant_gdn<br/>config / layer / connector"]
    V -->|ok| S["MambaSpec.state_format<br/>leapquant-int8/v1/..."]
    V -->|unsupported| X["stop at start-up<br/>names flag and cause"]
    S --> P["planner: pack GDN layers<br/>(only with the PACK switch)"]
Loading

1-2. Flag, validator, format id

  • Rejected: non-gfx10x, head dims other than 128, stochastic rounding, --use-replayssm, any KV connector or offloading, V1 runner with align mode, AITER GDN, FlashInfer GDN, fused CUDA spec decode, models without GDN.
  • Still supported: MTP and --monokernel.
  • Deviations from RFC vllm#64725: --use-replayssm is rejected, because Qwen GDN models do not declare ReplaySSM support. The window is fixed at 32, and --replayssm-buffer-len is not read. All connectors are rejected, stricter than the RFC.
  • Format id: MambaSpec.state_format (for example leapquant-int8/v1/k128v128/r4/p32/cap34/rec-float16) is part of the spec key. There is no marker in the blob, because the kernels are not rebuilt here.
  • Changed with the flag off: one new --help option, one new vllm:cache_config_info label (replayssm_state_dtype), MambaSpec gains state_format (default None).
  • Details: docs/rdna2/leapquant-config-guards-2026-11-14.md.

3. Numerics

  • The old fold did not give NaN on low-rank states. It lost accuracy: relative error 1.6e-3 to 2.4e-3 on rank-1 states, against 1.2e-6 after the fix. NaN appeared only near 1e37 and above, for any rank. Both are fixed in the oracle and mirrored in gdn_leapquant_rdna2.cu.
  • The fold change alters compensators for rank-deficient heads, so feat(rdna2): add LeapQuant GDN state with 512-token pages #168's quality checks must be re-run on GPU before this part ships.
  • CPU: 139 oracle and benchmark tests (the new oracle file against the old oracle: 50 failed, 72 passed). 68 GPU tests are written and skip cleanly without a GPU.
  • Stale-state tests found no stale read that reaches an output. Two header-trust spots are noted in the doc and left unchanged.
  • benchmarks/replayssm/e2e_decode_speedup.py gains a LeapQuant arm. The 16-bit per-window baseline arm is not done, because it needs the GDN ReplaySSM kernel from vllm#62004.
  • Details: docs/rdna2/leapquant-gdn-numerics-and-isolation-2026-11-14.md.

4. Page packing

Layers of one KV-cache group already share a block, and all groups draw from one block-id pool. Capacity depends on bytes per block and on how many groups a request holds. Today 3 GDN groups of 12 come from padding the GDN page to the attention page. With the switch, the planner skips the padding and uses cdiv(layers, layers per block) groups.

CPU planner, Flash-Next TP4, MTP2 (capacity tokens at max_model_len 262,144):

Block padded (#168) packed GDN groups packed
512 273,456 273,456 3 x 12
608 270,824 273,846 2 x 18
640 270,050 273,220 2 x 18

At max context packing moves capacity by 0.14% or less. Merging the PLE group into a GDN group would add more (+21% to +41% at 1k-4k tokens in the planner what-if, not built). Details: docs/rdna2/leapquant-page-packing-2026-11-14.md.

Verification

  • CPU on the box: 250 passed, 1 failed, 1 skipped over the 8 test files of this PR. The failure is test_leapquant_config.py::test_cli_flag_parses_and_restricts_its_values: it calls EngineArgs.from_cli_args with --model dummy, which needs an HF download and the box runs offline. It was not run on the config-guards branch alone.
  • GPU test files (test_gdn_leapquant_rdna2.py, test_gdn_leapquant_safety_rdna2.py) skip without a GPU.
  • Known failures that predate this work: test_mamba_align_chunk_split.py::test_flash_next_tp4_materializes_replay_state[3200|3602].

Pending GPU validation

  • Boot flashnext-leapquant-mtp2 with the flag: KV capacity 273,456 tokens, group sizes (512,512,512,512,8,512), greedy, prefix-cache and concurrency probes; compare with feat(rdna2): add LeapQuant GDN state with 512-token pages #168's numbers
  • Build _rocm_C with the fold change; run the kernel tests (22 existing, 46 new) on gfx1030
  • Re-run feat(rdna2): add LeapQuant GDN state with 512-token pages #168's quality checks (PPL, decode-KL) with the fold guard
  • Determinism (200 repeats under load), stale-state and flush-race tests
  • Validator inside real workers, including MTP drafter layers and MonoKernel; V2 align copy marker
  • FULL_AND_PIECEWISE, MTP2 and --monokernel unchanged with the flag
  • Rejection at boot for a connector or offloading
  • Page packing: boot at block 640 (5 groups, expected 273,220 tokens), packed-page kernels, graph replay, serving matrix versus feat(rdna2): add LeapQuant GDN state with 512-token pages #168
  • The image build after merge is green

Credits

LeapQuant (arXiv 2609.38166), ReplaySSM (vllm#48018, vllm-project#62004) and the RFC vllm#64725 (lihaoyang-amd) are credited in file headers and docs. The flag name and its place next to --replayssm-buffer-len follow draft vllm#52962 (askliar). No outside code was copied; the validator, layout id, fold guard, tests and planner change are original to this fork.

…t-in)

VLLM_RDNA_GDN_LEAPQUANT_PACK=1 (needs VLLM_RDNA_GDN_LEAPQUANT=1, default off)
keeps GDN and PLE pages at their natural size and lets the KV-cache planner use
the fewest groups whose layers fit one block, instead of 3 groups of 12.

In this allocator the unit of capacity is the KV-cache group, not the page.
Layers of a group already sit side by side in one block and every group draws
block ids from one pool. Padding GDN pages to the attention page caps a block
at 13 pages, and the repeat-pattern split makes 3 groups of 12 for any page
size, so a smaller LeapQuant page frees nothing. The flag removes both limits.

- leapquant_gdn_pack_enabled(): the flag; raises if LeapQuant is off.
- Platform._align_hybrid_block_size: skip the Mamba page padding under the flag.
- _get_packed_kv_cache_groups: Mamba buckets use cdiv(layers, layers per block)
  groups under the flag and log the split. Flag off is unchanged.
- serve_rdna.sh: list the flag in the PRINT=1 environment plan.

Flash-Next TP4 MTP2 from the real planner (CPU): the flag is a no-op at the
512-token recipe (15 layers fit a block, 3 groups stay). From 598 tokens up
(608, 640, 1024) 36 GDN layers fit 2 groups of 18, so one request holds 10 GDN
block ids instead of 15. Capacity at 262,144 tokens stays within 0.15 % of the
512-token recipe (608: 273,846, 640: 273,220, against 273,456); 1k contexts gain
+7.6 % (608) and +2.0 % (640). Tables and byte math:
docs/rdna2/leapquant-page-packing-2026-11-14.md.

Tests: 29 CPU tests (tests/v1/core/test_leapquant_page_packing.py) run the real
planner and allocate_kv_cache without a GPU: flag-off layout identical to the
base branch, group counts, capacity-log math, per-layer view offsets, strides
and 16-byte alignment, grouped normalize offsets. Two GPU kernel tests for
packed pages are added and have not run. Nothing here ran on a GPU.

Signed-off-by: BlivionIaG <kev29lt@gmail.com>
The one-layer PLE bucket logged "1 Mamba layers". Use "layer(s)".

Signed-off-by: BlivionIaG <kev29lt@gmail.com>
Chosen option (fit GDN layers into a block by bytes), rejected options, byte
math, the capacity table from the real planner, a what-if for merging the PLE
layer into a GDN group, risks and the pending GPU checklist. Credits mirror the
first LeapQuant note.

Signed-off-by: BlivionIaG <kev29lt@gmail.com>
…rate states

The LeapQuant RFC (vllm-project#64725, lihaoyang-amd, sections 2.2 and 5)
reports NaN in the fold for exactly low-rank states and asks for a max-abs
scaled, zero-guarded normalization plus regression tests. The RFC has no code.

CPU evidence on the oracle of PR #168 (7b4aff7): it never gives NaN for a
low-rank state. It loses accuracy instead: a rounding-noise compensator column
above the absolute 1e-12 limit is normalized, stays half parallel to the
earlier columns, and the INT8 residual carries a state-sized error (relative
error 1.6e-3 to 2.4e-3 against 1.2e-6 after this change for rank 1; the
outcome also depended on the state scale). It gives NaN only for states of
about 1e37 and more, of any rank: the sum of |R| for the smoothing factor
overflows, and z * bscale * csmooth becomes 0 * inf.

Oracle (leapquant_gdn.py):
- _orthonormalize scales each column by its max |entry|, sweeps twice (Kahan's
  "twice is enough", Parlett 1980) and zeroes a column when the second sweep
  removed more than half of the largest entry (dependence test of Daniel,
  Gragg, Kaufman and Stewart, Math. Comp. 30, 1976). Every column is zero or
  unit.
- A dead column of the previous comp_k restarts from its fixed integer column.
- The smoothing factor divides |R| by V before the sum (bit-identical to
  mean() for V a power of two).
- Header words outside the lq_header_ok range read as an empty ring, like the
  kernels (the old oracle raised IndexError or AssertionError there).

HIP fold (gdn_leapquant_rdna2.cu): the same logic. NOT BUILT, NOT RUN, no HIP
compiler was available; it is marked as pending GPU validation.

Tests: tests/kernels/mamba/test_gdn_leapquant_oracle.py, 122 CPU tests (92 for
the fold, 30 for stale state of recycled blocks). Against the old oracle 50 of
them fail (4 NaN, 35 accuracy or orthogonality, 11 invalid headers).

Signed-off-by: BlivionIaG <kev29lt@gmail.com>
…ing GPU validation)

Tests for the safety list of the LeapQuant RFC (vllm-project#64725,
lihaoyang-amd, sections 2.2, 4 item 11 and 5). The RFC has no code; the tests
are original work for this fork. They skip without a gfx103x GPU.

STATUS: written without GPU access, never run on a GPU. 46 tests:
- fold of zero, tiny, huge, exactly low-rank and float-limit states: finite,
  accurate, orthonormal compensators; a degenerate head leaves its neighbours
  bitwise unchanged; a rank-1 trajectory through window flushes
- determinism: store, normalize, replay and decode (and the MonoKernel path),
  200 repeats under a second-stream matmul load, bitwise equal outputs, states
  and blobs; the output buffers carry a different poison in each repeat
- stale state of recycled blocks: random bytes, a previous tenant, the same with
  NaN, a valid-looking header; with and without the normalize and materialize
  pass of the serving prefill path; MonoKernel on a zero block and after a
  store; ten headers outside the range check; rows past the cursor; the align
  pre-copy overwriting every byte of a dirty destination
- flush race: full windows on many (block, head) pairs with MTP accepted
  counts, one grouped normalize over six layers against per-layer launches,
  bitwise, repeated, with and without load; batched decode against singleton
  launches

Only the first two header words [cursor, pending] are compared.

Signed-off-by: BlivionIaG <kev29lt@gmail.com>
…enchmark

Item 13 of section 4 of the LeapQuant RFC (vllm-project#64725,
lihaoyang-amd) asks to extend benchmarks/replayssm/e2e_decode_speedup.py. The
script is from vllm-project#48018 (Johnny-Liou); its header keeps that
credit and records this change.

- new arm `leapquant`: sets VLLM_RDNA_GDN_LEAPQUANT=1 in its own worker
  process, imports leapquant_gdn only there (gfx10x, fails loudly elsewhere);
  every other arm refuses to start when the flag is set in the environment
- `--modes` (default `standard,replayssm`, so the old behaviour is unchanged;
  the first mode is the baseline of the speedup lines), `--num-speculative-tokens`
  (MTP), `--monokernel`, `--tensor-parallel-size`, repeatable `--llm-kwarg
  KEY=JSON` and `--env KEY=VALUE`, `--dry-run` (imports neither torch nor vllm)
- the FP32 baseline against LeapQuant on the RDNA path is `--modes
  standard,leapquant --mamba-ssm-cache-dtype float32`
- NOT done: the 16-bit per-window baseline of the RFC. It needs the Gated
  DeltaNet ReplaySSM decode kernel (vllm-project#62004), which this tree
  does not have; use_replayssm is Mamba2 only. The docstring says so.

tests/benchmarks/test_replayssm_e2e_decode_speedup.py: 17 CPU tests pin the
arguments, the arm and environment rules, the unchanged default arms, the
worker commands, --help and --dry-run without torch. No benchmark was run.

Signed-off-by: BlivionIaG <kev29lt@gmail.com>
…s [skip ci]

One section per item of the safety list of the LeapQuant RFC (vllm-project/vllm
vllm-project#64725, lihaoyang-amd): what changed, what was verified on the CPU, what is
pending GPU validation.

- NaN question answered with CPU evidence: the oracle of PR #168 never gives NaN
  for low-rank states (it loses accuracy: 1.6e-3 to 2.4e-3 against 1.2e-6 for
  rank 1) and gives NaN only for states of about 1e37 and more (the smoothing
  sum overflows), for any rank. Old-vs-new tables, the design that failed, the
  host emulation of lq_orthonormalize (g++, not the HIP compiler).
- Determinism: status "not yet run".
- Stale state of recycled blocks: what the serving prefill path does with the
  leftovers, findings (no stale read at the contract level; two header cases
  that the kernels cannot check), tests.
- Flush race: review by construction with line numbers for normalize, decode,
  the MonoKernel and the preconditions; the one disagreement case.
- Benchmark arm, commands, the 16-bit baseline NOT done and why.
- Pending GPU checklist and not-done list.

Credits the RFC for the item list, Kahan/Parlett and Daniel-Gragg-Kaufman-Stewart
for the two textbook results, and vllm-project#48018 (Johnny-Liou) for the
benchmark. No external code was used.

Signed-off-by: BlivionIaG <kev29lt@gmail.com>
The new GPU tests never ran on a GPU, so a failure is either a kernel bug or a
test bug.

Signed-off-by: BlivionIaG <kev29lt@gmail.com>
…te it in one place

Replace the VLLM_RDNA_GDN_LEAPQUANT environment variable with
--replayssm-state-dtype {auto,leapquant} (CacheConfig.replayssm_state_dtype).
There is no alias: the engine does not read the variable any more.

- verify_leapquant_gdn is the one place that refuses an unsupported setup. It
  runs at config time (VllmConfig) and for every LeapQuant GDN layer. The
  refusals that were spread over the layer code move into it with the same
  meaning. New config-time refusals: --use-replayssm, stochastic rounding, KV
  connectors, KV offloading, head dimensions other than 128, and models
  without GDN layers. MTP and --monokernel stay supported.
- MambaSpec carries a state format identity (format, layout version, head
  dimensions, rank, window, ring capacity, record dtype) from
  LeapQuantLayout.state_format. FP32 and LeapQuant state never share a KV
  cache group, and the V2 align copy marker follows the spec.
- KVConnectorFactory.create_connector refuses a KV cache config that holds
  LeapQuant state.
- The scheduler growth gate, the max-memory estimate, and the GDN state
  calculators read the flag. Flag-off hashes, dtypes, shapes, page sizes and
  launcher plans equal the first PR head (CPU runs, see the receipts).
- The launcher takes REPLAYSSM_STATE_DTYPE and passes the flag after
  --block-size. It stops when VLLM_RDNA_GDN_LEAPQUANT=1 is still set, because
  the engine would ignore it and serve FP32 state. The
  flashnext-leapquant-mtp2 plan equals the old one except for the flag
  spelling.

Credits: the list of checks comes from RFC vllm-project#64725
(lihaoyang-amd); the flag name and its place next to --replayssm-buffer-len
come from draft PR vllm-project#52962 (askliar). No code was taken from
either. Nothing in this change ran on a GPU.

Signed-off-by: BlivionIaG <kev29lt@gmail.com>
…eipts [skip ci]

New leapquant-config-guards doc: the flag table, the rejected setups, why
--use-replayssm is rejected, why the window stays 32, the state format
identity, the connector refusal, flag-off identity, the launcher dry run, the
tests, the checks that still need a GPU, and credits (RFC vllm#64725,
vllm#52962, vllm#48018, vllm#62004, arXiv 2609.38166). The first LeapQuant
doc names the new flag and says that its receipts used the environment
variable.

bench_results/2026-11-14_leapquant-config-guards holds the dry-run plans
before and after and the CPU receipts.

Signed-off-by: BlivionIaG <kev29lt@gmail.com>
Signed-off-by: BlivionIaG <kev29lt@gmail.com>
…-packing branch

# Conflicts:
#	tools/rdna/serve_rdna.sh
#	vllm/model_executor/layers/mamba/ops/leapquant_gdn.py

Signed-off-by: BlivionIaG <kev29lt@gmail.com>
@BlivionIaG BlivionIaG changed the title [draft] feat(rdna2): pack LeapQuant GDN layers into fewer KV-cache groups (stacked on #168) [draft] feat(rdna2): LeapQuant config flag, validator, numerics tests and opt-in page packing (follow-up to #168) Nov 14, 2026
@BlivionIaG
BlivionIaG changed the base branch from feat/gdn-leapquant to rdna_extras November 14, 2026 22:36
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