use crate::model::ParsingContext;
use crate::pb::NodeProto;
use tract_core::ops::change_axes::AxisOp;
use tract_hir::internal::*;
use tract_transformers::ops::sdpa::Sdpa;
pub fn group_query_attention(
_ctx: &ParsingContext,
node: &NodeProto,
) -> TractResult<(Box<dyn InferenceOp>, Vec<String>)> {
let num_heads: usize = node.get_attr("num_heads")?;
let kv_num_heads: usize = node.get_attr("kv_num_heads")?;
let scale = node.get_attr_opt::<f32>("scale")?;
ensure!(
node.get_attr_opt::<i64>("do_rotary")?.unwrap_or(0) == 0,
"GroupQueryAttention: internal rotary (do_rotary) is unsupported; apply RotaryEmbedding separately"
);
let window = node.get_attr_opt::<i64>("local_window_size")?.unwrap_or(0).max(0) as usize;
ensure!(
node.get_attr_opt::<f32>("softcap")?.unwrap_or(0.0) == 0.0,
"GroupQueryAttention: softcap is unsupported"
);
let have_past = (node.input.len() > 3 && !node.input[3].is_empty())
|| (node.input.len() > 4 && !node.input[4].is_empty());
ensure!(
!have_past,
"GroupQueryAttention: past KV cache (decode step) is unsupported; only prefill is handled"
);
Ok((expand(GroupQueryAttention { num_heads, kv_num_heads, scale, window }), vec![]))
}
#[derive(Debug, Clone)]
struct GroupQueryAttention {
num_heads: usize,
kv_num_heads: usize,
scale: Option<f32>,
window: usize,
}
fn windowed_causal_mask(qs: usize, ks: usize, window: usize) -> tract_ndarray::Array2<f32> {
tract_ndarray::Array2::<f32>::from_shape_fn((qs, ks), |(i, j)| {
if j <= i && (window == 0 || i - j < window) { 0.0f32 } else { f32::NEG_INFINITY }
})
}
fn to_4d(
model: &mut TypedModel,
prefix: &str,
x: OutletId,
total: TDim,
heads: usize,
) -> TractResult<OutletId> {
let head_dim = total.clone() / heads;
let reshaped = model.wire_node(
format!("{prefix}.reshape"),
AxisOp::Reshape(2, tvec![total], tvec![heads.to_dim(), head_dim]),
&[x],
)?[0];
Ok(model.wire_node(format!("{prefix}.transpose"), AxisOp::Move(2, 1), &[reshaped])?[0])
}
impl Expansion for GroupQueryAttention {
fn name(&self) -> StaticName {
"GroupQueryAttention".into()
}
fn nboutputs(&self) -> TractResult<usize> {
Ok(3)
}
fn rules<'r, 'p: 'r, 's: 'r>(
&'s self,
s: &mut Solver<'r>,
inputs: &'p [TensorProxy],
outputs: &'p [TensorProxy],
) -> InferenceResult {
check_output_arity(outputs, 3)?;
s.equals(&inputs[0].datum_type, &outputs[0].datum_type)?;
s.equals(&inputs[0].shape, &outputs[0].shape)?;
s.equals(&inputs[0].datum_type, &outputs[1].datum_type)?;
s.equals(&inputs[0].datum_type, &outputs[2].datum_type)?;
let kvh = self.kv_num_heads;
s.given(&inputs[1].shape, move |s, ks| {
s.equals(
&outputs[1].shape,
tvec![ks[0].clone(), kvh.to_dim(), ks[1].clone(), ks[2].clone() / kvh],
)
})?;
s.given(&inputs[2].shape, move |s, vs| {
s.equals(
&outputs[2].shape,
tvec![vs[0].clone(), kvh.to_dim(), vs[1].clone(), vs[2].clone() / kvh],
)
})?;
Ok(())
}
fn wire(
&self,
prefix: &str,
model: &mut TypedModel,
inputs: &[OutletId],
) -> TractResult<TVec<OutletId>> {
let q_fact = model.outlet_fact(inputs[0])?.clone();
let dt = q_fact.datum_type;
ensure!(q_fact.rank() == 3, "GroupQueryAttention: expected 3D query [B, S, hidden]");
let q_hidden = q_fact.shape[2].clone();
let k_hidden = model.outlet_fact(inputs[1])?.shape[2].clone();
let v_hidden = model.outlet_fact(inputs[2])?.shape[2].clone();
let q4 = to_4d(model, &format!("{prefix}.q"), inputs[0], q_hidden.clone(), self.num_heads)?;
let k4 = to_4d(model, &format!("{prefix}.k"), inputs[1], k_hidden, self.kv_num_heads)?;
let v4 = to_4d(model, &format!("{prefix}.v"), inputs[2], v_hidden, self.kv_num_heads)?;
let q_seq = model.outlet_fact(q4)?.shape[2].to_usize().ok();
let kv_seq = model.outlet_fact(k4)?.shape[2].to_usize().ok();
let window = self.window;
let (mask, is_causal) = if let (Some(qs), Some(ks)) = (q_seq, kv_seq) {
let arr = windowed_causal_mask(qs, ks, window);
let mask_tensor: Tensor = arr.into();
let mut m = model.add_const(format!("{prefix}.causal_mask"), mask_tensor)?;
for i in 0..2 {
m = model.wire_node(
format!("{prefix}.mask_unsqueeze_{i}"),
AxisOp::Add(0),
&[m],
)?[0];
}
(Some(m), false)
} else {
ensure!(
window == 0,
"GroupQueryAttention: sliding window (local_window_size) requires static \
sequence lengths to materialise the banded mask"
);
(None, true)
};
let mut sdpa_inputs = tvec![q4, k4, v4];
if let Some(m) = mask {
sdpa_inputs.push(m);
}
let sdpa = Sdpa {
scale: self.scale.map(tensor0),
datum_type: dt,
acc_datum_type: DatumType::F32,
is_causal,
};
let y4 = model.wire_node(format!("{prefix}.sdpa"), sdpa, &sdpa_inputs)?[0];
let y_t = model.wire_node(format!("{prefix}.y_transpose"), AxisOp::Move(1, 2), &[y4])?[0];
let yf = model.outlet_fact(y4)?.clone();
let (heads_dim, head_dim) = (yf.shape[1].clone(), yf.shape[3].clone());
let y = model.wire_node(
format!("{prefix}.y_reshape"),
AxisOp::Reshape(
2,
tvec![heads_dim.clone(), head_dim.clone()],
tvec![heads_dim * head_dim],
),
&[y_t],
)?[0];
Ok(tvec!(y, k4, v4))
}
}
#[cfg(test)]
mod tests {
use super::windowed_causal_mask;
#[test]
fn band_mask_causal_and_window() {
let m = windowed_causal_mask(5, 5, 3);
for i in 0..5 {
for j in 0..5 {
let want_open = j <= i && i - j < 3;
assert_eq!(m[(i, j)] == 0.0, want_open, "window=3 at (i={i}, j={j})");
}
}
let c = windowed_causal_mask(4, 4, 0);
for i in 0..4 {
for j in 0..4 {
assert_eq!(c[(i, j)] == 0.0, j <= i, "causal at (i={i}, j={j})");
}
}
}
}