Skip to content

Commit 050bb59

Browse files
MayCXCmceachen
authored andcommitted
perf(sqlite-vec.c): use NEON for cosine distance
distance_cosine_float() and distance_cosine_int8() had no NEON kernel, so cosine KNN queries in NEON builds, which include the darwin-arm64 binary, ran the scalar loop. asg017/sqlite-vec 0de765f added cosine_float_neon() and cosine_int8_neon(), dispatched above 16 and 15 dimensions. This ports them with two changes. asg017's float32 kernel sums in float32, the loss asg017#324 reported and 2.1.0 fixed in the scalar loop by summing in double: vectors with elements below about 3e-23 or above about 2e19 get wrong distances. Here each float32x4 load is widened to two float64x2 halves before it is multiplied and added, in asg017's 16-element blocks with four accumulators per sum. And both kernels return 1.0 for a zero vector, as the scalar loops do, where asg017's return NaN. On Linux arm64 (GCC 14, -O3), one 768-dimension float32 distance takes about 155 ns instead of 895 ns, and a KNN query on a 100,000-row float[768] table takes 25.9 ms instead of 94.4 ms for k = 10 and 30.1 ms instead of 98.9 ms for k = 100 (medians of 20 queries), returning the same rows. The float32 distances match the scalar loop's on 12,000 random pairs of 16 to 1,536 dimensions, except that a vector's distance to itself, 0 or about 2e-16 in either, can change between the two. The int8 kernel sums products exactly in 32-bit lanes, and the scalar loop sums them in float, which is exact while a sum stays below 2^24: their distances match up to 1,024 dimensions, and past that the kernel's are closer to the exact value for vectors whose squares sum past 2^24 (a mean error of 2.3e-8 instead of 2.5e-7 at 2,048 dimensions). The cosine tests used vectors of 2 and 3 elements, below both kernels' thresholds, and pass against asg017's kernels as they are. test_vec_distance_cosine now checks lengths of 17 to 768, with and without a remainder past the 16-element blocks, against a reference computed in float64, since numpy sums int8 products in int8; the zero-vector and extreme-magnitude tests repeat at 40 dimensions. asg017's float32 kernel fails both, and with a zero-vector check added it still fails the extreme-magnitude test. The full suite passes on the NEON build and on the scalar one, and make test-unit, which builds without SIMD, passes too.
1 parent 350c67a commit 050bb59

3 files changed

Lines changed: 271 additions & 13 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
- Reading a vector column without a KNN query, as in `select embedding from v`, reads each chunk's vectors through one blob handle instead of opening the chunk's blob again for every row: selecting every vector of a 10,000-row `float[768]` table took 25 ms instead of 2,080 ms, and `select vec_distance_cosine(embedding, ?) from v` 41 ms instead of 2,093 ms.
88
- The linux-arm64 and linux-arm64-musl binaries use NEON, as the darwin-arm64 binary does: `make` enables it on Linux arm64, and `OMIT_SIMD=1` builds without it. An L2 KNN query on a 100,000-row `float[768]` table took 19 ms instead of 57 ms there. Hamming and `int8` L1 distances are unchanged; `float32` L1 distances can differ in the last digit when their double-precision sum rounds, and `float32` L2 distances can differ from 2.1.1's in the last digits, closer to the exact value (at 768 dimensions, a mean relative error of 3.0e-8 instead of 1.7e-7), so a KNN query can order rows with nearly equal distances differently; `int8` L2 distances whose squared differences sum past 2^24 are computed from the exact sum.
9+
- Cosine distance between `float32` vectors of more than 16 dimensions, or `int8` vectors of more than 15, uses NEON in NEON builds, which include the darwin-arm64 binary: a KNN query on a 100,000-row `float[768]` table took 26 ms instead of 94 ms on Linux arm64. `float32` distances match the scalar code's on random vectors, though a vector's distance to an identical vector, 0 or about 2e-16 in either, can change between the two. `int8` distances match up to 1,024 dimensions; past that, vectors whose squared elements sum past 2^24 get distances closer to the exact value, as NEON sums them exactly. Ported from [asg017@0de765f](https://github.com/asg017/sqlite-vec/commit/0de765f), except that `float32` sums stay in `double` ([asg017#324](https://github.com/asg017/sqlite-vec/issues/324)); asg017's kernel sums them in `float32`.
910

1011
### Fixed
1112

‎sqlite-vec.c‎

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,234 @@ static double l1_f32_neon(const void *pVect1v, const void *pVect2v,
432432

433433
return vaddvq_f64(acc) + sum;
434434
}
435+
436+
// asg017/sqlite-vec's cosine_float_neon, with its sums kept in double as
437+
// distance_cosine_float keeps them: each float32x4 load is widened to two
438+
// float64x2 halves before it is multiplied and added.
439+
static f32 cosine_float_neon(const void *pVect1v, const void *pVect2v,
440+
const void *qty_ptr) {
441+
f32 *pVect1 = (f32 *)pVect1v;
442+
f32 *pVect2 = (f32 *)pVect2v;
443+
size_t qty = *((size_t *)qty_ptr);
444+
size_t qty16 = qty >> 4;
445+
const f32 *pEnd1 = pVect1 + (qty16 << 4);
446+
447+
float64x2_t dot0 = vdupq_n_f64(0), dot1 = vdupq_n_f64(0);
448+
float64x2_t dot2 = vdupq_n_f64(0), dot3 = vdupq_n_f64(0);
449+
float64x2_t amag0 = vdupq_n_f64(0), amag1 = vdupq_n_f64(0);
450+
float64x2_t amag2 = vdupq_n_f64(0), amag3 = vdupq_n_f64(0);
451+
float64x2_t bmag0 = vdupq_n_f64(0), bmag1 = vdupq_n_f64(0);
452+
float64x2_t bmag2 = vdupq_n_f64(0), bmag3 = vdupq_n_f64(0);
453+
454+
while (pVect1 < pEnd1) {
455+
float32x4_t v1, v2;
456+
float64x2_t a, b;
457+
458+
v1 = vld1q_f32(pVect1);
459+
pVect1 += 4;
460+
v2 = vld1q_f32(pVect2);
461+
pVect2 += 4;
462+
a = vcvt_f64_f32(vget_low_f32(v1));
463+
b = vcvt_f64_f32(vget_low_f32(v2));
464+
dot0 = vfmaq_f64(dot0, a, b);
465+
amag0 = vfmaq_f64(amag0, a, a);
466+
bmag0 = vfmaq_f64(bmag0, b, b);
467+
a = vcvt_high_f64_f32(v1);
468+
b = vcvt_high_f64_f32(v2);
469+
dot0 = vfmaq_f64(dot0, a, b);
470+
amag0 = vfmaq_f64(amag0, a, a);
471+
bmag0 = vfmaq_f64(bmag0, b, b);
472+
473+
v1 = vld1q_f32(pVect1);
474+
pVect1 += 4;
475+
v2 = vld1q_f32(pVect2);
476+
pVect2 += 4;
477+
a = vcvt_f64_f32(vget_low_f32(v1));
478+
b = vcvt_f64_f32(vget_low_f32(v2));
479+
dot1 = vfmaq_f64(dot1, a, b);
480+
amag1 = vfmaq_f64(amag1, a, a);
481+
bmag1 = vfmaq_f64(bmag1, b, b);
482+
a = vcvt_high_f64_f32(v1);
483+
b = vcvt_high_f64_f32(v2);
484+
dot1 = vfmaq_f64(dot1, a, b);
485+
amag1 = vfmaq_f64(amag1, a, a);
486+
bmag1 = vfmaq_f64(bmag1, b, b);
487+
488+
v1 = vld1q_f32(pVect1);
489+
pVect1 += 4;
490+
v2 = vld1q_f32(pVect2);
491+
pVect2 += 4;
492+
a = vcvt_f64_f32(vget_low_f32(v1));
493+
b = vcvt_f64_f32(vget_low_f32(v2));
494+
dot2 = vfmaq_f64(dot2, a, b);
495+
amag2 = vfmaq_f64(amag2, a, a);
496+
bmag2 = vfmaq_f64(bmag2, b, b);
497+
a = vcvt_high_f64_f32(v1);
498+
b = vcvt_high_f64_f32(v2);
499+
dot2 = vfmaq_f64(dot2, a, b);
500+
amag2 = vfmaq_f64(amag2, a, a);
501+
bmag2 = vfmaq_f64(bmag2, b, b);
502+
503+
v1 = vld1q_f32(pVect1);
504+
pVect1 += 4;
505+
v2 = vld1q_f32(pVect2);
506+
pVect2 += 4;
507+
a = vcvt_f64_f32(vget_low_f32(v1));
508+
b = vcvt_f64_f32(vget_low_f32(v2));
509+
dot3 = vfmaq_f64(dot3, a, b);
510+
amag3 = vfmaq_f64(amag3, a, a);
511+
bmag3 = vfmaq_f64(bmag3, b, b);
512+
a = vcvt_high_f64_f32(v1);
513+
b = vcvt_high_f64_f32(v2);
514+
dot3 = vfmaq_f64(dot3, a, b);
515+
amag3 = vfmaq_f64(amag3, a, a);
516+
bmag3 = vfmaq_f64(bmag3, b, b);
517+
}
518+
519+
double dot =
520+
vaddvq_f64(vaddq_f64(vaddq_f64(dot0, dot1), vaddq_f64(dot2, dot3)));
521+
double aMag =
522+
vaddvq_f64(vaddq_f64(vaddq_f64(amag0, amag1), vaddq_f64(amag2, amag3)));
523+
double bMag =
524+
vaddvq_f64(vaddq_f64(vaddq_f64(bmag0, bmag1), vaddq_f64(bmag2, bmag3)));
525+
526+
const f32 *pEnd2 = pVect1 + (qty - (qty16 << 4));
527+
while (pVect1 < pEnd2) {
528+
dot += (double)*pVect1 * (double)*pVect2;
529+
aMag += (double)*pVect1 * (double)*pVect1;
530+
bMag += (double)*pVect2 * (double)*pVect2;
531+
pVect1++;
532+
pVect2++;
533+
}
534+
535+
// Handle zero vectors: return max distance (1.0) to avoid division by zero
536+
if (aMag == 0 || bMag == 0) {
537+
return 1.0f;
538+
}
539+
return 1 - (dot / (sqrt(aMag) * sqrt(bMag)));
540+
}
541+
542+
// asg017/sqlite-vec's cosine_int8_neon, with distance_cosine_int8's zero
543+
// vector case. Products of int8 elements are summed exactly in 32-bit lanes.
544+
static f32 cosine_int8_neon(const void *pA, const void *pB, const void *pD) {
545+
const i8 *a = (const i8 *)pA;
546+
const i8 *b = (const i8 *)pB;
547+
size_t d = *((const size_t *)pD);
548+
const i8 *aEnd = a + d;
549+
550+
int32x4_t dot_acc1 = vdupq_n_s32(0);
551+
int32x4_t dot_acc2 = vdupq_n_s32(0);
552+
int32x4_t aMag_acc1 = vdupq_n_s32(0);
553+
int32x4_t aMag_acc2 = vdupq_n_s32(0);
554+
int32x4_t bMag_acc1 = vdupq_n_s32(0);
555+
int32x4_t bMag_acc2 = vdupq_n_s32(0);
556+
557+
while (a < aEnd - 31) {
558+
int8x16_t va1 = vld1q_s8(a);
559+
int8x16_t vb1 = vld1q_s8(b);
560+
int16x8_t a1_lo = vmovl_s8(vget_low_s8(va1));
561+
int16x8_t a1_hi = vmovl_s8(vget_high_s8(va1));
562+
int16x8_t b1_lo = vmovl_s8(vget_low_s8(vb1));
563+
int16x8_t b1_hi = vmovl_s8(vget_high_s8(vb1));
564+
565+
dot_acc1 = vmlal_s16(dot_acc1, vget_low_s16(a1_lo), vget_low_s16(b1_lo));
566+
dot_acc1 = vmlal_s16(dot_acc1, vget_high_s16(a1_lo), vget_high_s16(b1_lo));
567+
dot_acc2 = vmlal_s16(dot_acc2, vget_low_s16(a1_hi), vget_low_s16(b1_hi));
568+
dot_acc2 = vmlal_s16(dot_acc2, vget_high_s16(a1_hi), vget_high_s16(b1_hi));
569+
570+
aMag_acc1 = vmlal_s16(aMag_acc1, vget_low_s16(a1_lo), vget_low_s16(a1_lo));
571+
aMag_acc1 =
572+
vmlal_s16(aMag_acc1, vget_high_s16(a1_lo), vget_high_s16(a1_lo));
573+
aMag_acc2 = vmlal_s16(aMag_acc2, vget_low_s16(a1_hi), vget_low_s16(a1_hi));
574+
aMag_acc2 =
575+
vmlal_s16(aMag_acc2, vget_high_s16(a1_hi), vget_high_s16(a1_hi));
576+
577+
bMag_acc1 = vmlal_s16(bMag_acc1, vget_low_s16(b1_lo), vget_low_s16(b1_lo));
578+
bMag_acc1 =
579+
vmlal_s16(bMag_acc1, vget_high_s16(b1_lo), vget_high_s16(b1_lo));
580+
bMag_acc2 = vmlal_s16(bMag_acc2, vget_low_s16(b1_hi), vget_low_s16(b1_hi));
581+
bMag_acc2 =
582+
vmlal_s16(bMag_acc2, vget_high_s16(b1_hi), vget_high_s16(b1_hi));
583+
584+
int8x16_t va2 = vld1q_s8(a + 16);
585+
int8x16_t vb2 = vld1q_s8(b + 16);
586+
int16x8_t a2_lo = vmovl_s8(vget_low_s8(va2));
587+
int16x8_t a2_hi = vmovl_s8(vget_high_s8(va2));
588+
int16x8_t b2_lo = vmovl_s8(vget_low_s8(vb2));
589+
int16x8_t b2_hi = vmovl_s8(vget_high_s8(vb2));
590+
591+
dot_acc1 = vmlal_s16(dot_acc1, vget_low_s16(a2_lo), vget_low_s16(b2_lo));
592+
dot_acc1 = vmlal_s16(dot_acc1, vget_high_s16(a2_lo), vget_high_s16(b2_lo));
593+
dot_acc2 = vmlal_s16(dot_acc2, vget_low_s16(a2_hi), vget_low_s16(b2_hi));
594+
dot_acc2 = vmlal_s16(dot_acc2, vget_high_s16(a2_hi), vget_high_s16(b2_hi));
595+
596+
aMag_acc1 = vmlal_s16(aMag_acc1, vget_low_s16(a2_lo), vget_low_s16(a2_lo));
597+
aMag_acc1 =
598+
vmlal_s16(aMag_acc1, vget_high_s16(a2_lo), vget_high_s16(a2_lo));
599+
aMag_acc2 = vmlal_s16(aMag_acc2, vget_low_s16(a2_hi), vget_low_s16(a2_hi));
600+
aMag_acc2 =
601+
vmlal_s16(aMag_acc2, vget_high_s16(a2_hi), vget_high_s16(a2_hi));
602+
603+
bMag_acc1 = vmlal_s16(bMag_acc1, vget_low_s16(b2_lo), vget_low_s16(b2_lo));
604+
bMag_acc1 =
605+
vmlal_s16(bMag_acc1, vget_high_s16(b2_lo), vget_high_s16(b2_lo));
606+
bMag_acc2 = vmlal_s16(bMag_acc2, vget_low_s16(b2_hi), vget_low_s16(b2_hi));
607+
bMag_acc2 =
608+
vmlal_s16(bMag_acc2, vget_high_s16(b2_hi), vget_high_s16(b2_hi));
609+
610+
a += 32;
611+
b += 32;
612+
}
613+
614+
while (a < aEnd - 15) {
615+
int8x16_t va = vld1q_s8(a);
616+
int8x16_t vb = vld1q_s8(b);
617+
int16x8_t a_lo = vmovl_s8(vget_low_s8(va));
618+
int16x8_t a_hi = vmovl_s8(vget_high_s8(va));
619+
int16x8_t b_lo = vmovl_s8(vget_low_s8(vb));
620+
int16x8_t b_hi = vmovl_s8(vget_high_s8(vb));
621+
622+
dot_acc1 = vmlal_s16(dot_acc1, vget_low_s16(a_lo), vget_low_s16(b_lo));
623+
dot_acc1 = vmlal_s16(dot_acc1, vget_high_s16(a_lo), vget_high_s16(b_lo));
624+
dot_acc1 = vmlal_s16(dot_acc1, vget_low_s16(a_hi), vget_low_s16(b_hi));
625+
dot_acc1 = vmlal_s16(dot_acc1, vget_high_s16(a_hi), vget_high_s16(b_hi));
626+
627+
aMag_acc1 = vmlal_s16(aMag_acc1, vget_low_s16(a_lo), vget_low_s16(a_lo));
628+
aMag_acc1 = vmlal_s16(aMag_acc1, vget_high_s16(a_lo), vget_high_s16(a_lo));
629+
aMag_acc1 = vmlal_s16(aMag_acc1, vget_low_s16(a_hi), vget_low_s16(a_hi));
630+
aMag_acc1 = vmlal_s16(aMag_acc1, vget_high_s16(a_hi), vget_high_s16(a_hi));
631+
632+
bMag_acc1 = vmlal_s16(bMag_acc1, vget_low_s16(b_lo), vget_low_s16(b_lo));
633+
bMag_acc1 = vmlal_s16(bMag_acc1, vget_high_s16(b_lo), vget_high_s16(b_lo));
634+
bMag_acc1 = vmlal_s16(bMag_acc1, vget_low_s16(b_hi), vget_low_s16(b_hi));
635+
bMag_acc1 = vmlal_s16(bMag_acc1, vget_high_s16(b_hi), vget_high_s16(b_hi));
636+
637+
a += 16;
638+
b += 16;
639+
}
640+
641+
int32x4_t dot_sum = vaddq_s32(dot_acc1, dot_acc2);
642+
int32x4_t aMag_sum = vaddq_s32(aMag_acc1, aMag_acc2);
643+
int32x4_t bMag_sum = vaddq_s32(bMag_acc1, bMag_acc2);
644+
645+
i32 dot = vaddvq_s32(dot_sum);
646+
i32 aMag = vaddvq_s32(aMag_sum);
647+
i32 bMag = vaddvq_s32(bMag_sum);
648+
649+
while (a < aEnd) {
650+
dot += (i32)*a * (i32)*b;
651+
aMag += (i32)*a * (i32)*a;
652+
bMag += (i32)*b * (i32)*b;
653+
a++;
654+
b++;
655+
}
656+
657+
// Handle zero vectors: return max distance (1.0) to avoid division by zero
658+
if (aMag == 0 || bMag == 0) {
659+
return 1.0f;
660+
}
661+
return 1.0f - ((f32)dot / (sqrtf((f32)aMag) * sqrtf((f32)bMag)));
662+
}
435663
#endif
436664

437665
static f32 l2_sqr_float(const void *pVect1v, const void *pVect2v,
@@ -622,6 +850,11 @@ static f32 distance_cosine_bit(const void *pA, const void *pB, const void *pD) {
622850

623851
static f32 distance_cosine_float(const void *pVect1v, const void *pVect2v,
624852
const void *qty_ptr) {
853+
#ifdef SQLITE_VEC_ENABLE_NEON
854+
if ((*(const size_t *)qty_ptr) > 16) {
855+
return cosine_float_neon(pVect1v, pVect2v, qty_ptr);
856+
}
857+
#endif
625858
f32 *pVect1 = (f32 *)pVect1v;
626859
f32 *pVect2 = (f32 *)pVect2v;
627860
size_t qty = *((size_t *)qty_ptr);
@@ -648,6 +881,11 @@ static f32 distance_cosine_float(const void *pVect1v, const void *pVect2v,
648881
}
649882
static f32 distance_cosine_int8(const void *pA, const void *pB,
650883
const void *pD) {
884+
#ifdef SQLITE_VEC_ENABLE_NEON
885+
if ((*(const size_t *)pD) > 15) {
886+
return cosine_int8_neon(pA, pB, pD);
887+
}
888+
#endif
651889
i8 *a = (i8 *)pA;
652890
i8 *b = (i8 *)pB;
653891
size_t d = *((size_t *)pD);

‎tests/test-loadable.py‎

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -471,14 +471,22 @@ def check(a, b, dtype=np.float32):
471471
b = np.array(b, dtype=dtype)
472472

473473
x = vec_distance_cosine(a, b, a=transform, b=transform)
474-
y = npy_cosine(a, b)
474+
# the reference in float64: numpy sums int8 products in int8
475+
y = npy_cosine(a.astype(np.float64), b.astype(np.float64))
475476
assert isclose(x, y, abs_tol=1e-6)
476477

477478
check([1.2, 0.1], [0.4, -0.4])
478479
check([-1.2, -0.1], [-0.4, 0.4])
479480
check([1, 2, 3], [-9, -8, -7], dtype=np.int8)
480481
assert vec_distance_cosine("[1.1, 1.0]", "[1.2, 1.2]") == 0.0011318627512082458
481482

483+
# lengths past the SIMD kernels' thresholds, with and without a remainder
484+
# past their 16-element blocks
485+
rng = np.random.default_rng(324)
486+
for n in [17, 31, 32, 40, 127, 768]:
487+
check(rng.uniform(-1, 1, n), rng.uniform(-1, 1, n))
488+
check(rng.integers(-128, 128, n), rng.integers(-128, 128, n), dtype=np.int8)
489+
482490
vec_distance_cosine_bit = lambda *args: db.execute(
483491
"select vec_distance_cosine(vec_bit(?), vec_bit(?))", args
484492
).fetchone()[0]
@@ -524,6 +532,13 @@ def test_vec_distance_cosine_zero_vector():
524532
assert vec_distance_cosine_int8("[0,0,0]", "[1,2,3]") == 1.0
525533
assert vec_distance_cosine_int8("[1,2,3]", "[0,0,0]") == 1.0
526534

535+
# the same past the SIMD kernels' thresholds
536+
zero, ones = json.dumps([0] * 40), json.dumps([1] * 40)
537+
assert vec_distance_cosine(zero, ones) == 1.0
538+
assert vec_distance_cosine(zero, zero) == 1.0
539+
assert vec_distance_cosine_int8(zero, ones) == 1.0
540+
assert vec_distance_cosine_int8(ones, zero) == 1.0
541+
527542
# bit zero vectors
528543
vec_distance_cosine_bit = lambda a, b: db.execute(
529544
"select vec_distance_cosine(vec_bit(?), vec_bit(?))", [a, b]
@@ -542,18 +557,22 @@ def test_vec_distance_cosine_extreme_magnitudes():
542557
"select vec_distance_cosine(?, ?)", [_f32(a), _f32(b)]
543558
).fetchone()[0]
544559

545-
for scale in [1e-45, 1e-30, 1e-25, 1e-20, 1.0, 1e20, 3e38]:
546-
# cosine distance is invariant under positive scaling
547-
assert vec_distance_cosine([1, 0], [scale, 0]) == 0.0
548-
assert vec_distance_cosine([scale, 0], [1, 0]) == 0.0
549-
assert vec_distance_cosine([scale, 0], [scale, 0]) == 0.0
550-
assert vec_distance_cosine([1, 0], [-scale, 0]) == 2.0
551-
assert vec_distance_cosine([1, 0], [0, scale]) == 1.0
552-
assert isclose(
553-
vec_distance_cosine([1, 0], [scale, scale]),
554-
1 - 1 / np.sqrt(2),
555-
abs_tol=1e-6,
556-
)
560+
# the same 2-element vectors padded to 40 elements, past the SIMD kernels'
561+
# threshold, with a remainder past their 16-element blocks
562+
for pad in [0, 38]:
563+
z = [0] * pad
564+
for scale in [1e-45, 1e-30, 1e-25, 1e-20, 1.0, 1e20, 3e38]:
565+
# cosine distance is invariant under positive scaling
566+
assert vec_distance_cosine([1, 0] + z, [scale, 0] + z) == 0.0
567+
assert vec_distance_cosine([scale, 0] + z, [1, 0] + z) == 0.0
568+
assert vec_distance_cosine([scale, 0] + z, [scale, 0] + z) == 0.0
569+
assert vec_distance_cosine([1, 0] + z, [-scale, 0] + z) == 2.0
570+
assert vec_distance_cosine([1, 0] + z, [0, scale] + z) == 1.0
571+
assert isclose(
572+
vec_distance_cosine([1, 0] + z, [scale, scale] + z),
573+
1 - 1 / np.sqrt(2),
574+
abs_tol=1e-6,
575+
)
557576

558577

559578
def test_ensure_vector_match_cleanup_on_second_vector_error():

0 commit comments

Comments
 (0)