Files
petrbalvin af4ee19703
Release / gates (push) Successful in 4m38s
Test / test (push) Successful in 5m16s
Release / release (push) Successful in 35s
feat: initial release
Assisted-by: GLM 5.3 Flash
2026-09-03 10:00:00 +02:00

1495 lines
45 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package core
import (
"fmt"
"math"
"strconv"
"sourcedock.dev/petrbalvin/tensor/internal/engine"
)
// Scalar boxes a single numeric result whose dtype is known only at
// runtime: Sum of an int array is an int, of a float array a
// float, of a complex array a complex. Int, Float and Complex unpack it;
// IsFloat and IsComplex say which.
type Scalar struct {
isFloat bool
isComplex bool
i int64
f float64
c complex128
}
// Int returns the scalar as an int64. It converts a float or complex
// scalar by truncation, mirroring Go's conversions; use IsFloat and
// IsComplex when the distinction matters.
func (s Scalar) Int() int64 {
if s.isFloat {
return int64(s.f)
}
if s.isComplex {
return int64(real(s.c))
}
return s.i
}
// Float returns the scalar as a float64, converting int and complex
// scalars (a complex scalar contributes its real part).
func (s Scalar) Float() float64 {
if s.isFloat {
return s.f
}
if s.isComplex {
return real(s.c)
}
return float64(s.i)
}
// Complex returns the scalar as a complex128, converting real scalars.
func (s Scalar) Complex() complex128 {
if s.isComplex {
return s.c
}
if s.isFloat {
return complex(s.f, 0)
}
return complex(float64(s.i), 0)
}
// IsFloat reports whether the scalar came from a float computation.
func (s Scalar) IsFloat() bool { return s.isFloat }
// IsComplex reports whether the scalar came from a complex computation.
func (s Scalar) IsComplex() bool { return s.isComplex }
// String renders the scalar with its dtype, as in "int 7" or
// "complex (4-2i)". The complex form is the one %v prints, matching
// Array.String: a single sign between the parts, never "+-".
func (s Scalar) String() string {
switch {
case s.isComplex:
return "complex " + fmt.Sprintf("%v", s.c)
case s.isFloat:
return "float " + strconv.FormatFloat(s.f, 'g', -1, 64)
default:
return "int " + strconv.FormatInt(s.i, 10)
}
}
// Sum returns the sum of all elements. An int sum wraps on overflow like
// Go's int64 arithmetic; the narrow integer widths widen
// exactly and accumulate in int64, answered as an Int scalar, and a bool
// sum counts its true elements into the same Int scalar; float16 and
// float32 sums accumulate in float64 and answer a float scalar;
// an empty array sums to zero.
//
// The fold is partitioned by length alone, never by the worker count, so
// the same input answers the same scalar on any machine and under any
// worker setting: the range is cut into fixed chunks, each chunk
// accumulates into four interleaved partials, and the chunk results
// combine through a balanced pairwise tree over the chunk indices
// (treeSum). The tree is the shape the distributed reduction shares: a
// range cut at chunk boundaries into shards answers the same bits as
// this fold, because shard partials and array chunks are entries of one
// tree combined by one function. The integer sum is exact under any
// order. The floating-point fold rounds differently from the single
// chain it replaced, not always in the same direction: the interleaved
// partials shorten each dependency chain and the tree adds a logarithmic
// number of combination roundings, which measured within a small factor
// of the chain at every length tried and better than it once the chain
// is long enough for its own roundings to accumulate. Only the array's
// own elements take part: a rebased view's payload may run past its
// element count, and those invisible tail slots never contribute.
func Sum(a *Array) Scalar {
n := a.Len()
switch a.dt {
case Int:
return Scalar{i: intFoldSum(a.ints[:n])}
case Bool:
// The bool sum counts the true elements, answered as the Int
// scalar every integer-class reduction answers.
return Scalar{i: boolFoldCount(a.bools[:n])}
case Int8:
return Scalar{i: intFoldSumNarrow(a.i8s[:n])}
case Uint8:
return Scalar{i: intFoldSumNarrow(a.u8s[:n])}
case Int16:
return Scalar{i: intFoldSumNarrow(a.i16s[:n])}
case Uint16:
return Scalar{i: intFoldSumNarrow(a.u16s[:n])}
case Int32:
return Scalar{i: intFoldSumNarrow(a.i32s[:n])}
case Uint32:
return Scalar{i: intFoldSumNarrow(a.u32s[:n])}
case Float16:
// The half sum accumulates in float64 and answers a float
// scalar, exactly as the float32 sum does: every
// widening is exact, so the fold sees the same addends an
// accessor walk would hand over.
return Scalar{isFloat: true, f: floatFoldSumF16(a.halves[:n])}
case Float32:
return Scalar{isFloat: true, f: floatFoldSumF32(a.floats32[:n])}
case Float:
return Scalar{isFloat: true, f: floatFoldSum(a.floats[:n])}
default:
return Scalar{isComplex: true, c: complexFoldSum(a.complexes[:n])}
}
}
// foldChunk is the element count one partial fold carries. It is a
// constant of the arithmetic: the partition follows from the length
// alone, never from the worker count, so the same input answers the
// same scalar on any machine and under any worker setting.
const foldChunk = 1 << 16
// foldParts is the number of chunks a length is cut into, bounded so the
// partial table stays small for very long arrays.
func foldParts(n int) int {
if n <= foldChunk {
return 1
}
parts := min((n+foldChunk-1)/foldChunk, 1<<12)
return parts
}
// FoldParts reports how many blocks the canonical reduction partition
// cuts a range of n elements into. The partition is a function of the
// length alone, so it is the same on every machine and under any worker
// setting; the spmd package cuts distributed data on these boundaries,
// which is what makes a sharded reduction compose into the single-array
// fold's exact bits.
func FoldParts(n int) int { return foldParts(n) }
// FoldBoundary reports the index where block c of the canonical
// partition of n elements begins; block c spans [FoldBoundary(n, c),
// FoldBoundary(n, c+1)). Block 0 begins at 0 and FoldBoundary(n,
// FoldParts(n)) is n.
func FoldBoundary(n, c int) int { return c * n / foldParts(n) }
// TreeSum combines reduction partials through the balanced pairwise
// tree the folds combine their chunk partials with: the node over a
// range splits at its midpoint and adds the two halves' nodes, left
// before right. The value of any contiguous range of partials is one
// node of the tree, so partials gathered from a sharded cut combine
// into the single-array fold's exact bits; the spmd package feeds it
// the block values the shards folded.
func TreeSum[N int64 | float64 | complex128](vals []N) N { return treeSum(vals) }
// FoldRange is one canonical block's fold over a float64 payload: the
// four interleaved chains combined as a balanced pair. Shards fold
// their own blocks with it, so a block's value is the same bits
// wherever its elements live.
func FoldRange(src []float64) float64 { return foldRange(src) }
// FoldRangeF32 is FoldRange over a float32 payload; every widening is
// exact.
func FoldRangeF32(src []float32) float64 { return foldRangeF32(src) }
// FoldRangeF16 is FoldRange over a half payload's raw bit patterns;
// every widening is exact.
func FoldRangeF16(src []uint16) float64 { return foldRangeF16(src) }
// FoldRangeC128 is FoldRange over a complex payload.
func FoldRangeC128(src []complex128) complex128 { return foldRangeC128(src) }
// ExtremeRange is the serial extremum rule on one canonical block of a
// float64 payload: seed past the leading NaNs, keep the first strictly
// better value, and report whether the block holds a candidate at all.
func ExtremeRange(src []float64, greater bool) (float64, bool) { return floatExtreme(src, greater) }
// ExtremeRangeF32 is ExtremeRange over a float32 payload; every
// widening is exact.
func ExtremeRangeF32(src []float32, greater bool) (float64, bool) {
return float32Extreme(src, greater)
}
// ExtremeRangeF16 is ExtremeRange over a half payload's raw bit
// patterns; every widening is exact.
func ExtremeRangeF16(src []uint16, greater bool) (float64, bool) { return halfExtreme(src, greater) }
// CombineExtrema combines the blocks' extrema with the serial walk's
// strict comparison in index order: a tie keeps the earlier block's
// value, a block with no candidate contributes nothing, and an array
// with no candidate anywhere answers the last block's fallback, which
// is the whole array's last element. The extrema the spmd shards fold
// combine through it, so a sharded extremum is the single-array
// extremum bit for bit.
func CombineExtrema(vals []float64, oks []bool, greater bool) float64 {
return combineExtreme(vals, oks, greater)
}
// FloatScalar boxes a float64 as the scalar the float reductions
// answer.
func FloatScalar(f float64) Scalar { return Scalar{isFloat: true, f: f} }
// IntScalar boxes an int64 as the scalar the integer-class reductions
// answer.
func IntScalar(i int64) Scalar { return Scalar{i: i} }
// ComplexScalar boxes a complex128 as the scalar the complex
// reductions answer.
func ComplexScalar(c complex128) Scalar { return Scalar{isComplex: true, c: c} }
// TreeProd combines partial products through the balanced midpoint
// tree, multiplying the left half before the right: partials gathered
// from a sharded cut combine into the single-array product's exact
// bits. The float32 partials multiply natively in float32, the way the
// float32 product fold keeps.
func TreeProd[N int64 | float64 | float32](vals []N) N { return treeProd(vals) }
// TreeProdHalf combines half-precision partial products: the values
// carry exact half bits in float64 and every combine narrows through
// half, the per-step rounding the half product fold keeps.
func TreeProdHalf(vals []float64) float64 { return treeProdHalf(vals) }
// FoldProd is one canonical block's product over a float64 payload.
// Shards fold their own blocks with it, so a block's product is the
// same bits wherever its elements live.
func FoldProd(src []float64) float64 { return foldProdRange(src) }
// FoldProdF32 is FoldProd over a float32 payload, multiplied natively
// in float32.
func FoldProdF32(src []float32) float32 { return foldProdRangeF32(src) }
// FoldProdF16 is FoldProd over a half payload's raw bit patterns, with
// the per-step half rounding the line product keeps; the answer is an
// exact half value carried in float64.
func FoldProdF16(src []uint16) float64 { return foldProdRangeF16(src) }
// FoldProdI64 is FoldProd over an int64 payload; the wrapping product
// is exact under any grouping.
func FoldProdI64(src []int64) int64 { return foldProdRangeI64(src) }
// FoldNormPower is one canonical block's sum of |v|^p over a float64
// payload, with the per-element arithmetic the norm fold keeps.
func FoldNormPower(src []float64, p float64) float64 { return foldNormPowerRange(src, p) }
// FoldNormPowerF32 is FoldNormPower over a float32 payload; every
// widening is exact.
func FoldNormPowerF32(src []float32, p float64) float64 { return foldNormPowerRangeF32(src, p) }
// FoldNormPowerF16 is FoldNormPower over a half payload's raw bit
// patterns; every widening is exact.
func FoldNormPowerF16(src []uint16, p float64) float64 { return foldNormPowerRangeF16(src, p) }
// FoldNormPowerI64 is FoldNormPower over an int64 payload.
func FoldNormPowerI64(src []int64, p float64) float64 { return foldNormPowerRangeI64(src, p) }
// NormRoot closes a power sum into the norm: Sqrt for p = 2, the sum
// for p = 1, Pow of the sum for every other exponent.
func NormRoot(sum, p float64) float64 { return normRoot(sum, p) }
// FoldDot is one canonical block's dot product over float64 payloads
// of equal length.
func FoldDot(x, y []float64) float64 { return foldDotRange(x, y) }
// FoldDotF32 is FoldDot over float32 payloads; every product is exact
// in float64.
func FoldDotF32(x, y []float32) float64 { return foldDotRangeF32(x, y) }
// FoldDotF16 is FoldDot over half payloads' raw bit patterns; every
// product is exact in float64.
func FoldDotF16(x, y []uint16) float64 { return foldDotRangeF16(x, y) }
// FoldDotC128 is FoldDot over complex payloads.
func FoldDotC128(x, y []complex128) complex128 { return foldDotRangeC128(x, y) }
// FoldDotI64 is FoldDot over int64 payloads: the wrapping products and
// the wrapping sums are exact under any grouping.
func FoldDotI64(x, y []int64) int64 {
var s int64
for i := range x {
s += x[i] * y[i]
}
return s
}
// foldRange runs one chunk's fold over a float payload: four interleaved
// chains, so the adds of a long array overlap instead of queueing on one
// adder, combined as ((s0+s1)+(s2+s3)), the pairing a balanced tree
// gives. Each chain carries a quarter of the chunk, so the dependency
// chain is four times shorter and the combination costs three roundings;
// against the single chain it replaced the total error measured within a
// small factor either way, so this form is chosen for the throughput and
// not for a claimed accuracy win.
func foldRange(src []float64) float64 {
var s0, s1, s2, s3 float64
i := 0
for ; i+4 <= len(src); i += 4 {
s0 += src[i]
s1 += src[i+1]
s2 += src[i+2]
s3 += src[i+3]
}
for ; i < len(src); i++ {
s0 += src[i]
}
return (s0 + s1) + (s2 + s3)
}
// treeSum combines chunk partials with a balanced pairwise tree over
// their indices: the node over a range splits at its midpoint and adds
// the two halves' nodes, left before right. The shape depends on nothing
// but the partial count, and the value of any contiguous range of
// partials is one node of the tree. That property is what lets a
// reduction cut at chunk boundaries into shards reproduce this fold
// exactly, for any number of shards: the spmd package gathers the shard
// partials and combines them with this same function.
func treeSum[N int64 | float64 | complex128](vals []N) N {
return treeSumRange(vals, 0, len(vals))
}
// treeSumRange is one node of the treeSum tree over the partials [l, r).
func treeSumRange[N int64 | float64 | complex128](vals []N, l, r int) N {
if r-l == 1 {
return vals[l]
}
m := l + (r-l)/2
return treeSumRange(vals, l, m) + treeSumRange(vals, m, r)
}
// treeProd combines partial products through the balanced midpoint
// tree treeSum uses, multiplying the left half before the right. The
// shape depends on nothing but the partial count, so partial products
// gathered from a sharded cut combine into the single-array product's
// exact bits; the spmd package feeds it the block values the shards
// folded.
func treeProd[N int64 | float64 | float32](vals []N) N {
return treeProdRange(vals, 0, len(vals))
}
func treeProdRange[N int64 | float64 | float32](vals []N, l, r int) N {
if r-l == 1 {
return vals[l]
}
m := l + (r-l)/2
return treeProdRange(vals, l, m) * treeProdRange(vals, m, r)
}
// treeProdHalf is treeProd for a half-precision product: the partials
// carry exact half values and every combine narrows through half, the
// per-step rounding the half product fold keeps.
func treeProdHalf(vals []float64) float64 {
return treeProdHalfRange(vals, 0, len(vals))
}
func treeProdHalfRange(vals []float64, l, r int) float64 {
if r-l == 1 {
return vals[l]
}
m := l + (r-l)/2
return halfRound(treeProdHalfRange(vals, l, m) * treeProdHalfRange(vals, m, r))
}
// halfRound narrows through half and back, the per-step rounding the
// half precision folds keep.
func halfRound(f float64) float64 {
return HalfToFloat64(HalfFromFloat64(f))
}
// foldProdRangeI64 is one canonical block's product over an int64
// payload: wrapping multiplication is associative, so the grouping
// cannot change the value.
func foldProdRangeI64(src []int64) int64 {
m := int64(1)
for _, v := range src {
m *= v
}
return m
}
// foldProdRange is one canonical block's product over a float64
// payload: a single chain from one, the association the product fold
// keeps inside a block.
func foldProdRange(src []float64) float64 {
m := 1.0
for _, v := range src {
m *= v
}
return m
}
// foldProdRangeF32 is one canonical block's product over a float32
// payload, multiplied natively in float32.
func foldProdRangeF32(src []float32) float32 {
m := float32(1)
for _, v := range src {
m *= v
}
return m
}
// foldProdRangeF16 is one canonical block's product over a half
// payload: the running product narrows through half before every
// combine, exactly the per-step rounding the line fold keeps, and the
// answer is an exact half value carried in float64.
func foldProdRangeF16(src []uint16) float64 {
m := 1.0
for _, v := range src {
m = halfRound(m * HalfToFloat64(v))
}
return m
}
// foldNormPowerRange is one canonical block's sum of |v|^p over a
// float64 payload, with the same per-element arithmetic the norm fold
// keeps: the bare absolute for p = 1, the squared absolute for p = 2,
// and Pow of the absolute for every other exponent.
func foldNormPowerRange(src []float64, p float64) float64 {
switch {
case p == 1:
var acc float64
for _, v := range src {
acc += math.Abs(v)
}
return acc
case p == 2:
var acc float64
for _, v := range src {
w := math.Abs(v)
acc += w * w
}
return acc
default:
var acc float64
for _, v := range src {
acc += math.Pow(math.Abs(v), p)
}
return acc
}
}
// foldNormPowerRangeF32 is foldNormPowerRange over a float32 payload;
// every widening is exact.
func foldNormPowerRangeF32(src []float32, p float64) float64 {
switch {
case p == 1:
var acc float64
for _, v := range src {
acc += math.Abs(float64(v))
}
return acc
case p == 2:
var acc float64
for _, v := range src {
w := math.Abs(float64(v))
acc += w * w
}
return acc
default:
var acc float64
for _, v := range src {
acc += math.Pow(math.Abs(float64(v)), p)
}
return acc
}
}
// foldNormPowerRangeF16 is foldNormPowerRange over a half payload's
// raw bit patterns; every widening is exact.
func foldNormPowerRangeF16(src []uint16, p float64) float64 {
switch {
case p == 1:
var acc float64
for _, v := range src {
acc += math.Abs(HalfToFloat64(v))
}
return acc
case p == 2:
var acc float64
for _, v := range src {
w := math.Abs(HalfToFloat64(v))
acc += w * w
}
return acc
default:
var acc float64
for _, v := range src {
acc += math.Pow(math.Abs(HalfToFloat64(v)), p)
}
return acc
}
}
// foldNormPowerRangeI64 is foldNormPowerRange over an int64 payload.
func foldNormPowerRangeI64(src []int64, p float64) float64 {
switch {
case p == 1:
var acc float64
for _, v := range src {
acc += math.Abs(float64(v))
}
return acc
case p == 2:
var acc float64
for _, v := range src {
w := math.Abs(float64(v))
acc += w * w
}
return acc
default:
var acc float64
for _, v := range src {
acc += math.Pow(math.Abs(float64(v)), p)
}
return acc
}
}
// normRoot closes a power sum into the norm: Sqrt for p = 2, the sum
// itself for p = 1, and Pow of the sum for every other exponent, the
// closing the norm fold keeps.
func normRoot(sum, p float64) float64 {
switch {
case p == 2:
return math.Sqrt(sum)
case p == 1:
return sum
default:
return math.Pow(sum, 1/p)
}
}
// foldDotRange is one canonical block's dot product over float64
// payloads of equal length: four interleaved product chains, combined
// as a balanced pair.
func foldDotRange(x, y []float64) float64 {
var s0, s1, s2, s3 float64
i := 0
for ; i+4 <= len(x); i += 4 {
s0 += x[i] * y[i]
s1 += x[i+1] * y[i+1]
s2 += x[i+2] * y[i+2]
s3 += x[i+3] * y[i+3]
}
for ; i < len(x); i++ {
s0 += x[i] * y[i]
}
return (s0 + s1) + (s2 + s3)
}
// foldDotRangeF32 is foldDotRange over float32 payloads; every product
// is exact in float64.
func foldDotRangeF32(x, y []float32) float64 {
var s0, s1, s2, s3 float64
i := 0
for ; i+4 <= len(x); i += 4 {
s0 += float64(x[i]) * float64(y[i])
s1 += float64(x[i+1]) * float64(y[i+1])
s2 += float64(x[i+2]) * float64(y[i+2])
s3 += float64(x[i+3]) * float64(y[i+3])
}
for ; i < len(x); i++ {
s0 += float64(x[i]) * float64(y[i])
}
return (s0 + s1) + (s2 + s3)
}
// foldDotRangeF16 is foldDotRange over half payloads' raw bit
// patterns; every product is exact in float64.
func foldDotRangeF16(x, y []uint16) float64 {
var s0, s1, s2, s3 float64
i := 0
for ; i+4 <= len(x); i += 4 {
s0 += HalfToFloat64(x[i]) * HalfToFloat64(y[i])
s1 += HalfToFloat64(x[i+1]) * HalfToFloat64(y[i+1])
s2 += HalfToFloat64(x[i+2]) * HalfToFloat64(y[i+2])
s3 += HalfToFloat64(x[i+3]) * HalfToFloat64(y[i+3])
}
for ; i < len(x); i++ {
s0 += HalfToFloat64(x[i]) * HalfToFloat64(y[i])
}
return (s0 + s1) + (s2 + s3)
}
// foldDotRangeC128 is foldDotRange over complex payloads.
func foldDotRangeC128(x, y []complex128) complex128 {
var s0, s1, s2, s3 complex128
i := 0
for ; i+4 <= len(x); i += 4 {
s0 += x[i] * y[i]
s1 += x[i+1] * y[i+1]
s2 += x[i+2] * y[i+2]
s3 += x[i+3] * y[i+3]
}
for ; i < len(x); i++ {
s0 += x[i] * y[i]
}
return (s0 + s1) + (s2 + s3)
}
// floatFoldSum sums a float64 payload over the fixed partition.
func floatFoldSum(src []float64) float64 {
parts := foldParts(len(src))
if parts == 1 {
return foldRange(src)
}
partials := make([]float64, parts)
engine.Parallel(parts, func(cs, ce int) {
for c := cs; c < ce; c++ {
partials[c] = foldRange(src[c*len(src)/parts : (c+1)*len(src)/parts])
}
})
return treeSum(partials)
}
// foldRangeF32 is one block's fold over a float32 payload: four
// interleaved float64 chains, every widening exact.
func foldRangeF32(part []float32) float64 {
var s0, s1, s2, s3 float64
i := 0
for ; i+4 <= len(part); i += 4 {
s0 += float64(part[i])
s1 += float64(part[i+1])
s2 += float64(part[i+2])
s3 += float64(part[i+3])
}
for ; i < len(part); i++ {
s0 += float64(part[i])
}
return (s0 + s1) + (s2 + s3)
}
// foldRangeF16 is one block's fold over a half payload's raw bit
// patterns, widening each element exactly as it is read.
func foldRangeF16(part []uint16) float64 {
var s0, s1, s2, s3 float64
i := 0
for ; i+4 <= len(part); i += 4 {
s0 += HalfToFloat64(part[i])
s1 += HalfToFloat64(part[i+1])
s2 += HalfToFloat64(part[i+2])
s3 += HalfToFloat64(part[i+3])
}
for ; i < len(part); i++ {
s0 += HalfToFloat64(part[i])
}
return (s0 + s1) + (s2 + s3)
}
// floatFoldSumF32 sums a float32 payload: every widening to float64 is
// exact, so the fold sees the values an accessor walk would hand over.
func floatFoldSumF32(src []float32) float64 {
parts := foldParts(len(src))
return sumOverParts(len(src), parts, func(c int) float64 {
return foldRangeF32(src[c*len(src)/parts : (c+1)*len(src)/parts])
})
}
// floatFoldSumF16 sums a half-precision payload, widening each element
// exactly as it is read.
func floatFoldSumF16(src []uint16) float64 {
parts := foldParts(len(src))
return sumOverParts(len(src), parts, func(c int) float64 {
return foldRangeF16(src[c*len(src)/parts : (c+1)*len(src)/parts])
})
}
// sumOverParts runs the per-chunk fold and combines the results through
// the balanced partial tree: the only order-dependent step, and its shape
// depends on nothing but the chunk count.
func sumOverParts(n, parts int, fold func(c int) float64) float64 {
if parts == 1 {
return fold(0)
}
partials := make([]float64, parts)
engine.Parallel(parts, func(cs, ce int) {
for c := cs; c < ce; c++ {
partials[c] = fold(c)
}
})
return treeSum(partials)
}
// intFoldSum sums an int64 payload: wrapping addition is associative, so
// the partition cannot change the value and a plain chunked scan needs no
// interleaved partials.
func intFoldSum(src []int64) int64 {
parts := foldParts(len(src))
if parts == 1 {
var s int64
for _, v := range src {
s += v
}
return s
}
partials := make([]int64, parts)
engine.Parallel(parts, func(cs, ce int) {
for c := cs; c < ce; c++ {
var s int64
for _, v := range src[c*len(src)/parts : (c+1)*len(src)/parts] {
s += v
}
partials[c] = s
}
})
var total int64
for _, v := range partials {
total += v
}
return total
}
// intFoldSumNarrow sums a narrow integer payload into int64 through the
// fixed partition intFoldSum uses; every widening is exact, so the sum
// stays machine-independent and the accumulation order changes nothing.
func intFoldSumNarrow[T int8 | uint8 | int16 | uint16 | int32 | uint32](src []T) int64 {
parts := foldParts(len(src))
if parts == 1 {
var s int64
for _, v := range src {
s += int64(v)
}
return s
}
partials := make([]int64, parts)
engine.Parallel(parts, func(cs, ce int) {
for c := cs; c < ce; c++ {
var s int64
for _, v := range src[c*len(src)/parts : (c+1)*len(src)/parts] {
s += int64(v)
}
partials[c] = s
}
})
var total int64
for _, v := range partials {
total += v
}
return total
}
// boolFoldCount counts the true elements of a bool payload through the
// same fixed partition intFoldSum uses.
func boolFoldCount(src []bool) int64 {
parts := foldParts(len(src))
if parts == 1 {
var s int64
for _, v := range src {
if v {
s++
}
}
return s
}
partials := make([]int64, parts)
engine.Parallel(parts, func(cs, ce int) {
for c := cs; c < ce; c++ {
var s int64
for _, v := range src[c*len(src)/parts : (c+1)*len(src)/parts] {
if v {
s++
}
}
partials[c] = s
}
})
var total int64
for _, v := range partials {
total += v
}
return total
}
// foldRangeC128 is one block's fold over a complex payload: four
// interleaved chains, combined as a balanced pair.
func foldRangeC128(part []complex128) complex128 {
var s0, s1, s2, s3 complex128
i := 0
for ; i+4 <= len(part); i += 4 {
s0 += part[i]
s1 += part[i+1]
s2 += part[i+2]
s3 += part[i+3]
}
for ; i < len(part); i++ {
s0 += part[i]
}
return (s0 + s1) + (s2 + s3)
}
// complexFoldSum sums a complex payload the way the float fold sums a
// real one: fixed chunks, four interleaved partials, partials combined
// through the balanced partial tree.
func complexFoldSum(src []complex128) complex128 {
parts := foldParts(len(src))
if parts == 1 {
return foldRangeC128(src)
}
partials := make([]complex128, parts)
engine.Parallel(parts, func(cs, ce int) {
for c := cs; c < ce; c++ {
partials[c] = foldRangeC128(src[c*len(src)/parts : (c+1)*len(src)/parts])
}
})
return treeSum(partials)
}
// Min returns the smallest element; an empty array, or a complex array
// (no ordering), is an error.
func Min(a *Array) (Scalar, error) {
return a.reduceOrder("Min", false)
}
// Max returns the largest element; an empty array, or a complex array
// (no ordering), is an error.
func Max(a *Array) (Scalar, error) {
return a.reduceOrder("Max", true)
}
// Mean returns the arithmetic mean of all elements as a float64,
// computed in float64 even for int arrays (true division); an empty or
// complex array is an error.
//
// The sum runs through Sum, so every ordinary input rounds exactly as
// the standalone Sum does. The one exception is the input whose largest
// magnitude could carry the running total past float64's range: there
// the plain sum overflows to an Inf the final division cannot undo
// (Mean of two MaxFloat64s answered +Inf). Such an input takes a scaled
// two-pass form instead: the elements are summed divided by the largest
// magnitude and the result rescaled, which stays finite. The scale is
// chosen by the pre-pass below, so any input below the overflow
// threshold, and every int array whose whole range sits far below
// float64's ceiling, keeps the plain path and its digest.
func Mean(a *Array) (float64, error) {
if a.dt == Complex {
return 0, errf("Mean: complex arrays have no float mean")
}
if a.Len() == 0 {
return 0, errf("Mean: an empty array has no mean")
}
n := a.Len()
fn := float64(n)
// Every widening in this pre-pass is exact, and NaN fails the
// comparison, so a NaN payload never triggers the scaled path and
// reaches the caller through Sum's fold as before. The walk reads the
// payload directly, which is the value the accessor would hand over.
maxAbs := a.maxAbs(n)
if maxAbs > math.MaxFloat64/fn && !math.IsInf(maxAbs, 0) {
var acc float64
for i := range n {
acc += a.floatAt(i) / maxAbs
}
// The rescale divides by n first: multiplying acc by maxAbs
// before the division could overflow again, which is the very
// failure this path exists to avoid.
return acc / fn * maxAbs, nil
}
return Sum(a).Float() / fn, nil
}
// maxAbs returns the largest magnitude among the first n elements, the
// seed pre-pass the scaled mean path needs. The dtype dispatch sits
// outside the walk and float16 and float32 widen exactly, so every
// value is the one an accessor read would hand over, at two
// instructions per element instead of a switch on the dtype.
//
// A magnitude maximum is associative and commutative, and it has no sign
// to lose: |+0| and |−0| are the same zero, so unlike the signed extrema
// the answer cannot depend on the visit order. The walk therefore takes
// the same fixed partition the sums use and combines the partial maxima
// with max, which makes it both parallel and deterministic.
func (a *Array) maxAbs(n int) float64 {
if a.strides != nil {
parts := foldParts(n)
if parts == 1 {
return a.maxAbsSerial(n)
}
partials := make([]float64, parts)
engine.Parallel(parts, func(cs, ce int) {
for c := cs; c < ce; c++ {
lo, hi := c*n/parts, (c+1)*n/parts
m := 0.0
for i := lo; i < hi; i++ {
if w := math.Abs(a.floatAt(i)); w > m {
m = w
}
}
partials[c] = m
}
})
m := 0.0
for _, v := range partials {
m = math.Max(m, v)
}
return m
}
switch a.dt {
case Int:
return foldMaxAbsParts(n, func(lo, hi int) float64 {
m := 0.0
for _, v := range a.ints[lo:hi] {
if w := math.Abs(float64(v)); w > m {
m = w
}
}
return m
})
case Float16:
return foldMaxAbsParts(n, func(lo, hi int) float64 {
m := 0.0
for _, v := range a.halves[lo:hi] {
if w := math.Abs(HalfToFloat64(v)); w > m {
m = w
}
}
return m
})
case Float32:
return foldMaxAbsParts(n, func(lo, hi int) float64 {
m := 0.0
for _, v := range a.floats32[lo:hi] {
if w := math.Abs(float64(v)); w > m {
m = w
}
}
return m
})
case Float:
return foldMaxAbsParts(n, func(lo, hi int) float64 {
m := 0.0
for _, v := range a.floats[lo:hi] {
if w := math.Abs(v); w > m {
m = w
}
}
return m
})
case Bool:
return foldMaxAbsParts(n, func(lo, hi int) float64 {
for _, v := range a.bools[lo:hi] {
if v {
return 1
}
}
return 0
})
case Int8:
return maxAbsNarrow(a.i8s, n)
case Uint8:
return maxAbsNarrow(a.u8s, n)
case Int16:
return maxAbsNarrow(a.i16s, n)
case Uint16:
return maxAbsNarrow(a.u16s, n)
case Int32:
return maxAbsNarrow(a.i32s, n)
case Uint32:
return maxAbsNarrow(a.u32s, n)
}
// Complex never reaches here: Mean rejects it before the guard.
return 0
}
// maxAbsNarrow is maxAbs's chunk walk for a narrow integer payload:
// every widening is exact, so the magnitude sees the accessor value.
func maxAbsNarrow[T int8 | uint8 | int16 | uint16 | int32 | uint32](src []T, n int) float64 {
return foldMaxAbsParts(n, func(lo, hi int) float64 {
m := 0.0
for _, v := range src[lo:hi] {
if w := math.Abs(float64(v)); w > m {
m = w
}
}
return m
})
}
// foldMaxAbsParts runs the magnitude maximum over the fixed partition of
// n elements. The chunk closure walks a payload slice, never a function
// per element, and the partial maxima combine with max, so the partition
// changes nothing.
func foldMaxAbsParts(n int, chunk func(lo, hi int) float64) float64 {
parts := foldParts(n)
if parts == 1 {
return chunk(0, n)
}
partials := make([]float64, parts)
engine.Parallel(parts, func(cs, ce int) {
for c := cs; c < ce; c++ {
partials[c] = chunk(c*n/parts, (c+1)*n/parts)
}
})
m := 0.0
for _, v := range partials {
m = math.Max(m, v)
}
return m
}
// maxAbsSerial is the strided fallback for a length below the partition
// floor.
func (a *Array) maxAbsSerial(n int) float64 {
var m float64
for i := range n {
if v := math.Abs(a.floatAt(i)); v > m {
m = v
}
}
return m
}
// Dot returns the dot product of two 1-D arrays of equal length.
// Integer-class pairs produce an int scalar accumulated in int64 (the
// products wrap like the int64 kernel's); bool pairs are refused: bool
// carries no arithmetic. Float32 operands accumulate in float64 and
// produce a float scalar; any float64 operand promotes the
// result to float, any complex operand to complex.
func Dot(a, b *Array) (Scalar, error) {
if a.NDim() != 1 || b.NDim() != 1 {
return Scalar{}, errf("Dot: needs 1-D arrays, got shapes %s and %s",
shapeText(a.shape), shapeText(b.shape))
}
if a.Len() != b.Len() {
return Scalar{}, errf("Dot: length mismatch %d vs %d", a.Len(), b.Len())
}
n := a.Len()
dt := promote(a.dt, b.dt)
if dt == Bool {
return Scalar{}, errf("Dot: bool arrays have no arithmetic")
}
switch {
case dt == Int && a.dt == Int && b.dt == Int:
return Scalar{i: intFoldDot(a.ints[:n], b.ints[:n])}, nil
case intClass(dt):
// Every integer-class pair widens exactly into int64; the
// products wrap exactly as the int64 kernel's do, and wrapping
// addition is associative, so the fixed partition changes
// nothing.
if a.dt == b.dt && a.dt != Bool && a.isContiguous() && b.isContiguous() {
switch a.dt {
case Int:
return Scalar{i: intFoldDot(a.ints[:n], b.ints[:n])}, nil
case Int8:
return Scalar{i: narrowFoldDot(a.i8s[:n], b.i8s[:n])}, nil
case Uint8:
return Scalar{i: narrowFoldDot(a.u8s[:n], b.u8s[:n])}, nil
case Int16:
return Scalar{i: narrowFoldDot(a.i16s[:n], b.i16s[:n])}, nil
case Uint16:
return Scalar{i: narrowFoldDot(a.u16s[:n], b.u16s[:n])}, nil
case Int32:
return Scalar{i: narrowFoldDot(a.i32s[:n], b.i32s[:n])}, nil
case Uint32:
return Scalar{i: narrowFoldDot(a.u32s[:n], b.u32s[:n])}, nil
}
}
parts := foldParts(n)
return Scalar{i: foldPartsOver(n, parts, func(c int) int64 {
lo, hi := c*n/parts, (c+1)*n/parts
var s int64
for i := lo; i < hi; i++ {
s += a.intAt(i) * b.intAt(i)
}
return s
})}, nil
}
switch dt {
case Float16:
var s float64
if a.dt == Float16 && b.dt == Float16 {
// Both payloads hold half bit patterns at their flat index,
// so the kernel streams the raw slices; every product is
// exact in float64 either way.
as, bs := a.halves[:n], b.halves[:n]
s = foldDotF16(as, bs)
} else {
for i := range n {
s += a.floatAt(i) * b.floatAt(i)
}
}
return Scalar{isFloat: true, f: s}, nil
case Float32:
var s float64
if a.dt == Float32 && b.dt == Float32 {
// Both payloads hold float32 at their flat index, so the
// kernel streams the raw slices; every product is exact in
// float64 either way, so the values match accessor reads.
as, bs := a.floats32[:n], b.floats32[:n]
s = foldDotF32(as, bs)
} else {
for i := range n {
s += a.floatAt(i) * b.floatAt(i)
}
}
return Scalar{isFloat: true, f: s}, nil
case Float:
var s float64
if a.dt == Float && b.dt == Float {
as, bs := a.floats[:n], b.floats[:n]
s = foldDotF64(as, bs)
} else {
for i := range n {
s += a.floatAt(i) * b.floatAt(i)
}
}
return Scalar{isFloat: true, f: s}, nil
default:
var s complex128
if a.dt == Complex && b.dt == Complex {
as, bs := a.complexes[:n], b.complexes[:n]
s = foldDotC128(as, bs)
} else {
for i := range n {
s += a.complexAt(i) * b.complexAt(i)
}
}
return Scalar{isComplex: true, c: s}, nil
}
}
// reduceOrder walks the array keeping the smallest (greater=false) or
// largest (greater=true) element per dtype. An empty array is an error,
// and so is a complex array: it has no ordering. The comparisons are
// inlined per dtype: a closure per element would dominate the walk.
// reduceOrder walks the array keeping the smallest (greater=false) or
// largest (greater=true) element per dtype. An empty array is an error,
// and so is a complex array: it has no ordering.
//
// The walk is partitioned by length alone, never by the worker count, so
// the same input answers the same element on any machine: the range is
// cut into fixed chunks, each chunk applies the serial rule to its own
// range, and the chunks combine in index order with the same strict
// comparison. A tie therefore keeps the earlier chunk's element and, in a
// chunk, the earlier element's, which is what the serial walk kept; the
// rules for a NaN (never a candidate) and an all-NaN array (the last
// element, where the seed walk ends) are preserved exactly, zeros
// included.
func (a *Array) reduceOrder(name string, greater bool) (Scalar, error) {
if a.dt == Complex {
return Scalar{}, errf("%s: complex arrays have no ordering", name)
}
if a.Len() == 0 {
return Scalar{}, errf("%s: an empty array has no %s", name, name)
}
n := a.Len()
switch a.dt {
case Int:
return Scalar{i: foldExtreme(n, greater, func(lo, hi int) (int64, bool) {
is := a.ints[lo:hi]
best := is[0]
if greater {
for _, v := range is[1:] {
if v > best {
best = v
}
}
} else {
for _, v := range is[1:] {
if v < best {
best = v
}
}
}
return best, true
})}, nil
case Bool:
return Scalar{i: foldExtremeBool(a.bools, n, greater)}, nil
case Int8:
return Scalar{i: foldExtremeNarrow(a.i8s, n, greater)}, nil
case Uint8:
return Scalar{i: foldExtremeNarrow(a.u8s, n, greater)}, nil
case Int16:
return Scalar{i: foldExtremeNarrow(a.i16s, n, greater)}, nil
case Uint16:
return Scalar{i: foldExtremeNarrow(a.u16s, n, greater)}, nil
case Int32:
return Scalar{i: foldExtremeNarrow(a.i32s, n, greater)}, nil
case Uint32:
return Scalar{i: foldExtremeNarrow(a.u32s, n, greater)}, nil
}
// The float folds read the payload slices directly: float16 and
// float32 widen exactly, so the raw walk sees the same values the
// accessor would hand over, in the same order.
var f float64
switch a.dt {
case Float16:
f = foldExtreme(n, greater, func(lo, hi int) (float64, bool) {
return halfExtreme(a.halves[lo:hi], greater)
})
case Float32:
f = foldExtreme(n, greater, func(lo, hi int) (float64, bool) {
return float32Extreme(a.floats32[lo:hi], greater)
})
default:
f = foldExtreme(n, greater, func(lo, hi int) (float64, bool) {
return floatExtreme(a.floats[lo:hi], greater)
})
}
return Scalar{isFloat: true, f: f}, nil
}
// floatExtreme is the serial fold's rule applied to one chunk of a float64
// payload: skip the leading NaNs to seed, then keep the first strictly
// better value. A chunk holding no non-NaN reports its last element, where
// the serial seed walk would have stopped, and says there is no candidate.
func floatExtreme(src []float64, greater bool) (float64, bool) {
best, k := src[0], 1
for math.IsNaN(best) && k < len(src) {
best = src[k]
k++
}
if math.IsNaN(best) {
return src[len(src)-1], false
}
if greater {
for _, v := range src[k:] {
if v > best {
best = v
}
}
} else {
for _, v := range src[k:] {
if v < best {
best = v
}
}
}
return best, true
}
// float32Extreme is floatExtreme over a float32 payload; every widening is
// exact, so the comparisons see the values an accessor read would.
func float32Extreme(src []float32, greater bool) (float64, bool) {
best, k := float64(src[0]), 1
for math.IsNaN(best) && k < len(src) {
best = float64(src[k])
k++
}
if math.IsNaN(best) {
return float64(src[len(src)-1]), false
}
if greater {
for _, v := range src[k:] {
if w := float64(v); w > best {
best = w
}
}
} else {
for _, v := range src[k:] {
if w := float64(v); w < best {
best = w
}
}
}
return best, true
}
// halfExtreme is floatExtreme over a half-precision payload.
func halfExtreme(src []uint16, greater bool) (float64, bool) {
best, k := HalfToFloat64(src[0]), 1
for math.IsNaN(best) && k < len(src) {
best = HalfToFloat64(src[k])
k++
}
if math.IsNaN(best) {
return HalfToFloat64(src[len(src)-1]), false
}
if greater {
for _, v := range src[k:] {
if w := HalfToFloat64(v); w > best {
best = w
}
}
} else {
for _, v := range src[k:] {
if w := HalfToFloat64(v); w < best {
best = w
}
}
}
return best, true
}
// foldExtreme runs a chunk fold over the fixed partition of n elements
// and combines the partials with the serial walk's rule, so the answer
// is the serial walk's element whatever the worker count. A chunk with
// no candidate contributes nothing; an array with no candidate at all
// is all NaN and answers the last element, which is what the final
// chunk stored.
func foldExtreme[N int64 | float64](n int, greater bool, chunk func(lo, hi int) (N, bool)) N {
parts := foldParts(n)
vals := make([]N, parts)
oks := make([]bool, parts)
if parts == 1 {
vals[0], oks[0] = chunk(0, n)
} else {
engine.Parallel(parts, func(cs, ce int) {
for c := cs; c < ce; c++ {
vals[c], oks[c] = chunk(c*n/parts, (c+1)*n/parts)
}
})
}
return combineExtreme(vals, oks, greater)
}
// combineExtreme is foldExtreme's combination: the partials compare in
// index order with the strict rule the serial walk used, so a tie keeps
// the earlier block's value; a block with no candidate contributes
// nothing; a whole with no candidate answers the last block's stored
// fallback.
func combineExtreme[N int64 | float64](vals []N, oks []bool, greater bool) N {
best, have := vals[0], false
for c := range len(vals) {
if !oks[c] {
continue
}
if !have || (greater && vals[c] > best) || (!greater && vals[c] < best) {
best, have = vals[c], true
}
}
if have {
return best
}
return vals[len(vals)-1]
}
// foldExtremeNarrow applies reduceOrder's chunk rule to a narrow integer
// payload: the comparison runs in the payload's own type, so no value
// ever meets a float64 rounding, and the winner widens exactly into the
// int64 the fold combines.
func foldExtremeNarrow[T int8 | uint8 | int16 | uint16 | int32 | uint32](src []T, n int, greater bool) int64 {
return foldExtreme(n, greater, func(lo, hi int) (int64, bool) {
is := src[lo:hi]
best := is[0]
if greater {
for _, v := range is[1:] {
if v > best {
best = v
}
}
} else {
for _, v := range is[1:] {
if v < best {
best = v
}
}
}
return int64(best), true
})
}
// foldExtremeBool is foldExtremeNarrow for a bool payload: false below
// true, widened to the 0/1 the Int scalar carries. Go orders no bool
// with < or >, so the strict improvement writes its own logic.
func foldExtremeBool(src []bool, n int, greater bool) int64 {
return foldExtreme(n, greater, func(lo, hi int) (int64, bool) {
is := src[lo:hi]
best := is[0]
if greater {
for _, v := range is[1:] {
if v && !best {
best = v
}
}
} else {
for _, v := range is[1:] {
if !v && best {
best = v
}
}
}
if best {
return 1, true
}
return 0, true
})
}
// foldPartsOver runs a per-chunk fold over the fixed partition of n
// elements and combines the results through the balanced partial tree:
// the only ordering step, and its shape depends on the chunk count
// alone. The chunk boundaries c·n/parts are a function of the length and
// the part count, so the total is the same on any machine and under any
// worker setting.
func foldPartsOver[N int64 | float64 | complex128](n, parts int, fold func(c int) N) N {
if parts == 1 {
return fold(0)
}
partials := make([]N, parts)
engine.Parallel(parts, func(cs, ce int) {
for c := cs; c < ce; c++ {
partials[c] = fold(c)
}
})
return treeSum(partials)
}
// intFoldDot is the integer dot product: wrapping addition is
// associative, so the partition cannot change the value.
func intFoldDot(x, y []int64) int64 {
n := len(x)
parts := foldParts(n)
return foldPartsOver(n, parts, func(c int) int64 {
lo, hi := c*n/parts, (c+1)*n/parts
var s int64
for i := lo; i < hi; i++ {
s += x[i] * y[i]
}
return s
})
}
// narrowFoldDot is the same-dtype narrow integer dot product: the
// products run in int64 over the exact widenings, exactly what the
// accessor fold's intAt reads produce, so the partition and the value
// are the accessor fold's own.
func narrowFoldDot[T int8 | uint8 | int16 | uint16 | int32 | uint32](x, y []T) int64 {
n := len(x)
parts := foldParts(n)
return foldPartsOver(n, parts, func(c int) int64 {
lo, hi := c*n/parts, (c+1)*n/parts
var s int64
for i := lo; i < hi; i++ {
s += int64(x[i]) * int64(y[i])
}
return s
})
}
// foldDotF64 is the float64 dot product: four interleaved product chains
// per chunk, combined as a balanced pair.
func foldDotF64(x, y []float64) float64 {
n := len(x)
parts := foldParts(n)
return foldPartsOver(n, parts, func(c int) float64 {
lo, hi := c*n/parts, (c+1)*n/parts
return foldDotRange(x[lo:hi], y[lo:hi])
})
}
// foldDotF32 is the float32 dot product: every product is exact in
// float64, so the accumulation sees the values an accessor read would.
func foldDotF32(x, y []float32) float64 {
n := len(x)
parts := foldParts(n)
return foldPartsOver(n, parts, func(c int) float64 {
lo, hi := c*n/parts, (c+1)*n/parts
return foldDotRangeF32(x[lo:hi], y[lo:hi])
})
}
// foldDotF16 is the half-precision dot product, widening each element
// exactly as it is read.
func foldDotF16(x, y []uint16) float64 {
n := len(x)
parts := foldParts(n)
return foldPartsOver(n, parts, func(c int) float64 {
lo, hi := c*n/parts, (c+1)*n/parts
return foldDotRangeF16(x[lo:hi], y[lo:hi])
})
}
// foldDotC128 is the complex dot product with the same partition.
func foldDotC128(x, y []complex128) complex128 {
n := len(x)
parts := foldParts(n)
return foldPartsOver(n, parts, func(c int) complex128 {
lo, hi := c*n/parts, (c+1)*n/parts
return foldDotRangeC128(x[lo:hi], y[lo:hi])
})
}