use candle_core::{Result, Tensor, D};
use candle_nn::{layer_norm, linear, LayerNorm, Linear, Module, VarBuilder};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValueAttentionConfig {
pub value_embed_dim: usize,
pub n_values: usize,
pub n_heads: usize,
pub self_attn_layers: usize,
pub ffn_hidden: usize,
pub pool_slots: usize,
pub dropout: f32,
#[serde(default = "default_true")]
pub keep_blender_concat: bool,
}
fn default_true() -> bool {
true
}
impl Default for ValueAttentionConfig {
fn default() -> Self {
Self {
value_embed_dim: 256,
n_values: 50,
n_heads: 4,
self_attn_layers: 1,
ffn_hidden: 512,
pool_slots: 4,
dropout: 0.1,
keep_blender_concat: true,
}
}
}
impl ValueAttentionConfig {
pub fn output_dim(&self) -> usize {
self.pool_slots * self.value_embed_dim
}
}
struct MultiHeadAttention {
q_proj: Linear,
k_proj: Linear,
v_proj: Linear,
out_proj: Linear,
n_heads: usize,
head_dim: usize,
}
impl MultiHeadAttention {
fn new(dim: usize, n_heads: usize, vb: VarBuilder) -> Result<Self> {
assert!(
dim.is_multiple_of(n_heads),
"value_embed_dim {dim} not divisible by n_heads {n_heads}"
);
Ok(Self {
q_proj: linear(dim, dim, vb.pp("q_proj"))?,
k_proj: linear(dim, dim, vb.pp("k_proj"))?,
v_proj: linear(dim, dim, vb.pp("v_proj"))?,
out_proj: linear(dim, dim, vb.pp("out_proj"))?,
n_heads,
head_dim: dim / n_heads,
})
}
fn split_heads(&self, x: &Tensor) -> Result<Tensor> {
let (b, l, _d) = x.dims3()?;
x.reshape((b, l, self.n_heads, self.head_dim))?
.transpose(1, 2)?
.contiguous()
}
fn forward(&self, q_in: &Tensor, kv_in: &Tensor, key_mask: &Tensor) -> Result<Tensor> {
let (b, lq, d) = q_in.dims3()?;
let lk = kv_in.dim(1)?;
let q = self.split_heads(&self.q_proj.forward(q_in)?)?; let k = self.split_heads(&self.k_proj.forward(kv_in)?)?; let v = self.split_heads(&self.v_proj.forward(kv_in)?)?;
let scale = (self.head_dim as f64).sqrt();
let scores = (q.matmul(&k.transpose(2, 3)?.contiguous()?)? / scale)?;
let add_mask = ((key_mask.reshape((b, 1, 1, lk))? - 1.0)? * 1e9_f64)?;
let scores = scores.broadcast_add(&add_mask)?;
let probs = candle_nn::ops::softmax(&scores, D::Minus1)?; let ctx = probs.matmul(&v)?;
let ctx = ctx
.transpose(1, 2)? .contiguous()?
.reshape((b, lq, d))?;
self.out_proj.forward(&ctx)
}
}
struct SelfAttnBlock {
ln1: LayerNorm,
attn: MultiHeadAttention,
ln2: LayerNorm,
fc1: Linear,
fc2: Linear,
dropout: f32,
}
impl SelfAttnBlock {
fn new(cfg: &ValueAttentionConfig, vb: VarBuilder) -> Result<Self> {
let d = cfg.value_embed_dim;
Ok(Self {
ln1: layer_norm(d, 1e-5, vb.pp("ln1"))?,
attn: MultiHeadAttention::new(d, cfg.n_heads, vb.pp("attn"))?,
ln2: layer_norm(d, 1e-5, vb.pp("ln2"))?,
fc1: linear(d, cfg.ffn_hidden, vb.pp("fc1"))?,
fc2: linear(cfg.ffn_hidden, d, vb.pp("fc2"))?,
dropout: cfg.dropout,
})
}
fn maybe_dropout(&self, x: &Tensor, train: bool) -> Result<Tensor> {
if train && self.dropout > 0.0 {
candle_nn::ops::dropout(x, self.dropout)
} else {
Ok(x.clone())
}
}
fn forward(&self, x: &Tensor, key_mask: &Tensor, train: bool) -> Result<Tensor> {
let h = self.ln1.forward(x)?;
let attn = self.attn.forward(&h, &h, key_mask)?;
let x = (x + self.maybe_dropout(&attn, train)?)?;
let h = self.ln2.forward(&x)?;
let ff = self.fc2.forward(&self.fc1.forward(&h)?.gelu_erf()?)?;
x + self.maybe_dropout(&ff, train)?
}
}
pub struct ValueAttentionPool {
blocks: Vec<SelfAttnBlock>,
seed: Tensor,
pool_attn: MultiHeadAttention,
cfg: ValueAttentionConfig,
}
impl ValueAttentionPool {
pub fn new(cfg: &ValueAttentionConfig, vb: VarBuilder) -> Result<Self> {
let d = cfg.value_embed_dim;
let mut blocks = Vec::with_capacity(cfg.self_attn_layers);
for i in 0..cfg.self_attn_layers {
blocks.push(SelfAttnBlock::new(cfg, vb.pp(format!("block{i}")))?);
}
let seed = vb.get_with_hints(
(cfg.pool_slots, d),
"pool_seed",
candle_nn::Init::Randn {
mean: 0.0,
stdev: 0.02,
},
)?;
let pool_attn = MultiHeadAttention::new(d, cfg.n_heads, vb.pp("pool_attn"))?;
Ok(Self {
blocks,
seed,
pool_attn,
cfg: cfg.clone(),
})
}
pub fn output_dim(&self) -> usize {
self.cfg.output_dim()
}
pub fn forward(&self, value_embeds: &Tensor, mask: &Tensor, train: bool) -> Result<Tensor> {
let (b, _n, d) = value_embeds.dims3()?;
let mut x = value_embeds.clone();
for block in &self.blocks {
x = block.forward(&x, mask, train)?;
}
let seed = self
.seed
.unsqueeze(0)?
.broadcast_as((b, self.cfg.pool_slots, d))?
.contiguous()?;
let pooled = self.pool_attn.forward(&seed, &x, mask)?; pooled.reshape((b, self.cfg.pool_slots * d))
}
}
#[cfg(test)]
mod tests {
use super::*;
use candle_core::{DType, Device};
use candle_nn::VarMap;
fn build(cfg: &ValueAttentionConfig) -> (ValueAttentionPool, Device) {
let dev = Device::Cpu;
let varmap = VarMap::new();
let vb = VarBuilder::from_varmap(&varmap, DType::F32, &dev);
(ValueAttentionPool::new(cfg, vb).unwrap(), dev)
}
#[test]
fn test_output_shape() {
let cfg = ValueAttentionConfig::default();
let (pool, dev) = build(&cfg);
let (b, n, d) = (3usize, cfg.n_values, cfg.value_embed_dim);
let x = Tensor::randn(0f32, 1.0, (b, n, d), &dev).unwrap();
let mask = Tensor::ones((b, n), DType::F32, &dev).unwrap();
let out = pool.forward(&x, &mask, false).unwrap();
assert_eq!(out.dims(), &[b, cfg.output_dim()]);
assert_eq!(cfg.output_dim(), cfg.pool_slots * cfg.value_embed_dim);
}
#[test]
fn test_padding_is_ignored() {
let cfg = ValueAttentionConfig {
n_values: 6,
..Default::default()
};
let (pool, dev) = build(&cfg);
let d = cfg.value_embed_dim;
let real = Tensor::randn(0f32, 1.0, (1, 3, d), &dev).unwrap();
let junk = Tensor::randn(5f32, 1.0, (1, 3, d), &dev).unwrap();
let zeros = Tensor::zeros((1, 3, d), DType::F32, &dev).unwrap();
let a = Tensor::cat(&[&real, &junk], 1).unwrap();
let b = Tensor::cat(&[&real, &zeros], 1).unwrap();
let mask = Tensor::from_vec(vec![1f32, 1., 1., 0., 0., 0.], (1, 6), &dev).unwrap();
let oa = pool.forward(&a, &mask, false).unwrap();
let ob = pool.forward(&b, &mask, false).unwrap();
let diff = (oa - ob).unwrap().abs().unwrap().max(D::Minus1).unwrap();
let maxd = diff.to_vec1::<f32>().unwrap()[0];
assert!(maxd < 1e-4, "masked tail leaked into pool: max diff {maxd}");
}
#[test]
fn test_save_load_roundtrip() {
let cfg = ValueAttentionConfig {
n_values: 6,
..Default::default()
};
let dev = Device::Cpu;
let varmap = VarMap::new();
let vb = VarBuilder::from_varmap(&varmap, DType::F32, &dev);
let pool1 = ValueAttentionPool::new(&cfg, vb.pp("value_attn")).unwrap();
let path = std::env::temp_dir().join(format!("va_pool_{}.safetensors", std::process::id()));
varmap.save(&path).unwrap();
let vb2 =
unsafe { VarBuilder::from_mmaped_safetensors(&[&path], DType::F32, &dev).unwrap() };
let pool2 = ValueAttentionPool::new(&cfg, vb2.pp("value_attn")).unwrap();
let (b, n, d) = (2usize, cfg.n_values, cfg.value_embed_dim);
let x = Tensor::randn(0f32, 1.0, (b, n, d), &dev).unwrap();
let mask = Tensor::ones((b, n), DType::F32, &dev).unwrap();
let o1 = pool1.forward(&x, &mask, false).unwrap();
let o2 = pool2.forward(&x, &mask, false).unwrap();
let maxd = (o1 - o2)
.unwrap()
.abs()
.unwrap()
.max(D::Minus1)
.unwrap()
.max(D::Minus1)
.unwrap()
.to_scalar::<f32>()
.unwrap();
std::fs::remove_file(&path).ok();
assert!(maxd < 1e-6, "reloaded pool diverged: max diff {maxd}");
}
#[test]
#[ignore] fn bench_latency_cpu_batch1() {
let cfg = ValueAttentionConfig::default(); let dev = Device::Cpu;
let varmap = VarMap::new();
let vb = VarBuilder::from_varmap(&varmap, DType::F32, &dev);
let pool = ValueAttentionPool::new(&cfg, vb).unwrap();
let x = Tensor::randn(0f32, 1.0, (1, cfg.n_values, cfg.value_embed_dim), &dev).unwrap();
let mask = Tensor::ones((1, cfg.n_values), DType::F32, &dev).unwrap();
for _ in 0..10 {
let _ = pool.forward(&x, &mask, false).unwrap();
}
let n = 200;
let t = std::time::Instant::now();
for _ in 0..n {
let o = pool.forward(&x, &mask, false).unwrap();
let _ = o.sum_all().unwrap().to_scalar::<f32>().unwrap(); }
let per = t.elapsed().as_secs_f64() * 1000.0 / n as f64;
println!(
"POOL forward CPU batch=1 [1,{},{}]: {per:.3} ms/column",
cfg.n_values, cfg.value_embed_dim
);
}
#[test]
fn test_single_valid_value() {
let cfg = ValueAttentionConfig {
n_values: 4,
..Default::default()
};
let (pool, dev) = build(&cfg);
let d = cfg.value_embed_dim;
let x = Tensor::randn(0f32, 1.0, (1, 4, d), &dev).unwrap();
let mask = Tensor::from_vec(vec![1f32, 0., 0., 0.], (1, 4), &dev).unwrap();
let out = pool.forward(&x, &mask, false).unwrap();
let vals = out.flatten_all().unwrap().to_vec1::<f32>().unwrap();
assert!(
vals.iter().all(|v| v.is_finite()),
"pool produced non-finite output"
);
}
}