Skip to content

feat(quantization): add native FP8 inference (serve_fp8_weight) for DenseGeneral and GMM v2 - #5207

Open
Shuwen-Fang wants to merge 1 commit into
qwen3.5-397b-fp8from
qwen3.5-serve-fp8-weight
Open

Shuwen-Fang wants to merge 1 commit into
qwen3.5-397b-fp8from
qwen3.5-serve-fp8-weight

Conversation

@Shuwen-Fang

@Shuwen-Fang Shuwen-Fang commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

Description

Implements native FP8 inference (quantization="serve_fp8_weight") for Qwen3.5 (and other FP8-quantized architectures) to execute forward passes directly using quantized FP8 weights without on-the-fly dequantization (b/557372531).

Previously, weights stored/loaded in FP8 were dequantized on-the-fly to BF16 prior to compute, causing substantial memory bandwidth and runtime overhead during inference and RL rollouts. This change allows weights to stay in FP8 and compute directly via native FP8 GEMMs across DenseGeneral (supporting per-tensor, per-channel, and block-wise scaling) and RoutedMoE (dispatching directly to tokamax v2 FP8 GMM kernel).


Benchmark & Performance Results

Evaluated on TPU v6e-8 (8 chips, GhostLite) using the per-tensor quantized qwen3.5-35b-a3b-fp8 checkpoint (/dev/shm/maxtext_qwen3.5_35b_fp8_pertensor_v3), running a standalone autoregressive decode forward loop simulating RL rollout forward inference (batch size = 1, sequence length = 64 tokens, scan_layers=True, attention="dot_product").

Performance Summary

Metric Baseline (quantization="") Diff (quantization="serve_fp8_weight") Delta / Speedup
Average Forward Step Time (15 steps) 471.59 ms (±3.07 ms) 259.80 ms (±3.25 ms) 1.81x faster (-45.0% latency)
Step 0 (JIT Compilation) 5.12 s 1.32 s 3.88x faster compile
Output Logits Finite Yes (all finite) Yes (all finite) Parity verified

XProf Trace Viewer Links

Persisted GCS Traces:

  • Baseline: gs://test-maxtext-output/shuwenf/xprof/20260911_run/xprof/baseline/
  • Diff: gs://test-maxtext-output/shuwenf/xprof/20260911_run/xprof/diff/

Tests

Unit Tests

  • python -m unittest tests/unit/native_fp8_dot_general_test.py (23/23 passed in 42.8s):
    • Per-tensor, per-channel, and block-wise numerical verification against independent references across all Qwen3.5 weight families (self_attn, linear_attn, MoE expert gate/up/down).
    • Multi-axis output kernel rank-matching tests (e.g. fused QKV (embed, heads, head_dim)).
    • DequantizeWeightPerChannelTest: square matrix 2D per-channel scaling & MoE 3D per-expert scaling.
    • MoENativeFp8PerChannelTest: GMM v2 native FP8 execution with per-channel qpl.QArray.
  • pytest tests/unit/moe_test.py -k "SparseMoeNativeGmmPerChannelAxisTest" (1/1 passed in 43.9s):
    • Validates K-axis (input-channel) scale correctly falls back to dequantize in RoutedMoE.
  • python tests/unit/configs_value_test.py (4/4 passed in 7.2s):
    • Validates serve_fp8_weight configuration, FP8 weight requirement, Qwix mutual exclusivity, and fused MoE warning.
  • python -m unittest tests/unit/nnx_decoders_test.py (53/53 passed in 91.9s):
    • Full decoder forward pass and configuration suite.
  • Pre-commit hooks clean: codespell, pyink, pylint (10.00/10), and yamllint all passed.

Numerical Parity & Alignment (Native FP8 OFF vs. ON)

Evaluated full NNXDecoder forward pass across 128 tokens (batch_size=4, seq_len=32, vocab_size=1024) on TPU v6e comparing Post-PR Native OFF (quantization="" / dequantize) vs. Post-PR Native ON (quantization="serve_fp8_weight"):

Metric Post-PR Native OFF vs. ON
Mean $D_{\text{KL}}$ $4.35 \times 10^{-3}\text{ nats}$ (Max: $1.96 \times 10^{-2}$)
Top-1 Token Match Rate 89.06% (114 / 128 tokens)
Top-5 Candidate Overlap 78.64%
Logit Cosine Similarity 0.9956 (99.56%)
SQNR 20.60 dB

BUGS: b/557372531

Checklist

  • I have performed a self-review of my code.
  • I have necessary comments in my code, particularly in hard-to-understand areas.
  • I have run end-to-end tests and provided workload links above if applicable.
  • I have made or will make corresponding changes to the doc if needed.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces a native FP8 inference feature (quantization="serve_fp8_weight") to load and execute FP8 checkpoints directly on TPU without on-the-fly dequantization, yielding significant performance improvements. It adds native_fp8_dot_general utilizing qwix and updates DenseGeneral and MoE layers to bypass standard dequantization when this mode is active. Feedback on the changes highlights two critical runtime issues: a dimension mismatch in moe.py where per_expert_scale is incorrectly reshaped to 4D instead of 3D for a 3D kernel, and a potential ValueError in quantizations.py when unconditionally reshaping kernel_scale when its size is greater than 1.

Comment thread src/maxtext/layers/moe.py
Comment thread src/maxtext/layers/quantizations.py Outdated
Comment on lines +148 to +149
if kernel_scale.ndim != quantized_kernel.ndim:
kernel_scale = kernel_scale.reshape((1,) * quantized_kernel.ndim)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Unconditionally reshaping kernel_scale to (1,) * quantized_kernel.ndim when kernel_scale.ndim != quantized_kernel.ndim will raise a ValueError if kernel_scale has a size greater than 1 (e.g., a 1D per-channel or per-expert scale of size > 1). We should only reshape to all-1s if kernel_scale.size == 1. If kernel_scale.size > 1 and its leading dimensions match the kernel, we should expand it by appending trailing dimensions of size 1 to match the kernel's rank.

Suggested change
if kernel_scale.ndim != quantized_kernel.ndim:
kernel_scale = kernel_scale.reshape((1,) * quantized_kernel.ndim)
if kernel_scale.ndim != quantized_kernel.ndim:
if kernel_scale.size == 1:
kernel_scale = kernel_scale.reshape((1,) * quantized_kernel.ndim)
elif kernel_scale.ndim < quantized_kernel.ndim and quantized_kernel.shape[: kernel_scale.ndim] == kernel_scale.shape:
kernel_scale = kernel_scale.reshape(kernel_scale.shape + (1,) * (quantized_kernel.ndim - kernel_scale.ndim))

@Shuwen-Fang
Shuwen-Fang force-pushed the qwen3.5-serve-fp8-weight branch 4 times, most recently from ef0c61e to ed2e194 Compare September 14, 2026 23:57
…enseGeneral and GMM v2

- Implements quantization="serve_fp8_weight" to load and execute FP8 weights directly at inference time without on-the-fly dequantization.
- DenseGeneral: Dispatches single-contraction GEMMs to native_fp8_dot_general using qwix.quantize (dynamic activation FP8 quantization) and qwix.dot_general, supporting per-tensor, per-channel, and block-wise scaling.
- RoutedMoE: Dispatches per-tensor and per-channel FP8 MoE weights wrapped in qpl.QArray directly to gmm_v2 (tokamax v2 FP8 GMM kernel), completely eliminating weight dequantization.
- linears.py: Guard against square 2D matrix broadcasting and handle 3D MoE per-expert scaling in dequantize_weight.
- Adds comprehensive unit test suites in tests/unit/native_fp8_dot_general_test.py and tests/unit/nnx_decoders_test.py.
- Validated on TPU v6e-8 with Qwen3.5-35B-FP8 per-tensor checkpoint, achieving 1.81x step speedup (471.59ms -> 259.80ms) and doubling operational intensity (1.11 -> 2.03 FLOP/byte).
@Shuwen-Fang
Shuwen-Fang force-pushed the qwen3.5-serve-fp8-weight branch from ed2e194 to 7f1db21 Compare September 15, 2026 00:22
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