Skip to main content

rlx_ir/
module.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//! [`GraphModule`] — unified higher-order DX over HIR / MIR / LIR.
6
7use std::ops::{Deref, DerefMut};
8
9use crate::hir::{FusionPolicy, HirModule, HirNodeId, LowerError};
10use crate::inspect::{inspect_hir, inspect_lir, inspect_mir};
11use crate::lir::LirModule;
12use crate::mir::MirModule;
13use crate::op::Activation;
14use crate::op::MaskKind;
15use crate::quant::QuantScheme;
16use crate::{Graph, NodeId, Op, Shape};
17
18/// Which stage of the HIR → MIR → LIR pipeline a [`GraphModule`] holds.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum GraphStage {
21    Hir,
22    Mir,
23    Lir,
24}
25
26#[derive(Debug, Clone)]
27enum Stage {
28    Hir(HirModule),
29    Mir(MirModule),
30    Lir(LirModule),
31}
32
33/// Unified model module — primary builder surface above HIR/MIR/LIR.
34#[derive(Debug, Clone)]
35pub struct GraphModule {
36    stage: Stage,
37}
38
39impl GraphModule {
40    pub fn define(
41        name: impl Into<String>,
42        build: impl FnOnce(&mut HirModule) -> HirNodeId,
43    ) -> Self {
44        let mut hir = HirModule::new(name);
45        let out = build(&mut hir);
46        hir.set_outputs(vec![out]);
47        Self {
48            stage: Stage::Hir(hir),
49        }
50    }
51
52    /// Start an empty HIR-stage module (like [`Graph::new`] for MIR).
53    pub fn hir(name: impl Into<String>) -> Self {
54        Self {
55            stage: Stage::Hir(HirModule::new(name)),
56        }
57    }
58
59    /// Start an empty MIR-stage module.
60    pub fn mir(name: impl Into<String>) -> Self {
61        Self {
62            stage: Stage::Mir(MirModule::new(name)),
63        }
64    }
65
66    pub fn from_hir(hir: HirModule) -> Self {
67        Self {
68            stage: Stage::Hir(hir),
69        }
70    }
71
72    pub fn from_graph(graph: Graph) -> Self {
73        Self {
74            stage: Stage::Mir(MirModule::from_graph(graph)),
75        }
76    }
77
78    pub fn from_mir(mir: MirModule) -> Self {
79        Self {
80            stage: Stage::Mir(mir),
81        }
82    }
83
84    pub fn from_lir(lir: LirModule) -> Self {
85        Self {
86            stage: Stage::Lir(lir),
87        }
88    }
89
90    pub fn block(
91        hir: &mut HirModule,
92        name: impl Into<String>,
93        build: impl FnOnce(&mut HirModule) -> HirNodeId,
94    ) -> HirNodeId {
95        hir.named(name, build)
96    }
97
98    pub fn fusion_policy(&self) -> Option<FusionPolicy> {
99        self.as_hir().map(|h| h.fusion_policy)
100    }
101
102    pub fn with_fusion_policy(mut self, policy: FusionPolicy) -> Self {
103        if let Stage::Hir(h) = &mut self.stage {
104            h.fusion_policy = policy;
105        } else {
106            panic!("GraphModule::with_fusion_policy requires HIR stage");
107        }
108        self
109    }
110
111    /// Set graph outputs at the current stage.
112    ///
113    /// At HIR stage accepts [`HirNodeId`] values; at MIR/LIR the same
114    /// indices map to [`NodeId`] (both are insertion-order node ids).
115    pub fn set_outputs(&mut self, outputs: Vec<HirNodeId>) {
116        match &mut self.stage {
117            Stage::Hir(h) => h.set_outputs(outputs),
118            Stage::Mir(m) => m.set_outputs(outputs.into_iter().map(|h| NodeId(h.0)).collect()),
119            Stage::Lir(l) => l
120                .mir
121                .set_outputs(outputs.into_iter().map(|h| NodeId(h.0)).collect()),
122        }
123    }
124
125    pub fn set_hir_outputs(&mut self, outputs: Vec<HirNodeId>) {
126        self.set_outputs(outputs);
127    }
128
129    /// Finish HIR construction and set the module output.
130    pub fn finish_hir(mut self, output: HirNodeId) -> Self {
131        self.set_hir_outputs(vec![output]);
132        self
133    }
134
135    fn hir_mut(&mut self) -> &mut HirModule {
136        self.as_hir_mut()
137            .expect("GraphModule: HIR builder methods require HIR stage — use GraphModule::hir() or Graph::define()")
138    }
139
140    // ── HIR block builders (forward to HirModule) ─────────────────
141
142    pub fn input(&mut self, name: impl Into<String>, shape: Shape) -> HirNodeId {
143        match &mut self.stage {
144            Stage::Hir(h) => h.input(name, shape),
145            Stage::Mir(m) => {
146                let id = m.as_graph_mut().input(name, shape);
147                HirNodeId(id.0)
148            }
149            Stage::Lir(l) => {
150                let id = l.mir.as_graph_mut().input(name, shape);
151                HirNodeId(id.0)
152            }
153        }
154    }
155
156    pub fn param(&mut self, name: impl Into<String>, shape: Shape) -> HirNodeId {
157        match &mut self.stage {
158            Stage::Hir(h) => h.param(name, shape),
159            Stage::Mir(m) => {
160                let id = m.as_graph_mut().param(name, shape);
161                HirNodeId(id.0)
162            }
163            Stage::Lir(l) => {
164                let id = l.mir.as_graph_mut().param(name, shape);
165                HirNodeId(id.0)
166            }
167        }
168    }
169
170    pub fn linear(
171        &mut self,
172        x: HirNodeId,
173        weight: HirNodeId,
174        bias: Option<HirNodeId>,
175        activation: Option<Activation>,
176        out_shape: Shape,
177    ) -> HirNodeId {
178        self.hir_mut()
179            .linear(x, weight, bias, activation, out_shape)
180    }
181
182    pub fn linear_fused(
183        &mut self,
184        x: HirNodeId,
185        weight: HirNodeId,
186        bias: HirNodeId,
187        activation: Option<Activation>,
188        out_shape: Shape,
189    ) -> HirNodeId {
190        self.hir_mut()
191            .linear_fused(x, weight, bias, activation, out_shape)
192    }
193
194    pub fn shared_linear_pair(
195        &mut self,
196        x: HirNodeId,
197        w_first: HirNodeId,
198        w_second: HirNodeId,
199        out_shape: Shape,
200    ) -> (HirNodeId, HirNodeId) {
201        self.hir_mut()
202            .shared_linear_pair(x, w_first, w_second, out_shape)
203    }
204
205    pub fn swiglu_ffn(
206        &mut self,
207        x: HirNodeId,
208        up_w: HirNodeId,
209        gate_w: HirNodeId,
210        down_w: HirNodeId,
211        out_shape: Shape,
212    ) -> HirNodeId {
213        self.hir_mut()
214            .swiglu_ffn(x, up_w, gate_w, down_w, out_shape)
215    }
216
217    pub fn residual_rms_norm(
218        &mut self,
219        x: HirNodeId,
220        residual: HirNodeId,
221        gamma: HirNodeId,
222        beta: HirNodeId,
223        eps: f32,
224        out_shape: Shape,
225    ) -> HirNodeId {
226        self.hir_mut()
227            .residual_rms_norm(x, residual, gamma, beta, eps, out_shape)
228    }
229
230    pub fn attention(
231        &mut self,
232        q: HirNodeId,
233        k: HirNodeId,
234        v: HirNodeId,
235        mask: Option<HirNodeId>,
236        num_heads: usize,
237        head_dim: usize,
238        mask_kind: MaskKind,
239        out_shape: Shape,
240    ) -> HirNodeId {
241        self.hir_mut()
242            .attention(q, k, v, mask, num_heads, head_dim, mask_kind, out_shape)
243    }
244
245    pub fn depthwise_conv1d_causal(
246        &mut self,
247        input: HirNodeId,
248        weight: HirNodeId,
249        left_pad: HirNodeId,
250        kernel_size: usize,
251        out_shape: Shape,
252    ) -> HirNodeId {
253        self.hir_mut()
254            .depthwise_conv1d_causal(input, weight, left_pad, kernel_size, out_shape)
255    }
256
257    pub fn dequant_matmul(
258        &mut self,
259        x: HirNodeId,
260        w: HirNodeId,
261        scale: Option<HirNodeId>,
262        zp: Option<HirNodeId>,
263        scheme: QuantScheme,
264        out_shape: Shape,
265    ) -> HirNodeId {
266        self.hir_mut()
267            .dequant_matmul(x, w, scale, zp, scheme, out_shape)
268    }
269
270    pub fn gated_delta_net(
271        &mut self,
272        q: HirNodeId,
273        k: HirNodeId,
274        v: HirNodeId,
275        g: HirNodeId,
276        beta: HirNodeId,
277        state_size: usize,
278        out_shape: Shape,
279    ) -> HirNodeId {
280        self.hir_mut()
281            .gated_delta_net(q, k, v, g, beta, state_size, out_shape)
282    }
283
284    pub fn gated_delta_net_carry(
285        &mut self,
286        q: HirNodeId,
287        k: HirNodeId,
288        v: HirNodeId,
289        g: HirNodeId,
290        beta: HirNodeId,
291        state: HirNodeId,
292        state_size: usize,
293        out_shape: Shape,
294    ) -> HirNodeId {
295        self.hir_mut()
296            .gated_delta_net_carry(q, k, v, g, beta, state, state_size, out_shape)
297    }
298
299    pub fn rope(
300        &mut self,
301        x: HirNodeId,
302        cos: HirNodeId,
303        sin: HirNodeId,
304        head_dim: usize,
305        n_rot: usize,
306        out_shape: Shape,
307    ) -> HirNodeId {
308        self.hir_mut().rope(x, cos, sin, head_dim, n_rot, out_shape)
309    }
310
311    pub fn rms_norm(
312        &mut self,
313        x: HirNodeId,
314        gamma: HirNodeId,
315        beta: HirNodeId,
316        eps: f32,
317        out_shape: Shape,
318    ) -> HirNodeId {
319        self.hir_mut().rms_norm(x, gamma, beta, eps, out_shape)
320    }
321
322    pub fn hir_mir(&mut self, op: Op, inputs: Vec<HirNodeId>, shape: Shape) -> HirNodeId {
323        self.hir_mut().mir(op, inputs, shape)
324    }
325
326    pub fn named(
327        &mut self,
328        name: impl Into<String>,
329        build: impl FnOnce(&mut HirModule) -> HirNodeId,
330    ) -> HirNodeId {
331        self.hir_mut().named(name, build)
332    }
333
334    pub fn stage(&self) -> GraphStage {
335        match &self.stage {
336            Stage::Hir(_) => GraphStage::Hir,
337            Stage::Mir(_) => GraphStage::Mir,
338            Stage::Lir(_) => GraphStage::Lir,
339        }
340    }
341
342    pub fn name(&self) -> &str {
343        match &self.stage {
344            Stage::Hir(h) => &h.name,
345            Stage::Mir(m) => m.name(),
346            Stage::Lir(l) => l.name(),
347        }
348    }
349
350    pub fn lower(self) -> Result<Self, LowerError> {
351        match self.stage {
352            Stage::Hir(hir) => Ok(Self {
353                stage: Stage::Mir(hir.lower_to_mir()?),
354            }),
355            other => Ok(Self { stage: other }),
356        }
357    }
358
359    pub fn into_hir(self) -> Option<HirModule> {
360        match self.stage {
361            Stage::Hir(h) => Some(h),
362            _ => None,
363        }
364    }
365
366    pub fn into_mir(self) -> Result<MirModule, LowerError> {
367        match self.stage {
368            Stage::Hir(hir) => hir.lower_to_mir(),
369            Stage::Mir(m) => Ok(m),
370            Stage::Lir(l) => Ok(l.mir),
371        }
372    }
373
374    pub fn into_lir(self) -> Option<LirModule> {
375        match self.stage {
376            Stage::Lir(l) => Some(l),
377            _ => None,
378        }
379    }
380
381    pub fn into_graph(self) -> Result<Graph, LowerError> {
382        Ok(self.into_mir()?.into_graph())
383    }
384
385    pub fn as_hir(&self) -> Option<&HirModule> {
386        match &self.stage {
387            Stage::Hir(h) => Some(h),
388            _ => None,
389        }
390    }
391
392    pub fn as_hir_mut(&mut self) -> Option<&mut HirModule> {
393        match &mut self.stage {
394            Stage::Hir(h) => Some(h),
395            _ => None,
396        }
397    }
398
399    pub fn as_mir(&self) -> Option<&MirModule> {
400        match &self.stage {
401            Stage::Mir(m) => Some(m),
402            Stage::Lir(l) => Some(&l.mir),
403            _ => None,
404        }
405    }
406
407    pub fn as_lir(&self) -> Option<&LirModule> {
408        match &self.stage {
409            Stage::Lir(l) => Some(l),
410            _ => None,
411        }
412    }
413
414    pub fn as_graph(&self) -> Option<&Graph> {
415        match &self.stage {
416            Stage::Mir(m) => Some(m.as_graph()),
417            Stage::Lir(l) => Some(l.as_graph()),
418            Stage::Hir(_) => None,
419        }
420    }
421
422    pub fn inspect(&self) -> String {
423        match &self.stage {
424            Stage::Hir(h) => inspect_hir(h),
425            Stage::Mir(m) => inspect_mir(m),
426            Stage::Lir(l) => inspect_lir(l),
427        }
428    }
429}
430
431impl Deref for GraphModule {
432    type Target = Graph;
433
434    fn deref(&self) -> &Graph {
435        self.as_graph()
436            .expect("GraphModule: HIR stage — call lower() before accessing MIR Graph")
437    }
438}
439
440impl DerefMut for GraphModule {
441    fn deref_mut(&mut self) -> &mut Graph {
442        match &mut self.stage {
443            Stage::Mir(m) => m.as_graph_mut(),
444            Stage::Lir(l) => l.mir.as_graph_mut(),
445            Stage::Hir(_) => panic!("GraphModule: HIR stage — use as_hir_mut() or lower() first"),
446        }
447    }
448}
449
450impl From<Graph> for GraphModule {
451    fn from(graph: Graph) -> Self {
452        Self::from_graph(graph)
453    }
454}
455
456impl TryFrom<GraphModule> for Graph {
457    type Error = LowerError;
458
459    fn try_from(module: GraphModule) -> Result<Self, LowerError> {
460        module.into_graph()
461    }
462}
463
464impl From<MirModule> for GraphModule {
465    fn from(mir: MirModule) -> Self {
466        Self::from_mir(mir)
467    }
468}
469
470impl From<HirModule> for GraphModule {
471    fn from(hir: HirModule) -> Self {
472        Self::from_hir(hir)
473    }
474}
475
476impl std::fmt::Display for GraphModule {
477    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
478        match &self.stage {
479            Stage::Hir(h) => write!(f, "{h}"),
480            Stage::Mir(m) => write!(f, "{m}"),
481            Stage::Lir(l) => write!(f, "lir @{}", l.name()),
482        }
483    }
484}
485
486#[cfg(test)]
487mod tests {
488    use super::*;
489    use crate::DType;
490    use crate::Graph;
491    use crate::Shape;
492
493    fn f32_shape(d: &[usize]) -> Shape {
494        Shape::new(d, DType::F32)
495    }
496
497    #[test]
498    fn define_lowers_to_mir_graph() {
499        let module = GraphModule::define("m", |m| {
500            let x = m.input("x", f32_shape(&[2, 8]));
501            let w = m.param("w", f32_shape(&[8, 8]));
502            m.linear(x, w, None, None, f32_shape(&[2, 8]))
503        });
504        assert_eq!(module.stage(), GraphStage::Hir);
505        let module = module.lower().expect("lower");
506        assert_eq!(module.stage(), GraphStage::Mir);
507        assert!(module.len() >= 3);
508    }
509
510    #[test]
511    fn mir_module_deref_builds_graph() {
512        let mut module = GraphModule::mir("raw");
513        let x = module.input("x", f32_shape(&[4]));
514        module.set_outputs(vec![x]);
515        assert_eq!(module.len(), 1);
516    }
517
518    #[test]
519    fn hir_module_block_builders_via_graph_module() {
520        use crate::quant::QuantScheme;
521
522        let mut module = GraphModule::hir("layer");
523        let x = module.input("x", f32_shape(&[2, 128]));
524        let w = module.param("w", f32_shape(&[128, 128]));
525        let y = module.dequant_matmul(x, w, None, None, QuantScheme::GgufQ4K, f32_shape(&[2, 128]));
526        module.set_outputs(vec![y]);
527        assert_eq!(module.stage(), GraphStage::Hir);
528
529        let module = module.lower().expect("lower");
530        assert_eq!(module.stage(), GraphStage::Mir);
531        assert!(module.len() >= 3);
532    }
533
534    #[test]
535    fn graph_hir_entry_matches_define() {
536        let via_graph = Graph::hir("m");
537        let via_define = Graph::define("m", |m| {
538            let x = m.input("x", f32_shape(&[4]));
539            m.rms_norm(x, x, x, 1e-5, f32_shape(&[4]))
540        });
541        assert_eq!(via_graph.stage(), GraphStage::Hir);
542        assert_eq!(via_define.stage(), GraphStage::Hir);
543    }
544}