haematite_cli/serve/embed/
embedder.rs1use std::sync::atomic::{AtomicUsize, Ordering};
4use std::sync::{Mutex, PoisonError};
5
6use haem_frames::embedding::{Distance, Kind, input_refusal};
7
8use super::Settings;
9use super::worker::{Batch, Ended, Ending, Worker};
10use crate::serve::storage::Failure;
11
12#[derive(Clone, Debug, PartialEq, Eq)]
14pub struct ModelId {
15 pub name: String,
16 pub revision: String,
17 pub manifest_sha256: String,
18 pub dimensions: u64,
19 pub distance: Distance,
20 pub normalised: bool,
21}
22
23#[derive(Debug, Clone, PartialEq)]
25pub enum Answer {
26 Vector(Vec<f32>),
27 Longer(Longer),
30 Windows(Vec<WindowVector>),
33 OverCap(Longer),
36}
37
38#[derive(Debug, Clone, PartialEq)]
40pub struct WindowVector {
41 pub start: u64,
42 pub end: u64,
43 pub vector: Vec<f32>,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub struct Longer {
50 pub tokens_total: u64,
51 pub tokens_limit: u64,
52}
53
54#[derive(Debug)]
57pub struct Embedded {
58 pub model: ModelId,
59 pub answers: Vec<Answer>,
60}
61
62pub struct Embedder {
64 settings: Option<Settings>,
65 worker: Mutex<Option<Worker>>,
66 waiting: AtomicUsize,
67 last: Mutex<Option<Ending>>,
70}
71
72struct Waiting<'a>(&'a AtomicUsize);
74
75impl Drop for Waiting<'_> {
76 fn drop(&mut self) {
77 self.0.fetch_sub(1, Ordering::AcqRel);
78 }
79}
80
81impl Embedder {
82 #[must_use]
85 pub const fn new(settings: Option<Settings>) -> Self {
86 Self {
87 settings,
88 worker: Mutex::new(None),
89 waiting: AtomicUsize::new(0),
90 last: Mutex::new(None),
91 }
92 }
93
94 pub fn embed(&self, kind: Kind, texts: &[String]) -> Result<Embedded, Failure> {
109 let settings = self.settings.as_ref().ok_or_else(|| {
110 Failure::new(
111 "embed_unavailable",
112 "no embedding worker is configured for this service",
113 )
114 })?;
115 asked(settings, texts)?;
116 let batches = texts
117 .chunks(settings.batch.get())
118 .enumerate()
119 .map(|(number, batch)| Batch::new(kind, batch, number * settings.batch.get(), settings))
120 .collect::<Result<Vec<_>, _>>()?;
121 let waiting = self.wait(settings)?;
122 let mut worker = self.worker.lock().map_err(|_| {
123 Failure::new(
124 "embed_worker_owner_failed",
125 "a request panicked while it held the worker",
126 )
127 })?;
128 drop(waiting);
129 let mut answers = Vec::with_capacity(texts.len());
130 let mut ending = None;
131 for batch in &batches {
132 match call(&mut worker, batch, settings, &mut ending) {
133 Ok(made) => answers.extend(made),
134 Err(ended) => {
135 drop(worker);
136 self.remember(ending);
137 return Err(ended);
138 }
139 }
140 }
141 let Some(kept) = worker.as_ref() else {
142 return Err(not_started());
143 };
144 let model = &kept.ready.model;
145 let model = ModelId {
146 name: model.name.clone(),
147 revision: model.revision.clone(),
148 manifest_sha256: model.manifest_sha256.clone(),
149 dimensions: model.dimensions,
150 distance: model.distance,
151 normalised: model.normalised,
152 };
153 drop(worker);
154 self.remember(ending);
155 Ok(Embedded { model, answers })
156 }
157
158 pub fn last_ending(&self) -> Option<Ending> {
161 let last = self.last.lock().unwrap_or_else(PoisonError::into_inner);
162 let ending = last.clone();
163 drop(last);
164 ending
165 }
166
167 fn remember(&self, ending: Option<Ending>) {
170 let Some(ending) = ending else {
171 return;
172 };
173 let mut last = self.last.lock().unwrap_or_else(PoisonError::into_inner);
174 *last = Some(ending);
175 drop(last);
176 }
177
178 fn wait(&self, settings: &Settings) -> Result<Waiting<'_>, Failure> {
179 let before = self.waiting.fetch_add(1, Ordering::AcqRel);
180 let waiting = Waiting(&self.waiting);
181 if before >= settings.waiting {
182 return Err(Failure::new(
183 "embed_busy",
184 format!("{before} requests already wait for the embedding worker"),
185 ));
186 }
187 Ok(waiting)
188 }
189}
190
191fn asked(settings: &Settings, texts: &[String]) -> Result<(), Failure> {
193 if texts.is_empty() {
194 return Err(Failure::new("embed_request_empty", "no texts were given"));
195 }
196 if texts.len() > settings.request_items.get() {
197 return Err(Failure::new(
198 "embed_request_too_large",
199 format!(
200 "{} texts were given; at most {} may be",
201 texts.len(),
202 settings.request_items
203 ),
204 ));
205 }
206 for (index, text) in texts.iter().enumerate() {
207 if let Some(code) = input_refusal(text, settings.limits.input_bytes) {
208 let code = serde_json::to_value(code).map_err(Failure::storage)?;
209 return Err(Failure::new(
210 code.as_str().unwrap_or("embed_input_refused"),
211 format!("text {index} cannot be embedded"),
212 ));
213 }
214 }
215 Ok(())
216}
217
218fn call(
222 worker: &mut Option<Worker>,
223 batch: &Batch,
224 settings: &Settings,
225 ending: &mut Option<Ending>,
226) -> Result<Vec<Answer>, Failure> {
227 for attempt in 0..2 {
228 if worker.is_none() {
229 *worker = Some(Worker::start(settings)?);
230 }
231 let Some(kept) = worker.as_mut() else {
232 return Err(not_started());
233 };
234 match kept.embed(batch, settings) {
235 Ok(made) => return Ok(made),
236 Err(Ended::Unsent(unsent)) if attempt == 0 => {
239 *worker = None;
240 *ending = Some(unsent.ending);
241 }
242 Err(ended) => {
243 *worker = None;
244 let lost = ended.lost();
245 *ending = Some(lost.ending);
246 return Err(lost.failure);
247 }
248 }
249 }
250 Err(not_started())
251}
252
253fn not_started() -> Failure {
254 Failure::new("embed_worker_not_ready", "the worker was not kept")
255}