Skip to content

Instantly share code, notes, and snippets.

@mwyau
Last active August 15, 2026 16:02
Show Gist options
  • Select an option

  • Save mwyau/aca47422d2d8acdc13837082eec60782 to your computer and use it in GitHub Desktop.

Select an option

Save mwyau/aca47422d2d8acdc13837082eec60782 to your computer and use it in GitHub Desktop.
DUCC0 ARM64 Build Benchmark
import time
import numpy as np
import ducc0
def bench_fft():
res = {}
# 1D FFT
a1 = (np.random.randn(1000000) + 1j * np.random.randn(1000000)).astype(np.complex128)
for nthreads in [1, 4]:
t0 = time.perf_counter()
iters = 50
for _ in range(iters):
ducc0.fft.c2c(a1, nthreads=nthreads)
el = (time.perf_counter() - t0) / iters
res[f"fft_1d_1M_threads_{nthreads}"] = el
# 2D FFT
a2 = (np.random.randn(1024, 1024) + 1j * np.random.randn(1024, 1024)).astype(np.complex128)
for nthreads in [1, 4]:
t0 = time.perf_counter()
iters = 30
for _ in range(iters):
ducc0.fft.c2c(a2, axes=(0, 1), nthreads=nthreads)
el = (time.perf_counter() - t0) / iters
res[f"fft_2d_1024x1024_threads_{nthreads}"] = el
# 3D FFT
a3 = (np.random.randn(128, 128, 128) + 1j * np.random.randn(128, 128, 128)).astype(np.complex128)
for nthreads in [1, 4]:
t0 = time.perf_counter()
iters = 30
for _ in range(iters):
ducc0.fft.c2c(a3, axes=(0, 1, 2), nthreads=nthreads)
el = (time.perf_counter() - t0) / iters
res[f"fft_3d_128x128x128_threads_{nthreads}"] = el
return res
def bench_healpix():
res = {}
nside = 512
npix = 12 * nside * nside
vlen = 100000
rng = np.random.default_rng(42)
pix = rng.integers(low=0, high=npix - 1, size=vlen, dtype=np.int64)
ptg = np.empty((vlen, 2), dtype=np.float64)
ptg[:, 0] = np.arccos((rng.random(vlen) - 0.5) * 2)
ptg[:, 1] = rng.random(vlen) * 2 * np.pi
base = ducc0.healpix.Healpix_Base(nside, "RING")
# pix2ang ring
t0 = time.perf_counter()
iters = 20
for _ in range(iters):
base.pix2ang(pix)
res["healpix_pix2ang_ring"] = (time.perf_counter() - t0) / iters
# ang2pix ring
t0 = time.perf_counter()
iters = 20
for _ in range(iters):
base.ang2pix(ptg)
res["healpix_ang2pix_ring"] = (time.perf_counter() - t0) / iters
# ring2nest
t0 = time.perf_counter()
iters = 20
for _ in range(iters):
base.ring2nest(pix)
res["healpix_ring2nest"] = (time.perf_counter() - t0) / iters
# nest2ring
t0 = time.perf_counter()
iters = 20
for _ in range(iters):
base.nest2ring(pix)
res["healpix_nest2ring"] = (time.perf_counter() - t0) / iters
return res
def bench_sht():
res = {}
lmax = 512
mmax = 512
nlat = lmax + 1
nlon = 2 * lmax + 1
nalm = ((mmax + 1) * (mmax + 2)) // 2 + (mmax + 1) * (lmax - mmax)
rng = np.random.default_rng(42)
alm = (rng.uniform(-1.0, 1.0, nalm) + 1j * rng.uniform(-1.0, 1.0, nalm)).astype(np.complex128)
alm[0 : lmax + 1].imag = 0.0
alm = alm.reshape((1, -1))
for nthreads in [1, 4]:
t0 = time.perf_counter()
iters = 5
for _ in range(iters):
ducc0.sht.synthesis_2d(
alm=alm,
ntheta=nlat,
nphi=nlon,
lmax=lmax,
mmax=mmax,
spin=0,
geometry="GL",
nthreads=nthreads,
)
el = (time.perf_counter() - t0) / iters
res[f"sht_synthesis_2d_gl_lmax512_threads_{nthreads}"] = el
return res
def bench_nufft():
res = {}
npoints = 500000
ndim = 2
shape = (512, 512)
rng = np.random.default_rng(42)
coord = (2 * np.pi * rng.uniform(size=(npoints, ndim)) - np.pi).astype(np.float64)
points = (rng.uniform(size=npoints) - 0.5 + 1j * (rng.uniform(size=npoints) - 0.5)).astype(np.complex128)
for nthreads in [1, 4]:
out = np.empty(shape, dtype=np.complex128)
t0 = time.perf_counter()
iters = 10
for _ in range(iters):
ducc0.nufft.nu2u(points=points, coord=coord, forward=True, epsilon=1e-5, nthreads=nthreads, out=out)
el = (time.perf_counter() - t0) / iters
res[f"nufft_type1_2d_500k_threads_{nthreads}"] = el
return res
def bench_wgridder():
res = {}
rng = np.random.default_rng(42)
nrow = 100000
nchan = 4
uvw = (rng.random((nrow, 3)) - 0.5) * 5000.0
freq = np.linspace(1e9, 1.4e9, nchan)
vis = (rng.standard_normal((nrow, nchan)) + 1j * rng.standard_normal((nrow, nchan))).astype(np.complex128)
wgt = rng.random((nrow, nchan)).astype(np.float64)
npix_x = 512
npix_y = 512
pixsize_x = 1e-4
pixsize_y = 1e-4
for nthreads in [1, 4]:
t0 = time.perf_counter()
iters = 5
for _ in range(iters):
ducc0.wgridder.vis2dirty(
uvw=uvw,
freq=freq,
vis=vis,
wgt=wgt,
npix_x=npix_x,
npix_y=npix_y,
pixsize_x=pixsize_x,
pixsize_y=pixsize_y,
epsilon=1e-5,
nthreads=nthreads,
do_wgridding=True
)
el = (time.perf_counter() - t0) / iters
res[f"wgridder_vis2dirty_threads_{nthreads}"] = el
return res
def bench_totalconvolve():
res = {}
lmax = 256
kmax = 13
ncomp = 1
separate = True
nptg = 500000
epsilon = 1e-4
rng = np.random.default_rng(42)
def nalm_fn(l, m):
return ((m+1)*(m+2))//2 + (m+1)*(l-m)
slm = (rng.uniform(-1., 1., (ncomp, nalm_fn(lmax, lmax))) + 1j*rng.uniform(-1., 1., (ncomp, nalm_fn(lmax, lmax)))).astype(np.complex128)
slm[:, 0:lmax+1].imag = 0.
blm = (rng.uniform(-1., 1., (ncomp, nalm_fn(lmax, kmax))) + 1j*rng.uniform(-1., 1., (ncomp, nalm_fn(lmax, kmax)))).astype(np.complex128)
blm[:, 0:lmax+1].imag = 0.
ptg = rng.uniform(0., 1., (nptg, 3)).astype(np.float64)
ptg[:, 0] *= np.pi
ptg[:, 1] *= 2*np.pi
ptg[:, 2] *= 2*np.pi
for nthreads in [1, 4]:
t0 = time.perf_counter()
iters = 5
for _ in range(iters):
interp = ducc0.totalconvolve.Interpolator(slm, blm, separate, lmax, kmax, epsilon=epsilon, npoints=nptg, nthreads=nthreads)
_ = interp.interpol(ptg)
el = (time.perf_counter() - t0) / iters
res[f"totalconvolve_interp_lmax256_500k_threads_{nthreads}"] = el
return res
def run_all_benchmarks():
print(f"ducc0 module: {ducc0.__file__}")
print(f"ducc0 version: {getattr(ducc0, '__version__', 'unknown')}")
all_res = {}
print("\n--- Running FFT Benchmark ---")
r = bench_fft()
all_res.update(r)
for k, v in r.items():
print(f" {k}: {v*1000:.3f} ms")
print("\n--- Running Healpix Benchmark ---")
r = bench_healpix()
all_res.update(r)
for k, v in r.items():
print(f" {k}: {v*1000:.3f} ms")
print("\n--- Running SHT Benchmark ---")
r = bench_sht()
all_res.update(r)
for k, v in r.items():
print(f" {k}: {v*1000:.3f} ms")
print("\n--- Running NUFFT Benchmark ---")
r = bench_nufft()
all_res.update(r)
for k, v in r.items():
print(f" {k}: {v*1000:.3f} ms")
print("\n--- Running WGridder Benchmark ---")
r = bench_wgridder()
all_res.update(r)
for k, v in r.items():
print(f" {k}: {v*1000:.3f} ms")
print("\n--- Running TotalConvolve Benchmark ---")
r = bench_totalconvolve()
all_res.update(r)
for k, v in r.items():
print(f" {k}: {v*1000:.3f} ms")
return all_res
if __name__ == "__main__":
import json
import sys
results = run_all_benchmarks()
if len(sys.argv) > 1:
with open(sys.argv[1], "w") as f:
json.dump(results, f, indent=2)
print(f"\nResults saved to {sys.argv[1]}")

DUCC0 ARM64 Performance Comparison

Build Configurations

  • GitHub Built Wheel (Binary): ducc0-0.41.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl (manylinux aarch64 binary wheel)
  • PyPI Build (Source): ducc0==0.41.0 (compiled from source with local GCC)

Benchmark Comparison

Benchmark Task GitHub Wheel (ms) PyPI Source Build (ms) Ratio (Source / Wheel) Difference
fft_1d_1M_threads_1 44.011 49.381 1.12x +12.2%
fft_1d_1M_threads_4 43.823 52.279 1.19x +19.3%
fft_2d_1024x1024_threads_1 41.478 39.348 0.95x -5.1%
fft_2d_1024x1024_threads_4 29.641 30.290 1.02x +2.2%
fft_3d_128x128x128_threads_1 105.291 102.996 0.98x -2.2%
fft_3d_128x128x128_threads_4 82.154 82.158 1.00x +0.0%
healpix_pix2ang_ring 4.432 4.436 1.00x +0.1%
healpix_ang2pix_ring 4.280 4.158 0.97x -2.9%
healpix_ring2nest 2.688 2.708 1.01x +0.8%
healpix_nest2ring 2.281 1.662 0.73x -27.1%
sht_synthesis_2d_gl_lmax512_threads_1 33.250 35.298 1.06x +6.2%
sht_synthesis_2d_gl_lmax512_threads_4 9.786 8.833 0.90x -9.7%
nufft_type1_2d_500k_threads_1 298.705 288.735 0.97x -3.3%
nufft_type1_2d_500k_threads_4 96.914 94.835 0.98x -2.1%
wgridder_vis2dirty_threads_1 1626.183 1611.023 0.99x -0.9%
wgridder_vis2dirty_threads_4 703.502 673.931 0.96x -4.2%
totalconvolve_interp_lmax256_500k_threads_1 742.668 736.084 0.99x -0.9%
totalconvolve_interp_lmax256_500k_threads_4 410.998 380.932 0.93x -7.3%

10 Consecutive Runs

Benchmark Task (10 Consecutive Runs) GitHub Wheel Total Time (ms) Source Build Total Time (ms) Ratio (Source / Wheel) Difference
fft_1d_1M_threads_1 419.46 539.20 1.29x +28.5%
fft_1d_1M_threads_4 408.08 489.79 1.20x +20.0%
fft_2d_1024x1024_threads_1 395.78 415.52 1.05x +5.0%
fft_2d_1024x1024_threads_4 293.96 327.63 1.11x +11.5%
fft_3d_128x128x128_threads_1 1020.80 990.95 0.97x -2.9%
fft_3d_128x128x128_threads_4 791.81 866.23 1.09x +9.4%
healpix_pix2ang_ring 43.76 48.65 1.11x +11.2%
healpix_ang2pix_ring 42.40 45.99 1.08x +8.5%
healpix_ring2nest 26.49 26.74 1.01x +0.9%
healpix_nest2ring 22.80 16.63 0.73x -27.1%
sht_synthesis_2d_gl_lmax512_threads_1 324.61 332.15 1.02x +2.3%
sht_synthesis_2d_gl_lmax512_threads_4 93.70 116.57 1.24x +24.4%
nufft_type1_2d_500k_threads_1 2955.76 2940.04 0.99x -0.5%
nufft_type1_2d_500k_threads_4 937.70 933.80 1.00x -0.4%
wgridder_vis2dirty_threads_1 16407.47 16504.87 1.01x +0.6%
wgridder_vis2dirty_threads_4 7096.04 6485.20 0.91x -8.6%
totalconvolve_interp_lmax256_500k_threads_1 7376.16 7046.94 0.96x -4.5%
totalconvolve_interp_lmax256_500k_threads_4 4056.19 3725.97 0.92x -8.1%

Analysis & Observations

  1. Functional Correctness: Both the GitHub Actions built wheel and the PyPI source compilation pass all unit test suites without any errors.
  2. Runtime Performance: Overall performance across core modules (FFT, Healpix, SHT, NUFFT, W-Gridder, TotalConvolve) is nearly identical between the manylinux aarch64 wheel and local compilation on this machine, with small fluctuations (< 5-10%) typical of timer jitter and thread contention.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment