use std::num::NonZeroUsize;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NoRopePhase {
LastOfPeriod,
FirstOfPeriod,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum RopeLayers {
#[default]
All,
SlidingOnly,
NoRopeEvery {
step: NonZeroUsize,
phase: NoRopePhase,
},
}
impl RopeLayers {
#[inline]
pub fn rotates(self, layer_idx: usize, layer_slides: bool) -> bool {
match self {
Self::All => true,
Self::SlidingOnly => layer_slides,
Self::NoRopeEvery { step, phase } => {
let step = step.get();
match phase {
NoRopePhase::LastOfPeriod => !(layer_idx + 1).is_multiple_of(step),
NoRopePhase::FirstOfPeriod => !layer_idx.is_multiple_of(step),
}
}
}
}
pub fn any_layer_unrotated(self, n_layers: usize, slides: impl Fn(usize) -> bool) -> bool {
(0..n_layers).any(|il| !self.rotates(il, slides(il)))
}
}
const LLAMA_CPP_DEFAULT_NO_ROPE_STEP: usize = 4;
const fn step(n: usize) -> NonZeroUsize {
match NonZeroUsize::new(n) {
Some(n) => n,
None => panic!("a no-RoPE step of 0 is not a rule"),
}
}
pub fn rope_layers(arch: &str, n_layers: usize, has_sliding_window: bool) -> RopeLayers {
let no_rope_every = |phase| RopeLayers::NoRopeEvery {
step: step(LLAMA_CPP_DEFAULT_NO_ROPE_STEP),
phase,
};
match arch {
"exaone4" | "exaone-moe" => {
if has_sliding_window {
RopeLayers::SlidingOnly
} else {
RopeLayers::All
}
}
"smollm3" => no_rope_every(NoRopePhase::LastOfPeriod),
"smallthinker" if has_sliding_window && n_layers != LLAMA_CPP_DEFAULT_NO_ROPE_STEP => {
no_rope_every(NoRopePhase::FirstOfPeriod)
}
"afmoe" => no_rope_every(NoRopePhase::LastOfPeriod),
"llama4" if has_sliding_window => no_rope_every(NoRopePhase::LastOfPeriod),
_ => RopeLayers::All,
}
}
pub const PER_LAYER_ROPE_GATES: &[(&str, &str)] = &[
("exaone4", "src/models/exaone4.cpp:116"),
("exaone-moe", "src/models/exaone-moe.cpp:136,155"),
("smollm3", "src/models/smollm3.cpp:5,69"),
("smallthinker", "src/models/smallthinker.cpp:18,108-109"),
("afmoe", "src/models/afmoe.cpp:137-138"),
("llama4", "src/models/llama4.cpp:11,145-146"),
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exaone4_and_exaone_moe_are_one_rule() {
for slides in [true, false] {
for il in 0..8 {
assert_eq!(
rope_layers("exaone4", 64, true).rotates(il, slides),
rope_layers("exaone-moe", 48, true).rotates(il, slides),
"layer {il}, slides={slides}"
);
}
}
assert_eq!(rope_layers("exaone4", 30, false), RopeLayers::All);
}
#[test]
fn exaone4_32b_rotates_three_layers_in_four() {
let rule = rope_layers("exaone4", 64, true);
let slides = |il: usize| il % 4 < 3;
for il in 0..64 {
assert_eq!(
rule.rotates(il, slides(il)),
il % 4 != 3,
"layer {il} of EXAONE-4 32B"
);
}
assert!(rule.any_layer_unrotated(64, slides));
}
#[test]
fn the_two_no_rope_phases_disagree_about_every_layer_they_name() {
let smollm3 = rope_layers("smollm3", 36, false);
let smallthinker = rope_layers("smallthinker", 32, true);
for il in 0..36 {
assert_eq!(smollm3.rotates(il, false), (il + 1) % 4 != 0);
}
for il in 0..32 {
assert_eq!(smallthinker.rotates(il, true), il % 4 != 0);
}
assert_ne!(smollm3, smallthinker);
assert!(smollm3.rotates(0, false) && !smallthinker.rotates(0, true));
assert!(!smollm3.rotates(3, false) && smallthinker.rotates(3, true));
}
#[test]
fn smallthinker_without_a_window_rotates_everything() {
assert_eq!(rope_layers("smallthinker", 32, false), RopeLayers::All);
}
#[test]
fn every_gated_architecture_is_in_the_table() {
for (arch, line) in PER_LAYER_ROPE_GATES {
let rule = rope_layers(arch, 32, true);
assert_ne!(
rule,
RopeLayers::All,
"{arch} is listed as gated at {line} but the table rotates every layer"
);
assert!(
rule.any_layer_unrotated(32, |il| il % 4 < 3),
"{arch}'s rule must actually leave a layer unrotated"
);
}
}
#[test]
fn an_ungated_architecture_rotates_every_layer() {
for arch in ["llama", "qwen3", "gemma3", "olmo2", "exaone", "granite"] {
assert_eq!(
rope_layers(arch, 32, true),
RopeLayers::All,
"{arch} has no `use_rope` in src/models/"
);
}
}
}