Skip to main content

bge_m3_embedding_server/embedder/
math.rs

1// Copyright (c) 2026 J. Patrick Fulton
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Pure dense/sparse math helpers (testable without ORT).
16
17use std::collections::HashMap;
18
19use ndarray::ArrayView1;
20
21/// CLS, PAD, SEP/EOS, UNK — excluded from sparse output.
22pub(super) const SPECIAL_TOKENS: [u32; 4] = [0, 1, 2, 3];
23
24/// L2-normalizes `vec` in place. If the norm is zero, leaves the vector unchanged.
25pub(super) fn normalize_l2(vec: &mut [f32]) {
26    let norm: f32 = vec.iter().map(|x| x * x).sum::<f32>().sqrt();
27    if norm > 0.0 {
28        for x in vec.iter_mut() {
29            *x /= norm;
30        }
31    }
32}
33
34/// Projects a single token's hidden state through the sparse-linear layer.
35///
36/// Returns `max(0, dot(hidden, weight) + bias)` (ReLU-gated score).
37pub(super) fn sparse_project(hidden: &[f32], weight: &ArrayView1<f32>, bias: f32) -> f32 {
38    let hidden_view = ArrayView1::from(hidden);
39    (hidden_view.dot(weight) + bias).max(0.0)
40}
41
42/// Max-pools sparse scores by vocabulary token ID, excluding special tokens
43/// and tokens masked by the attention mask.
44///
45/// Returns sorted `(indices, values)` vectors suitable for `SparseEmbedding`.
46pub(super) fn sparse_maxpool(ids: &[u32], mask: &[u32], scores: &[f32]) -> (Vec<usize>, Vec<f32>) {
47    let mut token_weights: HashMap<usize, f32> = HashMap::new();
48
49    for (j, &token_id) in ids.iter().enumerate() {
50        if mask[j] == 0 {
51            continue;
52        }
53        if SPECIAL_TOKENS.contains(&token_id) {
54            continue;
55        }
56        let score = scores[j];
57        if score > 0.0 {
58            token_weights
59                .entry(token_id as usize)
60                .and_modify(|w| *w = w.max(score))
61                .or_insert(score);
62        }
63    }
64
65    let mut indices: Vec<usize> = token_weights.keys().copied().collect();
66    indices.sort_unstable();
67    let values: Vec<f32> = indices.iter().map(|k| token_weights[k]).collect();
68    (indices, values)
69}
70
71/// Computes the median of a `Vec<usize>` in-place (sorts the slice).
72///
73/// Returns `0` for empty input. For even-length inputs returns the lower
74/// of the two middle elements (no floating-point required).
75pub(super) fn median_usize(values: &mut [usize]) -> usize {
76    if values.is_empty() {
77        return 0;
78    }
79    values.sort_unstable();
80    values[values.len() / 2]
81}
82
83/// Per-batch token-length distribution statistics.
84///
85/// All lengths are measured in tokens (post-tokenization id counts), before
86/// any padding. Carried in [`super::types::EmbedStats`] and logged on every
87/// completed embed request so operators can correlate latency spikes with new
88/// `(batch_size, seq_len)` shapes hitting TRT engine compile paths.
89#[derive(Debug, Clone, Copy, Default)]
90pub(super) struct SeqLenDistribution {
91    /// Minimum token sequence length in the batch.
92    pub min: usize,
93    /// Maximum token sequence length in the batch.
94    pub max: usize,
95    /// Mean token sequence length (integer, truncated toward zero).
96    pub mean: usize,
97    /// 95th-percentile token sequence length.
98    ///
99    /// Index is `(n * 95) / 100` on a sorted copy — a conservative floor
100    /// that never exceeds `n - 1`.  For a batch of 64 texts this maps to
101    /// index 60, meaning the 61st-shortest sequence.
102    pub p95: usize,
103}
104
105/// Computes min, max, mean, and p95 token-length statistics for an embed batch.
106///
107/// Returns a zeroed [`SeqLenDistribution`] for an empty slice.  Allocates a
108/// temporary sorted copy of `lens`; batch sizes are ≤ 256 so this is cheap.
109pub(super) fn seq_len_distribution(lens: &[usize]) -> SeqLenDistribution {
110    if lens.is_empty() {
111        return SeqLenDistribution::default();
112    }
113    let min = *lens.iter().min().expect("non-empty");
114    let max = *lens.iter().max().expect("non-empty");
115    let mean = lens.iter().sum::<usize>() / lens.len();
116    let mut sorted = lens.to_vec();
117    sorted.sort_unstable();
118    let p95_idx = (sorted.len() * 95) / 100;
119    let p95 = sorted[p95_idx.min(sorted.len() - 1)];
120    SeqLenDistribution {
121        min,
122        max,
123        mean,
124        p95,
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::seq_len_distribution;
131
132    #[test]
133    fn single_element() {
134        let d = seq_len_distribution(&[42]);
135        assert_eq!(d.min, 42);
136        assert_eq!(d.max, 42);
137        assert_eq!(d.mean, 42);
138        assert_eq!(d.p95, 42);
139    }
140
141    #[test]
142    fn empty_returns_zeros() {
143        let d = seq_len_distribution(&[]);
144        assert_eq!(d.min, 0);
145        assert_eq!(d.max, 0);
146        assert_eq!(d.mean, 0);
147        assert_eq!(d.p95, 0);
148    }
149
150    #[test]
151    fn uniform_batch() {
152        let lens: Vec<usize> = vec![100; 64];
153        let d = seq_len_distribution(&lens);
154        assert_eq!(d.min, 100);
155        assert_eq!(d.max, 100);
156        assert_eq!(d.mean, 100);
157        assert_eq!(d.p95, 100);
158    }
159
160    #[test]
161    fn ascending_sequence_p95() {
162        // lens = [1, 2, ..., 100]. Sorted same order.
163        // p95_idx = (100 * 95) / 100 = 95 → sorted[95] = 96.
164        let lens: Vec<usize> = (1..=100).collect();
165        let d = seq_len_distribution(&lens);
166        assert_eq!(d.min, 1);
167        assert_eq!(d.max, 100);
168        assert_eq!(d.mean, 50); // sum=5050, /100 = 50 (integer)
169        assert_eq!(d.p95, 96);
170    }
171
172    #[test]
173    fn two_elements() {
174        // p95_idx = (2 * 95) / 100 = 1, so sorted[1] = max.
175        let d = seq_len_distribution(&[10, 200]);
176        assert_eq!(d.min, 10);
177        assert_eq!(d.max, 200);
178        assert_eq!(d.mean, 105);
179        assert_eq!(d.p95, 200);
180    }
181
182    #[test]
183    fn p95_does_not_panic_on_small_batches() {
184        // For n in 1..20 verify p95_idx stays within bounds.
185        for n in 1usize..=20 {
186            let lens: Vec<usize> = (1..=n).collect();
187            let d = seq_len_distribution(&lens);
188            // p95 must be between min and max inclusive.
189            assert!(d.p95 >= d.min, "p95 < min for n={n}");
190            assert!(d.p95 <= d.max, "p95 > max for n={n}");
191        }
192    }
193
194    #[test]
195    fn mean_truncated() {
196        // sum=5, len=2 → 5/2=2 (integer truncation).
197        let d = seq_len_distribution(&[2, 3]);
198        assert_eq!(d.mean, 2);
199    }
200}