1use crate::arena::Arena;
23use crate::kernels;
24use rlx_ir::op::{Activation, BinaryOp, ReduceOp};
25use rlx_ir::{Graph, NodeId, Op};
26use std::collections::HashMap;
27
28pub struct ExternalBuffers<'a> {
30 pub buffers: HashMap<NodeId, &'a [f32]>,
32}
33
34pub fn execute(graph: &Graph, arena: &mut Arena, external: &ExternalBuffers) {
42 let schedule: Vec<NodeId> = arena.schedule().to_vec();
43 let scanner = rlx_ir::numeric_check::DebugScanner::from_env("cpu");
47 for &node_id in &schedule {
48 let node = graph.node(node_id);
49
50 match &node.op {
51 Op::Input { .. } | Op::Param { .. } | Op::Constant { .. } => {}
54
55 Op::FusedMatMulBiasAct { activation } => {
57 let input_id = node.inputs[0];
58 let weight_id = node.inputs[1];
59 let bias_id = node.inputs[2];
60
61 let input = get_data(arena, external, input_id);
62 let weight = get_data(arena, external, weight_id);
63 let bias = get_data(arena, external, bias_id);
64 let output = get_output(arena, node_id);
65
66 let shape = &node.shape;
68 let n = shape.dim(shape.rank() - 1).unwrap_static();
69 let m = shape.num_elements().unwrap() / n;
70 let k = input.len() / m;
71
72 matmul(input, weight, output, m, k, n);
76
77 match activation {
79 Some(Activation::Gelu) => kernels::par_bias_gelu(output, bias, m, n),
80 Some(Activation::Silu) => {
81 crate::blas::bias_add(output, bias, m, n);
82 kernels::silu_inplace(output);
83 }
84 _ => crate::blas::bias_add(output, bias, m, n),
85 }
86 }
87
88 Op::FusedResidualLN { has_bias, eps } => {
90 let x_id = node.inputs[0];
91 let residual_id = node.inputs[1];
92 let h = node.shape.dim(node.shape.rank() - 1).unwrap_static();
93 let zero_bias = vec![0f32; h];
94 let (gamma_id, beta_id, bias_slice) = if *has_bias {
95 let b = get_data(arena, external, node.inputs[2]);
96 (node.inputs[3], node.inputs[4], b)
97 } else {
98 (node.inputs[2], node.inputs[3], zero_bias.as_slice())
99 };
100
101 let x = get_data(arena, external, x_id);
102 let residual = get_data(arena, external, residual_id);
103 let gamma = get_data(arena, external, gamma_id);
104 let beta = get_data(arena, external, beta_id);
105 let output = get_output(arena, node_id);
106
107 let n = x.len() / h;
108
109 let x_ptr = x.as_ptr() as usize;
111 let r_ptr = residual.as_ptr() as usize;
112 let o_ptr = output.as_mut_ptr() as usize;
113 let bi_ptr = bias_slice.as_ptr() as usize;
114 let g_ptr = gamma.as_ptr() as usize;
115 let b_ptr = beta.as_ptr() as usize;
116 let e = *eps;
117 crate::pool::par_for(n, 4, &|off, cnt| unsafe {
118 let x_s =
119 std::slice::from_raw_parts((x_ptr as *const f32).add(off * h), cnt * h);
120 let r_s =
121 std::slice::from_raw_parts((r_ptr as *const f32).add(off * h), cnt * h);
122 let o_s =
123 std::slice::from_raw_parts_mut((o_ptr as *mut f32).add(off * h), cnt * h);
124 let bi = std::slice::from_raw_parts(bi_ptr as *const f32, h);
125 let g = std::slice::from_raw_parts(g_ptr as *const f32, h);
126 let b = std::slice::from_raw_parts(b_ptr as *const f32, h);
127 kernels::residual_bias_layer_norm(x_s, r_s, bi, g, b, o_s, cnt, h, e);
128 });
129 }
130
131 Op::FusedResidualRmsNorm { has_bias, eps } => {
133 let x_id = node.inputs[0];
134 let residual_id = node.inputs[1];
135 let h = node.shape.dim(node.shape.rank() - 1).unwrap_static();
136 let zero_bias = vec![0f32; h];
137 let (gamma_id, beta_id, bias_slice) = if *has_bias {
138 let b = get_data(arena, external, node.inputs[2]);
139 (node.inputs[3], node.inputs[4], b)
140 } else {
141 (node.inputs[2], node.inputs[3], zero_bias.as_slice())
142 };
143
144 let x = get_data(arena, external, x_id);
145 let residual = get_data(arena, external, residual_id);
146 let gamma = get_data(arena, external, gamma_id);
147 let beta = get_data(arena, external, beta_id);
148 let output = get_output(arena, node_id);
149
150 let n = x.len() / h;
151
152 let x_ptr = x.as_ptr() as usize;
153 let r_ptr = residual.as_ptr() as usize;
154 let o_ptr = output.as_mut_ptr() as usize;
155 let bi_ptr = bias_slice.as_ptr() as usize;
156 let g_ptr = gamma.as_ptr() as usize;
157 let b_ptr = beta.as_ptr() as usize;
158 let e = *eps;
159 crate::pool::par_for(n, 4, &|off, cnt| unsafe {
160 let x_s =
161 std::slice::from_raw_parts((x_ptr as *const f32).add(off * h), cnt * h);
162 let r_s =
163 std::slice::from_raw_parts((r_ptr as *const f32).add(off * h), cnt * h);
164 let o_s =
165 std::slice::from_raw_parts_mut((o_ptr as *mut f32).add(off * h), cnt * h);
166 let bi = std::slice::from_raw_parts(bi_ptr as *const f32, h);
167 let g = std::slice::from_raw_parts(g_ptr as *const f32, h);
168 let b = std::slice::from_raw_parts(b_ptr as *const f32, h);
169 kernels::residual_bias_rms_norm(x_s, r_s, bi, g, b, o_s, cnt, h, e);
170 });
171 }
172
173 Op::MatMul => {
175 let lhs = get_data(arena, external, node.inputs[0]);
176 let rhs = get_data(arena, external, node.inputs[1]);
177 let output = get_output(arena, node_id);
178
179 let shape = &node.shape;
180 let lhs_shape = &graph.node(node.inputs[0]).shape;
181 let rhs_shape = &graph.node(node.inputs[1]).shape;
182 let n = shape.dim(shape.rank() - 1).unwrap_static();
183 let out_m_inner = shape.dim(shape.rank() - 2).unwrap_static();
184 let k = lhs_shape.dim(lhs_shape.rank() - 1).unwrap_static();
185
186 let total = shape.num_elements().unwrap();
189 let per_batch_out = out_m_inner * n;
190 let batches = total / per_batch_out;
191
192 if batches == 1 {
193 matmul(lhs, rhs, output, out_m_inner, k, n);
194 } else {
195 let lhs_batched =
196 lhs_shape.num_elements().unwrap_or(0) == batches * out_m_inner * k;
197 let rhs_batched = rhs_shape.num_elements().unwrap_or(0) == batches * k * n;
198 for b in 0..batches {
199 let l_off = if lhs_batched { b * out_m_inner * k } else { 0 };
200 let r_off = if rhs_batched { b * k * n } else { 0 };
201 let o_off = b * out_m_inner * n;
202 let l_slice = &lhs[l_off..l_off + out_m_inner * k];
203 let r_slice = &rhs[r_off..r_off + k * n];
204 let o_slice = &mut output[o_off..o_off + out_m_inner * n];
205 matmul(l_slice, r_slice, o_slice, out_m_inner, k, n);
206 }
207 }
208 }
209
210 Op::Binary(op) => {
212 let lhs = get_data(arena, external, node.inputs[0]);
213 let rhs = get_data(arena, external, node.inputs[1]);
214 let output = get_output(arena, node_id);
215 let len = output.len();
216 let rhs_len = rhs.len();
217
218 if matches!(op, BinaryOp::Add) && rhs_len < len && len.is_multiple_of(rhs_len) {
220 output.copy_from_slice(lhs);
221 crate::blas::bias_add(output, rhs, len / rhs_len, rhs_len);
222 } else if rhs_len == len {
223 for i in 0..len {
224 output[i] = binary_op(*op, lhs[i], rhs[i]);
225 }
226 } else {
227 for i in 0..len {
228 output[i] = binary_op(*op, lhs[i], rhs[i % rhs_len]);
229 }
230 }
231 }
232
233 Op::Activation(act) => {
235 let input = get_data(arena, external, node.inputs[0]);
236 let output = get_output(arena, node_id);
237 output.copy_from_slice(input);
238 let zeros = vec![0f32; node.shape.dim(node.shape.rank() - 1).unwrap_static()];
239 let m = output.len() / zeros.len();
240 let n = zeros.len();
241 match act {
242 Activation::Gelu => kernels::par_bias_gelu(output, &zeros, m, n),
243 Activation::Silu => kernels::silu_inplace(output),
244 Activation::Relu => {
245 for v in output.iter_mut() {
246 *v = v.max(0.0);
247 }
248 }
249 Activation::Exp => {
250 for v in output.iter_mut() {
251 *v = v.exp();
252 }
253 }
254 Activation::Sqrt => {
255 for v in output.iter_mut() {
256 *v = v.sqrt();
257 }
258 }
259 Activation::Neg => {
260 for v in output.iter_mut() {
261 *v = -*v;
262 }
263 }
264 Activation::Tanh => {
265 for v in output.iter_mut() {
266 *v = v.tanh();
267 }
268 }
269 Activation::Sigmoid => {
270 for v in output.iter_mut() {
271 *v = 1.0 / (1.0 + (-*v).exp());
272 }
273 }
274 _ => {}
275 }
276 }
277
278 Op::Gather { axis } => {
280 let table = get_data(arena, external, node.inputs[0]);
281 let indices = get_data(arena, external, node.inputs[1]);
282 let output = get_output(arena, node_id);
283
284 let table_shape = &graph.node(node.inputs[0]).shape;
285 let _out_shape = &node.shape;
286
287 if *axis == 0 {
289 let trailing: usize = (1..table_shape.rank())
290 .map(|i| table_shape.dim(i).unwrap_static())
291 .product();
292 for (i, &idx_f32) in indices.iter().enumerate() {
293 let idx = idx_f32 as usize;
294 let src = idx * trailing;
295 let dst = i * trailing;
296 output[dst..dst + trailing].copy_from_slice(&table[src..src + trailing]);
297 }
298 } else {
299 let rank = table_shape.rank();
303 let outer: usize = (0..*axis)
304 .map(|i| table_shape.dim(i).unwrap_static())
305 .product();
306 let axis_size = table_shape.dim(*axis).unwrap_static();
307 let inner: usize = (*axis + 1..rank)
308 .map(|i| table_shape.dim(i).unwrap_static())
309 .product();
310 let n_idx = indices.len();
311 for o in 0..outer {
312 for (k, &idx_f32) in indices.iter().enumerate() {
313 let idx = (idx_f32 as usize).min(axis_size.saturating_sub(1));
314 let src = (o * axis_size + idx) * inner;
315 let dst = (o * n_idx + k) * inner;
316 output[dst..dst + inner].copy_from_slice(&table[src..src + inner]);
317 }
318 }
319 }
320 }
321
322 Op::Narrow { axis, start, len } => {
324 let input = get_data(arena, external, node.inputs[0]);
325 let output = get_output(arena, node_id);
326 let in_shape = &graph.node(node.inputs[0]).shape;
327
328 let rank = in_shape.rank();
329 let outer: usize = (0..*axis)
330 .map(|i| in_shape.dim(i).unwrap_static())
331 .product::<usize>()
332 .max(1);
333 let inner: usize = (*axis + 1..rank)
334 .map(|i| in_shape.dim(i).unwrap_static())
335 .product::<usize>()
336 .max(1);
337 let in_axis_size = in_shape.dim(*axis).unwrap_static();
338
339 for o in 0..outer {
340 for s in 0..*len {
341 let src_off = o * in_axis_size * inner + (*start + s) * inner;
342 let dst_off = o * len * inner + s * inner;
343 output[dst_off..dst_off + inner]
344 .copy_from_slice(&input[src_off..src_off + inner]);
345 }
346 }
347 }
348
349 Op::Transpose { perm } => {
351 let input = get_data(arena, external, node.inputs[0]);
352 let output = get_output(arena, node_id);
353 let in_shape = &graph.node(node.inputs[0]).shape;
354 let rank = in_shape.rank();
355
356 let in_dims: Vec<usize> =
357 (0..rank).map(|i| in_shape.dim(i).unwrap_static()).collect();
358 let out_dims: Vec<usize> = perm.iter().map(|&i| in_dims[i]).collect();
359
360 let mut in_strides = vec![1usize; rank];
363 for i in (0..rank - 1).rev() {
364 in_strides[i] = in_strides[i + 1] * in_dims[i + 1];
365 }
366 let mut out_strides = vec![1usize; rank];
367 for i in (0..rank - 1).rev() {
368 out_strides[i] = out_strides[i + 1] * out_dims[i + 1];
369 }
370
371 let total = output.len();
372 for flat_out in 0..total {
373 let mut in_flat = 0;
374 for d in 0..rank {
375 let coord = (flat_out / out_strides[d]) % out_dims[d];
377 in_flat += coord * in_strides[perm[d]];
379 }
380 output[flat_out] = input[in_flat];
381 }
382 }
383
384 Op::Concat { axis } => {
386 let output = get_output(arena, node_id);
387 let out_shape = &node.shape;
388 let rank = out_shape.rank();
389
390 let outer: usize = (0..*axis)
391 .map(|i| out_shape.dim(i).unwrap_static())
392 .product::<usize>()
393 .max(1);
394 let inner: usize = (*axis + 1..rank)
395 .map(|i| out_shape.dim(i).unwrap_static())
396 .product::<usize>()
397 .max(1);
398
399 let mut dst_off = 0;
400 for o in 0..outer {
401 for &inp_id in &node.inputs {
402 let inp = get_data(arena, external, inp_id);
403 let inp_shape = &graph.node(inp_id).shape;
404 let inp_axis = inp_shape.dim(*axis).unwrap_static();
405 let chunk = inp_axis * inner;
406 let src_off = o * chunk;
407 output[dst_off..dst_off + chunk]
408 .copy_from_slice(&inp[src_off..src_off + chunk]);
409 dst_off += chunk;
410 }
411 }
412 }
413
414 Op::Reshape { .. } => {
416 let input = get_data(arena, external, node.inputs[0]);
417 let output = get_output(arena, node_id);
418 output[..input.len()].copy_from_slice(input);
419 }
420 Op::Expand { .. } => {
424 let input = get_data(arena, external, node.inputs[0]);
425 let in_shape = &graph.node(node.inputs[0]).shape;
426 let out_shape = &node.shape;
427 let out_rank = out_shape.rank();
428 let pad = out_rank - in_shape.rank();
429 let out_dims: Vec<usize> = (0..out_rank)
430 .map(|i| out_shape.dim(i).unwrap_static())
431 .collect();
432 let in_dims: Vec<usize> = (0..out_rank)
433 .map(|i| {
434 if i < pad {
435 1
436 } else {
437 in_shape.dim(i - pad).unwrap_static()
438 }
439 })
440 .collect();
441 let mut in_strides = vec![0usize; out_rank];
443 let mut acc = 1usize;
444 for i in (0..out_rank).rev() {
445 in_strides[i] = if in_dims[i] == 1 { 0 } else { acc };
446 acc *= in_dims[i];
447 }
448 let output = get_output(arena, node_id);
449 let total: usize = out_dims.iter().product();
450 let mut coords = vec![0usize; out_rank];
451 for out_idx in 0..total {
452 let mut in_idx = 0usize;
453 for i in 0..out_rank {
454 in_idx += coords[i] * in_strides[i];
455 }
456 output[out_idx] = input[in_idx];
457 for i in (0..out_rank).rev() {
458 coords[i] += 1;
459 if coords[i] < out_dims[i] {
460 break;
461 }
462 coords[i] = 0;
463 }
464 }
465 }
466
467 Op::LayerNorm { eps, .. } => {
469 let input = get_data(arena, external, node.inputs[0]);
470 let gamma = get_data(arena, external, node.inputs[1]);
471 let beta = get_data(arena, external, node.inputs[2]);
472 let output = get_output(arena, node_id);
473 let h = node.shape.dim(node.shape.rank() - 1).unwrap_static();
474 let n = input.len() / h;
475 for row in 0..n {
476 let base = row * h;
477 kernels::layer_norm_row(
478 &input[base..base + h],
479 gamma,
480 beta,
481 &mut output[base..base + h],
482 h,
483 *eps,
484 );
485 }
486 }
487
488 Op::GroupNorm { num_groups, eps } => {
489 let input = get_data(arena, external, node.inputs[0]);
490 let gamma = get_data(arena, external, node.inputs[1]);
491 let beta = get_data(arena, external, node.inputs[2]);
492 let output = get_output(arena, node_id);
493 let n = node.shape.dim(0).unwrap_static();
494 let c = node.shape.dim(1).unwrap_static();
495 let h = node.shape.dim(2).unwrap_static();
496 let w = node.shape.dim(3).unwrap_static();
497 kernels::group_norm_nchw(input, gamma, beta, output, n, c, h, w, *num_groups, *eps);
498 }
499
500 Op::ResizeNearest2x => {
501 let input = get_data(arena, external, node.inputs[0]);
502 let output = get_output(arena, node_id);
503 let n = node.shape.dim(0).unwrap_static();
504 let c = node.shape.dim(1).unwrap_static();
505 let h = node.shape.dim(2).unwrap_static() / 2;
506 let w = node.shape.dim(3).unwrap_static() / 2;
507 let in_plane = c * h * w;
508 let out_plane = c * h * 2 * w * 2;
509 for ni in 0..n {
510 kernels::resize_nearest_2x_nchw(
511 &input[ni * in_plane..(ni + 1) * in_plane],
512 &mut output[ni * out_plane..(ni + 1) * out_plane],
513 c,
514 h,
515 w,
516 );
517 }
518 }
519
520 Op::AxialRope2d {
521 end_x,
522 end_y,
523 head_dim,
524 num_heads,
525 theta,
526 repeat_factor,
527 } => {
528 let input = get_data(arena, external, node.inputs[0]);
529 let output = get_output(arena, node_id);
530 let batch = node.shape.dim(0).unwrap_static();
531 let seq = node.shape.dim(1).unwrap_static();
532 let plane = seq * node.shape.dim(2).unwrap_static();
533 for bi in 0..batch {
534 let rotated = rlx_ir::ops::axial_rope2d::apply_axial_rope2d(
535 &input[bi * plane..(bi + 1) * plane],
536 *num_heads,
537 seq,
538 *head_dim,
539 *end_x,
540 *end_y,
541 *theta,
542 *repeat_factor,
543 );
544 output[bi * plane..(bi + 1) * plane].copy_from_slice(&rotated);
545 }
546 }
547
548 Op::Softmax { axis } => {
550 let input = get_data(arena, external, node.inputs[0]);
551 let output = get_output(arena, node_id);
552 output.copy_from_slice(input);
553 let rank = node.shape.rank();
554 let ax = if *axis < 0 {
555 (rank as i32 + axis) as usize
556 } else {
557 *axis as usize
558 };
559 let cols = node.shape.dim(ax).unwrap_static();
560 let rows = output.len() / cols;
561 crate::naive::softmax(output, rows, cols);
562 }
563
564 Op::Attention {
566 num_heads,
567 head_dim,
568 mask_kind,
569 score_scale,
570 attn_logit_softcap,
571 } => {
572 let q = get_data(arena, external, node.inputs[0]);
573 let k = get_data(arena, external, node.inputs[1]);
574 let v = get_data(arena, external, node.inputs[2]);
575 let mask: &[f32] = if matches!(
579 mask_kind,
580 rlx_ir::op::MaskKind::Custom | rlx_ir::op::MaskKind::Bias
581 ) {
582 get_data(arena, external, node.inputs[3])
583 } else {
584 &[]
585 };
586 let output = get_output(arena, node_id);
587
588 let q_shape = &graph.node(node.inputs[0]).shape;
589 let k_shape = &graph.node(node.inputs[1]).shape;
590 let hs = num_heads * head_dim;
591 let scale = score_scale.unwrap_or((*head_dim as f32).powf(-0.5));
592 let (batch_size, s_q) = if q_shape.rank() >= 3 {
593 (
594 q_shape.dim(0).unwrap_static(),
595 q_shape.dim(1).unwrap_static(),
596 )
597 } else {
598 (1, q_shape.dim(0).unwrap_static())
599 };
600 let s_k = if k_shape.rank() >= 3 {
605 k_shape.dim(1).unwrap_static()
606 } else {
607 k_shape.dim(0).unwrap_static()
608 };
609 let q_offset = s_k.saturating_sub(s_q);
610
611 let q_buf_len = s_q * head_dim;
613 let k_buf_len = s_k * head_dim;
614 let mut q_head = vec![0f32; q_buf_len];
615 let mut k_head = vec![0f32; k_buf_len];
616 let mut v_head = vec![0f32; k_buf_len];
617 let mut scores = vec![0f32; s_q * s_k];
618 let mut out_head = vec![0f32; q_buf_len];
619
620 for bi in 0..batch_size {
621 for hi in 0..*num_heads {
622 for si in 0..s_q {
624 let off = bi * s_q * hs + si * hs + hi * head_dim;
625 q_head[si * head_dim..(si + 1) * head_dim]
626 .copy_from_slice(&q[off..off + head_dim]);
627 }
628 for si in 0..s_k {
630 let off = bi * s_k * hs + si * hs + hi * head_dim;
631 k_head[si * head_dim..(si + 1) * head_dim]
632 .copy_from_slice(&k[off..off + head_dim]);
633 v_head[si * head_dim..(si + 1) * head_dim]
634 .copy_from_slice(&v[off..off + head_dim]);
635 }
636 if s_q.max(s_k) <= 32 {
639 for qi in 0..s_q {
640 for ki in 0..s_k {
641 let q_off = qi * head_dim;
642 let k_off = ki * head_dim;
643 #[cfg(target_arch = "aarch64")]
644 let mut dot;
645 #[cfg(not(target_arch = "aarch64"))]
646 let mut dot = 0f32;
647 #[cfg(target_arch = "aarch64")]
648 unsafe {
649 use std::arch::aarch64::*;
650 let chunks = head_dim / 4;
651 let mut acc = vdupq_n_f32(0.0);
652 for c in 0..chunks {
653 let vq = vld1q_f32(q_head.as_ptr().add(q_off + c * 4));
654 let vk = vld1q_f32(k_head.as_ptr().add(k_off + c * 4));
655 acc = vfmaq_f32(acc, vq, vk);
656 }
657 dot = vaddvq_f32(acc);
658 for d in (chunks * 4)..*head_dim {
659 dot += q_head[q_off + d] * k_head[k_off + d];
660 }
661 }
662 #[cfg(not(target_arch = "aarch64"))]
663 {
664 for d in 0..*head_dim {
665 dot += q_head[q_off + d] * k_head[k_off + d];
666 }
667 }
668 scores[qi * s_k + ki] = dot * scale;
669 }
670 }
671 } else {
672 crate::blas::sgemm_bt(
673 &q_head,
674 &k_head,
675 &mut scores,
676 s_q,
677 *head_dim,
678 s_k,
679 scale,
680 );
681 }
682 match mask_kind {
687 rlx_ir::op::MaskKind::None => {}
688 rlx_ir::op::MaskKind::Causal => {
689 for qi in 0..s_q {
690 let abs_q = q_offset + qi;
691 for ki in (abs_q + 1)..s_k {
692 scores[qi * s_k + ki] = -1e9;
693 }
694 }
695 }
696 rlx_ir::op::MaskKind::SlidingWindow(w) => {
697 for qi in 0..s_q {
698 let abs_q = q_offset + qi;
699 let lo = abs_q.saturating_sub(*w);
700 for ki in 0..s_k {
701 if ki < lo || ki > abs_q {
702 scores[qi * s_k + ki] = -1e9;
703 }
704 }
705 }
706 }
707 rlx_ir::op::MaskKind::Custom => {
708 if mask.len() >= (bi + 1) * s_k {
709 let m = &mask[bi * s_k..(bi + 1) * s_k];
710 for qi in 0..s_q {
711 for ki in 0..s_k {
712 if m[ki] < 0.5 {
713 scores[qi * s_k + ki] = -1e9;
714 }
715 }
716 }
717 }
718 }
719 rlx_ir::op::MaskKind::Bias => {
720 let per_bh = s_q * s_k;
724 let need = (bi * *num_heads + hi + 1) * per_bh;
725 if mask.len() >= need {
726 let bias_off = (bi * *num_heads + hi) * per_bh;
727 let b = &mask[bias_off..bias_off + per_bh];
728 for i in 0..per_bh {
729 scores[i] += b[i];
730 }
731 }
732 }
733 }
734 if let Some(cap) = attn_logit_softcap {
735 if *cap > 0.0 {
736 for s in scores.iter_mut() {
737 *s = cap * (*s / cap).tanh();
738 }
739 }
740 }
741 crate::naive::softmax(&mut scores, s_q, s_k);
742 if s_q.max(s_k) <= 32 {
744 out_head.fill(0.0);
745 for qi in 0..s_q {
746 for ki in 0..s_k {
747 let sc = scores[qi * s_k + ki];
748 if sc > 1e-8 {
749 let v_off = ki * head_dim;
750 let o_off = qi * head_dim;
751 #[cfg(target_arch = "aarch64")]
752 unsafe {
753 use std::arch::aarch64::*;
754 let vsc = vdupq_n_f32(sc);
755 let chunks = head_dim / 4;
756 for c in 0..chunks {
757 let off = c * 4;
758 let vo =
759 vld1q_f32(out_head.as_ptr().add(o_off + off));
760 let vv =
761 vld1q_f32(v_head.as_ptr().add(v_off + off));
762 vst1q_f32(
763 out_head.as_mut_ptr().add(o_off + off),
764 vfmaq_f32(vo, vsc, vv),
765 );
766 }
767 }
768 #[cfg(not(target_arch = "aarch64"))]
769 for d in 0..*head_dim {
770 out_head[o_off + d] += sc * v_head[v_off + d];
771 }
772 }
773 }
774 }
775 } else {
776 crate::blas::sgemm(
777 &scores,
778 &v_head,
779 &mut out_head,
780 s_q,
781 s_k,
782 *head_dim,
783 );
784 }
785 for si in 0..s_q {
787 let off = bi * s_q * hs + si * hs + hi * head_dim;
788 output[off..off + head_dim]
789 .copy_from_slice(&out_head[si * head_dim..(si + 1) * head_dim]);
790 }
791 }
792 }
793 }
794
795 Op::Rope {
812 head_dim, n_rot, ..
813 } => {
814 let head_dim = *head_dim;
815 let n_rot = *n_rot;
816 let x = get_data(arena, external, node.inputs[0]);
817 let cos_cache = get_data(arena, external, node.inputs[1]);
818 let sin_cache = get_data(arena, external, node.inputs[2]);
819 let x_shape = &graph.node(node.inputs[0]).shape;
820 let output = get_output(arena, node_id);
821 output.copy_from_slice(x);
822
823 let rot_half = n_rot / 2;
824 let tab_half = head_dim / 2;
825 let total = output.len();
826 let num_chunks = total / head_dim;
827
828 let cos_rows = cos_cache.len() / tab_half.max(1);
831 let (s_dim, heads_per_seq): (usize, usize) = {
832 let rank = x_shape.rank();
833 if rank == 0 {
834 (1, 1)
835 } else {
836 let last = if x_shape.dim(rank - 1).is_static() {
837 x_shape.dim(rank - 1).unwrap_static()
838 } else {
839 head_dim
840 };
841 if rank >= 3 && last > head_dim && last.is_multiple_of(head_dim) {
842 let s = if x_shape.dim(rank - 2).is_static() {
844 x_shape.dim(rank - 2).unwrap_static()
845 } else {
846 1
847 };
848 (s, last / head_dim)
849 } else if rank >= 4 && last == head_dim {
850 let s = if x_shape.dim(rank - 2).is_static() {
852 x_shape.dim(rank - 2).unwrap_static()
853 } else {
854 1
855 };
856 (s, 1)
857 } else if rank >= 3 && last == head_dim {
858 let s = if x_shape.dim(rank - 2).is_static() {
860 x_shape.dim(rank - 2).unwrap_static()
861 } else {
862 1
863 };
864 (s, 1)
865 } else {
866 (cos_rows.max(1), 1)
868 }
869 }
870 };
871
872 if std::env::var("RLX_ROPE_DEBUG").is_ok() {
873 eprintln!(
874 "[rope] shape={:?} num_chunks={num_chunks} cos_rows={cos_rows} s_dim={s_dim} heads_per_seq={heads_per_seq}",
875 x_shape.dims()
876 );
877 }
878 let total_tokens = num_chunks / heads_per_seq.max(1);
881 for chunk in 0..num_chunks {
882 let off = chunk * head_dim;
883 let token = if heads_per_seq > 1 {
887 chunk / heads_per_seq
888 } else {
889 chunk
890 };
891 let pos = if cos_rows == 1 {
892 0
894 } else if cos_rows == total_tokens && total_tokens != s_dim {
895 token.min(cos_rows - 1)
900 } else {
901 (token % s_dim).min(cos_rows.saturating_sub(1))
903 };
904 if std::env::var("RLX_ROPE_DEBUG").is_ok() && chunk < 4 {
905 eprintln!("[rope] chunk={chunk} pos={pos}");
906 }
907 let cos_off = pos * tab_half;
908
909 for i in 0..rot_half {
910 let cos_v = cos_cache[cos_off + i];
911 let sin_v = sin_cache[cos_off + i];
912 let x1 = output[off + i];
913 let x2 = output[off + rot_half + i];
914 output[off + i] = x1 * cos_v - x2 * sin_v;
915 output[off + rot_half + i] = x2 * cos_v + x1 * sin_v;
916 }
917 output[(n_rot + off)..(head_dim + off)]
918 .copy_from_slice(&x[(n_rot + off)..(head_dim + off)]);
919 }
920 }
921
922 Op::Compare(cmp) => {
924 let lhs = get_data(arena, external, node.inputs[0]);
925 let rhs = get_data(arena, external, node.inputs[1]);
926 let output = get_output(arena, node_id);
927 let rhs_len = rhs.len();
928 for i in 0..output.len() {
929 let a = lhs[i];
930 let b = rhs[i % rhs_len];
931 output[i] = if compare_op(*cmp, a, b) { 1.0 } else { 0.0 };
932 }
933 }
934
935 Op::Where => {
937 let cond = get_data(arena, external, node.inputs[0]);
938 let on_true = get_data(arena, external, node.inputs[1]);
939 let on_false = get_data(arena, external, node.inputs[2]);
940 let output = get_output(arena, node_id);
941 for i in 0..output.len() {
942 output[i] = if cond[i] > 0.5 {
943 on_true[i]
944 } else {
945 on_false[i]
946 };
947 }
948 }
949
950 Op::Reduce {
952 op: reduce_op,
953 axes,
954 keep_dim: _,
955 } => {
956 let input = get_data(arena, external, node.inputs[0]);
957 let output = get_output(arena, node_id);
958 output.fill(0.0);
959 if axes.len() == 1 {
961 let in_shape = &graph.node(node.inputs[0]).shape;
962 let axis = axes[0];
963 let rank = in_shape.rank();
964 let outer: usize = (0..axis)
965 .map(|i| in_shape.dim(i).unwrap_static())
966 .product::<usize>()
967 .max(1);
968 let axis_size = in_shape.dim(axis).unwrap_static();
969 let inner: usize = (axis + 1..rank)
970 .map(|i| in_shape.dim(i).unwrap_static())
971 .product::<usize>()
972 .max(1);
973
974 match reduce_op {
975 ReduceOp::Sum | ReduceOp::Mean => {
976 for o in 0..outer {
977 for i in 0..inner {
978 let mut acc = 0f32;
979 for a in 0..axis_size {
980 acc += input[o * axis_size * inner + a * inner + i];
981 }
982 if matches!(reduce_op, ReduceOp::Mean) {
983 acc /= axis_size as f32;
984 }
985 output[o * inner + i] = acc;
986 }
987 }
988 }
989 ReduceOp::Max => {
990 output.fill(f32::NEG_INFINITY);
991 for o in 0..outer {
992 for i in 0..inner {
993 for a in 0..axis_size {
994 let v = input[o * axis_size * inner + a * inner + i];
995 let idx = o * inner + i;
996 if v > output[idx] {
997 output[idx] = v;
998 }
999 }
1000 }
1001 }
1002 }
1003 _ => {} }
1005 }
1006 }
1007
1008 Op::Cast { .. } => {
1010 let input = get_data(arena, external, node.inputs[0]);
1011 let output = get_output(arena, node_id);
1012 output[..input.len()].copy_from_slice(input);
1013 }
1014
1015 Op::FusedSwiGLU { cast_to: _, .. } => {
1023 let input = get_data(arena, external, node.inputs[0]);
1024 let output = get_output(arena, node_id);
1025 let n = node.shape.dim(node.shape.rank() - 1).unwrap_static();
1029 let outer = output.len() / n;
1030 debug_assert_eq!(
1031 outer * 2 * n,
1032 input.len(),
1033 "FusedSwiGLU: input/output shape mismatch"
1034 );
1035 for o in 0..outer {
1036 let in_row = &input[o * 2 * n..(o + 1) * 2 * n];
1037 let out_row = &mut output[o * n..(o + 1) * n];
1038 for i in 0..n {
1039 let up = in_row[i];
1040 let gate = in_row[n + i];
1041 let silu_gate = gate / (1.0 + (-gate).exp());
1042 out_row[i] = up * silu_gate;
1043 }
1044 }
1045 }
1046
1047 Op::DenseSolve => {
1049 let a_shape = &graph.node(node.inputs[0]).shape;
1050 let n = a_shape.dim(0).unwrap_static();
1051 let b_elems = node.shape.num_elements().unwrap();
1052 let nrhs = b_elems / n.max(1);
1053 match node.shape.dtype() {
1054 rlx_ir::DType::F32 => {
1055 let a = get_data(arena, external, node.inputs[0]);
1056 let b = get_data(arena, external, node.inputs[1]);
1057 let x = get_output(arena, node_id);
1058 let mut a_scratch = a.to_vec();
1059 let mut x_buf = b.to_vec();
1060 let info = crate::blas::sgesv(&mut a_scratch, &mut x_buf, n, nrhs);
1061 if info != 0 {
1062 panic!("DenseSolve: singular matrix (info={info})");
1063 }
1064 x[..x_buf.len()].copy_from_slice(&x_buf);
1065 }
1066 rlx_ir::DType::F64 => {
1067 let (a_ptr, a_len) = arena.raw_ptr(node.inputs[0]);
1068 let (b_ptr, b_len) = arena.raw_ptr(node.inputs[1]);
1069 let (x_ptr, x_len) = arena.raw_ptr(node_id);
1070 unsafe {
1071 let a_src = std::slice::from_raw_parts(a_ptr as *const f64, a_len / 8);
1072 let b_src = std::slice::from_raw_parts(b_ptr as *const f64, b_len / 8);
1073 let mut a_scratch = a_src.to_vec();
1074 let mut x_buf = b_src.to_vec();
1075 let info = crate::blas::dgesv(&mut a_scratch, &mut x_buf, n, nrhs);
1076 if info != 0 {
1077 panic!("DenseSolve: singular matrix (info={info})");
1078 }
1079 std::slice::from_raw_parts_mut(x_ptr as *mut f64, x_len / 8)
1080 .copy_from_slice(&x_buf);
1081 }
1082 }
1083 other => panic!("DenseSolve executor: unsupported dtype {other:?}"),
1084 }
1085 }
1086
1087 _ => {
1089 if !node.inputs.is_empty() && arena.has_buffer(node_id) {
1090 let input = get_data(arena, external, node.inputs[0]);
1091 let output = get_output(arena, node_id);
1092 let len = output.len().min(input.len());
1093 output[..len].copy_from_slice(&input[..len]);
1094 }
1095 }
1096 }
1097
1098 if scanner.enabled() {
1102 scan_node_for_nans(&scanner, graph, node_id, arena, external);
1103 }
1104 }
1105}
1106
1107pub(crate) fn scan_node_for_nans(
1116 scanner: &rlx_ir::numeric_check::DebugScanner,
1117 graph: &Graph,
1118 node_id: NodeId,
1119 arena: &Arena,
1120 external: &ExternalBuffers,
1121) -> Option<rlx_ir::numeric_check::NanReport> {
1122 let node = graph.node(node_id);
1123 if node.shape.dtype() != rlx_ir::DType::F32 || !arena.has_buffer(node_id) {
1124 return None;
1125 }
1126 let out = get_data(arena, external, node_id);
1127 let mut inbufs: Vec<(NodeId, &[f32])> = Vec::with_capacity(node.inputs.len());
1128 for &inp in &node.inputs {
1129 if graph.node(inp).shape.dtype() == rlx_ir::DType::F32
1130 && (external.buffers.contains_key(&inp) || arena.has_buffer(inp))
1131 {
1132 inbufs.push((inp, get_data(arena, external, inp)));
1133 }
1134 }
1135 scanner.check(graph, node_id, out, &inbufs)
1136}
1137
1138fn get_data<'a>(arena: &'a Arena, external: &'a ExternalBuffers, id: NodeId) -> &'a [f32] {
1142 if let Some(&ext) = external.buffers.get(&id) {
1145 ext
1146 } else if arena.has_buffer(id) {
1147 let (ptr, len) = arena.raw_ptr(id);
1148 unsafe { std::slice::from_raw_parts(ptr, len) }
1149 } else {
1150 panic!("no data for node {id}")
1151 }
1152}
1153
1154#[allow(clippy::mut_from_ref)]
1160fn get_output(arena: &Arena, id: NodeId) -> &mut [f32] {
1161 let (ptr, len) = arena.raw_ptr(id);
1162 unsafe { std::slice::from_raw_parts_mut(ptr, len) }
1163}
1164
1165#[inline]
1167fn matmul(a: &[f32], b: &[f32], c: &mut [f32], m: usize, k: usize, n: usize) {
1168 crate::blas::sgemm(a, b, c, m, k, n);
1170}
1171
1172fn binary_op(op: rlx_ir::op::BinaryOp, a: f32, b: f32) -> f32 {
1173 use rlx_ir::op::BinaryOp::*;
1174 match op {
1175 Add => a + b,
1176 Sub => a - b,
1177 Mul => a * b,
1178 Div => a / b,
1179 Max => a.max(b),
1180 Min => a.min(b),
1181 Pow => a.powf(b),
1182 }
1183}
1184
1185fn compare_op(op: rlx_ir::op::CmpOp, a: f32, b: f32) -> bool {
1186 use rlx_ir::op::CmpOp::*;
1187 match op {
1188 Eq => a == b,
1189 Ne => a != b,
1190 Lt => a < b,
1191 Le => a <= b,
1192 Gt => a > b,
1193 Ge => a >= b,
1194 }
1195}
1196
1197#[allow(dead_code)]
1199fn scalar_gelu(x: f32) -> f32 {
1200 let sign = if x >= 0.0 { 1.0f32 } else { -1.0 };
1201 let xa = x.abs();
1202 let t = 1.0 / (1.0 + 0.3275911 * xa);
1203 let y = t
1204 * (0.254_829_6
1205 + t * (-0.284_496_72 + t * (1.421_413_8 + t * (-1.453_152_1 + t * 1.061_405_4))));
1206 let erf = sign * (1.0 - y * (-xa * xa).exp());
1207 x * 0.5 * (1.0 + erf)
1208}
1209
1210#[cfg(test)]
1211mod tests {
1212 use super::*;
1213 use rlx_ir::*;
1214
1215 use rlx_opt::fusion::FuseMatMulBiasAct;
1216 use rlx_opt::memory;
1217 use rlx_opt::pass::Pass;
1218
1219 #[test]
1221 fn execute_fused_matmul_bias_gelu() {
1222 let mut g = Graph::new("test");
1224 let x_id = g.input("x", Shape::new(&[2, 4], DType::F32));
1225 let w_id = g.param("w", Shape::new(&[4, 3], DType::F32));
1226 let b_id = g.param("b", Shape::new(&[3], DType::F32));
1227 let mm = g.matmul(x_id, w_id, Shape::new(&[2, 3], DType::F32));
1228 let add = g.binary(BinaryOp::Add, mm, b_id, Shape::new(&[2, 3], DType::F32));
1229 let out = g.activation(Activation::Gelu, add, Shape::new(&[2, 3], DType::F32));
1230 g.set_outputs(vec![out]);
1231
1232 let fused = FuseMatMulBiasAct.run(g);
1234 println!("{fused}");
1235
1236 let plan = memory::plan_memory(&fused);
1238 println!("Arena: {} bytes", plan.arena_size);
1239
1240 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 {
1246 buffers: HashMap::new(),
1247 };
1248 ext.buffers.insert(fused.outputs[0], &[]); for node in fused.nodes() {
1251 match &node.op {
1252 Op::Input { name } if name == "x" => {
1253 ext.buffers.insert(node.id, &x_data);
1254 }
1255 Op::Param { name } if name == "w" => {
1256 ext.buffers.insert(node.id, &w_data);
1257 }
1258 Op::Param { name } if name == "b" => {
1259 ext.buffers.insert(node.id, &b_data);
1260 }
1261 _ => {}
1262 }
1263 }
1264
1265 let mut arena = Arena::from_plan(plan);
1267 execute(&fused, &mut arena, &ext);
1268
1269 let output_id = fused.outputs[0];
1271 let result = arena.slice(output_id);
1272 println!("Result: {result:?}");
1273
1274 assert!((result[0] - 1.399).abs() < 0.01, "got {}", result[0]);
1278 assert!((result[1] - -0.154).abs() < 0.01, "got {}", result[1]);
1279 assert!((result[2] - 0.0).abs() < 0.01, "got {}", result[2]);
1280 assert!((result[3] - 0.346).abs() < 0.01, "got {}", result[3]);
1281 }
1282
1283 #[test]
1285 fn execute_gather() {
1286 use rlx_ir::infer::GraphExt;
1287 let mut g = Graph::new("gather_test");
1288 let table = g.param("table", Shape::new(&[4, 3], DType::F32));
1290 let indices = g.input("ids", Shape::new(&[2], DType::F32)); let out = g.gather_(table, indices, 0);
1292 g.set_outputs(vec![out]);
1293
1294 let plan = memory::plan_memory(&g);
1295 let mut arena = Arena::from_plan(plan);
1296
1297 let table_data = vec![
1298 10.0, 11.0, 12.0, 20.0, 21.0, 22.0, 30.0, 31.0, 32.0, 40.0, 41.0, 42.0, ];
1303 let ids_data = vec![2.0, 0.0]; let mut ext = ExternalBuffers {
1306 buffers: HashMap::new(),
1307 };
1308 for node in g.nodes() {
1309 match &node.op {
1310 Op::Param { name } if name == "table" => {
1311 ext.buffers.insert(node.id, &table_data);
1312 }
1313 Op::Input { name } if name == "ids" => {
1314 ext.buffers.insert(node.id, &ids_data);
1315 }
1316 _ => {}
1317 }
1318 }
1319
1320 execute(&g, &mut arena, &ext);
1321 let result = arena.slice(g.outputs[0]);
1322 assert_eq!(&result[..3], &[30.0, 31.0, 32.0]); assert_eq!(&result[3..6], &[10.0, 11.0, 12.0]); }
1325
1326 #[test]
1328 fn execute_narrow() {
1329 use rlx_ir::infer::GraphExt;
1330 let mut g = Graph::new("narrow_test");
1331 let x = g.input("x", Shape::new(&[2, 6], DType::F32));
1332 let sliced = g.narrow_(x, 1, 2, 3); g.set_outputs(vec![sliced]);
1334
1335 let plan = memory::plan_memory(&g);
1336 let mut arena = Arena::from_plan(plan);
1337
1338 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];
1339 let mut ext = ExternalBuffers {
1340 buffers: HashMap::new(),
1341 };
1342 for node in g.nodes() {
1343 if let Op::Input { .. } = &node.op {
1344 ext.buffers.insert(node.id, &data);
1345 }
1346 }
1347
1348 execute(&g, &mut arena, &ext);
1349 let result = arena.slice(g.outputs[0]);
1350 assert_eq!(result, &[2.0, 3.0, 4.0, 8.0, 9.0, 10.0]);
1351 }
1352
1353 #[test]
1355 fn execute_softmax() {
1356 use rlx_ir::infer::GraphExt;
1357 let mut g = Graph::new("softmax_test");
1358 let x = g.input("x", Shape::new(&[1, 4], DType::F32));
1359 let sm = g.sm(x, -1);
1360 g.set_outputs(vec![sm]);
1361
1362 let plan = memory::plan_memory(&g);
1363 let mut arena = Arena::from_plan(plan);
1364
1365 let data = vec![1.0, 2.0, 3.0, 4.0];
1366 let mut ext = ExternalBuffers {
1367 buffers: HashMap::new(),
1368 };
1369 for node in g.nodes() {
1370 if let Op::Input { .. } = &node.op {
1371 ext.buffers.insert(node.id, &data);
1372 }
1373 }
1374
1375 execute(&g, &mut arena, &ext);
1376 let result = arena.slice(g.outputs[0]);
1377 let sum: f32 = result.iter().sum();
1378 assert!(
1379 (sum - 1.0).abs() < 1e-5,
1380 "softmax should sum to 1, got {sum}"
1381 );
1382 assert!(result[0] < result[1]);
1384 assert!(result[1] < result[2]);
1385 assert!(result[2] < result[3]);
1386 }
1387
1388 #[test]
1390 fn execute_rope() {
1391 use rlx_ir::infer::GraphExt;
1392 let head_dim = 4;
1393 let half = head_dim / 2;
1394 let seq = 2;
1395
1396 let mut g = Graph::new("rope_test");
1397 let x = g.input("x", Shape::new(&[seq, head_dim], DType::F32));
1399 let cos = g.param("cos", Shape::new(&[seq, half], DType::F32));
1400 let sin = g.param("sin", Shape::new(&[seq, half], DType::F32));
1401 let rotated = g.rope(x, cos, sin, head_dim);
1402 g.set_outputs(vec![rotated]);
1403
1404 let plan = memory::plan_memory(&g);
1405 let mut arena = Arena::from_plan(plan);
1406
1407 let x_data = vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0f32];
1409 let cos_data = vec![1.0, 0.0, 0.0, 1.0f32];
1411 let sin_data = vec![0.0, 1.0, 1.0, 0.0f32];
1412
1413 let mut ext = ExternalBuffers {
1414 buffers: HashMap::new(),
1415 };
1416 for node in g.nodes() {
1417 match &node.op {
1418 Op::Input { name } if name == "x" => {
1419 ext.buffers.insert(node.id, &x_data);
1420 }
1421 Op::Param { name } if name == "cos" => {
1422 ext.buffers.insert(node.id, &cos_data);
1423 }
1424 Op::Param { name } if name == "sin" => {
1425 ext.buffers.insert(node.id, &sin_data);
1426 }
1427 _ => {}
1428 }
1429 }
1430
1431 execute(&g, &mut arena, &ext);
1432 let result = arena.slice(g.outputs[0]);
1433
1434 assert!((result[0] - 1.0).abs() < 1e-5, "pos0[0]={}", result[0]);
1440 assert!((result[1] - -1.0).abs() < 1e-5, "pos0[1]={}", result[1]);
1441 assert!((result[2] - 0.0).abs() < 1e-5, "pos0[2]={}", result[2]);
1442 assert!((result[3] - 0.0).abs() < 1e-5, "pos0[3]={}", result[3]);
1443
1444 assert!((result[4] - 0.0).abs() < 1e-5, "pos1[0]={}", result[4]);
1448 assert!((result[5] - 1.0).abs() < 1e-5, "pos1[1]={}", result[5]);
1449 assert!((result[6] - 1.0).abs() < 1e-5, "pos1[2]={}", result[6]);
1450 assert!((result[7] - 0.0).abs() < 1e-5, "pos1[3]={}", result[7]);
1451 }
1452
1453 #[test]
1457 fn epilogue_localizes_sqrt_of_negative() {
1458 use rlx_ir::op::Activation;
1459 let mut g = Graph::new("nan_localize");
1460 let x = g.input("x", Shape::new(&[2], DType::F32));
1461 let s = g.activation(Activation::Sqrt, x, Shape::new(&[2], DType::F32));
1462 g.set_outputs(vec![s]);
1463
1464 let plan = memory::plan_memory(&g);
1465 let mut arena = Arena::from_plan(plan);
1466 let data = vec![-1.0f32, 4.0]; let mut ext = ExternalBuffers {
1468 buffers: HashMap::new(),
1469 };
1470 for node in g.nodes() {
1471 if let Op::Input { .. } = &node.op {
1472 ext.buffers.insert(node.id, &data);
1473 }
1474 }
1475
1476 execute(&g, &mut arena, &ext);
1477
1478 let scanner = rlx_ir::numeric_check::DebugScanner::with_mode(
1480 rlx_ir::numeric_check::DebugMode::Warn,
1481 "cpu",
1482 );
1483 let report = super::scan_node_for_nans(&scanner, &g, g.outputs[0], &arena, &ext)
1484 .expect("sqrt(-1) must be localized as a NaN");
1485 assert_eq!(report.node, s);
1486 assert!(report.op.contains("Sqrt"), "op tag was {}", report.op);
1487 assert!(
1488 report.source_input.is_none(),
1489 "finite input → Sqrt is the culprit, not a propagator"
1490 );
1491 assert!(report.fix.is_some(), "culprit should carry a fix hint");
1492
1493 assert!(super::scan_node_for_nans(&scanner, &g, x, &arena, &ext).is_none());
1495 }
1496
1497 #[test]
1499 fn execute_layer_norm() {
1500 use rlx_ir::infer::GraphExt;
1501 let mut g = Graph::new("ln_test");
1502 let x = g.input("x", Shape::new(&[1, 4], DType::F32));
1503 let gamma = g.param("g", Shape::new(&[4], DType::F32));
1504 let beta = g.param("b", Shape::new(&[4], DType::F32));
1505 let ln = g.ln(x, gamma, beta, 1e-5);
1506 g.set_outputs(vec![ln]);
1507
1508 let plan = memory::plan_memory(&g);
1509 let mut arena = Arena::from_plan(plan);
1510
1511 let x_data = vec![1.0, 2.0, 3.0, 4.0];
1512 let g_data = vec![1.0, 1.0, 1.0, 1.0];
1513 let b_data = vec![0.0, 0.0, 0.0, 0.0];
1514
1515 let mut ext = ExternalBuffers {
1516 buffers: HashMap::new(),
1517 };
1518 for node in g.nodes() {
1519 match &node.op {
1520 Op::Input { name } if name == "x" => {
1521 ext.buffers.insert(node.id, &x_data);
1522 }
1523 Op::Param { name } if name == "g" => {
1524 ext.buffers.insert(node.id, &g_data);
1525 }
1526 Op::Param { name } if name == "b" => {
1527 ext.buffers.insert(node.id, &b_data);
1528 }
1529 _ => {}
1530 }
1531 }
1532
1533 execute(&g, &mut arena, &ext);
1534 let result = arena.slice(g.outputs[0]);
1535 let sum: f32 = result.iter().sum();
1536 assert!(
1537 sum.abs() < 1e-3,
1538 "LN output should be zero-centered, sum={sum}"
1539 );
1540 }
1541}