1#![forbid(unsafe_code)]
12
13use std::fmt;
14use std::future::Future;
15use std::pin::Pin;
16use std::sync::atomic::{AtomicUsize, Ordering};
17use std::sync::{Arc, Mutex, PoisonError};
18use std::task::{Context, Poll, Waker};
19use std::time::{Duration, Instant};
20
21use kime_core::answer::{LAYA_MODEL, Response, Temperatures, laya_answer};
22use kime_core::render::{compat_question, compat_state};
23use kime_core::request::{Limits, Problem, Question, Request, parse};
24use kime_model::Model;
25use kime_tensor::{BatchBuf, Buckets, Executor, Outputs};
26use kime_tok::Tokenizer;
27use kime_tok::layout::{CompatBudget, CompatSequence, Cut};
28use serde_json::Value;
29
30mod cache;
31pub mod hub;
32mod split;
33
34use cache::{AnswerCache, Entry};
35pub use cache::{CacheMode, CacheStats};
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
39pub enum Device {
40 #[default]
42 Auto,
43 Cpu {
45 threads: usize,
47 },
48 Cuda(usize),
50 Metal,
52 Ane,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
58pub enum Precision {
59 #[default]
61 F16,
62 F32,
64 Int8,
68}
69
70#[derive(Debug)]
72pub enum Error {
73 Invalid(Vec<Problem>),
75 NotFound(String),
77 Model(kime_model::Error),
79 Tokenizer(String),
81 Backend(kime_tensor::Error),
83 TooLong {
86 question: String,
88 options: usize,
90 fit: usize,
92 },
93 Unsupported(String),
95}
96
97impl fmt::Display for Error {
98 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99 match self {
100 Error::Invalid(p) => {
101 write!(f, "invalid request:")?;
102 for p in p {
103 write!(f, " {};", p.to_json())?;
104 }
105 Ok(())
106 }
107 Error::NotFound(m) | Error::Tokenizer(m) | Error::Unsupported(m) => f.write_str(m),
108 Error::Model(e) => write!(f, "{e}"),
109 Error::Backend(e) => write!(f, "{e}"),
110 Error::TooLong { question, options, fit } => write!(
111 f,
112 "question {question:?} has {options} options but only {fit} fit the head budget"
113 ),
114 }
115 }
116}
117
118impl std::error::Error for Error {}
119
120impl From<kime_tensor::Error> for Error {
121 fn from(e: kime_tensor::Error) -> Self {
122 Error::Backend(e)
123 }
124}
125
126#[derive(Debug, Clone, Default)]
128pub struct Builder {
129 model: Option<String>,
130 device: Device,
131 precision: Precision,
132 preload: bool,
133 answer_cache: usize,
134}
135
136impl Builder {
137 #[must_use]
140 pub fn model(mut self, name: impl Into<String>) -> Self {
141 self.model = Some(name.into());
142 self
143 }
144
145 #[must_use]
147 pub fn device(mut self, d: Device) -> Self {
148 self.device = d;
149 self
150 }
151
152 #[must_use]
154 pub fn precision(mut self, p: Precision) -> Self {
155 self.precision = p;
156 self
157 }
158
159 #[must_use]
162 pub fn preload(mut self, yes: bool) -> Self {
163 self.preload = yes;
164 self
165 }
166
167 #[must_use]
170 pub fn answer_cache(mut self, entries: usize) -> Self {
171 self.answer_cache = entries;
172 self
173 }
174
175 pub fn build(self) -> Result<Kime, Error> {
181 let name = self.model.unwrap_or_else(|| "laya".into());
182 let path = hub::resolve(&name).map_err(Error::NotFound)?;
183 let model = Model::open(&path).map_err(Error::Model)?;
184 let tok_json = model
185 .file("tokenizer/tokenizer.json")
186 .ok_or_else(|| Error::Tokenizer(format!("{}: no tokenizer.json", path.display())))?;
187 let tok = Tokenizer::from_bytes(tok_json, model.file("tokenizer/tokenizer_config.json"))
188 .map_err(|e| Error::Tokenizer(e.to_string()))?;
189 let agent = &model.spec.agent;
190 let temps = Temperatures::new(agent.temperature, &agent.temperature_by_options);
191 let budget = CompatBudget { max_len: agent.max_len, head_max_len: agent.head_max_len };
192 let mut runner = Runner::open(&model, self.device, self.precision)?;
193 let embed = runner.add_graph(model.graph.embed_plan(&model.spec));
194 if self.preload {
195 for b in Buckets::default().stage("compat").iter().take(4) {
196 runner.prepare(*b)?;
197 }
198 }
199 let buckets = Buckets::default().stage("compat").to_vec();
200 let embed_buckets = Buckets::default().stage(EMBED_STAGE).to_vec();
201 let (weights, plans) = runner.memory();
202 Ok(Kime {
203 inner: Arc::new(Inner {
204 id: model.spec.id.clone(),
205 mask: tok.mask_text().to_string(),
206 tok,
207 budget,
208 temps,
209 buckets,
210 embed_buckets,
211 d: model.spec.encoder.d,
212 memory: [AtomicUsize::new(weights), AtomicUsize::new(plans)],
213 cache: (self.answer_cache > 0).then(|| AnswerCache::new(self.answer_cache)),
214 runner: Mutex::new(Session {
215 runner,
216 embed,
217 buf: BatchBuf::default(),
218 out: Outputs::default(),
219 }),
220 }),
221 })
222 }
223}
224
225enum Runner {
226 Cpu(Box<Executor<kime_cpu::CpuBackend>>),
227 #[cfg(feature = "cuda")]
228 Cuda(Box<Executor<kime_cuda::CudaBackend>>),
229 #[cfg(target_os = "macos")]
230 Metal(Box<Executor<kime_metal::MetalBackend>>),
231}
232
233impl Runner {
234 fn open(model: &Model, device: Device, precision: Precision) -> Result<Self, Error> {
235 let cpu = |threads: usize| {
236 let t = if threads == 0 { kime_cpu::par::available() } else { threads };
237 let backend = kime_cpu::CpuBackend::new(t).with_int8(precision == Precision::Int8);
238 Ok(Runner::Cpu(Box::new(kime_cpu::executor_with(model, backend)?)))
239 };
240 match device {
241 Device::Cpu { threads } => cpu(threads),
242 #[cfg(feature = "cuda")]
243 Device::Cuda(n) => {
244 Ok(Runner::Cuda(Box::new(kime_cuda::executor(model, n, cuda(precision)?)?)))
245 }
246 Device::Auto => Runner::auto(model, precision).map_or_else(|| cpu(0), Ok),
247 #[cfg(not(feature = "cuda"))]
248 Device::Cuda(_) => Err(Error::Unsupported("this build has no CUDA backend".into())),
249 #[cfg(target_os = "macos")]
250 Device::Metal => {
251 Ok(Runner::Metal(Box::new(kime_metal::executor(model, metal(precision)?)?)))
252 }
253 #[cfg(not(target_os = "macos"))]
254 Device::Metal => Err(Error::Unsupported("Metal needs macOS".into())),
255 Device::Ane => {
256 Err(Error::Unsupported("the Neural Engine backend arrives with M4".into()))
257 }
258 }
259 }
260
261 #[allow(unused_variables)]
264 fn auto(model: &Model, precision: Precision) -> Option<Self> {
265 #[cfg(feature = "cuda")]
266 if let Ok(e) = cuda(precision).and_then(|p| Ok(kime_cuda::executor(model, 0, p)?)) {
267 return Some(Runner::Cuda(Box::new(e)));
268 }
269 #[cfg(target_os = "macos")]
270 if let Ok(e) = metal(precision).and_then(|p| Ok(kime_metal::executor(model, p)?)) {
271 return Some(Runner::Metal(Box::new(e)));
272 }
273 None
274 }
275
276 fn add_graph(&mut self, g: kime_tensor::Graph) -> usize {
277 let b = Buckets::default();
278 match self {
279 Runner::Cpu(e) => e.add_graph(g, &b, EMBED_STAGE),
280 #[cfg(feature = "cuda")]
281 Runner::Cuda(e) => e.add_graph(g, &b, EMBED_STAGE),
282 #[cfg(target_os = "macos")]
283 Runner::Metal(e) => e.add_graph(g, &b, EMBED_STAGE),
284 }
285 }
286
287 fn prepare(&mut self, b: kime_tensor::Bucket) -> Result<(), Error> {
288 match self {
289 Runner::Cpu(e) => e.prepare(b)?,
290 #[cfg(feature = "cuda")]
291 Runner::Cuda(e) => e.prepare(b)?,
292 #[cfg(target_os = "macos")]
293 Runner::Metal(e) => e.prepare(b)?,
294 };
295 Ok(())
296 }
297
298 fn run(&mut self, buf: &BatchBuf, out: &mut Outputs) -> Result<(), Error> {
299 self.run_lane(0, buf, out)
300 }
301
302 fn run_lane(&mut self, lane: usize, buf: &BatchBuf, out: &mut Outputs) -> Result<(), Error> {
303 match self {
304 Runner::Cpu(e) => e.run_lane(lane, &buf.batch(), out)?,
305 #[cfg(feature = "cuda")]
306 Runner::Cuda(e) => e.run_lane(lane, &buf.batch(), out)?,
307 #[cfg(target_os = "macos")]
308 Runner::Metal(e) => e.run_lane(lane, &buf.batch(), out)?,
309 };
310 Ok(())
311 }
312
313 fn memory(&self) -> (usize, usize) {
314 match self {
315 Runner::Cpu(e) => e.memory(),
316 #[cfg(feature = "cuda")]
317 Runner::Cuda(e) => e.memory(),
318 #[cfg(target_os = "macos")]
319 Runner::Metal(e) => e.memory(),
320 }
321 }
322
323 fn int8(&self) -> bool {
324 match self {
325 Runner::Cpu(e) => e.backend().int8(),
326 #[cfg(feature = "cuda")]
327 Runner::Cuda(_) => false,
328 #[cfg(target_os = "macos")]
329 Runner::Metal(_) => false,
330 }
331 }
332
333 fn describe(&self) -> String {
334 match self {
335 Runner::Cpu(e) => {
336 let int8 = if e.backend().int8() { ", int8" } else { "" };
337 format!("cpu, {} threads{int8}", kime_tensor::Backend::caps(e.backend()).threads)
338 }
339 #[cfg(feature = "cuda")]
340 Runner::Cuda(e) => format!("cuda, {}", e.backend().name()),
341 #[cfg(target_os = "macos")]
342 Runner::Metal(e) => format!("metal, {}", e.backend().name()),
343 }
344 }
345}
346
347#[cfg(target_os = "macos")]
348fn metal(p: Precision) -> Result<kime_metal::Precision, Error> {
349 match p {
350 Precision::F16 => Ok(kime_metal::Precision::F16),
351 Precision::F32 => Ok(kime_metal::Precision::F32),
352 Precision::Int8 => Err(Error::Unsupported("INT8 runs on the CPU only for now".into())),
353 }
354}
355
356#[cfg(feature = "cuda")]
357fn cuda(p: Precision) -> Result<kime_cuda::Precision, Error> {
358 match p {
359 Precision::F16 => Ok(kime_cuda::Precision::F16),
360 Precision::F32 => Ok(kime_cuda::Precision::F32),
361 Precision::Int8 => Err(Error::Unsupported("INT8 runs on the CPU only for now".into())),
362 }
363}
364
365const EMBED_STAGE: &str = "state";
367
368struct Session {
369 runner: Runner,
370 embed: usize,
372 buf: BatchBuf,
373 out: Outputs,
374}
375
376struct Inner {
377 id: String,
378 tok: Tokenizer,
379 mask: String,
380 budget: CompatBudget,
381 temps: Temperatures,
382 buckets: Vec<kime_tensor::Bucket>,
384 embed_buckets: Vec<kime_tensor::Bucket>,
386 d: usize,
388 runner: Mutex<Session>,
389 memory: [AtomicUsize; 2],
391 cache: Option<AnswerCache>,
392}
393
394#[derive(Clone)]
396pub struct Kime {
397 inner: Arc<Inner>,
398}
399
400impl fmt::Debug for Kime {
401 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
402 f.debug_struct("Kime").field("model", &self.inner.id).finish_non_exhaustive()
403 }
404}
405
406#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
408pub struct Timing {
409 pub tokenize: Duration,
411 pub device: Duration,
413 pub batches: usize,
415 pub truncated: usize,
417 pub cut_tokens: usize,
419 pub cached: usize,
421}
422
423#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
425pub struct Memory {
426 pub weights: usize,
428 pub plans: usize,
430}
431
432struct Item<'a> {
434 req: usize,
435 q: &'a Question,
436 seq: CompatSequence,
437 logits: Vec<f32>,
438 act: [f32; 2],
439}
440
441impl Item<'_> {
442 fn key(&self) -> cache::Key {
443 AnswerCache::key(&self.seq, self.q.qtype.index() as u8)
444 }
445
446 fn fill(&mut self, e: Entry) {
447 self.logits = e.logits.into_vec();
448 self.act = e.act;
449 }
450}
451
452fn mode(req: &Request) -> CacheMode {
454 req.kime
455 .as_ref()
456 .and_then(|k| k.get("cache"))
457 .and_then(Value::as_str)
458 .and_then(CacheMode::parse)
459 .unwrap_or_default()
460}
461
462impl Kime {
463 #[must_use]
465 pub fn builder() -> Builder {
466 Builder::default()
467 }
468
469 #[must_use]
471 pub fn model_id(&self) -> &str {
472 &self.inner.id
473 }
474
475 #[must_use]
477 pub fn device(&self) -> String {
478 self.lock().runner.describe()
479 }
480
481 #[must_use]
484 pub fn max_row_tokens(&self) -> usize {
485 self.inner.budget.max_len
486 }
487
488 #[must_use]
490 pub fn memory(&self) -> Memory {
491 let m = &self.inner.memory;
492 Memory { weights: m[0].load(Ordering::Relaxed), plans: m[1].load(Ordering::Relaxed) }
493 }
494
495 #[must_use]
497 pub fn cache_stats(&self) -> CacheStats {
498 self.inner.cache.as_ref().map(AnswerCache::stats).unwrap_or_default()
499 }
500
501 #[must_use]
505 pub fn cached(&self, req: &Request) -> Option<Response> {
506 let cache = self.inner.cache.as_ref()?;
507 if mode(req) != CacheMode::Use || req.questions.is_empty() {
508 return None;
509 }
510 let parsed = [parse(&req.to_json(), &Limits::LAYA).ok()?];
511 let mut items = self.lay_out(&parsed).ok()?;
512 let keys: Vec<_> = items.iter().map(Item::key).collect();
513 for (it, e) in items.iter_mut().zip(cache.all(&keys)?) {
514 it.fill(e);
515 }
516 self.respond(&parsed, &items).pop()
517 }
518
519 fn lock(&self) -> std::sync::MutexGuard<'_, Session> {
520 self.inner.runner.lock().unwrap_or_else(PoisonError::into_inner)
521 }
522
523 #[must_use]
527 pub fn count_tokens(&self, req: &Request) -> usize {
528 let inner = &*self.inner;
529 let mut n = inner.tok.encode(&compat_state(&req.state, &inner.mask)).len();
530 for q in &req.questions {
531 let text = compat_question(q, &inner.mask);
532 n += inner.tok.encode(&text.head).len();
533 n += text.options.iter().map(|o| inner.tok.encode(o).len()).sum::<usize>();
534 }
535 n
536 }
537
538 pub fn decide(&self, req: &Request) -> Result<Response, Error> {
545 Ok(self.decide_batch(std::slice::from_ref(req))?.remove(0))
546 }
547
548 pub fn decide_batch(&self, reqs: &[Request]) -> Result<Vec<Response>, Error> {
555 Ok(self.decide_batch_timed(reqs)?.0)
556 }
557
558 pub fn decide_batch_timed(&self, reqs: &[Request]) -> Result<(Vec<Response>, Timing), Error> {
564 let t0 = Instant::now();
565 let mut parsed = Vec::with_capacity(reqs.len());
566 for r in reqs {
567 parsed.push(parse(&r.to_json(), &Limits::LAYA).map_err(Error::Invalid)?);
568 }
569 let mut items = self.lay_out(&parsed)?;
570 let tokenize = t0.elapsed();
571 let t1 = Instant::now();
572 let (run, keys) = self.look_up(reqs, &mut items);
573 let batches = if run.is_empty() { 0 } else { self.run(&mut items, &run)? };
574 if let Some(c) = &self.inner.cache {
575 let keep = run.iter().filter(|&&i| mode(&reqs[items[i].req]) != CacheMode::Bypass);
576 c.insert(keep.map(|&i| {
577 (keys[i], Entry { logits: items[i].logits.clone().into(), act: items[i].act })
578 }));
579 }
580 let cached = items.len() - run.len();
581 let cut = items.iter().map(|it| it.seq.state_tokens - it.seq.state_tokens_used);
582 let timing = Timing {
583 tokenize,
584 device: t1.elapsed(),
585 batches,
586 truncated: cut.clone().filter(|&n| n > 0).count(),
587 cut_tokens: cut.sum(),
588 cached,
589 };
590 Ok((self.respond(&parsed, &items), timing))
591 }
592
593 fn lay_out<'a>(&self, parsed: &'a [Request]) -> Result<Vec<Item<'a>>, Error> {
595 let inner = &*self.inner;
596 let mut items = Vec::new();
597 for (i, r) in parsed.iter().enumerate() {
598 if r.questions.is_empty() {
599 continue;
600 }
601 let cut = if matches!(r.state, Value::Array(_)) { Cut::Head } else { Cut::Tail };
603 let state = inner.tok.encode_state(&compat_state(&r.state, &inner.mask));
604 for q in &r.questions {
605 let text = compat_question(q, &inner.mask);
606 let seq =
607 inner.tok.compat_sequence(&text.head, &text.options, &state, inner.budget, cut);
608 if seq.markers.len() != q.criteria.len() {
609 return Err(Error::TooLong {
610 question: q.id.clone(),
611 options: q.criteria.len(),
612 fit: seq.markers.len(),
613 });
614 }
615 items.push(Item { req: i, q, seq, logits: Vec::new(), act: [0.0; 2] });
616 }
617 }
618 Ok(items)
619 }
620
621 fn look_up(&self, reqs: &[Request], items: &mut [Item<'_>]) -> (Vec<usize>, Vec<cache::Key>) {
624 let Some(c) = &self.inner.cache else { return ((0..items.len()).collect(), Vec::new()) };
625 let keys: Vec<_> = items.iter().map(Item::key).collect();
626 let look: Vec<usize> =
627 (0..items.len()).filter(|&i| mode(&reqs[items[i].req]) == CacheMode::Use).collect();
628 let found = c.get(&look.iter().map(|&i| keys[i]).collect::<Vec<_>>());
629 let mut hit = vec![false; items.len()];
630 for (&i, e) in look.iter().zip(found) {
631 if let Some(e) = e {
632 items[i].fill(e);
633 hit[i] = true;
634 }
635 }
636 ((0..items.len()).filter(|&i| !hit[i]).collect(), keys)
637 }
638
639 fn respond(&self, parsed: &[Request], items: &[Item<'_>]) -> Vec<Response> {
641 let inner = &*self.inner;
642 let mut out: Vec<Response> = parsed
643 .iter()
644 .map(|_| Response { model: LAYA_MODEL.into(), answers: Vec::new(), input_tokens: 0 })
645 .collect();
646 for it in items {
647 let res = &mut out[it.req];
648 res.input_tokens += it.seq.ids.len();
649 res.answers
650 .push((it.q.id.clone(), laya_answer(it.q, &it.logits, it.act, &inner.temps)));
651 }
652 out
653 }
654
655 fn run(&self, items: &mut [Item<'_>], which: &[usize]) -> Result<usize, Error> {
658 let sizes: Vec<(usize, usize)> =
659 which.iter().map(|&i| (items[i].seq.ids.len(), items[i].seq.markers.len())).collect();
660 let batches: Vec<Vec<usize>> = split::split(&self.inner.buckets, &sizes)
661 .into_iter()
662 .map(|b| b.into_iter().map(|j| which[j]).collect())
663 .collect();
664 let mut s = self.lock();
665 let Session { runner, buf, out, .. } = &mut *s;
666 for batch in &batches {
667 buf.clear();
668 for &i in batch {
669 let it = &items[i];
670 buf.push(&it.seq.ids, &it.seq.markers, it.q.qtype.index() as u8);
671 }
672 runner.run(buf, out)?;
673 let mut at = 0;
674 for (&i, a) in batch.iter().zip(&out.act) {
675 let it = &mut items[i];
676 let k = it.seq.markers.len();
677 it.logits = out.logits[at..at + k].to_vec();
678 it.act = *a;
679 at += k;
680 }
681 }
682 let (weights, plans) = runner.memory();
683 self.inner.memory[0].store(weights, Ordering::Relaxed);
684 self.inner.memory[1].store(plans, Ordering::Relaxed);
685 Ok(batches.len())
686 }
687
688 pub fn embed(&self, texts: &[&str], max_length: usize) -> Result<Vec<Vec<f32>>, Error> {
697 let inner = &*self.inner;
698 if self.lock().runner.int8() {
699 return Err(Error::Unsupported("embeddings need FP32 or FP16, not INT8".into()));
701 }
702 let sp = inner.tok.specials();
703 let keep = max_length.saturating_sub(2);
704 let seqs: Vec<Vec<u32>> = texts
705 .iter()
706 .map(|t| {
707 let mut ids = Vec::with_capacity(keep.min(t.len()) + 2);
708 ids.push(sp.cls);
709 inner.tok.encode_into(t, &mut ids);
710 ids.truncate(keep + 1);
711 ids.push(sp.sep);
712 ids
713 })
714 .collect();
715 let sizes: Vec<(usize, usize)> = seqs.iter().map(|s| (s.len(), 0)).collect();
716 let batches = split::split(&inner.embed_buckets, &sizes);
717 let mut rows = vec![Vec::new(); texts.len()];
718 let mut s = self.lock();
719 let Session { runner, embed, buf, out } = &mut *s;
720 for batch in &batches {
721 buf.clear();
722 for &i in batch {
723 buf.push(&seqs[i], &[], 0);
724 }
725 runner.run_lane(*embed, buf, out)?;
726 for (&i, row) in batch.iter().zip(out.pooled.chunks_exact(inner.d)) {
727 rows[i] = row.to_vec();
728 }
729 }
730 let (weights, plans) = runner.memory();
731 inner.memory[0].store(weights, Ordering::Relaxed);
732 inner.memory[1].store(plans, Ordering::Relaxed);
733 Ok(rows)
734 }
735
736 #[must_use]
739 pub fn decide_async(&self, req: &Request) -> Decision {
740 let shared = Arc::new(Mutex::new((None, None::<Waker>)));
741 let (kime, req, done) = (self.clone(), req.clone(), shared.clone());
742 std::thread::spawn(move || {
743 let r = kime.decide(&req);
744 let mut g = done.lock().unwrap_or_else(PoisonError::into_inner);
745 g.0 = Some(r);
746 if let Some(w) = g.1.take() {
747 w.wake();
748 }
749 });
750 Decision { shared }
751 }
752}
753
754#[derive(Debug)]
756pub struct Decision {
757 #[allow(clippy::type_complexity)]
758 shared: Arc<Mutex<(Option<Result<Response, Error>>, Option<Waker>)>>,
759}
760
761impl Future for Decision {
762 type Output = Result<Response, Error>;
763
764 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
765 let mut g = self.shared.lock().unwrap_or_else(PoisonError::into_inner);
766 match g.0.take() {
767 Some(r) => Poll::Ready(r),
768 None => {
769 g.1 = Some(cx.waker().clone());
770 Poll::Pending
771 }
772 }
773 }
774}