use crate::registry::{
BackwardKind, FusedOpEntry, FusedOpFamily, FusedOpId, FusedOpParams, FusedOps, PatternMatch,
SubgraphPattern, decompose_via_recipe,
};
use crate::{Graph, Node, NodeId, Op};
use fuel_ir::{DType, Shape};
use fuel_kernel_seam_types::{OpAttrs, OpTag, PatternNode};
pub fn entry_q() -> FusedOpEntry {
FusedOpEntry {
destructive_input: None,
id: FusedOps::FLASH_ATTN_BACKWARD_Q,
name: "FlashAttnBackwardQ",
family: FusedOpFamily::Attention,
pattern: SubgraphPattern::Callable(no_pattern),
decompose,
backward: BackwardKind::NotDifferentiable,
shape_rule: shape_rule_q,
dtype_rule,
output_views: None,
}
}
pub fn entry_k() -> FusedOpEntry {
FusedOpEntry {
destructive_input: None,
id: FusedOps::FLASH_ATTN_BACKWARD_K,
name: "FlashAttnBackwardK",
family: FusedOpFamily::Attention,
pattern: SubgraphPattern::Callable(no_pattern),
decompose,
backward: BackwardKind::NotDifferentiable,
shape_rule: shape_rule_k,
dtype_rule,
output_views: None,
}
}
pub fn entry_v() -> FusedOpEntry {
FusedOpEntry {
destructive_input: None,
id: FusedOps::FLASH_ATTN_BACKWARD_V,
name: "FlashAttnBackwardV",
family: FusedOpFamily::Attention,
pattern: SubgraphPattern::Callable(no_pattern),
decompose,
backward: BackwardKind::NotDifferentiable,
shape_rule: shape_rule_v,
dtype_rule,
output_views: None,
}
}
fn shape_rule_q(input_shapes: &[Shape], _params: &FusedOpParams) -> Shape {
debug_assert!(
input_shapes.len() == 4 || input_shapes.len() == 5,
"FlashAttnBackwardQ takes 4-5 inputs (q, k, v, do, [alibi])",
);
input_shapes[0].clone()
}
fn shape_rule_k(input_shapes: &[Shape], _params: &FusedOpParams) -> Shape {
debug_assert!(
input_shapes.len() == 4 || input_shapes.len() == 5,
"FlashAttnBackwardK takes 4-5 inputs (q, k, v, do, [alibi])",
);
input_shapes[1].clone()
}
fn shape_rule_v(input_shapes: &[Shape], _params: &FusedOpParams) -> Shape {
debug_assert!(
input_shapes.len() == 4 || input_shapes.len() == 5,
"FlashAttnBackwardV takes 4-5 inputs (q, k, v, do, [alibi])",
);
input_shapes[2].clone()
}
fn dtype_rule(input_dtypes: &[DType], _params: &FusedOpParams) -> DType {
debug_assert!(
input_dtypes.len() == 4 || input_dtypes.len() == 5,
"FlashAttnBackward* takes 4-5 inputs",
);
input_dtypes[0]
}
fn no_pattern(_g: &Graph, _root: NodeId) -> Option<PatternMatch> {
None
}
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_axis(a: i64) -> OpAttrs {
OpAttrs {
axis: Some(a),
..OpAttrs::default()
}
}
fn fused_softmax_backward_attr() -> OpAttrs {
OpAttrs {
fused_op: Some("SoftmaxLastDimBackward".to_string()),
..OpAttrs::default()
}
}
fn fold_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, g, s, d]), vec![x]);
r_op(OpTag::SumDim, r_axis(2), vec![r5])
}
fn recipe_qk(
variant: FusedOpId,
b: usize,
hq: usize,
sq: usize,
d: usize,
hkv: usize,
sk: usize,
scale: f32,
causal: bool,
window_l: Option<usize>,
window_r: Option<usize>,
softcap: Option<f32>,
has_alibi: bool,
dtype: DType,
) -> PatternNode {
let alibi = if has_alibi { Some(r_bind(4)) } else { None };
let rc = super::flash_attn::recompute_probs_recipe(
r_bind(0),
r_bind(1),
r_bind(2),
alibi,
b,
hq,
sq,
sk,
d,
hkv,
scale,
causal,
window_l,
window_r,
softcap,
0,
dtype,
);
let vt = r_op(OpTag::Permute, r_perm(vec![0, 1, 3, 2]), vec![rc.v_rep]);
let dp = r_op(OpTag::MatMul, OpAttrs::default(), vec![r_bind(3), vt]);
let mut dscaled = r_op(
OpTag::Fused,
fused_softmax_backward_attr(),
vec![rc.probs, dp],
);
if let Some(t) = rc.softcap_tanh {
let t2 = r_op(OpTag::Mul, OpAttrs::default(), vec![t.clone(), t]);
let neg = r_op(OpTag::MulScalar, r_scalar(-1.0), vec![t2]);
let omt2 = r_op(OpTag::AddScalar, r_scalar(1.0), vec![neg]);
dscaled = r_op(OpTag::Mul, OpAttrs::default(), vec![dscaled, omt2]);
}
let dscores = r_op(OpTag::MulScalar, r_scalar(scale as f64), vec![dscaled]);
if variant == FusedOps::FLASH_ATTN_BACKWARD_Q {
return r_op(OpTag::MatMul, OpAttrs::default(), vec![dscores, rc.k_rep]);
}
let dst = r_op(OpTag::Permute, r_perm(vec![0, 1, 3, 2]), vec![dscores]);
let dk_rep = r_op(OpTag::MatMul, OpAttrs::default(), vec![dst, r_bind(0)]);
fold_recipe(dk_rep, b, hkv, hq, sk, d)
}
fn decompose(graph: &mut Graph, id: NodeId, params: &FusedOpParams) -> NodeId {
let variant = if let Op::Fused(fid, _) = &graph.node(id).op {
*fid
} else {
return id;
};
let (scale, causal, window_l, window_r, softcap) = match params {
FusedOpParams::FlashAttnBackward {
softmax_scale,
causal,
window_size_left,
window_size_right,
softcap,
} => (
*softmax_scale,
*causal,
*window_size_left,
*window_size_right,
*softcap,
),
_ => return id,
};
let (q_shape, k_shape, n_inputs, dtype) = {
let n = graph.node(id);
if n.inputs.len() != 4 && n.inputs.len() != 5 {
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, sk) = (k_dims[1], k_dims[2]);
let has_alibi = n_inputs == 5;
if variant == FusedOps::FLASH_ATTN_BACKWARD_Q || variant == FusedOps::FLASH_ATTN_BACKWARD_K {
let recipe_node = recipe_qk(
variant, b, hq, sq, d, hkv, sk, scale, causal, window_l, window_r, softcap, has_alibi,
dtype,
);
return decompose_via_recipe(graph, id, &recipe_node, Some(Vec::new()));
}
if variant != FusedOps::FLASH_ATTN_BACKWARD_V {
return id;
}
let g = hq / hkv;
let (q_id, k_id, v_id, do_id, alibi_id) = {
let n = graph.node(id);
let alibi = if n.inputs.len() == 5 {
Some(n.inputs[4])
} else {
None
};
(n.inputs[0], n.inputs[1], n.inputs[2], n.inputs[3], alibi)
};
let rc = super::flash_attn::recompute_probs(
graph, q_id, k_id, v_id, alibi_id, b, hq, sq, sk, d, hkv, scale, causal, window_l,
window_r, softcap, 0, dtype,
);
let fold = |graph: &mut Graph, x: NodeId, s: usize| -> NodeId {
if hq == hkv {
return x;
}
let r5 = graph.push(Node {
op: Op::Reshape(Shape::from_dims(&[b, hkv, g, s, d])),
inputs: vec![x],
shape: Shape::from_dims(&[b, hkv, g, s, d]),
dtype,
});
graph.push(Node {
op: Op::SumDim(2),
inputs: vec![r5],
shape: Shape::from_dims(&[b, hkv, s, d]),
dtype,
})
};
let pt = graph.push(Node {
op: Op::Permute(vec![0, 1, 3, 2]),
inputs: vec![rc.probs],
shape: Shape::from_dims(&[b, hq, sk, sq]),
dtype,
});
let dv_rep = graph.push(Node {
op: Op::MatMul,
inputs: vec![pt, do_id],
shape: Shape::from_dims(&[b, hq, sk, d]),
dtype,
});
fold(graph, dv_rep, sk)
}
#[cfg(test)]
mod tests {
use super::*;
fn frozen_legacy_decompose(graph: &mut Graph, id: NodeId, params: &FusedOpParams) -> NodeId {
let variant = if let Op::Fused(fid, _) = &graph.node(id).op {
*fid
} else {
return id;
};
let (q_id, k_id, v_id, do_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() == 5 {
Some(n.inputs[4])
} else {
None
};
(
n.inputs[0],
n.inputs[1],
n.inputs[2],
n.inputs[3],
alibi,
q_shape,
k_shape,
n.dtype,
)
};
let (scale, causal, window_l, window_r, softcap) = match params {
FusedOpParams::FlashAttnBackward {
softmax_scale,
causal,
window_size_left,
window_size_right,
softcap,
} => (
*softmax_scale,
*causal,
*window_size_left,
*window_size_right,
*softcap,
),
_ => 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, sk) = (k_dims[1], k_dims[2]);
let g = hq / hkv;
let rc = super::super::flash_attn::recompute_probs(
graph, q_id, k_id, v_id, alibi_id, b, hq, sq, sk, d, hkv, scale, causal, window_l,
window_r, softcap, 0, dtype,
);
let scores_shape = rc.scores_shape.clone();
let fold = |graph: &mut Graph, x: NodeId, s: usize| -> NodeId {
if hq == hkv {
return x;
}
let r5 = graph.push(Node {
op: Op::Reshape(Shape::from_dims(&[b, hkv, g, s, d])),
inputs: vec![x],
shape: Shape::from_dims(&[b, hkv, g, s, d]),
dtype,
});
graph.push(Node {
op: Op::SumDim(2),
inputs: vec![r5],
shape: Shape::from_dims(&[b, hkv, s, d]),
dtype,
})
};
if variant == FusedOps::FLASH_ATTN_BACKWARD_V {
let pt = graph.push(Node {
op: Op::Permute(vec![0, 1, 3, 2]),
inputs: vec![rc.probs],
shape: Shape::from_dims(&[b, hq, sk, sq]),
dtype,
});
let dv_rep = graph.push(Node {
op: Op::MatMul,
inputs: vec![pt, do_id],
shape: Shape::from_dims(&[b, hq, sk, d]),
dtype,
});
return fold(graph, dv_rep, sk);
}
let vt = graph.push(Node {
op: Op::Permute(vec![0, 1, 3, 2]),
inputs: vec![rc.v_rep],
shape: Shape::from_dims(&[b, hq, d, sk]),
dtype,
});
let dp = graph.push(Node {
op: Op::MatMul,
inputs: vec![do_id, vt],
shape: scores_shape.clone(),
dtype,
});
let mut dscaled = graph.push(Node {
op: Op::Fused(
FusedOps::SOFTMAX_LAST_DIM_BACKWARD,
FusedOpParams::SoftmaxLastDimBackward,
),
inputs: vec![rc.probs, dp],
shape: scores_shape.clone(),
dtype,
});
if let Some(t) = rc.softcap_tanh {
let t2 = graph.push(Node {
op: Op::Mul,
inputs: vec![t, t],
shape: scores_shape.clone(),
dtype,
});
let neg = graph.push(Node {
op: Op::MulScalar(-1.0),
inputs: vec![t2],
shape: scores_shape.clone(),
dtype,
});
let omt2 = graph.push(Node {
op: Op::AddScalar(1.0),
inputs: vec![neg],
shape: scores_shape.clone(),
dtype,
});
dscaled = graph.push(Node {
op: Op::Mul,
inputs: vec![dscaled, omt2],
shape: scores_shape.clone(),
dtype,
});
}
let dscores = graph.push(Node {
op: Op::MulScalar(scale as f64),
inputs: vec![dscaled],
shape: scores_shape,
dtype,
});
if variant == FusedOps::FLASH_ATTN_BACKWARD_Q {
return graph.push(Node {
op: Op::MatMul,
inputs: vec![dscores, rc.k_rep],
shape: q_shape,
dtype,
});
}
let dst = graph.push(Node {
op: Op::Permute(vec![0, 1, 3, 2]),
inputs: vec![dscores],
shape: Shape::from_dims(&[b, hq, sk, sq]),
dtype,
});
let dk_rep = graph.push(Node {
op: Op::MatMul,
inputs: vec![dst, q_id],
shape: Shape::from_dims(&[b, hq, sk, d]),
dtype,
});
fold(graph, dk_rep, sk)
}
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,
}
fn backward_node(
g: &mut Graph,
variant: FusedOpId,
b: usize,
sq: usize,
sk: usize,
d: usize,
cfg: Cfg,
) -> NodeId {
let q_shape = Shape::from_dims(&[b, cfg.hq, sq, d]);
let kv_shape = Shape::from_dims(&[b, cfg.hkv, sk, d]);
let q = g.push(Node {
op: Op::Const,
inputs: vec![],
shape: q_shape.clone(),
dtype: DType::F32,
});
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.clone(),
dtype: DType::F32,
});
let do_ = g.push(Node {
op: Op::Const,
inputs: vec![],
shape: q_shape.clone(),
dtype: DType::F32,
});
let mut inputs = vec![q, k, v, do_];
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);
}
let out_shape = if variant == FusedOps::FLASH_ATTN_BACKWARD_Q {
q_shape
} else {
kv_shape
};
g.push(Node {
op: Op::Fused(
variant,
FusedOpParams::FlashAttnBackward {
softmax_scale: 0.5,
causal: cfg.causal,
window_size_left: cfg.window_l,
window_size_right: cfg.window_r,
softcap: cfg.softcap,
},
),
inputs,
shape: out_shape,
dtype: DType::F32,
})
}
#[test]
fn flash_attn_backward_recipe_matches_frozen_legacy_across_variants_and_configs() {
let (b, sq, sk, d) = (1usize, 3usize, 3usize, 2usize);
let masks: [(bool, Option<usize>, Option<usize>); 5] = [
(false, None, None), (true, None, None), (false, Some(2), Some(1)), (false, Some(2), None), (true, Some(2), Some(1)), ];
let variants = [
FusedOps::FLASH_ATTN_BACKWARD_Q,
FusedOps::FLASH_ATTN_BACKWARD_K,
FusedOps::FLASH_ATTN_BACKWARD_V,
];
let mut fired = 0usize;
for variant in variants {
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)] {
let cfg = Cfg {
hq,
hkv,
causal,
window_l,
window_r,
softcap,
alibi,
};
let mut g = Graph::new();
let fused = backward_node(&mut g, variant, b, sq, sk, 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,
"decompose fires (variant={variant:?}, causal={causal}, \
wl={window_l:?}, wr={window_r:?}, softcap={softcap:?}, \
alibi={alibi}, hq={hq}, hkv={hkv})"
);
assert_eq!(
g.node(new_root).shape,
out_shape,
"gradient output = shape_rule (variant={variant:?})"
);
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,
3 * 5 * 2 * 2 * 2,
"every (variant × config) was checked"
);
}
#[test]
fn flash_attn_backward_v_stays_imperative_bind_gap() {
let cfg = Cfg {
hq: 4,
hkv: 2,
causal: true,
window_l: None,
window_r: None,
softcap: Some(30.0),
alibi: true,
};
let mut g = Graph::new();
let fused = backward_node(&mut g, FusedOps::FLASH_ATTN_BACKWARD_V, 1, 3, 3, 2, cfg);
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,
"V decompose fires (imperative primitive form, not a self-return)"
);
assert_eq!(g.node(new_root).shape, Shape::from_dims(&[1, 2, 3, 2]));
let legacy_root = frozen_legacy_decompose(&mut g, fused, ¶ms);
assert_structural_eq(&g, new_root, legacy_root);
}
#[test]
fn flash_attn_backward_wrong_params_is_a_fixpoint_not_a_crash() {
for variant in [
FusedOps::FLASH_ATTN_BACKWARD_Q,
FusedOps::FLASH_ATTN_BACKWARD_K,
FusedOps::FLASH_ATTN_BACKWARD_V,
] {
let cfg = Cfg {
hq: 2,
hkv: 2,
causal: false,
window_l: None,
window_r: None,
softcap: None,
alibi: false,
};
let mut g = Graph::new();
let fused = backward_node(&mut g, variant, 1, 3, 3, 2, cfg);
let before = g.len();
let out = decompose(&mut g, fused, &FusedOpParams::Rope);
assert_eq!(
out, fused,
"wrong params => typed decline => fixpoint ({variant:?})"
);
assert_eq!(
g.len(),
before,
"declined before any emission ({variant:?})"
);
}
}
}