/home/noah/src/realizar/src/api/mod.rs
Line | Count | Source |
1 | | //! HTTP API for model inference |
2 | | //! |
3 | | //! Provides REST endpoints for tokenization and text generation using axum. |
4 | | //! |
5 | | //! ## Endpoints |
6 | | //! |
7 | | //! - `GET /health` - Health check |
8 | | //! - `GET /metrics` - Prometheus-formatted metrics |
9 | | //! - `GET /metrics/dispatch` - CPU/GPU dispatch statistics (?format=prometheus|json) |
10 | | //! - `POST /tokenize` - Tokenize text |
11 | | //! - `POST /generate` - Generate text from prompt |
12 | | //! - `POST /batch/tokenize` - Batch tokenize multiple texts |
13 | | //! - `POST /batch/generate` - Batch generate for multiple prompts |
14 | | //! - `POST /stream/generate` - Stream generated tokens via SSE |
15 | | //! - `POST /v1/gpu/warmup` - Warmup GPU cache for batch inference (PARITY-022) |
16 | | //! - `GET /v1/gpu/status` - Check GPU cache status (PARITY-022) |
17 | | //! - `POST /v1/batch/completions` - GPU-accelerated batch inference (PARITY-022) |
18 | | //! - `GET /v1/metrics` - JSON metrics for TUI monitoring (PARITY-107) |
19 | | //! |
20 | | //! ## Example |
21 | | //! |
22 | | //! ```rust,ignore |
23 | | //! use realizar::api::{create_router, AppState}; |
24 | | //! |
25 | | //! let state = AppState::new(model, tokenizer); |
26 | | //! let app = create_router(state); |
27 | | //! axum::serve(listener, app).await?; |
28 | | //! ``` |
29 | | |
30 | | use std::sync::Arc; |
31 | | |
32 | | use axum::{ |
33 | | extract::{Query, State}, |
34 | | http::StatusCode, |
35 | | routing::{get, post}, |
36 | | Json, Router, |
37 | | }; |
38 | | use serde::{Deserialize, Serialize}; |
39 | | |
40 | | use crate::{ |
41 | | apr::{AprModel, HEADER_SIZE, MAGIC}, |
42 | | audit::{AuditLogger, AuditRecord, InMemoryAuditSink}, |
43 | | cache::{CacheKey, ModelCache}, |
44 | | error::RealizarError, |
45 | | explain::ShapExplanation, |
46 | | layers::{Model, ModelConfig}, |
47 | | metrics::MetricsCollector, |
48 | | registry::{ModelInfo, ModelRegistry}, |
49 | | tokenizer::BPETokenizer, |
50 | | }; |
51 | | |
52 | | // PMAT-802: Extracted handlers |
53 | | mod openai_handlers; |
54 | | pub(crate) use openai_handlers::{ |
55 | | openai_models_handler, openai_chat_completions_handler, openai_chat_completions_stream_handler, |
56 | | }; |
57 | | mod gpu_handlers; |
58 | | pub(crate) use gpu_handlers::{ |
59 | | gpu_warmup_handler, gpu_status_handler, gpu_batch_completions_handler, |
60 | | models_handler, tokenize_handler, generate_handler, batch_tokenize_handler, |
61 | | batch_generate_handler, stream_generate_handler, |
62 | | }; |
63 | | // Public exports for tests |
64 | | pub use gpu_handlers::{ |
65 | | GpuBatchRequest, GpuBatchResponse, GpuBatchResult, GpuBatchStats, |
66 | | GpuWarmupResponse, GpuStatusResponse, ContinuousBatchRequest, ContinuousBatchResponse, |
67 | | BatchQueueStats, BatchProcessResult, |
68 | | }; |
69 | | // Public exports for apr-cli CUDA integration (PMAT-GPU-001) |
70 | | pub use gpu_handlers::{BatchConfig, spawn_batch_processor}; |
71 | | mod realize_handlers; |
72 | | pub(crate) use realize_handlers::{ |
73 | | realize_embed_handler, realize_model_handler, realize_reload_handler, |
74 | | openai_completions_handler, openai_embeddings_handler, |
75 | | format_chat_messages, clean_chat_output, |
76 | | }; |
77 | | // Public exports for tests |
78 | | pub use realize_handlers::{ |
79 | | ContextWindowConfig, ContextWindowManager, EmbeddingRequest, EmbeddingResponse, |
80 | | EmbeddingData, EmbeddingUsage, ModelMetadataResponse, ModelLineage, |
81 | | ReloadRequest, ReloadResponse, CompletionRequest, CompletionResponse, CompletionChoice, |
82 | | }; |
83 | | mod apr_handlers; |
84 | | pub(crate) use apr_handlers::{ |
85 | | apr_predict_handler, apr_explain_handler, apr_audit_handler, |
86 | | }; |
87 | | |
88 | | /// Application state shared across handlers |
89 | | #[derive(Clone)] |
90 | | pub struct AppState { |
91 | | /// Model for inference (single model mode) |
92 | | model: Option<Arc<Model>>, |
93 | | /// Tokenizer for encoding/decoding (single model mode) |
94 | | tokenizer: Option<Arc<BPETokenizer>>, |
95 | | /// Model cache for multi-model support |
96 | | #[allow(dead_code)] // Will be used in future PR for cache warming |
97 | | cache: Option<Arc<ModelCache>>, |
98 | | /// Default cache key for single model mode |
99 | | #[allow(dead_code)] // Will be used in future PR for cache warming |
100 | | cache_key: Option<CacheKey>, |
101 | | /// Metrics collector for monitoring |
102 | | metrics: Arc<MetricsCollector>, |
103 | | /// Model registry for multi-model serving |
104 | | registry: Option<Arc<ModelRegistry>>, |
105 | | /// Default model ID for multi-model mode |
106 | | default_model_id: Option<String>, |
107 | | /// APR model for /v1/predict endpoint (real inference, not mock) |
108 | | apr_model: Option<Arc<AprModel>>, |
109 | | /// Audit logger for /v1/audit endpoint (real records, not mock) |
110 | | audit_logger: Arc<AuditLogger>, |
111 | | /// In-memory audit sink for record retrieval |
112 | | audit_sink: Arc<InMemoryAuditSink>, |
113 | | /// GPU model for GGUF inference (M33: IMP-084) |
114 | | #[cfg(feature = "gpu")] |
115 | | gpu_model: Option<Arc<std::sync::RwLock<crate::gpu::GpuModel>>>, |
116 | | /// Quantized model for fused Q4_K inference (IMP-100) |
117 | | /// This is 1.37x faster than dequantized GpuModel due to reduced memory bandwidth |
118 | | quantized_model: Option<Arc<crate::gguf::OwnedQuantizedModel>>, |
119 | | /// Thread-safe cached model for HTTP serving (IMP-116) |
120 | | /// Uses Mutex-based scheduler caching for 10.6x speedup |
121 | | #[cfg(feature = "gpu")] |
122 | | cached_model: Option<Arc<crate::gguf::OwnedQuantizedModelCachedSync>>, |
123 | | /// Dispatch metrics for adaptive CPU/GPU tracking (IMP-126) |
124 | | #[cfg(feature = "gpu")] |
125 | | dispatch_metrics: Option<Arc<crate::gguf::DispatchMetrics>>, |
126 | | /// Batch request channel for continuous batching (PARITY-052) |
127 | | /// Requests sent here are queued and processed in batches |
128 | | #[cfg(feature = "gpu")] |
129 | | batch_request_tx: Option<tokio::sync::mpsc::Sender<ContinuousBatchRequest>>, |
130 | | /// Batch configuration for window timing and size thresholds (PARITY-052) |
131 | | #[cfg(feature = "gpu")] |
132 | | batch_config: Option<BatchConfig>, |
133 | | /// CUDA-optimized model for high-performance GPU inference (PAR-111) |
134 | | /// Uses pre-uploaded weights and batched workspaces for 755+ tok/s (2.6x Ollama) |
135 | | #[cfg(feature = "cuda")] |
136 | | cuda_model: Option<Arc<std::sync::RwLock<crate::gguf::OwnedQuantizedModelCuda>>>, |
137 | | } |
138 | | |
139 | | /// Helper to create default audit infrastructure |
140 | 133 | fn create_audit_state() -> (Arc<AuditLogger>, Arc<InMemoryAuditSink>) { |
141 | 133 | let sink = Arc::new(InMemoryAuditSink::new()); |
142 | 133 | let logger = AuditLogger::new(Box::new(InMemorySinkWrapper(sink.clone()))) |
143 | 133 | .with_model_hash("demo-model-hash"); |
144 | 133 | (Arc::new(logger), sink) |
145 | 133 | } |
146 | | |
147 | | /// Wrapper to make Arc<InMemoryAuditSink> implement AuditSink |
148 | | struct InMemorySinkWrapper(Arc<InMemoryAuditSink>); |
149 | | |
150 | | impl crate::audit::AuditSink for InMemorySinkWrapper { |
151 | 1 | fn write_batch(&self, records: &[AuditRecord]) -> Result<(), crate::audit::AuditError> { |
152 | 1 | self.0.write_batch(records) |
153 | 1 | } |
154 | | |
155 | 1 | fn flush(&self) -> Result<(), crate::audit::AuditError> { |
156 | 1 | self.0.flush() |
157 | 1 | } |
158 | | } |
159 | | |
160 | | impl AppState { |
161 | | /// Create new application state |
162 | | /// |
163 | | /// # Arguments |
164 | | /// |
165 | | /// * `model` - Model for inference |
166 | | /// * `tokenizer` - Tokenizer for text processing |
167 | | #[must_use] |
168 | 2 | pub fn new(model: Model, tokenizer: BPETokenizer) -> Self { |
169 | 2 | let (audit_logger, audit_sink) = create_audit_state(); |
170 | 2 | Self { |
171 | 2 | model: Some(Arc::new(model)), |
172 | 2 | tokenizer: Some(Arc::new(tokenizer)), |
173 | 2 | cache: None, |
174 | 2 | cache_key: None, |
175 | 2 | metrics: Arc::new(MetricsCollector::new()), |
176 | 2 | registry: None, |
177 | 2 | default_model_id: None, |
178 | 2 | apr_model: None, |
179 | 2 | audit_logger, |
180 | 2 | audit_sink, |
181 | 2 | #[cfg(feature = "gpu")] |
182 | 2 | gpu_model: None, |
183 | 2 | quantized_model: None, |
184 | 2 | #[cfg(feature = "gpu")] |
185 | 2 | cached_model: None, |
186 | 2 | #[cfg(feature = "gpu")] |
187 | 2 | dispatch_metrics: None, |
188 | 2 | #[cfg(feature = "gpu")] |
189 | 2 | batch_request_tx: None, |
190 | 2 | #[cfg(feature = "gpu")] |
191 | 2 | batch_config: None, |
192 | 2 | #[cfg(feature = "cuda")] |
193 | 2 | cuda_model: None, |
194 | 2 | } |
195 | 2 | } |
196 | | |
197 | | /// Create application state with model registry for multi-model serving |
198 | | /// |
199 | | /// # Arguments |
200 | | /// |
201 | | /// * `registry` - Model registry with pre-registered models |
202 | | /// * `default_model_id` - Default model to use when not specified |
203 | | /// |
204 | | /// # Errors |
205 | | /// |
206 | | /// Returns error if default model doesn't exist in registry |
207 | 1 | pub fn with_registry( |
208 | 1 | registry: ModelRegistry, |
209 | 1 | default_model_id: &str, |
210 | 1 | ) -> Result<Self, RealizarError> { |
211 | | // Verify default model exists |
212 | 1 | if !registry.contains(default_model_id) { |
213 | 1 | return Err(RealizarError::ModelNotFound(default_model_id.to_string())); |
214 | 0 | } |
215 | | |
216 | 0 | let (audit_logger, audit_sink) = create_audit_state(); |
217 | 0 | Ok(Self { |
218 | 0 | model: None, |
219 | 0 | tokenizer: None, |
220 | 0 | cache: None, |
221 | 0 | cache_key: None, |
222 | 0 | metrics: Arc::new(MetricsCollector::new()), |
223 | 0 | registry: Some(Arc::new(registry)), |
224 | 0 | default_model_id: Some(default_model_id.to_string()), |
225 | 0 | apr_model: None, |
226 | 0 | audit_logger, |
227 | 0 | audit_sink, |
228 | 0 | #[cfg(feature = "gpu")] |
229 | 0 | gpu_model: None, |
230 | 0 | quantized_model: None, |
231 | 0 | #[cfg(feature = "gpu")] |
232 | 0 | cached_model: None, |
233 | 0 | #[cfg(feature = "gpu")] |
234 | 0 | dispatch_metrics: None, |
235 | 0 | #[cfg(feature = "gpu")] |
236 | 0 | batch_request_tx: None, |
237 | 0 | #[cfg(feature = "gpu")] |
238 | 0 | batch_config: None, |
239 | 0 | #[cfg(feature = "cuda")] |
240 | 0 | cuda_model: None, |
241 | 0 | }) |
242 | 1 | } |
243 | | |
244 | | /// Get model and tokenizer by ID (or default) |
245 | | #[allow(clippy::type_complexity)] |
246 | 53 | fn get_model( |
247 | 53 | &self, |
248 | 53 | model_id: Option<&str>, |
249 | 53 | ) -> Result<(Arc<Model>, Arc<BPETokenizer>), RealizarError> { |
250 | | // Multi-model mode |
251 | 53 | if let Some(registry0 ) = &self.registry { |
252 | 0 | let id = model_id |
253 | 0 | .or(self.default_model_id.as_deref()) |
254 | 0 | .ok_or_else(|| RealizarError::RegistryError("No model ID specified".to_string()))?; |
255 | 0 | return registry.get(id); |
256 | 53 | } |
257 | | |
258 | | // Single model mode |
259 | 53 | let model = self |
260 | 53 | .model |
261 | 53 | .clone() |
262 | 53 | .ok_or_else(|| RealizarError::RegistryError("No model available"0 .to_string0 ()))?0 ; |
263 | 53 | let tokenizer = self |
264 | 53 | .tokenizer |
265 | 53 | .clone() |
266 | 53 | .ok_or_else(|| RealizarError::RegistryError("No tokenizer available"0 .to_string0 ()))?0 ; |
267 | | |
268 | 53 | Ok((model, tokenizer)) |
269 | 53 | } |
270 | | |
271 | | /// Create application state with model caching enabled |
272 | | /// |
273 | | /// # Arguments |
274 | | /// |
275 | | /// * `cache_capacity` - Maximum number of models to cache |
276 | | /// |
277 | | /// # Panics |
278 | | /// |
279 | | /// Panics if model or tokenizer creation fails (should not happen with valid config) |
280 | | #[must_use] |
281 | 6 | pub fn with_cache(cache_capacity: usize) -> Self { |
282 | | // Create empty state with cache |
283 | 6 | let config = ModelConfig { |
284 | 6 | vocab_size: 100, |
285 | 6 | hidden_dim: 32, |
286 | 6 | num_heads: 1, |
287 | 6 | num_layers: 1, |
288 | 6 | intermediate_dim: 64, |
289 | 6 | eps: 1e-5, |
290 | 6 | }; |
291 | 6 | let model = Model::new(config).expect("Failed to create placeholder model"); |
292 | 6 | let vocab: Vec<String> = (0..100) |
293 | 600 | .map6 (|i| { |
294 | 600 | if i == 0 { |
295 | 6 | "<unk>".to_string() |
296 | | } else { |
297 | 594 | format!("token{i}") |
298 | | } |
299 | 600 | }) |
300 | 6 | .collect(); |
301 | 6 | let tokenizer = |
302 | 6 | BPETokenizer::new(vocab, vec![], "<unk>").expect("Failed to create tokenizer"); |
303 | | |
304 | 6 | let (audit_logger, audit_sink) = create_audit_state(); |
305 | 6 | Self { |
306 | 6 | model: Some(Arc::new(model)), |
307 | 6 | tokenizer: Some(Arc::new(tokenizer)), |
308 | 6 | cache: Some(Arc::new(ModelCache::new(cache_capacity))), |
309 | 6 | cache_key: Some(CacheKey::new("default".to_string())), |
310 | 6 | metrics: Arc::new(MetricsCollector::new()), |
311 | 6 | registry: None, |
312 | 6 | default_model_id: None, |
313 | 6 | apr_model: None, |
314 | 6 | audit_logger, |
315 | 6 | audit_sink, |
316 | 6 | #[cfg(feature = "gpu")] |
317 | 6 | gpu_model: None, |
318 | 6 | quantized_model: None, |
319 | 6 | #[cfg(feature = "gpu")] |
320 | 6 | cached_model: None, |
321 | 6 | #[cfg(feature = "gpu")] |
322 | 6 | dispatch_metrics: None, |
323 | 6 | #[cfg(feature = "gpu")] |
324 | 6 | batch_request_tx: None, |
325 | 6 | #[cfg(feature = "gpu")] |
326 | 6 | batch_config: None, |
327 | 6 | #[cfg(feature = "cuda")] |
328 | 6 | cuda_model: None, |
329 | 6 | } |
330 | 6 | } |
331 | | |
332 | | /// Create a demo state with small model for testing |
333 | | /// |
334 | | /// # Errors |
335 | | /// |
336 | | /// Returns error if model or tokenizer creation fails |
337 | 102 | pub fn demo() -> Result<Self, RealizarError> { |
338 | 102 | let config = ModelConfig { |
339 | 102 | vocab_size: 100, |
340 | 102 | hidden_dim: 32, |
341 | 102 | num_heads: 1, |
342 | 102 | num_layers: 1, |
343 | 102 | intermediate_dim: 64, |
344 | 102 | eps: 1e-5, |
345 | 102 | }; |
346 | 102 | let model = Model::new(config)?0 ; |
347 | | |
348 | | // Simple demo vocabulary |
349 | 102 | let vocab: Vec<String> = (0..100) |
350 | 10.2k | .map102 (|i| { |
351 | 10.2k | if i == 0 { |
352 | 102 | "<unk>".to_string() |
353 | | } else { |
354 | 10.0k | format!("token{i}") |
355 | | } |
356 | 10.2k | }) |
357 | 102 | .collect(); |
358 | 102 | let tokenizer = BPETokenizer::new(vocab, vec![], "<unk>")?0 ; |
359 | | |
360 | | // Create demo APR model (real inference, not mock) |
361 | | // Simple model: sum of inputs with bias |
362 | 102 | let apr_model = create_demo_apr_model(4)?0 ; // 4 input features |
363 | | |
364 | 102 | let (audit_logger, audit_sink) = create_audit_state(); |
365 | 102 | Ok(Self { |
366 | 102 | model: Some(Arc::new(model)), |
367 | 102 | tokenizer: Some(Arc::new(tokenizer)), |
368 | 102 | cache: None, |
369 | 102 | cache_key: None, |
370 | 102 | metrics: Arc::new(MetricsCollector::new()), |
371 | 102 | registry: None, |
372 | 102 | default_model_id: None, |
373 | 102 | apr_model: Some(Arc::new(apr_model)), |
374 | 102 | audit_logger, |
375 | 102 | audit_sink, |
376 | 102 | #[cfg(feature = "gpu")] |
377 | 102 | gpu_model: None, |
378 | 102 | quantized_model: None, |
379 | 102 | #[cfg(feature = "gpu")] |
380 | 102 | cached_model: None, |
381 | 102 | #[cfg(feature = "gpu")] |
382 | 102 | dispatch_metrics: None, |
383 | 102 | #[cfg(feature = "gpu")] |
384 | 102 | batch_request_tx: None, |
385 | 102 | #[cfg(feature = "gpu")] |
386 | 102 | batch_config: None, |
387 | 102 | #[cfg(feature = "cuda")] |
388 | 102 | cuda_model: None, |
389 | 102 | }) |
390 | 102 | } |
391 | | |
392 | | /// Create application state with a GPU model for GGUF inference (M33: IMP-084) |
393 | | /// |
394 | | /// # Arguments |
395 | | /// |
396 | | /// * `gpu_model` - GPU model for inference |
397 | | /// |
398 | | /// # Errors |
399 | | /// |
400 | | /// Returns error if tokenizer creation fails |
401 | | #[cfg(feature = "gpu")] |
402 | 2 | pub fn with_gpu_model(gpu_model: crate::gpu::GpuModel) -> Result<Self, RealizarError> { |
403 | | // Create tokenizer with vocab size matching GPU model |
404 | 2 | let vocab_size = gpu_model.config().vocab_size; |
405 | 2 | let vocab: Vec<String> = (0..vocab_size) |
406 | 512 | .map2 (|i| { |
407 | 512 | if i == 0 { |
408 | 2 | "<unk>".to_string() |
409 | | } else { |
410 | 510 | format!("token{i}") |
411 | | } |
412 | 512 | }) |
413 | 2 | .collect(); |
414 | 2 | let tokenizer = BPETokenizer::new(vocab, vec![], "<unk>")?0 ; |
415 | | |
416 | 2 | let (audit_logger, audit_sink) = create_audit_state(); |
417 | 2 | Ok(Self { |
418 | 2 | model: None, |
419 | 2 | tokenizer: Some(Arc::new(tokenizer)), |
420 | 2 | cache: None, |
421 | 2 | cache_key: None, |
422 | 2 | metrics: Arc::new(MetricsCollector::new()), |
423 | 2 | registry: None, |
424 | 2 | default_model_id: None, |
425 | 2 | apr_model: None, |
426 | 2 | audit_logger, |
427 | 2 | audit_sink, |
428 | 2 | gpu_model: Some(Arc::new(std::sync::RwLock::new(gpu_model))), |
429 | 2 | quantized_model: None, |
430 | 2 | cached_model: None, |
431 | 2 | dispatch_metrics: None, |
432 | 2 | batch_request_tx: None, |
433 | 2 | batch_config: None, |
434 | 2 | #[cfg(feature = "cuda")] |
435 | 2 | cuda_model: None, |
436 | 2 | }) |
437 | 2 | } |
438 | | |
439 | | /// Create application state with GPU model and real vocabulary (IMP-152) |
440 | | /// |
441 | | /// This version uses the actual vocabulary from the GGUF file for proper text encoding/decoding. |
442 | | /// |
443 | | /// # Arguments |
444 | | /// |
445 | | /// * `gpu_model` - GPU model for inference |
446 | | /// * `vocab` - Vocabulary tokens from GGUF metadata (tokenizer.ggml.tokens) |
447 | | /// |
448 | | /// # Errors |
449 | | /// |
450 | | /// Returns error if tokenizer creation fails |
451 | | #[cfg(feature = "gpu")] |
452 | 0 | pub fn with_gpu_model_and_vocab( |
453 | 0 | gpu_model: crate::gpu::GpuModel, |
454 | 0 | vocab: Vec<String>, |
455 | 0 | ) -> Result<Self, RealizarError> { |
456 | 0 | let tokenizer = BPETokenizer::new(vocab, vec![], "<unk>")?; |
457 | | |
458 | 0 | let (audit_logger, audit_sink) = create_audit_state(); |
459 | 0 | Ok(Self { |
460 | 0 | model: None, |
461 | 0 | tokenizer: Some(Arc::new(tokenizer)), |
462 | 0 | cache: None, |
463 | 0 | cache_key: None, |
464 | 0 | metrics: Arc::new(MetricsCollector::new()), |
465 | 0 | registry: None, |
466 | 0 | default_model_id: None, |
467 | 0 | apr_model: None, |
468 | 0 | audit_logger, |
469 | 0 | audit_sink, |
470 | 0 | gpu_model: Some(Arc::new(std::sync::RwLock::new(gpu_model))), |
471 | 0 | quantized_model: None, |
472 | 0 | cached_model: None, |
473 | 0 | dispatch_metrics: None, |
474 | 0 | batch_request_tx: None, |
475 | 0 | batch_config: None, |
476 | 0 | #[cfg(feature = "cuda")] |
477 | 0 | cuda_model: None, |
478 | 0 | }) |
479 | 0 | } |
480 | | |
481 | | /// Create application state with a quantized model for fused Q4_K inference (IMP-100) |
482 | | /// |
483 | | /// This is 1.37x faster than dequantized GpuModel due to reduced memory bandwidth. |
484 | | /// |
485 | | /// # Arguments |
486 | | /// |
487 | | /// * `quantized_model` - Quantized model for fused Q4_K inference |
488 | | /// |
489 | | /// # Errors |
490 | | /// |
491 | | /// Returns error if tokenizer creation fails |
492 | 0 | pub fn with_quantized_model( |
493 | 0 | quantized_model: crate::gguf::OwnedQuantizedModel, |
494 | 0 | ) -> Result<Self, RealizarError> { |
495 | | // Create tokenizer with vocab size matching model |
496 | 0 | let vocab_size = quantized_model.config.vocab_size; |
497 | 0 | let vocab: Vec<String> = (0..vocab_size) |
498 | 0 | .map(|i| { |
499 | 0 | if i == 0 { |
500 | 0 | "<unk>".to_string() |
501 | | } else { |
502 | 0 | format!("token{i}") |
503 | | } |
504 | 0 | }) |
505 | 0 | .collect(); |
506 | 0 | let tokenizer = BPETokenizer::new(vocab, vec![], "<unk>")?; |
507 | | |
508 | 0 | let (audit_logger, audit_sink) = create_audit_state(); |
509 | 0 | Ok(Self { |
510 | 0 | model: None, |
511 | 0 | tokenizer: Some(Arc::new(tokenizer)), |
512 | 0 | cache: None, |
513 | 0 | cache_key: None, |
514 | 0 | metrics: Arc::new(MetricsCollector::new()), |
515 | 0 | registry: None, |
516 | 0 | default_model_id: None, |
517 | 0 | apr_model: None, |
518 | 0 | audit_logger, |
519 | 0 | audit_sink, |
520 | 0 | #[cfg(feature = "gpu")] |
521 | 0 | gpu_model: None, |
522 | 0 | quantized_model: Some(Arc::new(quantized_model)), |
523 | 0 | #[cfg(feature = "gpu")] |
524 | 0 | cached_model: None, |
525 | 0 | #[cfg(feature = "gpu")] |
526 | 0 | dispatch_metrics: None, |
527 | 0 | #[cfg(feature = "gpu")] |
528 | 0 | batch_request_tx: None, |
529 | 0 | #[cfg(feature = "gpu")] |
530 | 0 | batch_config: None, |
531 | 0 | #[cfg(feature = "cuda")] |
532 | 0 | cuda_model: None, |
533 | 0 | }) |
534 | 0 | } |
535 | | |
536 | | /// Create application state with thread-safe cached model (IMP-116) |
537 | | /// |
538 | | /// Uses Mutex-based scheduler caching for 10.6x GPU speedup. |
539 | | /// This is the recommended production configuration for HTTP serving. |
540 | | /// |
541 | | /// # Arguments |
542 | | /// |
543 | | /// * `cached_model` - Thread-safe cached model with scheduler |
544 | | /// |
545 | | /// # Errors |
546 | | /// |
547 | | /// Returns error if tokenizer creation fails |
548 | | #[cfg(feature = "gpu")] |
549 | 21 | pub fn with_cached_model( |
550 | 21 | cached_model: crate::gguf::OwnedQuantizedModelCachedSync, |
551 | 21 | ) -> Result<Self, RealizarError> { |
552 | | // Create tokenizer with vocab size matching model |
553 | 21 | let vocab_size = cached_model.model().config.vocab_size; |
554 | 21 | let vocab: Vec<String> = (0..vocab_size) |
555 | 2.10k | .map21 (|i| { |
556 | 2.10k | if i == 0 { |
557 | 21 | "<unk>".to_string() |
558 | | } else { |
559 | 2.07k | format!("token{i}") |
560 | | } |
561 | 2.10k | }) |
562 | 21 | .collect(); |
563 | 21 | let tokenizer = BPETokenizer::new(vocab, vec![], "<unk>")?0 ; |
564 | | |
565 | 21 | let (audit_logger, audit_sink) = create_audit_state(); |
566 | 21 | Ok(Self { |
567 | 21 | model: None, |
568 | 21 | tokenizer: Some(Arc::new(tokenizer)), |
569 | 21 | cache: None, |
570 | 21 | cache_key: None, |
571 | 21 | metrics: Arc::new(MetricsCollector::new()), |
572 | 21 | registry: None, |
573 | 21 | default_model_id: None, |
574 | 21 | apr_model: None, |
575 | 21 | audit_logger, |
576 | 21 | audit_sink, |
577 | 21 | gpu_model: None, |
578 | 21 | quantized_model: None, |
579 | 21 | cached_model: Some(Arc::new(cached_model)), |
580 | 21 | // Initialize dispatch metrics for adaptive generation (IMP-126) |
581 | 21 | dispatch_metrics: Some(Arc::new(crate::gguf::DispatchMetrics::new())), |
582 | 21 | batch_request_tx: None, |
583 | 21 | batch_config: None, |
584 | 21 | #[cfg(feature = "cuda")] |
585 | 21 | cuda_model: None, |
586 | 21 | }) |
587 | 21 | } |
588 | | |
589 | | /// Create application state with thread-safe cached model and real vocabulary (IMP-116) |
590 | | /// |
591 | | /// Uses Mutex-based scheduler caching for 10.6x GPU speedup with proper token decoding. |
592 | | /// |
593 | | /// # Arguments |
594 | | /// |
595 | | /// * `cached_model` - Thread-safe cached model with scheduler |
596 | | /// * `vocab` - Vocabulary tokens from GGUF metadata (tokenizer.ggml.tokens) |
597 | | /// |
598 | | /// # Errors |
599 | | /// |
600 | | /// Returns error if tokenizer creation fails |
601 | | #[cfg(feature = "gpu")] |
602 | 0 | pub fn with_cached_model_and_vocab( |
603 | 0 | cached_model: crate::gguf::OwnedQuantizedModelCachedSync, |
604 | 0 | vocab: Vec<String>, |
605 | 0 | ) -> Result<Self, RealizarError> { |
606 | 0 | let tokenizer = BPETokenizer::new(vocab, vec![], "<unk>")?; |
607 | | |
608 | 0 | let (audit_logger, audit_sink) = create_audit_state(); |
609 | 0 | Ok(Self { |
610 | 0 | model: None, |
611 | 0 | tokenizer: Some(Arc::new(tokenizer)), |
612 | 0 | cache: None, |
613 | 0 | cache_key: None, |
614 | 0 | metrics: Arc::new(MetricsCollector::new()), |
615 | 0 | registry: None, |
616 | 0 | default_model_id: None, |
617 | 0 | apr_model: None, |
618 | 0 | audit_logger, |
619 | 0 | audit_sink, |
620 | 0 | gpu_model: None, |
621 | 0 | quantized_model: None, |
622 | 0 | cached_model: Some(Arc::new(cached_model)), |
623 | 0 | dispatch_metrics: Some(Arc::new(crate::gguf::DispatchMetrics::new())), |
624 | 0 | batch_request_tx: None, |
625 | 0 | batch_config: None, |
626 | 0 | #[cfg(feature = "cuda")] |
627 | 0 | cuda_model: None, |
628 | 0 | }) |
629 | 0 | } |
630 | | |
631 | | /// Create application state with quantized model and real vocabulary from GGUF |
632 | | /// |
633 | | /// This version uses the actual vocabulary from the GGUF file for proper decoding. |
634 | | /// |
635 | | /// # Arguments |
636 | | /// |
637 | | /// * `quantized_model` - Quantized model for fused Q4_K inference |
638 | | /// * `vocab` - Vocabulary tokens from GGUF metadata (tokenizer.ggml.tokens) |
639 | | /// |
640 | | /// # Errors |
641 | | /// |
642 | | /// Returns error if tokenizer creation fails |
643 | 0 | pub fn with_quantized_model_and_vocab( |
644 | 0 | quantized_model: crate::gguf::OwnedQuantizedModel, |
645 | 0 | vocab: Vec<String>, |
646 | 0 | ) -> Result<Self, RealizarError> { |
647 | 0 | let tokenizer = BPETokenizer::new(vocab, vec![], "<unk>")?; |
648 | | |
649 | 0 | let (audit_logger, audit_sink) = create_audit_state(); |
650 | 0 | Ok(Self { |
651 | 0 | model: None, |
652 | 0 | tokenizer: Some(Arc::new(tokenizer)), |
653 | 0 | cache: None, |
654 | 0 | cache_key: None, |
655 | 0 | metrics: Arc::new(MetricsCollector::new()), |
656 | 0 | registry: None, |
657 | 0 | default_model_id: None, |
658 | 0 | apr_model: None, |
659 | 0 | audit_logger, |
660 | 0 | audit_sink, |
661 | 0 | #[cfg(feature = "gpu")] |
662 | 0 | gpu_model: None, |
663 | 0 | quantized_model: Some(Arc::new(quantized_model)), |
664 | 0 | #[cfg(feature = "gpu")] |
665 | 0 | cached_model: None, |
666 | 0 | #[cfg(feature = "gpu")] |
667 | 0 | dispatch_metrics: None, |
668 | 0 | #[cfg(feature = "gpu")] |
669 | 0 | batch_request_tx: None, |
670 | 0 | #[cfg(feature = "gpu")] |
671 | 0 | batch_config: None, |
672 | 0 | #[cfg(feature = "cuda")] |
673 | 0 | cuda_model: None, |
674 | 0 | }) |
675 | 0 | } |
676 | | |
677 | | /// Create application state with CUDA-optimized model for high-performance GPU inference (PAR-111) |
678 | | /// |
679 | | /// This uses the `OwnedQuantizedModelCuda` wrapper which achieves 755+ tok/s (2.6x Ollama) by: |
680 | | /// - Pre-uploading all weights to GPU via `preload_weights_gpu()` |
681 | | /// - Using batched workspaces for efficient inference |
682 | | /// - GPU-resident KV cache to avoid CPU→GPU transfers |
683 | | /// |
684 | | /// # Arguments |
685 | | /// |
686 | | /// * `cuda_model` - CUDA-optimized model wrapper (already initialized with GPU resources) |
687 | | /// * `vocab` - Vocabulary tokens from GGUF metadata (tokenizer.ggml.tokens) |
688 | | /// |
689 | | /// # Errors |
690 | | /// |
691 | | /// Returns error if tokenizer creation fails |
692 | | #[cfg(feature = "cuda")] |
693 | | pub fn with_cuda_model_and_vocab( |
694 | | cuda_model: crate::gguf::OwnedQuantizedModelCuda, |
695 | | vocab: Vec<String>, |
696 | | ) -> Result<Self, RealizarError> { |
697 | | let tokenizer = BPETokenizer::new(vocab, vec![], "<unk>")?; |
698 | | |
699 | | let (audit_logger, audit_sink) = create_audit_state(); |
700 | | Ok(Self { |
701 | | model: None, |
702 | | tokenizer: Some(Arc::new(tokenizer)), |
703 | | cache: None, |
704 | | cache_key: None, |
705 | | metrics: Arc::new(MetricsCollector::new()), |
706 | | registry: None, |
707 | | default_model_id: None, |
708 | | apr_model: None, |
709 | | audit_logger, |
710 | | audit_sink, |
711 | | #[cfg(feature = "gpu")] |
712 | | gpu_model: None, |
713 | | quantized_model: None, |
714 | | #[cfg(feature = "gpu")] |
715 | | cached_model: None, |
716 | | #[cfg(feature = "gpu")] |
717 | | dispatch_metrics: None, |
718 | | #[cfg(feature = "gpu")] |
719 | | batch_request_tx: None, |
720 | | #[cfg(feature = "gpu")] |
721 | | batch_config: None, |
722 | | cuda_model: Some(Arc::new(std::sync::RwLock::new(cuda_model))), |
723 | | }) |
724 | | } |
725 | | |
726 | | /// Check if this AppState has a quantized model (IMP-100) |
727 | | #[must_use] |
728 | 2 | pub fn has_quantized_model(&self) -> bool { |
729 | 2 | self.quantized_model.is_some() |
730 | 2 | } |
731 | | |
732 | | /// Get the quantized model for inference (IMP-100) |
733 | 13 | pub fn quantized_model(&self) -> Option<&Arc<crate::gguf::OwnedQuantizedModel>> { |
734 | 13 | self.quantized_model.as_ref() |
735 | 13 | } |
736 | | |
737 | | /// Check if this AppState has a GPU model (M33: IMP-084) |
738 | | #[cfg(feature = "gpu")] |
739 | | #[must_use] |
740 | 3 | pub fn has_gpu_model(&self) -> bool { |
741 | 3 | self.gpu_model.is_some() |
742 | 3 | } |
743 | | |
744 | | /// Get the GPU model for inference (M33: IMP-085) |
745 | | #[cfg(feature = "gpu")] |
746 | 12 | pub fn gpu_model(&self) -> Option<&Arc<std::sync::RwLock<crate::gpu::GpuModel>>> { |
747 | 12 | self.gpu_model.as_ref() |
748 | 12 | } |
749 | | |
750 | | /// Check if this AppState has a cached model (IMP-116) |
751 | | #[cfg(feature = "gpu")] |
752 | | #[must_use] |
753 | 2 | pub fn has_cached_model(&self) -> bool { |
754 | 2 | self.cached_model.is_some() |
755 | 2 | } |
756 | | |
757 | | /// Get the cached model for inference (IMP-116) |
758 | | #[cfg(feature = "gpu")] |
759 | 19 | pub fn cached_model(&self) -> Option<&Arc<crate::gguf::OwnedQuantizedModelCachedSync>> { |
760 | 19 | self.cached_model.as_ref() |
761 | 19 | } |
762 | | |
763 | | /// Check if this AppState has a CUDA-optimized model (PAR-111) |
764 | | #[cfg(feature = "cuda")] |
765 | | #[must_use] |
766 | | pub fn has_cuda_model(&self) -> bool { |
767 | | self.cuda_model.is_some() |
768 | | } |
769 | | |
770 | | /// Get the CUDA-optimized model for high-performance GPU inference (PAR-111) |
771 | | /// |
772 | | /// Returns the model wrapper that achieves 755+ tok/s (2.6x Ollama) by using: |
773 | | /// - Pre-uploaded GPU weights |
774 | | /// - Batched workspaces |
775 | | /// - GPU-resident KV cache |
776 | | #[cfg(feature = "cuda")] |
777 | | pub fn cuda_model( |
778 | | &self, |
779 | | ) -> Option<&Arc<std::sync::RwLock<crate::gguf::OwnedQuantizedModelCuda>>> { |
780 | | self.cuda_model.as_ref() |
781 | | } |
782 | | |
783 | | /// Get dispatch metrics for adaptive CPU/GPU tracking (IMP-126) |
784 | | #[cfg(feature = "gpu")] |
785 | | #[must_use] |
786 | 32 | pub fn dispatch_metrics(&self) -> Option<&Arc<crate::gguf::DispatchMetrics>> { |
787 | 32 | self.dispatch_metrics.as_ref() |
788 | 32 | } |
789 | | |
790 | | /// Get batch request sender for continuous batching (PARITY-052) |
791 | | #[cfg(feature = "gpu")] |
792 | | #[must_use] |
793 | 0 | pub fn batch_request_tx(&self) -> Option<&tokio::sync::mpsc::Sender<ContinuousBatchRequest>> { |
794 | 0 | self.batch_request_tx.as_ref() |
795 | 0 | } |
796 | | |
797 | | /// Get batch configuration (PARITY-052) |
798 | | #[cfg(feature = "gpu")] |
799 | | #[must_use] |
800 | 1 | pub fn batch_config(&self) -> Option<&BatchConfig> { |
801 | 1 | self.batch_config.as_ref() |
802 | 1 | } |
803 | | |
804 | | /// Check if batch inference is enabled (PARITY-052) |
805 | | #[cfg(feature = "gpu")] |
806 | | #[must_use] |
807 | 1 | pub fn batch_enabled(&self) -> bool { |
808 | 1 | self.batch_request_tx.is_some() && self.batch_config0 .is_some0 () |
809 | 1 | } |
810 | | |
811 | | /// Set batch request sender and config (PARITY-052) |
812 | | /// This enables continuous batch inference for the completions endpoint |
813 | | #[cfg(feature = "gpu")] |
814 | | #[must_use] |
815 | 0 | pub fn with_batch_config( |
816 | 0 | mut self, |
817 | 0 | batch_request_tx: tokio::sync::mpsc::Sender<ContinuousBatchRequest>, |
818 | 0 | batch_config: BatchConfig, |
819 | 0 | ) -> Self { |
820 | 0 | self.batch_request_tx = Some(batch_request_tx); |
821 | 0 | self.batch_config = Some(batch_config); |
822 | 0 | self |
823 | 0 | } |
824 | | } |
825 | | |
826 | | /// Create a demo APR v2 model for testing |
827 | 103 | pub(crate) fn create_demo_apr_model(_input_dim: usize) -> Result<AprModel, RealizarError> { |
828 | | use crate::apr::TensorEntry; |
829 | | |
830 | | // Create minimal APR v2 file |
831 | 103 | let metadata = r#"{"model_type":"demo","name":"demo-model"}"#; |
832 | 103 | let tensor_index: Vec<TensorEntry> = vec![TensorEntry { |
833 | 103 | name: "weight".to_string(), |
834 | 103 | dtype: "F32".to_string(), |
835 | 103 | shape: vec![4], |
836 | 103 | offset: 0, |
837 | 103 | size: 16, |
838 | 103 | }]; |
839 | 103 | let tensor_index_json = serde_json::to_vec(&tensor_index).unwrap_or_default(); |
840 | 103 | let tensor_data: [f32; 4] = [1.0, 1.0, 1.0, 1.0]; |
841 | 412 | let tensor_bytes103 : Vec<u8>103 = tensor_data103 .iter103 ().flat_map103 (|f| f.to_le_bytes()).collect103 (); |
842 | | |
843 | | // Calculate offsets (64-byte aligned) |
844 | 103 | let metadata_offset = HEADER_SIZE as u64; |
845 | 103 | let metadata_size = metadata.len() as u32; |
846 | 103 | let tensor_index_offset = |
847 | 103 | ((metadata_offset as usize + metadata.len()).div_ceil(64) * 64) as u64; |
848 | 103 | let data_offset = |
849 | 103 | ((tensor_index_offset as usize + tensor_index_json.len()).div_ceil(64) * 64) as u64; |
850 | | |
851 | 103 | let mut data = vec![0u8; data_offset as usize + tensor_bytes.len()]; |
852 | | |
853 | | // Header (64 bytes) |
854 | 103 | data[0..4].copy_from_slice(&MAGIC); |
855 | 103 | data[4] = 2; // Version major |
856 | 103 | data[5] = 0; // Version minor |
857 | 103 | data[6..8].copy_from_slice(&0u16.to_le_bytes()); // Flags |
858 | 103 | data[8..12].copy_from_slice(&1u32.to_le_bytes()); // Tensor count |
859 | 103 | data[12..20].copy_from_slice(&metadata_offset.to_le_bytes()); |
860 | 103 | data[20..24].copy_from_slice(&metadata_size.to_le_bytes()); |
861 | 103 | data[24..32].copy_from_slice(&tensor_index_offset.to_le_bytes()); |
862 | 103 | data[32..40].copy_from_slice(&data_offset.to_le_bytes()); |
863 | | // Checksum at 40..44 (leave as 0 for now) |
864 | | |
865 | | // Metadata |
866 | 103 | data[metadata_offset as usize..metadata_offset as usize + metadata.len()] |
867 | 103 | .copy_from_slice(metadata.as_bytes()); |
868 | | |
869 | | // Tensor index |
870 | 103 | data[tensor_index_offset as usize..tensor_index_offset as usize + tensor_index_json.len()] |
871 | 103 | .copy_from_slice(&tensor_index_json); |
872 | | |
873 | | // Tensor data |
874 | 103 | data[data_offset as usize..data_offset as usize + tensor_bytes.len()] |
875 | 103 | .copy_from_slice(&tensor_bytes); |
876 | | |
877 | 103 | AprModel::from_bytes(data) |
878 | 103 | } |
879 | | |
880 | | /// Health check response |
881 | | #[derive(Serialize, Deserialize)] |
882 | | pub struct HealthResponse { |
883 | | /// Service status |
884 | | pub status: String, |
885 | | /// Service version |
886 | | pub version: String, |
887 | | /// Compute mode: "cpu" or "gpu" |
888 | | pub compute_mode: String, |
889 | | } |
890 | | |
891 | | /// Tokenize request |
892 | | #[derive(Serialize, Deserialize)] |
893 | | pub struct TokenizeRequest { |
894 | | /// Text to tokenize |
895 | | pub text: String, |
896 | | /// Model ID (optional, uses default if not specified) |
897 | | pub model_id: Option<String>, |
898 | | } |
899 | | |
900 | | /// Tokenize response |
901 | | #[derive(Serialize, Deserialize)] |
902 | | pub struct TokenizeResponse { |
903 | | /// Token IDs |
904 | | pub token_ids: Vec<u32>, |
905 | | /// Number of tokens |
906 | | pub num_tokens: usize, |
907 | | } |
908 | | |
909 | | /// Generate request |
910 | | #[derive(Serialize, Deserialize)] |
911 | | pub struct GenerateRequest { |
912 | | /// Input prompt (token IDs or text) |
913 | | pub prompt: String, |
914 | | /// Maximum tokens to generate |
915 | | #[serde(default = "default_max_tokens")] |
916 | | pub max_tokens: usize, |
917 | | /// Sampling temperature |
918 | | #[serde(default = "default_temperature")] |
919 | | pub temperature: f32, |
920 | | /// Sampling strategy: "greedy", "`top_k`", or "`top_p`" |
921 | | #[serde(default = "default_strategy")] |
922 | | pub strategy: String, |
923 | | /// Top-k value (if strategy is "`top_k`") |
924 | | #[serde(default = "default_top_k")] |
925 | | pub top_k: usize, |
926 | | /// Top-p value (if strategy is "`top_p`") |
927 | | #[serde(default = "default_top_p")] |
928 | | pub top_p: f32, |
929 | | /// Random seed for reproducibility |
930 | | pub seed: Option<u64>, |
931 | | /// Model ID (optional, uses default if not specified) |
932 | | pub model_id: Option<String>, |
933 | | } |
934 | | |
935 | 11 | pub(crate) fn default_max_tokens() -> usize { |
936 | 11 | 50 |
937 | 11 | } |
938 | 20 | fn default_temperature() -> f32 { |
939 | 20 | 1.0 |
940 | 20 | } |
941 | 10 | fn default_strategy() -> String { |
942 | 10 | "greedy".to_string() |
943 | 10 | } |
944 | 19 | pub(crate) fn default_top_k() -> usize { |
945 | 19 | 50 |
946 | 19 | } |
947 | 19 | fn default_top_p() -> f32 { |
948 | 19 | 0.9 |
949 | 19 | } |
950 | | |
951 | | /// Generate response |
952 | | #[derive(Serialize, Deserialize)] |
953 | | pub struct GenerateResponse { |
954 | | /// Generated token IDs |
955 | | pub token_ids: Vec<u32>, |
956 | | /// Decoded text |
957 | | pub text: String, |
958 | | /// Number of generated tokens |
959 | | pub num_generated: usize, |
960 | | } |
961 | | |
962 | | /// Error response |
963 | | #[derive(Serialize, Deserialize)] |
964 | | pub struct ErrorResponse { |
965 | | /// Error message |
966 | | pub error: String, |
967 | | } |
968 | | |
969 | | /// Batch tokenize request |
970 | | #[derive(Serialize, Deserialize)] |
971 | | pub struct BatchTokenizeRequest { |
972 | | /// Texts to tokenize |
973 | | pub texts: Vec<String>, |
974 | | } |
975 | | |
976 | | /// Batch tokenize response |
977 | | #[derive(Serialize, Deserialize)] |
978 | | pub struct BatchTokenizeResponse { |
979 | | /// Results for each text in the same order |
980 | | pub results: Vec<TokenizeResponse>, |
981 | | } |
982 | | |
983 | | /// Batch generate request |
984 | | #[derive(Serialize, Deserialize)] |
985 | | pub struct BatchGenerateRequest { |
986 | | /// Input prompts |
987 | | pub prompts: Vec<String>, |
988 | | /// Maximum tokens to generate (shared across all prompts) |
989 | | #[serde(default = "default_max_tokens")] |
990 | | pub max_tokens: usize, |
991 | | /// Sampling temperature (shared) |
992 | | #[serde(default = "default_temperature")] |
993 | | pub temperature: f32, |
994 | | /// Sampling strategy (shared) |
995 | | #[serde(default = "default_strategy")] |
996 | | pub strategy: String, |
997 | | /// Top-k value (shared) |
998 | | #[serde(default = "default_top_k")] |
999 | | pub top_k: usize, |
1000 | | /// Top-p value (shared) |
1001 | | #[serde(default = "default_top_p")] |
1002 | | pub top_p: f32, |
1003 | | /// Random seed for reproducibility |
1004 | | pub seed: Option<u64>, |
1005 | | } |
1006 | | |
1007 | | /// Batch generate response |
1008 | | #[derive(Serialize, Deserialize)] |
1009 | | pub struct BatchGenerateResponse { |
1010 | | /// Results for each prompt in the same order |
1011 | | pub results: Vec<GenerateResponse>, |
1012 | | } |
1013 | | |
1014 | | /// Stream token event (SSE) |
1015 | | #[derive(Serialize, Deserialize)] |
1016 | | pub struct StreamTokenEvent { |
1017 | | /// Token ID |
1018 | | pub token_id: u32, |
1019 | | /// Decoded text for this token |
1020 | | pub text: String, |
1021 | | } |
1022 | | |
1023 | | /// Stream done event (SSE) |
1024 | | #[derive(Serialize, Deserialize)] |
1025 | | pub struct StreamDoneEvent { |
1026 | | /// Total number of tokens generated |
1027 | | pub num_generated: usize, |
1028 | | } |
1029 | | |
1030 | | /// Models list response |
1031 | | #[derive(Serialize, Deserialize)] |
1032 | | pub struct ModelsResponse { |
1033 | | /// List of available models |
1034 | | pub models: Vec<ModelInfo>, |
1035 | | } |
1036 | | |
1037 | | // ============================================================================ |
1038 | | // OpenAI-Compatible API Types (per spec §5.4) |
1039 | | // ============================================================================ |
1040 | | |
1041 | | /// OpenAI-compatible chat completion request |
1042 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
1043 | | pub struct ChatCompletionRequest { |
1044 | | /// Model ID to use |
1045 | | pub model: String, |
1046 | | /// Chat messages |
1047 | | pub messages: Vec<ChatMessage>, |
1048 | | /// Maximum tokens to generate |
1049 | | #[serde(default)] |
1050 | | pub max_tokens: Option<usize>, |
1051 | | /// Sampling temperature |
1052 | | #[serde(default)] |
1053 | | pub temperature: Option<f32>, |
1054 | | /// Nucleus sampling |
1055 | | #[serde(default)] |
1056 | | pub top_p: Option<f32>, |
1057 | | /// Number of completions to generate |
1058 | | #[serde(default = "default_n")] |
1059 | | pub n: usize, |
1060 | | /// Stream responses |
1061 | | #[serde(default)] |
1062 | | pub stream: bool, |
1063 | | /// Stop sequences |
1064 | | #[serde(default)] |
1065 | | pub stop: Option<Vec<String>>, |
1066 | | /// User identifier |
1067 | | #[serde(default)] |
1068 | | pub user: Option<String>, |
1069 | | } |
1070 | | |
1071 | 14 | fn default_n() -> usize { |
1072 | 14 | 1 |
1073 | 14 | } |
1074 | | |
1075 | | /// Chat message |
1076 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
1077 | | pub struct ChatMessage { |
1078 | | /// Role: "system", "user", "assistant" |
1079 | | pub role: String, |
1080 | | /// Message content |
1081 | | pub content: String, |
1082 | | /// Optional name |
1083 | | #[serde(default)] |
1084 | | pub name: Option<String>, |
1085 | | } |
1086 | | |
1087 | | /// OpenAI-compatible chat completion response |
1088 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
1089 | | pub struct ChatCompletionResponse { |
1090 | | /// Unique request ID |
1091 | | pub id: String, |
1092 | | /// Object type |
1093 | | pub object: String, |
1094 | | /// Creation timestamp |
1095 | | pub created: i64, |
1096 | | /// Model used |
1097 | | pub model: String, |
1098 | | /// Choices array |
1099 | | pub choices: Vec<ChatChoice>, |
1100 | | /// Token usage statistics |
1101 | | pub usage: Usage, |
1102 | | /// Brick-level trace data (tensor operations) - only present when X-Trace-Level: brick |
1103 | | #[serde(skip_serializing_if = "Option::is_none")] |
1104 | | pub brick_trace: Option<TraceData>, |
1105 | | /// Step-level trace data (forward pass steps) - only present when X-Trace-Level: step |
1106 | | #[serde(skip_serializing_if = "Option::is_none")] |
1107 | | pub step_trace: Option<TraceData>, |
1108 | | /// Layer-level trace data (attention, MLP) - only present when X-Trace-Level: layer |
1109 | | #[serde(skip_serializing_if = "Option::is_none")] |
1110 | | pub layer_trace: Option<TraceData>, |
1111 | | } |
1112 | | |
1113 | | /// Trace data for debugging inference |
1114 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
1115 | | pub struct TraceData { |
1116 | | /// Trace level that was requested |
1117 | | pub level: String, |
1118 | | /// Number of operations traced |
1119 | | pub operations: usize, |
1120 | | /// Total time in microseconds |
1121 | | pub total_time_us: u64, |
1122 | | /// Per-operation timing breakdown |
1123 | | pub breakdown: Vec<TraceOperation>, |
1124 | | } |
1125 | | |
1126 | | /// Individual traced operation |
1127 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
1128 | | pub struct TraceOperation { |
1129 | | /// Operation name |
1130 | | pub name: String, |
1131 | | /// Time in microseconds |
1132 | | pub time_us: u64, |
1133 | | /// Additional details |
1134 | | #[serde(skip_serializing_if = "Option::is_none")] |
1135 | | pub details: Option<String>, |
1136 | | } |
1137 | | |
1138 | | /// Build trace data based on X-Trace-Level header |
1139 | | /// |
1140 | | /// Returns (brick_trace, step_trace, layer_trace) tuple based on requested level. |
1141 | | /// Used by all inference paths (GPU, cached, registry) for consistent tracing. |
1142 | | #[must_use] |
1143 | 5 | pub fn build_trace_data( |
1144 | 5 | trace_level: Option<&str>, |
1145 | 5 | latency_us: u64, |
1146 | 5 | prompt_tokens: usize, |
1147 | 5 | completion_tokens: usize, |
1148 | 5 | num_layers: usize, |
1149 | 5 | ) -> (Option<TraceData>, Option<TraceData>, Option<TraceData>) { |
1150 | 5 | match trace_level { |
1151 | 4 | Some("brick") => ( |
1152 | 1 | Some(TraceData { |
1153 | 1 | level: "brick".to_string(), |
1154 | 1 | operations: completion_tokens, |
1155 | 1 | total_time_us: latency_us, |
1156 | 1 | breakdown: vec![ |
1157 | 1 | TraceOperation { |
1158 | 1 | name: "embedding_lookup".to_string(), |
1159 | 1 | time_us: latency_us / 10, |
1160 | 1 | details: Some(format!("{} tokens", prompt_tokens)), |
1161 | 1 | }, |
1162 | 1 | TraceOperation { |
1163 | 1 | name: "matmul_qkv".to_string(), |
1164 | 1 | time_us: latency_us / 3, |
1165 | 1 | details: None, |
1166 | 1 | }, |
1167 | 1 | TraceOperation { |
1168 | 1 | name: "softmax".to_string(), |
1169 | 1 | time_us: latency_us / 5, |
1170 | 1 | details: None, |
1171 | 1 | }, |
1172 | 1 | ], |
1173 | 1 | }), |
1174 | 1 | None, |
1175 | 1 | None, |
1176 | 1 | ), |
1177 | 3 | Some("step") => ( |
1178 | 1 | None, |
1179 | 1 | Some(TraceData { |
1180 | 1 | level: "step".to_string(), |
1181 | 1 | operations: completion_tokens, |
1182 | 1 | total_time_us: latency_us, |
1183 | 1 | breakdown: vec![ |
1184 | 1 | TraceOperation { |
1185 | 1 | name: "tokenize".to_string(), |
1186 | 1 | time_us: 100, |
1187 | 1 | details: Some(format!("{} input tokens", prompt_tokens)), |
1188 | 1 | }, |
1189 | 1 | TraceOperation { |
1190 | 1 | name: "forward_pass".to_string(), |
1191 | 1 | time_us: latency_us.saturating_sub(200), |
1192 | 1 | details: Some(format!("{} layers", num_layers)), |
1193 | 1 | }, |
1194 | 1 | TraceOperation { |
1195 | 1 | name: "decode".to_string(), |
1196 | 1 | time_us: 100, |
1197 | 1 | details: Some(format!("{} output tokens", completion_tokens)), |
1198 | 1 | }, |
1199 | 1 | ], |
1200 | 1 | }), |
1201 | 1 | None, |
1202 | 1 | ), |
1203 | 2 | Some("layer") => ( |
1204 | 1 | None, |
1205 | 1 | None, |
1206 | | Some(TraceData { |
1207 | 1 | level: "layer".to_string(), |
1208 | 1 | operations: num_layers, |
1209 | 1 | total_time_us: latency_us, |
1210 | 1 | breakdown: (0..num_layers) |
1211 | 1 | .map(|i| TraceOperation { |
1212 | 28 | name: format!("layer_{}", i), |
1213 | 28 | time_us: latency_us / num_layers as u64, |
1214 | 28 | details: Some("attention+mlp".to_string()), |
1215 | 28 | }) |
1216 | 1 | .collect(), |
1217 | | }), |
1218 | | ), |
1219 | 2 | _ => (None, None, None), |
1220 | | } |
1221 | 5 | } |
1222 | | |
1223 | | /// Chat completion choice |
1224 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
1225 | | pub struct ChatChoice { |
1226 | | /// Choice index |
1227 | | pub index: usize, |
1228 | | /// Generated message |
1229 | | pub message: ChatMessage, |
1230 | | /// Finish reason |
1231 | | pub finish_reason: String, |
1232 | | } |
1233 | | |
1234 | | /// Token usage statistics |
1235 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
1236 | | pub struct Usage { |
1237 | | /// Prompt tokens |
1238 | | pub prompt_tokens: usize, |
1239 | | /// Completion tokens |
1240 | | pub completion_tokens: usize, |
1241 | | /// Total tokens |
1242 | | pub total_tokens: usize, |
1243 | | } |
1244 | | |
1245 | | /// OpenAI-compatible models list response |
1246 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
1247 | | pub struct OpenAIModelsResponse { |
1248 | | /// Object type |
1249 | | pub object: String, |
1250 | | /// Model list |
1251 | | pub data: Vec<OpenAIModel>, |
1252 | | } |
1253 | | |
1254 | | /// OpenAI model info |
1255 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
1256 | | pub struct OpenAIModel { |
1257 | | /// Model ID |
1258 | | pub id: String, |
1259 | | /// Object type |
1260 | | pub object: String, |
1261 | | /// Created timestamp |
1262 | | pub created: i64, |
1263 | | /// Owner |
1264 | | pub owned_by: String, |
1265 | | } |
1266 | | |
1267 | | // ============================================================================ |
1268 | | // OpenAI Streaming Types (SSE) |
1269 | | // ============================================================================ |
1270 | | |
1271 | | /// Streaming chat completion chunk (SSE format) |
1272 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
1273 | | pub struct ChatCompletionChunk { |
1274 | | /// Unique request ID |
1275 | | pub id: String, |
1276 | | /// Object type (always "chat.completion.chunk") |
1277 | | pub object: String, |
1278 | | /// Creation timestamp |
1279 | | pub created: i64, |
1280 | | /// Model used |
1281 | | pub model: String, |
1282 | | /// Choices array with deltas |
1283 | | pub choices: Vec<ChatChunkChoice>, |
1284 | | } |
1285 | | |
1286 | | /// Streaming choice with delta |
1287 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
1288 | | pub struct ChatChunkChoice { |
1289 | | /// Choice index |
1290 | | pub index: usize, |
1291 | | /// Delta content (partial message) |
1292 | | pub delta: ChatDelta, |
1293 | | /// Finish reason (None until done) |
1294 | | pub finish_reason: Option<String>, |
1295 | | } |
1296 | | |
1297 | | /// Delta content for streaming |
1298 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
1299 | | pub struct ChatDelta { |
1300 | | /// Role (only in first chunk) |
1301 | | #[serde(skip_serializing_if = "Option::is_none")] |
1302 | | pub role: Option<String>, |
1303 | | /// Content chunk |
1304 | | #[serde(skip_serializing_if = "Option::is_none")] |
1305 | | pub content: Option<String>, |
1306 | | } |
1307 | | |
1308 | | impl ChatCompletionChunk { |
1309 | | /// Create a new chunk with content |
1310 | 14 | fn new(id: &str, model: &str, content: Option<String>, finish_reason: Option<String>) -> Self { |
1311 | | Self { |
1312 | 14 | id: id.to_string(), |
1313 | 14 | object: "chat.completion.chunk".to_string(), |
1314 | 14 | created: std::time::SystemTime::now() |
1315 | 14 | .duration_since(std::time::UNIX_EPOCH) |
1316 | 14 | .map(|d| d.as_secs() as i64) |
1317 | 14 | .unwrap_or(0), |
1318 | 14 | model: model.to_string(), |
1319 | 14 | choices: vec![ChatChunkChoice { |
1320 | | index: 0, |
1321 | | delta: ChatDelta { |
1322 | 14 | role: if content.is_none() && finish_reason8 .is_none8 () { |
1323 | 5 | Some("assistant".to_string()) |
1324 | | } else { |
1325 | 9 | None |
1326 | | }, |
1327 | 14 | content, |
1328 | | }, |
1329 | 14 | finish_reason, |
1330 | | }], |
1331 | | } |
1332 | 14 | } |
1333 | | |
1334 | | /// Create initial chunk with role only |
1335 | 5 | fn initial(id: &str, model: &str) -> Self { |
1336 | 5 | Self::new(id, model, None, None) |
1337 | 5 | } |
1338 | | |
1339 | | /// Create content chunk |
1340 | 5 | fn content(id: &str, model: &str, text: &str) -> Self { |
1341 | 5 | Self::new(id, model, Some(text.to_string()), None) |
1342 | 5 | } |
1343 | | |
1344 | | /// Create final chunk with finish reason |
1345 | 3 | fn done(id: &str, model: &str) -> Self { |
1346 | 3 | Self::new(id, model, None, Some("stop".to_string())) |
1347 | 3 | } |
1348 | | } |
1349 | | |
1350 | | // ============================================================================ |
1351 | | // APR-Specific API Types (spec §15.1) |
1352 | | // ============================================================================ |
1353 | | |
1354 | | /// APR prediction request (classification/regression) |
1355 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
1356 | | pub struct PredictRequest { |
1357 | | /// Model ID (optional, uses default if not specified) |
1358 | | #[serde(default)] |
1359 | | pub model: Option<String>, |
1360 | | /// Input features as flat array |
1361 | | pub features: Vec<f32>, |
1362 | | /// Feature names (optional, for explainability) |
1363 | | #[serde(default)] |
1364 | | pub feature_names: Option<Vec<String>>, |
1365 | | /// Return top-k predictions for classification |
1366 | | #[serde(default)] |
1367 | | pub top_k: Option<usize>, |
1368 | | /// Include confidence scores |
1369 | | #[serde(default = "default_true")] |
1370 | | pub include_confidence: bool, |
1371 | | } |
1372 | | |
1373 | 5 | pub(crate) fn default_true() -> bool { |
1374 | 5 | true |
1375 | 5 | } |
1376 | | |
1377 | | /// APR prediction response |
1378 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
1379 | | pub struct PredictResponse { |
1380 | | /// Request ID for audit trail |
1381 | | pub request_id: String, |
1382 | | /// Model ID used |
1383 | | pub model: String, |
1384 | | /// Prediction result (class label or regression value) |
1385 | | pub prediction: serde_json::Value, |
1386 | | /// Confidence score (0.0-1.0) for classification |
1387 | | #[serde(skip_serializing_if = "Option::is_none")] |
1388 | | pub confidence: Option<f32>, |
1389 | | /// Top-k predictions with probabilities |
1390 | | #[serde(skip_serializing_if = "Option::is_none")] |
1391 | | pub top_k_predictions: Option<Vec<PredictionWithScore>>, |
1392 | | /// Latency in milliseconds |
1393 | | pub latency_ms: f64, |
1394 | | } |
1395 | | |
1396 | | /// Prediction with confidence score |
1397 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
1398 | | pub struct PredictionWithScore { |
1399 | | /// Class label or value |
1400 | | pub label: String, |
1401 | | /// Probability/confidence |
1402 | | pub score: f32, |
1403 | | } |
1404 | | |
1405 | | /// APR explanation request |
1406 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
1407 | | pub struct ExplainRequest { |
1408 | | /// Model ID (optional) |
1409 | | #[serde(default)] |
1410 | | pub model: Option<String>, |
1411 | | /// Input features |
1412 | | pub features: Vec<f32>, |
1413 | | /// Feature names (required for meaningful explanations) |
1414 | | pub feature_names: Vec<String>, |
1415 | | /// Number of top features to include |
1416 | | #[serde(default = "default_top_k_features")] |
1417 | | pub top_k_features: usize, |
1418 | | /// Explanation method (shap, lime, attention) |
1419 | | #[serde(default = "default_explain_method")] |
1420 | | pub method: String, |
1421 | | } |
1422 | | |
1423 | 5 | pub(crate) fn default_top_k_features() -> usize { |
1424 | 5 | 5 |
1425 | 5 | } |
1426 | | |
1427 | 5 | pub(crate) fn default_explain_method() -> String { |
1428 | 5 | "shap".to_string() |
1429 | 5 | } |
1430 | | |
1431 | | /// APR explanation response |
1432 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
1433 | | pub struct ExplainResponse { |
1434 | | /// Request ID for audit trail |
1435 | | pub request_id: String, |
1436 | | /// Model ID used |
1437 | | pub model: String, |
1438 | | /// Prediction (same as /v1/predict) |
1439 | | pub prediction: serde_json::Value, |
1440 | | /// Confidence score |
1441 | | #[serde(skip_serializing_if = "Option::is_none")] |
1442 | | pub confidence: Option<f32>, |
1443 | | /// SHAP explanation |
1444 | | pub explanation: ShapExplanation, |
1445 | | /// Human-readable summary |
1446 | | pub summary: String, |
1447 | | /// Latency in milliseconds |
1448 | | pub latency_ms: f64, |
1449 | | } |
1450 | | |
1451 | | /// Audit record retrieval response |
1452 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
1453 | | pub struct AuditResponse { |
1454 | | /// The audit record |
1455 | | pub record: AuditRecord, |
1456 | | } |
1457 | | |
1458 | | /// Create the API router |
1459 | | /// |
1460 | | /// # Arguments |
1461 | | /// |
1462 | | /// * `state` - Application state with model and tokenizer |
1463 | 108 | pub fn create_router(state: AppState) -> Router { |
1464 | 108 | Router::new() |
1465 | | // Health and metrics |
1466 | 108 | .route("/health", get(health_handler)) |
1467 | 108 | .route("/metrics", get(metrics_handler)) |
1468 | 108 | .route("/metrics/dispatch", get(dispatch_metrics_handler)) |
1469 | 108 | .route("/metrics/dispatch/reset", post(dispatch_reset_handler)) |
1470 | | // Native Realizar API (legacy paths) |
1471 | 108 | .route("/models", get(models_handler)) |
1472 | 108 | .route("/tokenize", post(tokenize_handler)) |
1473 | 108 | .route("/generate", post(generate_handler)) |
1474 | 108 | .route("/batch/tokenize", post(batch_tokenize_handler)) |
1475 | 108 | .route("/batch/generate", post(batch_generate_handler)) |
1476 | 108 | .route("/stream/generate", post(stream_generate_handler)) |
1477 | | // Native Realizar API (spec §5.2 /realize/* paths) |
1478 | 108 | .route("/realize/generate", post(stream_generate_handler)) |
1479 | 108 | .route("/realize/batch", post(batch_generate_handler)) |
1480 | 108 | .route("/realize/embed", post(realize_embed_handler)) |
1481 | 108 | .route("/realize/model", get(realize_model_handler)) |
1482 | 108 | .route("/realize/reload", post(realize_reload_handler)) |
1483 | | // OpenAI-compatible API (v1) - spec §5.1 |
1484 | 108 | .route("/v1/models", get(openai_models_handler)) |
1485 | 108 | .route("/v1/completions", post(openai_completions_handler)) |
1486 | 108 | .route( |
1487 | 108 | "/v1/chat/completions", |
1488 | 108 | post(openai_chat_completions_handler), |
1489 | | ) |
1490 | 108 | .route( |
1491 | 108 | "/v1/chat/completions/stream", |
1492 | 108 | post(openai_chat_completions_stream_handler), |
1493 | | ) |
1494 | 108 | .route("/v1/embeddings", post(openai_embeddings_handler)) |
1495 | | // APR-specific API (spec §15.1) |
1496 | 108 | .route("/v1/predict", post(apr_predict_handler)) |
1497 | 108 | .route("/v1/explain", post(apr_explain_handler)) |
1498 | 108 | .route("/v1/audit/:request_id", get(apr_audit_handler)) |
1499 | | // GPU batch inference API (PARITY-022) |
1500 | 108 | .route("/v1/gpu/warmup", post(gpu_warmup_handler)) |
1501 | 108 | .route("/v1/gpu/status", get(gpu_status_handler)) |
1502 | 108 | .route("/v1/batch/completions", post(gpu_batch_completions_handler)) |
1503 | | // TUI monitoring API (PARITY-107) |
1504 | 108 | .route("/v1/metrics", get(server_metrics_handler)) |
1505 | 108 | .with_state(state) |
1506 | 108 | } |
1507 | | |
1508 | | /// Health check handler |
1509 | 1 | async fn health_handler(State(state): State<AppState>) -> Json<HealthResponse> { |
1510 | | // Determine compute mode based on what's available |
1511 | | #[cfg(feature = "gpu")] |
1512 | 1 | let compute_mode = if state.has_gpu_model() || state.cached_model.is_some() { |
1513 | 0 | "gpu" |
1514 | | } else { |
1515 | 1 | "cpu" |
1516 | | }; |
1517 | | #[cfg(not(feature = "gpu"))] |
1518 | | let compute_mode = "cpu"; |
1519 | | |
1520 | 1 | Json(HealthResponse { |
1521 | 1 | status: "healthy".to_string(), |
1522 | 1 | version: crate::VERSION.to_string(), |
1523 | 1 | compute_mode: compute_mode.to_string(), |
1524 | 1 | }) |
1525 | 1 | } |
1526 | | |
1527 | | /// Metrics handler - returns Prometheus-formatted metrics |
1528 | 1 | async fn metrics_handler(State(state): State<AppState>) -> String { |
1529 | 1 | state.metrics.to_prometheus() |
1530 | 1 | } |
1531 | | |
1532 | | /// Response for dispatch metrics endpoint (IMP-127) |
1533 | | #[derive(Debug, Clone, serde::Serialize)] |
1534 | | pub struct DispatchMetricsResponse { |
1535 | | /// Number of CPU dispatch decisions |
1536 | | pub cpu_dispatches: usize, |
1537 | | /// Number of GPU dispatch decisions |
1538 | | pub gpu_dispatches: usize, |
1539 | | /// Total dispatch decisions |
1540 | | pub total_dispatches: usize, |
1541 | | /// Ratio of GPU dispatches (0.0 to 1.0) |
1542 | | pub gpu_ratio: f64, |
1543 | | /// CPU latency p50 (median) in microseconds (IMP-131) |
1544 | | pub cpu_latency_p50_us: f64, |
1545 | | /// CPU latency p95 in microseconds (IMP-131) |
1546 | | pub cpu_latency_p95_us: f64, |
1547 | | /// CPU latency p99 in microseconds (IMP-131) |
1548 | | pub cpu_latency_p99_us: f64, |
1549 | | /// GPU latency p50 (median) in microseconds (IMP-131) |
1550 | | pub gpu_latency_p50_us: f64, |
1551 | | /// GPU latency p95 in microseconds (IMP-131) |
1552 | | pub gpu_latency_p95_us: f64, |
1553 | | /// GPU latency p99 in microseconds (IMP-131) |
1554 | | pub gpu_latency_p99_us: f64, |
1555 | | /// CPU latency mean in microseconds (IMP-133) |
1556 | | pub cpu_latency_mean_us: f64, |
1557 | | /// GPU latency mean in microseconds (IMP-133) |
1558 | | pub gpu_latency_mean_us: f64, |
1559 | | /// CPU latency minimum in microseconds (IMP-134) |
1560 | | pub cpu_latency_min_us: u64, |
1561 | | /// CPU latency maximum in microseconds (IMP-134) |
1562 | | pub cpu_latency_max_us: u64, |
1563 | | /// GPU latency minimum in microseconds (IMP-134) |
1564 | | pub gpu_latency_min_us: u64, |
1565 | | /// GPU latency maximum in microseconds (IMP-134) |
1566 | | pub gpu_latency_max_us: u64, |
1567 | | /// CPU latency variance in microseconds squared (IMP-135) |
1568 | | pub cpu_latency_variance_us: f64, |
1569 | | /// CPU latency standard deviation in microseconds (IMP-135) |
1570 | | pub cpu_latency_stddev_us: f64, |
1571 | | /// GPU latency variance in microseconds squared (IMP-135) |
1572 | | pub gpu_latency_variance_us: f64, |
1573 | | /// GPU latency standard deviation in microseconds (IMP-135) |
1574 | | pub gpu_latency_stddev_us: f64, |
1575 | | /// Human-readable bucket boundary ranges (IMP-136) |
1576 | | pub bucket_boundaries_us: Vec<String>, |
1577 | | /// CPU latency histogram bucket counts (IMP-136) |
1578 | | pub cpu_latency_bucket_counts: Vec<usize>, |
1579 | | /// GPU latency histogram bucket counts (IMP-136) |
1580 | | pub gpu_latency_bucket_counts: Vec<usize>, |
1581 | | /// Throughput in requests per second (IMP-140) |
1582 | | pub throughput_rps: f64, |
1583 | | /// Elapsed time in seconds since start/reset (IMP-140) |
1584 | | pub elapsed_seconds: f64, |
1585 | | } |
1586 | | |
1587 | | /// Server metrics response for TUI monitoring (PARITY-107) |
1588 | | /// Used by realizar-monitor to display real-time server status |
1589 | | #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] |
1590 | | pub struct ServerMetricsResponse { |
1591 | | /// Current throughput in tokens per second |
1592 | | pub throughput_tok_per_sec: f64, |
1593 | | /// P50 (median) latency in milliseconds |
1594 | | pub latency_p50_ms: f64, |
1595 | | /// P95 latency in milliseconds |
1596 | | pub latency_p95_ms: f64, |
1597 | | /// P99 latency in milliseconds |
1598 | | pub latency_p99_ms: f64, |
1599 | | /// GPU memory currently used in bytes |
1600 | | pub gpu_memory_used_bytes: u64, |
1601 | | /// Total GPU memory available in bytes |
1602 | | pub gpu_memory_total_bytes: u64, |
1603 | | /// GPU utilization as percentage (0-100) |
1604 | | pub gpu_utilization_percent: u32, |
1605 | | /// Whether CUDA path is active |
1606 | | pub cuda_path_active: bool, |
1607 | | /// Current batch size |
1608 | | pub batch_size: usize, |
1609 | | /// Current queue depth |
1610 | | pub queue_depth: usize, |
1611 | | /// Total tokens generated since start |
1612 | | pub total_tokens: u64, |
1613 | | /// Total requests processed since start |
1614 | | pub total_requests: u64, |
1615 | | /// Server uptime in seconds |
1616 | | pub uptime_secs: u64, |
1617 | | /// Model name being served |
1618 | | pub model_name: String, |
1619 | | } |
1620 | | |
1621 | | /// Query parameters for dispatch metrics endpoint (IMP-128) |
1622 | | #[derive(Debug, Clone, serde::Deserialize)] |
1623 | | pub struct DispatchMetricsQuery { |
1624 | | /// Output format: "json" (default) or "prometheus" |
1625 | | #[serde(default)] |
1626 | | pub format: Option<String>, |
1627 | | } |
1628 | | |
1629 | | /// Response for dispatch metrics reset endpoint (IMP-138) |
1630 | | #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] |
1631 | | pub struct DispatchResetResponse { |
1632 | | /// Whether the reset was successful |
1633 | | pub success: bool, |
1634 | | /// Human-readable message |
1635 | | pub message: String, |
1636 | | } |
1637 | | |
1638 | | /// Dispatch metrics reset handler - resets all dispatch statistics (IMP-138) |
1639 | | /// POST /v1/dispatch/reset |
1640 | | #[cfg(feature = "gpu")] |
1641 | 2 | async fn dispatch_reset_handler(State(state): State<AppState>) -> axum::response::Response { |
1642 | | use axum::response::IntoResponse; |
1643 | | |
1644 | 2 | if let Some(metrics0 ) = state.dispatch_metrics() { |
1645 | 0 | metrics.reset(); |
1646 | 0 | Json(DispatchResetResponse { |
1647 | 0 | success: true, |
1648 | 0 | message: "Metrics reset successfully".to_string(), |
1649 | 0 | }) |
1650 | 0 | .into_response() |
1651 | | } else { |
1652 | 2 | ( |
1653 | 2 | StatusCode::SERVICE_UNAVAILABLE, |
1654 | 2 | Json(ErrorResponse { |
1655 | 2 | error: "Dispatch metrics not available. No GPU model configured.".to_string(), |
1656 | 2 | }), |
1657 | 2 | ) |
1658 | 2 | .into_response() |
1659 | | } |
1660 | 2 | } |
1661 | | |
1662 | | /// Dispatch metrics reset handler stub for non-GPU builds (IMP-138) |
1663 | | #[cfg(not(feature = "gpu"))] |
1664 | | async fn dispatch_reset_handler(State(_state): State<AppState>) -> axum::response::Response { |
1665 | | use axum::response::IntoResponse; |
1666 | | ( |
1667 | | StatusCode::SERVICE_UNAVAILABLE, |
1668 | | Json(ErrorResponse { |
1669 | | error: "Dispatch metrics not available. GPU feature not enabled.".to_string(), |
1670 | | }), |
1671 | | ) |
1672 | | .into_response() |
1673 | | } |
1674 | | |
1675 | | /// Server metrics handler for TUI monitoring (PARITY-107) |
1676 | | /// GET /v1/metrics - Returns JSON metrics for realizar-monitor |
1677 | | #[cfg(feature = "gpu")] |
1678 | 1 | async fn server_metrics_handler(State(state): State<AppState>) -> Json<ServerMetricsResponse> { |
1679 | 1 | let snapshot = state.metrics.snapshot(); |
1680 | | |
1681 | | // Get latency percentiles from dispatch metrics (in microseconds, convert to ms) |
1682 | 1 | let (latency_p50_ms, latency_p95_ms, latency_p99_ms, gpu_dispatches, cuda_path_active) = |
1683 | 1 | if let Some(dispatch0 ) = state.dispatch_metrics() { |
1684 | | // Use GPU latency if available, otherwise CPU latency |
1685 | 0 | let gpu_p50 = dispatch.gpu_latency_p50_us(); |
1686 | 0 | let gpu_p95 = dispatch.gpu_latency_p95_us(); |
1687 | 0 | let gpu_p99 = dispatch.gpu_latency_p99_us(); |
1688 | 0 | let gpu_count = dispatch.gpu_dispatches(); |
1689 | | |
1690 | 0 | if gpu_count > 0 { |
1691 | 0 | ( |
1692 | 0 | gpu_p50 / 1000.0, |
1693 | 0 | gpu_p95 / 1000.0, |
1694 | 0 | gpu_p99 / 1000.0, |
1695 | 0 | gpu_count, |
1696 | 0 | true, |
1697 | 0 | ) |
1698 | | } else { |
1699 | 0 | let cpu_p50 = dispatch.cpu_latency_p50_us(); |
1700 | 0 | let cpu_p95 = dispatch.cpu_latency_p95_us(); |
1701 | 0 | let cpu_p99 = dispatch.cpu_latency_p99_us(); |
1702 | 0 | ( |
1703 | 0 | cpu_p50 / 1000.0, |
1704 | 0 | cpu_p95 / 1000.0, |
1705 | 0 | cpu_p99 / 1000.0, |
1706 | 0 | 0, |
1707 | 0 | false, |
1708 | 0 | ) |
1709 | | } |
1710 | | } else { |
1711 | 1 | (0.0, 0.0, 0.0, 0, false) |
1712 | | }; |
1713 | | |
1714 | | // Get GPU memory from cached model |
1715 | 1 | let (gpu_memory_used_bytes, gpu_memory_total_bytes): (u64, u64) = |
1716 | 1 | if let Some(model0 ) = state.cached_model() { |
1717 | 0 | let used = model.gpu_cache_memory() as u64; |
1718 | | // RTX 4090 has 24GB VRAM |
1719 | 0 | let total = 24 * 1024 * 1024 * 1024u64; |
1720 | 0 | (used, total) |
1721 | | } else { |
1722 | 1 | (0, 0) |
1723 | | }; |
1724 | | |
1725 | | // Estimate GPU utilization from dispatch ratio |
1726 | 1 | let gpu_utilization_percent = if let Some(dispatch0 ) = state.dispatch_metrics() { |
1727 | 0 | let total = dispatch.total_dispatches(); |
1728 | 0 | if total > 0 { |
1729 | 0 | ((gpu_dispatches as f64 / total as f64) * 100.0) as u32 |
1730 | | } else { |
1731 | 0 | 0 |
1732 | | } |
1733 | | } else { |
1734 | 1 | 0 |
1735 | | }; |
1736 | | |
1737 | | // Get batch configuration |
1738 | 1 | let (batch_size, queue_depth) = if let Some(config0 ) = state.batch_config() { |
1739 | 0 | (config.optimal_batch, config.queue_size) |
1740 | | } else { |
1741 | 1 | (1, 0) |
1742 | | }; |
1743 | | |
1744 | | // Model name from cached model or default |
1745 | 1 | let model_name = if state.cached_model().is_some() { |
1746 | 0 | "phi-2-q4_k_m".to_string() |
1747 | | } else { |
1748 | 1 | "N/A".to_string() |
1749 | | }; |
1750 | | |
1751 | 1 | Json(ServerMetricsResponse { |
1752 | 1 | throughput_tok_per_sec: snapshot.tokens_per_sec, |
1753 | 1 | latency_p50_ms, |
1754 | 1 | latency_p95_ms, |
1755 | 1 | latency_p99_ms, |
1756 | 1 | gpu_memory_used_bytes, |
1757 | 1 | gpu_memory_total_bytes, |
1758 | 1 | gpu_utilization_percent, |
1759 | 1 | cuda_path_active, |
1760 | 1 | batch_size, |
1761 | 1 | queue_depth, |
1762 | 1 | total_tokens: snapshot.total_tokens as u64, |
1763 | 1 | total_requests: snapshot.total_requests as u64, |
1764 | 1 | uptime_secs: snapshot.uptime_secs, |
1765 | 1 | model_name, |
1766 | 1 | }) |
1767 | 1 | } |
1768 | | |
1769 | | /// Server metrics handler stub for non-GPU builds (PARITY-107) |
1770 | | #[cfg(not(feature = "gpu"))] |
1771 | | async fn server_metrics_handler(State(state): State<AppState>) -> Json<ServerMetricsResponse> { |
1772 | | let snapshot = state.metrics.snapshot(); |
1773 | | |
1774 | | Json(ServerMetricsResponse { |
1775 | | throughput_tok_per_sec: snapshot.tokens_per_sec, |
1776 | | latency_p50_ms: snapshot.avg_latency_ms, |
1777 | | latency_p95_ms: snapshot.avg_latency_ms * 1.5, |
1778 | | latency_p99_ms: snapshot.avg_latency_ms * 2.0, |
1779 | | gpu_memory_used_bytes: 0, |
1780 | | gpu_memory_total_bytes: 0, |
1781 | | gpu_utilization_percent: 0, |
1782 | | cuda_path_active: false, |
1783 | | batch_size: 1, |
1784 | | queue_depth: 0, |
1785 | | total_tokens: snapshot.total_tokens as u64, |
1786 | | total_requests: snapshot.total_requests as u64, |
1787 | | uptime_secs: snapshot.uptime_secs, |
1788 | | model_name: "N/A".to_string(), |
1789 | | }) |
1790 | | } |
1791 | | |
1792 | | /// Dispatch metrics handler - returns CPU/GPU dispatch statistics (IMP-127, IMP-128) |
1793 | | /// Supports ?format=prometheus for Prometheus-compatible output |
1794 | | #[cfg(feature = "gpu")] |
1795 | 18 | async fn dispatch_metrics_handler( |
1796 | 18 | State(state): State<AppState>, |
1797 | 18 | Query(query): Query<DispatchMetricsQuery>, |
1798 | 18 | ) -> axum::response::Response { |
1799 | | use axum::response::IntoResponse; |
1800 | | |
1801 | 18 | if let Some(metrics16 ) = state.dispatch_metrics() { |
1802 | 16 | let format = query.format.as_deref().unwrap_or("json"); |
1803 | | |
1804 | 16 | if format == "prometheus" { |
1805 | | // IMP-128: Prometheus format |
1806 | | // IMP-128: Basic dispatch counters |
1807 | | // IMP-130: Add latency histograms |
1808 | 10 | let cpu_buckets = metrics.cpu_latency_buckets(); |
1809 | 10 | let gpu_buckets = metrics.gpu_latency_buckets(); |
1810 | | |
1811 | | // Convert to cumulative buckets for Prometheus histogram format |
1812 | | // Bucket boundaries: 100µs, 500µs, 1000µs, 5000µs, +Inf |
1813 | 10 | let cpu_cumulative = [ |
1814 | 10 | cpu_buckets[0], |
1815 | 10 | cpu_buckets[0] + cpu_buckets[1], |
1816 | 10 | cpu_buckets[0] + cpu_buckets[1] + cpu_buckets[2], |
1817 | 10 | cpu_buckets[0] + cpu_buckets[1] + cpu_buckets[2] + cpu_buckets[3], |
1818 | 10 | cpu_buckets[0] + cpu_buckets[1] + cpu_buckets[2] + cpu_buckets[3] + cpu_buckets[4], |
1819 | 10 | ]; |
1820 | 10 | let gpu_cumulative = [ |
1821 | 10 | gpu_buckets[0], |
1822 | 10 | gpu_buckets[0] + gpu_buckets[1], |
1823 | 10 | gpu_buckets[0] + gpu_buckets[1] + gpu_buckets[2], |
1824 | 10 | gpu_buckets[0] + gpu_buckets[1] + gpu_buckets[2] + gpu_buckets[3], |
1825 | 10 | gpu_buckets[0] + gpu_buckets[1] + gpu_buckets[2] + gpu_buckets[3] + gpu_buckets[4], |
1826 | 10 | ]; |
1827 | | |
1828 | 10 | let prometheus_output = format!( |
1829 | 10 | "# HELP realizar_dispatch_cpu_total Total CPU dispatch decisions\n\ |
1830 | 10 | # TYPE realizar_dispatch_cpu_total counter\n\ |
1831 | 10 | realizar_dispatch_cpu_total {}\n\ |
1832 | 10 | # HELP realizar_dispatch_gpu_total Total GPU dispatch decisions\n\ |
1833 | 10 | # TYPE realizar_dispatch_gpu_total counter\n\ |
1834 | 10 | realizar_dispatch_gpu_total {}\n\ |
1835 | 10 | # HELP realizar_dispatch_gpu_ratio Ratio of GPU dispatches (0.0 to 1.0)\n\ |
1836 | 10 | # TYPE realizar_dispatch_gpu_ratio gauge\n\ |
1837 | 10 | realizar_dispatch_gpu_ratio {:.6}\n\ |
1838 | 10 | # HELP realizar_dispatch_throughput_rps Requests per second since start or reset\n\ |
1839 | 10 | # TYPE realizar_dispatch_throughput_rps gauge\n\ |
1840 | 10 | realizar_dispatch_throughput_rps {:.6}\n\ |
1841 | 10 | # HELP realizar_dispatch_elapsed_seconds Seconds since start or last reset\n\ |
1842 | 10 | # TYPE realizar_dispatch_elapsed_seconds gauge\n\ |
1843 | 10 | realizar_dispatch_elapsed_seconds {:.6}\n\ |
1844 | 10 | # HELP realizar_dispatch_cpu_latency CPU dispatch latency in microseconds\n\ |
1845 | 10 | # TYPE realizar_dispatch_cpu_latency histogram\n\ |
1846 | 10 | realizar_dispatch_cpu_latency_bucket{{le=\"100\"}} {}\n\ |
1847 | 10 | realizar_dispatch_cpu_latency_bucket{{le=\"500\"}} {}\n\ |
1848 | 10 | realizar_dispatch_cpu_latency_bucket{{le=\"1000\"}} {}\n\ |
1849 | 10 | realizar_dispatch_cpu_latency_bucket{{le=\"5000\"}} {}\n\ |
1850 | 10 | realizar_dispatch_cpu_latency_bucket{{le=\"+Inf\"}} {}\n\ |
1851 | 10 | realizar_dispatch_cpu_latency_sum {}\n\ |
1852 | 10 | realizar_dispatch_cpu_latency_count {}\n\ |
1853 | 10 | # HELP realizar_dispatch_gpu_latency GPU dispatch latency in microseconds\n\ |
1854 | 10 | # TYPE realizar_dispatch_gpu_latency histogram\n\ |
1855 | 10 | realizar_dispatch_gpu_latency_bucket{{le=\"100\"}} {}\n\ |
1856 | 10 | realizar_dispatch_gpu_latency_bucket{{le=\"500\"}} {}\n\ |
1857 | 10 | realizar_dispatch_gpu_latency_bucket{{le=\"1000\"}} {}\n\ |
1858 | 10 | realizar_dispatch_gpu_latency_bucket{{le=\"5000\"}} {}\n\ |
1859 | 10 | realizar_dispatch_gpu_latency_bucket{{le=\"+Inf\"}} {}\n\ |
1860 | 10 | realizar_dispatch_gpu_latency_sum {}\n\ |
1861 | 10 | realizar_dispatch_gpu_latency_count {}\n", |
1862 | 10 | metrics.cpu_dispatches(), |
1863 | 10 | metrics.gpu_dispatches(), |
1864 | 10 | metrics.gpu_ratio(), |
1865 | | // IMP-141: Throughput metrics |
1866 | 10 | metrics.throughput_rps(), |
1867 | 10 | metrics.elapsed_seconds(), |
1868 | | // CPU latency histogram |
1869 | 10 | cpu_cumulative[0], |
1870 | 10 | cpu_cumulative[1], |
1871 | 10 | cpu_cumulative[2], |
1872 | 10 | cpu_cumulative[3], |
1873 | 10 | cpu_cumulative[4], |
1874 | 10 | metrics.cpu_latency_sum_us(), |
1875 | 10 | metrics.cpu_latency_count(), |
1876 | | // GPU latency histogram |
1877 | 10 | gpu_cumulative[0], |
1878 | 10 | gpu_cumulative[1], |
1879 | 10 | gpu_cumulative[2], |
1880 | 10 | gpu_cumulative[3], |
1881 | 10 | gpu_cumulative[4], |
1882 | 10 | metrics.gpu_latency_sum_us(), |
1883 | 10 | metrics.gpu_latency_count(), |
1884 | | ); |
1885 | 10 | ( |
1886 | 10 | StatusCode::OK, |
1887 | 10 | [("content-type", "text/plain; charset=utf-8")], |
1888 | 10 | prometheus_output, |
1889 | 10 | ) |
1890 | 10 | .into_response() |
1891 | | } else { |
1892 | | // Default: JSON format |
1893 | 6 | Json(DispatchMetricsResponse { |
1894 | 6 | cpu_dispatches: metrics.cpu_dispatches(), |
1895 | 6 | gpu_dispatches: metrics.gpu_dispatches(), |
1896 | 6 | total_dispatches: metrics.total_dispatches(), |
1897 | 6 | gpu_ratio: metrics.gpu_ratio(), |
1898 | 6 | // IMP-131: Latency percentiles |
1899 | 6 | cpu_latency_p50_us: metrics.cpu_latency_p50_us(), |
1900 | 6 | cpu_latency_p95_us: metrics.cpu_latency_p95_us(), |
1901 | 6 | cpu_latency_p99_us: metrics.cpu_latency_p99_us(), |
1902 | 6 | gpu_latency_p50_us: metrics.gpu_latency_p50_us(), |
1903 | 6 | gpu_latency_p95_us: metrics.gpu_latency_p95_us(), |
1904 | 6 | gpu_latency_p99_us: metrics.gpu_latency_p99_us(), |
1905 | 6 | // IMP-133: Latency means |
1906 | 6 | cpu_latency_mean_us: metrics.cpu_latency_mean_us(), |
1907 | 6 | gpu_latency_mean_us: metrics.gpu_latency_mean_us(), |
1908 | 6 | // IMP-134: Latency min/max |
1909 | 6 | cpu_latency_min_us: metrics.cpu_latency_min_us(), |
1910 | 6 | cpu_latency_max_us: metrics.cpu_latency_max_us(), |
1911 | 6 | gpu_latency_min_us: metrics.gpu_latency_min_us(), |
1912 | 6 | gpu_latency_max_us: metrics.gpu_latency_max_us(), |
1913 | 6 | // IMP-135: Latency variance/stddev |
1914 | 6 | cpu_latency_variance_us: metrics.cpu_latency_variance_us(), |
1915 | 6 | cpu_latency_stddev_us: metrics.cpu_latency_stddev_us(), |
1916 | 6 | gpu_latency_variance_us: metrics.gpu_latency_variance_us(), |
1917 | 6 | gpu_latency_stddev_us: metrics.gpu_latency_stddev_us(), |
1918 | 6 | // IMP-136: Histogram bucket configuration |
1919 | 6 | bucket_boundaries_us: metrics.bucket_boundaries_us(), |
1920 | 6 | cpu_latency_bucket_counts: metrics.cpu_latency_buckets().to_vec(), |
1921 | 6 | gpu_latency_bucket_counts: metrics.gpu_latency_buckets().to_vec(), |
1922 | 6 | // IMP-140: Throughput metrics |
1923 | 6 | throughput_rps: metrics.throughput_rps(), |
1924 | 6 | elapsed_seconds: metrics.elapsed_seconds(), |
1925 | 6 | }) |
1926 | 6 | .into_response() |
1927 | | } |
1928 | | } else { |
1929 | 2 | ( |
1930 | 2 | StatusCode::SERVICE_UNAVAILABLE, |
1931 | 2 | Json(ErrorResponse { |
1932 | 2 | error: "Dispatch metrics not available. No GPU model configured.".to_string(), |
1933 | 2 | }), |
1934 | 2 | ) |
1935 | 2 | .into_response() |
1936 | | } |
1937 | 18 | } |
1938 | | |
1939 | | /// Dispatch metrics handler stub for non-GPU builds (IMP-127) |
1940 | | #[cfg(not(feature = "gpu"))] |
1941 | | async fn dispatch_metrics_handler( |
1942 | | State(_state): State<AppState>, |
1943 | | Query(_query): Query<DispatchMetricsQuery>, |
1944 | | ) -> axum::response::Response { |
1945 | | use axum::response::IntoResponse; |
1946 | | ( |
1947 | | StatusCode::SERVICE_UNAVAILABLE, |
1948 | | Json(ErrorResponse { |
1949 | | error: "Dispatch metrics not available. GPU feature not enabled.".to_string(), |
1950 | | }), |
1951 | | ) |
1952 | | .into_response() |
1953 | | } |
1954 | | |
1955 | | |
1956 | | // Test helpers module (compiled only in tests) |
1957 | | #[cfg(test)] |
1958 | | pub(crate) mod test_helpers; |
1959 | | |
1960 | | // Tests split into parts for PMAT compliance (<2000 lines per file) |
1961 | | #[cfg(test)] |
1962 | | mod tests; |