metrale_config/position_encoding.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-29: The positional encoding a model's full-attention layers apply to Q and K.
4//!
5//! Owner: config.
6//! Invariants:
7//! - A config returned by `parse_config` or `config_from_gguf` has
8//! `attn_position_encoding` set (`resolve_attn_position_encoding`).
9//! - A config that declares no positional encoding carries no RoPE variant parameter
10//! (explicit `rotary_dim`, MRoPE, YaRN); such a config is refused, not guessed at.
11
12use anyhow::{Context, Result, bail};
13
14use super::ModelConfig;
15
16/// 2026-09-29: What full attention does to Q and K before the dot product.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum AttnPositionEncoding {
19 /// 2026-09-29: Rotary embedding over `rotary_dim()` dims at `rope_theta`, or the layer's
20 /// RoPE variant (YaRN, proportional, MRoPE) where the loader configures one.
21 Rope,
22 /// 2026-09-29: No positional encoding: Q and K enter attention as projected. Nemotron-H
23 /// attention has none; position reaches it through the Mamba-2 layers only (HF
24 /// `NemotronHAttention.forward` never calls `apply_rotary_pos_emb`).
25 None,
26}
27
28impl ModelConfig {
29 /// 2026-09-29: The resolved positional encoding of full attention. An error for a config
30 /// that never went through `parse_config` / `config_from_gguf` and was not built by a
31 /// factory that sets it.
32 pub fn attn_position_encoding(&self) -> Result<AttnPositionEncoding> {
33 self.attn_position_encoding.with_context(|| {
34 format!(
35 "model_type `{}`: attention positional encoding was never resolved \
36 (config not built by parse_config/config_from_gguf)",
37 self.model_type
38 )
39 })
40 }
41}
42
43/// 2026-09-29: Settle `attn_position_encoding` after the family parse. A family parser that
44/// knows its attention has no positional encoding declares `None`; any other family rotates
45/// Q/K by its rope fields, which is what `Rope` records. A `None` declaration next to a RoPE
46/// variant parameter is contradictory and refused.
47pub(crate) fn resolve_attn_position_encoding(config: &mut ModelConfig) -> Result<()> {
48 match config.attn_position_encoding {
49 Some(AttnPositionEncoding::None) => {
50 let conflicts: Vec<&str> = [
51 (config.rotary_dim > 0, "rotary_dim"),
52 (config.mrope_interleaved, "mrope_interleaved"),
53 (config.mrope_section.iter().any(|&s| s > 0), "mrope_section"),
54 (config.yarn_factor > 0.0, "yarn_factor"),
55 ]
56 .into_iter()
57 .filter_map(|(set, name)| set.then_some(name))
58 .collect();
59 if !conflicts.is_empty() {
60 bail!(
61 "model_type `{}` declares no positional encoding for attention, but the \
62 config also sets RoPE parameters: {}. Refusing an ambiguous config.",
63 config.model_type,
64 conflicts.join(", ")
65 );
66 }
67 }
68 Some(AttnPositionEncoding::Rope) => {}
69 None => config.attn_position_encoding = Some(AttnPositionEncoding::Rope),
70 }
71 Ok(())
72}
73
74#[cfg(test)]
75#[path = "position_encoding_tests.rs"]
76mod tests;