python: bind PixelHistogramCUDA
Build on RHEL8 / build (push) Successful in 3m48s
Build on RHEL9 / build (push) Successful in 4m5s
Run tests using data on local RHEL8 / build (push) Failing after 4m48s

PixelHistogramCUDA_<input>_<storage> for uint16, float32 and float64
frames and the six storage types of PixelHistogram, behind an
aare.PixelHistogramCUDA() factory. Frames are never converted: a wrong
dtype raises TypeError, so a registered buffer is always the one copied.
The GIL is released during GPU work.

- C++: fill_histogram() and submit_batch() take const views, so
  read-only arrays (np.memmap mode="r") work; register_output_buffer()
  pins the destination of values(out).
- Python: register_input_buffer()/register_output_buffer() keep their
  array alive; values(out=...) writes into a given array. With a pinned
  out, reading back a 640 MB histogram takes 28 ms instead of 230 ms.
- Tests in test_PixelHistogramCUDA.py; PixelHistogramCUDA.ipynb compares
  CPU and GPU on 100k Moench frames (identical, ~13x faster on the GPU).
This commit is contained in:
kferjaoui committed 2026-10-21 17:35:42 +02:00
1 parent 3621d77093
commit ea32b530e8
8 files changed
+757 -10

No files matched your search

+224
View File
@@ -0,0 +1,224 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "9b1ef299",
"metadata": {},
"source": [
"# PixelHistogram — CPU vs CUDA\n",
"\n",
"Per-pixel histograms of a data file, filled by `PixelHistogram` on the CPU and\n",
"by `PixelHistogramCUDA` on the GPU. Times both, then checks that the results are\n",
"identical."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "6c8b158f",
"metadata": {},
"outputs": [],
"source": [
"import sys; sys.path.append('/home/ferjao_k/aare/build')\n",
"\n",
"import time\n",
"from pathlib import Path\n",
"\n",
"import matplotlib.pyplot as plt\n",
"import numpy as np\n",
"\n",
"from aare import File, PixelHistogram_u32, PixelHistogramCUDA"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "518fd5c4",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"(100000, 400, 400) uint16 32.00 GB\n"
]
}
],
"source": [
"base = Path('/mnt/sls_det_storage/moench_data/2603_MaxIVBeamtime/2026032408/process/xrf/')\n",
"f = File(base / 'Cu_factor_10_data_master_0.json')\n",
"frames = f.read_n(100_000) # (n_frames, rows, cols), uint16\n",
"n_frames, rows, cols = frames.shape\n",
"\n",
"n_bins, xmin, xmax = 1000, 3500, 7500 # 4 ADU per bin\n",
"n_threads = 16 # CPU threads\n",
"print(frames.shape, frames.dtype, f'{frames.nbytes / 1e9:.2f} GB')"
]
},
{
"cell_type": "markdown",
"id": "7c40949d",
"metadata": {},
"source": [
"## CPU"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "a418bbd4",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"CPU: 21.75 s, 4,598 frames/s\n"
]
}
],
"source": [
"cpu = PixelHistogram_u32(rows=rows, cols=cols, n_bins=n_bins, xmin=xmin, xmax=xmax,\n",
" n_threads=n_threads)\n",
"\n",
"t0 = time.perf_counter()\n",
"for frame in frames:\n",
" cpu.fill_async(frame.astype(np.float64)) # the CPU binding takes float64\n",
"values_cpu = cpu.values()\n",
"t_cpu = time.perf_counter() - t0\n",
"print(f'CPU: {t_cpu:.2f} s, {n_frames / t_cpu:,.0f} frames/s')"
]
},
{
"cell_type": "markdown",
"id": "0b95f098",
"metadata": {},
"source": [
"## GPU"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "a60415d8",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"GPU fill: 1.512 s, 66,119 frames/s, 21.2 GB/s\n",
"GPU values: 0.032 s\n",
"GPU total: 1.545 s, speed-up 14.1x\n"
]
}
],
"source": [
"gpu = PixelHistogramCUDA(rows=rows, cols=cols, n_bins=n_bins, xmin=xmin, xmax=xmax,\n",
" dtype=frames.dtype, storage_dtype=np.uint32, batch_size=128)\n",
"gpu.register_input_buffer(frames) # pin once, before the timed loop\n",
"values_gpu = np.empty((rows, cols, n_bins), dtype=np.uint32)\n",
"gpu.register_output_buffer(values_gpu)\n",
"\n",
"t0 = time.perf_counter()\n",
"gpu.fill_histogram(frames)\n",
"gpu.flush() # fill_histogram only queues the work\n",
"t1 = time.perf_counter()\n",
"gpu.values(out=values_gpu) # transpose + copy back to the host\n",
"t2 = time.perf_counter()\n",
"\n",
"t_gpu = t2 - t0\n",
"print(f'GPU fill: {t1 - t0:.3f} s, {n_frames / (t1 - t0):,.0f} frames/s, '\n",
" f'{frames.nbytes / (t1 - t0) / 1e9:.1f} GB/s')\n",
"print(f'GPU values: {t2 - t1:.3f} s')\n",
"print(f'GPU total: {t_gpu:.3f} s, speed-up {t_cpu / t_gpu:.1f}x')"
]
},
{
"cell_type": "markdown",
"id": "43fff4d8",
"metadata": {},
"source": [
"## Compare"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "014cfc32",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"identical: True\n",
"counts: 15997375206 15997375206\n"
]
}
],
"source": [
"print('identical:', np.array_equal(values_cpu, values_gpu))\n",
"print('counts: ', values_cpu.sum(), values_gpu.sum())"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "a0571bd1",
"metadata": {},
"outputs":
[
{
"data": {
"image/png": "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 truncated
"text/plain": [
"<Figure size 1200x400 with 2 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"x = gpu.bin_centers()\n",
"row, col = rows // 2, cols // 2\n",
"\n",
"fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))\n",
"ax1.step(x, values_cpu[row, col], where='mid', label='CPU')\n",
"ax1.plot(x, values_gpu[row, col], 'rx', ms=5, label='GPU')\n",
"ax1.set_xlim(x[values_cpu[row, col].argmax()] + np.array([-100, 100]))\n",
"ax1.set(title=f'pixel ({row}, {col})', xlabel='ADU', ylabel='counts')\n",
"ax1.legend()\n",
"\n",
"ax2.step(x, values_cpu.sum(axis=(0, 1)), where='mid', label='CPU')\n",
"ax2.plot(x, values_gpu.sum(axis=(0, 1)), 'rx', ms=5, markevery=10, label='GPU (every 10th bin)')\n",
"ax2.set_yscale('log')\n",
"ax2.set(title='all pixels', xlabel='ADU', ylabel='counts')\n",
"ax2.legend()\n",
"plt.show()"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.15"
}
},
"nbformat": 4,
"nbformat_minor": 5
}