rlx_macros/lib.rs
1// RLX — versatile ML compiler + runtime.
2// Copyright (C) 2026 Eugene Hauptmann, Nataliya Kosmyna.
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, version 3.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with this program. If not, see <https://www.gnu.org/licenses/>.
15
16//! RLX proc macros for AOT model compilation.
17//!
18//! `#[rlx_model]` transforms a function that uses the RLX tracing API
19//! into an optimized, cached, zero-overhead execution path.
20//!
21//! # Usage
22//! ```rust,ignore
23//! use rlx_macros::rlx_model;
24//! use rlx_runtime::trace::*;
25//!
26//! #[rlx_model]
27//! fn my_encoder(t: &Tracer) -> Vec<TracedTensor> {
28//! let x = t.input("x", &[4, 15, 384], DType::F32);
29//! let w = t.param("w", &[384, 1536], DType::F32);
30//! let b = t.param("b", &[1536], DType::F32);
31//! let out = t.matmul(x, w);
32//! let out = (out + b).gelu();
33//! vec![out]
34//! }
35//!
36//! // Generated: my_encoder_compiled() returns a cached CompiledGraph
37//! // that's built once and reused on every call.
38//! ```
39
40use proc_macro::TokenStream;
41use quote::quote;
42use syn::{ItemFn, parse_macro_input};
43
44mod lm_runner;
45mod pipeline;
46
47/// Compile-time pipeline scheduler (plan #11). See `pipeline_schedule_impl`
48/// in this crate's private `pipeline` module for the full grammar.
49///
50/// ```ignore
51/// pipeline_schedule! {
52/// name: AttentionBlock,
53/// stages: {
54/// qkv_proj => [],
55/// narrow_q => [qkv_proj],
56/// attention => [narrow_q],
57/// }
58/// }
59/// ```
60///
61/// Emits a unit struct + `ORDER`/`DEPS` const slices, with
62/// topological sort + cycle detection at compile time.
63#[proc_macro]
64pub fn pipeline_schedule(item: TokenStream) -> TokenStream {
65 pipeline::pipeline_schedule_impl(item.into()).into()
66}
67
68/// AOT compilation macro for RLX models.
69///
70/// Wraps a tracing function with a `static OnceCell` cache that:
71/// 1. On first call: traces the function → builds IR graph → fuses → compiles thunks
72/// 2. On subsequent calls: executes pre-compiled thunks (zero overhead)
73///
74/// The original function becomes the "graph builder". A new `_compiled` function
75/// is generated that manages the cache and execution.
76///
77/// # Opt-in self-check
78/// `#[rlx_model(check)]` injects a call to
79/// [`rlx_runtime::check::model_self_check`] right after the graph is traced, so
80/// building the model surfaces shape/dtype, backend-dispatch, missed-fusion and
81/// numeric findings on stderr. It runs on the CPU reference backend by default;
82/// tune with `RLX_CHECK` (`off` / `all` / `strict`). No extra dependency — the
83/// generated code already routes through `::rlx_runtime`.
84#[proc_macro_attribute]
85pub fn rlx_model(attr: TokenStream, item: TokenStream) -> TokenStream {
86 // `#[rlx_model(check)]` opts this model into the post-trace self-check.
87 let want_check = attr
88 .to_string()
89 .split(|c: char| !c.is_alphanumeric() && c != '_')
90 .any(|w| w == "check");
91 let input_fn = parse_macro_input!(item as ItemFn);
92 let fn_name = &input_fn.sig.ident;
93 let fn_vis = &input_fn.vis;
94 let fn_block = &input_fn.block;
95 let fn_inputs = &input_fn.sig.inputs;
96 let fn_output = &input_fn.sig.output;
97
98 // Generate the compiled version name
99 let compiled_name = syn::Ident::new(&format!("{fn_name}_compiled"), fn_name.span());
100
101 // The graph builder function name (original, kept for debugging)
102 let builder_name = syn::Ident::new(&format!("{fn_name}_build_graph"), fn_name.span());
103
104 // Optional post-trace self-check (see `#[rlx_model(check)]`).
105 let check_hook = if want_check {
106 quote! { ::rlx_runtime::check::model_self_check(stringify!(#fn_name), &graph); }
107 } else {
108 quote! {}
109 };
110
111 let expanded = quote! {
112 /// Graph builder (the original function — builds IR graph via tracing).
113 fn #builder_name(#fn_inputs) #fn_output {
114 #fn_block
115 }
116
117 /// Compiled model — traces once, caches, executes with zero overhead.
118 ///
119 /// Returns a reference to the cached `CompiledGraph`. Call `.run()` or
120 /// `.run_raw()` to execute.
121 #fn_vis fn #compiled_name() -> &'static ::std::sync::Mutex<::rlx_runtime::CompiledGraph> {
122 use ::std::sync::{Mutex, OnceLock};
123
124 static COMPILED: OnceLock<Mutex<::rlx_runtime::CompiledGraph>> = OnceLock::new();
125
126 COMPILED.get_or_init(|| {
127 // Trace the function to build the IR graph
128 let graph = ::rlx_runtime::trace::trace(stringify!(#fn_name), |t| {
129 #builder_name(t)
130 });
131
132 // Opt-in `#[rlx_model(check)]` self-check (no-op otherwise).
133 #check_hook
134
135 // Compile: fuse → memory plan → thunks
136 let session = ::rlx_runtime::Session::new(::rlx_runtime::Device::Cpu);
137 let compiled = session.compile(graph);
138
139 Mutex::new(compiled)
140 })
141 }
142
143 // Keep original function accessible for debugging
144 #[allow(dead_code)]
145 #input_fn
146 };
147
148 TokenStream::from(expanded)
149}
150
151/// Register a per-family LM runner so [`rlx_runtime::auto_runner_name`]
152/// can route a weights file to it.
153///
154/// ```ignore
155/// rlx_macros::register_lm_runner! {
156/// family = "qwen3",
157/// description = "Qwen 3 LM",
158/// arches = ["qwen3", "qwen3moe"]
159/// }
160/// ```
161///
162/// Backed by `inventory` at startup; no per-bin `register_cli` call
163/// is needed once each family invokes this macro at the crate root.
164#[proc_macro]
165pub fn register_lm_runner(input: TokenStream) -> TokenStream {
166 lm_runner::register_lm_runner_impl(input)
167}
168
169/// `fn main()` for a per-family runner binary. Replaces the 8-line
170/// boilerplate at the top of every `rlx-<family>/src/bin/rlx_*.rs`.
171///
172/// ```ignore
173/// // src/bin/rlx_qwen3.rs
174/// rlx_macros::rlx_runner_main!(rlx_qwen3::cli::run, "rlx-qwen3");
175/// ```
176#[proc_macro]
177pub fn rlx_runner_main(input: TokenStream) -> TokenStream {
178 lm_runner::rlx_runner_main_impl(input)
179}