1use candle_core::{DType, Device, Tensor};
14use candle_nn::VarBuilder;
15use candle_transformers::models::bert::{BertModel, Config};
16use serde::{Deserialize, Serialize};
17use std::{
18 collections::HashMap,
19 sync::{Arc, OnceLock},
20};
21use tokenizers::{Tokenizer, TruncationParams};
22
23#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
26pub struct ModelInfo {
27 pub id: String,
29 pub revision: String,
31 pub upstream_model: String,
33 pub upstream_revision: String,
35 pub tokenizer_sha256: String,
37 pub dimensions: usize,
39 pub max_tokens: usize,
41 pub pooling: String,
43}
44
45pub fn model_info() -> ModelInfo {
47 let value: serde_json::Value = serde_json::from_str(include_str!("../assets/provenance.json"))
48 .expect("bundled provenance");
49 ModelInfo {
50 id: "pocket-encoder/minilm-l3-q4-g32-v1".into(),
51 revision: format!("sha256:{}", value["bundled_sha256"].as_str().unwrap()),
52 upstream_model: value["upstream_model"].as_str().unwrap().into(),
53 upstream_revision: value["upstream_revision"].as_str().unwrap().into(),
54 tokenizer_sha256: value["tokenizer_sha256"].as_str().unwrap().into(),
55 dimensions: 384,
56 max_tokens: 128,
57 pooling: "attention-masked mean; L2 normalized; no query prefix; right truncation".into(),
58 }
59}
60
61#[derive(Debug, thiserror::Error)]
63pub enum Error {
64 #[error("{0}")]
66 Input(&'static str),
67 #[error("encoder failed: {0}")]
69 Inference(String),
70}
71
72struct Inner {
73 model: BertModel,
74 tokenizer: Tokenizer,
75}
76
77#[derive(Clone)]
79pub struct Encoder {
80 inner: Arc<Inner>,
81}
82
83impl Encoder {
84 pub fn new() -> Result<Self, Error> {
86 static MODEL: OnceLock<Result<Arc<Inner>, String>> = OnceLock::new();
87 MODEL
88 .get_or_init(|| load_bundled().map(Arc::new).map_err(|e| e.to_string()))
89 .clone()
90 .map(|inner| Self { inner })
91 .map_err(Error::Inference)
92 }
93
94 pub fn embed(&self, texts: &[impl AsRef<str>]) -> Result<Vec<Vec<f32>>, Error> {
99 if texts.len() > 4096 {
100 return Err(Error::Input("at most 4096 texts per call"));
101 }
102 let mut bytes = 0usize;
103 for text in texts {
104 let text = text.as_ref();
105 if text.trim().is_empty() {
106 return Err(Error::Input("text must not be empty"));
107 }
108 if text.len() > 65536 {
109 return Err(Error::Input("text exceeds 64 KiB"));
110 }
111 bytes = bytes.saturating_add(text.len());
112 }
113 if bytes > 4 * 1024 * 1024 {
114 return Err(Error::Input("texts exceed 4 MiB"));
115 }
116 texts
117 .iter()
118 .map(|text| {
119 self.one(text.as_ref())
120 .map_err(|e| Error::Inference(e.to_string()))
121 })
122 .collect()
123 }
124
125 fn one(&self, text: &str) -> candle_core::Result<Vec<f32>> {
126 let encoded = self
127 .inner
128 .tokenizer
129 .encode(text, true)
130 .map_err(|e| candle_core::Error::Msg(e.to_string()))?;
131 let n = encoded.len();
132 let ids = Tensor::new(encoded.get_ids(), &Device::Cpu)?.unsqueeze(0)?;
133 let types = Tensor::new(encoded.get_type_ids(), &Device::Cpu)?.unsqueeze(0)?;
134 let mask = Tensor::new(encoded.get_attention_mask(), &Device::Cpu)?.unsqueeze(0)?;
135 let hidden = self.inner.model.forward(&ids, &types, Some(&mask))?;
136 let weights = mask.to_dtype(DType::F32)?.unsqueeze(2)?;
137 let pooled = hidden
138 .broadcast_mul(&weights)?
139 .sum(1)?
140 .squeeze(0)?
141 .to_vec1::<f32>()?;
142 let norm = pooled
143 .iter()
144 .map(|v| (*v as f64).powi(2))
145 .sum::<f64>()
146 .sqrt();
147 if n == 0 || !norm.is_finite() || norm <= 0.0 {
148 candle_core::bail!("non-finite or zero embedding");
149 }
150 Ok(pooled
151 .into_iter()
152 .map(|v| (v as f64 / norm) as f32)
153 .collect())
154 }
155}
156
157fn tokenizer() -> candle_core::Result<Tokenizer> {
158 let mut tokenizer = Tokenizer::from_bytes(include_bytes!("../assets/tokenizer.json"))
159 .map_err(|e| candle_core::Error::Msg(e.to_string()))?;
160 tokenizer.with_padding(None);
161 tokenizer
162 .with_truncation(Some(TruncationParams {
163 max_length: 128,
164 ..Default::default()
165 }))
166 .map_err(|e| candle_core::Error::Msg(e.to_string()))?;
167 Ok(tokenizer)
168}
169
170fn load_bundled() -> candle_core::Result<Inner> {
171 let file = safetensors::SafeTensors::deserialize(include_bytes!("../assets/model.safetensors"))
172 .map_err(|e| candle_core::Error::Msg(e.to_string()))?;
173 let shapes: HashMap<String, Vec<usize>> =
174 serde_json::from_str(include_str!("../assets/shapes.json"))
175 .map_err(|e| candle_core::Error::Msg(e.to_string()))?;
176 let floats = |bytes: &[u8]| {
177 bytes
178 .as_chunks::<4>()
179 .0
180 .iter()
181 .map(|b| f32::from_le_bytes(*b))
182 .collect::<Vec<_>>()
183 };
184 let mut tensors = HashMap::new();
185 for (name, shape) in shapes {
186 let data = if shape.len() < 2 {
187 floats(
188 file.tensor(&name)
189 .map_err(|e| candle_core::Error::Msg(e.to_string()))?
190 .data(),
191 )
192 } else {
193 let packed = file
194 .tensor(&format!("{name}.q4"))
195 .map_err(|e| candle_core::Error::Msg(e.to_string()))?;
196 let scales = file
197 .tensor(&format!("{name}.scales"))
198 .map_err(|e| candle_core::Error::Msg(e.to_string()))?;
199 let scales = scales
200 .data()
201 .as_chunks::<2>()
202 .0
203 .iter()
204 .map(|b| half::f16::from_bits(u16::from_le_bytes(*b)).to_f32())
205 .collect::<Vec<_>>();
206 (0..shape.iter().product())
207 .map(|i| {
208 let nibble = (packed.data()[i / 2] >> ((i % 2) * 4)) & 15;
209 (nibble as i16 - 8) as f32 * scales[i / 32]
210 })
211 .collect()
212 };
213 tensors.insert(name, Tensor::from_vec(data, shape, &Device::Cpu)?);
214 }
215 let config: Config = serde_json::from_str(include_str!("../assets/config.json"))
216 .map_err(|e| candle_core::Error::Msg(e.to_string()))?;
217 let model = BertModel::load(
218 VarBuilder::from_tensors(tensors, DType::F32, &Device::Cpu),
219 &config,
220 )?;
221 Ok(Inner {
222 model,
223 tokenizer: tokenizer()?,
224 })
225}