use crate::model::ParsingContext;
use crate::pb::NodeProto;
use tract_core::ops::array::TypedConcat;
use tract_core::ops::change_axes::AxisOp;
use tract_core::ops::math::add;
use tract_hir::internal::*;
use tract_hir::ops::logic::wire_with_rank_broadcast;
use tract_transformers::ops::sdpa::Sdpa;
pub fn attention(
_ctx: &ParsingContext,
node: &NodeProto,
) -> TractResult<(Box<dyn InferenceOp>, Vec<String>)> {
let softcap = node.get_attr_opt::<f32>("softcap")?.unwrap_or(0.0);
if softcap != 0.0 {
bail!("Attention: softcap is not supported");
}
let qk_matmul_output_mode = node.get_attr_opt::<i64>("qk_matmul_output_mode")?.unwrap_or(0);
if qk_matmul_output_mode != 0 {
bail!("Attention: qk_matmul_output_mode is not supported");
}
let q_num_heads = node.get_attr_opt::<i64>("q_num_heads")?.map(|v| v as usize);
let kv_num_heads = node.get_attr_opt::<i64>("kv_num_heads")?.map(|v| v as usize);
let is_causal = node.get_attr_opt::<i64>("is_causal")?.unwrap_or(0) != 0;
let scale = node.get_attr_opt::<f32>("scale")?;
let have_nonpad_kv_seqlen = node.input.len() > 6 && !node.input[6].is_empty();
if have_nonpad_kv_seqlen {
bail!("Attention: nonpad_kv_seqlen input is not supported");
}
let have_mask = node.input.len() > 3 && !node.input[3].is_empty();
let have_past_key = node.input.len() > 4 && !node.input[4].is_empty();
let have_past_value = node.input.len() > 5 && !node.input[5].is_empty();
let have_present_key = node.output.len() > 1 && !node.output[1].is_empty();
let have_present_value = node.output.len() > 2 && !node.output[2].is_empty();
Ok((
expand(AttentionOp {
q_num_heads,
kv_num_heads,
is_causal,
scale,
have_mask,
have_past_key,
have_past_value,
have_present_key,
have_present_value,
}),
vec![],
))
}
#[derive(Debug, Clone)]
struct AttentionOp {
q_num_heads: Option<usize>,
kv_num_heads: Option<usize>,
is_causal: bool,
scale: Option<f32>,
have_mask: bool,
have_past_key: bool,
have_past_value: bool,
have_present_key: bool,
have_present_value: bool,
}
impl AttentionOp {
fn mask_input_idx(&self) -> Option<usize> {
self.have_mask.then_some(3)
}
fn past_key_input_idx(&self) -> Option<usize> {
self.have_past_key.then_some(3 + self.have_mask as usize)
}
fn past_value_input_idx(&self) -> Option<usize> {
self.have_past_value.then_some(3 + self.have_mask as usize + self.have_past_key as usize)
}
}
fn wire_3d_to_4d(
prefix: &str,
model: &mut TypedModel,
x: OutletId,
total_dim: TDim,
num_heads: usize,
) -> TractResult<OutletId> {
let head_dim = total_dim.clone() / num_heads;
let after_reshape = model.wire_node(
format!("{prefix}.reshape"),
AxisOp::Reshape(2, tvec![total_dim], tvec![num_heads.to_dim(), head_dim]),
&[x],
)?[0];
model
.wire_node(format!("{prefix}.transpose"), AxisOp::Move(2, 1), &[after_reshape])
.map(|v| v[0])
}
impl Expansion for AttentionOp {
fn name(&self) -> StaticName {
"OnnxAttention".into()
}
fn nboutputs(&self) -> TractResult<usize> {
Ok(1 + self.have_present_key as usize + self.have_present_value as usize)
}
fn rules<'r, 'p: 'r, 's: 'r>(
&'s self,
s: &mut Solver<'r>,
inputs: &'p [TensorProxy],
outputs: &'p [TensorProxy],
) -> InferenceResult {
let n_in = 3
+ self.have_mask as usize
+ self.have_past_key as usize
+ self.have_past_value as usize;
let n_out = 1 + self.have_present_key as usize + self.have_present_value as usize;
check_input_arity(inputs, n_in)?;
check_output_arity(outputs, n_out)?;
s.equals(&inputs[0].datum_type, &outputs[0].datum_type)?;
s.equals(&inputs[0].rank, &outputs[0].rank)?;
if self.have_present_key {
s.equals(&inputs[0].datum_type, &outputs[1].datum_type)?;
}
if self.have_present_value {
let pv_idx = 1 + self.have_present_key as usize;
s.equals(&inputs[0].datum_type, &outputs[pv_idx].datum_type)?;
}
Ok(())
}
fn wire(
&self,
prefix: &str,
model: &mut TypedModel,
inputs: &[OutletId],
) -> TractResult<TVec<OutletId>> {
let q_fact = model.outlet_fact(inputs[0])?.clone();
let is_4d = q_fact.rank() == 4;
let dt = q_fact.datum_type;
let acc_dt = DatumType::F32;
let (q4, k4, v4, q_hdim_3d) = if is_4d {
(inputs[0], inputs[1], inputs[2], None)
} else {
let q_hdim = q_fact.shape[2].clone();
let k_hdim = model.outlet_fact(inputs[1])?.shape[2].clone();
let v_hdim = model.outlet_fact(inputs[2])?.shape[2].clone();
let q_num_heads = self.q_num_heads.context("q_num_heads required for 3D Attention")?;
let kv_num_heads =
self.kv_num_heads.context("kv_num_heads required for 3D Attention")?;
let q4 = wire_3d_to_4d(
&format!("{prefix}.q"),
model,
inputs[0],
q_hdim.clone(),
q_num_heads,
)?;
let k4 = wire_3d_to_4d(&format!("{prefix}.k"), model, inputs[1], k_hdim, kv_num_heads)?;
let v4 = wire_3d_to_4d(&format!("{prefix}.v"), model, inputs[2], v_hdim, kv_num_heads)?;
(q4, k4, v4, Some(q_hdim))
};
let (k_for_attn, v_for_attn, present_k, present_v) =
if self.have_past_key || self.have_present_key {
let k_full = if self.have_past_key {
let past_k = inputs[self.past_key_input_idx().unwrap()];
model.wire_node(
format!("{prefix}.concat_k"),
TypedConcat { axis: 2 },
&[past_k, k4],
)?[0]
} else {
k4
};
let v_full = if self.have_past_value {
let past_v = inputs[self.past_value_input_idx().unwrap()];
model.wire_node(
format!("{prefix}.concat_v"),
TypedConcat { axis: 2 },
&[past_v, v4],
)?[0]
} else {
v4
};
let pk = self.have_present_key.then_some(k_full);
let pv = self.have_present_value.then_some(v_full);
(k_full, v_full, pk, pv)
} else {
(k4, v4, None, None)
};
let explicit_mask = if self.have_mask {
let m = inputs[self.mask_input_idx().unwrap()];
let m_rank = model.outlet_fact(m)?.rank();
let mut m = m;
for i in m_rank..4 {
m = model.wire_node(format!("{prefix}.mask_add_axis_{i}"), AxisOp::Add(0), &[m])?
[0];
}
Some(m)
} else {
None
};
let (causal_mask, sdpa_is_causal) = if self.is_causal {
let q_seq = model.outlet_fact(q4)?.shape[2].to_usize().ok();
let kv_seq = model.outlet_fact(k_for_attn)?.shape[2].to_usize().ok();
if let (Some(qs), Some(ks)) = (q_seq, kv_seq) {
let arr = tract_ndarray::Array2::<f32>::from_shape_fn((qs, ks), |(i, j)| {
if j <= i { 0.0f32 } else { f32::NEG_INFINITY }
});
let mask_tensor: Tensor = arr.into();
let c = model.add_const(format!("{prefix}.causal_mask"), mask_tensor)?;
let mut m = c;
for i in 0..2 {
m = model.wire_node(
format!("{prefix}.causal_mask_unsqueeze_{i}"),
AxisOp::Add(0),
&[m],
)?[0];
}
(Some(m), false)
} else {
(None, true)
}
} else {
(None, false)
};
let mask = match (explicit_mask, causal_mask) {
(Some(em), Some(cm)) => Some(
wire_with_rank_broadcast(
format!("{prefix}.mask_combined"),
model,
add(),
&[em, cm],
)?[0],
),
(m, None) | (None, m) => m,
};
let mut sdpa_inputs = tvec![q4, k_for_attn, v_for_attn];
if let Some(m) = mask {
sdpa_inputs.push(m);
}
let sdpa = Sdpa {
scale: self.scale.map(tensor0),
datum_type: dt,
acc_datum_type: acc_dt,
is_causal: sdpa_is_causal,
};
let y4 = model.wire_node(format!("{prefix}.sdpa"), sdpa, &sdpa_inputs)?[0];
let y = if q_hdim_3d.is_some() {
let y_transposed =
model.wire_node(format!("{prefix}.y_transpose"), AxisOp::Move(1, 2), &[y4])?[0];
let y4_fact = model.outlet_fact(y4)?.clone();
let q_heads_dim = y4_fact.shape[1].clone();
let v_head_dim = y4_fact.shape[3].clone();
let y_hdim = q_heads_dim.clone() * v_head_dim.clone();
model.wire_node(
format!("{prefix}.y_reshape"),
AxisOp::Reshape(2, tvec![q_heads_dim, v_head_dim], tvec![y_hdim]),
&[y_transposed],
)?[0]
} else {
y4
};
let mut result = tvec![y];
if let Some(pk) = present_k {
result.push(pk);
}
if let Some(pv) = present_v {
result.push(pv);
}
Ok(result)
}
}