1use std::marker::PhantomData;
2
3use llama_cpp_bindings_sys::{
4 llama_batch, llama_batch_free, llama_batch_init, llama_pos, llama_seq_id,
5};
6
7use crate::batch_add_error::BatchAddError;
8use crate::sampled_token::SampledToken;
9use crate::token::LlamaToken;
10
11fn checked_n_tokens_plus_one_as_usize(n_tokens: i32) -> Result<usize, BatchAddError> {
12 let incremented = n_tokens.checked_add(1).ok_or_else(|| {
13 BatchAddError::IntegerOverflow(format!("n_tokens + 1 overflows i32: {n_tokens}"))
14 })?;
15
16 usize::try_from(incremented).map_err(|convert_error| {
17 BatchAddError::IntegerOverflow(format!("cannot fit n_tokens into a usize: {convert_error}"))
18 })
19}
20
21fn checked_i32_as_usize(value: i32, description: &str) -> Result<usize, BatchAddError> {
22 usize::try_from(value).map_err(|convert_error| {
23 BatchAddError::IntegerOverflow(format!(
24 "cannot fit {description} into a usize: {convert_error}"
25 ))
26 })
27}
28
29fn checked_usize_as_llama_seq_id(
30 value: usize,
31 description: &str,
32) -> Result<llama_seq_id, BatchAddError> {
33 llama_seq_id::try_from(value).map_err(|convert_error| {
34 BatchAddError::IntegerOverflow(format!(
35 "cannot fit {description} into a llama_seq_id: {convert_error}"
36 ))
37 })
38}
39
40fn checked_usize_as_i32(value: usize, description: &str) -> Result<i32, BatchAddError> {
41 i32::try_from(value).map_err(|convert_error| {
42 BatchAddError::IntegerOverflow(format!(
43 "cannot fit {description} into a i32: {convert_error}"
44 ))
45 })
46}
47
48fn checked_usize_as_llama_pos(value: usize, description: &str) -> Result<llama_pos, BatchAddError> {
49 llama_pos::try_from(value).map_err(|convert_error| {
50 BatchAddError::IntegerOverflow(format!(
51 "cannot fit {description} into a llama_pos: {convert_error}"
52 ))
53 })
54}
55
56#[derive(Debug)]
57pub struct LlamaBatch<'tokens> {
58 allocated: usize,
59 pub initialized_logits: Vec<i32>,
60 pub llama_batch: llama_batch,
61 phantom: PhantomData<&'tokens [LlamaToken]>,
62}
63
64impl<'tokens> LlamaBatch<'tokens> {
65 pub fn clear(&mut self) {
66 self.llama_batch.n_tokens = 0;
67 self.initialized_logits.clear();
68 }
69
70 pub fn add(
74 &mut self,
75 sampled_token: &SampledToken,
76 pos: llama_pos,
77 seq_ids: &[i32],
78 logits: bool,
79 ) -> Result<(), BatchAddError> {
80 let (SampledToken::Content(LlamaToken(id))
81 | SampledToken::Reasoning(LlamaToken(id))
82 | SampledToken::ToolCall(LlamaToken(id))
83 | SampledToken::Undeterminable(LlamaToken(id))) = *sampled_token;
84 let required = checked_n_tokens_plus_one_as_usize(self.n_tokens())?;
85
86 if self.allocated < required {
87 return Err(BatchAddError::InsufficientSpace(self.allocated));
88 }
89
90 let offset = self.llama_batch.n_tokens;
91 let offset_usize = checked_i32_as_usize(offset, "n_tokens")?;
92 let n_seq_id = checked_usize_as_llama_seq_id(seq_ids.len(), "seq_ids.len()")?;
93
94 unsafe {
95 self.llama_batch.token.add(offset_usize).write(id);
96 self.llama_batch.pos.add(offset_usize).write(pos);
97 self.llama_batch.n_seq_id.add(offset_usize).write(n_seq_id);
98 for (seq_index, seq_id) in seq_ids.iter().enumerate() {
99 let tmp = *self.llama_batch.seq_id.add(offset_usize);
100 tmp.add(seq_index).write(*seq_id);
101 }
102 self.llama_batch
103 .logits
104 .add(offset_usize)
105 .write(i8::from(logits));
106 }
107
108 if logits {
109 self.initialized_logits.push(offset);
110 }
111
112 self.llama_batch.n_tokens += 1;
113
114 Ok(())
115 }
116
117 pub fn add_sequence(
121 &mut self,
122 tokens: &[LlamaToken],
123 seq_id: i32,
124 logits_all: bool,
125 ) -> Result<(), BatchAddError> {
126 let last_index = checked_usize_as_llama_pos(tokens.len().saturating_sub(1), "n_tokens")?;
127
128 for (position, token) in (0..).zip(tokens.iter()) {
129 self.add(
130 &SampledToken::Content(*token),
131 position,
132 &[seq_id],
133 logits_all || position == last_index,
134 )?;
135 }
136
137 Ok(())
138 }
139
140 pub fn new(n_tokens: usize, n_seq_max: i32) -> Result<Self, BatchAddError> {
144 let n_tokens_i32 = checked_usize_as_i32(n_tokens, "n_tokens")?;
145 let batch = unsafe { llama_batch_init(n_tokens_i32, 0, n_seq_max) };
146
147 Ok(LlamaBatch {
148 allocated: n_tokens,
149 initialized_logits: vec![],
150 llama_batch: batch,
151 phantom: PhantomData,
152 })
153 }
154
155 pub fn get_one(tokens: &'tokens [LlamaToken]) -> Result<Self, BatchAddError> {
159 if tokens.is_empty() {
160 return Err(BatchAddError::EmptyBuffer);
161 }
162
163 let token_count = checked_usize_as_i32(tokens.len(), "token count")?;
164
165 let batch = unsafe {
166 let ptr = tokens.as_ptr().cast::<i32>().cast_mut();
167 llama_cpp_bindings_sys::llama_batch_get_one(ptr, token_count)
168 };
169
170 let last_token_index = checked_usize_as_i32(tokens.len() - 1, "last token index")?;
171
172 Ok(Self {
173 allocated: 0,
174 initialized_logits: vec![last_token_index],
175 llama_batch: batch,
176 phantom: PhantomData,
177 })
178 }
179
180 #[must_use]
181 pub const fn n_tokens(&self) -> i32 {
182 self.llama_batch.n_tokens
183 }
184}
185
186impl Drop for LlamaBatch<'_> {
187 fn drop(&mut self) {
188 unsafe {
189 if self.allocated > 0 {
190 llama_batch_free(self.llama_batch);
191 }
192 }
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use crate::sampled_token::SampledToken;
199 use crate::token::LlamaToken;
200
201 use super::{
202 BatchAddError, LlamaBatch, checked_i32_as_usize, checked_n_tokens_plus_one_as_usize,
203 checked_usize_as_i32, checked_usize_as_llama_pos, checked_usize_as_llama_seq_id,
204 };
205
206 #[test]
207 fn new_creates_empty_batch() {
208 let batch = LlamaBatch::new(16, 1).unwrap();
209
210 assert_eq!(batch.n_tokens(), 0);
211 assert!(batch.initialized_logits.is_empty());
212 }
213
214 #[test]
215 fn clear_resets_batch() {
216 let mut batch = LlamaBatch::new(16, 1).unwrap();
217 batch
218 .add(&SampledToken::Content(LlamaToken::new(1)), 0, &[0], true)
219 .unwrap();
220 assert_eq!(batch.n_tokens(), 1);
221
222 batch.clear();
223
224 assert_eq!(batch.n_tokens(), 0);
225 assert!(batch.initialized_logits.is_empty());
226 }
227
228 #[test]
229 fn add_increments_token_count() {
230 let mut batch = LlamaBatch::new(16, 1).unwrap();
231
232 batch
233 .add(&SampledToken::Content(LlamaToken::new(1)), 0, &[0], false)
234 .unwrap();
235 assert_eq!(batch.n_tokens(), 1);
236
237 batch
238 .add(&SampledToken::Content(LlamaToken::new(2)), 1, &[0], false)
239 .unwrap();
240 assert_eq!(batch.n_tokens(), 2);
241 }
242
243 #[test]
244 fn add_tracks_logits() {
245 let mut batch = LlamaBatch::new(16, 1).unwrap();
246
247 batch
248 .add(&SampledToken::Content(LlamaToken::new(1)), 0, &[0], false)
249 .unwrap();
250 assert!(batch.initialized_logits.is_empty());
251
252 batch
253 .add(&SampledToken::Content(LlamaToken::new(2)), 1, &[0], true)
254 .unwrap();
255 assert_eq!(batch.initialized_logits, vec![1]);
256 }
257
258 #[test]
259 fn add_returns_insufficient_space_when_full() {
260 let mut batch = LlamaBatch::new(1, 1).unwrap();
261 batch
262 .add(&SampledToken::Content(LlamaToken::new(1)), 0, &[0], false)
263 .unwrap();
264
265 let result = batch.add(&SampledToken::Content(LlamaToken::new(2)), 1, &[0], false);
266
267 assert_eq!(result, Err(BatchAddError::InsufficientSpace(1)));
268 }
269
270 #[test]
271 fn add_accepts_reasoning_sampled_token_variant() {
272 let mut batch = LlamaBatch::new(4, 1).unwrap();
273
274 batch
275 .add(&SampledToken::Reasoning(LlamaToken::new(11)), 0, &[0], true)
276 .unwrap();
277
278 assert_eq!(batch.n_tokens(), 1);
279 }
280
281 #[test]
282 fn add_accepts_tool_call_sampled_token_variant() {
283 let mut batch = LlamaBatch::new(4, 1).unwrap();
284
285 batch
286 .add(&SampledToken::ToolCall(LlamaToken::new(22)), 0, &[0], true)
287 .unwrap();
288
289 assert_eq!(batch.n_tokens(), 1);
290 }
291
292 #[test]
293 fn add_accepts_undeterminable_sampled_token_variant() {
294 let mut batch = LlamaBatch::new(4, 1).unwrap();
295
296 batch
297 .add(
298 &SampledToken::Undeterminable(LlamaToken::new(33)),
299 0,
300 &[0],
301 false,
302 )
303 .unwrap();
304
305 assert_eq!(batch.n_tokens(), 1);
306 }
307
308 #[test]
309 fn add_sequence_adds_all_tokens() {
310 let mut batch = LlamaBatch::new(16, 1).unwrap();
311 let tokens = vec![
312 LlamaToken::new(10),
313 LlamaToken::new(20),
314 LlamaToken::new(30),
315 ];
316
317 batch.add_sequence(&tokens, 0, false).unwrap();
318
319 assert_eq!(batch.n_tokens(), 3);
320 }
321
322 #[test]
323 fn add_sequence_sets_logits_on_last_token() {
324 let mut batch = LlamaBatch::new(16, 1).unwrap();
325 let tokens = vec![
326 LlamaToken::new(10),
327 LlamaToken::new(20),
328 LlamaToken::new(30),
329 ];
330
331 batch.add_sequence(&tokens, 0, false).unwrap();
332
333 assert_eq!(batch.initialized_logits, vec![2]);
334 }
335
336 #[test]
337 fn add_sequence_insufficient_space() {
338 let mut batch = LlamaBatch::new(2, 1).unwrap();
339 let tokens = vec![
340 LlamaToken::new(10),
341 LlamaToken::new(20),
342 LlamaToken::new(30),
343 ];
344
345 let result = batch.add_sequence(&tokens, 0, false);
346
347 assert!(result.is_err());
348 }
349
350 #[test]
351 fn add_sequence_fails_mid_loop_when_batch_fills() {
352 let mut batch = LlamaBatch::new(2, 1).unwrap();
353 batch
354 .add(&SampledToken::Content(LlamaToken::new(1)), 0, &[0], false)
355 .unwrap();
356
357 let tokens = vec![LlamaToken::new(10), LlamaToken::new(20)];
358 let result = batch.add_sequence(&tokens, 0, false);
359
360 assert!(result.is_err());
361 }
362
363 #[test]
364 fn get_one_with_valid_tokens() {
365 let tokens = vec![LlamaToken::new(1), LlamaToken::new(2)];
366 let batch = LlamaBatch::get_one(&tokens).expect("test: get_one should succeed");
367
368 assert_eq!(batch.n_tokens(), 2);
369 assert_eq!(batch.initialized_logits, vec![1]);
370 }
371
372 #[test]
373 fn get_one_empty_slice_returns_error() {
374 let tokens: Vec<LlamaToken> = vec![];
375 let result = LlamaBatch::get_one(&tokens);
376
377 assert_eq!(result.unwrap_err(), BatchAddError::EmptyBuffer);
378 }
379
380 #[test]
381 fn get_one_single_token() {
382 let tokens = vec![LlamaToken::new(42)];
383 let batch = LlamaBatch::get_one(&tokens).expect("test: get_one should succeed");
384
385 assert_eq!(batch.n_tokens(), 1);
386 assert_eq!(batch.initialized_logits, vec![0]);
387 }
388
389 #[test]
390 fn add_with_logits_false_retains_only_previous_logits() {
391 let mut batch = LlamaBatch::new(16, 1).unwrap();
392
393 batch
394 .add(&SampledToken::Content(LlamaToken::new(1)), 0, &[0], true)
395 .unwrap();
396 assert_eq!(batch.initialized_logits, vec![0]);
397
398 batch
399 .add(&SampledToken::Content(LlamaToken::new(2)), 0, &[0], false)
400 .unwrap();
401 assert_eq!(batch.initialized_logits, vec![0]);
402 }
403
404 #[test]
405 fn add_sequence_with_logits_all_marks_every_token() -> Result<(), BatchAddError> {
406 let mut batch = LlamaBatch::new(16, 1)?;
407 let tokens = vec![
408 LlamaToken::new(10),
409 LlamaToken::new(20),
410 LlamaToken::new(30),
411 ];
412
413 batch.add_sequence(&tokens, 0, true)?;
414
415 assert_eq!(batch.n_tokens(), 3);
416 assert_eq!(batch.initialized_logits, vec![0, 1, 2]);
417
418 Ok(())
419 }
420
421 #[test]
422 fn add_with_multiple_seq_ids() -> Result<(), BatchAddError> {
423 let mut batch = LlamaBatch::new(16, 4)?;
424
425 batch.add(
426 &SampledToken::Content(LlamaToken::new(1)),
427 0,
428 &[0, 1, 2],
429 true,
430 )?;
431
432 assert_eq!(batch.n_tokens(), 1);
433 assert_eq!(batch.initialized_logits, vec![0]);
434
435 Ok(())
436 }
437
438 #[test]
439 fn drop_does_not_free_get_one_batch() {
440 let tokens = vec![LlamaToken::new(1), LlamaToken::new(2)];
441 let batch = LlamaBatch::get_one(&tokens).expect("test: get_one should succeed");
442
443 assert_eq!(batch.allocated, 0);
444 drop(batch);
445 }
446
447 #[test]
448 fn checked_n_tokens_plus_one_as_usize_succeeds_for_zero() {
449 let result = checked_n_tokens_plus_one_as_usize(0);
450
451 assert_eq!(result, Ok(1));
452 }
453
454 #[test]
455 fn checked_n_tokens_plus_one_as_usize_fails_for_negative() {
456 let result = checked_n_tokens_plus_one_as_usize(-2);
457
458 assert_eq!(
459 std::mem::discriminant(&result.unwrap_err()),
460 std::mem::discriminant(&BatchAddError::IntegerOverflow(String::new())),
461 );
462 }
463
464 #[test]
465 fn checked_n_tokens_plus_one_as_usize_fails_for_i32_max() {
466 let result = checked_n_tokens_plus_one_as_usize(i32::MAX);
467
468 assert_eq!(
469 std::mem::discriminant(&result.unwrap_err()),
470 std::mem::discriminant(&BatchAddError::IntegerOverflow(String::new())),
471 );
472 }
473
474 #[test]
475 fn checked_i32_as_usize_succeeds_for_zero() {
476 let result = checked_i32_as_usize(0, "test_value");
477
478 assert_eq!(result, Ok(0));
479 }
480
481 #[test]
482 fn checked_i32_as_usize_fails_for_negative() {
483 let result = checked_i32_as_usize(i32::MIN, "test_value");
484
485 assert_eq!(
486 std::mem::discriminant(&result.unwrap_err()),
487 std::mem::discriminant(&BatchAddError::IntegerOverflow(String::new())),
488 );
489 }
490
491 #[test]
492 fn checked_usize_as_llama_seq_id_succeeds_for_zero() {
493 let result = checked_usize_as_llama_seq_id(0, "test_value");
494
495 assert_eq!(result, Ok(0));
496 }
497
498 #[test]
499 fn checked_usize_as_llama_seq_id_fails_for_overflow() {
500 let result = checked_usize_as_llama_seq_id(usize::MAX, "test_value");
501
502 assert_eq!(
503 std::mem::discriminant(&result.unwrap_err()),
504 std::mem::discriminant(&BatchAddError::IntegerOverflow(String::new())),
505 );
506 }
507
508 #[test]
509 fn checked_usize_as_i32_succeeds_for_zero() {
510 let result = checked_usize_as_i32(0, "test_value");
511
512 assert_eq!(result, Ok(0));
513 }
514
515 #[test]
516 fn checked_usize_as_i32_fails_for_overflow() {
517 let result = checked_usize_as_i32(usize::MAX, "test_value");
518
519 assert_eq!(
520 std::mem::discriminant(&result.unwrap_err()),
521 std::mem::discriminant(&BatchAddError::IntegerOverflow(String::new())),
522 );
523 }
524
525 #[test]
526 fn checked_usize_as_llama_pos_succeeds_for_zero() {
527 let result = checked_usize_as_llama_pos(0, "test_value");
528
529 assert_eq!(result, Ok(0));
530 }
531
532 #[test]
533 fn checked_usize_as_llama_pos_fails_for_overflow() {
534 let result = checked_usize_as_llama_pos(usize::MAX, "test_value");
535
536 assert_eq!(
537 std::mem::discriminant(&result.unwrap_err()),
538 std::mem::discriminant(&BatchAddError::IntegerOverflow(String::new())),
539 );
540 }
541
542 #[test]
543 fn new_fails_for_oversized_n_tokens() {
544 let result = LlamaBatch::new(usize::MAX, 1);
545
546 assert_eq!(
547 std::mem::discriminant(&result.unwrap_err()),
548 std::mem::discriminant(&BatchAddError::IntegerOverflow(String::new())),
549 );
550 }
551
552 #[test]
553 fn add_fails_when_required_token_count_overflows_i32() {
554 let mut batch = LlamaBatch::new(16, 1).unwrap();
555 batch.llama_batch.n_tokens = i32::MAX;
556
557 let result = batch.add(&SampledToken::Content(LlamaToken::new(1)), 0, &[0], false);
558
559 assert_eq!(
560 std::mem::discriminant(&result.unwrap_err()),
561 std::mem::discriminant(&BatchAddError::IntegerOverflow(String::new())),
562 );
563 }
564
565 #[test]
566 fn add_fails_when_existing_offset_is_negative() {
567 let mut batch = LlamaBatch::new(16, 1).unwrap();
568 batch.llama_batch.n_tokens = -1;
569
570 let result = batch.add(&SampledToken::Content(LlamaToken::new(1)), 0, &[0], false);
571
572 assert_eq!(
573 std::mem::discriminant(&result.unwrap_err()),
574 std::mem::discriminant(&BatchAddError::IntegerOverflow(String::new())),
575 );
576 }
577}