use crate::registry::{BackwardKind, FusedOpEntry, FusedOpFamily, FusedOpParams, FusedOps};
use crate::{Graph, Node, NodeId, Op};
use fuel_ir::{DType, Shape};
pub const AWQ_UNPACK_ORDER: [u32; 8] = [0, 2, 4, 6, 1, 3, 5, 7];
pub fn entry() -> FusedOpEntry {
FusedOpEntry {
destructive_input: None,
id: FusedOps::AWQ_MATMUL,
name: "AwqMatmul",
family: FusedOpFamily::Quantized,
pattern: crate::registry::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(),
4,
"AwqMatmul takes 4 inputs (activations, qweight, qzeros, scales)",
);
let a_dims = input_shapes[0].dims();
let w_dims = input_shapes[1].dims();
debug_assert!(
a_dims.len() >= 2,
"AwqMatmul: activations must be rank >= 2, got {a_dims:?}"
);
debug_assert_eq!(
w_dims.len(),
2,
"AwqMatmul: qweight must be rank 2 [N, K/8], 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(),
4,
"AwqMatmul takes 4 inputs (activations, qweight, qzeros, scales)",
);
input_dtypes[0]
}
pub fn canonical_pattern(_graph: &Graph, _root: NodeId) -> Option<crate::registry::PatternMatch> {
None
}
fn push(graph: &mut Graph, op: Op, inputs: Vec<NodeId>, shape: Shape, dtype: DType) -> NodeId {
graph.push(Node {
op,
inputs,
shape,
dtype,
})
}
fn unpack_u32_nibbles(graph: &mut Graph, packed: NodeId, out_shape: &Shape) -> [NodeId; 8] {
let f64_ = DType::F64;
let w = push(graph, Op::Cast(f64_), vec![packed], out_shape.clone(), f64_);
let mut positions: Vec<NodeId> = Vec::with_capacity(8);
for p in 0..8u32 {
let divisor = 16f64.powi(p as i32);
let shifted = if p == 0 {
w
} else {
push(
graph,
Op::MulScalar(1.0 / divisor),
vec![w],
out_shape.clone(),
f64_,
)
};
let shifted = push(graph, Op::Floor, vec![shifted], out_shape.clone(), f64_);
let shifted_div16 = push(
graph,
Op::MulScalar(1.0 / 16.0),
vec![shifted],
out_shape.clone(),
f64_,
);
let hi = push(
graph,
Op::Floor,
vec![shifted_div16],
out_shape.clone(),
f64_,
);
let hi16 = push(
graph,
Op::MulScalar(16.0),
vec![hi],
out_shape.clone(),
f64_,
);
let nibble = push(graph, Op::Sub, vec![shifted, hi16], out_shape.clone(), f64_);
positions.push(nibble);
}
positions.try_into().unwrap_or_else(|_| unreachable!())
}
fn interleave_and_cast_to_f32(
graph: &mut Graph,
positions: &[NodeId; 8],
half_shape: &Shape,
logical_shape: &Shape,
) -> NodeId {
let mut concat_list = [0usize; 8];
for (p, &slot) in AWQ_UNPACK_ORDER.iter().enumerate() {
concat_list[slot as usize] = p;
}
let half_dims = half_shape.dims();
let mut unsq_shape_dims = half_dims.to_vec();
unsq_shape_dims.push(1);
let unsq_shape = Shape::from_dims(&unsq_shape_dims);
let unsqueezed: Vec<NodeId> = concat_list
.iter()
.map(|&p| {
push(
graph,
Op::Unsqueeze {
dim: half_dims.len(),
},
vec![positions[p]],
unsq_shape.clone(),
DType::F64,
)
})
.collect();
let mut stacked_dims = half_dims.to_vec();
stacked_dims.push(8);
let stacked_shape = Shape::from_dims(&stacked_dims);
let stacked = push(
graph,
Op::Concat {
dim: half_dims.len(),
},
unsqueezed,
stacked_shape,
DType::F64,
);
let codes_f64 = push(
graph,
Op::Reshape(logical_shape.clone()),
vec![stacked],
logical_shape.clone(),
DType::F64,
);
push(
graph,
Op::Cast(DType::F32),
vec![codes_f64],
logical_shape.clone(),
DType::F32,
)
}
pub fn decompose(graph: &mut Graph, id: NodeId, params: &FusedOpParams) -> NodeId {
let (group_size, _split_k_iters) = match params {
FusedOpParams::AwqMatmul {
group_size,
split_k_iters,
} => (*group_size, *split_k_iters),
_ => return id,
};
let (a_shape, w_shape, z_shape, s_shape, dtype) = {
let n = graph.node(id);
if n.inputs.len() != 4 {
return id;
}
let a_shape = graph.node(n.inputs[0]).shape.clone();
let w_shape = graph.node(n.inputs[1]).shape.clone();
let z_shape = graph.node(n.inputs[2]).shape.clone();
let s_shape = graph.node(n.inputs[3]).shape.clone();
(a_shape, w_shape, z_shape, s_shape, n.dtype)
};
let (a_id, w_id, z_id, s_id) = {
let n = graph.node(id);
(n.inputs[0], n.inputs[1], n.inputs[2], n.inputs[3])
};
let w_dims = w_shape.dims();
let z_dims = z_shape.dims();
let s_dims = s_shape.dims();
let a_dims = a_shape.dims();
if w_dims.len() != 2 || z_dims.len() != 2 || s_dims.len() != 2 || a_dims.len() < 2 {
return id;
}
let n_out = w_dims[0];
let k_over_8 = w_dims[1];
let k = k_over_8 * 8;
if group_size == 0 || k % group_size != 0 || n_out % 8 != 0 {
return id;
}
let n_groups = k / group_size;
if z_dims != [n_groups, n_out / 8] || s_dims != [n_groups, n_out] {
return id;
}
let k_from_act = a_dims[a_dims.len() - 1];
if k_from_act != k {
return id;
}
let half_w_shape = Shape::from_dims(&[n_out, k_over_8]);
let w_positions = unpack_u32_nibbles(graph, w_id, &half_w_shape);
let wcodes = interleave_and_cast_to_f32(
graph,
&w_positions,
&half_w_shape,
&Shape::from_dims(&[n_out, k]),
);
let half_z_shape = Shape::from_dims(&[n_groups, n_out / 8]);
let z_positions = unpack_u32_nibbles(graph, z_id, &half_z_shape);
let zcodes = interleave_and_cast_to_f32(
graph,
&z_positions,
&half_z_shape,
&Shape::from_dims(&[n_groups, n_out]),
);
let wcodes_t = push(
graph,
Op::Transpose,
vec![wcodes],
Shape::from_dims(&[k, n_out]),
DType::F32,
);
let expand = |graph: &mut Graph, x: NodeId, dtype_x: DType| -> NodeId {
let unsq = push(
graph,
Op::Unsqueeze { dim: 1 },
vec![x],
Shape::from_dims(&[n_groups, 1, n_out]),
dtype_x,
);
let bcast = push(
graph,
Op::BroadcastTo(Shape::from_dims(&[n_groups, group_size, n_out])),
vec![unsq],
Shape::from_dims(&[n_groups, group_size, n_out]),
dtype_x,
);
push(
graph,
Op::Reshape(Shape::from_dims(&[k, n_out])),
vec![bcast],
Shape::from_dims(&[k, n_out]),
dtype_x,
)
};
let zcodes_full = expand(graph, zcodes, DType::F32);
let scales_full = expand(graph, s_id, DType::F32);
let centered = push(
graph,
Op::Sub,
vec![wcodes_t, zcodes_full],
Shape::from_dims(&[k, n_out]),
DType::F32,
);
let dequant = push(
graph,
Op::Mul,
vec![centered, scales_full],
Shape::from_dims(&[k, n_out]),
DType::F32,
);
let dequant_typed = if dtype == DType::F32 {
dequant
} else {
push(
graph,
Op::Cast(dtype),
vec![dequant],
Shape::from_dims(&[k, n_out]),
dtype,
)
};
let m_prime: usize = a_dims[..a_dims.len() - 1].iter().product();
let a2 = push(
graph,
Op::Reshape(Shape::from_dims(&[m_prime, k])),
vec![a_id],
Shape::from_dims(&[m_prime, k]),
dtype,
);
let out2 = push(
graph,
Op::MatMul,
vec![a2, dequant_typed],
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);
push(
graph,
Op::Reshape(Shape::from_dims(&out_dims)),
vec![out2],
Shape::from_dims(&out_dims),
dtype,
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Node, Op};
fn pack_u32_nibbles(codes: [u32; 8]) -> u32 {
let mut word = 0u32;
for (p, &slot) in AWQ_UNPACK_ORDER.iter().enumerate() {
word |= (codes[slot as usize] & 0xF) << (4 * p);
}
word
}
fn const_node(g: &mut Graph, shape: Shape, dtype: DType) -> NodeId {
g.push(Node {
op: Op::Const,
inputs: vec![],
shape,
dtype,
})
}
fn fused_node_1group(g: &mut Graph, m: usize, work: DType) -> (NodeId, NodeId, NodeId, NodeId) {
let k = 8usize;
let n = 8usize;
let group_size = k;
let act = const_node(g, Shape::from_dims(&[m, k]), work);
let qweight = const_node(g, Shape::from_dims(&[n, k / 8]), DType::U32);
let qzeros = const_node(g, Shape::from_dims(&[1, n / 8]), DType::U32);
let scales = const_node(g, Shape::from_dims(&[1, n]), DType::F32);
let fused = g.push(Node {
op: Op::Fused(
FusedOps::AWQ_MATMUL,
FusedOpParams::AwqMatmul {
group_size,
split_k_iters: 8,
},
),
inputs: vec![act, qweight, qzeros, scales],
shape: Shape::from_dims(&[m, n]),
dtype: work,
});
(fused, qweight, qzeros, scales)
}
#[test]
fn awq_matmul_wrong_params_is_a_fixpoint_not_a_crash() {
let mut g = Graph::new();
let (fused, _, _, _) = fused_node_1group(&mut g, 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 awq_matmul_decompose_fires_and_matches_declared_shape_dtype() {
let mut g = Graph::new();
let (fused, ..) = fused_node_1group(&mut g, 2, DType::F32);
let out_sh = g.node(fused).shape.clone();
let out_dt = g.node(fused).dtype;
let params = FusedOpParams::AwqMatmul {
group_size: 8,
split_k_iters: 8,
};
let new_root = decompose(&mut g, fused, ¶ms);
assert_ne!(new_root, fused, "decompose must fire, not fixpoint");
assert_eq!(g.node(new_root).shape, out_sh);
assert_eq!(g.node(new_root).dtype, out_dt);
}
#[test]
fn awq_matmul_decompose_dequant_matches_hand_computed_example() {
let mut g = Graph::new();
let m = 1usize;
let (fused, qweight, qzeros, scales) = fused_node_1group(&mut g, m, DType::F32);
let w_codes_per_row: [u32; 8] = [1, 2, 3, 4, 5, 6, 7, 8];
let packed_w_word = pack_u32_nibbles(w_codes_per_row);
let w_bytes: Vec<u32> = vec![packed_w_word; 8];
let z_codes: [u32; 8] = [0, 1, 2, 3, 4, 5, 6, 7];
let packed_z_word = pack_u32_nibbles(z_codes);
let z_bytes: Vec<u32> = vec![packed_z_word];
let scale_val = 2.0f32;
let scale_bytes = [scale_val; 8];
for (p, &expected_slot_value) in w_codes_per_row.iter().enumerate() {
let slot = AWQ_UNPACK_ORDER
.iter()
.position(|&s| s == p as u32)
.unwrap();
let shift = 4 * slot;
let extracted = (packed_w_word >> shift) & 0xF;
assert_eq!(
extracted, expected_slot_value,
"round-trip check: logical col {p} should extract to {expected_slot_value}"
);
}
let mut expected = vec![vec![0f32; 8]; 8];
for (k, row) in expected.iter_mut().enumerate() {
for (n, cell) in row.iter_mut().enumerate() {
*cell = (w_codes_per_row[k] as f32 - z_codes[n] as f32) * scale_val;
}
}
let params = FusedOpParams::AwqMatmul {
group_size: 8,
split_k_iters: 8,
};
let new_root = decompose(&mut g, fused, ¶ms);
assert_ne!(new_root, fused);
let act_input = {
let n = g.node(fused);
n.inputs[0]
};
let bindings: std::collections::HashMap<NodeId, Vec<f64>> = [
(act_input, vec![1.0f64; m * 8]),
(
qweight,
w_bytes.iter().map(|&x| x as f64).collect::<Vec<_>>(),
),
(
qzeros,
z_bytes.iter().map(|&x| x as f64).collect::<Vec<_>>(),
),
(
scales,
scale_bytes.iter().map(|&x| x as f64).collect::<Vec<_>>(),
),
]
.into_iter()
.collect();
let result = eval_node(&g, new_root, &bindings);
let expected_out: Vec<f64> = (0..8)
.map(|n| expected.iter().map(|row| row[n] as f64).sum())
.collect();
for (i, (&got, &want)) in result.iter().zip(expected_out.iter()).enumerate() {
assert!(
(got - want).abs() < 1e-3,
"output[{i}]: got {got}, want {want}"
);
}
}
#[test]
fn awq_matmul_sabotage_skipping_zero_point_changes_the_result() {
let mut g = Graph::new();
let m = 1usize;
let (fused, qweight, qzeros, scales) = fused_node_1group(&mut g, m, DType::F32);
let w_codes_per_row: [u32; 8] = [1, 2, 3, 4, 5, 6, 7, 8];
let packed_w_word = pack_u32_nibbles(w_codes_per_row);
let w_bytes: Vec<u32> = vec![packed_w_word; 8];
let z_codes: [u32; 8] = [0, 1, 2, 3, 4, 5, 6, 7];
let packed_z_word = pack_u32_nibbles(z_codes);
let z_bytes: Vec<u32> = vec![packed_z_word];
let scale_val = 2.0f32;
let scale_bytes = [scale_val; 8];
let params = FusedOpParams::AwqMatmul {
group_size: 8,
split_k_iters: 8,
};
let new_root = decompose(&mut g, fused, ¶ms);
let act_input = {
let n = g.node(fused);
n.inputs[0]
};
let bindings: std::collections::HashMap<NodeId, Vec<f64>> = [
(act_input, vec![1.0f64; m * 8]),
(
qweight,
w_bytes.iter().map(|&x| x as f64).collect::<Vec<_>>(),
),
(
qzeros,
z_bytes.iter().map(|&x| x as f64).collect::<Vec<_>>(),
),
(
scales,
scale_bytes.iter().map(|&x| x as f64).collect::<Vec<_>>(),
),
]
.into_iter()
.collect();
let result = eval_node(&g, new_root, &bindings);
let all_equal = result.windows(2).all(|w| (w[0] - w[1]).abs() < 1e-6);
assert!(
!all_equal,
"the correct decompose's output must vary across n (zero-point \
subtracted per-channel) -- if this fires, the zero-point term \
silently dropped out somewhere"
);
}
fn eval_node(
g: &Graph,
id: NodeId,
bindings: &std::collections::HashMap<NodeId, Vec<f64>>,
) -> Vec<f64> {
if let Some(v) = bindings.get(&id) {
return v.clone();
}
let n = g.node(id);
let elem_count = n.shape.dims().iter().product::<usize>().max(1);
match &n.op {
Op::Cast(_) => eval_node(g, n.inputs[0], bindings),
Op::Floor => eval_node(g, n.inputs[0], bindings)
.into_iter()
.map(f64::floor)
.collect(),
Op::MulScalar(s) => eval_node(g, n.inputs[0], bindings)
.into_iter()
.map(|x| x * s)
.collect(),
Op::AddScalar(s) => eval_node(g, n.inputs[0], bindings)
.into_iter()
.map(|x| x + s)
.collect(),
Op::Sub => {
let a = eval_node(g, n.inputs[0], bindings);
let b = eval_node(g, n.inputs[1], bindings);
a.iter().zip(b.iter()).map(|(x, y)| x - y).collect()
}
Op::Mul => {
let a = eval_node(g, n.inputs[0], bindings);
let b = eval_node(g, n.inputs[1], bindings);
a.iter().zip(b.iter()).map(|(x, y)| x * y).collect()
}
Op::Unsqueeze { .. } | Op::Reshape(_) => eval_node(g, n.inputs[0], bindings),
Op::Concat { .. } => {
let arrs: Vec<Vec<f64>> = n
.inputs
.iter()
.map(|&i| eval_node(g, i, bindings))
.collect();
let k = arrs.len();
let outer = arrs.first().map(|a| a.len()).unwrap_or(0);
let mut out = vec![0f64; outer * k];
for (o, slot) in out.chunks_mut(k).enumerate() {
for (j, arr) in arrs.iter().enumerate() {
slot[j] = arr[o];
}
}
out
}
Op::BroadcastTo(target) => {
let src = eval_node(g, n.inputs[0], bindings);
let src_shape = g.node(n.inputs[0]).shape.dims().to_vec();
let target_dims = target.dims();
assert_eq!(src_shape.len(), 3);
assert_eq!(target_dims.len(), 3);
let (g0, _one, n0) = (src_shape[0], src_shape[1], src_shape[2]);
let gsize = target_dims[1];
let mut out = Vec::with_capacity(g0 * gsize * n0);
for gi in 0..g0 {
for _ in 0..gsize {
out.extend_from_slice(&src[gi * n0..(gi + 1) * n0]);
}
}
out
}
Op::Transpose => {
let src = eval_node(g, n.inputs[0], bindings);
let src_shape = g.node(n.inputs[0]).shape.dims().to_vec();
assert_eq!(src_shape.len(), 2);
let (rows, cols) = (src_shape[0], src_shape[1]);
let mut out = vec![0f64; rows * cols];
for r in 0..rows {
for c in 0..cols {
out[c * rows + r] = src[r * cols + c];
}
}
out
}
Op::MatMul => {
let a = eval_node(g, n.inputs[0], bindings);
let b = eval_node(g, n.inputs[1], bindings);
let a_shape = g.node(n.inputs[0]).shape.dims().to_vec();
let b_shape = g.node(n.inputs[1]).shape.dims().to_vec();
let (m, k) = (a_shape[0], a_shape[1]);
let (_k2, nn) = (b_shape[0], b_shape[1]);
let mut out = vec![0f64; m * nn];
for i in 0..m {
for j in 0..nn {
let mut acc = 0f64;
for kk in 0..k {
acc += a[i * k + kk] * b[kk * nn + j];
}
out[i * nn + j] = acc;
}
}
out
}
other => panic!("eval_node: unhandled op {other:?} (elem_count={elem_count})"),
}
}
}