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}