use candle_core::{DType, Device, Module, Result as CandleResult, Tensor, D};
use candle_nn::{embedding, linear, linear_no_bias, Embedding, Linear, VarBuilder};
use serde::Deserialize;
use crate::flash_attention::{FlashAttention, FlashAttentionConfig};
use crate::kv_cache_quant::QuantizedKvCache;
#[cfg(feature = "cuda")]
use crate::kv_cache_quant_cuda::cuda::CudaQuantizedKvCache;
use crate::kv_cache_quant_cuda::{DynamicQuantConfig, OptimizedQuantizedKvCache};
#[derive(Debug, Clone, Deserialize)]
pub struct Qwen2Config {
pub hidden_size: usize,
pub intermediate_size: usize,
pub vocab_size: usize,
pub num_hidden_layers: usize,
pub num_attention_heads: usize,
#[serde(default, alias = "num_kv_heads")]
pub num_key_value_heads: Option<usize>,
#[serde(default = "default_rms_norm_eps")]
pub rms_norm_eps: f64,
#[serde(default = "default_rope_theta")]
pub rope_theta: f64,
#[serde(default = "default_max_position_embeddings")]
pub max_position_embeddings: usize,
#[serde(default)]
pub tie_word_embeddings: bool,
#[serde(default)]
pub bos_token_id: Option<u32>,
#[serde(default)]
pub eos_token_id: Option<u32>,
#[serde(default)]
pub use_sliding_window: bool,
#[serde(default)]
pub sliding_window: Option<usize>,
}
fn default_rms_norm_eps() -> f64 {
1e-6 }
fn default_rope_theta() -> f64 {
1000000.0 }
fn default_max_position_embeddings() -> usize {
32768 }
impl Qwen2Config {
pub fn num_kv_heads(&self) -> usize {
self.num_key_value_heads.unwrap_or(self.num_attention_heads)
}
pub fn head_dim(&self) -> usize {
self.hidden_size / self.num_attention_heads
}
}
struct RmsNorm {
weight: Tensor,
eps: f64,
}
impl RmsNorm {
fn load(size: usize, eps: f64, vb: VarBuilder) -> CandleResult<Self> {
let weight = vb.get(size, "weight")?;
Ok(Self { weight, eps })
}
}
impl Module for RmsNorm {
fn forward(&self, x: &Tensor) -> CandleResult<Tensor> {
let dtype = x.dtype();
let x = x.to_dtype(DType::F32)?;
let variance = x.sqr()?.mean_keepdim(D::Minus1)?;
let x_normed = x.broadcast_div(&(variance + self.eps)?.sqrt()?)?;
x_normed.to_dtype(dtype)?.broadcast_mul(&self.weight)
}
}
struct RotaryEmbedding {
cos: Tensor,
sin: Tensor,
}
impl RotaryEmbedding {
fn new(config: &Qwen2Config, dtype: DType, device: &Device) -> CandleResult<Self> {
let head_dim = config.head_dim();
let max_seq_len = config.max_position_embeddings;
let theta = config.rope_theta;
let inv_freq: Vec<f32> = (0..head_dim)
.step_by(2)
.map(|i| 1.0 / theta.powf(i as f64 / head_dim as f64) as f32)
.collect();
let inv_freq = Tensor::new(inv_freq.as_slice(), device)?;
let positions: Vec<f32> = (0..max_seq_len).map(|p| p as f32).collect();
let positions = Tensor::new(positions.as_slice(), device)?.unsqueeze(1)?;
let freqs = positions.matmul(&inv_freq.unsqueeze(0)?)?;
let emb = freqs;
let cos = emb.cos()?.to_dtype(dtype)?;
let sin = emb.sin()?.to_dtype(dtype)?;
Ok(Self { cos, sin })
}
fn apply(&self, q: &Tensor, k: &Tensor, start_pos: usize) -> CandleResult<(Tensor, Tensor)> {
let seq_len = q.dim(1)?;
let cos = self.cos.narrow(0, start_pos, seq_len)?;
let sin = self.sin.narrow(0, start_pos, seq_len)?;
let cos = Tensor::cat(&[&cos, &cos], D::Minus1)?;
let sin = Tensor::cat(&[&sin, &sin], D::Minus1)?;
let q_embed = Self::apply_rotary(q, &cos, &sin)?;
let k_embed = Self::apply_rotary(k, &cos, &sin)?;
Ok((q_embed, k_embed))
}
fn apply_rotary(x: &Tensor, cos: &Tensor, sin: &Tensor) -> CandleResult<Tensor> {
let cos = cos.unsqueeze(0)?.unsqueeze(2)?; let sin = sin.unsqueeze(0)?.unsqueeze(2)?;
let x_cos = x.broadcast_mul(&cos)?;
let x_rot = Self::rotate_half(x)?;
let x_sin = x_rot.broadcast_mul(&sin)?;
x_cos + x_sin
}
fn rotate_half(x: &Tensor) -> CandleResult<Tensor> {
let last_dim = x.dim(D::Minus1)?;
let half = last_dim / 2;
let x1 = x.narrow(D::Minus1, 0, half)?;
let x2 = x.narrow(D::Minus1, half, half)?;
Tensor::cat(&[&x2.neg()?, &x1], D::Minus1)
}
}
enum KvCacheStorage {
Standard(Option<(Tensor, Tensor)>),
Quantized(QuantizedKvCache),
OptimizedQuantized(OptimizedQuantizedKvCache),
#[cfg(feature = "cuda")]
CudaQuantized(CudaQuantizedKvCache),
}
struct Attention {
q_proj: Linear,
k_proj: Linear,
v_proj: Linear,
o_proj: Linear,
num_heads: usize,
num_kv_heads: usize,
head_dim: usize,
kv_cache: KvCacheStorage,
flash_attn: Option<FlashAttention>,
}
#[derive(Debug, Clone)]
pub enum CacheType {
Standard,
Quantized,
OptimizedQuantized(DynamicQuantConfig),
#[cfg(feature = "cuda")]
CudaQuantized(usize),
}
impl Default for CacheType {
fn default() -> Self {
Self::Standard
}
}
impl Attention {
#[allow(dead_code)]
fn load(
config: &Qwen2Config,
vb: VarBuilder,
use_flash_attn: bool,
use_quantized_cache: bool,
device: &Device,
dtype: DType,
) -> CandleResult<Self> {
let cache_type = if use_quantized_cache {
CacheType::Quantized
} else {
CacheType::Standard
};
Self::load_with_cache_type(config, vb, use_flash_attn, cache_type, device, dtype)
}
fn load_with_cache_type(
config: &Qwen2Config,
vb: VarBuilder,
use_flash_attn: bool,
cache_type: CacheType,
device: &Device,
dtype: DType,
) -> CandleResult<Self> {
let hidden_size = config.hidden_size;
let num_heads = config.num_attention_heads;
let num_kv_heads = config.num_kv_heads();
let head_dim = config.head_dim();
let q_proj = linear(hidden_size, num_heads * head_dim, vb.pp("q_proj"))?;
let k_proj = linear(hidden_size, num_kv_heads * head_dim, vb.pp("k_proj"))?;
let v_proj = linear(hidden_size, num_kv_heads * head_dim, vb.pp("v_proj"))?;
let o_proj = linear_no_bias(num_heads * head_dim, hidden_size, vb.pp("o_proj"))?;
let flash_attn = if use_flash_attn {
let flash_config =
FlashAttentionConfig::default().with_scale(1.0 / (head_dim as f32).sqrt());
Some(FlashAttention::new(flash_config))
} else {
None
};
let kv_cache = match cache_type {
CacheType::Standard => KvCacheStorage::Standard(None),
CacheType::Quantized => KvCacheStorage::Quantized(QuantizedKvCache::new(
num_kv_heads,
head_dim,
device,
dtype,
)),
CacheType::OptimizedQuantized(quant_config) => KvCacheStorage::OptimizedQuantized(
OptimizedQuantizedKvCache::new(num_kv_heads, head_dim, device, dtype, quant_config),
),
#[cfg(feature = "cuda")]
CacheType::CudaQuantized(device_id) => {
let cuda_cache = CudaQuantizedKvCache::new(num_kv_heads, head_dim, device_id)
.map_err(|e| candle_core::Error::Msg(format!("CUDA cache init failed: {e}")))?;
KvCacheStorage::CudaQuantized(cuda_cache)
},
};
Ok(Self {
q_proj,
k_proj,
v_proj,
o_proj,
num_heads,
num_kv_heads,
head_dim,
kv_cache,
flash_attn,
})
}
fn forward(
&mut self,
x: &Tensor,
rotary: &RotaryEmbedding,
mask: Option<&Tensor>,
start_pos: usize,
) -> CandleResult<Tensor> {
let (batch_size, seq_len, _) = x.dims3()?;
let q = self.q_proj.forward(x)?;
let k = self.k_proj.forward(x)?;
let v = self.v_proj.forward(x)?;
let q = q.reshape((batch_size, seq_len, self.num_heads, self.head_dim))?;
let k = k.reshape((batch_size, seq_len, self.num_kv_heads, self.head_dim))?;
let v = v.reshape((batch_size, seq_len, self.num_kv_heads, self.head_dim))?;
let (q, k) = rotary.apply(&q, &k, start_pos)?;
let q = q.transpose(1, 2)?.contiguous()?;
let k = k.transpose(1, 2)?.contiguous()?;
let v = v.transpose(1, 2)?.contiguous()?;
#[cfg(feature = "cuda")]
let cuda_attn_output: Option<Tensor> =
if let KvCacheStorage::CudaQuantized(cuda_cache) = &mut self.kv_cache {
cuda_cache.append(&k, &v).map_err(|e| {
candle_core::Error::Msg(format!("CUDA cache append failed: {e}"))
})?;
let attn_scale = 1.0 / (self.head_dim as f32).sqrt();
let output = cuda_cache
.forward_attention(&q, self.num_heads, attn_scale)
.map_err(|e| candle_core::Error::Msg(format!("CUDA attention failed: {e}")))?;
Some(output)
} else {
None
};
#[cfg(not(feature = "cuda"))]
let cuda_attn_output: Option<Tensor> = None;
let attn_output = if let Some(cuda_out) = cuda_attn_output {
cuda_out
} else {
let (k_full, v_full, cache_is_empty) = match &mut self.kv_cache {
KvCacheStorage::Standard(cache) => {
let (k_out, v_out) = match cache {
Some((prev_k, prev_v)) => {
let k_cat = Tensor::cat(&[prev_k.as_ref(), &k], 2)?;
let v_cat = Tensor::cat(&[prev_v.as_ref(), &v], 2)?;
(k_cat, v_cat)
},
None => (k.clone(), v.clone()),
};
let is_empty = cache.is_none();
*cache = Some((k_out.clone(), v_out.clone()));
(k_out, v_out, is_empty)
},
KvCacheStorage::Quantized(cache) => {
let is_empty = cache.seq_len() == 0;
cache.append(&k, &v)?;
let (k_full, v_full) = cache.get_dequantized()?.unwrap_or((k, v));
(k_full, v_full, is_empty)
},
KvCacheStorage::OptimizedQuantized(cache) => {
let is_empty = cache.seq_len() == 0;
cache.append(&k, &v)?;
let (k_full, v_full) = cache.get_kv()?.unwrap_or((k, v));
(k_full, v_full, is_empty)
},
#[cfg(feature = "cuda")]
KvCacheStorage::CudaQuantized(_) => unreachable!("CUDA path handled above"),
};
let k = Self::repeat_kv(k_full, self.num_heads / self.num_kv_heads)?;
let v = Self::repeat_kv(v_full, self.num_heads / self.num_kv_heads)?;
if let Some(flash) = &self.flash_attn {
if seq_len > 1 || cache_is_empty {
flash.forward(&q, &k, &v, None, Some(true))?
} else {
self.standard_attention(&q, &k, &v, mask)?
}
} else {
self.standard_attention(&q, &k, &v, mask)?
}
};
let attn_output = attn_output.transpose(1, 2)?.reshape((
batch_size,
seq_len,
self.num_heads * self.head_dim,
))?;
self.o_proj.forward(&attn_output)
}
fn standard_attention(
&self,
q: &Tensor,
k: &Tensor,
v: &Tensor,
mask: Option<&Tensor>,
) -> CandleResult<Tensor> {
let scale = (self.head_dim as f64).sqrt();
let attn_weights = q.matmul(&k.transpose(D::Minus2, D::Minus1)?)?;
let attn_weights = (attn_weights / scale)?;
let attn_weights = match mask {
Some(m) => attn_weights.broadcast_add(m)?,
None => attn_weights,
};
let attn_weights = candle_nn::ops::softmax_last_dim(&attn_weights)?;
attn_weights.matmul(v)
}
fn repeat_kv(x: Tensor, n_rep: usize) -> CandleResult<Tensor> {
if n_rep == 1 {
return Ok(x);
}
let (batch, num_kv_heads, seq_len, head_dim) = x.dims4()?;
let x = x
.unsqueeze(2)?
.expand((batch, num_kv_heads, n_rep, seq_len, head_dim))?
.reshape((batch, num_kv_heads * n_rep, seq_len, head_dim))?;
Ok(x)
}
fn clear_cache(&mut self) {
match &mut self.kv_cache {
KvCacheStorage::Standard(cache) => *cache = None,
KvCacheStorage::Quantized(cache) => cache.clear(),
KvCacheStorage::OptimizedQuantized(cache) => cache.clear(),
#[cfg(feature = "cuda")]
KvCacheStorage::CudaQuantized(cache) => cache.clear(),
}
}
fn cache_memory_bytes(&self) -> usize {
match &self.kv_cache {
KvCacheStorage::Standard(cache) => {
cache.as_ref().map_or(0, |(k, v)| {
k.elem_count() * 2 + v.elem_count() * 2 })
},
KvCacheStorage::Quantized(cache) => cache.memory_bytes(),
KvCacheStorage::OptimizedQuantized(cache) => cache.memory_bytes(),
#[cfg(feature = "cuda")]
KvCacheStorage::CudaQuantized(cache) => cache.memory_bytes(),
}
}
}
struct Mlp {
gate_proj: Linear,
up_proj: Linear,
down_proj: Linear,
}
impl Mlp {
fn load(config: &Qwen2Config, vb: VarBuilder) -> CandleResult<Self> {
let hidden_size = config.hidden_size;
let intermediate_size = config.intermediate_size;
let gate_proj = linear_no_bias(hidden_size, intermediate_size, vb.pp("gate_proj"))?;
let up_proj = linear_no_bias(hidden_size, intermediate_size, vb.pp("up_proj"))?;
let down_proj = linear_no_bias(intermediate_size, hidden_size, vb.pp("down_proj"))?;
Ok(Self {
gate_proj,
up_proj,
down_proj,
})
}
}
impl Module for Mlp {
fn forward(&self, x: &Tensor) -> CandleResult<Tensor> {
let gate = self.gate_proj.forward(x)?;
let gate = candle_nn::ops::silu(&gate)?;
let up = self.up_proj.forward(x)?;
let x = (gate * up)?;
self.down_proj.forward(&x)
}
}
struct DecoderLayer {
self_attn: Attention,
mlp: Mlp,
input_layernorm: RmsNorm,
post_attention_layernorm: RmsNorm,
}
impl DecoderLayer {
fn load(
config: &Qwen2Config,
vb: VarBuilder,
use_flash_attn: bool,
cache_type: CacheType,
device: &Device,
dtype: DType,
) -> CandleResult<Self> {
let self_attn = Attention::load_with_cache_type(
config,
vb.pp("self_attn"),
use_flash_attn,
cache_type,
device,
dtype,
)?;
let mlp = Mlp::load(config, vb.pp("mlp"))?;
let input_layernorm = RmsNorm::load(
config.hidden_size,
config.rms_norm_eps,
vb.pp("input_layernorm"),
)?;
let post_attention_layernorm = RmsNorm::load(
config.hidden_size,
config.rms_norm_eps,
vb.pp("post_attention_layernorm"),
)?;
Ok(Self {
self_attn,
mlp,
input_layernorm,
post_attention_layernorm,
})
}
fn forward(
&mut self,
x: &Tensor,
rotary: &RotaryEmbedding,
mask: Option<&Tensor>,
start_pos: usize,
) -> CandleResult<Tensor> {
let residual = x;
let x = self.input_layernorm.forward(x)?;
let x = self.self_attn.forward(&x, rotary, mask, start_pos)?;
let x = (residual + x)?;
let residual = &x;
let x = self.post_attention_layernorm.forward(&x)?;
let x = self.mlp.forward(&x)?;
residual + x
}
fn clear_cache(&mut self) {
self.self_attn.clear_cache();
}
}
pub struct Qwen2 {
embed_tokens: Embedding,
layers: Vec<DecoderLayer>,
norm: RmsNorm,
lm_head: Linear,
rotary: RotaryEmbedding,
config: Qwen2Config,
device: Device,
dtype: DType,
}
impl Qwen2 {
pub fn load(config: Qwen2Config, vb: VarBuilder) -> CandleResult<Self> {
Self::load_with_cache_type(config, vb, false, CacheType::Standard)
}
pub fn load_with_flash_attention(config: Qwen2Config, vb: VarBuilder) -> CandleResult<Self> {
Self::load_with_cache_type(config, vb, true, CacheType::Standard)
}
pub fn load_with_quantized_cache(config: Qwen2Config, vb: VarBuilder) -> CandleResult<Self> {
Self::load_with_cache_type(config, vb, false, CacheType::Quantized)
}
pub fn load_with_flash_and_quantized_cache(
config: Qwen2Config,
vb: VarBuilder,
) -> CandleResult<Self> {
Self::load_with_cache_type(config, vb, true, CacheType::Quantized)
}
pub fn load_with_optimized_cache(
config: Qwen2Config,
vb: VarBuilder,
quant_config: DynamicQuantConfig,
) -> CandleResult<Self> {
Self::load_with_cache_type(
config,
vb,
false,
CacheType::OptimizedQuantized(quant_config),
)
}
pub fn load_with_flash_and_optimized_cache(
config: Qwen2Config,
vb: VarBuilder,
quant_config: DynamicQuantConfig,
) -> CandleResult<Self> {
Self::load_with_cache_type(
config,
vb,
true,
CacheType::OptimizedQuantized(quant_config),
)
}
#[cfg(feature = "cuda")]
pub fn load_with_cuda_quantized_cache(
config: Qwen2Config,
vb: VarBuilder,
cuda_device_id: usize,
) -> CandleResult<Self> {
Self::load_with_cache_type(config, vb, false, CacheType::CudaQuantized(cuda_device_id))
}
fn load_with_cache_type(
config: Qwen2Config,
vb: VarBuilder,
use_flash_attn: bool,
cache_type: CacheType,
) -> CandleResult<Self> {
let device = vb.device().clone();
let dtype = vb.dtype();
let embed_tokens = embedding(
config.vocab_size,
config.hidden_size,
vb.pp("model.embed_tokens"),
)?;
let mut layers = Vec::with_capacity(config.num_hidden_layers);
for i in 0..config.num_hidden_layers {
let layer = DecoderLayer::load(
&config,
vb.pp(format!("model.layers.{}", i)),
use_flash_attn,
cache_type.clone(),
&device,
dtype,
)?;
layers.push(layer);
}
let norm = RmsNorm::load(config.hidden_size, config.rms_norm_eps, vb.pp("model.norm"))?;
let lm_head = if config.tie_word_embeddings {
Linear::new(embed_tokens.embeddings().clone(), None)
} else {
linear_no_bias(config.hidden_size, config.vocab_size, vb.pp("lm_head"))?
};
let rotary = RotaryEmbedding::new(&config, dtype, &device)?;
Ok(Self {
embed_tokens,
layers,
norm,
lm_head,
rotary,
config,
device,
dtype,
})
}
pub fn forward(&mut self, input_ids: &Tensor, start_pos: usize) -> CandleResult<Tensor> {
let (_batch_size, seq_len) = input_ids.dims2()?;
let mut hidden_states = self.embed_tokens.forward(input_ids)?;
let mask = if seq_len > 1 {
Some(Self::create_causal_mask(
seq_len,
start_pos,
&self.device,
self.dtype,
)?)
} else {
None
};
for layer in &mut self.layers {
hidden_states =
layer.forward(&hidden_states, &self.rotary, mask.as_ref(), start_pos)?;
}
let hidden_states = self.norm.forward(&hidden_states)?;
self.lm_head.forward(&hidden_states)
}
fn create_causal_mask(
seq_len: usize,
start_pos: usize,
device: &Device,
dtype: DType,
) -> CandleResult<Tensor> {
let mask: Vec<f32> = (0..seq_len)
.flat_map(|i| {
(0..seq_len + start_pos).map(move |j| {
if j > i + start_pos {
f32::NEG_INFINITY
} else {
0.0
}
})
})
.collect();
Tensor::from_vec(mask, (seq_len, seq_len + start_pos), device)?.to_dtype(dtype)
}
pub fn clear_cache(&mut self) {
for layer in &mut self.layers {
layer.clear_cache();
}
}
pub fn config(&self) -> &Qwen2Config {
&self.config
}
pub fn device(&self) -> &Device {
&self.device
}
pub fn dtype(&self) -> DType {
self.dtype
}
pub fn cache_memory_bytes(&self) -> usize {
self.layers
.iter()
.map(|l| l.self_attn.cache_memory_bytes())
.sum()
}
pub fn cache_seq_len(&self) -> usize {
self.layers
.first()
.map_or(0, |l| match &l.self_attn.kv_cache {
KvCacheStorage::Standard(cache) => cache.as_ref().map_or(0, |(k, _)| k.dims()[2]),
KvCacheStorage::Quantized(cache) => cache.seq_len(),
KvCacheStorage::OptimizedQuantized(cache) => cache.seq_len(),
#[cfg(feature = "cuda")]
KvCacheStorage::CudaQuantized(cache) => cache.seq_len(),
})
}
pub fn forward_embedding(&mut self, input_ids: &Tensor) -> CandleResult<Tensor> {
self.clear_cache();
let (_batch_size, seq_len) = input_ids.dims2()?;
let mut hidden_states = self.embed_tokens.forward(input_ids)?;
let mask = if seq_len > 1 {
Some(Self::create_causal_mask(
seq_len,
0,
&self.device,
self.dtype,
)?)
} else {
None
};
for layer in &mut self.layers {
hidden_states = layer.forward(&hidden_states, &self.rotary, mask.as_ref(), 0)?;
}
self.norm.forward(&hidden_states)
}
pub fn extract_embeddings(&mut self, input_ids: &Tensor) -> CandleResult<Tensor> {
let hidden_states = self.forward_embedding(input_ids)?;
hidden_states.mean(1)
}
}