Add a preprocessor in Turbo (#548)

Co-authored-by: ray <rui.xing@alibaba-inc.com>
This commit is contained in:
Zhuanglin Zheng 2026-07-24 14:01:52 +08:00 committed by GitHub
parent 764754427c
commit a4347e31f1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 2210 additions and 39 deletions

4
.gitignore vendored
View File

@ -60,9 +60,9 @@ allure-*
!build_android.sh !build_android.sh
!build_ios.sh !build_ios.sh
# congfig # config
doc/ doc/
config/ config/
examples/python/ examples/python/
examples/c_api/ examples/c_api/
logs/ logs/

View File

@ -23,6 +23,13 @@
#include <cpuid.h> #include <cpuid.h>
#endif #endif
#if defined(__aarch64__) && defined(__linux__)
#include <sys/auxv.h>
#ifndef HWCAP_ASIMD
#define HWCAP_ASIMD (1 << 1)
#endif
#endif
namespace zvec { namespace zvec {
namespace ailego { namespace ailego {
namespace internal { namespace internal {
@ -336,6 +343,17 @@ bool CpuFeatures::HYPERVISOR(void) {
return !!(flags_.L1_ECX & (1u << 31)); return !!(flags_.L1_ECX & (1u << 31));
} }
//! ARM NEON (ASIMD) support
bool CpuFeatures::NEON(void) {
#if defined(__aarch64__) && defined(__linux__)
return !!(getauxval(AT_HWCAP) & HWCAP_ASIMD);
#elif defined(__ARM_NEON)
return true;
#else
return false;
#endif
}
const char *CpuFeatures::Intrinsics(void) { const char *CpuFeatures::Intrinsics(void) {
return "" return ""
#if defined(__ARM_NEON) #if defined(__ARM_NEON)

View File

@ -182,6 +182,9 @@ class CpuFeatures {
// Running on a hypervisor // Running on a hypervisor
static bool HYPERVISOR(void); static bool HYPERVISOR(void);
//! ARM NEON (ASIMD) support
static bool NEON(void);
//! Intrinsics of compiling //! Intrinsics of compiling
static const char *Intrinsics(void); static const char *Intrinsics(void);
@ -361,6 +364,9 @@ class CpuFeatures {
// Running on a hypervisor // Running on a hypervisor
bool HYPERVISOR = CpuFeatures::HYPERVISOR(); bool HYPERVISOR = CpuFeatures::HYPERVISOR();
//! ARM NEON (ASIMD) support
bool NEON = CpuFeatures::NEON();
}; };
static StaticFlags static_flags_; static StaticFlags static_flags_;
}; };

View File

@ -13,11 +13,35 @@
// limitations under the License. // limitations under the License.
#pragma once #pragma once
#include <cstddef>
#include <cstdint>
#include <functional> #include <functional>
#include <zvec/ailego/math_batch/utils.h> #include <zvec/ailego/math_batch/utils.h>
namespace zvec::turbo { namespace zvec::turbo {
//! Error code literals mirroring core::IndexError::Code integer values.
//!
//! Turbo quantizer sources use these directly instead of the
//! `IndexError_NotImplemented` / `IndexError_Unsupported` const objects
//! because MSVC's WINDOWS_EXPORT_ALL_SYMBOLS does not export const data
//! with constructors from zvec_shared.dll. zvec_turbo is a static library
//! linked with /WHOLEARCHIVE, so referencing those unexported symbols across
//! the DLL boundary triggers LNK2019 on Windows.
//!
//! IndexError::Code stores -val in its constructor, so NotImplemented(11)
//! yields -11 and Unsupported(12) yields -12.
constexpr int kErrRuntime = -1;
constexpr int kErrNotImplemented = -11;
constexpr int kErrUnsupported = -12;
constexpr int kErrInvalidArgument = -31;
//! Magic number ('QTZR') stamped at the start of a serialized quantizer blob.
constexpr uint32_t kQuantizerMagic = 0x52545A51u;
//! Current quantizer serialization format version.
constexpr uint16_t kQuantizerSerVersion = 1;
using DistanceFunc = using DistanceFunc =
std::function<void(const void *m, const void *q, size_t dim, float *out)>; std::function<void(const void *m, const void *q, size_t dim, float *out)>;
using BatchDistanceFunc = std::function<void( using BatchDistanceFunc = std::function<void(
@ -33,6 +57,19 @@ using QueryPreprocessFunc =
using UniformQuantizeFunc = void (*)(const float *in, size_t dim, float scale, using UniformQuantizeFunc = void (*)(const float *in, size_t dim, float scale,
float bias, int8_t *out); float bias, int8_t *out);
// Generic rotate / unrotate function pointer types.
// ctx is an opaque context (e.g. FhtCtx*) managed by the caller.
using RotateFunc = void (*)(const float *in, float *out, size_t in_dim,
size_t out_dim, void *ctx);
using UnrotateFunc = void (*)(const float *in, float *out, size_t in_dim,
size_t out_dim, void *ctx);
// ISA-dispatched rotate/unrotate kernels.
struct RotatorKernels {
RotateFunc rotate = nullptr;
UnrotateFunc unrotate = nullptr;
};
enum class MetricType { enum class MetricType {
kSquaredEuclidean, kSquaredEuclidean,
kCosine, kCosine,
@ -59,6 +96,10 @@ enum class QuantizeType {
kRabit kRabit
}; };
enum class RotateType : uint16_t {
kFht = 1, //!< O(d log d) FHT-based Kac random rotation
};
enum class CpuArchType { enum class CpuArchType {
kAuto, kAuto,
kScalar, kScalar,
@ -95,4 +136,8 @@ QueryPreprocessFunc get_query_preprocess_func(
// interface can grow to cover other output types (e.g. fp16) in the future. // interface can grow to cover other output types (e.g. fp16) in the future.
UniformQuantizeFunc get_uniform_quantize_func(DataType data_type); UniformQuantizeFunc get_uniform_quantize_func(DataType data_type);
// Returns rotator kernels dispatched for the current CPU.
RotatorKernels get_rotator_kernels(
RotateType rotate_type, CpuArchType cpu_arch_type = CpuArchType::kAuto);
} // namespace zvec::turbo } // namespace zvec::turbo

View File

@ -5,23 +5,40 @@ if(NOT ANDROID AND AUTO_DETECT_ARCH)
if (HOST_ARCH MATCHES "^(x86|x64)$") if (HOST_ARCH MATCHES "^(x86|x64)$")
setup_compiler_march_for_x86(TURBO_MARCH_FLAG_SSE TURBO_MARCH_FLAG_AVX2 TURBO_MARCH_FLAG_AVX512 TURBO_MARCH_FLAG_AVX512FP16) setup_compiler_march_for_x86(TURBO_MARCH_FLAG_SSE TURBO_MARCH_FLAG_AVX2 TURBO_MARCH_FLAG_AVX512 TURBO_MARCH_FLAG_AVX512FP16)
elseif (HOST_ARCH MATCHES "^(arm|arm64)$") elseif (HOST_ARCH MATCHES "^(arm|arm64)$")
# ARM64 architecture - no special march flags needed for now # ARM64 architecture - NEON is enabled by default on aarch64,
# NEON implementations can be added here if needed # no special march flags needed.
message(STATUS "turbo: ARM64 detected, skipping x86-specific optimizations") message(STATUS "turbo: ARM64 detected, NEON enabled by default")
endif() endif()
endif() endif()
file(GLOB_RECURSE ALL_SRCS *.cc *.c *.h) file(GLOB_RECURSE ALL_SRCS *.cc *.c *.h)
# Set per-file compile flags for AVX512-VNNI sources. # Set per-file compile flags for SIMD sources.
# set_source_files_properties is directory-scoped, so it must be called in the # set_source_files_properties is directory-scoped, so it must be called in the
# same directory that adds the sources to a target (i.e. here, not in a # same directory that adds the sources to a target (i.e. here, not in a
# subdirectory). # subdirectory).
if(NOT ANDROID AND AUTO_DETECT_ARCH) if(NOT ANDROID AND AUTO_DETECT_ARCH)
if (HOST_ARCH MATCHES "^(x86|x64)$") if (HOST_ARCH MATCHES "^(x86|x64)$")
file(GLOB_RECURSE AVX512_VNNI_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/distance/avx512_vnni/*.cc) # SSE
file(GLOB_RECURSE SSE_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/distance/sse/*.cc)
set_source_files_properties( set_source_files_properties(
${AVX512_VNNI_SRCS} ${SSE_SRCS}
PROPERTIES
COMPILE_FLAGS "${TURBO_MARCH_FLAG_SSE}"
)
# AVX2
file(GLOB_RECURSE AVX2_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/distance/avx2/*.cc)
set_source_files_properties(
${AVX2_SRCS}
PROPERTIES
COMPILE_FLAGS "${TURBO_MARCH_FLAG_AVX2}"
)
# AVX512 (avx512_vnni and avx512/fht both use AVX512 march)
file(GLOB_RECURSE AVX512_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/distance/avx512_vnni/*.cc ${CMAKE_CURRENT_SOURCE_DIR}/distance/avx512/*.cc)
set_source_files_properties(
${AVX512_SRCS}
PROPERTIES PROPERTIES
COMPILE_FLAGS "${TURBO_MARCH_FLAG_AVX512}" COMPILE_FLAGS "${TURBO_MARCH_FLAG_AVX512}"
) )

View File

@ -0,0 +1,198 @@
// Copyright 2025-present the zvec project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// This file is compiled with per-file -march=core-avx2 (set in CMakeLists.txt)
// so that AVX2 intrinsics are available. When the build toolchain cannot emit
// AVX2 code, each function falls back to a no-op stub guarded by
// #if defined(__AVX2__).
#include "fht.h"
#if defined(__AVX2__)
#include <immintrin.h>
#endif
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include "common/fht_common.h"
namespace zvec::turbo::avx2 {
void fht_flip_sign_avx2(const uint8_t *flip, float *data, size_t dim) {
#if defined(__AVX2__)
size_t simd_end = dim & ~31u;
constexpr size_t kChunk = 32;
const __m256i bit_select =
_mm256_setr_epi32(0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80);
const __m256 sign_flip = _mm256_castsi256_ps(_mm256_set1_epi32(0x80000000));
for (size_t i = 0; i < simd_end; i += kChunk) {
uint32_t mask_bits;
std::memcpy(&mask_bits, &flip[i / 8], sizeof(mask_bits));
for (int b = 0; b < 4; ++b) {
__m256i mb = _mm256_set1_epi32((mask_bits >> (b * 8)) & 0xFF);
__m256i test = _mm256_and_si256(mb, bit_select);
__m256i cmp = _mm256_cmpeq_epi32(test, bit_select);
__m256 xor_mask = _mm256_and_ps(_mm256_castsi256_ps(cmp), sign_flip);
__m256 v = _mm256_loadu_ps(&data[i + b * 8]);
v = _mm256_xor_ps(v, xor_mask);
_mm256_storeu_ps(&data[i + b * 8], v);
}
}
// Scalar tail
for (size_t i = simd_end; i < dim; ++i) {
if (flip[i / 8] & (1u << (i % 8))) {
data[i] = -data[i];
}
}
#else
(void)flip;
(void)data;
(void)dim;
#endif
}
void fht_kacs_walk_avx2(float *data, size_t len) {
#if defined(__AVX2__)
size_t half = len / 2;
size_t base = len % 2;
size_t offset = base + half;
size_t half_end = half & ~7u;
for (size_t i = 0; i < half_end; i += 8) {
__m256 x = _mm256_loadu_ps(&data[i]);
__m256 y = _mm256_loadu_ps(&data[i + offset]);
_mm256_storeu_ps(&data[i], _mm256_add_ps(x, y));
_mm256_storeu_ps(&data[i + offset], _mm256_sub_ps(x, y));
}
// Scalar tail
for (size_t i = half_end; i < half; ++i) {
float x = data[i];
float y = data[i + offset];
data[i] = x + y;
data[i + offset] = x - y;
}
if (base != 0) {
data[half] *= std::sqrt(2.0f);
}
#else
(void)data;
(void)len;
#endif
}
void fht_inv_kacs_walk_avx2(float *data, size_t len) {
#if defined(__AVX2__)
size_t half = len / 2;
size_t base = len % 2;
size_t offset = base + half;
if (base != 0) {
data[half] *= std::sqrt(0.5f);
}
size_t half_end = half & ~7u;
const __m256 half_fac = _mm256_set1_ps(0.5f);
for (size_t i = 0; i < half_end; i += 8) {
__m256 a = _mm256_loadu_ps(&data[i]);
__m256 b = _mm256_loadu_ps(&data[i + offset]);
_mm256_storeu_ps(&data[i], _mm256_mul_ps(_mm256_add_ps(a, b), half_fac));
_mm256_storeu_ps(&data[i + offset],
_mm256_mul_ps(_mm256_sub_ps(a, b), half_fac));
}
// Scalar tail
for (size_t i = half_end; i < half; ++i) {
float a = data[i];
float b = data[i + offset];
data[i] = (a + b) * 0.5f;
data[i + offset] = (a - b) * 0.5f;
}
#else
(void)data;
(void)len;
#endif
}
void fht_inplace_avx2(float *data, size_t n) {
#if defined(__AVX2__)
for (size_t len = 1; len < n; len <<= 1) {
size_t step = len << 1;
size_t simd_end = len & ~7u;
for (size_t i = 0; i < n; i += step) {
for (size_t j = 0; j < simd_end; j += 8) {
__m256 u = _mm256_loadu_ps(&data[i + j]);
__m256 v = _mm256_loadu_ps(&data[i + j + len]);
_mm256_storeu_ps(&data[i + j], _mm256_add_ps(u, v));
_mm256_storeu_ps(&data[i + j + len], _mm256_sub_ps(u, v));
}
for (size_t j = simd_end; j < len; ++j) {
float u = data[i + j];
float v = data[i + j + len];
data[i + j] = u + v;
data[i + j + len] = u - v;
}
}
}
#else
(void)data;
(void)n;
#endif
}
void fht_vec_rescale_avx2(float *data, size_t n, float factor) {
#if defined(__AVX2__)
const __m256 fac = _mm256_set1_ps(factor);
size_t simd_end = n & ~7u;
for (size_t i = 0; i < simd_end; i += 8) {
__m256 v = _mm256_loadu_ps(&data[i]);
_mm256_storeu_ps(&data[i], _mm256_mul_ps(v, fac));
}
// Scalar tail
for (size_t i = simd_end; i < n; ++i) {
data[i] *= factor;
}
#else
(void)data;
(void)n;
(void)factor;
#endif
}
void fht_rotate_avx2(const float *in, float *out, size_t in_dim,
size_t /*out_dim*/, void *ctx) {
#if defined(__AVX2__)
static constexpr FhtPrimitives kPrim = {
fht_flip_sign_avx2, fht_inplace_avx2, fht_kacs_walk_avx2,
fht_inv_kacs_walk_avx2, fht_vec_rescale_avx2};
fht_rotate_impl(in, out, in_dim, ctx, kPrim);
#else
(void)in;
(void)out;
(void)in_dim;
(void)ctx;
#endif
}
void fht_unrotate_avx2(const float *in, float *out, size_t in_dim,
size_t /*out_dim*/, void *ctx) {
#if defined(__AVX2__)
static constexpr FhtPrimitives kPrim = {
fht_flip_sign_avx2, fht_inplace_avx2, fht_kacs_walk_avx2,
fht_inv_kacs_walk_avx2, fht_vec_rescale_avx2};
fht_unrotate_impl(in, out, in_dim, ctx, kPrim);
#else
(void)in;
(void)out;
(void)in_dim;
(void)ctx;
#endif
}
} // namespace zvec::turbo::avx2

View File

@ -0,0 +1,45 @@
// Copyright 2025-present the zvec project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#include <cstddef>
#include <cstdint>
namespace zvec::turbo::avx2 {
//! Apply bitwise sign-flip mask to a float vector (AVX2).
void fht_flip_sign_avx2(const uint8_t *flip, float *data, size_t dim);
//! Apply KacsWalk butterfly operation (AVX2).
void fht_kacs_walk_avx2(float *data, size_t len);
//! Inverse KacsWalk butterfly operation (AVX2).
void fht_inv_kacs_walk_avx2(float *data, size_t len);
//! In-place Fast Hadamard Transform (AVX2, n must be power-of-2).
void fht_inplace_avx2(float *data, size_t n);
//! Element-wise rescale: data[i] *= factor (AVX2).
void fht_vec_rescale_avx2(float *data, size_t n, float factor);
//! Forward FHT rotation (AVX2).
void fht_rotate_avx2(const float *in, float *out, size_t in_dim, size_t out_dim,
void *ctx);
//! Inverse FHT rotation (AVX2).
void fht_unrotate_avx2(const float *in, float *out, size_t in_dim,
size_t out_dim, void *ctx);
} // namespace zvec::turbo::avx2

View File

@ -0,0 +1,206 @@
// Copyright 2025-present the zvec project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// This file is compiled with per-file -march=icelake-server (set in
// CMakeLists.txt) so that AVX512 intrinsics are available. When the build
// toolchain cannot emit AVX-512 code, each function falls back to a no-op
// stub guarded by #if defined(__AVX512F__).
#include "fht.h"
#if defined(__AVX512F__)
#include <immintrin.h>
#endif
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include "common/fht_common.h"
namespace zvec::turbo::avx512 {
void fht_flip_sign_avx512(const uint8_t *flip, float *data, size_t dim) {
#if defined(__AVX512F__)
size_t simd_end = dim & ~63u;
constexpr size_t kChunk = 64;
// Sign-flip is a pure bitwise op (x ^ 0x80000000), done in the integer
// domain so only AVX512F is required. The float-domain VXORPS (zmm) would
// pull in an AVX512DQ dependency, which we deliberately avoid here.
const __m512i sign_bit = _mm512_set1_epi32(0x80000000);
for (size_t i = 0; i < simd_end; i += kChunk) {
uint64_t mask_bits;
std::memcpy(&mask_bits, &flip[i / 8], sizeof(mask_bits));
const __mmask16 m0 = _cvtu32_mask16(mask_bits & 0xFFFF);
const __mmask16 m1 = _cvtu32_mask16((mask_bits >> 16) & 0xFFFF);
const __mmask16 m2 = _cvtu32_mask16((mask_bits >> 32) & 0xFFFF);
const __mmask16 m3 = _cvtu32_mask16((mask_bits >> 48) & 0xFFFF);
__m512i v0 = _mm512_castps_si512(_mm512_loadu_ps(&data[i]));
v0 = _mm512_mask_xor_epi32(v0, m0, v0, sign_bit);
_mm512_storeu_ps(&data[i], _mm512_castsi512_ps(v0));
__m512i v1 = _mm512_castps_si512(_mm512_loadu_ps(&data[i + 16]));
v1 = _mm512_mask_xor_epi32(v1, m1, v1, sign_bit);
_mm512_storeu_ps(&data[i + 16], _mm512_castsi512_ps(v1));
__m512i v2 = _mm512_castps_si512(_mm512_loadu_ps(&data[i + 32]));
v2 = _mm512_mask_xor_epi32(v2, m2, v2, sign_bit);
_mm512_storeu_ps(&data[i + 32], _mm512_castsi512_ps(v2));
__m512i v3 = _mm512_castps_si512(_mm512_loadu_ps(&data[i + 48]));
v3 = _mm512_mask_xor_epi32(v3, m3, v3, sign_bit);
_mm512_storeu_ps(&data[i + 48], _mm512_castsi512_ps(v3));
}
// Scalar tail
for (size_t i = simd_end; i < dim; ++i) {
if (flip[i / 8] & (1u << (i % 8))) {
data[i] = -data[i];
}
}
#else
(void)flip;
(void)data;
(void)dim;
#endif
}
void fht_kacs_walk_avx512(float *data, size_t len) {
#if defined(__AVX512F__)
size_t half = len / 2;
size_t base = len % 2;
size_t offset = base + half;
size_t half_end = half & ~15u;
for (size_t i = 0; i < half_end; i += 16) {
__m512 x = _mm512_loadu_ps(&data[i]);
__m512 y = _mm512_loadu_ps(&data[i + offset]);
_mm512_storeu_ps(&data[i], _mm512_add_ps(x, y));
_mm512_storeu_ps(&data[i + offset], _mm512_sub_ps(x, y));
}
// Scalar tail
for (size_t i = half_end; i < half; ++i) {
float x = data[i];
float y = data[i + offset];
data[i] = x + y;
data[i + offset] = x - y;
}
if (base != 0) {
data[half] *= std::sqrt(2.0f);
}
#else
(void)data;
(void)len;
#endif
}
void fht_inv_kacs_walk_avx512(float *data, size_t len) {
#if defined(__AVX512F__)
size_t half = len / 2;
size_t base = len % 2;
size_t offset = base + half;
if (base != 0) {
data[half] *= std::sqrt(0.5f);
}
size_t half_end = half & ~15u;
const __m512 half_fac = _mm512_set1_ps(0.5f);
for (size_t i = 0; i < half_end; i += 16) {
__m512 a = _mm512_loadu_ps(&data[i]);
__m512 b = _mm512_loadu_ps(&data[i + offset]);
_mm512_storeu_ps(&data[i], _mm512_mul_ps(_mm512_add_ps(a, b), half_fac));
_mm512_storeu_ps(&data[i + offset],
_mm512_mul_ps(_mm512_sub_ps(a, b), half_fac));
}
// Scalar tail
for (size_t i = half_end; i < half; ++i) {
float a = data[i];
float b = data[i + offset];
data[i] = (a + b) * 0.5f;
data[i + offset] = (a - b) * 0.5f;
}
#else
(void)data;
(void)len;
#endif
}
void fht_inplace_avx512(float *data, size_t n) {
#if defined(__AVX512F__)
for (size_t len = 1; len < n; len <<= 1) {
size_t step = len << 1;
size_t simd_end = len & ~15u;
for (size_t i = 0; i < n; i += step) {
for (size_t j = 0; j < simd_end; j += 16) {
__m512 u = _mm512_loadu_ps(&data[i + j]);
__m512 v = _mm512_loadu_ps(&data[i + j + len]);
_mm512_storeu_ps(&data[i + j], _mm512_add_ps(u, v));
_mm512_storeu_ps(&data[i + j + len], _mm512_sub_ps(u, v));
}
for (size_t j = simd_end; j < len; ++j) {
float u = data[i + j];
float v = data[i + j + len];
data[i + j] = u + v;
data[i + j + len] = u - v;
}
}
}
#else
(void)data;
(void)n;
#endif
}
void fht_vec_rescale_avx512(float *data, size_t n, float factor) {
#if defined(__AVX512F__)
const __m512 fac = _mm512_set1_ps(factor);
size_t simd_end = n & ~15u;
for (size_t i = 0; i < simd_end; i += 16) {
__m512 v = _mm512_loadu_ps(&data[i]);
_mm512_storeu_ps(&data[i], _mm512_mul_ps(v, fac));
}
// Scalar tail
for (size_t i = simd_end; i < n; ++i) {
data[i] *= factor;
}
#else
(void)data;
(void)n;
(void)factor;
#endif
}
void fht_rotate_avx512(const float *in, float *out, size_t in_dim,
size_t /*out_dim*/, void *ctx) {
#if defined(__AVX512F__)
static constexpr FhtPrimitives kPrim = {
fht_flip_sign_avx512, fht_inplace_avx512, fht_kacs_walk_avx512,
fht_inv_kacs_walk_avx512, fht_vec_rescale_avx512};
fht_rotate_impl(in, out, in_dim, ctx, kPrim);
#else
(void)in;
(void)out;
(void)in_dim;
(void)ctx;
#endif
}
void fht_unrotate_avx512(const float *in, float *out, size_t in_dim,
size_t /*out_dim*/, void *ctx) {
#if defined(__AVX512F__)
static constexpr FhtPrimitives kPrim = {
fht_flip_sign_avx512, fht_inplace_avx512, fht_kacs_walk_avx512,
fht_inv_kacs_walk_avx512, fht_vec_rescale_avx512};
fht_unrotate_impl(in, out, in_dim, ctx, kPrim);
#else
(void)in;
(void)out;
(void)in_dim;
(void)ctx;
#endif
}
} // namespace zvec::turbo::avx512

View File

@ -0,0 +1,45 @@
// Copyright 2025-present the zvec project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#include <cstddef>
#include <cstdint>
namespace zvec::turbo::avx512 {
//! Apply bitwise sign-flip mask to a float vector (AVX512).
void fht_flip_sign_avx512(const uint8_t *flip, float *data, size_t dim);
//! Apply KacsWalk butterfly operation (AVX512).
void fht_kacs_walk_avx512(float *data, size_t len);
//! Inverse KacsWalk butterfly operation (AVX512).
void fht_inv_kacs_walk_avx512(float *data, size_t len);
//! In-place Fast Hadamard Transform (AVX512, n must be power-of-2).
void fht_inplace_avx512(float *data, size_t n);
//! Element-wise rescale: data[i] *= factor (AVX512).
void fht_vec_rescale_avx512(float *data, size_t n, float factor);
//! Forward FHT rotation (AVX512).
void fht_rotate_avx512(const float *in, float *out, size_t in_dim,
size_t out_dim, void *ctx);
//! Inverse FHT rotation (AVX512).
void fht_unrotate_avx512(const float *in, float *out, size_t in_dim,
size_t out_dim, void *ctx);
} // namespace zvec::turbo::avx512

View File

@ -0,0 +1,133 @@
// Copyright 2025-present the zvec project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#include <cstddef>
#include <cstdint>
#include <cstring>
namespace zvec::turbo {
/// ISA-level FHT primitive function pointers.
/// Each ISA fills in its own SIMD-optimized (or scalar-fallback) functions.
struct FhtPrimitives {
void (*flip_sign)(const uint8_t *flip, float *data, size_t dim);
void (*inplace)(float *data, size_t n);
void (*kacs_walk)(float *data, size_t len);
void (*inv_kacs_walk)(float *data, size_t len);
void (*rescale)(float *data, size_t n, float factor);
};
/// FhtCtx memory layout (accessed by address, NOT by type):
/// offset 0: size_t flip_offset
/// offset 8: size_t trunc_dim
/// offset 16: float fac
/// offset 20: uint8_t pad[4]
/// offset 24: uint8_t flip[]
inline void fht_rotate_impl(const float *in, float *out, size_t dim, void *ctx,
const FhtPrimitives &p) {
if (out != in) {
std::memcpy(out, in, sizeof(float) * dim);
}
float *data = out;
auto *base = reinterpret_cast<const uint8_t *>(ctx);
const size_t flip_offset = *reinterpret_cast<const size_t *>(base);
const size_t trunc_dim = *reinterpret_cast<const size_t *>(base + 8);
const float fac = *reinterpret_cast<const float *>(base + 16);
const uint8_t *flip = base + 24;
if (trunc_dim == dim) {
for (size_t r = 0; r < 4; ++r) {
p.flip_sign(flip + r * flip_offset, data, dim);
p.inplace(data, trunc_dim);
p.rescale(data, trunc_dim, fac);
}
return;
}
size_t start = dim - trunc_dim;
float *trunc_ptr = data + start;
p.flip_sign(flip, data, dim);
p.inplace(data, trunc_dim);
p.rescale(data, trunc_dim, fac);
p.kacs_walk(data, dim);
p.flip_sign(flip + flip_offset, data, dim);
p.inplace(trunc_ptr, trunc_dim);
p.rescale(trunc_ptr, trunc_dim, fac);
p.kacs_walk(data, dim);
p.flip_sign(flip + 2 * flip_offset, data, dim);
p.inplace(data, trunc_dim);
p.rescale(data, trunc_dim, fac);
p.kacs_walk(data, dim);
p.flip_sign(flip + 3 * flip_offset, data, dim);
p.inplace(trunc_ptr, trunc_dim);
p.rescale(trunc_ptr, trunc_dim, fac);
p.kacs_walk(data, dim);
p.rescale(data, dim, 0.25f);
}
inline void fht_unrotate_impl(const float *in, float *out, size_t dim,
void *ctx, const FhtPrimitives &p) {
if (out != in) {
std::memcpy(out, in, sizeof(float) * dim);
}
float *data = out;
auto *base = reinterpret_cast<const uint8_t *>(ctx);
const size_t flip_offset = *reinterpret_cast<const size_t *>(base);
const size_t trunc_dim = *reinterpret_cast<const size_t *>(base + 8);
const float fac = *reinterpret_cast<const float *>(base + 16);
const uint8_t *flip = base + 24;
if (trunc_dim == dim) {
for (int round = 3; round >= 0; --round) {
p.inplace(data, trunc_dim);
p.rescale(data, trunc_dim, fac);
p.flip_sign(flip + static_cast<size_t>(round) * flip_offset, data, dim);
}
return;
}
p.rescale(data, dim, 4.0f);
size_t start = dim - trunc_dim;
float *trunc_ptr = data + start;
p.inv_kacs_walk(data, dim);
p.inplace(trunc_ptr, trunc_dim);
p.rescale(trunc_ptr, trunc_dim, fac);
p.flip_sign(flip + 3 * flip_offset, data, dim);
p.inv_kacs_walk(data, dim);
p.inplace(data, trunc_dim);
p.rescale(data, trunc_dim, fac);
p.flip_sign(flip + 2 * flip_offset, data, dim);
p.inv_kacs_walk(data, dim);
p.inplace(trunc_ptr, trunc_dim);
p.rescale(trunc_ptr, trunc_dim, fac);
p.flip_sign(flip + flip_offset, data, dim);
p.inv_kacs_walk(data, dim);
p.inplace(data, trunc_dim);
p.rescale(data, trunc_dim, fac);
p.flip_sign(flip, data, dim);
}
} // namespace zvec::turbo

View File

@ -0,0 +1,171 @@
// Copyright 2025-present the zvec project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "fht.h"
#if defined(__ARM_NEON) && defined(__aarch64__)
#include <arm_neon.h>
#endif
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include "common/fht_common.h"
#include "scalar/rotate/fht/fht.h"
namespace zvec::turbo::neon {
void fht_flip_sign_neon(const uint8_t *flip, float *data, size_t dim) {
#if defined(__ARM_NEON) && defined(__aarch64__)
const uint32x4_t sign_bit = vdupq_n_u32(0x80000000u);
size_t simd_end = dim & ~3u;
size_t flip_bytes = (dim + 7) / 8;
for (size_t i = 0; i < simd_end; i += 4) {
uint16_t bits16;
size_t byte_pos = i / 8;
if (byte_pos + 1 < flip_bytes) {
std::memcpy(&bits16, &flip[byte_pos], sizeof(bits16));
} else {
bits16 = flip[byte_pos];
}
bits16 >>= (i % 8);
uint32_t b0 = bits16 & 1u;
uint32_t b1 = (bits16 >> 1) & 1u;
uint32_t b2 = (bits16 >> 2) & 1u;
uint32_t b3 = (bits16 >> 3) & 1u;
uint32x4_t bit_mask = {b0, b1, b2, b3};
uint32x4_t sign_mask = vmulq_u32(bit_mask, sign_bit);
float32x4_t v = vld1q_f32(&data[i]);
v = vreinterpretq_f32_u32(veorq_u32(vreinterpretq_u32_f32(v), sign_mask));
vst1q_f32(&data[i], v);
}
// Scalar tail
for (size_t i = simd_end; i < dim; ++i) {
if (flip[i / 8] & (1u << (i % 8))) {
data[i] = -data[i];
}
}
#else
(void)flip;
(void)data;
(void)dim;
#endif
}
void fht_kacs_walk_neon(float *data, size_t len) {
#if defined(__ARM_NEON) && defined(__aarch64__)
size_t half = len / 2;
size_t base = len % 2;
size_t offset = base + half;
size_t half_end = half & ~3u;
for (size_t i = 0; i < half_end; i += 4) {
float32x4_t x = vld1q_f32(&data[i]);
float32x4_t y = vld1q_f32(&data[i + offset]);
vst1q_f32(&data[i], vaddq_f32(x, y));
vst1q_f32(&data[i + offset], vsubq_f32(x, y));
}
// Scalar tail
for (size_t i = half_end; i < half; ++i) {
float x = data[i];
float y = data[i + offset];
data[i] = x + y;
data[i + offset] = x - y;
}
if (base != 0) {
data[half] *= std::sqrt(2.0f);
}
#else
(void)data;
(void)len;
#endif
}
void fht_inv_kacs_walk_neon(float *data, size_t len) {
#if defined(__ARM_NEON) && defined(__aarch64__)
size_t half = len / 2;
size_t base = len % 2;
size_t offset = base + half;
if (base != 0) {
data[half] *= std::sqrt(0.5f);
}
size_t half_end = half & ~3u;
const float32x4_t half_fac = vdupq_n_f32(0.5f);
for (size_t i = 0; i < half_end; i += 4) {
float32x4_t a = vld1q_f32(&data[i]);
float32x4_t b = vld1q_f32(&data[i + offset]);
vst1q_f32(&data[i], vmulq_f32(vaddq_f32(a, b), half_fac));
vst1q_f32(&data[i + offset], vmulq_f32(vsubq_f32(a, b), half_fac));
}
// Scalar tail
for (size_t i = half_end; i < half; ++i) {
float a = data[i];
float b = data[i + offset];
data[i] = (a + b) * 0.5f;
data[i + offset] = (a - b) * 0.5f;
}
#else
(void)data;
(void)len;
#endif
}
void fht_vec_rescale_neon(float *data, size_t n, float factor) {
#if defined(__ARM_NEON) && defined(__aarch64__)
const float32x4_t fac = vdupq_n_f32(factor);
size_t simd_end = n & ~3u;
for (size_t i = 0; i < simd_end; i += 4) {
float32x4_t v = vld1q_f32(&data[i]);
vst1q_f32(&data[i], vmulq_f32(v, fac));
}
// Scalar tail
for (size_t i = simd_end; i < n; ++i) {
data[i] *= factor;
}
#else
(void)data;
(void)n;
(void)factor;
#endif
}
void fht_rotate_neon(const float *in, float *out, size_t in_dim,
size_t /*out_dim*/, void *ctx) {
#if defined(__ARM_NEON) && defined(__aarch64__)
static constexpr FhtPrimitives kPrim = {
fht_flip_sign_neon, scalar::fht_inplace, fht_kacs_walk_neon,
fht_inv_kacs_walk_neon, fht_vec_rescale_neon};
fht_rotate_impl(in, out, in_dim, ctx, kPrim);
#else
(void)in;
(void)out;
(void)in_dim;
(void)ctx;
#endif
}
void fht_unrotate_neon(const float *in, float *out, size_t in_dim,
size_t /*out_dim*/, void *ctx) {
#if defined(__ARM_NEON) && defined(__aarch64__)
static constexpr FhtPrimitives kPrim = {
fht_flip_sign_neon, scalar::fht_inplace, fht_kacs_walk_neon,
fht_inv_kacs_walk_neon, fht_vec_rescale_neon};
fht_unrotate_impl(in, out, in_dim, ctx, kPrim);
#else
(void)in;
(void)out;
(void)in_dim;
(void)ctx;
#endif
}
} // namespace zvec::turbo::neon

View File

@ -0,0 +1,42 @@
// Copyright 2025-present the zvec project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#include <cstddef>
#include <cstdint>
namespace zvec::turbo::neon {
//! Apply bitwise sign-flip mask to a float vector (NEON).
void fht_flip_sign_neon(const uint8_t *flip, float *data, size_t dim);
//! Apply KacsWalk butterfly operation (NEON).
void fht_kacs_walk_neon(float *data, size_t len);
//! Inverse KacsWalk butterfly operation (NEON).
void fht_inv_kacs_walk_neon(float *data, size_t len);
//! Element-wise rescale: data[i] *= factor (NEON).
void fht_vec_rescale_neon(float *data, size_t n, float factor);
//! Forward FHT rotation (NEON). Inplace falls back to scalar.
void fht_rotate_neon(const float *in, float *out, size_t in_dim, size_t out_dim,
void *ctx);
//! Inverse FHT rotation (NEON). Inplace falls back to scalar.
void fht_unrotate_neon(const float *in, float *out, size_t in_dim,
size_t out_dim, void *ctx);
} // namespace zvec::turbo::neon

View File

@ -0,0 +1,96 @@
// Copyright 2025-present the zvec project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "fht.h"
#include <cmath>
#include <cstddef>
#include <cstdint>
#include "common/fht_common.h"
namespace zvec::turbo::scalar {
void fht_flip_sign(const uint8_t *flip, float *data, size_t dim) {
for (size_t i = 0; i < dim; ++i) {
if (flip[i / 8] & (1u << (i % 8))) {
data[i] = -data[i];
}
}
}
void fht_kacs_walk(float *data, size_t len) {
size_t half = len / 2;
size_t base = len % 2;
size_t offset = base + half;
for (size_t i = 0; i < half; ++i) {
float x = data[i];
float y = data[i + offset];
data[i] = x + y;
data[i + offset] = x - y;
}
if (base != 0) {
data[half] *= std::sqrt(2.0f);
}
}
void fht_inv_kacs_walk(float *data, size_t len) {
size_t half = len / 2;
size_t base = len % 2;
size_t offset = base + half;
if (base != 0) {
data[half] *= std::sqrt(0.5f);
}
for (size_t i = 0; i < half; ++i) {
float a = data[i];
float b = data[i + offset];
data[i] = (a + b) * 0.5f;
data[i + offset] = (a - b) * 0.5f;
}
}
void fht_inplace(float *data, size_t n) {
for (size_t len = 1; len < n; len <<= 1) {
for (size_t i = 0; i < n; i += len << 1) {
for (size_t j = i; j < i + len; ++j) {
float u = data[j];
float v = data[j + len];
data[j] = u + v;
data[j + len] = u - v;
}
}
}
}
void fht_vec_rescale(float *data, size_t n, float factor) {
for (size_t i = 0; i < n; ++i) {
data[i] *= factor;
}
}
void fht_rotate(const float *in, float *out, size_t in_dim, size_t /*out_dim*/,
void *ctx) {
static constexpr FhtPrimitives kPrim = {fht_flip_sign, fht_inplace,
fht_kacs_walk, fht_inv_kacs_walk,
fht_vec_rescale};
fht_rotate_impl(in, out, in_dim, ctx, kPrim);
}
void fht_unrotate(const float *in, float *out, size_t in_dim,
size_t /*out_dim*/, void *ctx) {
static constexpr FhtPrimitives kPrim = {fht_flip_sign, fht_inplace,
fht_kacs_walk, fht_inv_kacs_walk,
fht_vec_rescale};
fht_unrotate_impl(in, out, in_dim, ctx, kPrim);
}
} // namespace zvec::turbo::scalar

View File

@ -0,0 +1,47 @@
// Copyright 2025-present the zvec project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#include <cstddef>
#include <cstdint>
namespace zvec::turbo::scalar {
//! Apply bitwise sign-flip mask to a float vector.
//! Each bit in \p flip controls one element: bit set means negate.
void fht_flip_sign(const uint8_t *flip, float *data, size_t dim);
//! Apply KacsWalk butterfly operation to non-power-of-2 FHT.
void fht_kacs_walk(float *data, size_t len);
//! Inverse KacsWalk butterfly operation.
void fht_inv_kacs_walk(float *data, size_t len);
//! In-place Fast Hadamard Transform on \p n elements (must be power-of-2).
void fht_inplace(float *data, size_t n);
//! Element-wise rescale: data[i] *= factor.
void fht_vec_rescale(float *data, size_t n, float factor);
//! Forward FHT rotation (compose flip -> FHT -> rescale, 4 rounds).
//! ctx is a FhtCtx* defined in preprocessor/fht_rotator/fht_rotator.h.
void fht_rotate(const float *in, float *out, size_t in_dim, size_t out_dim,
void *ctx);
//! Inverse FHT rotation (undo 4 rounds in reverse order).
void fht_unrotate(const float *in, float *out, size_t in_dim, size_t out_dim,
void *ctx);
} // namespace zvec::turbo::scalar

View File

@ -0,0 +1,175 @@
// Copyright 2025-present the zvec project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// This file is compiled with per-file -march=corei7 (set in CMakeLists.txt)
// so that SSE2 intrinsics are available. When the build toolchain cannot emit
// SSE2 code, each function falls back to a no-op stub guarded by
// #if defined(__SSE2__).
#include "fht.h"
#if defined(__SSE2__)
#include <emmintrin.h>
#endif
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include "common/fht_common.h"
#include "scalar/rotate/fht/fht.h"
namespace zvec::turbo::sse {
void fht_flip_sign_sse(const uint8_t *flip, float *data, size_t dim) {
#if defined(__SSE2__)
size_t simd_end = dim & ~3u;
size_t flip_bytes = (dim + 7) / 8;
for (size_t i = 0; i < simd_end; i += 4) {
uint16_t bits16;
size_t byte_pos = i / 8;
if (byte_pos + 1 < flip_bytes) {
std::memcpy(&bits16, &flip[byte_pos], sizeof(bits16));
} else {
bits16 = flip[byte_pos];
}
bits16 >>= (i % 8);
uint32_t b0 = bits16 & 1u;
uint32_t b1 = (bits16 >> 1) & 1u;
uint32_t b2 = (bits16 >> 2) & 1u;
uint32_t b3 = (bits16 >> 3) & 1u;
__m128i bit_mask = _mm_set_epi32(b3, b2, b1, b0);
__m128i sign_mask = _mm_slli_epi32(bit_mask, 31);
__m128 v = _mm_loadu_ps(&data[i]);
v = _mm_xor_ps(v, _mm_castsi128_ps(sign_mask));
_mm_storeu_ps(&data[i], v);
}
// Scalar tail
for (size_t i = simd_end; i < dim; ++i) {
if (flip[i / 8] & (1u << (i % 8))) {
data[i] = -data[i];
}
}
#else
(void)flip;
(void)data;
(void)dim;
#endif
}
void fht_kacs_walk_sse(float *data, size_t len) {
#if defined(__SSE2__)
size_t half = len / 2;
size_t base = len % 2;
size_t offset = base + half;
size_t half_end = half & ~3u;
for (size_t i = 0; i < half_end; i += 4) {
__m128 x = _mm_loadu_ps(&data[i]);
__m128 y = _mm_loadu_ps(&data[i + offset]);
_mm_storeu_ps(&data[i], _mm_add_ps(x, y));
_mm_storeu_ps(&data[i + offset], _mm_sub_ps(x, y));
}
// Scalar tail
for (size_t i = half_end; i < half; ++i) {
float x = data[i];
float y = data[i + offset];
data[i] = x + y;
data[i + offset] = x - y;
}
if (base != 0) {
data[half] *= std::sqrt(2.0f);
}
#else
(void)data;
(void)len;
#endif
}
void fht_inv_kacs_walk_sse(float *data, size_t len) {
#if defined(__SSE2__)
size_t half = len / 2;
size_t base = len % 2;
size_t offset = base + half;
if (base != 0) {
data[half] *= std::sqrt(0.5f);
}
size_t half_end = half & ~3u;
const __m128 half_fac = _mm_set1_ps(0.5f);
for (size_t i = 0; i < half_end; i += 4) {
__m128 a = _mm_loadu_ps(&data[i]);
__m128 b = _mm_loadu_ps(&data[i + offset]);
_mm_storeu_ps(&data[i], _mm_mul_ps(_mm_add_ps(a, b), half_fac));
_mm_storeu_ps(&data[i + offset], _mm_mul_ps(_mm_sub_ps(a, b), half_fac));
}
// Scalar tail
for (size_t i = half_end; i < half; ++i) {
float a = data[i];
float b = data[i + offset];
data[i] = (a + b) * 0.5f;
data[i + offset] = (a - b) * 0.5f;
}
#else
(void)data;
(void)len;
#endif
}
void fht_vec_rescale_sse(float *data, size_t n, float factor) {
#if defined(__SSE2__)
const __m128 fac = _mm_set1_ps(factor);
size_t simd_end = n & ~3u;
for (size_t i = 0; i < simd_end; i += 4) {
__m128 v = _mm_loadu_ps(&data[i]);
_mm_storeu_ps(&data[i], _mm_mul_ps(v, fac));
}
// Scalar tail
for (size_t i = simd_end; i < n; ++i) {
data[i] *= factor;
}
#else
(void)data;
(void)n;
(void)factor;
#endif
}
void fht_rotate_sse(const float *in, float *out, size_t in_dim,
size_t /*out_dim*/, void *ctx) {
#if defined(__SSE2__)
static constexpr FhtPrimitives kPrim = {
fht_flip_sign_sse, scalar::fht_inplace, fht_kacs_walk_sse,
fht_inv_kacs_walk_sse, fht_vec_rescale_sse};
fht_rotate_impl(in, out, in_dim, ctx, kPrim);
#else
(void)in;
(void)out;
(void)in_dim;
(void)ctx;
#endif
}
void fht_unrotate_sse(const float *in, float *out, size_t in_dim,
size_t /*out_dim*/, void *ctx) {
#if defined(__SSE2__)
static constexpr FhtPrimitives kPrim = {
fht_flip_sign_sse, scalar::fht_inplace, fht_kacs_walk_sse,
fht_inv_kacs_walk_sse, fht_vec_rescale_sse};
fht_unrotate_impl(in, out, in_dim, ctx, kPrim);
#else
(void)in;
(void)out;
(void)in_dim;
(void)ctx;
#endif
}
} // namespace zvec::turbo::sse

View File

@ -0,0 +1,42 @@
// Copyright 2025-present the zvec project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#include <cstddef>
#include <cstdint>
namespace zvec::turbo::sse {
//! Apply bitwise sign-flip mask to a float vector (SSE).
void fht_flip_sign_sse(const uint8_t *flip, float *data, size_t dim);
//! Apply KacsWalk butterfly operation (SSE).
void fht_kacs_walk_sse(float *data, size_t len);
//! Inverse KacsWalk butterfly operation (SSE).
void fht_inv_kacs_walk_sse(float *data, size_t len);
//! Element-wise rescale: data[i] *= factor (SSE).
void fht_vec_rescale_sse(float *data, size_t n, float factor);
//! Forward FHT rotation (SSE). Inplace falls back to scalar.
void fht_rotate_sse(const float *in, float *out, size_t in_dim, size_t out_dim,
void *ctx);
//! Inverse FHT rotation (SSE). Inplace falls back to scalar.
void fht_unrotate_sse(const float *in, float *out, size_t in_dim,
size_t out_dim, void *ctx);
} // namespace zvec::turbo::sse

View File

@ -0,0 +1,217 @@
// Copyright 2025-present the zvec project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "fht_rotator.h"
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <limits>
#include <random>
namespace zvec {
namespace turbo {
// ============================================================================
// FhtRotator method implementations
// ============================================================================
size_t FhtRotator::floor_pow2(size_t n) {
if (n == 0) return 0;
size_t p = 1;
while (p * 2 <= n) p *= 2;
return p;
}
FhtRotator::~FhtRotator() {
std::free(fht_ctx_);
}
FhtRotator::Pointer FhtRotator::create(int dim) {
if (dim <= 0) return nullptr;
Pointer r(new FhtRotator());
r->in_dim_ = dim;
r->out_dim_ = dim;
r->flip_offset_ = (static_cast<size_t>(dim) + kByteLen - 1) / kByteLen;
r->kernels_ = get_rotator_kernels(RotateType::kFht);
const size_t trunc_dim = floor_pow2(static_cast<size_t>(dim));
const float fac = 1.0f / std::sqrt(static_cast<float>(trunc_dim));
const size_t flip_size = 4 * r->flip_offset_;
// Single allocation: FhtCtx header + trailing flip data.
r->fht_ctx_ = static_cast<FhtCtx *>(std::malloc(sizeof(FhtCtx) + flip_size));
if (r->fht_ctx_ == nullptr) {
// Allocation failed: bail out before dereferencing. Returning nullptr lets
// the smart Pointer clean up the partially-built object (~FhtRotator frees
// the null fht_ctx_ safely).
return nullptr;
}
r->fht_ctx_->flip_offset = r->flip_offset_;
r->fht_ctx_->trunc_dim = trunc_dim;
r->fht_ctx_->fac = fac;
// Generate 4 rounds of random flip-sign arrays.
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<int> dist(0, 255);
for (size_t i = 0; i < flip_size; ++i)
r->fht_ctx_->flip[i] = static_cast<uint8_t>(dist(gen));
return r;
}
FhtRotator::Pointer FhtRotator::from_blob(const void *data, size_t len) {
if (!data || len < sizeof(RotatorSerHeader)) return nullptr;
// Copy the header into a properly aligned local before reading any field.
// `data` may point to an unaligned byte buffer (e.g. std::string::data()),
// so dereferencing a reinterpret_cast<const RotatorSerHeader *> directly
// would be undefined behavior on architectures that require alignment.
RotatorSerHeader hdr;
std::memcpy(&hdr, data, sizeof(RotatorSerHeader));
if (hdr.magic != kRotatorMagic) return nullptr;
if (hdr.version != kRotatorSerVersion) return nullptr;
if (static_cast<RotateType>(hdr.rotator_type) != RotateType::kFht) {
return nullptr;
}
Pointer r(new FhtRotator());
const size_t expected_total =
sizeof(RotatorSerHeader) + static_cast<size_t>(hdr.payload_size);
if (len < expected_total) return nullptr;
int rc = r->deserialize(data, len);
if (rc != 0) return nullptr;
return r;
}
void FhtRotator::train(const void * /*data*/, size_t /*num*/,
size_t /*stride*/) {
// No-op: flip-sign arrays are generated in create().
}
// ---------------------------------------------------------------------------
// apply (forward rotation)
// ---------------------------------------------------------------------------
void FhtRotator::apply(const float *in, float *out) const {
kernels_.rotate(in, out, static_cast<size_t>(in_dim_),
static_cast<size_t>(out_dim_), static_cast<void *>(fht_ctx_));
}
// ---------------------------------------------------------------------------
// apply_inverse (inverse rotation)
// ---------------------------------------------------------------------------
void FhtRotator::apply_inverse(const float *in, float *out) const {
kernels_.unrotate(in, out, static_cast<size_t>(in_dim_),
static_cast<size_t>(out_dim_),
static_cast<void *>(fht_ctx_));
}
// ---------------------------------------------------------------------------
// serialize / deserialize
// ---------------------------------------------------------------------------
int FhtRotator::serialize(std::string *out) const {
if (!out) return kErrInvalidArgument;
if (!fht_ctx_) return kErrRuntime;
const size_t flip_size = 4 * flip_offset_;
RotatorSerHeader hdr{};
hdr.magic = kRotatorMagic;
hdr.version = kRotatorSerVersion;
hdr.rotator_type = static_cast<uint16_t>(RotateType::kFht);
hdr.in_dim = static_cast<uint32_t>(in_dim_);
hdr.out_dim = static_cast<uint32_t>(out_dim_);
hdr.payload_size = static_cast<uint32_t>(flip_size);
hdr.reserved = 0;
out->resize(sizeof(hdr) + flip_size);
std::memcpy(&(*out)[0], &hdr, sizeof(hdr));
std::memcpy(&(*out)[sizeof(hdr)], fht_ctx_->flip, flip_size);
return 0;
}
int FhtRotator::deserialize(const void *data, size_t len) {
if (!data || len < sizeof(RotatorSerHeader)) return kErrInvalidArgument;
// Copy the header into a properly aligned local before reading any field.
// `data` may point to an unaligned byte buffer (e.g. std::string::data()),
// so dereferencing a reinterpret_cast<const RotatorSerHeader *> directly
// would be undefined behavior on architectures that require alignment.
RotatorSerHeader hdr;
std::memcpy(&hdr, data, sizeof(RotatorSerHeader));
if (hdr.magic != kRotatorMagic) return kErrUnsupported;
if (hdr.version != kRotatorSerVersion) return kErrUnsupported;
if (static_cast<RotateType>(hdr.rotator_type) != RotateType::kFht) {
return kErrUnsupported;
}
// Validate dimensions before any cast to int: must be strictly positive and
// representable as int. FHT keeps dimensionality unchanged (in == out).
if (hdr.in_dim == 0 ||
hdr.in_dim > static_cast<uint32_t>(std::numeric_limits<int>::max())) {
return kErrInvalidArgument;
}
if (hdr.out_dim != hdr.in_dim) return kErrInvalidArgument;
// Length check via subtraction to avoid size_t overflow on 32-bit
// (len >= sizeof(header) was guaranteed above, so the subtraction is safe).
if (hdr.payload_size > len - sizeof(RotatorSerHeader)) {
return kErrInvalidArgument;
}
// Payload must hold exactly 4 rounds of ceil(in_dim/8) flip bytes. The
// rotation kernels read 4 * flip_offset bytes, so any smaller payload would
// read out of bounds; require an exact match.
const size_t new_flip_offset =
(static_cast<size_t>(hdr.in_dim) + kByteLen - 1) / kByteLen;
const size_t expected_flip_size = 4 * new_flip_offset;
if (hdr.payload_size != expected_flip_size) return kErrInvalidArgument;
// Build the new context into a temporary first, so a failed allocation
// leaves the existing object completely untouched (deserialize may target
// an already-initialized rotator). Only commit member state once the
// allocation and payload copy have both succeeded.
const int new_in_dim = static_cast<int>(hdr.in_dim);
const int new_out_dim = static_cast<int>(hdr.out_dim);
const size_t trunc_dim = floor_pow2(static_cast<size_t>(new_in_dim));
const float fac = 1.0f / std::sqrt(static_cast<float>(trunc_dim));
FhtCtx *new_ctx =
static_cast<FhtCtx *>(std::malloc(sizeof(FhtCtx) + expected_flip_size));
if (new_ctx == nullptr) return kErrRuntime;
new_ctx->flip_offset = new_flip_offset;
new_ctx->trunc_dim = trunc_dim;
new_ctx->fac = fac;
std::memcpy(new_ctx->flip,
reinterpret_cast<const char *>(data) + sizeof(RotatorSerHeader),
expected_flip_size);
// All allocations succeeded: commit new state and free the old context.
std::free(fht_ctx_);
fht_ctx_ = new_ctx;
in_dim_ = new_in_dim;
out_dim_ = new_out_dim;
flip_offset_ = new_flip_offset;
kernels_ = get_rotator_kernels(RotateType::kFht);
return 0;
}
} // namespace turbo
} // namespace zvec

View File

@ -0,0 +1,114 @@
// Copyright 2025-present the zvec project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#include <cstddef>
#include <cstdint>
#include <memory>
#include <string>
#include "preprocessor/preprocessor.h"
namespace zvec {
namespace turbo {
// FHT context passed to ISA-level rotate/unrotate via void*.
// Layout (ISA code accesses by address, NOT by type):
// offset 0: size_t flip_offset (bytes per round)
// offset 8: size_t trunc_dim (largest power-of-2 <= in_dim)
// offset 16: float fac (1 / sqrt(trunc_dim))
// offset 20: uint8_t pad[4] (explicit padding)
// offset 24: uint8_t flip[] (4 * flip_offset bytes, trailing data)
//
// Allocated as a single block: malloc(sizeof(FhtCtx) + 4 * flip_offset).
struct FhtCtx {
size_t flip_offset;
size_t trunc_dim;
float fac;
uint8_t pad_[4];
uint8_t flip[]; // trailing flexible array (C++ extension)
};
static_assert(offsetof(FhtCtx, flip) == 24, "FhtCtx flip offset must be 24");
static_assert(sizeof(FhtCtx) == 24, "FhtCtx sizeof must be 24");
// ============================================================================
// FhtRotator - O(d log d) FHT-based Kac random rotation
//
// Works with any dimension (non-power-of-2 uses trunc_dim + KacsWalk).
// When dimension is a power of 2, uses 4 rounds of (flip -> FHT -> rescale).
// When dimension is NOT a power of 2, uses kacs_walk reduction.
// ============================================================================
class FhtRotator : public Preprocessor {
public:
using Pointer = std::shared_ptr<FhtRotator>;
//! Create a fully-initialized rotator for \p in_dim dimensions.
//! Random flip-sign arrays are generated during creation; the returned
//! object is immediately usable for apply() / apply_inverse().
static Pointer create(int in_dim);
//! Create and restore a rotator from a serialized blob (reads the type from
//! the embedded RotatorSerHeader). Returns nullptr on malformed input.
static Pointer from_blob(const void *data, size_t len);
// -- Preprocessor interface ------------------------------------------------
int in_dim() const override {
return in_dim_;
}
int out_dim() const override {
return out_dim_;
}
void apply(const float *in, float *out) const override;
void apply_inverse(const float *in, float *out) const override;
//! No-op for FhtRotator. Flip-sign arrays are generated in create().
//! Provided for interface compatibility with the Preprocessor contract.
void train(const void *data, size_t num, size_t stride) override;
int serialize(std::string *out) const override;
int deserialize(const void *data, size_t len) override;
//! Rotator type tag (kFht = 1).
RotateType rotate_type() const {
return RotateType::kFht;
}
~FhtRotator() override;
private:
FhtRotator() = default;
//! Largest power of 2 <= dim.
static size_t floor_pow2(size_t n);
int in_dim_{0};
int out_dim_{0};
//! Bytes per round: ceil(in_dim / 8). Kept for serialization.
size_t flip_offset_{0};
//! ISA-dispatched rotate/unrotate kernels.
RotatorKernels kernels_{};
//! FHT state (flip_offset, trunc_dim, fac, flip[]) -- single allocation.
FhtCtx *fht_ctx_{nullptr};
static constexpr size_t kByteLen = 8;
};
} // namespace turbo
} // namespace zvec

View File

@ -0,0 +1,88 @@
// Copyright 2025-present the zvec project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#include <memory>
#include <string>
#include <zvec/turbo/turbo.h>
namespace zvec {
namespace turbo {
//! Magic number ('ROTR') stamped at the start of a serialized rotator blob.
constexpr uint32_t kRotatorMagic = 0x52544F52u;
//! Current rotator serialization format version.
constexpr uint16_t kRotatorSerVersion = 1;
//! Self-describing, fixed-size header that prefixes every serialized rotator.
//! The type-specific payload (flip signs, rotation matrix, ...) follows
//! immediately after this header.
struct RotatorSerHeader {
uint32_t magic; // kRotatorMagic
uint16_t version; // kRotatorSerVersion
uint16_t rotator_type; // RotateType
uint32_t in_dim; // input dimensionality
uint32_t out_dim; // output dimensionality
uint32_t payload_size; // bytes following the header
uint32_t reserved; // 0, for future use / alignment
};
static_assert(sizeof(RotatorSerHeader) == 24,
"RotatorSerHeader must be 24 bytes");
//! Abstract preprocessor interface.
//!
//! A Preprocessor applies a deterministic, invertible transform to each
//! vector (e.g. random rotation). Concrete subclasses (FhtRotator, ...)
//! implement the actual algorithm.
class Preprocessor {
public:
using Pointer = std::shared_ptr<Preprocessor>;
virtual ~Preprocessor() = default;
//! Input dimensionality accepted by apply().
virtual int in_dim() const = 0;
//! Output dimensionality produced by apply(). A future preprocessor may
//! change dimensionality (out_dim() != in_dim()), but FhtRotator keeps it
//! unchanged: it operates on floor_pow2(in_dim) via the Hadamard transform
//! plus a Kac's walk over the remainder, so out_dim() == in_dim().
virtual int out_dim() const = 0;
//! Forward transform: map an input vector to the preprocessed space.
//! \p out must hold at least out_dim() elements.
virtual void apply(const float *in, float *out) const = 0;
//! Inverse transform: recover the original-space vector from a preprocessed
//! one. \p out must hold at least in_dim() elements.
virtual void apply_inverse(const float *in, float *out) const = 0;
//! Fit / initialize the preprocessor from a contiguous batch of training
//! data. For FhtRotator this generates the random flip-sign arrays.
//! \p data pointer to the first element of the batch.
//! \p num number of vectors in the batch.
//! \p stride byte offset between consecutive vectors (0 => packed).
virtual void train(const void *data, size_t num, size_t stride) = 0;
//! Serialize the preprocessor into a self-contained blob
//! (RotatorSerHeader + payload).
virtual int serialize(std::string *out) const = 0;
//! Deserialize the preprocessor from a raw, possibly mmap-backed buffer.
virtual int deserialize(const void *data, size_t len) = 0;
};
} // namespace turbo
} // namespace zvec

View File

@ -28,26 +28,6 @@ namespace turbo {
using namespace zvec::core; using namespace zvec::core;
//! Error code literals mirroring core::IndexError::Code integer values.
//!
//! Turbo quantizer sources use these directly instead of the
//! `IndexError_NotImplemented` / `IndexError_Unsupported` const objects
//! because MSVC's WINDOWS_EXPORT_ALL_SYMBOLS does not export const data
//! with constructors from zvec_shared.dll. zvec_turbo is a static library
//! linked with /WHOLEARCHIVE, so referencing those unexported symbols across
//! the DLL boundary triggers LNK2019 on Windows.
//!
//! IndexError::Code stores -val in its constructor, so NotImplemented(11)
//! yields -11 and Unsupported(12) yields -12.
constexpr int kErrNotImplemented = -11;
constexpr int kErrUnsupported = -12;
//! Magic number ('QTZR') stamped at the start of a serialized quantizer blob.
constexpr uint32_t kQuantizerMagic = 0x52545A51u;
//! Current quantizer serialization format version.
constexpr uint16_t kQuantizerSerVersion = 1;
//! Self-describing, fixed-size header that prefixes every serialized quantizer. //! Self-describing, fixed-size header that prefixes every serialized quantizer.
//! The type-specific payload (scalar params, codebook, rotation matrix, ...) //! The type-specific payload (scalar params, codebook, rotation matrix, ...)
//! follows immediately after this header. //! follows immediately after this header.

View File

@ -12,18 +12,29 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
#include <cassert>
#include <ailego/internal/cpu_features.h> #include <ailego/internal/cpu_features.h>
#include <zvec/turbo/turbo.h> #include <zvec/turbo/turbo.h>
#include "avx2/rotate/fht/fht.h"
#include "avx512/rotate/fht/fht.h"
#include "avx512_vnni/record_quantized_int8/cosine.h" #include "avx512_vnni/record_quantized_int8/cosine.h"
#include "avx512_vnni/record_quantized_int8/squared_euclidean.h" #include "avx512_vnni/record_quantized_int8/squared_euclidean.h"
#include "avx512_vnni/uniform_int8/quantize.h" #include "avx512_vnni/uniform_int8/quantize.h"
#include "avx512_vnni/uniform_int8/squared_euclidean.h" #include "avx512_vnni/uniform_int8/squared_euclidean.h"
#include "neon/rotate/fht/fht.h"
#include "scalar/fp32/cosine.h" #include "scalar/fp32/cosine.h"
#include "scalar/fp32/inner_product.h" #include "scalar/fp32/inner_product.h"
#include "scalar/fp32/squared_euclidean.h" #include "scalar/fp32/squared_euclidean.h"
#include "scalar/rotate/fht/fht.h"
#include "sse/rotate/fht/fht.h"
namespace zvec::turbo { namespace zvec::turbo {
// Helper: check if the requested arch matches the target or is auto-detect.
static bool IsArchMatch(CpuArchType actual, CpuArchType target) {
return actual == CpuArchType::kAuto || actual == target;
}
DistanceFunc get_distance_func(MetricType metric_type, DataType data_type, DistanceFunc get_distance_func(MetricType metric_type, DataType data_type,
QuantizeType quantize_type, QuantizeType quantize_type,
CpuArchType cpu_arch_type) { CpuArchType cpu_arch_type) {
@ -45,8 +56,7 @@ DistanceFunc get_distance_func(MetricType metric_type, DataType data_type,
if (data_type == DataType::kInt8) { if (data_type == DataType::kInt8) {
if (quantize_type == QuantizeType::kDefault) { if (quantize_type == QuantizeType::kDefault) {
if (zvec::ailego::internal::CpuFeatures::static_flags_.AVX512_VNNI && if (zvec::ailego::internal::CpuFeatures::static_flags_.AVX512_VNNI &&
(cpu_arch_type == CpuArchType::kAuto || IsArchMatch(cpu_arch_type, CpuArchType::kAVX512VNNI)) {
cpu_arch_type == CpuArchType::kAVX512VNNI)) {
if (metric_type == MetricType::kSquaredEuclidean) { if (metric_type == MetricType::kSquaredEuclidean) {
return avx512_vnni::squared_euclidean_int8_distance; return avx512_vnni::squared_euclidean_int8_distance;
} }
@ -57,8 +67,7 @@ DistanceFunc get_distance_func(MetricType metric_type, DataType data_type,
} }
if (quantize_type == QuantizeType::kUniform) { if (quantize_type == QuantizeType::kUniform) {
if (zvec::ailego::internal::CpuFeatures::static_flags_.AVX512_VNNI && if (zvec::ailego::internal::CpuFeatures::static_flags_.AVX512_VNNI &&
(cpu_arch_type == CpuArchType::kAuto || IsArchMatch(cpu_arch_type, CpuArchType::kAVX512VNNI)) {
cpu_arch_type == CpuArchType::kAVX512VNNI)) {
if (metric_type == MetricType::kSquaredEuclidean) { if (metric_type == MetricType::kSquaredEuclidean) {
return avx512_vnni::uniform_squared_euclidean_int8_distance; return avx512_vnni::uniform_squared_euclidean_int8_distance;
} }
@ -90,8 +99,7 @@ BatchDistanceFunc get_batch_distance_func(MetricType metric_type,
if (data_type == DataType::kInt8) { if (data_type == DataType::kInt8) {
if (quantize_type == QuantizeType::kDefault) { if (quantize_type == QuantizeType::kDefault) {
if (zvec::ailego::internal::CpuFeatures::static_flags_.AVX512_VNNI && if (zvec::ailego::internal::CpuFeatures::static_flags_.AVX512_VNNI &&
(cpu_arch_type == CpuArchType::kAuto || IsArchMatch(cpu_arch_type, CpuArchType::kAVX512VNNI)) {
cpu_arch_type == CpuArchType::kAVX512VNNI)) {
if (metric_type == MetricType::kSquaredEuclidean) { if (metric_type == MetricType::kSquaredEuclidean) {
return avx512_vnni::squared_euclidean_int8_batch_distance; return avx512_vnni::squared_euclidean_int8_batch_distance;
} }
@ -102,8 +110,7 @@ BatchDistanceFunc get_batch_distance_func(MetricType metric_type,
} }
if (quantize_type == QuantizeType::kUniform) { if (quantize_type == QuantizeType::kUniform) {
if (zvec::ailego::internal::CpuFeatures::static_flags_.AVX512_VNNI && if (zvec::ailego::internal::CpuFeatures::static_flags_.AVX512_VNNI &&
(cpu_arch_type == CpuArchType::kAuto || IsArchMatch(cpu_arch_type, CpuArchType::kAVX512VNNI)) {
cpu_arch_type == CpuArchType::kAVX512VNNI)) {
if (metric_type == MetricType::kSquaredEuclidean) { if (metric_type == MetricType::kSquaredEuclidean) {
return avx512_vnni::uniform_squared_euclidean_int8_batch_distance; return avx512_vnni::uniform_squared_euclidean_int8_batch_distance;
} }
@ -121,8 +128,7 @@ QueryPreprocessFunc get_query_preprocess_func(MetricType metric_type,
if (data_type == DataType::kInt8) { if (data_type == DataType::kInt8) {
if (quantize_type == QuantizeType::kDefault) { if (quantize_type == QuantizeType::kDefault) {
if (zvec::ailego::internal::CpuFeatures::static_flags_.AVX512_VNNI && if (zvec::ailego::internal::CpuFeatures::static_flags_.AVX512_VNNI &&
(cpu_arch_type == CpuArchType::kAuto || IsArchMatch(cpu_arch_type, CpuArchType::kAVX512VNNI)) {
cpu_arch_type == CpuArchType::kAVX512VNNI)) {
if (metric_type == MetricType::kSquaredEuclidean) { if (metric_type == MetricType::kSquaredEuclidean) {
return avx512_vnni::squared_euclidean_int8_query_preprocess; return avx512_vnni::squared_euclidean_int8_query_preprocess;
} }
@ -147,4 +153,35 @@ UniformQuantizeFunc get_uniform_quantize_func(DataType data_type) {
return nullptr; return nullptr;
} }
RotatorKernels get_rotator_kernels(RotateType rotate_type,
CpuArchType cpu_arch_type) {
switch (rotate_type) {
case RotateType::kFht: {
if (zvec::ailego::internal::CpuFeatures::static_flags_.AVX512F &&
IsArchMatch(cpu_arch_type, CpuArchType::kAVX512)) {
return {avx512::fht_rotate_avx512, avx512::fht_unrotate_avx512};
}
if (zvec::ailego::internal::CpuFeatures::static_flags_.AVX2 &&
IsArchMatch(cpu_arch_type, CpuArchType::kAVX2)) {
return {avx2::fht_rotate_avx2, avx2::fht_unrotate_avx2};
}
if (zvec::ailego::internal::CpuFeatures::static_flags_.SSE2 &&
IsArchMatch(cpu_arch_type, CpuArchType::kSSE)) {
return {sse::fht_rotate_sse, sse::fht_unrotate_sse};
}
if (zvec::ailego::internal::CpuFeatures::static_flags_.NEON &&
IsArchMatch(cpu_arch_type, CpuArchType::kNEON)) {
return {neon::fht_rotate_neon, neon::fht_unrotate_neon};
}
return {scalar::fht_rotate, scalar::fht_unrotate};
}
}
// Unsupported RotateType: assert in debug for early detection, but always
// return the scalar kernels so release builds never hand back null function
// pointers (which would crash on the first call).
assert(false && "unsupported RotateType");
return {scalar::fht_rotate, scalar::fht_unrotate};
}
} // namespace zvec::turbo } // namespace zvec::turbo

View File

@ -21,7 +21,6 @@ TEST(QueryParamsTest, QueryParamsBaseClass) {
// Test constructor // Test constructor
QueryParams params(IndexType::HNSW); QueryParams params(IndexType::HNSW);
EXPECT_EQ(params.type(), IndexType::HNSW); EXPECT_EQ(params.type(), IndexType::HNSW);
} }
TEST(QueryParamsTest, HnswQueryParams) { TEST(QueryParamsTest, HnswQueryParams) {

View File

@ -0,0 +1,450 @@
// Copyright 2025-present the zvec project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <cmath>
#include <cstring>
#include <random>
#include <string>
#include <vector>
#include <gtest/gtest.h>
#include "preprocessor/fht_rotator/fht_rotator.h"
using namespace zvec::turbo;
namespace {
// Helper: fill a vector with random floats.
void fill_random(float *data, size_t dim, std::mt19937 &gen) {
std::uniform_real_distribution<float> dist(-1.0f, 1.0f);
for (size_t i = 0; i < dim; ++i) data[i] = dist(gen);
}
// Helper: check round-trip (apply_inverse(apply(x)) == x) within tolerance.
void check_round_trip(const FhtRotator &rot, const std::vector<float> &input,
float tol = 1e-3f) {
const int dim = rot.in_dim();
std::vector<float> rotated(dim);
std::vector<float> recovered(dim);
rot.apply(input.data(), rotated.data());
rot.apply_inverse(rotated.data(), recovered.data());
for (int i = 0; i < dim; ++i) {
EXPECT_NEAR(input[i], recovered[i], tol)
<< "mismatch at i=" << i << " dim=" << dim;
}
}
} // anonymous namespace
// ---------------------------------------------------------------------------
// Power-of-2 dimensions
// ---------------------------------------------------------------------------
TEST(FhtRotator, PowerOf2RoundTrip) {
std::mt19937 gen(42);
for (int dim : {1, 2, 4, 8, 16, 32, 64, 128, 256}) {
auto rot = FhtRotator::create(dim);
ASSERT_TRUE(rot) << "create failed for dim=" << dim;
std::vector<float> input(dim);
fill_random(input.data(), dim, gen);
check_round_trip(*rot, input);
}
}
// ---------------------------------------------------------------------------
// Non-power-of-2 dimensions
// ---------------------------------------------------------------------------
TEST(FhtRotator, NonPowerOf2RoundTrip) {
std::mt19937 gen(123);
for (int dim : {3, 5, 7, 10, 13, 31, 50, 97, 100, 127, 192, 320}) {
auto rot = FhtRotator::create(dim);
ASSERT_TRUE(rot) << "create failed for dim=" << dim;
std::vector<float> input(dim);
fill_random(input.data(), dim, gen);
check_round_trip(*rot, input);
}
}
// ---------------------------------------------------------------------------
// Serialize / Deserialize round-trip
// ---------------------------------------------------------------------------
TEST(FhtRotator, SerializeDeserialize) {
std::mt19937 gen(999);
for (int dim : {32, 97, 128}) {
// Build original rotator.
auto rot = FhtRotator::create(dim);
ASSERT_TRUE(rot);
// Serialize.
std::string blob;
ASSERT_EQ(0, rot->serialize(&blob));
ASSERT_GT(blob.size(), sizeof(RotatorSerHeader));
// Restore from blob.
auto rot2 = FhtRotator::from_blob(blob.data(), blob.size());
ASSERT_TRUE(rot2) << "from_blob failed for dim=" << dim;
// Dimensions must match.
EXPECT_EQ(rot2->in_dim(), dim);
EXPECT_EQ(rot2->out_dim(), dim);
// Round-trip via the restored rotator must produce the same result
// as the original (same flip signs).
std::vector<float> input(dim);
fill_random(input.data(), dim, gen);
std::vector<float> r1(dim), r2(dim);
rot->apply(input.data(), r1.data());
rot2->apply(input.data(), r2.data());
for (int i = 0; i < dim; ++i) {
EXPECT_FLOAT_EQ(r1[i], r2[i]) << "apply mismatch at i=" << i;
}
// Inverse via restored rotator must recover the input.
check_round_trip(*rot2, input);
}
}
// ---------------------------------------------------------------------------
// Dimension preserved
// ---------------------------------------------------------------------------
TEST(FhtRotator, DimensionPreserved) {
for (int dim : {1, 7, 64, 97, 128}) {
auto rot = FhtRotator::create(dim);
ASSERT_TRUE(rot);
EXPECT_EQ(rot->in_dim(), dim);
EXPECT_EQ(rot->out_dim(), dim);
}
}
// ---------------------------------------------------------------------------
// Train generates non-zero flip signs
// ---------------------------------------------------------------------------
TEST(FhtRotator, CreateGeneratesFlip) {
for (int dim : {8, 64, 97}) {
auto rot = FhtRotator::create(dim);
ASSERT_TRUE(rot);
// After create, flip is already populated, so serialize must succeed.
std::string blob;
EXPECT_EQ(0, rot->serialize(&blob))
<< "serialize failed after create for dim=" << dim;
// Verify the serialized structure deterministically. Copy the header into
// an aligned local first: blob.data() may be unaligned and dereferencing a
// reinterpret_cast<const RotatorSerHeader *> would be undefined behavior on
// architectures that require alignment.
RotatorSerHeader hdr;
std::memcpy(&hdr, blob.data(), sizeof(RotatorSerHeader));
EXPECT_EQ(hdr.magic, kRotatorMagic);
EXPECT_EQ(hdr.version, kRotatorSerVersion);
EXPECT_EQ(static_cast<RotateType>(hdr.rotator_type), RotateType::kFht);
EXPECT_EQ(static_cast<int>(hdr.in_dim), dim);
EXPECT_EQ(static_cast<int>(hdr.out_dim), dim);
// Payload holds 4 rounds of ceil(dim/8) flip bytes. Checking the exact size
// (and total blob length) keeps the test deterministic: we avoid asserting
// on random bit values, which could theoretically be all-zero on a platform
// where std::random_device has low entropy.
const uint32_t expected_flip_size =
4u * ((static_cast<uint32_t>(dim) + 7u) / 8u);
EXPECT_EQ(hdr.payload_size, expected_flip_size);
EXPECT_EQ(blob.size(), sizeof(RotatorSerHeader) + expected_flip_size);
}
}
// ---------------------------------------------------------------------------
// Create with invalid dimension returns nullptr
// ---------------------------------------------------------------------------
TEST(FhtRotator, InvalidDimension) {
EXPECT_EQ(FhtRotator::create(0), nullptr);
EXPECT_EQ(FhtRotator::create(-1), nullptr);
}
// ---------------------------------------------------------------------------
// from_blob with malformed input returns nullptr
// ---------------------------------------------------------------------------
TEST(FhtRotator, FromBlobMalformed) {
EXPECT_EQ(FhtRotator::from_blob(nullptr, 0), nullptr);
// Too short.
char buf[4] = {};
EXPECT_EQ(FhtRotator::from_blob(buf, sizeof(buf)), nullptr);
// Wrong magic.
RotatorSerHeader hdr{};
hdr.magic = 0xDEADBEEF;
hdr.version = kRotatorSerVersion;
hdr.rotator_type = static_cast<uint16_t>(RotateType::kFht);
hdr.payload_size = 0;
EXPECT_EQ(FhtRotator::from_blob(&hdr, sizeof(hdr)), nullptr);
}
// ---------------------------------------------------------------------------
// L2 distance preserved (orthogonal transform)
// ---------------------------------------------------------------------------
TEST(FhtRotator, L2DistancePreserved) {
std::mt19937 gen(2024);
for (int dim : {32, 64, 97, 128}) {
auto rot = FhtRotator::create(dim);
ASSERT_TRUE(rot);
const int N = 50;
std::vector<std::vector<float>> raw(N, std::vector<float>(dim));
std::vector<std::vector<float>> rotated(N, std::vector<float>(dim));
for (int i = 0; i < N; ++i) {
fill_random(raw[i].data(), dim, gen);
rot->apply(raw[i].data(), rotated[i].data());
}
// Check that ||rotated[i] - rotated[j]|| ≈ ||raw[i] - raw[j]||.
for (int i = 1; i < N; ++i) {
float d_raw = 0.0f, d_rot = 0.0f;
for (int j = 0; j < dim; ++j) {
float dr = raw[i][j] - raw[0][j];
float dt = rotated[i][j] - rotated[0][j];
d_raw += dr * dr;
d_rot += dt * dt;
}
EXPECT_NEAR(d_raw, d_rot, 1e-2f)
<< "L2 mismatch for dim=" << dim << " i=" << i;
}
}
}
// ---------------------------------------------------------------------------
// Cosine distance preserved (orthogonal transform)
// ---------------------------------------------------------------------------
TEST(FhtRotator, CosineDistancePreserved) {
std::mt19937 gen(777);
auto cosine_dist = [](const float *a, const float *b, int dim) {
float dot = 0, na = 0, nb = 0;
for (int i = 0; i < dim; ++i) {
dot += a[i] * b[i];
na += a[i] * a[i];
nb += b[i] * b[i];
}
float denom = std::sqrt(na) * std::sqrt(nb);
return (denom < 1e-12f) ? 1.0f : 1.0f - dot / denom;
};
for (int dim : {32, 97, 128}) {
auto rot = FhtRotator::create(dim);
ASSERT_TRUE(rot);
const int N = 50;
std::vector<std::vector<float>> raw(N, std::vector<float>(dim));
std::vector<std::vector<float>> rotated(N, std::vector<float>(dim));
for (int i = 0; i < N; ++i) {
fill_random(raw[i].data(), dim, gen);
rot->apply(raw[i].data(), rotated[i].data());
}
for (int i = 1; i < N; ++i) {
float d_raw = cosine_dist(raw[i].data(), raw[0].data(), dim);
float d_rot = cosine_dist(rotated[i].data(), rotated[0].data(), dim);
EXPECT_NEAR(d_raw, d_rot, 1e-3f)
<< "Cosine mismatch for dim=" << dim << " i=" << i;
}
}
}
// ---------------------------------------------------------------------------
// Apply is non-trivial (not identity)
// ---------------------------------------------------------------------------
TEST(FhtRotator, ApplyIsNonTrivial) {
std::mt19937 gen(42);
for (int dim : {32, 97, 128}) {
auto rot = FhtRotator::create(dim);
ASSERT_TRUE(rot);
std::vector<float> input(dim);
fill_random(input.data(), dim, gen);
std::vector<float> output(dim);
rot->apply(input.data(), output.data());
// At least some elements should differ from the input.
bool any_diff = false;
for (int i = 0; i < dim; ++i) {
if (std::abs(input[i] - output[i]) > 1e-6f) {
any_diff = true;
break;
}
}
EXPECT_TRUE(any_diff) << "apply is identity for dim=" << dim;
}
}
// ---------------------------------------------------------------------------
// Apply is deterministic (same input → same output)
// ---------------------------------------------------------------------------
TEST(FhtRotator, ApplyDeterministic) {
std::mt19937 gen(55);
for (int dim : {32, 97, 128}) {
auto rot = FhtRotator::create(dim);
ASSERT_TRUE(rot);
std::vector<float> input(dim);
fill_random(input.data(), dim, gen);
std::vector<float> r1(dim), r2(dim);
rot->apply(input.data(), r1.data());
rot->apply(input.data(), r2.data());
for (int i = 0; i < dim; ++i) {
EXPECT_FLOAT_EQ(r1[i], r2[i])
<< "non-deterministic apply at i=" << i << " dim=" << dim;
}
}
}
// ---------------------------------------------------------------------------
// Deserialize on existing object (init → serialize → deserialize on new object)
// ---------------------------------------------------------------------------
TEST(FhtRotator, DeserializeOnExistingObject) {
std::mt19937 gen(314);
for (int dim : {32, 97, 128}) {
// Build original.
auto rot1 = FhtRotator::create(dim);
ASSERT_TRUE(rot1);
std::string blob;
ASSERT_EQ(0, rot1->serialize(&blob));
// Create a fresh rotator, then call deserialize() on it.
auto rot2 = FhtRotator::create(dim);
ASSERT_TRUE(rot2);
ASSERT_EQ(0, rot2->deserialize(blob.data(), blob.size()));
EXPECT_EQ(rot2->in_dim(), dim);
EXPECT_EQ(rot2->out_dim(), dim);
// Both rotators should produce identical results.
std::vector<float> input(dim);
fill_random(input.data(), dim, gen);
std::vector<float> r1(dim), r2(dim);
rot1->apply(input.data(), r1.data());
rot2->apply(input.data(), r2.data());
for (int i = 0; i < dim; ++i) {
EXPECT_FLOAT_EQ(r1[i], r2[i])
<< "apply mismatch at i=" << i << " dim=" << dim;
}
// Inverse via rot2 should recover input.
check_round_trip(*rot2, input);
}
}
// ---------------------------------------------------------------------------
// Deserialize with truncated payload fails
// ---------------------------------------------------------------------------
TEST(FhtRotator, DeserializeTruncatedPayload) {
std::mt19937 gen(42);
auto rot = FhtRotator::create(64);
ASSERT_TRUE(rot);
std::string blob;
ASSERT_EQ(0, rot->serialize(&blob));
// Truncate the blob: keep header but cut half the payload.
// Copy the header into an aligned local before reading fields (blob.data()
// may be unaligned; a direct reinterpret_cast dereference is UB on
// alignment-sensitive architectures).
RotatorSerHeader hdr;
std::memcpy(&hdr, blob.data(), sizeof(RotatorSerHeader));
size_t truncated_len = sizeof(RotatorSerHeader) + hdr.payload_size / 2;
auto rot2 = FhtRotator::create(64);
ASSERT_TRUE(rot2);
EXPECT_NE(0, rot2->deserialize(blob.data(), truncated_len));
// Also test from_blob with truncated data.
EXPECT_EQ(FhtRotator::from_blob(blob.data(), truncated_len), nullptr);
}
// ---------------------------------------------------------------------------
// Large dimension stress test
// ---------------------------------------------------------------------------
TEST(FhtRotator, LargeDimension) {
std::mt19937 gen(2025);
for (int dim : {1024, 2048, 4096}) {
auto rot = FhtRotator::create(dim);
ASSERT_TRUE(rot) << "create failed for dim=" << dim;
std::vector<float> input(dim);
fill_random(input.data(), dim, gen);
check_round_trip(*rot, input, 1e-2f);
// Verify serialize/deserialize round-trip.
std::string blob;
ASSERT_EQ(0, rot->serialize(&blob));
auto rot2 = FhtRotator::from_blob(blob.data(), blob.size());
ASSERT_TRUE(rot2);
std::vector<float> r1(dim), r2(dim);
rot->apply(input.data(), r1.data());
rot2->apply(input.data(), r2.data());
for (int i = 0; i < dim; ++i) {
EXPECT_FLOAT_EQ(r1[i], r2[i]) << "mismatch at i=" << i << " dim=" << dim;
}
}
}
// ---------------------------------------------------------------------------
// Norm preserved (orthogonal transform preserves vector norm)
// ---------------------------------------------------------------------------
TEST(FhtRotator, NormPreserved) {
std::mt19937 gen(99);
for (int dim : {32, 97, 128}) {
auto rot = FhtRotator::create(dim);
ASSERT_TRUE(rot);
std::vector<float> input(dim);
fill_random(input.data(), dim, gen);
float norm_in = 0.0f;
for (int i = 0; i < dim; ++i) norm_in += input[i] * input[i];
std::vector<float> output(dim);
rot->apply(input.data(), output.data());
float norm_out = 0.0f;
for (int i = 0; i < dim; ++i) norm_out += output[i] * output[i];
EXPECT_NEAR(norm_in, norm_out, 1e-2f)
<< "norm not preserved for dim=" << dim;
}
}