use frink_core::attention::causal_gqa_attention_row;
use frink_core::cache::{KvCache, PagedKvCache, SharedPagedKv};
use frink_core::matmul::rms_norm;
use frink_core::recurrent_state::RecurrentState;
use super::{Decoder, LayerWeights};
use crate::layer_shapes::AttnShape;
pub(crate) enum KvStep<'a> {
Decode(&'a mut KvCache),
Batched(&'a mut KvCache),
Paged {
cache: &'a mut PagedKvCache,
stores: &'a SharedPagedKv,
},
}
impl KvStep<'_> {
pub(crate) fn recurrent_slot(&mut self) -> &mut Option<RecurrentState> {
match self {
KvStep::Decode(cache) | KvStep::Batched(cache) => &mut cache.recurrent,
KvStep::Paged { cache, .. } => &mut cache.recurrent,
}
}
}
pub(crate) enum AttnTail<'a> {
Apply(std::marker::PhantomData<&'a ()>),
#[cfg(feature = "metal")]
Defer {
branch: &'a mut Option<Vec<f32>>,
ready: Option<&'a mut Option<AttnReady>>,
},
}
#[cfg_attr(not(feature = "metal"), allow(dead_code))]
pub(crate) struct AttnReady {
pub q: Vec<f32>,
pub k: Vec<f32>,
pub v: Vec<f32>,
pub gate: Option<Vec<f32>>,
}
impl AttnTail<'_> {
pub(crate) fn apply() -> Self {
AttnTail::Apply(std::marker::PhantomData)
}
}
impl Decoder {
fn project_qkv(layer: &LayerWeights, normed: &[f32]) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
#[cfg(any(feature = "cuda", feature = "metal"))]
{
if let Some(mut outs) = frink_core::WeightMatrix::apply_gpu_multi(
&[&layer.attn.q_proj, &layer.attn.k_proj, &layer.attn.v_proj],
normed,
) {
let v = outs.pop().unwrap();
let k = outs.pop().unwrap();
let q = outs.pop().unwrap();
(q, k, v)
} else {
frink_core::weight_matrix::WeightMatrix::apply_three(
&layer.attn.q_proj,
&layer.attn.k_proj,
&layer.attn.v_proj,
normed,
)
}
}
#[cfg(not(any(feature = "cuda", feature = "metal")))]
{
frink_core::weight_matrix::WeightMatrix::apply_three(
&layer.attn.q_proj,
&layer.attn.k_proj,
&layer.attn.v_proj,
normed,
)
}
}
pub(crate) fn attn_block(
&self,
layer_idx: usize,
layer: &LayerWeights,
normed: &[f32],
pos: usize,
kv: KvStep<'_>,
) -> Option<Vec<f32>> {
self.attn_block_tail(layer_idx, layer, normed, pos, kv, AttnTail::apply(), None)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn attn_block_tail(
&self,
layer_idx: usize,
layer: &LayerWeights,
normed: &[f32],
pos: usize,
mut kv: KvStep<'_>,
#[cfg_attr(not(feature = "metal"), allow(unused_variables, unused_mut))] mut tail: AttnTail<
'_,
>,
precomputed: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
) -> Option<Vec<f32>> {
let head_dim = self.config.head_dim;
let (n_heads, n_kv_heads) = match self.config.layer_shape(layer_idx).attention {
AttnShape::Gqa {
n_heads,
n_kv_heads,
} => (n_heads, n_kv_heads),
AttnShape::Linear => return Some(layer.attn.o_proj.apply(normed)),
AttnShape::Absent => return None,
AttnShape::ShortConv
| AttnShape::Mamba2
| AttnShape::Mamba1
| AttnShape::Plamo2Ssm
| AttnShape::Gdn
| AttnShape::Lightning => {
return Some(self.recurrent_block(layer_idx, layer, normed, 1, kv))
}
};
let (mut q, mut k, mut v) = match precomputed {
Some(qkv) => qkv,
None => Self::project_qkv(layer, normed),
};
let q_gate = layer.attn.q_gate_interleaved.then(|| {
let (qq, gate) = crate::attn_gate::split_interleaved_q_gate(&q, 1, n_heads, head_dim);
q = qq;
gate
});
let (q_width, kv_width, v_width) = (q.len(), k.len(), v.len());
self.apply_qkv_bias_and_clamp(layer, &mut q, &mut k, &mut v, q_width, kv_width, v_width);
self.apply_qk_norms_pre_rope(layer, &mut q, &mut k, q_width, kv_width);
self.apply_rope_attn_factor(&mut q, &mut k, layer_idx);
for h in 0..n_heads {
self.apply_rope_head_layer(&mut q[h * head_dim..(h + 1) * head_dim], pos, layer_idx);
}
for h in 0..n_kv_heads {
self.apply_rope_head_layer(&mut k[h * head_dim..(h + 1) * head_dim], pos, layer_idx);
}
self.apply_qk_norms_post_rope(layer, layer_idx, &mut q, &mut k, q_width, kv_width);
self.apply_attention_scale(&mut q);
self.apply_attn_temperature(layer_idx, &mut q, q_width, |_| pos);
let ssm = self.parallel_ssm_rows(layer_idx, layer, normed, 1, kv.recurrent_slot());
#[cfg(feature = "metal")]
if let AttnTail::Defer {
ready: Some(slot), ..
} = &mut tail
{
debug_assert!(
ssm.is_none(),
"a parallel SSM branch cannot defer its attention"
);
**slot = Some(AttnReady {
q,
k,
v,
gate: q_gate,
});
return None;
}
let mut attn_out = self.push_and_attend_row(kv, layer_idx, layer, &k, &v, &q);
let branch = self.attn_branch_rows(layer, normed, &mut attn_out, 1, q_gate.as_deref());
#[cfg(feature = "metal")]
if let AttnTail::Defer { branch: slot, .. } = tail {
debug_assert!(ssm.is_none(), "a parallel SSM branch cannot defer its tail");
*slot = Some(branch);
return None;
}
let mut projected = self.project_attn_rows(layer, &branch, 1);
Self::add_parallel_ssm(&mut projected, ssm);
Some(projected)
}
pub(crate) fn attn_out_to_residual_rows(
&self,
layer: &LayerWeights,
normed: &[f32],
attn_out: &mut [f32],
rows: usize,
q_gate: Option<&[f32]>,
) -> Vec<f32> {
let branch = self.attn_branch_rows(layer, normed, attn_out, rows, q_gate);
self.project_attn_rows(layer, &branch, rows)
}
pub(crate) fn attn_branch_rows(
&self,
layer: &LayerWeights,
normed: &[f32],
attn_out: &mut [f32],
rows: usize,
q_gate: Option<&[f32]>,
) -> Vec<f32> {
if let Some(gate) = q_gate {
crate::attn_gate::apply_interleaved_gate(attn_out, gate);
}
if let Some(gate) = &layer.attn.output_gate {
gate.apply_rows(normed, attn_out, rows, self.config.head_dim);
}
match &layer.attn.attn_sub_norm {
None => attn_out.to_vec(),
Some(w) => {
let width = w.len();
attn_out
.chunks(width)
.flat_map(|row| rms_norm(row, w, self.config.rms_norm_eps))
.collect::<Vec<f32>>()
}
}
}
pub(crate) fn project_attn_rows(
&self,
layer: &LayerWeights,
attn_out: &[f32],
rows: usize,
) -> Vec<f32> {
let mut projected = if rows == 1 {
layer.attn.o_proj.apply(attn_out)
} else {
layer.attn.o_proj.apply_batch(attn_out, rows)
};
if let Some(scale) = layer.attn.o_scale {
for x in projected.iter_mut() {
*x *= scale;
}
}
if let Some(b) = &layer.attn.o_bias {
let hidden = b.len();
for row in projected.chunks_mut(hidden) {
for (x, b) in row.iter_mut().zip(b.iter()) {
*x += b;
}
}
}
if let Some(scale) = self.config.attn_value_scale {
for x in projected.iter_mut() {
*x *= scale;
}
}
if let Some(post) = &layer.attn.post_attn_norm {
let hidden = post.len();
projected = projected
.chunks(hidden)
.flat_map(|row| rms_norm(row, post, self.config.post_norm_eps()))
.collect();
}
projected
}
pub(crate) fn push_and_attend_row(
&self,
kv: KvStep<'_>,
layer_idx: usize,
layer: &LayerWeights,
k: &[f32],
v: &[f32],
q: &[f32],
) -> Vec<f32> {
let sinks = layer.attn.sinks.as_deref();
let shape = self.config.layer_shape(layer_idx).attention;
let (n_heads, n_kv_heads) = (shape.n_heads(), shape.n_kv_heads());
assert!(
matches!(shape, AttnShape::Gqa { .. }),
"layer {layer_idx} has no KV to push ({shape:?})"
);
let head_dim = self.config.head_dim;
let v_head_dim = self.config.v_head_dim();
let query_pos = match &kv {
KvStep::Decode(cache) | KvStep::Batched(cache) => cache.positions(),
KvStep::Paged { cache, .. } => cache.seq_len(),
};
let window = self.config.layer_window_for_query(layer_idx, query_pos);
let softcap = if sinks.is_some() {
None
} else {
self.config.attn_logit_softcap
};
let cuda_resident_layer = match &kv {
KvStep::Decode(_)
if window.is_none()
&& sinks.is_none()
&& v_head_dim == head_dim
&& self.alibi_slopes.is_none() =>
{
Some(layer_idx)
}
KvStep::Decode(_) | KvStep::Batched(_) | KvStep::Paged { .. } => None,
};
match kv {
KvStep::Decode(cache) | KvStep::Batched(cache) => {
cache
.push(k, v)
.expect("unbounded/planned KvCache growth is infallible");
let out = match cuda_resident_layer {
Some(l) => self.gqa_attention(
l,
q,
&cache.k,
&cache.v,
n_heads,
n_kv_heads,
head_dim,
cache.rows(),
),
None => causal_gqa_attention_row(
q,
&cache.k,
&cache.v,
n_heads,
n_kv_heads,
head_dim,
v_head_dim,
cache.rows(),
window,
sinks,
softcap,
self.alibi_slopes.as_deref(),
),
};
self.evict_layer_kv(layer_idx, cache);
out
}
KvStep::Paged { cache, stores } => {
{
let mut store = stores.write(layer_idx);
cache
.push(&mut store, k, v)
.expect("every caller reserves this row's pages before the stack runs");
}
let store = stores.read(layer_idx);
frink_core::causal_gqa_attention_paged_sinks(
q,
&store,
cache.block_table(),
n_heads,
n_kv_heads,
head_dim,
cache.seq_len(),
window,
sinks,
softcap,
self.alibi_slopes.as_deref(),
)
}
}
}
}