metrale_model_engine/traits/
logprobs.rs1#[derive(Clone, Debug)]
12pub struct PromptTokenLogprob {
13 pub token_id: u32,
15 pub logprob: f32,
16 pub top: Vec<(u32, f32)>,
18}
19
20pub fn logprob_of(f32_logits: &[f32], target: u32, k: usize) -> (f32, Vec<(u32, f32)>) {
24 if f32_logits.is_empty() {
25 return (f32::NEG_INFINITY, Vec::new());
26 }
27 let max_logit = f32_logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
28 let log_sum_exp = max_logit
29 + f32_logits
30 .iter()
31 .map(|&l| (l - max_logit).exp())
32 .sum::<f32>()
33 .ln();
34 let target_logprob = if (target as usize) < f32_logits.len() {
35 f32_logits[target as usize] - log_sum_exp
36 } else {
37 f32::NEG_INFINITY
38 };
39 if k == 0 {
40 return (target_logprob, Vec::new());
41 }
42 let mut indexed: Vec<(u32, f32)> = f32_logits
43 .iter()
44 .enumerate()
45 .map(|(j, &l)| (j as u32, l - log_sum_exp))
46 .collect();
47 let nth = k.min(indexed.len().saturating_sub(1));
48 indexed.select_nth_unstable_by(nth, |a, b| {
49 b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)
50 });
51 let mut top: Vec<(u32, f32)> = indexed[..k.min(indexed.len())].to_vec();
52 top.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
53 (target_logprob, top)
54}
55
56#[inline]
58pub fn bf16_to_f32(lo: u8, hi: u8) -> f32 {
59 f32::from_bits(((lo as u32) | ((hi as u32) << 8)) << 16)
60}
61
62pub fn extract_bf16(bf16: &[u8], target: u32, k: usize, vocab: usize) -> PromptTokenLogprob {
64 let f32_logits: Vec<f32> = (0..vocab)
65 .map(|j| bf16_to_f32(bf16[j * 2], bf16[j * 2 + 1]))
66 .collect();
67 let (logprob, top) = logprob_of(&f32_logits, target, k);
68 PromptTokenLogprob {
69 token_id: target,
70 logprob,
71 top,
72 }
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78
79 #[test]
80 fn logprob_matches_manual_log_softmax() {
81 let logits = [1.0f32, 2.0, 3.0];
82 let sum: f32 = logits.iter().map(|l| l.exp()).sum();
83 let expect = 2.0 - sum.ln();
84 let (lp, top) = logprob_of(&logits, 1, 2);
85 assert!((lp - expect).abs() < 1e-6, "{lp} vs {expect}");
86 assert_eq!(top[0].0, 2);
87 assert_eq!(top[1].0, 1);
88 assert!(top[0].1 > top[1].1);
89 }
90
91 #[test]
92 fn k_zero_returns_empty_top() {
93 let (lp, top) = logprob_of(&[0.0, 1.0], 0, 0);
94 assert!(top.is_empty());
95 assert!(lp < 0.0);
96 }
97
98 #[test]
99 fn out_of_vocab_target_is_neg_inf_not_panic() {
100 let (lp, _) = logprob_of(&[0.0, 1.0], 99, 1);
101 assert_eq!(lp, f32::NEG_INFINITY);
102 }
103
104 #[test]
105 fn empty_vocab_is_neg_inf_with_no_alternatives() {
106 assert_eq!(logprob_of(&[], 0, 4), (f32::NEG_INFINITY, vec![]));
107 }
108
109 #[test]
110 fn bf16_slice_roundtrip_extract() {
111 let vals = [0.0f32, 1.0, 2.0];
112 let mut bytes = Vec::new();
113 for v in vals {
114 let b = (v.to_bits() >> 16) as u16;
115 bytes.push((b & 0xFF) as u8);
116 bytes.push((b >> 8) as u8);
117 }
118 let r = extract_bf16(&bytes, 2, 1, 3);
119 let sum: f32 = vals.iter().map(|l| l.exp()).sum();
120 let expect = 2.0 - sum.ln();
121 assert_eq!(r.token_id, 2);
122 assert!((r.logprob - expect).abs() < 1e-3);
123 assert_eq!(r.top[0].0, 2);
124 assert!((r.top[0].1 - expect).abs() < 1e-3);
125 }
126}