use std::collections::VecDeque;
use std::sync::Arc;
use cudarc::driver::CudaDevice;
use super::arch::ModelConfig;
use super::tensor::{GpuDType, GpuTensor};
use super::InferenceError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RequestState {
Waiting,
Prefill,
Decode,
Complete,
Failed,
}
#[derive(Debug, Clone)]
pub struct Request {
pub id: u64,
pub input_ids: Vec<u32>,
pub output_ids: Vec<u32>,
pub max_tokens: usize,
pub state: RequestState,
pub slot_idx: Option<usize>,
pub position: usize,
pub prompt_len: usize,
}
impl Request {
pub fn new(id: u64, input_ids: Vec<u32>, max_tokens: usize) -> Self {
let prompt_len = input_ids.len();
Self {
id,
input_ids,
output_ids: Vec::new(),
max_tokens,
state: RequestState::Waiting,
slot_idx: None,
position: 0,
prompt_len,
}
}
pub fn is_complete(&self) -> bool {
matches!(self.state, RequestState::Complete | RequestState::Failed)
}
pub fn total_len(&self) -> usize {
self.prompt_len + self.output_ids.len()
}
}
#[derive(Debug)]
pub struct BatchSlot {
pub index: usize,
pub request_id: Option<u64>,
pub seq_len: usize,
pub max_seq_len: usize,
}
impl BatchSlot {
pub fn new(index: usize, max_seq_len: usize) -> Self {
Self {
index,
request_id: None,
seq_len: 0,
max_seq_len,
}
}
pub fn is_free(&self) -> bool {
self.request_id.is_none()
}
pub fn assign(&mut self, request_id: u64) {
self.request_id = Some(request_id);
self.seq_len = 0;
}
pub fn release(&mut self) {
self.request_id = None;
self.seq_len = 0;
}
}
pub struct SlottedKvCache {
#[allow(dead_code)]
config: ModelConfig,
#[allow(dead_code)]
device: Arc<CudaDevice>,
#[allow(dead_code)]
keys: GpuTensor,
#[allow(dead_code)]
values: GpuTensor,
slots: Vec<BatchSlot>,
num_slots: usize,
#[allow(dead_code)]
max_seq_len: usize,
}
impl SlottedKvCache {
pub fn new(
config: &ModelConfig,
num_slots: usize,
max_seq_len: usize,
device: Arc<CudaDevice>,
) -> Result<Self, InferenceError> {
let num_layers = config.num_layers;
let num_kv_heads = config.num_kv_heads;
let head_dim = config.head_dim;
let cache_shape = vec![num_slots, num_layers, max_seq_len, num_kv_heads, head_dim];
let keys = GpuTensor::zeros(cache_shape.clone(), GpuDType::F16, Arc::clone(&device))?;
let values = GpuTensor::zeros(cache_shape, GpuDType::F16, Arc::clone(&device))?;
let slots = (0..num_slots)
.map(|i| BatchSlot::new(i, max_seq_len))
.collect();
let cache_bytes = keys.size_bytes() + values.size_bytes();
tracing::info!(
cache_mb = cache_bytes as f64 / 1024.0 / 1024.0,
num_slots = num_slots,
max_seq_len = max_seq_len,
"Created slotted KV cache"
);
Ok(Self {
config: config.clone(),
device,
keys,
values,
slots,
num_slots,
max_seq_len,
})
}
pub fn allocate_slot(&mut self) -> Option<usize> {
for slot in &mut self.slots {
if slot.is_free() {
return Some(slot.index);
}
}
None
}
pub fn assign_slot(&mut self, slot_idx: usize, request_id: u64) -> Result<(), InferenceError> {
if slot_idx >= self.num_slots {
return Err(InferenceError::InvalidParam(format!(
"Slot index {} out of range",
slot_idx
)));
}
self.slots[slot_idx].assign(request_id);
Ok(())
}
pub fn release_slot(&mut self, slot_idx: usize) -> Result<(), InferenceError> {
if slot_idx >= self.num_slots {
return Err(InferenceError::InvalidParam(format!(
"Slot index {} out of range",
slot_idx
)));
}
self.slots[slot_idx].release();
Ok(())
}
pub fn update_seq_len(&mut self, slot_idx: usize, seq_len: usize) {
if slot_idx < self.num_slots {
self.slots[slot_idx].seq_len = seq_len;
}
}
pub fn seq_len(&self, slot_idx: usize) -> usize {
self.slots.get(slot_idx).map_or(0, |s| s.seq_len)
}
pub fn num_free_slots(&self) -> usize {
self.slots.iter().filter(|s| s.is_free()).count()
}
pub fn num_active_slots(&self) -> usize {
self.num_slots - self.num_free_slots()
}
pub fn slot(&self, idx: usize) -> Option<&BatchSlot> {
self.slots.get(idx)
}
pub fn active_slots(&self) -> Vec<usize> {
self.slots
.iter()
.filter(|s| !s.is_free())
.map(|s| s.index)
.collect()
}
}
pub struct BatchScheduler {
waiting: VecDeque<Request>,
active: Vec<Option<Request>>,
completed: Vec<Request>,
kv_cache: SlottedKvCache,
#[allow(dead_code)]
max_batch_size: usize,
next_request_id: u64,
}
impl BatchScheduler {
pub fn new(
config: &ModelConfig,
max_batch_size: usize,
max_seq_len: usize,
device: Arc<CudaDevice>,
) -> Result<Self, InferenceError> {
let kv_cache = SlottedKvCache::new(config, max_batch_size, max_seq_len, device)?;
Ok(Self {
waiting: VecDeque::new(),
active: vec![None; max_batch_size],
completed: Vec::new(),
kv_cache,
max_batch_size,
next_request_id: 0,
})
}
pub fn add_request(&mut self, input_ids: Vec<u32>, max_tokens: usize) -> u64 {
let id = self.next_request_id;
self.next_request_id += 1;
let request = Request::new(id, input_ids, max_tokens);
self.waiting.push_back(request);
id
}
pub fn schedule(&mut self) -> Result<(), InferenceError> {
while !self.waiting.is_empty() {
if let Some(slot_idx) = self.kv_cache.allocate_slot() {
let mut request = self.waiting.pop_front().unwrap();
let request_id = request.id;
request.state = RequestState::Prefill;
request.slot_idx = Some(slot_idx);
self.kv_cache.assign_slot(slot_idx, request_id)?;
self.active[slot_idx] = Some(request);
} else {
break;
}
}
Ok(())
}
pub fn get_prefill_requests(&self) -> Vec<(usize, &Request)> {
self.active
.iter()
.enumerate()
.filter_map(|(idx, r)| {
r.as_ref().and_then(|req| {
if req.state == RequestState::Prefill {
Some((idx, req))
} else {
None
}
})
})
.collect()
}
pub fn get_decode_requests(&self) -> Vec<(usize, &Request)> {
self.active
.iter()
.enumerate()
.filter_map(|(idx, r)| {
r.as_ref().and_then(|req| {
if req.state == RequestState::Decode {
Some((idx, req))
} else {
None
}
})
})
.collect()
}
pub fn transition_to_decode(&mut self, slot_idx: usize) -> Result<(), InferenceError> {
if let Some(ref mut request) = self.active[slot_idx] {
request.state = RequestState::Decode;
request.position = request.prompt_len;
}
Ok(())
}
pub fn add_token(&mut self, slot_idx: usize, token: u32) -> Result<bool, InferenceError> {
if let Some(ref mut request) = self.active[slot_idx] {
request.output_ids.push(token);
request.position += 1;
self.kv_cache.update_seq_len(slot_idx, request.total_len());
let is_complete = request.output_ids.len() >= request.max_tokens;
if is_complete {
request.state = RequestState::Complete;
}
return Ok(is_complete);
}
Ok(false)
}
pub fn complete_request(&mut self, slot_idx: usize) -> Result<Option<Request>, InferenceError> {
if let Some(request) = self.active[slot_idx].take() {
self.kv_cache.release_slot(slot_idx)?;
self.completed.push(request);
return Ok(self.completed.last().cloned());
}
Ok(None)
}
pub fn get_completed(&mut self) -> Vec<Request> {
std::mem::take(&mut self.completed)
}
pub fn has_work(&self) -> bool {
!self.waiting.is_empty() || self.active.iter().any(|r| r.is_some())
}
pub fn stats(&self) -> BatchStats {
let prefill_count = self
.active
.iter()
.filter(|r| matches!(r, Some(req) if req.state == RequestState::Prefill))
.count();
let decode_count = self
.active
.iter()
.filter(|r| matches!(r, Some(req) if req.state == RequestState::Decode))
.count();
BatchStats {
waiting: self.waiting.len(),
prefill: prefill_count,
decode: decode_count,
completed: self.completed.len(),
free_slots: self.kv_cache.num_free_slots(),
}
}
}
#[derive(Debug, Clone)]
pub struct BatchStats {
pub waiting: usize,
pub prefill: usize,
pub decode: usize,
pub completed: usize,
pub free_slots: usize,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cuda_inference::arch::{Activation, ModelArch};
fn test_config() -> ModelConfig {
ModelConfig {
arch: ModelArch::Llama,
vocab_size: 32000,
hidden_size: 576,
intermediate_size: 1536,
num_layers: 4,
num_attention_heads: 9,
num_kv_heads: 3,
head_dim: 64,
max_seq_len: 2048,
rms_norm_eps: 1e-5,
rope_theta: 10000.0,
rope_scaling: None,
attention_bias: false,
mlp_bias: false,
hidden_act: Activation::SiLU,
tie_word_embeddings: true,
sliding_window: None,
bos_token_id: 1,
eos_token_id: 2,
pad_token_id: None,
}
}
#[test]
fn test_request_creation() {
let request = Request::new(1, vec![1, 2, 3, 4], 100);
assert_eq!(request.id, 1);
assert_eq!(request.prompt_len, 4);
assert_eq!(request.state, RequestState::Waiting);
assert!(!request.is_complete());
}
#[test]
fn test_batch_slot() {
let mut slot = BatchSlot::new(0, 1024);
assert!(slot.is_free());
slot.assign(42);
assert!(!slot.is_free());
assert_eq!(slot.request_id, Some(42));
slot.release();
assert!(slot.is_free());
}
}