Skip to main content

sim_lib_numbers_tensor/implementation/
execution_ops.rs

1//! Executor-routed tensor reductions, linear algebra, and f32 scalar functions.
2//!
3//! These operations use checked [`TensorRequest`](super::execution::TensorRequest)
4//! values so host, GPU, or remote providers see the same operation symbols as the
5//! CPU fallback. Reductions in this module reduce the whole tensor to one rank-0
6//! scalar. Empty sum and norm return zero; empty min and max fail closed. Floating
7//! tensors follow IEEE 754 propagation for NaN, infinity, and signed zero. Half
8//! tensors are widened to `f32` for reductions and transcendentals because SIM's
9//! scalar half domains are storage formats rather than arithmetic domains.
10
11use sim_kernel::{Cx, Error, Result, Symbol};
12
13use super::{
14    execution::{TensorExecError, TensorMeta, TensorOp, TensorRequest, execute_tensor_request},
15    execution_math_support::{
16        ProductSpec, float_output_dtype, matches_tensor_transcendental, matmul_output_shape,
17        norm_value, reduce_min_max, reduce_sum, reduction_output_dtype, reduction_pair_dtype,
18        scalar_tensor, sum_products, tensor_from_cells, transcendental_cell,
19    },
20    value::Tensor,
21};
22
23/// Open operation symbol for reducing all tensor cells with addition.
24pub fn sum_op_symbol() -> Symbol {
25    Symbol::qualified("tensor", "op/sum")
26}
27
28/// Open operation symbol for reducing all tensor cells to the minimum value.
29pub fn min_op_symbol() -> Symbol {
30    Symbol::qualified("tensor", "op/min")
31}
32
33/// Open operation symbol for reducing all tensor cells to the maximum value.
34pub fn max_op_symbol() -> Symbol {
35    Symbol::qualified("tensor", "op/max")
36}
37
38/// Open operation symbol for Euclidean norm over all tensor cells.
39pub fn norm_op_symbol() -> Symbol {
40    Symbol::qualified("tensor", "op/norm")
41}
42
43/// Open operation symbol for matrix transpose.
44pub fn transpose_exec_op_symbol() -> Symbol {
45    Symbol::qualified("tensor", "op/transpose")
46}
47
48/// Open operation symbol for vector dot product.
49pub fn dot_op_symbol() -> Symbol {
50    Symbol::qualified("tensor", "op/dot")
51}
52
53/// Open operation symbol for matrix multiplication.
54pub fn matmul_exec_op_symbol() -> Symbol {
55    Symbol::qualified("tensor", "op/matmul")
56}
57
58/// Open operation symbol for element-wise square root.
59pub fn sqrt_op_symbol() -> Symbol {
60    Symbol::qualified("tensor", "op/sqrt")
61}
62
63/// Open operation symbol for element-wise exponential.
64pub fn exp_op_symbol() -> Symbol {
65    Symbol::qualified("tensor", "op/exp")
66}
67
68/// Open operation symbol for element-wise sine.
69pub fn sin_op_symbol() -> Symbol {
70    Symbol::qualified("tensor", "op/sin")
71}
72
73/// Open operation symbol for element-wise cosine.
74pub fn cos_op_symbol() -> Symbol {
75    Symbol::qualified("tensor", "op/cos")
76}
77
78/// Returns the operation symbols accepted by executor math providers.
79pub fn tensor_executor_math_op_symbols() -> Vec<Symbol> {
80    vec![
81        sum_op_symbol(),
82        min_op_symbol(),
83        max_op_symbol(),
84        norm_op_symbol(),
85        transpose_exec_op_symbol(),
86        dot_op_symbol(),
87        matmul_exec_op_symbol(),
88        sqrt_op_symbol(),
89        exp_op_symbol(),
90        sin_op_symbol(),
91        cos_op_symbol(),
92    ]
93}
94
95pub(crate) fn is_tensor_executor_math_op(symbol: &Symbol) -> bool {
96    tensor_executor_math_op_symbols()
97        .iter()
98        .any(|candidate| candidate == symbol)
99}
100
101/// Runs a whole-tensor sum, min, or max through the active executor.
102pub fn execute_tensor_reduction(cx: &mut Cx, operator: Symbol, tensor: &Tensor) -> Result<Tensor> {
103    let output_dtype = reduction_output_dtype(tensor);
104    let op = TensorOp::without_attributes(cx, operator)?;
105    execute_tensor_request(
106        cx,
107        TensorRequest::new(
108            op,
109            vec![tensor.clone()],
110            TensorMeta::new(Vec::new(), output_dtype),
111        ),
112    )
113}
114
115/// Runs a Euclidean whole-tensor norm through the active executor.
116pub fn execute_tensor_norm(cx: &mut Cx, tensor: &Tensor) -> Result<Tensor> {
117    let op = TensorOp::without_attributes(cx, norm_op_symbol())?;
118    execute_tensor_request(
119        cx,
120        TensorRequest::new(
121            op,
122            vec![tensor.clone()],
123            TensorMeta::new(Vec::new(), float_output_dtype(tensor)),
124        ),
125    )
126}
127
128/// Runs a rank-2 transpose through the active executor.
129pub fn execute_tensor_transpose(cx: &mut Cx, tensor: &Tensor) -> Result<Tensor> {
130    let [rows, cols] = tensor.shape() else {
131        return Err(Error::Eval("transpose expects a rank-2 tensor".to_owned()));
132    };
133    let op = TensorOp::without_attributes(cx, transpose_exec_op_symbol())?;
134    execute_tensor_request(
135        cx,
136        TensorRequest::new(
137            op,
138            vec![tensor.clone()],
139            TensorMeta::new(vec![*cols, *rows], tensor.dtype().clone()),
140        ),
141    )
142}
143
144/// Runs a vector dot product through the active executor.
145pub fn execute_tensor_dot(cx: &mut Cx, left: &Tensor, right: &Tensor) -> Result<Tensor> {
146    if left.shape().len() != 1 || right.shape().len() != 1 {
147        return Err(Error::Eval("dot expects two rank-1 tensors".to_owned()));
148    }
149    if left.shape() != right.shape() {
150        return Err(Error::Eval(
151            "dot expects vectors with matching lengths".to_owned(),
152        ));
153    }
154    let op = TensorOp::without_attributes(cx, dot_op_symbol())?;
155    execute_tensor_request(
156        cx,
157        TensorRequest::new(
158            op,
159            vec![left.clone(), right.clone()],
160            TensorMeta::new(Vec::new(), reduction_pair_dtype(left, right)),
161        ),
162    )
163}
164
165/// Runs vector or matrix multiplication through the active executor.
166pub fn execute_tensor_matmul(cx: &mut Cx, left: &Tensor, right: &Tensor) -> Result<Tensor> {
167    let shape = matmul_output_shape(left.shape(), right.shape())?;
168    let op = TensorOp::without_attributes(cx, matmul_exec_op_symbol())?;
169    execute_tensor_request(
170        cx,
171        TensorRequest::new(
172            op,
173            vec![left.clone(), right.clone()],
174            TensorMeta::new(shape, reduction_pair_dtype(left, right)),
175        ),
176    )
177}
178
179/// Runs an f32/f64/half element-wise transcendental through the active executor.
180pub fn execute_tensor_transcendental(
181    cx: &mut Cx,
182    operator: Symbol,
183    tensor: &Tensor,
184) -> Result<Tensor> {
185    if !matches_tensor_transcendental(&operator) {
186        return Err(Error::Eval(format!(
187            "unsupported tensor transcendental operation {operator}"
188        )));
189    }
190    let op = TensorOp::without_attributes(cx, operator)?;
191    execute_tensor_request(
192        cx,
193        TensorRequest::new(
194            op,
195            vec![tensor.clone()],
196            TensorMeta::new(tensor.shape().to_vec(), float_output_dtype(tensor)),
197        ),
198    )
199}
200
201pub(crate) fn execute_tensor_math_request(
202    cx: &mut Cx,
203    request: &TensorRequest,
204) -> std::result::Result<Tensor, TensorExecError> {
205    let operation = &request.operation.symbol;
206    if *operation == sum_op_symbol()
207        || *operation == min_op_symbol()
208        || *operation == max_op_symbol()
209    {
210        execute_reduction_request(cx, request)
211    } else if *operation == norm_op_symbol() {
212        execute_norm_request(cx, request)
213    } else if *operation == transpose_exec_op_symbol() {
214        execute_transpose_request(cx, request)
215    } else if *operation == dot_op_symbol() {
216        execute_dot_request(cx, request)
217    } else if *operation == matmul_exec_op_symbol() {
218        execute_matmul_request(cx, request)
219    } else if matches_tensor_transcendental(operation) {
220        execute_transcendental_request(cx, request)
221    } else {
222        Err(TensorExecError::unsupported(
223            operation.clone(),
224            "unknown tensor math operation",
225        ))
226    }
227}
228
229fn execute_reduction_request(
230    cx: &mut Cx,
231    request: &TensorRequest,
232) -> std::result::Result<Tensor, TensorExecError> {
233    let [tensor] = request.inputs.as_ref() else {
234        return Err(TensorExecError::invalid(
235            "tensor reduction expects exactly one tensor input",
236        ));
237    };
238    let value = if request.operation.symbol == sum_op_symbol() {
239        reduce_sum(cx, tensor)?
240    } else if request.operation.symbol == min_op_symbol() {
241        reduce_min_max(cx, tensor, false)?
242    } else {
243        reduce_min_max(cx, tensor, true)?
244    };
245    scalar_tensor(cx, request.output.dtype().clone(), value)
246}
247
248fn execute_norm_request(
249    cx: &mut Cx,
250    request: &TensorRequest,
251) -> std::result::Result<Tensor, TensorExecError> {
252    let [tensor] = request.inputs.as_ref() else {
253        return Err(TensorExecError::invalid(
254            "tensor norm expects exactly one tensor input",
255        ));
256    };
257    let value = norm_value(cx, tensor)?;
258    scalar_tensor(cx, request.output.dtype().clone(), value)
259}
260
261fn execute_transpose_request(
262    cx: &mut Cx,
263    request: &TensorRequest,
264) -> std::result::Result<Tensor, TensorExecError> {
265    let [tensor] = request.inputs.as_ref() else {
266        return Err(TensorExecError::invalid(
267            "transpose expects exactly one tensor input",
268        ));
269    };
270    let [rows, cols] = tensor.shape() else {
271        return Err(TensorExecError::invalid("transpose expects rank-2 input"));
272    };
273    let mut out = Vec::with_capacity(tensor.len());
274    for col in 0..*cols {
275        for row in 0..*rows {
276            out.push(
277                tensor
278                    .cell(row * cols + col)
279                    .map_err(TensorExecError::from)?,
280            );
281        }
282    }
283    tensor_from_cells(
284        cx,
285        request.output.shape().to_vec(),
286        request.output.dtype().clone(),
287        out,
288    )
289}
290
291fn execute_dot_request(
292    cx: &mut Cx,
293    request: &TensorRequest,
294) -> std::result::Result<Tensor, TensorExecError> {
295    let [left, right] = request.inputs.as_ref() else {
296        return Err(TensorExecError::invalid(
297            "dot expects exactly two tensor inputs",
298        ));
299    };
300    if left.shape().len() != 1 || right.shape().len() != 1 || left.shape() != right.shape() {
301        return Err(TensorExecError::invalid(
302            "dot expects matching rank-1 tensor inputs",
303        ));
304    }
305    let value = sum_products(
306        cx,
307        ProductSpec {
308            left,
309            right,
310            left_start: 0,
311            right_start: 0,
312            count: left.shape()[0],
313            left_stride: 1,
314            right_stride: 1,
315        },
316    )?;
317    scalar_tensor(cx, request.output.dtype().clone(), value)
318}
319
320fn execute_matmul_request(
321    cx: &mut Cx,
322    request: &TensorRequest,
323) -> std::result::Result<Tensor, TensorExecError> {
324    let [left, right] = request.inputs.as_ref() else {
325        return Err(TensorExecError::invalid(
326            "matmul expects exactly two tensor inputs",
327        ));
328    };
329    let out_shape =
330        matmul_output_shape(left.shape(), right.shape()).map_err(TensorExecError::from)?;
331    if out_shape != request.output.shape() {
332        return Err(TensorExecError::shape(format!(
333            "matmul output shape {:?} did not match {:?}",
334            out_shape,
335            request.output.shape()
336        )));
337    }
338    match (left.shape(), right.shape()) {
339        ([n], [m]) if n == m => {
340            let value = sum_products(
341                cx,
342                ProductSpec {
343                    left,
344                    right,
345                    left_start: 0,
346                    right_start: 0,
347                    count: *n,
348                    left_stride: 1,
349                    right_stride: 1,
350                },
351            )?;
352            scalar_tensor(cx, request.output.dtype().clone(), value)
353        }
354        ([rows, inner_left], [inner_right, cols]) if inner_left == inner_right => {
355            let mut out = Vec::with_capacity(rows * cols);
356            for row in 0..*rows {
357                for col in 0..*cols {
358                    out.push(sum_products(
359                        cx,
360                        ProductSpec {
361                            left,
362                            right,
363                            left_start: row * inner_left,
364                            right_start: col,
365                            count: *inner_left,
366                            left_stride: 1,
367                            right_stride: *cols,
368                        },
369                    )?);
370                }
371            }
372            tensor_from_cells(cx, out_shape, request.output.dtype().clone(), out)
373        }
374        ([rows, inner_left], [inner_right]) if inner_left == inner_right => {
375            let mut out = Vec::with_capacity(*rows);
376            for row in 0..*rows {
377                out.push(sum_products(
378                    cx,
379                    ProductSpec {
380                        left,
381                        right,
382                        left_start: row * inner_left,
383                        right_start: 0,
384                        count: *inner_left,
385                        left_stride: 1,
386                        right_stride: 1,
387                    },
388                )?);
389            }
390            tensor_from_cells(cx, out_shape, request.output.dtype().clone(), out)
391        }
392        ([inner_left], [inner_right, cols]) if inner_left == inner_right => {
393            let mut out = Vec::with_capacity(*cols);
394            for col in 0..*cols {
395                out.push(sum_products(
396                    cx,
397                    ProductSpec {
398                        left,
399                        right,
400                        left_start: 0,
401                        right_start: col,
402                        count: *inner_left,
403                        left_stride: 1,
404                        right_stride: *cols,
405                    },
406                )?);
407            }
408            tensor_from_cells(cx, out_shape, request.output.dtype().clone(), out)
409        }
410        _ => Err(TensorExecError::invalid(
411            "matmul supports rank-1 and rank-2 tensors with matching inner dimensions",
412        )),
413    }
414}
415
416fn execute_transcendental_request(
417    cx: &mut Cx,
418    request: &TensorRequest,
419) -> std::result::Result<Tensor, TensorExecError> {
420    let [tensor] = request.inputs.as_ref() else {
421        return Err(TensorExecError::invalid(
422            "transcendental expects exactly one tensor input",
423        ));
424    };
425    let cells = tensor.cells().map_err(TensorExecError::from)?;
426    let mut out = Vec::with_capacity(cells.len());
427    for cell in cells.iter() {
428        out.push(transcendental_cell(
429            cx,
430            cell,
431            &request.operation.symbol,
432            request.output.dtype(),
433        )?);
434    }
435    tensor_from_cells(
436        cx,
437        request.output.shape().to_vec(),
438        request.output.dtype().clone(),
439        out,
440    )
441}