Skip to content

Commit 8d9b1d4

Browse files
authored
Merge pull request #378 from PyAutoLabs/feature/kernel-forward-chunking
refactor: chunk the kernel-CDF forward transform (512-query blocks)
2 parents d547fe7 + 4ca8b76 commit 8d9b1d4

2 files changed

Lines changed: 56 additions & 2 deletions

File tree

autoarray/inversion/mesh/interpolator/rectangular_kernel.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,14 @@
5757
KERNEL_CDF_DEFAULT_BANDWIDTH: float = 1.0
5858
KERNEL_CDF_DEFAULT_KNOTS: int = 64
5959

60+
# Queries per block of the forward-transform evaluation. The exact kernel sum
61+
# broadcasts an (M, N, 2) array; blocking the query dimension caps the peak at
62+
# (block, N, 2) — 512 queries × 15.4k points × 2 axes × 8 B ≈ 126 MB at
63+
# production imaging scale (previously ~60 GB unblocked at M ≈ 246k). Values
64+
# are identical to float precision: the sum over points is unchanged, blocks
65+
# only tile the query axis.
66+
KERNEL_FORWARD_BLOCK: int = 512
67+
6068
_SQRT2 = np.sqrt(2.0)
6169

6270

@@ -119,10 +127,34 @@ def create_transforms_kernel(
119127
span = hi - lo
120128
h = bandwidth * span / mesh_pixels
121129

122-
def F_raw(q):
123-
t = (q[:, None, :] - points[None, :, :]) / h[None, None, :]
130+
def F_raw_block(q_block):
131+
t = (q_block[:, None, :] - points[None, :, :]) / h[None, None, :]
124132
return xp.sum(w[None, :, None] * _norm_cdf(t, xp), axis=1)
125133

134+
if xp.__name__.startswith("jax"):
135+
136+
def F_raw(q):
137+
import jax
138+
139+
M = q.shape[0]
140+
n_blocks = -(-M // KERNEL_FORWARD_BLOCK)
141+
pad = n_blocks * KERNEL_FORWARD_BLOCK - M
142+
q_padded = xp.pad(q, ((0, pad), (0, 0)))
143+
blocks = q_padded.reshape(n_blocks, KERNEL_FORWARD_BLOCK, 2)
144+
out = jax.lax.map(F_raw_block, blocks)
145+
return out.reshape(n_blocks * KERNEL_FORWARD_BLOCK, 2)[:M]
146+
147+
else:
148+
149+
def F_raw(q):
150+
return np.concatenate(
151+
[
152+
F_raw_block(q[i : i + KERNEL_FORWARD_BLOCK])
153+
for i in range(0, q.shape[0], KERNEL_FORWARD_BLOCK)
154+
],
155+
axis=0,
156+
)
157+
126158
# The unit square maps onto the data bounding box exactly, matching the
127159
# linear variant's convention (its empirical-CDF knots end at the extreme
128160
# points); the kernel tails outside [lo, hi] are absorbed by the rescale.

test_autoarray/inversion/pixelization/mesh/test_rectangular_kernel.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,3 +338,25 @@ def __getattr__(self, item):
338338
assert areas.shape == (36,)
339339
assert np.all(np.isfinite(areas))
340340
assert np.all(areas > 0.0)
341+
342+
343+
def test__create_transforms_kernel__chunked_forward_is_block_size_invariant():
344+
"""The forward transform blocks the query axis (KERNEL_FORWARD_BLOCK) to
345+
cap peak memory; results must be identical to evaluating queries one at a
346+
time — exercised across a query count spanning multiple blocks and not a
347+
multiple of the block size."""
348+
from autoarray.inversion.mesh.interpolator.rectangular_kernel import (
349+
KERNEL_FORWARD_BLOCK,
350+
)
351+
352+
rng = np.random.default_rng(7)
353+
data_grid = rng.standard_normal((77, 2))
354+
355+
fwd, _ = create_transforms_kernel(data_grid, mesh_pixels=8, xp=np)
356+
357+
q = rng.standard_normal((KERNEL_FORWARD_BLOCK + 137, 2))
358+
batched = fwd(q)
359+
row_wise = np.vstack([fwd(q[i : i + 1]) for i in range(q.shape[0])])
360+
361+
assert batched.shape == (KERNEL_FORWARD_BLOCK + 137, 2)
362+
assert batched == pytest.approx(row_wise, abs=0.0)

0 commit comments

Comments
 (0)