Browse Source

Update native vector provider to use unsigned int7 values only (#108243)

This commit updates the native vector provider to reflect that Lucene's scalar quantization is unsigned int7, with a range of values from 0 to 127 inclusive. Stride has been pushed down into native, to allow other platforms to more easily select there own stride length.

Previously the implementation supports signed int8. We might want the more general signed int8 implementation in the future, but for now unsigned int7 is sufficient, and allows to provide more efficient implementations on x64.
Chris Hegarty 1 year ago
parent
commit
7f90a98ed5
19 changed files with 180 additions and 173 deletions
  1. 15 4
      benchmarks/src/main/java/org/elasticsearch/benchmark/vector/VectorScorerBenchmark.java
  2. 1 1
      libs/native/libraries/build.gradle
  3. 8 4
      libs/native/src/main/java/org/elasticsearch/nativeaccess/VectorSimilarityFunctions.java
  4. 24 77
      libs/native/src/main21/java/org/elasticsearch/nativeaccess/jdk/JdkVectorLibrary.java
  5. 16 12
      libs/native/src/test21/java/org/elasticsearch/nativeaccess/jdk/JDKVectorLibraryTests.java
  6. 7 1
      libs/vec/native/build.gradle
  7. 1 1
      libs/vec/native/publish_vec_binaries.sh
  8. 35 16
      libs/vec/native/src/vec/c/vec.c
  9. 2 6
      libs/vec/native/src/vec/headers/vec.h
  10. 3 3
      libs/vec/src/main/java/org/elasticsearch/vec/VectorScorerFactory.java
  11. 1 1
      libs/vec/src/main/java/org/elasticsearch/vec/VectorScorerFactoryImpl.java
  12. 7 7
      libs/vec/src/main21/java/org/elasticsearch/vec/VectorScorerFactoryImpl.java
  13. 9 8
      libs/vec/src/main21/java/org/elasticsearch/vec/internal/AbstractInt7ScalarQuantizedVectorScorer.java
  14. 3 3
      libs/vec/src/main21/java/org/elasticsearch/vec/internal/Int7DotProduct.java
  15. 3 3
      libs/vec/src/main21/java/org/elasticsearch/vec/internal/Int7Euclidean.java
  16. 3 3
      libs/vec/src/main21/java/org/elasticsearch/vec/internal/Int7MaximumInnerProduct.java
  17. 31 22
      libs/vec/src/test/java/org/elasticsearch/vec/VectorScorerFactoryTests.java
  18. 1 1
      server/src/main/java/org/elasticsearch/index/codec/vectors/ES814ScalarQuantizedVectorsWriter.java
  19. 10 0
      test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java

+ 15 - 4
benchmarks/src/main/java/org/elasticsearch/benchmark/vector/VectorScorerBenchmark.java

@@ -94,8 +94,8 @@ public class VectorScorerBenchmark {
         vec1 = new byte[dims];
         vec2 = new byte[dims];
 
-        ThreadLocalRandom.current().nextBytes(vec1);
-        ThreadLocalRandom.current().nextBytes(vec2);
+        randomInt7BytesBetween(vec1);
+        randomInt7BytesBetween(vec2);
         vec1Offset = ThreadLocalRandom.current().nextFloat();
         vec2Offset = ThreadLocalRandom.current().nextFloat();
 
@@ -113,8 +113,8 @@ public class VectorScorerBenchmark {
             scoreCorrectionConstant
         );
         luceneSqrScorer = ScalarQuantizedVectorSimilarity.fromVectorSimilarity(VectorSimilarityFunction.EUCLIDEAN, scoreCorrectionConstant);
-        nativeDotScorer = factory.getScalarQuantizedVectorScorer(dims, size, scoreCorrectionConstant, DOT_PRODUCT, in).get();
-        nativeSqrScorer = factory.getScalarQuantizedVectorScorer(dims, size, scoreCorrectionConstant, EUCLIDEAN, in).get();
+        nativeDotScorer = factory.getInt7ScalarQuantizedVectorScorer(dims, size, scoreCorrectionConstant, DOT_PRODUCT, in).get();
+        nativeSqrScorer = factory.getInt7ScalarQuantizedVectorScorer(dims, size, scoreCorrectionConstant, EUCLIDEAN, in).get();
 
         // sanity
         var f1 = dotProductLucene();
@@ -185,4 +185,15 @@ public class VectorScorerBenchmark {
         float adjustedDistance = squareDistance * scoreCorrectionConstant;
         return 1 / (1f + adjustedDistance);
     }
+
+    // Unsigned int7 byte vectors have values in the range of 0 to 127 (inclusive).
+    static final byte MIN_INT7_VALUE = 0;
+    static final byte MAX_INT7_VALUE = 127;
+
+    static void randomInt7BytesBetween(byte[] bytes) {
+        var random = ThreadLocalRandom.current();
+        for (int i = 0, len = bytes.length; i < len;) {
+            bytes[i++] = (byte) random.nextInt(MIN_INT7_VALUE, MAX_INT7_VALUE + 1);
+        }
+    }
 }

+ 1 - 1
libs/native/libraries/build.gradle

@@ -18,7 +18,7 @@ configurations {
 }
 
 var zstdVersion = "1.5.5"
-var vecVersion = "1.0.3"
+var vecVersion = "1.0.6"
 
 repositories {
   exclusiveContent {

+ 8 - 4
libs/native/src/main/java/org/elasticsearch/nativeaccess/VectorSimilarityFunctions.java

@@ -19,20 +19,24 @@ import java.lang.invoke.MethodHandle;
  */
 public interface VectorSimilarityFunctions {
     /**
-     * Produces a method handle returning the dot product of byte (signed int8) vectors.
+     * Produces a method handle returning the dot product of byte (unsigned int7) vectors.
+     *
+     * <p> Unsigned int7 byte vectors have values in the range of 0 to 127 (inclusive).
      *
      * <p> The type of the method handle will have {@code int} as return type, The type of
      * its first and second arguments will be {@code MemorySegment}, whose contents is the
      * vector data bytes. The third argument is the length of the vector data.
      */
-    MethodHandle dotProductHandle();
+    MethodHandle dotProductHandle7u();
 
     /**
-     * Produces a method handle returning the square distance of byte (signed int8) vectors.
+     * Produces a method handle returning the square distance of byte (unsigned int7) vectors.
+     *
+     * <p> Unsigned int7 byte vectors have values in the range of 0 to 127 (inclusive).
      *
      * <p> The type of the method handle will have {@code int} as return type, The type of
      * its first and second arguments will be {@code MemorySegment}, whose contents is the
      * vector data bytes. The third argument is the length of the vector data.
      */
-    MethodHandle squareDistanceHandle();
+    MethodHandle squareDistanceHandle7u();
 }

+ 24 - 77
libs/native/src/main21/java/org/elasticsearch/nativeaccess/jdk/JdkVectorLibrary.java

@@ -18,7 +18,6 @@ import java.lang.invoke.MethodHandles;
 import java.lang.invoke.MethodType;
 
 import static java.lang.foreign.ValueLayout.ADDRESS;
-import static java.lang.foreign.ValueLayout.JAVA_BYTE;
 import static java.lang.foreign.ValueLayout.JAVA_INT;
 import static org.elasticsearch.nativeaccess.jdk.LinkerHelper.downcallHandle;
 
@@ -51,32 +50,19 @@ public final class JdkVectorLibrary implements VectorLibrary {
 
     private static final class JdkVectorSimilarityFunctions implements VectorSimilarityFunctions {
 
-        static final MethodHandle dot8stride$mh = downcallHandle("dot8s_stride", FunctionDescriptor.of(JAVA_INT));
-        static final MethodHandle sqr8stride$mh = downcallHandle("sqr8s_stride", FunctionDescriptor.of(JAVA_INT));
-
-        static final MethodHandle dot8s$mh = downcallHandle("dot8s", FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS, JAVA_INT));
-        static final MethodHandle sqr8s$mh = downcallHandle("sqr8s", FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS, JAVA_INT));
-
-        // Stride of the native implementation - consumes this number of bytes per loop invocation.
-        // There must be at least this number of bytes/elements available when going native
-        static final int DOT_STRIDE = 32;
-        static final int SQR_STRIDE = 16;
-
-        static {
-            assert DOT_STRIDE > 0 && (DOT_STRIDE & (DOT_STRIDE - 1)) == 0 : "Not a power of two";
-            assert dot8Stride() == DOT_STRIDE : dot8Stride() + " != " + DOT_STRIDE;
-            assert SQR_STRIDE > 0 && (SQR_STRIDE & (SQR_STRIDE - 1)) == 0 : "Not a power of two";
-            assert sqr8Stride() == SQR_STRIDE : sqr8Stride() + " != " + SQR_STRIDE;
-        }
+        static final MethodHandle dot7u$mh = downcallHandle("dot7u", FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS, JAVA_INT));
+        static final MethodHandle sqr7u$mh = downcallHandle("sqr7u", FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS, JAVA_INT));
 
         /**
-         * Computes the dot product of given byte vectors.
+         * Computes the dot product of given unsigned int7 byte vectors.
+         *
+         * <p> Unsigned int7 byte vectors have values in the range of 0 to 127 (inclusive).
          *
          * @param a      address of the first vector
          * @param b      address of the second vector
          * @param length the vector dimensions
          */
-        static int dotProduct(MemorySegment a, MemorySegment b, int length) {
+        static int dotProduct7u(MemorySegment a, MemorySegment b, int length) {
             assert length >= 0;
             if (a.byteSize() != b.byteSize()) {
                 throw new IllegalArgumentException("dimensions differ: " + a.byteSize() + "!=" + b.byteSize());
@@ -84,29 +70,19 @@ public final class JdkVectorLibrary implements VectorLibrary {
             if (length > a.byteSize()) {
                 throw new IllegalArgumentException("length: " + length + ", greater than vector dimensions: " + a.byteSize());
             }
-            int i = 0;
-            int res = 0;
-            if (length >= DOT_STRIDE) {
-                i += length & ~(DOT_STRIDE - 1);
-                res = dot8s(a, b, i);
-            }
-
-            // tail
-            for (; i < length; i++) {
-                res += a.get(JAVA_BYTE, i) * b.get(JAVA_BYTE, i);
-            }
-            assert i == length;
-            return res;
+            return dot7u(a, b, length);
         }
 
         /**
-         * Computes the square distance of given byte vectors.
+         * Computes the square distance of given unsigned int7 byte vectors.
+         *
+         * <p> Unsigned int7 byte vectors have values in the range of 0 to 127 (inclusive).
          *
          * @param a      address of the first vector
          * @param b      address of the second vector
          * @param length the vector dimensions
          */
-        static int squareDistance(MemorySegment a, MemorySegment b, int length) {
+        static int squareDistance7u(MemorySegment a, MemorySegment b, int length) {
             assert length >= 0;
             if (a.byteSize() != b.byteSize()) {
                 throw new IllegalArgumentException("dimensions differ: " + a.byteSize() + "!=" + b.byteSize());
@@ -114,76 +90,47 @@ public final class JdkVectorLibrary implements VectorLibrary {
             if (length > a.byteSize()) {
                 throw new IllegalArgumentException("length: " + length + ", greater than vector dimensions: " + a.byteSize());
             }
-            int i = 0;
-            int res = 0;
-            if (length >= SQR_STRIDE) {
-                i += length & ~(SQR_STRIDE - 1);
-                res = sqr8s(a, b, i);
-            }
-
-            // tail
-            for (; i < length; i++) {
-                int dist = a.get(JAVA_BYTE, i) - b.get(JAVA_BYTE, i);
-                res += dist * dist;
-            }
-            assert i == length;
-            return res;
-        }
-
-        private static int dot8Stride() {
-            try {
-                return (int) dot8stride$mh.invokeExact();
-            } catch (Throwable t) {
-                throw new AssertionError(t);
-            }
-        }
-
-        private static int sqr8Stride() {
-            try {
-                return (int) sqr8stride$mh.invokeExact();
-            } catch (Throwable t) {
-                throw new AssertionError(t);
-            }
+            return sqr7u(a, b, length);
         }
 
-        private static int dot8s(MemorySegment a, MemorySegment b, int length) {
+        private static int dot7u(MemorySegment a, MemorySegment b, int length) {
             try {
-                return (int) dot8s$mh.invokeExact(a, b, length);
+                return (int) dot7u$mh.invokeExact(a, b, length);
             } catch (Throwable t) {
                 throw new AssertionError(t);
             }
         }
 
-        private static int sqr8s(MemorySegment a, MemorySegment b, int length) {
+        private static int sqr7u(MemorySegment a, MemorySegment b, int length) {
             try {
-                return (int) sqr8s$mh.invokeExact(a, b, length);
+                return (int) sqr7u$mh.invokeExact(a, b, length);
             } catch (Throwable t) {
                 throw new AssertionError(t);
             }
         }
 
-        static final MethodHandle DOT_HANDLE;
-        static final MethodHandle SQR_HANDLE;
+        static final MethodHandle DOT_HANDLE_7U;
+        static final MethodHandle SQR_HANDLE_7U;
 
         static {
             try {
                 var lookup = MethodHandles.lookup();
                 var mt = MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class, int.class);
-                DOT_HANDLE = lookup.findStatic(JdkVectorSimilarityFunctions.class, "dotProduct", mt);
-                SQR_HANDLE = lookup.findStatic(JdkVectorSimilarityFunctions.class, "squareDistance", mt);
+                DOT_HANDLE_7U = lookup.findStatic(JdkVectorSimilarityFunctions.class, "dotProduct7u", mt);
+                SQR_HANDLE_7U = lookup.findStatic(JdkVectorSimilarityFunctions.class, "squareDistance7u", mt);
             } catch (NoSuchMethodException | IllegalAccessException e) {
                 throw new RuntimeException(e);
             }
         }
 
         @Override
-        public MethodHandle dotProductHandle() {
-            return DOT_HANDLE;
+        public MethodHandle dotProductHandle7u() {
+            return DOT_HANDLE_7U;
         }
 
         @Override
-        public MethodHandle squareDistanceHandle() {
-            return SQR_HANDLE;
+        public MethodHandle squareDistanceHandle7u() {
+            return SQR_HANDLE_7U;
         }
     }
 }

+ 16 - 12
libs/native/src/test21/java/org/elasticsearch/nativeaccess/jdk/JDKVectorLibraryTests.java

@@ -22,6 +22,10 @@ import static org.hamcrest.Matchers.containsString;
 
 public class JDKVectorLibraryTests extends VectorSimilarityFunctionsTests {
 
+    // bounds of the range of values that can be seen by int7 scalar quantized vectors
+    static final byte MIN_INT7_VALUE = 0;
+    static final byte MAX_INT7_VALUE = 127;
+
     static final Class<IllegalArgumentException> IAE = IllegalArgumentException.class;
 
     static final int[] VECTOR_DIMS = { 1, 4, 6, 8, 13, 16, 25, 31, 32, 33, 64, 100, 128, 207, 256, 300, 512, 702, 1023, 1024, 1025 };
@@ -49,14 +53,14 @@ public class JDKVectorLibraryTests extends VectorSimilarityFunctionsTests {
         return () -> IntStream.of(VECTOR_DIMS).boxed().map(i -> new Object[] { i }).iterator();
     }
 
-    public void testBinaryVectors() {
+    public void testInt7BinaryVectors() {
         assumeTrue(notSupportedMsg(), supported());
         final int dims = size;
         final int numVecs = randomIntBetween(2, 101);
         var values = new byte[numVecs][dims];
         var segment = arena.allocate((long) dims * numVecs);
         for (int i = 0; i < numVecs; i++) {
-            random().nextBytes(values[i]);
+            randomBytesBetween(values[i], MIN_INT7_VALUE, MAX_INT7_VALUE);
             MemorySegment.copy(MemorySegment.ofArray(values[i]), 0L, segment, (long) i * dims, dims);
         }
 
@@ -65,29 +69,29 @@ public class JDKVectorLibraryTests extends VectorSimilarityFunctionsTests {
             int first = randomInt(numVecs - 1);
             int second = randomInt(numVecs - 1);
             // dot product
-            int implDot = dotProduct(segment.asSlice((long) first * dims, dims), segment.asSlice((long) second * dims, dims), dims);
+            int implDot = dotProduct7u(segment.asSlice((long) first * dims, dims), segment.asSlice((long) second * dims, dims), dims);
             int otherDot = dotProductScalar(values[first], values[second]);
             assertEquals(otherDot, implDot);
 
-            int squareDist = squareDistance(segment.asSlice((long) first * dims, dims), segment.asSlice((long) second * dims, dims), dims);
-            int otherSq = squareDistanceScalar(values[first], values[second]);
-            assertEquals(otherSq, squareDist);
+            int implSqr = squareDistance7u(segment.asSlice((long) first * dims, dims), segment.asSlice((long) second * dims, dims), dims);
+            int otherSqr = squareDistanceScalar(values[first], values[second]);
+            assertEquals(otherSqr, implSqr);
         }
     }
 
     public void testIllegalDims() {
         assumeTrue(notSupportedMsg(), supported());
         var segment = arena.allocate((long) size * 3);
-        var e = expectThrows(IAE, () -> dotProduct(segment.asSlice(0L, size), segment.asSlice(size, size + 1), size));
+        var e = expectThrows(IAE, () -> dotProduct7u(segment.asSlice(0L, size), segment.asSlice(size, size + 1), size));
         assertThat(e.getMessage(), containsString("dimensions differ"));
 
-        e = expectThrows(IAE, () -> dotProduct(segment.asSlice(0L, size), segment.asSlice(size, size), size + 1));
+        e = expectThrows(IAE, () -> dotProduct7u(segment.asSlice(0L, size), segment.asSlice(size, size), size + 1));
         assertThat(e.getMessage(), containsString("greater than vector dimensions"));
     }
 
-    int dotProduct(MemorySegment a, MemorySegment b, int length) {
+    int dotProduct7u(MemorySegment a, MemorySegment b, int length) {
         try {
-            return (int) getVectorDistance().dotProductHandle().invokeExact(a, b, length);
+            return (int) getVectorDistance().dotProductHandle7u().invokeExact(a, b, length);
         } catch (Throwable e) {
             if (e instanceof Error err) {
                 throw err;
@@ -99,9 +103,9 @@ public class JDKVectorLibraryTests extends VectorSimilarityFunctionsTests {
         }
     }
 
-    int squareDistance(MemorySegment a, MemorySegment b, int length) {
+    int squareDistance7u(MemorySegment a, MemorySegment b, int length) {
         try {
-            return (int) getVectorDistance().squareDistanceHandle().invokeExact(a, b, length);
+            return (int) getVectorDistance().squareDistanceHandle7u().invokeExact(a, b, length);
         } catch (Throwable e) {
             if (e instanceof Error err) {
                 throw err;

+ 7 - 1
libs/vec/native/build.gradle

@@ -9,13 +9,19 @@ apply plugin: 'c'
 
 var os = org.gradle.internal.os.OperatingSystem.current()
 
-// To update this library run publish_vec_binaries.sh
+// To update this library run publish_vec_binaries.sh  ( or ./gradlew vecSharedLibrary )
 // Or
 // For local development, build the docker image with:
 //   docker build --platform linux/arm64 --progress=plain .
 // Grab the image id from the console output, then, e.g.
 //   docker run 9c9f36564c148b275aeecc42749e7b4580ded79dcf51ff6ccc008c8861e7a979 > build/libs/vec/shared/libvec.so
 //
+// To run tests and benchmarks on a locally built libvec,
+//  1. Temporarily comment out the download in libs/native/library/build.gradle
+//       libs "org.elasticsearch:vec:${vecVersion}@zip"
+//  2. Copy your locally built libvec binary, e.g.
+//       cp libs/vec/native/build/libs/vec/shared/libvec.dylib libs/native/libraries/build/platform/darwin-aarch64/libvec.dylib
+//
 // Look at the disassemble:
 //  objdump --disassemble-symbols=_dot8s build/libs/vec/shared/libvec.dylib
 // Note: symbol decoration may differ on Linux, i.e. the leading underscore is not present

+ 1 - 1
libs/vec/native/publish_vec_binaries.sh

@@ -19,7 +19,7 @@ if [ -z "$ARTIFACTORY_API_KEY" ]; then
   exit 1;
 fi
 
-VERSION="1.0.3"
+VERSION="1.0.6"
 ARTIFACTORY_REPOSITORY="${ARTIFACTORY_REPOSITORY:-https://artifactory.elastic.dev/artifactory/elasticsearch-native/}"
 TEMP=$(mktemp -d)
 

+ 35 - 16
libs/vec/native/src/vec/c/vec.c

@@ -10,12 +10,12 @@
 #include <arm_neon.h>
 #include "vec.h"
 
-#ifndef DOT8_STRIDE_BYTES_LEN
-#define DOT8_STRIDE_BYTES_LEN 32
+#ifndef DOT7U_STRIDE_BYTES_LEN
+#define DOT7U_STRIDE_BYTES_LEN 32 // Must be a power of 2
 #endif
 
-#ifndef SQR8S_STRIDE_BYTES_LEN
-#define SQR8S_STRIDE_BYTES_LEN 16
+#ifndef SQR7U_STRIDE_BYTES_LEN
+#define SQR7U_STRIDE_BYTES_LEN 16 // Must be a power of 2
 #endif
 
 #ifdef __linux__
@@ -46,15 +46,7 @@ EXPORT int vec_caps() {
 #endif
 }
 
-EXPORT int dot8s_stride() {
-    return DOT8_STRIDE_BYTES_LEN;
-}
-
-EXPORT int sqr8s_stride() {
-    return SQR8S_STRIDE_BYTES_LEN;
-}
-
-EXPORT int32_t dot8s(int8_t* a, int8_t* b, size_t dims) {
+static inline int32_t dot7u_inner(int8_t* a, int8_t* b, size_t dims) {
     // We have contention in the instruction pipeline on the accumulation
     // registers if we use too few.
     int32x4_t acc1 = vdupq_n_s32(0);
@@ -63,7 +55,7 @@ EXPORT int32_t dot8s(int8_t* a, int8_t* b, size_t dims) {
     int32x4_t acc4 = vdupq_n_s32(0);
 
     // Some unrolling gives around 50% performance improvement.
-    for (int i = 0; i < dims; i += DOT8_STRIDE_BYTES_LEN) {
+    for (int i = 0; i < dims; i += DOT7U_STRIDE_BYTES_LEN) {
         // Read into 16 x 8 bit vectors.
         int8x16_t va1 = vld1q_s8(a + i);
         int8x16_t vb1 = vld1q_s8(b + i);
@@ -88,13 +80,26 @@ EXPORT int32_t dot8s(int8_t* a, int8_t* b, size_t dims) {
     return vaddvq_s32(vaddq_s32(acc5, acc6));
 }
 
-EXPORT int32_t sqr8s(int8_t *a, int8_t *b, size_t dims) {
+EXPORT int32_t dot7u(int8_t* a, int8_t* b, size_t dims) {
+    int32_t res = 0;
+    int i = 0;
+    if (dims > DOT7U_STRIDE_BYTES_LEN) {
+        i += dims & ~(DOT7U_STRIDE_BYTES_LEN - 1);
+        res = dot7u_inner(a, b, i);
+    }
+    for (; i < dims; i++) {
+        res += a[i] * b[i];
+    }
+    return res;
+}
+
+static inline int32_t sqr7u_inner(int8_t *a, int8_t *b, size_t dims) {
     int32x4_t acc1 = vdupq_n_s32(0);
     int32x4_t acc2 = vdupq_n_s32(0);
     int32x4_t acc3 = vdupq_n_s32(0);
     int32x4_t acc4 = vdupq_n_s32(0);
 
-    for (int i = 0; i < dims; i += SQR8S_STRIDE_BYTES_LEN) {
+    for (int i = 0; i < dims; i += SQR7U_STRIDE_BYTES_LEN) {
         int8x16_t va1 = vld1q_s8(a + i);
         int8x16_t vb1 = vld1q_s8(b + i);
 
@@ -112,3 +117,17 @@ EXPORT int32_t sqr8s(int8_t *a, int8_t *b, size_t dims) {
     int32x4_t acc6 = vaddq_s32(acc3, acc4);
     return vaddvq_s32(vaddq_s32(acc5, acc6));
 }
+
+EXPORT int32_t sqr7u(int8_t* a, int8_t* b, size_t dims) {
+    int32_t res = 0;
+    int i = 0;
+    if (i > SQR7U_STRIDE_BYTES_LEN) {
+        i += dims & ~(SQR7U_STRIDE_BYTES_LEN - 1);
+        res = sqr7u_inner(a, b, i);
+    }
+    for (; i < dims; i++) {
+        int32_t dist = a[i] - b[i];
+        res += dist * dist;
+    }
+    return res;
+}

+ 2 - 6
libs/vec/native/src/vec/headers/vec.h

@@ -16,10 +16,6 @@
 
 EXPORT int vec_caps();
 
-EXPORT int dot8s_stride();
+EXPORT int32_t dot7u(int8_t* a, int8_t* b, size_t dims);
 
-EXPORT int sqr8s_stride();
-
-EXPORT int32_t dot8s(int8_t* a, int8_t* b, size_t dims);
-
-EXPORT int32_t sqr8s(int8_t *a, int8_t *b, size_t length);
+EXPORT int32_t sqr7u(int8_t *a, int8_t *b, size_t length);

+ 3 - 3
libs/vec/src/main/java/org/elasticsearch/vec/VectorScorerFactory.java

@@ -20,8 +20,8 @@ public interface VectorScorerFactory {
     }
 
     /**
-     * Returns an optional containing a scalar quantized vector scorer for the
-     * given parameters, or an empty optional if a scorer is not supported.
+     * Returns an optional containing an int7 scalar quantized vector scorer for
+     * the given parameters, or an empty optional if a scorer is not supported.
      *
      * @param dims the vector dimensions
      * @param maxOrd the ordinal of the largest vector accessible
@@ -32,7 +32,7 @@ public interface VectorScorerFactory {
      *    the length must be (maxOrd + Float#BYTES) * dims
      * @return an optional containing the vector scorer, or empty
      */
-    Optional<VectorScorer> getScalarQuantizedVectorScorer(
+    Optional<VectorScorer> getInt7ScalarQuantizedVectorScorer(
         int dims,
         int maxOrd,
         float scoreCorrectionConstant,

+ 1 - 1
libs/vec/src/main/java/org/elasticsearch/vec/VectorScorerFactoryImpl.java

@@ -17,7 +17,7 @@ class VectorScorerFactoryImpl implements VectorScorerFactory {
     static final VectorScorerFactoryImpl INSTANCE = null;
 
     @Override
-    public Optional<VectorScorer> getScalarQuantizedVectorScorer(
+    public Optional<VectorScorer> getInt7ScalarQuantizedVectorScorer(
         int dims,
         int maxOrd,
         float scoreCorrectionConstant,

+ 7 - 7
libs/vec/src/main21/java/org/elasticsearch/vec/VectorScorerFactoryImpl.java

@@ -10,10 +10,10 @@ package org.elasticsearch.vec;
 
 import org.apache.lucene.store.IndexInput;
 import org.elasticsearch.nativeaccess.NativeAccess;
-import org.elasticsearch.vec.internal.DotProduct;
-import org.elasticsearch.vec.internal.Euclidean;
 import org.elasticsearch.vec.internal.IndexInputUtils;
-import org.elasticsearch.vec.internal.MaximumInnerProduct;
+import org.elasticsearch.vec.internal.Int7DotProduct;
+import org.elasticsearch.vec.internal.Int7Euclidean;
+import org.elasticsearch.vec.internal.Int7MaximumInnerProduct;
 
 import java.util.Optional;
 
@@ -28,7 +28,7 @@ class VectorScorerFactoryImpl implements VectorScorerFactory {
     }
 
     @Override
-    public Optional<VectorScorer> getScalarQuantizedVectorScorer(
+    public Optional<VectorScorer> getInt7ScalarQuantizedVectorScorer(
         int dims,
         int maxOrd,
         float scoreCorrectionConstant,
@@ -40,9 +40,9 @@ class VectorScorerFactoryImpl implements VectorScorerFactory {
             return Optional.empty(); // the input type is not MemorySegment based
         }
         return Optional.of(switch (similarityType) {
-            case COSINE, DOT_PRODUCT -> new DotProduct(dims, maxOrd, scoreCorrectionConstant, input);
-            case EUCLIDEAN -> new Euclidean(dims, maxOrd, scoreCorrectionConstant, input);
-            case MAXIMUM_INNER_PRODUCT -> new MaximumInnerProduct(dims, maxOrd, scoreCorrectionConstant, input);
+            case COSINE, DOT_PRODUCT -> new Int7DotProduct(dims, maxOrd, scoreCorrectionConstant, input);
+            case EUCLIDEAN -> new Int7Euclidean(dims, maxOrd, scoreCorrectionConstant, input);
+            case MAXIMUM_INNER_PRODUCT -> new Int7MaximumInnerProduct(dims, maxOrd, scoreCorrectionConstant, input);
         });
     }
 }

+ 9 - 8
libs/vec/src/main21/java/org/elasticsearch/vec/internal/AbstractScalarQuantizedVectorScorer.java → libs/vec/src/main21/java/org/elasticsearch/vec/internal/AbstractInt7ScalarQuantizedVectorScorer.java

@@ -18,7 +18,8 @@ import java.io.IOException;
 import java.lang.foreign.MemorySegment;
 import java.lang.invoke.MethodHandle;
 
-abstract sealed class AbstractScalarQuantizedVectorScorer implements VectorScorer permits DotProduct, Euclidean, MaximumInnerProduct {
+abstract sealed class AbstractInt7ScalarQuantizedVectorScorer implements VectorScorer permits Int7DotProduct, Int7Euclidean,
+    Int7MaximumInnerProduct {
 
     static final VectorSimilarityFunctions DISTANCE_FUNCS = NativeAccess.instance()
         .getVectorSimilarityFunctions()
@@ -36,7 +37,7 @@ abstract sealed class AbstractScalarQuantizedVectorScorer implements VectorScore
 
     private final ScalarQuantizedVectorSimilarity fallbackScorer;
 
-    protected AbstractScalarQuantizedVectorScorer(
+    protected AbstractInt7ScalarQuantizedVectorScorer(
         int dims,
         int maxOrd,
         float scoreCorrectionConstant,
@@ -114,13 +115,13 @@ abstract sealed class AbstractScalarQuantizedVectorScorer implements VectorScore
         return index >= 0 && index < length;
     }
 
-    static final MethodHandle DOT_PRODUCT = DISTANCE_FUNCS.dotProductHandle();
-    static final MethodHandle SQUARE_DISTANCE = DISTANCE_FUNCS.squareDistanceHandle();
+    static final MethodHandle DOT_PRODUCT_7U = DISTANCE_FUNCS.dotProductHandle7u();
+    static final MethodHandle SQUARE_DISTANCE_7U = DISTANCE_FUNCS.squareDistanceHandle7u();
 
-    static int dotProduct(MemorySegment a, MemorySegment b, int length) {
+    static int dotProduct7u(MemorySegment a, MemorySegment b, int length) {
         // assert assertSegments(a, b, length);
         try {
-            return (int) DOT_PRODUCT.invokeExact(a, b, length);
+            return (int) DOT_PRODUCT_7U.invokeExact(a, b, length);
         } catch (Throwable e) {
             if (e instanceof Error err) {
                 throw err;
@@ -132,10 +133,10 @@ abstract sealed class AbstractScalarQuantizedVectorScorer implements VectorScore
         }
     }
 
-    static int squareDistance(MemorySegment a, MemorySegment b, int length) {
+    static int squareDistance7u(MemorySegment a, MemorySegment b, int length) {
         // assert assertSegments(a, b, length);
         try {
-            return (int) SQUARE_DISTANCE.invokeExact(a, b, length);
+            return (int) SQUARE_DISTANCE_7U.invokeExact(a, b, length);
         } catch (Throwable e) {
             if (e instanceof Error err) {
                 throw err;

+ 3 - 3
libs/vec/src/main21/java/org/elasticsearch/vec/internal/DotProduct.java → libs/vec/src/main21/java/org/elasticsearch/vec/internal/Int7DotProduct.java

@@ -16,9 +16,9 @@ import java.io.IOException;
 import java.lang.foreign.MemorySegment;
 
 // Scalar Quantized vectors are inherently byte sized, so dims is equal to the length in bytes.
-public final class DotProduct extends AbstractScalarQuantizedVectorScorer {
+public final class Int7DotProduct extends AbstractInt7ScalarQuantizedVectorScorer {
 
-    public DotProduct(int dims, int maxOrd, float scoreCorrectionConstant, IndexInput input) {
+    public Int7DotProduct(int dims, int maxOrd, float scoreCorrectionConstant, IndexInput input) {
         super(
             dims,
             maxOrd,
@@ -46,7 +46,7 @@ public final class DotProduct extends AbstractScalarQuantizedVectorScorer {
         float secondOffset = Float.intBitsToFloat(input.readInt());
 
         if (firstSeg != null && secondSeg != null) {
-            int dotProduct = dotProduct(firstSeg, secondSeg, length);
+            int dotProduct = dotProduct7u(firstSeg, secondSeg, length);
             float adjustedDistance = dotProduct * scoreCorrectionConstant + firstOffset + secondOffset;
             return (1 + adjustedDistance) / 2;
         } else {

+ 3 - 3
libs/vec/src/main21/java/org/elasticsearch/vec/internal/Euclidean.java → libs/vec/src/main21/java/org/elasticsearch/vec/internal/Int7Euclidean.java

@@ -16,9 +16,9 @@ import java.io.IOException;
 import java.lang.foreign.MemorySegment;
 
 // Scalar Quantized vectors are inherently bytes.
-public final class Euclidean extends AbstractScalarQuantizedVectorScorer {
+public final class Int7Euclidean extends AbstractInt7ScalarQuantizedVectorScorer {
 
-    public Euclidean(int dims, int maxOrd, float scoreCorrectionConstant, IndexInput input) {
+    public Int7Euclidean(int dims, int maxOrd, float scoreCorrectionConstant, IndexInput input) {
         super(
             dims,
             maxOrd,
@@ -41,7 +41,7 @@ public final class Euclidean extends AbstractScalarQuantizedVectorScorer {
         MemorySegment secondSeg = segmentSlice(secondByteOffset, length);
 
         if (firstSeg != null && secondSeg != null) {
-            int squareDistance = squareDistance(firstSeg, secondSeg, length);
+            int squareDistance = squareDistance7u(firstSeg, secondSeg, length);
             float adjustedDistance = squareDistance * scoreCorrectionConstant;
             return 1 / (1f + adjustedDistance);
         } else {

+ 3 - 3
libs/vec/src/main21/java/org/elasticsearch/vec/internal/MaximumInnerProduct.java → libs/vec/src/main21/java/org/elasticsearch/vec/internal/Int7MaximumInnerProduct.java

@@ -16,9 +16,9 @@ import java.io.IOException;
 import java.lang.foreign.MemorySegment;
 
 // Scalar Quantized vectors are inherently bytes.
-public final class MaximumInnerProduct extends AbstractScalarQuantizedVectorScorer {
+public final class Int7MaximumInnerProduct extends AbstractInt7ScalarQuantizedVectorScorer {
 
-    public MaximumInnerProduct(int dims, int maxOrd, float scoreCorrectionConstant, IndexInput input) {
+    public Int7MaximumInnerProduct(int dims, int maxOrd, float scoreCorrectionConstant, IndexInput input) {
         super(
             dims,
             maxOrd,
@@ -46,7 +46,7 @@ public final class MaximumInnerProduct extends AbstractScalarQuantizedVectorScor
         float secondOffset = Float.intBitsToFloat(input.readInt());
 
         if (firstSeg != null && secondSeg != null) {
-            int dotProduct = dotProduct(firstSeg, secondSeg, length);
+            int dotProduct = dotProduct7u(firstSeg, secondSeg, length);
             float adjustedDistance = dotProduct * scoreCorrectionConstant + firstOffset + secondOffset;
             return scaleMaxInnerProductScore(adjustedDistance);
         } else {

+ 31 - 22
libs/vec/src/test/java/org/elasticsearch/vec/VectorScorerFactoryTests.java

@@ -13,7 +13,6 @@ import org.apache.lucene.store.IOContext;
 import org.apache.lucene.store.IndexInput;
 import org.apache.lucene.store.IndexOutput;
 import org.apache.lucene.store.MMapDirectory;
-import org.elasticsearch.test.ESTestCase;
 
 import java.io.IOException;
 import java.util.Arrays;
@@ -29,6 +28,10 @@ import static org.hamcrest.Matchers.equalTo;
 // @com.carrotsearch.randomizedtesting.annotations.Repeat(iterations = 100)
 public class VectorScorerFactoryTests extends AbstractVectorTestCase {
 
+    // bounds of the range of values that can be seen by int7 scalar quantized vectors
+    static final byte MIN_INT7_VALUE = 0;
+    static final byte MAX_INT7_VALUE = 127;
+
     // Tests that the provider instance is present or not on expected platforms/architectures
     public void testSupport() {
         supported();
@@ -66,22 +69,22 @@ public class VectorScorerFactoryTests extends AbstractVectorTestCase {
                 try (IndexInput in = dir.openInput(fileName, IOContext.DEFAULT)) {
                     // dot product
                     float expected = luceneScore(DOT_PRODUCT, vec1, vec2, 1, 1, 1);
-                    var scorer = factory.getScalarQuantizedVectorScorer(dims, 2, 1, DOT_PRODUCT, in).get();
+                    var scorer = factory.getInt7ScalarQuantizedVectorScorer(dims, 2, 1, DOT_PRODUCT, in).get();
                     assertThat(scorer.score(0, 1), equalTo(expected));
                     assertThat((new VectorScorerSupplierAdapter(scorer)).scorer(0).score(1), equalTo(expected));
                     // max inner product
                     expected = luceneScore(MAXIMUM_INNER_PRODUCT, vec1, vec2, 1, 1, 1);
-                    scorer = factory.getScalarQuantizedVectorScorer(dims, 2, 1, MAXIMUM_INNER_PRODUCT, in).get();
+                    scorer = factory.getInt7ScalarQuantizedVectorScorer(dims, 2, 1, MAXIMUM_INNER_PRODUCT, in).get();
                     assertThat(scorer.score(0, 1), equalTo(expected));
                     assertThat((new VectorScorerSupplierAdapter(scorer)).scorer(0).score(1), equalTo(expected));
                     // cosine
                     expected = luceneScore(COSINE, vec1, vec2, 1, 1, 1);
-                    scorer = factory.getScalarQuantizedVectorScorer(dims, 2, 1, COSINE, in).get();
+                    scorer = factory.getInt7ScalarQuantizedVectorScorer(dims, 2, 1, COSINE, in).get();
                     assertThat(scorer.score(0, 1), equalTo(expected));
                     assertThat((new VectorScorerSupplierAdapter(scorer)).scorer(0).score(1), equalTo(expected));
                     // euclidean
                     expected = luceneScore(EUCLIDEAN, vec1, vec2, 1, 1, 1);
-                    scorer = factory.getScalarQuantizedVectorScorer(dims, 2, 1, EUCLIDEAN, in).get();
+                    scorer = factory.getInt7ScalarQuantizedVectorScorer(dims, 2, 1, EUCLIDEAN, in).get();
                     assertThat(scorer.score(0, 1), equalTo(expected));
                     assertThat((new VectorScorerSupplierAdapter(scorer)).scorer(0).score(1), equalTo(expected));
                 }
@@ -91,24 +94,24 @@ public class VectorScorerFactoryTests extends AbstractVectorTestCase {
 
     public void testRandom() throws IOException {
         assumeTrue(notSupportedMsg(), supported());
-        testRandom(MMapDirectory.DEFAULT_MAX_CHUNK_SIZE, ESTestCase::randomByteArrayOfLength);
+        testRandom(MMapDirectory.DEFAULT_MAX_CHUNK_SIZE, BYTE_ARRAY_RANDOM_INT7_FUNC);
     }
 
     public void testRandomMaxChunkSizeSmall() throws IOException {
         assumeTrue(notSupportedMsg(), supported());
         long maxChunkSize = randomLongBetween(32, 128);
         logger.info("maxChunkSize=" + maxChunkSize);
-        testRandom(maxChunkSize, ESTestCase::randomByteArrayOfLength);
+        testRandom(maxChunkSize, BYTE_ARRAY_RANDOM_INT7_FUNC);
     }
 
     public void testRandomMax() throws IOException {
         assumeTrue(notSupportedMsg(), supported());
-        testRandom(MMapDirectory.DEFAULT_MAX_CHUNK_SIZE, BYTE_ARRAY_MAX_FUNC);
+        testRandom(MMapDirectory.DEFAULT_MAX_CHUNK_SIZE, BYTE_ARRAY_MAX_INT7_FUNC);
     }
 
     public void testRandomMin() throws IOException {
         assumeTrue(notSupportedMsg(), supported());
-        testRandom(MMapDirectory.DEFAULT_MAX_CHUNK_SIZE, BYTE_ARRAY_MIN_FUNC);
+        testRandom(MMapDirectory.DEFAULT_MAX_CHUNK_SIZE, BYTE_ARRAY_MIN_INT7_FUNC);
     }
 
     void testRandom(long maxChunkSize, Function<Integer, byte[]> byteArraySupplier) throws IOException {
@@ -139,22 +142,22 @@ public class VectorScorerFactoryTests extends AbstractVectorTestCase {
                     int idx1 = randomIntBetween(0, size - 1); // may be the same as idx0 - which is ok.
                     // dot product
                     float expected = luceneScore(DOT_PRODUCT, vectors[idx0], vectors[idx1], correction, offsets[idx0], offsets[idx1]);
-                    var scorer = factory.getScalarQuantizedVectorScorer(dims, size, correction, DOT_PRODUCT, in).get();
+                    var scorer = factory.getInt7ScalarQuantizedVectorScorer(dims, size, correction, DOT_PRODUCT, in).get();
                     assertThat(scorer.score(idx0, idx1), equalTo(expected));
                     assertThat((new VectorScorerSupplierAdapter(scorer)).scorer(idx0).score(idx1), equalTo(expected));
                     // max inner product
                     expected = luceneScore(MAXIMUM_INNER_PRODUCT, vectors[idx0], vectors[idx1], correction, offsets[idx0], offsets[idx1]);
-                    scorer = factory.getScalarQuantizedVectorScorer(dims, size, correction, MAXIMUM_INNER_PRODUCT, in).get();
+                    scorer = factory.getInt7ScalarQuantizedVectorScorer(dims, size, correction, MAXIMUM_INNER_PRODUCT, in).get();
                     assertThat(scorer.score(idx0, idx1), equalTo(expected));
                     assertThat((new VectorScorerSupplierAdapter(scorer)).scorer(idx0).score(idx1), equalTo(expected));
                     // cosine
                     expected = luceneScore(COSINE, vectors[idx0], vectors[idx1], correction, offsets[idx0], offsets[idx1]);
-                    scorer = factory.getScalarQuantizedVectorScorer(dims, size, correction, COSINE, in).get();
+                    scorer = factory.getInt7ScalarQuantizedVectorScorer(dims, size, correction, COSINE, in).get();
                     assertThat(scorer.score(idx0, idx1), equalTo(expected));
                     assertThat((new VectorScorerSupplierAdapter(scorer)).scorer(idx0).score(idx1), equalTo(expected));
                     // euclidean
                     expected = luceneScore(EUCLIDEAN, vectors[idx0], vectors[idx1], correction, offsets[idx0], offsets[idx1]);
-                    scorer = factory.getScalarQuantizedVectorScorer(dims, size, correction, EUCLIDEAN, in).get();
+                    scorer = factory.getInt7ScalarQuantizedVectorScorer(dims, size, correction, EUCLIDEAN, in).get();
                     assertThat(scorer.score(idx0, idx1), equalTo(expected));
                     assertThat((new VectorScorerSupplierAdapter(scorer)).scorer(idx0).score(idx1), equalTo(expected));
                 }
@@ -164,7 +167,7 @@ public class VectorScorerFactoryTests extends AbstractVectorTestCase {
 
     public void testRandomSlice() throws IOException {
         assumeTrue(notSupportedMsg(), supported());
-        testRandomSliceImpl(30, 64, 1, ESTestCase::randomByteArrayOfLength);
+        testRandomSliceImpl(30, 64, 1, BYTE_ARRAY_RANDOM_INT7_FUNC);
     }
 
     void testRandomSliceImpl(int dims, long maxChunkSize, int initialPadding, Function<Integer, byte[]> byteArraySupplier)
@@ -200,22 +203,22 @@ public class VectorScorerFactoryTests extends AbstractVectorTestCase {
                     int idx1 = randomIntBetween(0, size - 1); // may be the same as idx0 - which is ok.
                     // dot product
                     float expected = luceneScore(DOT_PRODUCT, vectors[idx0], vectors[idx1], correction, offsets[idx0], offsets[idx1]);
-                    var scorer = factory.getScalarQuantizedVectorScorer(dims, size, correction, DOT_PRODUCT, in).get();
+                    var scorer = factory.getInt7ScalarQuantizedVectorScorer(dims, size, correction, DOT_PRODUCT, in).get();
                     assertThat(scorer.score(idx0, idx1), equalTo(expected));
                     assertThat((new VectorScorerSupplierAdapter(scorer)).scorer(idx0).score(idx1), equalTo(expected));
                     // max inner product
                     expected = luceneScore(MAXIMUM_INNER_PRODUCT, vectors[idx0], vectors[idx1], correction, offsets[idx0], offsets[idx1]);
-                    scorer = factory.getScalarQuantizedVectorScorer(dims, size, correction, MAXIMUM_INNER_PRODUCT, in).get();
+                    scorer = factory.getInt7ScalarQuantizedVectorScorer(dims, size, correction, MAXIMUM_INNER_PRODUCT, in).get();
                     assertThat(scorer.score(idx0, idx1), equalTo(expected));
                     assertThat((new VectorScorerSupplierAdapter(scorer)).scorer(idx0).score(idx1), equalTo(expected));
                     // cosine
                     expected = luceneScore(COSINE, vectors[idx0], vectors[idx1], correction, offsets[idx0], offsets[idx1]);
-                    scorer = factory.getScalarQuantizedVectorScorer(dims, size, correction, COSINE, in).get();
+                    scorer = factory.getInt7ScalarQuantizedVectorScorer(dims, size, correction, COSINE, in).get();
                     assertThat(scorer.score(idx0, idx1), equalTo(expected));
                     assertThat((new VectorScorerSupplierAdapter(scorer)).scorer(idx0).score(idx1), equalTo(expected));
                     // euclidean
                     expected = luceneScore(EUCLIDEAN, vectors[idx0], vectors[idx1], correction, offsets[idx0], offsets[idx1]);
-                    scorer = factory.getScalarQuantizedVectorScorer(dims, size, correction, EUCLIDEAN, in).get();
+                    scorer = factory.getInt7ScalarQuantizedVectorScorer(dims, size, correction, EUCLIDEAN, in).get();
                     assertThat(scorer.score(idx0, idx1), equalTo(expected));
                     assertThat((new VectorScorerSupplierAdapter(scorer)).scorer(idx0).score(idx1), equalTo(expected));
                 }
@@ -223,15 +226,21 @@ public class VectorScorerFactoryTests extends AbstractVectorTestCase {
         }
     }
 
-    static Function<Integer, byte[]> BYTE_ARRAY_MAX_FUNC = size -> {
+    static Function<Integer, byte[]> BYTE_ARRAY_RANDOM_INT7_FUNC = size -> {
+        byte[] ba = new byte[size];
+        randomBytesBetween(ba, MIN_INT7_VALUE, MAX_INT7_VALUE);
+        return ba;
+    };
+
+    static Function<Integer, byte[]> BYTE_ARRAY_MAX_INT7_FUNC = size -> {
         byte[] ba = new byte[size];
-        Arrays.fill(ba, Byte.MAX_VALUE);
+        Arrays.fill(ba, MAX_INT7_VALUE);
         return ba;
     };
 
-    static Function<Integer, byte[]> BYTE_ARRAY_MIN_FUNC = size -> {
+    static Function<Integer, byte[]> BYTE_ARRAY_MIN_INT7_FUNC = size -> {
         byte[] ba = new byte[size];
-        Arrays.fill(ba, Byte.MIN_VALUE);
+        Arrays.fill(ba, MIN_INT7_VALUE);
         return ba;
     };
 

+ 1 - 1
server/src/main/java/org/elasticsearch/index/codec/vectors/ES814ScalarQuantizedVectorsWriter.java

@@ -430,7 +430,7 @@ public final class ES814ScalarQuantizedVectorsWriter extends FlatVectorsWriter {
             Optional<VectorScorerFactory> factory = VectorScorerFactory.instance();
             if (factory.isPresent()) {
                 var scorer = factory.get()
-                    .getScalarQuantizedVectorScorer(
+                    .getInt7ScalarQuantizedVectorScorer(
                         byteVectorValues.dimension(),
                         docsWithField.cardinality(),
                         mergedQuantizationState.getConstantMultiplier(),

+ 10 - 0
test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java

@@ -885,6 +885,16 @@ public abstract class ESTestCase extends LuceneTestCase {
         return bytes;
     }
 
+    public static byte randomByteBetween(byte minInclusive, byte maxInclusive) {
+        return (byte) randomIntBetween(minInclusive, maxInclusive);
+    }
+
+    public static void randomBytesBetween(byte[] bytes, byte minInclusive, byte maxInclusive) {
+        for (int i = 0, len = bytes.length; i < len;) {
+            bytes[i++] = randomByteBetween(minInclusive, maxInclusive);
+        }
+    }
+
     public static BytesReference randomBytesReference(int length) {
         final var slices = new ArrayList<BytesReference>();
         var remaining = length;