haematite_cli/serve/embed/
workers.rs1use std::num::NonZeroUsize;
17use std::thread;
18
19use super::job::Embed;
20use super::{Embedded, Embedder, Settings};
21use crate::serve::storage::Failure;
22
23pub struct Workers<E = Embedder> {
25 workers: Vec<E>,
26}
27
28impl Workers<Embedder> {
29 #[must_use]
32 pub fn new(settings: Option<&Settings>, count: NonZeroUsize) -> Self {
33 let workers = (0..count.get())
34 .map(|_worker| Embedder::new(settings.cloned()))
35 .collect();
36 Self { workers }
37 }
38}
39
40impl<E: Embed + Sync> Workers<E> {
41 pub fn of(workers: Vec<E>) -> Result<Self, Failure> {
46 if workers.is_empty() {
47 return Err(Failure::new(
48 "embed_workers_none",
49 "at least one worker is needed",
50 ));
51 }
52 Ok(Self { workers })
53 }
54}
55
56impl<E: Embed + Sync> Embed for Workers<E> {
57 fn documents(&self, texts: &[String]) -> Result<Embedded, Failure> {
58 let total = self.workers.len();
59 let Some(first) = self.workers.first() else {
60 return Err(Failure::new(
61 "embed_workers_none",
62 "at least one worker is needed",
63 ));
64 };
65 if texts.is_empty() || total == 1 {
66 return first.documents(texts);
67 }
68 let shares = total.min(texts.len());
69 let answers: Vec<Result<Embedded, Failure>> = thread::scope(|scope| {
70 let mut running = Vec::with_capacity(shares);
71 for (share, worker) in shares_of(texts, shares).into_iter().zip(&self.workers) {
72 running.push(scope.spawn(move || worker.documents(share)));
73 }
74 running
75 .into_iter()
76 .map(|handle| handle.join().unwrap_or_else(|_panic| Err(panicked())))
77 .collect()
78 });
79 let mut model = None;
80 let mut answered = Vec::with_capacity(texts.len());
81 for (index, answer) in answers.into_iter().enumerate() {
82 let made = answer.map_err(|failure| Failure {
83 message: format!("worker {} of {total}: {}", index + 1, failure.message),
84 ..failure
85 })?;
86 if model.as_ref().is_some_and(|known| *known != made.model) {
87 return Err(Failure::new(
88 "embed_model_mixed",
89 format!("worker {} of {total} answered another model", index + 1),
90 ));
91 }
92 if model.is_none() {
93 model = Some(made.model);
94 }
95 answered.extend(made.answers);
96 }
97 let model = model.ok_or_else(panicked)?;
98 Ok(Embedded {
99 model,
100 answers: answered,
101 })
102 }
103}
104
105fn shares_of(texts: &[String], shares: usize) -> Vec<&[String]> {
108 let base = texts.len() / shares;
109 let extra = texts.len() % shares;
110 let mut cut = Vec::with_capacity(shares);
111 let mut rest = texts;
112 for index in 0..shares {
113 let size = base + usize::from(index < extra);
114 let (share, after) = rest.split_at(size.min(rest.len()));
115 cut.push(share);
116 rest = after;
117 }
118 cut
119}
120
121fn panicked() -> Failure {
122 Failure::new(
123 "embed_worker_owner_failed",
124 "a share's thread panicked before it answered",
125 )
126}
127
128#[cfg(test)]
129#[path = "workers_tests.rs"]
130mod tests;