Skip to main content

frink_models/
act_layers.rs

1//! WHICH ACTIVATION EACH LAYER RUNS, with WHICH PARAMETERS -- the
2//! per-layer half of `ModelConfig::ffn_activation`.
3//!
4//! llama.cpp has two graphs whose FFN activation takes scalars that
5//! vary by layer, and both read them the same way: `get_key_or_arr` at
6//! `n_layer()` length, an array exactly that long or one scalar
7//! broadcast to every layer (`llama-model-loader.cpp:455-478`).
8//!
9//! * `apertus.cpp:6-9` reads FOUR arrays, `xielu.alpha_n`,
10//!   `xielu.alpha_p`, `xielu.beta`, `xielu.eps` (no architecture
11//!   prefix -- `llama-arch.cpp:370-373`), all REQUIRED, and `:132-138`
12//!   builds `ggml_xielu(up, alpha_n[il], alpha_p[il], beta[il],
13//!   eps[il])` for layer `il`. The activation is xIELU, UNGATED.
14//! * `step35.cpp:28-29` reads TWO optional arrays,
15//!   `{arch}.swiglu_clamp_exp` and `{arch}.swiglu_clamp_shexp`, and
16//!   the generic `build_moe_ffn` / `build_ffn` (`llama-graph.cpp:2146-
17//!   2164`, `:1751-1768`) clamp SwiGLU by layer `il`'s entry when it
18//!   is above `1e-6`. The activation is SwiGLU with one scalar; the
19//!   routed experts read one array and the shared experts AND the
20//!   leading dense layers read the other, because `build_ffn` is both.
21//!
22//! So the two are ONE plumbing question -- "layer `il` runs its FFN
23//! activation with these scalars" -- and TWO activation bodies. This
24//! module is the plumbing: [`XieluLayers`] holds one parameter set per
25//! trunk layer, [`read_xielu_layers`] reads the four keys the way
26//! `get_key_or_arr` does, and [`ModelConfig::layer_ffn_act`] is the
27//! ONE accessor every FFN body asks. The uniform case (every
28//! architecture but these) is the special case where every layer
29//! answers the same parameter-free [`GluAct`].
30//!
31//! The two bodies are `frink_moe::GluAct::Xielu` (caller: `apertus`)
32//! and `frink_moe::GluAct::SwigluClamped` (caller: `step35`), and the
33//! one thing the second needed of the plumbing that the first did not
34//! is the SITE: llama.cpp's `build_moe_ffn` reads one array and its
35//! `build_ffn` the other, so [`ModelConfig::layer_ffn_acts`] answers a
36//! [`LayerFfnActs`] pair -- `routed` for the top-k experts, `dense` for
37//! the dense layers and the shared experts -- and every FFN body names
38//! the field it runs. For every other activation the two fields are
39//! the same value.
40
41use std::sync::Arc;
42
43use frink_gguf::{GgufValue, TensorSource};
44use frink_moe::{ClampForm, GluAct, XieluParams};
45
46use crate::config::{FfnActivation, ModelConfig};
47use crate::loader::LoadError;
48
49/// One xIELU parameter set per TRUNK layer, already folded the way
50/// `ggml_xielu` folds them ([`XieluParams::from_gguf`]).
51///
52/// `Arc` because `ModelConfig` is cloned per request in the server and
53/// `FfnActivation` is compared in `ExecutionPlan`; a shared slice is
54/// both a cheap clone and a value comparison.
55#[derive(Debug, Clone, PartialEq)]
56pub struct XieluLayers(Arc<[XieluParams]>);
57
58impl XieluLayers {
59    /// One entry per trunk layer, in layer order.
60    pub fn new(layers: Vec<XieluParams>) -> Self {
61        Self(layers.into())
62    }
63
64    /// Layer `il`'s parameters.
65    ///
66    /// Indexing panics on a layer the table does not have, and that is
67    /// the right failure: the loader sized the table from the same
68    /// `n_layers` every layer loop runs over, so an out-of-range `il`
69    /// here is a decoder bug, not a file the user handed in.
70    pub fn layer(&self, il: usize) -> XieluParams {
71        self.0[il]
72    }
73
74    /// How many layers the table covers.
75    pub fn len(&self) -> usize {
76        self.0.len()
77    }
78
79    /// Never, for a loaded model; here so `len` has its clippy twin.
80    pub fn is_empty(&self) -> bool {
81        self.0.is_empty()
82    }
83}
84
85/// The four `xielu.*` keys, as `llama-arch.cpp:370-373` spells them:
86/// NO `{arch}.` prefix, unlike every other per-architecture
87/// hyper-parameter.
88pub const XIELU_KEYS: [&str; 4] = ["xielu.alpha_n", "xielu.alpha_p", "xielu.beta", "xielu.eps"];
89
90/// Reads one of the four keys the way `apertus.cpp:6-9` does through
91/// `get_key_or_arr(key, arr, n_layer())`: an array of EXACTLY
92/// `n_layers` floats, or one scalar broadcast to every layer, and an
93/// error when the key is absent (`required = true` is the default).
94fn read_f32_per_layer(
95    file: &impl TensorSource,
96    key: &str,
97    n_layers: usize,
98) -> Result<Vec<f32>, LoadError> {
99    let Some(value) = file.metadata(key) else {
100        return Err(LoadError::MissingHparam(key.to_string()));
101    };
102    match value {
103        GgufValue::Array(items) => {
104            if items.len() != n_layers {
105                return Err(LoadError::UnsupportedFeature(
106                    key.to_string(),
107                    format!(
108                        "array of {} entries for {n_layers} layers; llama.cpp refuses this too \
109                         (`key has wrong array length`, llama-model-loader.cpp:464-465)",
110                        items.len()
111                    ),
112                ));
113            }
114            let mut out = Vec::with_capacity(n_layers);
115            for (il, item) in items.iter().enumerate() {
116                out.push(item.as_f32().ok_or_else(|| {
117                    LoadError::UnsupportedFeature(
118                        key.to_string(),
119                        format!("entry {il} is not a float: {item:?}"),
120                    )
121                })?);
122            }
123            Ok(out)
124        }
125        scalar => scalar
126            .as_f32()
127            .map(|v| vec![v; n_layers])
128            .ok_or_else(|| LoadError::MissingHparam(key.to_string())),
129    }
130}
131
132/// The xIELU table for a file, read exactly as `apertus.cpp:6-9` reads
133/// it, folded exactly as `ggml_xielu` folds it.
134///
135/// `n_layers` is the TRUNK count. `apertus` reads no
136/// `nextn_predict_layers`, so `crate::mtp_blocks` refuses a nonzero one
137/// for it and the trunk is `block_count`; passing the trunk rather than
138/// `block_count` keeps that true if a NextN reader ever adopts xIELU.
139pub fn read_xielu_layers(
140    file: &impl TensorSource,
141    n_layers: usize,
142) -> Result<XieluLayers, LoadError> {
143    let [alpha_n, alpha_p, beta, eps] = XIELU_KEYS;
144    let alpha_n = read_f32_per_layer(file, alpha_n, n_layers)?;
145    let alpha_p = read_f32_per_layer(file, alpha_p, n_layers)?;
146    let beta = read_f32_per_layer(file, beta, n_layers)?;
147    let eps = read_f32_per_layer(file, eps, n_layers)?;
148    Ok(XieluLayers::new(
149        (0..n_layers)
150            .map(|il| XieluParams::from_gguf(alpha_n[il], alpha_p[il], beta[il], eps[il]))
151            .collect(),
152    ))
153}
154
155/// One layer's SwiGLU clamps, as `hparams.swiglu_clamp_exp[il]` /
156/// `swiglu_clamp_shexp[il]`: `0.0` (or anything at or below `1e-6`,
157/// llama-graph.cpp:1753) is no clamp on that site.
158///
159/// Both arrays are OPTIONAL upstream (`step35.cpp:28-29` pass
160/// `required = false`; the arrays are zero-filled at
161/// `llama-model.cpp:1146-1147`), so a file carrying neither runs
162/// plain SwiGLU and the loader picks `FfnActivation::Swiglu` for it.
163#[derive(Debug, Clone, PartialEq)]
164pub struct SwigluClamps {
165    /// `{arch}.swiglu_clamp_exp`, read by `build_moe_ffn` for the
166    /// ROUTED experts (llama-graph.cpp:2225).
167    routed: Arc<[f32]>,
168    /// `{arch}.swiglu_clamp_shexp`, read by `build_ffn` for the SHARED
169    /// experts AND the leading dense layers (llama-graph.cpp:1831),
170    /// because `build_ffn` is both.
171    dense: Arc<[f32]>,
172    /// WHERE the gate's clamp goes, which llama.cpp decides by
173    /// architecture (`llama-graph.cpp:2228`, `:1834`): the four
174    /// architectures listed in [`CLAMP_BEFORE_SILU`] call
175    /// `ggml_swiglu_clamp` and everyone else takes the `else` branch.
176    /// Carried HERE, beside the arrays, so a limit cannot be read
177    /// without the form that says what it means.
178    form: ClampForm,
179}
180
181/// The architectures whose clamped SwiGLU clamps the gate BEFORE the
182/// SiLU (`ggml_swiglu_clamp`), with the line that decides it.
183///
184/// `grep -n 'ggml_swiglu_clamp' src/llama-graph.cpp` is two sites --
185/// the routed experts at `:2228-2229` and the dense/shared FFN at
186/// `:1834-1835` -- and the two lists differ: `maple` and `hy_v4` take
187/// the special form for their ROUTED experts only, `deepseek4` and
188/// `dflash` (with `dsv4_hc_mult > 0`) for both. Only `maple` is on this
189/// engine, and it has no dense or shared FFN at all, so one field
190/// serves it; a row that needed the two sites to disagree would need a
191/// second field and this comment says so.
192pub const CLAMP_BEFORE_SILU: &[(&str, &str)] = &[
193    ("maple", "src/llama-graph.cpp:2228 (routed only)"),
194    ("deepseek4", "src/llama-graph.cpp:1834,2228 (own engine)"),
195    ("hy_v4", "src/llama-graph.cpp:2228 (refused: dedicated)"),
196    (
197        "dflash",
198        "src/llama-graph.cpp:1834,2228 when dsv4_hc_mult > 0 (deferred)",
199    ),
200];
201
202/// Which form `arch`'s clamped SwiGLU takes. See [`CLAMP_BEFORE_SILU`].
203pub fn clamp_form(arch: &str) -> ClampForm {
204    if CLAMP_BEFORE_SILU.iter().any(|(a, _)| *a == arch) {
205        ClampForm::BeforeSilu
206    } else {
207        ClampForm::AfterSilu
208    }
209}
210
211/// llama-graph.cpp:1753 / :2148: `constexpr float eps = 1e-6f; if
212/// (limit > eps)`.
213const CLAMP_EPS: f32 = 1e-6;
214
215impl SwigluClamps {
216    /// One entry per trunk layer in each array, in layer order, with
217    /// the architecture's clamp form.
218    pub fn new(routed: Vec<f32>, dense: Vec<f32>, form: ClampForm) -> Self {
219        assert_eq!(routed.len(), dense.len(), "one entry per layer in both");
220        Self {
221            routed: routed.into(),
222            dense: dense.into(),
223            form,
224        }
225    }
226
227    /// The activation layer `il`'s routed experts run.
228    pub fn routed(&self, il: usize) -> GluAct {
229        self.act(self.routed[il])
230    }
231
232    /// The activation layer `il`'s dense FFN or shared experts run.
233    pub fn dense(&self, il: usize) -> GluAct {
234        self.act(self.dense[il])
235    }
236
237    fn act(&self, limit: f32) -> GluAct {
238        if limit > CLAMP_EPS {
239            GluAct::SwigluClamped {
240                limit,
241                form: self.form,
242            }
243        } else {
244            GluAct::Swiglu
245        }
246    }
247
248    /// How many layers the tables cover.
249    pub fn len(&self) -> usize {
250        self.routed.len()
251    }
252
253    /// Never, for a loaded model; here so `len` has its clippy twin.
254    pub fn is_empty(&self) -> bool {
255        self.routed.is_empty()
256    }
257}
258
259/// Architectures on the generic path whose graph READS the two clamp
260/// arrays: `grep -l LLM_KV_SWIGLU_CLAMP src/models/*.cpp` was
261/// `step35.cpp`, `deepseek4.cpp` and `dflash.cpp` over the then-140
262/// graphs, and the last two are on frink's own DeepSeek-4 engine.
263/// Re-measured over 155 on 2026-09-19 it is SIX: `bailingmoe3`,
264/// `hy-v4` and `maple` read them too, all three refused today for
265/// other reasons, and `maple`'s verdict says the clamp is the one
266/// thing it does NOT need work for -- the verdict deliberately spells
267/// it "the SwiGLU clamp arrays" rather than the key, because
268/// `tests/unaudited_triage.rs`'s guard greps blockers for the key name
269/// and would read a SERVED mention as a missing one. For every other
270/// architecture the
271/// arrays stay zero-filled upstream whatever the file says, so the
272/// keys are dead metadata there and frink ignores them the same way.
273pub const SWIGLU_CLAMP_READERS: &[&str] = &["step35", "maple"];
274
275/// Does this architecture's graph read `swiglu_clamp_exp` / `_shexp`?
276pub fn reads_swiglu_clamps(arch: &str) -> bool {
277    SWIGLU_CLAMP_READERS.contains(&arch)
278}
279
280/// The two clamp arrays for a file, read as `step35.cpp:28-29` reads
281/// them: `get_key_or_arr` at `n_layer()` length with `required =
282/// false`, so an absent key is all zeros. `Ok(None)` when the file
283/// carries NEITHER key, which is plain SwiGLU with nothing per layer
284/// to carry.
285///
286/// `n_layers` is the TRUNK count: `step35.cpp:28-29` run AFTER `:32`
287/// has read `nextn_predict_layers`, so `n_layer()` is the trunk there
288/// -- but the converter writes both arrays at `block_count` length
289/// (`step3.py:207-220`, padded with `0.0` for the MTP blocks), and
290/// llama.cpp's `get_key_or_arr` refuses a length other than the one
291/// asked for. `block_count` is what a real export carries, so that is
292/// the length accepted here, and only the trunk's entries are kept.
293pub fn read_swiglu_clamps(
294    file: &impl TensorSource,
295    arch: &str,
296    trunk: &crate::mtp_blocks::TrunkLayers,
297) -> Result<Option<SwigluClamps>, LoadError> {
298    let key = |k: &str| format!("{arch}.{k}");
299    let (exp_key, shexp_key) = (key("swiglu_clamp_exp"), key("swiglu_clamp_shexp"));
300    if file.metadata(&exp_key).is_none() && file.metadata(&shexp_key).is_none() {
301        return Ok(None);
302    }
303    let read = |k: &str| -> Result<Vec<f32>, LoadError> {
304        if file.metadata(k).is_none() {
305            return Ok(vec![0.0; trunk.n_layers]);
306        }
307        let mut v = read_f32_per_layer(file, k, trunk.block_count)?;
308        v.truncate(trunk.n_layers);
309        Ok(v)
310    };
311    Ok(Some(SwigluClamps::new(
312        read(&exp_key)?,
313        read(&shexp_key)?,
314        clamp_form(arch),
315    )))
316}
317
318/// Architectures whose FFN activation is xIELU: the graphs that call
319/// `ggml_xielu`, measured by `grep -l ggml_xielu src/models/*.cpp` --
320/// `apertus.cpp` alone at this checkout.
321pub const XIELU_ARCHITECTURES: &[&str] = &["apertus"];
322
323/// Does this architecture's FFN run xIELU? See [`XIELU_ARCHITECTURES`].
324pub fn uses_xielu(arch: &str) -> bool {
325    XIELU_ARCHITECTURES.contains(&arch)
326}
327
328/// One layer's FFN activations, by SITE: llama.cpp builds the routed
329/// experts with `build_moe_ffn` and everything else -- the dense
330/// layers' FFN and the shared experts -- with `build_ffn`, and the two
331/// read different clamp arrays (`llama-graph.cpp:2146` vs `:1751`).
332///
333/// A struct rather than a second accessor argument so that a body
334/// cannot ask for "the activation" without saying which; for every
335/// activation but the clamped one the two fields are equal.
336#[derive(Debug, Clone, Copy, PartialEq)]
337pub struct LayerFfnActs {
338    /// What the top-k routed experts run (`build_moe_ffn`).
339    pub routed: GluAct,
340    /// What a dense layer's FFN and the shared experts run
341    /// (`build_ffn`).
342    pub dense: GluAct,
343}
344
345impl LayerFfnActs {
346    fn same(act: GluAct) -> Self {
347        Self {
348            routed: act,
349            dense: act,
350        }
351    }
352
353    /// True when both sites run plain SwiGLU -- the per-layer question
354    /// the fused Metal stacks ask, since their kernels spell nothing
355    /// else.
356    pub fn all_swiglu(self) -> bool {
357        self.routed.is_swiglu() && self.dense.is_swiglu()
358    }
359}
360
361impl ModelConfig {
362    /// Layer `il`'s FFN activations, with their parameters, by site.
363    /// THE accessor: every FFN body -- routed, shared, dense, batched,
364    /// slotted -- reads its activation here and nowhere else.
365    ///
366    /// For every architecture but the parameterised ones this is the
367    /// same answer for every `il` and both sites, which is what
368    /// `ffn_activation` used to be converted to directly; that
369    /// conversion no longer exists, because it could not be written for
370    /// a variant that needs the layer.
371    pub fn layer_ffn_acts(&self, il: usize) -> LayerFfnActs {
372        match &self.ffn_activation {
373            // `SwigluFused` is the same activation as `Swiglu`; it only
374            // says gate and up arrive as one on-disk tensor (Phi), which
375            // the loader has already split by the time a `WeightMatrix`
376            // exists.
377            FfnActivation::Swiglu | FfnActivation::SwigluFused => {
378                LayerFfnActs::same(GluAct::Swiglu)
379            }
380            FfnActivation::Gelu => LayerFfnActs::same(GluAct::Geglu),
381            // Ungated on disk: the loader aliases gate to up and the
382            // body reads `up` alone. See `FfnActivation::ReluSqr`.
383            FfnActivation::ReluSqr => LayerFfnActs::same(GluAct::ReluSqr),
384            FfnActivation::GeluUngated => LayerFfnActs::same(GluAct::GeluUngated),
385            // Gated on disk and in the body: a real gate. See
386            // `FfnActivation::Reglu`.
387            FfnActivation::Reglu => LayerFfnActs::same(GluAct::Reglu),
388            FfnActivation::Xielu(layers) => LayerFfnActs::same(GluAct::Xielu(layers.layer(il))),
389            FfnActivation::SwigluClamped(clamps) => LayerFfnActs {
390                routed: clamps.routed(il),
391                dense: clamps.dense(il),
392            },
393        }
394    }
395
396    /// The ONE activation every layer of this model runs, or `None`
397    /// when it varies by layer -- the whole-model question the fused
398    /// Metal stacks and their eligibility checks ask, since each takes
399    /// one activation uniform for a whole run of layers.
400    ///
401    /// `None` is a refusal at every such site. It is not derived by
402    /// comparing `layer_ffn_act` across layers, because a
403    /// parameterised activation is per layer BY TYPE: a two-layer
404    /// xIELU model whose two parameter sets happen to be equal is still
405    /// not something a kernel with no xIELU in it can serve.
406    pub fn model_ffn_act(&self) -> Option<GluAct> {
407        match &self.ffn_activation {
408            FfnActivation::Xielu(_) | FfnActivation::SwigluClamped(_) => None,
409            FfnActivation::Swiglu
410            | FfnActivation::SwigluFused
411            | FfnActivation::Gelu
412            | FfnActivation::ReluSqr
413            | FfnActivation::GeluUngated
414            | FfnActivation::Reglu => Some(self.layer_ffn_acts(0).dense),
415        }
416    }
417
418    /// Does this model's FFN have no gate matrix on disk?
419    ///
420    /// The two ungated activations share the loader's aliasing
421    /// (`load_dense_expert`), so the question is asked once here rather
422    /// than as `== ReluSqr` at the site, where the second variant would
423    /// have been forgotten.
424    pub fn ffn_is_ungated(&self) -> bool {
425        match &self.ffn_activation {
426            FfnActivation::ReluSqr | FfnActivation::GeluUngated | FfnActivation::Xielu(_) => true,
427            FfnActivation::Swiglu
428            | FfnActivation::SwigluFused
429            | FfnActivation::SwigluClamped(_)
430            | FfnActivation::Gelu
431            | FfnActivation::Reglu => false,
432        }
433    }
434}
435
436#[cfg(test)]
437mod tests {
438    use super::*;
439    use frink_moe::GluAct;
440
441    #[derive(Clone, Default)]
442    struct Meta(Vec<(String, GgufValue)>);
443    impl Meta {
444        fn insert(&mut self, key: &str, value: GgufValue) {
445            self.remove(key);
446            self.0.push((key.to_string(), value));
447        }
448        fn remove(&mut self, key: &str) {
449            self.0.retain(|(k, _)| k != key);
450        }
451    }
452    impl TensorSource for Meta {
453        fn metadata(&self, key: &str) -> Option<&GgufValue> {
454            self.0.iter().find(|(k, _)| k == key).map(|(_, v)| v)
455        }
456        fn find_tensor(&self, _: &str) -> Option<&frink_gguf::TensorInfo> {
457            None
458        }
459        fn tensor_bytes(&self, name: &str) -> Result<&[u8], frink_gguf::GgufError> {
460            Err(frink_gguf::GgufError::TensorNotFound(name.to_string()))
461        }
462        fn tensor_mapped_range(
463            &self,
464            name: &str,
465        ) -> Result<(Arc<frink_gguf::MmapHandle>, std::ops::Range<usize>), frink_gguf::GgufError>
466        {
467            Err(frink_gguf::GgufError::TensorNotFound(name.to_string()))
468        }
469    }
470
471    fn base_config() -> ModelConfig {
472        let mut cfg = crate::config::glm_5_2();
473        cfg.n_layers = 2;
474        cfg
475    }
476
477    fn xielu_config(layers: Vec<XieluParams>) -> ModelConfig {
478        let mut cfg = base_config();
479        cfg.n_layers = layers.len();
480        cfg.ffn_activation = FfnActivation::Xielu(XieluLayers::new(layers));
481        cfg
482    }
483
484    /// The accessor indexes by layer. If it read `[0]` for every layer
485    /// -- which is what a scalar `ffn_activation` conversion amounted
486    /// to -- layer 1 would answer layer 0's parameters here.
487    #[test]
488    fn layer_ffn_act_answers_each_layer_s_own_parameters() {
489        let p0 = XieluParams::from_gguf(0.8, 0.8, 0.5, -1e-6);
490        let p1 = XieluParams::from_gguf(0.2, 1.5, 0.75, -0.3);
491        let cfg = xielu_config(vec![p0, p1]);
492        assert_eq!(cfg.layer_ffn_acts(0), LayerFfnActs::same(GluAct::Xielu(p0)));
493        assert_eq!(cfg.layer_ffn_acts(1), LayerFfnActs::same(GluAct::Xielu(p1)));
494        assert_ne!(p0, p1, "the test needs two different parameter sets");
495        assert!(cfg.ffn_is_ungated());
496        assert_eq!(
497            cfg.model_ffn_act(),
498            None,
499            "a parameterised activation has no whole-model answer, even with equal parameters"
500        );
501        // Equal parameters on every layer are STILL per layer by type.
502        assert_eq!(xielu_config(vec![p0, p0]).model_ffn_act(), None);
503    }
504
505    /// The uniform kinds answer the same thing on every layer, and the
506    /// whole-model accessor agrees with the per-layer one.
507    #[test]
508    fn uniform_activations_answer_the_same_on_every_layer() {
509        for (kind, want, ungated) in [
510            (FfnActivation::Swiglu, GluAct::Swiglu, false),
511            (FfnActivation::SwigluFused, GluAct::Swiglu, false),
512            (FfnActivation::Gelu, GluAct::Geglu, false),
513            (FfnActivation::ReluSqr, GluAct::ReluSqr, true),
514            (FfnActivation::GeluUngated, GluAct::GeluUngated, true),
515            // NOT aliased: the gate is a real tensor, and the body is
516            // the one that reads it.
517            (FfnActivation::Reglu, GluAct::Reglu, false),
518        ] {
519            let mut cfg = base_config();
520            cfg.ffn_activation = kind.clone();
521            for il in 0..cfg.n_layers {
522                assert_eq!(
523                    cfg.layer_ffn_acts(il),
524                    LayerFfnActs::same(want),
525                    "{kind:?} layer {il}"
526                );
527                assert!(cfg.layer_ffn_acts(il).all_swiglu() == (want == GluAct::Swiglu));
528            }
529            assert_eq!(cfg.model_ffn_act(), Some(want), "{kind:?}");
530            assert_eq!(cfg.ffn_is_ungated(), ungated, "{kind:?}");
531        }
532    }
533
534    /// `get_key_or_arr`'s three answers: an array of the right length
535    /// is taken per layer, a scalar is broadcast, and an array of the
536    /// wrong length or a missing key is an error naming the key.
537    #[test]
538    fn the_four_keys_are_read_as_llama_cpp_reads_them() {
539        let arr = |v: &[f32]| GgufValue::Array(v.iter().map(|&x| GgufValue::F32(x)).collect());
540        let mut md = Meta::default();
541        md.insert("xielu.alpha_n", arr(&[0.8, 0.2]));
542        md.insert("xielu.alpha_p", arr(&[0.8, 1.5]));
543        md.insert("xielu.beta", GgufValue::F32(0.5)); // scalar, broadcast
544        md.insert("xielu.eps", arr(&[-1e-6, -0.3]));
545        let layers = read_xielu_layers(&md, 2).expect("reads");
546        assert_eq!(layers.len(), 2);
547        assert_eq!(
548            layers.layer(0),
549            XieluParams::from_gguf(0.8, 0.8, 0.5, -1e-6)
550        );
551        assert_eq!(layers.layer(1), XieluParams::from_gguf(0.2, 1.5, 0.5, -0.3));
552
553        let mut short = md.clone();
554        short.insert("xielu.eps", arr(&[-1e-6]));
555        let err = read_xielu_layers(&short, 2).expect_err("wrong length refuses");
556        let msg = err.to_string();
557        assert!(
558            msg.contains("xielu.eps") && msg.contains("1 entries"),
559            "{msg}"
560        );
561
562        let mut missing = md.clone();
563        missing.remove("xielu.alpha_p");
564        let err = read_xielu_layers(&missing, 2).expect_err("a missing key refuses");
565        assert!(err.to_string().contains("xielu.alpha_p"), "{err}");
566    }
567
568    /// The clamp FORM is decided by architecture, and the two forms
569    /// are different functions wherever the clamp binds.
570    ///
571    /// `maple` is the row that found this: everything else in its graph
572    /// matched libllama with the clamp arrays zeroed, and a fixture
573    /// whose limits never bind would have agreed with either form.
574    #[test]
575    fn the_clamp_form_is_per_architecture_and_the_two_forms_differ() {
576        assert_eq!(clamp_form("maple"), ClampForm::BeforeSilu);
577        assert_eq!(clamp_form("step35"), ClampForm::AfterSilu);
578        assert_eq!(clamp_form("llama"), ClampForm::AfterSilu);
579        for (arch, line) in CLAMP_BEFORE_SILU {
580            assert!(line.contains("llama-graph.cpp:"), "`{arch}` cites no line");
581            assert_eq!(clamp_form(arch), ClampForm::BeforeSilu);
582        }
583        // Above the limit the SiLU's output is clamped in one form and
584        // its INPUT in the other, and `silu(min(g, l)) != min(silu(g), l)`:
585        // at g = 6 and l = 2, `silu(2) = 1.7616` against `min(5.985, 2) = 2`.
586        let before = GluAct::SwigluClamped {
587            limit: 2.0,
588            form: ClampForm::BeforeSilu,
589        };
590        let after = GluAct::SwigluClamped {
591            limit: 2.0,
592            form: ClampForm::AfterSilu,
593        };
594        assert!((before.combine(6.0, 1.0) - 1.761_594).abs() < 1e-5);
595        assert!((after.combine(6.0, 1.0) - 2.0).abs() < 1e-5);
596        // Below it they agree, which is why a fixture has to bind.
597        assert!((before.combine(0.5, 1.0) - after.combine(0.5, 1.0)).abs() < 1e-7);
598    }
599
600    /// The clamp tables: each site reads its own array, a zero entry is
601    /// plain SwiGLU on that site alone, and the whole-model answer is
602    /// `None` even when every entry is zero, because the variant is per
603    /// layer by type.
604    #[test]
605    fn the_clamp_arrays_are_read_per_site_and_zero_means_plain_swiglu() {
606        let mut cfg = base_config();
607        cfg.n_layers = 3;
608        cfg.ffn_activation = FfnActivation::SwigluClamped(SwigluClamps::new(
609            vec![0.0, 1.5, 0.0],
610            vec![2.0, 0.0, 1e-7],
611            ClampForm::AfterSilu,
612        ));
613        assert_eq!(
614            cfg.layer_ffn_acts(0),
615            LayerFfnActs {
616                routed: GluAct::Swiglu,
617                dense: GluAct::SwigluClamped {
618                    limit: 2.0,
619                    form: ClampForm::AfterSilu
620                },
621            }
622        );
623        assert_eq!(
624            cfg.layer_ffn_acts(1),
625            LayerFfnActs {
626                routed: GluAct::SwigluClamped {
627                    limit: 1.5,
628                    form: ClampForm::AfterSilu
629                },
630                dense: GluAct::Swiglu,
631            }
632        );
633        // At or below llama.cpp's 1e-6 is no clamp.
634        assert_eq!(cfg.layer_ffn_acts(2), LayerFfnActs::same(GluAct::Swiglu));
635        assert!(cfg.layer_ffn_acts(2).all_swiglu() && !cfg.layer_ffn_acts(0).all_swiglu());
636        assert_eq!(cfg.model_ffn_act(), None);
637        assert!(!cfg.ffn_is_ungated());
638    }
639
640    /// The two clamp keys are read as `get_key_or_arr(..., false)` reads
641    /// them: absent is zeros, an array is taken at `block_count` length
642    /// and truncated to the trunk, a scalar is broadcast, and a file
643    /// with neither key has no table at all.
644    #[test]
645    fn the_clamp_keys_are_read_as_llama_cpp_reads_them() {
646        let arr = |v: &[f32]| GgufValue::Array(v.iter().map(|&x| GgufValue::F32(x)).collect());
647        let trunk = crate::mtp_blocks::TrunkLayers {
648            block_count: 3,
649            n_layers: 2,
650            n_mtp_blocks: 1,
651        };
652        let mut md = Meta::default();
653        assert_eq!(
654            read_swiglu_clamps(&md, "step35", &trunk).expect("reads"),
655            None
656        );
657        md.insert("step35.swiglu_clamp_exp", arr(&[0.0, 7.0, 0.0]));
658        let clamps = read_swiglu_clamps(&md, "step35", &trunk)
659            .expect("reads")
660            .expect("one key is a table");
661        assert_eq!(clamps.len(), 2, "trunk entries only");
662        assert_eq!(
663            clamps.routed(1),
664            GluAct::SwigluClamped {
665                limit: 7.0,
666                form: ClampForm::AfterSilu
667            }
668        );
669        assert_eq!(clamps.dense(1), GluAct::Swiglu, "the absent key is zeros");
670        md.insert("step35.swiglu_clamp_shexp", GgufValue::F32(16.0));
671        let clamps = read_swiglu_clamps(&md, "step35", &trunk)
672            .expect("reads")
673            .expect("table");
674        assert_eq!(
675            clamps.dense(0),
676            GluAct::SwigluClamped {
677                limit: 16.0,
678                form: ClampForm::AfterSilu
679            }
680        );
681        assert_eq!(
682            clamps.dense(1),
683            GluAct::SwigluClamped {
684                limit: 16.0,
685                form: ClampForm::AfterSilu
686            }
687        );
688        md.insert("step35.swiglu_clamp_exp", arr(&[0.0, 7.0]));
689        let err = read_swiglu_clamps(&md, "step35", &trunk).expect_err("wrong length refuses");
690        assert!(err.to_string().contains("swiglu_clamp_exp"), "{err}");
691        assert!(reads_swiglu_clamps("step35"));
692        for arch in ["llama", "apertus", "laguna", "deepseek2", "gpt-oss"] {
693            assert!(!reads_swiglu_clamps(arch), "{arch}");
694        }
695    }
696
697    /// The table is the measured list of graphs that call
698    /// `ggml_xielu`, and nothing else reads as xIELU.
699    #[test]
700    fn only_the_graphs_that_call_ggml_xielu_use_it() {
701        assert!(uses_xielu("apertus"));
702        for arch in ["llama", "arcee", "plm", "step35", "gemma3"] {
703            assert!(!uses_xielu(arch), "{arch}");
704        }
705        assert_eq!(XIELU_KEYS[0], "xielu.alpha_n", "no architecture prefix");
706    }
707}