tricoder 1.2.8__py3-none-any.whl

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.
tricoder/gpu_utils.py ADDED
@@ -0,0 +1,414 @@
1
+ """GPU acceleration utilities using CuPy (CUDA) or PyTorch (MPS for Mac)."""
2
+ import warnings
3
+ import platform
4
+ import sys
5
+ from typing import Optional, Tuple
6
+
7
+ import numpy as np
8
+ from scipy import sparse
9
+
10
+ # Try CuPy for CUDA (NVIDIA GPUs)
11
+ try:
12
+ import cupy as cp
13
+ import cupyx.scipy.sparse as cusp
14
+ CUPY_AVAILABLE = True
15
+ except ImportError:
16
+ CUPY_AVAILABLE = False
17
+ cp = None
18
+ cusp = None
19
+
20
+ # Try PyTorch for MPS (Mac GPUs) or CUDA fallback
21
+ try:
22
+ import torch
23
+ TORCH_AVAILABLE = True
24
+ TORCH_VERSION = torch.__version__
25
+ except ImportError:
26
+ TORCH_AVAILABLE = False
27
+ TORCH_VERSION = None
28
+ torch = None
29
+
30
+
31
+ def diagnose_gpu_support() -> dict:
32
+ """
33
+ Diagnose GPU support availability and provide helpful information.
34
+
35
+ Returns:
36
+ Dictionary with diagnostic information
37
+ """
38
+ diagnostics = {
39
+ 'platform': platform.system(),
40
+ 'is_mac': platform.system() == 'Darwin',
41
+ 'python_version': sys.version,
42
+ 'cupy_available': CUPY_AVAILABLE,
43
+ 'torch_available': TORCH_AVAILABLE,
44
+ 'torch_version': TORCH_VERSION,
45
+ 'gpu_backends': []
46
+ }
47
+
48
+ # Check CuPy/CUDA
49
+ if CUPY_AVAILABLE:
50
+ try:
51
+ _ = cp.array([1, 2, 3])
52
+ device = cp.cuda.Device(0)
53
+ device.use()
54
+ diagnostics['gpu_backends'].append('CUDA (CuPy)')
55
+ except Exception as e:
56
+ diagnostics['cupy_error'] = str(e)
57
+
58
+ # Check PyTorch MPS (Mac)
59
+ if TORCH_AVAILABLE and diagnostics['is_mac']:
60
+ try:
61
+ if hasattr(torch.backends, 'mps') and hasattr(torch.backends.mps, 'is_available'):
62
+ if torch.backends.mps.is_available():
63
+ diagnostics['gpu_backends'].append('MPS (Mac)')
64
+ else:
65
+ diagnostics['mps_unavailable_reason'] = 'MPS backend not available (requires macOS 12.3+ and Apple Silicon)'
66
+ else:
67
+ diagnostics['mps_unavailable_reason'] = f'PyTorch {TORCH_VERSION} does not support MPS (requires PyTorch 1.12+)'
68
+ except Exception as e:
69
+ diagnostics['mps_error'] = str(e)
70
+
71
+ # Check PyTorch CUDA
72
+ if TORCH_AVAILABLE:
73
+ try:
74
+ if torch.cuda.is_available():
75
+ diagnostics['gpu_backends'].append('CUDA (PyTorch)')
76
+ except Exception:
77
+ pass
78
+
79
+ return diagnostics
80
+
81
+
82
+ class GPUAccelerator:
83
+ """GPU accelerator with automatic CPU fallback. Supports CUDA (NVIDIA) and MPS (Mac)."""
84
+
85
+ def __init__(self, use_gpu: bool = False):
86
+ """
87
+ Initialize GPU accelerator.
88
+
89
+ Args:
90
+ use_gpu: Whether to attempt GPU acceleration
91
+ """
92
+ self.use_gpu = False
93
+ self.device_type = None # 'cuda', 'mps', or None
94
+ self.device = None
95
+ self.backend = None # 'cupy' or 'torch'
96
+
97
+ if not use_gpu:
98
+ return
99
+
100
+ # Detect platform and available GPU backends
101
+ is_mac = platform.system() == 'Darwin'
102
+
103
+ # Try CuPy (CUDA) first on non-Mac systems
104
+ if not is_mac and CUPY_AVAILABLE:
105
+ try:
106
+ _ = cp.array([1, 2, 3])
107
+ self.device = cp.cuda.Device(0)
108
+ self.device.use()
109
+ self.use_gpu = True
110
+ self.device_type = 'cuda'
111
+ self.backend = 'cupy'
112
+ return
113
+ except Exception:
114
+ pass
115
+
116
+ # Try PyTorch MPS (Mac GPU)
117
+ if TORCH_AVAILABLE and is_mac:
118
+ try:
119
+ # Check if MPS backend is available (requires PyTorch 1.12+ and macOS 12.3+)
120
+ if hasattr(torch.backends, 'mps') and hasattr(torch.backends.mps, 'is_available'):
121
+ if torch.backends.mps.is_available():
122
+ self.device = torch.device('mps')
123
+ # Test with a small operation
124
+ test_tensor = torch.tensor([1.0, 2.0, 3.0], device=self.device)
125
+ _ = test_tensor * 2
126
+ self.use_gpu = True
127
+ self.device_type = 'mps'
128
+ self.backend = 'torch'
129
+ return
130
+ else:
131
+ warnings.warn("MPS backend is not available. This requires macOS 12.3+ and Apple Silicon (M1/M2/M3). Falling back to CPU.")
132
+ else:
133
+ warnings.warn("PyTorch MPS backend not available. Please upgrade PyTorch to version 1.12+ for Mac GPU support. Falling back to CPU.")
134
+ except Exception as e:
135
+ warnings.warn(f"MPS GPU acceleration failed: {e}. Falling back to CPU.")
136
+
137
+ # Try PyTorch CUDA as fallback (if available)
138
+ if TORCH_AVAILABLE and torch.cuda.is_available():
139
+ try:
140
+ self.device = torch.device('cuda')
141
+ test_tensor = torch.tensor([1.0, 2.0, 3.0], device=self.device)
142
+ _ = test_tensor * 2
143
+ self.use_gpu = True
144
+ self.device_type = 'cuda'
145
+ self.backend = 'torch'
146
+ return
147
+ except Exception:
148
+ pass
149
+
150
+ # No GPU available
151
+ if use_gpu:
152
+ warnings.warn("GPU acceleration requested but no GPU backend available. Falling back to CPU.")
153
+
154
+ def to_gpu(self, arr: np.ndarray):
155
+ """Convert numpy array to GPU array."""
156
+ if not self.use_gpu:
157
+ return arr
158
+
159
+ if self.backend == 'cupy':
160
+ return cp.asarray(arr)
161
+ elif self.backend == 'torch':
162
+ return torch.from_numpy(arr).to(self.device)
163
+ return arr
164
+
165
+ def to_cpu(self, arr) -> np.ndarray:
166
+ """Convert GPU array back to CPU numpy array."""
167
+ if not self.use_gpu:
168
+ return arr
169
+
170
+ if self.backend == 'cupy' and isinstance(arr, cp.ndarray):
171
+ return cp.asnumpy(arr)
172
+ elif self.backend == 'torch' and isinstance(arr, torch.Tensor):
173
+ return arr.cpu().numpy()
174
+ return arr
175
+
176
+ def sparse_to_gpu(self, sp_matrix: sparse.spmatrix) -> 'cusp.spmatrix':
177
+ """Convert scipy sparse matrix to CuPy sparse matrix."""
178
+ if self.use_gpu:
179
+ if isinstance(sp_matrix, sparse.csr_matrix):
180
+ return cusp.csr_matrix(sp_matrix)
181
+ elif isinstance(sp_matrix, sparse.csc_matrix):
182
+ return cusp.csc_matrix(sp_matrix)
183
+ elif isinstance(sp_matrix, sparse.coo_matrix):
184
+ return cusp.coo_matrix(sp_matrix)
185
+ return sp_matrix
186
+
187
+ def sparse_to_cpu(self, sp_matrix) -> sparse.spmatrix:
188
+ """Convert CuPy sparse matrix back to scipy sparse matrix."""
189
+ if self.use_gpu and hasattr(sp_matrix, 'get'):
190
+ # CuPy sparse matrix
191
+ return sp_matrix.get()
192
+ return sp_matrix
193
+
194
+ def svd(self, matrix: np.ndarray, n_components: int,
195
+ random_state: Optional[int] = None) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
196
+ """
197
+ Perform Truncated SVD on GPU or CPU.
198
+
199
+ Args:
200
+ matrix: Input matrix (n_samples, n_features)
201
+ n_components: Number of components
202
+ random_state: Random seed
203
+
204
+ Returns:
205
+ (U, S, Vt) where U @ diag(S) @ Vt approximates the input
206
+ """
207
+ if self.use_gpu:
208
+ try:
209
+ gpu_matrix = self.to_gpu(matrix)
210
+
211
+ if self.backend == 'cupy':
212
+ # CuPy SVD
213
+ U, S, Vt = cp.linalg.svd(gpu_matrix, full_matrices=False)
214
+ elif self.backend == 'torch':
215
+ # PyTorch SVD
216
+ U, S, Vt = torch.linalg.svd(gpu_matrix, full_matrices=False)
217
+ else:
218
+ raise ValueError(f"Unknown backend: {self.backend}")
219
+
220
+ # Truncate to n_components
221
+ U = U[:, :n_components]
222
+ S = S[:n_components]
223
+ Vt = Vt[:n_components, :]
224
+
225
+ # Convert back to CPU
226
+ return self.to_cpu(U), self.to_cpu(S), self.to_cpu(Vt)
227
+ except Exception as e:
228
+ warnings.warn(f"GPU SVD failed: {e}. Falling back to CPU.")
229
+ self.use_gpu = False
230
+
231
+ # CPU fallback using sklearn
232
+ from sklearn.decomposition import TruncatedSVD
233
+ svd = TruncatedSVD(n_components=n_components, random_state=random_state, n_iter=5)
234
+ U = svd.fit_transform(matrix)
235
+ S = svd.singular_values_
236
+ Vt = svd.components_
237
+ return U, S, Vt
238
+
239
+ def pca(self, matrix: np.ndarray, n_components: int,
240
+ random_state: Optional[int] = None) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
241
+ """
242
+ Perform PCA on GPU or CPU.
243
+
244
+ Args:
245
+ matrix: Input matrix (n_samples, n_features)
246
+ n_components: Number of components
247
+ random_state: Random seed
248
+
249
+ Returns:
250
+ (transformed, components, mean)
251
+ """
252
+ if self.use_gpu:
253
+ try:
254
+ gpu_matrix = self.to_gpu(matrix)
255
+
256
+ # Center the data
257
+ if self.backend == 'cupy':
258
+ mean = cp.mean(gpu_matrix, axis=0)
259
+ centered = gpu_matrix - mean
260
+ # SVD
261
+ U, S, Vt = cp.linalg.svd(centered, full_matrices=False)
262
+ # Transform: U @ diag(S)
263
+ transformed = U @ cp.diag(S)
264
+ elif self.backend == 'torch':
265
+ mean = torch.mean(gpu_matrix, axis=0)
266
+ centered = gpu_matrix - mean
267
+ # SVD
268
+ U, S, Vt = torch.linalg.svd(centered, full_matrices=False)
269
+ # Transform: U @ diag(S)
270
+ transformed = U @ torch.diag(S)
271
+ else:
272
+ raise ValueError(f"Unknown backend: {self.backend}")
273
+
274
+ # Truncate
275
+ U = U[:, :n_components]
276
+ S = S[:n_components]
277
+ Vt = Vt[:n_components, :]
278
+ transformed = transformed[:, :n_components]
279
+
280
+ # Convert back to CPU
281
+ return self.to_cpu(transformed), self.to_cpu(Vt), self.to_cpu(mean)
282
+ except Exception as e:
283
+ warnings.warn(f"GPU PCA failed: {e}. Falling back to CPU.")
284
+ self.use_gpu = False
285
+
286
+ # CPU fallback using sklearn
287
+ from sklearn.decomposition import PCA
288
+ pca = PCA(n_components=n_components, random_state=random_state)
289
+ transformed = pca.fit_transform(matrix)
290
+ return transformed, pca.components_, pca.mean_
291
+
292
+ def matmul(self, a, b):
293
+ """Matrix multiplication on GPU or CPU."""
294
+ if self.use_gpu:
295
+ try:
296
+ a_gpu = self.to_gpu(a) if isinstance(a, np.ndarray) else a
297
+ b_gpu = self.to_gpu(b) if isinstance(b, np.ndarray) else b
298
+
299
+ if self.backend == 'cupy':
300
+ result = cp.matmul(a_gpu, b_gpu)
301
+ elif self.backend == 'torch':
302
+ result = torch.matmul(a_gpu, b_gpu)
303
+ else:
304
+ raise ValueError(f"Unknown backend: {self.backend}")
305
+
306
+ return self.to_cpu(result)
307
+ except Exception as e:
308
+ warnings.warn(f"GPU matmul failed: {e}. Falling back to CPU.")
309
+ self.use_gpu = False
310
+
311
+ return np.matmul(a, b)
312
+
313
+ def sparse_matmul(self, a, b):
314
+ """Sparse matrix multiplication on GPU or CPU."""
315
+ if self.use_gpu:
316
+ try:
317
+ # PyTorch sparse support is limited, so prefer CuPy for sparse ops
318
+ if self.backend == 'cupy':
319
+ a_gpu = self.sparse_to_gpu(a) if isinstance(a, sparse.spmatrix) else a
320
+ b_gpu = self.to_gpu(b) if isinstance(b, np.ndarray) else b
321
+ result = a_gpu @ b_gpu
322
+ return self.to_cpu(result)
323
+ elif self.backend == 'torch':
324
+ # PyTorch: convert sparse to dense for now (MPS doesn't support sparse well)
325
+ if isinstance(a, sparse.spmatrix):
326
+ a_dense = self.to_gpu(a.toarray())
327
+ b_gpu = self.to_gpu(b) if isinstance(b, np.ndarray) else b
328
+ result = torch.matmul(a_dense, b_gpu)
329
+ return self.to_cpu(result)
330
+ except Exception as e:
331
+ warnings.warn(f"GPU sparse matmul failed: {e}. Falling back to CPU.")
332
+ self.use_gpu = False
333
+
334
+ if isinstance(a, sparse.spmatrix):
335
+ return a @ b
336
+ return np.matmul(a, b)
337
+
338
+ def norm(self, arr: np.ndarray, axis: Optional[int] = None, keepdims: bool = False) -> np.ndarray:
339
+ """Compute L2 norm on GPU or CPU."""
340
+ if self.use_gpu:
341
+ try:
342
+ gpu_arr = self.to_gpu(arr)
343
+
344
+ if self.backend == 'cupy':
345
+ result = cp.linalg.norm(gpu_arr, axis=axis, keepdims=keepdims)
346
+ elif self.backend == 'torch':
347
+ result = torch.linalg.norm(gpu_arr, dim=axis, keepdim=keepdims)
348
+ else:
349
+ raise ValueError(f"Unknown backend: {self.backend}")
350
+
351
+ return self.to_cpu(result)
352
+ except Exception as e:
353
+ warnings.warn(f"GPU norm failed: {e}. Falling back to CPU.")
354
+ self.use_gpu = False
355
+
356
+ return np.linalg.norm(arr, axis=axis, keepdims=keepdims)
357
+
358
+ def sum(self, arr, axis: Optional[int] = None, keepdims: bool = False):
359
+ """Sum array on GPU or CPU."""
360
+ if self.use_gpu:
361
+ try:
362
+ gpu_arr = self.to_gpu(arr) if isinstance(arr, np.ndarray) else arr
363
+
364
+ if self.backend == 'cupy':
365
+ result = cp.sum(gpu_arr, axis=axis, keepdims=keepdims)
366
+ elif self.backend == 'torch':
367
+ result = torch.sum(gpu_arr, dim=axis, keepdim=keepdims)
368
+ else:
369
+ raise ValueError(f"Unknown backend: {self.backend}")
370
+
371
+ return self.to_cpu(result) if hasattr(result, 'cpu') or isinstance(result, cp.ndarray) else result
372
+ except Exception as e:
373
+ warnings.warn(f"GPU sum failed: {e}. Falling back to CPU.")
374
+ self.use_gpu = False
375
+
376
+ return np.sum(arr, axis=axis, keepdims=keepdims)
377
+
378
+ def maximum(self, a, b):
379
+ """Element-wise maximum on GPU or CPU."""
380
+ if self.use_gpu:
381
+ try:
382
+ a_gpu = self.to_gpu(a) if isinstance(a, np.ndarray) else a
383
+ b_gpu = self.to_gpu(b) if isinstance(b, np.ndarray) else b
384
+
385
+ if self.backend == 'cupy':
386
+ result = cp.maximum(a_gpu, b_gpu)
387
+ elif self.backend == 'torch':
388
+ result = torch.maximum(a_gpu, b_gpu)
389
+ else:
390
+ raise ValueError(f"Unknown backend: {self.backend}")
391
+
392
+ return self.to_cpu(result)
393
+ except Exception as e:
394
+ warnings.warn(f"GPU maximum failed: {e}. Falling back to CPU.")
395
+ self.use_gpu = False
396
+
397
+ return np.maximum(a, b)
398
+
399
+ def __enter__(self):
400
+ """Context manager entry."""
401
+ return self
402
+
403
+ def __exit__(self, exc_type, exc_val, exc_tb):
404
+ """Context manager exit - cleanup GPU memory."""
405
+ if self.use_gpu:
406
+ try:
407
+ if self.backend == 'cupy':
408
+ cp.get_default_memory_pool().free_all_blocks()
409
+ elif self.backend == 'torch':
410
+ torch.mps.empty_cache() if self.device_type == 'mps' else torch.cuda.empty_cache()
411
+ except:
412
+ pass
413
+ return False
414
+