Skip to main content
fuzzgpu logo

fuzzgpu

Hardware-Accelerated Fuzzy String Matching & Sequence Alignment

Cross-platform GPU compute via WebGPU (wgpu) & Multi-Core CPU parallelism with Rayon. Zero CUDA dependencies.

PyPI Version License: MIT Rust Cross Platform


Overview

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:

  • Apple Silicon (Metal)
  • Linux (Vulkan)
  • Windows (DirectX 12 / Vulkan)
  • Integrated GPUs (Intel Iris Xe, AMD Radeon)
  • WebAssembly (In-browser execution)

No NVIDIA CUDA drivers or complex toolkits required.


What's New in v0.1.7

Bug fixes (Windows DX12 / Jaro shader)

  • Jaro GPU shader FXC crash fixedjaro.wgsl and jaro_matrix.wgsl used dynamic vector component writes (v[j >> 5u] = ...) in bit_set(). FXC (the DX12 HLSL compiler) cannot emit a dynamic register-indexed store for a non-constant index, causing a hard compile failure with X3550/X3511 on every Windows DX12 runner — all 11 Jaro GPU tests were panicking. Fixed by rewriting bit_set with select()-based static construction, which compiles identically on all backends (Vulkan, Metal, DX12).

Bug fixes (arity mismatch — wasm & fuzz crates)

  • fuzzgpu-wasmpartial_ratio, token_sort_ratio, token_set_ratio, wratio gained a score_cutoff: f64 parameter in the v0.1.6 Rust core rewrite for rapidfuzz parity, but the WASM bindings were not updated, causing 4 E0061 compile errors. Fixed by passing 0.0 as the cutoff (no cutoff — identical to the previous behaviour).
  • fuzzgpu-fuzz — same four callsites in fuzz/src/lib.rs had the same arity mismatch. Fixed identically.

Includes all v0.1.6 features

All features from v0.1.6 are included — see the v0.1.6 changelog below.


Previous (v0.1.6)

Drop-in rapidfuzz parity (Python)

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, osa0 mismatches.

Bug fixes (Rust core, float parity)

  • ratio / partial_ratio cutoff imprecision — port of rapidfuzz's load-bearing NormSim_to_NormDist = min(1, 1 - cutoff/100 + 1e-5) term.
  • ratio score formula — switched from ((len-dist)/len)*100 to (1 - dist/len)*100 to match rapidfuzz C++'s exact ulp order.

New features (Python distance layer)

  • Editops / Opcodes / Editop / Opcode / MatchingBlock / ScoreAlignment classes (rapidfuzz-compatible).
  • Levenshtein.editops / .opcodes, LCSseq, Prefix, Postfix, Hamming.editops, Indel.editops modules.
  • fuzz.partial_ratio_alignment returns ScoreAlignment (rapidfuzz-compatible).
  • process.extract / extractOne / cdist default to WRatio.
  • All alignment types re-exported at the package root.
Previous (v0.1.5)

Bug fixes

  • 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
  • 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
  • Wavefront shader race condition fixed — diags[1] seed initialization consolidated into a single thread with a proper workgroupBarrier()
  • extract_one early-exit fixed — the break at score==100.0 now only fires after the is_better check
  • Distance module processor bug fixed across all distance/*.py modules — similarity/normalized_* now apply the processor once, then compute maximum on the processed strings

Optimizations

  • Zero-allocation SIMD hot pathslevenshtein_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
  • token_set_ratio uses Cow<str> to skip heap allocation when intersection/difference sets are empty
  • process.cdist fast path routes through the Rayon/GPU ratio_batch when the default scorer is used, instead of one Python call per cell

New features

  • partial_ratio_alignment(s1, s2)(score, src_start, dest_start, length) — rapidfuzz-compatible alignment result
  • partial_token_sort_ratio, partial_token_set_ratio, QRatio — now exposed at the top level
  • Jaro-Winkler GPU routing in Pythonjaro_winkler_batch and jaro_winkler_cdist now use the GPU kernel on discrete GPUs
  • Needleman-Wunsch GPU routing in Pythonneedleman_wunsch_affine_batch now uses GpuNeedlemanAffineKernel
  • editops and opcodes re-exported at the top level (fuzzgpu.editops, fuzzgpu.opcodes)
  • Complete type stubs (__init__.pyi, fuzz.pyi, process.pyi)

Benchmark Results

Hardware: Intel(R) Iris(R) Xe Graphics (Vulkan) + Intel Core i7 (Rayon uses all cores) Versions: fuzzgpu 0.1.6 · rapidfuzz 3.14.5 · python-Levenshtein 0.27.4 Median of 7 runs after warmup. Reproduce: python benchmarks/bench_compare.py

Levenshtein Batch (1 query × N candidates, 10-char strings)

Batch Size fuzzgpu (GPU) fuzzgpu (CPU) rapidfuzz vs RF (GPU) vs RF (CPU)
100 0.12 ms 0.01 ms 0.03 ms 0.25× 2.21×
1,000 0.83 ms 0.09 ms 0.15 ms 0.18× 1.67×
10,000 2.60 ms 0.87 ms 1.47 ms 0.57× 1.68×
50,000 9.92 ms 5.04 ms 6.38 ms 0.64× 1.27×

Damerau-Levenshtein Batch (unrestricted Lowrance-Wagner)

Batch Size fuzzgpu (GPU) fuzzgpu (CPU) rapidfuzz vs RF (GPU) vs RF (CPU)
1,000 0.58 ms 0.40 ms 1.63 ms 2.81× 4.06×
10,000 2.95 ms 2.15 ms 20.86 ms 7.06× 9.70×
50,000 17.49 ms 12.08 ms 120.25 ms 6.88× 9.95×

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.


Installation

pip install fuzzgpu
# Rust
[dependencies]
fuzzgpu-core = "0.1.7"

Quickstart

import fuzzgpu

# ── Core distance metrics ─────────────────────────────────────────────────────
lev = fuzzgpu.levenshtein("kitten", "sitting")              # 3
dam = fuzzgpu.damerau("ab", "ba")                           # 1  (transposition)
jw  = fuzzgpu.jaro_winkler("MARTHA", "MARHTA")             # 0.9611...

# ── Batch (auto-routed GPU/CPU) ───────────────────────────────────────────────
candidates = ["hallo", "hullo", "jello", "yellow", "hello world"] * 10_000
distances  = fuzzgpu.levenshtein_batch("hello", candidates)
jw_scores  = fuzzgpu.jaro_winkler_batch("hello", candidates, p=0.1)
nw_scores  = fuzzgpu.needleman_wunsch_affine_batch(
    "AGTACGCA", candidates, match=2, mismatch=-1, gap_open=-3, gap_extend=-1
)

# ── Cross-product distance matrix ─────────────────────────────────────────────
matrix = fuzzgpu.levenshtein_cdist(["abc", "def", "xyz"], ["abd", "axy", "def"])

# ── Zero-allocation outputs (write into preallocated numpy arrays) ────────────
import numpy as np
out_u32 = np.empty(len(candidates), dtype=np.uint32)
out_f64 = np.empty(len(candidates), dtype=np.float64)
mat_u32 = np.empty((3, 3), dtype=np.uint32)
fuzzgpu.levenshtein_batch_into("hello", candidates, out_u32)
fuzzgpu.jaro_winkler_batch_into("hello", candidates, out_f64)
fuzzgpu.levenshtein_cdist_into(["abc", "def", "xyz"], ["abd", "axy", "def"], mat_u32)

# ── Global sequence alignment (Gotoh 1982 affine gap) ────────────────────────
score = fuzzgpu.needleman_wunsch_affine("AGTACGCA", "TATGC", 2, -1, -3, -1)

# ── Fuzzy ratios (rapidfuzz-compatible) ──────────────────────────────────────
from fuzzgpu.fuzz import (
    ratio, partial_ratio, partial_ratio_alignment,
    token_sort_ratio, token_set_ratio,
    partial_token_sort_ratio, partial_token_set_ratio,
    QRatio, WRatio,
)

ratio("fuzzy was a bear", "fuzzy was a bear")           # 100.0
partial_ratio("hello", "oh hello there")               # 100.0
score, src, dst, length = partial_ratio_alignment("hello", "oh hello there")
# (100.0, 0, 3, 5)  ← window starts at char 3 of the longer string

token_sort_ratio("new york mets", "mets new york")      # 100.0
token_set_ratio("fuzzy was a bear", "fuzzy bear")       # 100.0

# ── Alignment helpers (rapidfuzz-compatible) ──────────────────────────────────
from fuzzgpu.distance import Levenshtein
ops   = fuzzgpu.editops("kitten", "sitting")           # top-level alias
codes = fuzzgpu.opcodes("kitten", "sitting")

# ── Search ────────────────────────────────────────────────────────────────────
from fuzzgpu.fuzz import extract, extractOne
best  = extractOne("hellp", ["hello", "world", "help"], score_cutoff=50.0)
# ("help", 88.88888888888889, 2)
top_3 = extract("apple", ["apply", "ape", "banana", "applesauce"],
                score_cutoff=50.0, limit=3)

# ── rapidfuzz.process-compatible API ─────────────────────────────────────────
from fuzzgpu.process import extract, extractOne, cdist
matrix = cdist(["hello", "world"], ["hallo", "wurld"])  # uses GPU/Rayon

# ── distance submodule (rapidfuzz.distance-compatible) ───────────────────────
from fuzzgpu.distance import Levenshtein, DamerauLevenshtein, Jaro, JaroWinkler
from fuzzgpu.distance import Hamming, OSA, Indel

Levenshtein.distance("kitten", "sitting")               # 3
Levenshtein.normalized_similarity("kitten", "sitting")  # 0.571...
Levenshtein.similarity(" abc ", "abc", processor=str.strip)  # 3
DamerauLevenshtein.distance("ca", "abc")                # 2  (unrestricted)
OSA.distance("ca", "abc")                               # 3  (OSA / rapidfuzz-compatible)
JaroWinkler.similarity("MARTHA", "MARHTA", prefix_weight=0.1)  # 0.9611...

# ── Hardware diagnostics ──────────────────────────────────────────────────────
print(fuzzgpu.gpu_info())        # "Intel(R) Iris(R) Xe Graphics (Vulkan)"
print(fuzzgpu.hardware_info())   # adapter, threshold, last routing stats

fuzzgpu.set_gpu_threshold(100)   # force GPU for batches >= 100 pairs
fuzzgpu.set_gpu_threshold(None)  # restore auto-selection
fuzzgpu.set_cpu_only(True)       # force CPU-only mode

Rust API

[dependencies]
fuzzgpu-core = "0.1.7"                                        # GPU + CPU fallback
# fuzzgpu-core = { version = "0.1.7", default-features = false } # CPU-only
use fuzzgpu_core::levenshtein::gpu_ext::GpuLevenshteinKernel;

fn main() -> fuzzgpu_core::Result<()> {
    let kernel = GpuLevenshteinKernel::get()?;

    // Batch
    let pairs = vec![("kitten", "sitting"), ("hello", "hullo")];
    let distances = kernel.compute(&pairs)?;   // [3, 1]

    // Cross-product matrix
    let matrix = kernel.compute_matrix(&["abc", "def"], &["abc", "xyz"])?;

    // Multi-op batch (one GPU dispatch + readback amortized across all ops)
    let mut batch = kernel.batch();
    batch.add(&pairs);
    batch.add(&[("foo", "bar"), ("test", "taste")]);
    let results = batch.execute()?;   // Vec<Vec<u32>>
    Ok(())
}

Available GPU kernels: GpuLevenshteinKernel, GpuJaroKernel, GpuNeedlemanAffineKernel, GpuDamerauKernel.


WebAssembly

cd crates/fuzzgpu-wasm
wasm-pack build --target web --release
import init, {
    levenshtein_distance, jaro_winkler, ratio, extract,
    needleman_wunsch, needleman_wunsch_affine,
} from './pkg/fuzzgpu_wasm.js';
await init();

levenshtein_distance('kitten', 'sitting');   // 3
jaro_winkler('MARTHA', 'MARHTA', 0.1);       // 0.9611...

// Needleman-Wunsch scores are i64 → JavaScript BigInt
needleman_wunsch('AGTACGCA', 'TATGC', 2n, -1n, -2n);        // 1n
needleman_wunsch_affine('AGTACGCA', 'TATGC', 2n, -1n, -3n, -1n); // -2n

Technical Architecture

Execution pipeline

                    ┌─────────────────────────┐
                    │     User Query / API    │
                    └────────────┬────────────┘
                                 │
                   Batch size / dataset assessment
                                 │
          ┌──────────────────────┴──────────────────────┐
          ▼                                             ▼
  Small batches (< threshold)              Large batches (≥ threshold)
          │                                             │
  ┌───────────────────┐                  ┌─────────────────────────────┐
  │  Rayon Parallel   │                  │   wgpu WebGPU Compute       │
  │  Myers bit-vector │                  │   WGSL shaders              │
  │  AVX512/AVX2/NEON │                  │   Metal / Vulkan / DX12     │
  └───────────────────┘                  └─────────────────────────────┘

Key design points

  • 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.
  • 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).
  • 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²⁴).
  • 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.
  • 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.
  • Dispatch lock — serializes GPU calls across threads to work around gfx-rs/wgpu#10085 (heap corruption under ≥3 concurrent dispatchers on Intel iGPUs).
  • Zero-copy Python bindingsBound<PyString> pointers, no Vec<String> copies; *_into APIs write directly into caller-supplied numpy arrays.

GPU kernels

Kernel Algorithm Max length Notes
levenshtein.wgsl Standard DP 256 chars General path
levenshtein_short.wgsl SLM row DP 64 chars Transposed layout, no register spill
levenshtein_myers.wgsl Myers bit-vector 64 chars Shared Peq per workgroup, 2×u32 bitmask
levenshtein_cdist_myers.wgsl Row-wise Myers 64 chars One workgroup per matrix row
levenshtein_matrix.wgsl 2D DP grid 256 chars O(N+M) data upload
jaro.wgsl Bitmap matcher 128 chars 128-bit registers, transposed layout
jaro_matrix.wgsl 2D Jaro grid 128 chars O(N+M) data upload
damerau.wgsl Lowrance-Wagner 32 chars ASCII Full matrix in SLM, non-adjacent transpositions
damerau_matrix.wgsl 2D Damerau grid 32 chars ASCII Same
needleman_affine.wgsl Gotoh serial 128 chars f32 scores, one thread per pair
needleman_wavefront.wgsl Gotoh wavefront 128 chars Anti-diagonal parallel, O(m+n) steps

Project Structure

fuzzgpu/
├── crates/
│   ├── fuzzgpu-core/        # Core Rust engine + GPU shaders
│   ├── fuzzgpu-python/      # PyO3 Python extension
│   └── fuzzgpu-wasm/        # wasm-bindgen WebAssembly module
├── python/fuzzgpu/          # Python package wrapper + type stubs
│   ├── distance/            # rapidfuzz.distance-compatible modules
│   ├── fuzz.py              # rapidfuzz.fuzz-compatible scorers
│   └── process.py           # rapidfuzz.process-compatible helpers
├── fuzz/                    # libFuzzer targets + stable self-harness
├── benchmarks/              # Comparative benchmark scripts
├── tests/                   # Python pytest suite (174 tests)
└── docs/GPU_TESTING.md      # Fault injection & GPU test conventions

Building from Source

# Prerequisites: Rust 1.87+, Python 3.10+, maturin
git clone https://github.com/kuntal-devrat/fuzzgpu.git
cd fuzzgpu
maturin develop --release
pytest tests/ -v
cargo test --workspace

Environment Variables

Variable Effect
FUZZGPU_USE_CPU Force CPU-only mode
FUZZGPU_FORCE_GPU Error (not fallback) on GPU failure in Python
FUZZGPU_DEBUG Log GPU→CPU fallback decisions
FUZZGPU_SIMD Force ISA: portable|neon|avx2|avx512
FUZZGPU_READBACK_TIMEOUT_MS GPU readback timeout (default 10000 ms)
FUZZGPU_SKIP_DISPATCH_LOCK Bypass serialization lock (repro only)
FUZZGPU_REQUIRE_GPU In tests: fail instead of skip when no GPU
WGPU_BACKEND Force wgpu backend: vulkan|metal|dx12
PROPTEST_CASES Override proptest case count

License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fuzzgpu-0.1.7.tar.gz (436.2 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

fuzzgpu-0.1.7-cp310-abi3-win_amd64.whl (3.3 MB view details)

Uploaded CPython 3.10+Windows x86-64

fuzzgpu-0.1.7-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (3.4 MB view details)

Uploaded CPython 3.10+manylinux: glibc 2.17+ x86-64

fuzzgpu-0.1.7-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (3.4 MB view details)

Uploaded CPython 3.10+manylinux: glibc 2.17+ ARM64

fuzzgpu-0.1.7-cp310-abi3-macosx_11_0_arm64.whl (2.7 MB view details)

Uploaded CPython 3.10+macOS 11.0+ ARM64

fuzzgpu-0.1.7-cp310-abi3-macosx_10_12_x86_64.whl (2.8 MB view details)

Uploaded CPython 3.10+macOS 10.12+ x86-64

File details

Details for the file fuzzgpu-0.1.7.tar.gz.

File metadata

  • Download URL: fuzzgpu-0.1.7.tar.gz
  • Upload date:
  • Size: 436.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: maturin/1.14.1

File hashes

Hashes for fuzzgpu-0.1.7.tar.gz
Algorithm Hash digest
SHA256 07af1b4fa4a8dc0bd532c4f6d49537de3aee3e88005d195ac3a2bed7d5f28c3b
MD5 0b2560695811083ef6a7f84d69352bb6
BLAKE2b-256 7e549d8b6c558be8734cb93b485b7e8df050b8949bfb72287bb5d9ba1d64c5b5

See more details on using hashes here.

File details

Details for the file fuzzgpu-0.1.7-cp310-abi3-win_amd64.whl.

File metadata

  • Download URL: fuzzgpu-0.1.7-cp310-abi3-win_amd64.whl
  • Upload date:
  • Size: 3.3 MB
  • Tags: CPython 3.10+, Windows x86-64
  • Uploaded using Trusted Publishing? No
  • Uploaded via: maturin/1.14.1

File hashes

Hashes for fuzzgpu-0.1.7-cp310-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 0516d3bc73ee5ffb0be12d760d2076a734250343e1d25cb7aa6f8293d2a691db
MD5 3cfeb79316cf109e06208d71eac49191
BLAKE2b-256 1b2306bdb7216396b2c318f9e25b30d9ae82d79d5ded6b1bf96eab949f82a06a

See more details on using hashes here.

File details

Details for the file fuzzgpu-0.1.7-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for fuzzgpu-0.1.7-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 2fb8f9e5c72b0219665d5b0dd5d5f99c32e7d8139d251ce0eb86b28b75c16dca
MD5 9db6142925b55e5b85cb75e5266eef15
BLAKE2b-256 eb669cfe913374423a863444f87f54b481d87ebbe35501c07e3dbf78042e3bb8

See more details on using hashes here.

File details

Details for the file fuzzgpu-0.1.7-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for fuzzgpu-0.1.7-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 490e3b774bdd11457fd3f87651996321358b2f66a4f399e968bd113094ca4f36
MD5 f785eb4255604c783cc955a7406c431d
BLAKE2b-256 fdae81765e503bc72792ad4065fd9eb07f300dd8815abbe2727482612a830a32

See more details on using hashes here.

File details

Details for the file fuzzgpu-0.1.7-cp310-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for fuzzgpu-0.1.7-cp310-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 58ce9fcfaf68c9b1b2b54e74aa259c938b9549c952812ef14cca717c4cb2ccb6
MD5 ea5c37955be4cf57694d2ec3e985a862
BLAKE2b-256 a22dcdc6edda38ff253994deb35769a71ec610065713f58e2f32bd7e92075d63

See more details on using hashes here.

File details

Details for the file fuzzgpu-0.1.7-cp310-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for fuzzgpu-0.1.7-cp310-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 0c7afa9045c59e7fc7e67e1720e3005459ee126105e77c4144e8b1f6bfbf4d68
MD5 f01f237843e45f276b36b3aa2ea217e9
BLAKE2b-256 29d2e694ecb905e12a39d5a47412ee19b14e1a7de002ca81998bc46d803713bb

See more details on using hashes here.

Release history Release notifications | RSS feed

0.4.0

6 files

0.3.0

6 files

0.2.0

6 files

0.1.8

6 files

This release

0.1.7 This release

6 files

0.1.6

6 files

0.1.5

6 files

0.1.4

6 files

0.1.3

6 files

0.1.2

6 files

0.1.1

6 files

0.1.0

6 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page