Skip to main content

rlx_ir/ops/
blocks.rs

1// RLX — versatile ML compiler + runtime.
2// Copyright (C) 2026 Eugene Hauptmann, Nataliya Kosmyna.
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5//! Fusion-friendly block builders — canonical subgraph shapes the
6//! optimizer passes in `rlx-opt` already recognize.
7//!
8//! Model authors should prefer these over hand-wiring `MatMul → Add →
9//! Activation` so fusion succeeds regardless of param declaration
10//! order elsewhere in the graph.
11
12use crate::infer::GraphExt;
13use crate::op::Activation;
14use crate::{Graph, NodeId, Op, Shape};
15
16impl Graph {
17    /// Dense linear layer: `matmul(input, weight)` with optional rank-1 bias.
18    pub fn linear_bias(&mut self, input: NodeId, weight: NodeId, bias: Option<NodeId>) -> NodeId {
19        let mm = self.mm(input, weight);
20        match bias {
21            Some(b) => self.add(mm, b),
22            None => mm,
23        }
24    }
25
26    /// Dense linear with optional bias and epilogue activation.
27    pub fn linear_bias_act(
28        &mut self,
29        input: NodeId,
30        weight: NodeId,
31        bias: Option<NodeId>,
32        activation: Option<Activation>,
33    ) -> NodeId {
34        let x = self.linear_bias(input, weight, bias);
35        activation.map_or(x, |act| self.activation_by_kind(x, act))
36    }
37
38    /// Emit `Op::FusedMatMulBiasAct` directly — deterministic fusion
39    /// without relying on the `FuseMatMulBiasAct` pass.
40    pub fn linear_fused(
41        &mut self,
42        input: NodeId,
43        weight: NodeId,
44        bias: NodeId,
45        activation: Option<Activation>,
46        out_shape: Shape,
47    ) -> NodeId {
48        self.fused_matmul_bias_act(input, weight, bias, activation, out_shape)
49    }
50
51    /// Two matmuls sharing the same input — canonical gate+up / QKV
52    /// pattern for `FuseSharedInputMatMul`.
53    ///
54    /// Returns `(first, second)` in declaration order. For SwiGLU,
55    /// pass **up** weight first and **gate** weight second so the
56    /// post-concat narrow layout matches `FuseSwiGLU` (up @ 0, gate @ N).
57    pub fn shared_matmul_pair(
58        &mut self,
59        input: NodeId,
60        w_first: NodeId,
61        w_second: NodeId,
62    ) -> (NodeId, NodeId) {
63        let first = self.mm(input, w_first);
64        let second = self.mm(input, w_second);
65        (first, second)
66    }
67
68    /// SwiGLU FFN block: shared-input gate+up → `silu(gate) * up` → down proj.
69    ///
70    /// Weight order matches `FuseSwiGLU`'s canonical narrow layout
71    /// (up projection first, gate projection second).
72    pub fn swiglu_ffn(
73        &mut self,
74        input: NodeId,
75        up_w: NodeId,
76        gate_w: NodeId,
77        down_w: NodeId,
78    ) -> NodeId {
79        let (up, gate) = self.shared_matmul_pair(input, up_w, gate_w);
80        let gate_silu = self.silu(gate);
81        let hidden = self.mul(up, gate_silu);
82        self.mm(hidden, down_w)
83    }
84
85    /// Fully fused SwiGLU FFN: concat weights → single matmul →
86    /// [`Op::FusedSwiGLU`] → down projection. Matches the rewrite
87    /// performed by [`FuseSwiGLUDualMatmul`](../../rlx-opt/src/fusion.rs)
88    /// without relying on the pass.
89    pub fn fused_swiglu_ffn(
90        &mut self,
91        input: NodeId,
92        up_w: NodeId,
93        gate_w: NodeId,
94        down_w: NodeId,
95        out_shape: Shape,
96    ) -> NodeId {
97        let wu_shape = self.shape(up_w);
98        let wg_shape = self.shape(gate_w);
99        let k = wu_shape.dim(0).unwrap_static();
100        let n_up = wu_shape.dim(1).unwrap_static();
101        let n_gate = wg_shape.dim(1).unwrap_static();
102        debug_assert_eq!(wu_shape.dim(0), wg_shape.dim(0));
103
104        let concat_shape = Shape::new(&[k, n_up + n_gate], wu_shape.dtype());
105        let concat_w = self.concat(vec![up_w, gate_w], 1, concat_shape);
106
107        let input_shape = self.shape(input);
108        let out_rank = input_shape.rank();
109        let dtype = input_shape.dtype();
110        let mut cat_dims: Vec<usize> = (0..out_rank)
111            .map(|i| input_shape.dim(i).unwrap_static())
112            .collect();
113        cat_dims[out_rank - 1] = n_up + n_gate;
114        let cat_shape = Shape::new(&cat_dims, dtype);
115        let cat_mm = self.matmul(input, concat_w, cat_shape);
116
117        let mut hidden_dims = cat_dims;
118        hidden_dims[out_rank - 1] = n_up;
119        let hidden_shape = Shape::new(&hidden_dims, dtype);
120        let hidden = self.add_node(
121            Op::FusedSwiGLU {
122                cast_to: None,
123                gate_first: false,
124            },
125            vec![cat_mm],
126            hidden_shape,
127        );
128
129        let _ = out_shape;
130        self.mm(hidden, down_w)
131    }
132
133    fn activation_by_kind(&mut self, x: NodeId, act: Activation) -> NodeId {
134        match act {
135            Activation::Gelu => self.gelu(x),
136            Activation::GeluApprox => self.gelu_approx(x),
137            Activation::Silu => self.silu(x),
138            Activation::Relu => self.relu(x),
139            Activation::Exp => self.exp(x),
140            Activation::Sqrt => self.sqrt(x),
141            Activation::Neg => self.neg(x),
142            Activation::Tanh => self.tanh(x),
143            Activation::Sigmoid => {
144                let s = self.shape(x).clone();
145                self.activation(Activation::Sigmoid, x, s)
146            }
147            Activation::Log => {
148                let s = self.shape(x).clone();
149                self.activation(Activation::Log, x, s)
150            }
151            _ => {
152                let s = self.shape(x).clone();
153                self.activation(act, x, s)
154            }
155        }
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162    use crate::op::BinaryOp;
163    use crate::{DType, Op};
164
165    fn f32_shape(dims: &[usize]) -> Shape {
166        Shape::new(dims, DType::F32)
167    }
168
169    #[test]
170    fn linear_bias_act_emits_canonical_chain() {
171        let mut g = Graph::new("linear");
172        let x = g.input("x", f32_shape(&[4, 8]));
173        let w = g.param("w", f32_shape(&[8, 16]));
174        let b = g.param("b", f32_shape(&[16]));
175        let out = g.linear_bias_act(x, w, Some(b), Some(Activation::Silu));
176        g.set_outputs(vec![out]);
177
178        let act = g.node(out);
179        assert!(matches!(act.op, Op::Activation(Activation::Silu)));
180        let add = g.node(act.inputs[0]);
181        assert!(matches!(add.op, Op::Binary(BinaryOp::Add)));
182        let mm = g.node(add.inputs[0]);
183        assert!(matches!(mm.op, Op::MatMul));
184    }
185}