use std::collections::HashMap; use half::bf16; use xserv_kernels::*; use xserv_tensor::{DType, Device, Tensor}; use crate::config::ModelConfig; use crate::gpt2::KVCache; pub struct Qwen3 { pub config: ModelConfig, embed_tokens: Tensor, layers: Vec, norm: Tensor, lm_head_t: Tensor, // precomputed transpose rope_cache: RopeCache, } struct Qwen3Block { input_norm: Tensor, // [hidden] q_proj_wt: Tensor, // TRANSPOSED: [hidden, num_heads*head_dim] k_proj_wt: Tensor, // TRANSPOSED: [hidden, num_kv_heads*head_dim] v_proj_wt: Tensor, o_proj_wt: Tensor, // TRANSPOSED: [num_heads*head_dim, hidden] q_norm: Tensor, // [head_dim] k_norm: Tensor, // [head_dim] post_norm: Tensor, // [hidden] gate_proj_wt: Tensor, // TRANSPOSED: [hidden, intermediate] up_proj_wt: Tensor, down_proj_wt: Tensor, // TRANSPOSED: [intermediate, hidden] } impl Qwen3 { pub fn from_weights(config: ModelConfig, mut w: HashMap) -> Self { let take = |w: &mut HashMap, name: &str| -> Tensor { w.remove(name).unwrap_or_else(|| panic!("missing weight: {name}")) }; let embed_tokens = take(&mut w, "model.embed_tokens.weight"); let norm = take(&mut w, "model.norm.weight"); let lm_head_raw = take(&mut w, "lm_head.weight"); let rope_cache = RopeCache::new( config.max_seq_len().min(8192), // limit for memory config.head_dim(), config.rope_theta.unwrap_or(1_000_000.0) as f32, ); // Precompute transposed weights: [out, in] → [in, out] so we can do x @ wt directly let transpose_w = |t: Tensor| -> Tensor { t.transpose(0, 1).contiguous() }; let num_layers = config.num_layers(); let mut layers = Vec::with_capacity(num_layers); eprintln!("Transposing weights for {} layers...", num_layers); for i in 0..num_layers { let p = format!("model.layers.{i}"); layers.push(Qwen3Block { input_norm: take(&mut w, &format!("{p}.input_layernorm.weight")), q_proj_wt: transpose_w(take(&mut w, &format!("{p}.self_attn.q_proj.weight"))), k_proj_wt: transpose_w(take(&mut w, &format!("{p}.self_attn.k_proj.weight"))), v_proj_wt: transpose_w(take(&mut w, &format!("{p}.self_attn.v_proj.weight"))), o_proj_wt: transpose_w(take(&mut w, &format!("{p}.self_attn.o_proj.weight"))), q_norm: take(&mut w, &format!("{p}.self_attn.q_norm.weight")), k_norm: take(&mut w, &format!("{p}.self_attn.k_norm.weight")), post_norm: take(&mut w, &format!("{p}.post_attention_layernorm.weight")), gate_proj_wt: transpose_w(take(&mut w, &format!("{p}.mlp.gate_proj.weight"))), up_proj_wt: transpose_w(take(&mut w, &format!("{p}.mlp.up_proj.weight"))), down_proj_wt: transpose_w(take(&mut w, &format!("{p}.mlp.down_proj.weight"))), }); } let lm_head_t = transpose_w(lm_head_raw); Self { config, embed_tokens, layers, norm, lm_head_t, rope_cache } } 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 num_kv_heads = self.config.num_kv_heads(); let head_dim = self.config.head_dim(); let eps = self.config.rms_norm_eps.unwrap_or(1e-6) as f32; let mut x = embedding(&self.embed_tokens, token_ids); let positions: Vec = (pos_offset..pos_offset + new_tokens).map(|p| p as u32).collect(); for (layer_idx, layer) in self.layers.iter().enumerate() { let residual = x.clone(); let normed = rmsnorm(&x, &layer.input_norm, eps); // Q/K/V projections (pre-transposed weights, x @ wt) let q = matmul_2d(&normed, &layer.q_proj_wt); let k = matmul_2d(&normed, &layer.k_proj_wt); let v = matmul_2d(&normed, &layer.v_proj_wt); // Reshape to [1, heads, seq, head_dim] let q = reshape_heads(&q, new_tokens, num_heads, head_dim); let k = reshape_heads(&k, new_tokens, num_kv_heads, head_dim); let v = reshape_heads(&v, new_tokens, num_kv_heads, head_dim); // QK normalization (per-head RMSNorm) let q = head_rmsnorm(&q, &layer.q_norm, eps); let k = head_rmsnorm(&k, &layer.k_norm, eps); // RoPE — kernel expects [S, H, D], our tensors are [1, H, S, D] // Transpose to [1, S, H, D] → reshape to [S, H, D] for RoPE let q = transpose_for_rope(&q, new_tokens, num_heads, head_dim); let k = transpose_for_rope(&k, new_tokens, num_kv_heads, head_dim); rope_inplace(&q, &self.rope_cache, &positions); rope_inplace(&k, &self.rope_cache, &positions); // Transpose back to [1, H, S, D] let q = transpose_from_rope(&q, new_tokens, num_heads, head_dim); let k = transpose_from_rope(&k, new_tokens, num_kv_heads, head_dim); // KV cache let k_cpu = k.to_device(Device::Cpu); let v_cpu = v.to_device(Device::Cpu); cache.append_kv_tensor(layer_idx, &k_cpu, &v_cpu, new_tokens); let (k_full, v_full) = cache.get_kv_tensors(layer_idx); // GQA: repeat K/V let n_rep = num_heads / num_kv_heads; let k_full = repeat_kv(&k_full, n_rep); let v_full = repeat_kv(&v_full, n_rep); // Attention let attn_out = attention(&q, &k_full, &v_full, true); let attn_merged = merge_heads_any(&attn_out, new_tokens, hidden); let attn_proj = matmul_2d(&attn_merged, &layer.o_proj_wt); x = add_any(&residual, &attn_proj); // SwiGLU FFN let residual = x.clone(); let normed = rmsnorm(&x, &layer.post_norm, eps); let gate = matmul_2d(&normed, &layer.gate_proj_wt); let up = matmul_2d(&normed, &layer.up_proj_wt); let gate_activated = silu(&gate); let hidden_states = mul_any(&gate_activated, &up); let down = matmul_2d(&hidden_states, &layer.down_proj_wt); x = add_any(&residual, &down); } let x = rmsnorm(&x, &self.norm, eps); matmul_2d(&x, &self.lm_head_t) } } // --- Helpers --- fn matmul_2d(a: &Tensor, b: &Tensor) -> Tensor { assert_eq!(a.ndim(), 2); assert_eq!(b.ndim(), 2); matmul(a, b, GemmBackend::CuBlas) } fn reshape_heads(x: &Tensor, seq_len: usize, num_heads: usize, head_dim: usize) -> Tensor { let x_cpu = x.to_device(Device::Cpu); let hidden = num_heads * head_dim; let src = x_cpu.as_slice::(); let mut out = vec![bf16::ZERO; num_heads * seq_len * head_dim]; for s in 0..seq_len { for h in 0..num_heads { let si = s * hidden + h * head_dim; let di = (h * seq_len + s) * head_dim; out[di..di + head_dim].copy_from_slice(&src[si..si + head_dim]); } } Tensor::from_slice(&out, &[1, num_heads, seq_len, head_dim]).to_device(x.device()) } fn merge_heads_any(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 src = x_cpu.as_slice::(); let mut out = vec![bf16::ZERO; seq_len * hidden]; for s in 0..seq_len { for h in 0..num_heads { let si = (h * seq_len + s) * head_dim; let di = s * hidden + h * head_dim; out[di..di + head_dim].copy_from_slice(&src[si..si + head_dim]); } } Tensor::from_slice(&out, &[seq_len, hidden]).to_device(x.device()) } /// Per-head RMSNorm: apply RMSNorm to each [head_dim] slice independently. /// x: [1, H, S, D], norm_weight: [D] fn head_rmsnorm(x: &Tensor, norm_weight: &Tensor, eps: f32) -> Tensor { let num_heads = x.shape()[1]; let seq_len = x.shape()[2]; let head_dim = x.shape()[3]; // Reshape to [H*S, D], apply rmsnorm, reshape back let total_rows = num_heads * seq_len; let flat = x.reshape(&[total_rows, head_dim]); let normed = rmsnorm(&flat, norm_weight, eps); normed.reshape(&[1, num_heads, seq_len, head_dim]) } /// [1, H, S, D] → [S, H, D] for RoPE kernel fn transpose_for_rope(x: &Tensor, seq_len: usize, num_heads: usize, head_dim: usize) -> Tensor { let x_cpu = x.to_device(Device::Cpu); let src = x_cpu.as_slice::(); let mut out = vec![bf16::ZERO; seq_len * num_heads * head_dim]; for h in 0..num_heads { for s in 0..seq_len { let si = (h * seq_len + s) * head_dim; let di = (s * num_heads + h) * head_dim; out[di..di + head_dim].copy_from_slice(&src[si..si + head_dim]); } } Tensor::from_slice(&out, &[seq_len, num_heads, head_dim]).to_device(x.device()) } /// [S, H, D] → [1, H, S, D] after RoPE fn transpose_from_rope(x: &Tensor, seq_len: usize, num_heads: usize, head_dim: usize) -> Tensor { let x_cpu = x.to_device(Device::Cpu); let src = x_cpu.as_slice::(); let mut out = vec![bf16::ZERO; num_heads * seq_len * head_dim]; for s in 0..seq_len { for h in 0..num_heads { let si = (s * num_heads + h) * head_dim; let di = (h * seq_len + s) * head_dim; out[di..di + head_dim].copy_from_slice(&src[si..si + head_dim]); } } Tensor::from_slice(&out, &[1, num_heads, seq_len, head_dim]).to_device(x.device()) } fn repeat_kv(x: &Tensor, n_rep: usize) -> Tensor { if n_rep == 1 { return x.clone(); } let kv_heads = x.shape()[1]; let seq_len = x.shape()[2]; let head_dim = x.shape()[3]; let x_cpu = x.to_device(Device::Cpu); let src = x_cpu.as_slice::(); let new_heads = kv_heads * n_rep; let mut out = vec![bf16::ZERO; new_heads * seq_len * head_dim]; let chunk = seq_len * head_dim; for kv_h in 0..kv_heads { for r in 0..n_rep { let dst_h = kv_h * n_rep + r; out[dst_h * chunk..(dst_h + 1) * chunk] .copy_from_slice(&src[kv_h * chunk..(kv_h + 1) * chunk]); } } Tensor::from_slice(&out, &[1, new_heads, seq_len, head_dim]).to_device(x.device()) } fn add_any(a: &Tensor, b: &Tensor) -> Tensor { assert_eq!(a.shape(), b.shape()); let a_cpu = a.to_device(Device::Cpu); let b_cpu = b.to_device(Device::Cpu); let ad = a_cpu.as_slice::(); let bd = b_cpu.as_slice::(); let r: Vec = ad.iter().zip(bd) .map(|(x, y)| bf16::from_f32(x.to_f32() + y.to_f32())) .collect(); Tensor::from_slice(&r, a.shape()).to_device(a.device()) } fn mul_any(a: &Tensor, b: &Tensor) -> Tensor { assert_eq!(a.shape(), b.shape()); let a_cpu = a.to_device(Device::Cpu); let b_cpu = b.to_device(Device::Cpu); let ad = a_cpu.as_slice::(); let bd = b_cpu.as_slice::(); let r: Vec = ad.iter().zip(bd) .map(|(x, y)| bf16::from_f32(x.to_f32() * y.to_f32())) .collect(); Tensor::from_slice(&r, a.shape()).to_device(a.device()) } pub fn sample_greedy(logits: &Tensor) -> u32 { 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| a.1.to_f32().partial_cmp(&b.1.to_f32()).unwrap()) .map(|(i, _)| i as u32).unwrap() }