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}