1use ferrox_core::attention::apply_rope_back;
34use ferrox_core::csa_hca_compress::compress_block;
35use ferrox_core::deepseek_v4_attention::hca_attention;
36use ferrox_core::matmul::rms_norm;
37use ferrox_core::tensor::Tensor;
38use ferrox_core::weight_matrix::WeightMatrix;
39use ferrox_moe::{combine_expert_outputs, route_top_k, run_expert, ExpertWeights, GatingFunction};
40
41use crate::hyper_connections::{
42 head as hc_head, post as hc_post, pre as hc_pre, HyperConnectionHeadWeights,
43 HyperConnectionPreWeights, HC_MULT,
44};
45use crate::output_projection::grouped_output_projection;
46
47pub struct DeepseekV4AttnWeights {
49 pub q_proj: WeightMatrix,
50 pub k_proj: WeightMatrix,
51 pub v_proj: WeightMatrix,
52 pub group_down: Vec<WeightMatrix>,
54 pub wo_b: WeightMatrix,
55 pub comp_gate: WeightMatrix,
57 pub comp_norm: Vec<f32>,
58}
59
60pub struct DeepseekV4MoeFfnWeights {
62 pub router_weight: WeightMatrix,
63 pub experts: Vec<ExpertWeights>,
64 pub shared_expert: ExpertWeights,
65}
66
67pub struct DeepseekV4DecoderLayerWeights {
68 pub attn_hc_pre: HyperConnectionPreWeights,
69 pub attn_norm_weight: Vec<f32>,
70 pub attn: DeepseekV4AttnWeights,
71 pub ffn_hc_pre: HyperConnectionPreWeights,
72 pub ffn_norm_weight: Vec<f32>,
73 pub ffn: DeepseekV4MoeFfnWeights,
74}
75
76pub struct DeepseekV4DecoderWeights {
77 pub embedding: Tensor,
78 pub layer: DeepseekV4DecoderLayerWeights,
79 pub hc_head: HyperConnectionHeadWeights,
80 pub final_norm_weight: Vec<f32>,
81 pub output_head: WeightMatrix,
82}
83
84pub struct DeepseekV4DecoderConfig {
85 pub rms_norm_eps: f32,
86 pub hc_sinkhorn_iters: u32,
87 pub hc_eps: f32,
88 pub n_heads: usize,
89 pub qk_head_dim: usize,
90 pub v_head_dim: usize,
91 pub qk_rope: usize,
92 pub compress_rope_theta: f32,
93 pub n_experts_active: usize,
94 pub moe_renormalize: bool,
95 pub hca_compress_ratio: usize,
97}
98
99pub struct DeepseekV4LayerState {
104 hc_streams: [Vec<f32>; HC_MULT],
105 raw_k: Vec<f32>,
106 raw_v: Vec<f32>,
107 compressed_k: Vec<f32>,
108 compressed_v: Vec<f32>,
109 token_count: usize,
110}
111
112impl DeepseekV4LayerState {
113 pub fn new(hidden_dim: usize) -> Self {
114 let zero = vec![0.0; hidden_dim];
115 Self {
116 hc_streams: std::array::from_fn(|_| zero.clone()),
117 raw_k: Vec::new(),
118 raw_v: Vec::new(),
119 compressed_k: Vec::new(),
120 compressed_v: Vec::new(),
121 token_count: 0,
122 }
123 }
124
125 fn reset_hc_from_hidden(&mut self, hidden: &[f32]) {
126 for stream in self.hc_streams.iter_mut() {
127 stream.copy_from_slice(hidden);
128 }
129 }
130}
131
132pub struct DeepseekV4DecodeState {
133 layer: DeepseekV4LayerState,
134}
135
136impl DeepseekV4DecodeState {
137 pub fn new(hidden_dim: usize) -> Self {
138 Self {
139 layer: DeepseekV4LayerState::new(hidden_dim),
140 }
141 }
142}
143
144fn derope_attn_out(
145 attn_out: &mut [f32],
146 n_heads: usize,
147 v_head_dim: usize,
148 qk_rope: usize,
149 pos: usize,
150 theta: f32,
151) {
152 assert!(qk_rope <= v_head_dim);
153 for h in 0..n_heads {
154 let head_start = h * v_head_dim;
155 let rope_start = head_start + v_head_dim - qk_rope;
156 apply_rope_back(
157 &mut attn_out[rope_start..head_start + v_head_dim],
158 pos,
159 theta,
160 );
161 }
162}
163
164fn attn_forward_token(
165 weights: &DeepseekV4AttnWeights,
166 cfg: &DeepseekV4DecoderConfig,
167 attn_in: &[f32],
168 state: &mut DeepseekV4LayerState,
169) -> Vec<f32> {
170 let q = weights.q_proj.apply(attn_in);
171 let k = weights.k_proj.apply(attn_in);
172 let v = weights.v_proj.apply(attn_in);
173 debug_assert_eq!(q.len(), cfg.n_heads * cfg.qk_head_dim);
174 debug_assert_eq!(k.len(), cfg.n_heads * cfg.qk_head_dim);
175 debug_assert_eq!(v.len(), cfg.n_heads * cfg.v_head_dim);
176
177 state.raw_k.extend_from_slice(&k);
178 state.raw_v.extend_from_slice(&v);
179 state.token_count += 1;
180 let n_raw = state.token_count;
181
182 let ratio = cfg.hca_compress_ratio;
183 if n_raw >= ratio && n_raw.is_multiple_of(ratio) {
184 let per_token_k = cfg.n_heads * cfg.qk_head_dim;
185 let per_token_v = cfg.n_heads * cfg.v_head_dim;
186 let block_start = (n_raw - ratio) * per_token_k;
187 let block_end = n_raw * per_token_k;
188 let kv_block: Vec<Vec<f32>> = state.raw_k[block_start..block_end]
189 .chunks(cfg.qk_head_dim)
190 .map(|row| row.to_vec())
191 .collect();
192 let score_block: Vec<Vec<f32>> = kv_block
193 .iter()
194 .map(|row| weights.comp_gate.apply(row))
195 .collect();
196 let compressed_k = compress_block(
197 &kv_block,
198 &score_block,
199 &weights.comp_norm,
200 cfg.rms_norm_eps,
201 cfg.qk_rope,
202 n_raw / ratio,
203 cfg.compress_rope_theta,
204 );
205 let v_block_start = (n_raw - ratio) * per_token_v;
206 let v_block_end = n_raw * per_token_v;
207 let v_block: Vec<Vec<f32>> = state.raw_v[v_block_start..v_block_end]
208 .chunks(cfg.v_head_dim)
209 .map(|row| row.to_vec())
210 .collect();
211 let compressed_v = compress_block(
212 &v_block,
213 &score_block,
214 &weights.comp_norm,
215 cfg.rms_norm_eps,
216 cfg.qk_rope,
217 n_raw / ratio,
218 cfg.compress_rope_theta,
219 );
220 state.compressed_k.extend_from_slice(&compressed_k);
221 state.compressed_v.extend_from_slice(&compressed_v);
222 }
223
224 let n_compressed = if cfg.qk_head_dim > 0 {
225 state.compressed_k.len() / (cfg.n_heads * cfg.qk_head_dim)
226 } else {
227 0
228 };
229 let mut attn_out = hca_attention(
230 &q,
231 &state.raw_k,
232 &state.raw_v,
233 n_raw,
234 &state.compressed_k,
235 &state.compressed_v,
236 n_compressed,
237 cfg.n_heads,
238 cfg.qk_head_dim,
239 cfg.v_head_dim,
240 );
241
242 derope_attn_out(
243 &mut attn_out,
244 cfg.n_heads,
245 cfg.v_head_dim,
246 cfg.qk_rope,
247 state.token_count.saturating_sub(1),
248 cfg.compress_rope_theta,
249 );
250
251 grouped_output_projection(&attn_out, &weights.group_down, &weights.wo_b)
252}
253
254fn moe_ffn_forward(
255 weights: &DeepseekV4MoeFfnWeights,
256 cfg: &DeepseekV4DecoderConfig,
257 x: &[f32],
258) -> Vec<f32> {
259 let router_logits = weights.router_weight.apply(x);
260 let decision = route_top_k(
261 &router_logits,
262 cfg.n_experts_active,
263 GatingFunction::SqrtSoftplus,
264 cfg.moe_renormalize,
265 );
266 let routed_outputs: Vec<(Vec<f32>, f32)> = decision
267 .expert_ids
268 .iter()
269 .zip(decision.weights.iter())
270 .map(|(&e, &w)| (run_expert(x, &weights.experts[e]), w))
271 .collect();
272 let shared_out = run_expert(x, &weights.shared_expert);
273 combine_expert_outputs(&routed_outputs, &[shared_out], x.len())
274}
275
276pub fn deepseek_v4_forward_token(
279 weights: &DeepseekV4DecoderWeights,
280 cfg: &DeepseekV4DecoderConfig,
281 token_id: usize,
282 state: &mut DeepseekV4DecodeState,
283) -> Vec<f32> {
284 let hidden_dim = weights.embedding.cols();
285 let hidden = weights.embedding.row(token_id).to_vec();
286 state.layer.reset_hc_from_hidden(&hidden);
287 let layer = &weights.layer;
288
289 let hc_residual = state.layer.hc_streams.clone();
290 let (attn_in, attn_post, attn_comb) = hc_pre(
291 &layer.attn_hc_pre,
292 &hc_residual,
293 cfg.rms_norm_eps,
294 cfg.hc_sinkhorn_iters,
295 cfg.hc_eps,
296 );
297 let attn_normed = rms_norm(&attn_in, &layer.attn_norm_weight, cfg.rms_norm_eps);
298 let attn_out = attn_forward_token(&layer.attn, cfg, &attn_normed, &mut state.layer);
299 state.layer.hc_streams = hc_post(&attn_out, &hc_residual, &attn_post, &attn_comb);
300
301 let hc_residual = state.layer.hc_streams.clone();
302 let (ffn_in, ffn_post, ffn_comb) = hc_pre(
303 &layer.ffn_hc_pre,
304 &hc_residual,
305 cfg.rms_norm_eps,
306 cfg.hc_sinkhorn_iters,
307 cfg.hc_eps,
308 );
309 let ffn_normed = rms_norm(&ffn_in, &layer.ffn_norm_weight, cfg.rms_norm_eps);
310 let ffn_out = moe_ffn_forward(&layer.ffn, cfg, &ffn_normed);
311 state.layer.hc_streams = hc_post(&ffn_out, &hc_residual, &ffn_post, &ffn_comb);
312
313 let collapsed = hc_head(
314 &weights.hc_head,
315 &state.layer.hc_streams,
316 cfg.rms_norm_eps,
317 cfg.hc_eps,
318 );
319 let final_normed = rms_norm(&collapsed, &weights.final_norm_weight, cfg.rms_norm_eps);
320 assert_eq!(final_normed.len(), hidden_dim);
321 weights.output_head.apply(&final_normed)
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327 use crate::hyper_connections::HyperConnectionPreWeights;
328
329 const HIDDEN_DIM: usize = 8;
330 const EPS: f32 = 1e-5;
331 const NUM_HEADS: usize = 1;
332 const QK_HEAD_DIM: usize = 4;
333 const V_HEAD_DIM: usize = 4;
334 const QK_ROPE: usize = 2;
335 const N_GROUPS: usize = 1;
336 const O_LORA_RANK: usize = 2;
337 const O_GROUP_DIM: usize = (NUM_HEADS * V_HEAD_DIM) / N_GROUPS;
338 const N_EXPERTS: usize = 4;
339 const N_EXPERTS_ACTIVE: usize = 2;
340 const MOE_FFN_DIM: usize = 3;
341 const OUTPUT_VOCAB: usize = 5;
342 const HC_FLAT: usize = HC_MULT * HIDDEN_DIM;
343
344 fn wm(data: Vec<f32>, rows: usize, cols: usize) -> WeightMatrix {
345 assert_eq!(data.len(), rows * cols);
346 WeightMatrix::F32(Tensor::new(data, vec![rows, cols]))
347 }
348
349 fn synth(seed: usize, n: usize) -> Vec<f32> {
350 (0..n)
351 .map(|i| (((seed * 131 + i * 17 + 7) % 23) as f32 * 0.05) - 0.55)
352 .collect()
353 }
354
355 fn make_hc_pre(seed: usize) -> HyperConnectionPreWeights {
356 HyperConnectionPreWeights {
357 fn_proj: wm(
358 synth(seed, (2 + HC_MULT) * HC_MULT * HC_FLAT),
359 (2 + HC_MULT) * HC_MULT,
360 HC_FLAT,
361 ),
362 scale: [0.5, 0.5, 0.5],
363 base_pre: [0.1; HC_MULT],
364 base_post: [0.2; HC_MULT],
365 base_comb: [0.01; HC_MULT * HC_MULT],
366 }
367 }
368
369 fn make_hc_head(seed: usize) -> HyperConnectionHeadWeights {
370 HyperConnectionHeadWeights {
371 fn_proj: wm(synth(seed, HC_MULT * HC_FLAT), HC_MULT, HC_FLAT),
372 scale: 0.5,
373 base: [0.1; HC_MULT],
374 }
375 }
376
377 fn make_weights() -> DeepseekV4DecoderWeights {
378 let expert = |seed: usize| ExpertWeights {
379 gate: wm(
380 synth(seed, MOE_FFN_DIM * HIDDEN_DIM),
381 MOE_FFN_DIM,
382 HIDDEN_DIM,
383 ),
384 up: wm(
385 synth(seed + 1, MOE_FFN_DIM * HIDDEN_DIM),
386 MOE_FFN_DIM,
387 HIDDEN_DIM,
388 ),
389 down: wm(
390 synth(seed + 2, HIDDEN_DIM * MOE_FFN_DIM),
391 HIDDEN_DIM,
392 MOE_FFN_DIM,
393 ),
394 };
395
396 DeepseekV4DecoderWeights {
397 embedding: Tensor::new(
398 synth(1000, OUTPUT_VOCAB * HIDDEN_DIM),
399 vec![OUTPUT_VOCAB, HIDDEN_DIM],
400 ),
401 layer: DeepseekV4DecoderLayerWeights {
402 attn_hc_pre: make_hc_pre(100),
403 attn_norm_weight: vec![1.0; HIDDEN_DIM],
404 attn: DeepseekV4AttnWeights {
405 q_proj: wm(
406 synth(110, NUM_HEADS * QK_HEAD_DIM * HIDDEN_DIM),
407 NUM_HEADS * QK_HEAD_DIM,
408 HIDDEN_DIM,
409 ),
410 k_proj: wm(
411 synth(111, NUM_HEADS * QK_HEAD_DIM * HIDDEN_DIM),
412 NUM_HEADS * QK_HEAD_DIM,
413 HIDDEN_DIM,
414 ),
415 v_proj: wm(
416 synth(112, NUM_HEADS * V_HEAD_DIM * HIDDEN_DIM),
417 NUM_HEADS * V_HEAD_DIM,
418 HIDDEN_DIM,
419 ),
420 group_down: (0..N_GROUPS)
421 .map(|g| {
422 wm(
423 synth(120 + g, O_LORA_RANK * O_GROUP_DIM),
424 O_LORA_RANK,
425 O_GROUP_DIM,
426 )
427 })
428 .collect(),
429 wo_b: wm(
430 synth(130, HIDDEN_DIM * O_LORA_RANK * N_GROUPS),
431 HIDDEN_DIM,
432 O_LORA_RANK * N_GROUPS,
433 ),
434 comp_gate: wm(
435 synth(140, QK_HEAD_DIM * QK_HEAD_DIM),
436 QK_HEAD_DIM,
437 QK_HEAD_DIM,
438 ),
439 comp_norm: vec![1.0; QK_HEAD_DIM],
440 },
441 ffn_hc_pre: make_hc_pre(200),
442 ffn_norm_weight: vec![1.0; HIDDEN_DIM],
443 ffn: DeepseekV4MoeFfnWeights {
444 router_weight: wm(synth(300, N_EXPERTS * HIDDEN_DIM), N_EXPERTS, HIDDEN_DIM),
445 experts: (0..N_EXPERTS).map(|e| expert(400 + e * 10)).collect(),
446 shared_expert: expert(900),
447 },
448 },
449 hc_head: make_hc_head(500),
450 final_norm_weight: vec![1.0; HIDDEN_DIM],
451 output_head: wm(
452 synth(1100, OUTPUT_VOCAB * HIDDEN_DIM),
453 OUTPUT_VOCAB,
454 HIDDEN_DIM,
455 ),
456 }
457 }
458
459 fn decoder_cfg() -> DeepseekV4DecoderConfig {
460 DeepseekV4DecoderConfig {
461 rms_norm_eps: EPS,
462 hc_sinkhorn_iters: 4,
463 hc_eps: 1e-6,
464 n_heads: NUM_HEADS,
465 qk_head_dim: QK_HEAD_DIM,
466 v_head_dim: V_HEAD_DIM,
467 qk_rope: QK_ROPE,
468 compress_rope_theta: 1_000_000.0,
469 n_experts_active: N_EXPERTS_ACTIVE,
470 moe_renormalize: true,
471 hca_compress_ratio: 2,
472 }
473 }
474
475 #[test]
476 fn one_layer_synthetic_forward_produces_finite_logits() {
477 let weights = make_weights();
478 let cfg = decoder_cfg();
479 let mut state = DeepseekV4DecodeState::new(HIDDEN_DIM);
480
481 for token_id in 0..3 {
482 let logits =
483 deepseek_v4_forward_token(&weights, &cfg, token_id % OUTPUT_VOCAB, &mut state);
484 assert_eq!(logits.len(), OUTPUT_VOCAB);
485 assert!(
486 logits.iter().all(|v| v.is_finite()),
487 "token {token_id}: logits must be finite, got {logits:?}"
488 );
489 assert!(
490 !logits.iter().any(|v| v.is_nan()),
491 "token {token_id}: logits must not contain NaN, got {logits:?}"
492 );
493 }
494 }
495}