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