use crate::embedding::colbert::{
DocIdAlignment, TokenEmbeddings, build_doc_token_ids, load_doc_id_alignment,
};
use crate::embedding::mixer::{MIXER_CENTER, MIXER_WINDOW, MicroMixer};
use crate::embedding::model_manager;
use crate::embedding::shortlists::CentroidShortlists;
use crate::embedding::static_table::StaticTokenTable;
use crate::embedding::static_token::window_ids_at;
use anyhow::{Context, Result};
use ndarray::Array2;
use rayon::prelude::*;
use std::path::Path;
pub struct CinderEncoder {
table: StaticTokenTable,
mixer: MicroMixer,
shortlists: CentroidShortlists,
alignment: DocIdAlignment,
}
impl CinderEncoder {
pub fn new(model_dir: &Path) -> Result<Self> {
let table_path = model_manager::static_token_table_path(model_dir);
let table = StaticTokenTable::load(&table_path).with_context(|| {
format!(
"failed to load Cinder static token table {}",
table_path.display()
)
})?;
let mixer_path = model_manager::cinder_mixer_path(model_dir);
let mixer = MicroMixer::load(&mixer_path)
.with_context(|| format!("failed to load Cinder mixer {}", mixer_path.display()))?;
anyhow::ensure!(
mixer.dims == table.dims,
"Cinder mixer dims {} != static token table dims {}",
mixer.dims,
table.dims
);
let shortlists_path = model_manager::cinder_shortlists_path(model_dir);
let shortlists = CentroidShortlists::load(&shortlists_path).with_context(|| {
format!(
"failed to load Cinder centroid shortlists {}",
shortlists_path.display()
)
})?;
let alignment = load_doc_id_alignment(model_dir)?;
Ok(Self {
table,
mixer,
shortlists,
alignment,
})
}
#[must_use]
pub fn shortlists(&self) -> &CentroidShortlists {
&self.shortlists
}
pub fn encode_documents(&self, texts: &[String]) -> Result<Vec<TokenEmbeddings>> {
texts
.par_iter()
.map(|text| {
let ids = build_doc_token_ids(&self.alignment, text)?;
Ok(self.encode_ids(&ids))
})
.collect()
}
pub fn encode_documents_with_window_ids(
&self,
texts: &[String],
) -> Result<Vec<(TokenEmbeddings, Vec<Vec<u32>>)>> {
texts
.par_iter()
.map(|text| {
let ids = build_doc_token_ids(&self.alignment, text)?;
Ok(self.encode_ids_with_window_ids(&ids))
})
.collect()
}
fn encode_ids(&self, ids: &[u32]) -> TokenEmbeddings {
let dims = self.table.dims;
let mut out = Array2::<f32>::zeros((ids.len(), dims));
let mut e = vec![0.0f32; dims];
for i in 0..ids.len() {
self.mix_at(ids, i, &mut e);
out.row_mut(i)
.assign(&ndarray::ArrayView1::from(e.as_slice()));
}
out
}
fn encode_ids_with_window_ids(&self, ids: &[u32]) -> (TokenEmbeddings, Vec<Vec<u32>>) {
let dims = self.table.dims;
let mut out = Array2::<f32>::zeros((ids.len(), dims));
let mut windows: Vec<Vec<u32>> = Vec::with_capacity(ids.len());
let mut e = vec![0.0f32; dims];
for i in 0..ids.len() {
let window_ids = self.mix_at(ids, i, &mut e);
out.row_mut(i)
.assign(&ndarray::ArrayView1::from(e.as_slice()));
windows.push(window_ids.to_vec());
}
(out, windows)
}
fn mix_at(&self, ids: &[u32], i: usize, e: &mut [f32]) -> [u32; MIXER_WINDOW] {
let dims = self.table.dims;
let window_ids = window_ids_at::<MIXER_WINDOW>(ids, i);
let window_rows: Vec<Vec<f32>> = window_ids
.iter()
.map(|&id| {
self.table
.lookup(id)
.map_or_else(|| vec![0.0f32; dims], <[f32]>::to_vec)
})
.collect();
let window_refs: [&[f32]; MIXER_WINDOW] =
std::array::from_fn(|k| window_rows[k].as_slice());
let center_row = window_rows[MIXER_CENTER].as_slice();
self.mixer.forward(&window_refs, center_row, e);
window_ids
}
pub fn agreement_samples(
&self,
texts: impl IntoIterator<Item = String>,
max_samples: usize,
seed: u64,
) -> Result<Vec<(Vec<f32>, Vec<u32>)>> {
if max_samples == 0 {
return Ok(Vec::new());
}
let dims = self.table.dims;
let mut reservoir: Vec<(Vec<f32>, Vec<u32>)> = Vec::new();
let mut n_seen: u64 = 0;
let mut rng = SplitMix64::new(seed);
let mut e = vec![0.0f32; dims];
for text in texts {
let ids = build_doc_token_ids(&self.alignment, &text)?;
for i in 0..ids.len() {
let window_ids = self.mix_at(&ids, i, &mut e);
if reservoir.len() < max_samples {
reservoir.push((e.clone(), window_ids.to_vec()));
} else {
let j = rng.next_below(n_seen + 1) as usize;
if j < max_samples {
reservoir[j] = (e.clone(), window_ids.to_vec());
}
}
n_seen += 1;
}
}
Ok(reservoir)
}
}
struct SplitMix64(u64);
impl SplitMix64 {
fn new(seed: u64) -> Self {
Self(seed)
}
fn next_u64(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
fn next_below(&mut self, bound: u64) -> u64 {
self.next_u64() % bound.max(1)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::embedding::centroid_train::save_centroids_npy;
use crate::embedding::colbert::ColbertEmbedder;
use crate::embedding::static_token::StaticTokenEmbedder;
use ndarray::Array2;
fn test_tokenizer_dir() -> Option<std::path::PathBuf> {
let dir = crate::config::SemantexConfig::default()
.models_dir()
.join("LateOn-Code-edge");
(dir.join("tokenizer.json").exists() && dir.join("onnx_config.json").exists())
.then_some(dir)
}
fn expect_err(result: Result<CinderEncoder>) -> anyhow::Error {
match result {
Ok(_) => panic!("expected an error, got Ok"),
Err(e) => e,
}
}
#[test]
fn new_errors_when_table_missing() {
let tmp = tempfile::TempDir::new().unwrap();
let err = expect_err(CinderEncoder::new(tmp.path()));
assert!(
err.to_string().contains("static_token_table.bin"),
"expected a static-token-table error, got: {err}"
);
}
#[test]
fn new_errors_naming_the_missing_mixer() {
let tmp = tempfile::TempDir::new().unwrap();
let table = StaticTokenTable::new(4, 2, [0.0, 0.0, 1.0, 0.0, 0.0]);
table
.save(&model_manager::static_token_table_path(tmp.path()))
.unwrap();
let err = expect_err(CinderEncoder::new(tmp.path()));
assert!(
err.to_string().contains("cinder_mixer.bin"),
"expected a mixer-loading error naming cinder_mixer.bin, got: {err}"
);
}
#[test]
fn new_errors_naming_the_missing_shortlists() {
let tmp = tempfile::TempDir::new().unwrap();
let table = StaticTokenTable::new(4, 2, [0.0, 0.0, 1.0, 0.0, 0.0]);
table
.save(&model_manager::static_token_table_path(tmp.path()))
.unwrap();
MicroMixer::zeros(2)
.save(&model_manager::cinder_mixer_path(tmp.path()))
.unwrap();
let err = expect_err(CinderEncoder::new(tmp.path()));
assert!(
err.to_string().contains("cinder_shortlists.bin"),
"expected a shortlists-loading error naming cinder_shortlists.bin, got: {err}"
);
}
#[test]
fn zero_mixer_equals_static_center_lookup_end_to_end() {
let Some(model_dir) = test_tokenizer_dir() else {
return;
};
let vocab_size = ColbertEmbedder::new(&model_dir)
.unwrap()
.tokenizer_vocab_size()
.unwrap();
let dims = 4usize;
let mut table = StaticTokenTable::new(vocab_size, dims, [0.0, 0.0, 1.0, 0.0, 0.0]);
for id in 0..vocab_size as u32 {
let f = id as f32;
table.set_row(
id,
&[(f % 97.0) - 48.0, (f % 13.0) - 6.0, (f % 5.0) - 2.0, 1.0],
);
}
let tmp = tempfile::TempDir::new().unwrap();
table
.save(&model_manager::static_token_table_path(tmp.path()))
.unwrap();
MicroMixer::zeros(dims)
.save(&model_manager::cinder_mixer_path(tmp.path()))
.unwrap();
let centroids =
Array2::<f32>::from_shape_fn((6, dims), |(i, j)| ((i * 7 + j) as f32 * 0.31).sin());
save_centroids_npy(
&model_manager::frozen_centroids_path(tmp.path()),
¢roids,
)
.unwrap();
CentroidShortlists::derive(&table, ¢roids.view(), 3)
.unwrap()
.save(&model_manager::cinder_shortlists_path(tmp.path()))
.unwrap();
for f in ["tokenizer.json", "onnx_config.json"] {
std::fs::copy(model_dir.join(f), tmp.path().join(f)).unwrap();
}
let cinder = CinderEncoder::new(tmp.path()).expect("all four artifacts valid");
assert_eq!(cinder.shortlists().m, 3);
let static_emb = StaticTokenEmbedder::new(tmp.path()).unwrap();
let texts = vec!["fn main() { let value = compute_sum(1, 2); }".to_string()];
let cinder_out = cinder.encode_documents(&texts).unwrap();
let static_out = static_emb.encode_documents(&texts).unwrap();
assert_eq!(cinder_out.len(), 1);
assert_eq!(cinder_out[0].shape(), static_out[0].shape());
assert!(cinder_out[0].nrows() > 0, "doc should produce token rows");
assert_eq!(cinder_out[0].ncols(), dims);
for (i, (cr, sr)) in cinder_out[0]
.rows()
.into_iter()
.zip(static_out[0].rows())
.enumerate()
{
let norm: f32 = cr.iter().map(|v| v * v).sum::<f32>().sqrt();
assert!(
(norm - 1.0).abs() < 1e-5,
"cinder row {i} norm {norm} not ~1.0"
);
for (a, b) in cr.iter().zip(sr.iter()) {
assert!(
(a - b).abs() < 1e-5,
"row {i}: zero-mixer Cinder {a} != static center lookup {b}"
);
}
}
}
fn read_all_codes(dir: &std::path::Path) -> Vec<i64> {
use ndarray_npy::ReadNpyExt;
let mut all = Vec::new();
let mut i = 0usize;
loop {
let p = dir.join(format!("{i}.codes.npy"));
if !p.exists() {
break;
}
let arr = ndarray::Array1::<i64>::read_npy(std::fs::File::open(&p).unwrap()).unwrap();
all.extend(arr.iter().copied());
i += 1;
}
all
}
#[test]
fn add_document_with_ids_writes_real_shortlist_codes() {
use crate::embedding::shortlists::{ArgmaxScratch, CentroidShortlists, shortlist_argmax};
use crate::embedding::static_table::StaticTokenTable;
use ndarray::{Array1, Array2};
use next_plaid::{CompiledIndexWriter, IdAwareCodeAssigner, IndexConfig};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
let dim = 8usize;
let n_centroids = 24usize;
let vocab = 40usize;
let mut centroids = Array2::<f32>::from_shape_fn((n_centroids, dim), |(i, j)| {
((i * 13 + j * 5) as f32 * 0.31).cos()
});
for mut r in centroids.rows_mut() {
let n = r.dot(&r).sqrt();
if n > 0.0 {
r.mapv_inplace(|v| v / n);
}
}
let mut table = StaticTokenTable::new(vocab, dim, [0.2; 5]);
for id in 0..vocab as u32 {
let row: Vec<f32> = (0..dim)
.map(|j| ((id as usize * 7 + j * 3) as f32 * 0.17).sin())
.collect();
table.set_row(id, &row);
}
let shortlists = CentroidShortlists::derive(&table, ¢roids.view(), 6).unwrap();
let make_doc = |seed: usize, n_tokens: usize| -> (Array2<f32>, Vec<Vec<u32>>) {
let mut e = Array2::<f32>::from_shape_fn((n_tokens, dim), |(i, j)| {
((seed * 31 + i * 7 + j) as f32 * 0.37).sin()
});
for mut r in e.rows_mut() {
let n = r.dot(&r).sqrt();
if n > 0.0 {
r.mapv_inplace(|v| v / n);
}
}
let windows: Vec<Vec<u32>> = (0..n_tokens)
.map(|i| {
let len = 1 + i % 3;
(0..len)
.map(|k| ((seed + i * 5 + k * 11) % vocab) as u32)
.collect()
})
.collect();
(e, windows)
};
let docs: Vec<(Array2<f32>, Vec<Vec<u32>>)> =
(0..37).map(|s| make_doc(s, 2 + s % 5)).collect();
let calls = Arc::new(AtomicUsize::new(0));
let calls_cl = Arc::clone(&calls);
let a_shortlists = shortlists.clone();
let a_centroids = centroids.clone();
let assigner: IdAwareCodeAssigner =
Box::new(move |batch: &Array2<f32>, windows: &[Vec<u32>]| {
calls_cl.fetch_add(1, Ordering::SeqCst);
let cview = a_centroids.view();
let mut scratch = ArgmaxScratch::new();
let mut out = Vec::with_capacity(batch.nrows());
for (r, row) in batch.rows().into_iter().enumerate() {
let e = row
.as_slice()
.expect("standard-layout batch rows are contiguous");
out.push(shortlist_argmax(
e,
&windows[r],
&a_shortlists,
&cview,
&mut scratch,
));
}
Array1::from_vec(out)
});
let config = IndexConfig {
nbits: 4,
batch_size: 16,
force_cpu: true,
..Default::default()
};
let tmp = tempfile::TempDir::new().unwrap();
let out_dir = tmp.path().join("idx");
let sample: Vec<Array2<f32>> = docs.iter().map(|(e, _)| e.clone()).collect();
let mut w = CompiledIndexWriter::new(
out_dir.to_str().unwrap(),
centroids.clone(),
&config,
&sample,
)
.unwrap()
.with_id_aware_assigner(assigner);
for (e, win) in &docs {
w.add_document_with_ids(e, win).unwrap();
}
w.finalize().unwrap();
assert!(
calls.load(Ordering::SeqCst) > 0,
"the id-aware shortlist assigner was never invoked"
);
let cview = centroids.view();
let mut scratch = ArgmaxScratch::new();
let mut expected: Vec<i64> = Vec::new();
for (e, win) in &docs {
for (r, row) in e.rows().into_iter().enumerate() {
let es = row.as_slice().unwrap();
expected
.push(shortlist_argmax(es, &win[r], &shortlists, &cview, &mut scratch) as i64);
}
}
let got = read_all_codes(&out_dir);
assert!(!expected.is_empty(), "no codes written");
assert_eq!(got.len(), expected.len(), "token counts differ");
assert_eq!(
got, expected,
"on-disk codes must EXACTLY equal direct shortlist_argmax output — \
the real shortlist-union function must be what the writer persisted"
);
}
}