commkit 1.0.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.
- commkit/__init__.py +74 -0
- commkit/_cuda/__init__.py +321 -0
- commkit/_cuda/compiler.py +88 -0
- commkit/_cuda/src/bps_min_d2.cu +104 -0
- commkit/_cuda/src/cs_block.cu +119 -0
- commkit/_cuda/src/selftest.cu +14 -0
- commkit/analysis/__init__.py +55 -0
- commkit/analysis/_common.py +236 -0
- commkit/analysis/allan.py +108 -0
- commkit/analysis/drift.py +213 -0
- commkit/analysis/interferometry.py +887 -0
- commkit/analysis/linewidth.py +480 -0
- commkit/analysis/trajectory.py +91 -0
- commkit/backend.py +507 -0
- commkit/coding/__init__.py +23 -0
- commkit/coding/base.py +17 -0
- commkit/coding/bch.py +6 -0
- commkit/coding/convolutional.py +7 -0
- commkit/coding/crc.py +7 -0
- commkit/coding/galois.py +8 -0
- commkit/coding/hamming.py +6 -0
- commkit/coding/interleaving.py +7 -0
- commkit/coding/ldpc.py +8 -0
- commkit/coding/polar.py +8 -0
- commkit/coding/ratematch.py +6 -0
- commkit/coding/reed_solomon.py +6 -0
- commkit/coding/turbo.py +8 -0
- commkit/core/__init__.py +32 -0
- commkit/core/frame.py +992 -0
- commkit/core/generation.py +581 -0
- commkit/core/signal.py +725 -0
- commkit/equalization/__init__.py +49 -0
- commkit/equalization/_block.py +1855 -0
- commkit/equalization/_common.py +606 -0
- commkit/equalization/_kernels_jax.py +1720 -0
- commkit/equalization/_kernels_numba.py +1704 -0
- commkit/equalization/blind.py +223 -0
- commkit/equalization/linear.py +365 -0
- commkit/equalization/polarization.py +790 -0
- commkit/equalization/result.py +191 -0
- commkit/equalization/sequential.py +2805 -0
- commkit/filtering.py +1120 -0
- commkit/frequency.py +1191 -0
- commkit/helpers.py +489 -0
- commkit/impairments/__init__.py +43 -0
- commkit/impairments/channel/__init__.py +20 -0
- commkit/impairments/channel/linear.py +310 -0
- commkit/impairments/channel/nonlinear.py +11 -0
- commkit/impairments/frontend.py +229 -0
- commkit/impairments/noise.py +105 -0
- commkit/impairments/source.py +219 -0
- commkit/io.py +308 -0
- commkit/logger.py +103 -0
- commkit/mapping/__init__.py +46 -0
- commkit/mapping/bits.py +240 -0
- commkit/mapping/constellation.py +153 -0
- commkit/mapping/gray.py +429 -0
- commkit/mapping/llr.py +253 -0
- commkit/mapping/shaping.py +218 -0
- commkit/metrics.py +949 -0
- commkit/multirate.py +476 -0
- commkit/plotting/__init__.py +78 -0
- commkit/plotting/analysis.py +627 -0
- commkit/plotting/constellation.py +483 -0
- commkit/plotting/equalizer.py +390 -0
- commkit/plotting/eye.py +388 -0
- commkit/plotting/spectral.py +575 -0
- commkit/plotting/sync.py +953 -0
- commkit/plotting/theme.py +203 -0
- commkit/plotting/waveform.py +200 -0
- commkit/py.typed +0 -0
- commkit/recovery/__init__.py +51 -0
- commkit/recovery/bps.py +337 -0
- commkit/recovery/corrections.py +751 -0
- commkit/recovery/pilots.py +803 -0
- commkit/recovery/pll.py +482 -0
- commkit/recovery/tikhonov.py +424 -0
- commkit/recovery/viterbi_viterbi.py +227 -0
- commkit/spectral.py +560 -0
- commkit/timing.py +841 -0
- commkit-1.0.0.dist-info/METADATA +145 -0
- commkit-1.0.0.dist-info/RECORD +84 -0
- commkit-1.0.0.dist-info/WHEEL +4 -0
- commkit-1.0.0.dist-info/licenses/LICENSE +21 -0
commkit/__init__.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
`commkit` is a high-performance library for simulating and analyzing
|
|
3
|
+
digital communication systems. It provides a unified API for generating,
|
|
4
|
+
transforming, and assessing signals across diverse computational backends
|
|
5
|
+
(CPU, GPU, and JAX).
|
|
6
|
+
|
|
7
|
+
Main Features
|
|
8
|
+
-------------
|
|
9
|
+
- **Signal Abstractions**: Unified `Signal` and `SingleCarrierFrame` containers.
|
|
10
|
+
- **Modulation**: Support for PAM, PSK, and QAM (NRZ/RZ) with Gray coding.
|
|
11
|
+
- **Impairments**: Simulation of AWGN, Phase Noise, and Frequency Offset.
|
|
12
|
+
- **Synchronization**: Time and frequency synchronization algorithms.
|
|
13
|
+
- **Execution backends**: Transparent NumPy, CuPy, and JAX support.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import warnings
|
|
17
|
+
|
|
18
|
+
__version__ = "1.0.0"
|
|
19
|
+
|
|
20
|
+
# Leaf modules import ``Signal`` from ``.core.signal`` at top level for
|
|
21
|
+
# Signal/array dispatch. ``core.signal`` is a leaf (it imports no sibling
|
|
22
|
+
# domain modules), so the first leaf module imported below pulls ``core`` in
|
|
23
|
+
# without a cycle, regardless of statement order here.
|
|
24
|
+
from . import (
|
|
25
|
+
analysis,
|
|
26
|
+
equalization,
|
|
27
|
+
frequency,
|
|
28
|
+
impairments,
|
|
29
|
+
metrics,
|
|
30
|
+
recovery,
|
|
31
|
+
spectral,
|
|
32
|
+
timing,
|
|
33
|
+
)
|
|
34
|
+
from .core import (
|
|
35
|
+
Preamble,
|
|
36
|
+
Signal,
|
|
37
|
+
SingleCarrierFrame,
|
|
38
|
+
generate,
|
|
39
|
+
generate_pam,
|
|
40
|
+
generate_psk,
|
|
41
|
+
generate_psqam,
|
|
42
|
+
generate_qam,
|
|
43
|
+
)
|
|
44
|
+
from .io import load_npz, save_npz
|
|
45
|
+
from .logger import set_log_level
|
|
46
|
+
from .plotting import apply_default_theme
|
|
47
|
+
|
|
48
|
+
# Filter the specific warning message using a regular expression match
|
|
49
|
+
warnings.filterwarnings("ignore", message=".*cupyx.jit.rawkernel is experimental.*")
|
|
50
|
+
|
|
51
|
+
__all__ = [
|
|
52
|
+
"Preamble",
|
|
53
|
+
"Signal",
|
|
54
|
+
"SingleCarrierFrame",
|
|
55
|
+
"__version__",
|
|
56
|
+
"analysis",
|
|
57
|
+
"equalization",
|
|
58
|
+
"frequency",
|
|
59
|
+
"generate",
|
|
60
|
+
"generate_pam",
|
|
61
|
+
"generate_psk",
|
|
62
|
+
"generate_psqam",
|
|
63
|
+
"generate_qam",
|
|
64
|
+
"impairments",
|
|
65
|
+
"load_npz",
|
|
66
|
+
"metrics",
|
|
67
|
+
"recovery",
|
|
68
|
+
"save_npz",
|
|
69
|
+
"set_log_level",
|
|
70
|
+
"spectral",
|
|
71
|
+
"timing",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
apply_default_theme()
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hand-written CUDA kernel infrastructure for the CuPy backend.
|
|
3
|
+
|
|
4
|
+
This subpackage hosts raw CUDA C++ kernels (under ``src/``) together with the
|
|
5
|
+
lazy compilation machinery built on :class:`cupy.RawModule`. Its public
|
|
6
|
+
surface is two functions:
|
|
7
|
+
|
|
8
|
+
is_available :
|
|
9
|
+
True when CuPy is functional and a CUDA device with compute
|
|
10
|
+
capability >= 7.0 is present.
|
|
11
|
+
get_kernel :
|
|
12
|
+
Returns a launchable kernel wrapper, or ``None`` - in which case the
|
|
13
|
+
caller **must** fall back to its existing ``xp`` implementation.
|
|
14
|
+
|
|
15
|
+
Importing this package (or ``commkit``) never touches NVRTC: kernel
|
|
16
|
+
sources are read and compiled on the first ``get_kernel()`` call for a
|
|
17
|
+
given specialization, and cached in-process afterwards. Cross-process
|
|
18
|
+
reuse comes from CuPy's on-disk kernel cache (``~/.cupy/kernel_cache``).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from collections.abc import Callable
|
|
22
|
+
from functools import lru_cache
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from ..backend import is_cupy_available
|
|
26
|
+
from ..logger import logger
|
|
27
|
+
|
|
28
|
+
# Volta and newer. Older parts lack the independent-thread-scheduling and
|
|
29
|
+
# shared-memory sizes the kernels in src/ are written against.
|
|
30
|
+
_MIN_COMPUTE_CAPABILITY = 70
|
|
31
|
+
|
|
32
|
+
# Kernel names that have already produced a compile/load warning; the
|
|
33
|
+
# fallback contract promises at most one warning per process per kernel.
|
|
34
|
+
_warned_kernels: set[str] = set()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@lru_cache(maxsize=1)
|
|
38
|
+
def _device_supported() -> bool:
|
|
39
|
+
"""Hardware probe, cached for the process lifetime.
|
|
40
|
+
|
|
41
|
+
Checks that at least one CUDA device is present and that the current
|
|
42
|
+
device has compute capability >= 7.0. Only called when CuPy itself is
|
|
43
|
+
importable and functional.
|
|
44
|
+
"""
|
|
45
|
+
import cupy as cp
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
if cp.cuda.runtime.getDeviceCount() < 1:
|
|
49
|
+
return False
|
|
50
|
+
cc = int(cp.cuda.Device().compute_capability)
|
|
51
|
+
except Exception:
|
|
52
|
+
return False
|
|
53
|
+
return cc >= _MIN_COMPUTE_CAPABILITY
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def is_available() -> bool:
|
|
57
|
+
"""Checks whether custom CUDA kernels can be compiled and launched.
|
|
58
|
+
|
|
59
|
+
Returns
|
|
60
|
+
-------
|
|
61
|
+
bool
|
|
62
|
+
True if CuPy is installed and functional (and not disabled via
|
|
63
|
+
``backend.use_cpu_only``), at least one CUDA device is present,
|
|
64
|
+
and the current device has compute capability >= 7.0.
|
|
65
|
+
"""
|
|
66
|
+
# The CuPy check is evaluated fresh on every call so that
|
|
67
|
+
# backend.use_cpu_only() is honored; only the hardware probe is cached.
|
|
68
|
+
if not is_cupy_available():
|
|
69
|
+
return False
|
|
70
|
+
return _device_supported()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_kernel(name: str, **spec: Any) -> Callable | None:
|
|
74
|
+
"""Returns a launchable kernel wrapper, or ``None`` if unavailable.
|
|
75
|
+
|
|
76
|
+
``None`` means the caller **must** fall back to the existing ``xp``
|
|
77
|
+
implementation. Any compile/load failure logs one warning per kernel
|
|
78
|
+
name per process and returns ``None``.
|
|
79
|
+
|
|
80
|
+
Parameters
|
|
81
|
+
----------
|
|
82
|
+
name : str
|
|
83
|
+
Registered kernel name (see ``_KERNEL_FACTORIES``).
|
|
84
|
+
**spec
|
|
85
|
+
Kernel-specific specialization options (e.g. mode or dtype),
|
|
86
|
+
forwarded to the kernel's wrapper factory.
|
|
87
|
+
|
|
88
|
+
Returns
|
|
89
|
+
-------
|
|
90
|
+
callable or None
|
|
91
|
+
A wrapper that handles grid/block computation and input
|
|
92
|
+
validation. Call sites pass CuPy arrays and plain Python scalars
|
|
93
|
+
only. ``None`` when no usable GPU is present or compilation fails.
|
|
94
|
+
|
|
95
|
+
Raises
|
|
96
|
+
------
|
|
97
|
+
KeyError
|
|
98
|
+
If `name` is not a registered kernel. Unknown names are
|
|
99
|
+
programming errors, not runtime fallback conditions.
|
|
100
|
+
"""
|
|
101
|
+
factory = _KERNEL_FACTORIES.get(name)
|
|
102
|
+
if factory is None:
|
|
103
|
+
raise KeyError(
|
|
104
|
+
f"Unknown CUDA kernel {name!r}; registered: {sorted(_KERNEL_FACTORIES)}"
|
|
105
|
+
)
|
|
106
|
+
if not is_available():
|
|
107
|
+
return None
|
|
108
|
+
try:
|
|
109
|
+
return factory(**spec)
|
|
110
|
+
except Exception as exc:
|
|
111
|
+
if name not in _warned_kernels:
|
|
112
|
+
_warned_kernels.add(name)
|
|
113
|
+
logger.warning(
|
|
114
|
+
"CUDA kernel %r failed to compile/load (%s); "
|
|
115
|
+
"falling back to the array-module implementation.",
|
|
116
|
+
name,
|
|
117
|
+
exc,
|
|
118
|
+
)
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _selftest_scale_factory(dtype: str = "float32") -> Callable:
|
|
123
|
+
"""Wrapper factory for the infrastructure self-test kernel.
|
|
124
|
+
|
|
125
|
+
Validates the full compile -> specialize -> launch path; not used by
|
|
126
|
+
any DSP code.
|
|
127
|
+
"""
|
|
128
|
+
import cupy as cp
|
|
129
|
+
|
|
130
|
+
from . import compiler
|
|
131
|
+
|
|
132
|
+
ctype = {"float32": "float", "float64": "double"}[dtype]
|
|
133
|
+
kern = compiler.get_raw_kernel("selftest", f"selftest_scale<{ctype}>")
|
|
134
|
+
np_dtype = cp.dtype(dtype)
|
|
135
|
+
block = 256
|
|
136
|
+
|
|
137
|
+
def launch(x: Any, alpha: float) -> Any:
|
|
138
|
+
x = cp.ascontiguousarray(x)
|
|
139
|
+
if x.dtype != np_dtype:
|
|
140
|
+
raise TypeError(f"expected {np_dtype} input, got {x.dtype}")
|
|
141
|
+
y = cp.empty_like(x)
|
|
142
|
+
n = x.size
|
|
143
|
+
grid = (n + block - 1) // block
|
|
144
|
+
kern((grid,), (block,), (x, y, np_dtype.type(alpha), cp.int64(n)))
|
|
145
|
+
return y
|
|
146
|
+
|
|
147
|
+
return launch
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _bps_min_d2_factory(mode: str = "table", return_argmin: bool = False) -> Callable:
|
|
151
|
+
"""Wrapper factory for the fused BPS minimum-distance kernel.
|
|
152
|
+
|
|
153
|
+
Computes ``min_d2[p, c, n] = min_m |x[c, n] * phasor[p] - const[m]|**2``
|
|
154
|
+
in one pass. ``mode="table"`` searches an explicit constellation table;
|
|
155
|
+
``mode="grid"`` snaps to the uniform square-QAM level grid. With
|
|
156
|
+
``return_argmin=True`` (TABLE only) the nearest-point indices are
|
|
157
|
+
returned alongside the distances.
|
|
158
|
+
"""
|
|
159
|
+
import cupy as cp
|
|
160
|
+
|
|
161
|
+
from . import compiler
|
|
162
|
+
|
|
163
|
+
mode_id = {"table": 0, "grid": 1}[mode]
|
|
164
|
+
if mode_id == 1 and return_argmin:
|
|
165
|
+
raise ValueError("return_argmin is only supported in TABLE mode")
|
|
166
|
+
kern = compiler.get_raw_kernel(
|
|
167
|
+
"bps_min_d2",
|
|
168
|
+
f"bps_min_d2<{mode_id}, {'true' if return_argmin else 'false'}>",
|
|
169
|
+
)
|
|
170
|
+
block_n = 128
|
|
171
|
+
_empty_i32 = cp.empty(0, dtype=cp.int32)
|
|
172
|
+
|
|
173
|
+
def launch(
|
|
174
|
+
x: Any,
|
|
175
|
+
phasor: Any,
|
|
176
|
+
constellation: Any = None,
|
|
177
|
+
lev_min: float = 0.0,
|
|
178
|
+
d_grid: float = 1.0,
|
|
179
|
+
side: int = 0,
|
|
180
|
+
) -> Any:
|
|
181
|
+
"""Returns min_d2 (P, C, N) float32; with argmin, an (min_d2, idx) tuple.
|
|
182
|
+
|
|
183
|
+
`x` is (C, N) complex64 with time on the last axis; `phasor` is
|
|
184
|
+
(P,) complex64, P <= 128. TABLE mode requires `constellation`
|
|
185
|
+
((M,) complex64, M <= 1024); GRID mode requires the level-grid
|
|
186
|
+
scalars `lev_min`, `d_grid`, `side`.
|
|
187
|
+
"""
|
|
188
|
+
x = cp.ascontiguousarray(x)
|
|
189
|
+
phasor = cp.ascontiguousarray(phasor)
|
|
190
|
+
if x.ndim != 2:
|
|
191
|
+
raise ValueError(f"x must be 2-D (C, N), got shape {x.shape}")
|
|
192
|
+
if x.dtype != cp.complex64 or phasor.dtype != cp.complex64:
|
|
193
|
+
raise TypeError(
|
|
194
|
+
f"x and phasor must be complex64, got {x.dtype}/{phasor.dtype}"
|
|
195
|
+
)
|
|
196
|
+
C, N = x.shape
|
|
197
|
+
P = int(phasor.size)
|
|
198
|
+
if P < 1 or P > 128:
|
|
199
|
+
raise ValueError(f"phasor count must be in [1, 128], got {P}")
|
|
200
|
+
|
|
201
|
+
if mode_id == 0:
|
|
202
|
+
constellation = cp.ascontiguousarray(constellation)
|
|
203
|
+
if constellation.dtype != cp.complex64:
|
|
204
|
+
raise TypeError(
|
|
205
|
+
f"constellation must be complex64, got {constellation.dtype}"
|
|
206
|
+
)
|
|
207
|
+
M = int(constellation.size)
|
|
208
|
+
if M < 1 or M > 1024:
|
|
209
|
+
raise ValueError(f"constellation size must be in [1, 1024], got {M}")
|
|
210
|
+
shared_mem = M * 8
|
|
211
|
+
else:
|
|
212
|
+
if side < 2:
|
|
213
|
+
raise ValueError(f"GRID mode requires side >= 2, got {side}")
|
|
214
|
+
constellation = _empty_i32 # never dereferenced (M == 0)
|
|
215
|
+
M = 0
|
|
216
|
+
shared_mem = 0
|
|
217
|
+
|
|
218
|
+
min_d2 = cp.empty((P, C, N), dtype=cp.float32)
|
|
219
|
+
argmin = cp.empty((P, C, N), dtype=cp.int32) if return_argmin else _empty_i32
|
|
220
|
+
grid = ((N + block_n - 1) // block_n, P, C)
|
|
221
|
+
kern(
|
|
222
|
+
grid,
|
|
223
|
+
(block_n,),
|
|
224
|
+
(
|
|
225
|
+
x,
|
|
226
|
+
phasor,
|
|
227
|
+
constellation,
|
|
228
|
+
cp.float32(lev_min),
|
|
229
|
+
cp.float32(d_grid),
|
|
230
|
+
cp.int32(side),
|
|
231
|
+
cp.int32(M),
|
|
232
|
+
cp.int64(N),
|
|
233
|
+
min_d2,
|
|
234
|
+
argmin,
|
|
235
|
+
),
|
|
236
|
+
shared_mem=shared_mem,
|
|
237
|
+
)
|
|
238
|
+
if return_argmin:
|
|
239
|
+
return min_d2, argmin
|
|
240
|
+
return min_d2
|
|
241
|
+
|
|
242
|
+
return launch
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _cs_block_factory() -> Callable:
|
|
246
|
+
"""Wrapper factory for the block_lms cycle-slip correction kernel.
|
|
247
|
+
|
|
248
|
+
Sequential per-channel slip detector (one block, one thread per channel)
|
|
249
|
+
operating in-place on device-resident state buffers. One launch processes
|
|
250
|
+
one equalizer block, replacing the per-block D2H -> CPU Numba -> H2D
|
|
251
|
+
round trip of the fallback path. All state arrays are float64/int64 and
|
|
252
|
+
are mutated in place - the wrapper therefore rejects non-contiguous
|
|
253
|
+
inputs instead of silently copying them.
|
|
254
|
+
"""
|
|
255
|
+
import cupy as cp
|
|
256
|
+
|
|
257
|
+
from . import compiler
|
|
258
|
+
|
|
259
|
+
kern = compiler.get_raw_kernel("cs_block", "cs_block")
|
|
260
|
+
|
|
261
|
+
def launch(
|
|
262
|
+
phi_blk: Any,
|
|
263
|
+
phi_corr: Any,
|
|
264
|
+
cs_buf_y: Any,
|
|
265
|
+
cs_buf_ptr: Any,
|
|
266
|
+
cs_buf_n: Any,
|
|
267
|
+
cs_stats: Any,
|
|
268
|
+
quantum: float,
|
|
269
|
+
threshold: float,
|
|
270
|
+
cs_H: int,
|
|
271
|
+
) -> Any:
|
|
272
|
+
"""Corrects phi_blk into phi_corr ((C, B) float64), updating the
|
|
273
|
+
per-channel circular history buffers in place."""
|
|
274
|
+
if phi_blk.ndim != 2:
|
|
275
|
+
raise ValueError(f"phi_blk must be 2-D (C, B), got shape {phi_blk.shape}")
|
|
276
|
+
C, B = phi_blk.shape
|
|
277
|
+
if C > 1024:
|
|
278
|
+
raise ValueError(f"channel count must be <= 1024, got {C}")
|
|
279
|
+
cs_H = int(cs_H)
|
|
280
|
+
for label, arr, dtype, shape in (
|
|
281
|
+
("phi_blk", phi_blk, cp.float64, (C, B)),
|
|
282
|
+
("phi_corr", phi_corr, cp.float64, (C, B)),
|
|
283
|
+
("cs_buf_y", cs_buf_y, cp.float64, (C, cs_H)),
|
|
284
|
+
("cs_buf_ptr", cs_buf_ptr, cp.int64, (C,)),
|
|
285
|
+
("cs_buf_n", cs_buf_n, cp.int64, (C,)),
|
|
286
|
+
("cs_stats", cs_stats, cp.float64, (C, 4)),
|
|
287
|
+
):
|
|
288
|
+
if arr.dtype != dtype:
|
|
289
|
+
raise TypeError(f"{label} must be {dtype}, got {arr.dtype}")
|
|
290
|
+
if arr.shape != shape:
|
|
291
|
+
raise ValueError(f"{label} must have shape {shape}, got {arr.shape}")
|
|
292
|
+
if not arr.flags.c_contiguous:
|
|
293
|
+
raise ValueError(f"{label} must be C-contiguous (mutated in place)")
|
|
294
|
+
kern(
|
|
295
|
+
(1,),
|
|
296
|
+
(C,),
|
|
297
|
+
(
|
|
298
|
+
phi_blk,
|
|
299
|
+
phi_corr,
|
|
300
|
+
cs_buf_y,
|
|
301
|
+
cs_buf_ptr,
|
|
302
|
+
cs_buf_n,
|
|
303
|
+
cs_stats,
|
|
304
|
+
cp.float64(quantum),
|
|
305
|
+
cp.float64(threshold),
|
|
306
|
+
cp.int32(cs_H),
|
|
307
|
+
cp.int32(B),
|
|
308
|
+
),
|
|
309
|
+
)
|
|
310
|
+
return phi_corr
|
|
311
|
+
|
|
312
|
+
return launch
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# name -> wrapper factory. Factories may raise; get_kernel translates any
|
|
316
|
+
# failure into the warn-once-and-return-None fallback contract.
|
|
317
|
+
_KERNEL_FACTORIES: dict[str, Callable[..., Callable]] = {
|
|
318
|
+
"selftest_scale": _selftest_scale_factory,
|
|
319
|
+
"bps_min_d2": _bps_min_d2_factory,
|
|
320
|
+
"cs_block": _cs_block_factory,
|
|
321
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lazy CUDA kernel compilation with in-process caching.
|
|
3
|
+
|
|
4
|
+
Kernel sources are real ``.cu`` files under ``commkit/_cuda/src/``
|
|
5
|
+
(shipped as package data) and are compiled via :class:`cupy.RawModule`
|
|
6
|
+
with C++17 and templates for mode/dtype specialization. ``name_expressions``
|
|
7
|
+
provides mangled-name resolution for the template instantiations.
|
|
8
|
+
|
|
9
|
+
Compilation is lazy: nothing here imports CuPy or touches NVRTC until
|
|
10
|
+
``get_raw_kernel()`` is called. Compiled kernels are cached per
|
|
11
|
+
``(source_name, options, name_expression)`` for the process lifetime;
|
|
12
|
+
cross-process reuse comes from CuPy's on-disk NVRTC cache.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from importlib import resources
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
DEFAULT_OPTIONS: tuple[str, ...] = ("-std=c++17", "--use_fast_math")
|
|
19
|
+
|
|
20
|
+
_SOURCE_CACHE: dict[str, str] = {}
|
|
21
|
+
# (source_name, options, name_expression) -> (RawModule, RawKernel).
|
|
22
|
+
# The module is cached alongside the kernel to keep the loaded CUmodule
|
|
23
|
+
# alive for the process lifetime.
|
|
24
|
+
_KERNEL_CACHE: dict[tuple[str, tuple[str, ...], str], tuple[Any, Any]] = {}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def read_source(source_name: str) -> str:
|
|
28
|
+
"""Loads a CUDA C++ source file from the package's ``src/`` directory.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
source_name : str
|
|
33
|
+
Base name of the source file, without the ``.cu`` extension
|
|
34
|
+
(e.g. ``"bps_min_d2"``).
|
|
35
|
+
|
|
36
|
+
Returns
|
|
37
|
+
-------
|
|
38
|
+
str
|
|
39
|
+
The file contents.
|
|
40
|
+
"""
|
|
41
|
+
src = _SOURCE_CACHE.get(source_name)
|
|
42
|
+
if src is None:
|
|
43
|
+
src = (
|
|
44
|
+
resources.files("commkit._cuda")
|
|
45
|
+
.joinpath(f"src/{source_name}.cu")
|
|
46
|
+
.read_text(encoding="utf-8")
|
|
47
|
+
)
|
|
48
|
+
_SOURCE_CACHE[source_name] = src
|
|
49
|
+
return src
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_raw_kernel(
|
|
53
|
+
source_name: str,
|
|
54
|
+
name_expression: str,
|
|
55
|
+
options: tuple[str, ...] = DEFAULT_OPTIONS,
|
|
56
|
+
) -> Any:
|
|
57
|
+
"""Compiles (or retrieves from cache) one kernel specialization.
|
|
58
|
+
|
|
59
|
+
Parameters
|
|
60
|
+
----------
|
|
61
|
+
source_name : str
|
|
62
|
+
Base name of the ``.cu`` file under ``src/``.
|
|
63
|
+
name_expression : str
|
|
64
|
+
C++ name expression for the kernel instantiation,
|
|
65
|
+
e.g. ``"bps_min_d2<TABLE>"``.
|
|
66
|
+
options : tuple of str, default ``DEFAULT_OPTIONS``
|
|
67
|
+
NVRTC compile options.
|
|
68
|
+
|
|
69
|
+
Returns
|
|
70
|
+
-------
|
|
71
|
+
cupy.RawKernel
|
|
72
|
+
The launchable kernel for `name_expression`.
|
|
73
|
+
"""
|
|
74
|
+
key = (source_name, options, name_expression)
|
|
75
|
+
cached = _KERNEL_CACHE.get(key)
|
|
76
|
+
if cached is not None:
|
|
77
|
+
return cached[1]
|
|
78
|
+
|
|
79
|
+
import cupy as cp
|
|
80
|
+
|
|
81
|
+
module = cp.RawModule(
|
|
82
|
+
code=read_source(source_name),
|
|
83
|
+
options=options,
|
|
84
|
+
name_expressions=[name_expression],
|
|
85
|
+
)
|
|
86
|
+
kernel = module.get_function(name_expression)
|
|
87
|
+
_KERNEL_CACHE[key] = (module, kernel)
|
|
88
|
+
return kernel
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Fused minimum-squared-distance kernel for Blind Phase Search (BPS) and
|
|
2
|
+
// decision-directed slicing.
|
|
3
|
+
//
|
|
4
|
+
// Computes, in a single pass over the input symbols:
|
|
5
|
+
//
|
|
6
|
+
// min_d2[p, c, n] = min_m | x[c, n] * phasor[p] - const[m] |^2
|
|
7
|
+
//
|
|
8
|
+
// replacing the materialized (.., P/B, M) candidate-distance tensors of the
|
|
9
|
+
// array-module implementations. The input symbols are read once and only the
|
|
10
|
+
// (P, C, N) float32 minima (plus optional int32 argmin indices) are written,
|
|
11
|
+
// removing the elementwise-chain DRAM traffic that dominates the xp path.
|
|
12
|
+
//
|
|
13
|
+
// Template modes
|
|
14
|
+
// MODE = MODE_TABLE : general constellation search over const[0..M-1],
|
|
15
|
+
// cooperatively staged in shared memory.
|
|
16
|
+
// MODE = MODE_GRID : square-QAM O(1) nearest point by per-component
|
|
17
|
+
// rounding onto the uniform level grid
|
|
18
|
+
// lev_min + k * d_grid, k in [0, side-1].
|
|
19
|
+
// RETURN_ARGMIN : additionally writes the index of the nearest point
|
|
20
|
+
// (TABLE: table index m; GRID: re_idx * side + im_idx).
|
|
21
|
+
//
|
|
22
|
+
// Layout contract (enforced by the Python wrapper):
|
|
23
|
+
// x (C, N) complex64, C-contiguous, time on the last axis
|
|
24
|
+
// phasor (P,) complex64, P <= 128
|
|
25
|
+
// constel (M,) complex64, M <= 1024 (TABLE mode only)
|
|
26
|
+
// min_d2 (P, C, N) float32
|
|
27
|
+
// argmin (P, C, N) int32 (RETURN_ARGMIN only)
|
|
28
|
+
//
|
|
29
|
+
// Launch contract: block = (BLOCK_N, 1, 1); grid = (ceil(N/BLOCK_N), P, C);
|
|
30
|
+
// dynamic shared memory = M * sizeof(complex<float>) in TABLE mode, 0 in GRID.
|
|
31
|
+
// Arithmetic is FP32 throughout, matching the float32 metric of the xp path.
|
|
32
|
+
|
|
33
|
+
#include <cupy/complex.cuh>
|
|
34
|
+
|
|
35
|
+
#define MODE_TABLE 0
|
|
36
|
+
#define MODE_GRID 1
|
|
37
|
+
|
|
38
|
+
template <int MODE, bool RETURN_ARGMIN>
|
|
39
|
+
__global__ void bps_min_d2(const complex<float>* __restrict__ x,
|
|
40
|
+
const complex<float>* __restrict__ phasor,
|
|
41
|
+
const complex<float>* __restrict__ constel,
|
|
42
|
+
const float lev_min,
|
|
43
|
+
const float d_grid,
|
|
44
|
+
const int side,
|
|
45
|
+
const int M,
|
|
46
|
+
const long long N,
|
|
47
|
+
float* __restrict__ min_d2,
|
|
48
|
+
int* __restrict__ argmin_out) {
|
|
49
|
+
extern __shared__ complex<float> s_const[];
|
|
50
|
+
|
|
51
|
+
const int p = blockIdx.y;
|
|
52
|
+
const int c = blockIdx.z;
|
|
53
|
+
|
|
54
|
+
if (MODE == MODE_TABLE) {
|
|
55
|
+
for (int m = threadIdx.x; m < M; m += blockDim.x) {
|
|
56
|
+
s_const[m] = constel[m];
|
|
57
|
+
}
|
|
58
|
+
__syncthreads();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const long long n =
|
|
62
|
+
static_cast<long long>(blockIdx.x) * blockDim.x + threadIdx.x;
|
|
63
|
+
if (n >= N) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Coalesced in n; the per-block phasor load is an L1-cached broadcast.
|
|
68
|
+
const complex<float> xr = x[static_cast<long long>(c) * N + n] * phasor[p];
|
|
69
|
+
const float re = xr.real();
|
|
70
|
+
const float im = xr.imag();
|
|
71
|
+
|
|
72
|
+
float best = 3.402823466e+38f; // FLT_MAX
|
|
73
|
+
int best_m = 0;
|
|
74
|
+
|
|
75
|
+
if (MODE == MODE_TABLE) {
|
|
76
|
+
// Warp-uniform m => shared-memory broadcast, no bank conflicts.
|
|
77
|
+
for (int m = 0; m < M; ++m) {
|
|
78
|
+
const float dr = re - s_const[m].real();
|
|
79
|
+
const float di = im - s_const[m].imag();
|
|
80
|
+
const float d2 = fmaf(dr, dr, di * di);
|
|
81
|
+
if (d2 < best) {
|
|
82
|
+
best = d2;
|
|
83
|
+
best_m = m;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} else { // MODE_GRID
|
|
87
|
+
const float hi = static_cast<float>(side - 1);
|
|
88
|
+
const float ri = fminf(fmaxf(roundf((re - lev_min) / d_grid), 0.0f), hi);
|
|
89
|
+
const float ii = fminf(fmaxf(roundf((im - lev_min) / d_grid), 0.0f), hi);
|
|
90
|
+
const float dr = re - fmaf(ri, d_grid, lev_min);
|
|
91
|
+
const float di = im - fmaf(ii, d_grid, lev_min);
|
|
92
|
+
best = fmaf(dr, dr, di * di);
|
|
93
|
+
if (RETURN_ARGMIN) {
|
|
94
|
+
best_m = static_cast<int>(ri) * side + static_cast<int>(ii);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const long long out_idx =
|
|
99
|
+
(static_cast<long long>(p) * gridDim.z + c) * N + n;
|
|
100
|
+
min_d2[out_idx] = best;
|
|
101
|
+
if (RETURN_ARGMIN) {
|
|
102
|
+
argmin_out[out_idx] = best_m;
|
|
103
|
+
}
|
|
104
|
+
}
|