1use ferrox_core::matmul::{rms_norm, swiglu};
48use ferrox_core::tensor::Tensor;
49use ferrox_core::weight_matrix::WeightMatrix;
50use ferrox_moe::{
51 combine_expert_outputs, route_top_k_sigmoid_with_bias, run_expert, ExpertWeights,
52};
53
54use crate::glm_dsa::{Glm52AttnState, Glm52AttnWeights, Glm52MlaConfig, IndexerConfig};
55
56pub struct Glm52DenseFfnWeights {
59 pub gate_proj: WeightMatrix,
60 pub up_proj: WeightMatrix,
61 pub down_proj: WeightMatrix,
62}
63
64impl Glm52DenseFfnWeights {
65 fn forward(&self, x: &[f32]) -> Vec<f32> {
66 let gate = self.gate_proj.apply(x);
67 let up = self.up_proj.apply(x);
68 let combined = swiglu(&gate, &up);
69 self.down_proj.apply(&combined)
70 }
71}
72
73pub struct Glm52MoeFfnWeights {
76 pub router_weight: WeightMatrix,
77 pub e_score_correction_bias: Vec<f32>,
78 pub experts: Vec<ExpertWeights>,
79 pub shared_expert: ExpertWeights,
80}
81
82pub enum Glm52LayerFfn {
83 Dense(Box<Glm52DenseFfnWeights>),
84 Moe(Box<Glm52MoeFfnWeights>),
85}
86
87pub struct Glm52DecoderLayerWeights {
88 pub attn_norm_weight: Vec<f32>,
89 pub attn: Glm52AttnWeights,
90 pub ffn_norm_weight: Vec<f32>,
91 pub ffn: Glm52LayerFfn,
92 pub is_full_indexer_layer: bool,
98}
99
100pub struct Glm52DecoderWeights {
101 pub embedding: Tensor, pub layers: Vec<Glm52DecoderLayerWeights>,
103 pub final_norm_weight: Vec<f32>,
104 pub output_head: WeightMatrix, }
106
107pub struct Glm52DecoderConfig {
108 pub rms_norm_eps: f32,
109 pub mla: Glm52MlaConfig,
110 pub indexer: IndexerConfig,
111 pub n_experts_active: usize,
112 pub moe_renormalize: bool,
115 pub routed_scaling_factor: f32,
116}
117
118pub struct Glm52DecodeState {
119 layer_states: Vec<Glm52AttnState>,
120}
121
122impl Glm52DecodeState {
123 pub fn new(weights: &Glm52DecoderWeights) -> Self {
124 Glm52DecodeState {
125 layer_states: weights
126 .layers
127 .iter()
128 .map(|_| Glm52AttnState::new())
129 .collect(),
130 }
131 }
132}
133
134fn moe_ffn_forward(weights: &Glm52MoeFfnWeights, cfg: &Glm52DecoderConfig, x: &[f32]) -> Vec<f32> {
135 let router_logits = weights.router_weight.apply(x);
136 let decision = route_top_k_sigmoid_with_bias(
137 &router_logits,
138 &weights.e_score_correction_bias,
139 cfg.n_experts_active,
140 cfg.moe_renormalize,
141 cfg.routed_scaling_factor,
142 );
143 let routed_outputs: Vec<(Vec<f32>, f32)> = decision
144 .expert_ids
145 .iter()
146 .zip(decision.weights.iter())
147 .map(|(&e, &w)| (run_expert(x, &weights.experts[e]), w))
148 .collect();
149 let shared_out = run_expert(x, &weights.shared_expert);
150 combine_expert_outputs(&routed_outputs, &[shared_out], x.len())
151}
152
153pub fn glm52_forward_token(
158 weights: &Glm52DecoderWeights,
159 cfg: &Glm52DecoderConfig,
160 token_id: usize,
161 state: &mut Glm52DecodeState,
162) -> Vec<f32> {
163 let hidden_dim = weights.embedding.cols();
164 let mut hidden = weights.embedding.row(token_id).to_vec();
165 let mut prev_top_k: Option<Vec<usize>> = None;
166
167 for (layer_idx, layer) in weights.layers.iter().enumerate() {
168 let attn_in = rms_norm(&hidden, &layer.attn_norm_weight, cfg.rms_norm_eps);
169 let attn_out = crate::glm_dsa::glm52_attn_forward_token(
170 &layer.attn,
171 &cfg.mla,
172 &cfg.indexer,
173 &attn_in,
174 cfg.rms_norm_eps,
175 layer.is_full_indexer_layer,
176 &mut state.layer_states[layer_idx],
177 &mut prev_top_k,
178 );
179
180 let mut ffn_in = hidden;
181 for (f, a) in ffn_in.iter_mut().zip(attn_out.iter()) {
182 *f += a;
183 }
184
185 let ffn_normed = rms_norm(&ffn_in, &layer.ffn_norm_weight, cfg.rms_norm_eps);
186 let ffn_out = match &layer.ffn {
187 Glm52LayerFfn::Dense(w) => w.forward(&ffn_normed),
188 Glm52LayerFfn::Moe(w) => moe_ffn_forward(w, cfg, &ffn_normed),
189 };
190
191 let mut next_hidden = ffn_in;
192 for (h, f) in next_hidden.iter_mut().zip(ffn_out.iter()) {
193 *h += f;
194 }
195 hidden = next_hidden;
196 }
197
198 let final_normed = rms_norm(&hidden, &weights.final_norm_weight, cfg.rms_norm_eps);
199 assert_eq!(final_normed.len(), hidden_dim);
200 weights.output_head.apply(&final_normed)
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206 use crate::config::MlaRopeConfig;
207 use crate::glm_dsa::IndexerWeights;
208
209 const HIDDEN_DIM: usize = 8;
210 const EPS: f32 = 1e-5;
211 const NUM_HEADS: usize = 2;
212 const QK_NOPE: usize = 4;
213 const QK_ROPE: usize = 4;
214 const KV_LORA: usize = 4;
215 const Q_LORA: usize = 6;
216 const V_HEAD_DIM: usize = 3;
217 const IDX_N_HEADS: usize = 2;
218 const IDX_HEAD_DIM: usize = 4;
219 const IDX_ROPE_DIM: usize = 2;
220 const TOP_K: usize = 2;
221 const MOE_HIDDEN_DIM: usize = 8;
222 const MOE_FFN_DIM: usize = 3;
223 const N_EXPERTS: usize = 4;
224 const N_EXPERTS_ACTIVE: usize = 2;
225 const OUTPUT_VOCAB: usize = 5;
226 const DENSE_FFN_DIM: usize = 5;
227
228 fn wm(data: Vec<f32>, rows: usize, cols: usize) -> WeightMatrix {
229 assert_eq!(data.len(), rows * cols);
230 WeightMatrix::F32(Tensor::new(data, vec![rows, cols]))
231 }
232
233 fn synth(seed: usize, n: usize) -> Vec<f32> {
237 (0..n)
238 .map(|i| (((seed * 131 + i * 17 + 7) % 23) as f32 * 0.05) - 0.55)
239 .collect()
240 }
241
242 fn make_attn_weights(seed: usize, is_full: bool) -> Glm52AttnWeights {
243 let q_head_dim = QK_NOPE + QK_ROPE;
244 Glm52AttnWeights {
245 q_a_proj: wm(synth(seed + 1, Q_LORA * HIDDEN_DIM), Q_LORA, HIDDEN_DIM),
246 q_a_layernorm: vec![1.0; Q_LORA],
247 q_b_proj: wm(
248 synth(seed + 2, NUM_HEADS * q_head_dim * Q_LORA),
249 NUM_HEADS * q_head_dim,
250 Q_LORA,
251 ),
252 kv_a_proj_with_mqa: wm(
253 synth(seed + 3, (KV_LORA + QK_ROPE) * HIDDEN_DIM),
254 KV_LORA + QK_ROPE,
255 HIDDEN_DIM,
256 ),
257 kv_a_layernorm: vec![1.0; KV_LORA],
258 wk_b: (0..NUM_HEADS)
259 .map(|h| wm(synth(seed + 4 + h, QK_NOPE * KV_LORA), QK_NOPE, KV_LORA))
260 .collect(),
261 wv_b: (0..NUM_HEADS)
262 .map(|h| {
263 wm(
264 synth(seed + 6 + h, V_HEAD_DIM * KV_LORA),
265 V_HEAD_DIM,
266 KV_LORA,
267 )
268 })
269 .collect(),
270 o_proj: wm(
271 synth(seed + 8, HIDDEN_DIM * NUM_HEADS * V_HEAD_DIM),
272 HIDDEN_DIM,
273 NUM_HEADS * V_HEAD_DIM,
274 ),
275 indexer: if is_full {
276 Some(IndexerWeights {
277 k_norm_weight: vec![1.0; IDX_HEAD_DIM],
278 k_norm_bias: vec![0.0; IDX_HEAD_DIM],
279 proj: wm(
280 synth(seed + 9, IDX_N_HEADS * HIDDEN_DIM),
281 IDX_N_HEADS,
282 HIDDEN_DIM,
283 ),
284 attn_k: wm(
285 synth(seed + 10, IDX_HEAD_DIM * HIDDEN_DIM),
286 IDX_HEAD_DIM,
287 HIDDEN_DIM,
288 ),
289 attn_q_b: wm(
290 synth(seed + 11, IDX_N_HEADS * IDX_HEAD_DIM * Q_LORA),
291 IDX_N_HEADS * IDX_HEAD_DIM,
292 Q_LORA,
293 ),
294 })
295 } else {
296 None
297 },
298 }
299 }
300
301 fn make_weights() -> Glm52DecoderWeights {
302 let layer0 = Glm52DecoderLayerWeights {
303 attn_norm_weight: vec![1.0; HIDDEN_DIM],
304 attn: make_attn_weights(100, true),
305 ffn_norm_weight: vec![1.0; HIDDEN_DIM],
306 ffn: Glm52LayerFfn::Dense(Box::new(Glm52DenseFfnWeights {
307 gate_proj: wm(
308 synth(200, DENSE_FFN_DIM * HIDDEN_DIM),
309 DENSE_FFN_DIM,
310 HIDDEN_DIM,
311 ),
312 up_proj: wm(
313 synth(201, DENSE_FFN_DIM * HIDDEN_DIM),
314 DENSE_FFN_DIM,
315 HIDDEN_DIM,
316 ),
317 down_proj: wm(
318 synth(202, HIDDEN_DIM * DENSE_FFN_DIM),
319 HIDDEN_DIM,
320 DENSE_FFN_DIM,
321 ),
322 })),
323 is_full_indexer_layer: true,
324 };
325
326 let expert = |seed: usize| ExpertWeights {
327 gate: wm(
328 synth(seed, MOE_FFN_DIM * MOE_HIDDEN_DIM),
329 MOE_FFN_DIM,
330 MOE_HIDDEN_DIM,
331 ),
332 up: wm(
333 synth(seed + 1, MOE_FFN_DIM * MOE_HIDDEN_DIM),
334 MOE_FFN_DIM,
335 MOE_HIDDEN_DIM,
336 ),
337 down: wm(
338 synth(seed + 2, MOE_HIDDEN_DIM * MOE_FFN_DIM),
339 MOE_HIDDEN_DIM,
340 MOE_FFN_DIM,
341 ),
342 };
343
344 let layer1 = Glm52DecoderLayerWeights {
345 attn_norm_weight: vec![1.0; HIDDEN_DIM],
346 attn: make_attn_weights(300, false),
347 ffn_norm_weight: vec![1.0; HIDDEN_DIM],
348 ffn: Glm52LayerFfn::Moe(Box::new(Glm52MoeFfnWeights {
349 router_weight: wm(synth(400, N_EXPERTS * HIDDEN_DIM), N_EXPERTS, HIDDEN_DIM),
350 e_score_correction_bias: vec![0.0; N_EXPERTS],
351 experts: (0..N_EXPERTS).map(|e| expert(500 + e * 10)).collect(),
352 shared_expert: expert(900),
353 })),
354 is_full_indexer_layer: false,
355 };
356
357 Glm52DecoderWeights {
358 embedding: Tensor::new(
359 synth(1000, OUTPUT_VOCAB * HIDDEN_DIM),
360 vec![OUTPUT_VOCAB, HIDDEN_DIM],
361 ),
362 layers: vec![layer0, layer1],
363 final_norm_weight: vec![1.0; HIDDEN_DIM],
364 output_head: wm(
365 synth(1100, OUTPUT_VOCAB * HIDDEN_DIM),
366 OUTPUT_VOCAB,
367 HIDDEN_DIM,
368 ),
369 }
370 }
371
372 fn decoder_cfg() -> Glm52DecoderConfig {
373 Glm52DecoderConfig {
374 rms_norm_eps: EPS,
375 mla: Glm52MlaConfig {
376 num_heads: NUM_HEADS,
377 q_lora_rank: Q_LORA,
378 kv_lora_rank: KV_LORA,
379 qk_nope_head_dim: QK_NOPE,
380 qk_rope_head_dim: QK_ROPE,
381 v_head_dim: V_HEAD_DIM,
382 rope: MlaRopeConfig { theta: 8_000_000.0 },
383 },
384 indexer: IndexerConfig {
385 n_heads: IDX_N_HEADS,
386 head_dim: IDX_HEAD_DIM,
387 rope_dim: IDX_ROPE_DIM,
388 top_k: TOP_K,
389 rope_theta: 8_000_000.0,
390 },
391 n_experts_active: N_EXPERTS_ACTIVE,
392 moe_renormalize: true,
393 routed_scaling_factor: 2.5,
394 }
395 }
396
397 #[test]
398 fn two_mixed_layers_run_end_to_end_across_three_tokens() {
399 let weights = make_weights();
400 let cfg = decoder_cfg();
401 let mut state = Glm52DecodeState::new(&weights);
402
403 for token_id in 0..3 {
404 let logits = glm52_forward_token(&weights, &cfg, token_id % OUTPUT_VOCAB, &mut state);
405 assert_eq!(logits.len(), OUTPUT_VOCAB);
406 assert!(
407 logits.iter().all(|v| v.is_finite()),
408 "token {token_id}: logits must be finite, got {logits:?}"
409 );
410 }
411 }
412
413 #[test]
414 fn shared_layer_without_a_prior_full_layer_in_the_same_token_panics() {
415 let mut weights = make_weights();
421 weights.layers.truncate(1);
422 weights.layers[0].is_full_indexer_layer = false;
423 weights.layers[0].attn.indexer = None;
424
425 let cfg = decoder_cfg();
426 let mut state = Glm52DecodeState::new(&weights);
427
428 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
429 glm52_forward_token(&weights, &cfg, 0, &mut state)
430 }));
431 assert!(
432 result.is_err(),
433 "a lone \"shared\" layer with no preceding \"full\" layer must panic"
434 );
435 }
436}