pyglass-portable 2.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 zh Wang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,68 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyglass-portable
3
+ Version: 2.1.0
4
+ Summary: Portable wheels of pyglass (zilliztech) — a fast HNSW/NSG graph ANN library that builds and runs on macOS (Apple Silicon and Intel) and non-AVX-512 x86 (AMD, older Intel). Imported as `glass`.
5
+ Author: Zihao Wang
6
+ Maintainer: Ian Driver
7
+ License: MIT
8
+ Project-URL: Homepage, https://github.com/iandriver/pyglass
9
+ Project-URL: Upstream, https://github.com/zilliztech/pyglass
10
+ Project-URL: Issues, https://github.com/iandriver/pyglass/issues
11
+ Keywords: nearest-neighbors,ann,hnsw,nsg,vector-search,pyglass,similarity-search
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: POSIX :: Linux
16
+ Classifier: Operating System :: MacOS
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: C++
19
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: numpy
24
+ Dynamic: license-file
25
+
26
+ # pyglass-portable
27
+
28
+ Portable prebuilt wheels of [**pyglass**](https://github.com/zilliztech/pyglass)
29
+ (zilliztech) — a fast HNSW / NSG graph-based approximate-nearest-neighbor library.
30
+
31
+ Upstream publishes only an **AVX-512** x86_64 wheel, which crashes (`SIGILL`) on
32
+ CPUs without AVX-512 — Apple Silicon, AMD, older Intel. pyglass's *source* is
33
+ portable (AVX2 / NEON / scalar kernels); this repackage builds it with a portable
34
+ baseline so it installs and runs on those machines too. **The import name stays
35
+ `glass`** — it's a drop-in for upstream, not a fork of the algorithm.
36
+
37
+ ```bash
38
+ uv pip install pyglass-portable # or: pip install pyglass-portable
39
+ ```
40
+
41
+ Prebuilt wheels: Linux x86_64 (AVX2) and macOS arm64 (NEON), Python 3.9+. Intel
42
+ Macs and ARM Linux build from the sdist — `pip install` compiles it portably (it
43
+ needs a C++20 compiler; on macOS, `brew install libomp`).
44
+
45
+ ```python
46
+ import numpy as np, glass
47
+
48
+ X = np.random.rand(100_000, 128).astype("float32")
49
+ index = glass.Index("HNSW", "L2", "FP32", R=32, L=200)
50
+ graph = index.build(X)
51
+ searcher = glass.Searcher(graph, X, "L2", "FP32")
52
+ searcher.set_ef(64); searcher.optimize()
53
+ ids, dists = searcher.batch_search(X[:1000], 10)
54
+ ```
55
+
56
+ It also works as a `scanpy` neighbors backend via
57
+ [`pipnn.contrib.GlassTransformer`](https://github.com/iandriver/pipnn-scanpy).
58
+
59
+ ## Relationship to upstream
60
+
61
+ This is an unofficial repackage for platform coverage only — **no algorithm
62
+ changes**. The portability fixes are proposed upstream in
63
+ [zilliztech/pyglass#24](https://github.com/zilliztech/pyglass/pull/24); if they
64
+ land and upstream ships portable wheels, prefer `glassppy`. Version tracks the
65
+ upstream source version.
66
+
67
+ Credit to the pyglass authors (Zihao Wang / zilliztech). MIT licensed, same as
68
+ upstream.
@@ -0,0 +1,155 @@
1
+ # Graph Library for Approximate Similarity Search
2
+
3
+ pyglass is a library for fast inference of graph index for approximate similarity search.
4
+
5
+ ## Features
6
+
7
+ - Supports multiple graph algorithms, like [**HNSW**](https://github.com/nmslib/hnswlib) and [**NSG**](https://github.com/ZJULearning/nsg).
8
+ - Supports multiple hardware platforms, like **X86** and **ARM**. Support for **GPU** is on the way
9
+ - No third-party library dependencies, does not rely on OpenBLAS / MKL or any other computing framework.
10
+ - Sophisticated memory management and data structure design, very low memory footprint.
11
+ - It's high performant.
12
+
13
+ ## Getting Started
14
+
15
+ Install pyglass with pip
16
+
17
+ ```bash
18
+ pip install -v -e "python"
19
+ ```
20
+
21
+ ### Using uv
22
+
23
+ It is recommended to use `uv` for setting up the development environment.
24
+
25
+ 1. **Install `uv`**
26
+ ```bash
27
+ pip install uv
28
+ ```
29
+
30
+ 2. **Create and Activate Virtual Environment**
31
+ ```bash
32
+ # Create the virtual environment in a .venv directory
33
+ uv venv
34
+ # Activate the environment (on bash/zsh)
35
+ source .venv/bin/activate
36
+ ```
37
+ *Note: For other shells like Fish or Powershell, use the corresponding activation script in the `.venv/bin/` directory.*
38
+
39
+ 3. **Install the Project**
40
+ ```bash
41
+ # Install the project and its dependencies in editable mode
42
+ uv pip install -v -e "python"
43
+ ```
44
+
45
+ ### Development
46
+
47
+ For C++ development, you might want to generate a `compile_commands.json` file for your editor's language server. You can use [Bear](https://github.com/rizsotto/Bear) to do this.
48
+
49
+ **1. Install Bear**
50
+
51
+ On Debian-based systems (like Ubuntu), you can install Bear with:
52
+ ```bash
53
+ apt install bear
54
+ ```
55
+
56
+ **2. Generate `compile_commands.json`**
57
+
58
+ Run the installation command from within `bear`:
59
+ ```bash
60
+ bear -- uv pip install -v -e "python"
61
+ ```
62
+ This will create a `compile_commands.json` file in the project's root directory.
63
+
64
+ ## Quick Tour
65
+ A runnable demo is at [examples/demo.ipynb](https://github.com/zilliztech/pyglass/blob/master/examples/demo.ipynb). It's highly recommended to try it.
66
+
67
+ ## Usage
68
+ **Import library**
69
+ ```python
70
+ >>> import glass
71
+ ```
72
+ **Load Data**
73
+ ```python
74
+ >>> import numpy as np
75
+ >>> n, d = 10000, 128
76
+ >>> X = np.random.randn(n, d)
77
+ >>> Y = np.random.randn(d)
78
+ ```
79
+ **Create Index**
80
+ pyglass supports **HNSW** and **NSG** index currently, with different quantization support
81
+ ```python
82
+ >>> index = glass.Index(index_type="HNSW", metric="L2", R=32, L=50)
83
+ >>> index = glass.Index(index_type="NSG", metric="L2", R=32, L=50, quant="SQ8U")
84
+ ```
85
+ **Build Graph**
86
+ ```python
87
+ >>> graph = index.build(X)
88
+ ```
89
+ **Create Searcher**
90
+ ```python
91
+ >>> searcher = glass.Searcher(graph=graph, data=X, metric="L2", quantizer="SQ4U")
92
+ >>> searcher.set_ef(32)
93
+ ```
94
+ **(Optional) Optimize Searcher**
95
+ ```python
96
+ >>> searcher.optimize()
97
+ ```
98
+ **Searching**
99
+ ```python
100
+ >>> ret = searcher.search(query=Y, k=10)
101
+ >>> print(ret)
102
+ ```
103
+
104
+ ## Supported Quantization Methods
105
+
106
+ - FP8_E5M2
107
+ - PQ8
108
+ - SQ8
109
+ - SQ8U
110
+ - SQ6
111
+ - SQ4
112
+ - SQ4U
113
+ - SQ4UA
114
+ - SQ2U
115
+ - BinaryQuant
116
+
117
+ **Rule of Thumb**: Use SQ8U for indexing, and SQ4U for searching is almost always a good choice.
118
+
119
+ ## Performance
120
+
121
+ Glass is among one of the top performant ann algorithms on [ann-benchmarks](https://ann-benchmarks.com/)
122
+
123
+ ### fashion-mnist-784-euclidean
124
+ ![](docs/figures/fashion-mnist-784-euclidean_10_euclidean.png)
125
+ ### gist-960-euclidean
126
+ ![](docs/figures/gist-960-euclidean_10_euclidean.png)
127
+ ### sift-128-euclidean
128
+ ![](docs/figures/sift-128-euclidean_10_euclidean.png)
129
+
130
+ ### Quick Benchmark
131
+
132
+ 1. Change configuration file `examples/config.json`
133
+ 2. Run benchmark
134
+ ```
135
+ python3 examples/main.py
136
+ ```
137
+ If you encounter network issues when downloading datasets, you can try setting the `HF_ENDPOINT` environment variable to `https://hf-mirror.com` or something else.
138
+
139
+ ## Citation
140
+
141
+ You can cite the PyGlass repo as follows:
142
+ ```bibtex
143
+ @misc{PyGlass,
144
+ author = {Zihao Wang},
145
+ title = {Graph Library for Approximate Similarity Search},
146
+ url = {https://github.com/zilliztech/pyglass},
147
+ year = {2025},
148
+ month = {4},
149
+ }
150
+ ```
151
+
152
+ ## Star History
153
+
154
+ [![Star History Chart](https://api.star-history.com/svg?repos=zilliztech/pyglass&type=Date)](https://www.star-history.com/#zilliztech/pyglass&Date)
155
+
@@ -0,0 +1,43 @@
1
+ # pyglass-portable
2
+
3
+ Portable prebuilt wheels of [**pyglass**](https://github.com/zilliztech/pyglass)
4
+ (zilliztech) — a fast HNSW / NSG graph-based approximate-nearest-neighbor library.
5
+
6
+ Upstream publishes only an **AVX-512** x86_64 wheel, which crashes (`SIGILL`) on
7
+ CPUs without AVX-512 — Apple Silicon, AMD, older Intel. pyglass's *source* is
8
+ portable (AVX2 / NEON / scalar kernels); this repackage builds it with a portable
9
+ baseline so it installs and runs on those machines too. **The import name stays
10
+ `glass`** — it's a drop-in for upstream, not a fork of the algorithm.
11
+
12
+ ```bash
13
+ uv pip install pyglass-portable # or: pip install pyglass-portable
14
+ ```
15
+
16
+ Prebuilt wheels: Linux x86_64 (AVX2) and macOS arm64 (NEON), Python 3.9+. Intel
17
+ Macs and ARM Linux build from the sdist — `pip install` compiles it portably (it
18
+ needs a C++20 compiler; on macOS, `brew install libomp`).
19
+
20
+ ```python
21
+ import numpy as np, glass
22
+
23
+ X = np.random.rand(100_000, 128).astype("float32")
24
+ index = glass.Index("HNSW", "L2", "FP32", R=32, L=200)
25
+ graph = index.build(X)
26
+ searcher = glass.Searcher(graph, X, "L2", "FP32")
27
+ searcher.set_ef(64); searcher.optimize()
28
+ ids, dists = searcher.batch_search(X[:1000], 10)
29
+ ```
30
+
31
+ It also works as a `scanpy` neighbors backend via
32
+ [`pipnn.contrib.GlassTransformer`](https://github.com/iandriver/pipnn-scanpy).
33
+
34
+ ## Relationship to upstream
35
+
36
+ This is an unofficial repackage for platform coverage only — **no algorithm
37
+ changes**. The portability fixes are proposed upstream in
38
+ [zilliztech/pyglass#24](https://github.com/zilliztech/pyglass/pull/24); if they
39
+ land and upstream ships portable wheels, prefer `glassppy`. Version tracks the
40
+ upstream source version.
41
+
42
+ Credit to the pyglass authors (Zihao Wang / zilliztech). MIT licensed, same as
43
+ upstream.
@@ -0,0 +1,48 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel", "numpy", "pybind11"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pyglass-portable"
7
+ version = "2.1.0"
8
+ description = "Portable wheels of pyglass (zilliztech) — a fast HNSW/NSG graph ANN library that builds and runs on macOS (Apple Silicon and Intel) and non-AVX-512 x86 (AMD, older Intel). Imported as `glass`."
9
+ readme = "README_portable.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Zihao Wang" }]
13
+ maintainers = [{ name = "Ian Driver" }]
14
+ keywords = ["nearest-neighbors", "ann", "hnsw", "nsg", "vector-search", "pyglass", "similarity-search"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Science/Research",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: POSIX :: Linux",
20
+ "Operating System :: MacOS",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: C++",
23
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
24
+ ]
25
+ dependencies = ["numpy"]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/iandriver/pyglass"
29
+ Upstream = "https://github.com/zilliztech/pyglass"
30
+ Issues = "https://github.com/iandriver/pyglass/issues"
31
+
32
+ # Wheels are built with cibuildwheel (see .github/workflows/release.yml).
33
+ [tool.cibuildwheel]
34
+ build = "cp39-* cp310-* cp311-* cp312-* cp313-*"
35
+ skip = "*-musllinux* *_i686 pp*"
36
+ build-frontend = "build"
37
+ test-command = 'python -c "import glass, numpy as np; g=glass.Index(\"HNSW\",\"L2\",\"FP32\",32,200).build(np.random.rand(2000,32).astype(\"float32\")); print(\"glass OK\")"'
38
+
39
+ # GLASS_MARCH (the portable x86 AVX2 baseline for redistributable wheels — without
40
+ # it the build uses -march=native and bakes in the CI runner's ISA) is set per-job
41
+ # in the release workflow, so it applies to x86 targets only and not arm64/NEON.
42
+ [tool.cibuildwheel.linux]
43
+ # manylinux_2_28 has a new-enough toolchain for C++20.
44
+ manylinux-x86_64-image = "manylinux_2_28"
45
+
46
+ [tool.cibuildwheel.macos]
47
+ # libomp for OpenMP (build_extension.py picks it up from Homebrew).
48
+ before-all = "brew install libomp"
@@ -0,0 +1,3 @@
1
+ from .dataset import dataset_dict
2
+
3
+ __all__ = ["dataset_dict"]
@@ -0,0 +1,93 @@
1
+ import os
2
+ import h5py
3
+ import numpy as np
4
+ from sklearn import preprocessing
5
+ import requests
6
+ from pathlib import Path
7
+
8
+ HF_ENDPOINT = os.getenv("HF_ENDPOINT", "https://huggingface.co")
9
+
10
+
11
+ class Dataset:
12
+ def __init__(self, name, metric="L2", normalize=False, url="", data_dir="datasets"):
13
+ self.name = name
14
+ self.metric = metric
15
+ self._normalize = normalize
16
+ self.file_path = Path(data_dir) / f"{self.name}.hdf5"
17
+ self._file = None
18
+ self._download_url = url
19
+
20
+ self._load_or_download_file()
21
+
22
+ def _load_or_download_file(self):
23
+ if not self.file_path.exists():
24
+ self.file_path.parent.mkdir(parents=True, exist_ok=True)
25
+ self._download_file()
26
+ self._file = h5py.File(self.file_path, "r")
27
+
28
+ def _download_file(self):
29
+ url = (
30
+ f"{HF_ENDPOINT}/datasets/{self._download_url}/resolve/main/{self.name}.hdf5"
31
+ )
32
+ print(f"Downloading {self.name} from {url} to {self.file_path}...")
33
+ response = requests.get(url, stream=True)
34
+ response.raise_for_status()
35
+ with open(self.file_path, "wb") as f:
36
+ for chunk in response.iter_content(chunk_size=8192):
37
+ f.write(chunk)
38
+ print("Download complete.")
39
+
40
+ def _preprocess(self, data):
41
+ if self._normalize:
42
+ return preprocessing.normalize(data, norm="l2", axis=1)
43
+ return data
44
+
45
+ def get_database(self):
46
+ return self._preprocess(np.array(self._file["train"]))
47
+
48
+ def get_queries(self):
49
+ return self._preprocess(np.array(self._file["test"]))
50
+
51
+ def get_groundtruth(self, k):
52
+ return np.array(self._file["neighbors"])[:, :k]
53
+
54
+ def evaluate(self, pred, k=None):
55
+ nq, topk = pred.shape
56
+ if k is not None:
57
+ topk = k
58
+ gt = self.get_groundtruth(topk)
59
+ cnt = 0
60
+ for i in range(nq):
61
+ cnt += np.intersect1d(pred[i], gt[i]).size
62
+ return cnt / nq / k
63
+
64
+
65
+ hhy3_url = "hhy3/ann-datasets"
66
+ vibe_url = "vector-index-bench/vibe"
67
+
68
+ dataset_configs = {
69
+ "sift-128-euclidean": {"metric": "L2", "normalize": False, "url": hhy3_url},
70
+ "fashion-mnist-784-euclidean": {
71
+ "metric": "L2",
72
+ "normalize": False,
73
+ "url": hhy3_url,
74
+ },
75
+ "nytimes-256-angular": {"metric": "IP", "normalize": True, "url": hhy3_url},
76
+ "glove-100-angular": {"metric": "IP", "normalize": False, "url": hhy3_url},
77
+ "glove-25-angular": {"metric": "IP", "normalize": True, "url": hhy3_url},
78
+ "lastfm-64-dot": {"metric": "IP", "normalize": True, "url": hhy3_url},
79
+ "gist-960-euclidean": {"metric": "L2", "normalize": False, "url": hhy3_url},
80
+ "cohere-768-angular": {"metric": "IP", "normalize": True, "url": hhy3_url},
81
+ "llama-128-ip": {"metric": "IP", "normalize": False, "url": vibe_url},
82
+ "yi-128-ip": {"metric": "IP", "normalize": False, "url": vibe_url},
83
+ "agnews-mxbai-1024-euclidean": {
84
+ "metric": "L2",
85
+ "normalize": False,
86
+ "url": vibe_url,
87
+ },
88
+ }
89
+
90
+ dataset_dict = {
91
+ name: lambda n=name, cfg=config: Dataset(n, **cfg)
92
+ for name, config in dataset_configs.items()
93
+ }
@@ -0,0 +1,295 @@
1
+ #include <pybind11/functional.h>
2
+ #include <pybind11/numpy.h>
3
+ #include <pybind11/pybind11.h>
4
+ #include <pybind11/stl.h>
5
+
6
+ #include <thread>
7
+
8
+ #include <omp.h>
9
+
10
+ #include "glass/algorithms/clustering.hpp"
11
+ #include "glass/builder.hpp"
12
+ #include "glass/hnsw/hnsw.hpp"
13
+ #include "glass/nsg/nsg.hpp"
14
+ #include "glass/nsg/rnndescent.hpp"
15
+ #include "glass/searcher/graph_searcher.hpp"
16
+
17
+ namespace py = pybind11;
18
+ using namespace pybind11::literals; // needed to bring in _a literal
19
+
20
+ inline void get_input_array_shapes(const py::buffer_info &buffer, size_t *rows,
21
+ size_t *features) {
22
+ if (buffer.ndim != 2 && buffer.ndim != 1) {
23
+ char msg[256];
24
+ snprintf(msg, sizeof(msg),
25
+ "Input vector data wrong shape. Number of dimensions %d. Data "
26
+ "must be a 1D or 2D array.",
27
+ buffer.ndim);
28
+ }
29
+ if (buffer.ndim == 2) {
30
+ *rows = buffer.shape[0];
31
+ *features = buffer.shape[1];
32
+ } else {
33
+ *rows = 1;
34
+ *features = buffer.shape[0];
35
+ }
36
+ }
37
+
38
+ void set_num_threads(int num_threads) { omp_set_num_threads(num_threads); }
39
+
40
+ struct Graph {
41
+ glass::Graph<int> graph;
42
+
43
+ Graph() = default;
44
+
45
+ explicit Graph(Graph &&rhs) : graph(std::move(rhs.graph)) {}
46
+
47
+ Graph(const std::string &filename, const std::string &format = "glass") {
48
+ graph.load(filename, format);
49
+ }
50
+
51
+ explicit Graph(glass::Graph<int> graph) : graph(std::move(graph)) {}
52
+
53
+ void save(const std::string &filename) { graph.save(filename); }
54
+
55
+ void load(const std::string &filename, const std::string &format = "glass") {
56
+ graph.load(filename, format);
57
+ }
58
+ };
59
+
60
+ struct Index {
61
+ std::unique_ptr<glass::Builder> index = nullptr;
62
+
63
+ Index(const std::string &index_type, const std::string &metric,
64
+ const std::string &quant = "FP32", int R = 32, int L = 200) {
65
+ if (index_type == "HNSW") {
66
+ index = glass::create_hnsw(metric, quant, R, L);
67
+ } else if (index_type == "NSG") {
68
+ index = glass::create_nsg(quant, R, L, false);
69
+ } else if (index_type == "SSG") {
70
+ index = glass::create_nsg(quant, R, L, true);
71
+ } else if (index_type == "RNNDESCENT") {
72
+ index = glass::create_rnndescent(quant, R);
73
+ } else {
74
+ printf("Unknown index type\n");
75
+ }
76
+ }
77
+
78
+ Graph build(py::object input) {
79
+ py::array_t<float, py::array::c_style | py::array::forcecast> items(input);
80
+ auto buffer = items.request();
81
+ size_t rows, features;
82
+ get_input_array_shapes(buffer, &rows, &features);
83
+ float *vector_data = (float *)items.data(0);
84
+ return Graph(index->Build(vector_data, rows, features));
85
+ }
86
+
87
+ double get_construction_time() const { return index->GetConstructionTime(); }
88
+ };
89
+
90
+ struct Searcher {
91
+
92
+ std::unique_ptr<glass::GraphSearcherBase> searcher;
93
+
94
+ Searcher(Graph &graph, py::object input, const std::string &metric,
95
+ const std::string &quantizer, const std::string &refine_quant = "")
96
+ : searcher(
97
+ std::unique_ptr<glass::GraphSearcherBase>(glass::create_searcher(
98
+ std::move(graph.graph), metric, quantizer, refine_quant))) {
99
+ py::array_t<float, py::array::c_style | py::array::forcecast> items(input);
100
+ auto buffer = items.request();
101
+ size_t rows, features;
102
+ get_input_array_shapes(buffer, &rows, &features);
103
+ float *vector_data = (float *)items.data(0);
104
+ searcher->SetData(vector_data, rows, features);
105
+ }
106
+
107
+ py::object search(py::object query, int k) {
108
+ py::array_t<float, py::array::c_style | py::array::forcecast> items(query);
109
+ int *ids;
110
+ float *dis;
111
+ {
112
+ py::gil_scoped_release l;
113
+ ids = new int[k];
114
+ dis = new float[k];
115
+ searcher->Search(items.data(0), k, ids, dis);
116
+ }
117
+ py::capsule free_when_done(ids, [](void *f) { delete[] f; });
118
+ py::capsule free_when_done_dis(dis, [](void *f) { delete[] f; });
119
+ return py::make_tuple(
120
+ py::array_t<int32_t>({(size_t)k}, {sizeof(int32_t)}, ids, free_when_done),
121
+ py::array_t<float>({(size_t)k}, {sizeof(float)}, dis, free_when_done_dis));
122
+ }
123
+
124
+ py::object batch_search(py::object query, int k, int num_threads = 0) {
125
+ py::array_t<float, py::array::c_style | py::array::forcecast> items(query);
126
+ auto buffer = items.request();
127
+ int32_t *ids;
128
+ float *dis;
129
+ size_t nq, dim;
130
+ {
131
+ py::gil_scoped_release l;
132
+ get_input_array_shapes(buffer, &nq, &dim);
133
+ ids = new int[nq * k];
134
+ dis = new float[nq * k];
135
+ if (num_threads != 0) {
136
+ omp_set_num_threads(num_threads);
137
+ }
138
+ searcher->SearchBatch(items.data(0), nq, k, ids, dis);
139
+ }
140
+ py::capsule free_when_done(ids, [](void *f) { delete[] f; });
141
+ py::capsule free_when_done_dis(dis, [](void *f) { delete[] f; });
142
+ return py::make_tuple(
143
+ py::array_t<int32_t>({(size_t)nq, (size_t)k}, {k * sizeof(int32_t), sizeof(int32_t)}, ids, free_when_done),
144
+ py::array_t<float>({(size_t)nq, (size_t)k}, {k * sizeof(float), sizeof(float)}, dis, free_when_done_dis));
145
+ }
146
+
147
+ void set_ef(int ef) { searcher->SetEf(ef); }
148
+
149
+ void optimize(int num_threads = 0) { searcher->Optimize(num_threads); }
150
+
151
+ double get_last_search_avg_dist_cmps() const {
152
+ return searcher->GetLastSearchAvgDistCmps();
153
+ }
154
+ };
155
+
156
+ struct Clustering {
157
+
158
+ glass::Clustering internal;
159
+
160
+ Clustering(int32_t n_cluster, int32_t epochs = 10,
161
+ const std::string &init = "random")
162
+ : internal(n_cluster, epochs, init) {}
163
+
164
+ void fit(py::object data) {
165
+ py::array_t<float, py::array::c_style | py::array::forcecast> items(data);
166
+ auto buffer = items.request();
167
+ size_t n, dim;
168
+ {
169
+ py::gil_scoped_release l;
170
+ get_input_array_shapes(buffer, &n, &dim);
171
+ internal.fit(items.data(0), n, dim);
172
+ }
173
+ }
174
+
175
+ py::object predict(py::object data) {
176
+ py::array_t<float, py::array::c_style | py::array::forcecast> items(data);
177
+ auto buffer = items.request();
178
+ std::vector<int32_t> ids;
179
+ size_t n, dim;
180
+ {
181
+ py::gil_scoped_release l;
182
+ get_input_array_shapes(buffer, &n, &dim);
183
+ ids = internal.predict(items.data(0), n, dim);
184
+ }
185
+ return py::array(ids.size(), ids.data());
186
+ }
187
+
188
+ py::object transform(py::object data, bool inplace = false) {
189
+ py::array_t<float, py::array::c_style | py::array::forcecast> items(data);
190
+ auto buffer = items.request();
191
+ size_t n, dim;
192
+ {
193
+ py::gil_scoped_release l;
194
+ get_input_array_shapes(buffer, &n, &dim);
195
+ if (inplace) {
196
+ internal.transform((float *)items.data(0), n, dim);
197
+ return py::none();
198
+ } else {
199
+ // TODO
200
+ return py::none();
201
+ }
202
+ }
203
+ }
204
+ };
205
+
206
+ #define DECLARE_QUANT(py_name, cxx_name) \
207
+ struct py_name { \
208
+ glass::cxx_name<glass::Metric::L2> quant; \
209
+ py_name(int32_t dim) : quant(dim) {} \
210
+ \
211
+ void train(py::object data) { \
212
+ py::array_t<float, py::array::c_style | py::array::forcecast> items( \
213
+ data); \
214
+ auto buffer = items.request(); \
215
+ size_t n, dim; \
216
+ { \
217
+ py::gil_scoped_release l; \
218
+ get_input_array_shapes(buffer, &n, &dim); \
219
+ quant.train(items.data(0), n); \
220
+ } \
221
+ } \
222
+ };
223
+
224
+ DECLARE_QUANT(quant_fp32, FP32Quantizer)
225
+ DECLARE_QUANT(quant_bf16, BF16Quantizer)
226
+ DECLARE_QUANT(quant_fp16, FP16Quantizer)
227
+ DECLARE_QUANT(quant_e5m2, E5M2Quantizer)
228
+ DECLARE_QUANT(quant_sq8, SQ8Quantizer)
229
+ DECLARE_QUANT(quant_sq8u, SQ8QuantizerUniform)
230
+
231
+ double build_graph(const std::string &index_type, py::object input,
232
+ const std::string &graph_file, const std::string &metric,
233
+ const std::string &quant = "FP32", int R = 32, int L = 200) {
234
+ Index index(index_type, metric, quant, R, L);
235
+ index.build(input).save(graph_file);
236
+ return index.get_construction_time();
237
+ }
238
+
239
+ PYBIND11_MODULE(glass, m) {
240
+ m.def("set_num_threads", &set_num_threads, py::arg("num_threads"));
241
+
242
+ py::class_<Graph>(m, "Graph")
243
+ .def(py::init<>())
244
+ .def(py::init<const std::string &, const std::string &>(),
245
+ py::arg("filename"), py::arg("format") = "glass")
246
+ .def("save", &Graph::save, py::arg("filename"))
247
+ .def("load", &Graph::load, py::arg("filename"),
248
+ py::arg("format") = "glass");
249
+
250
+ py::class_<Index>(m, "Index")
251
+ .def(py::init<const std::string &, const std::string &,
252
+ const std::string &, int, int>(),
253
+ py::arg("index_type"), py::arg("metric"), py::arg("quant") = "FP32",
254
+ py::arg("R") = 32, py::arg("L") = 0)
255
+ .def("build", &Index::build, py::arg("data"))
256
+ .def("get_construction_time", &Index::get_construction_time);
257
+
258
+ py::class_<Searcher>(m, "Searcher")
259
+ .def(py::init<Graph &, py::object, const std::string &,
260
+ const std::string &, const std::string &>(),
261
+ py::arg("graph"), py::arg("data"), py::arg("metric"),
262
+ py::arg("quantizer"), py::arg("refine_quant") = "")
263
+ .def("set_ef", &Searcher::set_ef, py::arg("ef"))
264
+ .def("search", &Searcher::search, py::arg("query"), py::arg("k"))
265
+ .def("batch_search", &Searcher::batch_search, py::arg("query"),
266
+ py::arg("k"), py::arg("num_threads") = 0)
267
+ .def("optimize", &Searcher::optimize, py::arg("num_threads") = 0)
268
+ .def("get_last_search_dist_cmps",
269
+ &Searcher::get_last_search_avg_dist_cmps);
270
+
271
+ py::class_<Clustering>(m, "Clustering")
272
+ .def(py::init<int32_t, int32_t, const std::string &>(),
273
+ py::arg("n_cluster"), py::arg("epochs") = 10,
274
+ py::arg("init") = "random")
275
+ .def("fit", &Clustering::fit, py::arg("data"))
276
+ .def("predict", &Clustering::predict, py::arg("data"))
277
+ .def("transform", &Clustering::transform, py::arg("data"),
278
+ py::arg("inplace") = false);
279
+
280
+ #define PYDECLARE_QUANT(py_name) \
281
+ py::class_<py_name>(m, #py_name) \
282
+ .def(py::init<int32_t>(), py::arg("dim")) \
283
+ .def("train", &py_name::train, py::arg("data"));
284
+
285
+ PYDECLARE_QUANT(quant_fp32)
286
+ PYDECLARE_QUANT(quant_bf16)
287
+ PYDECLARE_QUANT(quant_fp16)
288
+ PYDECLARE_QUANT(quant_e5m2)
289
+ PYDECLARE_QUANT(quant_sq8)
290
+ PYDECLARE_QUANT(quant_sq8u)
291
+
292
+ m.def("build_graph", &build_graph, py::arg("index_type"), py::arg("input"),
293
+ py::arg("graph_file"), py::arg("metric"), py::arg("quant"),
294
+ py::arg("R"), py::arg("L"));
295
+ }
@@ -0,0 +1,68 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyglass-portable
3
+ Version: 2.1.0
4
+ Summary: Portable wheels of pyglass (zilliztech) — a fast HNSW/NSG graph ANN library that builds and runs on macOS (Apple Silicon and Intel) and non-AVX-512 x86 (AMD, older Intel). Imported as `glass`.
5
+ Author: Zihao Wang
6
+ Maintainer: Ian Driver
7
+ License: MIT
8
+ Project-URL: Homepage, https://github.com/iandriver/pyglass
9
+ Project-URL: Upstream, https://github.com/zilliztech/pyglass
10
+ Project-URL: Issues, https://github.com/iandriver/pyglass/issues
11
+ Keywords: nearest-neighbors,ann,hnsw,nsg,vector-search,pyglass,similarity-search
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: POSIX :: Linux
16
+ Classifier: Operating System :: MacOS
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: C++
19
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: numpy
24
+ Dynamic: license-file
25
+
26
+ # pyglass-portable
27
+
28
+ Portable prebuilt wheels of [**pyglass**](https://github.com/zilliztech/pyglass)
29
+ (zilliztech) — a fast HNSW / NSG graph-based approximate-nearest-neighbor library.
30
+
31
+ Upstream publishes only an **AVX-512** x86_64 wheel, which crashes (`SIGILL`) on
32
+ CPUs without AVX-512 — Apple Silicon, AMD, older Intel. pyglass's *source* is
33
+ portable (AVX2 / NEON / scalar kernels); this repackage builds it with a portable
34
+ baseline so it installs and runs on those machines too. **The import name stays
35
+ `glass`** — it's a drop-in for upstream, not a fork of the algorithm.
36
+
37
+ ```bash
38
+ uv pip install pyglass-portable # or: pip install pyglass-portable
39
+ ```
40
+
41
+ Prebuilt wheels: Linux x86_64 (AVX2) and macOS arm64 (NEON), Python 3.9+. Intel
42
+ Macs and ARM Linux build from the sdist — `pip install` compiles it portably (it
43
+ needs a C++20 compiler; on macOS, `brew install libomp`).
44
+
45
+ ```python
46
+ import numpy as np, glass
47
+
48
+ X = np.random.rand(100_000, 128).astype("float32")
49
+ index = glass.Index("HNSW", "L2", "FP32", R=32, L=200)
50
+ graph = index.build(X)
51
+ searcher = glass.Searcher(graph, X, "L2", "FP32")
52
+ searcher.set_ef(64); searcher.optimize()
53
+ ids, dists = searcher.batch_search(X[:1000], 10)
54
+ ```
55
+
56
+ It also works as a `scanpy` neighbors backend via
57
+ [`pipnn.contrib.GlassTransformer`](https://github.com/iandriver/pipnn-scanpy).
58
+
59
+ ## Relationship to upstream
60
+
61
+ This is an unofficial repackage for platform coverage only — **no algorithm
62
+ changes**. The portability fixes are proposed upstream in
63
+ [zilliztech/pyglass#24](https://github.com/zilliztech/pyglass/pull/24); if they
64
+ land and upstream ships portable wheels, prefer `glassppy`. Version tracks the
65
+ upstream source version.
66
+
67
+ Credit to the pyglass authors (Zihao Wang / zilliztech). MIT licensed, same as
68
+ upstream.
@@ -0,0 +1,14 @@
1
+ LICENSE
2
+ README.md
3
+ README_portable.md
4
+ pyproject.toml
5
+ setup.py
6
+ ./python/bindings.cc
7
+ python/bindings.cc
8
+ python/ann_dataset/__init__.py
9
+ python/ann_dataset/dataset.py
10
+ python/pyglass_portable.egg-info/PKG-INFO
11
+ python/pyglass_portable.egg-info/SOURCES.txt
12
+ python/pyglass_portable.egg-info/dependency_links.txt
13
+ python/pyglass_portable.egg-info/requires.txt
14
+ python/pyglass_portable.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ ann_dataset
2
+ glass
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,24 @@
1
+ """Root build shim for the `pyglass-portable` repackage.
2
+
3
+ Upstream's setup.py lives in `python/` and references `../glass` / `../third_party`
4
+ relative to that directory. Building from the repo root instead makes the project
5
+ self-contained (`glass/` and `third_party/` sit right here), which is what
6
+ cibuildwheel needs. `python/build_extension.py` already resolves its sources and
7
+ include dirs relative to the current working directory, so this shim just calls
8
+ into it from the root.
9
+ """
10
+
11
+ import os
12
+ import sys
13
+
14
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "python"))
15
+
16
+ from build_extension import create_extension, get_build_ext_cmdclass # noqa: E402
17
+ from setuptools import setup # noqa: E402
18
+
19
+ setup(
20
+ ext_modules=create_extension(os.environ.get("GLASS_VERSION", "2.1.0")),
21
+ cmdclass=get_build_ext_cmdclass(),
22
+ package_dir={"": "python"},
23
+ packages=["ann_dataset"],
24
+ )