This is an automated email from the ASF dual-hosted git repository. asf-gitbox-commits pushed a commit to branch PHOENIX-7998 in repository https://gitbox.apache.org/repos/asf/phoenix.git
commit a6b14b32547d51918ca8971236ce918178257395 Author: Andrew Purtell <[email protected]> AuthorDate: Tue Oct 6 00:53:26 2026 -0700 PHOENIX-7998 Phase 15: Little-endian vector encoding and direct SIMD loads Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> --- .../function/BsonVectorValueFunction.java | 19 +++----- .../expression/function/ScalarDistanceKernel.java | 4 +- .../apache/phoenix/schema/types/PVectorDouble.java | 16 ++++--- .../apache/phoenix/schema/types/PVectorFloat.java | 17 +++++--- .../expression/function/DistanceKernel.java | 49 ++++++--------------- .../expression/function/PanamaDistanceKernel.java | 40 +++++++++++------ .../function/BsonVectorValueFunctionTest.java | 17 +++++++- .../phoenix/schema/types/VectorDataTypeTest.java | 8 +++- .../function/PanamaDistanceKernelParityTest.java | 51 ++++++++++++++++++---- 9 files changed, 135 insertions(+), 86 deletions(-) diff --git a/phoenix-core-client/src/main/java/org/apache/phoenix/expression/function/BsonVectorValueFunction.java b/phoenix-core-client/src/main/java/org/apache/phoenix/expression/function/BsonVectorValueFunction.java index 196031b8e1..cc3803d83a 100644 --- a/phoenix-core-client/src/main/java/org/apache/phoenix/expression/function/BsonVectorValueFunction.java +++ b/phoenix-core-client/src/main/java/org/apache/phoenix/expression/function/BsonVectorValueFunction.java @@ -57,9 +57,10 @@ import org.apache.phoenix.thirdparty.com.google.common.base.Preconditions; * <li>Vector dimension literal</li> * </ol> * </p> - * Transcodes BSON Binary subtype 9 (VECTOR) FLOAT32 payloads from little endian format into the big - * endian packed representation of {@link PVectorFloat}. Missing paths or documents evaluate to SQL - * NULL. Structural or dimension mismatches raise {@link IllegalDataException}. + * Extracts BSON Binary subtype 9 (VECTOR) FLOAT32 payloads, whose little endian float32 layout is + * the packed representation of {@link PVectorFloat}, without copying or transcoding. Missing paths + * or documents evaluate to SQL NULL. Structural or dimension mismatches raise + * {@link IllegalDataException}. */ @FunctionParseNode.BuiltInFunction(name = BsonVectorValueFunction.NAME, nodeClass = BsonVectorValueParseNode.class, @@ -202,16 +203,8 @@ public class BsonVectorValueFunction extends ScalarFunction { + ", found " + payloadLength / (double) Bytes.SIZEOF_FLOAT) .build().buildException()); } - // Convert little endian BSON float32 payload to big endian PVectorFloat encoding - byte[] out = new byte[payloadLength]; - for (int i = 0; i < payloadLength; i += Bytes.SIZEOF_FLOAT) { - int src = VECTOR_HEADER_SIZE + i; - out[i] = data[src + 3]; - out[i + 1] = data[src + 2]; - out[i + 2] = data[src + 1]; - out[i + 3] = data[src]; - } - ptr.set(out); + // The little endian BSON float32 payload is already the PVectorFloat encoding + ptr.set(data, VECTOR_HEADER_SIZE, payloadLength); return true; } diff --git a/phoenix-core-client/src/main/java/org/apache/phoenix/expression/function/ScalarDistanceKernel.java b/phoenix-core-client/src/main/java/org/apache/phoenix/expression/function/ScalarDistanceKernel.java index 7d014c1a28..32a4ae8d49 100644 --- a/phoenix-core-client/src/main/java/org/apache/phoenix/expression/function/ScalarDistanceKernel.java +++ b/phoenix-core-client/src/main/java/org/apache/phoenix/expression/function/ScalarDistanceKernel.java @@ -17,7 +17,7 @@ */ package org.apache.phoenix.expression.function; -import org.apache.hadoop.hbase.util.Bytes; +import org.apache.phoenix.schema.types.PVectorFloat; /** * Reference scalar distance kernels over packed float byte buffers and {@code float[]} arrays. @@ -30,7 +30,7 @@ final class ScalarDistanceKernel { } private static float read(byte[] buf, int off, int i) { - return Bytes.toFloat(buf, off + i * Bytes.SIZEOF_FLOAT); + return PVectorFloat.readElement(buf, off, i); } /** diff --git a/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PVectorDouble.java b/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PVectorDouble.java index b9c59ddea7..e3e99360bb 100644 --- a/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PVectorDouble.java +++ b/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PVectorDouble.java @@ -23,7 +23,8 @@ import org.apache.phoenix.schema.SortOrder; /** * Data type representing fixed-dimension double-precision floating point vectors, - * {@code VECTOR(DOUBLE, N)}. Stored as packed big-endian IEEE 754 values. + * {@code VECTOR(DOUBLE, N)}. Stored as packed little-endian IEEE 754 values, matching + * {@link PVectorFloat}. */ public class PVectorDouble extends PVectorDataType<double[]> { @@ -35,18 +36,22 @@ public class PVectorDouble extends PVectorDataType<double[]> { /** Reads one element by index from a packed ascending-order vector buffer. */ public static double readElement(byte[] buf, int offset, int index) { - return Bytes.toDouble(buf, offset + index * Bytes.SIZEOF_DOUBLE); + return Double.longBitsToDouble(readBits(buf, offset, index)); } /** Reads one element by index from a packed vector buffer with the given sort order. */ public static double readElement(byte[] buf, int offset, int index, SortOrder sortOrder) { - long b = Bytes.toLong(buf, offset + index * Bytes.SIZEOF_DOUBLE); + long b = readBits(buf, offset, index); if (sortOrder == SortOrder.DESC) { b ^= 0xFFFFFFFFFFFFFFFFL; } return Double.longBitsToDouble(b); } + private static long readBits(byte[] buf, int offset, int index) { + return Long.reverseBytes(Bytes.toLong(buf, offset + index * Bytes.SIZEOF_DOUBLE)); + } + /** Reads one element by index from a packed ascending-order vector pointer. */ public static double readElement(ImmutableBytesWritable ptr, int index) { return readElement(ptr.get(), ptr.getOffset(), index); @@ -67,7 +72,7 @@ public class PVectorDouble extends PVectorDataType<double[]> { } /** - * Writes a vector into a byte buffer as packed contiguous big-endian IEEE 754 values. + * Writes a vector into a byte buffer as packed contiguous little-endian IEEE 754 values. * @throws IllegalArgumentException if the buffer is too small to contain the vector */ public static void writeElements(double[] vector, byte[] buf, int offset) { @@ -76,7 +81,8 @@ public class PVectorDouble extends PVectorDataType<double[]> { + (offset + vector.length * Bytes.SIZEOF_DOUBLE) + " bytes, have " + buf.length); } for (int i = 0; i < vector.length; i++) { - Bytes.putDouble(buf, offset + i * Bytes.SIZEOF_DOUBLE, vector[i]); + Bytes.putLong(buf, offset + i * Bytes.SIZEOF_DOUBLE, + Long.reverseBytes(Double.doubleToRawLongBits(vector[i]))); } } diff --git a/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PVectorFloat.java b/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PVectorFloat.java index 619a719ae2..232189d87c 100644 --- a/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PVectorFloat.java +++ b/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PVectorFloat.java @@ -23,7 +23,9 @@ import org.apache.phoenix.schema.SortOrder; /** * Data type representing fixed-dimension single-precision floating point vectors, - * {@code VECTOR(FLOAT, N)}. Stored as packed big-endian IEEE 754 values. + * {@code VECTOR(FLOAT, N)}. Stored as packed little-endian IEEE 754 values, the byte order of + * x86_64 and arm64, so SIMD kernels load elements directly from cell bytes and BSON FLOAT32 vectors + * copy in unchanged. The encoding is not order preserving; vectors are not comparable by bytes. */ public class PVectorFloat extends PVectorDataType<float[]> { @@ -35,18 +37,22 @@ public class PVectorFloat extends PVectorDataType<float[]> { /** Reads one element by index from a packed ascending-order vector buffer. */ public static float readElement(byte[] buf, int offset, int index) { - return Bytes.toFloat(buf, offset + index * Bytes.SIZEOF_FLOAT); + return Float.intBitsToFloat(readBits(buf, offset, index)); } /** Reads one element by index from a packed vector buffer with the given sort order. */ public static float readElement(byte[] buf, int offset, int index, SortOrder sortOrder) { - int b = Bytes.toInt(buf, offset + index * Bytes.SIZEOF_FLOAT); + int b = readBits(buf, offset, index); if (sortOrder == SortOrder.DESC) { b ^= 0xFFFFFFFF; } return Float.intBitsToFloat(b); } + private static int readBits(byte[] buf, int offset, int index) { + return Integer.reverseBytes(Bytes.toInt(buf, offset + index * Bytes.SIZEOF_FLOAT)); + } + /** Reads one element by index from a packed ascending-order vector pointer. */ public static float readElement(ImmutableBytesWritable ptr, int index) { return readElement(ptr.get(), ptr.getOffset(), index); @@ -67,7 +73,7 @@ public class PVectorFloat extends PVectorDataType<float[]> { } /** - * Writes a vector into a byte buffer as packed contiguous big-endian IEEE 754 values. + * Writes a vector into a byte buffer as packed contiguous little-endian IEEE 754 values. * @throws IllegalArgumentException if the buffer is too small to contain the vector */ public static void writeElements(float[] vector, byte[] buf, int offset) { @@ -76,7 +82,8 @@ public class PVectorFloat extends PVectorDataType<float[]> { + (offset + vector.length * Bytes.SIZEOF_FLOAT) + " bytes, have " + buf.length); } for (int i = 0; i < vector.length; i++) { - Bytes.putFloat(buf, offset + i * Bytes.SIZEOF_FLOAT, vector[i]); + Bytes.putInt(buf, offset + i * Bytes.SIZEOF_FLOAT, + Integer.reverseBytes(Float.floatToRawIntBits(vector[i]))); } } diff --git a/phoenix-core-client/src/main/java21/org/apache/phoenix/expression/function/DistanceKernel.java b/phoenix-core-client/src/main/java21/org/apache/phoenix/expression/function/DistanceKernel.java index 6fff9f6cb8..3b53b51ffa 100644 --- a/phoenix-core-client/src/main/java21/org/apache/phoenix/expression/function/DistanceKernel.java +++ b/phoenix-core-client/src/main/java21/org/apache/phoenix/expression/function/DistanceKernel.java @@ -17,23 +17,17 @@ */ package org.apache.phoenix.expression.function; -import org.apache.hadoop.hbase.util.Bytes; +import org.apache.phoenix.schema.types.PVectorFloat; /** * Java 21 multi-release implementation of {@code DistanceKernel}. Dispatches to * {@link PanamaDistanceKernel} when {@code jdk.incubator.vector} is available, falling back to - * {@link ScalarDistanceKernel}. - * <p> - * Decodes packed big-endian float vectors into thread local float buffers prior to SIMD execution - * to satisfy Vector API buffer alignment and endianness requirements. + * {@link ScalarDistanceKernel}. Both kernels operate directly on packed vector bytes. */ final class DistanceKernel { private static final boolean SIMD = isPanamaAvailable(); - private static final ThreadLocal<float[][]> SCRATCH = - ThreadLocal.withInitial(() -> new float[][] { new float[0], new float[0] }); - private DistanceKernel() { } @@ -44,45 +38,28 @@ final class DistanceKernel { private static boolean isPanamaAvailable() { try { Class.forName("jdk.incubator.vector.FloatVector"); - return PanamaDistanceKernel.dotProduct(new float[] { 1f }, new float[] { 1f }, 1) == 1.0; + byte[] one = PVectorFloat.INSTANCE.toBytes(new float[] { 1f }); + return PanamaDistanceKernel.dotProduct(one, 0, one, 0, 1) == 1.0; } catch (Throwable t) { return false; } } - private static float[] decode(int slot, byte[] buf, int off, int dim) { - float[][] scratch = SCRATCH.get(); - float[] out = scratch[slot]; - if (out.length < dim) { - out = new float[dim]; - scratch[slot] = out; - } - for (int i = 0; i < dim; i++) { - out[i] = Bytes.toFloat(buf, off + i * Bytes.SIZEOF_FLOAT); - } - return out; - } - static double l2DistanceSquared(byte[] a, int aOff, byte[] b, int bOff, int dim, double bound) { - if (!SIMD) { - return ScalarDistanceKernel.l2DistanceSquared(a, aOff, b, bOff, dim, bound); - } - return PanamaDistanceKernel.l2DistanceSquared(decode(0, a, aOff, dim), decode(1, b, bOff, dim), - dim, bound); + return SIMD + ? PanamaDistanceKernel.l2DistanceSquared(a, aOff, b, bOff, dim, bound) + : ScalarDistanceKernel.l2DistanceSquared(a, aOff, b, bOff, dim, bound); } static double dotProduct(byte[] a, int aOff, byte[] b, int bOff, int dim) { - if (!SIMD) { - return ScalarDistanceKernel.dotProduct(a, aOff, b, bOff, dim); - } - return PanamaDistanceKernel.dotProduct(decode(0, a, aOff, dim), decode(1, b, bOff, dim), dim); + return SIMD + ? PanamaDistanceKernel.dotProduct(a, aOff, b, bOff, dim) + : ScalarDistanceKernel.dotProduct(a, aOff, b, bOff, dim); } static double cosineDistance(byte[] a, int aOff, byte[] b, int bOff, int dim) { - if (!SIMD) { - return ScalarDistanceKernel.cosineDistance(a, aOff, b, bOff, dim); - } - return PanamaDistanceKernel.cosineDistance(decode(0, a, aOff, dim), decode(1, b, bOff, dim), - dim); + return SIMD + ? PanamaDistanceKernel.cosineDistance(a, aOff, b, bOff, dim) + : ScalarDistanceKernel.cosineDistance(a, aOff, b, bOff, dim); } } diff --git a/phoenix-core-client/src/main/java21/org/apache/phoenix/expression/function/PanamaDistanceKernel.java b/phoenix-core-client/src/main/java21/org/apache/phoenix/expression/function/PanamaDistanceKernel.java index 386f1a4f86..d168677412 100644 --- a/phoenix-core-client/src/main/java21/org/apache/phoenix/expression/function/PanamaDistanceKernel.java +++ b/phoenix-core-client/src/main/java21/org/apache/phoenix/expression/function/PanamaDistanceKernel.java @@ -17,12 +17,20 @@ */ package org.apache.phoenix.expression.function; +import jdk.incubator.vector.ByteVector; import jdk.incubator.vector.FloatVector; import jdk.incubator.vector.VectorOperators; import jdk.incubator.vector.VectorSpecies; +import org.apache.hadoop.hbase.util.Bytes; +import org.apache.phoenix.schema.types.PVectorFloat; /** - * SIMD distance kernels leveraging the Panama Vector API over {@code float[]} operands. + * SIMD distance kernels leveraging the Panama Vector API over packed little-endian float vectors. + * <p> + * Operands are loaded directly from their {@code byte[]} encodings, at any offset, as byte vectors + * reinterpreted to float lanes. The Vector API defines that reinterpretation as little-endian on + * every platform, which matches the {@link PVectorFloat} encoding, so no decoding pass or copy + * precedes the arithmetic. * <p> * Accumulates intermediate lane sums in single precision across fixed size blocks of * {@link #BLOCK_STRIDES} vector strides before reducing to double precision. Block level reduction @@ -32,12 +40,19 @@ import jdk.incubator.vector.VectorSpecies; final class PanamaDistanceKernel { private static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_PREFERRED; + private static final VectorSpecies<Byte> BYTE_SPECIES = + VectorSpecies.of(byte.class, SPECIES.vectorShape()); static final int BLOCK_STRIDES = 8; private PanamaDistanceKernel() { } - static double l2DistanceSquared(float[] a, float[] b, int dim, double bound) { + private static FloatVector load(byte[] buf, int off, int i) { + return ByteVector.fromArray(BYTE_SPECIES, buf, off + i * Bytes.SIZEOF_FLOAT) + .reinterpretAsFloats(); + } + + static double l2DistanceSquared(byte[] a, int aOff, byte[] b, int bOff, int dim, double bound) { int step = SPECIES.length(); int upper = SPECIES.loopBound(dim); int blockLen = step * BLOCK_STRIDES; @@ -47,8 +62,7 @@ final class PanamaDistanceKernel { int blockEnd = Math.min(upper, i + blockLen); FloatVector acc = FloatVector.zero(SPECIES); for (; i < blockEnd; i += step) { - FloatVector diff = - FloatVector.fromArray(SPECIES, a, i).sub(FloatVector.fromArray(SPECIES, b, i)); + FloatVector diff = load(a, aOff, i).sub(load(b, bOff, i)); acc = diff.fma(diff, acc); } total += acc.reduceLanes(VectorOperators.ADD); @@ -57,7 +71,7 @@ final class PanamaDistanceKernel { } } for (; i < dim; i++) { - double diff = a[i] - b[i]; + double diff = PVectorFloat.readElement(a, aOff, i) - PVectorFloat.readElement(b, bOff, i); total += diff * diff; if (total > bound) { return Double.MAX_VALUE; @@ -66,7 +80,7 @@ final class PanamaDistanceKernel { return total; } - static double dotProduct(float[] a, float[] b, int dim) { + static double dotProduct(byte[] a, int aOff, byte[] b, int bOff, int dim) { int step = SPECIES.length(); int upper = SPECIES.loopBound(dim); int blockLen = step * BLOCK_STRIDES; @@ -76,17 +90,17 @@ final class PanamaDistanceKernel { int blockEnd = Math.min(upper, i + blockLen); FloatVector acc = FloatVector.zero(SPECIES); for (; i < blockEnd; i += step) { - acc = FloatVector.fromArray(SPECIES, a, i).fma(FloatVector.fromArray(SPECIES, b, i), acc); + acc = load(a, aOff, i).fma(load(b, bOff, i), acc); } total += acc.reduceLanes(VectorOperators.ADD); } for (; i < dim; i++) { - total += (double) a[i] * b[i]; + total += (double) PVectorFloat.readElement(a, aOff, i) * PVectorFloat.readElement(b, bOff, i); } return total; } - static double cosineDistance(float[] a, float[] b, int dim) { + static double cosineDistance(byte[] a, int aOff, byte[] b, int bOff, int dim) { int step = SPECIES.length(); int upper = SPECIES.loopBound(dim); int blockLen = step * BLOCK_STRIDES; @@ -100,8 +114,8 @@ final class PanamaDistanceKernel { FloatVector vNormA = FloatVector.zero(SPECIES); FloatVector vNormB = FloatVector.zero(SPECIES); for (; i < blockEnd; i += step) { - FloatVector va = FloatVector.fromArray(SPECIES, a, i); - FloatVector vb = FloatVector.fromArray(SPECIES, b, i); + FloatVector va = load(a, aOff, i); + FloatVector vb = load(b, bOff, i); vDot = va.fma(vb, vDot); vNormA = va.fma(va, vNormA); vNormB = vb.fma(vb, vNormB); @@ -111,8 +125,8 @@ final class PanamaDistanceKernel { normB += vNormB.reduceLanes(VectorOperators.ADD); } for (; i < dim; i++) { - double ai = a[i]; - double bi = b[i]; + double ai = PVectorFloat.readElement(a, aOff, i); + double bi = PVectorFloat.readElement(b, bOff, i); dot += ai * bi; normA += ai * ai; normB += bi * bi; diff --git a/phoenix-core/src/test/java/org/apache/phoenix/expression/function/BsonVectorValueFunctionTest.java b/phoenix-core/src/test/java/org/apache/phoenix/expression/function/BsonVectorValueFunctionTest.java index 8a6c22fb77..255f6dbd09 100644 --- a/phoenix-core/src/test/java/org/apache/phoenix/expression/function/BsonVectorValueFunctionTest.java +++ b/phoenix-core/src/test/java/org/apache/phoenix/expression/function/BsonVectorValueFunctionTest.java @@ -104,7 +104,7 @@ public class BsonVectorValueFunctionTest { @Test public void testExtractsSpecEncodedVector() throws Exception { - // Verify extraction and big-endian transcoding against IEEE 754 float values. + // Verify extraction against IEEE 754 float values. float[] expected = new float[] { 1.5f, -2.5f, 3.0e-3f }; BsonDocument doc = new BsonDocument("search", new BsonDocument("embedding", vector(expected))); BsonVectorValueFunction f = func(doc, "search.embedding", 3); @@ -115,6 +115,21 @@ public class BsonVectorValueFunctionTest { assertArrayEquals(expected, eval(f), DELTA); } + @Test + public void testOutputIsBsonPayloadUnchanged() throws Exception { + // The BSON FLOAT32 payload is already the PVectorFloat encoding, so extraction is a plain view. + float[] values = new float[] { 1.5f, -2.5f, 3.0e-3f, Float.MIN_VALUE }; + BsonBinary binary = vector(values); + BsonVectorValueFunction f = func(new BsonDocument("v", binary), "v", values.length); + ImmutableBytesWritable ptr = new ImmutableBytesWritable(); + assertTrue(f.evaluate(null, ptr)); + byte[] data = binary.getData(); + assertArrayEquals( + Arrays.copyOfRange(data, BsonVectorValueFunction.VECTOR_HEADER_SIZE, data.length), + ptr.copyBytes()); + assertArrayEquals(PVectorFloat.INSTANCE.toBytes(values), ptr.copyBytes()); + } + @Test public void testExtractsFromRawDocumentAsStoredInCell() throws Exception { // Verify extraction from lazily-decoded RawBsonDocument instances. diff --git a/phoenix-core/src/test/java/org/apache/phoenix/schema/types/VectorDataTypeTest.java b/phoenix-core/src/test/java/org/apache/phoenix/schema/types/VectorDataTypeTest.java index 03b9d6bd28..ad8a058647 100644 --- a/phoenix-core/src/test/java/org/apache/phoenix/schema/types/VectorDataTypeTest.java +++ b/phoenix-core/src/test/java/org/apache/phoenix/schema/types/VectorDataTypeTest.java @@ -26,6 +26,8 @@ import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; import org.apache.hadoop.hbase.io.ImmutableBytesWritable; import org.apache.hadoop.hbase.util.Bytes; import org.apache.phoenix.coprocessor.generated.PTableProtos; @@ -346,7 +348,8 @@ public class VectorDataTypeTest { assertEquals(original.length * Bytes.SIZEOF_FLOAT, encoded.length); for (int i = 0; i < original.length; i++) { - int rawBits = Bytes.toInt(encoded, i * Bytes.SIZEOF_FLOAT); + int rawBits = + ByteBuffer.wrap(encoded).order(ByteOrder.LITTLE_ENDIAN).getInt(i * Bytes.SIZEOF_FLOAT); assertEquals("Element " + i + " bit pattern mismatch", Float.floatToRawIntBits(original[i]), rawBits); } @@ -363,7 +366,8 @@ public class VectorDataTypeTest { assertEquals(original.length * Bytes.SIZEOF_DOUBLE, encoded.length); for (int i = 0; i < original.length; i++) { - long rawBits = Bytes.toLong(encoded, i * Bytes.SIZEOF_DOUBLE); + long rawBits = + ByteBuffer.wrap(encoded).order(ByteOrder.LITTLE_ENDIAN).getLong(i * Bytes.SIZEOF_DOUBLE); assertEquals("Element " + i + " bit pattern mismatch", Double.doubleToRawLongBits(original[i]), rawBits); } diff --git a/phoenix-core/src/test/java21/org/apache/phoenix/expression/function/PanamaDistanceKernelParityTest.java b/phoenix-core/src/test/java21/org/apache/phoenix/expression/function/PanamaDistanceKernelParityTest.java index bb21cc9f17..908e962711 100644 --- a/phoenix-core/src/test/java21/org/apache/phoenix/expression/function/PanamaDistanceKernelParityTest.java +++ b/phoenix-core/src/test/java21/org/apache/phoenix/expression/function/PanamaDistanceKernelParityTest.java @@ -21,6 +21,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import java.util.Random; +import org.apache.hadoop.hbase.util.Bytes; import org.apache.phoenix.schema.types.PVectorFloat; import org.junit.Test; @@ -32,6 +33,8 @@ public class PanamaDistanceKernelParityTest { private static final int[] DIMS = { 1, 3, 7, 8, 15, 16, 64, 100, 128, 129, 768, 1536 }; private static final double TOLERANCE = 1e-5; + /** Value offsets within a cell are arbitrary, so the kernels must load from any offset. */ + private static final int[] OFFSETS = { 0, 1, 3, 4 }; private static float[] random(Random rng, int dim) { float[] v = new float[dim]; @@ -41,9 +44,32 @@ public class PanamaDistanceKernelParityTest { return v; } + /** Packs {@code v} at {@code off} between junk bytes, which a kernel must never read. */ + private static byte[] pack(Random rng, float[] v, int off) { + byte[] buf = new byte[off + v.length * Bytes.SIZEOF_FLOAT + 64]; + rng.nextBytes(buf); + PVectorFloat.writeElements(v, buf, off); + return buf; + } + private static void assertClose(String what, double expected, double actual) { - double scale = Math.max(1.0, Math.abs(expected)); - assertEquals(what, expected, actual, TOLERANCE * scale); + assertClose(what, expected, actual, Math.abs(expected)); + } + + private static void assertClose(String what, double expected, double actual, double magnitude) { + assertEquals(what, expected, actual, TOLERANCE * Math.max(1.0, magnitude)); + } + + /** + * Single precision rounding in a dot product scales with the sum of term magnitudes, not with the + * result, which cancellation can make arbitrarily small. + */ + private static double termMagnitude(float[] a, float[] b) { + double sum = 0.0; + for (int i = 0; i < a.length; i++) { + sum += Math.abs((double) a[i] * b[i]); + } + return sum; } @Test @@ -59,14 +85,18 @@ public class PanamaDistanceKernelParityTest { for (int trial = 0; trial < 50; trial++) { float[] a = random(rng, dim); float[] b = random(rng, dim); - String ctx = "dim=" + dim + " trial=" + trial; + int aOff = OFFSETS[trial % OFFSETS.length]; + int bOff = OFFSETS[(trial / OFFSETS.length) % OFFSETS.length]; + byte[] pa = pack(rng, a, aOff); + byte[] pb = pack(rng, b, bOff); + String ctx = "dim=" + dim + " trial=" + trial + " aOff=" + aOff + " bOff=" + bOff; assertClose("l2sq " + ctx, ScalarDistanceKernel.l2DistanceSquared(a, b, dim, Double.MAX_VALUE), - PanamaDistanceKernel.l2DistanceSquared(a, b, dim, Double.MAX_VALUE)); + PanamaDistanceKernel.l2DistanceSquared(pa, aOff, pb, bOff, dim, Double.MAX_VALUE)); assertClose("dot " + ctx, ScalarDistanceKernel.dotProduct(a, b, dim), - PanamaDistanceKernel.dotProduct(a, b, dim)); + PanamaDistanceKernel.dotProduct(pa, aOff, pb, bOff, dim), termMagnitude(a, b)); assertClose("cosine " + ctx, ScalarDistanceKernel.cosineDistance(a, b, dim), - PanamaDistanceKernel.cosineDistance(a, b, dim)); + PanamaDistanceKernel.cosineDistance(pa, aOff, pb, bOff, dim)); } } } @@ -86,7 +116,8 @@ public class PanamaDistanceKernelParityTest { Math.sqrt(ScalarDistanceKernel.l2DistanceSquared(a, b, dim, Double.MAX_VALUE)), VectorDistanceUtil.l2DistanceWithBound(pa, 0, pb, 0, dim, Double.MAX_VALUE)); assertClose("ip " + ctx, -ScalarDistanceKernel.dotProduct(a, b, dim), - VectorDistanceUtil.innerProductDistanceWithBound(pa, 0, pb, 0, dim, Double.MAX_VALUE)); + VectorDistanceUtil.innerProductDistanceWithBound(pa, 0, pb, 0, dim, Double.MAX_VALUE), + termMagnitude(a, b)); assertClose("cosine " + ctx, ScalarDistanceKernel.cosineDistance(a, b, dim), VectorDistanceUtil.cosineDistanceWithBound(pa, 0, pb, 0, dim, Double.MAX_VALUE)); } @@ -98,11 +129,13 @@ public class PanamaDistanceKernelParityTest { for (int dim : DIMS) { float[] a = random(rng, dim); float[] b = random(rng, dim); + byte[] pa = pack(rng, a, 1); + byte[] pb = pack(rng, b, 0); double exact = ScalarDistanceKernel.l2DistanceSquared(a, b, dim, Double.MAX_VALUE); assertEquals(Double.MAX_VALUE, - PanamaDistanceKernel.l2DistanceSquared(a, b, dim, exact * 0.5), 0.0); + PanamaDistanceKernel.l2DistanceSquared(pa, 1, pb, 0, dim, exact * 0.5), 0.0); assertClose("dim=" + dim, exact, - PanamaDistanceKernel.l2DistanceSquared(a, b, dim, exact * 1.5)); + PanamaDistanceKernel.l2DistanceSquared(pa, 1, pb, 0, dim, exact * 1.5)); } } }
