1use rlx_fusion::pass::Pass;
18use rlx_ir::*;
19use std::collections::HashMap;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub enum Precision {
25 F32,
26 F16,
27 BF16,
28}
29
30impl Precision {
31 pub fn dtype(self) -> DType {
32 match self {
33 Precision::F32 => DType::F32,
34 Precision::F16 => DType::F16,
35 Precision::BF16 => DType::BF16,
36 }
37 }
38}
39
40#[derive(Debug, Clone, Copy, PartialEq)]
52pub struct CastConfig {
53 pub out_dtype: DType,
55 pub sf_block: Option<(usize, usize)>,
58 pub round_sf: bool,
60}
61
62impl CastConfig {
63 pub const fn plain(out_dtype: DType) -> Self {
65 Self {
66 out_dtype,
67 sf_block: None,
68 round_sf: false,
69 }
70 }
71 pub fn is_noop(&self, in_dtype: DType) -> bool {
73 self.out_dtype == in_dtype && self.sf_block.is_none()
74 }
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
79pub enum OpKind {
80 Compute,
83 Reduction,
85 Elementwise,
87 DataMovement,
89 Boundary,
91}
92
93fn op_kind(op: &Op) -> OpKind {
94 match op {
95 Op::MatMul
96 | Op::FusedMatMulBiasAct { .. }
97 | Op::FusedConvBiasAct { .. }
98 | Op::Conv { .. }
99 | Op::Im2Col { .. }
100 | Op::DotGeneral { .. }
101 | Op::DenseSolve
102 | Op::BatchedDenseSolve
103 | Op::Attention { .. }
104 | Op::FusedTransformerLayer { .. }
105 | Op::GroupedMatMul
106 | Op::DequantGroupedMatMul { .. }
107 | Op::DequantMoEWeights { .. }
108 | Op::LoraMatMul { .. }
109 | Op::DequantMatMul { .. }
110 | Op::SynthMatMul { .. }
111 | Op::ScaledMatMul { .. }
112 | Op::QMatMul { .. }
113 | Op::QConv2d { .. }
114 | Op::Conv2dBackwardInput { .. }
115 | Op::Conv2dBackwardWeight { .. }
116 | Op::AttentionBackward { .. } => OpKind::Compute,
117 Op::LayerNorm { .. }
118 | Op::RmsNorm { .. }
119 | Op::Softmax { .. }
120 | Op::FusedResidualLN { .. }
121 | Op::FusedResidualRmsNorm { .. }
122 | Op::Reduce { .. }
123 | Op::Cumsum { .. }
124 | Op::Sample { .. }
125 | Op::SelectiveScan { .. }
126 | Op::GatedDeltaNet { .. }
127 | Op::Lstm { .. }
128 | Op::Gru { .. }
129 | Op::Rnn { .. }
130 | Op::Mamba2 { .. }
131 | Op::SoftmaxCrossEntropy
132 | Op::SoftmaxCrossEntropyWithLogits
133 | Op::SoftmaxCrossEntropyBackward
134 | Op::LayerNormBackwardInput { .. }
135 | Op::LayerNormBackwardGamma { .. }
136 | Op::GroupNorm { .. }
137 | Op::AdaLayerNorm { .. }
139 | Op::AdaLayerNormBackward { .. } => OpKind::Reduction,
140 Op::Activation(_)
141 | Op::Binary(_)
142 | Op::FusedSwiGLU { .. }
143 | Op::Compare(_)
144 | Op::Where
145 | Op::ElementwiseRegion { .. }
146 | Op::Quantize { .. }
147 | Op::ScaledQuantize { .. }
148 | Op::ScaledQuantScale { .. }
149 | Op::ScaledDequantize { .. }
150 | Op::Dequantize { .. }
151 | Op::FakeQuantize { .. }
152 | Op::FakeQuantizeBackward { .. }
153 | Op::FakeQuantizeLSQ { .. }
154 | Op::FakeQuantizeLSQBackwardX { .. }
155 | Op::FakeQuantizeLSQBackwardScale { .. }
156 | Op::ReluBackward
157 | Op::ActivationBackward { .. }
158 | Op::ComplexNormSq
159 | Op::ComplexNormSqBackward
160 | Op::Conjugate
161 | Op::GatedResidual
163 | Op::GatedResidualBackward => OpKind::Elementwise,
164 Op::Gather { .. }
165 | Op::Narrow { .. }
166 | Op::Reshape { .. }
167 | Op::Transpose { .. }
168 | Op::Concat { .. }
169 | Op::Expand { .. }
170 | Op::Cast { .. }
171 | Op::Rope { .. }
172 | Op::Pool { .. }
173 | Op::FusedAttentionBlock { .. }
174 | Op::TopK { .. }
175 | Op::ScatterAdd
176 | Op::ScatterNd { .. }
177 | Op::ScatterElements { .. }
178 | Op::GatherNd { .. }
179 | Op::GatherElements { .. }
180 | Op::MaxPool2dBackward { .. }
181 | Op::ResizeNearest2x
182 | Op::AxialRope2d { .. } => OpKind::DataMovement,
183 Op::Input { .. } | Op::Param { .. } | Op::Constant { .. } => OpKind::Boundary,
184 Op::If { .. } | Op::While { .. } => OpKind::DataMovement,
187 Op::Custom { .. } => OpKind::Compute,
191 Op::Scan { .. } => OpKind::Compute,
192 Op::ScanBackward { .. } => OpKind::Compute,
193 Op::ScanBackwardXs { .. } => OpKind::Compute,
194 Op::CustomFn { .. } => OpKind::Compute,
195 Op::Fft { .. } => OpKind::Compute,
196 Op::FftButterflyStage { .. } => OpKind::Compute,
197 Op::LogMel => OpKind::Compute,
198 Op::LogMelBackward => OpKind::Compute,
199 Op::BiMap
202 | Op::ReEig { .. }
203 | Op::LogEig { .. }
204 | Op::SpdBatchNorm { .. }
205 | Op::SpdKarcherMean { .. }
206 | Op::SpdKarcherMeanWeighted { .. }
207 | Op::SpdLogMap
208 | Op::SpdExpMap
209 | Op::SpdParallelTransport
210 | Op::SpdMatrixFnBatch { .. }
211 | Op::ReEigBackward { .. }
212 | Op::LogEigBackward { .. }
213 | Op::SpdBatchNormBackwardX { .. }
214 | Op::SpdBatchNormBackwardG { .. }
215 | Op::SpdLogMapBackward
216 | Op::SpdExpMapBackward
217 | Op::SpdParallelTransportBackward
218 | Op::SpdMatrixFnBatchBackward { .. }
219 | Op::Eigh
220 | Op::EighBackward
221 | Op::EighBatch
222 | Op::EighBatchBackward => OpKind::Compute,
223 _ => OpKind::Compute,
224 }
225}
226
227#[derive(Debug, Clone, Default)]
229pub enum PrecisionPolicy {
230 #[default]
232 AlwaysF32,
233 AlwaysF16,
235 AutoMixedConservative,
246 AutoMixed,
258 AutoMixedBf16,
270 Custom(HashMap<OpKind, Precision>),
272}
273
274impl PrecisionPolicy {
275 pub fn precision_for(&self, kind: OpKind) -> Precision {
277 match self {
278 PrecisionPolicy::AlwaysF32 => Precision::F32,
279 PrecisionPolicy::AlwaysF16 => match kind {
280 OpKind::Boundary => Precision::F32, _ => Precision::F16,
282 },
283 PrecisionPolicy::AutoMixed => match kind {
284 OpKind::Compute => Precision::F32,
288 OpKind::Reduction => Precision::F16,
289 OpKind::Elementwise => Precision::F16,
290 OpKind::DataMovement => Precision::F32,
291 OpKind::Boundary => Precision::F32,
292 },
293 PrecisionPolicy::AutoMixedConservative => match kind {
294 OpKind::Compute => Precision::F32,
295 OpKind::Reduction => Precision::F32,
296 OpKind::Elementwise => Precision::F16,
297 OpKind::DataMovement => Precision::F32,
298 OpKind::Boundary => Precision::F32,
299 },
300 PrecisionPolicy::AutoMixedBf16 => match kind {
301 OpKind::Compute => Precision::BF16,
302 OpKind::Reduction => Precision::BF16,
303 OpKind::Elementwise => Precision::BF16,
304 OpKind::DataMovement => Precision::BF16,
305 OpKind::Boundary => Precision::F32,
306 },
307 PrecisionPolicy::Custom(map) => map.get(&kind).copied().unwrap_or(Precision::F32),
308 }
309 }
310}
311
312pub struct AutoMixedPrecision {
322 pub policy: PrecisionPolicy,
323}
324
325impl AutoMixedPrecision {
326 pub fn new(policy: PrecisionPolicy) -> Self {
327 Self { policy }
328 }
329}
330
331impl Pass for AutoMixedPrecision {
332 fn name(&self) -> &str {
333 "auto_mixed_precision"
334 }
335
336 fn run(&self, graph: Graph) -> Graph {
337 if matches!(self.policy, PrecisionPolicy::AlwaysF32) {
339 return graph;
340 }
341
342 let mut new_graph = Graph::new(&graph.name);
343 let mut id_map: HashMap<NodeId, NodeId> = HashMap::new();
345 let mut node_precision: HashMap<NodeId, Precision> = HashMap::new();
347 let mut cast_cache: HashMap<(NodeId, Precision), NodeId> = HashMap::new();
350
351 for node in graph.nodes() {
352 if matches!(
358 node.op,
359 Op::ScaledMatMul { .. }
360 | Op::ScaledQuantize { .. }
361 | Op::ScaledQuantScale { .. }
362 | Op::ScaledDequantize { .. }
363 ) {
364 let new_inputs: Vec<NodeId> = node
365 .inputs
366 .iter()
367 .map(|&in_id| {
368 let src = id_map[&in_id];
369 let sd = new_graph.node(src).shape.dtype();
370 if matches!(sd, DType::F16 | DType::BF16) {
371 let shape = new_graph.node(src).shape.clone().with_dtype(DType::F32);
372 new_graph.add_node(Op::Cast { to: DType::F32 }, vec![src], shape)
373 } else {
374 src
375 }
376 })
377 .collect();
378 let new_id = new_graph.add_node(node.op.clone(), new_inputs, node.shape.clone());
379 id_map.insert(node.id, new_id);
380 node_precision.insert(node.id, Precision::F32);
382 continue;
383 }
384
385 let force_f32 = match &node.op {
390 Op::GatedDeltaNet { .. }
391 | Op::SelectiveScan { .. }
392 | Op::Conv { .. }
393 | Op::Conv3d { .. }
394 | Op::ConvTranspose2d { .. }
395 | Op::ConvTranspose3d { .. }
396 | Op::Im2Col { .. }
397 | Op::Pool { .. }
398 | Op::QConv2d { .. }
399 | Op::Conv2dBackwardInput { .. }
400 | Op::Conv2dBackwardWeight { .. } => true,
401 Op::Cumsum { .. }
403 | Op::Sample { .. }
404 | Op::Lstm { .. }
405 | Op::Gru { .. }
406 | Op::Rnn { .. }
407 | Op::Mamba2 { .. }
408 | Op::SoftmaxCrossEntropy
409 | Op::SoftmaxCrossEntropyWithLogits
410 | Op::SoftmaxCrossEntropyBackward
411 | Op::LayerNormBackwardInput { .. }
412 | Op::LayerNormBackwardGamma { .. }
413 | Op::GroupNorm { .. }
414 | Op::FusedAttentionBlock { .. }
415 | Op::TopK { .. }
416 | Op::ScatterAdd
417 | Op::ScatterNd { .. }
418 | Op::ScatterElements { .. }
419 | Op::GatherNd { .. }
420 | Op::GatherElements { .. }
421 | Op::Expand { .. }
422 | Op::MaxPool2dBackward { .. }
423 | Op::ResizeNearest2x
424 | Op::AxialRope2d { .. } => true,
425 Op::BiMap
427 | Op::ReEig { .. }
428 | Op::LogEig { .. }
429 | Op::SpdBatchNorm { .. }
430 | Op::SpdKarcherMean { .. }
431 | Op::SpdKarcherMeanWeighted { .. }
432 | Op::SpdLogMap
433 | Op::SpdExpMap
434 | Op::SpdParallelTransport
435 | Op::SpdMatrixFnBatch { .. }
436 | Op::ReEigBackward { .. }
437 | Op::LogEigBackward { .. }
438 | Op::SpdBatchNormBackwardX { .. }
439 | Op::SpdBatchNormBackwardG { .. }
440 | Op::SpdLogMapBackward
441 | Op::SpdExpMapBackward
442 | Op::SpdParallelTransportBackward
443 | Op::SpdMatrixFnBatchBackward { .. }
444 | Op::Eigh
445 | Op::EighBackward
446 | Op::EighBatch
447 | Op::EighBatchBackward => true,
448 Op::DequantMatMul { .. } => true,
451 Op::SynthMatMul { .. } => true,
453 _ => false,
454 };
455 if force_f32 {
456 let new_inputs: Vec<NodeId> = node
457 .inputs
458 .iter()
459 .map(|&in_id| {
460 let src = id_map[&in_id];
461 let sd = new_graph.node(src).shape.dtype();
462 if matches!(sd, DType::F16 | DType::BF16) {
463 let shape = new_graph.node(src).shape.clone().with_dtype(DType::F32);
464 new_graph.add_node(Op::Cast { to: DType::F32 }, vec![src], shape)
465 } else {
466 src
467 }
468 })
469 .collect();
470 let new_id = new_graph.add_node(
471 node.op.clone(),
472 new_inputs,
473 node.shape.clone().with_dtype(DType::F32),
474 );
475 id_map.insert(node.id, new_id);
476 node_precision.insert(node.id, Precision::F32);
477 continue;
478 }
479
480 let kind = op_kind(&node.op);
481 let mut target = self.policy.precision_for(kind);
482
483 if kind == OpKind::Boundary {
486 target = Precision::F32;
487 }
488
489 if let Op::Cast { to } = &node.op {
492 target = match to {
493 DType::F16 => Precision::F16,
494 DType::BF16 => Precision::BF16,
495 DType::F32 => Precision::F32,
496 _ => {
497 let new_id = new_graph.add_node(
499 node.op.clone(),
500 vec![id_map[&node.inputs[0]]],
501 node.shape.clone(),
502 );
503 id_map.insert(node.id, new_id);
504 node_precision.insert(node.id, Precision::F32);
505 continue;
506 }
507 };
508 }
509
510 let mut new_inputs = Vec::with_capacity(node.inputs.len());
515 for &in_id in &node.inputs {
516 let src_new_id = id_map[&in_id];
517 let src_dtype = new_graph.node(src_new_id).shape.dtype();
518 if !matches!(src_dtype, DType::F32 | DType::F16 | DType::BF16) {
519 new_inputs.push(src_new_id);
520 continue;
521 }
522 let src_prec = node_precision
523 .get(&in_id)
524 .copied()
525 .unwrap_or(Precision::F32);
526 if src_prec == target {
527 new_inputs.push(src_new_id);
528 } else {
529 let cast_id = *cast_cache.entry((src_new_id, target)).or_insert_with(|| {
531 let shape = new_graph
532 .node(src_new_id)
533 .shape
534 .clone()
535 .with_dtype(target.dtype());
536 new_graph.add_node(Op::Cast { to: target.dtype() }, vec![src_new_id], shape)
537 });
538 new_inputs.push(cast_id);
539 }
540 }
541
542 let new_shape = if kind == OpKind::Boundary {
549 node.shape.clone()
550 } else if let Op::Cast { to } = &node.op {
551 node.shape.clone().with_dtype(*to)
552 } else {
553 node.shape.clone().with_dtype(target.dtype())
554 };
555 let new_id = new_graph.add_node(node.op.clone(), new_inputs, new_shape);
556 id_map.insert(node.id, new_id);
557 node_precision.insert(node.id, target);
558 }
559
560 let new_outputs: Vec<NodeId> = graph
562 .outputs
563 .iter()
564 .map(|&out_id| {
565 let src_new_id = id_map[&out_id];
566 let src_prec = node_precision
567 .get(&out_id)
568 .copied()
569 .unwrap_or(Precision::F32);
570 if src_prec == Precision::F32 {
571 src_new_id
572 } else {
573 let shape = new_graph
574 .node(src_new_id)
575 .shape
576 .clone()
577 .with_dtype(DType::F32);
578 new_graph.add_node(Op::Cast { to: DType::F32 }, vec![src_new_id], shape)
579 }
580 })
581 .collect();
582 new_graph.set_outputs(new_outputs);
583
584 new_graph
585 }
586}
587
588#[cfg(test)]
589mod tests {
590 use super::*;
591
592 #[test]
593 fn always_f32_is_noop() {
594 let mut g = Graph::new("test");
595 let x = g.input("x", Shape::new(&[2, 4], DType::F32));
596 let w = g.param("w", Shape::new(&[4, 3], DType::F32));
597 let mm = g.matmul(x, w, Shape::new(&[2, 3], DType::F32));
598 g.set_outputs(vec![mm]);
599
600 let pass = AutoMixedPrecision::new(PrecisionPolicy::AlwaysF32);
601 let out = pass.run(g);
602 assert_eq!(out.len(), 3); }
604
605 #[test]
606 fn auto_mixed_inserts_casts_at_boundary() {
607 let mut g = Graph::new("test");
608 let x = g.input("x", Shape::new(&[2, 4], DType::F32));
609 let w = g.param("w", Shape::new(&[4, 3], DType::F32));
610 let mm = g.matmul(x, w, Shape::new(&[2, 3], DType::F32));
611 let act = g.relu(mm);
614 g.set_outputs(vec![act]);
615
616 let pass = AutoMixedPrecision::new(PrecisionPolicy::AutoMixed);
617 let out = pass.run(g);
618
619 assert!(out.len() >= 6);
623 let final_node = out.node(out.outputs[0]);
624 assert!(matches!(final_node.op, Op::Cast { to: DType::F32 }));
625 }
626
627 #[test]
628 fn auto_mixed_preserves_u8_quant_weights() {
629 use rlx_ir::quant::QuantScheme;
630 let mut g = Graph::new("q1_amp");
631 let x = g.input("x", Shape::new(&[1, 128], DType::F32));
632 let w = g.param("w", Shape::new(&[2304], DType::U8));
633 let y = g.add_node(
634 Op::DequantMatMul {
635 scheme: QuantScheme::GgufQ1_0,
636 },
637 vec![x, w],
638 Shape::new(&[1, 64], DType::F32),
639 );
640 g.set_outputs(vec![y]);
641
642 let out = AutoMixedPrecision::new(PrecisionPolicy::AutoMixed).run(g);
643 let params: Vec<_> = out
644 .nodes()
645 .iter()
646 .filter(|n| matches!(n.op, Op::Param { .. }))
647 .collect();
648 assert_eq!(params.len(), 1);
649 assert_eq!(params[0].shape.dtype(), DType::U8);
650 for n in out.nodes() {
651 if let Op::Cast { to } = &n.op {
652 let src = out.node(n.inputs[0]);
653 assert!(
654 !matches!(src.op, Op::Param { .. }),
655 "AMP must not cast U8 Param to {to:?}"
656 );
657 }
658 }
659 }
660}