fix(ailego): accumulate FP16 dense distances in FP32 on AVX-512 FP16 CPUs - #787
JasonXuDeveloper wants to merge 4 commits into
Conversation
…CPUs The AVX512-FP16 inner-product and squared-L2 kernels (single-pair and batch) multiplied and accumulated in __m512h, rounding every partial sum to FP16. On 1024-d unit vectors this puts ~3e-4 of error on each score and drops FP16 cosine/IP recall@10 from ~0.997 to ~0.965 on Sapphire Rapids, while the same index is accurate on CPUs without AVX512-FP16. Drop the AVX512_FP16 dispatch branches so these paths use the AVX-512F kernels, which widen with vcvtph2ps and accumulate in FP32, and delete the now-unused FP16-accumulating kernels and helpers. turbo already leaves its AVX512-FP16 kernels unregistered for the same reason. Add a precision test on skewed unit vectors that fails when FP16 accumulation is used. Closes alibaba#786 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
A question for reviewers before this goes further: do you want to keep a native FP16 FMA path on AVX512-FP16 CPUs? This PR takes the simplest fix and routes these calls to the existing AVX-512F kernels (convert, then FP32 FMA). That fixes precision, but in compute-bound, cache-resident cases it will be slower than the removed Proposal: FP16 FMA with periodic widening to FP32 Accumulate in template <size_t kFlushSteps>
float InnerProductFp16Flush(const Float16 *lhs, const Float16 *rhs, size_t size) {
__m512 acc_lo = _mm512_setzero_ps(), acc_hi = _mm512_setzero_ps();
size_t i = 0;
while (i + 32 <= size) {
__m512h partial = _mm512_setzero_ph();
for (size_t s = 0; s < kFlushSteps && i + 32 <= size; ++s, i += 32) {
partial = _mm512_fmadd_ph(_mm512_loadu_ph(lhs + i),
_mm512_loadu_ph(rhs + i), partial);
}
__m512i bits = _mm512_castph_si512(partial);
acc_lo = _mm512_add_ps(acc_lo, _mm512_cvtph_ps(_mm512_castsi512_si256(bits)));
acc_hi = _mm512_add_ps(acc_hi, _mm512_cvtph_ps(_mm512_extracti64x4_epi64(bits, 1)));
}
float sum = _mm512_reduce_add_ps(_mm512_add_ps(acc_lo, acc_hi));
// masked tail as in the current kernels
return sum;
}
If you prefer this direction, I can rework the PR to use it for the IP (single and batch) and squared L2 kernels. I would pick |
When built with __ARM_FEATURE_FP16_VECTOR_ARITHMETIC (e.g. the iOS arm64 simulator, which targets apple-m1), the NEON FP16 inner-product, squared-L2 and MIPS kernels accumulated in float16x8_t via vfmaq_f16, with the same precision loss as the removed AVX512-FP16 kernels. This made the new InnerProduct_Fp32Accumulation test fail on iOS SIMULATORARM64 (up to ~6e-4 error per score). Drop the FP16-arithmetic branches so NEON always widens with vcvt_f32_f16 and accumulates in FP32, and remove the now-unused FMA_FP16_NEON, SSD_FP16_NEON and HorizontalAdd_FP16_NEON macros. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Problem
Closes #786.
The ailego AVX512-FP16 kernels for FP16 inner product and squared L2 (single-pair and batch) accumulate in
__m512h, so every partial sum is rounded to FP16. On 1024-d unit vectors this adds ~3e-4 of error to each score. On Sapphire Rapids, FP16 COSINE/IP recall@10 falls from ~0.997 to ~0.965, and the same index returns different neighbours on CPUs without AVX512-FP16. The numbers are in the issue.Change
AVX512_FP16branches from the FP16 dense dispatchers (InnerProductMatrix,MinusInnerProductMatrix,SquaredEuclideanDistanceMatrix,InnerProductDistanceBatchImpl,SquaredEuclideanDistanceBatchImpl). These calls now fall through to the existing AVX-512F kernels, which widen withvcvtph2psand accumulate in FP32.FMA_FP16_AVX512FP16,HorizontalAdd/Max_FP16_V512).Rewriting the AVX512-FP16 kernels to use
_mm512_cvtph_ps+_mm512_fmadd_pswould duplicate the AVX-512F kernels, so this PR routes to those instead. This matches turbo, which leaves its own AVX512-FP16 kernels unregistered for the same reason (turbo.cc:359-362).Not touched:
InnerProductSparseInSegmentFp16AVX512FP16.ACCUM_FP16_1X1_NEON, which the default build doesn't compile because the TUs get-march=armv8-a.neon_fp16cosine.These could be follow-ups if you want them.
Performance
Not benchmarked: I don't have an AVX512-FP16 machine. What to expect on one:
vextracti64x4, twovcvtph2psand two FP32 FMAs. The removed loop issued one load and onevfmadd231ph. When the data is cache-resident and the loop is compute-bound, this will be slower.turbo.cc:359-362says the same about turbo's FP16 kernels: they only helped for cache-resident dims <= 256.Existing coverage is thin. Only the 1x1 baseline timing in
DISABLED_InnerProduct_Benchmark(inner_product_matrix_fp16_test, dims 64 and 256) goes through the changed single-pair dispatch, and nothing benchmarks the batch kernel or large dims. The most useful number would be HNSW search latency on a 1024-dVECTOR_FP16COSINE collection on Sapphire Rapids, before and after this change. If the small-dimension cost matters, a PR comment below proposes an alternative.Test
DistanceMatrix.InnerProduct_Fp32Accumulationcompares single-pair and batch (12 + tail) FP16 inner products on skewed 1024-d unit vectors against a double-precision reference over the same FP16 values, with a bound of 2e-5. FP32 accumulation is off by less than 1e-6. FP16 accumulation is off by about 1.6e-4.inner_product_matrix_fp16_test,euclidean_distance_matrix_fp16_testandcosine_distance_matrix_fp16_testpass.-march=armv8.2-a+fp16, which turns on the FP16-accumulating NEON path, makes the new test fail (single-pair error 2.0e-4, batch 6.9e-5).-march=sapphirerapids. I don't have AVX512-FP16 hardware to run the test on x86. On such a machine it should fail before this change and pass after it.🤖 Generated with Claude Code