pixpoint 8.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.
- pixpoint/__init__.py +86 -0
- pixpoint/_numba.py +23 -0
- pixpoint/backend/__init__.py +22 -0
- pixpoint/backend/aperture.py +387 -0
- pixpoint/backend/combinations.py +177 -0
- pixpoint/backend/core.py +641 -0
- pixpoint/backend/degrade.py +139 -0
- pixpoint/backend/density.py +687 -0
- pixpoint/backend/moments.py +1262 -0
- pixpoint/backend/raw.py +407 -0
- pixpoint/backend/shear.py +532 -0
- pixpoint/backend/three_by_two.py +483 -0
- pixpoint/backend/threepoint.py +236 -0
- pixpoint/compute_context.py +176 -0
- pixpoint/correlation_helpers.py +784 -0
- pixpoint/correlations.py +6577 -0
- pixpoint/cuda/aperture_moments.cuh +68 -0
- pixpoint/cuda/aperture_tomo.cu +436 -0
- pixpoint/cuda/common.cuh +123 -0
- pixpoint/cuda/degrade_rows.cu +199 -0
- pixpoint/cuda/density_density_tomo_vectorized.cu +156 -0
- pixpoint/cuda/density_shear_tomo_vectorized.cu +255 -0
- pixpoint/cuda/moment_rows.cu +144 -0
- pixpoint/cuda/pair_tiles.cuh +989 -0
- pixpoint/cuda/threepoint_multipoles.cu +168 -0
- pixpoint/cuda/tomo_fused_3x2pt.cu +376 -0
- pixpoint/cuda/tomo_tiled_3x2pt.cu +161 -0
- pixpoint/cuda/tomo_vectorized_xipm.cu +277 -0
- pixpoint/global_2pcf.py +1118 -0
- pixpoint/global_3pcf.py +970 -0
- pixpoint/global_geometry.py +440 -0
- pixpoint/io_handler.py +607 -0
- pixpoint/moments.py +411 -0
- pixpoint/multi_device.py +334 -0
- pixpoint/onepoint.py +838 -0
- pixpoint/packing.py +317 -0
- pixpoint/pair_finder.py +210 -0
- pixpoint/pair_geometry.py +878 -0
- pixpoint/pipeline.py +288 -0
- pixpoint/py.typed +0 -0
- pixpoint/result_writer.py +481 -0
- pixpoint/results.py +445 -0
- pixpoint/treecode.py +416 -0
- pixpoint/utils.py +238 -0
- pixpoint/zeta_writer.py +222 -0
- pixpoint-8.0.0.dist-info/METADATA +169 -0
- pixpoint-8.0.0.dist-info/RECORD +50 -0
- pixpoint-8.0.0.dist-info/WHEEL +5 -0
- pixpoint-8.0.0.dist-info/licenses/LICENSE +21 -0
- pixpoint-8.0.0.dist-info/top_level.txt +1 -0
pixpoint/__init__.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PixPoint: GPU/CPU measurement of integrated, two- and three-point correlations.
|
|
3
|
+
|
|
4
|
+
Three estimators on HEALPix maps, named after the statistic they deliver:
|
|
5
|
+
:class:`Integrated3PCF` (per-patch aperture masses and 2PCFs, the i3PCF
|
|
6
|
+
ingredients), :class:`Global2PCF` (footprint-wide 2PCFs),
|
|
7
|
+
:class:`Global3PCF` (footprint-wide 3PCF multipoles) and :class:`OnePoint`
|
|
8
|
+
(PDFs and moments of aperture fields: ``g`` for the aperture mass, ``k`` for a
|
|
9
|
+
filtered scalar field). Methods are TreeCorr
|
|
10
|
+
field codes (``g`` = shear, ``k`` = scalar field): ``gg``, ``kk``, ``kg``,
|
|
11
|
+
``three_by_two_pt``, ``ggg``, ``kkk``, ``kgg``, ``kkg``. Every call returns a
|
|
12
|
+
labelled result (:class:`IntegratedResult`, :class:`TwoPointResult`,
|
|
13
|
+
:class:`ThreePointResult`, :class:`OnePointResult`).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .correlation_helpers import (
|
|
17
|
+
Q_crittenden,
|
|
18
|
+
Q_schneider,
|
|
19
|
+
U_crittenden,
|
|
20
|
+
U_schneider,
|
|
21
|
+
U_tophat,
|
|
22
|
+
calculate_all_zetas,
|
|
23
|
+
zeta_a_g,
|
|
24
|
+
zeta_a_minus,
|
|
25
|
+
zeta_a_plus,
|
|
26
|
+
zeta_a_t,
|
|
27
|
+
zeta_g_g,
|
|
28
|
+
zeta_g_minus,
|
|
29
|
+
zeta_g_plus,
|
|
30
|
+
zeta_g_t,
|
|
31
|
+
)
|
|
32
|
+
from .correlations import Integrated3PCF
|
|
33
|
+
from .global_2pcf import Global2PCF
|
|
34
|
+
from .global_3pcf import Global3PCF
|
|
35
|
+
from .onepoint import OnePoint
|
|
36
|
+
from .pipeline import MapFileLoader, MapLoader
|
|
37
|
+
from .result_writer import OnePointWriter, ResultWriter, result_arrays
|
|
38
|
+
from .results import (
|
|
39
|
+
Component,
|
|
40
|
+
IntegratedResult,
|
|
41
|
+
OnePointResult,
|
|
42
|
+
ThreePointResult,
|
|
43
|
+
TwoPointResult,
|
|
44
|
+
)
|
|
45
|
+
from .treecode import DEFAULT_RESOLUTION_FACTOR
|
|
46
|
+
from .utils import pixel2RaDec, select_patch_centers
|
|
47
|
+
from .zeta_writer import ZetaWriter
|
|
48
|
+
|
|
49
|
+
__version__ = "8.0.0"
|
|
50
|
+
__author__ = "David Gebauer"
|
|
51
|
+
__email__ = "git@gebauer.ai"
|
|
52
|
+
|
|
53
|
+
__all__ = [
|
|
54
|
+
"Component",
|
|
55
|
+
"Global2PCF",
|
|
56
|
+
"Global3PCF",
|
|
57
|
+
"Integrated3PCF",
|
|
58
|
+
"IntegratedResult",
|
|
59
|
+
"OnePoint",
|
|
60
|
+
"OnePointResult",
|
|
61
|
+
"OnePointWriter",
|
|
62
|
+
"ResultWriter",
|
|
63
|
+
"result_arrays",
|
|
64
|
+
"ThreePointResult",
|
|
65
|
+
"TwoPointResult",
|
|
66
|
+
"DEFAULT_RESOLUTION_FACTOR",
|
|
67
|
+
"MapLoader",
|
|
68
|
+
"MapFileLoader",
|
|
69
|
+
"Q_crittenden",
|
|
70
|
+
"Q_schneider",
|
|
71
|
+
"U_crittenden",
|
|
72
|
+
"U_schneider",
|
|
73
|
+
"U_tophat",
|
|
74
|
+
"calculate_all_zetas",
|
|
75
|
+
"zeta_g_plus",
|
|
76
|
+
"zeta_g_minus",
|
|
77
|
+
"zeta_a_plus",
|
|
78
|
+
"zeta_a_minus",
|
|
79
|
+
"zeta_g_g",
|
|
80
|
+
"zeta_a_g",
|
|
81
|
+
"zeta_g_t",
|
|
82
|
+
"zeta_a_t",
|
|
83
|
+
"pixel2RaDec",
|
|
84
|
+
"select_patch_centers",
|
|
85
|
+
"ZetaWriter",
|
|
86
|
+
]
|
pixpoint/_numba.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Numba entry points with types mypy can check.
|
|
2
|
+
|
|
3
|
+
``numba.prange`` is typed as a class without ``__iter__``; to a type checker
|
|
4
|
+
it is ``range`` (which is also what it is with ``NUMBA_DISABLE_JIT=1``). At
|
|
5
|
+
run time the jitted functions see the real ``numba.prange``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
9
|
+
|
|
10
|
+
import numba
|
|
11
|
+
from numba import njit
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
prange = range
|
|
15
|
+
else:
|
|
16
|
+
from numba import prange
|
|
17
|
+
|
|
18
|
+
#: ``numba.get_num_threads``, typed (numba < 0.68 leaves it untyped; going
|
|
19
|
+
#: through ``Any`` is correct for both).
|
|
20
|
+
_numba: Any = numba
|
|
21
|
+
get_num_threads: Callable[[], int] = _numba.get_num_threads
|
|
22
|
+
|
|
23
|
+
__all__ = ["get_num_threads", "njit", "prange"]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Backend abstraction layer for CPU (NumPy/Numba) and GPU (CuPy/CUDA) execution.
|
|
3
|
+
|
|
4
|
+
All correlation measurement kernels exist in two forms:
|
|
5
|
+
- **CPU**: Numba @njit functions with parallel=True for multi-core execution
|
|
6
|
+
- **GPU**: CuPy ElementwiseKernels for simple per-pair operations, and
|
|
7
|
+
compiled CUDA kernels (.cu files in cuda/) for fused/vectorised
|
|
8
|
+
tomographic operations, launched through :class:`raw.TypedKernel`
|
|
9
|
+
(every launch checked against the signature parsed from the source)
|
|
10
|
+
|
|
11
|
+
One module per kernel family, CPU kernel and GPU wrapper side by side:
|
|
12
|
+
:mod:`.aperture` (M_ap, M_g), :mod:`.density` (ξ_g, ξ_t), :mod:`.shear`
|
|
13
|
+
(ξ±), :mod:`.three_by_two` (the fused 3x2pt path), :mod:`.degrade` (treecode
|
|
14
|
+
rows), :mod:`.threepoint` (step 1 of Global3PCF); :mod:`.combinations` holds
|
|
15
|
+
the tomographic layouts of the tiled kernels, :mod:`.raw` compiling and
|
|
16
|
+
launching, :mod:`.core` the :class:`Backend` that holds the active kernel set
|
|
17
|
+
and provides to_device()/to_numpy() for data movement.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from .core import Backend, get_backend, safe_divide
|
|
21
|
+
|
|
22
|
+
__all__ = ["Backend", "get_backend", "safe_divide"]
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
"""Aperture statistics M_ap (shear) and M_g (density) per patch.
|
|
2
|
+
|
|
3
|
+
CPU: Numba kernels. GPU: per-pixel ``ElementwiseKernel`` contributions and
|
|
4
|
+
the block-reduced tomographic kernels of ``cuda/aperture_tomo.cu``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Optional, Tuple
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from .._numba import njit, prange
|
|
12
|
+
from .raw import _CUPY_FASTMATH_OPTIONS, KernelFile, _has_raw_cuda_compiler, c_real
|
|
13
|
+
|
|
14
|
+
# Beyond this many tomographic bins the fused aperture kernels (one block per
|
|
15
|
+
# patch, the bins looped inside it) hold too many per-thread accumulators --
|
|
16
|
+
# 2*NZ each -- and the per-(patch, bin) kernel is launched instead.
|
|
17
|
+
_MAX_FUSED_APERTURE_BINS = 16
|
|
18
|
+
# Map arguments the aperture kernels address through explicit strides
|
|
19
|
+
# (planar, strided and AoS views are all passed without a copy).
|
|
20
|
+
_STRIDED = ("g1", "g2", "weights", "values")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ── Aperture statistics kernels ──────────────────────────────────────────
|
|
24
|
+
# Compute the aperture-filtered field for each sky patch:
|
|
25
|
+
# M = A_patch · Σ(w · field · Q(θ)) / Σ(w)
|
|
26
|
+
# where Q(θ) is the compensated filter function.
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@njit(fastmath=True, parallel=True, cache=True)
|
|
30
|
+
def _cpu_aperture_density_kernel(
|
|
31
|
+
Q_inds: np.ndarray,
|
|
32
|
+
Q_val: np.ndarray,
|
|
33
|
+
Q_offsets: np.ndarray,
|
|
34
|
+
map_values: np.ndarray,
|
|
35
|
+
weights: np.ndarray,
|
|
36
|
+
Q_patch_area: np.ndarray,
|
|
37
|
+
out_aperture: np.ndarray,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Galaxy mean density M_g within an aperture: weighted δ_g convolved
|
|
40
|
+
with the compensated filter Q(θ)."""
|
|
41
|
+
n_patches = Q_offsets.shape[0] - 1
|
|
42
|
+
# float64 explicitly, not ``x[0] * 0.0``. That expression types as
|
|
43
|
+
# float64 under numba (python-float promotion) but as float32 under
|
|
44
|
+
# NUMBA_DISABLE_JIT=1, which pytest-env forces -- so the suite was
|
|
45
|
+
# measuring a float32 accumulation of a kernel that ships accumulating in
|
|
46
|
+
# float64. Pinning it keeps the shipped numbers and makes the tests
|
|
47
|
+
# exercise them. (It is deliberately not ``acc_dtype``: these four
|
|
48
|
+
# kernels reduce over a whole aperture disc or angular bin, where a
|
|
49
|
+
# float32 accumulator loses precision for nothing.)
|
|
50
|
+
zero = np.float64(0)
|
|
51
|
+
|
|
52
|
+
for patch_idx in prange(n_patches):
|
|
53
|
+
start = Q_offsets[patch_idx]
|
|
54
|
+
stop = Q_offsets[patch_idx + 1]
|
|
55
|
+
sum_w = zero
|
|
56
|
+
sum_wdelta_q = zero
|
|
57
|
+
for i in range(start, stop):
|
|
58
|
+
pix_idx = Q_inds[i]
|
|
59
|
+
weight = weights[pix_idx]
|
|
60
|
+
sum_w += weight
|
|
61
|
+
sum_wdelta_q += weight * map_values[pix_idx] * Q_val[i]
|
|
62
|
+
# 0, not NaN, for an empty aperture -- like every other M_g path
|
|
63
|
+
if sum_w == 0:
|
|
64
|
+
out_aperture[patch_idx] = 0.0
|
|
65
|
+
else:
|
|
66
|
+
out_aperture[patch_idx] = Q_patch_area[patch_idx] * sum_wdelta_q / sum_w
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@njit(fastmath=True, parallel=True, cache=True)
|
|
70
|
+
def _cpu_aperture_shear_kernel(
|
|
71
|
+
Q_inds: np.ndarray,
|
|
72
|
+
Q_cos: np.ndarray,
|
|
73
|
+
Q_sin: np.ndarray,
|
|
74
|
+
Q_val: np.ndarray,
|
|
75
|
+
Q_offsets: np.ndarray,
|
|
76
|
+
g1: np.ndarray,
|
|
77
|
+
g2: np.ndarray,
|
|
78
|
+
weights: np.ndarray,
|
|
79
|
+
Q_patch_area: np.ndarray,
|
|
80
|
+
out_aperture: np.ndarray,
|
|
81
|
+
) -> None:
|
|
82
|
+
"""Aperture mass M_ap: tangential shear γ_t convolved with Q(θ).
|
|
83
|
+
|
|
84
|
+
γ_t = -γ₁·cos(2φ) - γ₂·sin(2φ) is the tangential shear component
|
|
85
|
+
relative to the patch centre.
|
|
86
|
+
"""
|
|
87
|
+
n_patches = Q_offsets.shape[0] - 1
|
|
88
|
+
# float64 explicitly, not ``x[0] * 0.0``. That expression types as
|
|
89
|
+
# float64 under numba (python-float promotion) but as float32 under
|
|
90
|
+
# NUMBA_DISABLE_JIT=1, which pytest-env forces -- so the suite was
|
|
91
|
+
# measuring a float32 accumulation of a kernel that ships accumulating in
|
|
92
|
+
# float64. Pinning it keeps the shipped numbers and makes the tests
|
|
93
|
+
# exercise them. (It is deliberately not ``acc_dtype``: these four
|
|
94
|
+
# kernels reduce over a whole aperture disc or angular bin, where a
|
|
95
|
+
# float32 accumulator loses precision for nothing.)
|
|
96
|
+
zero = np.float64(0)
|
|
97
|
+
|
|
98
|
+
for patch_idx in prange(n_patches):
|
|
99
|
+
start = Q_offsets[patch_idx]
|
|
100
|
+
stop = Q_offsets[patch_idx + 1]
|
|
101
|
+
sum_w = zero
|
|
102
|
+
sum_wgt_q = zero
|
|
103
|
+
for i in range(start, stop):
|
|
104
|
+
pix_idx = Q_inds[i]
|
|
105
|
+
weight = weights[pix_idx]
|
|
106
|
+
# Tangential shear projection
|
|
107
|
+
gt = -g1[pix_idx] * Q_cos[i] - g2[pix_idx] * Q_sin[i]
|
|
108
|
+
sum_w += weight
|
|
109
|
+
sum_wgt_q += weight * gt * Q_val[i]
|
|
110
|
+
out_aperture[patch_idx] = Q_patch_area[patch_idx] * sum_wgt_q / sum_w
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ── CuPy ElementwiseKernel builders (GPU per-element operations) ──────────
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _build_cupy_aperture_density_kernel(module: Any) -> Any:
|
|
117
|
+
"""GPU kernel: per-pixel contribution to galaxy mean density M_g.
|
|
118
|
+
|
|
119
|
+
The filter geometry arrays (type letter ``Q``) may be narrower than the
|
|
120
|
+
map type ``T``; the ``(T)`` promotion at use is exact for float32 →
|
|
121
|
+
float64.
|
|
122
|
+
"""
|
|
123
|
+
return module.ElementwiseKernel(
|
|
124
|
+
"raw I Q_inds, raw Q Q_val, raw T map_values, raw T weights",
|
|
125
|
+
"T out_num, T out_den",
|
|
126
|
+
"""
|
|
127
|
+
const I idx = Q_inds[i];
|
|
128
|
+
const T w = weights[idx];
|
|
129
|
+
out_num = w * map_values[idx] * (T)Q_val[i];
|
|
130
|
+
out_den = w;
|
|
131
|
+
""",
|
|
132
|
+
"gpu_aperture_density_kernel",
|
|
133
|
+
options=_CUPY_FASTMATH_OPTIONS,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _build_cupy_aperture_shear_kernel(module: Any) -> Any:
|
|
138
|
+
"""GPU kernel: per-pixel contribution to aperture mass M_ap via γ_t·Q(θ).
|
|
139
|
+
|
|
140
|
+
The filter geometry arrays (type letter ``Q``) may be narrower than the
|
|
141
|
+
map type ``T``; the ``(T)`` promotion at use is exact for float32 →
|
|
142
|
+
float64.
|
|
143
|
+
"""
|
|
144
|
+
return module.ElementwiseKernel(
|
|
145
|
+
"raw I Q_inds, raw Q Q_cos, raw Q Q_sin, raw Q Q_val,"
|
|
146
|
+
" raw T g1, raw T g2, raw T weights",
|
|
147
|
+
"T out_num, T out_den",
|
|
148
|
+
"""
|
|
149
|
+
const I idx = Q_inds[i];
|
|
150
|
+
const T w = weights[idx];
|
|
151
|
+
const T gt = -g1[idx] * (T)Q_cos[i] - g2[idx] * (T)Q_sin[i];
|
|
152
|
+
out_num = w * gt * (T)Q_val[i];
|
|
153
|
+
out_den = w;
|
|
154
|
+
""",
|
|
155
|
+
"gpu_aperture_shear_kernel",
|
|
156
|
+
options=_CUPY_FASTMATH_OPTIONS,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ── Block-reduced aperture RawKernels (all tomo bins in one launch) ───────
|
|
161
|
+
# Replace the per-pixel ElementwiseKernel + add.reduceat path with a single
|
|
162
|
+
# launch over (patch, tomo bin) blocks; the wrappers fall back (return
|
|
163
|
+
# False) when no raw compiler is available so callers can use the legacy
|
|
164
|
+
# ElementwiseKernel path.
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _aperture_tomo_prepare_planar(module: Any, arr: Any) -> Tuple[Any, int, int]:
|
|
168
|
+
"""Return (array, bin stride, row stride) in elements for a 2D view.
|
|
169
|
+
|
|
170
|
+
The kernel takes both strides, so nothing is ever copied: planar
|
|
171
|
+
``(nz, npix)`` arrays, strided views such as ``shear[:, 0]`` of an
|
|
172
|
+
``(nz, 2, npix)`` array, and the transposed AoS buffers
|
|
173
|
+
``shear_aos[:, :, 0].T`` of an ``(npix, nz, 2)`` array all work.
|
|
174
|
+
"""
|
|
175
|
+
del module # no copy is needed for any layout
|
|
176
|
+
return (
|
|
177
|
+
arr,
|
|
178
|
+
int(arr.strides[0] // arr.itemsize),
|
|
179
|
+
int(arr.strides[1] // arr.itemsize),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# (runtime id, device) -> (runtime, SM count). The runtime object is kept
|
|
184
|
+
# alive by the value so its id cannot be reused by a different one.
|
|
185
|
+
_SM_COUNT_CACHE: dict[Tuple[int, int], Tuple[Any, Optional[int]]] = {}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _multiprocessor_count(module: Any) -> Optional[int]:
|
|
189
|
+
"""SM count of the active device, or None when it cannot be queried.
|
|
190
|
+
|
|
191
|
+
Only used to size a launch grid, so a stand-in module without
|
|
192
|
+
``cuda.runtime`` (the emulated cupy of the tests) simply imposes no
|
|
193
|
+
constraint.
|
|
194
|
+
"""
|
|
195
|
+
runtime = getattr(getattr(module, "cuda", None), "runtime", None)
|
|
196
|
+
if runtime is None:
|
|
197
|
+
return None
|
|
198
|
+
try:
|
|
199
|
+
device = int(runtime.getDevice())
|
|
200
|
+
except Exception: # pragma: no cover - no device behind the module
|
|
201
|
+
return None
|
|
202
|
+
key = (id(runtime), device)
|
|
203
|
+
if key not in _SM_COUNT_CACHE:
|
|
204
|
+
try:
|
|
205
|
+
props = runtime.getDeviceProperties(device)
|
|
206
|
+
count: Optional[int] = int(props["multiProcessorCount"])
|
|
207
|
+
except Exception: # pragma: no cover - older runtime without the field
|
|
208
|
+
count = None
|
|
209
|
+
_SM_COUNT_CACHE[key] = (runtime, count)
|
|
210
|
+
return _SM_COUNT_CACHE[key][1]
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _use_fused_aperture(module: Any, ntomo: int, npatches: int) -> bool:
|
|
214
|
+
"""Whether to launch the one-block-per-patch aperture kernel.
|
|
215
|
+
|
|
216
|
+
It reads the aperture disc geometry once per pixel instead of once per
|
|
217
|
+
tomographic bin: 1.72x on the kernel at nside 512 and 1.83x measured in
|
|
218
|
+
situ inside ``gg`` (the result is bitwise identical
|
|
219
|
+
either way). Two things take it back to the
|
|
220
|
+
per-(patch, bin) kernel: a bin set wide enough for the per-thread
|
|
221
|
+
accumulators to spill, and a patch set small enough that one block per
|
|
222
|
+
patch underfills the device -- the fused grid is ``ntomo`` times
|
|
223
|
+
smaller, which is the point of it.
|
|
224
|
+
"""
|
|
225
|
+
if ntomo <= 0 or ntomo > _MAX_FUSED_APERTURE_BINS:
|
|
226
|
+
return False
|
|
227
|
+
n_sm = _multiprocessor_count(module)
|
|
228
|
+
if n_sm is not None and npatches < 2 * n_sm:
|
|
229
|
+
return False
|
|
230
|
+
return True
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _build_cupy_aperture_tomo_shear_kernel(module: Any) -> Any:
|
|
234
|
+
"""Builder for the GPU block-reduced tomographic aperture-mass kernel.
|
|
235
|
+
|
|
236
|
+
Two kernels behind one launch contract (see ``aperture_tomo.cu``): the
|
|
237
|
+
fused one-block-per-patch kernel is the default and reads the disc
|
|
238
|
+
geometry once per pixel instead of once per tomographic bin; the
|
|
239
|
+
per-(patch, bin) kernel is the fallback for wide bin sets and small
|
|
240
|
+
patch counts. The two are bitwise identical, so which one ran is never
|
|
241
|
+
observable in the result. ``ntomo`` is a template argument of the
|
|
242
|
+
fused kernel and therefore part of the cache key; it is small and
|
|
243
|
+
stable, so compiling on demand per value is fine.
|
|
244
|
+
"""
|
|
245
|
+
kernels = KernelFile(
|
|
246
|
+
module, "aperture_tomo.cu", "Aperture-shear tomo", strided=_STRIDED
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
def _cupy_aperture_tomo_shear_kernel(
|
|
250
|
+
g1: Any,
|
|
251
|
+
g2: Any,
|
|
252
|
+
weights: Any,
|
|
253
|
+
q_inds: Any,
|
|
254
|
+
q_cos: Any,
|
|
255
|
+
q_sin: Any,
|
|
256
|
+
q_val: Any,
|
|
257
|
+
q_offsets: Any,
|
|
258
|
+
q_patch_area: Any,
|
|
259
|
+
out_num: Any,
|
|
260
|
+
out_den: Any,
|
|
261
|
+
) -> bool:
|
|
262
|
+
if not _has_raw_cuda_compiler(module):
|
|
263
|
+
return False
|
|
264
|
+
if g1.ndim != 2 or g2.ndim != 2 or weights.ndim != 2:
|
|
265
|
+
return False
|
|
266
|
+
|
|
267
|
+
targs: Tuple[Any, ...] = (c_real(weights.dtype), c_real(q_cos.dtype))
|
|
268
|
+
npatches = int(q_offsets.shape[0] - 1)
|
|
269
|
+
ntomo = int(g1.shape[0])
|
|
270
|
+
fused = _use_fused_aperture(module, ntomo, npatches)
|
|
271
|
+
raw_kernel = (
|
|
272
|
+
kernels("gpu_aperture_shear_tomo_fused", targs + (ntomo,))
|
|
273
|
+
if fused
|
|
274
|
+
else kernels("gpu_aperture_shear_tomo", targs)
|
|
275
|
+
)
|
|
276
|
+
if raw_kernel is None:
|
|
277
|
+
return False
|
|
278
|
+
|
|
279
|
+
g1, g1_stride, g1_elem = _aperture_tomo_prepare_planar(module, g1)
|
|
280
|
+
g2, g2_stride, g2_elem = _aperture_tomo_prepare_planar(module, g2)
|
|
281
|
+
if (g1_stride, g1_elem) != (g2_stride, g2_elem):
|
|
282
|
+
g1 = module.ascontiguousarray(g1)
|
|
283
|
+
g2 = module.ascontiguousarray(g2)
|
|
284
|
+
g1_stride = g2_stride = int(g1.shape[1])
|
|
285
|
+
g1_elem = g2_elem = 1
|
|
286
|
+
weights, w_stride, w_elem = _aperture_tomo_prepare_planar(module, weights)
|
|
287
|
+
|
|
288
|
+
blocks = (
|
|
289
|
+
(max(1, npatches), 1, 1) if fused else (max(1, npatches), max(1, ntomo), 1)
|
|
290
|
+
)
|
|
291
|
+
# The fused kernel knows NZ at compile time and takes no `ntomo`.
|
|
292
|
+
tail: Tuple[int, ...] = (npatches,) if fused else (npatches, ntomo)
|
|
293
|
+
raw_kernel(
|
|
294
|
+
blocks,
|
|
295
|
+
(256,),
|
|
296
|
+
(
|
|
297
|
+
g1,
|
|
298
|
+
g2,
|
|
299
|
+
g1_stride,
|
|
300
|
+
g1_elem,
|
|
301
|
+
weights,
|
|
302
|
+
w_stride,
|
|
303
|
+
w_elem,
|
|
304
|
+
q_inds,
|
|
305
|
+
q_cos,
|
|
306
|
+
q_sin,
|
|
307
|
+
q_val,
|
|
308
|
+
q_offsets,
|
|
309
|
+
q_patch_area,
|
|
310
|
+
out_num,
|
|
311
|
+
out_den,
|
|
312
|
+
*tail,
|
|
313
|
+
),
|
|
314
|
+
)
|
|
315
|
+
return True
|
|
316
|
+
|
|
317
|
+
return _cupy_aperture_tomo_shear_kernel
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _build_cupy_aperture_tomo_density_kernel(module: Any) -> Any:
|
|
321
|
+
"""Builder for the GPU block-reduced tomographic aperture-density kernel.
|
|
322
|
+
|
|
323
|
+
Same two-kernel contract as the aperture-mass builder above: the fused
|
|
324
|
+
one-block-per-patch kernel by default, the per-(patch, bin) kernel as
|
|
325
|
+
the fallback, bitwise identical results either way.
|
|
326
|
+
"""
|
|
327
|
+
kernels = KernelFile(
|
|
328
|
+
module, "aperture_tomo.cu", "Aperture-density tomo", strided=_STRIDED
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
def _cupy_aperture_tomo_density_kernel(
|
|
332
|
+
values: Any,
|
|
333
|
+
weights: Any,
|
|
334
|
+
q_inds: Any,
|
|
335
|
+
q_val: Any,
|
|
336
|
+
q_offsets: Any,
|
|
337
|
+
q_patch_area: Any,
|
|
338
|
+
out_num: Any,
|
|
339
|
+
out_den: Any,
|
|
340
|
+
) -> bool:
|
|
341
|
+
if not _has_raw_cuda_compiler(module):
|
|
342
|
+
return False
|
|
343
|
+
if values.ndim != 2 or weights.ndim != 2:
|
|
344
|
+
return False
|
|
345
|
+
|
|
346
|
+
targs: Tuple[Any, ...] = (c_real(weights.dtype), c_real(q_val.dtype))
|
|
347
|
+
npatches = int(q_offsets.shape[0] - 1)
|
|
348
|
+
ntomo = int(values.shape[0])
|
|
349
|
+
fused = _use_fused_aperture(module, ntomo, npatches)
|
|
350
|
+
raw_kernel = (
|
|
351
|
+
kernels("gpu_aperture_density_tomo_fused", targs + (ntomo,))
|
|
352
|
+
if fused
|
|
353
|
+
else kernels("gpu_aperture_density_tomo", targs)
|
|
354
|
+
)
|
|
355
|
+
if raw_kernel is None:
|
|
356
|
+
return False
|
|
357
|
+
|
|
358
|
+
values, v_stride, v_elem = _aperture_tomo_prepare_planar(module, values)
|
|
359
|
+
weights, w_stride, w_elem = _aperture_tomo_prepare_planar(module, weights)
|
|
360
|
+
|
|
361
|
+
blocks = (
|
|
362
|
+
(max(1, npatches), 1, 1) if fused else (max(1, npatches), max(1, ntomo), 1)
|
|
363
|
+
)
|
|
364
|
+
# The fused kernel knows NZ at compile time and takes no `ntomo`.
|
|
365
|
+
tail: Tuple[int, ...] = (npatches,) if fused else (npatches, ntomo)
|
|
366
|
+
raw_kernel(
|
|
367
|
+
blocks,
|
|
368
|
+
(256,),
|
|
369
|
+
(
|
|
370
|
+
values,
|
|
371
|
+
v_stride,
|
|
372
|
+
v_elem,
|
|
373
|
+
weights,
|
|
374
|
+
w_stride,
|
|
375
|
+
w_elem,
|
|
376
|
+
q_inds,
|
|
377
|
+
q_val,
|
|
378
|
+
q_offsets,
|
|
379
|
+
q_patch_area,
|
|
380
|
+
out_num,
|
|
381
|
+
out_den,
|
|
382
|
+
*tail,
|
|
383
|
+
),
|
|
384
|
+
)
|
|
385
|
+
return True
|
|
386
|
+
|
|
387
|
+
return _cupy_aperture_tomo_density_kernel
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Tomographic combination layouts of the combination-tiled pair kernels.
|
|
2
|
+
|
|
3
|
+
A tiled kernel produces a *canonical* set of combinations (the row-major
|
|
4
|
+
upper triangle of one bin set, or the full product of two); a requested list
|
|
5
|
+
is classified against it once and, when it is a subset, gathered from a
|
|
6
|
+
canonical temporary.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Any, Callable, Optional, Tuple, cast
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
from ..utils import live_object_serial
|
|
14
|
+
from .raw import _KERNEL_CACHE_MISS
|
|
15
|
+
|
|
16
|
+
_MAX_VECTOR_TOMO_BINS = 64
|
|
17
|
+
# Combination-tiled pair kernels keep every accumulator in a register; the
|
|
18
|
+
# tiles are used only while the per-thread accumulator count (numerators +
|
|
19
|
+
# weight sums over all combinations) stays within this budget. Beyond it
|
|
20
|
+
# the per-(bin, row) kernels are launched instead (unpacked geometry only).
|
|
21
|
+
_MAX_TILED_ACCUMULATORS = 64
|
|
22
|
+
# Budget of the single-pass 3x2pt tile (xi+- + xi_g + xi_t accumulators).
|
|
23
|
+
# Measured on an A100 up to 120 (4 source x 6 lens bins, all combinations),
|
|
24
|
+
# where it is still ahead of three separate tiles; untested beyond.
|
|
25
|
+
_MAX_TILED_3X2PT_ACCUMULATORS = 120
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _CombinationLayout:
|
|
29
|
+
"""How a requested list of tomographic combinations maps onto the
|
|
30
|
+
canonical layout generated by the combination-tiled kernels."""
|
|
31
|
+
|
|
32
|
+
__slots__ = ("mode", "canonical_ncomb", "rows")
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self, mode: str, canonical_ncomb: int, rows: Optional[np.ndarray]
|
|
36
|
+
) -> None:
|
|
37
|
+
self.mode = mode # "triangle" | "auto" | "cartesian"
|
|
38
|
+
self.canonical_ncomb = canonical_ncomb # rows the tile produces
|
|
39
|
+
self.rows = rows # canonical row per request, None = identity
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TiledWrapper:
|
|
43
|
+
"""A pair-kernel launch wrapper whose combination-tiled kernel can be
|
|
44
|
+
switched off: ``tiled = False`` forces the per-(bin, row) fallback
|
|
45
|
+
(tests and benchmarks compare the two)."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, launch: Callable[..., bool]) -> None:
|
|
48
|
+
self.tiled = True
|
|
49
|
+
self._launch = launch
|
|
50
|
+
|
|
51
|
+
def __call__(self, *args: Any) -> bool:
|
|
52
|
+
return self._launch(*args)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class SinglePassWrapper:
|
|
56
|
+
"""The launch wrapper of the single-pass 3x2pt tile. ``mode``:
|
|
57
|
+
``"auto"`` (within ``max_accumulators``), ``"single"`` (always, for
|
|
58
|
+
benchmarks) or ``"off"``; it returns which of (ξ±, ξ_g, ξ_t) it computed."""
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self, launch: Callable[..., Tuple[bool, bool, bool]], max_accumulators: int
|
|
62
|
+
) -> None:
|
|
63
|
+
self.mode = "auto"
|
|
64
|
+
self.max_accumulators = max_accumulators
|
|
65
|
+
self._launch = launch
|
|
66
|
+
|
|
67
|
+
def __call__(self, *args: Any) -> Tuple[bool, bool, bool]:
|
|
68
|
+
return self._launch(*args)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
_COMBINATION_LAYOUT_CACHE: dict[
|
|
72
|
+
tuple[int, int, str, int, int], Optional[_CombinationLayout]
|
|
73
|
+
] = {}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _combination_layout(
|
|
77
|
+
comb_i: Any, comb_j: Any, kind: str, n1: int, n2: int = 0
|
|
78
|
+
) -> Optional[_CombinationLayout]:
|
|
79
|
+
"""Classify ``(comb_i, comb_j)`` against the tiled kernels' layouts.
|
|
80
|
+
|
|
81
|
+
``kind="triangle"``: n1 bins, canonical = row-major upper triangle
|
|
82
|
+
(a request (i, j) with i > j is the same symmetric combination as
|
|
83
|
+
(j, i)); a request consisting of exactly the auto combinations gets
|
|
84
|
+
``mode="auto"`` (its own, smaller tile). ``kind="cartesian"``:
|
|
85
|
+
canonical = row-major n1 x n2 product. ``rows`` is None when the
|
|
86
|
+
request *is* the canonical layout, else the canonical row of every
|
|
87
|
+
requested combination. Returns None for out-of-range bins.
|
|
88
|
+
|
|
89
|
+
The (tiny) device arrays are compared once per array object and the
|
|
90
|
+
result cached by identity: the orchestrator caches and reuses them.
|
|
91
|
+
|
|
92
|
+
By ``live_object_serial`` and not ``id()``. The orchestrator caches the
|
|
93
|
+
*canonical* combination arrays, but an explicit ``ggl_bin_combinations``
|
|
94
|
+
selection builds fresh ones on every call and drops them, so a second
|
|
95
|
+
selection can land on the recycled addresses of the first -- and the
|
|
96
|
+
layout carries ``rows``, the canonical row of every requested
|
|
97
|
+
combination, so a stale hit scatters the results into the previous
|
|
98
|
+
selection's rows. The collision needs *both* ids to recycle at once and
|
|
99
|
+
CPython's LIFO free lists tend to swap rather than match them (it did not
|
|
100
|
+
fire in 4000 alternating calls here), but it is allocator luck, not a
|
|
101
|
+
guarantee.
|
|
102
|
+
"""
|
|
103
|
+
key = (
|
|
104
|
+
live_object_serial(comb_i),
|
|
105
|
+
live_object_serial(comb_j),
|
|
106
|
+
kind,
|
|
107
|
+
int(n1),
|
|
108
|
+
int(n2),
|
|
109
|
+
)
|
|
110
|
+
cached = _COMBINATION_LAYOUT_CACHE.get(key, _KERNEL_CACHE_MISS)
|
|
111
|
+
if cached is not _KERNEL_CACHE_MISS:
|
|
112
|
+
return cast(Optional[_CombinationLayout], cached)
|
|
113
|
+
|
|
114
|
+
def to_host(a: Any) -> np.ndarray:
|
|
115
|
+
return np.asarray(a.get() if hasattr(a, "get") else a).astype(np.int64)
|
|
116
|
+
|
|
117
|
+
ci, cj = to_host(comb_i), to_host(comb_j)
|
|
118
|
+
layout: Optional[_CombinationLayout]
|
|
119
|
+
if kind == "triangle":
|
|
120
|
+
if (
|
|
121
|
+
ci.size == 0
|
|
122
|
+
or ci.min() < 0
|
|
123
|
+
or cj.min() < 0
|
|
124
|
+
or ci.max() >= n1
|
|
125
|
+
or cj.max() >= n1
|
|
126
|
+
):
|
|
127
|
+
layout = None
|
|
128
|
+
else:
|
|
129
|
+
lo, hi = np.minimum(ci, cj), np.maximum(ci, cj)
|
|
130
|
+
rows = lo * n1 - (lo * (lo - 1)) // 2 + (hi - lo)
|
|
131
|
+
canonical = (n1 * (n1 + 1)) // 2
|
|
132
|
+
identity = rows.size == canonical and np.array_equal(
|
|
133
|
+
rows, np.arange(canonical)
|
|
134
|
+
)
|
|
135
|
+
if (
|
|
136
|
+
not identity
|
|
137
|
+
and ci.size == n1
|
|
138
|
+
and np.array_equal(ci, np.arange(n1))
|
|
139
|
+
and np.array_equal(cj, ci)
|
|
140
|
+
):
|
|
141
|
+
layout = _CombinationLayout("auto", int(n1), None)
|
|
142
|
+
else:
|
|
143
|
+
layout = _CombinationLayout(
|
|
144
|
+
"triangle", int(canonical), None if identity else rows
|
|
145
|
+
)
|
|
146
|
+
elif kind == "cartesian":
|
|
147
|
+
if (
|
|
148
|
+
ci.size == 0
|
|
149
|
+
or ci.min() < 0
|
|
150
|
+
or cj.min() < 0
|
|
151
|
+
or ci.max() >= n1
|
|
152
|
+
or cj.max() >= n2
|
|
153
|
+
):
|
|
154
|
+
layout = None
|
|
155
|
+
else:
|
|
156
|
+
rows = ci * n2 + cj
|
|
157
|
+
canonical = int(n1 * n2)
|
|
158
|
+
identity = rows.size == canonical and np.array_equal(
|
|
159
|
+
rows, np.arange(canonical)
|
|
160
|
+
)
|
|
161
|
+
layout = _CombinationLayout(
|
|
162
|
+
"cartesian", canonical, None if identity else rows
|
|
163
|
+
)
|
|
164
|
+
else:
|
|
165
|
+
raise ValueError(f"unknown combination layout kind {kind!r}")
|
|
166
|
+
if len(_COMBINATION_LAYOUT_CACHE) > 64:
|
|
167
|
+
_COMBINATION_LAYOUT_CACHE.clear()
|
|
168
|
+
_COMBINATION_LAYOUT_CACHE[key] = layout
|
|
169
|
+
return layout
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _scatter_canonical_rows(
|
|
173
|
+
module: Any, layout: _CombinationLayout, tmp: Any, out: Any
|
|
174
|
+
) -> None:
|
|
175
|
+
"""out[r] = tmp[layout.rows[r]] (tmp holds the canonical rows)."""
|
|
176
|
+
rows = module.asarray(layout.rows)
|
|
177
|
+
out[...] = tmp[rows]
|