use crate::{Graph, NodeId};
use fuel_ir::storage::OutputViewSpec;
use fuel_ir::{DType, DynScalar, Shape};
use fuel_kernel_seam_types::PatternNode;
use std::collections::HashMap;
pub mod awq_matmul;
pub mod causal_conv1d;
pub mod conv2d;
pub mod conv_transpose_2d;
pub mod flash_attn;
pub mod flash_attn_backward;
pub mod fused_linear;
pub mod fused_softmax_cross_entropy;
pub mod inplace_affine;
pub mod layer_norm_last_dim;
pub mod layer_norm_last_dim_backward;
pub mod nf4_matmul;
pub mod paged_attn;
pub mod powi_backward;
pub mod qmatmul;
pub mod reduce_max_to_backward;
pub mod rms_norm_last_dim;
pub mod rms_norm_last_dim_backward;
pub mod rope;
pub mod selective_scan;
pub mod softmax_last_dim;
pub mod softmax_last_dim_backward;
pub mod ssd_chunk_scan;
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct FusedOpId(pub u16);
impl FusedOpId {
pub const UNASSIGNED: FusedOpId = FusedOpId(0);
pub const RUNTIME_FUSED_BASE: u16 = 0x8000;
pub fn is_runtime(self) -> bool {
self.0 >= Self::RUNTIME_FUSED_BASE
}
}
pub struct FusedOpEntry {
pub id: FusedOpId,
pub name: &'static str,
pub family: FusedOpFamily,
pub pattern: SubgraphPattern,
pub decompose: fn(&mut Graph, NodeId, &FusedOpParams) -> NodeId,
pub backward: BackwardKind,
pub shape_rule: fn(&[Shape], &FusedOpParams) -> Shape,
pub dtype_rule: fn(&[DType], &FusedOpParams) -> DType,
#[expect(
clippy::type_complexity,
reason = "optional output-view rule fn-pointer; a type alias would add indirection for a single field"
)]
pub output_views: Option<fn(&[Shape], &[DType], &FusedOpParams) -> Vec<OutputViewSpec>>,
pub destructive_input: Option<usize>,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum FusedOpFamily {
Forward,
Backward,
Quantized,
Attention,
Norm,
}
#[derive(Debug, Clone, PartialEq)]
pub enum FusedOpParams {
SoftmaxLastDim,
FusedLinear,
RmsNormLastDim { eps: f64 },
LayerNormLastDim { eps: f64 },
Rope,
Conv2D {
stride: (usize, usize),
padding: (usize, usize),
groups: usize,
},
SoftmaxLastDimBackward,
LayerNormLastDimBackward { eps: f64 },
RmsNormLastDimBackward { eps: f64 },
ReduceMaxToBackward,
PowIBackward { exp: i32 },
ConvTranspose2D {
stride: (usize, usize),
padding: (usize, usize),
output_padding: (usize, usize),
dilation: (usize, usize),
groups: usize,
},
FlashAttn {
softmax_scale: f32,
causal: bool,
window_size_left: Option<usize>,
window_size_right: Option<usize>,
softcap: Option<f32>,
k_len: Option<DynScalar>,
},
PagedAttn {
softmax_scale: f32,
block_size: usize,
softcap: Option<f32>,
},
QMatMul {
quant_type: crate::QuantType,
k: usize,
n: usize,
},
InplaceAffine { mul: f64, add: f64 },
SsdChunkScan { chunk_size: usize },
Nf4Matmul { block_size: usize },
AwqMatmul {
group_size: usize,
split_k_iters: i32,
},
FlashAttnBackward {
softmax_scale: f32,
causal: bool,
window_size_left: Option<usize>,
window_size_right: Option<usize>,
softcap: Option<f32>,
},
SelectiveScan { delta_softplus: bool },
CausalConv1d { use_silu: bool },
FusedSoftmaxCrossEntropy {
reduction: Reduction,
ignore_index: i64,
},
Runtime { scalars: Vec<f64> },
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum Reduction {
Mean,
Sum,
None,
}
impl Reduction {
pub(crate) fn key(self) -> i64 {
match self {
Reduction::Mean => 0,
Reduction::Sum => 1,
Reduction::None => 2,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct FusedOpParamsKey {
pub tag: u16,
pub bits: Vec<u64>,
pub ints: Vec<i64>,
}
impl FusedOpParams {
pub fn key(&self) -> FusedOpParamsKey {
match self {
FusedOpParams::SoftmaxLastDim => FusedOpParamsKey {
tag: 1,
bits: Vec::new(),
ints: Vec::new(),
},
FusedOpParams::FusedLinear => FusedOpParamsKey {
tag: 2,
bits: Vec::new(),
ints: Vec::new(),
},
FusedOpParams::RmsNormLastDim { eps } => FusedOpParamsKey {
tag: 3,
bits: vec![eps.to_bits()],
ints: Vec::new(),
},
FusedOpParams::LayerNormLastDim { eps } => FusedOpParamsKey {
tag: 4,
bits: vec![eps.to_bits()],
ints: Vec::new(),
},
FusedOpParams::Rope => FusedOpParamsKey {
tag: 5,
bits: Vec::new(),
ints: Vec::new(),
},
FusedOpParams::Conv2D {
stride,
padding,
groups,
} => FusedOpParamsKey {
tag: 6,
bits: Vec::new(),
ints: vec![
stride.0 as i64,
stride.1 as i64,
padding.0 as i64,
padding.1 as i64,
*groups as i64,
],
},
FusedOpParams::SoftmaxLastDimBackward => FusedOpParamsKey {
tag: 7,
bits: Vec::new(),
ints: Vec::new(),
},
FusedOpParams::LayerNormLastDimBackward { eps } => FusedOpParamsKey {
tag: 8,
bits: vec![eps.to_bits()],
ints: Vec::new(),
},
FusedOpParams::RmsNormLastDimBackward { eps } => FusedOpParamsKey {
tag: 9,
bits: vec![eps.to_bits()],
ints: Vec::new(),
},
FusedOpParams::ReduceMaxToBackward => FusedOpParamsKey {
tag: 10,
bits: Vec::new(),
ints: Vec::new(),
},
FusedOpParams::ConvTranspose2D {
stride,
padding,
output_padding,
dilation,
groups,
} => FusedOpParamsKey {
tag: 11,
bits: Vec::new(),
ints: vec![
stride.0 as i64,
stride.1 as i64,
padding.0 as i64,
padding.1 as i64,
output_padding.0 as i64,
output_padding.1 as i64,
dilation.0 as i64,
dilation.1 as i64,
*groups as i64,
],
},
FusedOpParams::FlashAttn {
softmax_scale,
causal,
window_size_left,
window_size_right,
softcap,
k_len,
} => {
let (k_tag, k_val): (i64, i64) = match k_len {
None => (0, 0),
Some(DynScalar::Concrete(v)) => (1, *v as i64),
Some(DynScalar::Sym(s)) => (2, s.0 as i64),
};
FusedOpParamsKey {
tag: 12,
bits: vec![
softmax_scale.to_bits() as u64,
softcap.map(|s| s.to_bits() as u64).unwrap_or(u64::MAX),
],
ints: vec![
if *causal { 1 } else { 0 },
window_size_left.map(|w| w as i64).unwrap_or(i64::MIN),
window_size_right.map(|w| w as i64).unwrap_or(i64::MIN),
k_tag,
k_val,
],
}
}
FusedOpParams::PagedAttn {
softmax_scale,
block_size,
softcap,
} => FusedOpParamsKey {
tag: 13,
bits: vec![
softmax_scale.to_bits() as u64,
softcap.map(|s| s.to_bits() as u64).unwrap_or(u64::MAX),
],
ints: vec![*block_size as i64],
},
FusedOpParams::QMatMul { quant_type, k, n } => FusedOpParamsKey {
tag: 14,
bits: Vec::new(),
ints: vec![quant_type_key(*quant_type), *k as i64, *n as i64],
},
FusedOpParams::PowIBackward { exp } => FusedOpParamsKey {
tag: 15,
bits: Vec::new(),
ints: vec![*exp as i64],
},
FusedOpParams::InplaceAffine { mul, add } => FusedOpParamsKey {
tag: 16,
bits: vec![mul.to_bits(), add.to_bits()],
ints: Vec::new(),
},
FusedOpParams::FusedSoftmaxCrossEntropy {
reduction,
ignore_index,
} => FusedOpParamsKey {
tag: 17,
bits: Vec::new(),
ints: vec![reduction.key(), *ignore_index],
},
FusedOpParams::CausalConv1d { use_silu } => FusedOpParamsKey {
tag: 18,
bits: Vec::new(),
ints: vec![*use_silu as i64],
},
FusedOpParams::SelectiveScan { delta_softplus } => FusedOpParamsKey {
tag: 19,
bits: Vec::new(),
ints: vec![*delta_softplus as i64],
},
FusedOpParams::SsdChunkScan { chunk_size } => FusedOpParamsKey {
tag: 20,
bits: Vec::new(),
ints: vec![*chunk_size as i64],
},
FusedOpParams::Nf4Matmul { block_size } => FusedOpParamsKey {
tag: 21,
bits: Vec::new(),
ints: vec![*block_size as i64],
},
FusedOpParams::AwqMatmul {
group_size,
split_k_iters,
} => FusedOpParamsKey {
tag: 23,
bits: Vec::new(),
ints: vec![*group_size as i64, *split_k_iters as i64],
},
FusedOpParams::FlashAttnBackward {
softmax_scale,
causal,
window_size_left,
window_size_right,
softcap,
} => FusedOpParamsKey {
tag: 22,
bits: vec![
softmax_scale.to_bits() as u64,
softcap.map(|s| s.to_bits() as u64).unwrap_or(u64::MAX),
],
ints: vec![
if *causal { 1 } else { 0 },
window_size_left.map(|w| w as i64).unwrap_or(i64::MIN),
window_size_right.map(|w| w as i64).unwrap_or(i64::MIN),
],
},
FusedOpParams::Runtime { scalars } => FusedOpParamsKey {
tag: 0xF000,
bits: scalars.iter().map(|s| s.to_bits()).collect(),
ints: Vec::new(),
},
}
}
}
fn quant_type_key(q: crate::QuantType) -> i64 {
use crate::QuantType::*;
match q {
Q4_0 => 1,
Q4_1 => 2,
Q5_0 => 3,
Q5_1 => 4,
Q8_0 => 5,
Q8_1 => 6,
Q2K => 7,
Q3K => 8,
Q4_K_M => 9,
Q5K => 10,
Q6K => 11,
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum BackwardKind {
Fused(FusedOpId),
Decompose,
NotDifferentiable,
}
#[expect(
clippy::large_enum_variant,
reason = "matcher pattern enum; boxing the large variant is deferred to the same follow-up as PatternNode (docs/gaps.md GAP-215) -- a cross-cutting change to construction/match sites, not a lint-commit change"
)]
pub enum SubgraphPattern {
Declarative(PatternTree),
Callable(fn(&Graph, NodeId) -> Option<PatternMatch>),
}
#[derive(Debug, Clone)]
pub struct PatternTree {
pub root: crate::jit::PatternNode,
pub params: FusedOpParams,
}
#[derive(Debug, Clone)]
pub struct PatternMatch {
pub bindings: Vec<(usize, NodeId)>,
pub params: FusedOpParams,
}
#[derive(Default)]
pub struct FusedOpRegistry {
entries: Vec<FusedOpEntry>,
by_name: HashMap<&'static str, FusedOpId>,
}
impl FusedOpRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn with_entry(mut self, entry: FusedOpEntry) -> Self {
debug_assert!(
!self.by_name.contains_key(entry.name),
"FusedOpRegistry: duplicate entry name {:?}",
entry.name
);
debug_assert!(
entry.id.0 as usize == self.entries.len() + 1 || self.entry(entry.id).is_none(),
"FusedOpRegistry: id {:?} already populated",
entry.id
);
self.by_name.insert(entry.name, entry.id);
let slot = entry.id.0 as usize;
if self.entries.len() <= slot {
self.entries.reserve(slot + 1 - self.entries.len());
}
if self.entries.is_empty() {
self.entries.push(placeholder_entry());
}
if slot < self.entries.len() {
self.entries[slot] = entry;
} else {
while self.entries.len() < slot {
self.entries.push(placeholder_entry());
}
self.entries.push(entry);
}
self
}
pub fn entry(&self, id: FusedOpId) -> Option<&FusedOpEntry> {
let slot = id.0 as usize;
if slot == 0 || slot >= self.entries.len() {
return None;
}
let e = &self.entries[slot];
if e.id == FusedOpId::UNASSIGNED {
None
} else {
Some(e)
}
}
pub fn id_for_name(&self, name: &str) -> Option<FusedOpId> {
self.by_name.get(name).copied()
}
pub fn entries_iter(&self) -> impl Iterator<Item = &FusedOpEntry> {
self.entries
.iter()
.filter(|e| e.id != FusedOpId::UNASSIGNED)
}
pub fn len(&self) -> usize {
self.entries
.iter()
.filter(|e| e.id != FusedOpId::UNASSIGNED)
.count()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct PatternHash(pub u64);
pub fn recipe_for(graph: &Graph, id: NodeId) -> Option<PatternNode> {
let (fused_id, params) = match &graph.node(id).op {
crate::Op::Fused(fid, p) => (*fid, p.clone()),
_ => return None,
};
match fused_id {
FusedOps::PAGED_ATTN => crate::registry::paged_attn::recipe_for(graph, id, ¶ms),
_ => None,
}
}
pub fn decompose_via_recipe(
graph: &mut Graph,
id: NodeId,
recipe: &PatternNode,
scalars: Option<Vec<f64>>,
) -> NodeId {
let Some(scalars) = scalars else {
eprintln!(
"decompose_via_recipe: wrong params payload for node {id:?} — \
typed decline, node stays fused (G2 fixpoint)"
);
return id;
};
if let Err(err) = crate::runtime_fused::validate_recipe(recipe) {
eprintln!(
"decompose_via_recipe: recipe declined for node {id:?}: {err:?} — \
surfaced honest miss, node stays fused (G2 fixpoint)"
);
return id;
}
let inputs = graph.node(id).inputs.clone();
if recipe.bind_indices().len() != inputs.len() {
eprintln!(
"decompose_via_recipe: recipe binds {} != node inputs {} for node {id:?} — \
typed decline, node stays fused (G2 fixpoint)",
recipe.bind_indices().len(),
inputs.len(),
);
return id;
}
if scalars.len() != crate::runtime_fused::count_scalar_slots(recipe) {
eprintln!(
"decompose_via_recipe: {} projected scalars for {} open slots on node {id:?} — \
typed decline, node stays fused (G2 fixpoint)",
scalars.len(),
crate::runtime_fused::count_scalar_slots(recipe),
);
return id;
}
let emitted = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
crate::runtime_fused::try_emit_region(graph, recipe, &inputs, &scalars)
}));
match emitted {
Ok(Ok(root)) => root,
Ok(Err(err)) => {
eprintln!(
"decompose_via_recipe: rel-attr resolution declined for node {id:?}: {err:?} — \
surfaced gap, node stays fused (G2 fixpoint)"
);
id
}
Err(_) => {
eprintln!(
"decompose_via_recipe: emit panicked for node {id:?} — \
caught (never-panic last resort), node stays fused (G2 fixpoint)"
);
id
}
}
}
fn placeholder_entry() -> FusedOpEntry {
FusedOpEntry {
destructive_input: None,
id: FusedOpId::UNASSIGNED,
name: "<unassigned>",
family: FusedOpFamily::Forward,
pattern: SubgraphPattern::Callable(|_g, _id| None),
decompose: |_g, id, _p| id,
backward: BackwardKind::NotDifferentiable,
shape_rule: |_shapes, _params| Shape::from_dims(&[]),
dtype_rule: |_dtypes, _params| DType::F32,
output_views: None,
}
}
pub struct FusedOps;
impl FusedOps {
pub const SOFTMAX_LAST_DIM: FusedOpId = FusedOpId(1);
pub const FUSED_LINEAR: FusedOpId = FusedOpId(2);
pub const RMS_NORM_LAST_DIM: FusedOpId = FusedOpId(3);
pub const LAYER_NORM_LAST_DIM: FusedOpId = FusedOpId(4);
pub const ROPE: FusedOpId = FusedOpId(5);
pub const CONV2D: FusedOpId = FusedOpId(6);
pub const SOFTMAX_LAST_DIM_BACKWARD: FusedOpId = FusedOpId(7);
pub const LAYER_NORM_LAST_DIM_BACKWARD: FusedOpId = FusedOpId(8);
pub const RMS_NORM_LAST_DIM_BACKWARD: FusedOpId = FusedOpId(9);
pub const REDUCE_MAX_TO_BACKWARD: FusedOpId = FusedOpId(10);
pub const CONV_TRANSPOSE2D: FusedOpId = FusedOpId(11);
pub const FLASH_ATTN: FusedOpId = FusedOpId(12);
pub const PAGED_ATTN: FusedOpId = FusedOpId(13);
pub const QMATMUL: FusedOpId = FusedOpId(14);
pub const POWI_BACKWARD: FusedOpId = FusedOpId(15);
pub const INPLACE_AFFINE: FusedOpId = FusedOpId(16);
pub const FUSED_SOFTMAX_CROSS_ENTROPY: FusedOpId = FusedOpId(17);
pub const CAUSAL_CONV1D: FusedOpId = FusedOpId(18);
pub const SELECTIVE_SCAN: FusedOpId = FusedOpId(19);
pub const SSD_CHUNK_SCAN: FusedOpId = FusedOpId(20);
pub const NF4_MATMUL: FusedOpId = FusedOpId(21);
pub const FLASH_ATTN_BACKWARD_Q: FusedOpId = FusedOpId(22);
pub const FLASH_ATTN_BACKWARD_K: FusedOpId = FusedOpId(23);
pub const FLASH_ATTN_BACKWARD_V: FusedOpId = FusedOpId(24);
pub const AWQ_MATMUL: FusedOpId = FusedOpId(25);
}
pub fn default_registry() -> &'static FusedOpRegistry {
use std::sync::OnceLock;
static REGISTRY: OnceLock<FusedOpRegistry> = OnceLock::new();
REGISTRY.get_or_init(|| {
FusedOpRegistry::new()
.with_entry(softmax_last_dim::entry())
.with_entry(fused_linear::entry())
.with_entry(rms_norm_last_dim::entry())
.with_entry(layer_norm_last_dim::entry())
.with_entry(rope::entry())
.with_entry(conv2d::entry())
.with_entry(softmax_last_dim_backward::entry())
.with_entry(layer_norm_last_dim_backward::entry())
.with_entry(rms_norm_last_dim_backward::entry())
.with_entry(reduce_max_to_backward::entry())
.with_entry(conv_transpose_2d::entry())
.with_entry(flash_attn::entry())
.with_entry(flash_attn_backward::entry_q())
.with_entry(flash_attn_backward::entry_k())
.with_entry(flash_attn_backward::entry_v())
.with_entry(paged_attn::entry())
.with_entry(qmatmul::entry())
.with_entry(powi_backward::entry())
.with_entry(inplace_affine::entry())
.with_entry(fused_softmax_cross_entropy::entry())
.with_entry(causal_conv1d::entry())
.with_entry(selective_scan::entry())
.with_entry(ssd_chunk_scan::entry())
.with_entry(nf4_matmul::entry())
.with_entry(awq_matmul::entry())
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fused_op_id_basic() {
let a = FusedOpId(1);
let b = FusedOpId(1);
let c = FusedOpId(2);
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn registry_empty() {
let r = FusedOpRegistry::new();
assert!(r.is_empty());
assert_eq!(r.len(), 0);
assert!(r.entry(FusedOpId(1)).is_none());
assert!(r.entry(FusedOpId::UNASSIGNED).is_none());
assert!(r.id_for_name("anything").is_none());
}
#[test]
fn registry_register_one() {
fn dummy_decompose(_g: &mut Graph, id: NodeId, _p: &FusedOpParams) -> NodeId {
id
}
fn dummy_pattern(_g: &Graph, _id: NodeId) -> Option<PatternMatch> {
None
}
fn dummy_shape(_s: &[Shape], _p: &FusedOpParams) -> Shape {
Shape::from_dims(&[1])
}
fn dummy_dtype(_d: &[DType], _p: &FusedOpParams) -> DType {
DType::F32
}
let r = FusedOpRegistry::new().with_entry(FusedOpEntry {
destructive_input: None,
id: FusedOps::SOFTMAX_LAST_DIM,
name: "SoftmaxLastDim",
family: FusedOpFamily::Forward,
pattern: SubgraphPattern::Callable(dummy_pattern),
decompose: dummy_decompose,
backward: BackwardKind::NotDifferentiable,
shape_rule: dummy_shape,
dtype_rule: dummy_dtype,
output_views: None,
});
assert_eq!(r.len(), 1);
assert!(!r.is_empty());
let entry = r.entry(FusedOps::SOFTMAX_LAST_DIM).expect("registered");
assert_eq!(entry.name, "SoftmaxLastDim");
assert_eq!(entry.family, FusedOpFamily::Forward);
assert_eq!(
r.id_for_name("SoftmaxLastDim"),
Some(FusedOps::SOFTMAX_LAST_DIM)
);
}
#[test]
fn fused_op_params_key_softmax() {
let k1 = FusedOpParams::SoftmaxLastDim.key();
let k2 = FusedOpParams::SoftmaxLastDim.key();
assert_eq!(k1, k2);
assert_eq!(k1.tag, 1);
assert!(k1.bits.is_empty());
assert!(k1.ints.is_empty());
}
}