Skip to main content

runmat_vm/accel/
fusion.rs

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}