metrale_model_layers/layers/qwen3_ssm/init_fp8.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: FP8 weight-install setters and the NVFP4→FP8 `out_proj`
4//! pre-dequant for [`Qwen3SsmLayer`].
5//!
6//! Owner: model-layers (qwen3_ssm).
7//! Invariants:
8//! - `set_fp8_decode_weights` panics unless each weight is tagged
9//! `Fp8BlockScaled`, and `set_fp8_rowwise_prefill_weights` unless each is
10//! tagged `Fp8PerRow`, so neither FP8 layout reaches the other's readers.
11
12use anyhow::Result;
13use metrale_gpu_runtime::gpu::{DevicePtr, GpuBackend};
14
15use super::Qwen3SsmLayer;
16use crate::weight_map::Fp8Weight;
17
18impl Qwen3SsmLayer {
19 /// 2026-09-25: Install block-scaled FP8 weights as `qkvz_fp8w` /
20 /// `out_proj_fp8w`. Panics unless each is tagged `Fp8BlockScaled`: their
21 /// readers include `w8a16_gemv`, which computes
22 /// `sum_k A[k] * E4M3_LUT[B[n,k]] * block_scale[n/128, k/128]` with FP32
23 /// scales (`kernels/gb10/common/w8a16_gemv.cu`).
24 ///
25 /// It does not touch `qkvz_fp8` / `out_proj_fp8`: those feed
26 /// `fp8_gemm_n128`, which takes no scale argument, so block-scaled bytes
27 /// there would be read unscaled. `set_fp8_prefill_only_weights` installs
28 /// them.
29 pub fn set_fp8_decode_weights(&mut self, qkvz: Option<Fp8Weight>, out_proj: Option<Fp8Weight>) {
30 if let Some(ref w) = qkvz {
31 w.scale_format.expect(
32 crate::weight_map::WeightQuantFormat::Fp8BlockScaled,
33 "set_fp8_decode_weights::qkvz (w8a16_gemv expects [N/BS,K/BS] BF16 block scales)",
34 );
35 }
36 if let Some(ref w) = out_proj {
37 w.scale_format.expect(
38 crate::weight_map::WeightQuantFormat::Fp8BlockScaled,
39 "set_fp8_decode_weights::out_proj (w8a16_gemv expects [N/BS,K/BS] BF16 block scales)",
40 );
41 }
42 self.qkvz_fp8w = qkvz;
43 self.out_proj_fp8w = out_proj;
44 }
45
46 /// 2026-09-25: Install per-row FP8 weights as `qkvz_fp8w_rowwise` /
47 /// `out_proj_fp8w_rowwise`. Panics unless each is tagged `Fp8PerRow`. Only
48 /// the row-wise prefill arms read them (`trait_prefill_proj.rs`,
49 /// `trait_prefill_helper.rs`); decode never does.
50 pub fn set_fp8_rowwise_prefill_weights(
51 &mut self,
52 qkvz: Option<Fp8Weight>,
53 out_proj: Option<Fp8Weight>,
54 ) {
55 for (w, what) in [(&qkvz, "qkvz"), (&out_proj, "out_proj")] {
56 if let Some(w) = w {
57 w.scale_format.expect(
58 crate::weight_map::WeightQuantFormat::Fp8PerRow,
59 "set_fp8_rowwise_prefill_weights (cuBLASLt row-wise expects [N] f32)",
60 );
61 let _ = what;
62 }
63 }
64 self.qkvz_fp8w_rowwise = qkvz;
65 self.out_proj_fp8w_rowwise = out_proj;
66 }
67
68 /// 2026-09-25: Install unscaled FP8 weights as `qkvz_fp8` / `out_proj_fp8`,
69 /// the operands of the `fp8_gemm_n128` arms in prefill and in the batched
70 /// decode/verify projections (`trait_decode_batched.rs`). A `None`
71 /// argument leaves that field as it was.
72 pub fn set_fp8_prefill_only_weights(
73 &mut self,
74 qkvz_fp8: Option<DevicePtr>,
75 out_proj_fp8: Option<DevicePtr>,
76 ) {
77 if qkvz_fp8.is_some() {
78 self.qkvz_fp8 = qkvz_fp8;
79 }
80 if out_proj_fp8.is_some() {
81 self.out_proj_fp8 = out_proj_fp8;
82 }
83 }
84
85 /// 2026-09-25: When the layer holds `out_proj_nvfp4_t`, dequantize the NVFP4
86 /// `out_proj` to FP8 into a new allocation and store it as `out_proj_fp8`.
87 /// QKVZ is not converted. Synchronizes `stream`.
88 pub fn predequant_for_prefill(
89 &mut self,
90 gpu: &dyn GpuBackend,
91 config: &metrale_config::ModelConfig,
92 stream: u64,
93 ) -> Result<()> {
94 let predequant_k = gpu.kernel("w4a16", "predequant_nvfp4_to_fp8")?;
95 let h = config.hidden_size;
96 let qkvz_size = config.ssm_qkvz_size();
97 let value_dim = config.linear_num_value_heads * config.linear_value_head_dim;
98
99 let _ = qkvz_size;
100 // 2026-09-25: `predequant_nvfp4_to_fp8` reads a packed `[N, K/2]` weight,
101 // so this passes the untransposed `ssm.out_proj`.
102 if self.out_proj_nvfp4_t.is_some() {
103 self.out_proj_fp8 = Some(self.ssm.out_proj.predequant_to_fp8(
104 gpu,
105 predequant_k,
106 h,
107 value_dim,
108 stream,
109 )?);
110 }
111 Ok(())
112 }
113}