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.
Files changed (73) hide show
  1. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/Cargo.lock +2 -2
  2. fuzzgpu-0.1.7/PKG-INFO +418 -0
  3. fuzzgpu-0.1.7/README.md +388 -0
  4. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/Cargo.toml +1 -1
  5. fuzzgpu-0.1.7/crates/fuzzgpu-core/src/fuzz.rs +804 -0
  6. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/src/jaro.rs +89 -23
  7. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/src/levenshtein.rs +9 -0
  8. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/src/lib.rs +6 -1
  9. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/src/shaders/jaro.wgsl +48 -32
  10. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/src/shaders/jaro_matrix.wgsl +48 -28
  11. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/src/simd.rs +3 -3
  12. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-python/Cargo.toml +2 -2
  13. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-python/src/lib.rs +52 -62
  14. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-wasm/Cargo.toml +1 -1
  15. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-wasm/src/lib.rs +4 -4
  16. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/pyproject.toml +1 -1
  17. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/python/fuzzgpu/__init__.py +24 -4
  18. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/python/fuzzgpu/__init__.pyi +67 -6
  19. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/python/fuzzgpu/distance/Hamming.py +40 -1
  20. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/python/fuzzgpu/distance/Indel.py +13 -0
  21. fuzzgpu-0.1.7/python/fuzzgpu/distance/LCSseq.py +140 -0
  22. fuzzgpu-0.1.7/python/fuzzgpu/distance/Levenshtein.py +174 -0
  23. fuzzgpu-0.1.7/python/fuzzgpu/distance/Postfix.py +41 -0
  24. fuzzgpu-0.1.7/python/fuzzgpu/distance/Prefix.py +41 -0
  25. fuzzgpu-0.1.7/python/fuzzgpu/distance/__init__.py +34 -0
  26. fuzzgpu-0.1.7/python/fuzzgpu/distance/__init__.pyi +42 -0
  27. fuzzgpu-0.1.7/python/fuzzgpu/distance/_common.py +619 -0
  28. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/python/fuzzgpu/fuzz.py +30 -18
  29. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/python/fuzzgpu/fuzz.pyi +15 -3
  30. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/python/fuzzgpu/process.py +4 -4
  31. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/tests/test_api_signatures.py +1 -1
  32. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/tests/test_edge_cases.py +3 -3
  33. fuzzgpu-0.1.7/tests/test_rapidfuzz_compat.py +213 -0
  34. fuzzgpu-0.1.5/PKG-INFO +0 -559
  35. fuzzgpu-0.1.5/README.md +0 -529
  36. fuzzgpu-0.1.5/crates/fuzzgpu-core/src/fuzz.rs +0 -322
  37. fuzzgpu-0.1.5/python/fuzzgpu/distance/Levenshtein.py +0 -111
  38. fuzzgpu-0.1.5/python/fuzzgpu/distance/__init__.py +0 -5
  39. fuzzgpu-0.1.5/python/fuzzgpu/distance/_common.py +0 -6
  40. fuzzgpu-0.1.5/tests/test_rapidfuzz_compat.py +0 -43
  41. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/Cargo.toml +0 -0
  42. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/LICENSE +0 -0
  43. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/assets/logo.png +0 -0
  44. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/benches/bench.rs +0 -0
  45. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/src/damerau.rs +0 -0
  46. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/src/gpu.rs +0 -0
  47. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/src/needleman.rs +0 -0
  48. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/src/shaders/damerau.wgsl +0 -0
  49. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/src/shaders/damerau_matrix.wgsl +0 -0
  50. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/src/shaders/levenshtein.wgsl +0 -0
  51. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/src/shaders/levenshtein_cdist_myers.wgsl +0 -0
  52. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/src/shaders/levenshtein_matrix.wgsl +0 -0
  53. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/src/shaders/levenshtein_myers.wgsl +0 -0
  54. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/src/shaders/levenshtein_short.wgsl +0 -0
  55. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/src/shaders/needleman_affine.wgsl +0 -0
  56. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/src/shaders/needleman_wavefront.wgsl +0 -0
  57. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/tests/differential.proptest-regressions +0 -0
  58. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/tests/differential.rs +0 -0
  59. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/tests/fixtures/broken.wgsl +0 -0
  60. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-core/tests/kernel_registration.rs +0 -0
  61. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-wasm/tests/differential_harness.js +0 -0
  62. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/crates/fuzzgpu-wasm/tests/js_api.test.cjs +0 -0
  63. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/python/fuzzgpu/distance/DamerauLevenshtein.py +0 -0
  64. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/python/fuzzgpu/distance/Jaro.py +0 -0
  65. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/python/fuzzgpu/distance/JaroWinkler.py +0 -0
  66. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/python/fuzzgpu/distance/OSA.py +0 -0
  67. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/python/fuzzgpu/process.pyi +0 -0
  68. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/tests/test_basic.py +0 -0
  69. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/tests/test_concurrency.py +0 -0
  70. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/tests/test_invariants.py +0 -0
  71. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/tests/test_out_buffers.py +0 -0
  72. {fuzzgpu-0.1.5 → fuzzgpu-0.1.7}/tests/test_stress.py +0 -0
  73. {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.5"
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.5"
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
+ [![PyPI Version](https://img.shields.io/badge/pypi-v0.1.7-blue.svg?style=flat-square)](https://pypi.org/project/fuzzgpu/)
41
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg?style=flat-square)](https://opensource.org/licenses/MIT)
42
+ [![Rust](https://img.shields.io/badge/rust-1.87+-orange.svg?style=flat-square)](https://www.rust-lang.org)
43
+ [![Cross Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux%20%7C%20WASM-lightgrey.svg?style=flat-square)](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
+