Skip to content

[Experiment][SM70] Realign the QSA E4M3 + MTP lane on 1CatAI main - #19

Draft
Leonccaa wants to merge 8 commits into
mainfrom
feat/qsa-e4m3-mtp-realign-20260919
Draft

Leonccaa wants to merge 8 commits into
mainfrom
feat/qsa-e4m3-mtp-realign-20260919

Conversation

@Leonccaa

Copy link
Copy Markdown
Owner

Why this replaces the previous E4M3 lane

PRs #16, #17 and #18 grew into a single stack that mixed the E4M3 work with
the CPU KV offload lane (1CatAI#598, 26 commits), 1CatAI#637, 1CatAI#647 and an unmerged
experimental branch. That stack qualified, was promoted, and was rolled back
the same day. Untangling it turned out to be cheaper than continuing it,
because the E4M3 delta is far smaller than the stack suggested.

Against 1CatAI main (b711d53045), the whole QSA E4M3 main KV path is
already present. What was actually missing is four things, and this branch is
exactly those four plus their two prerequisites:

28697df2c7, c0c6a3fd87 1CatAI#647 (areslp). E4M3 prerequisite: grouped-page4 padding must not leak the null block's E4M3 NaN.
9e8ea99b61 1CatAI#637. MTP correctness: a K+1-token prompt tail must not replay a decode graph on shape match alone.
e1aa0103ff areslp's phase-2/2b E4M3 QSA main KV with MTP, gated on VLLM_QWEN4EXP_QSA_E4M3_MTP.
fdebb3e0a6 Require calibrated E4M3 MTP draft scales; no silent unit-scale fallback for the drafter.
87af57de30 The 1CatAI#647 overlay kept the stricter native E4M3 ABI but lost the Python workspace dtype split. Key the XQA page4 workspace by KV dtype and allocate FP32 temporary output for E4M3.
004db582d6 Repair a test that claimed to enforce the 24/24 scale gate but never exercised it.

The native tree is byte-identical to the already-qualified SM70 build, so the
compiled binaries are reused and only the Python layer is repacked.

This branch deliberately does not carry 1CatAI#598. CPU KV offload stays its own
lane; the separate H2D restore-ordering fix went into 1CatAI#598 directly.

Relationship to upstream vllm-project

vllm-project/vllm has merged its own QSA FP8 work: #54890 (FP8 indexer cache,
2026-09-07) and #55557 (fp8_e4m3 main KV on the QSA path, 2026-09-16). Neither
is in 1CatAI main yet.

They are not duplicates of this branch. Upstream's qsa_sparse_paged_attention
FP8 support targets the Triton split-K kernel, asserts q.dtype == torch.bfloat16, and adds an sm_120 tuning table. The SM70 deployment here runs
FP16 activations through the native page4/XQA routes. The two live in the same
files but on different code paths.

The contract is the same on both sides, which is what matters for the eventual
rebase: per-tensor host-side k_scale/v_scale floats, the K scale folded
into the attention scale, the V scale applied as an output scale, and an
E4M3 cache stored as uint8 and viewed as float8_e4m3fn. No device-side scale
buffers on either path.

Known constraint: draft_sample_method

draft_sample_method=probabilistic with MTP corrupts output at
max_num_seqs=4 and any temperature above 0. This is not introduced here and
is not E4M3-related: it reproduces on the current production image
(1.5.0+ct252.20260914.gf21df5b279) with FP16 KV by changing only
max_num_seqs from 2 to 4. It is temperature-gated because temperature 0 uses
the greedy argmax verify path, while temperature > 0 goes through the
probabilistic rejection-sampling ratio test.

greedy is upstream's default for MTP (arg_utils.py) and is unaffected at
every concurrency tested. This lane uses greedy; the probabilistic defect is
tracked separately.

Tests

CPU, this branch: tests/models/qwen4_exp/ 303 passed, 103 skipped, 0 failed.
The 168 errors in that run are teardown-only (cleanup_dist_env_and_memory
needs an accelerator) and reproduce identically on main.

pre-commit on all 19 changed files: all hooks pass, including ruff, mypy,
clang-format, SPDX, forbidden imports and the CUDA API policy.

GPU acceptance is in progress and will be posted here: quality gates
(real-text prompts, temperature sweep, >=256-token generations, same-seed A/B
against the known-good baseline), C1/C2/C4, MTP, and 1k/8k prefill and decode
throughput.

AI assistance

Claude produced the re-plan, the rebuilt stack, the test repair and this
description. Commits keep their original authors: 28697df2c7, c0c6a3fd87
and e1aa0103ff are areslp's. The human submitter must review every changed
line before merge.

areslp and others added 8 commits September 19, 2026 00:49
…3 NaN

The SM70 grouped-page4 QSA prefill route can emit NaN for every query row
and head on a fixed set of head dims when the KV cache is E4M3 and a hybrid
(GDN/Mamba) layout co-locates non-attention state in the paged KV pool. The
same batch is correct through the XQA route.

Root cause: the grouped planner pads each category to a multiple of eight
with the null block -- (physical microblock 0, mask 0) -- and counts the
padding in seq_len. The forward loads page 0's K/V for those padded rows and
masks them multiplicatively (P = 0). But 0 * NaN = NaN survives the P@V
tensor-core reduction, and under E4M3 the null block's bytes decode to NaN.
Every group's every tile reads the same page-0 microblock, so the whole
tile's rows and heads go NaN on identical dims. XQA never reads the null
block, which is why it stays clean on the same inputs.

Fix (defense in depth, both default-on):
- Kernel: in flash_attention_grouped_verify_e5m2_partial_kernel under
  SPARSE_PAGE4, after loading each K/V panel, zero every row no query
  attends (unattended iff (mask & (0x11111111u << (row & 3))) == 0) so the
  reduction sees 0 * 0 = 0. A leading __syncthreads() before the V-panel
  zeroing keeps the panel load from racing past the zero and resurrecting
  the NaN; the trailing one publishes the zeros before P@V.
- Python (VLLM_SM70_QSA_GROUPED_PAD_FIX, default 1): after the planner,
  repoint every mask==0 padding entry at the group's first real microblock.
  torch.where + copy_ has no host sync, so it stays CUDA-graph capturable.
  This backstops binaries built before the kernel fix.

The grouped route stays default-on (VLLM_SM70_QSA_GROUPED_PAGE4=1); only what
the padded rows contribute changes.

Adds tests/models/qwen4_exp/test_qsa_e4m3.py::test_qsa_grouped_page4_null_block_padding_no_nan,
an SM70-only regression that poisons the null block's value plane with E4M3
NaN and asserts the grouped route stays finite and matches XQA, parametrized
over the Python pad-fix off (kernel-only) and on. It reproduces the NaN on an
unfixed kernel and passes on either fix.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit cab8358)
…adding test

The regression added in the previous commit used the XQA page4 route as its
clean reference. On Flash-V100 builds whose XQA E4M3 page4 kernel requires an
fp32 partition buffer, that reference raises "XQA decode tmp_out must be fp32
for E4M3 KV" before the grouped route is exercised, so the test cannot run
against such a build.

Use a kernel-independent einsum ground truth instead: decode the E4M3 K/V to
fp32 and attend only the selected tokens (which map to physical pages >= 1, so
the null block is excluded). This is .so-agnostic and directly checks that the
grouped route stays finite and numerically correct. The grouped route and the
parametrized pad-fix coverage are unchanged.

Verified on V100 (SM70): the grouped null-block NaN reproduces with the stock
kernel and pad-fix off (test fails), and passes with either the kernel fix or
the Python pad-fix.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 31d137c)
Adapt vllm-project/vllm#51865 (0a94d85a66499cad8297ead86a470967de5c0212)
to the current target and Eagle/MTP draft paths. A K+1-token prompt tail
must not replay a decode graph merely because its shape matches.
Preserve dummy capture and genuine decode classification.

Co-authored-by: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>
Co-authored-by: Nick Hill <nickhill123@gmail.com>
Co-authored-by: Janelle Cai <janelle.cai@modal.com>
Assisted-by: OpenAI Codex
Signed-off-by: Leonccaa <166551845+Leonccaa@users.noreply.github.com>
(cherry picked from commit 1892fb2)
Staging branch for the separate E4M3 + MTP draft-scale enablement; NOT for
merge and no PR. Ports two work-in-progress patch sets on top of the
null-block-padding fix branch:

- phase2: opt-in gate (VLLM_QWEN4EXP_QSA_E4M3_MTP) that relaxes the QSA E4M3
  MTP0 requirement, an extracted _verify_e4m3_kv_requirements, the envs entry,
  an MTP weight-remap hook, calibration-overlay tooling, and CPU/GPU tests.
- phase2b: a CUDA-graph-capture-safe XQA workspace (torch.full on-device
  instead of a host torch.tensor copy) with capture/envelope tests.

CPU check (CUDA hidden): 27 passed, 19 skipped (GPU-only), 2 failed. The two
failures (test_validate_scale_overlay_lists_missing_names,
test_finalize_qsa_scale_load_success_and_missing) exercise draft-side q-scale
visibility whose model.py scale-load changes are not part of these two
patches; that piece is out of scope for this staging branch.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 464f538)
Finish the staged draft-scale path without changing the existing main-model fallback contract. Add a repository-owned, revision-checked target/draft scale overlay materializer and make its CPU tests portable.

Co-authored-by: OpenAI Codex <noreply@openai.com>

Signed-off-by: Leonccaa <166551845+Leonccaa@users.noreply.github.com>
(cherry picked from commit bb4bc78)
The 1CatAI#647 overlay kept the stricter native E4M3 ABI but lost the matching Python workspace dtype selection. Key the cached workspace by KV dtype and allocate FP32 temporary output for E4M3 while retaining FP16 for the existing path.

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Leonccaa <166551845+Leonccaa@users.noreply.github.com>
(cherry picked from commit b86e641)
ruff-format and clang-format only; no behaviour change. Keeps the native
tree byte-identical to the already-qualified SM70 build so its compiled
binaries can be reused.

Signed-off-by: Leonccaa <166551845+Leonccaa@users.noreply.github.com>
test_qsa_e4m3_loader_requires_all_24_scales asserted that an incomplete
overlay raises, but left VLLM_QWEN4EXP_QSA_E4M3_STRICT_SCALES unset, so
_validate_qsa_e4m3_scale_load took the non-strict warning path and the test
failed on main. The gate it claimed to cover was never exercised.

Set the env explicitly and assert both contracts: non-strict returns the
missing scale names so the caller can fall back to unit scales, strict
raises with the loaded/required count. Also assert the non-FP8 cache short
circuit returns an empty set rather than only that it does not raise.

Signed-off-by: Leonccaa <166551845+Leonccaa@users.noreply.github.com>
@Leonccaa

Copy link
Copy Markdown
Owner Author

GPU acceptance: PASS at C1, C2 and C4

Image leoncca/1cat-vllm:ct252-e4m3-realign-20260919-004db582d6
(sha256:105e3554…ce012), version 1.5.0+ct252.20260919.g004db582d6, source
004db582d6. Native binaries are reused from the already-qualified SM70 build
b22abc091b; the native tree is byte-identical, only the Python layer was
repacked. Tesla V100-PCIE-32GB x4, TP4, FP16 activations, MTP3,
draft_sample_method=greedy, gpu_memory_utilization 0.96, 262,144 context.

Startup gate on every run: calibrated main QSA 24/24 and MTP draft QSA 2/2
under VLLM_QWEN4EXP_QSA_E4M3_STRICT_SCALES=1.

Gates

Sixteen generations per run: four real prompts (Chinese scene, Chinese story,
a Chinese technical explanation, an English essay) x four temperatures
(0.0 / 0.3 / 0.8 / 1.0), same seed, natural stop.

run status KV dtype KV capacity shortest generation corruption errors preemptions 1CatAI#637 boundary
baseline FP16 C2 PASS auto 444,668 tok 390 0 0 0 parity
candidate C1 PASS fp8_e4m3 787,848 tok 372 0 0 0 parity
candidate C2 PASS fp8_e4m3 793,516 tok 372 0 0 0 parity
candidate C4 PASS fp8_e4m3 775,096 tok 372 0 0 0 parity

E4M3 buys +74% to +78% KV capacity at the same memory fraction.

#637 boundary is cold-vs-warm token-id equality at 803/804/805/1604 tokens.

The corruption screen is deliberately narrow: doubled CJK punctuation, U+FFFD,
and chat role or template markers. An earlier version also flagged an adjacent
repeated ideograph run and failed a clean generation on 咕嘟咕嘟, so that rule
was removed and repetition is now reported as a diagnostic instead. Every
corrupted sample collected from the rolled-back lane still trips the narrow
screen.

Same-seed A/B

All 64 generations (4 runs x 16) are corruption-free. The candidate is
byte-identical across C1/C2/C4 — same token counts, same speculative counters
(3974 drafts / 11922 draft tokens / 4778 accepted in all three) — so
concurrency does not perturb single-request output.

MTP

run drafts draft tokens accepted acceptance mean accepted length
baseline FP16 C2 7781 23343 10573 45.29% 2.359
candidate C1/C2/C4 3974 11922 4778 40.08% 2.202

Prefill and decode

Warm-up request discarded, then three measured repeats; medians. Output pinned
to 256 tokens with ignore_eos.

run ctx TTFT prefill tok/s decode tok/s step ms tokens/step
baseline FP16 C2 1,024 0.448 s 2,286 86.14 41.08 3.806
baseline FP16 C2 8,192 2.964 s 2,764 80.17 43.19 3.446
candidate C1 1,024 0.304 s 3,365 64.53 41.58 3.228
candidate C1 8,192 2.312 s 3,543 57.64 42.97 2.476
candidate C2 1,024 0.306 s 3,344 63.93 41.96 3.228
candidate C2 8,192 2.313 s 3,541 57.23 43.19 2.476
candidate C4 1,024 0.299 s 3,428 62.66 42.80 3.228
candidate C4 8,192 2.295 s 3,569 56.21 44.04 2.476

Prefill is 23% to 46% faster. Decode is 22% to 30% slower, and the
cause is visible in the last two columns: step time is unchanged (41-44 ms on
both sides), while tokens per step falls from 3.45-3.81 to 2.48-3.23. The loss
is MTP acceptance against an E4M3 draft KV, not slower attention.

That is the trade this lane buys: substantially more context and faster prefill
for lower decode throughput. Whoever promotes it should decide against their own
workload mix.

Warming matters when reading these numbers. The first request at a new shape
pays Triton JIT: an unwarmed 1k TTFT measured 1.751 s against 0.448 s warm,
which would have misreported prefill by about 4x. The harness now discards a
warm-up.

Not covered

  • No CPU KV offload: [KV Offload] Grouped hybrid RAM caching, filesystem restoration and retention-sized Mamba pools (stacked on #617) 1CatAI/1Cat-vLLM#598 stays its own lane, so nothing here exercises
    GPU->CPU store or H2D restore.
  • draft_sample_method=probabilistic is untested here and must not be enabled:
    it corrupts output above max_num_seqs=2 at any temperature above 0. That
    defect is pre-existing and unrelated to E4M3 (it reproduces on
    1.5.0+ct252.20260914.gf21df5b279 with FP16 KV by changing only
    max_num_seqs), and is tracked separately.
  • Synthetic decode figures use space-filled prompts with ignore_eos;
    acceptance on real text will be lower.
  • No long-running production load test.

Evidence outside the containers on llm252:
/mnt/llm_hfs/deployments/ct252-e4m3-realign-20260919/runtime/acceptance
with SHA256SUMS, including the full text of all 64 generations.

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