Skip to content

Commit d90422f

Browse files
authored
Merge pull request #5 from craftsangjae/feat/rust-acceleration
Rust acceleration via PyO3/maturin
2 parents de7adc8 + 47aba47 commit d90422f

19 files changed

Lines changed: 1223 additions & 62 deletions

.github/workflows/publish.yml

Lines changed: 63 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,72 @@ jobs:
4040
with:
4141
python-version: "3.11"
4242

43+
- name: Install Rust toolchain
44+
uses: dtolnay/rust-toolchain@stable
45+
4346
- name: Install dependencies
4447
run: |
4548
python -m pip install --upgrade pip
46-
pip install -e ".[dev]"
49+
pip install ".[dev]"
4750
4851
- name: Run tests
4952
run: pytest -v
5053

51-
publish:
54+
build-wheels:
5255
needs: [check-version, test]
5356
if: needs.check-version.outputs.should_release == 'true'
57+
strategy:
58+
matrix:
59+
include:
60+
- os: ubuntu-latest
61+
target: x86_64
62+
- os: ubuntu-latest
63+
target: aarch64
64+
- os: macos-latest
65+
target: x86_64
66+
- os: macos-latest
67+
target: aarch64
68+
- os: windows-latest
69+
target: x86_64
70+
runs-on: ${{ matrix.os }}
71+
steps:
72+
- uses: actions/checkout@v4
73+
74+
- name: Build wheels
75+
uses: PyO3/maturin-action@v1
76+
with:
77+
target: ${{ matrix.target }}
78+
args: --release --out dist
79+
manylinux: auto
80+
81+
- name: Upload wheels
82+
uses: actions/upload-artifact@v4
83+
with:
84+
name: wheels-${{ matrix.os }}-${{ matrix.target }}
85+
path: dist
86+
87+
build-sdist:
88+
needs: [check-version, test]
89+
if: needs.check-version.outputs.should_release == 'true'
90+
runs-on: ubuntu-latest
91+
steps:
92+
- uses: actions/checkout@v4
93+
94+
- name: Build sdist
95+
uses: PyO3/maturin-action@v1
96+
with:
97+
command: sdist
98+
args: --out dist
99+
100+
- name: Upload sdist
101+
uses: actions/upload-artifact@v4
102+
with:
103+
name: wheels-sdist
104+
path: dist
105+
106+
publish:
107+
needs: [check-version, build-wheels, build-sdist]
108+
if: needs.check-version.outputs.should_release == 'true'
54109
runs-on: ubuntu-latest
55110
permissions:
56111
id-token: write
@@ -59,24 +114,18 @@ jobs:
59114
steps:
60115
- uses: actions/checkout@v4
61116

62-
- name: Set up Python
63-
uses: actions/setup-python@v5
64-
with:
65-
python-version: "3.11"
66-
67117
- name: Create version tag
68118
run: |
69119
VERSION=${{ needs.check-version.outputs.version }}
70120
git tag "v$VERSION"
71121
git push origin "v$VERSION"
72122
73-
- name: Install build dependencies
74-
run: |
75-
python -m pip install --upgrade pip
76-
pip install build hatchling
77-
78-
- name: Build wheel and sdist
79-
run: python -m build
123+
- name: Download all artifacts
124+
uses: actions/download-artifact@v4
125+
with:
126+
pattern: wheels-*
127+
merge-multiple: true
128+
path: dist
80129

81130
- name: Publish to PyPI
82131
uses: pypa/gh-action-pypi-publish@release/v1

.github/workflows/test.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,13 @@ jobs:
2121
with:
2222
python-version: ${{ matrix.python-version }}
2323

24+
- name: Install Rust toolchain
25+
uses: dtolnay/rust-toolchain@stable
26+
2427
- name: Install dependencies
2528
run: |
2629
python -m pip install --upgrade pip
27-
pip install -e ".[dev]"
30+
pip install ".[dev]"
2831
2932
- name: Run ruff (lint)
3033
run: ruff check .

.gitignore

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,11 @@ examples/.cache/
2121
.vscode/
2222

2323
# Benchmark results (generated, not committed)
24-
benchmarks/results/*.json
24+
benchmarks/results/*.json
25+
26+
target/
27+
28+
# Rust/maturin build artifacts
29+
*.so
30+
*.dylib
31+
*.pyd

CLAUDE.md

Lines changed: 59 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@ pytest # Tests (must pass)
3939

4040
### Setup
4141
```bash
42-
pip install -e . # Install package in editable mode
43-
pip install -e ".[dev]" # Install with dev dependencies
42+
pip install maturin # Required for building Rust extension
43+
maturin develop --release # Build Rust extension (needs Rust toolchain)
44+
pip install -e ".[dev]" # Install with dev dependencies
4445
```
4546

4647
### Testing
@@ -88,25 +89,34 @@ python examples/colbert_nanobeir.py
8889

8990
**Local build (for testing):**
9091
```bash
91-
pip install build
92-
python -m build
93-
twine check dist/*
92+
maturin build --release # Build wheel with Rust extension
9493
```
9594

9695
## Architecture
9796

9897
### Core Components
9998

10099
**`muvera/muvera.py`** - Main `Muvera` class implementing Fixed Dimensional Encoding (FDE)
101-
- Three encoding paths: single document, uniform batch, variable-length batch
100+
- Two encoding paths: single document, variable-length batch
102101
- Document encoding uses AVERAGE aggregation within partitions
103102
- Query encoding uses SUM aggregation within partitions
104103
- Optional final dimensionality reduction via Count Sketch
104+
- Hot-path methods (`_aggregate_single`, `_scatter_add`, `_fill_empty_batch`) delegate to Rust kernels when available
105105

106106
**`muvera/helper.py`** - Low-level utilities (not public API)
107107
- Gray code manipulation for partition indexing
108108
- Random projection matrices (SimHash, AMS Sketch, Count Sketch)
109109
- Vectorized batch partition indexing
110+
- `partition_index_gray` and `partition_indices_gray_batch` delegate to Rust when available
111+
112+
**`src/`** - Rust extension module (`muvera._rust_kernels`) via PyO3/maturin
113+
- `gray_code.rs` — Gray code append and binary conversion
114+
- `partition.rs` — Single and batch Gray-code partition indexing
115+
- `scatter.rs` — Scatter-add kernel for batch aggregation
116+
- `fill_empty.rs` — Single-point-cloud aggregation + batch empty partition filling
117+
- `lib.rs` — PyO3 module definition exposing 5 functions
118+
119+
**`muvera/_rust_kernels.pyi`** - Type stubs for the Rust extension module
110120

111121
### Algorithm Flow
112122

@@ -120,24 +130,58 @@ twine check dist/*
120130
6. **Repetitions**: Repeat steps 1-5 with different random seeds, concatenating results
121131
7. **Final Projection** (optional): Apply Count Sketch to reduce final dimension
122132

133+
### Rust Acceleration
134+
135+
Performance-critical inner loops are implemented in Rust via PyO3, with automatic fallback to pure Python:
136+
137+
```python
138+
# muvera/__init__.py
139+
try:
140+
import muvera._rust_kernels
141+
_RUST_AVAILABLE = True
142+
except ImportError:
143+
_RUST_AVAILABLE = False
144+
```
145+
146+
**Accelerated functions:**
147+
| Rust function | Python fallback | Speedup |
148+
|---|---|---|
149+
| `aggregate_single` | `Muvera._aggregate_single_python` | 8-17x (single doc) |
150+
| `scatter_add_partitions` | `Muvera._scatter_add` (np.add.at loop) | 1-2.5x (batch) |
151+
| `fill_empty_partitions_batch` | `Muvera._fill_empty_batch` (Python loop) | 1-2.5x (batch) |
152+
| `partition_index_gray` | `helper._partition_index_gray_python` | part of aggregate |
153+
| `partition_indices_gray_batch` | `helper._partition_indices_gray_batch_python` | part of batch |
154+
155+
**What is NOT in Rust** (intentionally kept in NumPy for seed compatibility):
156+
- `simhash_matrix_from_seed`, `ams_projection_matrix_from_seed` — depend on `np.random.default_rng`
157+
- `count_sketch_vector_from_seed` — same reason
158+
- `Muvera.__init__`, public API signatures — 100% unchanged
159+
123160
### Batch Processing
124161

125-
The library supports three input formats:
162+
The library supports two input formats:
126163
- **Single**: `(num_vectors, dimension)` - processes one point cloud
127-
- **Uniform batch**: `(batch_size, num_vectors, dimension)` - all point clouds have same length
128164
- **Variable-length batch**: `list[np.ndarray]` - each point cloud has different length (recommended for real-world data)
129165

130-
Variable-length batch processing flattens all point clouds, processes them together, then aggregates per-document using `np.add.at()` for efficient scatter-add operations.
166+
Variable-length batch processing flattens all point clouds, processes them together, then aggregates per-document using Rust `scatter_add_partitions` (or `np.add.at()` fallback).
131167

132168
## Code Conventions
133169

170+
### Python
134171
- NumPy-style docstrings (configured in pyproject.toml)
135172
- Type hints required (Python 3.9+ syntax with `|` for unions)
136173
- Line length: 100 characters
137174
- Use `np.float32` for all embeddings (memory efficiency)
138175
- Use `np.uint32` for partition indices
139176
- Random number generation via `np.random.default_rng(seed)` for reproducibility
140177

178+
### Rust
179+
- Edition 2021
180+
- Dependencies: `pyo3` 0.23, `numpy` 0.23 (Rust crate, not Python package), `ndarray` 0.16
181+
- All three crates are version-locked together (upgrade all at once)
182+
- Use `f32` for all floating-point data, `u32` for partition indices, `i32` for counts, `i64` for boundaries
183+
- PyO3 functions accept `PyReadonlyArray*` for input arrays and `&Bound<PyArray*>` for in-place mutation
184+
141185
## Testing
142186

143187
### Test Organization
@@ -146,6 +190,7 @@ Variable-length batch processing flattens all point clouds, processes them toget
146190
- **`test_muvera.py`**: Core Muvera class tests (shapes, validation, reproducibility)
147191
- **`test_reference.py`**: Validation against reference implementation (sionic-ai/muvera-py)
148192
- **`test_real_colbert.py`**: Real-world ColBERT embedding tests using NanoBEIR fixtures
193+
- **`test_rust_equivalence.py`**: Numerical equivalence tests between Rust kernels and Python fallbacks (skipped if Rust extension is unavailable)
149194

150195
### Real Data Testing
151196

@@ -173,15 +218,17 @@ Output dimension: `num_repetitions * 2^num_simhash_projections * projection_dime
173218
**`.github/workflows/test.yml`** - Continuous Integration
174219
- Triggers: Push to main, all pull requests
175220
- Tests across Python 3.9-3.13
221+
- Installs Rust toolchain via `dtolnay/rust-toolchain@stable`
222+
- Builds Rust extension via `pip install ".[dev]"` (maturin build backend)
176223
- Runs ruff (lint + format check), mypy (type checking), pytest
177-
- Tests example scripts
178224

179225
**`.github/workflows/publish.yml`** - PyPI Publishing
180226
- Triggers: Push to main
181227
- Checks if `v{version}` tag already exists; skips release if it does
182228
- Runs full test suite
183-
- Creates git tag, builds wheel/sdist, publishes to PyPI via OIDC
184-
- Creates GitHub Release with release notes
229+
- Builds cross-platform wheels via `PyO3/maturin-action@v1` (Linux x86_64/aarch64, macOS x86_64/aarch64, Windows x86_64)
230+
- Builds sdist separately
231+
- Creates git tag, publishes to PyPI via OIDC, creates GitHub Release
185232

186233
### Deployment Policy
187234

0 commit comments

Comments
 (0)