use std::path::PathBuf; use std::sync::Arc; use std::time::Instant; use xserv_distributed::{TpContext, UniqueId, get_unique_id}; use xserv_model::{loader, GptOss, ModelConfig, PagedKVCache, BLOCK_SIZE}; use xserv_tensor::{DType, Device}; use xserv_tokenizer::Tokenizer; fn main() { let args: Vec = std::env::args().collect(); if args.len() < 2 { eprintln!("Usage: bench-gpt-oss [--max-tokens N] [--tp N]"); std::process::exit(1); } let model_dir = PathBuf::from(&args[1]); let max_tokens: usize = get_arg(&args, "--max-tokens").unwrap_or(32); let world: usize = get_arg(&args, "--tp").unwrap_or(2); let config = ModelConfig::from_file(&model_dir.join("config.json")); let tokenizer = Tokenizer::from_file(&model_dir.join("tokenizer.json")); eprintln!( "gpt-oss-20b: layers={}, hidden={}, heads={}/{} kv, experts={}, top_k={}, vocab={}", config.num_layers(), config.hidden(), config.num_heads(), config.num_kv_heads(), config.num_experts(), config.experts_per_token(), config.vocab_size ); eprintln!("TP world={world}, max_tokens={max_tokens}"); let max_seq_len: usize = 2048; let max_blocks_per_seq = (max_seq_len + BLOCK_SIZE - 1) / BLOCK_SIZE; // TP setup let uid = get_unique_id(); let local_kv = config.num_kv_heads() / world; // Spawn worker threads for ranks 1..world let mut worker_handles = Vec::new(); let mut worker_txs = Vec::new(); for rank in 1..world { let (tx, rx) = std::sync::mpsc::channel::(); let (ack_tx, ack_rx) = std::sync::mpsc::channel::<()>(); let cfg = config.clone(); let md = model_dir.clone(); let uid_copy = uid; worker_handles.push(( std::thread::spawn(move || { worker_loop(rank, world, uid_copy, md, cfg, max_seq_len, rx, ack_tx); }), ack_rx, )); worker_txs.push(tx); } // Rank 0 setup xserv_cuda::device::set_device(0).unwrap(); let tp0 = Arc::new(TpContext::init(0, world, uid, 0)); eprintln!("[rank 0] Loading weights..."); let weights = loader::load_model_dir(&model_dir, Device::Cpu); eprintln!("[rank 0] Loaded {} tensors, building model...", weights.len()); let model = GptOss::from_weights_tp(config.clone(), weights, 0, world, 0, Some(tp0)); let total_blocks = max_blocks_per_seq + 64; let mut cache = PagedKVCache::new_tp( &config, local_kv, total_blocks, 0, 4, max_blocks_per_seq, DType::BF16, 0, ); eprintln!("[rank 0] Ready."); // Prompt let prompt = "What is the meaning of life?"; let token_ids = tokenizer.encode(prompt); eprintln!("Prompt ({} tokens): {prompt}", token_ids.len()); // Register sequence let slot = 0; cache.register_sequence(slot).unwrap(); broadcast_cmd(&worker_txs, &worker_handles, WorkerCmd::Register(slot)); // Prefill let t0 = Instant::now(); broadcast_cmd(&worker_txs, &worker_handles, WorkerCmd::Prefill { tokens: token_ids.clone(), slot, }); let logits = model.forward_prefill_paged(&token_ids, slot, &mut cache); wait_workers(&worker_handles); let ttft = t0.elapsed(); let mut next = sample_greedy_last(&logits); let mut output_tokens = vec![next]; eprintln!("TTFT: {:.1}ms", ttft.as_secs_f64() * 1000.0); print!("{prompt}"); // Decode let decode_start = Instant::now(); for _ in 1..max_tokens { let text = tokenizer.decode(&[next]); print!("{text}"); if tokenizer.eos_token_id() == Some(next) { break; } let pos = cache.seq_len(slot); broadcast_cmd(&worker_txs, &worker_handles, WorkerCmd::Decode { tokens: vec![next], positions: vec![pos], slots: vec![slot], }); let logits = model.forward_decode_paged(&[next], &[pos], &[slot], &mut cache); wait_workers(&worker_handles); next = sample_greedy_last(&logits); output_tokens.push(next); } let decode_elapsed = decode_start.elapsed(); println!(); let gen_tokens = output_tokens.len(); let full_text = tokenizer.decode(&output_tokens); eprintln!("\nGenerated text: {full_text}"); eprintln!("Token IDs: {:?}", &output_tokens[..output_tokens.len().min(20)]); let tpot = if gen_tokens > 1 { decode_elapsed.as_secs_f64() * 1000.0 / (gen_tokens - 1) as f64 } else { 0.0 }; let tok_s = if gen_tokens > 1 { (gen_tokens - 1) as f64 / decode_elapsed.as_secs_f64() } else { 0.0 }; eprintln!("\n--- Performance ---"); eprintln!("Generated: {} tokens", gen_tokens); eprintln!("TTFT: {:.1}ms", ttft.as_secs_f64() * 1000.0); eprintln!("TPOT: {:.1}ms", tpot); eprintln!("Throughput: {:.1} tok/s", tok_s); // Cleanup broadcast_cmd(&worker_txs, &worker_handles, WorkerCmd::Shutdown); for (h, _) in worker_handles { h.join().unwrap(); } } // --- Worker infrastructure --- #[derive(Clone)] enum WorkerCmd { Register(usize), Prefill { tokens: Vec, slot: usize }, Decode { tokens: Vec, positions: Vec, slots: Vec }, Shutdown, } fn worker_loop( rank: usize, world: usize, uid: UniqueId, model_dir: PathBuf, config: ModelConfig, max_seq_len: usize, rx: std::sync::mpsc::Receiver, ack_tx: std::sync::mpsc::Sender<()>, ) { xserv_cuda::device::set_device(rank as u32).unwrap(); let tp = Arc::new(TpContext::init(rank, world, uid, rank as u32)); eprintln!("[rank {rank}] Loading weights..."); let weights = loader::load_model_dir(&model_dir, Device::Cpu); let model = GptOss::from_weights_tp(config.clone(), weights, rank, world, rank as u32, Some(tp)); let local_kv = config.num_kv_heads() / world; let max_blocks_per_seq = (max_seq_len + BLOCK_SIZE - 1) / BLOCK_SIZE; let total_blocks = max_blocks_per_seq + 64; let mut cache = PagedKVCache::new_tp( &config, local_kv, total_blocks, 0, 4, max_blocks_per_seq, DType::BF16, rank as u32, ); eprintln!("[rank {rank}] Ready."); ack_tx.send(()).unwrap(); while let Ok(cmd) = rx.recv() { match cmd { WorkerCmd::Register(slot) => { let _ = cache.register_sequence(slot); } WorkerCmd::Prefill { tokens, slot } => { let _ = model.forward_prefill_paged(&tokens, slot, &mut cache); } WorkerCmd::Decode { tokens, positions, slots } => { let _ = model.forward_decode_paged(&tokens, &positions, &slots, &mut cache); } WorkerCmd::Shutdown => break, } ack_tx.send(()).unwrap(); } } fn broadcast_cmd( txs: &[std::sync::mpsc::Sender], _handles: &[(std::thread::JoinHandle<()>, std::sync::mpsc::Receiver<()>)], cmd: WorkerCmd, ) { for tx in txs { tx.send(cmd.clone()).unwrap(); } } fn wait_workers(handles: &[(std::thread::JoinHandle<()>, std::sync::mpsc::Receiver<()>)]) { for (_, rx) in handles { rx.recv().unwrap(); } } fn sample_greedy_last(logits: &xserv_tensor::Tensor) -> u32 { use half::bf16; assert_eq!(logits.ndim(), 2); let logits_cpu = logits.to_device(Device::Cpu); let vocab_size = logits.shape()[1]; let seq_len = logits.shape()[0]; let data = logits_cpu.as_slice::(); let last = &data[(seq_len - 1) * vocab_size..seq_len * vocab_size]; last.iter().enumerate() .max_by(|a, b| { let af = a.1.to_f32(); let bf = b.1.to_f32(); af.partial_cmp(&bf).unwrap_or(std::cmp::Ordering::Equal) }) .map(|(i, _)| i as u32).unwrap() } fn get_arg(args: &[String], flag: &str) -> Option { args.iter() .position(|a| a == flag) .and_then(|i| args.get(i + 1)) .and_then(|s| s.parse().ok()) }