Skip to content

Commit cd0a11d

Browse files
Ramdam17claude
andauthored
feat(sync): GPU (torch/Metal/CUDA) and numba backends for all 9 metrics (#264)
Add hardware-accelerated backends to all connectivity metrics: - numba JIT (prange): PLV, CCorr, Coh, ImCoh, PLI, wPLI, EnvCorr, PowCorr - PyTorch MPS/CUDA/CPU (einsum): all 9 metrics - Metal compute shaders: PLI, wPLI, ACCorr (Apple Silicon) - CUDA raw kernels (CuPy): all 9 metrics (NVIDIA GPUs) Benchmark-driven AUTO_PRIORITY compiled from Mac M4 Max (131 runs) and Narval A100 (111 runs). The 'auto' optimization selects the best GPU backend per metric and platform: - MPS: torch for einsum metrics, Metal for sign-based + ACCorr - CUDA: cuda_kernel first (OOM-safe at 512ch), torch as fallback Add `priority` parameter on get_metric() and compute_sync() for custom backend ordering. New optional deps: pyobjc-framework-Metal, cupy-cuda12x. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 100b300 commit cd0a11d

24 files changed

Lines changed: 3384 additions & 154 deletions

CHANGELOG.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,21 @@
44

55
### Added
66
- **New `hypyp.sync` module**: Modular architecture for connectivity metrics
7-
- Extracted 9 connectivity metrics into separate classes: `PLV`, `CCorr`, `ACorr`, `Coh`, `ImCoh`, `PLI`, `WPLI`, `EnvCorr`, `PowCorr`
7+
- Extracted 9 connectivity metrics into separate classes: `PLV`, `CCorr`, `ACCorr`, `Coh`, `ImCoh`, `PLI`, `WPLI`, `EnvCorr`, `PowCorr`
88
- `BaseMetric` abstract class for uniform interface across all metrics
9-
- `get_metric(mode, backend)` function for easy metric instantiation
10-
- Backend support infrastructure (numpy default, with future support for numba/torch)
9+
- `get_metric(mode, optimization)` function for easy metric instantiation
1110
- Helper functions: `multiply_conjugate`, `multiply_conjugate_time`, `multiply_product`
11+
- **GPU and numba backends for all 9 sync metrics**:
12+
- numba JIT with `prange`: PLV, CCorr, Coh, ImCoh, PLI, wPLI, EnvCorr, PowCorr
13+
- PyTorch (MPS/CUDA/CPU) via batched einsum: all 9 metrics
14+
- Metal compute shaders (Apple Silicon): PLI, wPLI, ACCorr
15+
- CUDA raw kernels via CuPy (NVIDIA GPUs): all 9 metrics
16+
- Benchmark-driven `AUTO_PRIORITY` table for `optimization='auto'`, compiled from
17+
Mac M4 Max (131 runs) and Narval A100 (111 runs) benchmarks
18+
- `priority` parameter on `get_metric()` and `compute_sync()` for custom backend ordering
19+
- `hypyp/sync/kernels/` submodule with Metal and CUDA dispatch infrastructure
20+
- New optional dependencies: `pyobjc-framework-Metal` (Apple), `cupy-cuda12x` (NVIDIA)
21+
- `multiply_conjugate_torch` and `multiply_conjugate_time_torch` GPU helpers
1222

1323
### Changed
1424
- **BREAKING**: `accorr` metric now returns raw connectivity values with shape `(n_epoch, n_freq, 2*n_ch, 2*n_ch)` like all other metrics. The `swapaxes` and `epochs_average` operations are now handled by `compute_sync()` instead of being applied inside the metric.
@@ -18,7 +28,7 @@
1828
- `_multiply_conjugate()` in analyses.py - use `hypyp.sync.multiply_conjugate` instead (will be removed in 1.0.0)
1929
- `_multiply_conjugate_time()` in analyses.py - use `hypyp.sync.multiply_conjugate_time` instead (will be removed in 1.0.0)
2030
- `_multiply_product()` in analyses.py - use `hypyp.sync.multiply_product` instead (will be removed in 1.0.0)
21-
- `_accorr_hybrid()` in analyses.py - use `hypyp.sync.ACorr` instead (will be removed in 1.0.0)
31+
- `_accorr_hybrid()` in analyses.py - use `hypyp.sync.ACCorr` instead (will be removed in 1.0.0)
2232

2333
## [0.5.0b13] - 2025-09-18
2434

hypyp/analyses.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,8 @@ def pair_connectivity(data: Union[list, np.ndarray], sampling_rate: int,
439439

440440

441441
def compute_sync(complex_signal: np.ndarray, mode: str, epochs_average: bool = True,
442-
optimization: Optional[str] = None) -> np.ndarray:
442+
optimization: Optional[str] = None,
443+
priority: Optional[list] = None) -> np.ndarray:
443444
"""
444445
Computes frequency-domain connectivity measures from analytic signals.
445446
@@ -547,7 +548,7 @@ def compute_sync(complex_signal: np.ndarray, mode: str, epochs_average: bool = T
547548

548549
# Get the metric from the sync module
549550
try:
550-
metric = get_metric(mode_normalized, optimization=optimization)
551+
metric = get_metric(mode_normalized, optimization=optimization, priority=priority)
551552
con = metric.compute(complex_signal, n_samp, transpose_axes)
552553
except ValueError:
553554
raise ValueError(f'Metric type "{mode}" not supported.')

hypyp/sync/README.md

Lines changed: 109 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,6 @@ Arbitrary methodological decisions skew inter-brain synchronization estimates
5858
in hyperscanning-EEG studies. *Imaging Neuroscience*, 2.
5959
https://doi.org/10.1162/imag_a_00350
6060

61-
**Note:** ACCorr supports hardware acceleration via `optimization` parameter.
62-
See [Optimization Backends](#optimization-backends) below.
63-
6461
---
6562

6663
### Coherence (`coh`)
@@ -165,24 +162,110 @@ amplitude. More sensitive to high-amplitude bursts.
165162

166163
## Optimization Backends
167164

168-
ACCorr supports three computational backends via the `optimization` parameter
169-
in `compute_sync()` or the class constructor:
165+
All 9 metrics support multiple computational backends via the `optimization`
166+
parameter in `compute_sync()` or the class constructor.
167+
168+
### Backend Support Matrix
169+
170+
| Metric | numpy | numba | torch | metal | cuda_kernel |
171+
|--------|:-----:|:-----:|:-----:|:-----:|:-----------:|
172+
| PLV | x | x | x | -- | x |
173+
| CCorr | x | x | x | -- | x |
174+
| Coh | x | x | x | -- | x |
175+
| ImCoh | x | x | x | -- | x |
176+
| EnvCorr| x | x | x | -- | x |
177+
| PowCorr| x | x | x | -- | x |
178+
| PLI | x | x | x | x | x |
179+
| wPLI | x | x | x | x | x |
180+
| ACCorr | x | x | x | x | x |
181+
182+
### Backend Descriptions
170183

171184
| Value | Backend | Device | Notes |
172185
|-------|---------|--------|-------|
173186
| `None` (default) | NumPy | CPU | Standard, no extra dependencies |
174-
| `'auto'` | Best available | Auto | torch → numba → numpy |
175-
| `'numba'` | Numba JIT | CPU | ~2× speedup; install: `poetry install --with optim_numba` |
176-
| `'torch'` | PyTorch | GPU/CPU | ~20× speedup on GPU; install: `poetry install --with optim_torch` |
187+
| `'auto'` | Best available | Auto | Selects best GPU backend per metric and platform |
188+
| `'numba'` | Numba JIT | CPU | Fused single-pass kernels with `prange` parallelism |
189+
| `'torch'` | PyTorch | GPU/CPU | Batched einsum; MPS (Apple) / CUDA (NVIDIA) / CPU |
190+
| `'metal'` | Metal shaders | Apple GPU | Custom compute shaders for PLI, wPLI, ACCorr only |
191+
| `'cuda_kernel'` | CuPy RawKernel | NVIDIA GPU | Custom CUDA kernels; float64 precision |
192+
193+
### `optimization='auto'` — Benchmark-Driven Dispatch
194+
195+
The `'auto'` mode selects the best GPU backend for each metric based on
196+
benchmark data compiled from Mac M4 Max (131 runs) and Narval A100 (111 runs).
197+
198+
**MPS (Apple Silicon):**
199+
- Einsum metrics (PLV, CCorr, Coh, ImCoh, EnvCorr, PowCorr): torch (batched BLAS)
200+
- Sign-based (PLI, wPLI) + ACCorr: Metal custom kernels
201+
202+
**CUDA (NVIDIA):**
203+
- All metrics: `cuda_kernel` first (pairwise computation, OOM-safe at 512+ channels),
204+
with torch as fallback.
205+
206+
The priority can be overridden per-call:
207+
```python
208+
get_metric('plv', optimization='auto', priority=['torch', 'cuda_kernel'])
209+
```
210+
211+
If no GPU backend is available, `'auto'` falls back to numba, then numpy.
212+
213+
### Precision
214+
215+
- **CPU / CUDA (`float64`):** reference precision, `rtol=1e-9, atol=1e-10`
216+
- **MPS / Metal (`float32`):** up to ~1e-5 difference vs CPU reference.
217+
Sign-based metrics (PLI, wPLI) may show larger differences (`rtol=1e-2`)
218+
near the sign discontinuity at zero.
219+
220+
---
221+
222+
## Architecture
177223

178-
**Device priority for `'torch'` and `'auto'`:** MPS (Apple Silicon) > CUDA (NVIDIA) > CPU.
179-
MPS and CUDA are mutually exclusive; the best available device is selected automatically.
224+
```
225+
hypyp/sync/
226+
├── __init__.py # Registry, get_metric(), exports
227+
├── base.py # BaseMetric, AUTO_PRIORITY, helpers
228+
├── plv.py ... wpli.py # One file per metric (9 files)
229+
└── kernels/ # Custom GPU kernels
230+
├── __init__.py # METAL_AVAILABLE, CUPY_AVAILABLE flags
231+
├── _metal_dispatch.py # Shared Metal pairwise dispatch
232+
├── _cuda_dispatch.py # Shared CUDA pairwise dispatch
233+
├── metal_phase.py # PLI, wPLI Metal shaders
234+
├── metal_accorr.py # ACCorr Metal shader
235+
├── cuda_phase.py # PLI, wPLI, PLV, CCorr CUDA kernels
236+
├── cuda_amplitude.py # Coh, ImCoh, EnvCorr, PowCorr CUDA kernels
237+
└── cuda_accorr.py # ACCorr CUDA kernel
238+
```
180239

181-
**Precision note:** MPS uses `float32`, which may introduce numerical differences
182-
of up to ~1e-5 compared to CPU/CUDA (`float64`).
240+
Each metric class inherits from `BaseMetric` and implements:
241+
- `_compute_numpy()` — always available (reference implementation)
242+
- `_compute_numba()` — fused loop with `numba.prange` parallelism
243+
- `_compute_torch()` — batched einsum on auto-detected device
244+
- `_compute_metal()` — Metal shader dispatch (PLI, wPLI, ACCorr only)
245+
- `_compute_cuda()` — CUDA RawKernel dispatch
183246

184-
All other metrics currently use numpy only (`optimization` parameter is accepted
185-
but ignored for non-ACCorr metrics).
247+
Backend selection happens at `__init__()`, dispatch at `compute()`.
248+
249+
---
250+
251+
## Installation
252+
253+
```bash
254+
# Core (numpy backend always available)
255+
pip install hypyp
256+
257+
# CPU parallelism
258+
pip install "hypyp[numba]"
259+
260+
# GPU acceleration (PyTorch)
261+
pip install "hypyp[torch]"
262+
263+
# Apple Silicon Metal shaders (PLI, wPLI, ACCorr)
264+
pip install "hypyp[metal]"
265+
266+
# NVIDIA CUDA kernels (all metrics, requires CUDA 12.x)
267+
pip install "hypyp[cupy]"
268+
```
186269

187270
---
188271

@@ -192,13 +275,20 @@ but ignored for non-ACCorr metrics).
192275
from hypyp.analyses import compute_sync
193276

194277
# Standard (numpy)
195-
con = compute_sync(complex_signal, 'accorr')
278+
con = compute_sync(complex_signal, 'plv')
279+
280+
# Best available GPU backend
281+
con = compute_sync(complex_signal, 'plv', optimization='auto')
282+
283+
# Specific backend
284+
con = compute_sync(complex_signal, 'pli', optimization='metal')
196285

197-
# With GPU acceleration
198-
con = compute_sync(complex_signal, 'accorr', optimization='torch')
286+
# Custom priority
287+
con = compute_sync(complex_signal, 'coh', optimization='auto',
288+
priority=['torch', 'cuda_kernel'])
199289

200290
# Direct class instantiation
201-
from hypyp.sync import ACCorr
202-
metric = ACCorr(optimization='auto', show_progress=True)
291+
from hypyp.sync import get_metric
292+
metric = get_metric('accorr', optimization='auto')
203293
con = metric.compute(complex_signal_internal, n_samp, transpose_axes)
204294
```

hypyp/sync/__init__.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010

1111
from typing import Optional
1212

13-
from .base import BaseMetric, multiply_conjugate, multiply_conjugate_time, multiply_product
13+
from .base import (
14+
BaseMetric, multiply_conjugate, multiply_conjugate_time, multiply_product,
15+
multiply_conjugate_torch, multiply_conjugate_time_torch,
16+
)
1417
from .plv import PLV
1518
from .ccorr import CCorr
1619
from .accorr import ACCorr
@@ -40,6 +43,8 @@
4043
'multiply_conjugate',
4144
'multiply_conjugate_time',
4245
'multiply_product',
46+
'multiply_conjugate_torch',
47+
'multiply_conjugate_time_torch',
4348
# Metric classes
4449
'PLV',
4550
'CCorr',
@@ -56,7 +61,8 @@
5661
]
5762

5863

59-
def get_metric(mode: str, optimization: Optional[str] = None) -> BaseMetric:
64+
def get_metric(mode: str, optimization: Optional[str] = None,
65+
priority: Optional[list] = None) -> BaseMetric:
6066
"""
6167
Get a connectivity metric instance by name.
6268
@@ -66,8 +72,11 @@ def get_metric(mode: str, optimization: Optional[str] = None) -> BaseMetric:
6672
Name of the connectivity metric. One of: 'plv', 'ccorr', 'accorr',
6773
'coh', 'imcoh', 'pli', 'wpli', 'envcorr', 'powcorr'.
6874
optimization : str, optional
69-
Optimization strategy. Options: None, 'auto', 'numba', 'torch'.
70-
See BaseMetric for fallback behavior.
75+
Optimization strategy. Options: None, 'auto', 'numba', 'torch',
76+
'metal', 'cuda_kernel'. See BaseMetric for fallback behavior.
77+
priority : list of str, optional
78+
Custom backend priority for ``'auto'`` mode. Overrides the default
79+
``AUTO_PRIORITY`` table. Example: ``['metal', 'torch', 'numba']``.
7180
7281
Returns
7382
-------
@@ -82,12 +91,13 @@ def get_metric(mode: str, optimization: Optional[str] = None) -> BaseMetric:
8291
Examples
8392
--------
8493
>>> from hypyp.sync import get_metric
85-
>>> accorr = get_metric('accorr', optimization='torch')
86-
>>> result = accorr.compute(complex_signal, n_samp, transpose_axes)
94+
>>> plv = get_metric('plv', optimization='auto') # benchmark-driven
95+
>>> pli = get_metric('pli', optimization='auto',
96+
... priority=['numba', 'metal']) # custom priority
8797
"""
8898
mode_lower = mode.lower()
8999
if mode_lower not in METRICS:
90100
available = ', '.join(METRICS.keys())
91101
raise ValueError(f"Unknown metric mode '{mode}'. Available: {available}")
92102

93-
return METRICS[mode_lower](optimization=optimization)
103+
return METRICS[mode_lower](optimization=optimization, priority=priority)

0 commit comments

Comments
 (0)