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 be5a8678e7b0d74869ebd90ebe30c60cfc679dc2 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 | 28 +++++------ .../expression/function/ScalarDistanceKernel.java | 4 +- .../phoenix/schema/types/PVectorDataType.java | 10 ++-- .../apache/phoenix/schema/types/PVectorDouble.java | 20 +++++--- .../apache/phoenix/schema/types/PVectorFloat.java | 24 +++++++--- .../expression/function/DistanceKernel.java | 54 +++++++--------------- .../expression/function/PanamaDistanceKernel.java | 49 +++++++++++++------- .../function/BsonVectorValueFunctionTest.java | 17 ++++++- .../phoenix/schema/types/VectorDataTypeTest.java | 8 +++- .../function/PanamaDistanceKernelParityTest.java | 51 ++++++++++++++++---- 10 files changed, 160 insertions(+), 105 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 3d2fb0faca..346ab10559 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 @@ -48,18 +48,20 @@ import org.bson.BsonValue; import org.apache.phoenix.thirdparty.com.google.common.base.Preconditions; /** - * Function extracting a typed float vector from a BSON document field. + * Extracts a float vector from a field of a BSON document. * <p> * Arguments: * <ol> - * <li>BSON document expression (PBson, PJson, or PVarbinary)</li> - * <li>Target field path literal</li> - * <li>Vector dimension literal</li> + * <li>The BSON document expression (PBson, PJson, or PVarbinary)</li> + * <li>The literal path of the target field</li> + * <li>The literal vector dimension</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}. + * The field must hold a BSON Binary value of subtype 9 (VECTOR) with FLOAT32 elements. The + * little-endian FLOAT32 payload is the same as the {@link PVectorFloat} encoding, so the function + * returns the payload bytes with no element conversion. A missing document, a missing field, or a + * BSON null field gives SQL NULL. An incorrect structure, a NaN or infinite element, or an + * incorrect dimension causes an {@link IllegalDataException}. */ @FunctionParseNode.BuiltInFunction(name = BsonVectorValueFunction.NAME, nodeClass = BsonVectorValueParseNode.class, @@ -213,16 +215,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 FLOAT32 payload is the same as 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 64ac24423e..611979e8df 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 for packed float bytes and {@code float[]} arrays. The kernels @@ -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/PVectorDataType.java b/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PVectorDataType.java index 8127decd1c..2d25e4a61b 100644 --- a/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PVectorDataType.java +++ b/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PVectorDataType.java @@ -27,12 +27,12 @@ import org.apache.phoenix.schema.SortOrder; import org.apache.phoenix.util.ByteUtil; /** - * Base class for fixed-dimension vector types {@code VECTOR(FLOAT, N)} and + * Base class for the fixed-dimension vector types {@code VECTOR(FLOAT, N)} and * {@code VECTOR(DOUBLE, N)}. * <p> - * Vectors are serialized as dense, contiguous IEEE 754 big-endian values without header metadata or - * null masks. Vector dimension is stored in column max length. Vectors do not support primary key - * usage or ordering comparisons. + * The serialized form of a vector is a dense sequence of little-endian IEEE 754 values. It has no + * header and no null mask. The column max length holds the vector dimension. A vector column cannot + * be part of a primary key, and vectors do not support order comparisons. * @param <T> the primitive array type of the vector, {@code float[]} or {@code double[]} */ public abstract class PVectorDataType<T> extends PDataType<T> { @@ -65,7 +65,7 @@ public abstract class PVectorDataType<T> extends PDataType<T> { /** Parses one element from its string form. */ protected abstract Number parseElement(String s); - /** Packs a vector into a byte buffer as contiguous big-endian IEEE 754 values. */ + /** Writes a vector into a byte buffer as a dense sequence of little-endian IEEE 754 values. */ protected abstract void pack(T vector, byte[] buf, int offset); /** Unpacks a vector from a packed byte range that the writer encoded in the given sort order. */ 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 b6219ec928..cf48fe182b 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 @@ -22,8 +22,9 @@ import org.apache.hadoop.hbase.util.Bytes; 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. + * Data type for fixed-dimension vectors of double-precision floating point values, + * {@code VECTOR(DOUBLE, N)}. The serialized form is a packed sequence of little-endian IEEE 754 + * values, as in {@link PVectorFloat}. */ public class PVectorDouble extends PVectorDataType<double[]> { @@ -35,18 +36,22 @@ public class PVectorDouble extends PVectorDataType<double[]> { /** Reads the element at the given index from packed vector bytes in ASC sort order. */ 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 the element at the given index from packed vector bytes in 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 the element at the given index from a packed vector pointer in ASC sort order. */ public static double readElement(ImmutableBytesWritable ptr, int index) { return readElement(ptr.get(), ptr.getOffset(), index); @@ -67,8 +72,8 @@ public class PVectorDouble extends PVectorDataType<double[]> { } /** - * Writes a vector into a byte buffer as packed contiguous big-endian IEEE 754 values. - * @throws IllegalArgumentException if the buffer is too small to contain the vector + * Writes a vector into a byte buffer as a packed sequence of little-endian IEEE 754 values. + * @throws IllegalArgumentException if the buffer cannot hold the vector at the given offset */ public static void writeElements(double[] vector, byte[] buf, int offset) { if (buf.length < offset + (long) vector.length * Bytes.SIZEOF_DOUBLE) { @@ -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 814e48c044..a0c9eadef1 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 @@ -22,8 +22,13 @@ import org.apache.hadoop.hbase.util.Bytes; 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. + * Data type for fixed-dimension vectors of single-precision floating point values, + * {@code VECTOR(FLOAT, N)}. + * <p> + * The serialized form is a packed sequence of little-endian IEEE 754 values. Because x86_64 and + * arm64 use this byte order, SIMD kernels can load elements directly from cell bytes. BSON FLOAT32 + * vector payloads use the same encoding, so they need no conversion. The encoding does not keep the + * sort order of values, so a byte comparison of two encoded vectors does not give a numeric order. */ public class PVectorFloat extends PVectorDataType<float[]> { @@ -35,18 +40,22 @@ public class PVectorFloat extends PVectorDataType<float[]> { /** Reads the element at the given index from packed vector bytes in ASC sort order. */ 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 the element at the given index from packed vector bytes in 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 the element at the given index from a packed vector pointer in ASC sort order. */ public static float readElement(ImmutableBytesWritable ptr, int index) { return readElement(ptr.get(), ptr.getOffset(), index); @@ -67,8 +76,8 @@ public class PVectorFloat extends PVectorDataType<float[]> { } /** - * Writes a vector into a byte buffer as packed contiguous big-endian IEEE 754 values. - * @throws IllegalArgumentException if the buffer is too small to contain the vector + * Writes a vector into a byte buffer as a packed sequence of little-endian IEEE 754 values. + * @throws IllegalArgumentException if the buffer cannot hold the vector at the given offset */ public static void writeElements(float[] vector, byte[] buf, int offset) { if (buf.length < offset + (long) vector.length * Bytes.SIZEOF_FLOAT) { @@ -76,7 +85,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..22ba674adc 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,18 @@ */ 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. + * Java 21 multi-release implementation of {@code DistanceKernel}. It uses + * {@link PanamaDistanceKernel} if {@code jdk.incubator.vector} is available and passes a self-test. + * If not, it uses {@link ScalarDistanceKernel}. Both kernels read operands directly from 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 +39,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..5534d276e5 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,27 +17,43 @@ */ 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 that use the Panama Vector API on packed little-endian float vectors. * <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 - * bounds floating point accumulation drift relative to {@link ScalarDistanceKernel} while enabling - * periodic upper bound pruning without per-stride reduction overhead. + * The kernels load operands directly from their {@code byte[]} encodings, at any offset, as byte + * vectors. Then they reinterpret the bytes as float lanes. The Vector API defines this + * reinterpretation as little-endian on all platforms, which agrees with the {@link PVectorFloat} + * encoding. Because of this, the kernels do not decode or copy the operands. + * <p> + * Each kernel adds lane sums in single precision across a block of {@link #BLOCK_STRIDES} vector + * strides. Then it adds the block result to a double-precision total. This limits the rounding + * error relative to {@link ScalarDistanceKernel}. It also lets the L2 kernel compare the total with + * the upper bound after each block and stop early if the total is larger. The cost of a reduction + * occurs once for each block, not once for each stride. */ 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 +63,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 +72,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 +81,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 +91,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 +115,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 +126,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 a461806b77..a9789cd42e 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. + // Make sure that extraction gives the original 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 the PVectorFloat encoding, so the output must be the same bytes. + 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 { // The function must also read a RawBsonDocument, the form of a document in a stored cell 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 b32aea37c9..9b3fd8dcf4 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; @@ -354,7 +356,8 @@ public class VectorDataTypeTest { PVectorFloat.writeElements(original, encoded, 0); 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); } @@ -371,7 +374,8 @@ public class VectorDataTypeTest { PVectorDouble.writeElements(original, encoded, 0); 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 70a22febf5..069f04c697 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; @@ -33,6 +34,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; + /** A value can start at any offset in a cell, 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]; @@ -42,9 +45,32 @@ public class PanamaDistanceKernelParityTest { return v; } + /** Packs {@code v} at {@code off} between random bytes. A kernel must not read these bytes. */ + 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)); + } + + /** + * Returns the sum of the absolute term values of a dot product. Single-precision rounding error + * scales with this sum, not with the result, because cancellation can make the result very 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 @@ -60,14 +86,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)); } } } @@ -87,7 +117,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)); } @@ -99,11 +130,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)); } } }
