1use crate::types::*;
7use crate::models_v2::llama::{LlamaModelV2, LlamaConfig};
8use crate::model_core::{Model, GenerationConfig, ModelInputs, ModelConfig};
9use crate::tokenizer::Tokenizer;
10use crate::tensor_core::{Tensor, Device};
11
12pub struct Sampler {
16 _tensor_ops: crate::tensor_core::CpuTensorOpsImpl,
17}
18
19impl Sampler {
20 pub fn new() -> Self {
21 Self {
22 _tensor_ops: crate::tensor_core::CpuTensorOpsImpl::new(),
23 }
24 }
25
26 pub fn sample_greedy(&self, logits: &[f32]) -> ModelResult<u32> {
28 if logits.is_empty() {
29 return Err(ModelError::ComputationFailed("Empty logits".to_string()));
30 }
31
32 let mut max_idx = 0;
33 let mut max_val = logits[0];
34
35 for (idx, &val) in logits.iter().enumerate() {
36 if val > max_val {
37 max_val = val;
38 max_idx = idx;
39 }
40 }
41
42 Ok(max_idx as u32)
43 }
44
45 pub fn sample_temperature(&self, logits: &[f32], temperature: f32) -> ModelResult<u32> {
47 if logits.is_empty() {
48 return Err(ModelError::ComputationFailed("Empty logits".to_string()));
49 }
50
51 let scaled_logits: Vec<f32> = logits.iter().map(|&x| x / temperature).collect();
53
54 let probabilities = self.softmax(&scaled_logits)?;
56
57 self.sample_from_probabilities(&probabilities)
59 }
60
61 pub fn sample_top_p(&self, logits: &[f32], top_p: f32, temperature: f32) -> ModelResult<u32> {
63 if logits.is_empty() {
64 return Err(ModelError::ComputationFailed("Empty logits".to_string()));
65 }
66
67 let scaled_logits: Vec<f32> = logits.iter().map(|&x| x / temperature).collect();
69
70 let probabilities = self.softmax(&scaled_logits)?;
72
73 let mut indexed_probs: Vec<(usize, f32)> = probabilities
75 .iter()
76 .enumerate()
77 .map(|(i, &p)| (i, p))
78 .collect();
79 indexed_probs.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
80
81 let mut cumulative_prob = 0.0;
83 let mut cutoff_idx = indexed_probs.len();
84
85 for (i, (_, prob)) in indexed_probs.iter().enumerate() {
86 cumulative_prob += prob;
87 if cumulative_prob >= top_p {
88 cutoff_idx = i + 1;
89 break;
90 }
91 }
92
93 let mut filtered_probs = vec![0.0; logits.len()];
95 let mut total_prob = 0.0;
96
97 for (idx, prob) in indexed_probs.iter().take(cutoff_idx) {
98 filtered_probs[*idx] = *prob;
99 total_prob += prob;
100 }
101
102 if total_prob > 0.0 {
104 for prob in &mut filtered_probs {
105 *prob /= total_prob;
106 }
107 }
108
109 self.sample_from_probabilities(&filtered_probs)
110 }
111
112 fn softmax(&self, logits: &[f32]) -> ModelResult<Vec<f32>> {
113 if logits.is_empty() {
114 return Ok(Vec::new());
115 }
116
117 let max_val = logits.iter().fold(f32::NEG_INFINITY, |a, &b| a.max(b));
119
120 let mut exp_vals = Vec::with_capacity(logits.len());
122 let mut sum = 0.0;
123
124 for &logit in logits {
125 let exp_val = (logit - max_val).exp();
126 exp_vals.push(exp_val);
127 sum += exp_val;
128 }
129
130 for exp_val in &mut exp_vals {
132 *exp_val /= sum;
133 }
134
135 Ok(exp_vals)
136 }
137
138 fn sample_from_probabilities(&self, probabilities: &[f32]) -> ModelResult<u32> {
139 use rand::Rng;
140
141 let mut rng = rand::thread_rng();
143 let random_val: f32 = rng.gen();
144 let mut cumulative = 0.0;
145
146 for (idx, &prob) in probabilities.iter().enumerate() {
147 cumulative += prob;
148 if random_val <= cumulative {
149 return Ok(idx as u32);
150 }
151 }
152
153 Ok((probabilities.len() - 1) as u32)
155 }
156}
157
158pub struct InferencePipeline {
160 model: LlamaModelV2,
161 tokenizer: Tokenizer,
162 sampler: Sampler,
163}
164
165impl InferencePipeline {
166 pub fn new(model: LlamaModelV2, tokenizer: Tokenizer) -> Self {
168 Self {
169 model,
170 tokenizer,
171 sampler: Sampler::new(),
172 }
173 }
174
175 pub fn generate(&self, prompt: &str, config: &GenerationConfig) -> ModelResult<String> {
177 let input_tokens = self.tokenizer.encode(prompt);
179
180 let generated_tokens = self.generate_tokens(&input_tokens, config)?;
182
183 let output_text = self.tokenizer.decode(&generated_tokens);
185
186 Ok(output_text)
187 }
188
189 fn generate_tokens(&self, input_tokens: &[u32], config: &GenerationConfig) -> ModelResult<Vec<u32>> {
191 let mut current_tokens = input_tokens.to_vec();
192 let mut generated_count = 0;
193
194 while generated_count < config.max_new_tokens {
195 let input_tensor = self.create_input_tensor(¤t_tokens)?;
197 let inputs = ModelInputs::Text {
198 input_ids: input_tensor,
199 attention_mask: None,
200 position_ids: None
201 };
202
203 let outputs = self.model.forward(&inputs).map_err(|e| ModelError::ComputationFailed(format!("Forward pass failed: {}", e)))?;
205
206 let logits = match outputs {
208 crate::model_core::ModelOutputs::Logits { logits, .. } => logits,
209 _ => return Err(ModelError::ComputationFailed("Expected logits output".to_string())),
210 };
211
212 let last_token_logits = self.extract_last_token_logits(&logits)?;
214
215 let next_token = if config.do_sample {
217 if config.top_p < 1.0 {
218 self.sampler.sample_top_p(&last_token_logits, config.top_p, config.temperature)?
219 } else {
220 self.sampler.sample_temperature(&last_token_logits, config.temperature)?
221 }
222 } else {
223 self.sampler.sample_greedy(&last_token_logits)?
224 };
225
226 if next_token == config.eos_token_id {
228 break;
229 }
230
231 current_tokens.push(next_token);
233 generated_count += 1;
234 }
235
236 Ok(current_tokens)
237 }
238
239 fn create_input_tensor(&self, tokens: &[u32]) -> ModelResult<Tensor> {
241 let tokens_i64: Vec<i64> = tokens.iter().map(|&t| t as i64).collect();
243 let shape = &[1, tokens.len()]; Tensor::from_i64_slice(&tokens_i64, shape, &Device::CPU)
246 .map_err(|e| ModelError::ComputationFailed(format!("Failed to create input tensor: {}", e)))
247 }
248
249 fn extract_last_token_logits(&self, logits: &Tensor) -> ModelResult<Vec<f32>> {
251 let shape = logits.shape();
255 if shape.len() < 2 {
256 return Err(ModelError::ComputationFailed(
257 format!("Logits tensor has unexpected shape: {:?}", shape)
258 ));
259 }
260
261 let candle_logits = logits.to_candle()
263 .map_err(|e| ModelError::ComputationFailed(format!("Failed to convert logits: {}", e)))?;
264
265 let vocab_size = if shape.len() == 3 {
267 shape[2]
268 } else if shape.len() == 2 {
269 shape[1]
270 } else {
271 return Err(ModelError::ComputationFailed(
272 format!("Unexpected logits shape: {:?}", shape)
273 ));
274 };
275
276 let last_logits = if shape.len() == 3 {
279 let seq_len = shape[1];
280 candle_logits
281 .narrow(1, seq_len - 1, 1)
282 .map_err(|e| ModelError::ComputationFailed(format!("Failed to narrow: {}", e)))?
283 .squeeze(1)
284 .map_err(|e| ModelError::ComputationFailed(format!("Failed to squeeze: {}", e)))?
285 .squeeze(0)
286 .map_err(|e| ModelError::ComputationFailed(format!("Failed to squeeze batch: {}", e)))?
287 } else {
288 let seq_len = shape[0];
290 candle_logits
291 .narrow(0, seq_len - 1, 1)
292 .map_err(|e| ModelError::ComputationFailed(format!("Failed to narrow: {}", e)))?
293 .squeeze(0)
294 .map_err(|e| ModelError::ComputationFailed(format!("Failed to squeeze: {}", e)))?
295 };
296
297 let logits_vec: Vec<f32> = last_logits
299 .to_vec1()
300 .map_err(|e| ModelError::ComputationFailed(format!("Failed to convert to vec: {}", e)))?;
301
302 if logits_vec.len() != vocab_size {
303 return Err(ModelError::ComputationFailed(
304 format!("Logits size mismatch: got {}, expected {}", logits_vec.len(), vocab_size)
305 ));
306 }
307
308 Ok(logits_vec)
309 }
310
311 pub fn model_config(&self) -> &LlamaConfig {
313 self.model.config()
314 }
315
316 pub fn tokenizer(&self) -> &Tokenizer {
318 &self.tokenizer
319 }
320}
321
322pub struct InferencePipelineBuilder {
324 model_config: Option<LlamaConfig>,
325 tokenizer: Option<Tokenizer>,
326}
327
328impl InferencePipelineBuilder {
329 pub fn new() -> Self {
330 Self {
331 model_config: None,
332 tokenizer: None,
333 }
334 }
335
336 pub fn with_model_config(mut self, config: LlamaConfig) -> Self {
337 self.model_config = Some(config);
338 self
339 }
340
341 pub fn with_tokenizer(mut self, tokenizer: Tokenizer) -> Self {
342 self.tokenizer = Some(tokenizer);
343 self
344 }
345
346 pub fn build(self) -> ModelResult<InferencePipeline> {
347 let model_config = self.model_config.ok_or_else(|| {
348 ModelError::InitializationFailed("Model config not provided".to_string())
349 })?;
350
351 let tokenizer = self.tokenizer.unwrap_or_else(Tokenizer::new);
352
353 let model = LlamaModelV2::new(model_config)
355 .map_err(|e| ModelError::InitializationFailed(format!("Failed to create model: {}", e)))?;
356
357 Ok(InferencePipeline::new(model, tokenizer))
358 }
359}
360
361#[derive(Debug, Clone)]
363pub struct InferenceMetrics {
364 pub prompt_tokens: usize,
365 pub generated_tokens: usize,
366 pub total_tokens: usize,
367 pub inference_time_ms: f64,
368 pub tokens_per_second: f64,
369}
370
371impl InferenceMetrics {
372 pub fn new(prompt_tokens: usize, generated_tokens: usize, inference_time_ms: f64) -> Self {
373 let total_tokens = prompt_tokens + generated_tokens;
374 let tokens_per_second = if inference_time_ms > 0.0 {
375 (total_tokens as f64) / (inference_time_ms / 1000.0)
376 } else {
377 0.0
378 };
379
380 Self {
381 prompt_tokens,
382 generated_tokens,
383 total_tokens,
384 inference_time_ms,
385 tokens_per_second,
386 }
387 }
388}
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393
394 #[test]
395 fn test_generation_config() {
396 let config = GenerationConfig::default();
397 assert_eq!(config.max_new_tokens, 100);
398 assert_eq!(config.temperature, 1.0);
399 assert_eq!(config.top_p, 0.9);
400 assert!(config.do_sample); }
402
403 #[test]
404 fn test_sampler_greedy() {
405 let sampler = Sampler::new();
406 let logits = vec![0.1, 0.8, 0.3, 0.5];
407
408 let token = sampler.sample_greedy(&logits).unwrap();
409 assert_eq!(token, 1); }
411
412 #[test]
413 fn test_sampler_empty_logits() {
414 let sampler = Sampler::new();
415 let logits = vec![];
416
417 let result = sampler.sample_greedy(&logits);
418 assert!(result.is_err());
419 }
420
421 #[test]
422 fn test_sampler_temperature() {
423 let sampler = Sampler::new();
424 let logits = vec![1.0, 2.0, 1.5];
425
426 let token = sampler.sample_temperature(&logits, 1.0).unwrap();
427 assert!(token < 3); }
429
430 #[test]
431 fn test_sampler_top_p() {
432 let sampler = Sampler::new();
433 let logits = vec![1.0, 3.0, 2.0, 0.5];
434
435 let token = sampler.sample_top_p(&logits, 0.8, 1.0).unwrap();
436 assert!(token < 4); }
438
439 #[test]
440 fn test_softmax() {
441 let sampler = Sampler::new();
442 let logits = vec![1.0, 2.0, 3.0];
443
444 let probs = sampler.softmax(&logits).unwrap();
445 assert_eq!(probs.len(), 3);
446
447 let sum: f32 = probs.iter().sum();
449 assert!((sum - 1.0).abs() < 1e-6);
450
451 assert!(probs[0] < probs[1]);
453 assert!(probs[1] < probs[2]);
454 }
455
456 #[test]
457 fn test_inference_pipeline_builder() {
458 let config = LlamaConfig {
459 vocab_size: 1000,
460 hidden_size: 128,
461 num_hidden_layers: 2,
462 num_attention_heads: 8,
463 ..Default::default()
464 };
465
466 let tokenizer = Tokenizer::new();
467
468 let pipeline = InferencePipelineBuilder::new()
469 .with_model_config(config.clone())
470 .with_tokenizer(tokenizer)
471 .build()
472 .unwrap();
473
474 assert_eq!(pipeline.model_config().vocab_size(), config.vocab_size());
475 assert_eq!(pipeline.model_config().hidden_size(), config.hidden_size());
476 }
477
478 #[test]
479 fn test_inference_pipeline_generation() {
480 let config = LlamaConfig {
481 vocab_size: 1000,
482 hidden_size: 64,
483 num_hidden_layers: 1,
484 num_attention_heads: 4,
485 intermediate_size: 128,
486 max_position_embeddings: 64,
487 ..Default::default()
488 };
489
490 let pipeline = InferencePipelineBuilder::new()
491 .with_model_config(config)
492 .build()
493 .unwrap();
494
495 let gen_config = GenerationConfig {
496 max_new_tokens: 5,
497 temperature: 1.0,
498 do_sample: false, ..Default::default()
500 };
501
502 let result = pipeline.generate("hello world", &gen_config);
503 match result {
506 Ok(output) => {
507 assert!(!output.is_empty());
508 println!("Generated: {}", output);
509 }
510 Err(e) => {
511 assert!(e.to_string().contains("matmul") || e.to_string().contains("Forward pass failed"));
513 println!("Expected error with dummy tensors: {}", e);
514 }
515 }
516 }
517
518 #[test]
519 fn test_inference_metrics() {
520 let metrics = InferenceMetrics::new(10, 20, 1000.0);
521
522 assert_eq!(metrics.prompt_tokens, 10);
523 assert_eq!(metrics.generated_tokens, 20);
524 assert_eq!(metrics.total_tokens, 30);
525 assert_eq!(metrics.inference_time_ms, 1000.0);
526 assert_eq!(metrics.tokens_per_second, 30.0);
527 }
528
529 #[test]
530 fn test_empty_prompt() {
531 let config = LlamaConfig {
532 vocab_size: 500,
533 hidden_size: 32,
534 num_hidden_layers: 1,
535 num_attention_heads: 2,
536 intermediate_size: 64,
537 max_position_embeddings: 32,
538 ..Default::default()
539 };
540
541 let pipeline = InferencePipelineBuilder::new()
542 .with_model_config(config)
543 .build()
544 .unwrap();
545
546 let gen_config = GenerationConfig {
547 max_new_tokens: 3,
548 ..Default::default()
549 };
550
551 let result = pipeline.generate("", &gen_config);
552 match result {
555 Ok(output) => {
556 println!("Generated from empty prompt: '{}'", output);
557 }
558 Err(e) => {
559 assert!(e.to_string().contains("matmul") || e.to_string().contains("Forward pass failed"));
561 println!("Expected error with dummy tensors: {}", e);
562 }
563 }
564 }
565
566 #[test]
567 fn test_builder_missing_config() {
568 let result = InferencePipelineBuilder::new().build();
569 assert!(result.is_err());
570 }
571}