-
Notifications
You must be signed in to change notification settings - Fork 0
0027 r2 and explainedvariance vectorize only a single output
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.
- 0001-target-framework
- 0002-unicode-comparison-unit
- 0003-provenance-and-licensing
- 0004-levenshtein-myers-backlog
- 0005-hamming-jellyfish-divergence
- 0006-ratcliff-autojunk
- 0007-metaphone-scope
- 0008-italian-enza-nltk-divergence
- 0009-sample-consumes-a-local-feed
- 0010-stop-word-list-provenance
- 0011-persistence-format
- 0012-per-package-versioning
- 0013-sentencepiece-parity-scope
- 0014-precompiled-normalizer
- 0015-sonar-rules-in-the-build
- 0016-metrics-package-placement
- 0017-bpe-parity-scope
- 0018-multiclass-roc-auc-parallelism-is-opt-in
- 0019-the-net-analysers-run-in-the-build-too
- 0020-normalize-is-a-projection-not-a-parameter
- 0021-multioutput-is-a-method-not-an-enum
- 0022-added-token-matching-flags
- 0023-byte-level-decode-substitutes
- 0024-weighted-median-averages-within-scikit-learns-epsilon
- 0025-quickselect-replaces-a-full-sort-for-the-median
- 0026-r2-and-explainedvariance-split-their-undefined-cases-differently
- 0027-r2-and-explainedvariance-vectorize-only-a-single-output
- 0028-log1p-is-kahans-identity-not-math-log-1-plus-x
- 0029-balanced-accuracy-adjusted-is-left-to-ieee-754-at-the-edge
- 0030-cohen-kappa-keeps-scikit-learns-expected-matrix-orientation
- 0031-nosamplecorrect-mirrors-numpys-float64-upcast
- 0032-fbeta-substitutes-tp-predicted-and-support-algebraically
- 0033-compensated-sum-is-neumaiers-variant
- 0034-dropout-is-refused-for-want-of-a-user
- 0035-a-null-pre-split-is-removed-with-invert-not-isolated
- 0036-a-member-may-ship-without-an-oracle-if-it-says-so
- 0037-the-guards-run-before-the-commit
- 0038-the-gate-confronts-an-exception-tag-with-the-page-that-documents-it
- 0039-mutual-information-returns-zero-on-an-empty-input
- 0040-a-curve-is-a-sealed-class-per-curve
- 0041-one-sample-file-per-public-class
- 0042-phonetic-encoders-refuse-a-null-word
- 0043-the-equality-table-is-sized-to-the-pattern
- 0044-compression-belongs-to-the-caller
- 0045-a-console-call-carries-its-reason-on-the-line
- 0046-check-adr-immutable-runs-in-ci-only
- 0047-one-gate-per-kernel-not-one-per-alphabet
- 0048-the-gate-depends-on-the-kernel-and-the-alphabet
- 0049-two-gates-per-kernel-tested-where-the-width-is-known
- 0050-the-sentencepiece-bpe-lineage-stays-a-bpe-model
- benchmark_latest
- decisions
- equivalence
- matplotlib
- migration
- nightly_run
- numpy
- pandas
- performance
- pytorch
- seaborn
- sklearn
- statsmodels