1use anyhow::{Context, Result, anyhow};
2use half::{f16, slice::HalfFloatSliceExt};
3#[cfg(all(feature = "hf-hub", not(feature = "local-only")))]
4use hf_hub::api::sync::{Api, ApiBuilder, ApiRepo};
5use safetensors::{SafeTensors, tensor::Dtype};
6use serde_json::Value;
7use std::borrow::Cow;
8use std::cell::RefCell;
9use std::{
10 fs,
11 path::{Path, PathBuf},
12 sync::Arc,
13};
14use tokenizers::{
15 Tokenizer,
16 models::ModelWrapper,
17 parallelism::{MaybeParallelRefIterator, MaybeParallelRefMutIterator},
18};
19
20use crate::fast_unigram::FastUnigram;
21use crate::fast_wordpiece::FastWordPiece;
22
23thread_local! {
24 static TOKEN_IDS: RefCell<Vec<u32>> = const { RefCell::new(Vec::new()) };
25}
26
27#[derive(Debug, Clone)]
31pub struct StaticModel {
32 inner: Arc<StaticModelInner>,
33}
34
35#[derive(Debug)]
36struct StaticModelInner {
37 tokenizer: LoadedTokenizer,
38 embeddings: Embeddings,
39 dim: usize,
40 row_scales: Option<Vec<f32>>,
42 weights: Option<Cow<'static, [f32]>>,
43 token_mapping: Option<Cow<'static, [usize]>>,
44 normalize: bool,
45}
46
47#[derive(Debug, Clone)]
48#[allow(clippy::large_enum_variant)]
49enum Tokenization {
50 WordPiece(FastWordPiece),
52 Unigram(Tokenizer, FastUnigram),
54 Crate(Tokenizer),
56}
57
58#[derive(Debug, Clone)]
59struct LoadedTokenizer {
60 tokenization: Tokenization,
61 median_token_length: usize,
62 unk_token_id: Option<usize>,
63}
64
65impl LoadedTokenizer {
66 fn from_tokenizer(mut tokenizer: Tokenizer) -> Result<Self> {
67 tokenizer
68 .with_truncation(None)
69 .map_err(|e| anyhow!("failed to disable truncation: {e}"))?;
70 tokenizer.with_padding(None);
71 let (median_token_length, unk_token_id) = compute_metadata(&tokenizer)?;
72 let tokenization = if let Some(ft) = FastWordPiece::from_tokenizer(&tokenizer) {
73 Tokenization::WordPiece(ft)
74 } else if let Some(fu) = FastUnigram::from_tokenizer(&tokenizer, unk_token_id) {
75 Tokenization::Unigram(tokenizer, fu)
76 } else {
77 Tokenization::Crate(tokenizer)
78 };
79 Ok(Self {
80 tokenization,
81 median_token_length,
82 unk_token_id,
83 })
84 }
85
86 fn from_bytes(bytes: &[u8]) -> Result<Self> {
89 if let Some((ft, median_token_length)) = FastWordPiece::from_json(bytes) {
90 return Ok(Self {
91 unk_token_id: ft.unk_id().map(|id| id as usize),
92 tokenization: Tokenization::WordPiece(ft),
93 median_token_length,
94 });
95 }
96 let tokenizer = Tokenizer::from_bytes(bytes).map_err(|e| anyhow!("failed to load tokenizer: {e}"))?;
97 Self::from_tokenizer(tokenizer)
98 }
99}
100
101pub(crate) fn median_token_length<'a>(tokens: impl Iterator<Item = &'a str>) -> usize {
103 let mut lens: Vec<usize> = tokens.map(str::len).collect();
104 lens.sort_unstable();
105 lens.get(lens.len() / 2).copied().unwrap_or(1)
106}
107
108fn compute_metadata(tokenizer: &Tokenizer) -> Result<(usize, Option<usize>)> {
110 let median_token_length = median_token_length(tokenizer.get_vocab(false).keys().map(String::as_str));
111
112 let lookup = |tok: &str| {
113 tokenizer
114 .token_to_id(tok)
115 .map(|id| id as usize)
116 .ok_or_else(|| anyhow!("unk_token '{tok}' not found in vocabulary"))
117 };
118 let unk_token_id = match tokenizer.get_model() {
119 ModelWrapper::BPE(model) => model.unk_token.as_deref().map(lookup).transpose()?,
120 ModelWrapper::WordPiece(model) => Some(lookup(&model.unk_token)?),
121 ModelWrapper::WordLevel(model) => Some(lookup(&model.unk_token)?),
122 ModelWrapper::Unigram(model) => serde_json::to_value(model)
124 .context("failed to serialize unigram model")?
125 .get("unk_id")
126 .and_then(Value::as_u64)
127 .map(|id| id as usize),
128 };
129
130 Ok((median_token_length, unk_token_id))
131}
132
133#[derive(Debug)]
135enum Embeddings {
136 F32(Cow<'static, [f32]>),
137 I8(Cow<'static, [i8]>),
138}
139
140impl Embeddings {
141 fn len(&self) -> usize {
142 match self {
143 Embeddings::F32(table) => table.len(),
144 Embeddings::I8(table) => table.len(),
145 }
146 }
147}
148
149#[derive(Debug, Clone)]
150struct ModelFiles {
151 tokenizer: PathBuf,
152 model: PathBuf,
153 config: PathBuf,
154}
155
156fn layout_candidates<T: Copy>(base: T, subfolder: T, parent: T) -> [(T, T, &'static str); 4] {
157 [
158 (base, base, "config.json"),
159 (base, base, "config_sentence_transformers.json"),
160 (base, subfolder, "config_sentence_transformers.json"),
161 (parent, base, "config_sentence_transformers.json"),
162 ]
163}
164
165fn decode_f32(raw: &[u8]) -> Vec<f32> {
166 raw.as_chunks::<4>().0.iter().map(|&b| f32::from_le_bytes(b)).collect()
167}
168
169fn decode_f16(raw: &[u8]) -> Vec<f32> {
170 let raw = raw.as_chunks::<2>().0;
171 let mut out = vec![0.0_f32; raw.len()];
172 let mut halves = [f16::ZERO; 1024];
173 for (src, dst) in raw.chunks(halves.len()).zip(out.chunks_mut(halves.len())) {
174 let halves = &mut halves[..src.len()];
175 for (h, &b) in halves.iter_mut().zip(src) {
176 *h = f16::from_le_bytes(b);
177 }
178 halves.convert_to_f32_slice(dst);
179 }
180 out
181}
182
183fn match_local_layout(config_base: &Path, model_base: &Path, config_file: &str) -> Option<ModelFiles> {
184 let config = config_base.join(config_file);
185 let tokenizer = model_base.join("tokenizer.json");
186 let model = model_base.join("model.safetensors");
187 (config.exists() && tokenizer.exists() && model.exists()).then_some(ModelFiles {
188 tokenizer,
189 model,
190 config,
191 })
192}
193
194fn decode_token_mapping(dtype: Dtype, raw: &[u8]) -> Result<Vec<usize>> {
195 let mapping = match dtype {
196 Dtype::I64 => raw
197 .as_chunks::<8>()
198 .0
199 .iter()
200 .map(|&b| i64::from_le_bytes(b) as usize)
201 .collect(),
202 Dtype::I32 => raw
203 .as_chunks::<4>()
204 .0
205 .iter()
206 .map(|&b| i32::from_le_bytes(b) as usize)
207 .collect(),
208 other => return Err(anyhow!("unsupported mapping dtype: {:?}", other)),
209 };
210
211 Ok(mapping)
212}
213
214#[cfg(all(feature = "hf-hub", not(feature = "local-only")))]
215fn is_not_found(e: &hf_hub::api::sync::ApiError) -> bool {
216 use hf_hub::api::sync::ApiError;
217
218 matches!(e, ApiError::RequestError(e) if matches!(e.as_ref(), ureq::Error::Status(404, _)))
219}
220
221#[cfg(all(feature = "hf-hub", not(feature = "local-only")))]
222fn match_hub_layout(
223 repo: &ApiRepo,
224 config_prefix: &str,
225 model_prefix: &str,
226 config_file: &str,
227) -> Result<Option<ModelFiles>> {
228 let fetch = |path: String| -> Result<Option<PathBuf>> {
229 match repo.get(&path) {
230 Ok(p) => Ok(Some(p)),
231 Err(e) if is_not_found(&e) => Ok(None),
232 Err(e) => Err(e.into()),
233 }
234 };
235 let Some(config) = fetch(format!("{config_prefix}{config_file}"))? else {
236 return Ok(None);
237 };
238 let Some(tokenizer) = fetch(format!("{model_prefix}tokenizer.json"))? else {
239 return Ok(None);
240 };
241 let Some(model) = fetch(format!("{model_prefix}model.safetensors"))? else {
242 return Ok(None);
243 };
244 Ok(Some(ModelFiles {
245 tokenizer,
246 model,
247 config,
248 }))
249}
250
251fn resolve_local_model_files(folder: &Path) -> Option<ModelFiles> {
252 let subfolder = folder.join("0_StaticEmbedding");
253 layout_candidates(Some(folder), Some(subfolder.as_path()), folder.parent())
254 .into_iter()
255 .find_map(|(config_base, model_base, config_file)| match_local_layout(config_base?, model_base?, config_file))
256}
257
258#[cfg(all(feature = "hf-hub", not(feature = "local-only")))]
259fn resolve_hub_model_files(repo: &ApiRepo, prefix: &str) -> Result<ModelFiles> {
260 let sub_prefix = format!("{prefix}0_StaticEmbedding/");
261 let trimmed = prefix.trim_end_matches('/');
262 let parent = match Path::new(trimmed).parent() {
263 Some(path) if !path.as_os_str().is_empty() => format!("{}/", path.display()),
264 _ => String::new(),
265 };
266
267 for (config_prefix, model_prefix, config_file) in layout_candidates(prefix, sub_prefix.as_str(), parent.as_str()) {
268 if let Some(files) = match_hub_layout(repo, config_prefix, model_prefix, config_file)? {
269 return Ok(files);
270 }
271 }
272 Err(anyhow!("no valid model layout found in '{prefix}'"))
273}
274
275impl StaticModel {
276 pub fn from_bytes<T, M, C>(
281 tokenizer_bytes: T,
282 model_bytes: M,
283 config_bytes: C,
284 normalize: Option<bool>,
285 ) -> Result<Self>
286 where
287 T: AsRef<[u8]>,
288 M: AsRef<[u8]>,
289 C: AsRef<[u8]>,
290 {
291 let tokenizer = LoadedTokenizer::from_bytes(tokenizer_bytes.as_ref())?;
292 Self::from_loaded_bytes(tokenizer, model_bytes.as_ref(), config_bytes.as_ref(), normalize)
293 }
294
295 fn from_loaded_bytes(
296 tokenizer: LoadedTokenizer,
297 model_bytes: &[u8],
298 config_bytes: &[u8],
299 normalize: Option<bool>,
300 ) -> Result<Self> {
301 let cfg: Value = serde_json::from_slice(config_bytes).context("failed to parse config.json")?;
303 let cfg_norm = cfg.get("normalize").and_then(Value::as_bool).unwrap_or(true);
304 let normalize = normalize.unwrap_or(cfg_norm);
305
306 let safet = SafeTensors::deserialize(model_bytes).context("failed to parse safetensors")?;
308 let tensor = safet
309 .tensor("embeddings")
310 .or_else(|_| safet.tensor("0"))
311 .or_else(|_| safet.tensor("embedding.weight"))
312 .context("embeddings tensor not found")?;
313
314 let [rows, cols]: [usize; 2] = tensor.shape().try_into().context("embedding tensor is not 2-D")?;
315 let raw = tensor.data();
316 let embeddings = match tensor.dtype() {
317 Dtype::F32 => Embeddings::F32(Cow::Owned(decode_f32(raw))),
318 Dtype::F16 => Embeddings::F32(Cow::Owned(decode_f16(raw))),
319 Dtype::I8 => Embeddings::I8(Cow::Owned(raw.iter().map(|&b| b as i8).collect())),
320 other => return Err(anyhow!("unsupported tensor dtype: {other:?}")),
321 };
322
323 let weights = match safet.tensor("weights") {
324 Ok(t) => {
325 let raw = t.data();
326 let v: Vec<f32> = match t.dtype() {
327 Dtype::F64 => raw
328 .as_chunks::<8>()
329 .0
330 .iter()
331 .map(|&b| f64::from_le_bytes(b) as f32)
332 .collect(),
333 Dtype::F32 => decode_f32(raw),
334 Dtype::F16 => decode_f16(raw),
335 other => return Err(anyhow!("unsupported weights dtype: {:?}", other)),
336 };
337 Some(v)
338 }
339 Err(_) => None,
340 };
341
342 let token_mapping = match safet.tensor("mapping") {
343 Ok(t) => Some(decode_token_mapping(t.dtype(), t.data())?),
344 Err(_) => None,
345 };
346
347 Self::from_storage(
348 tokenizer,
349 embeddings,
350 rows,
351 cols,
352 normalize,
353 weights.map(Cow::Owned),
354 token_mapping.map(Cow::Owned),
355 )
356 }
357
358 pub fn from_pretrained<P: AsRef<Path>>(
366 repo_or_path: P,
367 token: Option<&str>,
368 normalize: Option<bool>,
369 subfolder: Option<&str>,
370 ) -> Result<Self> {
371 let files = resolve_model_files(repo_or_path, token, subfolder)?;
372 let tokenizer_bytes = fs::read(&files.tokenizer).context("failed to read tokenizer.json")?;
373 let model_bytes = fs::read(&files.model).context("failed to read model.safetensors")?;
374 let config_bytes = fs::read(&files.config).context("failed to read config.json")?;
375 Self::from_bytes(tokenizer_bytes, model_bytes, config_bytes, normalize)
376 }
377
378 pub fn from_owned(
389 tokenizer: Tokenizer,
390 embeddings: Vec<f32>,
391 rows: usize,
392 cols: usize,
393 normalize: bool,
394 weights: Option<Vec<f32>>,
395 token_mapping: Option<Vec<usize>>,
396 ) -> Result<Self> {
397 Self::from_storage(
398 LoadedTokenizer::from_tokenizer(tokenizer)?,
399 Embeddings::F32(Cow::Owned(embeddings)),
400 rows,
401 cols,
402 normalize,
403 weights.map(Cow::Owned),
404 token_mapping.map(Cow::Owned),
405 )
406 }
407
408 pub fn from_borrowed(
419 tokenizer: Tokenizer,
420 embeddings: &'static [f32],
421 rows: usize,
422 cols: usize,
423 normalize: bool,
424 weights: Option<&'static [f32]>,
425 token_mapping: Option<&'static [usize]>,
426 ) -> Result<Self> {
427 Self::from_storage(
428 LoadedTokenizer::from_tokenizer(tokenizer)?,
429 Embeddings::F32(Cow::Borrowed(embeddings)),
430 rows,
431 cols,
432 normalize,
433 weights.map(Cow::Borrowed),
434 token_mapping.map(Cow::Borrowed),
435 )
436 }
437
438 pub fn from_owned_i8(
450 tokenizer: Tokenizer,
451 embeddings: Vec<i8>,
452 rows: usize,
453 cols: usize,
454 normalize: bool,
455 weights: Option<Vec<f32>>,
456 token_mapping: Option<Vec<usize>>,
457 ) -> Result<Self> {
458 Self::from_storage(
459 LoadedTokenizer::from_tokenizer(tokenizer)?,
460 Embeddings::I8(Cow::Owned(embeddings)),
461 rows,
462 cols,
463 normalize,
464 weights.map(Cow::Owned),
465 token_mapping.map(Cow::Owned),
466 )
467 }
468
469 pub fn from_borrowed_i8(
481 tokenizer: Tokenizer,
482 embeddings: &'static [i8],
483 rows: usize,
484 cols: usize,
485 normalize: bool,
486 weights: Option<&'static [f32]>,
487 token_mapping: Option<&'static [usize]>,
488 ) -> Result<Self> {
489 Self::from_storage(
490 LoadedTokenizer::from_tokenizer(tokenizer)?,
491 Embeddings::I8(Cow::Borrowed(embeddings)),
492 rows,
493 cols,
494 normalize,
495 weights.map(Cow::Borrowed),
496 token_mapping.map(Cow::Borrowed),
497 )
498 }
499
500 fn from_storage(
501 tokenizer: LoadedTokenizer,
502 embeddings: Embeddings,
503 rows: usize,
504 cols: usize,
505 normalize: bool,
506 weights: Option<Cow<'static, [f32]>>,
507 token_mapping: Option<Cow<'static, [usize]>>,
508 ) -> Result<Self> {
509 if cols == 0 {
510 return Err(anyhow!("embedding dimension must be greater than 0"));
511 }
512 if rows.checked_mul(cols) != Some(embeddings.len()) {
513 return Err(anyhow!(
514 "embeddings length {} != rows {} * cols {}",
515 embeddings.len(),
516 rows,
517 cols
518 ));
519 }
520 Ok(Self {
521 inner: Arc::new(StaticModelInner {
522 tokenizer,
523 embeddings,
524 dim: cols,
525 row_scales: None,
526 weights,
527 token_mapping,
528 normalize,
529 }),
530 })
531 }
532
533 pub fn quantize_int8(self) -> StaticModel {
538 let Embeddings::F32(table) = &self.inner.embeddings else {
539 return self;
540 };
541 let (data, mut scales) = quantize_rows(table, self.inner.dim);
542 let embeddings = Embeddings::I8(Cow::Owned(data));
543 let mut inner = match Arc::try_unwrap(self.inner) {
544 Ok(mut inner) => {
545 inner.embeddings = embeddings;
546 inner
547 }
548 Err(shared) => StaticModelInner {
549 tokenizer: shared.tokenizer.clone(),
550 embeddings,
551 dim: shared.dim,
552 row_scales: None,
553 weights: shared.weights.clone(),
554 token_mapping: shared.token_mapping.clone(),
555 normalize: shared.normalize,
556 },
557 };
558 if inner.token_mapping.is_none()
559 && let Some(weights) = inner.weights.take()
560 {
561 for (row, scale) in scales.iter_mut().enumerate() {
562 *scale *= weights.get(row).copied().unwrap_or(1.0);
563 }
564 }
565 inner.row_scales = Some(scales);
566 StaticModel { inner: Arc::new(inner) }
567 }
568
569 fn truncate_str(s: &str, max_tokens: usize, median_len: usize) -> &str {
571 s.char_indices()
572 .nth(max_tokens.saturating_mul(median_len))
573 .map_or(s, |(byte_idx, _)| &s[..byte_idx])
574 }
575
576 pub fn encode_with_args(
583 &self,
584 sentences: &[String],
585 max_length: Option<usize>,
586 batch_size: usize,
587 ) -> Vec<Vec<f32>> {
588 let mut embeddings = Vec::with_capacity(sentences.len());
589 for batch in sentences.chunks(batch_size) {
590 let pooled: Vec<Vec<f32>> = batch
591 .maybe_par_iter()
592 .map(|text| self.encode_one(text, max_length))
593 .collect();
594 embeddings.extend(pooled);
595 }
596 embeddings
597 }
598
599 pub fn encode(&self, sentences: &[String]) -> Vec<Vec<f32>> {
601 self.encode_with_args(sentences, Some(512), sentences.len().max(1))
602 }
603
604 pub fn dim(&self) -> usize {
606 self.inner.dim
607 }
608
609 pub fn encode_flat_with_args(&self, sentences: &[String], max_length: Option<usize>) -> Vec<f32> {
615 let dim = self.inner.dim;
616 let mut out = vec![0.0_f32; sentences.len() * dim];
617 let mut jobs: Vec<(&str, &mut [f32])> = sentences.iter().map(String::as_str).zip(out.chunks_mut(dim)).collect();
618 jobs.maybe_par_iter_mut()
619 .for_each(|(text, row)| self.encode_into(text, max_length, row));
620 out
621 }
622
623 pub fn encode_flat(&self, sentences: &[String]) -> Vec<f32> {
625 self.encode_flat_with_args(sentences, Some(512))
626 }
627
628 pub fn encode_single(&self, sentence: &str) -> Vec<f32> {
630 self.encode_one(sentence, Some(512))
631 }
632
633 fn encode_one(&self, text: &str, max_length: Option<usize>) -> Vec<f32> {
637 let mut out = vec![0.0_f32; self.inner.dim];
638 self.encode_into(text, max_length, &mut out);
639 out
640 }
641
642 fn encode_into(&self, text: &str, max_length: Option<usize>, out: &mut [f32]) {
644 let model = &self.inner;
645 let text = max_length.map_or(text, |max_tok| {
646 Self::truncate_str(text, max_tok, model.tokenizer.median_token_length)
647 });
648 match &model.tokenizer.tokenization {
649 Tokenization::WordPiece(ft) => TOKEN_IDS.with_borrow_mut(|ids| {
650 ids.clear();
651 ft.encode_ids_into(text, ids);
652 self.pool_ids(ids, max_length, out)
653 }),
654 Tokenization::Unigram(tokenizer, fu) => {
655 let ids = fu.encode_ids(tokenizer, text).expect("tokenization failed");
656 self.pool_ids(&ids, max_length, out)
657 }
658 Tokenization::Crate(tokenizer) => {
659 let encoding = tokenizer.encode_fast(text, false).expect("tokenization failed");
660 self.pool_ids(encoding.get_ids(), max_length, out)
661 }
662 }
663 }
664
665 fn pool_ids(&self, ids: &[u32], max_length: Option<usize>, sum: &mut [f32]) {
667 let model = &self.inner;
668 let ids = &ids[..max_length.map_or(ids.len(), |max_tok| max_tok.min(ids.len()))];
669 let cnt = match &model.embeddings {
670 Embeddings::F32(table) => self.sum_rows(table, ids, sum),
671 Embeddings::I8(table) => self.sum_rows(table, ids, sum),
672 };
673 let denom = cnt.max(1) as f32;
674 if model.normalize {
675 let mut sq = 0.0_f32;
676 for x in sum.iter_mut() {
677 *x /= denom;
678 sq += *x * *x;
679 }
680 let norm = sq.sqrt().max(1e-12);
681 for x in sum.iter_mut() {
682 *x /= norm;
683 }
684 } else {
685 for x in sum.iter_mut() {
686 *x /= denom;
687 }
688 }
689 }
690
691 fn sum_rows<T: Copy>(&self, table: &[T], ids: &[u32], out: &mut [f32]) -> usize
693 where
694 f32: From<T>,
695 {
696 match self.inner.dim {
697 64 => self.sum_rows_fixed::<T, 64>(table, ids, out),
698 128 => self.sum_rows_fixed::<T, 128>(table, ids, out),
699 256 => self.sum_rows_fixed::<T, 256>(table, ids, out),
700 384 => self.sum_rows_fixed::<T, 384>(table, ids, out),
701 512 => self.sum_rows_fixed::<T, 512>(table, ids, out),
702 768 => self.sum_rows_fixed::<T, 768>(table, ids, out),
703 1024 => self.sum_rows_fixed::<T, 1024>(table, ids, out),
704 _ => self.sum_rows_dyn(table, ids, out),
705 }
706 }
707
708 fn sum_rows_fixed<T: Copy, const D: usize>(&self, table: &[T], ids: &[u32], out: &mut [f32]) -> usize
710 where
711 f32: From<T>,
712 {
713 let sum: &mut [f32; D] = out.try_into().expect("output has D columns");
714 self.sum_rows_blocked(table, ids, sum)
715 }
716
717 fn sum_rows_dyn<T: Copy>(&self, table: &[T], ids: &[u32], sum: &mut [f32]) -> usize
719 where
720 f32: From<T>,
721 {
722 self.sum_rows_blocked(table, ids, sum)
723 }
724
725 #[inline(always)]
728 fn sum_rows_blocked<T: Copy>(&self, table: &[T], ids: &[u32], sum: &mut [f32]) -> usize
729 where
730 f32: From<T>,
731 {
732 let mut pending: [(&[T], f32); BLOCK] = [(&[], 0.0); BLOCK];
733 let mut n = 0;
734 let cnt = self.for_each_row(table, ids, |row, scale| {
735 pending[n] = (row, scale);
736 n += 1;
737 if n == BLOCK {
738 add_rows(sum, &pending);
739 n = 0;
740 }
741 });
742 for &(row, scale) in &pending[..n] {
743 add_row(sum, row, scale);
744 }
745 cnt
746 }
747
748 #[inline(always)]
751 fn for_each_row<'t, T>(&self, table: &'t [T], ids: &[u32], mut f: impl FnMut(&'t [T], f32)) -> usize {
752 let model = &self.inner;
753 let dim = model.dim;
754 let mut cnt = 0;
755 let tokens = ids
756 .iter()
757 .map(|&id| id as usize)
758 .filter(|&tok| Some(tok) != model.tokenizer.unk_token_id);
759 match (&model.token_mapping, &model.weights, &model.row_scales) {
760 (None, None, None) => {
761 for tok in tokens {
762 f(&table[tok * dim..(tok + 1) * dim], 1.0);
763 cnt += 1;
764 }
765 }
766 (None, None, Some(row_scales)) => {
767 for tok in tokens {
768 f(&table[tok * dim..(tok + 1) * dim], row_scales[tok]);
769 cnt += 1;
770 }
771 }
772 (mapping, weights, row_scales) => {
773 for tok in tokens {
774 let row_idx = mapping.as_ref().and_then(|m| m.get(tok)).copied().unwrap_or(tok);
775 let mut scale = weights.as_ref().and_then(|w| w.get(tok)).copied().unwrap_or(1.0);
776 if let Some(row_scales) = row_scales {
777 scale *= row_scales[row_idx];
778 }
779 f(&table[row_idx * dim..(row_idx + 1) * dim], scale);
780 cnt += 1;
781 }
782 }
783 }
784 cnt
785 }
786}
787
788#[inline(always)]
790fn add_row<T: Copy>(sum: &mut [f32], row: &[T], scale: f32)
791where
792 f32: From<T>,
793{
794 for (s, &v) in sum.iter_mut().zip(row) {
795 *s += f32::from(v) * scale;
796 }
797}
798
799const BLOCK: usize = 8;
800
801#[inline(always)]
803fn add_rows<T: Copy>(sum: &mut [f32], rows: &[(&[T], f32); BLOCK])
804where
805 f32: From<T>,
806{
807 let n = sum.len();
808 let [
809 (r0, s0),
810 (r1, s1),
811 (r2, s2),
812 (r3, s3),
813 (r4, s4),
814 (r5, s5),
815 (r6, s6),
816 (r7, s7),
817 ] = *rows;
818 let (r0, r1, r2, r3, r4, r5, r6, r7) = (
819 &r0[..n],
820 &r1[..n],
821 &r2[..n],
822 &r3[..n],
823 &r4[..n],
824 &r5[..n],
825 &r6[..n],
826 &r7[..n],
827 );
828 for j in 0..n {
829 sum[j] = sum[j]
830 + f32::from(r0[j]) * s0
831 + f32::from(r1[j]) * s1
832 + f32::from(r2[j]) * s2
833 + f32::from(r3[j]) * s3
834 + f32::from(r4[j]) * s4
835 + f32::from(r5[j]) * s5
836 + f32::from(r6[j]) * s6
837 + f32::from(r7[j]) * s7;
838 }
839}
840
841fn quantize_rows(table: &[f32], dim: usize) -> (Vec<i8>, Vec<f32>) {
844 let mut data = Vec::with_capacity(table.len());
845 let mut scales = Vec::with_capacity(table.len() / dim.max(1));
846 for row in table.chunks_exact(dim.max(1)) {
847 let max = row.iter().fold(0.0_f32, |m, &v| m.max(v.abs()));
848 let scale = max / 127.0;
849 if scale > 0.0 {
850 data.extend(row.iter().map(|&v| (v / scale).round().clamp(-127.0, 127.0) as i8));
851 } else {
852 data.extend(std::iter::repeat_n(0, row.len()));
853 }
854 scales.push(scale);
855 }
856 (data, scales)
857}
858
859fn resolve_model_files<P: AsRef<Path>>(
860 repo_or_path: P,
861 token: Option<&str>,
862 subfolder: Option<&str>,
863) -> Result<ModelFiles> {
864 #[cfg(any(not(feature = "hf-hub"), feature = "local-only"))]
865 let _ = token;
866
867 let base = repo_or_path.as_ref();
868 if base.exists() {
869 let folder = subfolder.map(|s| base.join(s)).unwrap_or_else(|| base.to_path_buf());
870 return resolve_local_model_files(&folder).ok_or_else(|| {
871 anyhow!(
872 "no valid model layout found in {folder:?}. \
873 Tried: model2vec (config.json), sentence-transformers \
874 (config_sentence_transformers.json), and 0_StaticEmbedding subfolder."
875 )
876 });
877 }
878
879 #[cfg(all(feature = "hf-hub", not(feature = "local-only")))]
880 {
881 download_model_files(repo_or_path.as_ref().to_string_lossy().as_ref(), token, subfolder)
882 }
883 #[cfg(feature = "local-only")]
884 {
885 Err(anyhow!(
886 "remote model downloads are disabled by the `local-only` feature; pass a local model directory instead"
887 ))
888 }
889 #[cfg(all(not(feature = "hf-hub"), not(feature = "local-only")))]
890 {
891 Err(anyhow!(
892 "remote model downloads require the `hf-hub` feature; pass a local model directory instead"
893 ))
894 }
895}
896
897#[cfg(all(feature = "hf-hub", not(feature = "local-only")))]
898fn download_model_files(repo_id: &str, token: Option<&str>, subfolder: Option<&str>) -> Result<ModelFiles> {
899 let api = match token {
900 Some(token) => ApiBuilder::new().with_token(Some(token.to_owned())).build(),
901 None => Api::new(),
902 }
903 .context("hf-hub API init failed")?;
904 let repo = api.model(repo_id.to_owned());
905 let prefix = subfolder.map(|s| format!("{s}/")).unwrap_or_default();
906 resolve_hub_model_files(&repo, &prefix).with_context(|| format!("could not load '{repo_id}' from HuggingFace Hub"))
907}
908
909#[cfg(test)]
910mod tests {
911 use super::{Embeddings, StaticModel, Tokenization, decode_token_mapping};
912 use safetensors::tensor::Dtype;
913 use std::borrow::Cow;
914 use tokenizers::{
915 PaddingParams, PaddingStrategy, Tokenizer, TruncationParams, pre_tokenizers::whitespace::Whitespace,
916 };
917
918 #[test]
919 fn decode_token_mapping_supports_i32_and_i64() {
920 let i32_raw = [1i32, 2, 3]
921 .into_iter()
922 .flat_map(|value| value.to_le_bytes())
923 .collect::<Vec<_>>();
924 let i64_raw = [4i64, 5, 6]
925 .into_iter()
926 .flat_map(|value| value.to_le_bytes())
927 .collect::<Vec<_>>();
928
929 assert_eq!(decode_token_mapping(Dtype::I32, &i32_raw).unwrap(), vec![1, 2, 3]);
930 assert_eq!(decode_token_mapping(Dtype::I64, &i64_raw).unwrap(), vec![4, 5, 6]);
931 }
932
933 #[test]
934 fn decode_token_mapping_rejects_unsupported_dtype() {
935 let err = decode_token_mapping(Dtype::F32, &[0, 0, 0, 0]).unwrap_err();
936 assert!(err.to_string().contains("unsupported mapping dtype"));
937 }
938
939 #[test]
940 fn int8_models_keep_i8_storage() {
941 let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures");
942 let int8 = StaticModel::from_pretrained(format!("{dir}/test-model-int8"), None, None, None).unwrap();
943 assert!(matches!(&int8.inner.embeddings, Embeddings::I8(Cow::Owned(t)) if t.len() == 29528 * 2));
944 let f32_model = StaticModel::from_pretrained(format!("{dir}/test-model-float32"), None, None, None).unwrap();
945 assert!(matches!(&f32_model.inner.embeddings, Embeddings::F32(_)));
946 }
947
948 #[test]
949 fn quantize_int8_replaces_f32_storage() {
950 let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures");
951 let model = StaticModel::from_pretrained(format!("{dir}/test-model-vocab-quantized"), None, None, None)
952 .unwrap()
953 .quantize_int8();
954 let inner = &model.inner;
955 let rows = inner.embeddings.len() / inner.dim;
956 assert!(matches!(&inner.embeddings, Embeddings::I8(Cow::Owned(_))));
957 assert_eq!(inner.row_scales.as_ref().map(Vec::len), Some(rows));
958 assert_eq!(inner.weights.is_none(), inner.token_mapping.is_none());
959 }
960
961 #[test]
962 fn tokenizer_truncation_and_padding_are_ignored() {
963 let path = concat!(
964 env!("CARGO_MANIFEST_DIR"),
965 "/tests/fixtures/test-model-int8/tokenizer.json"
966 );
967 let mut plain = Tokenizer::from_file(path).unwrap();
968 plain.with_pre_tokenizer(Some(Whitespace {}));
969 let mut configured = plain.clone();
970 configured
971 .with_truncation(Some(TruncationParams {
972 max_length: 2,
973 ..Default::default()
974 }))
975 .unwrap();
976 configured.with_padding(Some(PaddingParams {
977 strategy: PaddingStrategy::Fixed(64),
978 ..Default::default()
979 }));
980
981 let rows = plain.get_vocab_size(false);
982 let embeddings: Vec<f32> = (0..rows * 2).map(|i| (i % 97) as f32).collect();
983 let load = |tok| StaticModel::from_owned(tok, embeddings.clone(), rows, 2, false, None, None).unwrap();
984 let (plain, configured) = (load(plain), load(configured));
985 assert!(matches!(
986 configured.inner.tokenizer.tokenization,
987 Tokenization::Crate(_)
988 ));
989
990 let text = "the quick brown fox jumps over the lazy dog";
991 assert_eq!(configured.encode_single(text), plain.encode_single(text));
992 }
993
994 #[test]
995 fn overflowing_shape_is_rejected() {
996 let path = concat!(
997 env!("CARGO_MANIFEST_DIR"),
998 "/tests/fixtures/test-model-int8/tokenizer.json"
999 );
1000 let tok = Tokenizer::from_file(path).unwrap();
1001 assert!(StaticModel::from_owned(tok, Vec::new(), usize::MAX / 2 + 1, 2, false, None, None).is_err());
1002 }
1003}