Repository navigation
[SIMD] Reductions: Add SIMD for Prod, ArgMax/ArgMin, and Axis Reductions #576
Copy link
Copy link
Closed
Labels
coreInternal engine: Shape, Storage, TensorEngine, iteratorsInternal engine: Shape, Storage, TensorEngine, iteratorsenhancementNew feature or requestNew feature or requestperformancePerformance improvements or optimizationsPerformance improvements or optimizations
Description
Overview
Extend the IL kernel generator's SIMD reduction support beyond the current Sum/Max/Min element-wise operations.
Parent issue: #545
Current State
| Reduction | Element-wise (axis=null) | Axis Reduction |
|---|---|---|
| Sum | ✅ SIMD | ❌ NDIterator |
| Max | ✅ SIMD | ❌ NDIterator |
| Min | ✅ SIMD | ❌ NDIterator |
| Prod | ❌ Scalar | ❌ NDIterator |
| ArgMax | ❌ Scalar | ❌ NDIterator |
| ArgMin | ❌ Scalar | ❌ NDIterator |
Existing SIMD code: ILKernelGenerator.cs:3996-4095
Task List
Tier 1: Element-wise Improvements
-
SIMD Prod (Product reduction)
- Challenge: No
Vector256.HorizontalMultiply()in .NET - Solution: Lane-wise multiply accumulator, horizontal product at end
-
Vector256<T> prodVec = Vector256.Create((T)1); for (i = 0; i <= vectorEnd; i += vectorCount) prodVec = prodVec * Vector256.Load(input + i); result = prodVec[0] * prodVec[1] * ... * prodVec[N-1];
- Expected: 1.5-2× speedup
- Challenge: No
-
SIMD ArgMax/ArgMin
- Challenge: Need to track indices alongside values
- Solution: Dual-accumulator (value + index vectors)
-
Vector256<T> maxVec = Vector256.Create(T.MinValue); Vector256<int> idxVec = Vector256.Create(0, 1, 2, ...); for (i = 0; i <= vectorEnd; i += vectorCount) { var vec = Vector256.Load(input + i); var mask = Vector256.GreaterThan(vec, maxVec); maxVec = Vector256.ConditionalSelect(mask, vec, maxVec); idxVec = Vector256.ConditionalSelect(mask.AsInt32(), currentIdx, idxVec); }
- Expected: 2× speedup
Tier 2: Axis Reductions
- SIMD for inner-axis reductions
- Current:
Default.Reduction.AMax.csuses slow NDIterator loops - Opportunity: For axis=-1 (inner), the reduction dimension is contiguous
-
// Current (slow): for each outer_coord: iter = arr[outer_coord, :].AsIterator() while iter.HasNext(): val = iter.MoveNext() // Virtual calls! // With SIMD: for each outer_coord: T* rowPtr = basePtr + outer_coord * outerStride result[outer_coord] = SimdReduce(rowPtr, innerSize) // Vector256 loop
- Expected: 3-5× speedup for large axis reductions
- Current:
Files to Modify
| File | Changes |
|---|---|
ILKernelGenerator.cs |
Add Prod/ArgMax/ArgMin to element-wise SIMD paths |
ReductionKernel.cs |
Add AxisReductionKernel delegate if needed |
Default.Reduction.*.cs |
Refactor axis reductions to use IL kernels |
DefaultEngine.ReductionOp.cs |
Wire up new kernels |
Benchmarks to Add
[Benchmark] public double Prod_10M() => np.prod(_array);
[Benchmark] public int ArgMax_10M() => np.argmax(_array);
[Benchmark] public NDArray Sum_Axis0() => np.sum(_matrix, axis: 0);
[Benchmark] public NDArray Max_Axis1() => np.amax(_matrix, axis: 1);Success Criteria
- Prod element-wise: ≥1.5× faster than current
- ArgMax/ArgMin element-wise: ≥1.5× faster than current
- Inner-axis reductions: ≥2× faster than current
- All existing reduction tests pass
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
coreInternal engine: Shape, Storage, TensorEngine, iteratorsInternal engine: Shape, Storage, TensorEngine, iteratorsenhancementNew feature or requestNew feature or requestperformancePerformance improvements or optimizationsPerformance improvements or optimizations
Activity
Nucs commentedon Mar 14, 2026
Implemented: SIMD Axis Reductions (PR pending)
Completed implementation of SIMD axis reductions with two major optimizations:
1. AVX2 Gather for Strided Float/Double
When reducing along non-contiguous axes (e.g.,
np.sum(arr, axis=0)on a 2D array), the current implementation used scalar loops. Now uses AVX2 gather instructions forfloatanddoubletypes:Expected speedup: ~2-3x for strided float/double reductions.
2. Parallel Outer Loop for Large Outputs
When the output array has >1000 elements, the outer loop now uses
Parallel.For:This is especially beneficial for operations like
np.sum(large_array, axis=1)where you're computing many independent row sums.3. Scalar Loop Unrolling
For integer types (no gather support), added 4x loop unrolling for better instruction-level parallelism:
Files Changed
ILKernelGenerator.Reduction.Axis.Simd.csAxisReductionParallelThreshold = 1000AxisReductionSimdHelper<T>()to use parallel loop for large outputsReduceAxisElement<T>()helper for parallel executionReduceStridedAxisGatherFloat()with AVX2 gatherReduceStridedAxisGatherDouble()with AVX2 gatherReduceStridedAxisScalar<T>()with 4x unrollingTests Added
17 new tests in
AxisReductionBenchmarkTests.cs:All 17 tests pass. No regressions in existing tests (85 pre-existing failures unchanged).
perf: SIMD axis reductions with AVX2 gather and parallel outer loop (#…
perf: SIMD axis reductions with AVX2 gather and parallel outer loop (#…
Nucs commentedon Sep 7, 2026
Following up on my "PR pending" note here, shipped in 0.70.0 (#628).
argmax/argminare a single-pass SIMD tournament, and the axis reductions are vectorized:(
prod,std,var, and thenan*reductions are vectorized along axes too.) Closing.