fuzzgpu 0.1.5__tar.gz → 0.1.7__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.
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/Cargo.lock +2 -2
- fuzzgpu-0.1.7/PKG-INFO +418 -0
- fuzzgpu-0.1.7/README.md +388 -0
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/Cargo.toml +1 -1
- fuzzgpu-0.1.7/crates/fuzzgpu-core/src/fuzz.rs +804 -0
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/src/jaro.rs +89 -23
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/src/levenshtein.rs +9 -0
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/src/lib.rs +6 -1
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/src/shaders/jaro.wgsl +48 -32
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/src/shaders/jaro_matrix.wgsl +48 -28
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/src/simd.rs +3 -3
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-python/Cargo.toml +2 -2
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-python/src/lib.rs +52 -62
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-wasm/Cargo.toml +1 -1
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-wasm/src/lib.rs +4 -4
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/pyproject.toml +1 -1
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/python/fuzzgpu/__init__.py +24 -4
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/python/fuzzgpu/__init__.pyi +67 -6
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/python/fuzzgpu/distance/Hamming.py +40 -1
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/python/fuzzgpu/distance/Indel.py +13 -0
- fuzzgpu-0.1.7/python/fuzzgpu/distance/LCSseq.py +140 -0
- fuzzgpu-0.1.7/python/fuzzgpu/distance/Levenshtein.py +174 -0
- fuzzgpu-0.1.7/python/fuzzgpu/distance/Postfix.py +41 -0
- fuzzgpu-0.1.7/python/fuzzgpu/distance/Prefix.py +41 -0
- fuzzgpu-0.1.7/python/fuzzgpu/distance/__init__.py +34 -0
- fuzzgpu-0.1.7/python/fuzzgpu/distance/__init__.pyi +42 -0
- fuzzgpu-0.1.7/python/fuzzgpu/distance/_common.py +619 -0
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/python/fuzzgpu/fuzz.py +30 -18
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/python/fuzzgpu/fuzz.pyi +15 -3
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/python/fuzzgpu/process.py +4 -4
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/tests/test_api_signatures.py +1 -1
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/tests/test_edge_cases.py +3 -3
- fuzzgpu-0.1.7/tests/test_rapidfuzz_compat.py +213 -0
- fuzzgpu-0.1.5/PKG-INFO +0 -559
- fuzzgpu-0.1.5/README.md +0 -529
- fuzzgpu-0.1.5/crates/fuzzgpu-core/src/fuzz.rs +0 -322
- fuzzgpu-0.1.5/python/fuzzgpu/distance/Levenshtein.py +0 -111
- fuzzgpu-0.1.5/python/fuzzgpu/distance/__init__.py +0 -5
- fuzzgpu-0.1.5/python/fuzzgpu/distance/_common.py +0 -6
- fuzzgpu-0.1.5/tests/test_rapidfuzz_compat.py +0 -43
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/Cargo.toml +0 -0
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/LICENSE +0 -0
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/assets/logo.png +0 -0
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/benches/bench.rs +0 -0
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/src/damerau.rs +0 -0
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/src/gpu.rs +0 -0
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/src/needleman.rs +0 -0
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/src/shaders/damerau.wgsl +0 -0
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/src/shaders/damerau_matrix.wgsl +0 -0
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/src/shaders/levenshtein.wgsl +0 -0
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/src/shaders/levenshtein_cdist_myers.wgsl +0 -0
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/src/shaders/levenshtein_matrix.wgsl +0 -0
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/src/shaders/levenshtein_myers.wgsl +0 -0
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/src/shaders/levenshtein_short.wgsl +0 -0
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/src/shaders/needleman_affine.wgsl +0 -0
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/src/shaders/needleman_wavefront.wgsl +0 -0
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/tests/differential.proptest-regressions +0 -0
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/tests/differential.rs +0 -0
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/tests/fixtures/broken.wgsl +0 -0
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/tests/kernel_registration.rs +0 -0
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-wasm/tests/differential_harness.js +0 -0
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-wasm/tests/js_api.test.cjs +0 -0
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/python/fuzzgpu/distance/DamerauLevenshtein.py +0 -0
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/python/fuzzgpu/distance/Jaro.py +0 -0
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/python/fuzzgpu/distance/JaroWinkler.py +0 -0
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/python/fuzzgpu/distance/OSA.py +0 -0
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/python/fuzzgpu/process.pyi +0 -0
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/tests/test_basic.py +0 -0
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/tests/test_concurrency.py +0 -0
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/tests/test_invariants.py +0 -0
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/tests/test_out_buffers.py +0 -0
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/tests/test_stress.py +0 -0
- {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/tests/wasm_python_differential.py +0 -0
|
@@ -372,7 +372,7 @@ dependencies = [
|
|
|
372
372
|
|
|
373
373
|
[[package]]
|
|
374
374
|
name = "fuzzgpu-core"
|
|
375
|
-
version = "0.1.
|
|
375
|
+
version = "0.1.7"
|
|
376
376
|
dependencies = [
|
|
377
377
|
"bytemuck",
|
|
378
378
|
"criterion",
|
|
@@ -386,7 +386,7 @@ dependencies = [
|
|
|
386
386
|
|
|
387
387
|
[[package]]
|
|
388
388
|
name = "fuzzgpu-python"
|
|
389
|
-
version = "0.1.
|
|
389
|
+
version = "0.1.7"
|
|
390
390
|
dependencies = [
|
|
391
391
|
"fuzzgpu-core",
|
|
392
392
|
"log",
|
fuzzgpu-0.1.7/PKG-INFO
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fuzzgpu
|
|
3
|
+
Version: 0.1.7
|
|
4
|
+
Classifier: Development Status :: 4 - Beta
|
|
5
|
+
Classifier: Intended Audience :: Developers
|
|
6
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Programming Language :: Rust
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering
|
|
14
|
+
Classifier: Topic :: Text Processing :: General
|
|
15
|
+
Requires-Dist: pytest>=7.0 ; extra == 'test'
|
|
16
|
+
Requires-Dist: pytest-benchmark ; extra == 'test'
|
|
17
|
+
Provides-Extra: test
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Summary: Hardware-accelerated fuzzy string matching & sequence alignment — cross-platform, no CUDA required
|
|
20
|
+
Keywords: gpu,fuzzy,string-matching,levenshtein,damerau,jaro-winkler,needleman-wunsch,webgpu
|
|
21
|
+
Home-Page: https://github.com/kuntal-devrat/fuzzgpu
|
|
22
|
+
Author: Devrat Kuntal
|
|
23
|
+
License: MIT
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
26
|
+
Project-URL: Homepage, https://github.com/kuntal-devrat/fuzzgpu
|
|
27
|
+
Project-URL: Issues, https://github.com/kuntal-devrat/fuzzgpu/issues
|
|
28
|
+
Project-URL: Repository, https://github.com/kuntal-devrat/fuzzgpu
|
|
29
|
+
|
|
30
|
+
<div align="center">
|
|
31
|
+
|
|
32
|
+
<img src="https://raw.githubusercontent.com/kuntal-devrat/fuzzgpu/main/assets/logo.png" alt="fuzzgpu logo" width="140" height="140" />
|
|
33
|
+
|
|
34
|
+
# fuzzgpu
|
|
35
|
+
|
|
36
|
+
**Hardware-Accelerated Fuzzy String Matching & Sequence Alignment**
|
|
37
|
+
|
|
38
|
+
*Cross-platform GPU compute via WebGPU (`wgpu`) & Multi-Core CPU parallelism with Rayon. Zero CUDA dependencies.*
|
|
39
|
+
|
|
40
|
+
[](https://pypi.org/project/fuzzgpu/)
|
|
41
|
+
[](https://opensource.org/licenses/MIT)
|
|
42
|
+
[](https://www.rust-lang.org)
|
|
43
|
+
[](https://github.com/kuntal-devrat/fuzzgpu)
|
|
44
|
+
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Overview
|
|
50
|
+
|
|
51
|
+
`fuzzgpu` is a high-throughput string distance and sequence alignment engine written in **Rust** with native **Python** and **WebAssembly** bindings. It leverages GPU compute shaders (`wgpu` / WGSL) and Rayon multi-threading to accelerate large-scale batch queries and distance matrix computations across:
|
|
52
|
+
|
|
53
|
+
- **Apple Silicon (Metal)**
|
|
54
|
+
- **Linux (Vulkan)**
|
|
55
|
+
- **Windows (DirectX 12 / Vulkan)**
|
|
56
|
+
- **Integrated GPUs (Intel Iris Xe, AMD Radeon)**
|
|
57
|
+
- **WebAssembly (In-browser execution)**
|
|
58
|
+
|
|
59
|
+
No NVIDIA CUDA drivers or complex toolkits required.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## What's New in v0.1.7
|
|
64
|
+
|
|
65
|
+
### Bug fixes (Windows DX12 / Jaro shader)
|
|
66
|
+
- **Jaro GPU shader FXC crash fixed** — `jaro.wgsl` and `jaro_matrix.wgsl` used dynamic vector
|
|
67
|
+
component writes (`v[j >> 5u] = ...`) in `bit_set()`. FXC (the DX12 HLSL compiler) cannot
|
|
68
|
+
emit a dynamic register-indexed store for a non-constant index, causing a hard compile failure
|
|
69
|
+
with `X3550`/`X3511` on every Windows DX12 runner — all 11 Jaro GPU tests were panicking.
|
|
70
|
+
Fixed by rewriting `bit_set` with `select()`-based static construction, which compiles
|
|
71
|
+
identically on all backends (Vulkan, Metal, DX12).
|
|
72
|
+
|
|
73
|
+
### Bug fixes (arity mismatch — wasm & fuzz crates)
|
|
74
|
+
- **`fuzzgpu-wasm`** — `partial_ratio`, `token_sort_ratio`, `token_set_ratio`, `wratio` gained
|
|
75
|
+
a `score_cutoff: f64` parameter in the v0.1.6 Rust core rewrite for rapidfuzz parity, but
|
|
76
|
+
the WASM bindings were not updated, causing 4 `E0061` compile errors. Fixed by passing
|
|
77
|
+
`0.0` as the cutoff (no cutoff — identical to the previous behaviour).
|
|
78
|
+
- **`fuzzgpu-fuzz`** — same four callsites in `fuzz/src/lib.rs` had the same arity mismatch.
|
|
79
|
+
Fixed identically.
|
|
80
|
+
|
|
81
|
+
### Includes all v0.1.6 features
|
|
82
|
+
All features from v0.1.6 are included — see the v0.1.6 changelog below.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
<details>
|
|
87
|
+
<summary><b>Previous (v0.1.6)</b></summary>
|
|
88
|
+
|
|
89
|
+
### Drop-in rapidfuzz parity (Python)
|
|
90
|
+
The full Python layer is now byte-identical to rapidfuzz 3.14.5 over a 169,744-pair differential harness across `ratio`, `partial_ratio`, `token_sort_ratio`, `token_set_ratio`, `token_ratio`, `WRatio`, `QRatio`, `partial_token_*`, `jaro`, `jaro_winkler`, `levenshtein`, `indel`, `hamming`, `osa` — **0 mismatches**.
|
|
91
|
+
|
|
92
|
+
### Bug fixes (Rust core, float parity)
|
|
93
|
+
- **`ratio` / `partial_ratio` cutoff imprecision** — port of rapidfuzz's load-bearing `NormSim_to_NormDist = min(1, 1 - cutoff/100 + 1e-5)` term.
|
|
94
|
+
- **`ratio` score formula** — switched from `((len-dist)/len)*100` to `(1 - dist/len)*100` to match rapidfuzz C++'s exact ulp order.
|
|
95
|
+
|
|
96
|
+
### New features (Python distance layer)
|
|
97
|
+
- **`Editops` / `Opcodes` / `Editop` / `Opcode` / `MatchingBlock` / `ScoreAlignment`** classes (rapidfuzz-compatible).
|
|
98
|
+
- **`Levenshtein.editops` / `.opcodes`**, **`LCSseq`**, **`Prefix`**, **`Postfix`**, **`Hamming.editops`**, **`Indel.editops`** modules.
|
|
99
|
+
- **`fuzz.partial_ratio_alignment`** returns `ScoreAlignment` (rapidfuzz-compatible).
|
|
100
|
+
- **`process.extract` / `extractOne` / `cdist`** default to `WRatio`.
|
|
101
|
+
- All alignment types re-exported at the package root.
|
|
102
|
+
|
|
103
|
+
</details>
|
|
104
|
+
|
|
105
|
+
<details>
|
|
106
|
+
<summary><b>Previous (v0.1.5)</b></summary>
|
|
107
|
+
|
|
108
|
+
### Bug fixes
|
|
109
|
+
- **Damerau-Levenshtein safety gate** now fires in release builds (`assert!` not `debug_assert!`) — non-ASCII inputs no longer silently produce wrong distances in production wheels
|
|
110
|
+
- **Needleman-Wunsch GPU f32 precision guard** — scoring parameters that exceed the exact f32 integer range (2²⁴ = 16,777,216) now automatically route to CPU, preventing silent precision loss
|
|
111
|
+
- **Wavefront shader race condition** fixed — `diags[1]` seed initialization consolidated into a single thread with a proper `workgroupBarrier()`
|
|
112
|
+
- **`extract_one` early-exit** fixed — the `break` at score==100.0 now only fires after the `is_better` check
|
|
113
|
+
- **Distance module processor bug** fixed across all `distance/*.py` modules — `similarity`/`normalized_*` now apply the processor once, then compute `maximum` on the processed strings
|
|
114
|
+
|
|
115
|
+
### Optimizations
|
|
116
|
+
- **Zero-allocation SIMD hot paths** — `levenshtein_cdist`, `levenshtein_batch`, and `jaro_winkler_batch` now use stack-allocated `[&[u8]; 8]` instead of per-group heap `Vec`, eliminating millions of tiny allocations at 1M-cell matrix scale
|
|
117
|
+
- **`token_set_ratio`** uses `Cow<str>` to skip heap allocation when intersection/difference sets are empty
|
|
118
|
+
- **`process.cdist`** fast path routes through the Rayon/GPU `ratio_batch` when the default scorer is used, instead of one Python call per cell
|
|
119
|
+
|
|
120
|
+
### New features
|
|
121
|
+
- `partial_ratio_alignment(s1, s2)` → `(score, src_start, dest_start, length)` — rapidfuzz-compatible alignment result
|
|
122
|
+
- `partial_token_sort_ratio`, `partial_token_set_ratio`, `QRatio` — now exposed at the top level
|
|
123
|
+
- **Jaro-Winkler GPU routing in Python** — `jaro_winkler_batch` and `jaro_winkler_cdist` now use the GPU kernel on discrete GPUs
|
|
124
|
+
- **Needleman-Wunsch GPU routing in Python** — `needleman_wunsch_affine_batch` now uses `GpuNeedlemanAffineKernel`
|
|
125
|
+
- `editops` and `opcodes` re-exported at the top level (`fuzzgpu.editops`, `fuzzgpu.opcodes`)
|
|
126
|
+
- Complete type stubs (`__init__.pyi`, `fuzz.pyi`, `process.pyi`)
|
|
127
|
+
|
|
128
|
+
</details>
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Benchmark Results
|
|
133
|
+
|
|
134
|
+
*Hardware: Intel(R) Iris(R) Xe Graphics (Vulkan) + Intel Core i7 (Rayon uses all cores)*
|
|
135
|
+
*Versions: fuzzgpu 0.1.6 · rapidfuzz 3.14.5 · python-Levenshtein 0.27.4*
|
|
136
|
+
*Median of 7 runs after warmup. Reproduce: `python benchmarks/bench_compare.py`*
|
|
137
|
+
|
|
138
|
+
### Levenshtein Batch (1 query × N candidates, 10-char strings)
|
|
139
|
+
| Batch Size | `fuzzgpu` (GPU) | `fuzzgpu` (CPU) | `rapidfuzz` | vs RF (GPU) | vs RF (CPU) |
|
|
140
|
+
| :--- | :---: | :---: | :---: | :---: | :---: |
|
|
141
|
+
| **100** | 0.12 ms | 0.01 ms | 0.03 ms | 0.25× | 2.21× |
|
|
142
|
+
| **1,000** | 0.83 ms | 0.09 ms | 0.15 ms | 0.18× | 1.67× |
|
|
143
|
+
| **10,000** | 2.60 ms | 0.87 ms | 1.47 ms | 0.57× | 1.68× |
|
|
144
|
+
| **50,000** | 9.92 ms | 5.04 ms | 6.38 ms | 0.64× | 1.27× |
|
|
145
|
+
|
|
146
|
+
### Damerau-Levenshtein Batch (unrestricted Lowrance-Wagner)
|
|
147
|
+
| Batch Size | `fuzzgpu` (GPU) | `fuzzgpu` (CPU) | `rapidfuzz` | vs RF (GPU) | vs RF (CPU) |
|
|
148
|
+
| :--- | :---: | :---: | :---: | :---: | :---: |
|
|
149
|
+
| **1,000** | 0.58 ms | 0.40 ms | 1.63 ms | 2.81× | 4.06× |
|
|
150
|
+
| **10,000** | 2.95 ms | 2.15 ms | 20.86 ms | 7.06× | 9.70× |
|
|
151
|
+
| **50,000** | 17.49 ms | 12.08 ms | 120.25 ms | 6.88× | 9.95× |
|
|
152
|
+
|
|
153
|
+
> **Note:** rapidfuzz's `DamerauLevenshtein` uses Optimal String Alignment (OSA). fuzzgpu implements the **unrestricted** Lowrance-Wagner (1975) algorithm which allows non-adjacent transpositions. For example: `damerau("ca", "abc") == 2` (fuzzgpu) vs `3` (rapidfuzz OSA). Use `fuzzgpu.distance.OSA` for OSA-compatible semantics.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Installation
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
pip install fuzzgpu
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
```toml
|
|
164
|
+
# Rust
|
|
165
|
+
[dependencies]
|
|
166
|
+
fuzzgpu-core = "0.1.7"
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Quickstart
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
import fuzzgpu
|
|
175
|
+
|
|
176
|
+
# ── Core distance metrics ─────────────────────────────────────────────────────
|
|
177
|
+
lev = fuzzgpu.levenshtein("kitten", "sitting") # 3
|
|
178
|
+
dam = fuzzgpu.damerau("ab", "ba") # 1 (transposition)
|
|
179
|
+
jw = fuzzgpu.jaro_winkler("MARTHA", "MARHTA") # 0.9611...
|
|
180
|
+
|
|
181
|
+
# ── Batch (auto-routed GPU/CPU) ───────────────────────────────────────────────
|
|
182
|
+
candidates = ["hallo", "hullo", "jello", "yellow", "hello world"] * 10_000
|
|
183
|
+
distances = fuzzgpu.levenshtein_batch("hello", candidates)
|
|
184
|
+
jw_scores = fuzzgpu.jaro_winkler_batch("hello", candidates, p=0.1)
|
|
185
|
+
nw_scores = fuzzgpu.needleman_wunsch_affine_batch(
|
|
186
|
+
"AGTACGCA", candidates, match=2, mismatch=-1, gap_open=-3, gap_extend=-1
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# ── Cross-product distance matrix ─────────────────────────────────────────────
|
|
190
|
+
matrix = fuzzgpu.levenshtein_cdist(["abc", "def", "xyz"], ["abd", "axy", "def"])
|
|
191
|
+
|
|
192
|
+
# ── Zero-allocation outputs (write into preallocated numpy arrays) ────────────
|
|
193
|
+
import numpy as np
|
|
194
|
+
out_u32 = np.empty(len(candidates), dtype=np.uint32)
|
|
195
|
+
out_f64 = np.empty(len(candidates), dtype=np.float64)
|
|
196
|
+
mat_u32 = np.empty((3, 3), dtype=np.uint32)
|
|
197
|
+
fuzzgpu.levenshtein_batch_into("hello", candidates, out_u32)
|
|
198
|
+
fuzzgpu.jaro_winkler_batch_into("hello", candidates, out_f64)
|
|
199
|
+
fuzzgpu.levenshtein_cdist_into(["abc", "def", "xyz"], ["abd", "axy", "def"], mat_u32)
|
|
200
|
+
|
|
201
|
+
# ── Global sequence alignment (Gotoh 1982 affine gap) ────────────────────────
|
|
202
|
+
score = fuzzgpu.needleman_wunsch_affine("AGTACGCA", "TATGC", 2, -1, -3, -1)
|
|
203
|
+
|
|
204
|
+
# ── Fuzzy ratios (rapidfuzz-compatible) ──────────────────────────────────────
|
|
205
|
+
from fuzzgpu.fuzz import (
|
|
206
|
+
ratio, partial_ratio, partial_ratio_alignment,
|
|
207
|
+
token_sort_ratio, token_set_ratio,
|
|
208
|
+
partial_token_sort_ratio, partial_token_set_ratio,
|
|
209
|
+
QRatio, WRatio,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
ratio("fuzzy was a bear", "fuzzy was a bear") # 100.0
|
|
213
|
+
partial_ratio("hello", "oh hello there") # 100.0
|
|
214
|
+
score, src, dst, length = partial_ratio_alignment("hello", "oh hello there")
|
|
215
|
+
# (100.0, 0, 3, 5) ← window starts at char 3 of the longer string
|
|
216
|
+
|
|
217
|
+
token_sort_ratio("new york mets", "mets new york") # 100.0
|
|
218
|
+
token_set_ratio("fuzzy was a bear", "fuzzy bear") # 100.0
|
|
219
|
+
|
|
220
|
+
# ── Alignment helpers (rapidfuzz-compatible) ──────────────────────────────────
|
|
221
|
+
from fuzzgpu.distance import Levenshtein
|
|
222
|
+
ops = fuzzgpu.editops("kitten", "sitting") # top-level alias
|
|
223
|
+
codes = fuzzgpu.opcodes("kitten", "sitting")
|
|
224
|
+
|
|
225
|
+
# ── Search ────────────────────────────────────────────────────────────────────
|
|
226
|
+
from fuzzgpu.fuzz import extract, extractOne
|
|
227
|
+
best = extractOne("hellp", ["hello", "world", "help"], score_cutoff=50.0)
|
|
228
|
+
# ("help", 88.88888888888889, 2)
|
|
229
|
+
top_3 = extract("apple", ["apply", "ape", "banana", "applesauce"],
|
|
230
|
+
score_cutoff=50.0, limit=3)
|
|
231
|
+
|
|
232
|
+
# ── rapidfuzz.process-compatible API ─────────────────────────────────────────
|
|
233
|
+
from fuzzgpu.process import extract, extractOne, cdist
|
|
234
|
+
matrix = cdist(["hello", "world"], ["hallo", "wurld"]) # uses GPU/Rayon
|
|
235
|
+
|
|
236
|
+
# ── distance submodule (rapidfuzz.distance-compatible) ───────────────────────
|
|
237
|
+
from fuzzgpu.distance import Levenshtein, DamerauLevenshtein, Jaro, JaroWinkler
|
|
238
|
+
from fuzzgpu.distance import Hamming, OSA, Indel
|
|
239
|
+
|
|
240
|
+
Levenshtein.distance("kitten", "sitting") # 3
|
|
241
|
+
Levenshtein.normalized_similarity("kitten", "sitting") # 0.571...
|
|
242
|
+
Levenshtein.similarity(" abc ", "abc", processor=str.strip) # 3
|
|
243
|
+
DamerauLevenshtein.distance("ca", "abc") # 2 (unrestricted)
|
|
244
|
+
OSA.distance("ca", "abc") # 3 (OSA / rapidfuzz-compatible)
|
|
245
|
+
JaroWinkler.similarity("MARTHA", "MARHTA", prefix_weight=0.1) # 0.9611...
|
|
246
|
+
|
|
247
|
+
# ── Hardware diagnostics ──────────────────────────────────────────────────────
|
|
248
|
+
print(fuzzgpu.gpu_info()) # "Intel(R) Iris(R) Xe Graphics (Vulkan)"
|
|
249
|
+
print(fuzzgpu.hardware_info()) # adapter, threshold, last routing stats
|
|
250
|
+
|
|
251
|
+
fuzzgpu.set_gpu_threshold(100) # force GPU for batches >= 100 pairs
|
|
252
|
+
fuzzgpu.set_gpu_threshold(None) # restore auto-selection
|
|
253
|
+
fuzzgpu.set_cpu_only(True) # force CPU-only mode
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## Rust API
|
|
259
|
+
|
|
260
|
+
```toml
|
|
261
|
+
[dependencies]
|
|
262
|
+
fuzzgpu-core = "0.1.7" # GPU + CPU fallback
|
|
263
|
+
# fuzzgpu-core = { version = "0.1.7", default-features = false } # CPU-only
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
```rust
|
|
267
|
+
use fuzzgpu_core::levenshtein::gpu_ext::GpuLevenshteinKernel;
|
|
268
|
+
|
|
269
|
+
fn main() -> fuzzgpu_core::Result<()> {
|
|
270
|
+
let kernel = GpuLevenshteinKernel::get()?;
|
|
271
|
+
|
|
272
|
+
// Batch
|
|
273
|
+
let pairs = vec![("kitten", "sitting"), ("hello", "hullo")];
|
|
274
|
+
let distances = kernel.compute(&pairs)?; // [3, 1]
|
|
275
|
+
|
|
276
|
+
// Cross-product matrix
|
|
277
|
+
let matrix = kernel.compute_matrix(&["abc", "def"], &["abc", "xyz"])?;
|
|
278
|
+
|
|
279
|
+
// Multi-op batch (one GPU dispatch + readback amortized across all ops)
|
|
280
|
+
let mut batch = kernel.batch();
|
|
281
|
+
batch.add(&pairs);
|
|
282
|
+
batch.add(&[("foo", "bar"), ("test", "taste")]);
|
|
283
|
+
let results = batch.execute()?; // Vec<Vec<u32>>
|
|
284
|
+
Ok(())
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Available GPU kernels: `GpuLevenshteinKernel`, `GpuJaroKernel`, `GpuNeedlemanAffineKernel`, `GpuDamerauKernel`.
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## WebAssembly
|
|
293
|
+
|
|
294
|
+
```bash
|
|
295
|
+
cd crates/fuzzgpu-wasm
|
|
296
|
+
wasm-pack build --target web --release
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
```js
|
|
300
|
+
import init, {
|
|
301
|
+
levenshtein_distance, jaro_winkler, ratio, extract,
|
|
302
|
+
needleman_wunsch, needleman_wunsch_affine,
|
|
303
|
+
} from './pkg/fuzzgpu_wasm.js';
|
|
304
|
+
await init();
|
|
305
|
+
|
|
306
|
+
levenshtein_distance('kitten', 'sitting'); // 3
|
|
307
|
+
jaro_winkler('MARTHA', 'MARHTA', 0.1); // 0.9611...
|
|
308
|
+
|
|
309
|
+
// Needleman-Wunsch scores are i64 → JavaScript BigInt
|
|
310
|
+
needleman_wunsch('AGTACGCA', 'TATGC', 2n, -1n, -2n); // 1n
|
|
311
|
+
needleman_wunsch_affine('AGTACGCA', 'TATGC', 2n, -1n, -3n, -1n); // -2n
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## Technical Architecture
|
|
317
|
+
|
|
318
|
+
### Execution pipeline
|
|
319
|
+
|
|
320
|
+
```
|
|
321
|
+
┌─────────────────────────┐
|
|
322
|
+
│ User Query / API │
|
|
323
|
+
└────────────┬────────────┘
|
|
324
|
+
│
|
|
325
|
+
Batch size / dataset assessment
|
|
326
|
+
│
|
|
327
|
+
┌──────────────────────┴──────────────────────┐
|
|
328
|
+
▼ ▼
|
|
329
|
+
Small batches (< threshold) Large batches (≥ threshold)
|
|
330
|
+
│ │
|
|
331
|
+
┌───────────────────┐ ┌─────────────────────────────┐
|
|
332
|
+
│ Rayon Parallel │ │ wgpu WebGPU Compute │
|
|
333
|
+
│ Myers bit-vector │ │ WGSL shaders │
|
|
334
|
+
│ AVX512/AVX2/NEON │ │ Metal / Vulkan / DX12 │
|
|
335
|
+
└───────────────────┘ └─────────────────────────────┘
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Key design points
|
|
339
|
+
|
|
340
|
+
- **Myers (1999) bit-vector** — O(n) Levenshtein for patterns ≤ 64 chars, zero inner DP loop. Vectorized with AVX512 (8 texts/vector), AVX2 (4), NEON (2), portable scalar fallback. ISA detected at runtime via cached CPUID; override with `FUZZGPU_SIMD=portable|neon|avx2|avx512`.
|
|
341
|
+
- **Unrestricted Damerau-Levenshtein** — Full Lowrance-Wagner (1975) with non-adjacent transpositions. GPU shader keeps the full DP matrix in workgroup shared memory (≤ 32 chars ASCII).
|
|
342
|
+
- **Gotoh (1982) affine gaps** — 3-state recurrence, O(n) memory. GPU shader computes in f32; automatically routes to CPU when scoring parameters exceed f32 exact range (2²⁴).
|
|
343
|
+
- **WGSL shaders require no adapter features** — bit-vectors implemented as u32×2 pairs (no `SHADER_INT64`), works on every WebGPU backend including browsers and integrated GPUs.
|
|
344
|
+
- **Metric-aware routing** — iGPUs auto-route Jaro/Damerau to CPU (where AVX2 SIMD wins); discrete GPUs dispatch above a scaled threshold. `hardware_info()` shows every routing decision.
|
|
345
|
+
- **Dispatch lock** — serializes GPU calls across threads to work around `gfx-rs/wgpu#10085` (heap corruption under ≥3 concurrent dispatchers on Intel iGPUs).
|
|
346
|
+
- **Zero-copy Python bindings** — `Bound<PyString>` pointers, no `Vec<String>` copies; `*_into` APIs write directly into caller-supplied numpy arrays.
|
|
347
|
+
|
|
348
|
+
### GPU kernels
|
|
349
|
+
|
|
350
|
+
| Kernel | Algorithm | Max length | Notes |
|
|
351
|
+
|--------|-----------|------------|-------|
|
|
352
|
+
| `levenshtein.wgsl` | Standard DP | 256 chars | General path |
|
|
353
|
+
| `levenshtein_short.wgsl` | SLM row DP | 64 chars | Transposed layout, no register spill |
|
|
354
|
+
| `levenshtein_myers.wgsl` | Myers bit-vector | 64 chars | Shared Peq per workgroup, 2×u32 bitmask |
|
|
355
|
+
| `levenshtein_cdist_myers.wgsl` | Row-wise Myers | 64 chars | One workgroup per matrix row |
|
|
356
|
+
| `levenshtein_matrix.wgsl` | 2D DP grid | 256 chars | O(N+M) data upload |
|
|
357
|
+
| `jaro.wgsl` | Bitmap matcher | 128 chars | 128-bit registers, transposed layout |
|
|
358
|
+
| `jaro_matrix.wgsl` | 2D Jaro grid | 128 chars | O(N+M) data upload |
|
|
359
|
+
| `damerau.wgsl` | Lowrance-Wagner | 32 chars ASCII | Full matrix in SLM, non-adjacent transpositions |
|
|
360
|
+
| `damerau_matrix.wgsl` | 2D Damerau grid | 32 chars ASCII | Same |
|
|
361
|
+
| `needleman_affine.wgsl` | Gotoh serial | 128 chars | f32 scores, one thread per pair |
|
|
362
|
+
| `needleman_wavefront.wgsl` | Gotoh wavefront | 128 chars | Anti-diagonal parallel, O(m+n) steps |
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
## Project Structure
|
|
367
|
+
|
|
368
|
+
```
|
|
369
|
+
fuzzgpu/
|
|
370
|
+
├── crates/
|
|
371
|
+
│ ├── fuzzgpu-core/ # Core Rust engine + GPU shaders
|
|
372
|
+
│ ├── fuzzgpu-python/ # PyO3 Python extension
|
|
373
|
+
│ └── fuzzgpu-wasm/ # wasm-bindgen WebAssembly module
|
|
374
|
+
├── python/fuzzgpu/ # Python package wrapper + type stubs
|
|
375
|
+
│ ├── distance/ # rapidfuzz.distance-compatible modules
|
|
376
|
+
│ ├── fuzz.py # rapidfuzz.fuzz-compatible scorers
|
|
377
|
+
│ └── process.py # rapidfuzz.process-compatible helpers
|
|
378
|
+
├── fuzz/ # libFuzzer targets + stable self-harness
|
|
379
|
+
├── benchmarks/ # Comparative benchmark scripts
|
|
380
|
+
├── tests/ # Python pytest suite (174 tests)
|
|
381
|
+
└── docs/GPU_TESTING.md # Fault injection & GPU test conventions
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
## Building from Source
|
|
387
|
+
|
|
388
|
+
```bash
|
|
389
|
+
# Prerequisites: Rust 1.87+, Python 3.10+, maturin
|
|
390
|
+
git clone https://github.com/kuntal-devrat/fuzzgpu.git
|
|
391
|
+
cd fuzzgpu
|
|
392
|
+
maturin develop --release
|
|
393
|
+
pytest tests/ -v
|
|
394
|
+
cargo test --workspace
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
---
|
|
398
|
+
|
|
399
|
+
## Environment Variables
|
|
400
|
+
|
|
401
|
+
| Variable | Effect |
|
|
402
|
+
|----------|--------|
|
|
403
|
+
| `FUZZGPU_USE_CPU` | Force CPU-only mode |
|
|
404
|
+
| `FUZZGPU_FORCE_GPU` | Error (not fallback) on GPU failure in Python |
|
|
405
|
+
| `FUZZGPU_DEBUG` | Log GPU→CPU fallback decisions |
|
|
406
|
+
| `FUZZGPU_SIMD` | Force ISA: `portable\|neon\|avx2\|avx512` |
|
|
407
|
+
| `FUZZGPU_READBACK_TIMEOUT_MS` | GPU readback timeout (default 10000 ms) |
|
|
408
|
+
| `FUZZGPU_SKIP_DISPATCH_LOCK` | Bypass serialization lock (repro only) |
|
|
409
|
+
| `FUZZGPU_REQUIRE_GPU` | In tests: fail instead of skip when no GPU |
|
|
410
|
+
| `WGPU_BACKEND` | Force wgpu backend: `vulkan\|metal\|dx12` |
|
|
411
|
+
| `PROPTEST_CASES` | Override proptest case count |
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
## License
|
|
416
|
+
|
|
417
|
+
[MIT](LICENSE)
|
|
418
|
+
|