use fuel_graph::registry::{FusedOpParams, FusedOps};
use fuel_graph::{Graph, Node, NodeId, Op};
use fuel_ir::probe::BackendId;
use fuel_ir::{DType, DynScalar, Result, Shape};
use crate::runtime_fused_kernels::fused_kernel_available;
use crate::topology::SystemTopology;
#[derive(Clone, Copy, Debug)]
pub struct FlashArmCapability {
pub cuda_flash_kernel: bool,
pub cuda_in_topology: bool,
}
impl FlashArmCapability {
pub fn production() -> Self {
Self {
cuda_flash_kernel: fused_kernel_available(FusedOps::FLASH_ATTN, BackendId::Cuda),
cuda_in_topology: SystemTopology::current()
.backends()
.contains(&BackendId::Cuda),
}
}
#[cfg(test)]
pub fn all_available() -> Self {
Self {
cuda_flash_kernel: true,
cuda_in_topology: true,
}
}
pub fn available(&self) -> bool {
self.cuda_flash_kernel && self.cuda_in_topology
}
}
#[derive(Clone, Debug)]
pub struct DecodeFlashSpec {
pub q: NodeId,
pub k: NodeId,
pub v: NodeId,
pub alibi: Option<NodeId>,
pub softmax_scale: f32,
pub causal: bool,
pub window_size_left: Option<usize>,
pub window_size_right: Option<usize>,
pub softcap: Option<f32>,
pub k_len: DynScalar,
pub decomposed_out: NodeId,
pub reconverge: NodeId,
}
#[allow(clippy::too_many_arguments)]
pub fn flash_decode_admissible(
q_shape: &Shape,
dtype: DType,
window_size_left: Option<usize>,
window_size_right: Option<usize>,
softcap: Option<f32>,
has_alibi: bool,
cap: FlashArmCapability,
) -> bool {
if !cap.available() {
return false;
}
if !matches!(dtype, DType::F16 | DType::BF16) {
return false;
}
let dims = q_shape.dims();
if dims.len() != 4 {
return false;
}
let sq = dims[2];
let d = dims[3];
if sq != 1 || d == 0 || d > 128 {
return false;
}
if window_size_left.is_some() || window_size_right.is_some() {
return false;
}
if softcap.is_some() {
return false;
}
if has_alibi {
return false;
}
true
}
pub fn offer_decode_flash_arm(
graph: &mut Graph,
spec: &DecodeFlashSpec,
cap: FlashArmCapability,
) -> Result<Option<NodeId>> {
let (q_shape, q_dtype) = {
let n = graph.node(spec.q);
(n.shape.clone(), n.dtype)
};
if !flash_decode_admissible(
&q_shape,
q_dtype,
spec.window_size_left,
spec.window_size_right,
spec.softcap,
spec.alibi.is_some(),
cap,
) {
return Ok(None);
}
let mut inputs = vec![spec.q, spec.k, spec.v];
if let Some(a) = spec.alibi {
inputs.push(a);
}
let flash = graph.push(Node {
op: Op::Fused(
FusedOps::FLASH_ATTN,
FusedOpParams::FlashAttn {
softmax_scale: spec.softmax_scale,
causal: spec.causal,
window_size_left: spec.window_size_left,
window_size_right: spec.window_size_right,
softcap: spec.softcap,
k_len: Some(spec.k_len),
},
),
inputs,
shape: q_shape,
dtype: q_dtype,
});
graph.set_target_backend(flash, BackendId::Cuda);
let mut builder = graph.open_branch(spec.q);
builder.add_arm(spec.decomposed_out); builder.add_arm(flash); let branch = builder.finalize_branches(graph, spec.reconverge)?;
Ok(branch)
}
#[cfg(test)]
mod tests {
use super::*;
fn leaf(g: &mut Graph, dims: &[usize], dtype: DType) -> NodeId {
g.push(Node {
op: Op::Const,
inputs: vec![],
shape: Shape::from_dims(dims),
dtype,
})
}
fn decode_region(
h: usize,
d: usize,
sk: usize,
dtype: DType,
) -> (Graph, NodeId, NodeId, NodeId, NodeId, NodeId) {
let mut g = Graph::new();
let q = leaf(&mut g, &[1, h, 1, d], dtype);
let k = leaf(&mut g, &[1, h, sk, d], dtype);
let v = leaf(&mut g, &[1, h, sk, d], dtype);
let mask = leaf(&mut g, &[1, h, 1, sk], dtype);
let kt = g.push(Node {
op: Op::Permute(vec![0, 1, 3, 2]),
inputs: vec![k],
shape: Shape::from_dims(&[1, h, d, sk]),
dtype,
});
let scores = g.push(Node {
op: Op::MatMul,
inputs: vec![q, kt],
shape: Shape::from_dims(&[1, h, 1, sk]),
dtype,
});
let scaled = g.push(Node {
op: Op::MulScalar(0.5),
inputs: vec![scores],
shape: Shape::from_dims(&[1, h, 1, sk]),
dtype,
});
let masked = g.push(Node {
op: Op::Add,
inputs: vec![scaled, mask],
shape: Shape::from_dims(&[1, h, 1, sk]),
dtype,
});
let probs = g.push(Node {
op: Op::Fused(FusedOps::SOFTMAX_LAST_DIM, FusedOpParams::SoftmaxLastDim),
inputs: vec![masked],
shape: Shape::from_dims(&[1, h, 1, sk]),
dtype,
});
let attn_v = g.push(Node {
op: Op::MatMul,
inputs: vec![probs, v],
shape: Shape::from_dims(&[1, h, 1, d]),
dtype,
});
let merged = g.push(Node {
op: Op::Permute(vec![0, 2, 1, 3]),
inputs: vec![attn_v],
shape: Shape::from_dims(&[1, 1, h, d]),
dtype,
});
(g, q, k, v, attn_v, merged)
}
fn spec_for(
q: NodeId,
k: NodeId,
v: NodeId,
attn_v: NodeId,
merged: NodeId,
sk: usize,
) -> DecodeFlashSpec {
DecodeFlashSpec {
q,
k,
v,
alibi: None,
softmax_scale: 0.5,
causal: true,
window_size_left: None,
window_size_right: None,
softcap: None,
k_len: DynScalar::Concrete(sk),
decomposed_out: attn_v,
reconverge: merged,
}
}
#[test]
fn flash_arm_offered_for_supported_decode_shape() {
let (mut g, q, k, v, attn_v, merged) = decode_region(4, 64, 37, DType::F16);
let spec = spec_for(q, k, v, attn_v, merged, 37);
let before = g.len();
let branch = offer_decode_flash_arm(&mut g, &spec, FlashArmCapability::all_available())
.expect("well-formed spec")
.expect("supported shape ⇒ arm offered");
assert!(
matches!(g.node(branch).op, Op::Branch { .. }),
"branch node emitted"
);
let arms = g.node(branch).inputs.clone();
assert_eq!(arms.len(), 2, "2-arm branch (decomposed + flash)");
assert_eq!(
arms[0], attn_v,
"arm 0 is the decomposed region output (the oracle)"
);
let flash = arms[1];
match &g.node(flash).op {
Op::Fused(fid, FusedOpParams::FlashAttn { k_len, .. }) => {
assert_eq!(*fid, FusedOps::FLASH_ATTN, "arm 1 is FLASH_ATTN");
assert_eq!(
*k_len,
Some(DynScalar::Concrete(37)),
"arm 1 carries the live k_len"
);
}
other => panic!("arm 1 must be Fused(FLASH_ATTN, FlashAttn), got {other:?}"),
}
assert_eq!(g.node(flash).inputs, vec![q, k, v], "flash reads q, k, v");
assert_eq!(
g.target_backend(flash),
Some(BackendId::Cuda),
"arm 1 pinned to CUDA"
);
assert!(
g.node(merged).inputs.contains(&attn_v),
"reconverge reads arm 0 (an unpicked graph realizes decomposed)",
);
assert!(g.len() > before, "the flash node + branch were appended");
}
#[test]
fn f32_dtype_gets_no_flash_arm() {
let (mut g, q, k, v, attn_v, merged) = decode_region(4, 64, 37, DType::F32);
let spec = spec_for(q, k, v, attn_v, merged, 37);
let before = g.len();
let branch = offer_decode_flash_arm(&mut g, &spec, FlashArmCapability::all_available())
.expect("well-formed spec");
assert!(branch.is_none(), "f32 ⇒ no flash arm");
assert_eq!(
g.len(),
before,
"graph untouched (no flash node, no branch)"
);
}
#[test]
fn head_dim_over_128_gets_no_flash_arm() {
let (mut g, q, k, v, attn_v, merged) = decode_region(2, 256, 8, DType::F16);
let spec = spec_for(q, k, v, attn_v, merged, 8);
let before = g.len();
let branch = offer_decode_flash_arm(&mut g, &spec, FlashArmCapability::all_available())
.expect("well-formed spec");
assert!(branch.is_none(), "head_dim 256 > 128 ⇒ no flash arm");
assert_eq!(g.len(), before, "graph untouched");
}
#[test]
fn multi_query_gets_no_flash_arm() {
let (mut g, _q, _k, _v, _av, _m) = decode_region(4, 64, 37, DType::F16);
let dtype = DType::F16;
let q = leaf(&mut g, &[1, 4, 8, 64], dtype);
let k = leaf(&mut g, &[1, 4, 37, 64], dtype);
let v = leaf(&mut g, &[1, 4, 37, 64], dtype);
let attn_v = g.push(Node {
op: Op::MatMul,
inputs: vec![q, v],
shape: Shape::from_dims(&[1, 4, 8, 64]),
dtype,
});
let merged = g.push(Node {
op: Op::Permute(vec![0, 2, 1, 3]),
inputs: vec![attn_v],
shape: Shape::from_dims(&[1, 8, 4, 64]),
dtype,
});
let spec = spec_for(q, k, v, attn_v, merged, 37);
let branch = offer_decode_flash_arm(&mut g, &spec, FlashArmCapability::all_available())
.expect("well-formed spec");
assert!(branch.is_none(), "seq_q = 8 != 1 ⇒ no flash arm");
}
#[test]
fn no_cuda_capability_gets_no_flash_arm() {
let (mut g, q, k, v, attn_v, merged) = decode_region(4, 64, 37, DType::F16);
let spec = spec_for(q, k, v, attn_v, merged, 37);
let before = g.len();
let no_kernel = FlashArmCapability {
cuda_flash_kernel: false,
cuda_in_topology: true,
};
assert!(
offer_decode_flash_arm(&mut g, &spec, no_kernel)
.unwrap()
.is_none()
);
let no_device = FlashArmCapability {
cuda_flash_kernel: true,
cuda_in_topology: false,
};
assert!(
offer_decode_flash_arm(&mut g, &spec, no_device)
.unwrap()
.is_none()
);
assert_eq!(
g.len(),
before,
"no capability ⇒ decomposed-only, graph untouched"
);
}
#[test]
fn window_softcap_alibi_get_no_flash_arm() {
for mutate in [
|s: &mut DecodeFlashSpec| s.window_size_left = Some(64),
|s: &mut DecodeFlashSpec| s.window_size_right = Some(64),
|s: &mut DecodeFlashSpec| s.softcap = Some(30.0),
] {
let (mut g, q, k, v, attn_v, merged) = decode_region(4, 64, 37, DType::F16);
let mut spec = spec_for(q, k, v, attn_v, merged, 37);
mutate(&mut spec);
assert!(
offer_decode_flash_arm(&mut g, &spec, FlashArmCapability::all_available())
.unwrap()
.is_none(),
"window/softcap disqualifies the flash arm",
);
}
let (mut g, q, k, v, attn_v, merged) = decode_region(4, 64, 37, DType::F16);
let alibi = leaf(&mut g, &[4], DType::F16);
let mut spec = spec_for(q, k, v, attn_v, merged, 37);
spec.alibi = Some(alibi);
assert!(
offer_decode_flash_arm(&mut g, &spec, FlashArmCapability::all_available())
.unwrap()
.is_none(),
"ALiBi disqualifies the flash arm",
);
}
#[test]
fn causal_flag_does_not_disqualify() {
let (mut g, q, k, v, attn_v, merged) = decode_region(4, 64, 37, DType::F16);
let mut spec = spec_for(q, k, v, attn_v, merged, 37);
spec.causal = true;
assert!(
offer_decode_flash_arm(&mut g, &spec, FlashArmCapability::all_available())
.unwrap()
.is_some(),
"causal is accepted-and-ignored ⇒ arm still offered",
);
}
#[test]
fn bf16_dtype_gets_flash_arm() {
let (mut g, q, k, v, attn_v, merged) = decode_region(4, 64, 37, DType::BF16);
let spec = spec_for(q, k, v, attn_v, merged, 37);
assert!(
offer_decode_flash_arm(&mut g, &spec, FlashArmCapability::all_available())
.unwrap()
.is_some(),
"bf16 is a supported decode dtype",
);
}
}