gpuarray 0.2.0__py3-none-any.whl → 0.4.0__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.
gpuarray/__init__.py CHANGED
@@ -27,6 +27,10 @@ from .api import (
27
27
  concatenate,
28
28
  stack,
29
29
  random,
30
+ fft,
31
+ ifft,
32
+ fft_freq,
33
+ fft_magnitude,
30
34
  )
31
35
 
32
36
  __version__ = "0.1.0"
@@ -58,4 +62,8 @@ __all__ = [
58
62
  "concatenate",
59
63
  "stack",
60
64
  "random",
65
+ "fft",
66
+ "ifft",
67
+ "fft_freq",
68
+ "fft_magnitude",
61
69
  ]
gpuarray/api.py CHANGED
@@ -6,7 +6,7 @@ from typing import Sequence
6
6
 
7
7
  import numpy as np
8
8
 
9
- from .core import GPUArray, _from_numpy, matmul as _matmul
9
+ from .core import GPUArray, _from_numpy, matmul as _matmul, _complex_magnitude
10
10
  from . import shaders
11
11
 
12
12
 
@@ -180,3 +180,37 @@ class _Random:
180
180
 
181
181
 
182
182
  random = _Random()
183
+
184
+
185
+ # ----- FFT functions -----
186
+
187
+ def fft(a) -> GPUArray:
188
+ """1D FFT. Input is real or complex (interleaved). Returns complex GPUArray."""
189
+ if not isinstance(a, GPUArray):
190
+ a = array(a)
191
+ return a.fft()
192
+
193
+
194
+ def ifft(a) -> GPUArray:
195
+ """1D inverse FFT. Input is complex interleaved. Returns complex GPUArray."""
196
+ if not isinstance(a, GPUArray):
197
+ a = array(a)
198
+ return a.ifft()
199
+
200
+
201
+ def fft_freq(n: int, d: float = 1.0) -> GPUArray:
202
+ """Return the DFT sample frequencies (like numpy.fft.fftfreq)."""
203
+ results = np.empty(n, dtype=np.float32)
204
+ N = (n - 1) // 2 + 1
205
+ results[:N] = np.arange(0, N, dtype=np.float32)
206
+ results[N:] = np.arange(-(n // 2), 0, dtype=np.float32)
207
+ results /= (n * d)
208
+ return _from_numpy(results)
209
+
210
+
211
+ def fft_magnitude(a) -> GPUArray:
212
+ """Compute magnitude spectrum |FFT(x)|. Returns real GPUArray."""
213
+ if not isinstance(a, GPUArray):
214
+ a = array(a)
215
+ fft_result = a.fft()
216
+ return _complex_magnitude(fft_result)
gpuarray/core.py CHANGED
@@ -13,6 +13,7 @@ from .device import get_device
13
13
  from . import shaders
14
14
 
15
15
  WORKGROUP_SIZE = 256
16
+ REDUCTION_ELEMENTS_PER_WORKGROUP = 1024 # 256 threads * 4 elements each
16
17
 
17
18
  # Global pipeline cache: shader_code -> (shader_module, compute_pipeline)
18
19
  _pipeline_cache: dict[str, tuple] = {}
@@ -174,7 +175,10 @@ class GPUArray:
174
175
  return GPUArray(out_buf, self._shape)
175
176
 
176
177
  def _run_reduction(self, shader_code: str) -> float:
177
- """Multi-pass parallel reduction until 1 element remains."""
178
+ """Multi-pass parallel reduction until 1 element remains.
179
+
180
+ Each workgroup of 256 threads handles 1024 elements (4 per thread).
181
+ """
178
182
  device = get_device()
179
183
  _, pipeline = _get_pipeline(shader_code)
180
184
 
@@ -182,16 +186,16 @@ class GPUArray:
182
186
  current_size = self.size
183
187
 
184
188
  while current_size > 1:
185
- n_workgroups = math.ceil(current_size / WORKGROUP_SIZE)
189
+ n_workgroups = math.ceil(current_size / REDUCTION_ELEMENTS_PER_WORKGROUP)
186
190
  out_buf = device.create_buffer(
187
- size=n_workgroups * 4,
191
+ size=max(n_workgroups * 4, 4),
188
192
  usage=wgpu.BufferUsage.STORAGE | wgpu.BufferUsage.COPY_SRC | wgpu.BufferUsage.COPY_DST,
189
193
  )
190
194
  bind_group = device.create_bind_group(
191
195
  layout=pipeline.get_bind_group_layout(0),
192
196
  entries=[
193
197
  {"binding": 0, "resource": {"buffer": current_buf, "offset": 0, "size": current_size * 4}},
194
- {"binding": 1, "resource": {"buffer": out_buf, "offset": 0, "size": n_workgroups * 4}},
198
+ {"binding": 1, "resource": {"buffer": out_buf, "offset": 0, "size": max(n_workgroups * 4, 4)}},
195
199
  ],
196
200
  )
197
201
  encoder = device.create_command_encoder()
@@ -214,6 +218,29 @@ class GPUArray:
214
218
  current_buf.destroy()
215
219
  return result
216
220
 
221
+ def _run_binary_op_inplace(self, other: "GPUArray", shader_code: str) -> "GPUArray":
222
+ """Run a binary op that writes the result into self's buffer (no allocation)."""
223
+ assert self.size == other.size, f"Size mismatch: {self.size} vs {other.size}"
224
+ device = get_device()
225
+ _, pipeline = _get_pipeline(shader_code)
226
+
227
+ bind_group = device.create_bind_group(
228
+ layout=pipeline.get_bind_group_layout(0),
229
+ entries=[
230
+ {"binding": 0, "resource": {"buffer": self._buffer, "offset": 0, "size": self.size * 4}},
231
+ {"binding": 1, "resource": {"buffer": other._buffer, "offset": 0, "size": other.size * 4}},
232
+ ],
233
+ )
234
+ n_workgroups = math.ceil(self.size / WORKGROUP_SIZE)
235
+ encoder = device.create_command_encoder()
236
+ pass_enc = encoder.begin_compute_pass()
237
+ pass_enc.set_pipeline(pipeline)
238
+ pass_enc.set_bind_group(0, bind_group)
239
+ pass_enc.dispatch_workgroups(n_workgroups)
240
+ pass_enc.end()
241
+ device.queue.submit([encoder.finish()])
242
+ return self
243
+
217
244
  # ----- dot product (two-input reduction) -----
218
245
 
219
246
  def _run_dot_reduction(self, other: "GPUArray") -> float:
@@ -259,11 +286,71 @@ class GPUArray:
259
286
  current_buf.destroy()
260
287
  return result
261
288
 
289
+ # ----- broadcast helper -----
290
+
291
+ def _run_broadcast_op(self, other: "GPUArray", shader_code: str, out_shape: tuple[int, ...]) -> "GPUArray":
292
+ """Run a broadcast binary shader with a uniform buffer for column count."""
293
+ device = get_device()
294
+ _, pipeline = _get_pipeline(shader_code)
295
+ out_size = 1
296
+ for d in out_shape:
297
+ out_size *= d
298
+ out_buf = device.create_buffer(
299
+ size=out_size * 4,
300
+ usage=wgpu.BufferUsage.STORAGE | wgpu.BufferUsage.COPY_SRC | wgpu.BufferUsage.COPY_DST,
301
+ )
302
+ cols = out_shape[-1] if len(out_shape) >= 2 else out_shape[0]
303
+ dims_data = struct.pack("IIII", cols, 0, 0, 0)
304
+ dims_buf = device.create_buffer_with_data(
305
+ data=dims_data, usage=wgpu.BufferUsage.UNIFORM,
306
+ )
307
+ bind_group = device.create_bind_group(
308
+ layout=pipeline.get_bind_group_layout(0),
309
+ entries=[
310
+ {"binding": 0, "resource": {"buffer": self._buffer, "offset": 0, "size": self.size * 4}},
311
+ {"binding": 1, "resource": {"buffer": other._buffer, "offset": 0, "size": other.size * 4}},
312
+ {"binding": 2, "resource": {"buffer": out_buf, "offset": 0, "size": out_size * 4}},
313
+ {"binding": 3, "resource": {"buffer": dims_buf, "offset": 0, "size": 16}},
314
+ ],
315
+ )
316
+ n_workgroups = math.ceil(out_size / WORKGROUP_SIZE)
317
+ encoder = device.create_command_encoder()
318
+ pass_enc = encoder.begin_compute_pass()
319
+ pass_enc.set_pipeline(pipeline)
320
+ pass_enc.set_bind_group(0, bind_group)
321
+ pass_enc.dispatch_workgroups(n_workgroups)
322
+ pass_enc.end()
323
+ device.queue.submit([encoder.finish()])
324
+ dims_buf.destroy()
325
+ return GPUArray(out_buf, out_shape)
326
+
327
+ def _binary_with_broadcast(self, other: "GPUArray",
328
+ same_shader: str, row_shader: str, col_shader: str) -> "GPUArray":
329
+ """Dispatch to the right shader based on shape compatibility."""
330
+ if self.shape == other.shape or self.size == other.size:
331
+ return self._run_binary_op(other, same_shader)
332
+ # (M, N) op (N,) or (M, N) op (1, N) — broadcast row
333
+ if self.ndim == 2:
334
+ M, N = self.shape
335
+ if (other.ndim == 1 and other.shape[0] == N) or \
336
+ (other.ndim == 2 and other.shape == (1, N)):
337
+ b = other.flatten() if other.ndim == 2 else other
338
+ return self._run_broadcast_op(b, row_shader, self.shape)
339
+ # (M, N) op (M, 1) — broadcast col
340
+ if other.ndim == 2 and other.shape == (M, 1):
341
+ b_flat = other.flatten()
342
+ return self._run_broadcast_op(b_flat, col_shader, self.shape)
343
+ raise ValueError(f"Incompatible shapes for broadcast: {self.shape} vs {other.shape}")
344
+
262
345
  # ----- operator overloads -----
263
346
 
264
347
  def __add__(self, other):
265
348
  if isinstance(other, (int, float)):
266
349
  return self._run_scalar_op(float(other), shaders.ADD_SCALAR_SHADER)
350
+ if isinstance(other, GPUArray) and self.shape != other.shape:
351
+ return self._binary_with_broadcast(
352
+ other, shaders.ADD_SHADER,
353
+ shaders.BINARY_BROADCAST_ROW_ADD, shaders.BINARY_BROADCAST_COL_ADD)
267
354
  return self._run_binary_op(other, shaders.ADD_SHADER)
268
355
 
269
356
  def __radd__(self, other):
@@ -274,6 +361,10 @@ class GPUArray:
274
361
  def __sub__(self, other):
275
362
  if isinstance(other, (int, float)):
276
363
  return self._run_scalar_op(float(-other), shaders.ADD_SCALAR_SHADER)
364
+ if isinstance(other, GPUArray) and self.shape != other.shape:
365
+ return self._binary_with_broadcast(
366
+ other, shaders.SUB_SHADER,
367
+ shaders.BINARY_BROADCAST_ROW_SUB, shaders.BINARY_BROADCAST_COL_SUB)
277
368
  return self._run_binary_op(other, shaders.SUB_SHADER)
278
369
 
279
370
  def __rsub__(self, other):
@@ -285,6 +376,10 @@ class GPUArray:
285
376
  def __mul__(self, other):
286
377
  if isinstance(other, (int, float)):
287
378
  return self._run_scalar_op(float(other), shaders.MUL_SCALAR_SHADER)
379
+ if isinstance(other, GPUArray) and self.shape != other.shape:
380
+ return self._binary_with_broadcast(
381
+ other, shaders.MUL_SHADER,
382
+ shaders.BINARY_BROADCAST_ROW_MUL, shaders.BINARY_BROADCAST_COL_MUL)
288
383
  return self._run_binary_op(other, shaders.MUL_SHADER)
289
384
 
290
385
  def __rmul__(self, other):
@@ -295,8 +390,32 @@ class GPUArray:
295
390
  def __truediv__(self, other):
296
391
  if isinstance(other, (int, float)):
297
392
  return self._run_scalar_op(1.0 / float(other), shaders.MUL_SCALAR_SHADER)
393
+ if isinstance(other, GPUArray) and self.shape != other.shape:
394
+ return self._binary_with_broadcast(
395
+ other, shaders.DIV_SHADER,
396
+ shaders.BINARY_BROADCAST_ROW_DIV, shaders.BINARY_BROADCAST_COL_DIV)
298
397
  return self._run_binary_op(other, shaders.DIV_SHADER)
299
398
 
399
+ def __iadd__(self, other):
400
+ if isinstance(other, (int, float)):
401
+ return self._run_scalar_op(float(other), shaders.ADD_SCALAR_SHADER)
402
+ return self._run_binary_op_inplace(other, shaders.ADD_INPLACE_SHADER)
403
+
404
+ def __isub__(self, other):
405
+ if isinstance(other, (int, float)):
406
+ return self._run_scalar_op(float(-other), shaders.ADD_SCALAR_SHADER)
407
+ return self._run_binary_op_inplace(other, shaders.SUB_INPLACE_SHADER)
408
+
409
+ def __imul__(self, other):
410
+ if isinstance(other, (int, float)):
411
+ return self._run_scalar_op(float(other), shaders.MUL_SCALAR_SHADER)
412
+ return self._run_binary_op_inplace(other, shaders.MUL_INPLACE_SHADER)
413
+
414
+ def __itruediv__(self, other):
415
+ if isinstance(other, (int, float)):
416
+ return self._run_scalar_op(1.0 / float(other), shaders.MUL_SCALAR_SHADER)
417
+ return self._run_binary_op_inplace(other, shaders.DIV_INPLACE_SHADER)
418
+
300
419
  def __neg__(self):
301
420
  return self._run_unary_op(shaders.NEG_SHADER)
302
421
 
@@ -305,12 +424,108 @@ class GPUArray:
305
424
  return f"GPUArray({data}, shape={self._shape})"
306
425
 
307
426
  def __getitem__(self, key):
427
+ if isinstance(key, GPUArray):
428
+ return self._getitem_mask(key)
308
429
  data = self.to_numpy()
309
430
  result = data[key]
310
431
  if isinstance(result, np.ndarray):
311
- return _from_numpy(result)
432
+ return _from_numpy(result.copy())
312
433
  return float(result)
313
434
 
435
+ def _getitem_mask(self, mask: "GPUArray") -> "GPUArray":
436
+ """Boolean mask indexing via gather shader."""
437
+ mask_np = mask.to_numpy().flatten()
438
+ indices = np.where(mask_np > 0.5)[0].astype(np.uint32)
439
+ if len(indices) == 0:
440
+ return _from_numpy(np.array([], dtype=np.float32))
441
+ device = get_device()
442
+ _, pipeline = _get_pipeline(shaders.GATHER_SHADER)
443
+ idx_buf = device.create_buffer_with_data(
444
+ data=indices.tobytes(),
445
+ usage=wgpu.BufferUsage.STORAGE | wgpu.BufferUsage.COPY_SRC,
446
+ )
447
+ out_buf = device.create_buffer(
448
+ size=len(indices) * 4,
449
+ usage=wgpu.BufferUsage.STORAGE | wgpu.BufferUsage.COPY_SRC | wgpu.BufferUsage.COPY_DST,
450
+ )
451
+ bind_group = device.create_bind_group(
452
+ layout=pipeline.get_bind_group_layout(0),
453
+ entries=[
454
+ {"binding": 0, "resource": {"buffer": self._buffer, "offset": 0, "size": self.size * 4}},
455
+ {"binding": 1, "resource": {"buffer": idx_buf, "offset": 0, "size": len(indices) * 4}},
456
+ {"binding": 2, "resource": {"buffer": out_buf, "offset": 0, "size": len(indices) * 4}},
457
+ ],
458
+ )
459
+ n_workgroups = math.ceil(len(indices) / WORKGROUP_SIZE)
460
+ encoder = device.create_command_encoder()
461
+ pass_enc = encoder.begin_compute_pass()
462
+ pass_enc.set_pipeline(pipeline)
463
+ pass_enc.set_bind_group(0, bind_group)
464
+ pass_enc.dispatch_workgroups(n_workgroups)
465
+ pass_enc.end()
466
+ device.queue.submit([encoder.finish()])
467
+ idx_buf.destroy()
468
+ return GPUArray(out_buf, (len(indices),))
469
+
470
+ def __setitem__(self, key, value):
471
+ if isinstance(key, GPUArray):
472
+ self._setitem_mask(key, value)
473
+ return
474
+ device = get_device()
475
+ if isinstance(key, (int, np.integer)):
476
+ idx = int(key)
477
+ if idx < 0:
478
+ idx += self.size
479
+ device.queue.write_buffer(self._buffer, idx * 4, struct.pack("f", float(value)))
480
+ elif isinstance(key, slice):
481
+ if self.ndim == 1:
482
+ indices = range(*key.indices(self._shape[0]))
483
+ if isinstance(value, (int, float)):
484
+ data = struct.pack("f", float(value)) * len(indices)
485
+ elif isinstance(value, GPUArray):
486
+ data = value.to_numpy().astype(np.float32).tobytes()
487
+ else:
488
+ data = np.asarray(value, dtype=np.float32).tobytes()
489
+ start = indices.start if len(indices) > 0 else 0
490
+ device.queue.write_buffer(self._buffer, start * 4, data)
491
+ else:
492
+ indices = range(*key.indices(self._shape[0]))
493
+ cols = self._shape[1]
494
+ if isinstance(value, (int, float)):
495
+ row_data = struct.pack("f", float(value)) * cols
496
+ for r in indices:
497
+ device.queue.write_buffer(self._buffer, r * cols * 4, row_data)
498
+ else:
499
+ val_np = value.to_numpy() if isinstance(value, GPUArray) else np.asarray(value, dtype=np.float32)
500
+ for i, r in enumerate(indices):
501
+ row_bytes = (val_np[i] if val_np.ndim >= 2 else val_np).astype(np.float32).tobytes()
502
+ device.queue.write_buffer(self._buffer, r * cols * 4, row_bytes)
503
+ elif isinstance(key, tuple) and len(key) == 2:
504
+ row, col = key
505
+ if isinstance(row, (int, np.integer)) and isinstance(col, (int, np.integer)):
506
+ cols = self._shape[1]
507
+ offset = (int(row) * cols + int(col)) * 4
508
+ device.queue.write_buffer(self._buffer, offset, struct.pack("f", float(value)))
509
+ else:
510
+ arr = self.to_numpy()
511
+ arr[key] = value if not isinstance(value, GPUArray) else value.to_numpy()
512
+ device.queue.write_buffer(self._buffer, 0, np.ascontiguousarray(arr, dtype=np.float32).tobytes())
513
+ else:
514
+ arr = self.to_numpy()
515
+ arr[key] = value if not isinstance(value, GPUArray) else value.to_numpy()
516
+ device.queue.write_buffer(self._buffer, 0, np.ascontiguousarray(arr, dtype=np.float32).tobytes())
517
+
518
+ def _setitem_mask(self, mask: "GPUArray", value):
519
+ """Masked assignment: a[mask] = value."""
520
+ data = self.to_numpy()
521
+ mask_np = mask.to_numpy() > 0.5
522
+ if isinstance(value, GPUArray):
523
+ data[mask_np] = value.to_numpy().flatten()[:mask_np.sum()]
524
+ else:
525
+ data[mask_np] = float(value)
526
+ device = get_device()
527
+ device.queue.write_buffer(self._buffer, 0, np.ascontiguousarray(data, dtype=np.float32).tobytes())
528
+
314
529
  # ----- reduction methods -----
315
530
 
316
531
  def sum(self) -> float:
@@ -443,6 +658,224 @@ class GPUArray:
443
658
  return self._shape[0]
444
659
 
445
660
 
661
+ # ----- FFT methods -----
662
+
663
+ def fft(self) -> "GPUArray":
664
+ """1D FFT. Input is real or complex (interleaved). Returns complex GPUArray (interleaved)."""
665
+ return _fft_impl(self, inverse=False)
666
+
667
+ def ifft(self) -> "GPUArray":
668
+ """1D inverse FFT. Input is complex interleaved. Returns complex GPUArray."""
669
+ return _fft_impl(self, inverse=True)
670
+
671
+ def fft_magnitude(self) -> "GPUArray":
672
+ """Compute magnitude spectrum |FFT(x)|. Returns real GPUArray."""
673
+ fft_result = self.fft()
674
+ return _complex_magnitude(fft_result)
675
+
676
+ def fft_phase(self) -> "GPUArray":
677
+ """Compute phase spectrum angle(FFT(x)). Returns real GPUArray."""
678
+ fft_result = self.fft()
679
+ return _complex_phase(fft_result)
680
+
681
+
682
+ def _next_power_of_2(n: int) -> int:
683
+ """Return the smallest power of 2 >= n."""
684
+ if n <= 1:
685
+ return 1
686
+ p = 1
687
+ while p < n:
688
+ p <<= 1
689
+ return p
690
+
691
+
692
+ def _fft_impl(arr: GPUArray, inverse: bool = False) -> GPUArray:
693
+ """Cooley-Tukey radix-2 FFT/IFFT on GPU."""
694
+ device = get_device()
695
+
696
+ # Determine if input is real or complex (interleaved)
697
+ # Real: 1D array of N floats -> N-point FFT
698
+ # Complex interleaved: 1D array of 2*N floats -> N-point FFT
699
+ # We treat it as real if ndim==1 and size is the number of points
700
+ # For simplicity: if the array was produced by a previous FFT (even size),
701
+ # the caller should know. We'll check: if doing IFFT, input must be complex.
702
+
703
+ flat = arr.to_numpy().flatten()
704
+
705
+ if inverse:
706
+ # Input must be complex interleaved (even number of floats)
707
+ assert len(flat) % 2 == 0, "IFFT input must be complex interleaved (even length)"
708
+ N_complex = len(flat) // 2
709
+ else:
710
+ # Assume real input -> convert to complex
711
+ N_complex = len(flat)
712
+
713
+ # Pad to next power of 2
714
+ N = _next_power_of_2(N_complex)
715
+ log2N = int(math.log2(N))
716
+
717
+ if inverse:
718
+ # Pad complex data
719
+ if N > N_complex:
720
+ padded = np.zeros(2 * N, dtype=np.float32)
721
+ padded[:len(flat)] = flat
722
+ else:
723
+ padded = flat.copy()
724
+ complex_data = padded
725
+ else:
726
+ # Convert real to complex interleaved
727
+ padded_real = np.zeros(N, dtype=np.float32)
728
+ padded_real[:len(flat)] = flat
729
+ complex_data = np.zeros(2 * N, dtype=np.float32)
730
+ complex_data[0::2] = padded_real # real parts
731
+ # imaginary parts are already 0
732
+
733
+ # Upload complex data to GPU
734
+ input_buf = device.create_buffer_with_data(
735
+ data=complex_data.tobytes(),
736
+ usage=wgpu.BufferUsage.STORAGE | wgpu.BufferUsage.COPY_SRC | wgpu.BufferUsage.COPY_DST,
737
+ )
738
+
739
+ # Step 1: Bit-reversal permutation
740
+ output_buf = device.create_buffer(
741
+ size=2 * N * 4,
742
+ usage=wgpu.BufferUsage.STORAGE | wgpu.BufferUsage.COPY_SRC | wgpu.BufferUsage.COPY_DST,
743
+ )
744
+
745
+ _, br_pipeline = _get_pipeline(shaders.FFT_BIT_REVERSE_SHADER)
746
+ params_data = struct.pack("IIII", N, log2N, 0, 0)
747
+ params_buf = device.create_buffer_with_data(
748
+ data=params_data, usage=wgpu.BufferUsage.UNIFORM,
749
+ )
750
+ bind_group = device.create_bind_group(
751
+ layout=br_pipeline.get_bind_group_layout(0),
752
+ entries=[
753
+ {"binding": 0, "resource": {"buffer": input_buf, "offset": 0, "size": 2 * N * 4}},
754
+ {"binding": 1, "resource": {"buffer": output_buf, "offset": 0, "size": 2 * N * 4}},
755
+ {"binding": 2, "resource": {"buffer": params_buf, "offset": 0, "size": 16}},
756
+ ],
757
+ )
758
+ n_workgroups = math.ceil(N / WORKGROUP_SIZE)
759
+ encoder = device.create_command_encoder()
760
+ pass_enc = encoder.begin_compute_pass()
761
+ pass_enc.set_pipeline(br_pipeline)
762
+ pass_enc.set_bind_group(0, bind_group)
763
+ pass_enc.dispatch_workgroups(n_workgroups)
764
+ pass_enc.end()
765
+ device.queue.submit([encoder.finish()])
766
+ input_buf.destroy()
767
+ params_buf.destroy()
768
+
769
+ # Step 2: Butterfly stages
770
+ butterfly_shader = shaders.IFFT_BUTTERFLY_SHADER if inverse else shaders.FFT_BUTTERFLY_SHADER
771
+ _, bf_pipeline = _get_pipeline(butterfly_shader)
772
+
773
+ for stage in range(log2N):
774
+ params_data = struct.pack("IIII", N, stage, 0, 0)
775
+ params_buf = device.create_buffer_with_data(
776
+ data=params_data, usage=wgpu.BufferUsage.UNIFORM,
777
+ )
778
+ bind_group = device.create_bind_group(
779
+ layout=bf_pipeline.get_bind_group_layout(0),
780
+ entries=[
781
+ {"binding": 0, "resource": {"buffer": output_buf, "offset": 0, "size": 2 * N * 4}},
782
+ {"binding": 1, "resource": {"buffer": params_buf, "offset": 0, "size": 16}},
783
+ ],
784
+ )
785
+ n_workgroups_bf = math.ceil(N // 2 / WORKGROUP_SIZE)
786
+ if n_workgroups_bf == 0:
787
+ n_workgroups_bf = 1
788
+ encoder = device.create_command_encoder()
789
+ pass_enc = encoder.begin_compute_pass()
790
+ pass_enc.set_pipeline(bf_pipeline)
791
+ pass_enc.set_bind_group(0, bind_group)
792
+ pass_enc.dispatch_workgroups(n_workgroups_bf)
793
+ pass_enc.end()
794
+ device.queue.submit([encoder.finish()])
795
+ params_buf.destroy()
796
+
797
+ # Step 3: For IFFT, scale by 1/N
798
+ if inverse:
799
+ _, scale_pipeline = _get_pipeline(shaders.FFT_SCALE_SHADER)
800
+ scale_val = 1.0 / float(N)
801
+ params_data = struct.pack("f", scale_val) + b"\x00" * 12
802
+ params_buf = device.create_buffer_with_data(
803
+ data=params_data, usage=wgpu.BufferUsage.UNIFORM,
804
+ )
805
+ bind_group = device.create_bind_group(
806
+ layout=scale_pipeline.get_bind_group_layout(0),
807
+ entries=[
808
+ {"binding": 0, "resource": {"buffer": output_buf, "offset": 0, "size": 2 * N * 4}},
809
+ {"binding": 1, "resource": {"buffer": params_buf, "offset": 0, "size": 16}},
810
+ ],
811
+ )
812
+ n_workgroups_sc = math.ceil(2 * N / WORKGROUP_SIZE)
813
+ encoder = device.create_command_encoder()
814
+ pass_enc = encoder.begin_compute_pass()
815
+ pass_enc.set_pipeline(scale_pipeline)
816
+ pass_enc.set_bind_group(0, bind_group)
817
+ pass_enc.dispatch_workgroups(n_workgroups_sc)
818
+ pass_enc.end()
819
+ device.queue.submit([encoder.finish()])
820
+ params_buf.destroy()
821
+
822
+ return GPUArray(output_buf, (2 * N,))
823
+
824
+
825
+ def _complex_magnitude(arr: GPUArray) -> GPUArray:
826
+ """Compute magnitude of complex interleaved array."""
827
+ device = get_device()
828
+ N = arr.size // 2
829
+ _, pipeline = _get_pipeline(shaders.COMPLEX_MAGNITUDE_SHADER)
830
+ out_buf = device.create_buffer(
831
+ size=N * 4,
832
+ usage=wgpu.BufferUsage.STORAGE | wgpu.BufferUsage.COPY_SRC | wgpu.BufferUsage.COPY_DST,
833
+ )
834
+ bind_group = device.create_bind_group(
835
+ layout=pipeline.get_bind_group_layout(0),
836
+ entries=[
837
+ {"binding": 0, "resource": {"buffer": arr._buffer, "offset": 0, "size": arr.size * 4}},
838
+ {"binding": 1, "resource": {"buffer": out_buf, "offset": 0, "size": N * 4}},
839
+ ],
840
+ )
841
+ n_workgroups = math.ceil(N / WORKGROUP_SIZE)
842
+ encoder = device.create_command_encoder()
843
+ pass_enc = encoder.begin_compute_pass()
844
+ pass_enc.set_pipeline(pipeline)
845
+ pass_enc.set_bind_group(0, bind_group)
846
+ pass_enc.dispatch_workgroups(n_workgroups)
847
+ pass_enc.end()
848
+ device.queue.submit([encoder.finish()])
849
+ return GPUArray(out_buf, (N,))
850
+
851
+
852
+ def _complex_phase(arr: GPUArray) -> GPUArray:
853
+ """Compute phase of complex interleaved array."""
854
+ device = get_device()
855
+ N = arr.size // 2
856
+ _, pipeline = _get_pipeline(shaders.COMPLEX_PHASE_SHADER)
857
+ out_buf = device.create_buffer(
858
+ size=N * 4,
859
+ usage=wgpu.BufferUsage.STORAGE | wgpu.BufferUsage.COPY_SRC | wgpu.BufferUsage.COPY_DST,
860
+ )
861
+ bind_group = device.create_bind_group(
862
+ layout=pipeline.get_bind_group_layout(0),
863
+ entries=[
864
+ {"binding": 0, "resource": {"buffer": arr._buffer, "offset": 0, "size": arr.size * 4}},
865
+ {"binding": 1, "resource": {"buffer": out_buf, "offset": 0, "size": N * 4}},
866
+ ],
867
+ )
868
+ n_workgroups = math.ceil(N / WORKGROUP_SIZE)
869
+ encoder = device.create_command_encoder()
870
+ pass_enc = encoder.begin_compute_pass()
871
+ pass_enc.set_pipeline(pipeline)
872
+ pass_enc.set_bind_group(0, bind_group)
873
+ pass_enc.dispatch_workgroups(n_workgroups)
874
+ pass_enc.end()
875
+ device.queue.submit([encoder.finish()])
876
+ return GPUArray(out_buf, (N,))
877
+
878
+
446
879
  def _from_numpy(data: np.ndarray) -> GPUArray:
447
880
  """Create a GPUArray from a numpy array."""
448
881
  device = get_device()
@@ -455,7 +888,10 @@ def _from_numpy(data: np.ndarray) -> GPUArray:
455
888
 
456
889
 
457
890
  def matmul(a: GPUArray, b: GPUArray) -> GPUArray:
458
- """Matrix multiplication C = A @ B."""
891
+ """Matrix multiplication C = A @ B.
892
+
893
+ Uses tiled shared-memory shader for matrices >= 64x64, naive for smaller.
894
+ """
459
895
  if a.ndim != 2 or b.ndim != 2:
460
896
  raise ValueError("matmul requires 2D arrays")
461
897
  M, K = a.shape
@@ -464,7 +900,11 @@ def matmul(a: GPUArray, b: GPUArray) -> GPUArray:
464
900
  raise ValueError(f"Incompatible shapes for matmul: {a.shape} and {b.shape}")
465
901
 
466
902
  device = get_device()
467
- _, pipeline = _get_pipeline(shaders.MATMUL_SHADER)
903
+
904
+ # Choose tiled or naive shader based on matrix size
905
+ use_tiled = M >= 64 and N >= 64 and K >= 64
906
+ shader_code = shaders.MATMUL_TILED if use_tiled else shaders.MATMUL_NAIVE
907
+ _, pipeline = _get_pipeline(shader_code)
468
908
 
469
909
  out_size = M * N
470
910
  out_buf = device.create_buffer(
@@ -489,13 +929,27 @@ def matmul(a: GPUArray, b: GPUArray) -> GPUArray:
489
929
  ],
490
930
  )
491
931
 
492
- n_workgroups = math.ceil(out_size / WORKGROUP_SIZE)
493
- encoder = device.create_command_encoder()
494
- pass_enc = encoder.begin_compute_pass()
495
- pass_enc.set_pipeline(pipeline)
496
- pass_enc.set_bind_group(0, bind_group)
497
- pass_enc.dispatch_workgroups(n_workgroups)
498
- pass_enc.end()
932
+ if use_tiled:
933
+ # Tiled shader uses @workgroup_size(16, 16); dispatch 2D grid
934
+ TILE_SIZE = 16
935
+ wg_x = math.ceil(N / TILE_SIZE)
936
+ wg_y = math.ceil(M / TILE_SIZE)
937
+ encoder = device.create_command_encoder()
938
+ pass_enc = encoder.begin_compute_pass()
939
+ pass_enc.set_pipeline(pipeline)
940
+ pass_enc.set_bind_group(0, bind_group)
941
+ pass_enc.dispatch_workgroups(wg_x, wg_y)
942
+ pass_enc.end()
943
+ else:
944
+ # Naive shader uses @workgroup_size(256); dispatch 1D
945
+ n_workgroups = math.ceil(out_size / WORKGROUP_SIZE)
946
+ encoder = device.create_command_encoder()
947
+ pass_enc = encoder.begin_compute_pass()
948
+ pass_enc.set_pipeline(pipeline)
949
+ pass_enc.set_bind_group(0, bind_group)
950
+ pass_enc.dispatch_workgroups(n_workgroups)
951
+ pass_enc.end()
952
+
499
953
  device.queue.submit([encoder.finish()])
500
954
  dims_buf.destroy()
501
955
  return GPUArray(out_buf, (M, N))
gpuarray/shaders.py CHANGED
@@ -74,6 +74,62 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
74
74
  }
75
75
  """
76
76
 
77
+ # ---------------------------------------------------------------------------
78
+ # In-place elementwise binary operations (result written to a)
79
+ # ---------------------------------------------------------------------------
80
+
81
+ ADD_INPLACE_SHADER = """
82
+ @group(0) @binding(0) var<storage, read_write> a: array<f32>;
83
+ @group(0) @binding(1) var<storage, read> b: array<f32>;
84
+
85
+ @compute @workgroup_size(256)
86
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
87
+ let i = gid.x;
88
+ if (i < arrayLength(&a)) {
89
+ a[i] = a[i] + b[i];
90
+ }
91
+ }
92
+ """
93
+
94
+ SUB_INPLACE_SHADER = """
95
+ @group(0) @binding(0) var<storage, read_write> a: array<f32>;
96
+ @group(0) @binding(1) var<storage, read> b: array<f32>;
97
+
98
+ @compute @workgroup_size(256)
99
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
100
+ let i = gid.x;
101
+ if (i < arrayLength(&a)) {
102
+ a[i] = a[i] - b[i];
103
+ }
104
+ }
105
+ """
106
+
107
+ MUL_INPLACE_SHADER = """
108
+ @group(0) @binding(0) var<storage, read_write> a: array<f32>;
109
+ @group(0) @binding(1) var<storage, read> b: array<f32>;
110
+
111
+ @compute @workgroup_size(256)
112
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
113
+ let i = gid.x;
114
+ if (i < arrayLength(&a)) {
115
+ a[i] = a[i] * b[i];
116
+ }
117
+ }
118
+ """
119
+
120
+ DIV_INPLACE_SHADER = """
121
+ @group(0) @binding(0) var<storage, read_write> a: array<f32>;
122
+ @group(0) @binding(1) var<storage, read> b: array<f32>;
123
+
124
+ @compute @workgroup_size(256)
125
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
126
+ let i = gid.x;
127
+ if (i < arrayLength(&a)) {
128
+ a[i] = a[i] / b[i];
129
+ }
130
+ }
131
+ """
132
+
77
133
  # ---------------------------------------------------------------------------
78
134
  # Elementwise unary operations
79
135
  # ---------------------------------------------------------------------------
@@ -236,17 +292,26 @@ var<workgroup> sdata: array<f32, 256>;
236
292
  fn main(
237
293
  @builtin(local_invocation_id) lid: vec3<u32>,
238
294
  @builtin(workgroup_id) wid: vec3<u32>,
239
- @builtin(global_invocation_id) gid: vec3<u32>,
240
295
  ) {
241
296
  let local_idx = lid.x;
242
- let global_idx = gid.x;
243
297
  let n = arrayLength(&input);
298
+ // Each thread loads and reduces 4 elements (workgroup covers 1024 elements)
299
+ let base = wid.x * 1024u + local_idx;
244
300
 
245
- if (global_idx < n) {
246
- sdata[local_idx] = input[global_idx];
247
- } else {
248
- sdata[local_idx] = 0.0;
301
+ var acc: f32 = 0.0;
302
+ if (base < n) {
303
+ acc = input[base];
249
304
  }
305
+ if (base + 256u < n) {
306
+ acc = acc + input[base + 256u];
307
+ }
308
+ if (base + 512u < n) {
309
+ acc = acc + input[base + 512u];
310
+ }
311
+ if (base + 768u < n) {
312
+ acc = acc + input[base + 768u];
313
+ }
314
+ sdata[local_idx] = acc;
250
315
  workgroupBarrier();
251
316
 
252
317
  // Tree reduction
@@ -276,17 +341,25 @@ var<workgroup> sdata: array<f32, 256>;
276
341
  fn main(
277
342
  @builtin(local_invocation_id) lid: vec3<u32>,
278
343
  @builtin(workgroup_id) wid: vec3<u32>,
279
- @builtin(global_invocation_id) gid: vec3<u32>,
280
344
  ) {
281
345
  let local_idx = lid.x;
282
- let global_idx = gid.x;
283
346
  let n = arrayLength(&input);
347
+ let base = wid.x * 1024u + local_idx;
284
348
 
285
- if (global_idx < n) {
286
- sdata[local_idx] = input[global_idx];
287
- } else {
288
- sdata[local_idx] = -3.402823e+38; // -FLT_MAX
349
+ var acc: f32 = -3.402823e+38;
350
+ if (base < n) {
351
+ acc = input[base];
352
+ }
353
+ if (base + 256u < n) {
354
+ acc = max(acc, input[base + 256u]);
355
+ }
356
+ if (base + 512u < n) {
357
+ acc = max(acc, input[base + 512u]);
289
358
  }
359
+ if (base + 768u < n) {
360
+ acc = max(acc, input[base + 768u]);
361
+ }
362
+ sdata[local_idx] = acc;
290
363
  workgroupBarrier();
291
364
 
292
365
  var stride: u32 = 128u;
@@ -315,17 +388,25 @@ var<workgroup> sdata: array<f32, 256>;
315
388
  fn main(
316
389
  @builtin(local_invocation_id) lid: vec3<u32>,
317
390
  @builtin(workgroup_id) wid: vec3<u32>,
318
- @builtin(global_invocation_id) gid: vec3<u32>,
319
391
  ) {
320
392
  let local_idx = lid.x;
321
- let global_idx = gid.x;
322
393
  let n = arrayLength(&input);
394
+ let base = wid.x * 1024u + local_idx;
323
395
 
324
- if (global_idx < n) {
325
- sdata[local_idx] = input[global_idx];
326
- } else {
327
- sdata[local_idx] = 3.402823e+38; // FLT_MAX
396
+ var acc: f32 = 3.402823e+38;
397
+ if (base < n) {
398
+ acc = input[base];
328
399
  }
400
+ if (base + 256u < n) {
401
+ acc = min(acc, input[base + 256u]);
402
+ }
403
+ if (base + 512u < n) {
404
+ acc = min(acc, input[base + 512u]);
405
+ }
406
+ if (base + 768u < n) {
407
+ acc = min(acc, input[base + 768u]);
408
+ }
409
+ sdata[local_idx] = acc;
329
410
  workgroupBarrier();
330
411
 
331
412
  var stride: u32 = 128u;
@@ -394,7 +475,7 @@ fn main(
394
475
  # Each thread computes one element of C.
395
476
  # ---------------------------------------------------------------------------
396
477
 
397
- MATMUL_SHADER = """
478
+ MATMUL_NAIVE = """
398
479
  struct Dims {
399
480
  M: u32,
400
481
  K: u32,
@@ -425,6 +506,74 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
425
506
  }
426
507
  """
427
508
 
509
+ # Keep backward compat alias
510
+ MATMUL_SHADER = MATMUL_NAIVE
511
+
512
+ # ---------------------------------------------------------------------------
513
+ # Tiled matrix multiplication (16x16 tiles with shared memory)
514
+ # ---------------------------------------------------------------------------
515
+
516
+ MATMUL_TILED = """
517
+ struct Dims {
518
+ M: u32,
519
+ K: u32,
520
+ N: u32,
521
+ _pad: u32,
522
+ }
523
+
524
+ @group(0) @binding(0) var<storage, read> a: array<f32>;
525
+ @group(0) @binding(1) var<storage, read> b: array<f32>;
526
+ @group(0) @binding(2) var<storage, read_write> c: array<f32>;
527
+ @group(0) @binding(3) var<uniform> dims: Dims;
528
+
529
+ const TILE_SIZE = 16u;
530
+ var<workgroup> tile_a: array<f32, 256>;
531
+ var<workgroup> tile_b: array<f32, 256>;
532
+
533
+ @compute @workgroup_size(16, 16)
534
+ fn main(@builtin(local_invocation_id) lid: vec3<u32>,
535
+ @builtin(workgroup_id) wid: vec3<u32>) {
536
+ let row = wid.y * TILE_SIZE + lid.y;
537
+ let col = wid.x * TILE_SIZE + lid.x;
538
+ let local_idx = lid.y * TILE_SIZE + lid.x;
539
+
540
+ let num_tiles = (dims.K + TILE_SIZE - 1u) / TILE_SIZE;
541
+
542
+ var sum: f32 = 0.0;
543
+
544
+ for (var t: u32 = 0u; t < num_tiles; t = t + 1u) {
545
+ // Load tile of A into shared memory
546
+ let a_col = t * TILE_SIZE + lid.x;
547
+ if (row < dims.M && a_col < dims.K) {
548
+ tile_a[local_idx] = a[row * dims.K + a_col];
549
+ } else {
550
+ tile_a[local_idx] = 0.0;
551
+ }
552
+
553
+ // Load tile of B into shared memory
554
+ let b_row = t * TILE_SIZE + lid.y;
555
+ if (b_row < dims.K && col < dims.N) {
556
+ tile_b[local_idx] = b[b_row * dims.N + col];
557
+ } else {
558
+ tile_b[local_idx] = 0.0;
559
+ }
560
+
561
+ workgroupBarrier();
562
+
563
+ // Compute partial dot product for this tile
564
+ for (var k: u32 = 0u; k < TILE_SIZE; k = k + 1u) {
565
+ sum = sum + tile_a[lid.y * TILE_SIZE + k] * tile_b[k * TILE_SIZE + lid.x];
566
+ }
567
+
568
+ workgroupBarrier();
569
+ }
570
+
571
+ if (row < dims.M && col < dims.N) {
572
+ c[row * dims.N + col] = sum;
573
+ }
574
+ }
575
+ """
576
+
428
577
  # ---------------------------------------------------------------------------
429
578
  # Clamp (requires uniform buffer with min_val, max_val)
430
579
  # ---------------------------------------------------------------------------
@@ -598,3 +747,342 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
598
747
  }
599
748
  }
600
749
  """
750
+
751
+ # ---------------------------------------------------------------------------
752
+ # Gather: result[i] = source[indices[i]]
753
+ # ---------------------------------------------------------------------------
754
+
755
+ GATHER_SHADER = """
756
+ @group(0) @binding(0) var<storage, read> source: array<f32>;
757
+ @group(0) @binding(1) var<storage, read> indices: array<u32>;
758
+ @group(0) @binding(2) var<storage, read_write> result: array<f32>;
759
+
760
+ @compute @workgroup_size(256)
761
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
762
+ let i = gid.x;
763
+ if (i < arrayLength(&indices)) {
764
+ result[i] = source[indices[i]];
765
+ }
766
+ }
767
+ """
768
+
769
+ # ---------------------------------------------------------------------------
770
+ # Broadcast binary ops: (M,N) op (N,) — broadcast 1D across rows
771
+ # Uniform: dims.x = N (number of columns)
772
+ # ---------------------------------------------------------------------------
773
+
774
+ BINARY_BROADCAST_ROW_ADD = """
775
+ struct Dims { cols: u32, _p1: u32, _p2: u32, _p3: u32, }
776
+ @group(0) @binding(0) var<storage, read> a: array<f32>;
777
+ @group(0) @binding(1) var<storage, read> b: array<f32>;
778
+ @group(0) @binding(2) var<storage, read_write> result: array<f32>;
779
+ @group(0) @binding(3) var<uniform> dims: Dims;
780
+ @compute @workgroup_size(256)
781
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
782
+ let i = gid.x;
783
+ if (i < arrayLength(&a)) { result[i] = a[i] + b[i % dims.cols]; }
784
+ }
785
+ """
786
+
787
+ BINARY_BROADCAST_ROW_SUB = """
788
+ struct Dims { cols: u32, _p1: u32, _p2: u32, _p3: u32, }
789
+ @group(0) @binding(0) var<storage, read> a: array<f32>;
790
+ @group(0) @binding(1) var<storage, read> b: array<f32>;
791
+ @group(0) @binding(2) var<storage, read_write> result: array<f32>;
792
+ @group(0) @binding(3) var<uniform> dims: Dims;
793
+ @compute @workgroup_size(256)
794
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
795
+ let i = gid.x;
796
+ if (i < arrayLength(&a)) { result[i] = a[i] - b[i % dims.cols]; }
797
+ }
798
+ """
799
+
800
+ BINARY_BROADCAST_ROW_MUL = """
801
+ struct Dims { cols: u32, _p1: u32, _p2: u32, _p3: u32, }
802
+ @group(0) @binding(0) var<storage, read> a: array<f32>;
803
+ @group(0) @binding(1) var<storage, read> b: array<f32>;
804
+ @group(0) @binding(2) var<storage, read_write> result: array<f32>;
805
+ @group(0) @binding(3) var<uniform> dims: Dims;
806
+ @compute @workgroup_size(256)
807
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
808
+ let i = gid.x;
809
+ if (i < arrayLength(&a)) { result[i] = a[i] * b[i % dims.cols]; }
810
+ }
811
+ """
812
+
813
+ BINARY_BROADCAST_ROW_DIV = """
814
+ struct Dims { cols: u32, _p1: u32, _p2: u32, _p3: u32, }
815
+ @group(0) @binding(0) var<storage, read> a: array<f32>;
816
+ @group(0) @binding(1) var<storage, read> b: array<f32>;
817
+ @group(0) @binding(2) var<storage, read_write> result: array<f32>;
818
+ @group(0) @binding(3) var<uniform> dims: Dims;
819
+ @compute @workgroup_size(256)
820
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
821
+ let i = gid.x;
822
+ if (i < arrayLength(&a)) { result[i] = a[i] / b[i % dims.cols]; }
823
+ }
824
+ """
825
+
826
+ # ---------------------------------------------------------------------------
827
+ # Broadcast binary ops: (M,N) op (M,1) — broadcast column vector
828
+ # Uniform: dims.x = N (number of columns)
829
+ # ---------------------------------------------------------------------------
830
+
831
+ BINARY_BROADCAST_COL_ADD = """
832
+ struct Dims { cols: u32, _p1: u32, _p2: u32, _p3: u32, }
833
+ @group(0) @binding(0) var<storage, read> a: array<f32>;
834
+ @group(0) @binding(1) var<storage, read> b: array<f32>;
835
+ @group(0) @binding(2) var<storage, read_write> result: array<f32>;
836
+ @group(0) @binding(3) var<uniform> dims: Dims;
837
+ @compute @workgroup_size(256)
838
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
839
+ let i = gid.x;
840
+ if (i < arrayLength(&a)) { result[i] = a[i] + b[i / dims.cols]; }
841
+ }
842
+ """
843
+
844
+ BINARY_BROADCAST_COL_SUB = """
845
+ struct Dims { cols: u32, _p1: u32, _p2: u32, _p3: u32, }
846
+ @group(0) @binding(0) var<storage, read> a: array<f32>;
847
+ @group(0) @binding(1) var<storage, read> b: array<f32>;
848
+ @group(0) @binding(2) var<storage, read_write> result: array<f32>;
849
+ @group(0) @binding(3) var<uniform> dims: Dims;
850
+ @compute @workgroup_size(256)
851
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
852
+ let i = gid.x;
853
+ if (i < arrayLength(&a)) { result[i] = a[i] - b[i / dims.cols]; }
854
+ }
855
+ """
856
+
857
+ BINARY_BROADCAST_COL_MUL = """
858
+ struct Dims { cols: u32, _p1: u32, _p2: u32, _p3: u32, }
859
+ @group(0) @binding(0) var<storage, read> a: array<f32>;
860
+ @group(0) @binding(1) var<storage, read> b: array<f32>;
861
+ @group(0) @binding(2) var<storage, read_write> result: array<f32>;
862
+ @group(0) @binding(3) var<uniform> dims: Dims;
863
+ @compute @workgroup_size(256)
864
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
865
+ let i = gid.x;
866
+ if (i < arrayLength(&a)) { result[i] = a[i] * b[i / dims.cols]; }
867
+ }
868
+ """
869
+
870
+ BINARY_BROADCAST_COL_DIV = """
871
+ struct Dims { cols: u32, _p1: u32, _p2: u32, _p3: u32, }
872
+ @group(0) @binding(0) var<storage, read> a: array<f32>;
873
+ @group(0) @binding(1) var<storage, read> b: array<f32>;
874
+ @group(0) @binding(2) var<storage, read_write> result: array<f32>;
875
+ @group(0) @binding(3) var<uniform> dims: Dims;
876
+ @compute @workgroup_size(256)
877
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
878
+ let i = gid.x;
879
+ if (i < arrayLength(&a)) { result[i] = a[i] / b[i / dims.cols]; }
880
+ }
881
+ """
882
+
883
+ # ---------------------------------------------------------------------------
884
+ # FFT shaders (Cooley-Tukey radix-2)
885
+ # Complex numbers stored as interleaved float32 pairs: [re0, im0, re1, im1, ...]
886
+ # ---------------------------------------------------------------------------
887
+
888
+ REAL_TO_COMPLEX_SHADER = """
889
+ @group(0) @binding(0) var<storage, read> input: array<f32>;
890
+ @group(0) @binding(1) var<storage, read_write> output: array<f32>;
891
+
892
+ @compute @workgroup_size(256)
893
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
894
+ let i = gid.x;
895
+ let n = arrayLength(&input);
896
+ if (i < n) {
897
+ output[i * 2u] = input[i];
898
+ output[i * 2u + 1u] = 0.0;
899
+ }
900
+ }
901
+ """
902
+
903
+ FFT_BIT_REVERSE_SHADER = """
904
+ struct Params {
905
+ N: u32,
906
+ log2N: u32,
907
+ _pad1: u32,
908
+ _pad2: u32,
909
+ }
910
+
911
+ @group(0) @binding(0) var<storage, read> input: array<f32>;
912
+ @group(0) @binding(1) var<storage, read_write> output: array<f32>;
913
+ @group(0) @binding(2) var<uniform> params: Params;
914
+
915
+ fn bit_reverse(x: u32, bits: u32) -> u32 {
916
+ var v = x;
917
+ var r: u32 = 0u;
918
+ for (var i: u32 = 0u; i < bits; i = i + 1u) {
919
+ r = (r << 1u) | (v & 1u);
920
+ v = v >> 1u;
921
+ }
922
+ return r;
923
+ }
924
+
925
+ @compute @workgroup_size(256)
926
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
927
+ let i = gid.x;
928
+ if (i >= params.N) {
929
+ return;
930
+ }
931
+ let j = bit_reverse(i, params.log2N);
932
+ output[j * 2u] = input[i * 2u];
933
+ output[j * 2u + 1u] = input[i * 2u + 1u];
934
+ }
935
+ """
936
+
937
+ FFT_BUTTERFLY_SHADER = """
938
+ struct Params {
939
+ N: u32,
940
+ stage: u32,
941
+ _pad1: u32,
942
+ _pad2: u32,
943
+ }
944
+
945
+ @group(0) @binding(0) var<storage, read_write> data: array<f32>;
946
+ @group(0) @binding(1) var<uniform> params: Params;
947
+
948
+ @compute @workgroup_size(256)
949
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
950
+ let i = gid.x;
951
+ let m = 1u << (params.stage + 1u);
952
+ let half = m >> 1u;
953
+
954
+ // Total number of butterflies is N/2
955
+ if (i >= params.N / 2u) {
956
+ return;
957
+ }
958
+
959
+ // Which butterfly group and position within group
960
+ let group = i / half;
961
+ let pos = i % half;
962
+ let j = group * m + pos;
963
+ let k = j + half;
964
+
965
+ let pi = 3.14159265358979323846;
966
+ let angle = -2.0 * pi * f32(pos) / f32(m);
967
+ let tw_re = cos(angle);
968
+ let tw_im = sin(angle);
969
+
970
+ // Load X[k]
971
+ let xk_re = data[k * 2u];
972
+ let xk_im = data[k * 2u + 1u];
973
+
974
+ // Twiddle multiply: W * X[k]
975
+ let t_re = tw_re * xk_re - tw_im * xk_im;
976
+ let t_im = tw_re * xk_im + tw_im * xk_re;
977
+
978
+ // Load X[j]
979
+ let xj_re = data[j * 2u];
980
+ let xj_im = data[j * 2u + 1u];
981
+
982
+ // Butterfly
983
+ data[j * 2u] = xj_re + t_re;
984
+ data[j * 2u + 1u] = xj_im + t_im;
985
+ data[k * 2u] = xj_re - t_re;
986
+ data[k * 2u + 1u] = xj_im - t_im;
987
+ }
988
+ """
989
+
990
+ IFFT_BUTTERFLY_SHADER = """
991
+ struct Params {
992
+ N: u32,
993
+ stage: u32,
994
+ _pad1: u32,
995
+ _pad2: u32,
996
+ }
997
+
998
+ @group(0) @binding(0) var<storage, read_write> data: array<f32>;
999
+ @group(0) @binding(1) var<uniform> params: Params;
1000
+
1001
+ @compute @workgroup_size(256)
1002
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
1003
+ let i = gid.x;
1004
+ let m = 1u << (params.stage + 1u);
1005
+ let half = m >> 1u;
1006
+
1007
+ if (i >= params.N / 2u) {
1008
+ return;
1009
+ }
1010
+
1011
+ let group = i / half;
1012
+ let pos = i % half;
1013
+ let j = group * m + pos;
1014
+ let k = j + half;
1015
+
1016
+ let pi = 3.14159265358979323846;
1017
+ // Conjugate twiddle for IFFT: positive angle
1018
+ let angle = 2.0 * pi * f32(pos) / f32(m);
1019
+ let tw_re = cos(angle);
1020
+ let tw_im = sin(angle);
1021
+
1022
+ let xk_re = data[k * 2u];
1023
+ let xk_im = data[k * 2u + 1u];
1024
+
1025
+ let t_re = tw_re * xk_re - tw_im * xk_im;
1026
+ let t_im = tw_re * xk_im + tw_im * xk_re;
1027
+
1028
+ let xj_re = data[j * 2u];
1029
+ let xj_im = data[j * 2u + 1u];
1030
+
1031
+ data[j * 2u] = xj_re + t_re;
1032
+ data[j * 2u + 1u] = xj_im + t_im;
1033
+ data[k * 2u] = xj_re - t_re;
1034
+ data[k * 2u + 1u] = xj_im - t_im;
1035
+ }
1036
+ """
1037
+
1038
+ FFT_SCALE_SHADER = """
1039
+ struct Params {
1040
+ scalar: f32,
1041
+ _pad1: u32,
1042
+ _pad2: u32,
1043
+ _pad3: u32,
1044
+ }
1045
+
1046
+ @group(0) @binding(0) var<storage, read_write> data: array<f32>;
1047
+ @group(0) @binding(1) var<uniform> params: Params;
1048
+
1049
+ @compute @workgroup_size(256)
1050
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
1051
+ let i = gid.x;
1052
+ if (i < arrayLength(&data)) {
1053
+ data[i] = data[i] * params.scalar;
1054
+ }
1055
+ }
1056
+ """
1057
+
1058
+ COMPLEX_MAGNITUDE_SHADER = """
1059
+ @group(0) @binding(0) var<storage, read> input: array<f32>;
1060
+ @group(0) @binding(1) var<storage, read_write> output: array<f32>;
1061
+
1062
+ @compute @workgroup_size(256)
1063
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
1064
+ let i = gid.x;
1065
+ let n = arrayLength(&output);
1066
+ if (i < n) {
1067
+ let re = input[i * 2u];
1068
+ let im = input[i * 2u + 1u];
1069
+ output[i] = sqrt(re * re + im * im);
1070
+ }
1071
+ }
1072
+ """
1073
+
1074
+ COMPLEX_PHASE_SHADER = """
1075
+ @group(0) @binding(0) var<storage, read> input: array<f32>;
1076
+ @group(0) @binding(1) var<storage, read_write> output: array<f32>;
1077
+
1078
+ @compute @workgroup_size(256)
1079
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
1080
+ let i = gid.x;
1081
+ let n = arrayLength(&output);
1082
+ if (i < n) {
1083
+ let re = input[i * 2u];
1084
+ let im = input[i * 2u + 1u];
1085
+ output[i] = atan2(im, re);
1086
+ }
1087
+ }
1088
+ """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gpuarray
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: NumPy-like GPU array library that works on every GPU — NVIDIA, AMD, Intel, Apple Silicon. No CUDA required.
5
5
  Author: Arnav Kewalram
6
6
  License: MIT
@@ -0,0 +1,9 @@
1
+ gpuarray/__init__.py,sha256=YmH2uwL9iez8AUGDE2OCY6sVNIrC49FNsUP5PFxrNmk,864
2
+ gpuarray/api.py,sha256=wiZnz4rs631nGiR_wW01Rr1BNgT0RL8fD2rMRQ7SSpg,5883
3
+ gpuarray/core.py,sha256=D2Q5Lwe8p2PmQjaUqMYEWd2qDSQkI1xt5IfTZ3MyIFY,39048
4
+ gpuarray/device.py,sha256=q80VOrKhy1u5i4AMLUKhWVBwFqQjoJ4tNBm234Wpf-A,452
5
+ gpuarray/shaders.py,sha256=0-71ErmWkBCHqPoROsORiVRgl48c4BsoqAjSLOn2QKg,30566
6
+ gpuarray-0.4.0.dist-info/METADATA,sha256=GRL-qds44XKfiHArZsRivg-WEVWondmQtOEXA0tgofI,3132
7
+ gpuarray-0.4.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
8
+ gpuarray-0.4.0.dist-info/top_level.txt,sha256=iruYli4LmPBqhaZxTVvzT1bT0VeN6dbC0GE29lT1CbI,9
9
+ gpuarray-0.4.0.dist-info/RECORD,,
@@ -1,9 +0,0 @@
1
- gpuarray/__init__.py,sha256=MB9k9oXPvA6wRCCZii72HtpRxjXPtkiT2RaJsiVY-wY,752
2
- gpuarray/api.py,sha256=PlYqtvY7jEV3xIneGdRoqXJjVOP3w2fnHyZgVP6fqis,4874
3
- gpuarray/core.py,sha256=bvBlQ9YSTRUcYawpnHpWnD-zJFdeMV16mv_rNFxYBGQ,18934
4
- gpuarray/device.py,sha256=q80VOrKhy1u5i4AMLUKhWVBwFqQjoJ4tNBm234Wpf-A,452
5
- gpuarray/shaders.py,sha256=LhS3vEZc08Mx_OPnsv06bT9gRPThuHIxBuWO0V2C_SI,16301
6
- gpuarray-0.2.0.dist-info/METADATA,sha256=8ZL33ZQ7eVBnm4GuvjLxNVCiVl39u2maLCpGXbuI24w,3132
7
- gpuarray-0.2.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
8
- gpuarray-0.2.0.dist-info/top_level.txt,sha256=iruYli4LmPBqhaZxTVvzT1bT0VeN6dbC0GE29lT1CbI,9
9
- gpuarray-0.2.0.dist-info/RECORD,,