metrale_model_arch/glm5next_kda/
config.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: KDA block geometry (`Glm5NextKdaConfig`) and device weights (`Glm5NextKdaWeights`).
4//!
5//! Owner: model-arch (GLM-5.3-Flash KDA).
6//! Invariants: none beyond the types; `Glm5NextKdaConfig::validate` states what the kernels need.
7
8use super::*;
9
10/// 2026-09-25: KDA geometry and the two numeric config values the block uses. The loader fills
11/// every field from `ModelConfig` except `l2_eps` and `chunk`; there is no `Default`.
12#[derive(Clone, Copy, Debug)]
13pub struct Glm5NextKdaConfig {
14    pub hidden: usize,
15    pub heads: usize,
16    pub head_dim: usize,
17    pub conv_kernel: usize,
18    /// 2026-09-25: `linear_attn_config.gate_lower_bound`.
19    pub gate_lower_bound: f32,
20    /// 2026-09-25: `rms_norm_eps`, read by `o_norm`.
21    pub rms_norm_eps: f32,
22    /// 2026-09-25: The L2 epsilon, applied as `1/sqrt(sum + eps)` rather than `max(norm, eps)`.
23    /// Not a config key.
24    pub l2_eps: f32,
25    /// 2026-09-25: Prefill chunk width, bounded by [`SMEM_CEILING`] through `validate`.
26    pub chunk: usize,
27}
28
29impl Glm5NextKdaConfig {
30    pub fn qkv_dim(&self) -> usize {
31        self.heads * self.head_dim
32    }
33    /// 2026-09-25: q | k | v concatenated: the channel count the depthwise conv sees.
34    pub fn conv_dim(&self) -> usize {
35        3 * self.qkv_dim()
36    }
37    /// 2026-09-25: The q | k channels, the only ones L2-normalised.
38    pub fn qk_channels(&self) -> usize {
39        2 * self.qkv_dim()
40    }
41    /// 2026-09-25: Elements of the FP32 `[heads, head_dim, head_dim]` K-major recurrent state.
42    pub fn recurrent_state_elems(&self) -> usize {
43        self.heads * self.head_dim * self.head_dim
44    }
45    /// 2026-09-25: Elements of the FP32 `[conv_dim, conv_kernel]` conv state (see `KdaSeqState`).
46    pub fn conv_state_elems(&self) -> usize {
47        self.conv_dim() * self.conv_kernel
48    }
49    pub fn smem_prepare(&self) -> usize {
50        (self.chunk * self.head_dim + self.chunk * self.chunk + self.chunk) * 4
51    }
52    pub fn smem_scan(&self) -> usize {
53        (2 * self.chunk * self.head_dim + self.chunk * self.chunk) * 4
54    }
55
56    pub fn validate(&self) -> Result<()> {
57        if !self.qk_channels().is_multiple_of(256) {
58            bail!("causal_conv1d_update_l2norm requires qk_channels % 256 == 0");
59        }
60        if self.head_dim != 128 {
61            bail!("the fused conv+L2 kernel hardcodes 2 heads per 256-thread block");
62        }
63        if self.conv_kernel > 4 {
64            bail!("the conv kernels keep the sliding window in 4 registers");
65        }
66        let (p, s) = (self.smem_prepare(), self.smem_scan());
67        if p > SMEM_CEILING || s > SMEM_CEILING {
68            bail!(
69                "chunk={} needs {p}/{s} B shared, ceiling {SMEM_CEILING}",
70                self.chunk
71            );
72        }
73        Ok(())
74    }
75}
76
77/// 2026-09-25: One KDA block's device weights. Torch `Linear` layout `[out, in]`, BF16, except the
78/// two F32 gate parameters. There is no `Z` tensor; the output gate is low-rank `g_a`/`g_b`.
79pub struct Glm5NextKdaWeights {
80    pub q_proj: DenseWeight,
81    pub k_proj: DenseWeight,
82    pub v_proj: DenseWeight,
83    /// 2026-09-25: `[conv_dim, conv_kernel]` BF16: the q, k and v conv tensors concatenated, with the
84    /// singleton middle dimension dropped.
85    pub conv: DenseWeight,
86    pub f_a: DenseWeight,
87    pub f_b: DenseWeight,
88    /// 2026-09-25: `[heads * head_dim]` F32, one value per channel.
89    pub dt_bias: DevicePtr,
90    /// 2026-09-25: `[heads]` F32, one value per head (unlike `dt_bias`).
91    pub a_log: DevicePtr,
92    pub b_proj: DenseWeight,
93    pub g_a: DenseWeight,
94    pub g_b: DenseWeight,
95    /// 2026-09-25: `[head_dim]` BF16.
96    pub o_norm: DenseWeight,
97    pub o_proj: DenseWeight,
98}