Skip to main content

ferrox_models/
deepseek_v4_decoder.rs

1//! A dedicated decoder skeleton for DeepSeek V4's real architecture,
2//! separate from `ferrox-models::decoder::Decoder` (the generic GQA path
3//! every other preset uses) — analogous to `glm52_decoder.rs` /
4//! `kimi_decoder.rs`: composes the already-independently-tested mHC,
5//! CSA/HCA compression + attention, derope, grouped output projection,
6//! and sqrtsoftplus MoE primitives into one synthetic forward pass.
7//!
8//! **Synthetic weights only — not a real checkpoint path.** No GGUF
9//! loader, no `Engine` wiring, no claim of oracle-correct output against
10//! a production DeepSeek V4 file. This module exists to prove the real
11//! primitives compose into a finite forward pass end-to-end on tiny dims,
12//! the same rigor `glm52_decoder.rs` applies before a real loader lands.
13//!
14//! Real per-layer flow (simplified to one layer here, transcribed from
15//! llama.cpp PR #24162 `deepseek4.cpp`'s layer loop):
16//!
17//! ```text
18//! attn_in  = mHC_pre(hc_streams, attn_hc_pre)
19//! attn_out = rms_norm(attn_in) |> HCA/CSA attention |> derope |> grouped wo_a/wo_b
20//! hc_streams = mHC_post(attn_out, ...)
21//! ffn_in   = mHC_pre(hc_streams, ffn_hc_pre)
22//! ffn_out  = rms_norm(ffn_in) |> MoE (sqrtsoftplus routing)
23//! hc_streams = mHC_post(ffn_out, ...)
24//! hidden   = mHC_head(hc_streams)
25//! logits   = output_head(rms_norm(hidden))
26//! ```
27//!
28//! Deliberately **not** implemented in this skeleton (real, cited scope
29//! for later slices): incremental DSV4 KV/compressor state
30//! (`llama-kv-cache-dsv4.cpp`), CSA's `coff=2` dual-role projection,
31//! hash-based first-layer MoE selection, and multi-layer stacking.
32
33use crate::deepseek_v4_budget::LayerCompressor;
34use ferrox_core::attention::apply_rope_back;
35use ferrox_core::csa_hca_compress::compress_block;
36use ferrox_core::deepseek_v4_attention::{csa_attention, hca_attention};
37use ferrox_core::matmul::rms_norm;
38use ferrox_core::tensor::Tensor;
39use ferrox_core::weight_matrix::WeightMatrix;
40use ferrox_moe::{
41    combine_expert_outputs, route_top_k, run_expert, ExpertWeights, GatingFunction, GluAct,
42};
43
44use crate::hyper_connections::{
45    head as hc_head, post as hc_post, pre as hc_pre, HyperConnectionHeadWeights,
46    HyperConnectionPreWeights, HC_MULT,
47};
48use crate::output_projection::grouped_output_projection;
49
50/// A CSA layer's extras: the doubled role projection and the Lightning
51/// Indexer. Present only on a [`LayerCompressor::Csa`] layer, and its
52/// absence there is a configuration error rather than a default.
53///
54/// # The doubled projection, which is the whole point of the split
55///
56/// On a CSA layer each raw token is projected **twice**: once for its
57/// role as the tail of the block ending at it, and once as the head of
58/// the next, overlapping block -- two different learned projections of
59/// the same token, not one reused twice (llama.cpp
60/// `load_arch_tensors`' `coff = ratio == 4 ? 2 : 1`, and
61/// `build_overlap_compressed_kv_from_state`'s
62/// `GGML_ASSERT(kv_state->ne[0] == 2*n_embd_head)`). A stack that
63/// applies one uniform ratio gives every CSA layer a single-width
64/// projection, which runs and produces numbers.
65pub struct DeepseekV4CsaWeights {
66    /// `qk_head_dim -> 2 * qk_head_dim`: the head-role projection in the
67    /// leading half, the tail-role projection in the trailing half.
68    pub role_proj: WeightMatrix,
69    /// The same split for `attn_comp_wgate`'s scores, so every block row
70    /// carries the gate belonging to the role it was projected for.
71    pub role_gate: WeightMatrix,
72    /// One indexer key per compressed entry.
73    ///
74    /// **A skeleton simplification, named rather than hidden**: upstream
75    /// runs a *separate* compressor (`indexer_comp_*`) over the raw
76    /// indexer projections, where this projects the already-compressed
77    /// entry. The indexer keys are compressed representations either
78    /// way, which is what the top-k selection needs; the second
79    /// compressor's own pooling is not reproduced here.
80    pub indexer_key_proj: WeightMatrix,
81    /// The query side, `qk_head_dim -> n_index_heads * index_head_dim`.
82    pub indexer_q_proj: WeightMatrix,
83    /// One weight per index head; its length is `n_index_heads`.
84    pub indexer_head_weights: Vec<f32>,
85    /// How many compressed entries survive selection.
86    pub indexer_top_k: usize,
87}
88
89/// One layer's attention-side weights (synthetic, tiny-dim fixtures only).
90pub struct DeepseekV4AttnWeights {
91    pub q_proj: WeightMatrix,
92    pub k_proj: WeightMatrix,
93    pub v_proj: WeightMatrix,
94    /// Per-group down-projections (`wo_a`), one per contiguous head block.
95    pub group_down: Vec<WeightMatrix>,
96    pub wo_b: WeightMatrix,
97    /// Block-compression gate (`attn_comp_wgate`) and norm for HCA pooling.
98    pub comp_gate: WeightMatrix,
99    pub comp_norm: Vec<f32>,
100    /// One learned logit per query head, joining the softmax denominator
101    /// with a zero value vector so a head can decline to attend rather
102    /// than being forced to spend a full unit of weight on the keys it
103    /// has. `None` for a checkpoint that ships no sinks.
104    pub attn_sinks: Option<Vec<f32>>,
105    /// Present iff this layer's compressor is [`LayerCompressor::Csa`].
106    pub csa: Option<DeepseekV4CsaWeights>,
107}
108
109/// MoE FFN weights for one layer (`sqrtsoftplus` gating, no hash routing).
110pub struct DeepseekV4MoeFfnWeights {
111    pub router_weight: WeightMatrix,
112    pub experts: Vec<ExpertWeights>,
113    pub shared_expert: ExpertWeights,
114}
115
116pub struct DeepseekV4DecoderLayerWeights {
117    pub attn_hc_pre: HyperConnectionPreWeights,
118    pub attn_norm_weight: Vec<f32>,
119    pub attn: DeepseekV4AttnWeights,
120    pub ffn_hc_pre: HyperConnectionPreWeights,
121    pub ffn_norm_weight: Vec<f32>,
122    pub ffn: DeepseekV4MoeFfnWeights,
123}
124
125pub struct DeepseekV4DecoderWeights {
126    pub embedding: Tensor,
127    pub layer: DeepseekV4DecoderLayerWeights,
128    pub hc_head: HyperConnectionHeadWeights,
129    pub final_norm_weight: Vec<f32>,
130    pub output_head: WeightMatrix,
131}
132
133pub struct DeepseekV4DecoderConfig {
134    pub rms_norm_eps: f32,
135    pub hc_sinkhorn_iters: u32,
136    pub hc_eps: f32,
137    pub n_heads: usize,
138    pub qk_head_dim: usize,
139    pub v_head_dim: usize,
140    pub qk_rope: usize,
141    pub compress_rope_theta: f32,
142    pub n_experts_active: usize,
143    pub moe_renormalize: bool,
144    /// Which compressor this layer runs.
145    ///
146    /// A mechanism, not a scalar ratio, and that is the correction:
147    /// upstream ships a per-layer array (`0, 0, 4, 128, 4, 128, 4, 0`)
148    /// where `0` means no compressor at all, `4` means CSA -- overlapping
149    /// blocks, a doubled projection width, and a Lightning Indexer --
150    /// and `128` means HCA, non-overlapping and dense with no indexer.
151    /// One uniform ratio across the stack builds an indexer on the HCA
152    /// layers or none on the CSA ones, and gives half of them the wrong
153    /// compressor width; all three failures run and produce numbers.
154    /// [`LayerCompressor`] derives every one of those parameters from
155    /// the mechanism, so they cannot be set inconsistently here.
156    pub compressor: LayerCompressor,
157}
158
159/// Minimal per-layer state: raw K/V for the SWA window plus optional
160/// compressed entries. No incremental DSV4 cache yet — callers append
161/// one token at a time and pool when the layer's compressor has enough
162/// raw positions for its next block.
163pub struct DeepseekV4LayerState {
164    hc_streams: [Vec<f32>; HC_MULT],
165    raw_k: Vec<f32>,
166    raw_v: Vec<f32>,
167    compressed_k: Vec<f32>,
168    compressed_v: Vec<f32>,
169    /// CSA only: each raw token's `2 * qk_head_dim` role projection and
170    /// its matching role gate, kept because a CSA block reaches back
171    /// into the *previous* half-block's head-role rows.
172    role_kv: Vec<f32>,
173    role_scores: Vec<f32>,
174    token_count: usize,
175}
176
177impl DeepseekV4LayerState {
178    pub fn new(hidden_dim: usize) -> Self {
179        let zero = vec![0.0; hidden_dim];
180        Self {
181            hc_streams: std::array::from_fn(|_| zero.clone()),
182            raw_k: Vec::new(),
183            raw_v: Vec::new(),
184            compressed_k: Vec::new(),
185            compressed_v: Vec::new(),
186            role_kv: Vec::new(),
187            role_scores: Vec::new(),
188            token_count: 0,
189        }
190    }
191
192    fn reset_hc_from_hidden(&mut self, hidden: &[f32]) {
193        for stream in self.hc_streams.iter_mut() {
194            stream.copy_from_slice(hidden);
195        }
196    }
197}
198
199pub struct DeepseekV4DecodeState {
200    layer: DeepseekV4LayerState,
201}
202
203impl DeepseekV4DecodeState {
204    pub fn new(hidden_dim: usize) -> Self {
205        Self {
206            layer: DeepseekV4LayerState::new(hidden_dim),
207        }
208    }
209}
210
211fn derope_attn_out(
212    attn_out: &mut [f32],
213    n_heads: usize,
214    v_head_dim: usize,
215    qk_rope: usize,
216    pos: usize,
217    theta: f32,
218) {
219    assert!(qk_rope <= v_head_dim);
220    for h in 0..n_heads {
221        let head_start = h * v_head_dim;
222        let rope_start = head_start + v_head_dim - qk_rope;
223        apply_rope_back(
224            &mut attn_out[rope_start..head_start + v_head_dim],
225            pos,
226            theta,
227        );
228    }
229}
230
231/// Pools one HCA block: `ratio` consecutive raw positions, gated by
232/// `attn_comp_wgate`, into a single compressed entry.
233///
234/// Non-overlapping, which is the whole difference from CSA: block `j`
235/// reads only its own `ratio` positions and never reaches back.
236fn hca_block(
237    weights: &DeepseekV4AttnWeights,
238    cfg: &DeepseekV4DecoderConfig,
239    state: &mut DeepseekV4LayerState,
240    ratio: usize,
241) {
242    let n_raw = state.token_count;
243    let per_token_k = cfg.n_heads * cfg.qk_head_dim;
244    let per_token_v = cfg.n_heads * cfg.v_head_dim;
245    let block_index = n_raw / ratio;
246
247    let kv_block: Vec<Vec<f32>> = state.raw_k[(n_raw - ratio) * per_token_k..n_raw * per_token_k]
248        .chunks(cfg.qk_head_dim)
249        .map(|row| row.to_vec())
250        .collect();
251    let score_block: Vec<Vec<f32>> = kv_block
252        .iter()
253        .map(|row| weights.comp_gate.apply(row))
254        .collect();
255    let compressed_k = compress_block(
256        &kv_block,
257        &score_block,
258        &weights.comp_norm,
259        cfg.rms_norm_eps,
260        cfg.qk_rope,
261        block_index,
262        cfg.compress_rope_theta,
263    );
264    let v_block: Vec<Vec<f32>> = state.raw_v[(n_raw - ratio) * per_token_v..n_raw * per_token_v]
265        .chunks(cfg.v_head_dim)
266        .map(|row| row.to_vec())
267        .collect();
268    let compressed_v = compress_block(
269        &v_block,
270        &score_block,
271        &weights.comp_norm,
272        cfg.rms_norm_eps,
273        cfg.qk_rope,
274        block_index,
275        cfg.compress_rope_theta,
276    );
277    state.compressed_k.extend_from_slice(&compressed_k);
278    state.compressed_v.extend_from_slice(&compressed_v);
279}
280
281/// Pools one CSA block: the `2 * ratio`-row concatenation of the
282/// PREVIOUS half-block's head-role rows and the CURRENT half-block's
283/// tail-role rows.
284///
285/// # Where the extra `ratio` rows come from
286///
287/// This is the overlap. Block `j` covers raw positions
288/// `[(j-1)*ratio, j*ratio)` through their head-role projection and
289/// `[j*ratio, (j+1)*ratio)` through their tail-role projection -- the
290/// same token contributing to two blocks under two different learned
291/// projections, which is what `coff = 2` buys and what a uniform ratio
292/// silently drops.
293///
294/// # The first block reaches off the start of the sequence
295///
296/// Block 0 has no previous half-block. The real implementation pads
297/// out-of-range reads with a zero-KV, `-inf`-score phantom row
298/// (`dsv4_append_zero_row`), and `ferrox_core::csa_hca_compress`'s
299/// module docs require callers to apply the same convention by hand.
300/// A `-inf` score contributes exactly zero weight after the softmax, so
301/// the phantom rows are present in the block and absent from the
302/// result -- which is not the same as shortening the block, because the
303/// per-channel softmax normalizes over whatever rows it is given.
304fn csa_block(
305    weights: &DeepseekV4AttnWeights,
306    cfg: &DeepseekV4DecoderConfig,
307    state: &mut DeepseekV4LayerState,
308    ratio: usize,
309) {
310    assert_eq!(
311        cfg.n_heads, 1,
312        "this skeleton compresses one head: a block is assembled as rows of qk_head_dim, so \
313         more heads would pool across them into a single entry and leave n_compressed \
314         fractional. Multi-head compression is real scope, not a silent approximation"
315    );
316    let n_raw = state.token_count;
317    // `blocks_closed` counts blocks including this one, so the block
318    // being built is ordinal `blocks_closed - 1` and its own tokens are
319    // the LAST `ratio` appended -- not the next `ratio`, which is the
320    // off-by-one that indexes past the end of the state.
321    let blocks_closed = n_raw / ratio;
322    let block_ord = blocks_closed - 1;
323    let role_width = 2 * cfg.qk_head_dim;
324
325    let mut kv_block: Vec<Vec<f32>> = Vec::with_capacity(2 * ratio);
326    let mut score_block: Vec<Vec<f32>> = Vec::with_capacity(2 * ratio);
327    let mut v_block: Vec<Vec<f32>> = Vec::with_capacity(2 * ratio);
328
329    // The previous half-block, through its HEAD-role projection -- or
330    // phantom rows when block 0 reaches off the start of the sequence.
331    for step in 0..ratio {
332        if block_ord == 0 {
333            kv_block.push(vec![0.0; cfg.qk_head_dim]);
334            score_block.push(vec![f32::NEG_INFINITY; cfg.qk_head_dim]);
335            v_block.push(vec![0.0; cfg.v_head_dim]);
336            continue;
337        }
338        let token = (block_ord - 1) * ratio + step;
339        let at = token * role_width;
340        kv_block.push(state.role_kv[at..at + cfg.qk_head_dim].to_vec());
341        score_block.push(state.role_scores[at..at + cfg.qk_head_dim].to_vec());
342        let v_at = token * cfg.v_head_dim;
343        v_block.push(state.raw_v[v_at..v_at + cfg.v_head_dim].to_vec());
344    }
345    // The current half-block, through its TAIL-role projection.
346    for step in 0..ratio {
347        let token = block_ord * ratio + step;
348        let at = token * role_width + cfg.qk_head_dim;
349        kv_block.push(state.role_kv[at..at + cfg.qk_head_dim].to_vec());
350        score_block.push(state.role_scores[at..at + cfg.qk_head_dim].to_vec());
351        let v_at = token * cfg.v_head_dim;
352        v_block.push(state.raw_v[v_at..v_at + cfg.v_head_dim].to_vec());
353    }
354
355    let compressed_k = compress_block(
356        &kv_block,
357        &score_block,
358        &weights.comp_norm,
359        cfg.rms_norm_eps,
360        cfg.qk_rope,
361        blocks_closed,
362        cfg.compress_rope_theta,
363    );
364    // V follows the same index structure but is not role-projected: the
365    // doubled state the reference asserts on is the shared MLA latent,
366    // and `v_head_dim` need not equal `qk_head_dim`, so a role split
367    // here would be this skeleton's invention rather than a
368    // transcription.
369    let compressed_v = compress_block(
370        &v_block,
371        &score_block,
372        &weights.comp_norm,
373        cfg.rms_norm_eps,
374        cfg.qk_rope,
375        blocks_closed,
376        cfg.compress_rope_theta,
377    );
378    state.compressed_k.extend_from_slice(&compressed_k);
379    state.compressed_v.extend_from_slice(&compressed_v);
380}
381
382fn attn_forward_token(
383    weights: &DeepseekV4AttnWeights,
384    cfg: &DeepseekV4DecoderConfig,
385    attn_in: &[f32],
386    state: &mut DeepseekV4LayerState,
387) -> Vec<f32> {
388    let q = weights.q_proj.apply(attn_in);
389    let k = weights.k_proj.apply(attn_in);
390    let v = weights.v_proj.apply(attn_in);
391    debug_assert_eq!(q.len(), cfg.n_heads * cfg.qk_head_dim);
392    debug_assert_eq!(k.len(), cfg.n_heads * cfg.qk_head_dim);
393    debug_assert_eq!(v.len(), cfg.n_heads * cfg.v_head_dim);
394
395    state.raw_k.extend_from_slice(&k);
396    state.raw_v.extend_from_slice(&v);
397    state.token_count += 1;
398    let n_raw = state.token_count;
399
400    // A CSA layer keeps every token's two role projections, because the
401    // block that closes at token t reaches back into the head-role rows
402    // of the tokens before it.
403    if let Some(csa) = weights.csa.as_ref() {
404        for head in k.chunks(cfg.qk_head_dim) {
405            state.role_kv.extend_from_slice(&csa.role_proj.apply(head));
406            state
407                .role_scores
408                .extend_from_slice(&csa.role_gate.apply(head));
409        }
410    }
411
412    let ratio = cfg.compressor.ratio() as usize;
413    if ratio > 0 && n_raw.is_multiple_of(ratio) {
414        match cfg.compressor {
415            LayerCompressor::Hca => hca_block(weights, cfg, state, ratio),
416            LayerCompressor::Csa => {
417                csa_block(weights, cfg, state, ratio);
418            }
419            LayerCompressor::None => unreachable!("ratio 0 is filtered above"),
420        }
421    }
422
423    let n_compressed = if cfg.qk_head_dim > 0 {
424        state.compressed_k.len() / (cfg.n_heads * cfg.qk_head_dim)
425    } else {
426        0
427    };
428    let sinks = weights.attn_sinks.as_deref();
429
430    // The three-way dispatch. A layer with no compressor sees the raw
431    // window and nothing else -- a real entry in the shipped schedule
432    // rather than a disabled state, so it runs the dense path over an
433    // empty compressed set rather than being skipped.
434    let mut attn_out = match cfg.compressor {
435        LayerCompressor::None | LayerCompressor::Hca => hca_attention(
436            &q,
437            &state.raw_k,
438            &state.raw_v,
439            n_raw,
440            &state.compressed_k,
441            &state.compressed_v,
442            n_compressed,
443            cfg.n_heads,
444            cfg.qk_head_dim,
445            cfg.v_head_dim,
446            sinks,
447        ),
448        LayerCompressor::Csa => {
449            let csa = weights
450                .csa
451                .as_ref()
452                .expect("a CSA layer must carry its role projections and indexer");
453            let n_index_heads = csa.indexer_head_weights.len();
454            // Projected once. Splitting the width across the index
455            // heads needs the length, and computing it by projecting a
456            // second time would run the indexer's matvec twice on every
457            // decode step of every CSA layer.
458            let projected = csa.indexer_q_proj.apply(&q[..cfg.qk_head_dim]);
459            // `checked_div` rather than a guarded `/`: a layer that
460            // declares no index heads has no width to split, and zero
461            // is the honest answer rather than a panic.
462            let index_head_dim = projected.len().checked_div(n_index_heads).unwrap_or(0);
463            let indexer_q: Vec<Vec<f32>> = projected
464                .chunks(index_head_dim.max(1))
465                .map(|c| c.to_vec())
466                .collect();
467            let indexer_keys: Vec<Vec<f32>> = state
468                .compressed_k
469                .chunks(cfg.qk_head_dim)
470                .take(n_compressed)
471                .map(|entry| csa.indexer_key_proj.apply(entry))
472                .collect();
473            csa_attention(
474                &q,
475                &state.raw_k,
476                &state.raw_v,
477                n_raw,
478                &state.compressed_k,
479                &state.compressed_v,
480                n_compressed,
481                &indexer_q,
482                &indexer_keys,
483                &csa.indexer_head_weights,
484                csa.indexer_top_k,
485                cfg.n_heads,
486                cfg.qk_head_dim,
487                cfg.v_head_dim,
488                sinks,
489            )
490        }
491    };
492
493    derope_attn_out(
494        &mut attn_out,
495        cfg.n_heads,
496        cfg.v_head_dim,
497        cfg.qk_rope,
498        state.token_count.saturating_sub(1),
499        cfg.compress_rope_theta,
500    );
501
502    grouped_output_projection(&attn_out, &weights.group_down, &weights.wo_b)
503}
504
505fn moe_ffn_forward(
506    weights: &DeepseekV4MoeFfnWeights,
507    cfg: &DeepseekV4DecoderConfig,
508    x: &[f32],
509) -> Vec<f32> {
510    let router_logits = weights.router_weight.apply(x);
511    let decision = route_top_k(
512        &router_logits,
513        cfg.n_experts_active,
514        GatingFunction::SqrtSoftplus,
515        cfg.moe_renormalize,
516    );
517    let routed_outputs: Vec<(Vec<f32>, f32)> = decision
518        .expert_ids
519        .iter()
520        .zip(decision.weights.iter())
521        .map(|(&e, &w)| (run_expert(x, &weights.experts[e], GluAct::Swiglu), w))
522        .collect();
523    let shared_out = run_expert(x, &weights.shared_expert, GluAct::Swiglu);
524    combine_expert_outputs(&routed_outputs, &[shared_out], x.len())
525}
526
527/// One decode step through the single synthetic layer, then final norm +
528/// output projection. `token_id` indexes the embedding table.
529pub fn deepseek_v4_forward_token(
530    weights: &DeepseekV4DecoderWeights,
531    cfg: &DeepseekV4DecoderConfig,
532    token_id: usize,
533    state: &mut DeepseekV4DecodeState,
534) -> Vec<f32> {
535    let hidden_dim = weights.embedding.cols();
536    let hidden = weights.embedding.row(token_id).to_vec();
537    state.layer.reset_hc_from_hidden(&hidden);
538    let layer = &weights.layer;
539
540    let hc_residual = state.layer.hc_streams.clone();
541    let (attn_in, attn_post, attn_comb) = hc_pre(
542        &layer.attn_hc_pre,
543        &hc_residual,
544        cfg.rms_norm_eps,
545        cfg.hc_sinkhorn_iters,
546        cfg.hc_eps,
547    );
548    let attn_normed = rms_norm(&attn_in, &layer.attn_norm_weight, cfg.rms_norm_eps);
549    let attn_out = attn_forward_token(&layer.attn, cfg, &attn_normed, &mut state.layer);
550    state.layer.hc_streams = hc_post(&attn_out, &hc_residual, &attn_post, &attn_comb);
551
552    let hc_residual = state.layer.hc_streams.clone();
553    let (ffn_in, ffn_post, ffn_comb) = hc_pre(
554        &layer.ffn_hc_pre,
555        &hc_residual,
556        cfg.rms_norm_eps,
557        cfg.hc_sinkhorn_iters,
558        cfg.hc_eps,
559    );
560    let ffn_normed = rms_norm(&ffn_in, &layer.ffn_norm_weight, cfg.rms_norm_eps);
561    let ffn_out = moe_ffn_forward(&layer.ffn, cfg, &ffn_normed);
562    state.layer.hc_streams = hc_post(&ffn_out, &hc_residual, &ffn_post, &ffn_comb);
563
564    let collapsed = hc_head(
565        &weights.hc_head,
566        &state.layer.hc_streams,
567        cfg.rms_norm_eps,
568        cfg.hc_eps,
569    );
570    let final_normed = rms_norm(&collapsed, &weights.final_norm_weight, cfg.rms_norm_eps);
571    assert_eq!(final_normed.len(), hidden_dim);
572    weights.output_head.apply(&final_normed)
573}
574
575#[cfg(test)]
576mod tests {
577    use super::*;
578    use crate::hyper_connections::HyperConnectionPreWeights;
579
580    const HIDDEN_DIM: usize = 8;
581    const EPS: f32 = 1e-5;
582    const NUM_HEADS: usize = 1;
583    const QK_HEAD_DIM: usize = 4;
584    const V_HEAD_DIM: usize = 4;
585    const QK_ROPE: usize = 2;
586    const N_GROUPS: usize = 1;
587    const O_LORA_RANK: usize = 2;
588    const O_GROUP_DIM: usize = (NUM_HEADS * V_HEAD_DIM) / N_GROUPS;
589    const N_EXPERTS: usize = 4;
590    const N_EXPERTS_ACTIVE: usize = 2;
591    const MOE_FFN_DIM: usize = 3;
592    const OUTPUT_VOCAB: usize = 5;
593    const HC_FLAT: usize = HC_MULT * HIDDEN_DIM;
594    const N_INDEX_HEADS: usize = 2;
595    const INDEX_HEAD_DIM: usize = 2;
596
597    fn wm(data: Vec<f32>, rows: usize, cols: usize) -> WeightMatrix {
598        assert_eq!(data.len(), rows * cols);
599        WeightMatrix::F32(Tensor::new(data, vec![rows, cols]))
600    }
601
602    fn synth(seed: usize, n: usize) -> Vec<f32> {
603        (0..n)
604            .map(|i| (((seed * 131 + i * 17 + 7) % 23) as f32 * 0.05) - 0.55)
605            .collect()
606    }
607
608    fn make_hc_pre(seed: usize) -> HyperConnectionPreWeights {
609        HyperConnectionPreWeights {
610            fn_proj: wm(
611                synth(seed, (2 + HC_MULT) * HC_MULT * HC_FLAT),
612                (2 + HC_MULT) * HC_MULT,
613                HC_FLAT,
614            ),
615            scale: [0.5, 0.5, 0.5],
616            base_pre: [0.1; HC_MULT],
617            base_post: [0.2; HC_MULT],
618            base_comb: [0.01; HC_MULT * HC_MULT],
619        }
620    }
621
622    fn make_hc_head(seed: usize) -> HyperConnectionHeadWeights {
623        HyperConnectionHeadWeights {
624            fn_proj: wm(synth(seed, HC_MULT * HC_FLAT), HC_MULT, HC_FLAT),
625            scale: 0.5,
626            base: [0.1; HC_MULT],
627        }
628    }
629
630    fn make_weights_for(csa: bool, sinks: Option<Vec<f32>>) -> DeepseekV4DecoderWeights {
631        let expert = |seed: usize| ExpertWeights {
632            gate: wm(
633                synth(seed, MOE_FFN_DIM * HIDDEN_DIM),
634                MOE_FFN_DIM,
635                HIDDEN_DIM,
636            ),
637            up: wm(
638                synth(seed + 1, MOE_FFN_DIM * HIDDEN_DIM),
639                MOE_FFN_DIM,
640                HIDDEN_DIM,
641            ),
642            down: wm(
643                synth(seed + 2, HIDDEN_DIM * MOE_FFN_DIM),
644                HIDDEN_DIM,
645                MOE_FFN_DIM,
646            ),
647        };
648
649        DeepseekV4DecoderWeights {
650            embedding: Tensor::new(
651                synth(1000, OUTPUT_VOCAB * HIDDEN_DIM),
652                vec![OUTPUT_VOCAB, HIDDEN_DIM],
653            ),
654            layer: DeepseekV4DecoderLayerWeights {
655                attn_hc_pre: make_hc_pre(100),
656                attn_norm_weight: vec![1.0; HIDDEN_DIM],
657                attn: DeepseekV4AttnWeights {
658                    q_proj: wm(
659                        synth(110, NUM_HEADS * QK_HEAD_DIM * HIDDEN_DIM),
660                        NUM_HEADS * QK_HEAD_DIM,
661                        HIDDEN_DIM,
662                    ),
663                    k_proj: wm(
664                        synth(111, NUM_HEADS * QK_HEAD_DIM * HIDDEN_DIM),
665                        NUM_HEADS * QK_HEAD_DIM,
666                        HIDDEN_DIM,
667                    ),
668                    v_proj: wm(
669                        synth(112, NUM_HEADS * V_HEAD_DIM * HIDDEN_DIM),
670                        NUM_HEADS * V_HEAD_DIM,
671                        HIDDEN_DIM,
672                    ),
673                    group_down: (0..N_GROUPS)
674                        .map(|g| {
675                            wm(
676                                synth(120 + g, O_LORA_RANK * O_GROUP_DIM),
677                                O_LORA_RANK,
678                                O_GROUP_DIM,
679                            )
680                        })
681                        .collect(),
682                    wo_b: wm(
683                        synth(130, HIDDEN_DIM * O_LORA_RANK * N_GROUPS),
684                        HIDDEN_DIM,
685                        O_LORA_RANK * N_GROUPS,
686                    ),
687                    comp_gate: wm(
688                        synth(140, QK_HEAD_DIM * QK_HEAD_DIM),
689                        QK_HEAD_DIM,
690                        QK_HEAD_DIM,
691                    ),
692                    comp_norm: vec![1.0; QK_HEAD_DIM],
693                    attn_sinks: sinks,
694                    csa: csa.then(|| DeepseekV4CsaWeights {
695                        role_proj: wm(
696                            synth(150, 2 * QK_HEAD_DIM * QK_HEAD_DIM),
697                            2 * QK_HEAD_DIM,
698                            QK_HEAD_DIM,
699                        ),
700                        role_gate: wm(
701                            synth(151, 2 * QK_HEAD_DIM * QK_HEAD_DIM),
702                            2 * QK_HEAD_DIM,
703                            QK_HEAD_DIM,
704                        ),
705                        indexer_key_proj: wm(
706                            synth(152, INDEX_HEAD_DIM * QK_HEAD_DIM),
707                            INDEX_HEAD_DIM,
708                            QK_HEAD_DIM,
709                        ),
710                        indexer_q_proj: wm(
711                            synth(153, N_INDEX_HEADS * INDEX_HEAD_DIM * QK_HEAD_DIM),
712                            N_INDEX_HEADS * INDEX_HEAD_DIM,
713                            QK_HEAD_DIM,
714                        ),
715                        indexer_head_weights: vec![1.0; N_INDEX_HEADS],
716                        indexer_top_k: 1,
717                    }),
718                },
719                ffn_hc_pre: make_hc_pre(200),
720                ffn_norm_weight: vec![1.0; HIDDEN_DIM],
721                ffn: DeepseekV4MoeFfnWeights {
722                    router_weight: wm(synth(300, N_EXPERTS * HIDDEN_DIM), N_EXPERTS, HIDDEN_DIM),
723                    experts: (0..N_EXPERTS).map(|e| expert(400 + e * 10)).collect(),
724                    shared_expert: expert(900),
725                },
726            },
727            hc_head: make_hc_head(500),
728            final_norm_weight: vec![1.0; HIDDEN_DIM],
729            output_head: wm(
730                synth(1100, OUTPUT_VOCAB * HIDDEN_DIM),
731                OUTPUT_VOCAB,
732                HIDDEN_DIM,
733            ),
734        }
735    }
736
737    fn make_weights() -> DeepseekV4DecoderWeights {
738        make_weights_for(false, None)
739    }
740
741    fn decoder_cfg_for(compressor: LayerCompressor) -> DeepseekV4DecoderConfig {
742        DeepseekV4DecoderConfig {
743            rms_norm_eps: EPS,
744            hc_sinkhorn_iters: 4,
745            hc_eps: 1e-6,
746            n_heads: NUM_HEADS,
747            qk_head_dim: QK_HEAD_DIM,
748            v_head_dim: V_HEAD_DIM,
749            qk_rope: QK_ROPE,
750            compress_rope_theta: 1_000_000.0,
751            n_experts_active: N_EXPERTS_ACTIVE,
752            moe_renormalize: true,
753            compressor,
754        }
755    }
756
757    fn decoder_cfg() -> DeepseekV4DecoderConfig {
758        decoder_cfg_for(LayerCompressor::Hca)
759    }
760
761    /// All three arms of the schedule run and stay finite. The point of
762    /// the dispatch is that these are three different mechanisms, so
763    /// each has to be exercised as itself rather than one standing in
764    /// for the others.
765    #[test]
766    fn every_compressor_in_the_schedule_produces_finite_logits() {
767        for compressor in [
768            LayerCompressor::None,
769            LayerCompressor::Csa,
770            LayerCompressor::Hca,
771        ] {
772            let weights = make_weights_for(compressor == LayerCompressor::Csa, None);
773            let cfg = decoder_cfg_for(compressor);
774            let mut state = DeepseekV4DecodeState::new(HIDDEN_DIM);
775            // Enough tokens for CSA to close two blocks; HCA's 128 needs
776            // more than any of these, which is itself the correct
777            // behaviour for a layer whose first block has not closed.
778            for token_id in 0..9 {
779                let logits =
780                    deepseek_v4_forward_token(&weights, &cfg, token_id % OUTPUT_VOCAB, &mut state);
781                assert!(
782                    logits.iter().all(|v| v.is_finite()),
783                    "{compressor:?} token {token_id}: {logits:?}"
784                );
785            }
786        }
787    }
788
789    /// Ratio 0 is a real entry in the shipped schedule, not a disabled
790    /// state: the layer runs, and it never builds a compressed entry at
791    /// any length. A uniform ratio gives these layers a compressor they
792    /// do not have.
793    #[test]
794    fn a_layer_with_no_compressor_never_builds_a_compressed_entry() {
795        let weights = make_weights_for(false, None);
796        let cfg = decoder_cfg_for(LayerCompressor::None);
797        let mut state = DeepseekV4DecodeState::new(HIDDEN_DIM);
798        for token_id in 0..20 {
799            deepseek_v4_forward_token(&weights, &cfg, token_id % OUTPUT_VOCAB, &mut state);
800        }
801        assert_eq!(state.layer.token_count, 20);
802        assert!(
803            state.layer.compressed_k.is_empty() && state.layer.compressed_v.is_empty(),
804            "a ratio-0 layer compressed something"
805        );
806    }
807
808    /// A compressed entry appears exactly when a block closes, and one
809    /// per closed block -- for both mechanisms, at their own ratios.
810    /// `LayerCompressor::visible_compressed` is the same count stated
811    /// independently, so the two agreeing is the check.
812    #[test]
813    fn one_compressed_entry_appears_per_closed_block() {
814        let weights = make_weights_for(true, None);
815        let cfg = decoder_cfg_for(LayerCompressor::Csa);
816        let mut state = DeepseekV4DecodeState::new(HIDDEN_DIM);
817        for token in 0..12 {
818            deepseek_v4_forward_token(&weights, &cfg, token % OUTPUT_VOCAB, &mut state);
819            let entries = state.layer.compressed_k.len() / (NUM_HEADS * QK_HEAD_DIM);
820            assert_eq!(
821                entries,
822                LayerCompressor::Csa.visible_compressed(token),
823                "after token {token}"
824            );
825        }
826    }
827
828    /// The overlap: a CSA block spans `2 * ratio` rows, reaching back
829    /// into the previous half-block through the head-role projection.
830    /// Block 0 has no previous half-block, so it is padded with
831    /// zero-KV/`-inf`-score phantom rows -- present in the block and
832    /// absent from the result, which is not the same as shortening it,
833    /// because the per-channel softmax normalizes over whatever rows it
834    /// is given.
835    ///
836    /// Checked by construction: a block that did NOT reach back would
837    /// be unaffected by the earlier tokens' role projections, so
838    /// changing only those must change the second compressed entry.
839    #[test]
840    fn a_csa_block_reaches_back_into_the_previous_half_block() {
841        let cfg = decoder_cfg_for(LayerCompressor::Csa);
842        let ratio = LayerCompressor::Csa.ratio() as usize;
843
844        let run = |first_tokens: &[usize]| -> Vec<f32> {
845            let weights = make_weights_for(true, None);
846            let mut state = DeepseekV4DecodeState::new(HIDDEN_DIM);
847            for &t in first_tokens {
848                deepseek_v4_forward_token(&weights, &cfg, t, &mut state);
849            }
850            // Then a fixed second half-block, so only the FIRST block's
851            // tokens differ between the two runs.
852            for t in 0..ratio {
853                deepseek_v4_forward_token(&weights, &cfg, t % OUTPUT_VOCAB, &mut state);
854            }
855            let per_entry = NUM_HEADS * QK_HEAD_DIM;
856            assert_eq!(state.layer.compressed_k.len() / per_entry, 2);
857            state.layer.compressed_k[per_entry..].to_vec()
858        };
859
860        let a = run(&[0, 1, 2, 3]);
861        let b = run(&[4, 3, 2, 1]);
862        assert_ne!(
863            a, b,
864            "the second block must depend on the first half-block it overlaps"
865        );
866    }
867
868    /// A per-head sink changes the answer, which is the whole reason to
869    /// carry it: without one, every head must spend a full unit of
870    /// weight on the keys it has.
871    #[test]
872    fn a_per_head_attention_sink_changes_the_output() {
873        let cfg = decoder_cfg_for(LayerCompressor::None);
874        let logits_for = |sinks: Option<Vec<f32>>| {
875            let weights = make_weights_for(false, sinks);
876            let mut state = DeepseekV4DecodeState::new(HIDDEN_DIM);
877            let mut last = Vec::new();
878            for token in 0..4 {
879                last = deepseek_v4_forward_token(&weights, &cfg, token % OUTPUT_VOCAB, &mut state);
880            }
881            last
882        };
883
884        let plain = logits_for(None);
885        let negligible = logits_for(Some(vec![-40.0; NUM_HEADS]));
886        let dominant = logits_for(Some(vec![40.0; NUM_HEADS]));
887
888        for (p, n) in plain.iter().zip(negligible.iter()) {
889            assert!(
890                (p - n).abs() < 1e-4,
891                "a sink far below every score: {p} vs {n}"
892            );
893        }
894        assert!(
895            plain
896                .iter()
897                .zip(dominant.iter())
898                .any(|(p, d)| (p - d).abs() > 1e-3),
899            "a dominant sink must change the answer"
900        );
901        assert!(dominant.iter().all(|v| v.is_finite()));
902    }
903
904    #[test]
905    fn one_layer_synthetic_forward_produces_finite_logits() {
906        let weights = make_weights();
907        let cfg = decoder_cfg();
908        let mut state = DeepseekV4DecodeState::new(HIDDEN_DIM);
909
910        for token_id in 0..3 {
911            let logits =
912                deepseek_v4_forward_token(&weights, &cfg, token_id % OUTPUT_VOCAB, &mut state);
913            assert_eq!(logits.len(), OUTPUT_VOCAB);
914            assert!(
915                logits.iter().all(|v| v.is_finite()),
916                "token {token_id}: logits must be finite, got {logits:?}"
917            );
918            assert!(
919                !logits.iter().any(|v| v.is_nan()),
920                "token {token_id}: logits must not contain NaN, got {logits:?}"
921            );
922        }
923    }
924}