1use crate::accel::residency as accel_residency;
2use crate::bytecode::program::ExecutionContext;
3use crate::bytecode::Instr;
4use crate::interpreter::engine as interp_engine;
5use crate::interpreter::errors::mex;
6use crate::runtime::workspace::refresh_workspace_state;
7use runmat_accelerate::fusion::FusionStoreMaterialization;
8use runmat_accelerate::fusion_exec::{
9 execute_centered_gram, execute_elementwise, execute_explained_variance,
10 execute_image_normalize, execute_matmul_epilogue, execute_power_step_normalize,
11 execute_reduction, FusionExecutionRequest,
12};
13use runmat_accelerate::InstrSpan;
14use runmat_accelerate::{value_is_all_keyword, FusionKind, ShapeInfo, ValueOrigin, VarKind};
15use runmat_builtins::Value;
16use runmat_runtime::builtins::common::shape::is_scalar_shape;
17use runmat_runtime::RuntimeError;
18use std::collections::HashMap;
19
20#[inline]
21pub fn value_kind(value: &Value) -> &'static str {
22 match value {
23 Value::Int(_) => "Int",
24 Value::Num(_) => "Num",
25 Value::Complex(_, _) => "Complex",
26 Value::Bool(_) => "Bool",
27 Value::LogicalArray(_) => "LogicalArray",
28 Value::String(_) => "String",
29 Value::StringArray(_) => "StringArray",
30 Value::Symbolic(_) => "Symbolic",
31 Value::SymbolicArray(_) => "SymbolicArray",
32 Value::CharArray(_) => "CharArray",
33 Value::Tensor(_) => "Tensor",
34 Value::SparseTensor(_) => "SparseTensor",
35 Value::ComplexTensor(_) => "ComplexTensor",
36 Value::Cell(_) => "Cell",
37 Value::Struct(_) => "Struct",
38 Value::GpuTensor(_) => "GpuTensor",
39 Value::Object(_) => "Object",
40 Value::HandleObject(_) => "HandleObject",
41 Value::Listener(_) => "Listener",
42 Value::FunctionHandle(_)
43 | Value::ExternalFunctionHandle(_)
44 | Value::MethodFunctionHandle(_) => "FunctionHandle",
45 Value::BoundFunctionHandle { .. } => "FunctionHandle",
46 Value::Closure(_) => "Closure",
47 Value::ClassRef(_) => "ClassRef",
48 Value::MException(_) => "MException",
49 Value::OutputList(_) => "OutputList",
50 }
51}
52
53#[inline]
54pub fn summarize_value(i: usize, v: &Value) -> String {
55 match v {
56 Value::GpuTensor(h) => format!("in#{i}:GpuTensor shape={:?}", h.shape),
57 Value::Tensor(t) => format!("in#{i}:Tensor shape={:?}", t.shape),
58 Value::Num(n) => format!("in#{i}:Num({n:.6})"),
59 Value::Int(n) => format!("in#{i}:Int({})", n.to_i64()),
60 Value::Bool(b) => format!("in#{i}:Bool({})", if *b { 1 } else { 0 }),
61 Value::String(s) => format!("in#{i}:String({})", s),
62 _ => format!("in#{i}:{}", value_kind(v)),
63 }
64}
65
66#[inline]
67fn is_scalarish_runtime_value(value: &Value) -> bool {
68 match value {
69 Value::Num(_) | Value::Int(_) | Value::Bool(_) | Value::Complex(_, _) => true,
70 Value::Tensor(tensor) => is_scalar_shape(&tensor.shape),
71 Value::ComplexTensor(tensor) => is_scalar_shape(&tensor.shape),
72 Value::LogicalArray(array) => is_scalar_shape(&array.shape),
73 Value::GpuTensor(handle) => is_scalar_shape(&handle.shape),
74 Value::CharArray(array) => array.rows * array.cols == 1,
75 _ => false,
76 }
77}
78
79pub fn fusion_span_live_result_count(instructions: &[Instr], span: &InstrSpan) -> Option<usize> {
80 if span.start > span.end || span.end >= instructions.len() {
81 return None;
82 }
83 let mut current_depth = 0usize;
84 for instr in &instructions[span.start..=span.end] {
85 let effect = instr.stack_effect()?;
86 if current_depth < effect.pops {
87 current_depth = effect.pops;
88 }
89 current_depth = current_depth - effect.pops + effect.pushes;
90 }
91 Some(current_depth)
92}
93
94pub fn fusion_span_has_vm_barrier(instructions: &[Instr], span: &InstrSpan) -> bool {
95 if span.start > span.end || span.end >= instructions.len() {
96 return true;
97 }
98 for instr in &instructions[span.start..=span.end] {
99 if matches!(
100 instr,
101 Instr::StoreIndex(_)
102 | Instr::StoreIndexDelete(_)
103 | Instr::StoreSlice(_, _, _, _)
104 | Instr::StoreSliceDelete(_, _, _, _)
105 | Instr::StoreSliceExpr { .. }
106 | Instr::StoreSliceExprDelete { .. }
107 | Instr::StoreIndexCell { .. }
108 | Instr::StoreIndexCellDelete { .. }
109 | Instr::StoreMember(_)
110 | Instr::StoreMemberOrInit(_)
111 | Instr::StoreMemberDynamic
112 | Instr::StoreMemberDynamicOrInit
113 ) {
114 return true;
115 }
116 }
117 fusion_span_live_result_count(instructions, span) != Some(1)
118}
119
120pub struct StackSliceGuard<'a> {
121 stack: *mut Vec<Value>,
122 slice: Option<Vec<Value>>,
123 _marker: std::marker::PhantomData<&'a mut Vec<Value>>,
124}
125
126impl<'a> StackSliceGuard<'a> {
127 pub fn new(stack: &'a mut Vec<Value>, slice_start: usize) -> Self {
128 let slice = stack.split_off(slice_start);
129 Self {
130 stack,
131 slice: Some(slice),
132 _marker: std::marker::PhantomData,
133 }
134 }
135
136 pub fn slice(&self) -> &[Value] {
137 self.slice.as_ref().expect("stack slice missing").as_slice()
138 }
139
140 pub fn commit(mut self) {
141 self.slice = None;
142 }
143}
144
145impl Drop for StackSliceGuard<'_> {
146 fn drop(&mut self) {
147 if let Some(slice) = self.slice.take() {
148 unsafe { (&mut *self.stack).extend(slice) }
149 }
150 }
151}
152
153pub fn gather_fusion_inputs<'a>(
154 plan: &'a runmat_accelerate::FusionGroupPlan,
155 graph: &runmat_accelerate::AccelGraph,
156 stack: &'a mut Vec<Value>,
157 vars: &mut [Value],
158 context: &mut ExecutionContext,
159) -> Result<
160 (
161 StackSliceGuard<'a>,
162 FusionExecutionRequest<'a>,
163 Vec<Option<Value>>,
164 ),
165 RuntimeError,
166> {
167 if plan.group.stack_layout.is_none() && !plan.stack_pattern.is_empty() {
168 return Err(mex(
169 "FusionMissingStackLayout",
170 "fusion: missing compile-time stack layout metadata",
171 ));
172 }
173 let required_stack_operands = plan
174 .group
175 .stack_layout
176 .as_ref()
177 .map(|layout| layout.required_stack_operands)
178 .unwrap_or_else(|| plan.stack_pattern.len());
179 let mut inputs: Vec<Option<Value>> = vec![None; plan.inputs.len()];
180
181 for (idx, value) in &plan.constants {
182 if let Some(slot) = inputs.get_mut(*idx) {
183 if slot.is_none() {
184 *slot = Some(value.clone());
185 }
186 }
187 }
188
189 for (idx, value_id) in plan.inputs.iter().enumerate() {
190 let info = graph
191 .value(*value_id)
192 .ok_or_else(|| format!("fusion: missing value metadata for id {value_id}"))?;
193 match &info.origin {
194 ValueOrigin::Variable { kind, index } => {
195 let value =
196 match kind {
197 VarKind::Global => vars
198 .get(*index)
199 .cloned()
200 .ok_or_else(|| format!("fusion: global var {index} out of range"))?,
201 VarKind::Local => {
202 if let Some(frame) = context.call_stack.last() {
203 let absolute = frame.locals_start + index;
204 context.locals.get(absolute).cloned().ok_or_else(|| {
205 format!("fusion: local var {index} unavailable")
206 })?
207 } else {
208 vars.get(*index).cloned().ok_or_else(|| {
209 format!("fusion: local var {index} unavailable")
210 })?
211 }
212 }
213 };
214 debug_assert!(
215 inputs[idx].is_none(),
216 "fusion: duplicate input slot {} for plan {}",
217 idx,
218 plan.index
219 );
220 inputs[idx] = Some(value);
221 }
222 ValueOrigin::Constant | ValueOrigin::NodeOutput { .. } | ValueOrigin::Unknown => {}
223 }
224 }
225
226 if log::log_enabled!(log::Level::Debug) && interp_engine::fusion_debug_enabled() {
227 let stack_needed_preview = required_stack_operands;
228 let stack_snapshot: Vec<&Value> = stack.iter().rev().take(stack_needed_preview).collect();
229 let stack_kinds: Vec<&'static str> =
230 stack_snapshot.iter().rev().map(|v| value_kind(v)).collect();
231 let input_meta: Vec<String> = plan
232 .inputs
233 .iter()
234 .enumerate()
235 .map(|(i, value_id)| {
236 if let Some(info) = graph.value(*value_id) {
237 format!("#{i}:id={} origin={:?}", value_id, info.origin)
238 } else {
239 format!("#{i}:id={} origin=<missing>", value_id)
240 }
241 })
242 .collect();
243 log::debug!(
244 "fusion group {} gather: stack_depth={} stack_needed={} stack_kinds={:?} pattern={:?} inputs={:?}",
245 plan.index, stack.len(), stack_needed_preview, stack_kinds, &plan.stack_pattern, input_meta
246 );
247 }
248
249 if stack.len() < required_stack_operands {
250 if interp_engine::fusion_debug_enabled() {
251 log::debug!(
252 "fusion stack underflow: plan={} needed={} available={} pattern={:?}",
253 plan.index,
254 required_stack_operands,
255 stack.len(),
256 plan.stack_pattern
257 );
258 }
259 return Err(mex(
260 "FusionStackUnderflow",
261 "fusion: stack underflow gathering inputs",
262 ));
263 }
264 let available = required_stack_operands;
265 let slice_start = stack.len() - available;
266 let stack_guard = StackSliceGuard::new(stack, slice_start);
267 let slice = stack_guard.slice().to_vec();
268 let mut consumed_inputs: Vec<Option<Value>> = vec![None; plan.inputs.len()];
269 let input_positions: HashMap<runmat_accelerate::graph::ValueId, usize> = plan
270 .inputs
271 .iter()
272 .enumerate()
273 .map(|(idx, value_id)| (*value_id, idx))
274 .collect();
275
276 let allow_stack_value = |val: &Value| {
277 if plan.group.kind.is_reduction() {
278 matches!(val, Value::GpuTensor(_) | Value::Tensor(_))
279 } else {
280 true
281 }
282 };
283
284 if let Some(layout) = plan.group.stack_layout.as_ref() {
285 for binding in &layout.bindings {
286 let Some(input_idx) = input_positions.get(&binding.value_id).copied() else {
287 continue;
288 };
289 let Some(val) = slice.get(binding.stack_offset).cloned() else {
290 continue;
291 };
292 consumed_inputs[input_idx] = Some(val.clone());
293 if inputs[input_idx].is_none() && allow_stack_value(&val) {
294 inputs[input_idx] = Some(val);
295 }
296 }
297 } else {
298 for (offset, input_idx) in plan.stack_pattern.iter().enumerate() {
299 let Some(val) = slice.get(offset).cloned() else {
300 continue;
301 };
302 consumed_inputs[*input_idx] = Some(val.clone());
303 if inputs[*input_idx].is_none() && allow_stack_value(&val) {
304 inputs[*input_idx] = Some(val);
305 }
306 }
307 }
308
309 for (idx, slot) in inputs.iter_mut().enumerate() {
310 if slot.is_some() {
311 continue;
312 }
313 let vid = plan.inputs[idx];
314 let info = graph.value(vid);
315 if let Some(info) = info {
316 match &info.origin {
317 ValueOrigin::Variable { kind, index } => {
318 let value_opt = match kind {
319 VarKind::Global => vars.get(*index).cloned(),
320 VarKind::Local => {
321 if let Some(frame) = context.call_stack.last() {
322 let absolute = frame.locals_start + index;
323 context.locals.get(absolute).cloned()
324 } else {
325 vars.get(*index).cloned()
326 }
327 }
328 };
329 if let Some(value) = value_opt {
330 *slot = Some(value);
331 continue;
332 }
333 }
334 ValueOrigin::Constant => {
335 if let Some(value) = plan.const_values.get(&vid) {
336 *slot = Some(value.clone());
337 continue;
338 }
339 }
340 _ => {}
341 }
342 }
343 if slot.is_none() {
344 if let Some(binding) = graph.var_binding(vid) {
345 let value_opt = match binding.kind {
346 VarKind::Global => vars.get(binding.index).cloned(),
347 VarKind::Local => {
348 if let Some(frame) = context.call_stack.last() {
349 let absolute = frame.locals_start + binding.index;
350 context.locals.get(absolute).cloned()
351 } else {
352 vars.get(binding.index).cloned()
353 }
354 }
355 };
356 if let Some(value) = value_opt {
357 *slot = Some(value);
358 continue;
359 }
360 }
361 }
362 if slot.is_none() {
363 if let Some(info) = info {
364 if let ValueOrigin::NodeOutput { node, .. } = info.origin {
365 if let Some(binding) = graph.node_binding(node) {
366 let value_opt = match binding.kind {
367 VarKind::Global => vars.get(binding.index).cloned(),
368 VarKind::Local => {
369 if let Some(frame) = context.call_stack.last() {
370 let absolute = frame.locals_start + binding.index;
371 context.locals.get(absolute).cloned()
372 } else {
373 vars.get(binding.index).cloned()
374 }
375 }
376 };
377 if let Some(value) = value_opt {
378 *slot = Some(value);
379 continue;
380 }
381 }
382 }
383 }
384 }
385 if slot.is_none() {
386 if let Some(value) = plan.const_values.get(&vid) {
387 *slot = Some(value.clone());
388 }
389 }
390 }
391
392 let inputs: Vec<Value> = inputs
393 .into_iter()
394 .map(|opt| opt.ok_or_else(|| mex("FusionMissingInput", "fusion: missing input value")))
395 .collect::<Result<_, _>>()?;
396
397 if log::log_enabled!(log::Level::Debug) {
398 let summaries: Vec<String> = inputs
399 .iter()
400 .enumerate()
401 .map(|(i, v)| summarize_value(i, v))
402 .collect();
403 log::debug!("fusion inputs runtime: [{}]", summaries.join(", "));
404 }
405
406 Ok((
407 stack_guard,
408 FusionExecutionRequest { plan, inputs },
409 consumed_inputs,
410 ))
411}
412
413pub fn write_elementwise_materialized_stores(
414 materialized_stores: Vec<(FusionStoreMaterialization, Value)>,
415 vars: &mut Vec<Value>,
416 context: &mut ExecutionContext,
417) {
418 for (store, value) in materialized_stores {
419 match store.binding.kind {
420 VarKind::Global => {
421 let i = store.binding.index;
422 if i < vars.len() {
423 if let Err(err) = accel_residency::clear_value_excluding(&vars[i], &value) {
424 log::warn!("failed to clear fused global GPU residency: {err}");
425 }
426 }
427 if i >= vars.len() {
428 vars.resize(i + 1, Value::Num(0.0));
429 refresh_workspace_state(vars);
430 }
431 vars[i] = value;
432 }
433 VarKind::Local => {
434 if let Some(frame) = context.call_stack.last() {
435 let absolute = frame.locals_start + store.binding.index;
436 while context.locals.len() <= absolute {
437 context.locals.push(Value::Num(0.0));
438 }
439 if let Err(err) =
440 accel_residency::clear_value_excluding(&context.locals[absolute], &value)
441 {
442 log::warn!("failed to clear fused local GPU residency: {err}");
443 }
444 context.locals[absolute] = value;
445 } else {
446 let i = store.binding.index;
447 if i < vars.len() {
448 if let Err(err) = accel_residency::clear_value_excluding(&vars[i], &value) {
449 log::warn!("failed to clear fused fallback GPU residency: {err}");
450 }
451 }
452 if i >= vars.len() {
453 vars.resize(i + 1, Value::Num(0.0));
454 refresh_workspace_state(vars);
455 }
456 vars[i] = value;
457 }
458 }
459 }
460 }
461}
462
463pub fn execute_fusion_elementwise(
464 request: FusionExecutionRequest<'_>,
465 stack_guard: StackSliceGuard<'_>,
466 vars: &mut Vec<Value>,
467 context: &mut ExecutionContext,
468) -> Result<Value, RuntimeError> {
469 match execute_elementwise(request) {
470 Ok(result) => {
471 write_elementwise_materialized_stores(result.materialized_stores, vars, context);
472 stack_guard.commit();
473 Ok(result.final_value)
474 }
475 Err(err) => Err(mex("FusionExecutionFailed", &err.to_string())),
476 }
477}
478
479pub async fn execute_fusion_special_kind(
480 kind: FusionKind,
481 plan_inputs: &[runmat_accelerate::graph::ValueId],
482 request: FusionExecutionRequest<'_>,
483 stack_guard: StackSliceGuard<'_>,
484) -> Result<Value, RuntimeError> {
485 match kind {
486 FusionKind::CenteredGram => match execute_centered_gram(request).await {
487 Ok(result) => {
488 stack_guard.commit();
489 Ok(result)
490 }
491 Err(err) => Err(mex("FusionExecutionFailed", &err.to_string())),
492 },
493 FusionKind::PowerStepNormalize => match execute_power_step_normalize(request).await {
494 Ok(result) => {
495 stack_guard.commit();
496 Ok(result)
497 }
498 Err(err) => Err(mex("FusionExecutionFailed", &err.to_string())),
499 },
500 FusionKind::ExplainedVariance => {
501 log::debug!("explained variance plan inputs {:?}", plan_inputs);
502 match execute_explained_variance(request).await {
503 Ok(result) => {
504 stack_guard.commit();
505 Ok(result)
506 }
507 Err(err) => {
508 log::debug!("explained variance fusion fallback: {}", err);
509 Err(mex("FusionExecutionFailed", &err.to_string()))
510 }
511 }
512 }
513 FusionKind::MatmulEpilogue => match execute_matmul_epilogue(request).await {
514 Ok(result) => {
515 stack_guard.commit();
516 Ok(result)
517 }
518 Err(err) => Err(mex("FusionExecutionFailed", &err.to_string())),
519 },
520 FusionKind::ImageNormalize => match execute_image_normalize(request).await {
521 Ok(result) => {
522 stack_guard.commit();
523 Ok(result)
524 }
525 Err(err) => Err(mex("FusionExecutionFailed", &err.to_string())),
526 },
527 _ => Err(mex(
528 "FusionUnsupportedKind",
529 "fusion: unsupported fusion kind",
530 )),
531 }
532}
533
534pub struct ReductionGeometry {
535 pub axis: usize,
536 pub reduce_len: usize,
537 pub num_slices: usize,
538}
539
540pub fn resolve_reduction_geometry(
541 plan: &runmat_accelerate::FusionGroupPlan,
542 graph: &runmat_accelerate::AccelGraph,
543 request: &FusionExecutionRequest<'_>,
544 consumed_inputs: &[Option<Value>],
545 vars: &[Value],
546 context: &ExecutionContext,
547) -> Result<ReductionGeometry, RuntimeError> {
548 fn detect_reduce_all(
549 plan: &runmat_accelerate::FusionGroupPlan,
550 graph: &runmat_accelerate::AccelGraph,
551 ) -> bool {
552 let mut reduce_all = matches!(
553 plan.reduction_axes,
554 Some(runmat_accelerate::ReductionAxes::All)
555 );
556 let has_all = reduce_all
557 || plan.constants.values().any(value_is_all_keyword)
558 || plan.const_values.values().any(value_is_all_keyword);
559 if has_all {
560 return true;
561 }
562 for node_id in &plan.group.nodes {
563 if let Some(node) = graph.node(*node_id) {
564 if let runmat_accelerate::graph::AccelNodeLabel::Builtin { name } = &node.label {
565 if name.eq_ignore_ascii_case("mean") {
566 for input_vid in &node.inputs {
567 if let Some(info) = graph.value(*input_vid) {
568 if let Some(constant) = &info.constant {
569 if value_is_all_keyword(constant) {
570 reduce_all = true;
571 break;
572 }
573 }
574 }
575 }
576 }
577 }
578 }
579 if reduce_all {
580 break;
581 }
582 }
583 reduce_all
584 }
585
586 fn resolve_reduction_axis(plan: &runmat_accelerate::FusionGroupPlan) -> (usize, bool) {
587 let mut axis = 0usize;
588 let mut axis_explicit = false;
589 if let Some(runmat_accelerate::ReductionAxes::Explicit(dims)) = &plan.reduction_axes {
590 if let Some(first) = dims.first().copied() {
591 axis = first.saturating_sub(1);
592 axis_explicit = true;
593 }
594 }
595 if let Some(dim_vid) = plan.reduction_dim {
596 if let Some(cv) = plan.const_values.get(&dim_vid) {
597 axis = match cv {
598 Value::Num(n) if *n >= 1.0 => (*n as usize).saturating_sub(1),
599 Value::Int(i) => (i.to_f64() as usize).saturating_sub(1),
600 _ => axis,
601 };
602 axis_explicit = true;
603 } else if let Some(input_idx) = plan.inputs.iter().position(|v| *v == dim_vid) {
604 if let Some(cv) = plan.constants.get(&input_idx) {
605 axis = match cv {
606 Value::Num(n) if *n >= 1.0 => (*n as usize).saturating_sub(1),
607 Value::Int(i) => (i.to_f64() as usize).saturating_sub(1),
608 _ => axis,
609 };
610 axis_explicit = true;
611 }
612 }
613 } else if let Some(dim_const) = plan.constants.get(&1) {
614 axis = match dim_const {
615 Value::Num(n) if *n >= 1.0 => (*n as usize).saturating_sub(1),
616 Value::Int(i) => (i.to_f64() as usize).saturating_sub(1),
617 _ => axis,
618 };
619 axis_explicit = true;
620 }
621 (axis, axis_explicit)
622 }
623
624 fn derive_rows_cols(
625 plan: &runmat_accelerate::FusionGroupPlan,
626 graph: &runmat_accelerate::AccelGraph,
627 request: &FusionExecutionRequest<'_>,
628 consumed_inputs: &[Option<Value>],
629 vars: &[Value],
630 context: &ExecutionContext,
631 ) -> Option<(usize, usize)> {
632 let shape_of = |value: &Value| -> Option<(usize, usize)> {
633 match value {
634 Value::GpuTensor(h) => Some((
635 h.shape.first().copied().unwrap_or(1).max(1),
636 h.shape.get(1).copied().unwrap_or(1).max(1),
637 )),
638 Value::Tensor(t) => Some((
639 t.shape.first().copied().unwrap_or(1).max(1),
640 t.shape.get(1).copied().unwrap_or(1).max(1),
641 )),
642 _ => None,
643 }
644 };
645
646 if let Some(shape) = plan.reduction_data_shape(graph) {
647 if shape.len() >= 2 {
648 return Some((shape[0].max(1), shape[1].max(1)));
649 }
650 if shape.len() == 1 {
651 return Some((shape[0].max(1), 1));
652 }
653 }
654
655 for &vid in &plan.inputs {
656 if let Some(binding) = graph.var_binding(vid) {
657 let value_opt = match binding.kind {
658 VarKind::Global => vars.get(binding.index).cloned(),
659 VarKind::Local => {
660 if let Some(frame) = context.call_stack.last() {
661 let absolute = frame.locals_start + binding.index;
662 context.locals.get(absolute).cloned()
663 } else {
664 vars.get(binding.index).cloned()
665 }
666 }
667 };
668 if let Some(value) = value_opt {
669 if let Some(shape) = shape_of(&value) {
670 return Some(shape);
671 }
672 }
673 }
674 }
675
676 for v in consumed_inputs.iter().filter_map(|v| v.as_ref()) {
677 if let Some(shape) = shape_of(v) {
678 return Some(shape);
679 }
680 }
681
682 if let Some(data_id) = plan.reduction_data {
683 if let Some(input_index) = plan.inputs.iter().position(|vid| *vid == data_id) {
684 if let Some(val) = consumed_inputs.get(input_index).and_then(|v| v.as_ref()) {
685 if let Some(shape) = shape_of(val) {
686 return Some(shape);
687 }
688 }
689 if let Some(val) = request.inputs.get(input_index) {
690 if let Some(shape) = shape_of(val) {
691 return Some(shape);
692 }
693 }
694 }
695 if let Some(info) = graph.value(data_id) {
696 if let ValueOrigin::Variable { kind, index } = &info.origin {
697 let val = match kind {
698 VarKind::Global => vars.get(*index).cloned(),
699 VarKind::Local => {
700 if let Some(frame) = context.call_stack.last() {
701 let absolute = frame.locals_start + index;
702 context.locals.get(absolute).cloned()
703 } else {
704 vars.get(*index).cloned()
705 }
706 }
707 };
708 if let Some(v) = val {
709 if let Some(shape) = shape_of(&v) {
710 return Some(shape);
711 }
712 }
713 }
714 if let ShapeInfo::Tensor(dims) = &info.shape {
715 if !dims.is_empty() {
716 let r = dims.first().and_then(|d| *d).unwrap_or(1);
717 let c = dims.get(1).and_then(|d| *d).unwrap_or(1);
718 return Some((r.max(1), c.max(1)));
719 }
720 }
721 }
722 }
723
724 for v in &request.inputs {
725 if let Some(shape) = shape_of(v) {
726 return Some(shape);
727 }
728 }
729
730 if let ShapeInfo::Tensor(dims) = &plan.group.shape {
731 if !dims.is_empty() {
732 let r = dims.first().and_then(|d| *d).unwrap_or(1);
733 let c = dims.get(1).and_then(|d| *d).unwrap_or(1);
734 return Some((r.max(1), c.max(1)));
735 }
736 }
737 None
738 }
739
740 if log::log_enabled!(log::Level::Debug) {
741 let meta: Vec<String> = plan
742 .inputs
743 .iter()
744 .map(|vid| {
745 if let Some(info) = graph.value(*vid) {
746 format!(
747 "vid={} origin={:?} shape={:?}",
748 vid, info.origin, info.shape
749 )
750 } else {
751 format!("vid={} origin=<missing>", vid)
752 }
753 })
754 .collect();
755 log::debug!("reduction gather meta: [{}]", meta.join(", "));
756 }
757
758 let reduce_all = detect_reduce_all(plan, graph);
759 let (mut axis, axis_explicit) = if reduce_all {
760 (0usize, false)
761 } else {
762 resolve_reduction_axis(plan)
763 };
764 if reduce_all && interp_engine::fusion_debug_enabled() {
765 log::debug!(
766 "fusion reduction (all) meta: data_vid={:?} inputs={:?} stack_pattern={:?}",
767 plan.reduction_data,
768 plan.inputs,
769 plan.stack_pattern
770 );
771 }
772
773 let (r, c) =
774 derive_rows_cols(plan, graph, request, consumed_inputs, vars, context).unwrap_or((1, 1));
775 let (reduce_len, num_slices) = if reduce_all {
776 let total_from_runtime = consumed_inputs
777 .iter()
778 .filter_map(|v| v.as_ref())
779 .chain(request.inputs.iter())
780 .find_map(|value| match value {
781 Value::GpuTensor(handle) => Some(if handle.shape.is_empty() {
782 1
783 } else {
784 handle
785 .shape
786 .iter()
787 .copied()
788 .map(|d| d.max(1))
789 .product::<usize>()
790 }),
791 Value::Tensor(tensor) => Some(if tensor.shape.is_empty() {
792 1
793 } else {
794 tensor
795 .shape
796 .iter()
797 .copied()
798 .map(|d| d.max(1))
799 .product::<usize>()
800 }),
801 _ => None,
802 });
803 let total = plan
804 .reduction_data_shape(graph)
805 .map(|shape| shape.into_iter().map(|d| d.max(1)).product::<usize>())
806 .or(total_from_runtime)
807 .or_else(|| plan.element_count())
808 .filter(|v| *v > 0)
809 .ok_or_else(|| {
810 mex(
811 "FusionReductionExtentUnknown",
812 "fusion: reduction all extent unknown",
813 )
814 })?;
815 if interp_engine::fusion_debug_enabled() {
816 log::debug!(
817 "fusion reduction (all): total_elems={} fallback_rows={} fallback_cols={}",
818 total,
819 r,
820 c
821 );
822 }
823 (total, 1usize)
824 } else {
825 if !axis_explicit {
826 axis = if r == 1 && c > 1 {
827 1
828 } else if r > 1 {
829 0
830 } else {
831 axis
832 };
833 }
834 if interp_engine::fusion_debug_enabled() {
835 if r == 1 && c == 1 {
836 log::debug!(
837 "fusion reduction: unresolved shape (defaulted to 1x1); axis={}, constants={:?}",
838 axis,
839 plan.constants
840 );
841 } else {
842 log::debug!(
843 "fusion reduction: resolved shape rows={} cols={} axis={} constants={:?}",
844 r,
845 c,
846 axis,
847 plan.constants
848 );
849 }
850 }
851 if axis == 0 {
852 (r, c)
853 } else {
854 (c, r)
855 }
856 };
857
858 if interp_engine::fusion_debug_enabled() {
859 log::debug!(
860 "fusion reduction: axis={} reduce_len={} num_slices={} constants={:?}",
861 axis,
862 reduce_len,
863 num_slices,
864 plan.constants
865 );
866 }
867
868 let looks_wrong = reduce_len == 1 && num_slices == 1 && {
869 let mut big = false;
870 let mut check_val = |v: &Value| match v {
871 Value::GpuTensor(h) => {
872 let prod = h.shape.iter().copied().product::<usize>();
873 if prod > 1 {
874 big = true;
875 }
876 }
877 Value::Tensor(t) => {
878 let prod = t.shape.iter().copied().product::<usize>();
879 if prod > 1 {
880 big = true;
881 }
882 }
883 _ => {}
884 };
885 for v in consumed_inputs.iter().filter_map(|v| v.as_ref()) {
886 check_val(v);
887 }
888 for v in &request.inputs {
889 check_val(v);
890 }
891 big
892 };
893 if looks_wrong {
894 log::debug!("fusion reduction: skipping fusion due to unresolved shape; falling back to provider path");
895 return Err(mex(
896 "FusionReductionShapeUnresolved",
897 "fusion: reduction shape unresolved",
898 ));
899 }
900 if std::env::var("RUNMAT_DISABLE_FUSED_REDUCTION")
901 .ok()
902 .as_deref()
903 == Some("1")
904 {
905 return Err(mex(
906 "FusionReductionDisabled",
907 "fusion: fused reductions disabled",
908 ));
909 }
910
911 Ok(ReductionGeometry {
912 axis,
913 reduce_len,
914 num_slices,
915 })
916}
917
918pub fn execute_fusion_reduction(
919 plan: &runmat_accelerate::FusionGroupPlan,
920 graph: &runmat_accelerate::AccelGraph,
921 request: FusionExecutionRequest<'_>,
922 consumed_inputs: &[Option<Value>],
923 stack_guard: StackSliceGuard<'_>,
924 vars: &[Value],
925 context: &ExecutionContext,
926) -> Result<Value, RuntimeError> {
927 let geom = resolve_reduction_geometry(plan, graph, &request, consumed_inputs, vars, context)?;
928 match execute_reduction(request, geom.reduce_len, geom.num_slices, 256u32) {
929 Ok(result) => {
930 stack_guard.commit();
931 Ok(result)
932 }
933 Err(err) => Err(mex("FusionExecutionFailed", &err.to_string())),
934 }
935}
936
937pub async fn try_execute_fusion_group(
938 plan: &runmat_accelerate::FusionGroupPlan,
939 graph: &runmat_accelerate::AccelGraph,
940 stack: &mut Vec<Value>,
941 vars: &mut Vec<Value>,
942 context: &mut ExecutionContext,
943) -> Result<Value, RuntimeError> {
944 let (stack_guard, request, consumed_inputs) =
945 gather_fusion_inputs(plan, graph, stack, vars, context)?;
946 if plan.group.kind.is_elementwise()
947 && !request.inputs.is_empty()
948 && request.inputs.iter().all(is_scalarish_runtime_value)
949 {
950 return Err(mex(
951 "FusionScalarBypass",
952 "fusion: bypass scalar-only elementwise group",
953 ));
954 }
955 log::debug!(
956 "dispatch fusion kind {:?}, supported {}",
957 plan.group.kind,
958 plan.kernel.supported
959 );
960 if plan.group.kind.is_elementwise() {
961 execute_fusion_elementwise(request, stack_guard, vars, context)
962 } else if plan.group.kind.is_reduction() {
963 execute_fusion_reduction(
964 plan,
965 graph,
966 request,
967 &consumed_inputs,
968 stack_guard,
969 vars,
970 context,
971 )
972 } else {
973 execute_fusion_special_kind(plan.group.kind.clone(), &plan.inputs, request, stack_guard)
974 .await
975 }
976}
977
978#[cfg(all(test, feature = "native-accel"))]
979mod tests {
980 use super::write_elementwise_materialized_stores;
981 use crate::bytecode::program::ExecutionContext;
982 use runmat_accelerate::fusion::FusionStoreMaterialization;
983 use runmat_accelerate::fusion_residency;
984 use runmat_accelerate::graph::VarBinding;
985 use runmat_accelerate::VarKind;
986 use runmat_accelerate_api::GpuTensorHandle;
987 use runmat_builtins::Value;
988
989 #[test]
990 fn fusion_writeback_preserves_shared_gpu_handles() {
991 let shared = GpuTensorHandle {
992 shape: vec![1],
993 device_id: 17,
994 buffer_id: 17001,
995 };
996 let old_only = GpuTensorHandle {
997 shape: vec![1],
998 device_id: 17,
999 buffer_id: 17002,
1000 };
1001 fusion_residency::mark(&shared);
1002 fusion_residency::mark(&old_only);
1003 assert!(fusion_residency::is_resident(&shared));
1004 assert!(fusion_residency::is_resident(&old_only));
1005
1006 let mut vars = vec![Value::OutputList(vec![
1007 Value::GpuTensor(shared.clone()),
1008 Value::GpuTensor(old_only.clone()),
1009 ])];
1010 let mut context = ExecutionContext {
1011 call_stack: Vec::new(),
1012 locals: Vec::new(),
1013 instruction_pointer: 0,
1014 spawned_task_ids: std::collections::HashSet::new(),
1015 next_spawn_task_id: 0,
1016 };
1017 write_elementwise_materialized_stores(
1018 vec![(
1019 FusionStoreMaterialization {
1020 value_id: 1,
1021 binding: VarBinding {
1022 kind: VarKind::Global,
1023 index: 0,
1024 },
1025 },
1026 Value::GpuTensor(shared.clone()),
1027 )],
1028 &mut vars,
1029 &mut context,
1030 );
1031
1032 assert!(fusion_residency::is_resident(&shared));
1033 assert!(!fusion_residency::is_resident(&old_only));
1034 fusion_residency::clear(&shared);
1035 }
1036}