Skip to main content

frink_models/
swa_layers.rs

1//! **Which layers slide.** llama.cpp's `hparams.is_swa_impl[il]`, as
2//! one value a `ModelConfig` carries, with the per-layer ARRAY form of
3//! `{arch}.attention.sliding_window_pattern` read the way each
4//! architecture's graph reads it.
5//!
6//! Until 2026-09-11 frink carried the layout as a scalar period plus a
7//! phase, read the key as a scalar, and REFUSED the array form for every
8//! architecture. llama.cpp reads the key with `get_key_or_arr`, and that
9//! name hides THREE behaviours, decided per architecture by which
10//! overload the graph's `load_arch_hparams` calls:
11//!
12//! | mode | call | scalar in the file | ARRAY in the file |
13//! |---|---|---|---|
14//! | [`PatternKeyRead::ScalarPeriod`] | `get_key_or_arr(kid, swa_period, false)` | overrides the seeded period | **IGNORED**: the scalar overload returns `false` on an array when `required` is false (`llama-model-loader.cpp:490-512`), and the seeded period stands |
15//! | [`PatternKeyRead::PerLayerBool`] | `get_key_or_arr(kid, hparams.is_swa_impl, n_layer())` | **broadcast** to every layer as a bool (`:474-478`: `result[i] = value`), so `1` slides everything and `0` nothing -- NOT a period | the per-layer truth, length-checked against `n_layer()` (`:461-465`) |
16//! | [`PatternKeyRead::ScalarThenArray`] | the first, then the second on `false` | a period | the per-layer truth |
17//!
18//! The census is measured, not remembered: `grep -n
19//! LLM_KV_ATTENTION_SLIDING_WINDOW_PATTERN src/models/*.cpp` over all
20//! 155 graphs. Twenty-two read the key. Five read only the array
21//! ([`PER_LAYER_ARRAY_READERS`]), two try the scalar and fall back to
22//! the array ([`SCALAR_THEN_ARRAY_READERS`]), and the other fifteen read
23//! only the scalar. An architecture that reads the key in NO form
24//! (`llama`, `qwen2`, ...) is treated as the scalar-only mode here,
25//! which is what the loader did before: a scalar period in such a file
26//! is honoured, an array is ignored.
27//!
28//! **The IGNORED cell is the one that matters for real files.** The
29//! converters write the array for `exaone4`, `exaone-moe`
30//! (`conversion/exaone.py:84`), `olmo2` (`olmo.py:59-66`) and `gemma3n`
31//! (`gemma.py:532-535`), and all four graphs read the SCALAR overload,
32//! so every real EXAONE-4 32B, EXAONE-MoE and Olmo-3 export carries an
33//! array that llama.cpp never looks at and runs on the literal period
34//! (`exaone4.cpp:7`, `exaone-moe.cpp:6`, `olmo2.cpp:9`). frink refused
35//! all of them. That the array and the literal AGREE for every real
36//! checkpoint is why upstream gets away with it; `tests/
37//! window_array_graphs.rs` measures that libllama's logits do not move
38//! when the array is rewritten to DISAGREE, and frink matches both.
39//!
40//! **The length is `block_count`, not the trunk.** `mimo2.cpp:12` and
41//! `step35.cpp:26` pass `hparams.n_layer()` as the array length BEFORE
42//! `:19` / `:32` read `nextn_predict_layers`, so `n_layer()` is still
43//! `n_layer_all` there and the converters write the array at the full
44//! `block_count` with the MTP entries appended (`mimo.py:146-153`,
45//! `step3.py:164-173`). [`read_swa_layers`] therefore checks the length
46//! against `block_count` and keeps the first `n_layers` entries, and
47//! `set_swa_pattern` (`llama-hparams.cpp:19-21`) zeroes the entries past
48//! the trunk anyway. `cohere2moe.cpp:23` reads `nextn_predict_layers`
49//! BEFORE `:35` reads the array, so `n_layer()` is the TRUNK there and
50//! its converter writes one entry per trunk layer (`command_r.py:98`
51//! from `layer_types`); [`ARRAY_AT_TRUNK_LENGTH`] is that one row, and
52//! a file with the array at `block_count` length is refused there as
53//! llama.cpp refuses it.
54
55use std::num::NonZeroUsize;
56use std::sync::Arc;
57
58use frink_gguf::{GgufValue, TensorSource};
59
60use crate::capability::SwaPattern;
61use crate::loader::LoadError;
62use crate::mtp_blocks::TrunkLayers;
63
64/// llama.cpp's `is_swa_impl`, as a rule or as the array itself.
65///
66/// Three spellings, one accessor ([`Self::slides`]). Every consumer --
67/// the CPU attention mask, the KV block layout, the fused Metal
68/// launches, the per-layer RoPE gate -- asks `ModelConfig::
69/// layer_sliding_window(il)`, which asks this. Replacing the two fields
70/// it used to be (`swa_pattern: Option<usize>`, `swa_dense_first:
71/// bool`) with one enum is what makes a fourth spelling a compile error
72/// at every match rather than a silently unhandled case.
73#[derive(Debug, Clone, PartialEq, Eq)]
74pub enum SwaLayers {
75    /// Every layer slides. llama.cpp's `set_swa_pattern(0)`
76    /// (`deepseek4.cpp:68`, `dflash.cpp:54`), and frink's answer for a
77    /// declared window on an architecture with no seeded period.
78    All,
79    /// `set_swa_pattern(period, dense_first)`, `llama-hparams.cpp:8-22`:
80    ///
81    /// - `dense_first = false`: `is_swa[il] = il % p < p - 1`, the LAST
82    ///   layer of every period is full attention;
83    /// - `dense_first = true`: `is_swa[il] = il % p != 0`, the FIRST.
84    ///
85    /// A period of 1 windows NOTHING under either phase, which is
86    /// `phi3.cpp:23`'s spelling and the opposite of [`Self::All`]; it
87    /// is why the period is `NonZeroUsize` rather than `usize` with a
88    /// zero that means "all".
89    Period {
90        period: NonZeroUsize,
91        dense_first: bool,
92    },
93    /// The file's own per-layer answer, one entry per TRUNK layer.
94    /// Indexing past the end answers `false`, which is what
95    /// `set_swa_pattern` writes for every layer past `n_layer()`.
96    PerLayer(Arc<[bool]>),
97}
98
99impl SwaLayers {
100    /// `set_swa_pattern(period, dense_first)` with llama.cpp's own
101    /// degenerate case folded in: a period of 0 is [`Self::All`].
102    pub fn period(period: usize, dense_first: bool) -> Self {
103        match NonZeroUsize::new(period) {
104            Some(period) => Self::Period {
105                period,
106                dense_first,
107            },
108            None => Self::All,
109        }
110    }
111
112    /// The seeded layout for an architecture, or every layer when it
113    /// seeds none.
114    pub fn from_default(layout: Option<SwaPattern>) -> Self {
115        match layout {
116            Some(p) => Self::period(p.period, p.dense_first),
117            None => Self::All,
118        }
119    }
120
121    /// Does layer `layer_idx` slide? llama.cpp's `hparams.is_swa(il)`
122    /// for a model that has a window at all; the window's presence is
123    /// `ModelConfig::sliding_window`'s question, not this one's.
124    #[inline]
125    pub fn slides(&self, layer_idx: usize) -> bool {
126        match self {
127            Self::All => true,
128            Self::Period {
129                period,
130                dense_first,
131            } => {
132                let period = period.get();
133                if *dense_first {
134                    !layer_idx.is_multiple_of(period)
135                } else {
136                    layer_idx % period < period - 1
137                }
138            }
139            Self::PerLayer(layers) => layers.get(layer_idx).copied().unwrap_or(false),
140        }
141    }
142}
143
144/// Which `get_key_or_arr` overload an architecture's `load_arch_hparams`
145/// reads `{arch}.attention.sliding_window_pattern` through. See the
146/// module doc for what each does with each shape.
147#[derive(Debug, Clone, Copy, PartialEq, Eq)]
148pub enum PatternKeyRead {
149    /// `get_key_or_arr(kid, uint32_t & swa_period, false)`: scalar
150    /// honoured as the period, array ignored.
151    ScalarPeriod,
152    /// `get_key_or_arr(kid, hparams.is_swa_impl, n_layer())`: array
153    /// honoured, scalar broadcast as a bool, key REQUIRED.
154    PerLayerBool,
155    /// The scalar overload first, the array on its `false`. The array
156    /// read is REQUIRED, so an absent key refuses.
157    ScalarThenArray,
158}
159
160/// Every graph that reads the key ONLY as the per-layer array, with the
161/// line. `gemma4` and `gemma4-assistant` run on their own engine
162/// (`gemma4_gguf_loader.rs` has read the array since it existed);
163/// `step35` and `mimo2` closed on other seams and run on this one;
164/// `dflash` is deferred.
165pub const PER_LAYER_ARRAY_READERS: &[(&str, &str)] = &[
166    ("gemma4", "src/models/gemma4.cpp:5"),
167    ("gemma4-assistant", "src/models/gemma4-assistant.cpp:7"),
168    ("dflash", "src/models/dflash.cpp:69"),
169    ("step35", "src/models/step35.cpp:26"),
170    ("mimo2", "src/models/mimo2.cpp:12"),
171    // Landed upstream after the 2026-08-04 pin; `ml.get_arr` with no
172    // scalar attempt, like the four above.
173    ("spark2_5", "src/models/spark2-5.cpp:8"),
174    ("maple", "src/models/maple.cpp:8"),
175    ("granite_swa", "src/models/granite-swa.cpp:17"),
176];
177
178/// The graphs that read `nextn_predict_layers` BEFORE the array, so
179/// `hparams.n_layer()` at the read is the trunk and the array is one
180/// entry per TRUNK layer (module doc). Every other array reader takes
181/// it at `block_count`.
182pub const ARRAY_AT_TRUNK_LENGTH: &[(&str, &str)] =
183    &[("cohere2moe", "src/models/cohere2moe.cpp:23,35")];
184
185/// The length `arch`'s array read passes as `n`.
186pub fn array_length(arch: &str, trunk: &TrunkLayers) -> usize {
187    if ARRAY_AT_TRUNK_LENGTH.iter().any(|(a, _)| *a == arch) {
188        trunk.n_layers
189    } else {
190        trunk.block_count
191    }
192}
193
194/// Every graph that tries the scalar and falls back to the array, with
195/// the line.
196pub const SCALAR_THEN_ARRAY_READERS: &[(&str, &str)] = &[
197    ("mellum", "src/models/mellum.cpp:12-17"),
198    ("cohere2moe", "src/models/cohere2moe.cpp:32-36"),
199    // `muse-glimmer.cpp:26` calls `load_swa_pattern(ml, 4)`, which
200    // tries the ARRAY first (`llama-model.cpp:3309`) and falls back to
201    // the scalar and then to a seeded period of 4 with `dense_first =
202    // false`. Array-first versus scalar-first is not observable: one
203    // key holds either a scalar or an array, and each overload returns
204    // false for the other's spelling.
205    ("muse-glimmer", "src/models/muse-glimmer.cpp:26"),
206];
207
208/// THE table. Derived from the two censuses above rather than restated
209/// beside them: an architecture in neither is the scalar mode, which is
210/// also what an architecture that never reads the key gets.
211pub fn pattern_key_read(arch: &str) -> PatternKeyRead {
212    if PER_LAYER_ARRAY_READERS.iter().any(|(a, _)| *a == arch) {
213        PatternKeyRead::PerLayerBool
214    } else if SCALAR_THEN_ARRAY_READERS.iter().any(|(a, _)| *a == arch) {
215        PatternKeyRead::ScalarThenArray
216    } else {
217        PatternKeyRead::ScalarPeriod
218    }
219}
220
221/// Reads `{arch}.attention.sliding_window_pattern` the way `arch`'s
222/// graph does, and falls back to `seeded` -- the architecture's literal
223/// period from `capability::default_swa_layout`, or the family default
224/// -- where llama.cpp would keep its seed.
225///
226/// Called only for a file that HAS a window: for one that does not, no
227/// graph consults `is_swa` and the answer is irrelevant.
228///
229/// `trunk` is here for the array length. See the module doc.
230pub fn read_swa_layers(
231    file: &impl TensorSource,
232    arch: &str,
233    key: &str,
234    trunk: &TrunkLayers,
235    seeded: Option<SwaPattern>,
236) -> Result<SwaLayers, LoadError> {
237    let value = file.metadata(key);
238    let period_from_scalar = |v: &GgufValue| -> Result<SwaLayers, LoadError> {
239        let period = v.as_u64().ok_or_else(|| {
240            LoadError::UnsupportedFeature(
241                arch.to_string(),
242                format!("{key} is neither an unsigned integer nor an array: {v:?}"),
243            )
244        })?;
245        Ok(SwaLayers::period(
246            period as usize,
247            seeded.is_some_and(|p| p.dense_first),
248        ))
249    };
250    let per_layer = |items: &[GgufValue]| -> Result<SwaLayers, LoadError> {
251        // `llama-model-loader.cpp:461-465`: the length must equal the
252        // `n` passed, which is `block_count` for every reader but
253        // `ARRAY_AT_TRUNK_LENGTH` (module doc).
254        let want = array_length(arch, trunk);
255        if items.len() != want {
256            return Err(LoadError::UnsupportedFeature(
257                arch.to_string(),
258                format!(
259                    "{key} has {} entries where llama.cpp's read passes n_layer() = {want} \
260                     (block_count {}, trunk {}); llama.cpp refuses this too (`key has wrong \
261                     array length`, llama-model-loader.cpp:461-465)",
262                    items.len(),
263                    trunk.block_count,
264                    trunk.n_layers
265                ),
266            ));
267        }
268        let mut out = Vec::with_capacity(trunk.n_layers);
269        for (il, item) in items.iter().take(trunk.n_layers).enumerate() {
270            // `get_arr` admits BOOL, UINT32 and INT32 arrays into the
271            // `uint32_t` layer array (`:361-364`) and tests a bool entry
272            // as `x != 0` (`:383-387`); `as_bool` is that rule for every
273            // integer width.
274            out.push(item.as_bool().ok_or_else(|| {
275                LoadError::UnsupportedFeature(
276                    arch.to_string(),
277                    format!("{key} entry {il} is not a bool or integer: {item:?}"),
278                )
279            })?);
280        }
281        Ok(SwaLayers::PerLayer(out.into()))
282    };
283    match pattern_key_read(arch) {
284        PatternKeyRead::ScalarPeriod => match value {
285            // `get_key_or_arr(kid, swa_period, false)` returns false on
286            // an array and the seed stands. NOT a refusal, because every
287            // real EXAONE-4 32B / EXAONE-MoE / Olmo-3 export carries one;
288            // see the module doc.
289            None | Some(GgufValue::Array(_)) => Ok(SwaLayers::from_default(seeded)),
290            Some(scalar) => period_from_scalar(scalar),
291        },
292        PatternKeyRead::ScalarThenArray => match value {
293            // The array read is REQUIRED on the fallback path
294            // (`mellum.cpp:16`, `cohere2moe.cpp:35` pass no `required`),
295            // so an absent key is llama.cpp's `key not found in model`.
296            None => Err(LoadError::MissingHparam(key.to_string())),
297            Some(GgufValue::Array(items)) => per_layer(items),
298            Some(scalar) => period_from_scalar(scalar),
299        },
300        PatternKeyRead::PerLayerBool => match value {
301            // REQUIRED: the array overload defaults `required` to true
302            // and `mimo2.cpp:12` / `step35.cpp:26` pass nothing.
303            None => Err(LoadError::MissingHparam(key.to_string())),
304            Some(GgufValue::Array(items)) => per_layer(items),
305            // `llama-model-loader.cpp:474-478`: the scalar is written
306            // into EVERY entry of `is_swa_impl`, and the graph tests
307            // each entry as a bool. So `6` here does not mean "period
308            // 6", it means every layer slides.
309            Some(scalar) => {
310                let every = scalar.as_bool().ok_or_else(|| {
311                    LoadError::UnsupportedFeature(
312                        arch.to_string(),
313                        format!("{key} is neither a bool, an integer nor an array: {scalar:?}"),
314                    )
315                })?;
316                Ok(SwaLayers::PerLayer(vec![every; trunk.n_layers].into()))
317            }
318        },
319    }
320}
321
322#[cfg(test)]
323mod tests {
324    use super::*;
325
326    fn trunk(block_count: usize, n_layers: usize) -> TrunkLayers {
327        TrunkLayers {
328            block_count,
329            n_layers,
330            n_mtp_blocks: block_count - n_layers,
331        }
332    }
333
334    struct Meta(Vec<(String, GgufValue)>);
335    impl TensorSource for Meta {
336        fn metadata(&self, key: &str) -> Option<&GgufValue> {
337            self.0.iter().find(|(k, _)| k == key).map(|(_, v)| v)
338        }
339        fn find_tensor(&self, _: &str) -> Option<&frink_gguf::TensorInfo> {
340            None
341        }
342        fn tensor_bytes(&self, name: &str) -> Result<&[u8], frink_gguf::GgufError> {
343            Err(frink_gguf::GgufError::TensorNotFound(name.to_string()))
344        }
345        fn tensor_mapped_range(
346            &self,
347            name: &str,
348        ) -> Result<(Arc<frink_gguf::MmapHandle>, std::ops::Range<usize>), frink_gguf::GgufError>
349        {
350            Err(frink_gguf::GgufError::TensorNotFound(name.to_string()))
351        }
352    }
353
354    const KEY: &str = "x.attention.sliding_window_pattern";
355
356    fn with(value: Option<GgufValue>) -> Meta {
357        Meta(value.into_iter().map(|v| (KEY.to_string(), v)).collect())
358    }
359
360    fn bools(v: &[bool]) -> GgufValue {
361        GgufValue::Array(v.iter().map(|&b| GgufValue::Bool(b)).collect())
362    }
363
364    const LAST_DENSE_4: Option<SwaPattern> = Some(SwaPattern {
365        period: 4,
366        dense_first: false,
367    });
368    const DENSE_FIRST_4: Option<SwaPattern> = Some(SwaPattern {
369        period: 4,
370        dense_first: true,
371    });
372
373    /// The two phases of `set_swa_pattern` and its two degenerate
374    /// periods, layer by layer against `llama-hparams.cpp:8-22`.
375    #[test]
376    fn period_matches_set_swa_pattern_in_both_phases() {
377        let last_dense = SwaLayers::period(4, false);
378        let dense_first = SwaLayers::period(4, true);
379        let got: Vec<(bool, bool)> = (0..8)
380            .map(|il| (last_dense.slides(il), dense_first.slides(il)))
381            .collect();
382        let want: Vec<(bool, bool)> = (0..8u32).map(|il| (il % 4 < 3, il % 4 != 0)).collect();
383        assert_eq!(got, want);
384        assert_eq!(SwaLayers::period(0, false), SwaLayers::All);
385        assert!((0..8).all(|il| SwaLayers::All.slides(il)));
386        assert!((0..8).all(|il| !SwaLayers::period(1, false).slides(il)));
387        assert!((0..8).all(|il| !SwaLayers::period(1, true).slides(il)));
388    }
389
390    /// Past the array's end is "does not slide", which is what
391    /// `set_swa_pattern` writes for `il >= n_layer()`.
392    #[test]
393    fn per_layer_answers_false_past_the_trunk() {
394        let layers = SwaLayers::PerLayer(vec![true, false].into());
395        assert!(layers.slides(0));
396        assert!(!layers.slides(1));
397        assert!(!layers.slides(2));
398    }
399
400    /// The census and the table are one thing: every listed reader
401    /// answers its listed mode, and an unlisted name answers the
402    /// scalar mode.
403    #[test]
404    fn every_listed_reader_answers_its_mode() {
405        for (arch, _) in PER_LAYER_ARRAY_READERS {
406            assert_eq!(
407                pattern_key_read(arch),
408                PatternKeyRead::PerLayerBool,
409                "{arch}"
410            );
411        }
412        for (arch, _) in SCALAR_THEN_ARRAY_READERS {
413            assert_eq!(
414                pattern_key_read(arch),
415                PatternKeyRead::ScalarThenArray,
416                "{arch}"
417            );
418        }
419        for arch in ["exaone4", "exaone-moe", "olmo2", "gemma3", "llama"] {
420            assert_eq!(
421                pattern_key_read(arch),
422                PatternKeyRead::ScalarPeriod,
423                "{arch}"
424            );
425        }
426    }
427
428    /// The IGNORED cell: an array in a scalar-mode file leaves the
429    /// seeded period standing, even when the array DISAGREES with it.
430    /// This is the EXAONE / Olmo-3 over-refusal, lifted.
431    #[test]
432    fn a_scalar_mode_architecture_ignores_the_array_and_keeps_its_seed() {
433        // Disagrees with last-dense-4 on every layer.
434        let file = with(Some(bools(&[false, false, false, true])));
435        let got = read_swa_layers(&file, "exaone-moe", KEY, &trunk(4, 4), LAST_DENSE_4).unwrap();
436        assert_eq!(got, SwaLayers::period(4, false));
437        // And an absent key is the same answer.
438        let got =
439            read_swa_layers(&with(None), "exaone-moe", KEY, &trunk(4, 4), LAST_DENSE_4).unwrap();
440        assert_eq!(got, SwaLayers::period(4, false));
441        // A scalar still overrides the seed, keeping the seed's phase.
442        let file = with(Some(GgufValue::U32(2)));
443        let got = read_swa_layers(&file, "exaone-moe", KEY, &trunk(4, 4), LAST_DENSE_4).unwrap();
444        assert_eq!(got, SwaLayers::period(2, false));
445    }
446
447    /// The array mode honours the array, checks its length against
448    /// `block_count`, keeps the trunk's entries, and REQUIRES the key.
449    #[test]
450    fn an_array_mode_architecture_honours_the_array_at_block_count_length() {
451        // Five entries for block_count 5, trunk 4: the MTP entry is
452        // dropped, the trunk's four are kept verbatim.
453        let file = with(Some(bools(&[false, true, true, false, true])));
454        let got = read_swa_layers(&file, "mimo2", KEY, &trunk(5, 4), None).unwrap();
455        assert_eq!(
456            got,
457            SwaLayers::PerLayer(vec![false, true, true, false].into())
458        );
459        // Wrong length is llama.cpp's own refusal.
460        let file = with(Some(bools(&[false, true, true, false])));
461        assert!(matches!(
462            read_swa_layers(&file, "mimo2", KEY, &trunk(5, 4), None),
463            Err(LoadError::UnsupportedFeature(a, m)) if a == "mimo2" && m.contains("4 entries where llama.cpp's read passes n_layer() = 5")
464        ));
465        // Absent is REQUIRED.
466        assert!(matches!(
467            read_swa_layers(&with(None), "mimo2", KEY, &trunk(4, 4), None),
468            Err(LoadError::MissingHparam(k)) if k == KEY
469        ));
470    }
471
472    /// `cohere2moe.cpp:23,35` read the MTP count first, so its array
473    /// is one entry per TRUNK layer, and the `block_count`-long
474    /// spelling is the wrong length there.
475    #[test]
476    fn cohere2moe_s_array_is_trunk_length() {
477        assert_eq!(array_length("cohere2moe", &trunk(5, 4)), 4);
478        assert_eq!(array_length("mimo2", &trunk(5, 4)), 5);
479        let file = with(Some(bools(&[false, true, true, false])));
480        let got = read_swa_layers(&file, "cohere2moe", KEY, &trunk(5, 4), DENSE_FIRST_4).unwrap();
481        assert_eq!(
482            got,
483            SwaLayers::PerLayer(vec![false, true, true, false].into())
484        );
485        let file = with(Some(bools(&[false, true, true, false, true])));
486        assert!(matches!(
487            read_swa_layers(&file, "cohere2moe", KEY, &trunk(5, 4), DENSE_FIRST_4),
488            Err(LoadError::UnsupportedFeature(a, m)) if a == "cohere2moe" && m.contains("n_layer() = 4")
489        ));
490    }
491
492    /// In the array mode a scalar is a broadcast BOOL, not a period:
493    /// `6` slides every layer, `0` slides none.
494    #[test]
495    fn an_array_mode_architecture_broadcasts_a_scalar_as_a_bool() {
496        let got = read_swa_layers(
497            &with(Some(GgufValue::U32(6))),
498            "step35",
499            KEY,
500            &trunk(3, 3),
501            None,
502        )
503        .unwrap();
504        assert_eq!(got, SwaLayers::PerLayer(vec![true; 3].into()));
505        let got = read_swa_layers(
506            &with(Some(GgufValue::U32(0))),
507            "step35",
508            KEY,
509            &trunk(3, 3),
510            None,
511        )
512        .unwrap();
513        assert_eq!(got, SwaLayers::PerLayer(vec![false; 3].into()));
514    }
515
516    /// The scalar-then-array mode takes whichever shape the file has,
517    /// and refuses an absent key because its fallback read is REQUIRED.
518    #[test]
519    fn a_scalar_then_array_architecture_takes_either_shape_and_requires_one() {
520        let got = read_swa_layers(
521            &with(Some(GgufValue::U32(2))),
522            "mellum",
523            KEY,
524            &trunk(4, 4),
525            LAST_DENSE_4,
526        )
527        .unwrap();
528        assert_eq!(got, SwaLayers::period(2, false));
529        let got = read_swa_layers(
530            &with(Some(bools(&[true, true, false, true]))),
531            "mellum",
532            KEY,
533            &trunk(4, 4),
534            LAST_DENSE_4,
535        )
536        .unwrap();
537        assert_eq!(
538            got,
539            SwaLayers::PerLayer(vec![true, true, false, true].into())
540        );
541        assert!(matches!(
542            read_swa_layers(&with(None), "mellum", KEY, &trunk(4, 4), LAST_DENSE_4),
543            Err(LoadError::MissingHparam(k)) if k == KEY
544        ));
545    }
546}