fuzzgpu 0.1.7__tar.gz → 0.2.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.
Files changed (69) hide show
  1. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/Cargo.lock +2 -2
  2. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/PKG-INFO +95 -29
  3. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/README.md +94 -28
  4. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/crates/fuzzgpu-core/Cargo.toml +1 -1
  5. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/crates/fuzzgpu-core/benches/bench.rs +60 -16
  6. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/crates/fuzzgpu-core/src/damerau.rs +604 -158
  7. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/crates/fuzzgpu-core/src/fuzz.rs +385 -32
  8. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/crates/fuzzgpu-core/src/gpu.rs +224 -80
  9. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/crates/fuzzgpu-core/src/jaro.rs +925 -249
  10. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/crates/fuzzgpu-core/src/levenshtein.rs +1453 -436
  11. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/crates/fuzzgpu-core/src/lib.rs +23 -16
  12. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/crates/fuzzgpu-core/src/needleman.rs +731 -188
  13. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/crates/fuzzgpu-core/src/simd.rs +464 -132
  14. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/crates/fuzzgpu-core/tests/differential.rs +130 -58
  15. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/crates/fuzzgpu-core/tests/kernel_registration.rs +18 -14
  16. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/crates/fuzzgpu-python/Cargo.toml +2 -2
  17. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/crates/fuzzgpu-python/src/lib.rs +269 -48
  18. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/crates/fuzzgpu-wasm/Cargo.toml +1 -1
  19. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/pyproject.toml +1 -1
  20. fuzzgpu-0.2.0/python/fuzzgpu/distance/DamerauLevenshtein.py +145 -0
  21. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/python/fuzzgpu/distance/Jaro.py +3 -3
  22. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/python/fuzzgpu/distance/JaroWinkler.py +3 -3
  23. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/python/fuzzgpu/fuzz.py +54 -3
  24. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/python/fuzzgpu/process.py +21 -14
  25. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/tests/test_api_signatures.py +20 -7
  26. fuzzgpu-0.2.0/tests/test_gpu_health.py +102 -0
  27. fuzzgpu-0.2.0/tests/test_gpu_threshold_env.py +159 -0
  28. fuzzgpu-0.1.7/python/fuzzgpu/distance/DamerauLevenshtein.py +0 -43
  29. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/Cargo.toml +0 -0
  30. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/LICENSE +0 -0
  31. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/assets/logo.png +0 -0
  32. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/crates/fuzzgpu-core/src/shaders/damerau.wgsl +0 -0
  33. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/crates/fuzzgpu-core/src/shaders/damerau_matrix.wgsl +0 -0
  34. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/crates/fuzzgpu-core/src/shaders/jaro.wgsl +0 -0
  35. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/crates/fuzzgpu-core/src/shaders/jaro_matrix.wgsl +0 -0
  36. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/crates/fuzzgpu-core/src/shaders/levenshtein.wgsl +0 -0
  37. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/crates/fuzzgpu-core/src/shaders/levenshtein_cdist_myers.wgsl +0 -0
  38. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/crates/fuzzgpu-core/src/shaders/levenshtein_matrix.wgsl +0 -0
  39. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/crates/fuzzgpu-core/src/shaders/levenshtein_myers.wgsl +0 -0
  40. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/crates/fuzzgpu-core/src/shaders/levenshtein_short.wgsl +0 -0
  41. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/crates/fuzzgpu-core/src/shaders/needleman_affine.wgsl +0 -0
  42. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/crates/fuzzgpu-core/src/shaders/needleman_wavefront.wgsl +0 -0
  43. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/crates/fuzzgpu-core/tests/differential.proptest-regressions +0 -0
  44. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/crates/fuzzgpu-core/tests/fixtures/broken.wgsl +0 -0
  45. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/crates/fuzzgpu-wasm/src/lib.rs +0 -0
  46. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/crates/fuzzgpu-wasm/tests/differential_harness.js +0 -0
  47. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/crates/fuzzgpu-wasm/tests/js_api.test.cjs +0 -0
  48. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/python/fuzzgpu/__init__.py +0 -0
  49. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/python/fuzzgpu/__init__.pyi +0 -0
  50. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/python/fuzzgpu/distance/Hamming.py +0 -0
  51. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/python/fuzzgpu/distance/Indel.py +0 -0
  52. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/python/fuzzgpu/distance/LCSseq.py +0 -0
  53. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/python/fuzzgpu/distance/Levenshtein.py +0 -0
  54. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/python/fuzzgpu/distance/OSA.py +0 -0
  55. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/python/fuzzgpu/distance/Postfix.py +0 -0
  56. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/python/fuzzgpu/distance/Prefix.py +0 -0
  57. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/python/fuzzgpu/distance/__init__.py +0 -0
  58. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/python/fuzzgpu/distance/__init__.pyi +0 -0
  59. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/python/fuzzgpu/distance/_common.py +0 -0
  60. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/python/fuzzgpu/fuzz.pyi +0 -0
  61. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/python/fuzzgpu/process.pyi +0 -0
  62. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/tests/test_basic.py +0 -0
  63. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/tests/test_concurrency.py +0 -0
  64. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/tests/test_edge_cases.py +0 -0
  65. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/tests/test_invariants.py +0 -0
  66. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/tests/test_out_buffers.py +0 -0
  67. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/tests/test_rapidfuzz_compat.py +0 -0
  68. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/tests/test_stress.py +0 -0
  69. {fuzzgpu-0.1.7 → fuzzgpu-0.2.0}/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.7"
375
+ version = "0.2.0"
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.7"
389
+ version = "0.2.0"
390
390
  dependencies = [
391
391
  "fuzzgpu-core",
392
392
  "log",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fuzzgpu
3
- Version: 0.1.7
3
+ Version: 0.2.0
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -37,7 +37,7 @@ Project-URL: Repository, https://github.com/kuntal-devrat/fuzzgpu
37
37
 
38
38
  *Cross-platform GPU compute via WebGPU (`wgpu`) & Multi-Core CPU parallelism with Rayon. Zero CUDA dependencies.*
39
39
 
40
- [![PyPI Version](https://img.shields.io/badge/pypi-v0.1.7-blue.svg?style=flat-square)](https://pypi.org/project/fuzzgpu/)
40
+ [![PyPI Version](https://img.shields.io/badge/pypi-v0.2.0-blue.svg?style=flat-square)](https://pypi.org/project/fuzzgpu/)
41
41
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg?style=flat-square)](https://opensource.org/licenses/MIT)
42
42
  [![Rust](https://img.shields.io/badge/rust-1.87+-orange.svg?style=flat-square)](https://www.rust-lang.org)
43
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)
@@ -60,28 +60,60 @@ No NVIDIA CUDA drivers or complex toolkits required.
60
60
 
61
61
  ---
62
62
 
63
- ## What's New in v0.1.7
63
+ ## What's New in v0.2.0
64
+
65
+ ### Production hardening
66
+ - **Kernel `get()` panics eliminated** — all four GPU kernels (`GpuLevenshteinKernel`,
67
+ `GpuJaroKernel`, `GpuNeedlemanAffineKernel`, `GpuDamerauKernel`) previously called
68
+ `.unwrap()` on `OnceLock::get()` after initialization, which could panic the Python
69
+ interpreter under rare concurrent races. Replaced with `.ok_or_else(...)` returning
70
+ a proper `FuzzGpuError::NoDevice`.
71
+ - **`debug_assert!` → real release guards** — `MyersPattern::new`, `jaro_bitpar`, `jaro_4way`
72
+ were guarded only by `debug_assert!`. In release builds, calling them with inputs outside
73
+ their contract (non-ASCII or > 64 bytes) would silently produce wrong results. Promoted to
74
+ proper `assert!` with descriptive messages that surface immediately in both debug and release.
75
+
76
+ ### New API surface
77
+ - **`fuzz.cdist`** — pairwise score matrix, mirrors `rapidfuzz.fuzz.cdist`. Delegates to
78
+ `process.cdist` with `ratio` as the default scorer.
79
+ - **`DamerauLevenshtein.editops` / `.opcodes`** — full Lowrance-Wagner traceback returning
80
+ `Editops` / `Opcodes` (insert/delete/replace), completing parity with rapidfuzz's alignment
81
+ API for this module.
82
+ - **`fuzz.__all__`** now includes `partial_ratio_alignment` and `cdist` (were missing).
83
+
84
+ ### API correctness fixes
85
+ - **`Jaro.similarity` / `JaroWinkler.similarity` `score_cutoff`** — changed default from
86
+ `0.0` to `None`, matching rapidfuzz's semantics (`0.0` treated scores of exactly 0.0 as
87
+ filtered, which was wrong).
88
+ - **`ratio_batch(workers=)`** — was silently ignored (`del workers`). Now wires up a
89
+ `ThreadPoolExecutor` for the processor path; the no-processor path continues to use Rayon
90
+ internally (ignoring `workers` is correct there — Rayon already uses all cores).
91
+ - **`process.cdist` fast path** — rewrote to use `fuzz_ratio_batch` row-by-row (each row
92
+ runs under Rayon across all cores) instead of a dead `raw = _native.fuzz_ratio_batch`
93
+ assignment followed by the same loop. Removed the dead `raw:` type-hint-only line.
94
+ - **`process.cdist` silent swallow** — `except Exception: pass` replaced with
95
+ `warnings.warn(...)` so unexpected fast-path failures are visible instead of silently
96
+ producing slow results.
97
+
98
+ ### Includes all v0.1.7 fixes
99
+ All fixes from v0.1.7 are included — see the v0.1.7 changelog below.
100
+
101
+ ---
102
+
103
+ <details>
104
+ <summary><b>Previous (v0.1.7)</b></summary>
64
105
 
65
106
  ### Bug fixes (Windows DX12 / Jaro shader)
66
107
  - **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).
108
+ component writes (`v[j >> 5u] = ...`) in `bit_set()`. Fixed by rewriting `bit_set` with
109
+ `select()`-based static construction (Vulkan, Metal, DX12 all compile identically).
72
110
 
73
111
  ### 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.
112
+ - **`fuzzgpu-wasm`** and **`fuzzgpu-fuzz`** 4 × `E0061` arity mismatch for
113
+ `partial_ratio`/`token_sort_ratio`/`token_set_ratio`/`wratio`. Fixed by passing `0.0`
114
+ as the cutoff.
83
115
 
84
- ---
116
+ </details>
85
117
 
86
118
  <details>
87
119
  <summary><b>Previous (v0.1.6)</b></summary>
@@ -131,27 +163,61 @@ The full Python layer is now byte-identical to rapidfuzz 3.14.5 over a 169,744-p
131
163
 
132
164
  ## Benchmark Results
133
165
 
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*
166
+ *Hardware: Intel(R) Iris(R) Xe Graphics — integrated GPU (Vulkan) + Intel Core i7 (Rayon, all cores)*
167
+ *Versions: fuzzgpu 0.2.0 · rapidfuzz 3.14.5 · python-Levenshtein 0.27.4*
136
168
  *Median of 7 runs after warmup. Reproduce: `python benchmarks/bench_compare.py`*
137
169
 
170
+ > **GPU class note:** These numbers are from an **integrated GPU** (iGPU), which shares memory
171
+ > bandwidth with the CPU and has a ~1 ms dispatch round-trip. On a **discrete GPU** (dGPU) the
172
+ > GPU columns improve significantly — expect 3–10× better GPU throughput and GPU routing kicking
173
+ > in at much smaller batch sizes (threshold drops from ~500 pairs to ~64 pairs automatically).
174
+ > Concurrency gains (multiple Python threads, after the wgpu fix ships) add a further 2–4×
175
+ > on top for server workloads regardless of GPU class.
176
+
138
177
  ### Levenshtein Batch (1 query × N candidates, 10-char strings)
139
178
  | Batch Size | `fuzzgpu` (GPU) | `fuzzgpu` (CPU) | `rapidfuzz` | vs RF (GPU) | vs RF (CPU) |
140
179
  | :--- | :---: | :---: | :---: | :---: | :---: |
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× |
180
+ | **100** | 0.04 ms | 0.00 ms | 0.01 ms | 0.24× | 1.90× |
181
+ | **1,000** | 0.50 ms | 0.04 ms | 0.07 ms | 0.13× | 1.89× |
182
+ | **10,000** | 1.53 ms | 0.40 ms | 0.64 ms | 0.42× | 1.61× |
183
+ | **50,000** | 6.87 ms | 3.36 ms | 4.46 ms | 0.65× | 1.33× |
145
184
 
146
185
  ### Damerau-Levenshtein Batch (unrestricted Lowrance-Wagner)
147
186
  | Batch Size | `fuzzgpu` (GPU) | `fuzzgpu` (CPU) | `rapidfuzz` | vs RF (GPU) | vs RF (CPU) |
148
187
  | :--- | :---: | :---: | :---: | :---: | :---: |
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× |
188
+ | **100** | 0.04 ms | 0.04 ms | 0.10 ms | 2.68× | 2.63× |
189
+ | **1,000** | 0.21 ms | 0.20 ms | 0.98 ms | 4.71× | 4.89× |
190
+ | **10,000** | 1.39 ms | 1.49 ms | 9.93 ms | 7.14× | 6.67× |
191
+ | **50,000** | 10.19 ms | 9.91 ms | 66.57 ms | 6.54× | 6.72× |
152
192
 
153
193
  > **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
194
 
195
+ ### Jaro-Winkler Batch (p = 0.1)
196
+ | Batch Size | `fuzzgpu` (GPU) | `fuzzgpu` (CPU) | `rapidfuzz` | vs RF (GPU) | vs RF (CPU) |
197
+ | :--- | :---: | :---: | :---: | :---: | :---: |
198
+ | **100** | 0.02 ms | 0.01 ms | 0.02 ms | 0.87× | 1.14× |
199
+ | **1,000** | 0.18 ms | 0.13 ms | 0.12 ms | 0.70× | 0.94× |
200
+ | **10,000** | 1.01 ms | 0.83 ms | 0.97 ms | 0.96× | 1.18× |
201
+ | **50,000** | 5.61 ms | 3.25 ms | 6.69 ms | 1.19× | 2.06× |
202
+
203
+ ### Needleman-Wunsch Affine Batch (match=1, mismatch=-1, gap_open=-2, gap_extend=-1)
204
+ | Batch Size | `fuzzgpu` (GPU) | `fuzzgpu` (CPU) | `rapidfuzz` |
205
+ | :--- | :---: | :---: | :---: |
206
+ | **100** | 0.07 ms | 0.05 ms | — |
207
+ | **1,000** | 1.65 ms | 0.43 ms | — |
208
+ | **10,000** | 12.49 ms | 3.91 ms | — |
209
+ | **50,000** | 47.86 ms | 26.95 ms | — |
210
+
211
+ > rapidfuzz has no Needleman-Wunsch API — no comparison available.
212
+
213
+ ### Levenshtein Cross-Product Matrix (`cdist`)
214
+ | Matrix Size | Total Pairs | `fuzzgpu` (GPU) | `fuzzgpu` (CPU) | `rapidfuzz` | python-Levenshtein | vs RF (GPU) | vs RF (CPU) |
215
+ | :--- | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
216
+ | **10 × 10** | 100 | 0.05 ms | 0.02 ms | 0.01 ms | 0.06 ms | 0.18× | 0.56× |
217
+ | **50 × 50** | 2,500 | 0.58 ms | 0.11 ms | 0.09 ms | 2.17 ms | 0.16× | 0.81× |
218
+ | **100 × 100** | 10,000 | 0.67 ms | 0.25 ms | 0.18 ms | 5.75 ms | 0.27× | 0.70× |
219
+ | **200 × 200** | 40,000 | 1.19 ms | 0.81 ms | 0.61 ms | 22.31 ms | 0.51× | 0.75× |
220
+
155
221
  ---
156
222
 
157
223
  ## Installation
@@ -163,7 +229,7 @@ pip install fuzzgpu
163
229
  ```toml
164
230
  # Rust
165
231
  [dependencies]
166
- fuzzgpu-core = "0.1.7"
232
+ fuzzgpu-core = "0.2.0"
167
233
  ```
168
234
 
169
235
  ---
@@ -259,7 +325,7 @@ fuzzgpu.set_cpu_only(True) # force CPU-only mode
259
325
 
260
326
  ```toml
261
327
  [dependencies]
262
- fuzzgpu-core = "0.1.7" # GPU + CPU fallback
328
+ fuzzgpu-core = "0.2.0" # GPU + CPU fallback
263
329
  # fuzzgpu-core = { version = "0.1.7", default-features = false } # CPU-only
264
330
  ```
265
331
 
@@ -8,7 +8,7 @@
8
8
 
9
9
  *Cross-platform GPU compute via WebGPU (`wgpu`) & Multi-Core CPU parallelism with Rayon. Zero CUDA dependencies.*
10
10
 
11
- [![PyPI Version](https://img.shields.io/badge/pypi-v0.1.7-blue.svg?style=flat-square)](https://pypi.org/project/fuzzgpu/)
11
+ [![PyPI Version](https://img.shields.io/badge/pypi-v0.2.0-blue.svg?style=flat-square)](https://pypi.org/project/fuzzgpu/)
12
12
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg?style=flat-square)](https://opensource.org/licenses/MIT)
13
13
  [![Rust](https://img.shields.io/badge/rust-1.87+-orange.svg?style=flat-square)](https://www.rust-lang.org)
14
14
  [![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)
@@ -31,28 +31,60 @@ No NVIDIA CUDA drivers or complex toolkits required.
31
31
 
32
32
  ---
33
33
 
34
- ## What's New in v0.1.7
34
+ ## What's New in v0.2.0
35
+
36
+ ### Production hardening
37
+ - **Kernel `get()` panics eliminated** — all four GPU kernels (`GpuLevenshteinKernel`,
38
+ `GpuJaroKernel`, `GpuNeedlemanAffineKernel`, `GpuDamerauKernel`) previously called
39
+ `.unwrap()` on `OnceLock::get()` after initialization, which could panic the Python
40
+ interpreter under rare concurrent races. Replaced with `.ok_or_else(...)` returning
41
+ a proper `FuzzGpuError::NoDevice`.
42
+ - **`debug_assert!` → real release guards** — `MyersPattern::new`, `jaro_bitpar`, `jaro_4way`
43
+ were guarded only by `debug_assert!`. In release builds, calling them with inputs outside
44
+ their contract (non-ASCII or > 64 bytes) would silently produce wrong results. Promoted to
45
+ proper `assert!` with descriptive messages that surface immediately in both debug and release.
46
+
47
+ ### New API surface
48
+ - **`fuzz.cdist`** — pairwise score matrix, mirrors `rapidfuzz.fuzz.cdist`. Delegates to
49
+ `process.cdist` with `ratio` as the default scorer.
50
+ - **`DamerauLevenshtein.editops` / `.opcodes`** — full Lowrance-Wagner traceback returning
51
+ `Editops` / `Opcodes` (insert/delete/replace), completing parity with rapidfuzz's alignment
52
+ API for this module.
53
+ - **`fuzz.__all__`** now includes `partial_ratio_alignment` and `cdist` (were missing).
54
+
55
+ ### API correctness fixes
56
+ - **`Jaro.similarity` / `JaroWinkler.similarity` `score_cutoff`** — changed default from
57
+ `0.0` to `None`, matching rapidfuzz's semantics (`0.0` treated scores of exactly 0.0 as
58
+ filtered, which was wrong).
59
+ - **`ratio_batch(workers=)`** — was silently ignored (`del workers`). Now wires up a
60
+ `ThreadPoolExecutor` for the processor path; the no-processor path continues to use Rayon
61
+ internally (ignoring `workers` is correct there — Rayon already uses all cores).
62
+ - **`process.cdist` fast path** — rewrote to use `fuzz_ratio_batch` row-by-row (each row
63
+ runs under Rayon across all cores) instead of a dead `raw = _native.fuzz_ratio_batch`
64
+ assignment followed by the same loop. Removed the dead `raw:` type-hint-only line.
65
+ - **`process.cdist` silent swallow** — `except Exception: pass` replaced with
66
+ `warnings.warn(...)` so unexpected fast-path failures are visible instead of silently
67
+ producing slow results.
68
+
69
+ ### Includes all v0.1.7 fixes
70
+ All fixes from v0.1.7 are included — see the v0.1.7 changelog below.
71
+
72
+ ---
73
+
74
+ <details>
75
+ <summary><b>Previous (v0.1.7)</b></summary>
35
76
 
36
77
  ### Bug fixes (Windows DX12 / Jaro shader)
37
78
  - **Jaro GPU shader FXC crash fixed** — `jaro.wgsl` and `jaro_matrix.wgsl` used dynamic vector
38
- component writes (`v[j >> 5u] = ...`) in `bit_set()`. FXC (the DX12 HLSL compiler) cannot
39
- emit a dynamic register-indexed store for a non-constant index, causing a hard compile failure
40
- with `X3550`/`X3511` on every Windows DX12 runner — all 11 Jaro GPU tests were panicking.
41
- Fixed by rewriting `bit_set` with `select()`-based static construction, which compiles
42
- identically on all backends (Vulkan, Metal, DX12).
79
+ component writes (`v[j >> 5u] = ...`) in `bit_set()`. Fixed by rewriting `bit_set` with
80
+ `select()`-based static construction (Vulkan, Metal, DX12 all compile identically).
43
81
 
44
82
  ### Bug fixes (arity mismatch — wasm & fuzz crates)
45
- - **`fuzzgpu-wasm`** — `partial_ratio`, `token_sort_ratio`, `token_set_ratio`, `wratio` gained
46
- a `score_cutoff: f64` parameter in the v0.1.6 Rust core rewrite for rapidfuzz parity, but
47
- the WASM bindings were not updated, causing 4 `E0061` compile errors. Fixed by passing
48
- `0.0` as the cutoff (no cutoff — identical to the previous behaviour).
49
- - **`fuzzgpu-fuzz`** — same four callsites in `fuzz/src/lib.rs` had the same arity mismatch.
50
- Fixed identically.
51
-
52
- ### Includes all v0.1.6 features
53
- All features from v0.1.6 are included — see the v0.1.6 changelog below.
83
+ - **`fuzzgpu-wasm`** and **`fuzzgpu-fuzz`** 4 × `E0061` arity mismatch for
84
+ `partial_ratio`/`token_sort_ratio`/`token_set_ratio`/`wratio`. Fixed by passing `0.0`
85
+ as the cutoff.
54
86
 
55
- ---
87
+ </details>
56
88
 
57
89
  <details>
58
90
  <summary><b>Previous (v0.1.6)</b></summary>
@@ -102,27 +134,61 @@ The full Python layer is now byte-identical to rapidfuzz 3.14.5 over a 169,744-p
102
134
 
103
135
  ## Benchmark Results
104
136
 
105
- *Hardware: Intel(R) Iris(R) Xe Graphics (Vulkan) + Intel Core i7 (Rayon uses all cores)*
106
- *Versions: fuzzgpu 0.1.6 · rapidfuzz 3.14.5 · python-Levenshtein 0.27.4*
137
+ *Hardware: Intel(R) Iris(R) Xe Graphics — integrated GPU (Vulkan) + Intel Core i7 (Rayon, all cores)*
138
+ *Versions: fuzzgpu 0.2.0 · rapidfuzz 3.14.5 · python-Levenshtein 0.27.4*
107
139
  *Median of 7 runs after warmup. Reproduce: `python benchmarks/bench_compare.py`*
108
140
 
141
+ > **GPU class note:** These numbers are from an **integrated GPU** (iGPU), which shares memory
142
+ > bandwidth with the CPU and has a ~1 ms dispatch round-trip. On a **discrete GPU** (dGPU) the
143
+ > GPU columns improve significantly — expect 3–10× better GPU throughput and GPU routing kicking
144
+ > in at much smaller batch sizes (threshold drops from ~500 pairs to ~64 pairs automatically).
145
+ > Concurrency gains (multiple Python threads, after the wgpu fix ships) add a further 2–4×
146
+ > on top for server workloads regardless of GPU class.
147
+
109
148
  ### Levenshtein Batch (1 query × N candidates, 10-char strings)
110
149
  | Batch Size | `fuzzgpu` (GPU) | `fuzzgpu` (CPU) | `rapidfuzz` | vs RF (GPU) | vs RF (CPU) |
111
150
  | :--- | :---: | :---: | :---: | :---: | :---: |
112
- | **100** | 0.12 ms | 0.01 ms | 0.03 ms | 0.25× | 2.21× |
113
- | **1,000** | 0.83 ms | 0.09 ms | 0.15 ms | 0.18× | 1.67× |
114
- | **10,000** | 2.60 ms | 0.87 ms | 1.47 ms | 0.57× | 1.68× |
115
- | **50,000** | 9.92 ms | 5.04 ms | 6.38 ms | 0.64× | 1.27× |
151
+ | **100** | 0.04 ms | 0.00 ms | 0.01 ms | 0.24× | 1.90× |
152
+ | **1,000** | 0.50 ms | 0.04 ms | 0.07 ms | 0.13× | 1.89× |
153
+ | **10,000** | 1.53 ms | 0.40 ms | 0.64 ms | 0.42× | 1.61× |
154
+ | **50,000** | 6.87 ms | 3.36 ms | 4.46 ms | 0.65× | 1.33× |
116
155
 
117
156
  ### Damerau-Levenshtein Batch (unrestricted Lowrance-Wagner)
118
157
  | Batch Size | `fuzzgpu` (GPU) | `fuzzgpu` (CPU) | `rapidfuzz` | vs RF (GPU) | vs RF (CPU) |
119
158
  | :--- | :---: | :---: | :---: | :---: | :---: |
120
- | **1,000** | 0.58 ms | 0.40 ms | 1.63 ms | 2.81× | 4.06× |
121
- | **10,000** | 2.95 ms | 2.15 ms | 20.86 ms | 7.06× | 9.70× |
122
- | **50,000** | 17.49 ms | 12.08 ms | 120.25 ms | 6.88× | 9.95× |
159
+ | **100** | 0.04 ms | 0.04 ms | 0.10 ms | 2.68× | 2.63× |
160
+ | **1,000** | 0.21 ms | 0.20 ms | 0.98 ms | 4.71× | 4.89× |
161
+ | **10,000** | 1.39 ms | 1.49 ms | 9.93 ms | 7.14× | 6.67× |
162
+ | **50,000** | 10.19 ms | 9.91 ms | 66.57 ms | 6.54× | 6.72× |
123
163
 
124
164
  > **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.
125
165
 
166
+ ### Jaro-Winkler Batch (p = 0.1)
167
+ | Batch Size | `fuzzgpu` (GPU) | `fuzzgpu` (CPU) | `rapidfuzz` | vs RF (GPU) | vs RF (CPU) |
168
+ | :--- | :---: | :---: | :---: | :---: | :---: |
169
+ | **100** | 0.02 ms | 0.01 ms | 0.02 ms | 0.87× | 1.14× |
170
+ | **1,000** | 0.18 ms | 0.13 ms | 0.12 ms | 0.70× | 0.94× |
171
+ | **10,000** | 1.01 ms | 0.83 ms | 0.97 ms | 0.96× | 1.18× |
172
+ | **50,000** | 5.61 ms | 3.25 ms | 6.69 ms | 1.19× | 2.06× |
173
+
174
+ ### Needleman-Wunsch Affine Batch (match=1, mismatch=-1, gap_open=-2, gap_extend=-1)
175
+ | Batch Size | `fuzzgpu` (GPU) | `fuzzgpu` (CPU) | `rapidfuzz` |
176
+ | :--- | :---: | :---: | :---: |
177
+ | **100** | 0.07 ms | 0.05 ms | — |
178
+ | **1,000** | 1.65 ms | 0.43 ms | — |
179
+ | **10,000** | 12.49 ms | 3.91 ms | — |
180
+ | **50,000** | 47.86 ms | 26.95 ms | — |
181
+
182
+ > rapidfuzz has no Needleman-Wunsch API — no comparison available.
183
+
184
+ ### Levenshtein Cross-Product Matrix (`cdist`)
185
+ | Matrix Size | Total Pairs | `fuzzgpu` (GPU) | `fuzzgpu` (CPU) | `rapidfuzz` | python-Levenshtein | vs RF (GPU) | vs RF (CPU) |
186
+ | :--- | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
187
+ | **10 × 10** | 100 | 0.05 ms | 0.02 ms | 0.01 ms | 0.06 ms | 0.18× | 0.56× |
188
+ | **50 × 50** | 2,500 | 0.58 ms | 0.11 ms | 0.09 ms | 2.17 ms | 0.16× | 0.81× |
189
+ | **100 × 100** | 10,000 | 0.67 ms | 0.25 ms | 0.18 ms | 5.75 ms | 0.27× | 0.70× |
190
+ | **200 × 200** | 40,000 | 1.19 ms | 0.81 ms | 0.61 ms | 22.31 ms | 0.51× | 0.75× |
191
+
126
192
  ---
127
193
 
128
194
  ## Installation
@@ -134,7 +200,7 @@ pip install fuzzgpu
134
200
  ```toml
135
201
  # Rust
136
202
  [dependencies]
137
- fuzzgpu-core = "0.1.7"
203
+ fuzzgpu-core = "0.2.0"
138
204
  ```
139
205
 
140
206
  ---
@@ -230,7 +296,7 @@ fuzzgpu.set_cpu_only(True) # force CPU-only mode
230
296
 
231
297
  ```toml
232
298
  [dependencies]
233
- fuzzgpu-core = "0.1.7" # GPU + CPU fallback
299
+ fuzzgpu-core = "0.2.0" # GPU + CPU fallback
234
300
  # fuzzgpu-core = { version = "0.1.7", default-features = false } # CPU-only
235
301
  ```
236
302
 
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "fuzzgpu-core"
3
- version = "0.1.7"
3
+ version = "0.2.0"
4
4
  edition = "2021"
5
5
  description = "GPU-accelerated fuzzy string matching engine"
6
6
  license = "MIT"
@@ -13,7 +13,9 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion};
13
13
  use fuzzgpu_core::damerau::damerau_levenshtein_batch;
14
14
  use fuzzgpu_core::fuzz::ratio;
15
15
  use fuzzgpu_core::jaro::{jaro_winkler, jaro_winkler_batch};
16
- use fuzzgpu_core::levenshtein::{levenshtein_cdist_cpu, levenshtein_distance_raw, LevenshteinKernel};
16
+ use fuzzgpu_core::levenshtein::{
17
+ levenshtein_cdist_cpu, levenshtein_distance_raw, LevenshteinKernel,
18
+ };
17
19
  use fuzzgpu_core::needleman::{needleman_wunsch_affine, needleman_wunsch_affine_batch};
18
20
  use fuzzgpu_core::simd::levenshtein_myers;
19
21
 
@@ -24,7 +26,9 @@ fn gen_strings(count: usize, len: usize, seed: u64) -> Vec<String> {
24
26
  for _ in 0..count {
25
27
  let mut s = String::with_capacity(len);
26
28
  for _ in 0..len {
27
- state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
29
+ state = state
30
+ .wrapping_mul(6364136223846793005)
31
+ .wrapping_add(1442695040888963407);
28
32
  s.push((b'a' + ((state >> 33) as u8 % 26)) as char);
29
33
  }
30
34
  out.push(s);
@@ -47,7 +51,11 @@ fn cpu_benches(c: &mut Criterion) {
47
51
  let long = gen_strings(2, 256, 3);
48
52
 
49
53
  let mut g = c.benchmark_group("scalar");
50
- for (name, s) in [("short_8", &short), ("medium_64", &medium), ("long_256", &long)] {
54
+ for (name, s) in [
55
+ ("short_8", &short),
56
+ ("medium_64", &medium),
57
+ ("long_256", &long),
58
+ ] {
51
59
  let (s1, s2) = (&s[0], &s[1]);
52
60
  g.bench_function(format!("levenshtein/{name}"), |b| {
53
61
  b.iter(|| black_box(levenshtein_distance_raw(s1, s2)))
@@ -62,7 +70,11 @@ fn cpu_benches(c: &mut Criterion) {
62
70
  b.iter(|| black_box(ratio(s1, s2)))
63
71
  });
64
72
  g.bench_function(format!("needleman_affine/{name}"), |b| {
65
- b.iter(|| black_box(needleman_wunsch_affine(s1, s2, MATCH, MISMATCH, GAP_OPEN, GAP_EXTEND)))
73
+ b.iter(|| {
74
+ black_box(needleman_wunsch_affine(
75
+ s1, s2, MATCH, MISMATCH, GAP_OPEN, GAP_EXTEND,
76
+ ))
77
+ })
66
78
  });
67
79
  g.bench_function(format!("damerau/{name}"), |b| {
68
80
  b.iter(|| black_box(fuzzgpu_core::damerau::damerau_levenshtein_distance(s1, s2)))
@@ -89,14 +101,27 @@ fn cpu_benches(c: &mut Criterion) {
89
101
  b.iter(|| black_box(jaro_winkler_batch(query, &cand_refs, 0.1)))
90
102
  });
91
103
  g.bench_function("needleman_affine_batch_1000", |b| {
92
- b.iter(|| black_box(needleman_wunsch_affine_batch(query, &cand_refs, MATCH, MISMATCH, GAP_OPEN, GAP_EXTEND)))
104
+ b.iter(|| {
105
+ black_box(needleman_wunsch_affine_batch(
106
+ query, &cand_refs, MATCH, MISMATCH, GAP_OPEN, GAP_EXTEND,
107
+ ))
108
+ })
93
109
  });
94
110
  // Long-string needleman (80 chars): the anti-diagonal wavefront GPU path's
95
111
  // target workload; Rayon CPU is the comparison baseline.
96
112
  let cands_nw80 = gen_strings(1000, 80, 0xAB);
97
113
  let cand_refs_nw80: Vec<&str> = cands_nw80.iter().map(|s| s.as_str()).collect();
98
114
  g.bench_function("needleman_affine_batch_1000_long80_large", |b| {
99
- b.iter(|| black_box(needleman_wunsch_affine_batch(query, &cand_refs_nw80, MATCH, MISMATCH, GAP_OPEN, GAP_EXTEND)))
115
+ b.iter(|| {
116
+ black_box(needleman_wunsch_affine_batch(
117
+ query,
118
+ &cand_refs_nw80,
119
+ MATCH,
120
+ MISMATCH,
121
+ GAP_OPEN,
122
+ GAP_EXTEND,
123
+ ))
124
+ })
100
125
  });
101
126
  g.bench_function("damerau_batch_1000", |b| {
102
127
  b.iter(|| black_box(damerau_levenshtein_batch(query, &cand_refs)))
@@ -128,8 +153,9 @@ fn cpu_benches(c: &mut Criterion) {
128
153
  b.iter(|| black_box(levenshtein_cdist_cpu(&refs_a_1k, &refs_b_1k)))
129
154
  });
130
155
  // Sequential 10 x 1000 pairs: the CPU baseline for the batched-GPU bench.
131
- let cands10: Vec<Vec<String>> =
132
- (0..10).map(|i| gen_strings(1000, 16, 0x100 + i as u64)).collect();
156
+ let cands10: Vec<Vec<String>> = (0..10)
157
+ .map(|i| gen_strings(1000, 16, 0x100 + i as u64))
158
+ .collect();
133
159
  let ops10: Vec<Vec<(&str, &str)>> = cands10.iter().map(|c| make_pairs(query, c)).collect();
134
160
  g.bench_function("levenshtein_10x1000_seq_cpu", |b| {
135
161
  b.iter(|| {
@@ -190,13 +216,23 @@ fn gpu_benches(c: &mut Criterion) {
190
216
  match GpuNeedlemanAffineKernel::get() {
191
217
  Ok(kernel) => {
192
218
  g.bench_function("needleman_affine_batch_1000", |b| {
193
- b.iter(|| black_box(kernel.compute_batch(&pairs, MATCH, MISMATCH, GAP_OPEN, GAP_EXTEND)))
219
+ b.iter(|| {
220
+ black_box(kernel.compute_batch(&pairs, MATCH, MISMATCH, GAP_OPEN, GAP_EXTEND))
221
+ })
194
222
  });
195
223
  // 80-char pairs: routes to the anti-diagonal wavefront kernel.
196
224
  let cands_nw80 = gen_strings(1000, 80, 0xAB);
197
225
  let pairs_nw80 = make_pairs(query, &cands_nw80);
198
226
  g.bench_function("needleman_affine_batch_1000_long80_large", |b| {
199
- b.iter(|| black_box(kernel.compute_batch(&pairs_nw80, MATCH, MISMATCH, GAP_OPEN, GAP_EXTEND)))
227
+ b.iter(|| {
228
+ black_box(kernel.compute_batch(
229
+ &pairs_nw80,
230
+ MATCH,
231
+ MISMATCH,
232
+ GAP_OPEN,
233
+ GAP_EXTEND,
234
+ ))
235
+ })
200
236
  });
201
237
  }
202
238
  Err(e) => {
@@ -247,9 +283,11 @@ fn gpu_benches(c: &mut Criterion) {
247
283
  });
248
284
  // 10 x 1000 pairs: one batched dispatch+readback vs 10 sequential
249
285
  // sync-round-trips. Same total work as cpu_batch/..._seq_cpu.
250
- let cands10: Vec<Vec<String>> =
251
- (0..10).map(|i| gen_strings(1000, 16, 0x100 + i as u64)).collect();
252
- let ops10: Vec<Vec<(&str, &str)>> = cands10.iter().map(|c| make_pairs(query, c)).collect();
286
+ let cands10: Vec<Vec<String>> = (0..10)
287
+ .map(|i| gen_strings(1000, 16, 0x100 + i as u64))
288
+ .collect();
289
+ let ops10: Vec<Vec<(&str, &str)>> =
290
+ cands10.iter().map(|c| make_pairs(query, c)).collect();
253
291
  g.bench_function("levenshtein_10x1000_seq", |b| {
254
292
  b.iter(|| {
255
293
  for op in &ops10 {
@@ -268,10 +306,16 @@ fn gpu_benches(c: &mut Criterion) {
268
306
  });
269
307
  }
270
308
  Err(e) => {
271
- eprintln!("WARN: no GPU device, skipping large-scale GpuLevenshteinKernel benches: {e}");
309
+ eprintln!(
310
+ "WARN: no GPU device, skipping large-scale GpuLevenshteinKernel benches: {e}"
311
+ );
272
312
  g.bench_function("levenshtein_batch_100k_large", |b| b.iter(|| black_box(0)));
273
- g.bench_function("levenshtein_batch_1000_long256_large", |b| b.iter(|| black_box(0)));
274
- g.bench_function("levenshtein_cdist_1000x1000_large", |b| b.iter(|| black_box(0)));
313
+ g.bench_function("levenshtein_batch_1000_long256_large", |b| {
314
+ b.iter(|| black_box(0))
315
+ });
316
+ g.bench_function("levenshtein_cdist_1000x1000_large", |b| {
317
+ b.iter(|| black_box(0))
318
+ });
275
319
  g.bench_function("levenshtein_10x1000_seq", |b| b.iter(|| black_box(0)));
276
320
  g.bench_function("levenshtein_10x1000_batched", |b| b.iter(|| black_box(0)));
277
321
  }