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}