use crate::registry::{
BackwardKind, FusedOpEntry, FusedOpFamily, FusedOpParams, FusedOps, PatternMatch,
SubgraphPattern, decompose_via_recipe,
};
use crate::{Graph, NodeId};
use fuel_ir::{DType, Shape};
use fuel_kernel_seam_types::{OpAttrs, OpTag, PatternNode};
const NF4_LUT: [f32; 16] = [
-1.0,
-0.696_192_8,
-0.525_073_05,
-0.394_917_5,
-0.284_441_38,
-0.184_773_43,
-0.091_050_036,
0.0,
0.079_580_3,
0.160_930_2,
0.246_112_3,
0.337_915_24,
0.440_709_83,
0.562_617,
0.722_956_84,
1.0,
];
pub fn entry() -> FusedOpEntry {
FusedOpEntry {
destructive_input: None,
id: FusedOps::NF4_MATMUL,
name: "Nf4Matmul",
family: FusedOpFamily::Quantized,
pattern: SubgraphPattern::Callable(canonical_pattern),
decompose,
backward: BackwardKind::Decompose,
shape_rule,
dtype_rule,
output_views: None,
}
}
fn shape_rule(input_shapes: &[Shape], _params: &FusedOpParams) -> Shape {
debug_assert_eq!(
input_shapes.len(),
3,
"Nf4Matmul takes 3 inputs (activations, w_packed, absmax)",
);
let a_dims = input_shapes[0].dims();
let w_dims = input_shapes[1].dims();
debug_assert!(
a_dims.len() >= 2,
"Nf4Matmul: activations must be rank ≥ 2, got {a_dims:?}"
);
debug_assert_eq!(
w_dims.len(),
2,
"Nf4Matmul: w_packed must be rank 2 [N, K/2], got {w_dims:?}"
);
let n = w_dims[0];
let mut out_dims: Vec<usize> = a_dims[..a_dims.len() - 1].to_vec();
out_dims.push(n);
Shape::from_dims(&out_dims)
}
fn dtype_rule(input_dtypes: &[DType], _params: &FusedOpParams) -> DType {
debug_assert_eq!(
input_dtypes.len(),
3,
"Nf4Matmul takes 3 inputs (activations, w_packed, absmax)",
);
input_dtypes[0]
}
fn recipe(a_shape: &Shape, w_shape: &Shape, dtype: DType, block_size: usize) -> PatternNode {
use OpTag as T;
let w_dims = w_shape.dims();
let n_out = w_dims[0];
let k_half = w_dims[1];
let k = k_half * 2;
let a_dims = a_shape.dims();
let m_prime: usize = a_dims[..a_dims.len() - 1].iter().product();
let n_blocks = k / block_size;
let op = |op, attrs, operands| PatternNode::Op {
op,
attrs,
operands,
};
let bind = |i: u8| PatternNode::Bind { index: i };
let shape_attr = |dims: &[usize]| OpAttrs {
target_shape: dims.iter().map(|&d| d as i64).collect(),
..OpAttrs::default()
};
let cast_attr = |dt: DType| OpAttrs {
cast_dtype: Some(dt.as_str().to_string()),
..OpAttrs::default()
};
let scalar_attr = |v: f64| OpAttrs {
scalars: vec![v],
..OpAttrs::default()
};
let unsqueeze2 = || OpAttrs {
dims: vec![2],
..OpAttrs::default()
};
let concat2 = || OpAttrs {
axis: Some(2),
..OpAttrs::default()
};
let wf = op(T::Cast, cast_attr(DType::F32), vec![bind(1)]);
let wf_div16 = op(T::MulScalar, scalar_attr(1.0 / 16.0), vec![wf.clone()]);
let upper = op(T::Floor, OpAttrs::default(), vec![wf_div16]);
let up16 = op(T::MulScalar, scalar_attr(16.0), vec![upper.clone()]);
let lower = op(T::Sub, OpAttrs::default(), vec![wf, up16]);
let lower3 = op(T::Unsqueeze, unsqueeze2(), vec![lower]);
let upper3 = op(T::Unsqueeze, unsqueeze2(), vec![upper]);
let stacked = op(T::Concat, concat2(), vec![lower3, upper3]);
let codes = op(T::Reshape, shape_attr(&[n_out, k]), vec![stacked]);
let mut nf4val: Option<PatternNode> = None;
for (i, &v) in NF4_LUT.iter().enumerate() {
if v == 0.0 {
continue;
}
let diff = op(T::AddScalar, scalar_attr(-(i as f64)), vec![codes.clone()]);
let ad = op(T::Abs, OpAttrs::default(), vec![diff]);
let neg = op(T::Neg, OpAttrs::default(), vec![ad]);
let one_minus = op(T::AddScalar, scalar_attr(1.0), vec![neg]);
let ind = op(T::Relu, OpAttrs::default(), vec![one_minus]);
let term = op(T::MulScalar, scalar_attr(v as f64), vec![ind]);
nf4val = Some(match nf4val {
None => term,
Some(prev) => op(T::Add, OpAttrs::default(), vec![prev, term]),
});
}
let nf4val = nf4val.unwrap_or(codes);
let abs3 = op(T::Unsqueeze, unsqueeze2(), vec![bind(2)]);
let abs_b = op(
T::BroadcastTo,
shape_attr(&[n_out, n_blocks, block_size]),
vec![abs3],
);
let scale_full = op(T::Reshape, shape_attr(&[n_out, k]), vec![abs_b]);
let dequant = op(T::Mul, OpAttrs::default(), vec![nf4val, scale_full]);
let dequant_typed = if dtype == DType::F32 {
dequant
} else {
op(T::Cast, cast_attr(dtype), vec![dequant])
};
let dequant_t = op(T::Transpose, OpAttrs::default(), vec![dequant_typed]);
let a2 = op(T::Reshape, shape_attr(&[m_prime, k]), vec![bind(0)]);
let out2 = op(T::MatMul, OpAttrs::default(), vec![a2, dequant_t]);
let mut out_dims: Vec<usize> = a_dims[..a_dims.len() - 1].to_vec();
out_dims.push(n_out);
op(T::Reshape, shape_attr(&out_dims), vec![out2])
}
pub fn decompose(graph: &mut Graph, id: NodeId, params: &FusedOpParams) -> NodeId {
let block_size = match params {
FusedOpParams::Nf4Matmul { block_size } => *block_size,
_ => return id,
};
let (a_shape, w_shape, dtype) = {
let n = graph.node(id);
if n.inputs.len() != 3 {
return id;
}
let a_shape = graph.node(n.inputs[0]).shape.clone();
let w_shape = graph.node(n.inputs[1]).shape.clone();
(a_shape, w_shape, n.dtype)
};
let w_dims = w_shape.dims();
if w_dims.len() != 2 || a_shape.rank() < 2 {
return id;
}
let k = w_dims[1] * 2;
if block_size == 0 || k % block_size != 0 {
return id;
}
let recipe_node = recipe(&a_shape, &w_shape, dtype, block_size);
decompose_via_recipe(graph, id, &recipe_node, Some(Vec::new()))
}
pub fn canonical_pattern(_graph: &Graph, _root: NodeId) -> Option<PatternMatch> {
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Node, Op};
fn frozen_legacy_nf4_matmul_decompose(
graph: &mut Graph,
id: NodeId,
params: &FusedOpParams,
) -> NodeId {
let block_size = match params {
FusedOpParams::Nf4Matmul { block_size } => *block_size,
_ => return id,
};
let (a_id, w_id, abs_id, a_shape, w_shape, dtype) = {
let n = graph.node(id);
let a_shape = graph.node(n.inputs[0]).shape.clone();
let w_shape = graph.node(n.inputs[1]).shape.clone();
(
n.inputs[0],
n.inputs[1],
n.inputs[2],
a_shape,
w_shape,
n.dtype,
)
};
let f32 = DType::F32;
let w_dims = w_shape.dims();
let n_out = w_dims[0];
let k_half = w_dims[1];
let k = k_half * 2;
let a_dims = a_shape.dims();
let m_prime: usize = a_dims[..a_dims.len() - 1].iter().product();
let half_shape = Shape::from_dims(&[n_out, k_half]);
let code_shape = Shape::from_dims(&[n_out, k]);
let wf = graph.push(Node {
op: Op::Cast(f32),
inputs: vec![w_id],
shape: half_shape.clone(),
dtype: f32,
});
let wf_div16 = graph.push(Node {
op: Op::MulScalar(1.0 / 16.0),
inputs: vec![wf],
shape: half_shape.clone(),
dtype: f32,
});
let upper = graph.push(Node {
op: Op::Floor,
inputs: vec![wf_div16],
shape: half_shape.clone(),
dtype: f32,
});
let up16 = graph.push(Node {
op: Op::MulScalar(16.0),
inputs: vec![upper],
shape: half_shape.clone(),
dtype: f32,
});
let lower = graph.push(Node {
op: Op::Sub,
inputs: vec![wf, up16],
shape: half_shape.clone(),
dtype: f32,
});
let three_shape = Shape::from_dims(&[n_out, k_half, 1]);
let lower3 = graph.push(Node {
op: Op::Unsqueeze { dim: 2 },
inputs: vec![lower],
shape: three_shape.clone(),
dtype: f32,
});
let upper3 = graph.push(Node {
op: Op::Unsqueeze { dim: 2 },
inputs: vec![upper],
shape: three_shape,
dtype: f32,
});
let stacked = graph.push(Node {
op: Op::Concat { dim: 2 },
inputs: vec![lower3, upper3],
shape: Shape::from_dims(&[n_out, k_half, 2]),
dtype: f32,
});
let codes = graph.push(Node {
op: Op::Reshape(code_shape.clone()),
inputs: vec![stacked],
shape: code_shape.clone(),
dtype: f32,
});
let mut nf4val: Option<NodeId> = None;
for (i, &v) in NF4_LUT.iter().enumerate() {
if v == 0.0 {
continue;
}
let diff = graph.push(Node {
op: Op::AddScalar(-(i as f64)),
inputs: vec![codes],
shape: code_shape.clone(),
dtype: f32,
});
let ad = graph.push(Node {
op: Op::Abs,
inputs: vec![diff],
shape: code_shape.clone(),
dtype: f32,
});
let neg = graph.push(Node {
op: Op::Neg,
inputs: vec![ad],
shape: code_shape.clone(),
dtype: f32,
});
let one_minus = graph.push(Node {
op: Op::AddScalar(1.0),
inputs: vec![neg],
shape: code_shape.clone(),
dtype: f32,
});
let ind = graph.push(Node {
op: Op::Relu,
inputs: vec![one_minus],
shape: code_shape.clone(),
dtype: f32,
});
let term = graph.push(Node {
op: Op::MulScalar(v as f64),
inputs: vec![ind],
shape: code_shape.clone(),
dtype: f32,
});
nf4val = Some(match nf4val {
None => term,
Some(prev) => graph.push(Node {
op: Op::Add,
inputs: vec![prev, term],
shape: code_shape.clone(),
dtype: f32,
}),
});
}
let nf4val = nf4val.unwrap_or(codes);
let n_blocks = k / block_size;
let abs3 = graph.push(Node {
op: Op::Unsqueeze { dim: 2 },
inputs: vec![abs_id],
shape: Shape::from_dims(&[n_out, n_blocks, 1]),
dtype: f32,
});
let abs_b = graph.push(Node {
op: Op::BroadcastTo(Shape::from_dims(&[n_out, n_blocks, block_size])),
inputs: vec![abs3],
shape: Shape::from_dims(&[n_out, n_blocks, block_size]),
dtype: f32,
});
let scale_full = graph.push(Node {
op: Op::Reshape(code_shape.clone()),
inputs: vec![abs_b],
shape: code_shape.clone(),
dtype: f32,
});
let dequant = graph.push(Node {
op: Op::Mul,
inputs: vec![nf4val, scale_full],
shape: code_shape,
dtype: f32,
});
let dequant_typed = if dtype == f32 {
dequant
} else {
graph.push(Node {
op: Op::Cast(dtype),
inputs: vec![dequant],
shape: Shape::from_dims(&[n_out, k]),
dtype,
})
};
let dequant_t = graph.push(Node {
op: Op::Transpose,
inputs: vec![dequant_typed],
shape: Shape::from_dims(&[k, n_out]),
dtype,
});
let a2 = graph.push(Node {
op: Op::Reshape(Shape::from_dims(&[m_prime, k])),
inputs: vec![a_id],
shape: Shape::from_dims(&[m_prime, k]),
dtype,
});
let out2 = graph.push(Node {
op: Op::MatMul,
inputs: vec![a2, dequant_t],
shape: Shape::from_dims(&[m_prime, n_out]),
dtype,
});
let mut out_dims: Vec<usize> = a_dims[..a_dims.len() - 1].to_vec();
out_dims.push(n_out);
graph.push(Node {
op: Op::Reshape(Shape::from_dims(&out_dims)),
inputs: vec![out2],
shape: Shape::from_dims(&out_dims),
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);
}
}
fn fused_node(
g: &mut Graph,
leading: &[usize],
k: usize,
n: usize,
block_size: usize,
work: DType,
) -> NodeId {
let mut a_dims = leading.to_vec();
a_dims.push(k);
let act = g.push(Node {
op: Op::Const,
inputs: vec![],
shape: Shape::from_dims(&a_dims),
dtype: work,
});
let w = g.push(Node {
op: Op::Const,
inputs: vec![],
shape: Shape::from_dims(&[n, k / 2]),
dtype: DType::U8,
});
let abs = g.push(Node {
op: Op::Const,
inputs: vec![],
shape: Shape::from_dims(&[n, k / block_size]),
dtype: DType::F32,
});
let mut out_dims = leading.to_vec();
out_dims.push(n);
g.push(Node {
op: Op::Fused(
FusedOps::NF4_MATMUL,
FusedOpParams::Nf4Matmul { block_size },
),
inputs: vec![act, w, abs],
shape: Shape::from_dims(&out_dims),
dtype: work,
})
}
#[test]
fn nf4_matmul_recipe_decompose_is_polymorphic_and_matches_frozen_legacy() {
for block_size in [2usize, 4] {
for work in [DType::F32, DType::F16] {
for leading in [vec![1usize], vec![2usize, 3]] {
let k = 8;
let n = 5;
let params = FusedOpParams::Nf4Matmul { block_size };
let mut g = Graph::new();
let fused = fused_node(&mut g, &leading, k, n, block_size, work);
let out_sh = g.node(fused).shape.clone();
let new_root = decompose(&mut g, fused, ¶ms);
assert_ne!(
new_root, fused,
"recipe decompose fires (block_size={block_size}, work={work:?}, leading={leading:?})"
);
assert_eq!(
g.node(new_root).shape,
out_sh,
"output shape matches shape_rule (block_size={block_size}, leading={leading:?})"
);
assert_eq!(
g.node(new_root).dtype,
work,
"output dtype matches activations (work={work:?})"
);
let legacy_root = frozen_legacy_nf4_matmul_decompose(&mut g, fused, ¶ms);
assert_structural_eq(&g, new_root, legacy_root);
}
}
}
}
#[test]
fn nf4_matmul_recipe_wrong_params_is_a_fixpoint_not_a_crash() {
let mut g = Graph::new();
let fused = fused_node(&mut g, &[2, 3], 8, 5, 2, DType::F32);
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");
}
#[test]
fn nf4_lut_byte_identical_to_cpu_backend() {
assert_eq!(
NF4_LUT,
fuel_cpu_backend::byte_kernels::NF4_LUT,
"fuel-graph NF4_LUT diverged from fuel_cpu_backend::byte_kernels::NF4_LUT"
);
}
}