1use ferrox_core::matmul::{rms_norm, silu};
31use ferrox_core::weight_matrix::WeightMatrix;
32
33#[derive(Debug, Clone, Copy)]
35pub struct GdnConfig {
36 pub hidden_dim: usize,
37 pub num_v_heads: usize,
38 pub head_dim: usize,
39 pub conv_kernel_size: usize,
40 pub rms_norm_eps: f32,
41}
42
43impl GdnConfig {
44 pub fn qkv_dim(&self) -> usize {
45 3 * self.num_v_heads * self.head_dim
47 }
48
49 pub fn v_dim(&self) -> usize {
50 self.num_v_heads * self.head_dim
51 }
52}
53
54pub struct GdnWeights {
56 pub attn_qkv: WeightMatrix, pub attn_gate: WeightMatrix, pub ssm_conv1d: Vec<f32>,
60 pub ssm_dt: Vec<f32>, pub ssm_a: Vec<f32>, pub ssm_beta: WeightMatrix, pub ssm_alpha: WeightMatrix, pub ssm_norm: Vec<f32>, pub ssm_out: WeightMatrix, }
67
68pub struct GdnState {
70 conv_hist: Vec<f32>,
71 recurrent: Vec<f32>,
73}
74
75impl GdnState {
76 pub fn new(cfg: &GdnConfig) -> Self {
77 Self {
78 conv_hist: Vec::new(),
79 recurrent: vec![0f32; cfg.num_v_heads * cfg.head_dim * cfg.head_dim],
80 }
81 }
82}
83
84fn softplus(x: f32) -> f32 {
85 if x > 20.0 {
86 x
87 } else {
88 (1.0 + x.exp()).ln()
89 }
90}
91
92fn sigmoid(x: f32) -> f32 {
93 1.0 / (1.0 + (-x).exp())
94}
95
96fn l2_normalize(v: &mut [f32], eps: f32) {
97 let norm_sq: f32 = v.iter().map(|x| x * x).sum();
98 let scale = 1.0 / (norm_sq + eps).sqrt();
99 for x in v.iter_mut() {
100 *x *= scale;
101 }
102}
103
104fn causal_conv_step(
106 weight: &[f32],
107 history: &mut Vec<f32>,
108 current: &[f32],
109 kernel_size: usize,
110 dim: usize,
111) -> Vec<f32> {
112 let hist_len = history.len() / dim.max(1);
113 let missing = (kernel_size - 1).saturating_sub(hist_len);
114
115 let mut y = vec![0f32; dim];
116 for j in 0..kernel_size {
117 if j < missing {
118 continue;
119 }
120 let src: &[f32] = if j == kernel_size - 1 {
121 current
122 } else {
123 let hist_idx = j - missing;
124 &history[hist_idx * dim..(hist_idx + 1) * dim]
125 };
126 for d in 0..dim {
127 y[d] += weight[d * kernel_size + j] * src[d];
128 }
129 }
130 for v in y.iter_mut() {
131 *v = silu(*v);
132 }
133
134 history.extend_from_slice(current);
135 let max_hist_len = (kernel_size - 1) * dim;
136 if history.len() > max_hist_len {
137 let excess = history.len() - max_hist_len;
138 history.drain(0..excess);
139 }
140 y
141}
142
143pub fn gdn_forward_token(
146 weights: &GdnWeights,
147 cfg: &GdnConfig,
148 hidden: &[f32],
149 state: &mut GdnState,
150) -> Vec<f32> {
151 assert_eq!(hidden.len(), cfg.hidden_dim);
152 let qkv_dim = cfg.qkv_dim();
153 let v_dim = cfg.v_dim();
154 let head_dim = cfg.head_dim;
155 let n_heads = cfg.num_v_heads;
156
157 let qkv_lin = weights.attn_qkv.apply(hidden);
158 let z = weights.attn_gate.apply(hidden);
159 let beta_raw = weights.ssm_beta.apply(hidden);
160 let alpha_raw = weights.ssm_alpha.apply(hidden);
161
162 let qkv = causal_conv_step(
163 &weights.ssm_conv1d,
164 &mut state.conv_hist,
165 &qkv_lin,
166 cfg.conv_kernel_size,
167 qkv_dim,
168 );
169
170 let qk_dim = n_heads * head_dim;
171 let (q_all, rest) = qkv.split_at(qk_dim);
172 let (k_all, v_all) = rest.split_at(qk_dim);
173
174 let scale = 1.0 / (head_dim as f32).sqrt();
175 let mut y_flat = vec![0f32; v_dim];
176
177 #[allow(clippy::needless_range_loop)]
178 for h in 0..n_heads {
179 let base = h * head_dim;
180 let mut q_h = q_all[base..base + head_dim].to_vec();
181 let mut k_h = k_all[base..base + head_dim].to_vec();
182 let v_h = &v_all[base..base + head_dim];
183
184 l2_normalize(&mut q_h, 1e-6);
185 l2_normalize(&mut k_h, 1e-6);
186 for x in q_h.iter_mut() {
187 *x *= scale;
188 }
189
190 let gate = softplus(alpha_raw[h] + weights.ssm_dt[h]) * weights.ssm_a[h];
192 let decay = gate.exp();
193 let beta = sigmoid(beta_raw[h]);
194
195 let s_base = h * head_dim * head_dim;
196 let s = &mut state.recurrent[s_base..s_base + head_dim * head_dim];
197
198 for cell in s.iter_mut() {
200 *cell *= decay;
201 }
202
203 let mut kv_mem = vec![0f32; head_dim];
205 for v_idx in 0..head_dim {
206 let mut acc = 0f32;
207 for k_idx in 0..head_dim {
208 acc += s[v_idx * head_dim + k_idx] * k_h[k_idx];
209 }
210 kv_mem[v_idx] = acc;
211 }
212
213 for v_idx in 0..head_dim {
215 let delta = (v_h[v_idx] - kv_mem[v_idx]) * beta;
216 for k_idx in 0..head_dim {
217 s[v_idx * head_dim + k_idx] += delta * k_h[k_idx];
218 }
219 }
220
221 for v_idx in 0..head_dim {
223 let mut acc = 0f32;
224 for k_idx in 0..head_dim {
225 acc += s[v_idx * head_dim + k_idx] * q_h[k_idx];
226 }
227 y_flat[base + v_idx] = acc;
228 }
229 }
230
231 let mut gated = vec![0f32; v_dim];
233 for h in 0..n_heads {
234 let base = h * head_dim;
235 let normed = rms_norm(
236 &y_flat[base..base + head_dim],
237 &weights.ssm_norm,
238 cfg.rms_norm_eps,
239 );
240 for i in 0..head_dim {
241 gated[base + i] = silu(z[base + i]) * normed[i];
242 }
243 }
244
245 weights.ssm_out.apply(&gated)
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251 use ferrox_core::tensor::Tensor;
252
253 const HIDDEN: usize = 4;
254 const N_HEADS: usize = 2;
255 const HEAD_DIM: usize = 2;
256 const CONV_K: usize = 2;
257 const QKV_DIM: usize = 3 * N_HEADS * HEAD_DIM; const V_DIM: usize = N_HEADS * HEAD_DIM; fn wm(data: &[f32], rows: usize, cols: usize) -> WeightMatrix {
261 assert_eq!(data.len(), rows * cols);
262 WeightMatrix::F32(Tensor::new(data.to_vec(), vec![rows, cols]))
263 }
264
265 fn cfg() -> GdnConfig {
266 GdnConfig {
267 hidden_dim: HIDDEN,
268 num_v_heads: N_HEADS,
269 head_dim: HEAD_DIM,
270 conv_kernel_size: CONV_K,
271 rms_norm_eps: 1e-5,
272 }
273 }
274
275 fn make_weights() -> GdnWeights {
276 let mut qkv = Vec::with_capacity(QKV_DIM * HIDDEN);
278 for i in 0..QKV_DIM * HIDDEN {
279 qkv.push(((i % 7) as f32 - 3.0) * 0.1);
280 }
281 let mut gate = Vec::with_capacity(V_DIM * HIDDEN);
282 for i in 0..V_DIM * HIDDEN {
283 gate.push(((i % 5) as f32 - 2.0) * 0.08);
284 }
285 let mut conv = Vec::with_capacity(QKV_DIM * CONV_K);
286 for i in 0..QKV_DIM * CONV_K {
287 conv.push(if i % CONV_K == CONV_K - 1 { 1.0 } else { 0.1 });
288 }
289 let mut beta = Vec::with_capacity(N_HEADS * HIDDEN);
290 let mut alpha = Vec::with_capacity(N_HEADS * HIDDEN);
291 for i in 0..N_HEADS * HIDDEN {
292 beta.push(((i % 3) as f32 - 1.0) * 0.2);
293 alpha.push(((i % 4) as f32 - 1.5) * 0.15);
294 }
295 let mut out = Vec::with_capacity(HIDDEN * V_DIM);
296 for i in 0..HIDDEN * V_DIM {
297 out.push(((i % 6) as f32 - 2.5) * 0.12);
298 }
299 GdnWeights {
300 attn_qkv: wm(&qkv, QKV_DIM, HIDDEN),
301 attn_gate: wm(&gate, V_DIM, HIDDEN),
302 ssm_conv1d: conv,
303 ssm_dt: vec![0.1, -0.05],
304 ssm_a: vec![-0.5, -0.75],
306 ssm_beta: wm(&beta, N_HEADS, HIDDEN),
307 ssm_alpha: wm(&alpha, N_HEADS, HIDDEN),
308 ssm_norm: vec![1.0, 1.0],
309 ssm_out: wm(&out, HIDDEN, V_DIM),
310 }
311 }
312
313 #[test]
314 fn gdn_forward_token_tiny_dims_finite_and_shaped() {
315 let weights = make_weights();
316 let cfg = cfg();
317 let mut state = GdnState::new(&cfg);
318 let hidden = [0.2f32, -0.1, 0.3, -0.4];
319
320 let out0 = gdn_forward_token(&weights, &cfg, &hidden, &mut state);
321 assert_eq!(out0.len(), HIDDEN);
322 assert!(out0.iter().all(|x| x.is_finite()));
323
324 let out1 = gdn_forward_token(&weights, &cfg, &hidden, &mut state);
325 assert_eq!(out1.len(), HIDDEN);
326 assert!(out1.iter().all(|x| x.is_finite()));
327 assert!(
329 out0.iter()
330 .zip(out1.iter())
331 .any(|(a, b)| (a - b).abs() > 1e-6),
332 "recurrent state should change the second token"
333 );
334 }
335
336 #[test]
337 fn softplus_matches_closed_form_at_zero() {
338 assert!((softplus(0.0) - (2.0f32).ln()).abs() < 1e-6);
339 }
340}