fuzzgpu 0.1.2__tar.gz → 0.1.3__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.2 → fuzzgpu-0.1.3}/Cargo.lock +2 -2
- {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/PKG-INFO +6 -3
- {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/README.md +2 -2
- {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/crates/fuzzgpu-core/Cargo.toml +1 -1
- {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/crates/fuzzgpu-core/src/damerau.rs +1 -0
- {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/crates/fuzzgpu-core/src/gpu.rs +18 -23
- {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/crates/fuzzgpu-core/src/needleman.rs +41 -36
- {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/crates/fuzzgpu-core/src/simd.rs +13 -13
- {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/crates/fuzzgpu-python/Cargo.toml +2 -2
- {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/crates/fuzzgpu-python/src/lib.rs +43 -20
- {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/crates/fuzzgpu-wasm/Cargo.toml +1 -1
- {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/pyproject.toml +4 -1
- {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/tests/test_basic.py +1 -1
- {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/tests/test_edge_cases.py +15 -0
- {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/Cargo.toml +0 -0
- {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/LICENSE +0 -0
- {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/assets/banner.svg +0 -0
- {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/assets/github-stats.svg +0 -0
- {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/assets/logo.png +0 -0
- {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/assets/logo.svg +0 -0
- {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/assets/top-languages.svg +0 -0
- {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/crates/fuzzgpu-core/src/fuzz.rs +0 -0
- {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/crates/fuzzgpu-core/src/jaro.rs +0 -0
- {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/crates/fuzzgpu-core/src/levenshtein.rs +0 -0
- {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/crates/fuzzgpu-core/src/lib.rs +0 -0
- {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/crates/fuzzgpu-core/src/shaders/jaro.wgsl +0 -0
- {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/crates/fuzzgpu-core/src/shaders/jaro_matrix.wgsl +0 -0
- {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/crates/fuzzgpu-core/src/shaders/levenshtein.wgsl +0 -0
- {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/crates/fuzzgpu-core/src/shaders/levenshtein_matrix.wgsl +0 -0
- {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/crates/fuzzgpu-wasm/src/lib.rs +0 -0
- {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/python/fuzzgpu/__init__.py +0 -0
- {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/python/fuzzgpu/fuzz.py +0 -0
- {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/tests/test_api_signatures.py +0 -0
- {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/tests/test_concurrency.py +0 -0
- {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/tests/test_invariants.py +0 -0
- {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/tests/test_stress.py +0 -0
|
@@ -248,7 +248,7 @@ dependencies = [
|
|
|
248
248
|
|
|
249
249
|
[[package]]
|
|
250
250
|
name = "fuzzgpu-core"
|
|
251
|
-
version = "0.1.
|
|
251
|
+
version = "0.1.3"
|
|
252
252
|
dependencies = [
|
|
253
253
|
"bytemuck",
|
|
254
254
|
"log",
|
|
@@ -260,7 +260,7 @@ dependencies = [
|
|
|
260
260
|
|
|
261
261
|
[[package]]
|
|
262
262
|
name = "fuzzgpu-python"
|
|
263
|
-
version = "0.1.
|
|
263
|
+
version = "0.1.3"
|
|
264
264
|
dependencies = [
|
|
265
265
|
"fuzzgpu-core",
|
|
266
266
|
"pollster",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fuzzgpu
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Classifier: Development Status :: 4 - Beta
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -13,6 +13,9 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
13
13
|
Classifier: Programming Language :: Rust
|
|
14
14
|
Classifier: Topic :: Scientific/Engineering
|
|
15
15
|
Classifier: Topic :: Text Processing :: General
|
|
16
|
+
Requires-Dist: pytest>=7.0 ; extra == 'test'
|
|
17
|
+
Requires-Dist: pytest-benchmark ; extra == 'test'
|
|
18
|
+
Provides-Extra: test
|
|
16
19
|
License-File: LICENSE
|
|
17
20
|
Summary: Hardware-accelerated fuzzy string matching & sequence alignment — cross-platform, no CUDA required
|
|
18
21
|
Keywords: gpu,fuzzy,string-matching,levenshtein,damerau,jaro-winkler,needleman-wunsch,webgpu
|
|
@@ -35,7 +38,7 @@ Project-URL: Repository, https://github.com/kuntal-devrat/fuzzgpu
|
|
|
35
38
|
|
|
36
39
|
*Cross-platform GPU compute via WebGPU (`wgpu`) & Multi-Core CPU parallelism with Rayon. Zero CUDA dependencies.*
|
|
37
40
|
|
|
38
|
-
[](https://pypi.org/project/fuzzgpu/)
|
|
39
42
|
[](https://opensource.org/licenses/MIT)
|
|
40
43
|
[](https://www.rust-lang.org)
|
|
41
44
|
[](https://github.com/kuntal-devrat/fuzzgpu)
|
|
@@ -100,7 +103,7 @@ pip install fuzzgpu
|
|
|
100
103
|
### Rust (Cargo.toml)
|
|
101
104
|
```toml
|
|
102
105
|
[dependencies]
|
|
103
|
-
fuzzgpu-core = "0.1.
|
|
106
|
+
fuzzgpu-core = "0.1.3"
|
|
104
107
|
```
|
|
105
108
|
|
|
106
109
|
---
|
|
@@ -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
|
-
[](https://pypi.org/project/fuzzgpu/)
|
|
12
12
|
[](https://opensource.org/licenses/MIT)
|
|
13
13
|
[](https://www.rust-lang.org)
|
|
14
14
|
[](https://github.com/kuntal-devrat/fuzzgpu)
|
|
@@ -73,7 +73,7 @@ pip install fuzzgpu
|
|
|
73
73
|
### Rust (Cargo.toml)
|
|
74
74
|
```toml
|
|
75
75
|
[dependencies]
|
|
76
|
-
fuzzgpu-core = "0.1.
|
|
76
|
+
fuzzgpu-core = "0.1.3"
|
|
77
77
|
```
|
|
78
78
|
|
|
79
79
|
---
|
|
@@ -16,6 +16,7 @@ pub fn damerau_levenshtein_distance(a: &str, b: &str) -> u32 {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
fn damerau_bytes(a: &[u8], b: &[u8]) -> u32 {
|
|
19
|
+
debug_assert!(a.is_ascii() && b.is_ascii(), "damerau_bytes requires ASCII inputs");
|
|
19
20
|
let (m, n) = (a.len(), b.len());
|
|
20
21
|
if m == 0 { return n as u32; }
|
|
21
22
|
if n == 0 { return m as u32; }
|
|
@@ -28,6 +28,7 @@ pub struct GpuEngine {
|
|
|
28
28
|
|
|
29
29
|
static GLOBAL_ENGINE: OnceLock<Arc<GpuEngine>> = OnceLock::new();
|
|
30
30
|
static CPU_ONLY_FLAG: AtomicBool = AtomicBool::new(false);
|
|
31
|
+
static ENV_CPU_CHECK: OnceLock<bool> = OnceLock::new();
|
|
31
32
|
|
|
32
33
|
#[derive(Debug, Clone)]
|
|
33
34
|
pub struct GpuInfo {
|
|
@@ -41,17 +42,18 @@ impl GpuEngine {
|
|
|
41
42
|
CPU_ONLY_FLAG.store(cpu_only, Ordering::SeqCst);
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
/// Check whether CPU-only mode is active.
|
|
45
|
+
/// Check whether CPU-only mode is active (cached, zero syscall overhead in loops).
|
|
45
46
|
pub fn is_cpu_only() -> bool {
|
|
46
47
|
if CPU_ONLY_FLAG.load(Ordering::Relaxed) {
|
|
47
48
|
return true;
|
|
48
49
|
}
|
|
49
|
-
|
|
50
|
-
if
|
|
51
|
-
|
|
50
|
+
*ENV_CPU_CHECK.get_or_init(|| {
|
|
51
|
+
if let Ok(v) = std::env::var("FUZZGPU_USE_CPU") {
|
|
52
|
+
v == "1" || v.eq_ignore_ascii_case("true") || v.eq_ignore_ascii_case("yes")
|
|
53
|
+
} else {
|
|
54
|
+
false
|
|
52
55
|
}
|
|
53
|
-
}
|
|
54
|
-
false
|
|
56
|
+
})
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
/// Returns whether a GPU device is available and ready for compute.
|
|
@@ -65,7 +67,7 @@ impl GpuEngine {
|
|
|
65
67
|
/// Get or lazily initialize the singleton GPU engine.
|
|
66
68
|
pub fn get() -> Result<Arc<Self>> {
|
|
67
69
|
if Self::is_cpu_only() {
|
|
68
|
-
return Err(FuzzGpuError::NoDevice("CPU-only mode is
|
|
70
|
+
return Err(FuzzGpuError::NoDevice("CPU-only mode is active (FUZZGPU_USE_CPU)".into()));
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
if let Some(engine) = GLOBAL_ENGINE.get() {
|
|
@@ -105,7 +107,7 @@ impl GpuEngine {
|
|
|
105
107
|
backend: format!("{:?}", adapter_info.backend),
|
|
106
108
|
};
|
|
107
109
|
|
|
108
|
-
// Query adapter limits dynamically rather than
|
|
110
|
+
// Query adapter limits dynamically rather than hardcoding static limits
|
|
109
111
|
let adapter_limits = adapter.limits();
|
|
110
112
|
let target_storage_size = (128 * 1024 * 1024).min(adapter_limits.max_storage_buffer_binding_size);
|
|
111
113
|
let target_buffer_size = (128 * 1024 * 1024).min(adapter_limits.max_buffer_size);
|
|
@@ -113,7 +115,7 @@ impl GpuEngine {
|
|
|
113
115
|
let required_limits = wgpu::Limits {
|
|
114
116
|
max_storage_buffer_binding_size: target_storage_size,
|
|
115
117
|
max_buffer_size: target_buffer_size,
|
|
116
|
-
max_compute_workgroup_storage_size:
|
|
118
|
+
max_compute_workgroup_storage_size: 16384.min(adapter_limits.max_compute_workgroup_storage_size),
|
|
117
119
|
max_compute_invocations_per_workgroup: 256.min(adapter_limits.max_compute_invocations_per_workgroup),
|
|
118
120
|
..Default::default()
|
|
119
121
|
};
|
|
@@ -121,31 +123,24 @@ impl GpuEngine {
|
|
|
121
123
|
let (device, queue) = adapter
|
|
122
124
|
.request_device(
|
|
123
125
|
&wgpu::DeviceDescriptor {
|
|
124
|
-
label: Some("fuzzgpu"),
|
|
126
|
+
label: Some("fuzzgpu device"),
|
|
125
127
|
required_features: wgpu::Features::empty(),
|
|
126
128
|
required_limits,
|
|
127
|
-
memory_hints:
|
|
129
|
+
memory_hints: Default::default(),
|
|
128
130
|
},
|
|
129
131
|
None,
|
|
130
132
|
)
|
|
131
133
|
.await
|
|
132
134
|
.map_err(|e| FuzzGpuError::NoDevice(format!("Failed to create GPU device: {}", e)))?;
|
|
133
135
|
|
|
134
|
-
|
|
135
|
-
eprintln!(
|
|
136
|
-
"fuzzgpu: Initialized {} ({}) with max buffer size {} MB",
|
|
137
|
-
info.name,
|
|
138
|
-
info.backend,
|
|
139
|
-
target_buffer_size / (1024 * 1024)
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
Ok(Arc::new(Self {
|
|
136
|
+
let engine = Arc::new(Self {
|
|
144
137
|
device,
|
|
145
138
|
queue,
|
|
146
139
|
info,
|
|
147
|
-
max_buffer_size: target_buffer_size,
|
|
140
|
+
max_buffer_size: target_buffer_size as u64,
|
|
148
141
|
max_storage_buffer_binding_size: target_storage_size,
|
|
149
|
-
})
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
Ok(engine)
|
|
150
145
|
}
|
|
151
146
|
}
|
|
@@ -4,7 +4,8 @@ use rayon::prelude::*;
|
|
|
4
4
|
///
|
|
5
5
|
/// Uses single-row DP + scalar diagonal for minimal memory.
|
|
6
6
|
/// Supports both ASCII fast-path and full Unicode characters.
|
|
7
|
-
|
|
7
|
+
/// All scores use `i64` with saturating arithmetic to prevent integer overflow on long sequences.
|
|
8
|
+
pub fn needleman_wunsch(a: &str, b: &str, match_score: i64, mismatch_score: i64, gap_penalty: i64) -> i64 {
|
|
8
9
|
if a.is_ascii() && b.is_ascii() {
|
|
9
10
|
needleman_wunsch_bytes(a.as_bytes(), b.as_bytes(), match_score, mismatch_score, gap_penalty)
|
|
10
11
|
} else {
|
|
@@ -14,29 +15,29 @@ pub fn needleman_wunsch(a: &str, b: &str, match_score: i32, mismatch_score: i32,
|
|
|
14
15
|
}
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
fn needleman_wunsch_bytes(a: &[u8], b: &[u8], match_score:
|
|
18
|
+
fn needleman_wunsch_bytes(a: &[u8], b: &[u8], match_score: i64, mismatch_score: i64, gap_penalty: i64) -> i64 {
|
|
18
19
|
needleman_wunsch_slice(a, b, match_score, mismatch_score, gap_penalty)
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
fn needleman_wunsch_chars(a: &[char], b: &[char], match_score:
|
|
22
|
+
fn needleman_wunsch_chars(a: &[char], b: &[char], match_score: i64, mismatch_score: i64, gap_penalty: i64) -> i64 {
|
|
22
23
|
needleman_wunsch_slice(a, b, match_score, mismatch_score, gap_penalty)
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
fn needleman_wunsch_slice<T: PartialEq>(a: &[T], b: &[T], match_score:
|
|
26
|
+
fn needleman_wunsch_slice<T: PartialEq>(a: &[T], b: &[T], match_score: i64, mismatch_score: i64, gap_penalty: i64) -> i64 {
|
|
26
27
|
let (m, n) = (a.len(), b.len());
|
|
27
28
|
|
|
28
|
-
if m == 0 { return (n as
|
|
29
|
-
if n == 0 { return (m as
|
|
30
|
-
if a == b { return (m as
|
|
29
|
+
if m == 0 { return (n as i64).saturating_mul(gap_penalty); }
|
|
30
|
+
if n == 0 { return (m as i64).saturating_mul(gap_penalty); }
|
|
31
|
+
if a == b { return (m as i64).saturating_mul(match_score); }
|
|
31
32
|
|
|
32
|
-
let mut row = vec![
|
|
33
|
+
let mut row = vec![0i64; n + 1];
|
|
33
34
|
for (j, item) in row.iter_mut().enumerate() {
|
|
34
|
-
*item = (j as
|
|
35
|
+
*item = (j as i64).saturating_mul(gap_penalty);
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
for i in 1..=m {
|
|
38
39
|
let mut prev_diag = row[0];
|
|
39
|
-
row[0] = (i as
|
|
40
|
+
row[0] = (i as i64).saturating_mul(gap_penalty);
|
|
40
41
|
let ai = &a[i - 1];
|
|
41
42
|
for j in 1..=n {
|
|
42
43
|
let old = row[j];
|
|
@@ -53,14 +54,14 @@ fn needleman_wunsch_slice<T: PartialEq>(a: &[T], b: &[T], match_score: i32, mism
|
|
|
53
54
|
/// Batch Needleman-Wunsch with linear gap penalty.
|
|
54
55
|
pub fn needleman_wunsch_batch(
|
|
55
56
|
query: &str, candidates: &[&str],
|
|
56
|
-
match_score:
|
|
57
|
-
) -> Vec<
|
|
57
|
+
match_score: i64, mismatch_score: i64, gap_penalty: i64,
|
|
58
|
+
) -> Vec<i64> {
|
|
58
59
|
candidates.par_iter().map(|c| {
|
|
59
60
|
needleman_wunsch(query, c, match_score, mismatch_score, gap_penalty)
|
|
60
61
|
}).collect()
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
const NEG_INF:
|
|
64
|
+
const NEG_INF: i64 = -1_000_000_000_000_000_000;
|
|
64
65
|
|
|
65
66
|
/// Needleman-Wunsch global alignment score with affine gap penalties (Gotoh 1982 algorithm).
|
|
66
67
|
///
|
|
@@ -68,11 +69,11 @@ const NEG_INF: i32 = -1_000_000_000;
|
|
|
68
69
|
pub fn needleman_wunsch_affine(
|
|
69
70
|
a: &str,
|
|
70
71
|
b: &str,
|
|
71
|
-
match_score:
|
|
72
|
-
mismatch_score:
|
|
73
|
-
gap_open:
|
|
74
|
-
gap_extend:
|
|
75
|
-
) ->
|
|
72
|
+
match_score: i64,
|
|
73
|
+
mismatch_score: i64,
|
|
74
|
+
gap_open: i64,
|
|
75
|
+
gap_extend: i64,
|
|
76
|
+
) -> i64 {
|
|
76
77
|
if a.is_ascii() && b.is_ascii() {
|
|
77
78
|
needleman_wunsch_affine_slice(a.as_bytes(), b.as_bytes(), match_score, mismatch_score, gap_open, gap_extend)
|
|
78
79
|
} else {
|
|
@@ -85,17 +86,17 @@ pub fn needleman_wunsch_affine(
|
|
|
85
86
|
fn needleman_wunsch_affine_slice<T: PartialEq>(
|
|
86
87
|
a: &[T],
|
|
87
88
|
b: &[T],
|
|
88
|
-
match_score:
|
|
89
|
-
mismatch_score:
|
|
90
|
-
gap_open:
|
|
91
|
-
gap_extend:
|
|
92
|
-
) ->
|
|
89
|
+
match_score: i64,
|
|
90
|
+
mismatch_score: i64,
|
|
91
|
+
gap_open: i64,
|
|
92
|
+
gap_extend: i64,
|
|
93
|
+
) -> i64 {
|
|
93
94
|
let (m, n) = (a.len(), b.len());
|
|
94
95
|
|
|
95
96
|
if m == 0 && n == 0 { return 0; }
|
|
96
|
-
if m == 0 { return gap_open
|
|
97
|
-
if n == 0 { return gap_open
|
|
98
|
-
if a == b { return (m as
|
|
97
|
+
if m == 0 { return gap_open.saturating_add((n as i64).saturating_mul(gap_extend)); }
|
|
98
|
+
if n == 0 { return gap_open.saturating_add((m as i64).saturating_mul(gap_extend)); }
|
|
99
|
+
if a == b { return (m as i64).saturating_mul(match_score); }
|
|
99
100
|
|
|
100
101
|
let mut m_row = vec![NEG_INF; n + 1];
|
|
101
102
|
let mut ix_row = vec![NEG_INF; n + 1];
|
|
@@ -103,7 +104,7 @@ fn needleman_wunsch_affine_slice<T: PartialEq>(
|
|
|
103
104
|
|
|
104
105
|
m_row[0] = 0;
|
|
105
106
|
for j in 1..=n {
|
|
106
|
-
let gap_cost = gap_open
|
|
107
|
+
let gap_cost = gap_open.saturating_add((j as i64).saturating_mul(gap_extend));
|
|
107
108
|
iy_row[j] = gap_cost;
|
|
108
109
|
m_row[j] = gap_cost;
|
|
109
110
|
}
|
|
@@ -113,7 +114,7 @@ fn needleman_wunsch_affine_slice<T: PartialEq>(
|
|
|
113
114
|
let mut prev_ix_diag = ix_row[0];
|
|
114
115
|
let mut prev_iy_diag = iy_row[0];
|
|
115
116
|
|
|
116
|
-
let gap_cost_i = gap_open
|
|
117
|
+
let gap_cost_i = gap_open.saturating_add((i as i64).saturating_mul(gap_extend));
|
|
117
118
|
ix_row[0] = gap_cost_i;
|
|
118
119
|
m_row[0] = gap_cost_i;
|
|
119
120
|
iy_row[0] = NEG_INF;
|
|
@@ -125,10 +126,14 @@ fn needleman_wunsch_affine_slice<T: PartialEq>(
|
|
|
125
126
|
let sub_score = if ai == bj { match_score } else { mismatch_score };
|
|
126
127
|
|
|
127
128
|
let prev_diag_best = prev_m_diag.max(prev_ix_diag).max(prev_iy_diag);
|
|
128
|
-
let new_m = prev_diag_best
|
|
129
|
+
let new_m = prev_diag_best.saturating_add(sub_score);
|
|
129
130
|
|
|
130
|
-
let new_ix = (ix_row[j]
|
|
131
|
-
|
|
131
|
+
let new_ix = (ix_row[j].saturating_add(gap_extend))
|
|
132
|
+
.max(m_row[j].saturating_add(gap_open).saturating_add(gap_extend))
|
|
133
|
+
.max(iy_row[j].saturating_add(gap_open).saturating_add(gap_extend));
|
|
134
|
+
let new_iy = (iy_row[j - 1].saturating_add(gap_extend))
|
|
135
|
+
.max(m_row[j - 1].saturating_add(gap_open).saturating_add(gap_extend))
|
|
136
|
+
.max(ix_row[j - 1].saturating_add(gap_open).saturating_add(gap_extend));
|
|
132
137
|
|
|
133
138
|
prev_m_diag = m_row[j];
|
|
134
139
|
prev_ix_diag = ix_row[j];
|
|
@@ -147,11 +152,11 @@ fn needleman_wunsch_affine_slice<T: PartialEq>(
|
|
|
147
152
|
pub fn needleman_wunsch_affine_batch(
|
|
148
153
|
query: &str,
|
|
149
154
|
candidates: &[&str],
|
|
150
|
-
match_score:
|
|
151
|
-
mismatch_score:
|
|
152
|
-
gap_open:
|
|
153
|
-
gap_extend:
|
|
154
|
-
) -> Vec<
|
|
155
|
+
match_score: i64,
|
|
156
|
+
mismatch_score: i64,
|
|
157
|
+
gap_open: i64,
|
|
158
|
+
gap_extend: i64,
|
|
159
|
+
) -> Vec<i64> {
|
|
155
160
|
candidates.par_iter().map(|c| {
|
|
156
161
|
needleman_wunsch_affine(query, c, match_score, mismatch_score, gap_open, gap_extend)
|
|
157
162
|
}).collect()
|
|
@@ -106,23 +106,23 @@ fn levenshtein_dp_optimized(a: &[u8], b: &[u8]) -> u32 {
|
|
|
106
106
|
|
|
107
107
|
/// Optimized Needleman-Wunsch with cache-friendly loop tiling.
|
|
108
108
|
/// (Renamed from `needleman_wunsch_simd` — no actual SIMD intrinsics are used.)
|
|
109
|
-
pub fn needleman_wunsch_striped(a: &[u8], b: &[u8], match_score:
|
|
109
|
+
pub fn needleman_wunsch_striped(a: &[u8], b: &[u8], match_score: i64, mismatch_score: i64, gap_penalty: i64) -> i64 {
|
|
110
110
|
let (m, n) = (a.len(), b.len());
|
|
111
|
-
if m == 0 { return (n as
|
|
112
|
-
if n == 0 { return (m as
|
|
111
|
+
if m == 0 { return (n as i64).saturating_mul(gap_penalty); }
|
|
112
|
+
if n == 0 { return (m as i64).saturating_mul(gap_penalty); }
|
|
113
113
|
|
|
114
114
|
// Fast path: identical strings.
|
|
115
|
-
if a == b { return (m as
|
|
115
|
+
if a == b { return (m as i64).saturating_mul(match_score); }
|
|
116
116
|
|
|
117
117
|
// Single-row + diagonal optimization.
|
|
118
|
-
let mut row = vec![
|
|
118
|
+
let mut row = vec![0i64; n + 1];
|
|
119
119
|
for (j, item) in row.iter_mut().enumerate() {
|
|
120
|
-
*item = (j as
|
|
120
|
+
*item = (j as i64).saturating_mul(gap_penalty);
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
for i in 1..=m {
|
|
124
124
|
let mut prev_diag = row[0];
|
|
125
|
-
row[0] = (i as
|
|
125
|
+
row[0] = (i as i64).saturating_mul(gap_penalty);
|
|
126
126
|
let ai = a[i - 1];
|
|
127
127
|
|
|
128
128
|
// Process in cache-friendly blocks of 8.
|
|
@@ -132,9 +132,9 @@ pub fn needleman_wunsch_striped(a: &[u8], b: &[u8], match_score: i32, mismatch_s
|
|
|
132
132
|
let jj = j + k;
|
|
133
133
|
let old = row[jj];
|
|
134
134
|
let cost = if ai == b[jj - 1] { match_score } else { mismatch_score };
|
|
135
|
-
row[jj] = (prev_diag
|
|
136
|
-
.max(row[jj]
|
|
137
|
-
.max(row[jj - 1]
|
|
135
|
+
row[jj] = (prev_diag.saturating_add(cost))
|
|
136
|
+
.max(row[jj].saturating_add(gap_penalty))
|
|
137
|
+
.max(row[jj - 1].saturating_add(gap_penalty));
|
|
138
138
|
prev_diag = old;
|
|
139
139
|
}
|
|
140
140
|
j += 8;
|
|
@@ -143,9 +143,9 @@ pub fn needleman_wunsch_striped(a: &[u8], b: &[u8], match_score: i32, mismatch_s
|
|
|
143
143
|
while j <= n {
|
|
144
144
|
let old = row[j];
|
|
145
145
|
let cost = if ai == b[j - 1] { match_score } else { mismatch_score };
|
|
146
|
-
row[j] = (prev_diag
|
|
147
|
-
.max(row[j]
|
|
148
|
-
.max(row[j - 1]
|
|
146
|
+
row[j] = (prev_diag.saturating_add(cost))
|
|
147
|
+
.max(row[j].saturating_add(gap_penalty))
|
|
148
|
+
.max(row[j - 1].saturating_add(gap_penalty));
|
|
149
149
|
prev_diag = old;
|
|
150
150
|
j += 1;
|
|
151
151
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "fuzzgpu-python"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.3"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
description = "Python bindings for FuzzGPU: GPU-accelerated fuzzy string matching engine"
|
|
6
6
|
license = "MIT"
|
|
@@ -17,6 +17,6 @@ default = ["gpu"]
|
|
|
17
17
|
gpu = ["fuzzgpu-core/gpu"]
|
|
18
18
|
|
|
19
19
|
[dependencies]
|
|
20
|
-
fuzzgpu-core = { path = "../fuzzgpu-core", version = "0.1.
|
|
20
|
+
fuzzgpu-core = { path = "../fuzzgpu-core", version = "0.1.3", default-features = false }
|
|
21
21
|
pyo3 = { workspace = true, features = ["abi3-py39"] }
|
|
22
22
|
pollster = { workspace = true }
|
|
@@ -1,17 +1,34 @@
|
|
|
1
1
|
use pyo3::prelude::*;
|
|
2
|
+
use std::sync::OnceLock;
|
|
2
3
|
|
|
3
4
|
#[cfg(feature = "gpu")]
|
|
4
5
|
use fuzzgpu_core::gpu::GpuEngine;
|
|
5
6
|
|
|
6
|
-
// ──
|
|
7
|
+
// ── Cached Environment Configuration ─────────────────────────
|
|
8
|
+
|
|
9
|
+
static FORCE_GPU_CACHE: OnceLock<bool> = OnceLock::new();
|
|
10
|
+
static DEBUG_CACHE: OnceLock<bool> = OnceLock::new();
|
|
7
11
|
|
|
8
12
|
#[inline]
|
|
9
13
|
fn is_force_gpu() -> bool {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
*FORCE_GPU_CACHE.get_or_init(|| {
|
|
15
|
+
if let Ok(v) = std::env::var("FUZZGPU_FORCE_GPU") {
|
|
16
|
+
v == "1" || v.eq_ignore_ascii_case("true") || v.eq_ignore_ascii_case("yes")
|
|
17
|
+
} else {
|
|
18
|
+
false
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
#[inline]
|
|
24
|
+
fn is_debug_mode() -> bool {
|
|
25
|
+
*DEBUG_CACHE.get_or_init(|| {
|
|
26
|
+
if let Ok(v) = std::env::var("FUZZGPU_DEBUG") {
|
|
27
|
+
v == "1" || v.eq_ignore_ascii_case("true") || v.eq_ignore_ascii_case("yes")
|
|
28
|
+
} else {
|
|
29
|
+
false
|
|
30
|
+
}
|
|
31
|
+
})
|
|
15
32
|
}
|
|
16
33
|
|
|
17
34
|
// ── Levenshtein ──────────────────────────────────────────────
|
|
@@ -57,7 +74,7 @@ fn levenshtein_batch(py: Python, query: String, candidates: Vec<String>) -> PyRe
|
|
|
57
74
|
e
|
|
58
75
|
)));
|
|
59
76
|
}
|
|
60
|
-
if
|
|
77
|
+
if is_debug_mode() {
|
|
61
78
|
eprintln!("fuzzgpu [fallback]: GPU kernel failed ({}), switching to Rayon CPU", e);
|
|
62
79
|
}
|
|
63
80
|
}
|
|
@@ -91,7 +108,7 @@ fn levenshtein_cdist(py: Python, list_a: Vec<String>, list_b: Vec<String>) -> Py
|
|
|
91
108
|
e
|
|
92
109
|
)));
|
|
93
110
|
}
|
|
94
|
-
if
|
|
111
|
+
if is_debug_mode() {
|
|
95
112
|
eprintln!("fuzzgpu [fallback]: GPU matrix failed ({}), switching to Rayon CPU", e);
|
|
96
113
|
}
|
|
97
114
|
}
|
|
@@ -131,30 +148,30 @@ fn damerau_ratio(py: Python, a: &str, b: &str) -> PyResult<f64> {
|
|
|
131
148
|
py.allow_threads(|| Ok(fuzzgpu_core::damerau_ratio(a, b)))
|
|
132
149
|
}
|
|
133
150
|
|
|
134
|
-
// ── Needleman-Wunsch
|
|
151
|
+
// ── Needleman-Wunsch (i64 Score Support) ─────────────────────
|
|
135
152
|
|
|
136
153
|
#[pyfunction]
|
|
137
154
|
#[pyo3(text_signature = "(a, b, match_score, mismatch_score, gap_penalty, /)")]
|
|
138
|
-
fn needleman_wunsch_score(py: Python, a: &str, b: &str, match_score:
|
|
155
|
+
fn needleman_wunsch_score(py: Python, a: &str, b: &str, match_score: i64, mismatch_score: i64, gap_penalty: i64) -> PyResult<i64> {
|
|
139
156
|
py.allow_threads(|| Ok(fuzzgpu_core::needleman_wunsch(a, b, match_score, mismatch_score, gap_penalty)))
|
|
140
157
|
}
|
|
141
158
|
|
|
142
159
|
#[pyfunction]
|
|
143
160
|
#[pyo3(text_signature = "(query, candidates, match_score, mismatch_score, gap_penalty, /)")]
|
|
144
|
-
fn needleman_wunsch_batch_fn(py: Python, query: String, candidates: Vec<String>, match_score:
|
|
161
|
+
fn needleman_wunsch_batch_fn(py: Python, query: String, candidates: Vec<String>, match_score: i64, mismatch_score: i64, gap_penalty: i64) -> PyResult<Vec<i64>> {
|
|
145
162
|
let refs: Vec<&str> = candidates.iter().map(|s| s.as_str()).collect();
|
|
146
163
|
py.allow_threads(|| Ok(fuzzgpu_core::needleman_wunsch_batch(&query, &refs, match_score, mismatch_score, gap_penalty)))
|
|
147
164
|
}
|
|
148
165
|
|
|
149
166
|
#[pyfunction]
|
|
150
167
|
#[pyo3(text_signature = "(a, b, match_score, mismatch_score, gap_open, gap_extend, /)")]
|
|
151
|
-
fn needleman_wunsch_affine(py: Python, a: &str, b: &str, match_score:
|
|
168
|
+
fn needleman_wunsch_affine(py: Python, a: &str, b: &str, match_score: i64, mismatch_score: i64, gap_open: i64, gap_extend: i64) -> PyResult<i64> {
|
|
152
169
|
py.allow_threads(|| Ok(fuzzgpu_core::needleman_wunsch_affine(a, b, match_score, mismatch_score, gap_open, gap_extend)))
|
|
153
170
|
}
|
|
154
171
|
|
|
155
172
|
#[pyfunction]
|
|
156
173
|
#[pyo3(text_signature = "(query, candidates, match_score, mismatch_score, gap_open, gap_extend, /)")]
|
|
157
|
-
fn needleman_wunsch_affine_batch(py: Python, query: String, candidates: Vec<String>, match_score:
|
|
174
|
+
fn needleman_wunsch_affine_batch(py: Python, query: String, candidates: Vec<String>, match_score: i64, mismatch_score: i64, gap_open: i64, gap_extend: i64) -> PyResult<Vec<i64>> {
|
|
158
175
|
let refs: Vec<&str> = candidates.iter().map(|s| s.as_str()).collect();
|
|
159
176
|
py.allow_threads(|| Ok(fuzzgpu_core::needleman_wunsch_affine_batch(&query, &refs, match_score, mismatch_score, gap_open, gap_extend)))
|
|
160
177
|
}
|
|
@@ -200,7 +217,7 @@ fn jaro_winkler_batch_fn(py: Python, query: String, candidates: Vec<String>, p:
|
|
|
200
217
|
e
|
|
201
218
|
)));
|
|
202
219
|
}
|
|
203
|
-
if
|
|
220
|
+
if is_debug_mode() {
|
|
204
221
|
eprintln!("fuzzgpu [fallback]: GPU kernel failed ({}), switching to Rayon CPU", e);
|
|
205
222
|
}
|
|
206
223
|
}
|
|
@@ -235,7 +252,7 @@ fn jaro_winkler_cdist(py: Python, list_a: Vec<String>, list_b: Vec<String>, p: f
|
|
|
235
252
|
e
|
|
236
253
|
)));
|
|
237
254
|
}
|
|
238
|
-
if
|
|
255
|
+
if is_debug_mode() {
|
|
239
256
|
eprintln!("fuzzgpu [fallback]: GPU matrix failed ({}), switching to Rayon CPU", e);
|
|
240
257
|
}
|
|
241
258
|
}
|
|
@@ -315,7 +332,7 @@ fn levenshtein_myers(py: Python, a: &str, b: &str) -> PyResult<u32> {
|
|
|
315
332
|
|
|
316
333
|
#[pyfunction]
|
|
317
334
|
#[pyo3(text_signature = "(a, b, match_score, mismatch_score, gap_penalty, /)")]
|
|
318
|
-
fn needleman_wunsch_striped(py: Python, a: &str, b: &str, match_score:
|
|
335
|
+
fn needleman_wunsch_striped(py: Python, a: &str, b: &str, match_score: i64, mismatch_score: i64, gap_penalty: i64) -> PyResult<i64> {
|
|
319
336
|
py.allow_threads(|| {
|
|
320
337
|
if a.is_ascii() && b.is_ascii() {
|
|
321
338
|
Ok(fuzzgpu_core::needleman_wunsch_striped(a.as_bytes(), b.as_bytes(), match_score, mismatch_score, gap_penalty))
|
|
@@ -367,14 +384,20 @@ fn is_gpu_available() -> bool {
|
|
|
367
384
|
|
|
368
385
|
#[pyfunction]
|
|
369
386
|
#[pyo3(text_signature = "()")]
|
|
370
|
-
fn warmup() -> bool {
|
|
387
|
+
fn warmup() -> (bool, String) {
|
|
371
388
|
#[cfg(feature = "gpu")]
|
|
372
389
|
{
|
|
373
|
-
GpuEngine::
|
|
390
|
+
if GpuEngine::is_cpu_only() {
|
|
391
|
+
return (false, "CPU-only mode is active (set via set_cpu_only or FUZZGPU_USE_CPU)".into());
|
|
392
|
+
}
|
|
393
|
+
match GpuEngine::get() {
|
|
394
|
+
Ok(engine) => (true, format!("GPU initialized: {} ({})", engine.info.name, engine.info.backend)),
|
|
395
|
+
Err(e) => (false, format!("GPU initialization failed ({}), using Rayon CPU fallback", e)),
|
|
396
|
+
}
|
|
374
397
|
}
|
|
375
398
|
#[cfg(not(feature = "gpu"))]
|
|
376
399
|
{
|
|
377
|
-
|
|
400
|
+
(false, "Built without GPU support (cpu-only)".into())
|
|
378
401
|
}
|
|
379
402
|
}
|
|
380
403
|
|
|
@@ -388,7 +411,7 @@ fn gpu_info() -> PyResult<String> {
|
|
|
388
411
|
}
|
|
389
412
|
match GpuEngine::get() {
|
|
390
413
|
Ok(engine) => Ok(format!("{} ({})", engine.info.name, engine.info.backend)),
|
|
391
|
-
Err(
|
|
414
|
+
Err(e) => Ok(format!("CPU-only fallback mode (GPU unavailable: {})", e)),
|
|
392
415
|
}
|
|
393
416
|
}
|
|
394
417
|
#[cfg(not(feature = "gpu"))]
|
|
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "fuzzgpu"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.3"
|
|
8
8
|
description = "Hardware-accelerated fuzzy string matching & sequence alignment — cross-platform, no CUDA required"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -26,6 +26,9 @@ classifiers = [
|
|
|
26
26
|
"Topic :: Text Processing :: General",
|
|
27
27
|
]
|
|
28
28
|
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
test = ["pytest>=7.0", "pytest-benchmark"]
|
|
31
|
+
|
|
29
32
|
[project.urls]
|
|
30
33
|
Homepage = "https://github.com/kuntal-devrat/fuzzgpu"
|
|
31
34
|
Repository = "https://github.com/kuntal-devrat/fuzzgpu"
|
|
@@ -336,3 +336,18 @@ class TestAlgorithmicEdgeScenarios:
|
|
|
336
336
|
assert fuzzgpu.levenshtein_myers("café", "cafe") == 1
|
|
337
337
|
assert fuzzgpu.levenshtein_myers("🚀", "") == 1
|
|
338
338
|
assert fuzzgpu.jaro_optimized("café", "cafe") > 0.8
|
|
339
|
+
|
|
340
|
+
def test_needleman_i64_large_scores(self):
|
|
341
|
+
"""Test that Needleman-Wunsch scores safely handle values exceeding 32-bit integer limits."""
|
|
342
|
+
large_match = 5_000_000_000
|
|
343
|
+
score = fuzzgpu.needleman_wunsch("AGCT", "AGCT", large_match, -1, -2)
|
|
344
|
+
assert score == 4 * large_match
|
|
345
|
+
score_affine = fuzzgpu.needleman_wunsch_affine("AGCT", "AGCT", large_match, -1, -3, -1)
|
|
346
|
+
assert score_affine == 4 * large_match
|
|
347
|
+
|
|
348
|
+
def test_warmup_diagnostics(self):
|
|
349
|
+
"""Test warmup returns detailed status tuple."""
|
|
350
|
+
is_ok, msg = fuzzgpu.warmup()
|
|
351
|
+
assert isinstance(is_ok, bool)
|
|
352
|
+
assert isinstance(msg, str)
|
|
353
|
+
assert len(msg) > 0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|