From 4b69c910d95a8b828d47322e10e59546d962cc2b Mon Sep 17 00:00:00 2001 From: sunhailinLeo <379978424@qq.com> Date: Mon, 2 Aug 2021 10:53:09 +0800 Subject: [PATCH] :hammer: version 1.0.0 --- .gitignore | 48 ++ .gitmodules | 3 + Makefile | 31 + README.md | 36 +- example/demo.go | 44 ++ go.mod | 3 + hnsw.go | 100 +++ hnsw_wrapper.cc | 59 ++ hnsw_wrapper.h | 14 + hnsw_wrapper.o | Bin 0 -> 82808 bytes hnswlib/bruteforce.h | 152 +++++ hnswlib/hnswalg.h | 1192 +++++++++++++++++++++++++++++++++++ hnswlib/hnswlib.h | 108 ++++ hnswlib/space_ip.h | 282 +++++++++ hnswlib/space_l2.h | 281 +++++++++ hnswlib/visited_list_pool.h | 78 +++ libhnsw.a | Bin 0 -> 89536 bytes 17 files changed, 2430 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 Makefile create mode 100644 example/demo.go create mode 100644 go.mod create mode 100644 hnsw.go create mode 100644 hnsw_wrapper.cc create mode 100644 hnsw_wrapper.h create mode 100644 hnsw_wrapper.o create mode 100644 hnswlib/bruteforce.h create mode 100644 hnswlib/hnswalg.h create mode 100644 hnswlib/hnswlib.h create mode 100644 hnswlib/space_ip.h create mode 100644 hnswlib/space_l2.h create mode 100644 hnswlib/visited_list_pool.h create mode 100644 libhnsw.a diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9410e73 --- /dev/null +++ b/.gitignore @@ -0,0 +1,48 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..1052d2b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "hnswlib"] + path = hnswlib + url = https://github.com/nmslib/hnswlib.git diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..14db09a --- /dev/null +++ b/Makefile @@ -0,0 +1,31 @@ +# +# Copyright (c) 2016-present, Facebook, Inc. +# All rights reserved. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. +# +CXX = c++ +INCLUDES = -I. +CXXFLAGS = -pthread -std=c++0x -march=native -std=c++11 $(INCLUDES) +OBJS = hnsw_wrapper.o + +opt: CXXFLAGS += -O3 -funroll-loops +opt: build + +coverage: CXXFLAGS += -O0 -fno-inline -fprofile-arcs --coverage + +hnsw_wrapper.o: hnsw_wrapper.h hnsw_wrapper.cc hnswlib/*.h + $(CXX) $(CXXFLAGS) -c hnsw_wrapper.cc + +libhnsw.a: $(OBJS) + $(AR) rcs libhnsw.a $(OBJS) + +clean: + rm -rf *.o libhnsw.a *.o *.gcno *.gcda hnsw + +build: libhnsw.a + env CGO_CXXFLAGS="$(INCLUDES) -std=c++11" go build + +test: build + go test diff --git a/README.md b/README.md index 353de55..89e0506 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,36 @@ # hnswlib-to-go -Hnswlib to go. Golang interface to hnswlib(https://github.com/nmslib/hnswlib) +Hnswlib to go. Golang interface to hnswlib(https://github.com/nmslib/hnswlib). This is a golang interface of [hnswlib](https://github.com/nmslib/hnswlib). For more information, please follow [hnswlib](https://github.com/nmslib/hnswlib) and [Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs.](https://arxiv.org/abs/1603.09320). +**But in this project, we make compatible hnswlib to 0.5.2.** + + +### Version + +* version 1.0.0 + * hnswlib compatible version 0.5.2. + + +### Build + +* Linux/MacOS + * Build Golang Env + * `go mod init` + * `make` + +### Usage + +* When building golang program, please add `export CGO_CXXFLAGS=-std=c++11` command before `go build / run / test ...` + +| argument | type | | +| -------------- | ---- | ----- | +| dim | int | vector dimension | +| M | int | see[ALGO_PARAMS.md](https://github.com/nmslib/hnswlib/blob/master/ALGO_PARAMS.md) | +| efConstruction | int | see[ALGO_PARAMS.md](https://github.com/nmslib/hnswlib/blob/master/ALGO_PARAMS.md) | +| randomSeed | int | random seed for hnsw | +| maxElements | int | max records in data | +| spaceType | str | | + +| spaceType | distance | +| --------- |:-----------------:| +| ip | inner product | +| cosine | cosine similarity | +| l2 | l2 | diff --git a/example/demo.go b/example/demo.go new file mode 100644 index 0000000..15a3c03 --- /dev/null +++ b/example/demo.go @@ -0,0 +1,44 @@ +package main + +import ( + "fmt" + "math/rand" + "time" + + hnswgo "github.com/sunhailin-Leo/hnswlib-to-go" +) + +func randVector(dim int) []float32 { + vec := make([]float32, dim) + for j := 0; j < dim; j++ { + vec[j] = rand.Float32() + } + return vec +} + +func main() { + var dim, M, ef int = 128, 32, 300 + // Max elements + var maxElements uint32 = 1000 + // Distance cosine + var spaceType, indexLocation string = "cosine", "hnsw_demo_index.bin" + var randomSeed int = 100 + // Init new index + h := hnswgo.New(dim, M, ef, randomSeed, maxElements, spaceType) + // Insert 1000 vectors to index. Label Type is uint32 + var i uint32 + for ; i < maxElements; i++ { + h.AddPoint(randVector(dim), i) + } + h.Save(indexLocation) + h = hnswgo.Load(indexLocation, dim, spaceType) + // Search vector with maximum 5 NN + h.SetEf(15) + searchVector := randVector(dim) + // Count query time + startTime := time.Now().UnixNano() + labels, vectors := h.SearchKNN(searchVector, 5) + endTime := time.Now().UnixNano() + fmt.Println(endTime - startTime) + fmt.Println(labels, vectors) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..eda0b84 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/sunhailin-Leo/hnswlib-to-go + +go 1.15 diff --git a/hnsw.go b/hnsw.go new file mode 100644 index 0000000..085d10f --- /dev/null +++ b/hnsw.go @@ -0,0 +1,100 @@ +package hnswgo + +// #cgo LDFLAGS: -L${SRCDIR} -lhnsw -lm +// #include +// #include "hnsw_wrapper.h" +// HNSW initHNSW(int dim, unsigned long int max_elements, int M, int ef_construction, int rand_seed, char stype); +// HNSW loadHNSW(char *location, int dim, char stype); +// void addPoint(HNSW index, float *vec, unsigned long int label); +// int searchKnn(HNSW index, float *vec, int N, unsigned long int *label, float *dist); +// void setEf(HNSW index, int ef); +import "C" +import ( + "math" + "unsafe" +) + +type HNSW struct { + index C.HNSW + spaceType string + dim int + normalize bool +} + +func New(dim, M, efConstruction, randSeed int, maxElements uint32, spaceType string) *HNSW { + var hnsw HNSW + hnsw.dim = dim + hnsw.spaceType = spaceType + if spaceType == "ip" { + hnsw.index = C.initHNSW(C.int(dim), C.ulong(maxElements), C.int(M), C.int(efConstruction), C.int(randSeed), C.char('i')) + } else if spaceType == "cosine" { + hnsw.normalize = true + hnsw.index = C.initHNSW(C.int(dim), C.ulong(maxElements), C.int(M), C.int(efConstruction), C.int(randSeed), C.char('i')) + } else { + hnsw.index = C.initHNSW(C.int(dim), C.ulong(maxElements), C.int(M), C.int(efConstruction), C.int(randSeed), C.char('l')) + } + return &hnsw +} + +func Load(location string, dim int, spaceType string) *HNSW { + var hnsw HNSW + hnsw.dim = dim + hnsw.spaceType = spaceType + + pLocation := C.CString(location) + if spaceType == "ip" { + hnsw.index = C.loadHNSW(pLocation, C.int(dim), C.char('i')) + } else if spaceType == "cosine" { + hnsw.normalize = true + hnsw.index = C.loadHNSW(pLocation, C.int(dim), C.char('i')) + } else { + hnsw.index = C.loadHNSW(pLocation, C.int(dim), C.char('l')) + } + C.free(unsafe.Pointer(pLocation)) + return &hnsw +} + +func (h *HNSW) Save(location string) { + pLocation := C.CString(location) + C.saveHNSW(h.index, pLocation) + C.free(unsafe.Pointer(pLocation)) +} + +func normalizeVector(vector []float32) []float32 { + var norm float32 + for i := 0; i < len(vector); i++ { + norm += vector[i] * vector[i] + } + norm = 1.0 / (float32(math.Sqrt(float64(norm))) + 1e-15) + for i := 0; i < len(vector); i++ { + vector[i] = vector[i] * norm + } + return vector +} + +func (h *HNSW) AddPoint(vector []float32, label uint32) { + if h.normalize { + vector = normalizeVector(vector) + } + C.addPoint(h.index, (*C.float)(unsafe.Pointer(&vector[0])), C.ulong(label)) +} + +func (h *HNSW) SearchKNN(vector []float32, N int) ([]uint32, []float32) { + Clabel := make([]C.ulong, N, N) + Cdist := make([]C.float, N, N) + if h.normalize { + vector = normalizeVector(vector) + } + numResult := int(C.searchKnn(h.index, (*C.float)(unsafe.Pointer(&vector[0])), C.int(N), &Clabel[0], &Cdist[0])) + labels := make([]uint32, N) + dists := make([]float32, N) + for i := 0; i < numResult; i++ { + labels[i] = uint32(Clabel[i]) + dists[i] = float32(Cdist[i]) + } + return labels[:numResult], dists[:numResult] +} + +func (h *HNSW) SetEf(ef int) { + C.setEf(h.index, C.int(ef)) +} diff --git a/hnsw_wrapper.cc b/hnsw_wrapper.cc new file mode 100644 index 0000000..17f5c18 --- /dev/null +++ b/hnsw_wrapper.cc @@ -0,0 +1,59 @@ +//hnsw_wrapper.cpp +#include +#include "hnswlib/hnswlib.h" +#include "hnsw_wrapper.h" +#include +#include + +HNSW initHNSW(int dim, unsigned long int max_elements, int M, int ef_construction, int rand_seed, char stype) { + hnswlib::SpaceInterface *space; + if (stype == 'i') { + space = new hnswlib::InnerProductSpace(dim); + } else { + space = new hnswlib::L2Space(dim); + } + hnswlib::HierarchicalNSW *appr_alg = new hnswlib::HierarchicalNSW(space, max_elements, M, ef_construction, rand_seed); + return (void*)appr_alg; +} + +HNSW loadHNSW(char *location, int dim, char stype) { + hnswlib::SpaceInterface *space; + if (stype == 'i') { + space = new hnswlib::InnerProductSpace(dim); + } else { + space = new hnswlib::L2Space(dim); + } + hnswlib::HierarchicalNSW *appr_alg = new hnswlib::HierarchicalNSW(space, std::string(location), false, 0); + return (void*)appr_alg; +} + +HNSW saveHNSW(HNSW index, char *location) { + ((hnswlib::HierarchicalNSW*)index)->saveIndex(location); +} + +void addPoint(HNSW index, float *vec, unsigned long int label) { + ((hnswlib::HierarchicalNSW*)index)->addPoint(vec, label); +} + +int searchKnn(HNSW index, float *vec, int N, unsigned long int *label, float *dist) { + std::priority_queue> gt; + try { + gt = ((hnswlib::HierarchicalNSW*)index)->searchKnn(vec, N); + } catch (const std::exception& e) { + return 0; + } + + int n = gt.size(); + std::pair pair; + for (int i = n - 1; i >= 0; i--) { + pair = gt.top(); + *(dist+i) = pair.first; + *(label+i) = pair.second; + gt.pop(); + } + return n; +} + +void setEf(HNSW index, int ef) { + ((hnswlib::HierarchicalNSW*)index)->ef_ = ef; +} diff --git a/hnsw_wrapper.h b/hnsw_wrapper.h new file mode 100644 index 0000000..9aeab15 --- /dev/null +++ b/hnsw_wrapper.h @@ -0,0 +1,14 @@ +// hnsw_wrapper.h +#ifdef __cplusplus +extern "C" { +#endif + typedef void* HNSW; + HNSW initHNSW(int dim, unsigned long int max_elements, int M, int ef_construction, int rand_seed, char stype); + HNSW loadHNSW(char *location, int dim, char stype); + HNSW saveHNSW(HNSW index, char *location); + void addPoint(HNSW index, float *vec, unsigned long int label); + int searchKnn(HNSW index, float *vec, int N, unsigned long int *label, float *dist); + void setEf(HNSW index, int ef); +#ifdef __cplusplus +} +#endif diff --git a/hnsw_wrapper.o b/hnsw_wrapper.o new file mode 100644 index 0000000000000000000000000000000000000000..d986021c9df71ea8affe2fc8af3031f1de2303bc GIT binary patch literal 82808 zcmeFa4SZD9nfRX}0|bcNK~ZC+5^Z)j(bgpuDh3%lE{?6y{_qBD??fEI(gidyBRtul_6qs%--#9h2tSnGo#t;8N{twAVHUIow#Q$RX zeviMNS9L^%WaQ8Prpz+=A1;CX&7U8?ZADza>+f07IkSr2{@afp(Qi_o6xo|AE4xza z50d@lv z{P}-+<{(P6tt;y!ub5B&V0R*(+~>SHKp^^FXLV zMtb4?qfp)i9ddekp-vrM?PaLyDJv^&Q)$_fWd8hxU;M(d;(*Mb-xQe@@iOxm=asj> zD`bLKo(cTs6q6O+&Ma@>c&w*?@;6qHjK2+!>*ss@Qr@!o(%ZlAc`w1~{k@;|$lrBd z`yPBkfBU~O)}?>azB`tDc42&e;tMUef8lfU=i5!C@qJ!<7L3rL{ZDH6Ra6AZzOI7? z6ghwXZHwnGTKd^LZqt#cw{OOCI($V)Kj*xt%UfTm@sDa2>lpp}aUNCtYw!?Tt{Wni zO85ry?c=%jLxHls+Ojfal8yJtRe2PPesiN9&2(6XQ(x90@U#nULieW=efo&h&~+ zM!a!TdHlvm@>FQ~JTD@*UKy#PT# zlhx`N z`VHOb+?k#39N6hQ==7_q)314*4t})Ea)+3y1Ryz@pHX#sqX(j8p_garo`3YFW z(;aHMW%Kg;jj?Co>x39H+Ihls7@kw+!d<32mXbG`&bIu_3V^)PbfcS0Cu7zfGc(zs z>BM^5cN+J0GMCI$OUO<&S2K%xAr!Pa+LIgKb$nF7c5WmuA#Sv1k@~yRhp&~g@@6vK zXC;pXEQgZ5LP@S+x;IxeI`-F2-r&r7q8|*g>fW`I?~Ypeq-(e{`kV_Lv(I#=Q|gR< z)493Abj*I@tK;`SER>Vi`jwNrj)#VHX1)@t=<2_+r{zL^Ge(C{R&v&w&f8{qdeySA z4-09voo%EZ&i_!I>~r~iK6gHe1_s9nwrQK~F4d(Di-n&x?)e14R_5KHm5RL!SgxOz z8dGN_x4&!Ue`W1CVLHbO^ucs?o6a8hT;~GYSzcjl zDbs7C?QCyZ%MUGG)|&lmV$SH+assn8bi!=U#|P`F;L4QjoKP~6lE(uHK+>(^>(he2 z>NWSYA83qF__YuU+0g@_=WW;cHmIxr=Cs+KHrhS{uzx8-RR5GQmX((#SAM(e?OQ6p z)pdMM#@Ut6nBm@*EWzjOgmvqBdv1E(_C<>d_@c_&(nnMIyWafBATt&0P^E|ajWwU7 zVEL%u>El<|$xx-i&n%<5>kqe7j_f))C)4%%E%wN2c>~Y}uxnt^YB0mwmhR~~enAfn z+1dGZgn0ZX-EX%Fyuc57Fc9P*>kABgp|msU%Bj``LP4>{H{z-e{;k4d{>U z4v#rKR&sye)KsN67&N@WAB?9;`Y+>gp^V3cYCJCR$KwKTJc1WO4Hyr`B;2!fkCi-N z7;WPyX=-Zpny$W^cRs3+Ctn!f
N?0W5%(GPXKJ!cnma)%W@){@NyC9N8#<8z*p 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 NfhoCxN!b}f{3lDbnl=Cc literal 0 HcmV?d00001 diff --git a/hnswlib/bruteforce.h b/hnswlib/bruteforce.h new file mode 100644 index 0000000..2426040 --- /dev/null +++ b/hnswlib/bruteforce.h @@ -0,0 +1,152 @@ +#pragma once +#include +#include +#include +#include + +namespace hnswlib { + template + class BruteforceSearch : public AlgorithmInterface { + public: + BruteforceSearch(SpaceInterface *s) { + + } + BruteforceSearch(SpaceInterface *s, const std::string &location) { + loadIndex(location, s); + } + + BruteforceSearch(SpaceInterface *s, size_t maxElements) { + maxelements_ = maxElements; + data_size_ = s->get_data_size(); + fstdistfunc_ = s->get_dist_func(); + dist_func_param_ = s->get_dist_func_param(); + size_per_element_ = data_size_ + sizeof(labeltype); + data_ = (char *) malloc(maxElements * size_per_element_); + if (data_ == nullptr) + std::runtime_error("Not enough memory: BruteforceSearch failed to allocate data"); + cur_element_count = 0; + } + + ~BruteforceSearch() { + free(data_); + } + + char *data_; + size_t maxelements_; + size_t cur_element_count; + size_t size_per_element_; + + size_t data_size_; + DISTFUNC fstdistfunc_; + void *dist_func_param_; + std::mutex index_lock; + + std::unordered_map dict_external_to_internal; + + void addPoint(const void *datapoint, labeltype label) { + + int idx; + { + std::unique_lock lock(index_lock); + + + + auto search=dict_external_to_internal.find(label); + if (search != dict_external_to_internal.end()) { + idx=search->second; + } + else{ + if (cur_element_count >= maxelements_) { + throw std::runtime_error("The number of elements exceeds the specified limit\n"); + } + idx=cur_element_count; + dict_external_to_internal[label] = idx; + cur_element_count++; + } + } + memcpy(data_ + size_per_element_ * idx + data_size_, &label, sizeof(labeltype)); + memcpy(data_ + size_per_element_ * idx, datapoint, data_size_); + + + + + }; + + void removePoint(labeltype cur_external) { + size_t cur_c=dict_external_to_internal[cur_external]; + + dict_external_to_internal.erase(cur_external); + + labeltype label=*((labeltype*)(data_ + size_per_element_ * (cur_element_count-1) + data_size_)); + dict_external_to_internal[label]=cur_c; + memcpy(data_ + size_per_element_ * cur_c, + data_ + size_per_element_ * (cur_element_count-1), + data_size_+sizeof(labeltype)); + cur_element_count--; + + } + + + std::priority_queue> + searchKnn(const void *query_data, size_t k) const { + std::priority_queue> topResults; + if (cur_element_count == 0) return topResults; + for (int i = 0; i < k; i++) { + dist_t dist = fstdistfunc_(query_data, data_ + size_per_element_ * i, dist_func_param_); + topResults.push(std::pair(dist, *((labeltype *) (data_ + size_per_element_ * i + + data_size_)))); + } + dist_t lastdist = topResults.top().first; + for (int i = k; i < cur_element_count; i++) { + dist_t dist = fstdistfunc_(query_data, data_ + size_per_element_ * i, dist_func_param_); + if (dist <= lastdist) { + topResults.push(std::pair(dist, *((labeltype *) (data_ + size_per_element_ * i + + data_size_)))); + if (topResults.size() > k) + topResults.pop(); + lastdist = topResults.top().first; + } + + } + return topResults; + }; + + void saveIndex(const std::string &location) { + std::ofstream output(location, std::ios::binary); + std::streampos position; + + writeBinaryPOD(output, maxelements_); + writeBinaryPOD(output, size_per_element_); + writeBinaryPOD(output, cur_element_count); + + output.write(data_, maxelements_ * size_per_element_); + + output.close(); + } + + void loadIndex(const std::string &location, SpaceInterface *s) { + + + std::ifstream input(location, std::ios::binary); + std::streampos position; + + readBinaryPOD(input, maxelements_); + readBinaryPOD(input, size_per_element_); + readBinaryPOD(input, cur_element_count); + + data_size_ = s->get_data_size(); + fstdistfunc_ = s->get_dist_func(); + dist_func_param_ = s->get_dist_func_param(); + size_per_element_ = data_size_ + sizeof(labeltype); + data_ = (char *) malloc(maxelements_ * size_per_element_); + if (data_ == nullptr) + std::runtime_error("Not enough memory: loadIndex failed to allocate data"); + + input.read(data_, maxelements_ * size_per_element_); + + input.close(); + + } + + }; +} diff --git a/hnswlib/hnswalg.h b/hnswlib/hnswalg.h new file mode 100644 index 0000000..f23c17d --- /dev/null +++ b/hnswlib/hnswalg.h @@ -0,0 +1,1192 @@ +#pragma once + +#include "visited_list_pool.h" +#include "hnswlib.h" +#include +#include +#include +#include +#include +#include + +namespace hnswlib { + typedef unsigned int tableint; + typedef unsigned int linklistsizeint; + + template + class HierarchicalNSW : public AlgorithmInterface { + public: + static const tableint max_update_element_locks = 65536; + HierarchicalNSW(SpaceInterface *s) { + + } + + HierarchicalNSW(SpaceInterface *s, const std::string &location, bool nmslib = false, size_t max_elements=0) { + loadIndex(location, s, max_elements); + } + + HierarchicalNSW(SpaceInterface *s, size_t max_elements, size_t M = 16, size_t ef_construction = 200, size_t random_seed = 100) : + link_list_locks_(max_elements), link_list_update_locks_(max_update_element_locks), element_levels_(max_elements) { + max_elements_ = max_elements; + + has_deletions_=false; + data_size_ = s->get_data_size(); + fstdistfunc_ = s->get_dist_func(); + dist_func_param_ = s->get_dist_func_param(); + M_ = M; + maxM_ = M_; + maxM0_ = M_ * 2; + ef_construction_ = std::max(ef_construction,M_); + ef_ = 10; + + level_generator_.seed(random_seed); + update_probability_generator_.seed(random_seed + 1); + + size_links_level0_ = maxM0_ * sizeof(tableint) + sizeof(linklistsizeint); + size_data_per_element_ = size_links_level0_ + data_size_ + sizeof(labeltype); + offsetData_ = size_links_level0_; + label_offset_ = size_links_level0_ + data_size_; + offsetLevel0_ = 0; + + data_level0_memory_ = (char *) malloc(max_elements_ * size_data_per_element_); + if (data_level0_memory_ == nullptr) + throw std::runtime_error("Not enough memory"); + + cur_element_count = 0; + + visited_list_pool_ = new VisitedListPool(1, max_elements); + + + + //initializations for special treatment of the first node + enterpoint_node_ = -1; + maxlevel_ = -1; + + linkLists_ = (char **) malloc(sizeof(void *) * max_elements_); + if (linkLists_ == nullptr) + throw std::runtime_error("Not enough memory: HierarchicalNSW failed to allocate linklists"); + size_links_per_element_ = maxM_ * sizeof(tableint) + sizeof(linklistsizeint); + mult_ = 1 / log(1.0 * M_); + revSize_ = 1.0 / mult_; + } + + struct CompareByFirst { + constexpr bool operator()(std::pair const &a, + std::pair const &b) const noexcept { + return a.first < b.first; + } + }; + + ~HierarchicalNSW() { + + free(data_level0_memory_); + for (tableint i = 0; i < cur_element_count; i++) { + if (element_levels_[i] > 0) + free(linkLists_[i]); + } + free(linkLists_); + delete visited_list_pool_; + } + + size_t max_elements_; + size_t cur_element_count; + size_t size_data_per_element_; + size_t size_links_per_element_; + + size_t M_; + size_t maxM_; + size_t maxM0_; + size_t ef_construction_; + + double mult_, revSize_; + int maxlevel_; + + + VisitedListPool *visited_list_pool_; + std::mutex cur_element_count_guard_; + + std::vector link_list_locks_; + + // Locks to prevent race condition during update/insert of an element at same time. + // Note: Locks for additions can also be used to prevent this race condition if the querying of KNN is not exposed along with update/inserts i.e multithread insert/update/query in parallel. + std::vector link_list_update_locks_; + tableint enterpoint_node_; + + + size_t size_links_level0_; + size_t offsetData_, offsetLevel0_; + + + char *data_level0_memory_; + char **linkLists_; + std::vector element_levels_; + + size_t data_size_; + + bool has_deletions_; + + + size_t label_offset_; + DISTFUNC fstdistfunc_; + void *dist_func_param_; + std::unordered_map label_lookup_; + + std::default_random_engine level_generator_; + std::default_random_engine update_probability_generator_; + + inline labeltype getExternalLabel(tableint internal_id) const { + labeltype return_label; + memcpy(&return_label,(data_level0_memory_ + internal_id * size_data_per_element_ + label_offset_), sizeof(labeltype)); + return return_label; + } + + inline void setExternalLabel(tableint internal_id, labeltype label) const { + memcpy((data_level0_memory_ + internal_id * size_data_per_element_ + label_offset_), &label, sizeof(labeltype)); + } + + inline labeltype *getExternalLabeLp(tableint internal_id) const { + return (labeltype *) (data_level0_memory_ + internal_id * size_data_per_element_ + label_offset_); + } + + inline char *getDataByInternalId(tableint internal_id) const { + return (data_level0_memory_ + internal_id * size_data_per_element_ + offsetData_); + } + + int getRandomLevel(double reverse_size) { + std::uniform_real_distribution distribution(0.0, 1.0); + double r = -log(distribution(level_generator_)) * reverse_size; + return (int) r; + } + + + std::priority_queue, std::vector>, CompareByFirst> + searchBaseLayer(tableint ep_id, const void *data_point, int layer) { + VisitedList *vl = visited_list_pool_->getFreeVisitedList(); + vl_type *visited_array = vl->mass; + vl_type visited_array_tag = vl->curV; + + std::priority_queue, std::vector>, CompareByFirst> top_candidates; + std::priority_queue, std::vector>, CompareByFirst> candidateSet; + + dist_t lowerBound; + if (!isMarkedDeleted(ep_id)) { + dist_t dist = fstdistfunc_(data_point, getDataByInternalId(ep_id), dist_func_param_); + top_candidates.emplace(dist, ep_id); + lowerBound = dist; + candidateSet.emplace(-dist, ep_id); + } else { + lowerBound = std::numeric_limits::max(); + candidateSet.emplace(-lowerBound, ep_id); + } + visited_array[ep_id] = visited_array_tag; + + while (!candidateSet.empty()) { + std::pair curr_el_pair = candidateSet.top(); + if ((-curr_el_pair.first) > lowerBound) { + break; + } + candidateSet.pop(); + + tableint curNodeNum = curr_el_pair.second; + + std::unique_lock lock(link_list_locks_[curNodeNum]); + + int *data;// = (int *)(linkList0_ + curNodeNum * size_links_per_element0_); + if (layer == 0) { + data = (int*)get_linklist0(curNodeNum); + } else { + data = (int*)get_linklist(curNodeNum, layer); +// data = (int *) (linkLists_[curNodeNum] + (layer - 1) * size_links_per_element_); + } + size_t size = getListCount((linklistsizeint*)data); + tableint *datal = (tableint *) (data + 1); +#ifdef USE_SSE + _mm_prefetch((char *) (visited_array + *(data + 1)), _MM_HINT_T0); + _mm_prefetch((char *) (visited_array + *(data + 1) + 64), _MM_HINT_T0); + _mm_prefetch(getDataByInternalId(*datal), _MM_HINT_T0); + _mm_prefetch(getDataByInternalId(*(datal + 1)), _MM_HINT_T0); +#endif + + for (size_t j = 0; j < size; j++) { + tableint candidate_id = *(datal + j); +// if (candidate_id == 0) continue; +#ifdef USE_SSE + _mm_prefetch((char *) (visited_array + *(datal + j + 1)), _MM_HINT_T0); + _mm_prefetch(getDataByInternalId(*(datal + j + 1)), _MM_HINT_T0); +#endif + if (visited_array[candidate_id] == visited_array_tag) continue; + visited_array[candidate_id] = visited_array_tag; + char *currObj1 = (getDataByInternalId(candidate_id)); + + dist_t dist1 = fstdistfunc_(data_point, currObj1, dist_func_param_); + if (top_candidates.size() < ef_construction_ || lowerBound > dist1) { + candidateSet.emplace(-dist1, candidate_id); +#ifdef USE_SSE + _mm_prefetch(getDataByInternalId(candidateSet.top().second), _MM_HINT_T0); +#endif + + if (!isMarkedDeleted(candidate_id)) + top_candidates.emplace(dist1, candidate_id); + + if (top_candidates.size() > ef_construction_) + top_candidates.pop(); + + if (!top_candidates.empty()) + lowerBound = top_candidates.top().first; + } + } + } + visited_list_pool_->releaseVisitedList(vl); + + return top_candidates; + } + + mutable std::atomic metric_distance_computations; + mutable std::atomic metric_hops; + + template + std::priority_queue, std::vector>, CompareByFirst> + searchBaseLayerST(tableint ep_id, const void *data_point, size_t ef) const { + VisitedList *vl = visited_list_pool_->getFreeVisitedList(); + vl_type *visited_array = vl->mass; + vl_type visited_array_tag = vl->curV; + + std::priority_queue, std::vector>, CompareByFirst> top_candidates; + std::priority_queue, std::vector>, CompareByFirst> candidate_set; + + dist_t lowerBound; + if (!has_deletions || !isMarkedDeleted(ep_id)) { + dist_t dist = fstdistfunc_(data_point, getDataByInternalId(ep_id), dist_func_param_); + lowerBound = dist; + top_candidates.emplace(dist, ep_id); + candidate_set.emplace(-dist, ep_id); + } else { + lowerBound = std::numeric_limits::max(); + candidate_set.emplace(-lowerBound, ep_id); + } + + visited_array[ep_id] = visited_array_tag; + + while (!candidate_set.empty()) { + + std::pair current_node_pair = candidate_set.top(); + + if ((-current_node_pair.first) > lowerBound) { + break; + } + candidate_set.pop(); + + tableint current_node_id = current_node_pair.second; + int *data = (int *) get_linklist0(current_node_id); + size_t size = getListCount((linklistsizeint*)data); +// bool cur_node_deleted = isMarkedDeleted(current_node_id); + if(collect_metrics){ + metric_hops++; + metric_distance_computations+=size; + } + +#ifdef USE_SSE + _mm_prefetch((char *) (visited_array + *(data + 1)), _MM_HINT_T0); + _mm_prefetch((char *) (visited_array + *(data + 1) + 64), _MM_HINT_T0); + _mm_prefetch(data_level0_memory_ + (*(data + 1)) * size_data_per_element_ + offsetData_, _MM_HINT_T0); + _mm_prefetch((char *) (data + 2), _MM_HINT_T0); +#endif + + for (size_t j = 1; j <= size; j++) { + int candidate_id = *(data + j); +// if (candidate_id == 0) continue; +#ifdef USE_SSE + _mm_prefetch((char *) (visited_array + *(data + j + 1)), _MM_HINT_T0); + _mm_prefetch(data_level0_memory_ + (*(data + j + 1)) * size_data_per_element_ + offsetData_, + _MM_HINT_T0);//////////// +#endif + if (!(visited_array[candidate_id] == visited_array_tag)) { + + visited_array[candidate_id] = visited_array_tag; + + char *currObj1 = (getDataByInternalId(candidate_id)); + dist_t dist = fstdistfunc_(data_point, currObj1, dist_func_param_); + + if (top_candidates.size() < ef || lowerBound > dist) { + candidate_set.emplace(-dist, candidate_id); +#ifdef USE_SSE + _mm_prefetch(data_level0_memory_ + candidate_set.top().second * size_data_per_element_ + + offsetLevel0_,/////////// + _MM_HINT_T0);//////////////////////// +#endif + + if (!has_deletions || !isMarkedDeleted(candidate_id)) + top_candidates.emplace(dist, candidate_id); + + if (top_candidates.size() > ef) + top_candidates.pop(); + + if (!top_candidates.empty()) + lowerBound = top_candidates.top().first; + } + } + } + } + + visited_list_pool_->releaseVisitedList(vl); + return top_candidates; + } + + void getNeighborsByHeuristic2( + std::priority_queue, std::vector>, CompareByFirst> &top_candidates, + const size_t M) { + if (top_candidates.size() < M) { + return; + } + + std::priority_queue> queue_closest; + std::vector> return_list; + while (top_candidates.size() > 0) { + queue_closest.emplace(-top_candidates.top().first, top_candidates.top().second); + top_candidates.pop(); + } + + while (queue_closest.size()) { + if (return_list.size() >= M) + break; + std::pair curent_pair = queue_closest.top(); + dist_t dist_to_query = -curent_pair.first; + queue_closest.pop(); + bool good = true; + + for (std::pair second_pair : return_list) { + dist_t curdist = + fstdistfunc_(getDataByInternalId(second_pair.second), + getDataByInternalId(curent_pair.second), + dist_func_param_);; + if (curdist < dist_to_query) { + good = false; + break; + } + } + if (good) { + return_list.push_back(curent_pair); + } + } + + for (std::pair curent_pair : return_list) { + top_candidates.emplace(-curent_pair.first, curent_pair.second); + } + } + + + linklistsizeint *get_linklist0(tableint internal_id) const { + return (linklistsizeint *) (data_level0_memory_ + internal_id * size_data_per_element_ + offsetLevel0_); + }; + + linklistsizeint *get_linklist0(tableint internal_id, char *data_level0_memory_) const { + return (linklistsizeint *) (data_level0_memory_ + internal_id * size_data_per_element_ + offsetLevel0_); + }; + + linklistsizeint *get_linklist(tableint internal_id, int level) const { + return (linklistsizeint *) (linkLists_[internal_id] + (level - 1) * size_links_per_element_); + }; + + linklistsizeint *get_linklist_at_level(tableint internal_id, int level) const { + return level == 0 ? get_linklist0(internal_id) : get_linklist(internal_id, level); + }; + + tableint mutuallyConnectNewElement(const void *data_point, tableint cur_c, + std::priority_queue, std::vector>, CompareByFirst> &top_candidates, + int level, bool isUpdate) { + size_t Mcurmax = level ? maxM_ : maxM0_; + getNeighborsByHeuristic2(top_candidates, M_); + if (top_candidates.size() > M_) + throw std::runtime_error("Should be not be more than M_ candidates returned by the heuristic"); + + std::vector selectedNeighbors; + selectedNeighbors.reserve(M_); + while (top_candidates.size() > 0) { + selectedNeighbors.push_back(top_candidates.top().second); + top_candidates.pop(); + } + + tableint next_closest_entry_point = selectedNeighbors.back(); + + { + linklistsizeint *ll_cur; + if (level == 0) + ll_cur = get_linklist0(cur_c); + else + ll_cur = get_linklist(cur_c, level); + + if (*ll_cur && !isUpdate) { + throw std::runtime_error("The newly inserted element should have blank link list"); + } + setListCount(ll_cur,selectedNeighbors.size()); + tableint *data = (tableint *) (ll_cur + 1); + for (size_t idx = 0; idx < selectedNeighbors.size(); idx++) { + if (data[idx] && !isUpdate) + throw std::runtime_error("Possible memory corruption"); + if (level > element_levels_[selectedNeighbors[idx]]) + throw std::runtime_error("Trying to make a link on a non-existent level"); + + data[idx] = selectedNeighbors[idx]; + + } + } + + for (size_t idx = 0; idx < selectedNeighbors.size(); idx++) { + + std::unique_lock lock(link_list_locks_[selectedNeighbors[idx]]); + + linklistsizeint *ll_other; + if (level == 0) + ll_other = get_linklist0(selectedNeighbors[idx]); + else + ll_other = get_linklist(selectedNeighbors[idx], level); + + size_t sz_link_list_other = getListCount(ll_other); + + if (sz_link_list_other > Mcurmax) + throw std::runtime_error("Bad value of sz_link_list_other"); + if (selectedNeighbors[idx] == cur_c) + throw std::runtime_error("Trying to connect an element to itself"); + if (level > element_levels_[selectedNeighbors[idx]]) + throw std::runtime_error("Trying to make a link on a non-existent level"); + + tableint *data = (tableint *) (ll_other + 1); + + bool is_cur_c_present = false; + if (isUpdate) { + for (size_t j = 0; j < sz_link_list_other; j++) { + if (data[j] == cur_c) { + is_cur_c_present = true; + break; + } + } + } + + // If cur_c is already present in the neighboring connections of `selectedNeighbors[idx]` then no need to modify any connections or run the heuristics. + if (!is_cur_c_present) { + if (sz_link_list_other < Mcurmax) { + data[sz_link_list_other] = cur_c; + setListCount(ll_other, sz_link_list_other + 1); + } else { + // finding the "weakest" element to replace it with the new one + dist_t d_max = fstdistfunc_(getDataByInternalId(cur_c), getDataByInternalId(selectedNeighbors[idx]), + dist_func_param_); + // Heuristic: + std::priority_queue, std::vector>, CompareByFirst> candidates; + candidates.emplace(d_max, cur_c); + + for (size_t j = 0; j < sz_link_list_other; j++) { + candidates.emplace( + fstdistfunc_(getDataByInternalId(data[j]), getDataByInternalId(selectedNeighbors[idx]), + dist_func_param_), data[j]); + } + + getNeighborsByHeuristic2(candidates, Mcurmax); + + int indx = 0; + while (candidates.size() > 0) { + data[indx] = candidates.top().second; + candidates.pop(); + indx++; + } + + setListCount(ll_other, indx); + // Nearest K: + /*int indx = -1; + for (int j = 0; j < sz_link_list_other; j++) { + dist_t d = fstdistfunc_(getDataByInternalId(data[j]), getDataByInternalId(rez[idx]), dist_func_param_); + if (d > d_max) { + indx = j; + d_max = d; + } + } + if (indx >= 0) { + data[indx] = cur_c; + } */ + } + } + } + + return next_closest_entry_point; + } + + std::mutex global; + size_t ef_; + + void setEf(size_t ef) { + ef_ = ef; + } + + + std::priority_queue> searchKnnInternal(void *query_data, int k) { + std::priority_queue> top_candidates; + if (cur_element_count == 0) return top_candidates; + tableint currObj = enterpoint_node_; + dist_t curdist = fstdistfunc_(query_data, getDataByInternalId(enterpoint_node_), dist_func_param_); + + for (size_t level = maxlevel_; level > 0; level--) { + bool changed = true; + while (changed) { + changed = false; + int *data; + data = (int *) get_linklist(currObj,level); + int size = getListCount(data); + tableint *datal = (tableint *) (data + 1); + for (int i = 0; i < size; i++) { + tableint cand = datal[i]; + if (cand < 0 || cand > max_elements_) + throw std::runtime_error("cand error"); + dist_t d = fstdistfunc_(query_data, getDataByInternalId(cand), dist_func_param_); + + if (d < curdist) { + curdist = d; + currObj = cand; + changed = true; + } + } + } + } + + if (has_deletions_) { + std::priority_queue> top_candidates1=searchBaseLayerST(currObj, query_data, + ef_); + top_candidates.swap(top_candidates1); + } + else{ + std::priority_queue> top_candidates1=searchBaseLayerST(currObj, query_data, + ef_); + top_candidates.swap(top_candidates1); + } + + while (top_candidates.size() > k) { + top_candidates.pop(); + } + return top_candidates; + }; + + void resizeIndex(size_t new_max_elements){ + if (new_max_elements(new_max_elements).swap(link_list_locks_); + + // Reallocate base layer + char * data_level0_memory_new = (char *) realloc(data_level0_memory_, new_max_elements * size_data_per_element_); + if (data_level0_memory_new == nullptr) + throw std::runtime_error("Not enough memory: resizeIndex failed to allocate base layer"); + data_level0_memory_ = data_level0_memory_new; + + // Reallocate all other layers + char ** linkLists_new = (char **) realloc(linkLists_, sizeof(void *) * new_max_elements); + if (linkLists_new == nullptr) + throw std::runtime_error("Not enough memory: resizeIndex failed to allocate other layers"); + linkLists_ = linkLists_new; + + max_elements_ = new_max_elements; + } + + void saveIndex(const std::string &location) { + std::ofstream output(location, std::ios::binary); + std::streampos position; + + writeBinaryPOD(output, offsetLevel0_); + writeBinaryPOD(output, max_elements_); + writeBinaryPOD(output, cur_element_count); + writeBinaryPOD(output, size_data_per_element_); + writeBinaryPOD(output, label_offset_); + writeBinaryPOD(output, offsetData_); + writeBinaryPOD(output, maxlevel_); + writeBinaryPOD(output, enterpoint_node_); + writeBinaryPOD(output, maxM_); + + writeBinaryPOD(output, maxM0_); + writeBinaryPOD(output, M_); + writeBinaryPOD(output, mult_); + writeBinaryPOD(output, ef_construction_); + + output.write(data_level0_memory_, cur_element_count * size_data_per_element_); + + for (size_t i = 0; i < cur_element_count; i++) { + unsigned int linkListSize = element_levels_[i] > 0 ? size_links_per_element_ * element_levels_[i] : 0; + writeBinaryPOD(output, linkListSize); + if (linkListSize) + output.write(linkLists_[i], linkListSize); + } + output.close(); + } + + void loadIndex(const std::string &location, SpaceInterface *s, size_t max_elements_i=0) { + + + std::ifstream input(location, std::ios::binary); + + if (!input.is_open()) + throw std::runtime_error("Cannot open file"); + + // get file size: + input.seekg(0,input.end); + std::streampos total_filesize=input.tellg(); + input.seekg(0,input.beg); + + readBinaryPOD(input, offsetLevel0_); + readBinaryPOD(input, max_elements_); + readBinaryPOD(input, cur_element_count); + + size_t max_elements=max_elements_i; + if(max_elements < cur_element_count) + max_elements = max_elements_; + max_elements_ = max_elements; + readBinaryPOD(input, size_data_per_element_); + readBinaryPOD(input, label_offset_); + readBinaryPOD(input, offsetData_); + readBinaryPOD(input, maxlevel_); + readBinaryPOD(input, enterpoint_node_); + + readBinaryPOD(input, maxM_); + readBinaryPOD(input, maxM0_); + readBinaryPOD(input, M_); + readBinaryPOD(input, mult_); + readBinaryPOD(input, ef_construction_); + + + data_size_ = s->get_data_size(); + fstdistfunc_ = s->get_dist_func(); + dist_func_param_ = s->get_dist_func_param(); + + auto pos=input.tellg(); + + + /// Optional - check if index is ok: + + input.seekg(cur_element_count * size_data_per_element_,input.cur); + for (size_t i = 0; i < cur_element_count; i++) { + if(input.tellg() < 0 || input.tellg()>=total_filesize){ + throw std::runtime_error("Index seems to be corrupted or unsupported"); + } + + unsigned int linkListSize; + readBinaryPOD(input, linkListSize); + if (linkListSize != 0) { + input.seekg(linkListSize,input.cur); + } + } + + // throw exception if it either corrupted or old index + if(input.tellg()!=total_filesize) + throw std::runtime_error("Index seems to be corrupted or unsupported"); + + input.clear(); + + /// Optional check end + + input.seekg(pos,input.beg); + + + data_level0_memory_ = (char *) malloc(max_elements * size_data_per_element_); + if (data_level0_memory_ == nullptr) + throw std::runtime_error("Not enough memory: loadIndex failed to allocate level0"); + input.read(data_level0_memory_, cur_element_count * size_data_per_element_); + + + + + size_links_per_element_ = maxM_ * sizeof(tableint) + sizeof(linklistsizeint); + + + size_links_level0_ = maxM0_ * sizeof(tableint) + sizeof(linklistsizeint); + std::vector(max_elements).swap(link_list_locks_); + std::vector(max_update_element_locks).swap(link_list_update_locks_); + + + visited_list_pool_ = new VisitedListPool(1, max_elements); + + + linkLists_ = (char **) malloc(sizeof(void *) * max_elements); + if (linkLists_ == nullptr) + throw std::runtime_error("Not enough memory: loadIndex failed to allocate linklists"); + element_levels_ = std::vector(max_elements); + revSize_ = 1.0 / mult_; + ef_ = 10; + for (size_t i = 0; i < cur_element_count; i++) { + label_lookup_[getExternalLabel(i)]=i; + unsigned int linkListSize; + readBinaryPOD(input, linkListSize); + if (linkListSize == 0) { + element_levels_[i] = 0; + + linkLists_[i] = nullptr; + } else { + element_levels_[i] = linkListSize / size_links_per_element_; + linkLists_[i] = (char *) malloc(linkListSize); + if (linkLists_[i] == nullptr) + throw std::runtime_error("Not enough memory: loadIndex failed to allocate linklist"); + input.read(linkLists_[i], linkListSize); + } + } + + has_deletions_=false; + + for (size_t i = 0; i < cur_element_count; i++) { + if(isMarkedDeleted(i)) + has_deletions_=true; + } + + input.close(); + + return; + } + + template + std::vector getDataByLabel(labeltype label) + { + tableint label_c; + auto search = label_lookup_.find(label); + if (search == label_lookup_.end() || isMarkedDeleted(search->second)) { + throw std::runtime_error("Label not found"); + } + label_c = search->second; + + char* data_ptrv = getDataByInternalId(label_c); + size_t dim = *((size_t *) dist_func_param_); + std::vector data; + data_t* data_ptr = (data_t*) data_ptrv; + for (int i = 0; i < dim; i++) { + data.push_back(*data_ptr); + data_ptr += 1; + } + return data; + } + + static const unsigned char DELETE_MARK = 0x01; +// static const unsigned char REUSE_MARK = 0x10; + /** + * Marks an element with the given label deleted, does NOT really change the current graph. + * @param label + */ + void markDelete(labeltype label) + { + has_deletions_=true; + auto search = label_lookup_.find(label); + if (search == label_lookup_.end()) { + throw std::runtime_error("Label not found"); + } + markDeletedInternal(search->second); + } + + /** + * Uses the first 8 bits of the memory for the linked list to store the mark, + * whereas maxM0_ has to be limited to the lower 24 bits, however, still large enough in almost all cases. + * @param internalId + */ + void markDeletedInternal(tableint internalId) { + unsigned char *ll_cur = ((unsigned char *)get_linklist0(internalId))+2; + *ll_cur |= DELETE_MARK; + } + + /** + * Remove the deleted mark of the node. + * @param internalId + */ + void unmarkDeletedInternal(tableint internalId) { + unsigned char *ll_cur = ((unsigned char *)get_linklist0(internalId))+2; + *ll_cur &= ~DELETE_MARK; + } + + /** + * Checks the first 8 bits of the memory to see if the element is marked deleted. + * @param internalId + * @return + */ + bool isMarkedDeleted(tableint internalId) const { + unsigned char *ll_cur = ((unsigned char*)get_linklist0(internalId))+2; + return *ll_cur & DELETE_MARK; + } + + unsigned short int getListCount(linklistsizeint * ptr) const { + return *((unsigned short int *)ptr); + } + + void setListCount(linklistsizeint * ptr, unsigned short int size) const { + *((unsigned short int*)(ptr))=*((unsigned short int *)&size); + } + + void addPoint(const void *data_point, labeltype label) { + addPoint(data_point, label,-1); + } + + void updatePoint(const void *dataPoint, tableint internalId, float updateNeighborProbability) { + // update the feature vector associated with existing point with new vector + memcpy(getDataByInternalId(internalId), dataPoint, data_size_); + + int maxLevelCopy = maxlevel_; + tableint entryPointCopy = enterpoint_node_; + // If point to be updated is entry point and graph just contains single element then just return. + if (entryPointCopy == internalId && cur_element_count == 1) + return; + + int elemLevel = element_levels_[internalId]; + std::uniform_real_distribution distribution(0.0, 1.0); + for (int layer = 0; layer <= elemLevel; layer++) { + std::unordered_set sCand; + std::unordered_set sNeigh; + std::vector listOneHop = getConnectionsWithLock(internalId, layer); + if (listOneHop.size() == 0) + continue; + + sCand.insert(internalId); + + for (auto&& elOneHop : listOneHop) { + sCand.insert(elOneHop); + + if (distribution(update_probability_generator_) > updateNeighborProbability) + continue; + + sNeigh.insert(elOneHop); + + std::vector listTwoHop = getConnectionsWithLock(elOneHop, layer); + for (auto&& elTwoHop : listTwoHop) { + sCand.insert(elTwoHop); + } + } + + for (auto&& neigh : sNeigh) { +// if (neigh == internalId) +// continue; + + std::priority_queue, std::vector>, CompareByFirst> candidates; + size_t size = sCand.find(neigh) == sCand.end() ? sCand.size() : sCand.size() - 1; // sCand guaranteed to have size >= 1 + size_t elementsToKeep = std::min(ef_construction_, size); + for (auto&& cand : sCand) { + if (cand == neigh) + continue; + + dist_t distance = fstdistfunc_(getDataByInternalId(neigh), getDataByInternalId(cand), dist_func_param_); + if (candidates.size() < elementsToKeep) { + candidates.emplace(distance, cand); + } else { + if (distance < candidates.top().first) { + candidates.pop(); + candidates.emplace(distance, cand); + } + } + } + + // Retrieve neighbours using heuristic and set connections. + getNeighborsByHeuristic2(candidates, layer == 0 ? maxM0_ : maxM_); + + { + std::unique_lock lock(link_list_locks_[neigh]); + linklistsizeint *ll_cur; + ll_cur = get_linklist_at_level(neigh, layer); + size_t candSize = candidates.size(); + setListCount(ll_cur, candSize); + tableint *data = (tableint *) (ll_cur + 1); + for (size_t idx = 0; idx < candSize; idx++) { + data[idx] = candidates.top().second; + candidates.pop(); + } + } + } + } + + repairConnectionsForUpdate(dataPoint, entryPointCopy, internalId, elemLevel, maxLevelCopy); + }; + + void repairConnectionsForUpdate(const void *dataPoint, tableint entryPointInternalId, tableint dataPointInternalId, int dataPointLevel, int maxLevel) { + tableint currObj = entryPointInternalId; + if (dataPointLevel < maxLevel) { + dist_t curdist = fstdistfunc_(dataPoint, getDataByInternalId(currObj), dist_func_param_); + for (int level = maxLevel; level > dataPointLevel; level--) { + bool changed = true; + while (changed) { + changed = false; + unsigned int *data; + std::unique_lock lock(link_list_locks_[currObj]); + data = get_linklist_at_level(currObj,level); + int size = getListCount(data); + tableint *datal = (tableint *) (data + 1); +#ifdef USE_SSE + _mm_prefetch(getDataByInternalId(*datal), _MM_HINT_T0); +#endif + for (int i = 0; i < size; i++) { +#ifdef USE_SSE + _mm_prefetch(getDataByInternalId(*(datal + i + 1)), _MM_HINT_T0); +#endif + tableint cand = datal[i]; + dist_t d = fstdistfunc_(dataPoint, getDataByInternalId(cand), dist_func_param_); + if (d < curdist) { + curdist = d; + currObj = cand; + changed = true; + } + } + } + } + } + + if (dataPointLevel > maxLevel) + throw std::runtime_error("Level of item to be updated cannot be bigger than max level"); + + for (int level = dataPointLevel; level >= 0; level--) { + std::priority_queue, std::vector>, CompareByFirst> topCandidates = searchBaseLayer( + currObj, dataPoint, level); + + std::priority_queue, std::vector>, CompareByFirst> filteredTopCandidates; + while (topCandidates.size() > 0) { + if (topCandidates.top().second != dataPointInternalId) + filteredTopCandidates.push(topCandidates.top()); + + topCandidates.pop(); + } + + // Since element_levels_ is being used to get `dataPointLevel`, there could be cases where `topCandidates` could just contains entry point itself. + // To prevent self loops, the `topCandidates` is filtered and thus can be empty. + if (filteredTopCandidates.size() > 0) { + bool epDeleted = isMarkedDeleted(entryPointInternalId); + if (epDeleted) { + filteredTopCandidates.emplace(fstdistfunc_(dataPoint, getDataByInternalId(entryPointInternalId), dist_func_param_), entryPointInternalId); + if (filteredTopCandidates.size() > ef_construction_) + filteredTopCandidates.pop(); + } + + currObj = mutuallyConnectNewElement(dataPoint, dataPointInternalId, filteredTopCandidates, level, true); + } + } + } + + std::vector getConnectionsWithLock(tableint internalId, int level) { + std::unique_lock lock(link_list_locks_[internalId]); + unsigned int *data = get_linklist_at_level(internalId, level); + int size = getListCount(data); + std::vector result(size); + tableint *ll = (tableint *) (data + 1); + memcpy(result.data(), ll,size * sizeof(tableint)); + return result; + }; + + tableint addPoint(const void *data_point, labeltype label, int level) { + + tableint cur_c = 0; + { + // Checking if the element with the same label already exists + // if so, updating it *instead* of creating a new element. + std::unique_lock templock_curr(cur_element_count_guard_); + auto search = label_lookup_.find(label); + if (search != label_lookup_.end()) { + tableint existingInternalId = search->second; + templock_curr.unlock(); + + std::unique_lock lock_el_update(link_list_update_locks_[(existingInternalId & (max_update_element_locks - 1))]); + + if (isMarkedDeleted(existingInternalId)) { + unmarkDeletedInternal(existingInternalId); + } + updatePoint(data_point, existingInternalId, 1.0); + + return existingInternalId; + } + + if (cur_element_count >= max_elements_) { + throw std::runtime_error("The number of elements exceeds the specified limit"); + }; + + cur_c = cur_element_count; + cur_element_count++; + label_lookup_[label] = cur_c; + } + + // Take update lock to prevent race conditions on an element with insertion/update at the same time. + std::unique_lock lock_el_update(link_list_update_locks_[(cur_c & (max_update_element_locks - 1))]); + std::unique_lock lock_el(link_list_locks_[cur_c]); + int curlevel = getRandomLevel(mult_); + if (level > 0) + curlevel = level; + + element_levels_[cur_c] = curlevel; + + + std::unique_lock templock(global); + int maxlevelcopy = maxlevel_; + if (curlevel <= maxlevelcopy) + templock.unlock(); + tableint currObj = enterpoint_node_; + tableint enterpoint_copy = enterpoint_node_; + + + memset(data_level0_memory_ + cur_c * size_data_per_element_ + offsetLevel0_, 0, size_data_per_element_); + + // Initialisation of the data and label + memcpy(getExternalLabeLp(cur_c), &label, sizeof(labeltype)); + memcpy(getDataByInternalId(cur_c), data_point, data_size_); + + + if (curlevel) { + linkLists_[cur_c] = (char *) malloc(size_links_per_element_ * curlevel + 1); + if (linkLists_[cur_c] == nullptr) + throw std::runtime_error("Not enough memory: addPoint failed to allocate linklist"); + memset(linkLists_[cur_c], 0, size_links_per_element_ * curlevel + 1); + } + + if ((signed)currObj != -1) { + + if (curlevel < maxlevelcopy) { + + dist_t curdist = fstdistfunc_(data_point, getDataByInternalId(currObj), dist_func_param_); + for (int level = maxlevelcopy; level > curlevel; level--) { + + + bool changed = true; + while (changed) { + changed = false; + unsigned int *data; + std::unique_lock lock(link_list_locks_[currObj]); + data = get_linklist(currObj,level); + int size = getListCount(data); + + tableint *datal = (tableint *) (data + 1); + for (int i = 0; i < size; i++) { + tableint cand = datal[i]; + if (cand < 0 || cand > max_elements_) + throw std::runtime_error("cand error"); + dist_t d = fstdistfunc_(data_point, getDataByInternalId(cand), dist_func_param_); + if (d < curdist) { + curdist = d; + currObj = cand; + changed = true; + } + } + } + } + } + + bool epDeleted = isMarkedDeleted(enterpoint_copy); + for (int level = std::min(curlevel, maxlevelcopy); level >= 0; level--) { + if (level > maxlevelcopy || level < 0) // possible? + throw std::runtime_error("Level error"); + + std::priority_queue, std::vector>, CompareByFirst> top_candidates = searchBaseLayer( + currObj, data_point, level); + if (epDeleted) { + top_candidates.emplace(fstdistfunc_(data_point, getDataByInternalId(enterpoint_copy), dist_func_param_), enterpoint_copy); + if (top_candidates.size() > ef_construction_) + top_candidates.pop(); + } + currObj = mutuallyConnectNewElement(data_point, cur_c, top_candidates, level, false); + } + + + } else { + // Do nothing for the first element + enterpoint_node_ = 0; + maxlevel_ = curlevel; + + } + + //Releasing lock for the maximum level + if (curlevel > maxlevelcopy) { + enterpoint_node_ = cur_c; + maxlevel_ = curlevel; + } + return cur_c; + }; + + std::priority_queue> + searchKnn(const void *query_data, size_t k) const { + std::priority_queue> result; + if (cur_element_count == 0) return result; + + tableint currObj = enterpoint_node_; + dist_t curdist = fstdistfunc_(query_data, getDataByInternalId(enterpoint_node_), dist_func_param_); + + for (int level = maxlevel_; level > 0; level--) { + bool changed = true; + while (changed) { + changed = false; + unsigned int *data; + + data = (unsigned int *) get_linklist(currObj, level); + int size = getListCount(data); + metric_hops++; + metric_distance_computations+=size; + + tableint *datal = (tableint *) (data + 1); + for (int i = 0; i < size; i++) { + tableint cand = datal[i]; + if (cand < 0 || cand > max_elements_) + throw std::runtime_error("cand error"); + dist_t d = fstdistfunc_(query_data, getDataByInternalId(cand), dist_func_param_); + + if (d < curdist) { + curdist = d; + currObj = cand; + changed = true; + } + } + } + } + + std::priority_queue, std::vector>, CompareByFirst> top_candidates; + if (has_deletions_) { + top_candidates=searchBaseLayerST( + currObj, query_data, std::max(ef_, k)); + } + else{ + top_candidates=searchBaseLayerST( + currObj, query_data, std::max(ef_, k)); + } + + while (top_candidates.size() > k) { + top_candidates.pop(); + } + while (top_candidates.size() > 0) { + std::pair rez = top_candidates.top(); + result.push(std::pair(rez.first, getExternalLabel(rez.second))); + top_candidates.pop(); + } + return result; + }; + + void checkIntegrity(){ + int connections_checked=0; + std::vector inbound_connections_num(cur_element_count,0); + for(int i = 0;i < cur_element_count; i++){ + for(int l = 0;l <= element_levels_[i]; l++){ + linklistsizeint *ll_cur = get_linklist_at_level(i,l); + int size = getListCount(ll_cur); + tableint *data = (tableint *) (ll_cur + 1); + std::unordered_set s; + for (int j=0; j 0); + assert(data[j] < cur_element_count); + assert (data[j] != i); + inbound_connections_num[data[j]]++; + s.insert(data[j]); + connections_checked++; + + } + assert(s.size() == size); + } + } + if(cur_element_count > 1){ + int min1=inbound_connections_num[0], max1=inbound_connections_num[0]; + for(int i=0; i < cur_element_count; i++){ + assert(inbound_connections_num[i] > 0); + min1=std::min(inbound_connections_num[i],min1); + max1=std::max(inbound_connections_num[i],max1); + } + std::cout << "Min inbound: " << min1 << ", Max inbound:" << max1 << "\n"; + } + std::cout << "integrity ok, checked " << connections_checked << " connections\n"; + + } + + }; + +} diff --git a/hnswlib/hnswlib.h b/hnswlib/hnswlib.h new file mode 100644 index 0000000..9409c38 --- /dev/null +++ b/hnswlib/hnswlib.h @@ -0,0 +1,108 @@ +#pragma once +#ifndef NO_MANUAL_VECTORIZATION +#ifdef __SSE__ +#define USE_SSE +#ifdef __AVX__ +#define USE_AVX +#endif +#endif +#endif + +#if defined(USE_AVX) || defined(USE_SSE) +#ifdef _MSC_VER +#include +#include +#else +#include +#endif + +#if defined(__GNUC__) +#define PORTABLE_ALIGN32 __attribute__((aligned(32))) +#else +#define PORTABLE_ALIGN32 __declspec(align(32)) +#endif +#endif + +#include +#include +#include +#include + +namespace hnswlib { + typedef size_t labeltype; + + template + class pairGreater { + public: + bool operator()(const T& p1, const T& p2) { + return p1.first > p2.first; + } + }; + + template + static void writeBinaryPOD(std::ostream &out, const T &podRef) { + out.write((char *) &podRef, sizeof(T)); + } + + template + static void readBinaryPOD(std::istream &in, T &podRef) { + in.read((char *) &podRef, sizeof(T)); + } + + template + using DISTFUNC = MTYPE(*)(const void *, const void *, const void *); + + + template + class SpaceInterface { + public: + //virtual void search(void *); + virtual size_t get_data_size() = 0; + + virtual DISTFUNC get_dist_func() = 0; + + virtual void *get_dist_func_param() = 0; + + virtual ~SpaceInterface() {} + }; + + template + class AlgorithmInterface { + public: + virtual void addPoint(const void *datapoint, labeltype label)=0; + virtual std::priority_queue> searchKnn(const void *, size_t) const = 0; + + // Return k nearest neighbor in the order of closer fist + virtual std::vector> + searchKnnCloserFirst(const void* query_data, size_t k) const; + + virtual void saveIndex(const std::string &location)=0; + virtual ~AlgorithmInterface(){ + } + }; + + template + std::vector> + AlgorithmInterface::searchKnnCloserFirst(const void* query_data, size_t k) const { + std::vector> result; + + // here searchKnn returns the result in the order of further first + auto ret = searchKnn(query_data, k); + { + size_t sz = ret.size(); + result.resize(sz); + while (!ret.empty()) { + result[--sz] = ret.top(); + ret.pop(); + } + } + + return result; + } + +} + +#include "space_l2.h" +#include "space_ip.h" +#include "bruteforce.h" +#include "hnswalg.h" diff --git a/hnswlib/space_ip.h b/hnswlib/space_ip.h new file mode 100644 index 0000000..d0497ff --- /dev/null +++ b/hnswlib/space_ip.h @@ -0,0 +1,282 @@ +#pragma once +#include "hnswlib.h" + +namespace hnswlib { + + static float + InnerProduct(const void *pVect1, const void *pVect2, const void *qty_ptr) { + size_t qty = *((size_t *) qty_ptr); + float res = 0; + for (unsigned i = 0; i < qty; i++) { + res += ((float *) pVect1)[i] * ((float *) pVect2)[i]; + } + return (1.0f - res); + + } + +#if defined(USE_AVX) + +// Favor using AVX if available. + static float + InnerProductSIMD4Ext(const void *pVect1v, const void *pVect2v, const void *qty_ptr) { + float PORTABLE_ALIGN32 TmpRes[8]; + float *pVect1 = (float *) pVect1v; + float *pVect2 = (float *) pVect2v; + size_t qty = *((size_t *) qty_ptr); + + size_t qty16 = qty / 16; + size_t qty4 = qty / 4; + + const float *pEnd1 = pVect1 + 16 * qty16; + const float *pEnd2 = pVect1 + 4 * qty4; + + __m256 sum256 = _mm256_set1_ps(0); + + while (pVect1 < pEnd1) { + //_mm_prefetch((char*)(pVect2 + 16), _MM_HINT_T0); + + __m256 v1 = _mm256_loadu_ps(pVect1); + pVect1 += 8; + __m256 v2 = _mm256_loadu_ps(pVect2); + pVect2 += 8; + sum256 = _mm256_add_ps(sum256, _mm256_mul_ps(v1, v2)); + + v1 = _mm256_loadu_ps(pVect1); + pVect1 += 8; + v2 = _mm256_loadu_ps(pVect2); + pVect2 += 8; + sum256 = _mm256_add_ps(sum256, _mm256_mul_ps(v1, v2)); + } + + __m128 v1, v2; + __m128 sum_prod = _mm_add_ps(_mm256_extractf128_ps(sum256, 0), _mm256_extractf128_ps(sum256, 1)); + + while (pVect1 < pEnd2) { + v1 = _mm_loadu_ps(pVect1); + pVect1 += 4; + v2 = _mm_loadu_ps(pVect2); + pVect2 += 4; + sum_prod = _mm_add_ps(sum_prod, _mm_mul_ps(v1, v2)); + } + + _mm_store_ps(TmpRes, sum_prod); + float sum = TmpRes[0] + TmpRes[1] + TmpRes[2] + TmpRes[3];; + return 1.0f - sum; +} + +#elif defined(USE_SSE) + + static float + InnerProductSIMD4Ext(const void *pVect1v, const void *pVect2v, const void *qty_ptr) { + float PORTABLE_ALIGN32 TmpRes[8]; + float *pVect1 = (float *) pVect1v; + float *pVect2 = (float *) pVect2v; + size_t qty = *((size_t *) qty_ptr); + + size_t qty16 = qty / 16; + size_t qty4 = qty / 4; + + const float *pEnd1 = pVect1 + 16 * qty16; + const float *pEnd2 = pVect1 + 4 * qty4; + + __m128 v1, v2; + __m128 sum_prod = _mm_set1_ps(0); + + while (pVect1 < pEnd1) { + v1 = _mm_loadu_ps(pVect1); + pVect1 += 4; + v2 = _mm_loadu_ps(pVect2); + pVect2 += 4; + sum_prod = _mm_add_ps(sum_prod, _mm_mul_ps(v1, v2)); + + v1 = _mm_loadu_ps(pVect1); + pVect1 += 4; + v2 = _mm_loadu_ps(pVect2); + pVect2 += 4; + sum_prod = _mm_add_ps(sum_prod, _mm_mul_ps(v1, v2)); + + v1 = _mm_loadu_ps(pVect1); + pVect1 += 4; + v2 = _mm_loadu_ps(pVect2); + pVect2 += 4; + sum_prod = _mm_add_ps(sum_prod, _mm_mul_ps(v1, v2)); + + v1 = _mm_loadu_ps(pVect1); + pVect1 += 4; + v2 = _mm_loadu_ps(pVect2); + pVect2 += 4; + sum_prod = _mm_add_ps(sum_prod, _mm_mul_ps(v1, v2)); + } + + while (pVect1 < pEnd2) { + v1 = _mm_loadu_ps(pVect1); + pVect1 += 4; + v2 = _mm_loadu_ps(pVect2); + pVect2 += 4; + sum_prod = _mm_add_ps(sum_prod, _mm_mul_ps(v1, v2)); + } + + _mm_store_ps(TmpRes, sum_prod); + float sum = TmpRes[0] + TmpRes[1] + TmpRes[2] + TmpRes[3]; + + return 1.0f - sum; + } + +#endif + +#if defined(USE_AVX) + + static float + InnerProductSIMD16Ext(const void *pVect1v, const void *pVect2v, const void *qty_ptr) { + float PORTABLE_ALIGN32 TmpRes[8]; + float *pVect1 = (float *) pVect1v; + float *pVect2 = (float *) pVect2v; + size_t qty = *((size_t *) qty_ptr); + + size_t qty16 = qty / 16; + + + const float *pEnd1 = pVect1 + 16 * qty16; + + __m256 sum256 = _mm256_set1_ps(0); + + while (pVect1 < pEnd1) { + //_mm_prefetch((char*)(pVect2 + 16), _MM_HINT_T0); + + __m256 v1 = _mm256_loadu_ps(pVect1); + pVect1 += 8; + __m256 v2 = _mm256_loadu_ps(pVect2); + pVect2 += 8; + sum256 = _mm256_add_ps(sum256, _mm256_mul_ps(v1, v2)); + + v1 = _mm256_loadu_ps(pVect1); + pVect1 += 8; + v2 = _mm256_loadu_ps(pVect2); + pVect2 += 8; + sum256 = _mm256_add_ps(sum256, _mm256_mul_ps(v1, v2)); + } + + _mm256_store_ps(TmpRes, sum256); + float sum = TmpRes[0] + TmpRes[1] + TmpRes[2] + TmpRes[3] + TmpRes[4] + TmpRes[5] + TmpRes[6] + TmpRes[7]; + + return 1.0f - sum; + } + +#elif defined(USE_SSE) + + static float + InnerProductSIMD16Ext(const void *pVect1v, const void *pVect2v, const void *qty_ptr) { + float PORTABLE_ALIGN32 TmpRes[8]; + float *pVect1 = (float *) pVect1v; + float *pVect2 = (float *) pVect2v; + size_t qty = *((size_t *) qty_ptr); + + size_t qty16 = qty / 16; + + const float *pEnd1 = pVect1 + 16 * qty16; + + __m128 v1, v2; + __m128 sum_prod = _mm_set1_ps(0); + + while (pVect1 < pEnd1) { + v1 = _mm_loadu_ps(pVect1); + pVect1 += 4; + v2 = _mm_loadu_ps(pVect2); + pVect2 += 4; + sum_prod = _mm_add_ps(sum_prod, _mm_mul_ps(v1, v2)); + + v1 = _mm_loadu_ps(pVect1); + pVect1 += 4; + v2 = _mm_loadu_ps(pVect2); + pVect2 += 4; + sum_prod = _mm_add_ps(sum_prod, _mm_mul_ps(v1, v2)); + + v1 = _mm_loadu_ps(pVect1); + pVect1 += 4; + v2 = _mm_loadu_ps(pVect2); + pVect2 += 4; + sum_prod = _mm_add_ps(sum_prod, _mm_mul_ps(v1, v2)); + + v1 = _mm_loadu_ps(pVect1); + pVect1 += 4; + v2 = _mm_loadu_ps(pVect2); + pVect2 += 4; + sum_prod = _mm_add_ps(sum_prod, _mm_mul_ps(v1, v2)); + } + _mm_store_ps(TmpRes, sum_prod); + float sum = TmpRes[0] + TmpRes[1] + TmpRes[2] + TmpRes[3]; + + return 1.0f - sum; + } + +#endif + +#if defined(USE_SSE) || defined(USE_AVX) + static float + InnerProductSIMD16ExtResiduals(const void *pVect1v, const void *pVect2v, const void *qty_ptr) { + size_t qty = *((size_t *) qty_ptr); + size_t qty16 = qty >> 4 << 4; + float res = InnerProductSIMD16Ext(pVect1v, pVect2v, &qty16); + float *pVect1 = (float *) pVect1v + qty16; + float *pVect2 = (float *) pVect2v + qty16; + + size_t qty_left = qty - qty16; + float res_tail = InnerProduct(pVect1, pVect2, &qty_left); + return res + res_tail - 1.0f; + } + + static float + InnerProductSIMD4ExtResiduals(const void *pVect1v, const void *pVect2v, const void *qty_ptr) { + size_t qty = *((size_t *) qty_ptr); + size_t qty4 = qty >> 2 << 2; + + float res = InnerProductSIMD4Ext(pVect1v, pVect2v, &qty4); + size_t qty_left = qty - qty4; + + float *pVect1 = (float *) pVect1v + qty4; + float *pVect2 = (float *) pVect2v + qty4; + float res_tail = InnerProduct(pVect1, pVect2, &qty_left); + + return res + res_tail - 1.0f; + } +#endif + + class InnerProductSpace : public SpaceInterface { + + DISTFUNC fstdistfunc_; + size_t data_size_; + size_t dim_; + public: + InnerProductSpace(size_t dim) { + fstdistfunc_ = InnerProduct; + #if defined(USE_AVX) || defined(USE_SSE) + if (dim % 16 == 0) + fstdistfunc_ = InnerProductSIMD16Ext; + else if (dim % 4 == 0) + fstdistfunc_ = InnerProductSIMD4Ext; + else if (dim > 16) + fstdistfunc_ = InnerProductSIMD16ExtResiduals; + else if (dim > 4) + fstdistfunc_ = InnerProductSIMD4ExtResiduals; + #endif + dim_ = dim; + data_size_ = dim * sizeof(float); + } + + size_t get_data_size() { + return data_size_; + } + + DISTFUNC get_dist_func() { + return fstdistfunc_; + } + + void *get_dist_func_param() { + return &dim_; + } + + ~InnerProductSpace() {} + }; + + +} diff --git a/hnswlib/space_l2.h b/hnswlib/space_l2.h new file mode 100644 index 0000000..e86e13b --- /dev/null +++ b/hnswlib/space_l2.h @@ -0,0 +1,281 @@ +#pragma once +#include "hnswlib.h" + +namespace hnswlib { + + static float + L2Sqr(const void *pVect1v, const void *pVect2v, const void *qty_ptr) { + float *pVect1 = (float *) pVect1v; + float *pVect2 = (float *) pVect2v; + size_t qty = *((size_t *) qty_ptr); + + float res = 0; + for (size_t i = 0; i < qty; i++) { + float t = *pVect1 - *pVect2; + pVect1++; + pVect2++; + res += t * t; + } + return (res); + } + +#if defined(USE_AVX) + + // Favor using AVX if available. + static float + L2SqrSIMD16Ext(const void *pVect1v, const void *pVect2v, const void *qty_ptr) { + float *pVect1 = (float *) pVect1v; + float *pVect2 = (float *) pVect2v; + size_t qty = *((size_t *) qty_ptr); + float PORTABLE_ALIGN32 TmpRes[8]; + size_t qty16 = qty >> 4; + + const float *pEnd1 = pVect1 + (qty16 << 4); + + __m256 diff, v1, v2; + __m256 sum = _mm256_set1_ps(0); + + while (pVect1 < pEnd1) { + v1 = _mm256_loadu_ps(pVect1); + pVect1 += 8; + v2 = _mm256_loadu_ps(pVect2); + pVect2 += 8; + diff = _mm256_sub_ps(v1, v2); + sum = _mm256_add_ps(sum, _mm256_mul_ps(diff, diff)); + + v1 = _mm256_loadu_ps(pVect1); + pVect1 += 8; + v2 = _mm256_loadu_ps(pVect2); + pVect2 += 8; + diff = _mm256_sub_ps(v1, v2); + sum = _mm256_add_ps(sum, _mm256_mul_ps(diff, diff)); + } + + _mm256_store_ps(TmpRes, sum); + return TmpRes[0] + TmpRes[1] + TmpRes[2] + TmpRes[3] + TmpRes[4] + TmpRes[5] + TmpRes[6] + TmpRes[7]; + } + +#elif defined(USE_SSE) + + static float + L2SqrSIMD16Ext(const void *pVect1v, const void *pVect2v, const void *qty_ptr) { + float *pVect1 = (float *) pVect1v; + float *pVect2 = (float *) pVect2v; + size_t qty = *((size_t *) qty_ptr); + float PORTABLE_ALIGN32 TmpRes[8]; + size_t qty16 = qty >> 4; + + const float *pEnd1 = pVect1 + (qty16 << 4); + + __m128 diff, v1, v2; + __m128 sum = _mm_set1_ps(0); + + while (pVect1 < pEnd1) { + //_mm_prefetch((char*)(pVect2 + 16), _MM_HINT_T0); + v1 = _mm_loadu_ps(pVect1); + pVect1 += 4; + v2 = _mm_loadu_ps(pVect2); + pVect2 += 4; + diff = _mm_sub_ps(v1, v2); + sum = _mm_add_ps(sum, _mm_mul_ps(diff, diff)); + + v1 = _mm_loadu_ps(pVect1); + pVect1 += 4; + v2 = _mm_loadu_ps(pVect2); + pVect2 += 4; + diff = _mm_sub_ps(v1, v2); + sum = _mm_add_ps(sum, _mm_mul_ps(diff, diff)); + + v1 = _mm_loadu_ps(pVect1); + pVect1 += 4; + v2 = _mm_loadu_ps(pVect2); + pVect2 += 4; + diff = _mm_sub_ps(v1, v2); + sum = _mm_add_ps(sum, _mm_mul_ps(diff, diff)); + + v1 = _mm_loadu_ps(pVect1); + pVect1 += 4; + v2 = _mm_loadu_ps(pVect2); + pVect2 += 4; + diff = _mm_sub_ps(v1, v2); + sum = _mm_add_ps(sum, _mm_mul_ps(diff, diff)); + } + + _mm_store_ps(TmpRes, sum); + return TmpRes[0] + TmpRes[1] + TmpRes[2] + TmpRes[3]; + } +#endif + +#if defined(USE_SSE) || defined(USE_AVX) + static float + L2SqrSIMD16ExtResiduals(const void *pVect1v, const void *pVect2v, const void *qty_ptr) { + size_t qty = *((size_t *) qty_ptr); + size_t qty16 = qty >> 4 << 4; + float res = L2SqrSIMD16Ext(pVect1v, pVect2v, &qty16); + float *pVect1 = (float *) pVect1v + qty16; + float *pVect2 = (float *) pVect2v + qty16; + + size_t qty_left = qty - qty16; + float res_tail = L2Sqr(pVect1, pVect2, &qty_left); + return (res + res_tail); + } +#endif + + +#ifdef USE_SSE + static float + L2SqrSIMD4Ext(const void *pVect1v, const void *pVect2v, const void *qty_ptr) { + float PORTABLE_ALIGN32 TmpRes[8]; + float *pVect1 = (float *) pVect1v; + float *pVect2 = (float *) pVect2v; + size_t qty = *((size_t *) qty_ptr); + + + size_t qty4 = qty >> 2; + + const float *pEnd1 = pVect1 + (qty4 << 2); + + __m128 diff, v1, v2; + __m128 sum = _mm_set1_ps(0); + + while (pVect1 < pEnd1) { + v1 = _mm_loadu_ps(pVect1); + pVect1 += 4; + v2 = _mm_loadu_ps(pVect2); + pVect2 += 4; + diff = _mm_sub_ps(v1, v2); + sum = _mm_add_ps(sum, _mm_mul_ps(diff, diff)); + } + _mm_store_ps(TmpRes, sum); + return TmpRes[0] + TmpRes[1] + TmpRes[2] + TmpRes[3]; + } + + static float + L2SqrSIMD4ExtResiduals(const void *pVect1v, const void *pVect2v, const void *qty_ptr) { + size_t qty = *((size_t *) qty_ptr); + size_t qty4 = qty >> 2 << 2; + + float res = L2SqrSIMD4Ext(pVect1v, pVect2v, &qty4); + size_t qty_left = qty - qty4; + + float *pVect1 = (float *) pVect1v + qty4; + float *pVect2 = (float *) pVect2v + qty4; + float res_tail = L2Sqr(pVect1, pVect2, &qty_left); + + return (res + res_tail); + } +#endif + + class L2Space : public SpaceInterface { + + DISTFUNC fstdistfunc_; + size_t data_size_; + size_t dim_; + public: + L2Space(size_t dim) { + fstdistfunc_ = L2Sqr; + #if defined(USE_SSE) || defined(USE_AVX) + if (dim % 16 == 0) + fstdistfunc_ = L2SqrSIMD16Ext; + else if (dim % 4 == 0) + fstdistfunc_ = L2SqrSIMD4Ext; + else if (dim > 16) + fstdistfunc_ = L2SqrSIMD16ExtResiduals; + else if (dim > 4) + fstdistfunc_ = L2SqrSIMD4ExtResiduals; + #endif + dim_ = dim; + data_size_ = dim * sizeof(float); + } + + size_t get_data_size() { + return data_size_; + } + + DISTFUNC get_dist_func() { + return fstdistfunc_; + } + + void *get_dist_func_param() { + return &dim_; + } + + ~L2Space() {} + }; + + static int + L2SqrI4x(const void *__restrict pVect1, const void *__restrict pVect2, const void *__restrict qty_ptr) { + + size_t qty = *((size_t *) qty_ptr); + int res = 0; + unsigned char *a = (unsigned char *) pVect1; + unsigned char *b = (unsigned char *) pVect2; + + qty = qty >> 2; + for (size_t i = 0; i < qty; i++) { + + res += ((*a) - (*b)) * ((*a) - (*b)); + a++; + b++; + res += ((*a) - (*b)) * ((*a) - (*b)); + a++; + b++; + res += ((*a) - (*b)) * ((*a) - (*b)); + a++; + b++; + res += ((*a) - (*b)) * ((*a) - (*b)); + a++; + b++; + } + return (res); + } + + static int L2SqrI(const void* __restrict pVect1, const void* __restrict pVect2, const void* __restrict qty_ptr) { + size_t qty = *((size_t*)qty_ptr); + int res = 0; + unsigned char* a = (unsigned char*)pVect1; + unsigned char* b = (unsigned char*)pVect2; + + for(size_t i = 0; i < qty; i++) + { + res += ((*a) - (*b)) * ((*a) - (*b)); + a++; + b++; + } + return (res); + } + + class L2SpaceI : public SpaceInterface { + + DISTFUNC fstdistfunc_; + size_t data_size_; + size_t dim_; + public: + L2SpaceI(size_t dim) { + if(dim % 4 == 0) { + fstdistfunc_ = L2SqrI4x; + } + else { + fstdistfunc_ = L2SqrI; + } + dim_ = dim; + data_size_ = dim * sizeof(unsigned char); + } + + size_t get_data_size() { + return data_size_; + } + + DISTFUNC get_dist_func() { + return fstdistfunc_; + } + + void *get_dist_func_param() { + return &dim_; + } + + ~L2SpaceI() {} + }; + + +} \ No newline at end of file diff --git a/hnswlib/visited_list_pool.h b/hnswlib/visited_list_pool.h new file mode 100644 index 0000000..6b0f445 --- /dev/null +++ b/hnswlib/visited_list_pool.h @@ -0,0 +1,78 @@ +#pragma once + +#include +#include + +namespace hnswlib { + typedef unsigned short int vl_type; + + class VisitedList { + public: + vl_type curV; + vl_type *mass; + unsigned int numelements; + + VisitedList(int numelements1) { + curV = -1; + numelements = numelements1; + mass = new vl_type[numelements]; + } + + void reset() { + curV++; + if (curV == 0) { + memset(mass, 0, sizeof(vl_type) * numelements); + curV++; + } + }; + + ~VisitedList() { delete[] mass; } + }; +/////////////////////////////////////////////////////////// +// +// Class for multi-threaded pool-management of VisitedLists +// +///////////////////////////////////////////////////////// + + class VisitedListPool { + std::deque pool; + std::mutex poolguard; + int numelements; + + public: + VisitedListPool(int initmaxpools, int numelements1) { + numelements = numelements1; + for (int i = 0; i < initmaxpools; i++) + pool.push_front(new VisitedList(numelements)); + } + + VisitedList *getFreeVisitedList() { + VisitedList *rez; + { + std::unique_lock lock(poolguard); + if (pool.size() > 0) { + rez = pool.front(); + pool.pop_front(); + } else { + rez = new VisitedList(numelements); + } + } + rez->reset(); + return rez; + }; + + void releaseVisitedList(VisitedList *vl) { + std::unique_lock lock(poolguard); + pool.push_front(vl); + }; + + ~VisitedListPool() { + while (pool.size()) { + VisitedList *rez = pool.front(); + pool.pop_front(); + delete rez; + } + }; + }; +} + diff --git a/libhnsw.a b/libhnsw.a new file mode 100644 index 0000000000000000000000000000000000000000..1853d3fa473497a4ccaf5852b8c7832ff6a79339 GIT binary patch literal 89536 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� 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