1use rlx_ir::infer::GraphExt;
22use rlx_ir::*;
23use std::cell::RefCell;
24use std::rc::Rc;
25
26#[derive(Clone)]
28pub struct TracedTensor {
29 pub(crate) id: NodeId,
30 graph: Rc<RefCell<Graph>>,
31}
32
33pub struct Tracer {
35 graph: Rc<RefCell<Graph>>,
36}
37
38impl Tracer {
39 fn new(name: &str) -> Self {
40 Self {
41 graph: Rc::new(RefCell::new(Graph::new(name))),
42 }
43 }
44
45 pub fn input(&self, name: &str, dims: &[usize], dtype: DType) -> TracedTensor {
47 let id = self.graph.borrow_mut().input(name, Shape::new(dims, dtype));
48 TracedTensor {
49 id,
50 graph: self.graph.clone(),
51 }
52 }
53
54 pub fn input_dyn(&self, name: &str, dims: &[Dim], dtype: DType) -> TracedTensor {
56 let id = self
57 .graph
58 .borrow_mut()
59 .input(name, Shape::from_dims(dims, dtype));
60 TracedTensor {
61 id,
62 graph: self.graph.clone(),
63 }
64 }
65
66 pub fn param(&self, name: &str, dims: &[usize], dtype: DType) -> TracedTensor {
68 let id = self.graph.borrow_mut().param(name, Shape::new(dims, dtype));
69 TracedTensor {
70 id,
71 graph: self.graph.clone(),
72 }
73 }
74
75 pub fn matmul(&self, lhs: TracedTensor, rhs: TracedTensor) -> TracedTensor {
77 let id = self.graph.borrow_mut().mm(lhs.id, rhs.id);
78 TracedTensor {
79 id,
80 graph: self.graph.clone(),
81 }
82 }
83
84 pub fn layer_norm(
86 &self,
87 x: TracedTensor,
88 gamma: TracedTensor,
89 beta: TracedTensor,
90 eps: f32,
91 ) -> TracedTensor {
92 let id = self.graph.borrow_mut().ln(x.id, gamma.id, beta.id, eps);
93 TracedTensor {
94 id,
95 graph: self.graph.clone(),
96 }
97 }
98
99 pub fn softmax(&self, x: TracedTensor, axis: i32) -> TracedTensor {
101 let id = self.graph.borrow_mut().sm(x.id, axis);
102 TracedTensor {
103 id,
104 graph: self.graph.clone(),
105 }
106 }
107
108 pub fn gather(&self, table: TracedTensor, indices: TracedTensor, axis: usize) -> TracedTensor {
110 let id = self.graph.borrow_mut().gather_(table.id, indices.id, axis);
111 TracedTensor {
112 id,
113 graph: self.graph.clone(),
114 }
115 }
116}
117
118impl TracedTensor {
121 pub fn matmul(self, rhs: TracedTensor) -> TracedTensor {
122 let id = self.graph.borrow_mut().mm(self.id, rhs.id);
123 TracedTensor {
124 id,
125 graph: self.graph.clone(),
126 }
127 }
128
129 pub fn gelu(self) -> TracedTensor {
130 let id = self.graph.borrow_mut().gelu(self.id);
131 TracedTensor {
132 id,
133 graph: self.graph.clone(),
134 }
135 }
136
137 pub fn silu(self) -> TracedTensor {
138 let id = self.graph.borrow_mut().silu(self.id);
139 TracedTensor {
140 id,
141 graph: self.graph.clone(),
142 }
143 }
144
145 pub fn relu(self) -> TracedTensor {
146 let id = self.graph.borrow_mut().relu(self.id);
147 TracedTensor {
148 id,
149 graph: self.graph.clone(),
150 }
151 }
152
153 pub fn layer_norm(self, gamma: TracedTensor, beta: TracedTensor, eps: f32) -> TracedTensor {
154 let id = self.graph.borrow_mut().ln(self.id, gamma.id, beta.id, eps);
155 TracedTensor {
156 id,
157 graph: self.graph.clone(),
158 }
159 }
160
161 pub fn softmax(self, axis: i32) -> TracedTensor {
162 let id = self.graph.borrow_mut().sm(self.id, axis);
163 TracedTensor {
164 id,
165 graph: self.graph.clone(),
166 }
167 }
168
169 pub fn reshape(self, new_shape: &[i64]) -> TracedTensor {
170 let id = self
171 .graph
172 .borrow_mut()
173 .reshape_(self.id, new_shape.to_vec());
174 TracedTensor {
175 id,
176 graph: self.graph.clone(),
177 }
178 }
179
180 pub fn transpose(self, perm: &[usize]) -> TracedTensor {
181 let id = self.graph.borrow_mut().transpose_(self.id, perm.to_vec());
182 TracedTensor {
183 id,
184 graph: self.graph.clone(),
185 }
186 }
187
188 pub fn narrow(self, axis: usize, start: usize, len: usize) -> TracedTensor {
189 let id = self.graph.borrow_mut().narrow_(self.id, axis, start, len);
190 TracedTensor {
191 id,
192 graph: self.graph.clone(),
193 }
194 }
195
196 pub fn rank(&self) -> usize {
200 self.graph.borrow().shape(self.id).rank()
201 }
202
203 pub fn shape(&self) -> rlx_ir::Shape {
205 self.graph.borrow().shape(self.id).clone()
206 }
207
208 pub fn t(&self) -> TracedTensor {
211 let rank = self.rank();
212 assert!(rank >= 2, ".t() requires rank >= 2");
213 let mut perm: Vec<usize> = (0..rank).collect();
214 perm.swap(rank - 2, rank - 1);
215 let id = self.graph.borrow_mut().transpose_(self.id, perm);
216 TracedTensor {
217 id,
218 graph: self.graph.clone(),
219 }
220 }
221
222 pub fn permute(&self, perm: &[usize]) -> TracedTensor {
225 let id = self.graph.borrow_mut().transpose_(self.id, perm.to_vec());
226 TracedTensor {
227 id,
228 graph: self.graph.clone(),
229 }
230 }
231
232 pub fn unsqueeze(&self, axis: usize) -> TracedTensor {
235 let s = self.shape();
236 let rank = s.rank();
237 assert!(
238 axis <= rank,
239 "unsqueeze axis {axis} out of range for rank {rank}"
240 );
241 let mut new_shape: Vec<i64> = (0..rank).map(|i| s.dim(i).unwrap_static() as i64).collect();
242 new_shape.insert(axis, 1);
243 let id = self.graph.borrow_mut().reshape_(self.id, new_shape);
244 TracedTensor {
245 id,
246 graph: self.graph.clone(),
247 }
248 }
249
250 pub fn squeeze(&self, axis: usize) -> TracedTensor {
253 let s = self.shape();
254 let rank = s.rank();
255 assert!(
256 axis < rank,
257 "squeeze axis {axis} out of range for rank {rank}"
258 );
259 assert_eq!(
260 s.dim(axis).unwrap_static(),
261 1,
262 "squeeze axis {axis} has dim {} (must be 1)",
263 s.dim(axis).unwrap_static()
264 );
265 let new_shape: Vec<i64> = (0..rank)
266 .filter(|&i| i != axis)
267 .map(|i| s.dim(i).unwrap_static() as i64)
268 .collect();
269 let id = self.graph.borrow_mut().reshape_(self.id, new_shape);
270 TracedTensor {
271 id,
272 graph: self.graph.clone(),
273 }
274 }
275
276 pub fn mm(&self, rhs: &TracedTensor) -> TracedTensor {
278 let id = self.graph.borrow_mut().mm(self.id, rhs.id);
279 TracedTensor {
280 id,
281 graph: self.graph.clone(),
282 }
283 }
284}
285
286impl std::ops::Add for TracedTensor {
289 type Output = TracedTensor;
290 fn add(self, rhs: TracedTensor) -> TracedTensor {
291 let id = self.graph.borrow_mut().add(self.id, rhs.id);
292 TracedTensor {
293 id,
294 graph: self.graph.clone(),
295 }
296 }
297}
298
299impl std::ops::Sub for TracedTensor {
300 type Output = TracedTensor;
301 fn sub(self, rhs: TracedTensor) -> TracedTensor {
302 let id = self.graph.borrow_mut().sub(self.id, rhs.id);
303 TracedTensor {
304 id,
305 graph: self.graph.clone(),
306 }
307 }
308}
309
310impl std::ops::Mul for TracedTensor {
311 type Output = TracedTensor;
312 fn mul(self, rhs: TracedTensor) -> TracedTensor {
313 let id = self.graph.borrow_mut().mul(self.id, rhs.id);
314 TracedTensor {
315 id,
316 graph: self.graph.clone(),
317 }
318 }
319}
320
321impl std::ops::Div for TracedTensor {
322 type Output = TracedTensor;
323 fn div(self, rhs: TracedTensor) -> TracedTensor {
324 let id = self.graph.borrow_mut().div(self.id, rhs.id);
325 TracedTensor {
326 id,
327 graph: self.graph.clone(),
328 }
329 }
330}
331
332impl std::ops::Neg for TracedTensor {
333 type Output = TracedTensor;
334 fn neg(self) -> TracedTensor {
335 let id = self.graph.borrow_mut().neg(self.id);
336 TracedTensor {
337 id,
338 graph: self.graph.clone(),
339 }
340 }
341}
342
343macro_rules! impl_ref_binop {
351 ($trait:ident, $method:ident, $graph_method:ident) => {
352 impl std::ops::$trait<&TracedTensor> for &TracedTensor {
354 type Output = TracedTensor;
355 fn $method(self, rhs: &TracedTensor) -> TracedTensor {
356 let id = self.graph.borrow_mut().$graph_method(self.id, rhs.id);
357 TracedTensor {
358 id,
359 graph: self.graph.clone(),
360 }
361 }
362 }
363 impl std::ops::$trait<&TracedTensor> for TracedTensor {
365 type Output = TracedTensor;
366 fn $method(self, rhs: &TracedTensor) -> TracedTensor {
367 (&self).$method(rhs)
368 }
369 }
370 impl std::ops::$trait<TracedTensor> for &TracedTensor {
372 type Output = TracedTensor;
373 fn $method(self, rhs: TracedTensor) -> TracedTensor {
374 self.$method(&rhs)
375 }
376 }
377 };
378}
379
380impl_ref_binop!(Add, add, add);
381impl_ref_binop!(Sub, sub, sub);
382impl_ref_binop!(Mul, mul, mul);
383impl_ref_binop!(Div, div, div);
384
385impl std::ops::Neg for &TracedTensor {
386 type Output = TracedTensor;
387 fn neg(self) -> TracedTensor {
388 let id = self.graph.borrow_mut().neg(self.id);
389 TracedTensor {
390 id,
391 graph: self.graph.clone(),
392 }
393 }
394}
395
396pub fn trace<F>(name: &str, f: F) -> Graph
403where
404 F: FnOnce(&Tracer) -> Vec<TracedTensor>,
405{
406 let tracer = Tracer::new(name);
407 let outputs = f(&tracer);
408 let output_ids: Vec<NodeId> = outputs.iter().map(|t| t.id).collect();
409 drop(outputs);
411 let mut graph = Rc::try_unwrap(tracer.graph)
412 .expect("tracer graph still borrowed")
413 .into_inner();
414 graph.set_outputs(output_ids);
415 graph
416}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421 use rlx_ir::op::Activation;
422
423 #[test]
424 fn trace_matmul_bias_gelu() {
425 let graph = trace("test", |t| {
426 let x = t.input("x", &[4, 15, 384], DType::F32);
427 let w = t.param("w", &[384, 1536], DType::F32);
428 let b = t.param("b", &[1536], DType::F32);
429 let mm = t.matmul(x, w);
430 let out = (mm + b).gelu();
431 vec![out]
432 });
433
434 assert_eq!(graph.len(), 6); assert_eq!(
436 graph.shape(graph.outputs[0]),
437 &Shape::new(&[4, 15, 1536], DType::F32)
438 );
439 println!("{graph}");
440 }
441
442 #[test]
443 fn trace_operator_overloads() {
444 let graph = trace("ops", |t| {
445 let a = t.input("a", &[4, 384], DType::F32);
446 let b = t.input("b", &[4, 384], DType::F32);
447 let c = a.clone() + b.clone();
448 let d = a.clone() * b.clone();
449 let e = c - d;
450 vec![e]
451 });
452
453 assert_eq!(graph.len(), 5); assert_eq!(
455 graph.shape(graph.outputs[0]),
456 &Shape::new(&[4, 384], DType::F32)
457 );
458 }
459
460 #[test]
461 fn trace_method_chaining() {
462 let graph = trace("chain", |t| {
463 let x = t.input("x", &[4, 15, 384], DType::F32);
464 let w = t.param("w", &[384, 1536], DType::F32);
465 let out = x.matmul(w).gelu();
466 vec![out]
467 });
468
469 assert_eq!(graph.len(), 4); assert_eq!(
471 graph.shape(graph.outputs[0]),
472 &Shape::new(&[4, 15, 1536], DType::F32)
473 );
474 }
475
476 #[test]
477 fn pytorch_shaped_ergonomics() {
478 let graph = trace("ergonomics", |t| {
482 let a = t.input("a", &[4, 8], DType::F32);
483 let b = t.param("b", &[8, 4], DType::F32);
484 let c = a.mm(&b); let d = &c + &c; let e = d.t(); let f = e.unsqueeze(0); let g = f.squeeze(0); let h = g.permute(&[1, 0]); vec![h]
492 });
493 assert_eq!(
494 graph.shape(graph.outputs[0]),
495 &Shape::new(&[4, 4], DType::F32)
496 );
497 }
498
499 #[test]
500 fn trace_produces_fuseable_graph() {
501 use rlx_opt::fusion::FuseMatMulBiasAct;
502 use rlx_opt::pass::Pass;
503
504 let graph = trace("fuseable", |t| {
505 let x = t.input("x", &[4, 15, 384], DType::F32);
506 let w = t.param("w", &[384, 1536], DType::F32);
507 let b = t.param("b", &[1536], DType::F32);
508 let mm = t.matmul(x, w);
509 let out = (mm + b).gelu();
510 vec![out]
511 });
512
513 assert_eq!(graph.len(), 6);
515
516 let fused = FuseMatMulBiasAct.run(graph);
518 assert_eq!(fused.len(), 4);
519
520 let out_node = fused.node(fused.outputs[0]);
521 assert!(matches!(
522 out_node.op,
523 Op::FusedMatMulBiasAct {
524 activation: Some(Activation::Gelu)
525 }
526 ));
527 }
528}