Skip to content

nvfp4 MLA KV cache + per-layer outer-scale + fp8-RoPE KV-cell (sm120) - #33

Closed
brandonmmusic-max wants to merge 2 commits into
local-inference-lab:masterfrom
brandonmmusic-max:feat/nvfp4-mla-kv-outer-scale-fp8-rope
Closed

nvfp4 MLA KV cache + per-layer outer-scale + fp8-RoPE KV-cell (sm120)#33
brandonmmusic-max wants to merge 2 commits into
local-inference-lab:masterfrom
brandonmmusic-max:feat/nvfp4-mla-kv-outer-scale-fp8-rope

Conversation

@brandonmmusic-max

@brandonmmusic-max brandonmmusic-max commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

nvfp4 MLA KV cache + per-layer outer-scale + fp8-RoPE KV-cell (sm120)

Adds a 4-bit NVFP4 MLA KV cache path to the b12x MLA kernels, plus two quality/efficiency
refinements, all validated on GLM-5.2 (4x RTX PRO 6000 Blackwell, TP4/DCP4):

  • nvfp4_ds_mla record (432 B/token): 512-D latent as NVFP4 (E2M1 + E4M3 g16 scales) instead of
    fp8 — ~1.5x the context pool vs fp8_ds_mla.
  • Per-layer outer-scale (scale_format==2): in-kernel s_l restore on read; recovers latent
    quantization loss (teacher-forced KLD 0.184 -> 0.152).
  • fp8-RoPE KV-cell: store the decoupled RoPE key as fp8-e4m3 + amax; record 432 -> 368 B
    (+15.8% pool, genuine 1M single-request). 64K needle retrieval 15/15 = 15/15, KL below noise.

Companion vLLM plumbing: local-inference-lab/vllm#95.

⚠️ Merge note (draft)

This is 3-way merged from the fork-point 5af873a onto current master. The nvfp4/fp8-rope
additions overlap master's post-5af873a MLA changes in ~48 spots (decode_math/kernel/prefill/
prefill_mg), which were resolved toward preserving the feature. Those resolutions need a careful
review against your recent MLA work
— flagging so we can reconcile any infra changes I may have
shadowed. Opening as draft for exactly that reason.

Summary by CodeRabbit

  • New Features
    • Added NVFP4 precision support for sparse MLA decode and prefill workloads.
    • Added configurable latent scaling and scale-format options for improved flexibility across supported configurations.
    • Added optional FP8 RoPE handling, including an environment-based process-wide toggle.
    • Extended decoding and prefill APIs to accept the new configuration options.
  • Bug Fixes
    • Improved cache-layout validation and routing for NVFP4 and FP8 RoPE combinations.

3-way merged from fork-point 5af873a onto master; ~48 conflicts resolved toward
the feature (see PR note). Companion: local-inference-lab/vllm#95.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c9bb96bb-117b-4656-b620-5598d2837f96

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

Adds NVFP4/E4M3 sparse MLA support with optional FP8 RoPE cache layouts, latent scaling, new trait metadata, cache geometry selection, decode kernel integration, and SM120 MG prefill routing.

Changes

NVFP4 sparse MLA

Layer / File(s) Summary
Traits and public dispatch contracts
b12x/attention/mla/traits.py, b12x/attention/mla/api.py
Adds the NVFP4 scale format, FP8 RoPE environment gate, rope layout metadata, and public latent_scale, scale_format, and fp8_rope parameters with cache-record validation.
NVFP4 decode and accumulation math
b12x/attention/mla/decode_math.py
Adds BF16 Q staging, NVFP4 QK/PV dequantization, FP8 RoPE decoding, latent scaling, and updated softmax handling.
NVFP4 cache gathering
b12x/attention/mla/io.py, b12x/attention/mla/io_mg.py
Selects NVFP4 and FP8-RoPE record strides, offsets, transfer sizes, and barrier expectations for decode and MG gathers.
Unified decode kernel integration
b12x/attention/mla/kernel.py
Threads latent scale and FP8 RoPE through device entrypoints, custom operators, compile keys, cache validation, Q staging, and decode math dispatch.
MG prefill NVFP4 path
b12x/attention/mla/prefill.py, b12x/attention/mla/prefill_mg.py
Adds NVFP4 shape routing, trait overrides, cache stride handling, RoPE loading, QK/PV dequantization, probability staging, and latent-scale propagation.
Estimated code review effort: 5 (Critical) ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant sparse_mla_api
  participant UnifiedMLATraits
  participant UnifiedDecodeKernel
  participant decode_math
  Caller->>sparse_mla_api: NVFP4 parameters and latent_scale
  sparse_mla_api->>UnifiedMLATraits: resolve scale format and fp8_rope
  UnifiedMLATraits-->>UnifiedDecodeKernel: cache geometry and specialization
  UnifiedDecodeKernel->>decode_math: stage, dequantize, and accumulate NVFP4 data
  decode_math-->>Caller: sparse MLA output
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 43.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main changes: NVFP4 MLA KV-cache support, outer-scale handling, and FP8 RoPE KV-cell work on sm120.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@brandonmmusic-max
brandonmmusic-max marked this pull request as ready for review July 15, 2026 03:03

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
b12x/attention/mla/traits.py (1)

15-24: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate KV_FP8_ROPE env-gate caching across traits.py and api.py. Both modules independently parse and cache the same os.environ.get("KV_FP8_ROPE", "0") == "1" boolean at import time instead of sharing one source of truth; the shared root cause is that api.py never imports traits.py's existing accessor.

  • b12x/attention/mla/traits.py#L15-L24: keep this as the single canonical definition of KV_FP8_ROPE_ENV/KV_FP8_ROPE_ENABLED/kv_fp8_rope_enabled().
  • b12x/attention/mla/api.py#L30-L31: drop the local _KV_FP8_ROPE_ENV/_KV_FP8_ROPE_ENABLED constants and have _resolve_kv_fp8_rope (Lines 141-148) call traits.kv_fp8_rope_enabled() instead of its own cached copy.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f45930c0-991e-40d7-bf10-dbd70020ffee

📥 Commits

Reviewing files that changed from the base of the PR and between f5241d6 and 5526981.

📒 Files selected for processing (8)
  • b12x/attention/mla/api.py
  • b12x/attention/mla/decode_math.py
  • b12x/attention/mla/io.py
  • b12x/attention/mla/io_mg.py
  • b12x/attention/mla/kernel.py
  • b12x/attention/mla/prefill.py
  • b12x/attention/mla/prefill_mg.py
  • b12x/attention/mla/traits.py

Comment thread b12x/attention/mla/api.py
Comment on lines 571 to +599
_sm120_route = _use_sm120_sparse_mla(backend=backend, device=q_all.device)
# NVFP4 (scale_format=2) selection: explicit kwarg wins; otherwise the
# scratch/workspace planned kv_cache_dtype supplies it. None -> inferred
# from q_head_dim inside the kernel launchers (fp8 GLM default).
scale_format_for_call = (
scale_format if scale_format is not None else getattr(workspace, "scale_format", None)
)
if int(scale_format_for_call or -1) == 2 and fp8_rope is None:
# Normal vLLM calls arrive through b12x.integration.mla, whose stable
# public signature does not carry this new option. The allocated record
# is therefore the authoritative process-lifetime ABI at this boundary;
# derive the specialization from it instead of rereading a mutable env.
record_bytes = int(kv_cache.shape[-1])
if record_bytes not in (368, 432):
raise ValueError(
"NVFP4 sparse MLA cache record must be 368 or 432 bytes, got "
f"{record_bytes}"
)
fp8_rope_for_call = record_bytes == 368
else:
fp8_rope_for_call = _resolve_kv_fp8_rope(fp8_rope)
if int(scale_format_for_call or -1) == 2:
expected_record_bytes = 368 if fp8_rope_for_call else 432
if int(kv_cache.shape[-1]) != expected_record_bytes:
raise ValueError(
"NVFP4 sparse MLA cache record disagrees with KV_FP8_ROPE: "
f"got {int(kv_cache.shape[-1])} bytes, expected "
f"{expected_record_bytes}"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm sparse_mla_reference has no NVFP4/scale_format awareness.
rg -n 'def sparse_mla_reference' -A 40 b12x/attention/mla/ | head -80
rg -n 'scale_format' b12x/attention/mla/reference.py b12x/attention/mla/compressed_reference.py 2>/dev/null

Repository: lukealonso/b12x

Length of output: 2935


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== api.py excerpt =="
sed -n '560,720p' b12x/attention/mla/api.py | cat -n

echo
echo "== reference.py excerpt =="
sed -n '1,220p' b12x/attention/mla/reference.py | cat -n

Repository: lukealonso/b12x

Length of output: 17815


NVFP4 needs an SM120-only guard

scale_format=2 is validated here, but the non-_sm120_route path still falls through to sparse_mla_reference(), which only understands the packed NSA layout and has no NVFP4 handling. Add the same SM120 gate used for attn_sink so non-SM120 callers fail fast with a clear error instead of reaching the reference path with an unsupported cache format.

Comment on lines +725 to +745
if cutlass.const_expr(t.scale_format == ScaleFormat.NVFP4_E4M3):
s0_load_q_bf16_to_smem(
q_token, q_fp8_addr, q_rope_addr,
head_base, Int32(self.valid_hpb), tid,
d_nope=t.d_nope, d_rope=t.d_rope, hpb=t.hpb,
q_nope_bf16_stride=t.q_nope_stride,
q_rope_stride=L.q_rope_stride,
num_threads=self.math_threads, barrier_id=2,
)
else:
s0_quantize_q_to_smem(
q_token, q_fp8_addr, q_sc_view, q_rope_addr, amax_view,
head_base, Int32(self.valid_hpb), tid,
d_nope=t.d_nope, d_rope=t.d_rope,
d_qk=t.d_nope + t.d_rope,
quant_tile=t.quant_tile, num_scales=t.num_scales,
hpb=(8 if self.native_h8 else t.hpb),
q_nope_stride=t.q_nope_stride,
q_rope_stride=L.q_rope_stride,
num_threads=self.math_threads, barrier_id=2,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate the shared-storage class and layout builder to confirm q_fp8 sizing
# accounts for BF16 (2B/elem) vs FP8 (1B/elem) staging under scale_format==NVFP4_E4M3.
rg -n 'def get_unified_shared_storage_cls' -A 80 b12x/attention/mla/
rg -n 'def make_smem_layout\b' -A 60 b12x/attention/mla/
rg -n 'q_nope_bf16' b12x/attention/mla/
rg -n '\bq_fp8\b.*=' b12x/attention/mla/

Repository: lukealonso/b12x

Length of output: 16803


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== kernel.py locations =="
sed -n '710,750p' b12x/attention/mla/kernel.py
echo
sed -n '1454,1498p' b12x/attention/mla/kernel.py
echo
echo "== smem layout =="
sed -n '189,240p' b12x/attention/mla/smem.py
echo
echo "== run_unified_decode and validation =="
sed -n '2230,2385p' b12x/attention/mla/kernel.py
echo
sed -n '560,610p' b12x/attention/mla/api.py
echo
echo "== callers of run_unified_decode =="
rg -n 'run_unified_decode\(' b12x/attention/mla/

Repository: lukealonso/b12x

Length of output: 16642


Allocate a BF16 staging buffer for the NVFP4 path kernel.py:725-745, 1464-1491 writes BF16 Q-NoPE into q_fp8, but smem.py allocates q_fp8 as 1 byte/elem (hpb * q_nope_stride). That can overrun the packed shared-memory struct; mirror the dedicated q_nope_bf16 buffer used in prefill_mg.py, or widen this specialization.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f45930c0-991e-40d7-bf10-dbd70020ffee

📥 Commits

Reviewing files that changed from the base of the PR and between f5241d6 and 5526981.

📒 Files selected for processing (8)
  • b12x/attention/mla/api.py
  • b12x/attention/mla/decode_math.py
  • b12x/attention/mla/io.py
  • b12x/attention/mla/io_mg.py
  • b12x/attention/mla/kernel.py
  • b12x/attention/mla/prefill.py
  • b12x/attention/mla/prefill_mg.py
  • b12x/attention/mla/traits.py

Comment thread b12x/attention/mla/api.py
Comment on lines 2357 to +2365
model_type, compute_mode, scale_format = infer_model_type(q_head_dim, swa_k_cache.dtype)
traits = make_unified_traits(model_type, compute_mode, scale_format)
if scale_format_override is not None:
scale_format = int(scale_format_override)
traits = make_unified_traits(
model_type,
compute_mode,
scale_format,
fp8_rope=fp8_rope_override,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Validate the NVFP4 cache ABI inside run_unified_decode.

Line 2360 derives 368/432-byte geometry from the override or environment, but this launcher does not verify swa_k_cache.shape[-1]. Direct callers can therefore select the wrong record stride and misread the flattened cache.

Proposed validation
     if scale_format_override is not None:
         scale_format = int(scale_format_override)
+    if scale_format == ScaleFormat.NVFP4_E4M3 and fp8_rope_override is None:
+        record_bytes = int(swa_k_cache.shape[-1])
+        if record_bytes not in (368, 432):
+            raise ValueError(
+                f"NVFP4 cache record must be 368 or 432 bytes, got {record_bytes}"
+            )
+        fp8_rope_override = record_bytes == 368
     traits = make_unified_traits(
         model_type,
         compute_mode,
         scale_format,
         fp8_rope=fp8_rope_override,
     )
+    if (
+        scale_format == ScaleFormat.NVFP4_E4M3
+        and int(swa_k_cache.shape[-1]) != int(traits.kv_gmem_stride)
+    ):
+        raise ValueError("NVFP4 cache record width disagrees with fp8_rope_override")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
model_type, compute_mode, scale_format = infer_model_type(q_head_dim, swa_k_cache.dtype)
traits = make_unified_traits(model_type, compute_mode, scale_format)
if scale_format_override is not None:
scale_format = int(scale_format_override)
traits = make_unified_traits(
model_type,
compute_mode,
scale_format,
fp8_rope=fp8_rope_override,
)
model_type, compute_mode, scale_format = infer_model_type(q_head_dim, swa_k_cache.dtype)
if scale_format_override is not None:
scale_format = int(scale_format_override)
if scale_format == ScaleFormat.NVFP4_E4M3 and fp8_rope_override is None:
record_bytes = int(swa_k_cache.shape[-1])
if record_bytes not in (368, 432):
raise ValueError(
f"NVFP4 cache record must be 368 or 432 bytes, got {record_bytes}"
)
fp8_rope_override = record_bytes == 368
traits = make_unified_traits(
model_type,
compute_mode,
scale_format,
fp8_rope=fp8_rope_override,
)
if (
scale_format == ScaleFormat.NVFP4_E4M3
and int(swa_k_cache.shape[-1]) != int(traits.kv_gmem_stride)
):
raise ValueError("NVFP4 cache record width disagrees with fp8_rope_override")

@brandonmmusic-max
brandonmmusic-max marked this pull request as draft July 15, 2026 03:54
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@voipmonitor

Copy link
Copy Markdown
Contributor

Superseded by the clean current-master replacement #35. The replacement preserves the current Spark H8/H16 optimizations, fixes the BF16 shared-memory sizing and duplicate-definition issues found in this draft, adds direct ABI guards and focused tests, and is ready/non-draft and mergeable. Closing this conflicting draft so there is one canonical NVFP4 MLA PR.

@lukealonso

Copy link
Copy Markdown
Collaborator

superseded by #35

@lukealonso lukealonso closed this Jul 17, 2026
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.

3 participants