model: paged KV cache with CPU swap pool, decode graph, qwen3 updates

- paged_kv_cache: new block-paged KV cache; adds a pinned-host swap pool with
  a second BlockAllocator, per-sequence Location {Gpu,Cpu}, and lossless
  swap_out/swap_in (block-granular D2H/H2D) for vLLM-style preemption.
  bytes_per_block helper exposes per-block cost for VRAM-based sizing.
- decode_graph: CUDA-graph decode path.
- qwen3/gpt2/kv_cache: paged prefill/decode forward + related updates.
- tokenizer/bins: BPE updates, new xserv-chat CLI, bench-qwen3 tweaks.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-28 19:58:54 +08:00
parent 4c3f914459
commit d52baa0006
9 changed files with 1896 additions and 44 deletions

View File

@@ -1,14 +1,14 @@
use std::path::PathBuf;
use std::time::Instant;
use xserv_model::qwen3::sample_greedy;
use xserv_model::{loader, GpuKVCache, ModelConfig, Qwen3};
use xserv_model::{loader, DecodeGraphState, GpuKVCache, ModelConfig, Qwen3};
use xserv_tensor::{DType, Device};
use xserv_tokenizer::Tokenizer;
fn main() {
let args: Vec<String> = std::env::args().collect();
if args.len() < 2 {
eprintln!("Usage: bench-qwen3 <model-dir> [--gen-tokens N]");
eprintln!("Usage: bench-qwen3 <model-dir> [--gen-tokens N] [--cuda-graph]");
std::process::exit(1);
}
let model_dir = PathBuf::from(&args[1]);
@@ -18,6 +18,7 @@ fn main() {
.and_then(|i| args.get(i + 1))
.and_then(|s| s.parse().ok())
.unwrap_or(20);
let use_cuda_graph = args.iter().any(|a| a == "--cuda-graph");
xserv_cuda::device::set_device(0).unwrap();
@@ -34,6 +35,18 @@ fn main() {
let mut cache = GpuKVCache::new(&config, 256, DType::BF16, 0);
let _ = model.forward_gpu_cache(&ids, &mut cache);
}
// CUDA Graph setup
let layer_ptrs = model.layer_weight_ptrs();
let (norm_w, lm_head, embed, cos, sin) = model.graph_capture_ptrs();
let mut decode_graph = if use_cuda_graph {
eprintln!("CUDA Graph mode enabled");
Some(DecodeGraphState::new(&config))
} else {
None
};
let mut graph_captured = false;
eprintln!("Warmup done. Running benchmark...");
let prompts: Vec<&str> = vec![
@@ -96,6 +109,12 @@ fn main() {
let mut cache = GpuKVCache::new(&config, 256, DType::BF16, 0);
// Reset graph state for new prompt
graph_captured = false;
if let Some(ref mut g) = decode_graph {
g.invalidate();
}
// Prefill
let t0 = Instant::now();
let logits = model.forward_gpu_cache(&input_ids, &mut cache);
@@ -109,8 +128,35 @@ fn main() {
for _ in 1..gen_tokens {
let last = *generated.last().unwrap();
let t_start = Instant::now();
let logits = model.forward_gpu_cache(&[last], &mut cache);
let next = sample_greedy(&logits);
let next = if let Some(ref mut graph) = decode_graph {
if !graph_captured {
// First decode token: run ungraphed, then capture
let logits = model.forward_gpu_cache(&[last], &mut cache);
graph_captured = true;
graph.capture(&layer_ptrs, norm_w, lm_head, embed, cos, sin);
sample_greedy(&logits)
} else {
// Replay captured graphs
let pos = cache.seq_len() as u32;
graph.execute(last, pos, &mut cache, &layer_ptrs, embed, config.vocab_size as i32, config.hidden() as i32);
cache.advance_seq_len(1);
// Read logits from graph buffer
let vocab_size = config.vocab_size;
let mut logits_bytes = vec![0u8; vocab_size * 2];
graph.logits_buffer().copy_to_host(&mut logits_bytes).unwrap();
let logits_data: &[half::bf16] = unsafe {
std::slice::from_raw_parts(logits_bytes.as_ptr() as *const half::bf16, vocab_size)
};
logits_data.iter().enumerate()
.max_by(|a, b| a.1.to_f32().partial_cmp(&b.1.to_f32()).unwrap())
.map(|(idx, _)| idx as u32).unwrap()
}
} else {
let logits = model.forward_gpu_cache(&[last], &mut cache);
sample_greedy(&logits)
};
token_times.push(t_start.elapsed().as_micros());
generated.push(next);
if tokenizer.eos_token_id() == Some(next) { break; }