1use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
9use std::sync::Arc;
10use std::time::Instant;
11
12use oxibonsai_core::config::Qwen3Config;
13use oxibonsai_core::gguf::reader::GgufFile;
14use oxibonsai_kernels::traits::OneBitKernel;
15use oxibonsai_kernels::{KernelDispatcher, KernelTier};
16use oxibonsai_model::model::BonsaiModel;
17
18use crate::batch_engine::{self, BatchResult};
19use crate::error::{RuntimeError, RuntimeResult};
20use crate::metrics::InferenceMetrics;
21#[cfg(all(feature = "metal", target_os = "macos"))]
22use crate::ngram_cache::NgramCache;
23use crate::request_id::RequestId;
24use crate::request_metrics::{RequestRateAggregator, RequestRateSnapshot, RequestRateTracker};
25use crate::sampling::{Sampler, SamplingParams};
26
27pub const EOS_TOKEN_ID: u32 = 151645;
29
30#[derive(Debug)]
32pub struct EngineStats {
33 pub total_tokens_generated: AtomicU64,
35 pub total_requests: AtomicU64,
37 pub active_sessions: AtomicUsize,
39 pub start_time: Instant,
41}
42
43impl EngineStats {
44 pub fn new() -> Self {
46 Self {
47 total_tokens_generated: AtomicU64::new(0),
48 total_requests: AtomicU64::new(0),
49 active_sessions: AtomicUsize::new(0),
50 start_time: Instant::now(),
51 }
52 }
53
54 pub fn uptime_seconds(&self) -> f64 {
56 self.start_time.elapsed().as_secs_f64()
57 }
58
59 pub fn record_request(&self, tokens_generated: usize) {
61 self.total_tokens_generated
62 .fetch_add(tokens_generated as u64, Ordering::Relaxed);
63 self.total_requests.fetch_add(1, Ordering::Relaxed);
64 }
65
66 pub fn tokens_generated(&self) -> u64 {
68 self.total_tokens_generated.load(Ordering::Relaxed)
69 }
70
71 pub fn requests_completed(&self) -> u64 {
73 self.total_requests.load(Ordering::Relaxed)
74 }
75
76 pub fn active_session_count(&self) -> usize {
78 self.active_sessions.load(Ordering::Relaxed)
79 }
80
81 pub fn avg_tokens_per_request(&self) -> f64 {
83 let reqs = self.requests_completed();
84 if reqs == 0 {
85 return 0.0;
86 }
87 self.tokens_generated() as f64 / reqs as f64
88 }
89}
90
91impl Default for EngineStats {
92 fn default() -> Self {
93 Self::new()
94 }
95}
96
97pub struct InferenceEngine<'a> {
99 model: BonsaiModel<'a>,
100 kernel: KernelDispatcher,
101 sampler: Sampler,
102 metrics: Option<Arc<InferenceMetrics>>,
103 stats: Arc<EngineStats>,
104 prefill_token_count: u64,
112 rate_aggregator: Option<Arc<RequestRateAggregator>>,
118}
119
120impl<'a> InferenceEngine<'a> {
121 pub fn new(config: Qwen3Config, sampling_params: SamplingParams, seed: u64) -> Self {
123 let model = BonsaiModel::new(config);
124 let kernel = KernelDispatcher::auto_detect();
125 let sampler = Sampler::new(sampling_params, seed);
126
127 tracing::info!(kernel = kernel.name(), "inference engine initialized");
128
129 Self {
130 model,
131 kernel,
132 sampler,
133 metrics: None,
134 stats: Arc::new(EngineStats::new()),
135 prefill_token_count: 0,
136 rate_aggregator: None,
137 }
138 }
139
140 pub fn from_model(model: BonsaiModel<'a>, sampling_params: SamplingParams, seed: u64) -> Self {
146 Self::from_model_with_kernel(
147 model,
148 KernelDispatcher::auto_detect(),
149 sampling_params,
150 seed,
151 )
152 }
153
154 pub fn from_model_with_kernel(
161 model: BonsaiModel<'a>,
162 kernel: KernelDispatcher,
163 sampling_params: SamplingParams,
164 seed: u64,
165 ) -> Self {
166 let sampler = Sampler::new(sampling_params, seed);
167 Self {
168 model,
169 kernel,
170 sampler,
171 metrics: None,
172 stats: Arc::new(EngineStats::new()),
173 prefill_token_count: 0,
174 rate_aggregator: None,
175 }
176 }
177
178 pub fn from_model_with_tier(
186 model: BonsaiModel<'a>,
187 tier: KernelTier,
188 sampling_params: SamplingParams,
189 seed: u64,
190 ) -> Self {
191 Self::from_model_with_kernel(
192 model,
193 KernelDispatcher::with_tier(tier),
194 sampling_params,
195 seed,
196 )
197 }
198
199 pub fn from_gguf(
201 gguf: &'a GgufFile<'a>,
202 sampling_params: SamplingParams,
203 seed: u64,
204 max_seq_len: usize,
205 ) -> RuntimeResult<Self> {
206 let model = BonsaiModel::from_gguf(gguf, max_seq_len)?;
207 Self::from_model_with_gpu_warmup(model, sampling_params, seed)
208 }
209
210 pub fn from_gguf_with_embd(
222 gguf: &'a GgufFile<'a>,
223 sampling_params: SamplingParams,
224 seed: u64,
225 max_seq_len: usize,
226 token_embd: std::sync::Arc<[f32]>,
227 ) -> RuntimeResult<Self> {
228 let model = BonsaiModel::from_gguf_with_embd(gguf, max_seq_len, token_embd)?;
229 Self::from_model_with_gpu_warmup(model, sampling_params, seed)
230 }
231
232 fn from_model_with_gpu_warmup(
240 mut model: BonsaiModel<'a>,
241 sampling_params: SamplingParams,
242 seed: u64,
243 ) -> RuntimeResult<Self> {
244 let kernel = KernelDispatcher::auto_detect();
245
246 model.upload_weights_to_gpu(&kernel);
248
249 #[cfg(all(feature = "metal", target_os = "macos"))]
251 {
252 tracing::info!("pre-building GPU weight cache");
253 model.get_or_create_gpu_cache().map_err(|e| {
254 RuntimeError::Model(oxibonsai_model::error::ModelError::Internal(format!(
255 "GPU weight cache init: {e}"
256 )))
257 })?;
258 }
259
260 #[cfg(all(
275 feature = "native-cuda",
276 not(all(feature = "metal", target_os = "macos")),
277 any(target_os = "linux", target_os = "windows")
278 ))]
279 {
280 tracing::info!("CUDA warmup: pre-capturing driver graph + prefill modules");
281 let _ = model.forward(0, 0, &kernel);
283 let _ = model.forward_prefill(&[0u32; 17], 0, &kernel);
291 tracing::info!("CUDA warmup complete");
292 }
293
294 let sampler = Sampler::new(sampling_params, seed);
295
296 tracing::info!(kernel = kernel.name(), "inference engine loaded from GGUF");
297
298 Ok(Self {
299 model,
300 kernel,
301 sampler,
302 metrics: None,
303 stats: Arc::new(EngineStats::new()),
304 prefill_token_count: 0,
305 rate_aggregator: None,
306 })
307 }
308
309 pub fn set_metrics(&mut self, metrics: Arc<InferenceMetrics>) {
311 self.metrics = Some(metrics);
312 }
313
314 pub fn set_rate_aggregator(&mut self, aggregator: Arc<RequestRateAggregator>) {
322 self.rate_aggregator = Some(aggregator);
323 }
324
325 pub fn rate_aggregator(&self) -> Option<&Arc<RequestRateAggregator>> {
327 self.rate_aggregator.as_ref()
328 }
329
330 pub fn model(&self) -> &BonsaiModel<'a> {
332 &self.model
333 }
334
335 pub fn model_token_embd(&self) -> std::sync::Arc<[f32]> {
343 self.model.shared_token_embd()
344 }
345
346 pub fn model_mut(&mut self) -> &mut BonsaiModel<'a> {
351 &mut self.model
352 }
353
354 pub fn kernel(&self) -> &KernelDispatcher {
356 &self.kernel
357 }
358
359 pub fn kernel_tier(&self) -> KernelTier {
365 self.kernel.tier()
366 }
367
368 pub fn prefill_from_pos(
377 &mut self,
378 prompt_tokens: &[u32],
379 pos_start: usize,
380 ) -> RuntimeResult<Vec<f32>> {
381 let logits = self
382 .model
383 .forward_prefill(prompt_tokens, pos_start, &self.kernel)?;
384 self.prefill_token_count = self
385 .prefill_token_count
386 .saturating_add(prompt_tokens.len() as u64);
387 Ok(logits)
388 }
389
390 pub fn decode_step(&mut self, token: u32, pos: usize) -> RuntimeResult<Vec<f32>> {
392 Ok(self.model.forward(token, pos, &self.kernel)?)
393 }
394
395 pub fn sample(&mut self, logits: &[f32]) -> RuntimeResult<u32> {
397 self.sampler.sample(logits)
398 }
399
400 pub fn prefill_token_count(&self) -> u64 {
403 self.prefill_token_count
404 }
405
406 pub fn reset(&mut self) {
408 self.model.reset();
409 }
410
411 pub fn stats(&self) -> &Arc<EngineStats> {
413 &self.stats
414 }
415
416 pub fn active_sessions(&self) -> usize {
418 self.stats.active_session_count()
419 }
420
421 pub fn session_count(&self) -> u64 {
423 self.stats.requests_completed()
424 }
425
426 pub fn batch_generate(
430 &mut self,
431 prompts: &[Vec<u32>],
432 max_tokens: usize,
433 ) -> Vec<RuntimeResult<BatchResult>> {
434 self.stats.active_sessions.fetch_add(1, Ordering::Relaxed);
435
436 let results = batch_engine::batch_generate(self, prompts, max_tokens);
437
438 for br in results.iter().flatten() {
440 self.stats.record_request(br.generated_tokens.len());
441 }
442
443 self.stats.active_sessions.fetch_sub(1, Ordering::Relaxed);
444
445 results
446 }
447
448 #[tracing::instrument(skip(self, prompt_tokens), fields(prompt_len = prompt_tokens.len()))]
454 pub fn generate(
455 &mut self,
456 prompt_tokens: &[u32],
457 max_tokens: usize,
458 ) -> RuntimeResult<Vec<u32>> {
459 if prompt_tokens.is_empty() {
460 return Ok(vec![]);
461 }
462
463 let prefill_start = std::time::Instant::now();
467 let mut last_logits = self.model.forward_prefill(prompt_tokens, 0, &self.kernel)?;
468 if let Some(m) = &self.metrics {
469 m.prefill_duration_seconds
470 .observe(prefill_start.elapsed().as_secs_f64());
471 }
472
473 let decode_start = std::time::Instant::now();
477 let mut output_tokens = Vec::with_capacity(max_tokens);
478
479 for (pos, _) in (prompt_tokens.len()..).zip(0..max_tokens) {
480 let step_start = std::time::Instant::now();
481
482 let next_token = self.sampler.sample(&last_logits)?;
484
485 if next_token == EOS_TOKEN_ID {
487 tracing::debug!(pos, "EOS token generated");
488 break;
489 }
490
491 output_tokens.push(next_token);
492
493 last_logits = self.model.forward(next_token, pos, &self.kernel)?;
495
496 if let Some(m) = &self.metrics {
497 m.decode_token_duration_seconds
498 .observe(step_start.elapsed().as_secs_f64());
499 }
500 }
501
502 if let Some(m) = &self.metrics {
504 let decode_elapsed = decode_start.elapsed().as_secs_f64();
505 if decode_elapsed > 0.0 && !output_tokens.is_empty() {
506 let tok_per_sec = output_tokens.len() as f64 / decode_elapsed;
507 m.tokens_per_second.observe(tok_per_sec);
508 }
509 m.tokens_generated_total.inc_by(output_tokens.len() as u64);
510 m.update_memory_from_rss();
511 }
512
513 self.stats.record_request(output_tokens.len());
515
516 tracing::info!(
517 prompt_len = prompt_tokens.len(),
518 generated = output_tokens.len(),
519 "generation complete"
520 );
521
522 Ok(output_tokens)
523 }
524
525 #[tracing::instrument(skip(self, prompt_tokens, tracker), fields(prompt_len = prompt_tokens.len()))]
537 pub fn generate_tracked(
538 &mut self,
539 prompt_tokens: &[u32],
540 max_tokens: usize,
541 tracker: &mut RequestRateTracker,
542 ) -> RuntimeResult<Vec<u32>> {
543 if prompt_tokens.is_empty() {
544 return Ok(vec![]);
545 }
546 tracker.record_admission();
547
548 let prefill_start = std::time::Instant::now();
549 let mut last_logits = self.model.forward_prefill(prompt_tokens, 0, &self.kernel)?;
550 if let Some(m) = &self.metrics {
551 m.prefill_duration_seconds
552 .observe(prefill_start.elapsed().as_secs_f64());
553 }
554
555 let decode_start = std::time::Instant::now();
556 let mut output_tokens = Vec::with_capacity(max_tokens);
557 let mut first_token_recorded = false;
558
559 for (pos, _) in (prompt_tokens.len()..).zip(0..max_tokens) {
560 let step_start = std::time::Instant::now();
561 let next_token = self.sampler.sample(&last_logits)?;
562 if next_token == EOS_TOKEN_ID {
563 tracing::debug!(pos, "EOS token generated");
564 break;
565 }
566 output_tokens.push(next_token);
567 if !first_token_recorded {
568 tracker.record_first_token();
569 first_token_recorded = true;
570 } else {
571 tracker.record_token();
572 }
573 last_logits = self.model.forward(next_token, pos, &self.kernel)?;
574
575 if let Some(m) = &self.metrics {
576 m.decode_token_duration_seconds
577 .observe(step_start.elapsed().as_secs_f64());
578 }
579 }
580
581 if let Some(m) = &self.metrics {
582 let decode_elapsed = decode_start.elapsed().as_secs_f64();
583 if decode_elapsed > 0.0 && !output_tokens.is_empty() {
584 let tok_per_sec = output_tokens.len() as f64 / decode_elapsed;
585 m.tokens_per_second.observe(tok_per_sec);
586 }
587 m.tokens_generated_total.inc_by(output_tokens.len() as u64);
588 m.update_memory_from_rss();
589 }
590 self.stats.record_request(output_tokens.len());
591
592 if let Some(agg) = &self.rate_aggregator {
593 let snap: RequestRateSnapshot = tracker.snapshot();
594 agg.record(snap);
595 }
596
597 tracing::info!(
598 prompt_len = prompt_tokens.len(),
599 generated = output_tokens.len(),
600 "tracked generation complete"
601 );
602
603 Ok(output_tokens)
604 }
605
606 pub fn generate_with_request_id(
614 &mut self,
615 request_id: RequestId,
616 prompt_tokens: &[u32],
617 max_tokens: usize,
618 ) -> RuntimeResult<(Vec<u32>, RequestRateTracker)> {
619 let span = tracing::info_span!("generate_request", request_id = %request_id);
620 let _enter = span.enter();
621 let mut tracker = RequestRateTracker::new();
622 let tokens = self.generate_tracked(prompt_tokens, max_tokens, &mut tracker)?;
623 Ok((tokens, tracker))
624 }
625
626 pub fn generate_with_seed(
632 &mut self,
633 prompt_tokens: &[u32],
634 max_tokens: usize,
635 seed: u64,
636 params: &crate::sampling::SamplingParams,
637 ) -> RuntimeResult<Vec<u32>> {
638 let old_sampler = std::mem::replace(
640 &mut self.sampler,
641 crate::sampling::Sampler::new(params.clone(), seed),
642 );
643 let result = self.generate(prompt_tokens, max_tokens);
644 self.sampler = old_sampler;
646 result
647 }
648
649 pub fn generate_with_params(
660 &mut self,
661 prompt_tokens: &[u32],
662 max_tokens: usize,
663 params: &crate::sampling::SamplingParams,
664 ) -> RuntimeResult<Vec<u32>> {
665 let prev_params = self.sampler.params().clone();
666 self.sampler.set_params(params.clone());
667 let result = self.generate(prompt_tokens, max_tokens);
668 self.sampler.set_params(prev_params);
669 result
670 }
671
672 #[cfg(not(target_arch = "wasm32"))]
677 #[tracing::instrument(skip(self, prompt_tokens, tx), fields(prompt_len = prompt_tokens.len()))]
678 pub fn generate_streaming(
679 &mut self,
680 prompt_tokens: &[u32],
681 max_tokens: usize,
682 tx: &tokio::sync::mpsc::UnboundedSender<u32>,
683 ) -> RuntimeResult<usize> {
684 if prompt_tokens.is_empty() {
685 return Ok(0);
686 }
687
688 let prefill_start = std::time::Instant::now();
690 let mut logits = self.model.forward_prefill(prompt_tokens, 0, &self.kernel)?;
691 if let Some(m) = &self.metrics {
692 m.prefill_duration_seconds
693 .observe(prefill_start.elapsed().as_secs_f64());
694 }
695
696 let decode_start = std::time::Instant::now();
697 let mut generated = 0;
698
699 for (pos, _) in (prompt_tokens.len()..).zip(0..max_tokens) {
700 let step_start = std::time::Instant::now();
701 let next_token = self.sampler.sample(&logits)?;
702
703 if next_token == EOS_TOKEN_ID {
704 tracing::debug!(pos, "EOS token generated (streaming)");
705 break;
706 }
707
708 if tx.send(next_token).is_err() {
710 tracing::debug!(pos, "receiver dropped, stopping generation");
711 break;
712 }
713
714 logits = self.model.forward(next_token, pos, &self.kernel)?;
715 generated += 1;
716
717 if let Some(m) = &self.metrics {
718 m.decode_token_duration_seconds
719 .observe(step_start.elapsed().as_secs_f64());
720 }
721 }
722
723 if let Some(m) = &self.metrics {
725 let decode_elapsed = decode_start.elapsed().as_secs_f64();
726 if decode_elapsed > 0.0 && generated > 0 {
727 let tok_per_sec = generated as f64 / decode_elapsed;
728 m.tokens_per_second.observe(tok_per_sec);
729 }
730 m.tokens_generated_total.inc_by(generated as u64);
731 m.update_memory_from_rss();
732 }
733
734 tracing::info!(
735 prompt_len = prompt_tokens.len(),
736 generated,
737 "streaming generation complete"
738 );
739
740 Ok(generated)
741 }
742
743 #[cfg(not(target_arch = "wasm32"))]
753 pub fn generate_streaming_with_params(
754 &mut self,
755 prompt_tokens: &[u32],
756 max_tokens: usize,
757 params: &crate::sampling::SamplingParams,
758 tx: &tokio::sync::mpsc::UnboundedSender<u32>,
759 ) -> RuntimeResult<usize> {
760 let prev_params = self.sampler.params().clone();
761 self.sampler.set_params(params.clone());
762 let result = self.generate_streaming(prompt_tokens, max_tokens, tx);
763 self.sampler.set_params(prev_params);
764 result
765 }
766
767 #[tracing::instrument(skip(self, prompt_tokens, tx), fields(prompt_len = prompt_tokens.len()))]
772 pub fn generate_streaming_sync(
773 &mut self,
774 prompt_tokens: &[u32],
775 max_tokens: usize,
776 tx: &std::sync::mpsc::Sender<u32>,
777 ) -> RuntimeResult<usize> {
778 if prompt_tokens.is_empty() {
779 return Ok(0);
780 }
781
782 let prefill_start = std::time::Instant::now();
784 let mut logits = self.model.forward_prefill(prompt_tokens, 0, &self.kernel)?;
785 if let Some(m) = &self.metrics {
786 m.prefill_duration_seconds
787 .observe(prefill_start.elapsed().as_secs_f64());
788 }
789
790 let decode_start = std::time::Instant::now();
791 let mut generated = 0;
792
793 for (pos, _) in (prompt_tokens.len()..).zip(0..max_tokens) {
794 let step_start = std::time::Instant::now();
795
796 let next_token = self.sampler.sample(&logits)?;
797
798 if next_token == EOS_TOKEN_ID {
799 tracing::debug!(pos, "EOS token generated (streaming_sync)");
800 break;
801 }
802
803 if tx.send(next_token).is_err() {
804 tracing::debug!(pos, "receiver dropped, stopping generation");
805 break;
806 }
807
808 logits = self.model.forward(next_token, pos, &self.kernel)?;
809 generated += 1;
810
811 if let Some(m) = &self.metrics {
812 m.decode_token_duration_seconds
813 .observe(step_start.elapsed().as_secs_f64());
814 }
815 }
816
817 if let Some(m) = &self.metrics {
818 let decode_elapsed = decode_start.elapsed().as_secs_f64();
819 if decode_elapsed > 0.0 && generated > 0 {
820 let tok_per_sec = generated as f64 / decode_elapsed;
821 m.tokens_per_second.observe(tok_per_sec);
822 }
823 m.tokens_generated_total.inc_by(generated as u64);
824 m.update_memory_from_rss();
825 }
826
827 tracing::info!(
828 prompt_len = prompt_tokens.len(),
829 generated,
830 "streaming sync generation complete"
831 );
832
833 Ok(generated)
834 }
835
836 #[cfg(all(feature = "metal", target_os = "macos"))]
845 #[tracing::instrument(skip(self, prompt_tokens), fields(prompt_len = prompt_tokens.len()))]
846 pub fn generate_greedy_gpu(
847 &mut self,
848 prompt_tokens: &[u32],
849 max_tokens: usize,
850 ) -> RuntimeResult<Vec<u32>> {
851 if prompt_tokens.is_empty() {
852 return Ok(vec![]);
853 }
854
855 let prefill_start = std::time::Instant::now();
859 let last_logits = self.model.forward_prefill(prompt_tokens, 0, &self.kernel)?;
860 if let Some(m) = &self.metrics {
861 m.prefill_duration_seconds
862 .observe(prefill_start.elapsed().as_secs_f64());
863 }
864
865 let first_token = {
867 let mut best_idx = 0u32;
868 let mut best_val = f32::NEG_INFINITY;
869 for (i, &v) in last_logits.iter().enumerate() {
870 if v > best_val {
871 best_val = v;
872 best_idx = i as u32;
873 }
874 }
875 best_idx
876 };
877
878 let decode_start = std::time::Instant::now();
882 let mut output_tokens = Vec::with_capacity(max_tokens);
883
884 if first_token == EOS_TOKEN_ID {
885 self.stats.record_request(0);
886 return Ok(vec![]);
887 }
888 output_tokens.push(first_token);
889
890 let mut ngram_cache = NgramCache::new();
892 ngram_cache.record(prompt_tokens);
893
894 let mut context: Vec<u32> = prompt_tokens.to_vec();
896 context.push(first_token);
897
898 let speculation_k: usize = 4;
899 let mut spec_attempts: u64 = 0;
900 let mut spec_accepted_total: u64 = 0;
901 let spec_enabled = std::env::var("OXIBONSAI_SPEC")
902 .map(|v| v == "1")
903 .unwrap_or(false);
904 let spec_warmup = 15_usize; let mut next_token = first_token;
907 let mut pos = prompt_tokens.len() + 1;
908 let max_pos = prompt_tokens.len() + max_tokens;
909
910 while pos < max_pos && output_tokens.len() < max_tokens {
911 let step_start = std::time::Instant::now();
912 let tokens_generated = output_tokens.len();
913
914 let draft = if !spec_enabled || tokens_generated < spec_warmup {
916 Vec::new()
917 } else {
918 ngram_cache.draft(&context, speculation_k)
919 };
920
921 let spec_ok = if spec_attempts >= 5 {
924 let accuracy = spec_accepted_total as f64
925 / (spec_attempts as f64 * speculation_k as f64).max(1.0);
926 accuracy > 0.6 || spec_attempts % 20 == 0
927 } else {
928 true };
930
931 if !draft.is_empty() && spec_ok {
932 let mut batch = Vec::with_capacity(1 + draft.len());
934 batch.push(next_token);
935 batch.extend_from_slice(&draft);
936
937 match self
938 .model
939 .forward_prefill_verify(&batch, pos - 1, &self.kernel)
940 {
941 Ok(model_preds) => {
942 spec_attempts += 1;
943
944 let mut accepted: usize = 0;
946 for i in 0..draft.len() {
947 if i < model_preds.len() && draft[i] == model_preds[i] {
948 accepted += 1;
949 } else {
950 break;
951 }
952 }
953 spec_accepted_total += accepted as u64;
954
955 let mut eos_seen = false;
957 for &token in draft.iter().take(accepted) {
958 if token == EOS_TOKEN_ID {
959 eos_seen = true;
960 break;
961 }
962 output_tokens.push(token);
963 context.push(token);
964 }
965
966 if !eos_seen {
967 let bonus = if accepted < model_preds.len() {
969 model_preds[accepted]
970 } else {
971 match model_preds.last() {
973 Some(&tok) => tok,
974 None => break,
975 }
976 };
977
978 if bonus == EOS_TOKEN_ID {
979 tracing::debug!(pos, accepted, "EOS from speculative bonus");
980 break;
981 }
982
983 output_tokens.push(bonus);
984 context.push(bonus);
985 next_token = bonus;
986 pos += accepted + 1;
987
988 let window_start = context.len().saturating_sub(accepted + 4);
990 ngram_cache.record(&context[window_start..]);
991 } else {
992 tracing::debug!(pos, accepted, "EOS in draft tokens");
993 break;
994 }
995 }
996 Err(_e) => {
997 tracing::debug!("speculative verify failed, using single-token decode");
999 match self.model.forward_greedy_gpu(next_token, pos - 1) {
1000 Ok(token_id) => {
1001 if token_id == EOS_TOKEN_ID {
1002 tracing::debug!(pos, "EOS token generated (greedy GPU)");
1003 break;
1004 }
1005 output_tokens.push(token_id);
1006 context.push(token_id);
1007 let window_start = context.len().saturating_sub(3);
1008 ngram_cache.record(&context[window_start..]);
1009 next_token = token_id;
1010 pos += 1;
1011 }
1012 Err(e) => {
1013 tracing::warn!(
1014 error = %e, pos,
1015 "greedy GPU path failed, falling back to normal forward"
1016 );
1017 let logits =
1018 self.model.forward(next_token, pos - 1, &self.kernel)?;
1019 let mut best_idx = 0u32;
1020 let mut best_val = f32::NEG_INFINITY;
1021 for (i, &v) in logits.iter().enumerate() {
1022 if v > best_val {
1023 best_val = v;
1024 best_idx = i as u32;
1025 }
1026 }
1027 if best_idx == EOS_TOKEN_ID {
1028 tracing::debug!(pos, "EOS from CPU fallback");
1029 break;
1030 }
1031 output_tokens.push(best_idx);
1032 context.push(best_idx);
1033 let window_start = context.len().saturating_sub(3);
1034 ngram_cache.record(&context[window_start..]);
1035 next_token = best_idx;
1036 pos += 1;
1037 }
1038 }
1039 }
1040 }
1041 } else {
1042 match self.model.forward_greedy_gpu(next_token, pos - 1) {
1044 Ok(token_id) => {
1045 if token_id == EOS_TOKEN_ID {
1046 tracing::debug!(pos, "EOS token generated (greedy GPU)");
1047 break;
1048 }
1049 output_tokens.push(token_id);
1050 context.push(token_id);
1051 let window_start = context.len().saturating_sub(3);
1052 ngram_cache.record(&context[window_start..]);
1053 next_token = token_id;
1054 pos += 1;
1055 }
1056 Err(e) => {
1057 tracing::warn!(
1058 error = %e, pos,
1059 "greedy GPU path failed, falling back to normal forward"
1060 );
1061 let logits = self.model.forward(next_token, pos - 1, &self.kernel)?;
1062 let mut best_idx = 0u32;
1063 let mut best_val = f32::NEG_INFINITY;
1064 for (i, &v) in logits.iter().enumerate() {
1065 if v > best_val {
1066 best_val = v;
1067 best_idx = i as u32;
1068 }
1069 }
1070 if best_idx == EOS_TOKEN_ID {
1071 tracing::debug!(pos, "EOS from CPU fallback");
1072 break;
1073 }
1074 output_tokens.push(best_idx);
1075 context.push(best_idx);
1076 let window_start = context.len().saturating_sub(3);
1077 ngram_cache.record(&context[window_start..]);
1078 next_token = best_idx;
1079 pos += 1;
1080 }
1081 }
1082 }
1083
1084 if let Some(m) = &self.metrics {
1085 m.decode_token_duration_seconds
1086 .observe(step_start.elapsed().as_secs_f64());
1087 }
1088
1089 if output_tokens.last() == Some(&EOS_TOKEN_ID) {
1091 output_tokens.pop(); break;
1093 }
1094 }
1095
1096 if spec_attempts > 0 {
1098 let avg_accepted = spec_accepted_total as f64 / spec_attempts as f64;
1099 let accuracy =
1100 spec_accepted_total as f64 / (spec_attempts as f64 * speculation_k as f64).max(1.0);
1101 tracing::info!(
1102 spec_attempts,
1103 spec_accepted_total,
1104 avg_accepted = format!("{:.2}", avg_accepted),
1105 accuracy = format!("{:.1}%", accuracy * 100.0),
1106 "speculative decode stats"
1107 );
1108 }
1109
1110 if let Some(m) = &self.metrics {
1112 let decode_elapsed = decode_start.elapsed().as_secs_f64();
1113 if decode_elapsed > 0.0 && !output_tokens.is_empty() {
1114 let tok_per_sec = output_tokens.len() as f64 / decode_elapsed;
1115 m.tokens_per_second.observe(tok_per_sec);
1116 }
1117 m.tokens_generated_total.inc_by(output_tokens.len() as u64);
1118 m.update_memory_from_rss();
1119 }
1120
1121 self.stats.record_request(output_tokens.len());
1122
1123 tracing::info!(
1124 prompt_len = prompt_tokens.len(),
1125 generated = output_tokens.len(),
1126 "greedy GPU generation complete"
1127 );
1128
1129 Ok(output_tokens)
1130 }
1131}
1132
1133impl InferenceEngine<'static> {
1134 pub fn from_gguf_static(
1151 gguf: &'static GgufFile<'static>,
1152 sampling_params: SamplingParams,
1153 seed: u64,
1154 max_seq_len: usize,
1155 ) -> RuntimeResult<Self> {
1156 Self::from_gguf(gguf, sampling_params, seed, max_seq_len)
1159 }
1160
1161 pub fn from_gguf_static_with_embd(
1174 gguf: &'static GgufFile<'static>,
1175 sampling_params: SamplingParams,
1176 seed: u64,
1177 max_seq_len: usize,
1178 token_embd: std::sync::Arc<[f32]>,
1179 ) -> RuntimeResult<Self> {
1180 Self::from_gguf_with_embd(gguf, sampling_params, seed, max_seq_len, token_embd)
1181 }
1182
1183 pub fn from_gguf_path_leaked(
1196 path: impl AsRef<std::path::Path>,
1197 sampling_params: SamplingParams,
1198 seed: u64,
1199 max_seq_len: usize,
1200 ) -> RuntimeResult<(Self, &'static GgufFile<'static>)> {
1201 let path_ref = path.as_ref();
1202 if !path_ref.exists() {
1203 return Err(RuntimeError::FileNotFound {
1204 path: path_ref.display().to_string(),
1205 });
1206 }
1207
1208 let mmap = oxibonsai_core::gguf::reader::mmap_gguf_file(path_ref)?;
1211 let mmap: &'static memmap2::Mmap = Box::leak(Box::new(mmap));
1212 let gguf = oxibonsai_core::gguf::reader::GgufFile::parse(mmap)?;
1213 let gguf: &'static GgufFile<'static> = Box::leak(Box::new(gguf));
1214
1215 let engine = Self::from_gguf_static(gguf, sampling_params, seed, max_seq_len)?;
1216 Ok((engine, gguf))
1217 }
1218
1219 pub fn from_gguf_path(
1234 path: impl AsRef<std::path::Path>,
1235 sampling_params: SamplingParams,
1236 seed: u64,
1237 max_seq_len: usize,
1238 ) -> RuntimeResult<Self> {
1239 Self::from_gguf_path_leaked(path, sampling_params, seed, max_seq_len)
1240 .map(|(engine, _gguf)| engine)
1241 }
1242}
1243
1244#[cfg(test)]
1245mod tests {
1246 use super::*;
1247
1248 #[test]
1249 fn engine_creation() {
1250 let config = Qwen3Config::bonsai_8b();
1251 let engine = InferenceEngine::new(config, SamplingParams::default(), 42);
1252 assert_eq!(engine.model().config().num_layers, 36);
1253 }
1254
1255 #[test]
1256 fn engine_stats_initial() {
1257 let config = Qwen3Config::bonsai_8b();
1258 let engine = InferenceEngine::new(config, SamplingParams::default(), 42);
1259 let stats = engine.stats();
1260 assert_eq!(stats.tokens_generated(), 0);
1261 assert_eq!(stats.requests_completed(), 0);
1262 assert_eq!(stats.active_session_count(), 0);
1263 assert!(stats.uptime_seconds() >= 0.0);
1264 assert!((stats.avg_tokens_per_request() - 0.0).abs() < f64::EPSILON);
1265 }
1266
1267 #[test]
1268 fn engine_stats_record() {
1269 let stats = EngineStats::new();
1270 stats.record_request(10);
1271 stats.record_request(20);
1272 assert_eq!(stats.tokens_generated(), 30);
1273 assert_eq!(stats.requests_completed(), 2);
1274 assert!((stats.avg_tokens_per_request() - 15.0).abs() < f64::EPSILON);
1275 }
1276
1277 #[test]
1278 fn engine_session_tracking() {
1279 let config = Qwen3Config::bonsai_8b();
1280 let engine = InferenceEngine::new(config, SamplingParams::default(), 42);
1281 assert_eq!(engine.active_sessions(), 0);
1282 assert_eq!(engine.session_count(), 0);
1283 }
1284
1285 #[test]
1286 fn engine_batch_generate_empty() {
1287 let config = Qwen3Config::bonsai_8b();
1288 let mut engine = InferenceEngine::new(config, SamplingParams::default(), 42);
1289 let results = engine.batch_generate(&[], 10);
1290 assert!(results.is_empty());
1291 assert_eq!(engine.session_count(), 0);
1292 }
1293
1294 #[test]
1295 fn engine_batch_generate_empty_prompts() {
1296 let config = Qwen3Config::bonsai_8b();
1297 let mut engine = InferenceEngine::new(config, SamplingParams::default(), 42);
1298 let prompts = vec![vec![], vec![]];
1299 let results = engine.batch_generate(&prompts, 5);
1300 assert_eq!(results.len(), 2);
1301 for r in &results {
1302 assert!(r.is_ok());
1303 }
1304 assert_eq!(engine.stats().requests_completed(), 2);
1306 }
1307
1308 #[test]
1309 fn engine_stats_default() {
1310 let stats = EngineStats::default();
1311 assert_eq!(stats.tokens_generated(), 0);
1312 assert_eq!(stats.requests_completed(), 0);
1313 }
1314}