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.
Files changed (36) hide show
  1. {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/Cargo.lock +2 -2
  2. {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/PKG-INFO +6 -3
  3. {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/README.md +2 -2
  4. {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/crates/fuzzgpu-core/Cargo.toml +1 -1
  5. {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/crates/fuzzgpu-core/src/damerau.rs +1 -0
  6. {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/crates/fuzzgpu-core/src/gpu.rs +18 -23
  7. {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/crates/fuzzgpu-core/src/needleman.rs +41 -36
  8. {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/crates/fuzzgpu-core/src/simd.rs +13 -13
  9. {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/crates/fuzzgpu-python/Cargo.toml +2 -2
  10. {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/crates/fuzzgpu-python/src/lib.rs +43 -20
  11. {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/crates/fuzzgpu-wasm/Cargo.toml +1 -1
  12. {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/pyproject.toml +4 -1
  13. {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/tests/test_basic.py +1 -1
  14. {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/tests/test_edge_cases.py +15 -0
  15. {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/Cargo.toml +0 -0
  16. {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/LICENSE +0 -0
  17. {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/assets/banner.svg +0 -0
  18. {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/assets/github-stats.svg +0 -0
  19. {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/assets/logo.png +0 -0
  20. {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/assets/logo.svg +0 -0
  21. {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/assets/top-languages.svg +0 -0
  22. {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/crates/fuzzgpu-core/src/fuzz.rs +0 -0
  23. {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/crates/fuzzgpu-core/src/jaro.rs +0 -0
  24. {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/crates/fuzzgpu-core/src/levenshtein.rs +0 -0
  25. {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/crates/fuzzgpu-core/src/lib.rs +0 -0
  26. {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/crates/fuzzgpu-core/src/shaders/jaro.wgsl +0 -0
  27. {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/crates/fuzzgpu-core/src/shaders/jaro_matrix.wgsl +0 -0
  28. {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/crates/fuzzgpu-core/src/shaders/levenshtein.wgsl +0 -0
  29. {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/crates/fuzzgpu-core/src/shaders/levenshtein_matrix.wgsl +0 -0
  30. {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/crates/fuzzgpu-wasm/src/lib.rs +0 -0
  31. {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/python/fuzzgpu/__init__.py +0 -0
  32. {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/python/fuzzgpu/fuzz.py +0 -0
  33. {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/tests/test_api_signatures.py +0 -0
  34. {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/tests/test_concurrency.py +0 -0
  35. {fuzzgpu-0.1.2 → fuzzgpu-0.1.3}/tests/test_invariants.py +0 -0
  36. {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.2"
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.2"
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.2
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
- [![PyPI Version](https://img.shields.io/badge/pypi-v0.1.2-blue.svg?style=flat-square)](https://pypi.org/project/fuzzgpu/)
41
+ [![PyPI Version](https://img.shields.io/badge/pypi-v0.1.3-blue.svg?style=flat-square)](https://pypi.org/project/fuzzgpu/)
39
42
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg?style=flat-square)](https://opensource.org/licenses/MIT)
40
43
  [![Rust](https://img.shields.io/badge/rust-1.75+-orange.svg?style=flat-square)](https://www.rust-lang.org)
41
44
  [![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)
@@ -100,7 +103,7 @@ pip install fuzzgpu
100
103
  ### Rust (Cargo.toml)
101
104
  ```toml
102
105
  [dependencies]
103
- fuzzgpu-core = "0.1.2"
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
- [![PyPI Version](https://img.shields.io/badge/pypi-v0.1.2-blue.svg?style=flat-square)](https://pypi.org/project/fuzzgpu/)
11
+ [![PyPI Version](https://img.shields.io/badge/pypi-v0.1.3-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.75+-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)
@@ -73,7 +73,7 @@ pip install fuzzgpu
73
73
  ### Rust (Cargo.toml)
74
74
  ```toml
75
75
  [dependencies]
76
- fuzzgpu-core = "0.1.2"
76
+ fuzzgpu-core = "0.1.3"
77
77
  ```
78
78
 
79
79
  ---
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "fuzzgpu-core"
3
- version = "0.1.2"
3
+ version = "0.1.3"
4
4
  edition = "2021"
5
5
  description = "GPU-accelerated fuzzy string matching engine"
6
6
  license = "MIT"
@@ -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
- if let Ok(v) = std::env::var("FUZZGPU_USE_CPU") {
50
- if v == "1" || v.eq_ignore_ascii_case("true") || v.eq_ignore_ascii_case("yes") {
51
- return true;
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 forced (FUZZGPU_USE_CPU)".into()));
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 blindly hardcoding 128MB
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: 16 * 1024.min(adapter_limits.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: wgpu::MemoryHints::Performance,
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
- if std::env::var("FUZZGPU_DEBUG").is_ok() {
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
- pub fn needleman_wunsch(a: &str, b: &str, match_score: i32, mismatch_score: i32, gap_penalty: i32) -> i32 {
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: i32, mismatch_score: i32, gap_penalty: i32) -> i32 {
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: i32, mismatch_score: i32, gap_penalty: i32) -> i32 {
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: i32, mismatch_score: i32, gap_penalty: i32) -> i32 {
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 i32).saturating_mul(gap_penalty); }
29
- if n == 0 { return (m as i32).saturating_mul(gap_penalty); }
30
- if a == b { return (m as i32).saturating_mul(match_score); }
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![0i32; n + 1];
33
+ let mut row = vec![0i64; n + 1];
33
34
  for (j, item) in row.iter_mut().enumerate() {
34
- *item = (j as i32).saturating_mul(gap_penalty);
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 i32).saturating_mul(gap_penalty);
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: i32, mismatch_score: i32, gap_penalty: i32,
57
- ) -> Vec<i32> {
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: i32 = -1_000_000_000;
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: i32,
72
- mismatch_score: i32,
73
- gap_open: i32,
74
- gap_extend: i32,
75
- ) -> i32 {
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: i32,
89
- mismatch_score: i32,
90
- gap_open: i32,
91
- gap_extend: i32,
92
- ) -> i32 {
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 + (n as i32) * gap_extend; }
97
- if n == 0 { return gap_open + (m as i32) * gap_extend; }
98
- if a == b { return (m as i32) * match_score; }
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 + (j as i32) * gap_extend;
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 + (i as i32) * gap_extend;
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 + sub_score;
129
+ let new_m = prev_diag_best.saturating_add(sub_score);
129
130
 
130
- let new_ix = (ix_row[j] + gap_extend).max(m_row[j] + gap_open + gap_extend).max(iy_row[j] + gap_open + gap_extend);
131
- let new_iy = (iy_row[j - 1] + gap_extend).max(m_row[j - 1] + gap_open + gap_extend).max(ix_row[j - 1] + gap_open + gap_extend);
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: i32,
151
- mismatch_score: i32,
152
- gap_open: i32,
153
- gap_extend: i32,
154
- ) -> Vec<i32> {
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: i32, mismatch_score: i32, gap_penalty: i32) -> i32 {
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 i32) * gap_penalty; }
112
- if n == 0 { return (m as i32) * gap_penalty; }
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 i32) * match_score; }
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![0i32; n + 1];
118
+ let mut row = vec![0i64; n + 1];
119
119
  for (j, item) in row.iter_mut().enumerate() {
120
- *item = (j as i32) * gap_penalty;
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 i32) * gap_penalty;
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 + cost)
136
- .max(row[jj] + gap_penalty)
137
- .max(row[jj - 1] + gap_penalty);
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 + cost)
147
- .max(row[j] + gap_penalty)
148
- .max(row[j - 1] + gap_penalty);
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.2"
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.2", default-features = false }
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
- // ── Helpers ──────────────────────────────────────────────────
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
- if let Ok(v) = std::env::var("FUZZGPU_FORCE_GPU") {
11
- v == "1" || v.eq_ignore_ascii_case("true") || v.eq_ignore_ascii_case("yes")
12
- } else {
13
- false
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 std::env::var("FUZZGPU_DEBUG").is_ok() {
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 std::env::var("FUZZGPU_DEBUG").is_ok() {
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: i32, mismatch_score: i32, gap_penalty: i32) -> PyResult<i32> {
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: i32, mismatch_score: i32, gap_penalty: i32) -> PyResult<Vec<i32>> {
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: i32, mismatch_score: i32, gap_open: i32, gap_extend: i32) -> PyResult<i32> {
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: i32, mismatch_score: i32, gap_open: i32, gap_extend: i32) -> PyResult<Vec<i32>> {
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 std::env::var("FUZZGPU_DEBUG").is_ok() {
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 std::env::var("FUZZGPU_DEBUG").is_ok() {
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: i32, mismatch_score: i32, gap_penalty: i32) -> PyResult<i32> {
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::get().is_ok()
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
- true
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(_) => Ok("CPU-only fallback mode (no GPU device detected)".into()),
414
+ Err(e) => Ok(format!("CPU-only fallback mode (GPU unavailable: {})", e)),
392
415
  }
393
416
  }
394
417
  #[cfg(not(feature = "gpu"))]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "fuzzgpu-wasm"
3
- version = "0.1.2"
3
+ version = "0.1.3"
4
4
  edition = "2021"
5
5
 
6
6
  [workspace]
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "fuzzgpu"
7
- version = "0.1.2"
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"
@@ -320,4 +320,4 @@ class TestJaroOptimized:
320
320
 
321
321
  class TestVersion:
322
322
  def test_version_string(self):
323
- assert fuzzgpu.__version__ == "0.1.2"
323
+ assert fuzzgpu.__version__ == "0.1.3"
@@ -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