1use rlx_ir::*;
22use std::collections::HashMap;
23
24#[derive(Debug, Clone, Copy)]
26pub struct ScaledQuantConfig {
27 pub lhs_format: ScaledFormat,
28 pub rhs_format: ScaledFormat,
29 pub scale_layout: ScaleLayout,
30}
31
32impl ScaledQuantConfig {
33 pub fn fp8_e4m3() -> Self {
35 Self {
36 lhs_format: ScaledFormat::F8E4M3,
37 rhs_format: ScaledFormat::F8E4M3,
38 scale_layout: ScaleLayout::PerTensor,
39 }
40 }
41 pub fn mxfp8_e4m3() -> Self {
43 Self {
44 lhs_format: ScaledFormat::F8E4M3,
45 rhs_format: ScaledFormat::F8E4M3,
46 scale_layout: ScaleLayout::mx(),
47 }
48 }
49}
50
51pub fn insert_scaled_matmul(graph: Graph, cfg: ScaledQuantConfig) -> Graph {
54 let mut out = Graph::new(&graph.name);
55 let mut id_map: HashMap<NodeId, NodeId> = HashMap::new();
56 let mut qcache: HashMap<(NodeId, ScaledFormat), (NodeId, NodeId)> = HashMap::new();
58 let mut tcache: HashMap<NodeId, NodeId> = HashMap::new();
60
61 for node in graph.nodes() {
62 let new_inputs: Vec<NodeId> = node.inputs.iter().map(|i| id_map[i]).collect();
63
64 let new_id = match &node.op {
65 Op::MatMul if node.shape.rank() == 2 => {
66 let lhs = new_inputs[0];
67 let rhs = new_inputs[1];
68 let lhs_shape = out.node(lhs).shape.clone();
69 let rhs_shape = out.node(rhs).shape.clone();
70 let m = lhs_shape.dim(0).unwrap_static();
71 let k = lhs_shape.dim(1).unwrap_static();
72 let n = rhs_shape.dim(1).unwrap_static();
73
74 let rhs_t = *tcache.entry(rhs).or_insert_with(|| {
76 out.add_node(
77 Op::Transpose { perm: vec![1, 0] },
78 vec![rhs],
79 Shape::new(&[n, k], rhs_shape.dtype()),
80 )
81 });
82
83 let (lq, ls) = quantize_operand(
84 &mut out,
85 &mut qcache,
86 lhs,
87 m,
88 k,
89 cfg.lhs_format,
90 cfg.scale_layout,
91 );
92 let (rq, rs) = quantize_operand(
93 &mut out,
94 &mut qcache,
95 rhs_t,
96 n,
97 k,
98 cfg.rhs_format,
99 cfg.scale_layout,
100 );
101
102 out.add_node(
103 Op::ScaledMatMul {
104 lhs_format: cfg.lhs_format,
105 rhs_format: cfg.rhs_format,
106 scale_layout: cfg.scale_layout,
107 has_bias: false,
108 },
109 vec![lq, rq, ls, rs],
110 Shape::new(&[m, n], DType::F32),
111 )
112 }
113 _ => out.add_node(node.op.clone(), new_inputs, node.shape.clone()),
114 };
115 id_map.insert(node.id, new_id);
116 }
117
118 let new_outputs: Vec<NodeId> = graph.outputs.iter().map(|&id| id_map[&id]).collect();
119 out.set_outputs(new_outputs);
120 out
121}
122
123fn quantize_operand(
127 out: &mut Graph,
128 qcache: &mut HashMap<(NodeId, ScaledFormat), (NodeId, NodeId)>,
129 operand: NodeId,
130 rows: usize,
131 cols: usize,
132 fmt: ScaledFormat,
133 layout: ScaleLayout,
134) -> (NodeId, NodeId) {
135 if let Some(&hit) = qcache.get(&(operand, fmt)) {
136 return hit;
137 }
138 let scale_shape = match layout {
139 ScaleLayout::PerTensor => Shape::new(&[1], layout.scale_dtype()),
140 _ => Shape::new(
141 &[rows, cols.div_ceil(layout.block() as usize)],
142 layout.scale_dtype(),
143 ),
144 };
145 let scale = out.add_node(
146 Op::ScaledQuantScale {
147 format: fmt,
148 scale_layout: layout,
149 },
150 vec![operand],
151 scale_shape,
152 );
153 let codes = out.add_node(
154 Op::ScaledQuantize {
155 format: fmt,
156 scale_layout: layout,
157 },
158 vec![operand, scale],
159 Shape::new(&[rows, cols], DType::U8),
160 );
161 qcache.insert((operand, fmt), (codes, scale));
162 (codes, scale)
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 fn count_kind(g: &Graph, k: OpKind) -> usize {
170 g.nodes().iter().filter(|n| n.op.kind() == k).count()
171 }
172
173 #[test]
174 fn rewrites_matmul_to_scaled() {
175 let mut g = Graph::new("mm");
176 let x = g.input("x", Shape::new(&[2, 4], DType::F32));
177 let w = g.param("w", Shape::new(&[4, 3], DType::F32));
178 let mm = g.matmul(x, w, Shape::new(&[2, 3], DType::F32));
179 g.set_outputs(vec![mm]);
180
181 let out = insert_scaled_matmul(g, ScaledQuantConfig::fp8_e4m3());
182
183 assert_eq!(count_kind(&out, OpKind::MatMul), 0, "plain matmul remains");
184 assert_eq!(count_kind(&out, OpKind::ScaledMatMul), 1);
185 assert_eq!(count_kind(&out, OpKind::ScaledQuantize), 2);
187 assert_eq!(count_kind(&out, OpKind::ScaledQuantScale), 2);
188 assert_eq!(count_kind(&out, OpKind::Transpose), 1);
190
191 let out_node = out.node(out.outputs[0]);
193 assert!(matches!(out_node.op, Op::ScaledMatMul { .. }));
194 assert_eq!(out_node.shape.dim(0).unwrap_static(), 2);
195 assert_eq!(out_node.shape.dim(1).unwrap_static(), 3);
196 assert_eq!(out_node.shape.dtype(), DType::F32);
197 }
198
199 #[test]
200 fn quantize_once_for_shared_activation() {
201 let mut g = Graph::new("shared");
203 let x = g.input("x", Shape::new(&[2, 4], DType::F32));
204 let w1 = g.param("w1", Shape::new(&[4, 3], DType::F32));
205 let w2 = g.param("w2", Shape::new(&[4, 5], DType::F32));
206 let mm1 = g.matmul(x, w1, Shape::new(&[2, 3], DType::F32));
207 let mm2 = g.matmul(x, w2, Shape::new(&[2, 5], DType::F32));
208 g.set_outputs(vec![mm1, mm2]);
209
210 let out = insert_scaled_matmul(g, ScaledQuantConfig::fp8_e4m3());
211
212 assert_eq!(count_kind(&out, OpKind::ScaledMatMul), 2);
213 assert_eq!(count_kind(&out, OpKind::ScaledQuantize), 3);
215 assert_eq!(count_kind(&out, OpKind::ScaledQuantScale), 3);
216 }
217
218 #[test]
219 fn coexists_with_f16_amp() {
220 use crate::precision::{AutoMixedPrecision, PrecisionPolicy};
225 use rlx_fusion::pass::Pass;
226
227 let mut g = Graph::new("mix");
228 let x = g.input("x", Shape::new(&[2, 4], DType::F32));
229 let w = g.param("w", Shape::new(&[4, 3], DType::F32));
230 let mm = g.matmul(x, w, Shape::new(&[2, 3], DType::F32));
231 let relu = g.activation(
232 rlx_ir::op::Activation::Relu,
233 mm,
234 Shape::new(&[2, 3], DType::F32),
235 );
236 g.set_outputs(vec![relu]);
237
238 let g = insert_scaled_matmul(g, ScaledQuantConfig::fp8_e4m3());
239 let g = AutoMixedPrecision::new(PrecisionPolicy::AutoMixed).run(g);
240
241 let sm = g
242 .nodes()
243 .iter()
244 .find(|n| matches!(n.op, Op::ScaledMatMul { .. }))
245 .expect("ScaledMatMul survived the f16 AMP pass");
246 assert_eq!(
247 sm.shape.dtype(),
248 DType::F32,
249 "ScaledMatMul output stayed f32"
250 );
251 for &i in &sm.inputs {
252 let d = g.node(i).shape.dtype();
253 assert!(
254 d == DType::U8 || d == DType::F32,
255 "ScaledMatMul input wrongly relabeled to {d}"
256 );
257 }
258 let errs = rlx_ir::verify::verify(&g);
260 assert!(errs.is_empty(), "post-AMP graph verifies: {errs:?}");
261 }
262
263 #[test]
264 fn leaves_batched_matmul_alone() {
265 let mut g = Graph::new("batched");
266 let a = g.input("a", Shape::new(&[8, 2, 4], DType::F32));
267 let b = g.param("b", Shape::new(&[8, 4, 3], DType::F32));
268 let mm = g.matmul(a, b, Shape::new(&[8, 2, 3], DType::F32));
269 g.set_outputs(vec![mm]);
270
271 let out = insert_scaled_matmul(g, ScaledQuantConfig::fp8_e4m3());
272 assert_eq!(
273 count_kind(&out, OpKind::MatMul),
274 1,
275 "batched matmul untouched"
276 );
277 assert_eq!(count_kind(&out, OpKind::ScaledMatMul), 0);
278 }
279}