Skip to content

Commit 1725a84

Browse files
authored
feat(qwen3_8): wire real NVFP4 + FP8 GEMM via Myelins add_dynamic_quantize (#1326)
feat(qwen3_8): wire real NVFP4 + FP8 GEMM via Myelin's add_dynamic_quantize Wires families/qwen3_8 to build genuine W4A4 NVFP4 and W8A8 FP8 GEMM engines for RadixArk/Qwen3.8-27B-NVFP4-style ModelOpt MIXED_PRECISION checkpoints, without tensorrt-edge-llm's custom CUTLASS plugin. families/qwen3_8/quantization.py (new): - Qwen38QuantContext / calibrate_qwen3_8_nvfp4() reads the checkpoint's own packed NVFP4 (MLP gate/up/down, lm_head) and FP8 (DeltaNet in_proj_qkv/in_proj_z/out_proj, attention q/k/v/o) weights plus their real calibrated weight_scale/weight_scale_2/input_scale tensors directly -- bit-exact reuse, no dequantize-then-requantize round trip. - NVFP4 activations use TensorRT's add_dynamic_quantize (IDynamicQuantizeLayer): the standard add_quantize+block_shape path is unconditionally rejected for FP4 output by Myelin's shape checker (src/compiler/analysis/shape.cpp:3350, "Blockwise quantization requires output type to be int8 or fp8e4m3", confirmed on TensorRT 11.1.0.106 and 11.3.0.99). add_dynamic_quantize is the one layer type with a real fused FP4 tensor-core kernel. - FP8 activations use the plain add_quantize/add_dequantize pattern (no blockwise restriction applies), matching families/qwen's proven FP8 approach. - Weight constants are fed in native [out_features, in_features] checkpoint layout with MatrixOperation.TRANSPOSE on the matmul, to avoid unpack/transpose/repack of packed sub-byte FP4 data. families/qwen3_8/engine_builder.py: - Removes the quant_ctx NotImplementedError guard. - Threads quant_ctx through DeltaNet (in_proj_qkv/in_proj_z/out_proj) and attention (q/gate/k/v/o) matmuls via graph_blocks.make_matmul_fn (already quant_ctx-aware, shared infra); MLP/lm_head already routed through it once quant_ctx stopped being force-None. - Also includes an unrelated-but-required precision-threading fix (_transpose_2d's precision param was never passed through load_weights()/_load_*_weights(), so every weight was stored as FP32 regardless of --precision, OOMing a full 27B build). This exact fix also lives isolated on zhenshanx/qwen3_8-precision-threading-fix for landing as its own PR -- drop this hunk on rebase once that merges. - Sets ProfilingVerbosity.DETAILED for engine-inspector tactic/constant visibility. families/qwen3_8/model.py: - Accepts quantization="nvfp4", builds quant_ctx via calibrate_qwen3_8_nvfp4(), threads precision into load_weights(). Verified on a real B100/Blackwell (SM100) node against the actual RadixArk/Qwen3.8-27B-NVFP4 checkpoint: - Full 64-layer engine builds end-to-end (~500s, 20.2GB, down from a naive-quantize 53.8GB and an unquantized ~54GB FP16 baseline). - Engine inspector confirms 193 FP4E2M1-typed and 546 FP8-typed constants (matching every quantized weight_name registered), and real fused Blackwell tensor-core kernels (tensorop*/cga*/sm* tactics, Myelin-auto-fused dual_gemm for gate+up, RMSNorm+DynamicQuantize fused into single prologue kernels). - Real generation test (RadixArk/Qwen3.8-27B-NVFP4 tokenizer + chat template, hand-driven single-step decode loop matching the C++ runtime's exact mask/state/position semantics) produces correct, coherent output for "What is the capital of France? Answer in one word." -> "Paris<|im_end|>". Known follow-up (tracked separately, not done here): checkpoint tensors for quantized weight_names are still redundantly loaded+dequantized by load_weights() even though maybe_quantized_matmul() never uses that copy -- wasted CPU/memory, not a correctness issue, worth its own perf-only PR. Signed-off-by: Zhenshan Xie <zhenshanx@nvidia.com>
1 parent d9b4fd9 commit 1725a84

3 files changed

Lines changed: 554 additions & 60 deletions

File tree

families/qwen3_8/engine_builder.py

Lines changed: 58 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ def cast_all(tensors):
154154

155155
class Qwen38Model:
156156
def load_weights(
157-
self, model_dir: str, config: ModelConfig,
157+
self, model_dir: str, config: ModelConfig, *, precision: str = "fp32",
158158
) -> WeightDict:
159159
model_dir_path = Path(model_dir)
160160
readers = _open_safetensors(model_dir_path)
@@ -246,20 +246,20 @@ def load_weights(
246246
readers, weights, prefix, hf_prefix,
247247
hidden, d_inner, conv_dim, d_conv,
248248
deltanet_num_heads, deltanet_num_kv_heads,
249-
deltanet_head_dim)
249+
deltanet_head_dim, precision=precision)
250250
deltanet_count += 1
251251

252252
elif lt == "attention":
253253
self._load_attention_weights(
254254
readers, weights, prefix, hf_prefix,
255255
hidden, attn_size, kv_size,
256-
num_heads, num_kv_heads, head_dim)
256+
num_heads, num_kv_heads, head_dim, precision=precision)
257257
attn_count += 1
258258

259259
# SwiGLU MLP (all layer types)
260260
self._load_mlp_weights(
261261
readers, weights, prefix, hf_prefix,
262-
hidden, mlp_size)
262+
hidden, mlp_size, precision=precision)
263263

264264
# Final norm (also uses (1+weight) centering)
265265
final_norm_key = "model.language_model.norm.weight"
@@ -275,10 +275,10 @@ def load_weights(
275275
lm_head_key = "lm_head.weight"
276276
if _has_tensor(readers, lm_head_key):
277277
weights["w_lm_head"] = _transpose_2d(
278-
_load_tensor(readers, lm_head_key), "lm_head")
278+
_load_tensor(readers, lm_head_key), "lm_head", precision)
279279
else:
280280
weights["w_lm_head"] = _transpose_2d(
281-
embedding.copy(), "embedding_tied")
281+
embedding.copy(), "embedding_tied", precision)
282282

283283
# Metadata for engine builder
284284
weights["_layer_types"] = layer_types
@@ -300,30 +300,30 @@ def load_weights(
300300
def _load_deltanet_weights(
301301
self, readers, weights, prefix, hf_prefix,
302302
hidden, d_inner, conv_dim, d_conv,
303-
num_heads, num_kv_heads, head_dim,
303+
num_heads, num_kv_heads, head_dim, *, precision: str = "fp32",
304304
):
305305
"""Load DeltaNet (linear attention) layer weights."""
306306
attn_prefix = f"{hf_prefix}.linear_attn"
307307

308308
# in_proj_qkv (QKV combined): [conv_dim, hidden] -> transpose
309309
in_proj_raw = _load_tensor(readers, f"{attn_prefix}.in_proj_qkv.weight")
310310
weights[f"{prefix}.deltanet_in_proj_qkv"] = _transpose_2d(
311-
in_proj_raw, "deltanet_in_proj_qkv")
311+
in_proj_raw, "deltanet_in_proj_qkv", precision)
312312

313313
# Gate projection (z): [d_inner, hidden] -> transpose
314314
z_proj_raw = _load_tensor(readers, f"{attn_prefix}.in_proj_z.weight")
315315
weights[f"{prefix}.deltanet_z_proj"] = _transpose_2d(
316-
z_proj_raw, "deltanet_z_proj")
316+
z_proj_raw, "deltanet_z_proj", precision)
317317

318318
# Decay projection (a): [num_heads, hidden] -> transpose
319319
a_proj_raw = _load_tensor(readers, f"{attn_prefix}.in_proj_a.weight")
320320
weights[f"{prefix}.deltanet_a_proj"] = _transpose_2d(
321-
a_proj_raw, "deltanet_a_proj")
321+
a_proj_raw, "deltanet_a_proj", precision)
322322

323323
# Beta projection (b): [num_heads, hidden] -> transpose
324324
b_proj_raw = _load_tensor(readers, f"{attn_prefix}.in_proj_b.weight")
325325
weights[f"{prefix}.deltanet_b_proj"] = _transpose_2d(
326-
b_proj_raw, "deltanet_b_proj")
326+
b_proj_raw, "deltanet_b_proj", precision)
327327

328328
# A_log: [num_heads] -> precompute -exp(A_log)
329329
A_log = _load_tensor(readers, f"{attn_prefix}.A_log")
@@ -361,12 +361,12 @@ def _load_deltanet_weights(
361361
# Output projection: [hidden, d_inner] -> transpose
362362
out_raw = _load_tensor(readers, f"{attn_prefix}.out_proj.weight")
363363
weights[f"{prefix}.deltanet_out_proj"] = _transpose_2d(
364-
out_raw, "deltanet_out_proj")
364+
out_raw, "deltanet_out_proj", precision)
365365

366366
def _load_attention_weights(
367367
self, readers, weights, prefix, hf_prefix,
368368
hidden, attn_size, kv_size,
369-
num_heads, num_kv_heads, head_dim,
369+
num_heads, num_kv_heads, head_dim, *, precision: str = "fp32",
370370
):
371371
"""Load full self-attention layer weights."""
372372
attn_prefix = f"{hf_prefix}.self_attn"
@@ -380,22 +380,22 @@ def _load_attention_weights(
380380
q_reshaped = q_raw.reshape(num_heads, 2 * head_dim, hidden)
381381
q_part = q_reshaped[:, :head_dim, :].reshape(attn_size, hidden)
382382
gate_part = q_reshaped[:, head_dim:, :].reshape(attn_size, hidden)
383-
weights[f"{prefix}.w_q"] = _transpose_2d(q_part, "q_proj")
384-
weights[f"{prefix}.w_gate_attn"] = _transpose_2d(gate_part, "gate_proj")
383+
weights[f"{prefix}.w_q"] = _transpose_2d(q_part, "q_proj", precision)
384+
weights[f"{prefix}.w_gate_attn"] = _transpose_2d(gate_part, "gate_proj", precision)
385385

386386
# k_proj: [kv_size, hidden] -> keep compact
387387
k_raw = _load_tensor(readers, f"{attn_prefix}.k_proj.weight")
388-
k_t = _transpose_2d(k_raw, "k_proj")
388+
k_t = _transpose_2d(k_raw, "k_proj", precision)
389389
weights[f"{prefix}.w_k"] = k_t
390390

391391
# v_proj: [kv_size, hidden] -> keep compact
392392
v_raw = _load_tensor(readers, f"{attn_prefix}.v_proj.weight")
393-
v_t = _transpose_2d(v_raw, "v_proj")
393+
v_t = _transpose_2d(v_raw, "v_proj", precision)
394394
weights[f"{prefix}.w_v"] = v_t
395395

396396
# o_proj: [hidden, attn_size] -> transpose
397397
o_raw = _load_tensor(readers, f"{attn_prefix}.o_proj.weight")
398-
weights[f"{prefix}.w_o"] = _transpose_2d(o_raw, "o_proj")
398+
weights[f"{prefix}.w_o"] = _transpose_2d(o_raw, "o_proj", precision)
399399

400400
# QK-norm with (1+weight) centering, tiled to num_heads
401401
q_norm_key = f"{attn_prefix}.q_norm.weight"
@@ -413,7 +413,7 @@ def _load_attention_weights(
413413

414414
def _load_mlp_weights(
415415
self, readers, weights, prefix, hf_prefix,
416-
hidden, mlp_size,
416+
hidden, mlp_size, *, precision: str = "fp32",
417417
):
418418
"""Load SwiGLU MLP weights."""
419419
gate_key = f"{hf_prefix}.mlp.gate_proj.weight"
@@ -422,11 +422,11 @@ def _load_mlp_weights(
422422

423423
if _has_tensor(readers, gate_key):
424424
weights[f"{prefix}.w_gate"] = _transpose_2d(
425-
_load_tensor(readers, gate_key), "gate_proj")
425+
_load_tensor(readers, gate_key), "gate_proj", precision)
426426
weights[f"{prefix}.w_up"] = _transpose_2d(
427-
_load_tensor(readers, up_key), "up_proj")
427+
_load_tensor(readers, up_key), "up_proj", precision)
428428
weights[f"{prefix}.w_down"] = _transpose_2d(
429-
_load_tensor(readers, down_key), "down_proj")
429+
_load_tensor(readers, down_key), "down_proj", precision)
430430

431431
def build_engine(
432432
self, config: ModelConfig, weights: WeightDict,
@@ -435,14 +435,6 @@ def build_engine(
435435
debug_layer_outputs: bool = False,
436436
) -> bytes:
437437
"""Build hybrid TRT engine with DeltaNet + attention layers."""
438-
if quant_ctx is not None:
439-
# This graph emits plain matmuls; it never threads a quantization
440-
# context into its projections. Accepting quant_ctx silently would
441-
# return an unquantized engine for a build the caller asked to
442-
# quantize, so fail loudly instead.
443-
raise NotImplementedError(
444-
"Qwen3.8 does not support quantized builds; "
445-
"build without --quantize/--fp8")
446438
hidden = config.hidden_size
447439
vocab = config.vocab_size
448440
num_layers = config.num_hidden_layers
@@ -488,6 +480,7 @@ def build_engine(
488480
network = builder.create_network(1 << int(trt.NetworkDefinitionCreationFlag.STRONGLY_TYPED))
489481
trt_config = builder.create_builder_config()
490482
trt_config.builder_optimization_level = 1
483+
trt_config.profiling_verbosity = trt.ProfilingVerbosity.DETAILED
491484

492485
# --- Inputs ---
493486
token_id = network.add_input("token_id", trt.int32, (1,))
@@ -612,6 +605,7 @@ def layer_cast(tensor):
612605
head_dim=deltanet_head_dim,
613606
mlp_size=mlp_size,
614607
dtype=layer_np_dtype,
608+
quant_ctx=quant_ctx,
615609
)
616610
hidden_state = result["hidden"]
617611
present_conv_outputs.append(result["present_conv"])
@@ -641,6 +635,7 @@ def layer_cast(tensor):
641635
max_cache_length=max_cache_length,
642636
mlp_size=mlp_size,
643637
dtype=layer_np_dtype,
638+
quant_ctx=quant_ctx,
644639
)
645640
hidden_state = result["hidden"]
646641
present_k_outputs.append(result["present_k"])
@@ -662,9 +657,9 @@ def layer_cast(tensor):
662657
dtype=work_np_dtype)
663658

664659
# --- LM head ---
665-
logits = graph_ops.add_matmul_rhs_constant(
666-
network, hidden_state, hidden, vocab, weights["w_lm_head"],
667-
dtype=work_np_dtype)
660+
lm_head_matmul = graph_blocks.make_matmul_fn(network, work_np_dtype, quant_ctx)
661+
logits = lm_head_matmul(
662+
hidden_state, hidden, vocab, weights["w_lm_head"], "w_lm_head")
668663
b_out = np.zeros(vocab, dtype=work_np_dtype)
669664
logits = graph_ops.add_bias_sum(
670665
network, logits, vocab, b_out, dtype=work_np_dtype)
@@ -807,6 +802,7 @@ def _add_deltanet_layer(
807802
head_dim: int,
808803
mlp_size: int,
809804
dtype: np.dtype = np.float32,
805+
quant_ctx=None,
810806
) -> dict[str, trt.ITensor]:
811807
"""Add one Gated DeltaNet layer (single-step decode).
812808
@@ -828,15 +824,17 @@ def _add_deltanet_layer(
828824
weights[f"{prefix}.input_norm"], eps_tensor, dtype=dtype)
829825

830826
# ===== 2. Input projections =====
827+
matmul = graph_blocks.make_matmul_fn(network, dtype, quant_ctx)
828+
831829
# QKV combined: [1, hidden] -> [1, conv_dim]
832-
qkv = graph_ops.add_matmul_rhs_constant(
833-
network, normed, hidden_size, conv_dim,
834-
weights[f"{prefix}.deltanet_in_proj_qkv"], dtype=dtype)
830+
qkv = matmul(
831+
normed, hidden_size, conv_dim,
832+
weights[f"{prefix}.deltanet_in_proj_qkv"], f"{prefix}.deltanet_in_proj_qkv")
835833

836834
# Gate (z): [1, hidden] -> [1, d_inner]
837-
z = graph_ops.add_matmul_rhs_constant(
838-
network, normed, hidden_size, d_inner,
839-
weights[f"{prefix}.deltanet_z_proj"], dtype=dtype)
835+
z = matmul(
836+
normed, hidden_size, d_inner,
837+
weights[f"{prefix}.deltanet_z_proj"], f"{prefix}.deltanet_z_proj")
840838

841839
# Decay projection (a): [1, hidden] -> [1, num_heads]
842840
a_raw = graph_ops.add_matmul_rhs_constant(
@@ -1109,9 +1107,9 @@ def recurrent_cast(tensor: trt.ITensor) -> trt.ITensor:
11091107
trt.ElementWiseOperation.PROD)
11101108

11111109
# ===== 11. Output projection + residual =====
1112-
out = graph_ops.add_matmul_rhs_constant(
1113-
network, gated.get_output(0), d_inner, hidden_size,
1114-
weights[f"{prefix}.deltanet_out_proj"], dtype=dtype)
1110+
out = matmul(
1111+
gated.get_output(0), d_inner, hidden_size,
1112+
weights[f"{prefix}.deltanet_out_proj"], f"{prefix}.deltanet_out_proj")
11151113

11161114
residual = network.add_elementwise(
11171115
hidden, out, trt.ElementWiseOperation.SUM)
@@ -1129,6 +1127,7 @@ def recurrent_cast(tensor: trt.ITensor) -> trt.ITensor:
11291127
hidden_size=hidden_size,
11301128
mlp_size=mlp_size,
11311129
dtype=dtype,
1130+
quant_ctx=quant_ctx,
11321131
)
11331132

11341133
mlp_residual = network.add_elementwise(
@@ -1165,6 +1164,7 @@ def _add_full_attention_layer(
11651164
max_cache_length: int,
11661165
mlp_size: int,
11671166
dtype: np.dtype = np.float32,
1167+
quant_ctx=None,
11681168
) -> dict[str, trt.ITensor]:
11691169
"""Add one full self-attention layer with output gating.
11701170
@@ -1186,15 +1186,16 @@ def _add_full_attention_layer(
11861186
eps_tensor, "rmsnorm", dtype=dtype)
11871187

11881188
# QKV projections
1189-
q = graph_ops.add_matmul_rhs_constant(
1190-
network, normed, hidden_size, attn_size,
1191-
weights[f"{prefix}.w_q"], dtype=dtype)
1192-
k = graph_ops.add_matmul_rhs_constant(
1193-
network, normed, hidden_size, kv_attention_size,
1194-
weights[f"{prefix}.w_k"], dtype=dtype)
1195-
v = graph_ops.add_matmul_rhs_constant(
1196-
network, normed, hidden_size, kv_attention_size,
1197-
weights[f"{prefix}.w_v"], dtype=dtype)
1189+
matmul = graph_blocks.make_matmul_fn(network, dtype, quant_ctx)
1190+
q = matmul(
1191+
normed, hidden_size, attn_size,
1192+
weights[f"{prefix}.w_q"], f"{prefix}.w_q")
1193+
k = matmul(
1194+
normed, hidden_size, kv_attention_size,
1195+
weights[f"{prefix}.w_k"], f"{prefix}.w_k")
1196+
v = matmul(
1197+
normed, hidden_size, kv_attention_size,
1198+
weights[f"{prefix}.w_v"], f"{prefix}.w_v")
11981199

11991200
# Per-head QK norm
12001201
q_norm = weights.get(f"{prefix}.q_norm")
@@ -1244,19 +1245,18 @@ def _add_full_attention_layer(
12441245
gate_attn_w = weights.get(f"{prefix}.w_gate_attn")
12451246
attn_out = context_flat
12461247
if gate_attn_w is not None:
1247-
gate = graph_ops.add_matmul_rhs_constant(
1248-
network, normed, hidden_size, attn_size, gate_attn_w,
1249-
dtype=dtype)
1248+
gate = matmul(
1249+
normed, hidden_size, attn_size, gate_attn_w, f"{prefix}.w_gate_attn")
12501250
gate_sigmoid = network.add_activation(gate, trt.ActivationType.SIGMOID)
12511251
gated = network.add_elementwise(
12521252
attn_out, gate_sigmoid.get_output(0),
12531253
trt.ElementWiseOperation.PROD)
12541254
attn_out = gated.get_output(0)
12551255

12561256
# Output projection (AFTER gate)
1257-
attn_out = graph_ops.add_matmul_rhs_constant(
1258-
network, attn_out, attn_size, hidden_size,
1259-
weights[f"{prefix}.w_o"], dtype=dtype)
1257+
attn_out = matmul(
1258+
attn_out, attn_size, hidden_size,
1259+
weights[f"{prefix}.w_o"], f"{prefix}.w_o")
12601260

12611261
# Residual after attention
12621262
residual = network.add_elementwise(
@@ -1275,6 +1275,7 @@ def _add_full_attention_layer(
12751275
hidden_size=hidden_size,
12761276
mlp_size=mlp_size,
12771277
dtype=dtype,
1278+
quant_ctx=quant_ctx,
12781279
)
12791280

12801281
mlp_residual = network.add_elementwise(

families/qwen3_8/model.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,10 @@ def build(request, writer) -> None:
9595
raise NotImplementedError("qwen3_8 requires max_batch_size=1")
9696
if request.tensor_parallel_size != 1 or request.context_parallel_size != 1:
9797
raise NotImplementedError("qwen3_8 supports only single-device builds")
98-
if request.quantization not in {None, "none"}:
99-
raise NotImplementedError("qwen3_8 does not expose quantized engine builds")
98+
quantized = request.quantization == "nvfp4"
99+
if request.quantization not in {None, "none", "nvfp4"}:
100+
raise NotImplementedError(
101+
f"qwen3_8 does not support quantization={request.quantization!r}")
100102

101103
model_dir = Path(request.model_dir)
102104
config = ModelConfig.from_dir(model_dir)
@@ -121,12 +123,22 @@ def build(request, writer) -> None:
121123
config.raw["_model_dir"] = str(model_dir)
122124
config.raw["_fp32_layers"] = list(request.fp32_layers)
123125
config.raw["_resolved_build_precision"] = precision
124-
weights = model.load_weights(str(model_dir), config)
126+
config.raw["_quantized_build_requested"] = quantized
127+
128+
quant_ctx = None
129+
if quantized:
130+
from . import graph_ops
131+
from .quantization import calibrate_qwen3_8_nvfp4
132+
133+
quant_ctx = calibrate_qwen3_8_nvfp4(model_dir, config, graph_ops)
134+
135+
weights = model.load_weights(str(model_dir), config, precision=precision)
125136
plan = model.build_engine(
126137
config,
127138
weights,
128139
max_sequence_length,
129140
precision=precision,
141+
quant_ctx=quant_ctx,
130142
verbose=bool(request.verbose),
131143
debug_layer_outputs=False,
132144
)

0 commit comments

Comments
 (0)