use frink_core::WeightMatrix;
use frink_gguf::TensorSource;
use crate::loader::{load_weight_matrix, LoadError};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GateAct {
Sigmoid,
Softplus,
}
impl GateAct {
#[inline]
pub fn apply(self, x: f32) -> f32 {
match self {
GateAct::Sigmoid => 1.0 / (1.0 + (-x).exp()),
GateAct::Softplus => {
if x > 20.0 {
x
} else {
(1.0 + x.exp()).ln()
}
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GateWidth {
PerHead,
PerElement,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GatePresence {
Required,
Optional,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AttnGateSpec {
pub act: GateAct,
pub widths: &'static [GateWidth],
pub presence: GatePresence,
pub lines: &'static str,
}
pub const ATTN_GATE_ARCHS: &[(&str, AttnGateSpec)] = &[
(
"afmoe",
AttnGateSpec {
act: GateAct::Sigmoid,
widths: &[GateWidth::PerElement],
presence: GatePresence::Required,
lines: "src/models/afmoe.cpp:73,154,183-185",
},
),
(
"laguna",
AttnGateSpec {
act: GateAct::Softplus,
widths: &[GateWidth::PerHead, GateWidth::PerElement],
presence: GatePresence::Required,
lines: "src/models/laguna.cpp:110-124,211,246-257",
},
),
(
"step35",
AttnGateSpec {
act: GateAct::Sigmoid,
widths: &[GateWidth::PerHead],
presence: GatePresence::Optional,
lines: "src/models/step35.cpp:96,268-284",
},
),
(
"hrm_text",
AttnGateSpec {
act: GateAct::Sigmoid,
widths: &[GateWidth::PerElement],
presence: GatePresence::Required,
lines: "src/models/hrm-text.cpp:77-78,113-134",
},
),
(
"muse-glimmer",
AttnGateSpec {
act: GateAct::Sigmoid,
widths: &[GateWidth::PerElement],
presence: GatePresence::Required,
lines: "src/models/muse-glimmer.cpp:46,100-135",
},
),
(
"spark2_5",
AttnGateSpec {
act: GateAct::Sigmoid,
widths: &[GateWidth::PerHead],
presence: GatePresence::Required,
lines: "src/models/spark2-5.cpp:41,97-105",
},
),
];
pub const Q_INTERLEAVED_GATE_ARCHS: &[(&str, &str)] = &[
("qwen35", "src/models/qwen35.cpp:59,191-199,229-231"),
("qwen35moe", "src/models/qwen35moe.cpp"),
("qwen3next", "src/models/qwen3next.cpp"),
];
pub fn q_gate_interleaved(arch: &str) -> bool {
Q_INTERLEAVED_GATE_ARCHS.iter().any(|(a, _)| *a == arch)
}
pub fn split_interleaved_q_gate(
fused: &[f32],
rows: usize,
n_heads: usize,
head_dim: usize,
) -> (Vec<f32>, Vec<f32>) {
let width = n_heads * head_dim;
assert_eq!(fused.len(), rows * 2 * width);
let mut q = Vec::with_capacity(rows * width);
let mut gate = Vec::with_capacity(rows * width);
for r in 0..rows {
let row = &fused[r * 2 * width..(r + 1) * 2 * width];
for h in 0..n_heads {
q.extend_from_slice(&row[h * 2 * head_dim..h * 2 * head_dim + head_dim]);
gate.extend_from_slice(&row[h * 2 * head_dim + head_dim..(h + 1) * 2 * head_dim]);
}
}
(q, gate)
}
pub fn apply_interleaved_gate(attn_out: &mut [f32], gate: &[f32]) {
assert_eq!(attn_out.len(), gate.len());
for (a, g) in attn_out.iter_mut().zip(gate) {
*a *= 1.0 / (1.0 + (-g).exp());
}
}
pub const GDN_Z_GATE_ARCHS: &[(&str, &str)] = &[
("qwen3next", "src/models/qwen3next.cpp:92,335"),
("qwen35", "src/models/qwen35.cpp:82,241"),
("qwen35moe", "src/models/qwen35moe.cpp:88,265"),
];
pub fn attn_gate_spec(arch: &str) -> Option<AttnGateSpec> {
ATTN_GATE_ARCHS
.iter()
.find(|(n, _)| *n == arch)
.map(|(_, s)| *s)
}
pub struct AttnGate {
pub proj: WeightMatrix,
pub act: GateAct,
pub width: GateWidth,
}
impl std::fmt::Debug for AttnGate {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AttnGate")
.field(
"proj",
&format_args!("{}x{}", self.proj.rows(), self.proj.cols()),
)
.field("act", &self.act)
.field("width", &self.width)
.finish()
}
}
impl AttnGate {
pub fn load(
file: &impl TensorSource,
arch: &str,
l: usize,
n_heads: usize,
head_dim: usize,
hidden_dim: usize,
) -> Result<Option<AttnGate>, LoadError> {
let Some(spec) = attn_gate_spec(arch) else {
return Ok(None);
};
let name = format!("blk.{l}.attn_gate.weight");
if file.find_tensor(&name).is_none() {
return match spec.presence {
GatePresence::Optional => Ok(None),
GatePresence::Required => Err(LoadError::UnsupportedFeature(
arch.to_string(),
format!(
"{name} is missing; {arch}'s graph gates its attention output through \
it ({}) and llama.cpp refuses the file without it",
spec.lines
),
)),
};
}
let proj = load_weight_matrix(file, &name)?;
let width = match proj.rows() {
r if r == n_heads * head_dim && spec.widths.contains(&GateWidth::PerElement) => {
GateWidth::PerElement
}
r if r == n_heads && spec.widths.contains(&GateWidth::PerHead) => GateWidth::PerHead,
r => {
let admissible: Vec<String> = spec
.widths
.iter()
.map(|w| match w {
GateWidth::PerHead => format!("{n_heads} (per head)"),
GateWidth::PerElement => {
format!("{} (per element)", n_heads * head_dim)
}
})
.collect();
return Err(LoadError::UnsupportedFeature(
arch.to_string(),
format!(
"{name} has {r} output rows; {arch}'s graph ({}) accepts {}",
spec.lines,
admissible.join(" or ")
),
));
}
};
if proj.cols() != hidden_dim {
return Err(LoadError::UnsupportedFeature(
arch.to_string(),
format!(
"{name} reads {} inputs but the hidden width is {hidden_dim}",
proj.cols()
),
));
}
Ok(Some(AttnGate {
proj,
act: spec.act,
width,
}))
}
pub fn apply_rows(&self, normed: &[f32], attn_out: &mut [f32], rows: usize, head_dim: usize) {
debug_assert_eq!(normed.len(), rows * self.proj.cols());
let gate_width = self.proj.rows();
let gates = if rows == 1 {
self.proj.apply(normed)
} else {
self.proj.apply_batch(normed, rows)
};
debug_assert_eq!(gates.len(), rows * gate_width);
let out_width = attn_out.len() / rows;
for (row, g) in attn_out.chunks_mut(out_width).zip(gates.chunks(gate_width)) {
match self.width {
GateWidth::PerElement => {
debug_assert_eq!(row.len(), g.len());
for (x, &gv) in row.iter_mut().zip(g.iter()) {
*x *= self.act.apply(gv);
}
}
GateWidth::PerHead => {
debug_assert_eq!(row.len(), g.len() * head_dim);
for (head, &gv) in row.chunks_mut(head_dim).zip(g.iter()) {
let s = self.act.apply(gv);
for x in head.iter_mut() {
*x *= s;
}
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use frink_core::Tensor;
fn gate(rows: usize, cols: usize, act: GateAct, width: GateWidth) -> AttnGate {
let data: Vec<f32> = (0..rows * cols).map(|i| (i as f32 * 0.37).sin()).collect();
AttnGate {
proj: WeightMatrix::F32(Tensor::new(data, vec![rows, cols])),
act,
width,
}
}
#[test]
fn the_activations_are_ggml_s() {
assert!((GateAct::Sigmoid.apply(0.0) - 0.5).abs() < 1e-7);
assert!((GateAct::Sigmoid.apply(2.0) - 0.880_797).abs() < 1e-5);
assert!((GateAct::Softplus.apply(0.0) - std::f32::consts::LN_2).abs() < 1e-7);
assert!((GateAct::Softplus.apply(-30.0)).abs() < 1e-7);
assert_eq!(GateAct::Softplus.apply(25.0), 25.0);
assert_eq!(GateAct::Softplus.apply(100.0), 100.0);
}
#[test]
fn per_head_broadcasts_over_head_dim_and_per_element_does_not() {
let (n_heads, head_dim, hidden) = (3, 4, 5);
let normed: Vec<f32> = (0..hidden).map(|i| 0.1 * i as f32 - 0.2).collect();
let base: Vec<f32> = (0..n_heads * head_dim).map(|i| 1.0 + i as f32).collect();
let ph = gate(n_heads, hidden, GateAct::Softplus, GateWidth::PerHead);
let mut out = base.clone();
ph.apply_rows(&normed, &mut out, 1, head_dim);
let g = ph.proj.apply(&normed);
for (h, &gh) in g.iter().enumerate() {
for d in 0..head_dim {
let i = h * head_dim + d;
assert!((out[i] - base[i] * GateAct::Softplus.apply(gh)).abs() < 1e-6);
}
}
let pe = gate(
n_heads * head_dim,
hidden,
GateAct::Sigmoid,
GateWidth::PerElement,
);
let mut out = base.clone();
pe.apply_rows(&normed, &mut out, 1, head_dim);
let g = pe.proj.apply(&normed);
for i in 0..n_heads * head_dim {
assert!((out[i] - base[i] * GateAct::Sigmoid.apply(g[i])).abs() < 1e-6);
}
}
#[test]
fn a_batch_gates_each_row_as_the_row_body_would() {
let (n_heads, head_dim, hidden) = (2, 3, 4);
let g = gate(n_heads, hidden, GateAct::Sigmoid, GateWidth::PerHead);
let normed: Vec<f32> = (0..2 * hidden).map(|i| (i as f32).cos()).collect();
let base: Vec<f32> = (0..2 * n_heads * head_dim)
.map(|i| i as f32 * 0.5)
.collect();
let mut batched = base.clone();
g.apply_rows(&normed, &mut batched, 2, head_dim);
for b in 0..2 {
let mut row = base[b * 6..(b + 1) * 6].to_vec();
g.apply_rows(&normed[b * hidden..(b + 1) * hidden], &mut row, 1, head_dim);
assert_eq!(row, &batched[b * 6..(b + 1) * 6], "row {b}");
}
}
#[test]
fn the_table_covers_the_softmax_gates_and_excludes_the_gdn_z_gates() {
let names: Vec<&str> = ATTN_GATE_ARCHS.iter().map(|(n, _)| *n).collect();
assert_eq!(
names,
[
"afmoe",
"laguna",
"step35",
"hrm_text",
"muse-glimmer",
"spark2_5"
]
);
for (arch, spec) in ATTN_GATE_ARCHS {
assert!(spec.lines.contains(".cpp:"), "`{arch}` cites no line");
assert!(!spec.widths.is_empty(), "`{arch}` admits no width");
}
for (arch, _) in GDN_Z_GATE_ARCHS {
assert!(attn_gate_spec(arch).is_none(), "`{arch}` is a z gate");
}
assert!(attn_gate_spec("llama").is_none());
assert_eq!(attn_gate_spec("laguna").unwrap().act, GateAct::Softplus);
assert_eq!(
attn_gate_spec("step35").unwrap().presence,
GatePresence::Optional
);
}
}