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 for &node_id in &schedule {
44 let node = graph.node(node_id);
45
46 match &node.op {
47 Op::Input { .. } | Op::Param { .. } | Op::Constant { .. } => {}
50
51 Op::FusedMatMulBiasAct { activation } => {
53 let input_id = node.inputs[0];
54 let weight_id = node.inputs[1];
55 let bias_id = node.inputs[2];
56
57 let input = get_data(arena, external, input_id);
58 let weight = get_data(arena, external, weight_id);
59 let bias = get_data(arena, external, bias_id);
60 let output = get_output(arena, node_id);
61
62 let shape = &node.shape;
64 let n = shape.dim(shape.rank() - 1).unwrap_static();
65 let m = shape.num_elements().unwrap() / n;
66 let k = input.len() / m;
67
68 matmul(input, weight, output, m, k, n);
72
73 match activation {
75 Some(Activation::Gelu) => kernels::par_bias_gelu(output, bias, m, n),
76 Some(Activation::Silu) => {
77 crate::blas::bias_add(output, bias, m, n);
78 kernels::silu_inplace(output);
79 }
80 _ => crate::blas::bias_add(output, bias, m, n),
81 }
82 }
83
84 Op::FusedResidualLN { has_bias, eps } => {
86 let x_id = node.inputs[0];
87 let residual_id = node.inputs[1];
88 let h = node.shape.dim(node.shape.rank() - 1).unwrap_static();
89 let zero_bias = vec![0f32; h];
90 let (gamma_id, beta_id, bias_slice) = if *has_bias {
91 let b = get_data(arena, external, node.inputs[2]);
92 (node.inputs[3], node.inputs[4], b)
93 } else {
94 (node.inputs[2], node.inputs[3], zero_bias.as_slice())
95 };
96
97 let x = get_data(arena, external, x_id);
98 let residual = get_data(arena, external, residual_id);
99 let gamma = get_data(arena, external, gamma_id);
100 let beta = get_data(arena, external, beta_id);
101 let output = get_output(arena, node_id);
102
103 let n = x.len() / h;
104
105 let x_ptr = x.as_ptr() as usize;
107 let r_ptr = residual.as_ptr() as usize;
108 let o_ptr = output.as_mut_ptr() as usize;
109 let bi_ptr = bias_slice.as_ptr() as usize;
110 let g_ptr = gamma.as_ptr() as usize;
111 let b_ptr = beta.as_ptr() as usize;
112 let e = *eps;
113 crate::pool::par_for(n, 4, &|off, cnt| unsafe {
114 let x_s =
115 std::slice::from_raw_parts((x_ptr as *const f32).add(off * h), cnt * h);
116 let r_s =
117 std::slice::from_raw_parts((r_ptr as *const f32).add(off * h), cnt * h);
118 let o_s =
119 std::slice::from_raw_parts_mut((o_ptr as *mut f32).add(off * h), cnt * h);
120 let bi = std::slice::from_raw_parts(bi_ptr as *const f32, h);
121 let g = std::slice::from_raw_parts(g_ptr as *const f32, h);
122 let b = std::slice::from_raw_parts(b_ptr as *const f32, h);
123 kernels::residual_bias_layer_norm(x_s, r_s, bi, g, b, o_s, cnt, h, e);
124 });
125 }
126
127 Op::FusedResidualRmsNorm { has_bias, eps } => {
129 let x_id = node.inputs[0];
130 let residual_id = node.inputs[1];
131 let h = node.shape.dim(node.shape.rank() - 1).unwrap_static();
132 let zero_bias = vec![0f32; h];
133 let (gamma_id, beta_id, bias_slice) = if *has_bias {
134 let b = get_data(arena, external, node.inputs[2]);
135 (node.inputs[3], node.inputs[4], b)
136 } else {
137 (node.inputs[2], node.inputs[3], zero_bias.as_slice())
138 };
139
140 let x = get_data(arena, external, x_id);
141 let residual = get_data(arena, external, residual_id);
142 let gamma = get_data(arena, external, gamma_id);
143 let beta = get_data(arena, external, beta_id);
144 let output = get_output(arena, node_id);
145
146 let n = x.len() / h;
147
148 let x_ptr = x.as_ptr() as usize;
149 let r_ptr = residual.as_ptr() as usize;
150 let o_ptr = output.as_mut_ptr() as usize;
151 let bi_ptr = bias_slice.as_ptr() as usize;
152 let g_ptr = gamma.as_ptr() as usize;
153 let b_ptr = beta.as_ptr() as usize;
154 let e = *eps;
155 crate::pool::par_for(n, 4, &|off, cnt| unsafe {
156 let x_s =
157 std::slice::from_raw_parts((x_ptr as *const f32).add(off * h), cnt * h);
158 let r_s =
159 std::slice::from_raw_parts((r_ptr as *const f32).add(off * h), cnt * h);
160 let o_s =
161 std::slice::from_raw_parts_mut((o_ptr as *mut f32).add(off * h), cnt * h);
162 let bi = std::slice::from_raw_parts(bi_ptr as *const f32, h);
163 let g = std::slice::from_raw_parts(g_ptr as *const f32, h);
164 let b = std::slice::from_raw_parts(b_ptr as *const f32, h);
165 kernels::residual_bias_rms_norm(x_s, r_s, bi, g, b, o_s, cnt, h, e);
166 });
167 }
168
169 Op::MatMul => {
171 let lhs = get_data(arena, external, node.inputs[0]);
172 let rhs = get_data(arena, external, node.inputs[1]);
173 let output = get_output(arena, node_id);
174
175 let shape = &node.shape;
176 let lhs_shape = &graph.node(node.inputs[0]).shape;
177 let rhs_shape = &graph.node(node.inputs[1]).shape;
178 let n = shape.dim(shape.rank() - 1).unwrap_static();
179 let out_m_inner = shape.dim(shape.rank() - 2).unwrap_static();
180 let k = lhs_shape.dim(lhs_shape.rank() - 1).unwrap_static();
181
182 let total = shape.num_elements().unwrap();
185 let per_batch_out = out_m_inner * n;
186 let batches = total / per_batch_out;
187
188 if batches == 1 {
189 matmul(lhs, rhs, output, out_m_inner, k, n);
190 } else {
191 let lhs_batched =
192 lhs_shape.num_elements().unwrap_or(0) == batches * out_m_inner * k;
193 let rhs_batched = rhs_shape.num_elements().unwrap_or(0) == batches * k * n;
194 for b in 0..batches {
195 let l_off = if lhs_batched { b * out_m_inner * k } else { 0 };
196 let r_off = if rhs_batched { b * k * n } else { 0 };
197 let o_off = b * out_m_inner * n;
198 let l_slice = &lhs[l_off..l_off + out_m_inner * k];
199 let r_slice = &rhs[r_off..r_off + k * n];
200 let o_slice = &mut output[o_off..o_off + out_m_inner * n];
201 matmul(l_slice, r_slice, o_slice, out_m_inner, k, n);
202 }
203 }
204 }
205
206 Op::Binary(op) => {
208 let lhs = get_data(arena, external, node.inputs[0]);
209 let rhs = get_data(arena, external, node.inputs[1]);
210 let output = get_output(arena, node_id);
211 let len = output.len();
212 let rhs_len = rhs.len();
213
214 if matches!(op, BinaryOp::Add) && rhs_len < len && len.is_multiple_of(rhs_len) {
216 output.copy_from_slice(lhs);
217 crate::blas::bias_add(output, rhs, len / rhs_len, rhs_len);
218 } else if rhs_len == len {
219 for i in 0..len {
220 output[i] = binary_op(*op, lhs[i], rhs[i]);
221 }
222 } else {
223 for i in 0..len {
224 output[i] = binary_op(*op, lhs[i], rhs[i % rhs_len]);
225 }
226 }
227 }
228
229 Op::Activation(act) => {
231 let input = get_data(arena, external, node.inputs[0]);
232 let output = get_output(arena, node_id);
233 output.copy_from_slice(input);
234 let zeros = vec![0f32; node.shape.dim(node.shape.rank() - 1).unwrap_static()];
235 let m = output.len() / zeros.len();
236 let n = zeros.len();
237 match act {
238 Activation::Gelu => kernels::par_bias_gelu(output, &zeros, m, n),
239 Activation::Silu => kernels::silu_inplace(output),
240 Activation::Relu => {
241 for v in output.iter_mut() {
242 *v = v.max(0.0);
243 }
244 }
245 Activation::Exp => {
246 for v in output.iter_mut() {
247 *v = v.exp();
248 }
249 }
250 Activation::Sqrt => {
251 for v in output.iter_mut() {
252 *v = v.sqrt();
253 }
254 }
255 Activation::Neg => {
256 for v in output.iter_mut() {
257 *v = -*v;
258 }
259 }
260 Activation::Tanh => {
261 for v in output.iter_mut() {
262 *v = v.tanh();
263 }
264 }
265 Activation::Sigmoid => {
266 for v in output.iter_mut() {
267 *v = 1.0 / (1.0 + (-*v).exp());
268 }
269 }
270 _ => {}
271 }
272 }
273
274 Op::Gather { axis } => {
276 let table = get_data(arena, external, node.inputs[0]);
277 let indices = get_data(arena, external, node.inputs[1]);
278 let output = get_output(arena, node_id);
279
280 let table_shape = &graph.node(node.inputs[0]).shape;
281 let _out_shape = &node.shape;
282
283 if *axis == 0 {
285 let trailing: usize = (1..table_shape.rank())
286 .map(|i| table_shape.dim(i).unwrap_static())
287 .product();
288 for (i, &idx_f32) in indices.iter().enumerate() {
289 let idx = idx_f32 as usize;
290 let src = idx * trailing;
291 let dst = i * trailing;
292 output[dst..dst + trailing].copy_from_slice(&table[src..src + trailing]);
293 }
294 } else {
295 let rank = table_shape.rank();
299 let outer: usize = (0..*axis)
300 .map(|i| table_shape.dim(i).unwrap_static())
301 .product();
302 let axis_size = table_shape.dim(*axis).unwrap_static();
303 let inner: usize = (*axis + 1..rank)
304 .map(|i| table_shape.dim(i).unwrap_static())
305 .product();
306 let n_idx = indices.len();
307 for o in 0..outer {
308 for (k, &idx_f32) in indices.iter().enumerate() {
309 let idx = (idx_f32 as usize).min(axis_size.saturating_sub(1));
310 let src = (o * axis_size + idx) * inner;
311 let dst = (o * n_idx + k) * inner;
312 output[dst..dst + inner].copy_from_slice(&table[src..src + inner]);
313 }
314 }
315 }
316 }
317
318 Op::Narrow { axis, start, len } => {
320 let input = get_data(arena, external, node.inputs[0]);
321 let output = get_output(arena, node_id);
322 let in_shape = &graph.node(node.inputs[0]).shape;
323
324 let rank = in_shape.rank();
325 let outer: usize = (0..*axis)
326 .map(|i| in_shape.dim(i).unwrap_static())
327 .product::<usize>()
328 .max(1);
329 let inner: usize = (*axis + 1..rank)
330 .map(|i| in_shape.dim(i).unwrap_static())
331 .product::<usize>()
332 .max(1);
333 let in_axis_size = in_shape.dim(*axis).unwrap_static();
334
335 for o in 0..outer {
336 for s in 0..*len {
337 let src_off = o * in_axis_size * inner + (*start + s) * inner;
338 let dst_off = o * len * inner + s * inner;
339 output[dst_off..dst_off + inner]
340 .copy_from_slice(&input[src_off..src_off + inner]);
341 }
342 }
343 }
344
345 Op::Transpose { perm } => {
347 let input = get_data(arena, external, node.inputs[0]);
348 let output = get_output(arena, node_id);
349 let in_shape = &graph.node(node.inputs[0]).shape;
350 let rank = in_shape.rank();
351
352 let in_dims: Vec<usize> =
353 (0..rank).map(|i| in_shape.dim(i).unwrap_static()).collect();
354 let out_dims: Vec<usize> = perm.iter().map(|&i| in_dims[i]).collect();
355
356 let mut in_strides = vec![1usize; rank];
359 for i in (0..rank - 1).rev() {
360 in_strides[i] = in_strides[i + 1] * in_dims[i + 1];
361 }
362 let mut out_strides = vec![1usize; rank];
363 for i in (0..rank - 1).rev() {
364 out_strides[i] = out_strides[i + 1] * out_dims[i + 1];
365 }
366
367 let total = output.len();
368 for flat_out in 0..total {
369 let mut in_flat = 0;
370 for d in 0..rank {
371 let coord = (flat_out / out_strides[d]) % out_dims[d];
373 in_flat += coord * in_strides[perm[d]];
375 }
376 output[flat_out] = input[in_flat];
377 }
378 }
379
380 Op::Concat { axis } => {
382 let output = get_output(arena, node_id);
383 let out_shape = &node.shape;
384 let rank = out_shape.rank();
385
386 let outer: usize = (0..*axis)
387 .map(|i| out_shape.dim(i).unwrap_static())
388 .product::<usize>()
389 .max(1);
390 let inner: usize = (*axis + 1..rank)
391 .map(|i| out_shape.dim(i).unwrap_static())
392 .product::<usize>()
393 .max(1);
394
395 let mut dst_off = 0;
396 for o in 0..outer {
397 for &inp_id in &node.inputs {
398 let inp = get_data(arena, external, inp_id);
399 let inp_shape = &graph.node(inp_id).shape;
400 let inp_axis = inp_shape.dim(*axis).unwrap_static();
401 let chunk = inp_axis * inner;
402 let src_off = o * chunk;
403 output[dst_off..dst_off + chunk]
404 .copy_from_slice(&inp[src_off..src_off + chunk]);
405 dst_off += chunk;
406 }
407 }
408 }
409
410 Op::Reshape { .. } => {
412 let input = get_data(arena, external, node.inputs[0]);
413 let output = get_output(arena, node_id);
414 output[..input.len()].copy_from_slice(input);
415 }
416 Op::Expand { .. } => {
420 let input = get_data(arena, external, node.inputs[0]);
421 let in_shape = &graph.node(node.inputs[0]).shape;
422 let out_shape = &node.shape;
423 let out_rank = out_shape.rank();
424 let pad = out_rank - in_shape.rank();
425 let out_dims: Vec<usize> = (0..out_rank)
426 .map(|i| out_shape.dim(i).unwrap_static())
427 .collect();
428 let in_dims: Vec<usize> = (0..out_rank)
429 .map(|i| {
430 if i < pad {
431 1
432 } else {
433 in_shape.dim(i - pad).unwrap_static()
434 }
435 })
436 .collect();
437 let mut in_strides = vec![0usize; out_rank];
439 let mut acc = 1usize;
440 for i in (0..out_rank).rev() {
441 in_strides[i] = if in_dims[i] == 1 { 0 } else { acc };
442 acc *= in_dims[i];
443 }
444 let output = get_output(arena, node_id);
445 let total: usize = out_dims.iter().product();
446 let mut coords = vec![0usize; out_rank];
447 for out_idx in 0..total {
448 let mut in_idx = 0usize;
449 for i in 0..out_rank {
450 in_idx += coords[i] * in_strides[i];
451 }
452 output[out_idx] = input[in_idx];
453 for i in (0..out_rank).rev() {
454 coords[i] += 1;
455 if coords[i] < out_dims[i] {
456 break;
457 }
458 coords[i] = 0;
459 }
460 }
461 }
462
463 Op::LayerNorm { eps, .. } => {
465 let input = get_data(arena, external, node.inputs[0]);
466 let gamma = get_data(arena, external, node.inputs[1]);
467 let beta = get_data(arena, external, node.inputs[2]);
468 let output = get_output(arena, node_id);
469 let h = node.shape.dim(node.shape.rank() - 1).unwrap_static();
470 let n = input.len() / h;
471 for row in 0..n {
472 let base = row * h;
473 kernels::layer_norm_row(
474 &input[base..base + h],
475 gamma,
476 beta,
477 &mut output[base..base + h],
478 h,
479 *eps,
480 );
481 }
482 }
483
484 Op::GroupNorm { num_groups, eps } => {
485 let input = get_data(arena, external, node.inputs[0]);
486 let gamma = get_data(arena, external, node.inputs[1]);
487 let beta = get_data(arena, external, node.inputs[2]);
488 let output = get_output(arena, node_id);
489 let n = node.shape.dim(0).unwrap_static();
490 let c = node.shape.dim(1).unwrap_static();
491 let h = node.shape.dim(2).unwrap_static();
492 let w = node.shape.dim(3).unwrap_static();
493 kernels::group_norm_nchw(input, gamma, beta, output, n, c, h, w, *num_groups, *eps);
494 }
495
496 Op::ResizeNearest2x => {
497 let input = get_data(arena, external, node.inputs[0]);
498 let output = get_output(arena, node_id);
499 let n = node.shape.dim(0).unwrap_static();
500 let c = node.shape.dim(1).unwrap_static();
501 let h = node.shape.dim(2).unwrap_static() / 2;
502 let w = node.shape.dim(3).unwrap_static() / 2;
503 let in_plane = c * h * w;
504 let out_plane = c * h * 2 * w * 2;
505 for ni in 0..n {
506 kernels::resize_nearest_2x_nchw(
507 &input[ni * in_plane..(ni + 1) * in_plane],
508 &mut output[ni * out_plane..(ni + 1) * out_plane],
509 c,
510 h,
511 w,
512 );
513 }
514 }
515
516 Op::AxialRope2d {
517 end_x,
518 end_y,
519 head_dim,
520 num_heads,
521 theta,
522 repeat_factor,
523 } => {
524 let input = get_data(arena, external, node.inputs[0]);
525 let output = get_output(arena, node_id);
526 let batch = node.shape.dim(0).unwrap_static();
527 let seq = node.shape.dim(1).unwrap_static();
528 let plane = seq * node.shape.dim(2).unwrap_static();
529 for bi in 0..batch {
530 let rotated = rlx_ir::ops::axial_rope2d::apply_axial_rope2d(
531 &input[bi * plane..(bi + 1) * plane],
532 *num_heads,
533 seq,
534 *head_dim,
535 *end_x,
536 *end_y,
537 *theta,
538 *repeat_factor,
539 );
540 output[bi * plane..(bi + 1) * plane].copy_from_slice(&rotated);
541 }
542 }
543
544 Op::Softmax { axis } => {
546 let input = get_data(arena, external, node.inputs[0]);
547 let output = get_output(arena, node_id);
548 output.copy_from_slice(input);
549 let rank = node.shape.rank();
550 let ax = if *axis < 0 {
551 (rank as i32 + axis) as usize
552 } else {
553 *axis as usize
554 };
555 let cols = node.shape.dim(ax).unwrap_static();
556 let rows = output.len() / cols;
557 crate::naive::softmax(output, rows, cols);
558 }
559
560 Op::Attention {
562 num_heads,
563 head_dim,
564 mask_kind,
565 score_scale,
566 attn_logit_softcap,
567 } => {
568 let q = get_data(arena, external, node.inputs[0]);
569 let k = get_data(arena, external, node.inputs[1]);
570 let v = get_data(arena, external, node.inputs[2]);
571 let mask: &[f32] = if matches!(
575 mask_kind,
576 rlx_ir::op::MaskKind::Custom | rlx_ir::op::MaskKind::Bias
577 ) {
578 get_data(arena, external, node.inputs[3])
579 } else {
580 &[]
581 };
582 let output = get_output(arena, node_id);
583
584 let q_shape = &graph.node(node.inputs[0]).shape;
585 let k_shape = &graph.node(node.inputs[1]).shape;
586 let hs = num_heads * head_dim;
587 let scale = score_scale.unwrap_or((*head_dim as f32).powf(-0.5));
588 let (batch_size, s_q) = if q_shape.rank() >= 3 {
589 (
590 q_shape.dim(0).unwrap_static(),
591 q_shape.dim(1).unwrap_static(),
592 )
593 } else {
594 (1, q_shape.dim(0).unwrap_static())
595 };
596 let s_k = if k_shape.rank() >= 3 {
601 k_shape.dim(1).unwrap_static()
602 } else {
603 k_shape.dim(0).unwrap_static()
604 };
605 let q_offset = s_k.saturating_sub(s_q);
606
607 let q_buf_len = s_q * head_dim;
609 let k_buf_len = s_k * head_dim;
610 let mut q_head = vec![0f32; q_buf_len];
611 let mut k_head = vec![0f32; k_buf_len];
612 let mut v_head = vec![0f32; k_buf_len];
613 let mut scores = vec![0f32; s_q * s_k];
614 let mut out_head = vec![0f32; q_buf_len];
615
616 for bi in 0..batch_size {
617 for hi in 0..*num_heads {
618 for si in 0..s_q {
620 let off = bi * s_q * hs + si * hs + hi * head_dim;
621 q_head[si * head_dim..(si + 1) * head_dim]
622 .copy_from_slice(&q[off..off + head_dim]);
623 }
624 for si in 0..s_k {
626 let off = bi * s_k * hs + si * hs + hi * head_dim;
627 k_head[si * head_dim..(si + 1) * head_dim]
628 .copy_from_slice(&k[off..off + head_dim]);
629 v_head[si * head_dim..(si + 1) * head_dim]
630 .copy_from_slice(&v[off..off + head_dim]);
631 }
632 if s_q.max(s_k) <= 32 {
635 for qi in 0..s_q {
636 for ki in 0..s_k {
637 let q_off = qi * head_dim;
638 let k_off = ki * head_dim;
639 #[cfg(target_arch = "aarch64")]
640 let mut dot;
641 #[cfg(not(target_arch = "aarch64"))]
642 let mut dot = 0f32;
643 #[cfg(target_arch = "aarch64")]
644 unsafe {
645 use std::arch::aarch64::*;
646 let chunks = head_dim / 4;
647 let mut acc = vdupq_n_f32(0.0);
648 for c in 0..chunks {
649 let vq = vld1q_f32(q_head.as_ptr().add(q_off + c * 4));
650 let vk = vld1q_f32(k_head.as_ptr().add(k_off + c * 4));
651 acc = vfmaq_f32(acc, vq, vk);
652 }
653 dot = vaddvq_f32(acc);
654 for d in (chunks * 4)..*head_dim {
655 dot += q_head[q_off + d] * k_head[k_off + d];
656 }
657 }
658 #[cfg(not(target_arch = "aarch64"))]
659 {
660 for d in 0..*head_dim {
661 dot += q_head[q_off + d] * k_head[k_off + d];
662 }
663 }
664 scores[qi * s_k + ki] = dot * scale;
665 }
666 }
667 } else {
668 crate::blas::sgemm_bt(
669 &q_head,
670 &k_head,
671 &mut scores,
672 s_q,
673 *head_dim,
674 s_k,
675 scale,
676 );
677 }
678 match mask_kind {
683 rlx_ir::op::MaskKind::None => {}
684 rlx_ir::op::MaskKind::Causal => {
685 for qi in 0..s_q {
686 let abs_q = q_offset + qi;
687 for ki in (abs_q + 1)..s_k {
688 scores[qi * s_k + ki] = -1e9;
689 }
690 }
691 }
692 rlx_ir::op::MaskKind::SlidingWindow(w) => {
693 for qi in 0..s_q {
694 let abs_q = q_offset + qi;
695 let lo = abs_q.saturating_sub(*w);
696 for ki in 0..s_k {
697 if ki < lo || ki > abs_q {
698 scores[qi * s_k + ki] = -1e9;
699 }
700 }
701 }
702 }
703 rlx_ir::op::MaskKind::Custom => {
704 if mask.len() >= (bi + 1) * s_k {
705 let m = &mask[bi * s_k..(bi + 1) * s_k];
706 for qi in 0..s_q {
707 for ki in 0..s_k {
708 if m[ki] < 0.5 {
709 scores[qi * s_k + ki] = -1e9;
710 }
711 }
712 }
713 }
714 }
715 rlx_ir::op::MaskKind::Bias => {
716 let per_bh = s_q * s_k;
720 let need = (bi * *num_heads + hi + 1) * per_bh;
721 if mask.len() >= need {
722 let bias_off = (bi * *num_heads + hi) * per_bh;
723 let b = &mask[bias_off..bias_off + per_bh];
724 for i in 0..per_bh {
725 scores[i] += b[i];
726 }
727 }
728 }
729 }
730 if let Some(cap) = attn_logit_softcap {
731 if *cap > 0.0 {
732 for s in scores.iter_mut() {
733 *s = cap * (*s / cap).tanh();
734 }
735 }
736 }
737 crate::naive::softmax(&mut scores, s_q, s_k);
738 if s_q.max(s_k) <= 32 {
740 out_head.fill(0.0);
741 for qi in 0..s_q {
742 for ki in 0..s_k {
743 let sc = scores[qi * s_k + ki];
744 if sc > 1e-8 {
745 let v_off = ki * head_dim;
746 let o_off = qi * head_dim;
747 #[cfg(target_arch = "aarch64")]
748 unsafe {
749 use std::arch::aarch64::*;
750 let vsc = vdupq_n_f32(sc);
751 let chunks = head_dim / 4;
752 for c in 0..chunks {
753 let off = c * 4;
754 let vo =
755 vld1q_f32(out_head.as_ptr().add(o_off + off));
756 let vv =
757 vld1q_f32(v_head.as_ptr().add(v_off + off));
758 vst1q_f32(
759 out_head.as_mut_ptr().add(o_off + off),
760 vfmaq_f32(vo, vsc, vv),
761 );
762 }
763 }
764 #[cfg(not(target_arch = "aarch64"))]
765 for d in 0..*head_dim {
766 out_head[o_off + d] += sc * v_head[v_off + d];
767 }
768 }
769 }
770 }
771 } else {
772 crate::blas::sgemm(
773 &scores,
774 &v_head,
775 &mut out_head,
776 s_q,
777 s_k,
778 *head_dim,
779 );
780 }
781 for si in 0..s_q {
783 let off = bi * s_q * hs + si * hs + hi * head_dim;
784 output[off..off + head_dim]
785 .copy_from_slice(&out_head[si * head_dim..(si + 1) * head_dim]);
786 }
787 }
788 }
789 }
790
791 Op::Rope {
808 head_dim, n_rot, ..
809 } => {
810 let head_dim = *head_dim;
811 let n_rot = *n_rot;
812 let x = get_data(arena, external, node.inputs[0]);
813 let cos_cache = get_data(arena, external, node.inputs[1]);
814 let sin_cache = get_data(arena, external, node.inputs[2]);
815 let x_shape = &graph.node(node.inputs[0]).shape;
816 let output = get_output(arena, node_id);
817 output.copy_from_slice(x);
818
819 let rot_half = n_rot / 2;
820 let tab_half = head_dim / 2;
821 let total = output.len();
822 let num_chunks = total / head_dim;
823
824 let cos_rows = cos_cache.len() / tab_half.max(1);
827 let (s_dim, heads_per_seq): (usize, usize) = {
828 let rank = x_shape.rank();
829 if rank == 0 {
830 (1, 1)
831 } else {
832 let last = if x_shape.dim(rank - 1).is_static() {
833 x_shape.dim(rank - 1).unwrap_static()
834 } else {
835 head_dim
836 };
837 if rank >= 3 && last > head_dim && last.is_multiple_of(head_dim) {
838 let s = if x_shape.dim(rank - 2).is_static() {
840 x_shape.dim(rank - 2).unwrap_static()
841 } else {
842 1
843 };
844 (s, last / head_dim)
845 } else if rank >= 4 && last == head_dim {
846 let s = if x_shape.dim(rank - 2).is_static() {
848 x_shape.dim(rank - 2).unwrap_static()
849 } else {
850 1
851 };
852 (s, 1)
853 } else if rank >= 3 && last == head_dim {
854 let s = if x_shape.dim(rank - 2).is_static() {
856 x_shape.dim(rank - 2).unwrap_static()
857 } else {
858 1
859 };
860 (s, 1)
861 } else {
862 (cos_rows.max(1), 1)
864 }
865 }
866 };
867
868 if std::env::var("RLX_ROPE_DEBUG").is_ok() {
869 eprintln!(
870 "[rope] shape={:?} num_chunks={num_chunks} cos_rows={cos_rows} s_dim={s_dim} heads_per_seq={heads_per_seq}",
871 x_shape.dims()
872 );
873 }
874 let total_tokens = num_chunks / heads_per_seq.max(1);
877 for chunk in 0..num_chunks {
878 let off = chunk * head_dim;
879 let token = if heads_per_seq > 1 {
883 chunk / heads_per_seq
884 } else {
885 chunk
886 };
887 let pos = if cos_rows == 1 {
888 0
890 } else if cos_rows == total_tokens && total_tokens != s_dim {
891 token.min(cos_rows - 1)
896 } else {
897 (token % s_dim).min(cos_rows.saturating_sub(1))
899 };
900 if std::env::var("RLX_ROPE_DEBUG").is_ok() && chunk < 4 {
901 eprintln!("[rope] chunk={chunk} pos={pos}");
902 }
903 let cos_off = pos * tab_half;
904
905 for i in 0..rot_half {
906 let cos_v = cos_cache[cos_off + i];
907 let sin_v = sin_cache[cos_off + i];
908 let x1 = output[off + i];
909 let x2 = output[off + rot_half + i];
910 output[off + i] = x1 * cos_v - x2 * sin_v;
911 output[off + rot_half + i] = x2 * cos_v + x1 * sin_v;
912 }
913 output[(n_rot + off)..(head_dim + off)]
914 .copy_from_slice(&x[(n_rot + off)..(head_dim + off)]);
915 }
916 }
917
918 Op::Compare(cmp) => {
920 let lhs = get_data(arena, external, node.inputs[0]);
921 let rhs = get_data(arena, external, node.inputs[1]);
922 let output = get_output(arena, node_id);
923 let rhs_len = rhs.len();
924 for i in 0..output.len() {
925 let a = lhs[i];
926 let b = rhs[i % rhs_len];
927 output[i] = if compare_op(*cmp, a, b) { 1.0 } else { 0.0 };
928 }
929 }
930
931 Op::Where => {
933 let cond = get_data(arena, external, node.inputs[0]);
934 let on_true = get_data(arena, external, node.inputs[1]);
935 let on_false = get_data(arena, external, node.inputs[2]);
936 let output = get_output(arena, node_id);
937 for i in 0..output.len() {
938 output[i] = if cond[i] > 0.5 {
939 on_true[i]
940 } else {
941 on_false[i]
942 };
943 }
944 }
945
946 Op::Reduce {
948 op: reduce_op,
949 axes,
950 keep_dim: _,
951 } => {
952 let input = get_data(arena, external, node.inputs[0]);
953 let output = get_output(arena, node_id);
954 output.fill(0.0);
955 if axes.len() == 1 {
957 let in_shape = &graph.node(node.inputs[0]).shape;
958 let axis = axes[0];
959 let rank = in_shape.rank();
960 let outer: usize = (0..axis)
961 .map(|i| in_shape.dim(i).unwrap_static())
962 .product::<usize>()
963 .max(1);
964 let axis_size = in_shape.dim(axis).unwrap_static();
965 let inner: usize = (axis + 1..rank)
966 .map(|i| in_shape.dim(i).unwrap_static())
967 .product::<usize>()
968 .max(1);
969
970 match reduce_op {
971 ReduceOp::Sum | ReduceOp::Mean => {
972 for o in 0..outer {
973 for i in 0..inner {
974 let mut acc = 0f32;
975 for a in 0..axis_size {
976 acc += input[o * axis_size * inner + a * inner + i];
977 }
978 if matches!(reduce_op, ReduceOp::Mean) {
979 acc /= axis_size as f32;
980 }
981 output[o * inner + i] = acc;
982 }
983 }
984 }
985 ReduceOp::Max => {
986 output.fill(f32::NEG_INFINITY);
987 for o in 0..outer {
988 for i in 0..inner {
989 for a in 0..axis_size {
990 let v = input[o * axis_size * inner + a * inner + i];
991 let idx = o * inner + i;
992 if v > output[idx] {
993 output[idx] = v;
994 }
995 }
996 }
997 }
998 }
999 _ => {} }
1001 }
1002 }
1003
1004 Op::Cast { .. } => {
1006 let input = get_data(arena, external, node.inputs[0]);
1007 let output = get_output(arena, node_id);
1008 output[..input.len()].copy_from_slice(input);
1009 }
1010
1011 Op::FusedSwiGLU { cast_to: _, .. } => {
1019 let input = get_data(arena, external, node.inputs[0]);
1020 let output = get_output(arena, node_id);
1021 let n = node.shape.dim(node.shape.rank() - 1).unwrap_static();
1025 let outer = output.len() / n;
1026 debug_assert_eq!(
1027 outer * 2 * n,
1028 input.len(),
1029 "FusedSwiGLU: input/output shape mismatch"
1030 );
1031 for o in 0..outer {
1032 let in_row = &input[o * 2 * n..(o + 1) * 2 * n];
1033 let out_row = &mut output[o * n..(o + 1) * n];
1034 for i in 0..n {
1035 let up = in_row[i];
1036 let gate = in_row[n + i];
1037 let silu_gate = gate / (1.0 + (-gate).exp());
1038 out_row[i] = up * silu_gate;
1039 }
1040 }
1041 }
1042
1043 Op::DenseSolve => {
1045 let a_shape = &graph.node(node.inputs[0]).shape;
1046 let n = a_shape.dim(0).unwrap_static();
1047 let b_elems = node.shape.num_elements().unwrap();
1048 let nrhs = b_elems / n.max(1);
1049 match node.shape.dtype() {
1050 rlx_ir::DType::F32 => {
1051 let a = get_data(arena, external, node.inputs[0]);
1052 let b = get_data(arena, external, node.inputs[1]);
1053 let x = get_output(arena, node_id);
1054 let mut a_scratch = a.to_vec();
1055 let mut x_buf = b.to_vec();
1056 let info = crate::blas::sgesv(&mut a_scratch, &mut x_buf, n, nrhs);
1057 if info != 0 {
1058 panic!("DenseSolve: singular matrix (info={info})");
1059 }
1060 x[..x_buf.len()].copy_from_slice(&x_buf);
1061 }
1062 rlx_ir::DType::F64 => {
1063 let (a_ptr, a_len) = arena.raw_ptr(node.inputs[0]);
1064 let (b_ptr, b_len) = arena.raw_ptr(node.inputs[1]);
1065 let (x_ptr, x_len) = arena.raw_ptr(node_id);
1066 unsafe {
1067 let a_src = std::slice::from_raw_parts(a_ptr as *const f64, a_len / 8);
1068 let b_src = std::slice::from_raw_parts(b_ptr as *const f64, b_len / 8);
1069 let mut a_scratch = a_src.to_vec();
1070 let mut x_buf = b_src.to_vec();
1071 let info = crate::blas::dgesv(&mut a_scratch, &mut x_buf, n, nrhs);
1072 if info != 0 {
1073 panic!("DenseSolve: singular matrix (info={info})");
1074 }
1075 std::slice::from_raw_parts_mut(x_ptr as *mut f64, x_len / 8)
1076 .copy_from_slice(&x_buf);
1077 }
1078 }
1079 other => panic!("DenseSolve executor: unsupported dtype {other:?}"),
1080 }
1081 }
1082
1083 _ => {
1085 if !node.inputs.is_empty() && arena.has_buffer(node_id) {
1086 let input = get_data(arena, external, node.inputs[0]);
1087 let output = get_output(arena, node_id);
1088 let len = output.len().min(input.len());
1089 output[..len].copy_from_slice(&input[..len]);
1090 }
1091 }
1092 }
1093 }
1094}
1095
1096fn get_data<'a>(arena: &'a Arena, external: &'a ExternalBuffers, id: NodeId) -> &'a [f32] {
1100 if let Some(&ext) = external.buffers.get(&id) {
1103 ext
1104 } else if arena.has_buffer(id) {
1105 let (ptr, len) = arena.raw_ptr(id);
1106 unsafe { std::slice::from_raw_parts(ptr, len) }
1107 } else {
1108 panic!("no data for node {id}")
1109 }
1110}
1111
1112#[allow(clippy::mut_from_ref)]
1118fn get_output(arena: &Arena, id: NodeId) -> &mut [f32] {
1119 let (ptr, len) = arena.raw_ptr(id);
1120 unsafe { std::slice::from_raw_parts_mut(ptr, len) }
1121}
1122
1123#[inline]
1125fn matmul(a: &[f32], b: &[f32], c: &mut [f32], m: usize, k: usize, n: usize) {
1126 crate::blas::sgemm(a, b, c, m, k, n);
1128}
1129
1130fn binary_op(op: rlx_ir::op::BinaryOp, a: f32, b: f32) -> f32 {
1131 use rlx_ir::op::BinaryOp::*;
1132 match op {
1133 Add => a + b,
1134 Sub => a - b,
1135 Mul => a * b,
1136 Div => a / b,
1137 Max => a.max(b),
1138 Min => a.min(b),
1139 Pow => a.powf(b),
1140 }
1141}
1142
1143fn compare_op(op: rlx_ir::op::CmpOp, a: f32, b: f32) -> bool {
1144 use rlx_ir::op::CmpOp::*;
1145 match op {
1146 Eq => a == b,
1147 Ne => a != b,
1148 Lt => a < b,
1149 Le => a <= b,
1150 Gt => a > b,
1151 Ge => a >= b,
1152 }
1153}
1154
1155#[allow(dead_code)]
1157fn scalar_gelu(x: f32) -> f32 {
1158 let sign = if x >= 0.0 { 1.0f32 } else { -1.0 };
1159 let xa = x.abs();
1160 let t = 1.0 / (1.0 + 0.3275911 * xa);
1161 let y = t
1162 * (0.254_829_6
1163 + t * (-0.284_496_72 + t * (1.421_413_8 + t * (-1.453_152_1 + t * 1.061_405_4))));
1164 let erf = sign * (1.0 - y * (-xa * xa).exp());
1165 x * 0.5 * (1.0 + erf)
1166}
1167
1168#[cfg(test)]
1169mod tests {
1170 use super::*;
1171 use rlx_ir::*;
1172
1173 use rlx_opt::fusion::FuseMatMulBiasAct;
1174 use rlx_opt::memory;
1175 use rlx_opt::pass::Pass;
1176
1177 #[test]
1179 fn execute_fused_matmul_bias_gelu() {
1180 let mut g = Graph::new("test");
1182 let x_id = g.input("x", Shape::new(&[2, 4], DType::F32));
1183 let w_id = g.param("w", Shape::new(&[4, 3], DType::F32));
1184 let b_id = g.param("b", Shape::new(&[3], DType::F32));
1185 let mm = g.matmul(x_id, w_id, Shape::new(&[2, 3], DType::F32));
1186 let add = g.binary(BinaryOp::Add, mm, b_id, Shape::new(&[2, 3], DType::F32));
1187 let out = g.activation(Activation::Gelu, add, Shape::new(&[2, 3], DType::F32));
1188 g.set_outputs(vec![out]);
1189
1190 let fused = FuseMatMulBiasAct.run(g);
1192 println!("{fused}");
1193
1194 let plan = memory::plan_memory(&fused);
1196 println!("Arena: {} bytes", plan.arena_size);
1197
1198 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 {
1204 buffers: HashMap::new(),
1205 };
1206 ext.buffers.insert(fused.outputs[0], &[]); for node in fused.nodes() {
1209 match &node.op {
1210 Op::Input { name } if name == "x" => {
1211 ext.buffers.insert(node.id, &x_data);
1212 }
1213 Op::Param { name } if name == "w" => {
1214 ext.buffers.insert(node.id, &w_data);
1215 }
1216 Op::Param { name } if name == "b" => {
1217 ext.buffers.insert(node.id, &b_data);
1218 }
1219 _ => {}
1220 }
1221 }
1222
1223 let mut arena = Arena::from_plan(plan);
1225 execute(&fused, &mut arena, &ext);
1226
1227 let output_id = fused.outputs[0];
1229 let result = arena.slice(output_id);
1230 println!("Result: {result:?}");
1231
1232 assert!((result[0] - 1.399).abs() < 0.01, "got {}", result[0]);
1236 assert!((result[1] - -0.154).abs() < 0.01, "got {}", result[1]);
1237 assert!((result[2] - 0.0).abs() < 0.01, "got {}", result[2]);
1238 assert!((result[3] - 0.346).abs() < 0.01, "got {}", result[3]);
1239 }
1240
1241 #[test]
1243 fn execute_gather() {
1244 use rlx_ir::infer::GraphExt;
1245 let mut g = Graph::new("gather_test");
1246 let table = g.param("table", Shape::new(&[4, 3], DType::F32));
1248 let indices = g.input("ids", Shape::new(&[2], DType::F32)); let out = g.gather_(table, indices, 0);
1250 g.set_outputs(vec![out]);
1251
1252 let plan = memory::plan_memory(&g);
1253 let mut arena = Arena::from_plan(plan);
1254
1255 let table_data = vec![
1256 10.0, 11.0, 12.0, 20.0, 21.0, 22.0, 30.0, 31.0, 32.0, 40.0, 41.0, 42.0, ];
1261 let ids_data = vec![2.0, 0.0]; let mut ext = ExternalBuffers {
1264 buffers: HashMap::new(),
1265 };
1266 for node in g.nodes() {
1267 match &node.op {
1268 Op::Param { name } if name == "table" => {
1269 ext.buffers.insert(node.id, &table_data);
1270 }
1271 Op::Input { name } if name == "ids" => {
1272 ext.buffers.insert(node.id, &ids_data);
1273 }
1274 _ => {}
1275 }
1276 }
1277
1278 execute(&g, &mut arena, &ext);
1279 let result = arena.slice(g.outputs[0]);
1280 assert_eq!(&result[..3], &[30.0, 31.0, 32.0]); assert_eq!(&result[3..6], &[10.0, 11.0, 12.0]); }
1283
1284 #[test]
1286 fn execute_narrow() {
1287 use rlx_ir::infer::GraphExt;
1288 let mut g = Graph::new("narrow_test");
1289 let x = g.input("x", Shape::new(&[2, 6], DType::F32));
1290 let sliced = g.narrow_(x, 1, 2, 3); g.set_outputs(vec![sliced]);
1292
1293 let plan = memory::plan_memory(&g);
1294 let mut arena = Arena::from_plan(plan);
1295
1296 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];
1297 let mut ext = ExternalBuffers {
1298 buffers: HashMap::new(),
1299 };
1300 for node in g.nodes() {
1301 if let Op::Input { .. } = &node.op {
1302 ext.buffers.insert(node.id, &data);
1303 }
1304 }
1305
1306 execute(&g, &mut arena, &ext);
1307 let result = arena.slice(g.outputs[0]);
1308 assert_eq!(result, &[2.0, 3.0, 4.0, 8.0, 9.0, 10.0]);
1309 }
1310
1311 #[test]
1313 fn execute_softmax() {
1314 use rlx_ir::infer::GraphExt;
1315 let mut g = Graph::new("softmax_test");
1316 let x = g.input("x", Shape::new(&[1, 4], DType::F32));
1317 let sm = g.sm(x, -1);
1318 g.set_outputs(vec![sm]);
1319
1320 let plan = memory::plan_memory(&g);
1321 let mut arena = Arena::from_plan(plan);
1322
1323 let data = vec![1.0, 2.0, 3.0, 4.0];
1324 let mut ext = ExternalBuffers {
1325 buffers: HashMap::new(),
1326 };
1327 for node in g.nodes() {
1328 if let Op::Input { .. } = &node.op {
1329 ext.buffers.insert(node.id, &data);
1330 }
1331 }
1332
1333 execute(&g, &mut arena, &ext);
1334 let result = arena.slice(g.outputs[0]);
1335 let sum: f32 = result.iter().sum();
1336 assert!(
1337 (sum - 1.0).abs() < 1e-5,
1338 "softmax should sum to 1, got {sum}"
1339 );
1340 assert!(result[0] < result[1]);
1342 assert!(result[1] < result[2]);
1343 assert!(result[2] < result[3]);
1344 }
1345
1346 #[test]
1348 fn execute_rope() {
1349 use rlx_ir::infer::GraphExt;
1350 let head_dim = 4;
1351 let half = head_dim / 2;
1352 let seq = 2;
1353
1354 let mut g = Graph::new("rope_test");
1355 let x = g.input("x", Shape::new(&[seq, head_dim], DType::F32));
1357 let cos = g.param("cos", Shape::new(&[seq, half], DType::F32));
1358 let sin = g.param("sin", Shape::new(&[seq, half], DType::F32));
1359 let rotated = g.rope(x, cos, sin, head_dim);
1360 g.set_outputs(vec![rotated]);
1361
1362 let plan = memory::plan_memory(&g);
1363 let mut arena = Arena::from_plan(plan);
1364
1365 let x_data = vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0f32];
1367 let cos_data = vec![1.0, 0.0, 0.0, 1.0f32];
1369 let sin_data = vec![0.0, 1.0, 1.0, 0.0f32];
1370
1371 let mut ext = ExternalBuffers {
1372 buffers: HashMap::new(),
1373 };
1374 for node in g.nodes() {
1375 match &node.op {
1376 Op::Input { name } if name == "x" => {
1377 ext.buffers.insert(node.id, &x_data);
1378 }
1379 Op::Param { name } if name == "cos" => {
1380 ext.buffers.insert(node.id, &cos_data);
1381 }
1382 Op::Param { name } if name == "sin" => {
1383 ext.buffers.insert(node.id, &sin_data);
1384 }
1385 _ => {}
1386 }
1387 }
1388
1389 execute(&g, &mut arena, &ext);
1390 let result = arena.slice(g.outputs[0]);
1391
1392 assert!((result[0] - 1.0).abs() < 1e-5, "pos0[0]={}", result[0]);
1398 assert!((result[1] - -1.0).abs() < 1e-5, "pos0[1]={}", result[1]);
1399 assert!((result[2] - 0.0).abs() < 1e-5, "pos0[2]={}", result[2]);
1400 assert!((result[3] - 0.0).abs() < 1e-5, "pos0[3]={}", result[3]);
1401
1402 assert!((result[4] - 0.0).abs() < 1e-5, "pos1[0]={}", result[4]);
1406 assert!((result[5] - 1.0).abs() < 1e-5, "pos1[1]={}", result[5]);
1407 assert!((result[6] - 1.0).abs() < 1e-5, "pos1[2]={}", result[6]);
1408 assert!((result[7] - 0.0).abs() < 1e-5, "pos1[3]={}", result[7]);
1409 }
1410
1411 #[test]
1413 fn execute_layer_norm() {
1414 use rlx_ir::infer::GraphExt;
1415 let mut g = Graph::new("ln_test");
1416 let x = g.input("x", Shape::new(&[1, 4], DType::F32));
1417 let gamma = g.param("g", Shape::new(&[4], DType::F32));
1418 let beta = g.param("b", Shape::new(&[4], DType::F32));
1419 let ln = g.ln(x, gamma, beta, 1e-5);
1420 g.set_outputs(vec![ln]);
1421
1422 let plan = memory::plan_memory(&g);
1423 let mut arena = Arena::from_plan(plan);
1424
1425 let x_data = vec![1.0, 2.0, 3.0, 4.0];
1426 let g_data = vec![1.0, 1.0, 1.0, 1.0];
1427 let b_data = vec![0.0, 0.0, 0.0, 0.0];
1428
1429 let mut ext = ExternalBuffers {
1430 buffers: HashMap::new(),
1431 };
1432 for node in g.nodes() {
1433 match &node.op {
1434 Op::Input { name } if name == "x" => {
1435 ext.buffers.insert(node.id, &x_data);
1436 }
1437 Op::Param { name } if name == "g" => {
1438 ext.buffers.insert(node.id, &g_data);
1439 }
1440 Op::Param { name } if name == "b" => {
1441 ext.buffers.insert(node.id, &b_data);
1442 }
1443 _ => {}
1444 }
1445 }
1446
1447 execute(&g, &mut arena, &ext);
1448 let result = arena.slice(g.outputs[0]);
1449 let sum: f32 = result.iter().sum();
1450 assert!(
1451 sum.abs() < 1e-3,
1452 "LN output should be zero-centered, sum={sum}"
1453 );
1454 }
1455}