Skip to main content

frink_models/
attn_temperature.rs

1//! **Per-position attention temperature** -- llama.cpp's
2//! `llm_graph_input_attn_temp`, as one value a `ModelConfig` carries
3//! and one table that says which architectures compute it.
4//!
5//! # What it is
6//!
7//! `llama-graph.cpp:155-172` fills an `F32 [n_tokens]` input with one
8//! scalar per token,
9//!
10//! ```text
11//! attn_scale[i] = log(floor((pos[i] + offset) / floor_scale) + 1) * temp_scale + 1
12//! ```
13//!
14//! and the graphs that use it multiply Q by that vector -- broadcast
15//! over every head and every channel of a token's Q -- AFTER RoPE and
16//! BEFORE `build_attn`, with `kq_scale` untouched
17//! (`mistral3.cpp:153-156`). The scale is 1 at every position below
18//! `floor_scale`, so the effect is invisible on a short prompt and
19//! grows stepwise with the position: it is Llama-4's "attention
20//! temperature tuning" (`llama4.cpp:15-17` seeds the three constants
21//! and calls the input by that name), which mistral3 and deepseek2
22//! adopted through a GGUF key.
23//!
24//! # Who reads it -- MEASURED, not read off one file
25//!
26//! `grep -ln 'attn_temp\|temperature_scale\|build_inp_attn_scale'
27//! src/models/*.cpp` over all 155 graphs, 2026-09-11:
28//!
29//! | arch | scale | floor | offset | layers | line |
30//! |---|---|---|---|---|---|
31//! | `mistral3` | `attention.temperature_scale` | `n_ctx_orig_yarn` | 0 | all | `mistral3.cpp:5,14-17,109-111,153-156` |
32//! | `deepseek2` (+ `mistral4`, `models.h:1311` reuses its hparams) | `attention.temperature_scale` | `attention.temperature_length` | 0 | all | `deepseek2.cpp:46-49,457-460,595-598,632-635` |
33//! | `llama4` | literal `0.1` | literal `8192` | literal `1.0` | the NON-ROPE layers only, and only on its chunked-SWA branch | `llama4.cpp:15-17,122-123,175-176` |
34//!
35//! Two other hits are NOT this feature and are recorded so nobody
36//! re-derives it: `grok.cpp:23` reads `attention.temperature_length`
37//! into `hparams.attn_temp_length` and applies it nowhere
38//! (`crate::scalar_multipliers` already says so), and `dflash.cpp:133`
39//! / `deepseek4.cpp:124` name a `blk.N.hc_attn_scale` TENSOR, which is
40//! a hyper-connection weight. `plamo3.cpp:140` is a local variable.
41//!
42//! So `mistral3` is the ONLY generic-path reader of a KEY, and
43//! [`resolve_attn_temperature`] is written for the two that read one.
44//! `llama4` seeds LITERALS ([`LITERAL_ATTN_TEMPERATURE`]) on the
45//! branch every real export takes (`llama4.cpp:13-17`; the other
46//! branch is refused by name in `crate::chunked_swa`) and gates the
47//! multiply on `!use_rope` (`:175` is an `else if` on the RoPE branch),
48//! so [`AttnTemperature::unrotated_layers_only`] is the per-layer
49//! variant, read against `ModelConfig::layer_rotates` by the one
50//! helper. It landed with its one caller, as the OLMo lesson says a
51//! variant should.
52//!
53//! # The floor is `n_ctx_orig_yarn`, which is NOT a key
54//!
55//! `mistral3.cpp:15` takes the floor from `hparams.n_ctx_orig_yarn`,
56//! and `llama-model.cpp:1164-1165` seeds that from `n_ctx_train`
57//! (`{arch}.context_length`, REQUIRED) before letting
58//! `rope.scaling.original_context_length` override it. So a Ministral
59//! file without the YaRN key floors on its context length, not on
60//! nothing, and [`resolve_attn_temperature`] takes the already-resolved
61//! value rather than the key. `mistral3.cpp:16-17` then throws on a
62//! zero floor; frink refuses the same file.
63//!
64//! # Why a nonzero key on any other architecture is IGNORED here
65//!
66//! `build_qkv` and `build_attn` never look at `f_attn_temp_scale`; only
67//! the three graphs above build the input. A `llama` file carrying
68//! `llama.attention.temperature_scale = 0.5` runs unscaled in llama.cpp
69//! and must run unscaled here, and
70//! `a_nonzero_key_on_an_architecture_whose_graph_never_reads_it_is_dead_metadata`
71//! pins that. The defect this module closes is the other direction:
72//! before it, a `mistral3` file carrying the key loaded and ran at the
73//! wrong temperature with no error.
74//!
75//! # Where it is applied
76//!
77//! ONE helper, `Decoder::apply_attn_temperature`, called from the CPU
78//! row body and both batched host bodies at the point
79//! `attention_scale` is applied (after RoPE and the post-RoPE QK-norm,
80//! before attention). No fused Metal launch has a per-token Q scale
81//! uniform, so `Decoder::metal_can_serve_model` keeps a model with one
82//! on the host bodies -- the same fence `clamp_kqv` and
83//! `residual_scale` share, for the same reason.
84//!
85//! The MLA engine (`deepseek2` / `mistral4`) REFUSES a file that
86//! declares a nonzero scale rather than implementing it: that engine
87//! has no libllama-golden fixture at all, so an implementation there
88//! would be a guess with nothing to check it against.
89
90use std::num::NonZeroU32;
91
92/// llama.cpp's three attention-temperature hyper-parameters, for a
93/// model whose graph applies them.
94///
95/// Only ever `Some` in a `ModelConfig` when `scale != 0`, which is
96/// llama.cpp's own gate (`mistral3.cpp:14,109`): a zero scale builds
97/// no input tensor there and builds no value here.
98#[derive(Debug, Clone, Copy, PartialEq)]
99pub struct AttnTemperature {
100    /// `hparams.f_attn_temp_scale`, the multiplier on the log term.
101    pub scale: f32,
102    /// `hparams.n_attn_temp_floor_scale`, the position period at which
103    /// the log's argument steps. `llama-graph.cpp:160` asserts it is
104    /// nonzero, so the type says so.
105    pub floor_scale: NonZeroU32,
106    /// `hparams.f_attn_temp_offset`, added to the position before the
107    /// division. `0.0` for both key-driven readers; `llama4` uses `1.0`.
108    pub offset: f32,
109    /// Multiply Q on the layers that do NOT rotate and leave the
110    /// rotating ones alone: `llama4.cpp:163-177` is `if (use_rope) {
111    /// rope } else if (inp_attn_scale) { mul }`. The two key-driven
112    /// readers scale every layer (`mistral3.cpp:153-156` has no
113    /// branch).
114    pub unrotated_layers_only: bool,
115}
116
117impl AttnTemperature {
118    /// The multiplier for a token at `pos`, in the precision
119    /// `llama-graph.cpp:163-167` computes it.
120    ///
121    /// Transcribed rather than simplified: `pos` is a `float` there,
122    /// the offset add and the division by the (integer-promoted) floor
123    /// are single precision, `std::floor` keeps single precision, and
124    /// the `+ 1.0`, `std::log`, `* scale` and `+ 1.0` are double before
125    /// the store to `float`. Evaluating it all in `f64` would agree to
126    /// ~1e-7 on any real file and disagree by a whole step on a position
127    /// that lands exactly on a floor boundary after single-precision
128    /// rounding.
129    #[inline]
130    pub fn scale_at(self, pos: usize) -> f32 {
131        let pos = pos as f32;
132        let floored = ((pos + self.offset) / (self.floor_scale.get() as f32)).floor();
133        ((f64::from(floored) + 1.0).ln() * f64::from(self.scale) + 1.0) as f32
134    }
135
136    /// Multiplies each row of a `[rows, q_width]` Q batch by that row's
137    /// temperature.
138    ///
139    /// One body for the row path (`rows == 1`) and both batched bodies,
140    /// taking the position as a function of the row so that the prefill
141    /// body's `start_pos + b` and the multi-sequence body's `positions[b]`
142    /// are two callers of one loop rather than two loops.
143    pub fn apply_rows(self, q: &mut [f32], q_width: usize, pos_of_row: impl Fn(usize) -> usize) {
144        for (b, row) in q.chunks_mut(q_width).enumerate() {
145            let s = self.scale_at(pos_of_row(b));
146            // `scale_at` is exactly 1.0 below the first floor step
147            // (`ln(1) * scale + 1`), which is every position of a
148            // prompt shorter than `floor_scale`; skipping the multiply
149            // there is a no-op made cheaper, not a different answer.
150            if s != 1.0 {
151                for v in row.iter_mut() {
152                    *v *= s;
153                }
154            }
155        }
156    }
157}
158
159/// Where a reader's floor comes from.
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161pub enum FloorSource {
162    /// `hparams.n_ctx_orig_yarn` -- `rope.scaling.original_context_length`
163    /// with `context_length` as its default (`llama-model.cpp:1164-1165`).
164    /// `mistral3.cpp:15`.
165    OrigCtxYarn,
166    /// `{arch}.attention.temperature_length`, read as optional and
167    /// defaulting to 0 -- which `llama-graph.cpp:160` then aborts on.
168    /// `deepseek2.cpp:47`.
169    TemperatureLengthKey,
170}
171
172/// Every architecture whose graph multiplies Q by the temperature
173/// input, with where its floor comes from and the line that applies it.
174///
175/// This is the CENSUS and [`resolve_attn_temperature`] is the
176/// implementation; `every_key_driven_reader_resolves_to_a_temperature`
177/// checks one against the other so a name added to one without the
178/// other fails. `llama4` is absent because it reads no key (see the
179/// module doc); `capability::unaudited_triage` carries it.
180pub const ATTN_TEMPERATURE_READERS: &[(&str, FloorSource, &str)] = &[
181    (
182        "mistral3",
183        FloorSource::OrigCtxYarn,
184        "src/models/mistral3.cpp:5,14-17,153-156",
185    ),
186    (
187        "deepseek2",
188        FloorSource::TemperatureLengthKey,
189        "src/models/deepseek2.cpp:46-49,595-598,632-635",
190    ),
191    (
192        "mistral4",
193        FloorSource::TemperatureLengthKey,
194        "src/models/models.h:1311-1318 (reuses deepseek2's hparams and graph)",
195    ),
196];
197
198/// The graphs that seed the three constants from LITERALS rather than
199/// keys, with the values and the line. One of 155 (`grep -n
200/// f_attn_temp_scale src/models/*.cpp`): the three assignments at
201/// `llama4.cpp:15-17`, on the chunked branch its files take.
202pub const LITERAL_ATTN_TEMPERATURE: &[(&str, AttnTemperature, &str)] = &[(
203    "llama4",
204    AttnTemperature {
205        scale: 0.1,
206        floor_scale: NonZeroU32::new(8192).unwrap(),
207        offset: 1.0,
208        unrotated_layers_only: true,
209    },
210    "src/models/llama4.cpp:15-17,175-176",
211)];
212
213/// What the file declares, gathered by the loader.
214#[derive(Debug, Clone, Copy, PartialEq)]
215pub struct DeclaredTemperature {
216    /// `{arch}.attention.temperature_scale`.
217    pub scale: Option<f32>,
218    /// `{arch}.attention.temperature_length`.
219    pub length: Option<u64>,
220    /// `hparams.n_ctx_orig_yarn` as llama.cpp resolves it: the YaRN
221    /// original-context key, else `context_length`.
222    pub n_ctx_orig_yarn: Option<u64>,
223}
224
225/// Why a declared temperature cannot be honoured.
226#[derive(Debug, Clone, PartialEq, Eq)]
227pub enum AttnTemperatureError {
228    /// The floor resolved to zero or is absent, which llama.cpp refuses
229    /// at load (`mistral3.cpp:16-17`) or aborts on at the first batch
230    /// (`llama-graph.cpp:160`).
231    ZeroFloor(FloorSource),
232    /// The floor does not fit the `uint32_t` llama.cpp stores it in.
233    FloorTooLarge(u64),
234}
235
236impl AttnTemperatureError {
237    /// The sentence the loader puts in its error.
238    pub fn message(&self, arch: &str) -> String {
239        match self {
240            AttnTemperatureError::ZeroFloor(FloorSource::OrigCtxYarn) => format!(
241                "`{arch}.attention.temperature_scale` is nonzero but the floor it divides \
242                 positions by -- `{arch}.rope.scaling.original_context_length`, else \
243                 `{arch}.context_length` (llama-model.cpp:1164-1165) -- is zero or absent; \
244                 llama.cpp refuses the same file (`invalid n_ctx_orig_yarn for attention \
245                 temperature scaling`, src/models/mistral3.cpp:16-17)"
246            ),
247            AttnTemperatureError::ZeroFloor(FloorSource::TemperatureLengthKey) => format!(
248                "`{arch}.attention.temperature_scale` is nonzero but \
249                 `{arch}.attention.temperature_length` is zero or absent; llama.cpp reads \
250                 the floor from that key (src/models/deepseek2.cpp:47) and aborts on a zero \
251                 one at the first batch (llama-graph.cpp:160)"
252            ),
253            AttnTemperatureError::FloorTooLarge(v) => format!(
254                "`{arch}`'s attention-temperature floor {v} does not fit the uint32 \
255                 llama.cpp stores `n_attn_temp_floor_scale` in"
256            ),
257        }
258    }
259}
260
261/// The temperature the graph applies for `arch`, from what the file
262/// declares.
263///
264/// `Ok(None)` is "no temperature": an architecture whose graph never
265/// builds the input (every one not in [`ATTN_TEMPERATURE_READERS`],
266/// whatever the file says), or a reader whose scale is absent or
267/// exactly zero (`mistral3.cpp:14` and `deepseek2.cpp:458` both test
268/// `!= 0.0f`).
269pub fn resolve_attn_temperature(
270    arch: &str,
271    declared: DeclaredTemperature,
272) -> Result<Option<AttnTemperature>, AttnTemperatureError> {
273    // The literal seeders first: the file's keys are not read for them
274    // (`llama4.cpp:3-26` reads no `temperature_*` key), so a declared
275    // value is dead metadata as it is on every non-reader.
276    if let Some((_, literal, _)) = LITERAL_ATTN_TEMPERATURE
277        .iter()
278        .find(|(name, _, _)| *name == arch)
279    {
280        return Ok(Some(*literal));
281    }
282    let Some((_, floor_source, _)) = ATTN_TEMPERATURE_READERS
283        .iter()
284        .find(|(name, _, _)| *name == arch)
285    else {
286        return Ok(None);
287    };
288    let scale = match declared.scale {
289        Some(s) if s != 0.0 => s,
290        _ => return Ok(None),
291    };
292    let floor = match floor_source {
293        FloorSource::OrigCtxYarn => declared.n_ctx_orig_yarn,
294        FloorSource::TemperatureLengthKey => declared.length,
295    }
296    .unwrap_or(0);
297    let floor = u32::try_from(floor).map_err(|_| AttnTemperatureError::FloorTooLarge(floor))?;
298    let floor_scale =
299        NonZeroU32::new(floor).ok_or(AttnTemperatureError::ZeroFloor(*floor_source))?;
300    Ok(Some(AttnTemperature {
301        scale,
302        floor_scale,
303        // Both key-driven readers assign 0.0 (`mistral3.cpp:11`,
304        // `deepseek2.cpp:49`); only llama4's literal is 1.0.
305        offset: 0.0,
306        unrotated_layers_only: false,
307    }))
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313
314    fn temp(scale: f32, floor: u32) -> AttnTemperature {
315        AttnTemperature {
316            scale,
317            floor_scale: NonZeroU32::new(floor).unwrap(),
318            offset: 0.0,
319            unrotated_layers_only: false,
320        }
321    }
322
323    /// The literal seeder answers its literals whatever the file says,
324    /// including a declared key it never reads, and no key-driven
325    /// reader is in the literal table.
326    #[test]
327    fn llama4_takes_its_literals_and_ignores_a_declared_key() {
328        let declared = DeclaredTemperature {
329            scale: Some(0.5),
330            length: Some(16),
331            n_ctx_orig_yarn: Some(16),
332        };
333        let got = resolve_attn_temperature("llama4", declared)
334            .unwrap()
335            .expect("llama4 always scales");
336        assert_eq!(got, LITERAL_ATTN_TEMPERATURE[0].1);
337        assert!(got.unrotated_layers_only);
338        assert_eq!(got.scale_at(8190), 1.0);
339        assert!(got.scale_at(8191) > 1.0);
340        for (name, _, _) in ATTN_TEMPERATURE_READERS {
341            assert!(
342                !LITERAL_ATTN_TEMPERATURE.iter().any(|(n, _, _)| n == name),
343                "{name} reads a key and cannot also seed a literal"
344            );
345        }
346    }
347
348    /// The formula, position by position, against a hand evaluation of
349    /// `llama-graph.cpp:165-167`: exactly 1 below the first floor, then
350    /// `1 + scale * ln(k + 1)` on the k-th period.
351    #[test]
352    fn the_scale_steps_at_every_floor_boundary_and_is_one_before_the_first() {
353        let t = temp(0.5, 2);
354        for pos in 0..2 {
355            assert_eq!(t.scale_at(pos), 1.0, "position {pos} is below the floor");
356        }
357        for pos in 2..4 {
358            let want = (2f64.ln() * 0.5 + 1.0) as f32;
359            assert_eq!(t.scale_at(pos), want, "position {pos} is on period 1");
360        }
361        for pos in 4..6 {
362            let want = (3f64.ln() * 0.5 + 1.0) as f32;
363            assert_eq!(t.scale_at(pos), want, "position {pos} is on period 2");
364        }
365        // Not monotone-trivial: a sign error on the scale would still
366        // step, so pin the direction.
367        assert!(t.scale_at(5) > t.scale_at(3) && t.scale_at(3) > t.scale_at(1));
368    }
369
370    /// Llama-4's literal offset: `floor((pos + 1) / 8192)` steps one
371    /// position EARLIER than `floor(pos / 8192)`, at 8191 rather than
372    /// 8192. The offset is a field so that the day llama4 is served the
373    /// value is a table entry and not a second formula.
374    #[test]
375    fn the_offset_moves_the_step_by_one_position() {
376        let no_offset = temp(0.1, 8192);
377        let llama4 = AttnTemperature {
378            offset: 1.0,
379            ..no_offset
380        };
381        assert_eq!(no_offset.scale_at(8191), 1.0);
382        assert!(llama4.scale_at(8191) > 1.0, "llama4 steps at 8191");
383        assert_eq!(llama4.scale_at(8191), no_offset.scale_at(8192));
384    }
385
386    /// `apply_rows` scales every channel of a row by that ROW's
387    /// position, and a row below the floor is bit-identical.
388    #[test]
389    fn apply_rows_scales_each_row_by_its_own_position() {
390        let t = temp(0.5, 2);
391        let mut q = vec![1.0f32; 3 * 4];
392        // Rows at positions 1, 2, 5: below the floor, period 1, period 2.
393        let positions = [1usize, 2, 5];
394        t.apply_rows(&mut q, 4, |b| positions[b]);
395        assert_eq!(&q[..4], &[1.0; 4], "position 1 is untouched");
396        for v in &q[4..8] {
397            assert_eq!(*v, t.scale_at(2));
398        }
399        for v in &q[8..] {
400            assert_eq!(*v, t.scale_at(5));
401        }
402        assert_ne!(
403            q[4], q[8],
404            "the two scaled rows must differ, or this saw one scale"
405        );
406    }
407
408    /// The census and the resolver agree: every key-driven reader gets
409    /// a temperature from a file that declares one, and the floor comes
410    /// from the source the census names.
411    #[test]
412    fn every_key_driven_reader_resolves_to_a_temperature() {
413        for (arch, source, line) in ATTN_TEMPERATURE_READERS {
414            let declared = DeclaredTemperature {
415                scale: Some(0.25),
416                length: Some(64),
417                n_ctx_orig_yarn: Some(4096),
418            };
419            let got = resolve_attn_temperature(arch, declared)
420                .unwrap_or_else(|e| panic!("{arch} ({line}): {e:?}"))
421                .unwrap_or_else(|| panic!("{arch} ({line}) reads the key and must resolve"));
422            let want_floor = match source {
423                FloorSource::OrigCtxYarn => 4096,
424                FloorSource::TemperatureLengthKey => 64,
425            };
426            assert_eq!(got.floor_scale.get(), want_floor, "{arch}'s floor source");
427            assert_eq!(got.scale, 0.25);
428            assert_eq!(got.offset, 0.0);
429        }
430    }
431
432    /// The other direction, and the one the oracle decides: only three
433    /// graphs build the input, so the key on any other architecture is
434    /// dead metadata in llama.cpp and stays dead here.
435    #[test]
436    fn a_nonzero_key_on_an_architecture_whose_graph_never_reads_it_is_dead_metadata() {
437        let declared = DeclaredTemperature {
438            scale: Some(0.5),
439            length: Some(64),
440            n_ctx_orig_yarn: Some(4096),
441        };
442        for arch in ["llama", "qwen3", "gemma3", "olmo2", "granite", "smollm3"] {
443            assert_eq!(
444                resolve_attn_temperature(arch, declared),
445                Ok(None),
446                "{arch} has no build_inp_attn_scale in src/models/"
447            );
448        }
449    }
450
451    /// `!= 0.0f` is llama.cpp's gate on both readers: an absent key and
452    /// an explicit zero both build no input.
453    #[test]
454    fn a_zero_or_absent_scale_is_no_temperature() {
455        for scale in [None, Some(0.0)] {
456            let declared = DeclaredTemperature {
457                scale,
458                length: Some(64),
459                n_ctx_orig_yarn: Some(4096),
460            };
461            assert_eq!(resolve_attn_temperature("mistral3", declared), Ok(None));
462            assert_eq!(resolve_attn_temperature("deepseek2", declared), Ok(None));
463        }
464    }
465
466    /// `mistral3.cpp:15` floors on `n_ctx_orig_yarn` and NOT on
467    /// `attention.temperature_length`; `deepseek2.cpp:47` the reverse.
468    /// A file declaring only the other one's floor is refused, as
469    /// llama.cpp refuses (`mistral3`) or aborts on (`deepseek2`) it.
470    #[test]
471    fn each_reader_takes_its_own_floor_and_refuses_the_other_ones() {
472        let only_length = DeclaredTemperature {
473            scale: Some(0.5),
474            length: Some(64),
475            n_ctx_orig_yarn: None,
476        };
477        assert_eq!(
478            resolve_attn_temperature("mistral3", only_length),
479            Err(AttnTemperatureError::ZeroFloor(FloorSource::OrigCtxYarn))
480        );
481        let only_ctx = DeclaredTemperature {
482            scale: Some(0.5),
483            length: None,
484            n_ctx_orig_yarn: Some(4096),
485        };
486        assert_eq!(
487            resolve_attn_temperature("deepseek2", only_ctx),
488            Err(AttnTemperatureError::ZeroFloor(
489                FloorSource::TemperatureLengthKey
490            ))
491        );
492        // And an explicit zero floor is the same refusal, not a
493        // division by zero.
494        let zero_len = DeclaredTemperature {
495            length: Some(0),
496            ..only_length
497        };
498        assert_eq!(
499            resolve_attn_temperature("deepseek2", zero_len),
500            Err(AttnTemperatureError::ZeroFloor(
501                FloorSource::TemperatureLengthKey
502            ))
503        );
504        // Each message names the key the user has to look at.
505        let msg = AttnTemperatureError::ZeroFloor(FloorSource::OrigCtxYarn).message("mistral3");
506        assert!(
507            msg.contains("mistral3.rope.scaling.original_context_length"),
508            "{msg}"
509        );
510        assert!(msg.contains("mistral3.cpp:16-17"), "{msg}");
511        let msg =
512            AttnTemperatureError::ZeroFloor(FloorSource::TemperatureLengthKey).message("deepseek2");
513        assert!(
514            msg.contains("deepseek2.attention.temperature_length"),
515            "{msg}"
516        );
517    }
518}