bge_m3_embedding_server/embedder/
dense.rs1use anyhow::Result;
18use ort::value::TensorRef;
19
20use super::error::ort_err;
21use super::jit_guard::{self, TrtJitGuard};
22use super::math::{normalize_l2, seq_len_distribution};
23use super::tokenize::{build_chunk_arrays, tokenize_no_pad};
24use super::types::EmbedStats;
25use crate::binpack::{CostModel, bin_pack};
26use crate::config::ModelVariant;
27
28#[allow(clippy::cast_possible_truncation)]
41pub(super) fn embed_dense(
42 session: &mut ort::session::Session,
43 tokenizer: &tokenizers::Tokenizer,
44 texts: &[String],
45 cost_model: &CostModel,
46 model_variant: ModelVariant,
47 guard: Option<&TrtJitGuard>,
48) -> Result<(Vec<Vec<f32>>, EmbedStats)> {
49 let tokenize_start = std::time::Instant::now();
50 let encodings = tokenize_no_pad(tokenizer, texts)?;
51 let seq_lens: Vec<usize> = encodings.iter().map(|e| e.get_ids().len()).collect();
52 let tokenize_ms = u64::try_from(tokenize_start.elapsed().as_millis()).unwrap_or(u64::MAX);
53
54 let seq_dist = seq_len_distribution(&seq_lens);
55 let total_token_positions: usize = seq_lens.iter().sum();
56 let chunks = bin_pack(&seq_lens, cost_model);
57 jit_guard::guard_chunks(guard, &chunks, &seq_lens).map_err(anyhow::Error::new)?;
58
59 let mut all_embeddings: Vec<Vec<f32>> = (0..texts.len()).map(|_| Vec::new()).collect();
61
62 let mut max_chunk_seq: usize = 0;
63 let mut inference_ms: u64 = 0;
64
65 for (chunk_idx, chunk_indices) in chunks.iter().enumerate() {
66 let chunk_max = chunk_indices
67 .iter()
68 .map(|&i| seq_lens[i])
69 .max()
70 .unwrap_or(1)
71 .max(1); max_chunk_seq = max_chunk_seq.max(chunk_max);
74
75 let (ids_array, mask_array) = build_chunk_arrays(&encodings, chunk_indices, chunk_max)?;
76 let batch_len = ids_array.nrows();
77
78 let ids_tensor = TensorRef::from_array_view(ids_array.view()).map_err(ort_err)?;
79 let mask_tensor = TensorRef::from_array_view(mask_array.view()).map_err(ort_err)?;
80
81 let chunk_start = std::time::Instant::now();
82 let outputs = {
83 let _span = tracing::debug_span!(
84 "chunk",
85 chunk_idx,
86 batch = chunk_indices.len(),
87 max_seq = chunk_max
88 )
89 .entered();
90 session
91 .run(ort::inputs! {
92 "input_ids" => ids_tensor,
93 "attention_mask" => mask_tensor,
94 })
95 .map_err(ort_err)?
96 };
97 let chunk_ms = u64::try_from(chunk_start.elapsed().as_millis()).unwrap_or(u64::MAX);
98 inference_ms = inference_ms.saturating_add(chunk_ms);
99 tracing::debug!(
100 chunk_idx,
101 batch = chunk_indices.len(),
102 max_seq = chunk_max,
103 elapsed_ms = chunk_ms,
104 "dense chunk inference complete"
105 );
106
107 let emb: ndarray::ArrayD<f32> = match model_variant {
110 ModelVariant::Fp32 => outputs["sentence_embedding"]
111 .try_extract_array::<f32>()
112 .map_err(ort_err)?
113 .to_owned(),
114 ModelVariant::Fp16 | ModelVariant::Int8 => {
115 let lhs = outputs["last_hidden_state"]
116 .try_extract_array::<f32>()
117 .map_err(ort_err)?;
118 lhs.index_axis(ndarray::Axis(1), 0).to_owned()
119 }
120 };
121
122 for (chunk_pos, &orig_idx) in chunk_indices.iter().enumerate() {
123 debug_assert!(chunk_pos < batch_len, "chunk_pos must be within batch");
124 let row = emb.index_axis(ndarray::Axis(0), chunk_pos);
125 let mut vec = row
126 .as_slice()
127 .expect("embedding should be contiguous")
128 .to_vec();
129 normalize_l2(&mut vec);
130 all_embeddings[orig_idx] = vec;
131 }
132 }
133
134 let stats = EmbedStats {
135 chunks: chunks.len(),
136 max_chunk_seq,
137 total_token_positions,
138 tokenize_ms,
139 inference_ms,
140 seq_len_min: seq_dist.min,
141 seq_len_max: seq_dist.max,
142 seq_len_mean: seq_dist.mean,
143 seq_len_p95: seq_dist.p95,
144 };
145
146 Ok((all_embeddings, stats))
147}