Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/actions/check-files/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: 'NSparse Check Files Changed'
description: 'Check if specific files have changed for nsparse repo'
inputs:
files:
description: 'Additional files to check for changes'
default: ''
outputs:
files_changed:
description: 'Whether any files changed'
value: ${{ steps.changed-files.outputs.any_changed }}
runs:
using: 'composite'
steps:
- name: Combine files
id: combine-files
shell: bash
run: |
DEFAULT_FILES="CMakeLists.txt,nsparse/**,tests/**,cmake/**,benchmarks/**"
if [ -n "${{ inputs.files }}" ]; then
COMBINED_FILES="$DEFAULT_FILES,${{ inputs.files }}"
else
COMBINED_FILES="$DEFAULT_FILES"
fi
echo "combined_files=$COMBINED_FILES" >> $GITHUB_OUTPUT
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v47.0.0
with:
files: ${{ steps.combine-files.outputs.combined_files }}
files_separator: ","
202 changes: 202 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
name: Build and Test nsparse
on:
push:
branches:
- "*"
- "feature/**"
pull_request:
branches:
- "*"
- "feature/**"

jobs:
check-files:
name: Check files for Build and Test
runs-on: ubuntu-latest
outputs:
RUN_BUILD_AND_TEST: ${{ steps.check.outputs.files_changed }}
steps:
- uses: actions/checkout@v4
- name: Check files
id: check
uses: ./.github/actions/check-files
with:
files: .github/workflows/CI.yml

Get-CI-Image-Tag:
needs: check-files
if: needs.check-files.outputs.RUN_BUILD_AND_TEST == 'true'
uses: opensearch-project/opensearch-build/.github/workflows/get-ci-image-tag.yml@main
with:
product: opensearch

Build-nsparse-Linux:
name: Build and Test nsparse on Linux
runs-on: ubuntu-latest
needs: Get-CI-Image-Tag
container:
image: ${{ needs.Get-CI-Image-Tag.outputs.ci-image-version-linux }}
options: ${{ needs.Get-CI-Image-Tag.outputs.ci-image-start-options }}
steps:
- name: Run start commands
run: ${{ needs.Get-CI-Image-Tag.outputs.ci-image-start-command }}

- name: Checkout
uses: actions/checkout@v4

# Generic build
- name: Configure (generic)
run: |
cmake -B build \
-DNSPARSE_ENABLE_TESTS=ON \
-DNSPARSE_OPT_LEVEL=generic \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_EXE_LINKER_FLAGS="-static-libstdc++ -static-libgcc" \
-DCMAKE_SHARED_LINKER_FLAGS="-static-libstdc++ -static-libgcc"

- name: Build (generic)
run: cmake --build build -j$(nproc)

- name: Test (generic)
run: ctest --test-dir build --output-on-failure

# Detect SIMD and build+test the best available level
- name: Detect SIMD capability
id: detect-simd
run: |
ARCH=$(uname -m)
if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then
if cat /proc/cpuinfo 2>/dev/null | grep -qi sve; then
echo "opt_level=sve" >> $GITHUB_OUTPUT
else
echo "opt_level=neon" >> $GITHUB_OUTPUT
fi
elif [ "$ARCH" = "x86_64" ]; then
if lscpu | grep -qi avx512f && lscpu | grep -qi avx512cd && lscpu | grep -qi avx512vl && lscpu | grep -qi avx512dq && lscpu | grep -qi avx512bw; then
echo "opt_level=avx512" >> $GITHUB_OUTPUT
elif lscpu | grep -qi avx2; then
echo "opt_level=avx2" >> $GITHUB_OUTPUT
else
echo "opt_level=" >> $GITHUB_OUTPUT
fi
else
echo "opt_level=" >> $GITHUB_OUTPUT
fi

- name: Configure (SIMD)
if: steps.detect-simd.outputs.opt_level != ''
run: |
rm -rf build
cmake -B build \
-DNSPARSE_ENABLE_TESTS=ON \
-DNSPARSE_OPT_LEVEL=${{ steps.detect-simd.outputs.opt_level }} \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_EXE_LINKER_FLAGS="-static-libstdc++ -static-libgcc" \
-DCMAKE_SHARED_LINKER_FLAGS="-static-libstdc++ -static-libgcc"

- name: Build (SIMD)
if: steps.detect-simd.outputs.opt_level != ''
run: cmake --build build -j$(nproc)

- name: Test (SIMD)
if: steps.detect-simd.outputs.opt_level != ''
run: ctest --test-dir build --output-on-failure

Build-nsparse-MacOS:
name: Build and Test nsparse on MacOS
needs: check-files
if: needs.check-files.outputs.RUN_BUILD_AND_TEST == 'true'
runs-on: macos-15
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install dependencies
run: brew install libomp

- name: Detect SIMD capability
id: detect-simd
run: |
if sysctl -n machdep.cpu.features machdep.cpu.leaf7_features 2>/dev/null | grep -qi AVX2; then
echo "opt_level=avx2" >> $GITHUB_OUTPUT
elif uname -m | grep -q arm64; then
echo "opt_level=neon" >> $GITHUB_OUTPUT
else
echo "opt_level=generic" >> $GITHUB_OUTPUT
fi

# Generic build
- name: Configure (generic)
run: |
cmake -B build \
-DNSPARSE_ENABLE_TESTS=ON \
-DNSPARSE_OPT_LEVEL=generic \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_COMPILER=/usr/bin/clang \
-DCMAKE_CXX_COMPILER=/usr/bin/clang++ \
-DOpenMP_CXX_FLAGS="-Xpreprocessor -fopenmp -I$(brew --prefix libomp)/include" \
-DOpenMP_CXX_LIB_NAMES="omp" \
-DOpenMP_omp_LIBRARY=$(brew --prefix libomp)/lib/libomp.dylib

- name: Build (generic)
run: cmake --build build -j$(sysctl -n hw.ncpu)

- name: Test (generic)
run: ctest --test-dir build --output-on-failure

# SIMD build if different from generic
- name: Configure (SIMD)
if: steps.detect-simd.outputs.opt_level != 'generic'
run: |
rm -rf build
cmake -B build \
-DNSPARSE_ENABLE_TESTS=ON \
-DNSPARSE_OPT_LEVEL=${{ steps.detect-simd.outputs.opt_level }} \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_COMPILER=/usr/bin/clang \
-DCMAKE_CXX_COMPILER=/usr/bin/clang++ \
-DOpenMP_CXX_FLAGS="-Xpreprocessor -fopenmp -I$(brew --prefix libomp)/include" \
-DOpenMP_CXX_LIB_NAMES="omp" \
-DOpenMP_omp_LIBRARY=$(brew --prefix libomp)/lib/libomp.dylib

- name: Build (SIMD)
if: steps.detect-simd.outputs.opt_level != 'generic'
run: cmake --build build -j$(sysctl -n hw.ncpu)

- name: Test (SIMD)
if: steps.detect-simd.outputs.opt_level != 'generic'
run: ctest --test-dir build --output-on-failure

Build-nsparse-Windows:
name: Build and Test nsparse on Windows
needs: check-files
if: needs.check-files.outputs.RUN_BUILD_AND_TEST == 'true'
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Configure (generic)
run: |
cmake -B build `
-DNSPARSE_ENABLE_TESTS=ON `
-DNSPARSE_OPT_LEVEL=generic `
-DCMAKE_BUILD_TYPE=Release

- name: Build (generic)
run: cmake --build build --config Release -j $env:NUMBER_OF_PROCESSORS

- name: Test (generic)
run: ctest --test-dir build --build-config Release --output-on-failure

check-results:
needs: [check-files, Build-nsparse-Linux, Build-nsparse-MacOS, Build-nsparse-Windows]
if: always()
name: Check results
runs-on: ubuntu-latest
steps:
- name: Fail if build or test failed
if: |
needs.check-files.outputs.RUN_BUILD_AND_TEST == 'true' &&
(needs.Build-nsparse-Linux.result == 'failure' || needs.Build-nsparse-MacOS.result == 'failure' || needs.Build-nsparse-Windows.result == 'failure')
run: exit 1
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,6 @@ venv/

# third_party
third_party/

# build
build/
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
![OpenSearch logo](OpenSearch.svg)
<img src="https://opensearch.org/assets/img/opensearch-logo-themed.svg" height="64px">

- [Introduction](#introduction)
- [Introduction](#neural-sparse-cpp)
- [Project Resources](#project-resources)
- [Project Style Guidelines](#project-style-guidelines)
- [Code of Conduct](#code-of-conduct)
- [License](#license)
- [Copyright](#copyright)

## Introduction
## neural-sparse-cpp

**neural-sparse-cpp** is a C++ library for high-performance sparse vector similarity search, developed as part of the [OpenSearch Project](https://opensearch.org/). It implements the SEISMIC (Sparse Embeddings In Search via Inverted Multi-Index Clustering) algorithm for approximate nearest neighbor search over sparse vectors.
**neural-sparse-cpp** is a C++ library for high-performance sparse vector similarity search, developed as part of the [OpenSearch Project](https://opensearch.org/). It provides multiple index types for nearest neighbor search over sparse vectors.

Key features include:

- SEISMIC-based inverted index with clustering for fast approximate search
- Multiple index types: inverted index, SEISMIC, and SEISMIC with scalar quantization
- Scalar quantization support for reduced memory usage
- SIMD-optimized distance computations (AVX2, AVX512, NEON, SVE)
- ID mapping and ID selector filtering
Expand Down
2 changes: 1 addition & 1 deletion nsparse/cluster/random_kmeans.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ inline static size_t boundary_check_n_clusters(size_t n_docs,

// Ensure at least one cluster
n_clusters = n_clusters > n_docs ? n_docs : n_clusters;
n_clusters = std::max(1UL, n_clusters);
n_clusters = std::max(static_cast<size_t>(1), n_clusters);
return n_clusters;
}

Expand Down
17 changes: 15 additions & 2 deletions nsparse/cluster/random_kmeans.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
#define RANDOM_KMEANS_H

#include <vector>
#ifdef _MSC_VER
#include <malloc.h>
#endif

#include "nsparse/sparse_vectors.h"

Expand All @@ -22,12 +25,22 @@ class ClusterRepresentatives {
size_t alignmnt)
: num_clusters_(num_clusters), sketch_size_(sketch_size) {
// Align to 64-byte boundary for AVX-512

#ifdef _MSC_VER
data = static_cast<float*>(_aligned_malloc(
num_clusters * sketch_size * sizeof(float), alignmnt));
#else
data = static_cast<float*>(std::aligned_alloc(
alignmnt, num_clusters * sketch_size * sizeof(float)));
#endif
}

~ClusterRepresentatives() { std::free(data); }
~ClusterRepresentatives() {
#ifdef _MSC_VER
_aligned_free(data);
#else
std::free(data);
#endif
}

// Access element (i,j) where i is cluster index and j is dimension
float& operator()(size_t i, size_t j) { return data[i * sketch_size_ + j]; }
Expand Down
13 changes: 12 additions & 1 deletion nsparse/inverted_index.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@

#include "nsparse/inverted_index.h"

#ifdef _MSC_VER
#include <intrin.h>
#pragma intrinsic(_BitScanForward64)
#endif

#include <algorithm>
#include <cassert>
#include <cstring>
Expand Down Expand Up @@ -146,15 +151,21 @@ void evaluate_window_candidates(std::vector<DirectTermScorer>& scorers,
const uint64_t* bitmap,
detail::TopKHolder<idx_t>& heap) {
// Iterate only set bits in the bitmap.
// Each word covers 64 slots; __builtin_ctzll finds the next set bit.
// Each word covers 64 slots; ctzll finds the next set bit.
static constexpr int kBitmapWords = kScoreWindowSize / 64;
float threshold = heap.full() ? heap.peek_score() : 0.0F;
float non_essential_sum = max_score_prefix[first_essential];

for (int word_idx = 0; word_idx < kBitmapWords; ++word_idx) {
uint64_t word = bitmap[word_idx];
while (word != 0) {
#ifdef _MSC_VER
unsigned long bit_pos;
_BitScanForward64(&bit_pos, word);
int bit = static_cast<int>(bit_pos);
#else
int bit = __builtin_ctzll(word);
#endif
word &= word - 1; // clear lowest set bit
int slot = (word_idx << 6) | bit;

Expand Down
1 change: 1 addition & 0 deletions nsparse/invlists/inverted_lists.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

#include <algorithm>
#include <cstdint>
#include <iterator>
#include <map>
#include <memory>
#include <stdexcept>
Expand Down
3 changes: 2 additions & 1 deletion nsparse/seismic_common.h
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,8 @@ inline std::vector<InvertedListClusters> build_inverted_lists_clusters(
std::vector<InvertedListClusters> clustered_inverted_lists(
inverted_lists_size);
#pragma omp parallel for schedule(dynamic, 64)
for (size_t idx = 0; idx < inverted_lists_size; ++idx) {
for (int64_t idx = 0; idx < static_cast<int64_t>(inverted_lists_size);
++idx) {
auto& invlist = (*inverted_lists)[idx];
const auto& doc_ids = invlist.prune_and_keep_doc_ids(lambda);
InvertedListClusters inverted_list_clusters(
Expand Down
16 changes: 15 additions & 1 deletion nsparse/utils/dense_vector_matrix.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@

#include <cstddef>
#include <cstdlib>
#ifdef _MSC_VER
#include <malloc.h>
#endif

namespace nsparse::detail {

Expand All @@ -30,11 +33,22 @@ class DenseVectorMatrixT {

DenseVectorMatrixT(size_t row, size_t dimension)
: rows_(row), dimension_(dimension) {
#ifdef _MSC_VER
data_ = static_cast<T*>(
_aligned_malloc(row * dimension * sizeof(T), MATRIX_ALIGNMENT));
#else
data_ = static_cast<T*>(
std::aligned_alloc(MATRIX_ALIGNMENT, row * dimension * sizeof(T)));
#endif
}

~DenseVectorMatrixT() { std::free(data_); }
~DenseVectorMatrixT() {
#ifdef _MSC_VER
_aligned_free(data_);
#else
std::free(data_);
#endif
}

T get(size_t row, size_t col) const {
return data_[row * dimension_ + col];
Expand Down
Loading
Loading