0027 r2 and explainedvariance vectorize only a single output - CyrilB1531/lodestar GitHub Wiki
Status: accepted ยท Date: 2026-08-14
R2.AccumulateUnweighted and ExplainedVariance.AccumulateUnweighted both
need two passes over yTrue/yPred per output column โ one to accumulate a
mean, one to accumulate centred squares and residuals โ and both offer a
Vector<double>-based fast path (AccumulateUnweightedVectorized) on
net10.0.
That fast path is gated on outputCount == 1 && Vector.IsHardwareAccelerated,
not on Vector.IsHardwareAccelerated alone. outputCount == 1 is the only
shape where rows are contiguous in yTrue/yPred: with more than one
output, column col of row row sits at (row * outputCount) + col, a
strided access that a Vector<T> load cannot gather from a ReadOnlySpan
without scattering it into a temporary first โ not how SIMD is written
elsewhere in this repository (VectorMath.Dot, Pooling.cs,
EmbeddingIndex.Persistence.cs all vectorize over a single contiguous
span). outputCount > 1 therefore keeps the scalar loop, which walks the
strided layout directly.
Vector.IsHardwareAccelerated is checked independently of the shape
condition, and falls through to the same scalar loop on a runtime where
Vector<double> is software-emulated โ the same guard Pooling.cs and
EmbeddingIndex.Persistence.cs check before vectorizing (VectorMath.Dot
predates this repository checking it explicitly, per
0001).
AccumulateUnweighted vectorizes only when both conditions hold:
if (outputCount == 1 && Vector.IsHardwareAccelerated)
{
AccumulateUnweightedVectorized(...);
return;
}Every other combination โ multiple outputs, or no hardware acceleration โ runs the scalar loop.
-
R2.csandExplainedVariance.cseach carry a one-line pointer to this record at the guard, instead of restating the reasoning inline in both files. - The vectorized path is a different summation order from the scalar one and
is not asserted bit-identical to it; that is a separate question, answered
where
VectorCompensatedSumis defined (src/DataNet.Metrics/Internal/CompensatedSum.cs), not here. - A third caller that needs this shape (single contiguous output, two-pass mean-then-residual accumulation) can reuse the same guard rather than re-deriving it โ nothing today shares the accumulation itself, only the condition under which it vectorizes.
#321 update: the same two conditions now govern the shared walk. This decision was written about
R2andExplainedVariance, which carry their own accumulation.Outputs.WeightedMeanโ the walkmse,maeandRootMeanSquaredErrortake โ kept a scalar loop, and the nightly found the cost of that: 0.60ร against numpy at a million rows on a runner with AVX-512, below the gate../guides/performance.mdsets, whiler2on the same run stayed above it. The published table said the same thing more quietly โr2cost less doing two passes thanmsedoing one.
Outputs.ScoreVectorizedapplies this decision's rule unchanged:outputCount == 1for contiguity,Vector.IsHardwareAcceleratedchecked apart from it. Measured 1.65ร onmseand 1.60ร onmae, withr2re-run as an untouched control.What is new is which kernels may take it.
IResidualKernelgained a sibling,IVectorResidualKernel, rather than a second method: four of the six kernels cannot have a lane-wise form at all โ the Tweedie deviances reachMath.Powand the log errorsMath.Log. A single interface would have forced four implementations that could only throw.