1use crate::arena::Arena;
12use crate::kernels;
13use rlx_ir::op::{Activation, BinaryOp, ReduceOp};
14use rlx_ir::{Graph, NodeId, Op};
15use std::collections::HashMap;
16
17pub struct ExternalBuffers<'a> {
19 pub buffers: HashMap<NodeId, &'a [f32]>,
21}
22
23pub fn execute(graph: &Graph, arena: &mut Arena, external: &ExternalBuffers) {
31 let schedule: Vec<NodeId> = arena.schedule().to_vec();
32 let scanner = rlx_ir::numeric_check::DebugScanner::from_env("cpu");
36 let inspect_pass = if rlx_ir::tensor_inspect::op_tap_enabled() {
40 Some(rlx_ir::tensor_inspect::op_tap_begin_pass())
41 } else {
42 None
43 };
44 for &node_id in &schedule {
45 let node = graph.node(node_id);
46
47 match &node.op {
48 Op::Input { .. } | Op::Param { .. } | Op::Constant { .. } => {}
51
52 Op::FusedMatMulBiasAct { activation } => {
54 let input_id = node.inputs[0];
55 let weight_id = node.inputs[1];
56 let bias_id = node.inputs[2];
57
58 let input = get_data(arena, external, input_id);
59 let weight = get_data(arena, external, weight_id);
60 let bias = get_data(arena, external, bias_id);
61 let output = get_output(arena, node_id);
62
63 let shape = &node.shape;
65 let n = shape.dim(shape.rank() - 1).unwrap_static();
66 let m = shape.num_elements().unwrap() / n;
67 let k = input.len() / m;
68
69 matmul(input, weight, output, m, k, n);
73
74 match activation {
76 Some(Activation::Gelu) => kernels::par_bias_gelu(output, bias, m, n),
77 Some(Activation::Silu) => {
78 crate::blas::bias_add(output, bias, m, n);
79 kernels::silu_inplace(output);
80 }
81 _ => crate::blas::bias_add(output, bias, m, n),
82 }
83 }
84
85 Op::FusedResidualLN { has_bias, eps } => {
87 let x_id = node.inputs[0];
88 let residual_id = node.inputs[1];
89 let h = node.shape.dim(node.shape.rank() - 1).unwrap_static();
90 let zero_bias = vec![0f32; h];
91 let (gamma_id, beta_id, bias_slice) = if *has_bias {
92 let b = get_data(arena, external, node.inputs[2]);
93 (node.inputs[3], node.inputs[4], b)
94 } else {
95 (node.inputs[2], node.inputs[3], zero_bias.as_slice())
96 };
97
98 let x = get_data(arena, external, x_id);
99 let residual = get_data(arena, external, residual_id);
100 let gamma = get_data(arena, external, gamma_id);
101 let beta = get_data(arena, external, beta_id);
102 let output = get_output(arena, node_id);
103
104 let n = x.len() / h;
105
106 let x_ptr = x.as_ptr() as usize;
108 let r_ptr = residual.as_ptr() as usize;
109 let o_ptr = output.as_mut_ptr() as usize;
110 let bi_ptr = bias_slice.as_ptr() as usize;
111 let g_ptr = gamma.as_ptr() as usize;
112 let b_ptr = beta.as_ptr() as usize;
113 let e = *eps;
114 crate::pool::par_for(n, 4, &|off, cnt| unsafe {
115 let x_s =
116 std::slice::from_raw_parts((x_ptr as *const f32).add(off * h), cnt * h);
117 let r_s =
118 std::slice::from_raw_parts((r_ptr as *const f32).add(off * h), cnt * h);
119 let o_s =
120 std::slice::from_raw_parts_mut((o_ptr as *mut f32).add(off * h), cnt * h);
121 let bi = std::slice::from_raw_parts(bi_ptr as *const f32, h);
122 let g = std::slice::from_raw_parts(g_ptr as *const f32, h);
123 let b = std::slice::from_raw_parts(b_ptr as *const f32, h);
124 kernels::residual_bias_layer_norm(x_s, r_s, bi, g, b, o_s, cnt, h, e);
125 });
126 }
127
128 Op::FusedResidualRmsNorm { has_bias, eps } => {
130 let x_id = node.inputs[0];
131 let residual_id = node.inputs[1];
132 let h = node.shape.dim(node.shape.rank() - 1).unwrap_static();
133 let zero_bias = vec![0f32; h];
134 let (gamma_id, beta_id, bias_slice) = if *has_bias {
135 let b = get_data(arena, external, node.inputs[2]);
136 (node.inputs[3], node.inputs[4], b)
137 } else {
138 (node.inputs[2], node.inputs[3], zero_bias.as_slice())
139 };
140
141 let x = get_data(arena, external, x_id);
142 let residual = get_data(arena, external, residual_id);
143 let gamma = get_data(arena, external, gamma_id);
144 let beta = get_data(arena, external, beta_id);
145 let output = get_output(arena, node_id);
146
147 let n = x.len() / h;
148
149 let x_ptr = x.as_ptr() as usize;
150 let r_ptr = residual.as_ptr() as usize;
151 let o_ptr = output.as_mut_ptr() as usize;
152 let bi_ptr = bias_slice.as_ptr() as usize;
153 let g_ptr = gamma.as_ptr() as usize;
154 let b_ptr = beta.as_ptr() as usize;
155 let e = *eps;
156 crate::pool::par_for(n, 4, &|off, cnt| unsafe {
157 let x_s =
158 std::slice::from_raw_parts((x_ptr as *const f32).add(off * h), cnt * h);
159 let r_s =
160 std::slice::from_raw_parts((r_ptr as *const f32).add(off * h), cnt * h);
161 let o_s =
162 std::slice::from_raw_parts_mut((o_ptr as *mut f32).add(off * h), cnt * h);
163 let bi = std::slice::from_raw_parts(bi_ptr as *const f32, h);
164 let g = std::slice::from_raw_parts(g_ptr as *const f32, h);
165 let b = std::slice::from_raw_parts(b_ptr as *const f32, h);
166 kernels::residual_bias_rms_norm(x_s, r_s, bi, g, b, o_s, cnt, h, e);
167 });
168 }
169
170 Op::MatMul => {
172 let lhs = get_data(arena, external, node.inputs[0]);
173 let rhs = get_data(arena, external, node.inputs[1]);
174 let output = get_output(arena, node_id);
175
176 let shape = &node.shape;
177 let lhs_shape = &graph.node(node.inputs[0]).shape;
178 let rhs_shape = &graph.node(node.inputs[1]).shape;
179 let n = shape.dim(shape.rank() - 1).unwrap_static();
180 let out_m_inner = shape.dim(shape.rank() - 2).unwrap_static();
181 let k = lhs_shape.dim(lhs_shape.rank() - 1).unwrap_static();
182
183 let total = shape.num_elements().unwrap();
186 let per_batch_out = out_m_inner * n;
187 let batches = total / per_batch_out;
188
189 if batches == 1 {
190 matmul(lhs, rhs, output, out_m_inner, k, n);
191 } else {
192 let lhs_batched =
193 lhs_shape.num_elements().unwrap_or(0) == batches * out_m_inner * k;
194 let rhs_batched = rhs_shape.num_elements().unwrap_or(0) == batches * k * n;
195 for b in 0..batches {
196 let l_off = if lhs_batched { b * out_m_inner * k } else { 0 };
197 let r_off = if rhs_batched { b * k * n } else { 0 };
198 let o_off = b * out_m_inner * n;
199 let l_slice = &lhs[l_off..l_off + out_m_inner * k];
200 let r_slice = &rhs[r_off..r_off + k * n];
201 let o_slice = &mut output[o_off..o_off + out_m_inner * n];
202 matmul(l_slice, r_slice, o_slice, out_m_inner, k, n);
203 }
204 }
205 }
206
207 Op::Binary(op) => {
209 let lhs = get_data(arena, external, node.inputs[0]);
210 let rhs = get_data(arena, external, node.inputs[1]);
211 let output = get_output(arena, node_id);
212 let len = output.len();
213 let rhs_len = rhs.len();
214
215 if matches!(op, BinaryOp::Add) && rhs_len < len && len.is_multiple_of(rhs_len) {
217 output.copy_from_slice(lhs);
218 crate::blas::bias_add(output, rhs, len / rhs_len, rhs_len);
219 } else if rhs_len == len {
220 for i in 0..len {
221 output[i] = binary_op(*op, lhs[i], rhs[i]);
222 }
223 } else {
224 for i in 0..len {
225 output[i] = binary_op(*op, lhs[i], rhs[i % rhs_len]);
226 }
227 }
228 }
229
230 Op::Activation(act) => {
232 let input = get_data(arena, external, node.inputs[0]);
233 let output = get_output(arena, node_id);
234 output.copy_from_slice(input);
235 let zeros = vec![0f32; node.shape.dim(node.shape.rank() - 1).unwrap_static()];
236 let m = output.len() / zeros.len();
237 let n = zeros.len();
238 match act {
239 Activation::Gelu => kernels::par_bias_gelu(output, &zeros, m, n),
240 Activation::Silu => kernels::silu_inplace(output),
241 Activation::Relu => {
242 for v in output.iter_mut() {
243 *v = v.max(0.0);
244 }
245 }
246 Activation::Exp => {
247 for v in output.iter_mut() {
248 *v = v.exp();
249 }
250 }
251 Activation::Sqrt => {
252 for v in output.iter_mut() {
253 *v = v.sqrt();
254 }
255 }
256 Activation::Neg => {
257 for v in output.iter_mut() {
258 *v = -*v;
259 }
260 }
261 Activation::Tanh => {
262 for v in output.iter_mut() {
263 *v = v.tanh();
264 }
265 }
266 Activation::Sigmoid => {
267 for v in output.iter_mut() {
268 *v = 1.0 / (1.0 + (-*v).exp());
269 }
270 }
271 _ => {}
272 }
273 }
274
275 Op::Gather { axis } => {
277 let table = get_data(arena, external, node.inputs[0]);
278 let indices = get_data(arena, external, node.inputs[1]);
279 let output = get_output(arena, node_id);
280
281 let table_shape = &graph.node(node.inputs[0]).shape;
282 let _out_shape = &node.shape;
283
284 if *axis == 0 {
286 let trailing: usize = (1..table_shape.rank())
287 .map(|i| table_shape.dim(i).unwrap_static())
288 .product();
289 for (i, &idx_f32) in indices.iter().enumerate() {
290 let idx = idx_f32 as usize;
291 let src = idx * trailing;
292 let dst = i * trailing;
293 output[dst..dst + trailing].copy_from_slice(&table[src..src + trailing]);
294 }
295 } else {
296 let rank = table_shape.rank();
300 let outer: usize = (0..*axis)
301 .map(|i| table_shape.dim(i).unwrap_static())
302 .product();
303 let axis_size = table_shape.dim(*axis).unwrap_static();
304 let inner: usize = (*axis + 1..rank)
305 .map(|i| table_shape.dim(i).unwrap_static())
306 .product();
307 let n_idx = indices.len();
308 for o in 0..outer {
309 for (k, &idx_f32) in indices.iter().enumerate() {
310 let idx = (idx_f32 as usize).min(axis_size.saturating_sub(1));
311 let src = (o * axis_size + idx) * inner;
312 let dst = (o * n_idx + k) * inner;
313 output[dst..dst + inner].copy_from_slice(&table[src..src + inner]);
314 }
315 }
316 }
317 }
318
319 Op::Narrow { axis, start, len } => {
321 let input = get_data(arena, external, node.inputs[0]);
322 let output = get_output(arena, node_id);
323 let in_shape = &graph.node(node.inputs[0]).shape;
324
325 let rank = in_shape.rank();
326 let outer: usize = (0..*axis)
327 .map(|i| in_shape.dim(i).unwrap_static())
328 .product::<usize>()
329 .max(1);
330 let inner: usize = (*axis + 1..rank)
331 .map(|i| in_shape.dim(i).unwrap_static())
332 .product::<usize>()
333 .max(1);
334 let in_axis_size = in_shape.dim(*axis).unwrap_static();
335
336 for o in 0..outer {
337 for s in 0..*len {
338 let src_off = o * in_axis_size * inner + (*start + s) * inner;
339 let dst_off = o * len * inner + s * inner;
340 output[dst_off..dst_off + inner]
341 .copy_from_slice(&input[src_off..src_off + inner]);
342 }
343 }
344 }
345
346 Op::Transpose { perm } => {
348 let input = get_data(arena, external, node.inputs[0]);
349 let output = get_output(arena, node_id);
350 let in_shape = &graph.node(node.inputs[0]).shape;
351 let rank = in_shape.rank();
352
353 let in_dims: Vec<usize> =
354 (0..rank).map(|i| in_shape.dim(i).unwrap_static()).collect();
355 let out_dims: Vec<usize> = perm.iter().map(|&i| in_dims[i]).collect();
356
357 let mut in_strides = vec![1usize; rank];
360 for i in (0..rank - 1).rev() {
361 in_strides[i] = in_strides[i + 1] * in_dims[i + 1];
362 }
363 let mut out_strides = vec![1usize; rank];
364 for i in (0..rank - 1).rev() {
365 out_strides[i] = out_strides[i + 1] * out_dims[i + 1];
366 }
367
368 let total = output.len();
369 for flat_out in 0..total {
370 let mut in_flat = 0;
371 for d in 0..rank {
372 let coord = (flat_out / out_strides[d]) % out_dims[d];
374 in_flat += coord * in_strides[perm[d]];
376 }
377 output[flat_out] = input[in_flat];
378 }
379 }
380
381 Op::Concat { axis } => {
383 let output = get_output(arena, node_id);
384 let out_shape = &node.shape;
385 let rank = out_shape.rank();
386
387 let outer: usize = (0..*axis)
388 .map(|i| out_shape.dim(i).unwrap_static())
389 .product::<usize>()
390 .max(1);
391 let inner: usize = (*axis + 1..rank)
392 .map(|i| out_shape.dim(i).unwrap_static())
393 .product::<usize>()
394 .max(1);
395
396 let mut dst_off = 0;
397 for o in 0..outer {
398 for &inp_id in &node.inputs {
399 let inp = get_data(arena, external, inp_id);
400 let inp_shape = &graph.node(inp_id).shape;
401 let inp_axis = inp_shape.dim(*axis).unwrap_static();
402 let chunk = inp_axis * inner;
403 let src_off = o * chunk;
404 output[dst_off..dst_off + chunk]
405 .copy_from_slice(&inp[src_off..src_off + chunk]);
406 dst_off += chunk;
407 }
408 }
409 }
410
411 Op::Reshape { .. } => {
413 let input = get_data(arena, external, node.inputs[0]);
414 let output = get_output(arena, node_id);
415 output[..input.len()].copy_from_slice(input);
416 }
417 Op::Expand { .. } => {
421 let input = get_data(arena, external, node.inputs[0]);
422 let in_shape = &graph.node(node.inputs[0]).shape;
423 let out_shape = &node.shape;
424 let out_rank = out_shape.rank();
425 let pad = out_rank - in_shape.rank();
426 let out_dims: Vec<usize> = (0..out_rank)
427 .map(|i| out_shape.dim(i).unwrap_static())
428 .collect();
429 let in_dims: Vec<usize> = (0..out_rank)
430 .map(|i| {
431 if i < pad {
432 1
433 } else {
434 in_shape.dim(i - pad).unwrap_static()
435 }
436 })
437 .collect();
438 let mut in_strides = vec![0usize; out_rank];
440 let mut acc = 1usize;
441 for i in (0..out_rank).rev() {
442 in_strides[i] = if in_dims[i] == 1 { 0 } else { acc };
443 acc *= in_dims[i];
444 }
445 let output = get_output(arena, node_id);
446 let total: usize = out_dims.iter().product();
447 let mut coords = vec![0usize; out_rank];
448 for out_idx in 0..total {
449 let mut in_idx = 0usize;
450 for i in 0..out_rank {
451 in_idx += coords[i] * in_strides[i];
452 }
453 output[out_idx] = input[in_idx];
454 for i in (0..out_rank).rev() {
455 coords[i] += 1;
456 if coords[i] < out_dims[i] {
457 break;
458 }
459 coords[i] = 0;
460 }
461 }
462 }
463
464 Op::LayerNorm { eps, .. } => {
466 let input = get_data(arena, external, node.inputs[0]);
467 let gamma = get_data(arena, external, node.inputs[1]);
468 let beta = get_data(arena, external, node.inputs[2]);
469 let output = get_output(arena, node_id);
470 let h = node.shape.dim(node.shape.rank() - 1).unwrap_static();
471 let n = input.len() / h;
472 for row in 0..n {
473 let base = row * h;
474 kernels::layer_norm_row(
475 &input[base..base + h],
476 gamma,
477 beta,
478 &mut output[base..base + h],
479 h,
480 *eps,
481 );
482 }
483 }
484
485 Op::GroupNorm { num_groups, eps } => {
486 let input = get_data(arena, external, node.inputs[0]);
487 let gamma = get_data(arena, external, node.inputs[1]);
488 let beta = get_data(arena, external, node.inputs[2]);
489 let output = get_output(arena, node_id);
490 let n = node.shape.dim(0).unwrap_static();
491 let c = node.shape.dim(1).unwrap_static();
492 let h = node.shape.dim(2).unwrap_static();
493 let w = node.shape.dim(3).unwrap_static();
494 kernels::group_norm_nchw(input, gamma, beta, output, n, c, h, w, *num_groups, *eps);
495 }
496
497 Op::ResizeNearest2x => {
498 let input = get_data(arena, external, node.inputs[0]);
499 let output = get_output(arena, node_id);
500 let n = node.shape.dim(0).unwrap_static();
501 let c = node.shape.dim(1).unwrap_static();
502 let h = node.shape.dim(2).unwrap_static() / 2;
503 let w = node.shape.dim(3).unwrap_static() / 2;
504 let in_plane = c * h * w;
505 let out_plane = c * h * 2 * w * 2;
506 for ni in 0..n {
507 kernels::resize_nearest_2x_nchw(
508 &input[ni * in_plane..(ni + 1) * in_plane],
509 &mut output[ni * out_plane..(ni + 1) * out_plane],
510 c,
511 h,
512 w,
513 );
514 }
515 }
516
517 Op::AxialRope2d {
518 end_x,
519 end_y,
520 head_dim,
521 num_heads,
522 theta,
523 repeat_factor,
524 } => {
525 let input = get_data(arena, external, node.inputs[0]);
526 let output = get_output(arena, node_id);
527 let batch = node.shape.dim(0).unwrap_static();
528 let seq = node.shape.dim(1).unwrap_static();
529 let plane = seq * node.shape.dim(2).unwrap_static();
530 for bi in 0..batch {
531 let rotated = rlx_ir::ops::axial_rope2d::apply_axial_rope2d(
532 &input[bi * plane..(bi + 1) * plane],
533 *num_heads,
534 seq,
535 *head_dim,
536 *end_x,
537 *end_y,
538 *theta,
539 *repeat_factor,
540 );
541 output[bi * plane..(bi + 1) * plane].copy_from_slice(&rotated);
542 }
543 }
544
545 Op::Softmax { axis } => {
547 let input = get_data(arena, external, node.inputs[0]);
548 let output = get_output(arena, node_id);
549 output.copy_from_slice(input);
550 let rank = node.shape.rank();
551 let ax = if *axis < 0 {
552 (rank as i32 + axis) as usize
553 } else {
554 *axis as usize
555 };
556 let cols = node.shape.dim(ax).unwrap_static();
557 let rows = output.len() / cols;
558 crate::naive::softmax(output, rows, cols);
559 }
560
561 Op::Attention {
563 num_heads,
564 head_dim,
565 v_head_dim,
566 mask_kind,
567 score_scale,
568 attn_logit_softcap,
569 } => {
570 assert!(
574 v_head_dim.is_none_or(|v| v == *head_dim),
575 "rlx-cpu executor: asymmetric v_head_dim (MLA) not supported in the naive interpreter"
576 );
577 let q = get_data(arena, external, node.inputs[0]);
578 let k = get_data(arena, external, node.inputs[1]);
579 let v = get_data(arena, external, node.inputs[2]);
580 let mask: &[f32] = if matches!(
584 mask_kind,
585 rlx_ir::op::MaskKind::Custom | rlx_ir::op::MaskKind::Bias
586 ) {
587 get_data(arena, external, node.inputs[3])
588 } else {
589 &[]
590 };
591 let output = get_output(arena, node_id);
592
593 let q_shape = &graph.node(node.inputs[0]).shape;
594 let k_shape = &graph.node(node.inputs[1]).shape;
595 let hs = num_heads * head_dim;
596 let scale = score_scale.unwrap_or((*head_dim as f32).powf(-0.5));
597 let (batch_size, s_q) = if q_shape.rank() >= 3 {
598 (
599 q_shape.dim(0).unwrap_static(),
600 q_shape.dim(1).unwrap_static(),
601 )
602 } else {
603 (1, q_shape.dim(0).unwrap_static())
604 };
605 let s_k = if k_shape.rank() >= 3 {
610 k_shape.dim(1).unwrap_static()
611 } else {
612 k_shape.dim(0).unwrap_static()
613 };
614 let q_offset = s_k.saturating_sub(s_q);
615
616 let q_buf_len = s_q * head_dim;
618 let k_buf_len = s_k * head_dim;
619 let mut q_head = vec![0f32; q_buf_len];
620 let mut k_head = vec![0f32; k_buf_len];
621 let mut v_head = vec![0f32; k_buf_len];
622 let mut scores = vec![0f32; s_q * s_k];
623 let mut out_head = vec![0f32; q_buf_len];
624
625 for bi in 0..batch_size {
626 for hi in 0..*num_heads {
627 for si in 0..s_q {
629 let off = bi * s_q * hs + si * hs + hi * head_dim;
630 q_head[si * head_dim..(si + 1) * head_dim]
631 .copy_from_slice(&q[off..off + head_dim]);
632 }
633 for si in 0..s_k {
635 let off = bi * s_k * hs + si * hs + hi * head_dim;
636 k_head[si * head_dim..(si + 1) * head_dim]
637 .copy_from_slice(&k[off..off + head_dim]);
638 v_head[si * head_dim..(si + 1) * head_dim]
639 .copy_from_slice(&v[off..off + head_dim]);
640 }
641 if s_q.max(s_k) <= 32 {
644 for qi in 0..s_q {
645 for ki in 0..s_k {
646 let q_off = qi * head_dim;
647 let k_off = ki * head_dim;
648 #[cfg(target_arch = "aarch64")]
649 let mut dot;
650 #[cfg(not(target_arch = "aarch64"))]
651 let mut dot = 0f32;
652 #[cfg(target_arch = "aarch64")]
653 unsafe {
654 use std::arch::aarch64::*;
655 let chunks = head_dim / 4;
656 let mut acc = vdupq_n_f32(0.0);
657 for c in 0..chunks {
658 let vq = vld1q_f32(q_head.as_ptr().add(q_off + c * 4));
659 let vk = vld1q_f32(k_head.as_ptr().add(k_off + c * 4));
660 acc = vfmaq_f32(acc, vq, vk);
661 }
662 dot = vaddvq_f32(acc);
663 for d in (chunks * 4)..*head_dim {
664 dot += q_head[q_off + d] * k_head[k_off + d];
665 }
666 }
667 #[cfg(not(target_arch = "aarch64"))]
668 {
669 for d in 0..*head_dim {
670 dot += q_head[q_off + d] * k_head[k_off + d];
671 }
672 }
673 scores[qi * s_k + ki] = dot * scale;
674 }
675 }
676 } else {
677 crate::blas::sgemm_bt(
678 &q_head,
679 &k_head,
680 &mut scores,
681 s_q,
682 *head_dim,
683 s_k,
684 scale,
685 );
686 }
687 match mask_kind {
692 rlx_ir::op::MaskKind::None => {}
693 rlx_ir::op::MaskKind::Causal => {
694 for qi in 0..s_q {
695 let abs_q = q_offset + qi;
696 for ki in (abs_q + 1)..s_k {
697 scores[qi * s_k + ki] = -1e9;
698 }
699 }
700 }
701 rlx_ir::op::MaskKind::SlidingWindow(w) => {
702 for qi in 0..s_q {
703 let abs_q = q_offset + qi;
704 let lo = abs_q.saturating_sub(*w);
705 for ki in 0..s_k {
706 if ki < lo || ki > abs_q {
707 scores[qi * s_k + ki] = -1e9;
708 }
709 }
710 }
711 }
712 rlx_ir::op::MaskKind::Custom => {
713 if mask.len() >= (bi + 1) * s_k {
714 let m = &mask[bi * s_k..(bi + 1) * s_k];
715 for qi in 0..s_q {
716 for ki in 0..s_k {
717 if m[ki] < 0.5 {
718 scores[qi * s_k + ki] = -1e9;
719 }
720 }
721 }
722 }
723 }
724 rlx_ir::op::MaskKind::Bias => {
725 let per_bh = s_q * s_k;
729 let need = (bi * *num_heads + hi + 1) * per_bh;
730 if mask.len() >= need {
731 let bias_off = (bi * *num_heads + hi) * per_bh;
732 let b = &mask[bias_off..bias_off + per_bh];
733 for i in 0..per_bh {
734 scores[i] += b[i];
735 }
736 }
737 }
738 }
739 if let Some(cap) = attn_logit_softcap {
740 if *cap > 0.0 {
741 for s in scores.iter_mut() {
742 *s = cap * (*s / cap).tanh();
743 }
744 }
745 }
746 crate::naive::softmax(&mut scores, s_q, s_k);
747 if s_q.max(s_k) <= 32 {
749 out_head.fill(0.0);
750 for qi in 0..s_q {
751 for ki in 0..s_k {
752 let sc = scores[qi * s_k + ki];
753 if sc > 1e-8 {
754 let v_off = ki * head_dim;
755 let o_off = qi * head_dim;
756 #[cfg(target_arch = "aarch64")]
757 unsafe {
758 use std::arch::aarch64::*;
759 let vsc = vdupq_n_f32(sc);
760 let chunks = head_dim / 4;
761 for c in 0..chunks {
762 let off = c * 4;
763 let vo =
764 vld1q_f32(out_head.as_ptr().add(o_off + off));
765 let vv =
766 vld1q_f32(v_head.as_ptr().add(v_off + off));
767 vst1q_f32(
768 out_head.as_mut_ptr().add(o_off + off),
769 vfmaq_f32(vo, vsc, vv),
770 );
771 }
772 }
773 #[cfg(not(target_arch = "aarch64"))]
774 for d in 0..*head_dim {
775 out_head[o_off + d] += sc * v_head[v_off + d];
776 }
777 }
778 }
779 }
780 } else {
781 crate::blas::sgemm(
782 &scores,
783 &v_head,
784 &mut out_head,
785 s_q,
786 s_k,
787 *head_dim,
788 );
789 }
790 for si in 0..s_q {
792 let off = bi * s_q * hs + si * hs + hi * head_dim;
793 output[off..off + head_dim]
794 .copy_from_slice(&out_head[si * head_dim..(si + 1) * head_dim]);
795 }
796 }
797 }
798 }
799
800 Op::Rope {
817 head_dim, n_rot, ..
818 } => {
819 let head_dim = *head_dim;
820 let n_rot = *n_rot;
821 let x = get_data(arena, external, node.inputs[0]);
822 let cos_cache = get_data(arena, external, node.inputs[1]);
823 let sin_cache = get_data(arena, external, node.inputs[2]);
824 let x_shape = &graph.node(node.inputs[0]).shape;
825 let output = get_output(arena, node_id);
826 output.copy_from_slice(x);
827
828 let rot_half = n_rot / 2;
829 let tab_half = head_dim / 2;
830 let total = output.len();
831 let num_chunks = total / head_dim;
832
833 let cos_rows = cos_cache.len() / tab_half.max(1);
836 let (s_dim, heads_per_seq): (usize, usize) = {
837 let rank = x_shape.rank();
838 if rank == 0 {
839 (1, 1)
840 } else {
841 let last = if x_shape.dim(rank - 1).is_static() {
842 x_shape.dim(rank - 1).unwrap_static()
843 } else {
844 head_dim
845 };
846 if rank >= 3 && last > head_dim && last.is_multiple_of(head_dim) {
847 let s = if x_shape.dim(rank - 2).is_static() {
849 x_shape.dim(rank - 2).unwrap_static()
850 } else {
851 1
852 };
853 (s, last / head_dim)
854 } else if rank >= 4 && last == head_dim {
855 let s = if x_shape.dim(rank - 2).is_static() {
857 x_shape.dim(rank - 2).unwrap_static()
858 } else {
859 1
860 };
861 (s, 1)
862 } else if rank >= 3 && last == head_dim {
863 let s = if x_shape.dim(rank - 2).is_static() {
865 x_shape.dim(rank - 2).unwrap_static()
866 } else {
867 1
868 };
869 (s, 1)
870 } else {
871 (cos_rows.max(1), 1)
873 }
874 }
875 };
876
877 if rlx_ir::env::var("RLX_ROPE_DEBUG").is_some() {
878 eprintln!(
879 "[rope] shape={:?} num_chunks={num_chunks} cos_rows={cos_rows} s_dim={s_dim} heads_per_seq={heads_per_seq}",
880 x_shape.dims()
881 );
882 }
883 let total_tokens = num_chunks / heads_per_seq.max(1);
886 for chunk in 0..num_chunks {
887 let off = chunk * head_dim;
888 let token = if heads_per_seq > 1 {
892 chunk / heads_per_seq
893 } else {
894 chunk
895 };
896 let pos = if cos_rows == 1 {
897 0
899 } else if cos_rows == total_tokens && total_tokens != s_dim {
900 token.min(cos_rows - 1)
905 } else {
906 (token % s_dim).min(cos_rows.saturating_sub(1))
908 };
909 if rlx_ir::env::var("RLX_ROPE_DEBUG").is_some() && chunk < 4 {
910 eprintln!("[rope] chunk={chunk} pos={pos}");
911 }
912 let cos_off = pos * tab_half;
913
914 for i in 0..rot_half {
915 let cos_v = cos_cache[cos_off + i];
916 let sin_v = sin_cache[cos_off + i];
917 let x1 = output[off + i];
918 let x2 = output[off + rot_half + i];
919 output[off + i] = x1 * cos_v - x2 * sin_v;
920 output[off + rot_half + i] = x2 * cos_v + x1 * sin_v;
921 }
922 output[(n_rot + off)..(head_dim + off)]
923 .copy_from_slice(&x[(n_rot + off)..(head_dim + off)]);
924 }
925 }
926
927 Op::Compare(cmp) => {
929 let lhs = get_data(arena, external, node.inputs[0]);
930 let rhs = get_data(arena, external, node.inputs[1]);
931 let output = get_output(arena, node_id);
932 let rhs_len = rhs.len();
933 for i in 0..output.len() {
934 let a = lhs[i];
935 let b = rhs[i % rhs_len];
936 output[i] = if compare_op(*cmp, a, b) { 1.0 } else { 0.0 };
937 }
938 }
939
940 Op::Where => {
942 let cond = get_data(arena, external, node.inputs[0]);
943 let on_true = get_data(arena, external, node.inputs[1]);
944 let on_false = get_data(arena, external, node.inputs[2]);
945 let output = get_output(arena, node_id);
946 for i in 0..output.len() {
947 output[i] = if cond[i] > 0.5 {
948 on_true[i]
949 } else {
950 on_false[i]
951 };
952 }
953 }
954
955 Op::Reduce {
957 op: reduce_op,
958 axes,
959 keep_dim: _,
960 } => {
961 let input = get_data(arena, external, node.inputs[0]);
962 let output = get_output(arena, node_id);
963 output.fill(0.0);
964 if axes.len() == 1 {
966 let in_shape = &graph.node(node.inputs[0]).shape;
967 let axis = axes[0];
968 let rank = in_shape.rank();
969 let outer: usize = (0..axis)
970 .map(|i| in_shape.dim(i).unwrap_static())
971 .product::<usize>()
972 .max(1);
973 let axis_size = in_shape.dim(axis).unwrap_static();
974 let inner: usize = (axis + 1..rank)
975 .map(|i| in_shape.dim(i).unwrap_static())
976 .product::<usize>()
977 .max(1);
978
979 match reduce_op {
980 ReduceOp::Sum | ReduceOp::Mean => {
981 for o in 0..outer {
982 for i in 0..inner {
983 let mut acc = 0f32;
984 for a in 0..axis_size {
985 acc += input[o * axis_size * inner + a * inner + i];
986 }
987 if matches!(reduce_op, ReduceOp::Mean) {
988 acc /= axis_size as f32;
989 }
990 output[o * inner + i] = acc;
991 }
992 }
993 }
994 ReduceOp::Max => {
995 output.fill(f32::NEG_INFINITY);
996 for o in 0..outer {
997 for i in 0..inner {
998 for a in 0..axis_size {
999 let v = input[o * axis_size * inner + a * inner + i];
1000 let idx = o * inner + i;
1001 if v > output[idx] {
1002 output[idx] = v;
1003 }
1004 }
1005 }
1006 }
1007 }
1008 _ => {} }
1010 }
1011 }
1012
1013 Op::Cast { .. } => {
1015 let input = get_data(arena, external, node.inputs[0]);
1016 let output = get_output(arena, node_id);
1017 output[..input.len()].copy_from_slice(input);
1018 }
1019
1020 Op::FusedSwiGLU { .. } => {
1028 let input = get_data(arena, external, node.inputs[0]);
1029 let output = get_output(arena, node_id);
1030 let n = node.shape.dim(node.shape.rank() - 1).unwrap_static();
1034 let outer = output.len() / n;
1035 debug_assert_eq!(
1036 outer * 2 * n,
1037 input.len(),
1038 "FusedSwiGLU: input/output shape mismatch"
1039 );
1040 for o in 0..outer {
1041 let in_row = &input[o * 2 * n..(o + 1) * 2 * n];
1042 let out_row = &mut output[o * n..(o + 1) * n];
1043 for i in 0..n {
1044 let up = in_row[i];
1045 let gate = in_row[n + i];
1046 let silu_gate = gate / (1.0 + (-gate).exp());
1047 out_row[i] = up * silu_gate;
1048 }
1049 }
1050 }
1051
1052 Op::DenseSolve => {
1054 let a_shape = &graph.node(node.inputs[0]).shape;
1055 let n = a_shape.dim(0).unwrap_static();
1056 let b_elems = node.shape.num_elements().unwrap();
1057 let nrhs = b_elems / n.max(1);
1058 match node.shape.dtype() {
1059 rlx_ir::DType::F32 => {
1060 let a = get_data(arena, external, node.inputs[0]);
1061 let b = get_data(arena, external, node.inputs[1]);
1062 let x = get_output(arena, node_id);
1063 let mut a_scratch = a.to_vec();
1064 let mut x_buf = b.to_vec();
1065 let info = crate::blas::sgesv(&mut a_scratch, &mut x_buf, n, nrhs);
1066 if info != 0 {
1067 panic!("DenseSolve: singular matrix (info={info})");
1068 }
1069 x[..x_buf.len()].copy_from_slice(&x_buf);
1070 }
1071 rlx_ir::DType::F64 => {
1072 let (a_ptr, a_len) = arena.raw_ptr(node.inputs[0]);
1073 let (b_ptr, b_len) = arena.raw_ptr(node.inputs[1]);
1074 let (x_ptr, x_len) = arena.raw_ptr(node_id);
1075 unsafe {
1076 let a_src = std::slice::from_raw_parts(a_ptr as *const f64, a_len / 8);
1077 let b_src = std::slice::from_raw_parts(b_ptr as *const f64, b_len / 8);
1078 let mut a_scratch = a_src.to_vec();
1079 let mut x_buf = b_src.to_vec();
1080 let info = crate::blas::dgesv(&mut a_scratch, &mut x_buf, n, nrhs);
1081 if info != 0 {
1082 panic!("DenseSolve: singular matrix (info={info})");
1083 }
1084 std::slice::from_raw_parts_mut(x_ptr as *mut f64, x_len / 8)
1085 .copy_from_slice(&x_buf);
1086 }
1087 }
1088 other => panic!("DenseSolve executor: unsupported dtype {other:?}"),
1089 }
1090 }
1091
1092 Op::Cholesky => {
1097 let a_shape = &graph.node(node.inputs[0]).shape;
1098 let n = a_shape.dim(0).unwrap_static();
1099 let a = get_data(arena, external, node.inputs[0]);
1100 let l = get_output(arena, node_id);
1101 let mut buf: Vec<f64> = a.iter().map(|&v| v as f64).collect();
1102 let info = crate::blas::dpotrf(&mut buf, n, true);
1103 assert_eq!(
1104 info, 0,
1105 "Cholesky: matrix not SPD (dpotrf info={info}, n={n})"
1106 );
1107 for i in 0..n * n {
1108 l[i] = buf[i] as f32;
1109 }
1110 }
1111
1112 Op::TriangularSolve { lower, transpose } => {
1115 let a_shape = &graph.node(node.inputs[0]).shape;
1116 let n = a_shape.dim(0).unwrap_static();
1117 let b_elems = node.shape.num_elements().unwrap();
1118 let nrhs = b_elems / n.max(1);
1119 let a = get_data(arena, external, node.inputs[0]);
1120 let b = get_data(arena, external, node.inputs[1]);
1121 let x = get_output(arena, node_id);
1122 let a64: Vec<f64> = a.iter().map(|&v| v as f64).collect();
1123 let mut x64: Vec<f64> = b.iter().map(|&v| v as f64).collect();
1124 crate::blas::dtrsm_lower_or_upper(&a64, &mut x64, n, nrhs, *lower, *transpose);
1125 for i in 0..n * nrhs {
1126 x[i] = x64[i] as f32;
1127 }
1128 }
1129
1130 Op::Det | Op::LogDet => {
1135 let log_abs = matches!(node.op, Op::LogDet);
1136 let a_shape = &graph.node(node.inputs[0]).shape;
1137 let n = a_shape.dim(0).unwrap_static();
1138 let a = get_data(arena, external, node.inputs[0]);
1139 let out = get_output(arena, node_id);
1140 let mut a64: Vec<f64> = a.iter().map(|&v| v as f64).collect();
1141 let (logabs, _sign, det) = crate::blas::lu_slogdet(&mut a64, n);
1142 out[0] = if log_abs { logabs as f32 } else { det as f32 };
1143 }
1144
1145 Op::Svd { part } => {
1151 let a_shape = &graph.node(node.inputs[0]).shape;
1152 let m = a_shape.dim(0).unwrap_static();
1153 let n = a_shape.dim(1).unwrap_static();
1154 let k = m.min(n);
1155 let input = get_data(arena, external, node.inputs[0]);
1156 let out = get_output(arena, node_id);
1157 let mut a64: Vec<f64> = input.iter().map(|&v| v as f64).collect();
1158 let mut s = vec![0f64; k];
1159 let mut u = vec![0f64; m * k];
1160 let mut vt = vec![0f64; k * n];
1161 crate::blas::dgesdd_thin(&mut a64, m, n, &mut s, &mut u, &mut vt);
1162 let src: &[f64] = match part {
1163 rlx_ir::op::SvdPart::U => &u,
1164 rlx_ir::op::SvdPart::S => &s,
1165 rlx_ir::op::SvdPart::Vt => &vt,
1166 };
1167 for (i, v) in src.iter().enumerate() {
1168 out[i] = *v as f32;
1169 }
1170 }
1171 Op::Qr { part } => {
1172 let a_shape = &graph.node(node.inputs[0]).shape;
1173 let m = a_shape.dim(0).unwrap_static();
1174 let n = a_shape.dim(1).unwrap_static();
1175 let k = m.min(n);
1176 let input = get_data(arena, external, node.inputs[0]);
1177 let out = get_output(arena, node_id);
1178 let a64: Vec<f64> = input.iter().map(|&v| v as f64).collect();
1179 let mut q = vec![0f64; m * k];
1180 let mut r = vec![0f64; k * n];
1181 crate::blas::qr_thin(&a64, m, n, &mut q, &mut r);
1182 let src: &[f64] = match part {
1183 rlx_ir::op::QrPart::Q => &q,
1184 rlx_ir::op::QrPart::R => &r,
1185 };
1186 for (i, v) in src.iter().enumerate() {
1187 out[i] = *v as f32;
1188 }
1189 }
1190 Op::Sort { axis, descending } | Op::ArgSort { axis, descending } => {
1191 let arg = matches!(node.op, Op::ArgSort { .. });
1192 let descending = *descending;
1193 let axis = *axis;
1194 let in_shape = &graph.node(node.inputs[0]).shape;
1195 let dims: Vec<usize> = (0..in_shape.rank())
1196 .map(|d| in_shape.dim(d).unwrap_static())
1197 .collect();
1198 let axis_dim = dims[axis];
1199 let outer: usize = dims[..axis].iter().product();
1200 let inner: usize = dims[axis + 1..].iter().product();
1201 let input = get_data(arena, external, node.inputs[0]);
1202 let out = get_output(arena, node_id);
1203 for o in 0..outer {
1204 for i in 0..inner {
1205 let base_off = o * axis_dim * inner + i;
1206 let mut perm: Vec<usize> = (0..axis_dim).collect();
1207 perm.sort_by(|&a, &b| {
1208 let va = input[base_off + a * inner];
1209 let vb = input[base_off + b * inner];
1210 let ord = va.partial_cmp(&vb).unwrap_or(std::cmp::Ordering::Equal);
1211 if descending { ord.reverse() } else { ord }
1212 });
1213 for (k, &r) in perm.iter().enumerate() {
1214 out[base_off + k * inner] = if arg {
1215 r as f32
1216 } else {
1217 input[base_off + r * inner]
1218 };
1219 }
1220 }
1221 }
1222 }
1223
1224 _ => {
1226 if !node.inputs.is_empty() && arena.has_buffer(node_id) {
1227 let input = get_data(arena, external, node.inputs[0]);
1228 let output = get_output(arena, node_id);
1229 let len = output.len().min(input.len());
1230 output[..len].copy_from_slice(&input[..len]);
1231 }
1232 }
1233 }
1234
1235 if scanner.enabled() {
1239 scan_node_for_nans(&scanner, graph, node_id, arena, external);
1240 }
1241 if let Some(pass) = inspect_pass {
1242 inspect_node_ops(pass, graph, node_id, arena, external);
1243 }
1244 }
1245}
1246
1247pub(crate) fn inspect_node_ops(
1251 pass: usize,
1252 graph: &Graph,
1253 node_id: NodeId,
1254 arena: &Arena,
1255 external: &ExternalBuffers,
1256) {
1257 let node = graph.node(node_id);
1258 if node.shape.dtype() != rlx_ir::DType::F32 || !arena.has_buffer(node_id) {
1259 return;
1260 }
1261 if matches!(
1263 node.op,
1264 Op::Input { .. } | Op::Param { .. } | Op::Constant { .. }
1265 ) {
1266 return;
1267 }
1268 let out = get_data(arena, external, node_id);
1269 let dims: Vec<usize> = node
1270 .shape
1271 .dims()
1272 .iter()
1273 .map(|d| match d {
1274 rlx_ir::Dim::Static(n) => *n,
1275 rlx_ir::Dim::Dynamic(_) => 0, })
1277 .collect();
1278 let input_ids: Vec<usize> = node.inputs.iter().map(|i| i.0 as usize).collect();
1279 let op_kind = format!("{:?}", node.op.kind());
1280 rlx_ir::tensor_inspect::op_tap_record(
1281 pass,
1282 &op_kind,
1283 node_id.0 as usize,
1284 &dims,
1285 out,
1286 &input_ids,
1287 );
1288}
1289
1290pub(crate) fn scan_node_for_nans(
1299 scanner: &rlx_ir::numeric_check::DebugScanner,
1300 graph: &Graph,
1301 node_id: NodeId,
1302 arena: &Arena,
1303 external: &ExternalBuffers,
1304) -> Option<rlx_ir::numeric_check::NanReport> {
1305 let node = graph.node(node_id);
1306 if node.shape.dtype() != rlx_ir::DType::F32 || !arena.has_buffer(node_id) {
1307 return None;
1308 }
1309 let out = get_data(arena, external, node_id);
1310 let mut inbufs: Vec<(NodeId, &[f32])> = Vec::with_capacity(node.inputs.len());
1311 for &inp in &node.inputs {
1312 if graph.node(inp).shape.dtype() == rlx_ir::DType::F32
1313 && (external.buffers.contains_key(&inp) || arena.has_buffer(inp))
1314 {
1315 inbufs.push((inp, get_data(arena, external, inp)));
1316 }
1317 }
1318 scanner.check(graph, node_id, out, &inbufs)
1319}
1320
1321fn get_data<'a>(arena: &'a Arena, external: &'a ExternalBuffers, id: NodeId) -> &'a [f32] {
1325 if let Some(&ext) = external.buffers.get(&id) {
1328 ext
1329 } else if arena.has_buffer(id) {
1330 let (ptr, len) = arena.raw_ptr(id);
1331 unsafe { std::slice::from_raw_parts(ptr, len) }
1332 } else {
1333 panic!("no data for node {id}")
1334 }
1335}
1336
1337#[allow(clippy::mut_from_ref)]
1343fn get_output(arena: &Arena, id: NodeId) -> &mut [f32] {
1344 let (ptr, len) = arena.raw_ptr(id);
1345 unsafe { std::slice::from_raw_parts_mut(ptr, len) }
1346}
1347
1348#[inline]
1350fn matmul(a: &[f32], b: &[f32], c: &mut [f32], m: usize, k: usize, n: usize) {
1351 crate::blas::sgemm(a, b, c, m, k, n);
1353}
1354
1355fn binary_op(op: rlx_ir::op::BinaryOp, a: f32, b: f32) -> f32 {
1356 use rlx_ir::op::BinaryOp::*;
1357 match op {
1358 Add => a + b,
1359 Sub => a - b,
1360 Mul => a * b,
1361 Div => a / b,
1362 Max => a.max(b),
1363 Min => a.min(b),
1364 Pow => a.powf(b),
1365 Mod => a % b,
1366 Atan2 => a.atan2(b),
1367 BitAnd => ((a as i64) & (b as i64)) as f32,
1368 BitOr => ((a as i64) | (b as i64)) as f32,
1369 BitXor => ((a as i64) ^ (b as i64)) as f32,
1370 Shl => (a as i64).wrapping_shl(b as u32) as f32,
1371 Shr => (a as i64).wrapping_shr(b as u32) as f32,
1372 }
1373}
1374
1375fn compare_op(op: rlx_ir::op::CmpOp, a: f32, b: f32) -> bool {
1376 use rlx_ir::op::CmpOp::*;
1377 match op {
1378 Eq => a == b,
1379 Ne => a != b,
1380 Lt => a < b,
1381 Le => a <= b,
1382 Gt => a > b,
1383 Ge => a >= b,
1384 }
1385}
1386
1387#[allow(dead_code)]
1389fn scalar_gelu(x: f32) -> f32 {
1390 let sign = if x >= 0.0 { 1.0f32 } else { -1.0 };
1391 let xa = x.abs();
1392 let t = 1.0 / (1.0 + 0.3275911 * xa);
1393 let y = t
1394 * (0.254_829_6
1395 + t * (-0.284_496_72 + t * (1.421_413_8 + t * (-1.453_152_1 + t * 1.061_405_4))));
1396 let erf = sign * (1.0 - y * (-xa * xa).exp());
1397 x * 0.5 * (1.0 + erf)
1398}
1399
1400#[cfg(test)]
1401mod tests {
1402 use super::*;
1403 use rlx_ir::*;
1404
1405 use rlx_opt::fusion::FuseMatMulBiasAct;
1406 use rlx_opt::memory;
1407 use rlx_opt::pass::Pass;
1408
1409 #[test]
1411 fn execute_fused_matmul_bias_gelu() {
1412 let mut g = Graph::new("test");
1414 let x_id = g.input("x", Shape::new(&[2, 4], DType::F32));
1415 let w_id = g.param("w", Shape::new(&[4, 3], DType::F32));
1416 let b_id = g.param("b", Shape::new(&[3], DType::F32));
1417 let mm = g.matmul(x_id, w_id, Shape::new(&[2, 3], DType::F32));
1418 let add = g.binary(BinaryOp::Add, mm, b_id, Shape::new(&[2, 3], DType::F32));
1419 let out = g.activation(Activation::Gelu, add, Shape::new(&[2, 3], DType::F32));
1420 g.set_outputs(vec![out]);
1421
1422 let fused = FuseMatMulBiasAct.run(g);
1424 println!("{fused}");
1425
1426 let plan = memory::plan_memory(&fused);
1428 println!("Arena: {} bytes", plan.arena_size);
1429
1430 let x_data = vec![1.0f32, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0]; let w_data = vec![1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]; let b_data = vec![0.5, -0.5, 0.0]; let mut ext = ExternalBuffers {
1436 buffers: HashMap::new(),
1437 };
1438 ext.buffers.insert(fused.outputs[0], &[]); for node in fused.nodes() {
1441 match &node.op {
1442 Op::Input { name } if name == "x" => {
1443 ext.buffers.insert(node.id, &x_data);
1444 }
1445 Op::Param { name } if name == "w" => {
1446 ext.buffers.insert(node.id, &w_data);
1447 }
1448 Op::Param { name } if name == "b" => {
1449 ext.buffers.insert(node.id, &b_data);
1450 }
1451 _ => {}
1452 }
1453 }
1454
1455 let mut arena = Arena::from_plan(plan);
1457 execute(&fused, &mut arena, &ext);
1458
1459 let output_id = fused.outputs[0];
1461 let result = arena.slice(output_id);
1462 println!("Result: {result:?}");
1463
1464 assert!((result[0] - 1.399).abs() < 0.01, "got {}", result[0]);
1468 assert!((result[1] - -0.154).abs() < 0.01, "got {}", result[1]);
1469 assert!((result[2] - 0.0).abs() < 0.01, "got {}", result[2]);
1470 assert!((result[3] - 0.346).abs() < 0.01, "got {}", result[3]);
1471 }
1472
1473 #[test]
1475 fn execute_gather() {
1476 use rlx_ir::infer::GraphExt;
1477 let mut g = Graph::new("gather_test");
1478 let table = g.param("table", Shape::new(&[4, 3], DType::F32));
1480 let indices = g.input("ids", Shape::new(&[2], DType::F32)); let out = g.gather_(table, indices, 0);
1482 g.set_outputs(vec![out]);
1483
1484 let plan = memory::plan_memory(&g);
1485 let mut arena = Arena::from_plan(plan);
1486
1487 let table_data = vec![
1488 10.0, 11.0, 12.0, 20.0, 21.0, 22.0, 30.0, 31.0, 32.0, 40.0, 41.0, 42.0, ];
1493 let ids_data = vec![2.0, 0.0]; let mut ext = ExternalBuffers {
1496 buffers: HashMap::new(),
1497 };
1498 for node in g.nodes() {
1499 match &node.op {
1500 Op::Param { name } if name == "table" => {
1501 ext.buffers.insert(node.id, &table_data);
1502 }
1503 Op::Input { name } if name == "ids" => {
1504 ext.buffers.insert(node.id, &ids_data);
1505 }
1506 _ => {}
1507 }
1508 }
1509
1510 execute(&g, &mut arena, &ext);
1511 let result = arena.slice(g.outputs[0]);
1512 assert_eq!(&result[..3], &[30.0, 31.0, 32.0]); assert_eq!(&result[3..6], &[10.0, 11.0, 12.0]); }
1515
1516 #[test]
1518 fn execute_narrow() {
1519 use rlx_ir::infer::GraphExt;
1520 let mut g = Graph::new("narrow_test");
1521 let x = g.input("x", Shape::new(&[2, 6], DType::F32));
1522 let sliced = g.narrow_(x, 1, 2, 3); g.set_outputs(vec![sliced]);
1524
1525 let plan = memory::plan_memory(&g);
1526 let mut arena = Arena::from_plan(plan);
1527
1528 let data = vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0];
1529 let mut ext = ExternalBuffers {
1530 buffers: HashMap::new(),
1531 };
1532 for node in g.nodes() {
1533 if let Op::Input { .. } = &node.op {
1534 ext.buffers.insert(node.id, &data);
1535 }
1536 }
1537
1538 execute(&g, &mut arena, &ext);
1539 let result = arena.slice(g.outputs[0]);
1540 assert_eq!(result, &[2.0, 3.0, 4.0, 8.0, 9.0, 10.0]);
1541 }
1542
1543 #[test]
1545 fn execute_softmax() {
1546 use rlx_ir::infer::GraphExt;
1547 let mut g = Graph::new("softmax_test");
1548 let x = g.input("x", Shape::new(&[1, 4], DType::F32));
1549 let sm = g.sm(x, -1);
1550 g.set_outputs(vec![sm]);
1551
1552 let plan = memory::plan_memory(&g);
1553 let mut arena = Arena::from_plan(plan);
1554
1555 let data = vec![1.0, 2.0, 3.0, 4.0];
1556 let mut ext = ExternalBuffers {
1557 buffers: HashMap::new(),
1558 };
1559 for node in g.nodes() {
1560 if let Op::Input { .. } = &node.op {
1561 ext.buffers.insert(node.id, &data);
1562 }
1563 }
1564
1565 execute(&g, &mut arena, &ext);
1566 let result = arena.slice(g.outputs[0]);
1567 let sum: f32 = result.iter().sum();
1568 assert!(
1569 (sum - 1.0).abs() < 1e-5,
1570 "softmax should sum to 1, got {sum}"
1571 );
1572 assert!(result[0] < result[1]);
1574 assert!(result[1] < result[2]);
1575 assert!(result[2] < result[3]);
1576 }
1577
1578 #[test]
1580 fn execute_rope() {
1581 use rlx_ir::infer::GraphExt;
1582 let head_dim = 4;
1583 let half = head_dim / 2;
1584 let seq = 2;
1585
1586 let mut g = Graph::new("rope_test");
1587 let x = g.input("x", Shape::new(&[seq, head_dim], DType::F32));
1589 let cos = g.param("cos", Shape::new(&[seq, half], DType::F32));
1590 let sin = g.param("sin", Shape::new(&[seq, half], DType::F32));
1591 let rotated = g.rope(x, cos, sin, head_dim);
1592 g.set_outputs(vec![rotated]);
1593
1594 let plan = memory::plan_memory(&g);
1595 let mut arena = Arena::from_plan(plan);
1596
1597 let x_data = vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0f32];
1599 let cos_data = vec![1.0, 0.0, 0.0, 1.0f32];
1601 let sin_data = vec![0.0, 1.0, 1.0, 0.0f32];
1602
1603 let mut ext = ExternalBuffers {
1604 buffers: HashMap::new(),
1605 };
1606 for node in g.nodes() {
1607 match &node.op {
1608 Op::Input { name } if name == "x" => {
1609 ext.buffers.insert(node.id, &x_data);
1610 }
1611 Op::Param { name } if name == "cos" => {
1612 ext.buffers.insert(node.id, &cos_data);
1613 }
1614 Op::Param { name } if name == "sin" => {
1615 ext.buffers.insert(node.id, &sin_data);
1616 }
1617 _ => {}
1618 }
1619 }
1620
1621 execute(&g, &mut arena, &ext);
1622 let result = arena.slice(g.outputs[0]);
1623
1624 assert!((result[0] - 1.0).abs() < 1e-5, "pos0[0]={}", result[0]);
1630 assert!((result[1] - -1.0).abs() < 1e-5, "pos0[1]={}", result[1]);
1631 assert!((result[2] - 0.0).abs() < 1e-5, "pos0[2]={}", result[2]);
1632 assert!((result[3] - 0.0).abs() < 1e-5, "pos0[3]={}", result[3]);
1633
1634 assert!((result[4] - 0.0).abs() < 1e-5, "pos1[0]={}", result[4]);
1638 assert!((result[5] - 1.0).abs() < 1e-5, "pos1[1]={}", result[5]);
1639 assert!((result[6] - 1.0).abs() < 1e-5, "pos1[2]={}", result[6]);
1640 assert!((result[7] - 0.0).abs() < 1e-5, "pos1[3]={}", result[7]);
1641 }
1642
1643 #[test]
1647 fn epilogue_localizes_sqrt_of_negative() {
1648 use rlx_ir::op::Activation;
1649 let mut g = Graph::new("nan_localize");
1650 let x = g.input("x", Shape::new(&[2], DType::F32));
1651 let s = g.activation(Activation::Sqrt, x, Shape::new(&[2], DType::F32));
1652 g.set_outputs(vec![s]);
1653
1654 let plan = memory::plan_memory(&g);
1655 let mut arena = Arena::from_plan(plan);
1656 let data = vec![-1.0f32, 4.0]; let mut ext = ExternalBuffers {
1658 buffers: HashMap::new(),
1659 };
1660 for node in g.nodes() {
1661 if let Op::Input { .. } = &node.op {
1662 ext.buffers.insert(node.id, &data);
1663 }
1664 }
1665
1666 execute(&g, &mut arena, &ext);
1667
1668 let scanner = rlx_ir::numeric_check::DebugScanner::with_mode(
1670 rlx_ir::numeric_check::DebugMode::Warn,
1671 "cpu",
1672 );
1673 let report = super::scan_node_for_nans(&scanner, &g, g.outputs[0], &arena, &ext)
1674 .expect("sqrt(-1) must be localized as a NaN");
1675 assert_eq!(report.node, s);
1676 assert!(report.op.contains("Sqrt"), "op tag was {}", report.op);
1677 assert!(
1678 report.source_input.is_none(),
1679 "finite input → Sqrt is the culprit, not a propagator"
1680 );
1681 assert!(report.fix.is_some(), "culprit should carry a fix hint");
1682
1683 assert!(super::scan_node_for_nans(&scanner, &g, x, &arena, &ext).is_none());
1685 }
1686
1687 #[test]
1689 fn execute_layer_norm() {
1690 use rlx_ir::infer::GraphExt;
1691 let mut g = Graph::new("ln_test");
1692 let x = g.input("x", Shape::new(&[1, 4], DType::F32));
1693 let gamma = g.param("g", Shape::new(&[4], DType::F32));
1694 let beta = g.param("b", Shape::new(&[4], DType::F32));
1695 let ln = g.ln(x, gamma, beta, 1e-5);
1696 g.set_outputs(vec![ln]);
1697
1698 let plan = memory::plan_memory(&g);
1699 let mut arena = Arena::from_plan(plan);
1700
1701 let x_data = vec![1.0, 2.0, 3.0, 4.0];
1702 let g_data = vec![1.0, 1.0, 1.0, 1.0];
1703 let b_data = vec![0.0, 0.0, 0.0, 0.0];
1704
1705 let mut ext = ExternalBuffers {
1706 buffers: HashMap::new(),
1707 };
1708 for node in g.nodes() {
1709 match &node.op {
1710 Op::Input { name } if name == "x" => {
1711 ext.buffers.insert(node.id, &x_data);
1712 }
1713 Op::Param { name } if name == "g" => {
1714 ext.buffers.insert(node.id, &g_data);
1715 }
1716 Op::Param { name } if name == "b" => {
1717 ext.buffers.insert(node.id, &b_data);
1718 }
1719 _ => {}
1720 }
1721 }
1722
1723 execute(&g, &mut arena, &ext);
1724 let result = arena.slice(g.outputs[0]);
1725 let sum: f32 = result.iter().sum();
1726 assert!(
1727 sum.abs() < 1e-3,
1728 "LN output should be zero-centered, sum={sum}"
1729 );
1730 }
1731}