metrale_model_layers/layers/qwen3_attention/
circuit.rs1use std::collections::BTreeMap;
17
18use metrale_circuit::LinearRole;
19
20use super::Qwen3AttentionLayer;
21use crate::circuit_exec::{
22 AttnFacts, BoundWeight, CircuitBindings, CircuitLayer, MixerFacts, RopeFacts, WeightSlot,
23};
24use crate::weight_map::{DenseWeight, QuantWeight};
25
26fn proj(
29 w: Option<&QuantWeight>,
30 dense: DenseWeight,
31 what: &str,
32 unmodelled: &mut Vec<String>,
33) -> BoundWeight {
34 match w {
35 Some(QuantWeight::Nvfp4(q)) => BoundWeight::Nvfp4(*q),
36 Some(QuantWeight::Dense(d)) => BoundWeight::Dense(*d),
37 Some(QuantWeight::Fp8(_)) => {
38 unmodelled.push(format!("an FP8 {what} projection"));
39 BoundWeight::Dense(dense)
40 }
41 Some(QuantWeight::PackedQ2(_)) => {
42 unmodelled.push(format!("a packed-Q2 {what} projection"));
43 BoundWeight::Dense(dense)
44 }
45 None => BoundWeight::Dense(dense),
46 }
47}
48
49fn bind_w8a8(
54 w: &crate::layers::W8a8Mixer,
55 weights: &mut BTreeMap<WeightSlot, BoundWeight>,
56 unmodelled: &mut Vec<String>,
57) {
58 if w.input.segments() != 3 || !w.ctx.kernels.resolved(w.input.scale()) {
59 unmodelled.push("a W8A8 attention input that is not a resolved Q|K|V stack".to_string());
60 return;
61 }
62 let roles = [LinearRole::Q, LinearRole::K, LinearRole::V];
63 for (i, role) in roles.into_iter().enumerate() {
64 match w.input.segment(i) {
65 Ok(seg) => {
66 weights.insert(
67 WeightSlot::Linear(role),
68 BoundWeight::W8a8(seg, w.ctx.kernels),
69 );
70 }
71 Err(e) => unmodelled.push(format!("W8A8 {role:?} segment: {e:#}")),
72 }
73 }
74 weights.insert(
75 WeightSlot::Linear(LinearRole::O),
76 BoundWeight::W8a8(w.output, w.ctx.kernels),
77 );
78}
79
80impl CircuitBindings for Qwen3AttentionLayer {
81 fn circuit_prepare(
82 &self,
83 gpu: &dyn metrale_gpu_runtime::gpu::GpuBackend,
84 config: &metrale_config::ModelConfig,
85 levers: &crate::layers::ops::ModelLevers,
86 stream: u64,
87 ) -> anyhow::Result<()> {
88 self.ffn.circuit_prepare(gpu, config, levers, stream)
89 }
90
91 fn circuit_layer(
92 &self,
93 config: &metrale_config::ModelConfig,
94 levers: &crate::layers::ops::ModelLevers,
95 ) -> Option<CircuitLayer> {
96 let mut unmodelled = Vec::new();
97 let arms = [
98 (self.hc.is_some(), "hyper-connections"),
99 (self.lora.is_some(), "an attention LoRA adapter"),
100 (self.mla.is_some(), "multi-head latent attention"),
101 (self.qsa.is_some(), "a QSA indexer"),
102 (self.v_norm_weight.is_some(), "a V norm"),
103 (
104 self.head_gate_weight.is_some(),
105 "a per-head gate projection",
106 ),
107 (
108 self.post_attn_out_norm.is_some(),
109 "a post-attention output norm",
110 ),
111 (self.post_ffn_out_norm.is_some(), "a post-FFN output norm"),
112 (self.layer_scalar.is_some(), "a layer scalar"),
113 (self.moe_ffn.is_some(), "a second (MoE) FFN"),
114 (
115 self.shortcut_carry_in.is_some() || self.shortcut_carry_out.is_some(),
116 "a shortcut carry",
117 ),
118 (
119 self.attn.q_norm_full.is_some() || self.attn.k_norm_full.is_some(),
120 "a full-width Q/K norm",
121 ),
122 (!self.yarn_inv_freq.is_null(), "YaRN RoPE"),
123 (
124 self.rope_proportional && self.rope_proportional_k.0 != 0,
125 "proportional RoPE",
126 ),
127 (self.o_dense_bf16.is_some(), "a BF16 output projection"),
128 (
129 self.qkv_nvfp4_t.is_some()
130 && super::trait_impl::multi_seq::qkv::fused_qkv_enabled(),
131 "a fused [q | k | v] transposed twin",
132 ),
133 ];
134 for (present, what) in arms {
135 if present {
136 unmodelled.push(what.to_string());
137 }
138 }
139 let dense = BoundWeight::Dense;
140 let mut weights = BTreeMap::from([
141 (WeightSlot::InputNorm, dense(self.input_norm)),
142 (WeightSlot::PostNorm, dense(self.post_attn_norm)),
143 (WeightSlot::QNorm, dense(self.attn.q_norm)),
144 (WeightSlot::KNorm, dense(self.attn.k_norm)),
145 ]);
146 let q = proj(
147 self.q_weight.as_ref(),
148 self.attn.q_proj,
149 "q",
150 &mut unmodelled,
151 );
152 let k = proj(
153 self.k_weight.as_ref(),
154 self.attn.k_proj,
155 "k",
156 &mut unmodelled,
157 );
158 let v = proj(
159 self.v_weight.as_ref(),
160 self.attn.v_proj,
161 "v",
162 &mut unmodelled,
163 );
164 weights.insert(WeightSlot::Linear(LinearRole::Q), q);
165 weights.insert(WeightSlot::Linear(LinearRole::K), k);
166 weights.insert(WeightSlot::Linear(LinearRole::V), v);
167 if matches!(
168 self.o_weight,
169 Some(QuantWeight::Fp8(_)) | Some(QuantWeight::PackedQ2(_))
170 ) {
171 unmodelled.push("an FP8 or packed-Q2 output projection".to_string());
172 }
173 weights.insert(
174 WeightSlot::Linear(LinearRole::O),
175 BoundWeight::Nvfp4(self.attn.o_proj),
176 );
177 if let Some(w) = self.w8a8 {
178 bind_w8a8(&w, &mut weights, &mut unmodelled);
179 }
180 for (role, twin) in [
181 (LinearRole::Q, self.q_nvfp4_t),
182 (LinearRole::K, self.k_nvfp4_t),
183 (LinearRole::V, self.v_nvfp4_t),
184 (LinearRole::O, self.o_nvfp4_t),
185 ] {
186 if let Some(t) = twin {
187 weights.insert(WeightSlot::Transposed(role), BoundWeight::Nvfp4(t));
188 }
189 }
190 self.ffn.circuit_bind(levers, &mut weights, &mut unmodelled);
191 let nq = self
192 .num_q_heads_override
193 .unwrap_or(config.num_attention_heads) as u32;
194 let nkv = self
195 .num_kv_heads_override
196 .unwrap_or(config.num_key_value_heads) as u32;
197 let hd = self.head_dim_override.unwrap_or(config.head_dim) as u32;
198 let facts = AttnFacts {
199 attn_layer_idx: self.attn_layer_idx,
200 kv_dtype: self.kv_dtype,
201 num_q_heads: nq,
202 num_kv_heads: nkv,
203 head_dim: hd,
204 gated: self.gated,
205 rope: RopeFacts {
206 mrope_interleaved: self.mrope_interleaved && self.rope_mrope_interleaved_k.0 != 0,
207 theta: self.rope_theta_override.unwrap_or(config.rope_theta as f32),
208 rotary_dim: self
209 .rotary_dim_override
210 .unwrap_or(config.rotary_dim() as u32),
211 },
212 sliding_window: self.sliding_window.unwrap_or(0),
213 softmax_scale: self.effective_attn_scale(hd),
214 paged_decode_plain_rows: (1..=128u32)
215 .filter(|&r| self.bf16_decode_is_plain(nq, nkv, hd, r, levers.max_decode_seqs))
216 .fold(0u128, |m, r| m | 1 << (r - 1)),
217 };
218 Some(CircuitLayer {
219 mixer: MixerFacts::Attention(facts),
220 weights,
221 unmodelled,
222 })
223 }
224}