use crate::registry::{
BackwardKind, FusedOpEntry, FusedOpFamily, FusedOpParams, FusedOps, PatternMatch,
SubgraphPattern, decompose_via_recipe,
};
use crate::{Graph, Node, NodeId, Op};
use fuel_ir::{DType, DynScalar, Shape};
use fuel_kernel_seam_types::{OpAttrs, OpTag, PatternNode};
pub fn entry() -> FusedOpEntry {
FusedOpEntry {
destructive_input: None,
id: FusedOps::FLASH_ATTN,
name: "FlashAttn",
family: FusedOpFamily::Attention,
pattern: SubgraphPattern::Callable(canonical_pattern),
decompose,
backward: BackwardKind::NotDifferentiable,
shape_rule,
dtype_rule,
output_views: None,
}
}
fn shape_rule(input_shapes: &[Shape], _params: &FusedOpParams) -> Shape {
debug_assert!(
input_shapes.len() == 4 || input_shapes.len() == 5,
"FlashAttn takes 4 or 5 inputs (q, k, v, [softmax_lse], [alibi])",
);
input_shapes[0].clone()
}
fn dtype_rule(input_dtypes: &[DType], _params: &FusedOpParams) -> DType {
debug_assert!(
input_dtypes.len() == 4 || input_dtypes.len() == 5,
"FlashAttn takes 4 or 5 inputs",
);
input_dtypes[0]
}
fn r_op(op: OpTag, attrs: OpAttrs, operands: Vec<PatternNode>) -> PatternNode {
PatternNode::Op {
op,
attrs,
operands,
}
}
fn r_bind(i: u8) -> PatternNode {
PatternNode::Bind { index: i }
}
fn r_shape(dims: &[usize]) -> OpAttrs {
OpAttrs {
target_shape: dims.iter().map(|&d| d as i64).collect(),
..OpAttrs::default()
}
}
fn r_scalar(v: f64) -> OpAttrs {
OpAttrs {
scalars: vec![v],
..OpAttrs::default()
}
}
fn r_perm(p: Vec<u8>) -> OpAttrs {
OpAttrs {
perm: p,
..OpAttrs::default()
}
}
fn r_diag(diagonal: i64) -> OpAttrs {
OpAttrs {
axis: Some(diagonal),
..OpAttrs::default()
}
}
fn r_slice(dim: i64, start: u64, len: u64) -> OpAttrs {
OpAttrs {
axis: Some(dim),
slice_start: Some(start),
slice_len: Some(len),
..OpAttrs::default()
}
}
fn cast_attr(dtype: DType) -> OpAttrs {
OpAttrs {
cast_dtype: Some(dtype.as_str().to_string()),
..OpAttrs::default()
}
}
fn fused_softmax_attr() -> OpAttrs {
OpAttrs {
fused_op: Some("SoftmaxLastDim".to_string()),
..OpAttrs::default()
}
}
fn repeat_kv_heads_recipe(
x: PatternNode,
b: usize,
hkv: usize,
hq: usize,
s: usize,
d: usize,
) -> PatternNode {
if hq == hkv {
return x;
}
let g = hq / hkv;
let r5 = r_op(OpTag::Reshape, r_shape(&[b, hkv, 1, s, d]), vec![x]);
let bc = r_op(OpTag::BroadcastTo, r_shape(&[b, hkv, g, s, d]), vec![r5]);
r_op(OpTag::Reshape, r_shape(&[b, hq, s, d]), vec![bc])
}
fn alibi_bias_recipe(
alibi: PatternNode,
b: usize,
hq: usize,
sq: usize,
sk: usize,
q_pos_offset: usize,
dtype: DType,
) -> PatternNode {
let grid = &[sq, sk];
let full = &[b, hq, sq, sk];
let row = r_op(
OpTag::Reshape,
r_shape(&[sq, 1]),
vec![r_op(OpTag::Iota, r_shape(&[sq]), vec![])],
);
let col = r_op(
OpTag::Reshape,
r_shape(&[1, sk]),
vec![r_op(OpTag::Iota, r_shape(&[sk]), vec![])],
);
let row_bc = r_op(OpTag::BroadcastTo, r_shape(grid), vec![row]);
let col_bc = r_op(OpTag::BroadcastTo, r_shape(grid), vec![col]);
let mut rel = r_op(OpTag::Sub, OpAttrs::default(), vec![col_bc, row_bc]); if q_pos_offset != 0 {
rel = r_op(
OpTag::AddScalar,
r_scalar(-(q_pos_offset as f64)),
vec![rel],
);
}
let rel_re = r_op(OpTag::Reshape, r_shape(&[1, 1, sq, sk]), vec![rel]);
let rel_4d = r_op(OpTag::BroadcastTo, r_shape(full), vec![rel_re]);
let slope_re = r_op(OpTag::Reshape, r_shape(&[1, hq, 1, 1]), vec![alibi]);
let slope_4d = r_op(OpTag::BroadcastTo, r_shape(full), vec![slope_re]);
let bias_f32 = r_op(OpTag::Mul, OpAttrs::default(), vec![slope_4d, rel_4d]);
if dtype == DType::F32 {
bias_f32
} else {
r_op(OpTag::Cast, cast_attr(dtype), vec![bias_f32])
}
}
pub(crate) struct AttnRecomputeRecipe {
pub probs: PatternNode,
pub k_rep: PatternNode,
pub v_rep: PatternNode,
pub softcap_tanh: Option<PatternNode>,
}
pub(crate) fn recompute_probs_recipe(
q: PatternNode,
k_use: PatternNode,
v_use: PatternNode,
alibi: Option<PatternNode>,
b: usize,
hq: usize,
sq: usize,
sk: usize,
d: usize,
hkv: usize,
softmax_scale: f32,
causal: bool,
window_l: Option<usize>,
window_r: Option<usize>,
softcap: Option<f32>,
q_pos_offset: usize,
dtype: DType,
) -> AttnRecomputeRecipe {
let k_rep = repeat_kv_heads_recipe(k_use, b, hkv, hq, sk, d);
let v_rep = repeat_kv_heads_recipe(v_use, b, hkv, hq, sk, d);
let kt = r_op(
OpTag::Permute,
r_perm(vec![0, 1, 3, 2]),
vec![k_rep.clone()],
);
let scores = r_op(OpTag::MatMul, OpAttrs::default(), vec![q, kt]);
let mut scaled = r_op(
OpTag::MulScalar,
r_scalar(softmax_scale as f64),
vec![scores],
);
let mut softcap_tanh = None;
if let Some(cap) = softcap {
let pre = r_op(OpTag::MulScalar, r_scalar(1.0 / cap as f64), vec![scaled]);
let t = r_op(OpTag::Tanh, OpAttrs::default(), vec![pre]);
softcap_tanh = Some(t.clone());
scaled = r_op(OpTag::MulScalar, r_scalar(cap as f64), vec![t]);
}
if let Some(al) = alibi {
let bias = alibi_bias_recipe(al, b, hq, sq, sk, q_pos_offset, dtype);
scaled = r_op(OpTag::Add, OpAttrs::default(), vec![scaled, bias]);
}
let needs_mask = causal || window_r.is_some() || window_l.is_some();
if needs_mask {
let s0 = scaled;
let zeros = r_op(OpTag::MulScalar, r_scalar(0.0), vec![s0.clone()]);
let neg_inf = r_op(OpTag::AddScalar, r_scalar(f64::NEG_INFINITY), vec![zeros]);
let o = q_pos_offset as i64;
let add_band = |running: PatternNode, band_op: OpTag, diagonal: i64| -> PatternNode {
let band = r_op(band_op, r_diag(diagonal), vec![neg_inf.clone()]);
r_op(OpTag::Add, OpAttrs::default(), vec![running, band])
};
let mut running = s0;
if causal {
running = add_band(running, OpTag::Triu, o + 1);
}
if let Some(r) = window_r {
running = add_band(running, OpTag::Triu, o + r as i64 + 1);
}
if let Some(l) = window_l {
running = add_band(running, OpTag::Tril, o - l as i64 - 1);
}
scaled = running;
}
let probs = r_op(OpTag::Fused, fused_softmax_attr(), vec![scaled]);
AttnRecomputeRecipe {
probs,
k_rep,
v_rep,
softcap_tanh,
}
}
fn recipe(
b: usize,
hq: usize,
sq: usize,
d: usize,
hkv: usize,
sk_full: usize,
concrete_klen: Option<usize>,
softmax_scale: f32,
causal: bool,
window_l: Option<usize>,
window_r: Option<usize>,
softcap: Option<f32>,
has_alibi: bool,
dtype: DType,
) -> PatternNode {
let (k_use, v_use, sk, q_pos_offset) = match concrete_klen {
None => (r_bind(1), r_bind(2), sk_full, 0usize),
Some(kl) => {
let k_sl = r_op(OpTag::Slice, r_slice(2, 0, kl as u64), vec![r_bind(1)]);
let v_sl = r_op(OpTag::Slice, r_slice(2, 0, kl as u64), vec![r_bind(2)]);
(k_sl, v_sl, kl, kl.saturating_sub(sq))
}
};
let alibi = if has_alibi { Some(r_bind(3)) } else { None };
let rc = recompute_probs_recipe(
r_bind(0),
k_use,
v_use,
alibi,
b,
hq,
sq,
sk,
d,
hkv,
softmax_scale,
causal,
window_l,
window_r,
softcap,
q_pos_offset,
dtype,
);
r_op(OpTag::MatMul, OpAttrs::default(), vec![rc.probs, rc.v_rep])
}
pub fn decompose(graph: &mut Graph, id: NodeId, params: &FusedOpParams) -> NodeId {
let (softmax_scale, causal, window_l, window_r, softcap, k_len) = match params {
FusedOpParams::FlashAttn {
softmax_scale,
causal,
window_size_left,
window_size_right,
softcap,
k_len,
} => (
*softmax_scale,
*causal,
*window_size_left,
*window_size_right,
*softcap,
*k_len,
),
_ => return id,
};
let concrete_klen: Option<usize> = match k_len {
None => None,
Some(DynScalar::Concrete(kl)) => Some(kl),
Some(DynScalar::Sym(_)) => return id,
};
let (q_shape, k_shape, n_inputs, dtype) = {
let n = graph.node(id);
if n.inputs.len() != 3 && n.inputs.len() != 4 {
return id;
}
(
graph.node(n.inputs[0]).shape.clone(), graph.node(n.inputs[1]).shape.clone(), n.inputs.len(),
n.dtype,
)
};
let q_dims = q_shape.dims();
let k_dims = k_shape.dims();
if q_dims.len() != 4 || k_dims.len() != 4 {
return id;
}
let (b, hq, sq, d) = (q_dims[0], q_dims[1], q_dims[2], q_dims[3]);
let hkv = k_dims[1];
let sk_full = k_dims[2];
let has_alibi = n_inputs == 4;
let recipe_node = recipe(
b,
hq,
sq,
d,
hkv,
sk_full,
concrete_klen,
softmax_scale,
causal,
window_l,
window_r,
softcap,
has_alibi,
dtype,
);
decompose_via_recipe(graph, id, &recipe_node, Some(Vec::new()))
}
#[allow(dead_code)]
pub(crate) struct AttnRecompute {
pub probs: NodeId,
pub k_rep: NodeId,
pub v_rep: NodeId,
pub softcap_tanh: Option<NodeId>,
pub scores_shape: Shape,
}
pub(crate) fn recompute_probs(
graph: &mut Graph,
q_id: NodeId,
k_id: NodeId,
v_id: NodeId,
alibi_id: Option<NodeId>,
b: usize,
hq: usize,
sq: usize,
sk: usize,
d: usize,
hkv: usize,
softmax_scale: f32,
causal: bool,
window_l: Option<usize>,
window_r: Option<usize>,
softcap: Option<f32>,
q_pos_offset: usize,
dtype: DType,
) -> AttnRecompute {
let scores_shape = Shape::from_dims(&[b, hq, sq, sk]);
let k_rep = repeat_kv_heads(graph, k_id, b, hkv, hq, sk, d, dtype);
let v_rep = repeat_kv_heads(graph, v_id, b, hkv, hq, sk, d, dtype);
let kt_id = graph.push(Node {
op: Op::Permute(vec![0, 1, 3, 2]),
inputs: vec![k_rep],
shape: Shape::from_dims(&[b, hq, d, sk]),
dtype,
});
let scores_id = graph.push(Node {
op: Op::MatMul,
inputs: vec![q_id, kt_id],
shape: scores_shape.clone(),
dtype,
});
let mut scaled = graph.push(Node {
op: Op::MulScalar(softmax_scale as f64),
inputs: vec![scores_id],
shape: scores_shape.clone(),
dtype,
});
let mut softcap_tanh = None;
if let Some(cap) = softcap {
let pre = graph.push(Node {
op: Op::MulScalar(1.0 / cap as f64),
inputs: vec![scaled],
shape: scores_shape.clone(),
dtype,
});
let t = graph.push(Node {
op: Op::Tanh,
inputs: vec![pre],
shape: scores_shape.clone(),
dtype,
});
softcap_tanh = Some(t);
scaled = graph.push(Node {
op: Op::MulScalar(cap as f64),
inputs: vec![t],
shape: scores_shape.clone(),
dtype,
});
}
if let Some(alibi) = alibi_id {
let bias = alibi_bias(graph, alibi, b, hq, sq, sk, q_pos_offset, dtype);
scaled = graph.push(Node {
op: Op::Add,
inputs: vec![scaled, bias],
shape: scores_shape.clone(),
dtype,
});
}
let needs_mask = causal || window_r.is_some() || window_l.is_some();
if needs_mask {
let zeros = graph.push(Node {
op: Op::MulScalar(0.0),
inputs: vec![scaled],
shape: scores_shape.clone(),
dtype,
});
let neg_inf = graph.push(Node {
op: Op::AddScalar(f64::NEG_INFINITY),
inputs: vec![zeros],
shape: scores_shape.clone(),
dtype,
});
let add_band = |graph: &mut Graph, scaled: NodeId, op: Op| -> NodeId {
let band = graph.push(Node {
op,
inputs: vec![neg_inf],
shape: scores_shape.clone(),
dtype,
});
graph.push(Node {
op: Op::Add,
inputs: vec![scaled, band],
shape: scores_shape.clone(),
dtype,
})
};
let o = q_pos_offset as i64;
if causal {
scaled = add_band(graph, scaled, Op::Triu { diagonal: o + 1 });
}
if let Some(r) = window_r {
scaled = add_band(
graph,
scaled,
Op::Triu {
diagonal: o + r as i64 + 1,
},
);
}
if let Some(l) = window_l {
scaled = add_band(
graph,
scaled,
Op::Tril {
diagonal: o - l as i64 - 1,
},
);
}
}
let probs = graph.push(Node {
op: Op::Fused(FusedOps::SOFTMAX_LAST_DIM, FusedOpParams::SoftmaxLastDim),
inputs: vec![scaled],
shape: scores_shape.clone(),
dtype,
});
AttnRecompute {
probs,
k_rep,
v_rep,
softcap_tanh,
scores_shape,
}
}
pub(crate) fn repeat_kv_heads(
graph: &mut Graph,
x_id: NodeId,
b: usize,
hkv: usize,
hq: usize,
s: usize,
d: usize,
dtype: DType,
) -> NodeId {
if hq == hkv {
return x_id;
}
let g = hq / hkv;
let r5 = graph.push(Node {
op: Op::Reshape(Shape::from_dims(&[b, hkv, 1, s, d])),
inputs: vec![x_id],
shape: Shape::from_dims(&[b, hkv, 1, s, d]),
dtype,
});
let bc = graph.push(Node {
op: Op::BroadcastTo(Shape::from_dims(&[b, hkv, g, s, d])),
inputs: vec![r5],
shape: Shape::from_dims(&[b, hkv, g, s, d]),
dtype,
});
graph.push(Node {
op: Op::Reshape(Shape::from_dims(&[b, hq, s, d])),
inputs: vec![bc],
shape: Shape::from_dims(&[b, hq, s, d]),
dtype,
})
}
pub(crate) fn alibi_bias(
graph: &mut Graph,
alibi_id: NodeId,
b: usize,
hq: usize,
sq: usize,
sk: usize,
q_pos_offset: usize,
dtype: DType,
) -> NodeId {
let f32 = DType::F32;
let grid = Shape::from_dims(&[sq, sk]);
let row_iota = graph.push(Node {
op: Op::Iota { len: sq },
inputs: vec![],
shape: Shape::from_dims(&[sq]),
dtype: f32,
});
let row = graph.push(Node {
op: Op::Reshape(Shape::from_dims(&[sq, 1])),
inputs: vec![row_iota],
shape: Shape::from_dims(&[sq, 1]),
dtype: f32,
});
let col_iota = graph.push(Node {
op: Op::Iota { len: sk },
inputs: vec![],
shape: Shape::from_dims(&[sk]),
dtype: f32,
});
let col = graph.push(Node {
op: Op::Reshape(Shape::from_dims(&[1, sk])),
inputs: vec![col_iota],
shape: Shape::from_dims(&[1, sk]),
dtype: f32,
});
let row_bc = graph.push(Node {
op: Op::BroadcastTo(grid.clone()),
inputs: vec![row],
shape: grid.clone(),
dtype: f32,
});
let col_bc = graph.push(Node {
op: Op::BroadcastTo(grid.clone()),
inputs: vec![col],
shape: grid.clone(),
dtype: f32,
});
let mut rel = graph.push(Node {
op: Op::Sub,
inputs: vec![col_bc, row_bc],
shape: grid.clone(),
dtype: f32, });
if q_pos_offset != 0 {
rel = graph.push(Node {
op: Op::AddScalar(-(q_pos_offset as f64)),
inputs: vec![rel],
shape: grid,
dtype: f32,
});
}
let full = Shape::from_dims(&[b, hq, sq, sk]);
let rel_re = graph.push(Node {
op: Op::Reshape(Shape::from_dims(&[1, 1, sq, sk])),
inputs: vec![rel],
shape: Shape::from_dims(&[1, 1, sq, sk]),
dtype: f32,
});
let rel_4d = graph.push(Node {
op: Op::BroadcastTo(full.clone()),
inputs: vec![rel_re],
shape: full.clone(),
dtype: f32,
});
let slope_re = graph.push(Node {
op: Op::Reshape(Shape::from_dims(&[1, hq, 1, 1])),
inputs: vec![alibi_id],
shape: Shape::from_dims(&[1, hq, 1, 1]),
dtype: f32,
});
let slope_4d = graph.push(Node {
op: Op::BroadcastTo(full.clone()),
inputs: vec![slope_re],
shape: full.clone(),
dtype: f32,
});
let bias_f32 = graph.push(Node {
op: Op::Mul,
inputs: vec![slope_4d, rel_4d],
shape: full.clone(),
dtype: f32,
});
if dtype == f32 {
bias_f32
} else {
graph.push(Node {
op: Op::Cast(dtype),
inputs: vec![bias_f32],
shape: full,
dtype,
})
}
}
pub fn canonical_pattern(_graph: &Graph, _root: NodeId) -> Option<PatternMatch> {
None
}
#[cfg(test)]
mod tests {
use super::*;
fn frozen_legacy_decompose(graph: &mut Graph, id: NodeId, params: &FusedOpParams) -> NodeId {
let (q_id, k_id, v_id, alibi_id, q_shape, k_shape, dtype) = {
let n = graph.node(id);
let q_shape = graph.node(n.inputs[0]).shape.clone();
let k_shape = graph.node(n.inputs[1]).shape.clone();
let alibi = if n.inputs.len() == 4 {
Some(n.inputs[3])
} else {
None
};
(
n.inputs[0],
n.inputs[1],
n.inputs[2],
alibi,
q_shape,
k_shape,
n.dtype,
)
};
let (softmax_scale, causal, window_l, window_r, softcap, k_len) = match params {
FusedOpParams::FlashAttn {
softmax_scale,
causal,
window_size_left,
window_size_right,
softcap,
k_len,
} => (
*softmax_scale,
*causal,
*window_size_left,
*window_size_right,
*softcap,
*k_len,
),
_ => return id,
};
let concrete_klen: Option<usize> = match k_len {
None => None,
Some(DynScalar::Concrete(kl)) => Some(kl),
Some(DynScalar::Sym(_)) => return id,
};
let q_dims = q_shape.dims();
let k_dims = k_shape.dims();
let (b, hq, sq, d) = (q_dims[0], q_dims[1], q_dims[2], q_dims[3]);
let hkv = k_dims[1];
let (k_use, v_use, sk, q_pos_offset) = match concrete_klen {
None => (k_id, v_id, k_dims[2], 0usize),
Some(kl) => {
let sliced = Shape::from_dims(&[b, hkv, kl, d]);
let k_sl = graph.push(Node {
op: Op::Slice {
dim: 2,
start: 0,
len: kl,
},
inputs: vec![k_id],
shape: sliced.clone(),
dtype,
});
let v_sl = graph.push(Node {
op: Op::Slice {
dim: 2,
start: 0,
len: kl,
},
inputs: vec![v_id],
shape: sliced,
dtype,
});
(k_sl, v_sl, kl, kl.saturating_sub(sq))
}
};
let rc = recompute_probs(
graph,
q_id,
k_use,
v_use,
alibi_id,
b,
hq,
sq,
sk,
d,
hkv,
softmax_scale,
causal,
window_l,
window_r,
softcap,
q_pos_offset,
dtype,
);
graph.push(Node {
op: Op::MatMul,
inputs: vec![rc.probs, rc.v_rep],
shape: q_shape,
dtype,
})
}
fn assert_structural_eq(g: &Graph, a: NodeId, b: NodeId) {
if a == b {
return;
}
let na = g.node(a);
let nb = g.node(b);
assert_eq!(na.op, nb.op, "op mismatch: {:?} vs {:?}", na.op, nb.op);
assert_eq!(
na.shape, nb.shape,
"shape mismatch at {:?}: {:?} vs {:?}",
na.op, na.shape, nb.shape
);
assert_eq!(na.dtype, nb.dtype, "dtype mismatch at {:?}", na.op);
assert_eq!(
na.inputs.len(),
nb.inputs.len(),
"arity mismatch at {:?}",
na.op
);
for (&ia, &ib) in na.inputs.iter().zip(nb.inputs.iter()) {
assert_structural_eq(g, ia, ib);
}
}
#[derive(Clone, Copy)]
struct Cfg {
hq: usize,
hkv: usize,
causal: bool,
window_l: Option<usize>,
window_r: Option<usize>,
softcap: Option<f32>,
alibi: bool,
klen: Option<usize>,
}
fn flash_node(g: &mut Graph, b: usize, sq: usize, sk_cap: usize, d: usize, cfg: Cfg) -> NodeId {
let q_shape = Shape::from_dims(&[b, cfg.hq, sq, d]);
let q = g.push(Node {
op: Op::Const,
inputs: vec![],
shape: q_shape.clone(),
dtype: DType::F32,
});
let kv_shape = Shape::from_dims(&[b, cfg.hkv, sk_cap, d]);
let k = g.push(Node {
op: Op::Const,
inputs: vec![],
shape: kv_shape.clone(),
dtype: DType::F32,
});
let v = g.push(Node {
op: Op::Const,
inputs: vec![],
shape: kv_shape,
dtype: DType::F32,
});
let mut inputs = vec![q, k, v];
if cfg.alibi {
let al = g.push(Node {
op: Op::Const,
inputs: vec![],
shape: Shape::from_dims(&[cfg.hq]),
dtype: DType::F32,
});
inputs.push(al);
}
g.push(Node {
op: Op::Fused(
FusedOps::FLASH_ATTN,
FusedOpParams::FlashAttn {
softmax_scale: 0.5,
causal: cfg.causal,
window_size_left: cfg.window_l,
window_size_right: cfg.window_r,
softcap: cfg.softcap,
k_len: cfg.klen.map(DynScalar::Concrete),
},
),
inputs,
shape: q_shape,
dtype: DType::F32,
})
}
#[test]
fn flash_attn_recipe_decompose_is_polymorphic_and_matches_frozen_legacy() {
let (b, sq, sk_cap, d) = (1usize, 2usize, 4usize, 2usize);
let masks: [(bool, Option<usize>, Option<usize>); 6] = [
(false, None, None), (true, None, None), (false, Some(2), Some(1)), (false, Some(2), None), (false, None, Some(1)), (true, Some(2), Some(1)), ];
let mut fired = 0usize;
for (causal, window_l, window_r) in masks {
for softcap in [None, Some(30.0f32)] {
for alibi in [false, true] {
for (hq, hkv) in [(2usize, 2usize), (4usize, 2usize)] {
for klen in [None, Some(3usize)] {
let cfg = Cfg {
hq,
hkv,
causal,
window_l,
window_r,
softcap,
alibi,
klen,
};
let mut g = Graph::new();
let fused = flash_node(&mut g, b, sq, sk_cap, d, cfg);
let out_shape = g.node(fused).shape.clone();
let params = match &g.node(fused).op {
Op::Fused(_, p) => p.clone(),
_ => unreachable!(),
};
let new_root = decompose(&mut g, fused, ¶ms);
assert_ne!(
new_root, fused,
"recipe decompose fires (causal={causal}, wl={window_l:?}, \
wr={window_r:?}, softcap={softcap:?}, alibi={alibi}, \
hq={hq}, hkv={hkv}, klen={klen:?})"
);
assert_eq!(g.node(new_root).shape, out_shape, "output = q shape");
assert_eq!(g.node(new_root).dtype, DType::F32);
let legacy_root = frozen_legacy_decompose(&mut g, fused, ¶ms);
assert_structural_eq(&g, new_root, legacy_root);
fired += 1;
}
}
}
}
}
assert_eq!(
fired,
6 * 2 * 2 * 2 * 2,
"every config in the matrix was checked"
);
}
#[test]
fn flash_attn_symbolic_klen_stays_a_fixpoint_gap() {
let mut g = Graph::new();
let cfg = Cfg {
hq: 2,
hkv: 2,
causal: true,
window_l: None,
window_r: None,
softcap: None,
alibi: false,
klen: None,
};
let fused = flash_node(&mut g, 1, 2, 4, 2, cfg);
let sym_params = FusedOpParams::FlashAttn {
softmax_scale: 0.5,
causal: true,
window_size_left: None,
window_size_right: None,
softcap: None,
k_len: Some(DynScalar::Sym(fuel_ir::SymId(0))),
};
let before = g.len();
let out = decompose(&mut g, fused, &sym_params);
assert_eq!(
out, fused,
"symbolic k_len => registry-layer basis gap => fixpoint self-return"
);
assert_eq!(
g.len(),
before,
"declined before any emission (no recipe built)"
);
}
#[test]
fn flash_attn_recipe_wrong_params_is_a_fixpoint_not_a_crash() {
let mut g = Graph::new();
let cfg = Cfg {
hq: 2,
hkv: 2,
causal: false,
window_l: None,
window_r: None,
softcap: None,
alibi: false,
klen: None,
};
let fused = flash_node(&mut g, 1, 2, 4, 2, cfg);
let before = g.len();
let out = decompose(&mut g, fused, &FusedOpParams::Rope);
assert_eq!(out, fused, "wrong params => typed decline => fixpoint");
assert_eq!(g.len(), before, "declined before any emission");
}
}