Files
xserv/crates/xserv-model/src/gpt2.rs
Gahow Wang d52baa0006 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>
2026-05-28 19:58:54 +08:00

381 lines
15 KiB
Rust

use std::collections::HashMap;
use xserv_kernels::*;
use xserv_tensor::{DType, Device, Tensor};
use crate::config::ModelConfig;
pub struct GPT2 {
pub config: ModelConfig,
wte: Tensor,
wpe: Tensor,
layers: Vec<GPT2Block>,
ln_f_g: Tensor,
ln_f_b: Tensor,
lm_head: Tensor, // precomputed wte^T
}
struct GPT2Block {
ln_1_g: Tensor,
ln_1_b: Tensor,
attn_qkv_w: Tensor,
attn_qkv_b: Tensor,
attn_out_w: Tensor,
attn_out_b: Tensor,
ln_2_g: Tensor,
ln_2_b: Tensor,
mlp_fc_w: Tensor,
mlp_fc_b: Tensor,
mlp_proj_w: Tensor,
mlp_proj_b: Tensor,
}
pub struct KVCache {
// Per layer, per head: raw bytes (works for both f32 and bf16)
k: Vec<Vec<Vec<u8>>>, // [num_layers][num_heads][seq_len * head_dim * elem_size]
v: Vec<Vec<Vec<u8>>>,
len: usize,
num_heads: usize,
head_dim: usize,
elem_size: usize,
dtype: DType,
device: Device,
}
impl KVCache {
pub fn new(num_layers: usize, num_heads: usize, head_dim: usize, dtype: DType, device: Device) -> Self {
Self {
k: (0..num_layers).map(|_| vec![vec![]; num_heads]).collect(),
v: (0..num_layers).map(|_| vec![vec![]; num_heads]).collect(),
len: 0,
num_heads,
head_dim,
elem_size: dtype.size_bytes(),
dtype,
device,
}
}
pub fn seq_len(&self) -> usize { self.len }
/// Append from a CPU tensor with shape [1, H, new_tokens, D].
pub fn append_kv_tensor(&mut self, layer: usize, k_cpu: &Tensor, v_cpu: &Tensor, new_tokens: usize) {
let hd = self.head_dim;
let es = self.elem_size;
let k_bytes = k_cpu.storage().as_cpu_bytes();
let v_bytes = v_cpu.storage().as_cpu_bytes();
let chunk = new_tokens * hd * es;
for h in 0..self.num_heads {
let off = h * chunk;
self.k[layer][h].extend_from_slice(&k_bytes[off..off + chunk]);
self.v[layer][h].extend_from_slice(&v_bytes[off..off + chunk]);
}
if layer == 0 {
self.len += new_tokens;
}
}
/// Reconstruct [1, H, seq_len, D] tensors.
pub fn get_kv_tensors(&self, layer: usize) -> (Tensor, Tensor) {
let sl = self.len;
let hd = self.head_dim;
let nh = self.num_heads;
let es = self.elem_size;
let head_bytes = sl * hd * es;
let total = nh * head_bytes;
let mut k_data = vec![0u8; total];
let mut v_data = vec![0u8; total];
for h in 0..nh {
let off = h * head_bytes;
k_data[off..off + head_bytes].copy_from_slice(&self.k[layer][h]);
v_data[off..off + head_bytes].copy_from_slice(&self.v[layer][h]);
}
let shape = &[1, nh, sl, hd];
let k = tensor_from_raw_bytes(&k_data, shape, self.dtype).to_device(self.device);
let v = tensor_from_raw_bytes(&v_data, shape, self.dtype).to_device(self.device);
(k, v)
}
}
fn tensor_from_raw_bytes(bytes: &[u8], shape: &[usize], dtype: DType) -> Tensor {
match dtype {
DType::F32 => {
let data: &[f32] = unsafe {
std::slice::from_raw_parts(bytes.as_ptr() as *const f32, bytes.len() / 4)
};
Tensor::from_slice(data, shape)
}
DType::BF16 => {
let data: &[half::bf16] = unsafe {
std::slice::from_raw_parts(bytes.as_ptr() as *const half::bf16, bytes.len() / 2)
};
Tensor::from_slice(data, shape)
}
_ => panic!("unsupported dtype for KV cache"),
}
}
impl GPT2 {
pub fn from_weights(config: ModelConfig, mut w: HashMap<String, Tensor>) -> Self {
crate::init_kernels();
let take = |w: &mut HashMap<String, Tensor>, name: &str| -> Tensor {
w.remove(name).unwrap_or_else(|| panic!("missing weight: {name}"))
};
let wte = take(&mut w, "wte.weight");
let wpe = take(&mut w, "wpe.weight");
let ln_f_g = take(&mut w, "ln_f.weight");
let ln_f_b = take(&mut w, "ln_f.bias");
let lm_head = wte.transpose(0, 1).contiguous();
let num_layers = config.num_layers();
let mut layers = Vec::with_capacity(num_layers);
for i in 0..num_layers {
let p = format!("h.{i}");
layers.push(GPT2Block {
ln_1_g: take(&mut w, &format!("{p}.ln_1.weight")),
ln_1_b: take(&mut w, &format!("{p}.ln_1.bias")),
attn_qkv_w: take(&mut w, &format!("{p}.attn.c_attn.weight")),
attn_qkv_b: take(&mut w, &format!("{p}.attn.c_attn.bias")),
attn_out_w: take(&mut w, &format!("{p}.attn.c_proj.weight")),
attn_out_b: take(&mut w, &format!("{p}.attn.c_proj.bias")),
ln_2_g: take(&mut w, &format!("{p}.ln_2.weight")),
ln_2_b: take(&mut w, &format!("{p}.ln_2.bias")),
mlp_fc_w: take(&mut w, &format!("{p}.mlp.c_fc.weight")),
mlp_fc_b: take(&mut w, &format!("{p}.mlp.c_fc.bias")),
mlp_proj_w: take(&mut w, &format!("{p}.mlp.c_proj.weight")),
mlp_proj_b: take(&mut w, &format!("{p}.mlp.c_proj.bias")),
});
}
Self { config, wte, wpe, layers, ln_f_g, ln_f_b, lm_head }
}
/// Full forward pass without KV cache (for testing / correctness comparison).
pub fn forward(&self, token_ids: &[u32]) -> Tensor {
let seq_len = token_ids.len();
let hidden = self.config.hidden();
let num_heads = self.config.num_heads();
let head_dim = self.config.head_dim();
let tok_emb = embedding(&self.wte, token_ids);
let pos_ids: Vec<u32> = (0..seq_len as u32).collect();
let pos_emb = embedding(&self.wpe, &pos_ids);
let mut x = add_tensors(&tok_emb, &pos_emb);
for layer in &self.layers {
x = self.transformer_block(layer, &x, None, 0, seq_len, num_heads, head_dim, hidden);
}
let x = layernorm(&x, &self.ln_f_g, &self.ln_f_b, self.config.ln_eps());
matmul_2d(&x, &self.lm_head)
}
/// Forward pass with KV cache. First call = prefill, subsequent = decode.
pub fn forward_with_cache(&self, token_ids: &[u32], cache: &mut KVCache) -> Tensor {
let new_tokens = token_ids.len();
let pos_offset = cache.seq_len();
let hidden = self.config.hidden();
let num_heads = self.config.num_heads();
let head_dim = self.config.head_dim();
let tok_emb = embedding(&self.wte, token_ids);
let pos_ids: Vec<u32> = (pos_offset..pos_offset + new_tokens).map(|p| p as u32).collect();
let pos_emb = embedding(&self.wpe, &pos_ids);
let mut x = add_tensors(&tok_emb, &pos_emb);
for (layer_idx, layer) in self.layers.iter().enumerate() {
x = self.transformer_block(
layer, &x, Some((cache, layer_idx)),
pos_offset, new_tokens, num_heads, head_dim, hidden,
);
}
let x = layernorm(&x, &self.ln_f_g, &self.ln_f_b, self.config.ln_eps());
matmul_2d(&x, &self.lm_head)
}
fn transformer_block(
&self,
layer: &GPT2Block,
x: &Tensor,
cache: Option<(&mut KVCache, usize)>,
pos_offset: usize,
new_tokens: usize,
num_heads: usize,
head_dim: usize,
hidden: usize,
) -> Tensor {
let residual = x.clone();
let normed = layernorm(x, &layer.ln_1_g, &layer.ln_1_b, self.config.ln_eps());
let qkv = linear(&normed, &layer.attn_qkv_w, Some(&layer.attn_qkv_b));
let (q, k_new, v_new) = split_qkv(&qkv, num_heads, head_dim, new_tokens);
let (k_full, v_full) = if let Some((cache, layer_idx)) = cache {
let k_cpu = k_new.to_device(Device::Cpu);
let v_cpu = v_new.to_device(Device::Cpu);
cache.append_kv_tensor(layer_idx, &k_cpu, &v_cpu, new_tokens);
cache.get_kv_tensors(layer_idx)
} else {
(k_new, v_new)
};
let attn_out = attention(&q, &k_full, &v_full, true);
let attn_out = merge_heads(&attn_out, new_tokens, hidden);
let attn_out = linear(&attn_out, &layer.attn_out_w, Some(&layer.attn_out_b));
let x = add_tensors(&residual, &attn_out);
let residual = x.clone();
let normed = layernorm(&x, &layer.ln_2_g, &layer.ln_2_b, self.config.ln_eps());
let fc = linear(&normed, &layer.mlp_fc_w, Some(&layer.mlp_fc_b));
let activated = gelu(&fc);
let proj = linear(&activated, &layer.mlp_proj_w, Some(&layer.mlp_proj_b));
add_tensors(&residual, &proj)
}
}
// --- Helper ops (unchanged) ---
fn linear(x: &Tensor, weight: &Tensor, bias: Option<&Tensor>) -> Tensor {
let out = matmul_2d(x, weight);
if let Some(b) = bias { add_bias(&out, b) } else { out }
}
fn matmul_2d(a: &Tensor, b: &Tensor) -> Tensor {
assert_eq!(a.ndim(), 2);
assert_eq!(b.ndim(), 2);
matmul(a, b, GemmBackend::CuBlas)
}
fn add_tensors(a: &Tensor, b: &Tensor) -> Tensor {
xserv_kernels::add(a, b)
}
fn add_bias(x: &Tensor, bias: &Tensor) -> Tensor {
// bias: [N], x: [S, N] — broadcast add via reshape
assert_eq!(x.ndim(), 2);
assert_eq!(bias.ndim(), 1);
let n = bias.shape()[0];
assert_eq!(x.shape()[1], n);
let rows = x.shape()[0];
// Broadcast: tile bias to [S, N] on CPU, then GPU add
let b_cpu = bias.to_device(Device::Cpu);
match x.dtype() {
DType::F32 => {
let bd = b_cpu.as_slice::<f32>();
let tiled: Vec<f32> = (0..rows).flat_map(|_| bd.iter().copied()).collect();
let b_full = Tensor::from_slice(&tiled, x.shape()).to_device(x.device());
xserv_kernels::add(x, &b_full)
}
DType::BF16 => {
let bd = b_cpu.as_slice::<half::bf16>();
let tiled: Vec<half::bf16> = (0..rows).flat_map(|_| bd.iter().copied()).collect();
let b_full = Tensor::from_slice(&tiled, x.shape()).to_device(x.device());
xserv_kernels::add(x, &b_full)
}
_ => panic!("unsupported dtype"),
}
}
fn split_qkv(qkv: &Tensor, num_heads: usize, head_dim: usize, seq_len: usize) -> (Tensor, Tensor, Tensor) {
let hidden = num_heads * head_dim;
let qkv_cpu = qkv.to_device(Device::Cpu);
let device = qkv.device();
let dtype = qkv.dtype();
match dtype {
DType::F32 => {
let data = qkv_cpu.as_slice::<f32>();
let mut q_data = vec![0.0f32; num_heads * seq_len * head_dim];
let mut k_data = vec![0.0f32; num_heads * seq_len * head_dim];
let mut v_data = vec![0.0f32; num_heads * seq_len * head_dim];
for s in 0..seq_len {
let row = &data[s * 3 * hidden..(s + 1) * 3 * hidden];
for h in 0..num_heads {
let src_off = h * head_dim;
let dst_off = (h * seq_len + s) * head_dim;
q_data[dst_off..dst_off + head_dim].copy_from_slice(&row[src_off..src_off + head_dim]);
k_data[dst_off..dst_off + head_dim].copy_from_slice(&row[hidden + src_off..hidden + src_off + head_dim]);
v_data[dst_off..dst_off + head_dim].copy_from_slice(&row[2 * hidden + src_off..2 * hidden + src_off + head_dim]);
}
}
let q = Tensor::from_slice(&q_data, &[1, num_heads, seq_len, head_dim]).to_device(device);
let k = Tensor::from_slice(&k_data, &[1, num_heads, seq_len, head_dim]).to_device(device);
let v = Tensor::from_slice(&v_data, &[1, num_heads, seq_len, head_dim]).to_device(device);
(q, k, v)
}
DType::BF16 => {
let data = qkv_cpu.as_slice::<half::bf16>();
let mut q_data = vec![half::bf16::ZERO; num_heads * seq_len * head_dim];
let mut k_data = vec![half::bf16::ZERO; num_heads * seq_len * head_dim];
let mut v_data = vec![half::bf16::ZERO; num_heads * seq_len * head_dim];
for s in 0..seq_len {
let row = &data[s * 3 * hidden..(s + 1) * 3 * hidden];
for h in 0..num_heads {
let src_off = h * head_dim;
let dst_off = (h * seq_len + s) * head_dim;
q_data[dst_off..dst_off + head_dim].copy_from_slice(&row[src_off..src_off + head_dim]);
k_data[dst_off..dst_off + head_dim].copy_from_slice(&row[hidden + src_off..hidden + src_off + head_dim]);
v_data[dst_off..dst_off + head_dim].copy_from_slice(&row[2 * hidden + src_off..2 * hidden + src_off + head_dim]);
}
}
let q = Tensor::from_slice(&q_data, &[1, num_heads, seq_len, head_dim]).to_device(device);
let k = Tensor::from_slice(&k_data, &[1, num_heads, seq_len, head_dim]).to_device(device);
let v = Tensor::from_slice(&v_data, &[1, num_heads, seq_len, head_dim]).to_device(device);
(q, k, v)
}
_ => panic!("unsupported dtype {:?} in split_qkv", dtype),
}
}
fn merge_heads(x: &Tensor, seq_len: usize, hidden: usize) -> Tensor {
let num_heads = x.shape()[1];
let head_dim = x.shape()[3];
let x_cpu = x.to_device(Device::Cpu);
let device = x.device();
let dtype = x.dtype();
match dtype {
DType::F32 => {
let src = x_cpu.as_slice::<f32>();
let mut out = vec![0.0f32; seq_len * hidden];
for s in 0..seq_len {
for h in 0..num_heads {
let src_off = (h * seq_len + s) * head_dim;
let dst_off = s * hidden + h * head_dim;
out[dst_off..dst_off + head_dim].copy_from_slice(&src[src_off..src_off + head_dim]);
}
}
Tensor::from_slice(&out, &[seq_len, hidden]).to_device(device)
}
DType::BF16 => {
let src = x_cpu.as_slice::<half::bf16>();
let mut out = vec![half::bf16::ZERO; seq_len * hidden];
for s in 0..seq_len {
for h in 0..num_heads {
let src_off = (h * seq_len + s) * head_dim;
let dst_off = s * hidden + h * head_dim;
out[dst_off..dst_off + head_dim].copy_from_slice(&src[src_off..src_off + head_dim]);
}
}
Tensor::from_slice(&out, &[seq_len, hidden]).to_device(device)
}
_ => panic!("unsupported dtype {:?} in merge_heads", dtype),
}
}
/// Greedy sampling: return the argmax token ID from the last position's logits.
pub fn sample_greedy(logits: &Tensor) -> u32 {
assert_eq!(logits.ndim(), 2);
let logits_cpu = logits.to_device(Device::Cpu);
let data = logits_cpu.as_slice::<f32>();
let vocab_size = logits.shape()[1];
let seq_len = logits.shape()[0];
let last_row = &data[(seq_len - 1) * vocab_size..seq_len * vocab_size];
last_row.iter()
.enumerate()
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
.map(|(idx, _)| idx as u32)
.unwrap()
}