perf(ClusterFinderCUDA): FP32 device pedestal and bulk memcpy drain
Build on RHEL8 / build (push) Successful in 3m0s
Build on RHEL9 / build (push) Successful in 3m41s
Run tests using data on local RHEL8 / build (push) Successful in 3m47s

- Device pedestal arrays (mean/sum/sum2) are now float instead of
  double: halves global-memory bandwidth for pedestal reads/writes and
  eliminates FP64 arithmetic in the kernel (3.3x kernel speedup,
  15µs -> 4.6µs).

- Replace the per-cluster push_back loop in the D2H drain with a
  single resize()+memcpy().
This commit is contained in:
kferjaoui committed 2026-05-21 14:12:02 +02:00
1 parent 6a12e3de24
commit 4c66802980
4 files changed
+107 -87

No files matched your search

+49 -28
View File
@@ -53,7 +53,7 @@
"text": [
"Image size: (400, 400)\n",
"Pedestal frames: 1000\n",
"Data frames: 20000\n"
"Data frames: 88999\n"
]
}
],
@@ -62,9 +62,11 @@
"f = File(base / 'Moench03new/cu_half_speed_master_4.json')\n",
"\n",
"n_frames_pd = 1000\n",
"N = 20000 #88999\n",
"N = 88999 #88999\n",
"cluster_size = (9, 9)\n",
"image_size = (f.rows, f.cols)\n",
"rows = f.rows\n",
"cols = f.cols\n",
"image_size = (rows, cols)\n",
"capacity = 50_000 #3_000_000\n",
"\n",
"print(f'Image size: {image_size}')\n",
@@ -156,7 +158,7 @@
"cf_cuda = ClusterFinderCUDA(image_size, \n",
" cluster_size, \n",
" n_sigma=7, \n",
" max_clusters_per_frame=2048,\n",
" max_clusters_per_frame=1500,\n",
" n_streams=N_STREAMS) "
]
},
@@ -170,7 +172,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
"Pedestal (1000 frames): 0.496s\n"
"Pedestal (1000 frames): 0.499s\n"
]
}
],
@@ -201,7 +203,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
"Reading 20000 frames: 1.195s (16742 FPS, 5109.275 GB/s)\n"
"Reading 88999 frames: 51.133s (1741 FPS, 531.166 GB/s)\n"
]
}
],
@@ -225,20 +227,37 @@
{
"cell_type": "code",
"execution_count": 11,
"id": "ad47af54-c2ba-441a-a874-88dfe04d8a68",
"metadata": {},
"outputs": [],
"source": [
"from tqdm import tqdm"
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "fbb14fda-2852-4b73-ba2c-9952abac1d99",
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"100%|████████████████████████████████████| 88999/88999 [13:59<00:00, 106.07it/s]\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"CPU clustering: 188.615s (106 FPS, 20325971 clusters, 1016.30/frame)\n"
"CPU clustering: 839.049s (106 FPS, 90799856 clusters, 1020.23/frame)\n"
]
}
],
"source": [
"t0 = time.perf_counter()\n",
"for frame in data:\n",
"for frame in tqdm(data):\n",
" cf_cpu.find_clusters(frame)\n",
"t_cpu = time.perf_counter() - t0\n",
"\n",
@@ -273,7 +292,7 @@
},
{
"cell_type": "code",
"execution_count": 12,
"execution_count": 13,
"id": "4f94cf35-5796-463d-bed8-f44a02d91fc6",
"metadata": {},
"outputs": [],
@@ -283,16 +302,17 @@
},
{
"cell_type": "code",
"execution_count": 45,
"execution_count": 29,
"id": "50848f8c-2e66-45b8-b6d5-ca80f368e6ba",
"metadata": {},
"outputs": [],
"source": [
"if(BATCHED):\n",
" BATCH_SIZE = 5000\n",
" BATCH_SIZE = 2000\n",
"\n",
" # Before warmup, pin the entire data array once\n",
" cf_cuda.register_input_buffer(data)\n",
" # Before warmup, pin the fixed size buffer\n",
" batch_buffer = np.empty((BATCH_SIZE, rows, cols), dtype=np.uint16)\n",
" cf_cuda.register_input_buffer(batch_buffer) # fixed ~640 MB\n",
"\n",
" # Warmup: first kernel launch pays CUDA context + pedestal H2D upload cost\n",
" _ = cf_cuda.find_clusters_batched(data[0:BATCH_SIZE], first_frame=0)\n",
@@ -303,8 +323,9 @@
" t0 = time.perf_counter()\n",
" for start in range(0, N, BATCH_SIZE):\n",
" stop = min(start + BATCH_SIZE, N)\n",
" batch_buffer[:stop-start] = data[start:stop] # CPU memcpy into pinned buf\n",
" clusters_cuda_per_frame.extend(\n",
" cf_cuda.find_clusters_batched(data[start:stop], first_frame=start)\n",
" cf_cuda.find_clusters_batched(batch_buffer[:stop-start], first_frame=start)\n",
" )\n",
" t_cuda = time.perf_counter() - t0\n",
"\n",
@@ -344,7 +365,7 @@
},
{
"cell_type": "code",
"execution_count": 46,
"execution_count": 30,
"id": "502d0d3b-6b1e-4cc9-91df-9d998bd849b5",
"metadata": {},
"outputs": [
@@ -354,7 +375,7 @@
"(9, 9)"
]
},
"execution_count": 46,
"execution_count": 30,
"metadata": {},
"output_type": "execute_result"
}
@@ -365,7 +386,7 @@
},
{
"cell_type": "code",
"execution_count": 47,
"execution_count": 31,
"id": "2e3e4b9c-7f23-4fb7-bc3e-fa3d766d51fc",
"metadata": {},
"outputs": [
@@ -373,7 +394,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
"CPU clustering: 188.615s (106 FPS, 20325971 clusters, 1016.30/frame)\n"
"CPU clustering: 839.049s (106 FPS, 90799856 clusters, 1020.23/frame)\n"
]
}
],
@@ -384,7 +405,7 @@
},
{
"cell_type": "code",
"execution_count": 48,
"execution_count": 32,
"id": "4b8df93b-9a1b-41a5-9fed-9cda295fb523",
"metadata": {},
"outputs": [
@@ -392,10 +413,10 @@
"name": "stdout",
"output_type": "stream",
"text": [
"CUDA clustering: 1.304s (15332 FPS, 20238568 clusters, 1011.93/frame)\n",
" Kernel only: 0.038 ms/frame\n",
" PCIe + overhead: 0.027 ms/frame\n",
"Speedup (CPU / CUDA): 144.60×\n"
"CUDA clustering: 7.039s (12644 FPS, 89991186 clusters, 1011.15/frame)\n",
" Kernel only: 0.063 ms/frame\n",
" PCIe + overhead: 0.016 ms/frame\n",
"Speedup (CPU / CUDA): 119.20×\n"
]
}
],
@@ -419,7 +440,7 @@
},
{
"cell_type": "code",
"execution_count": 49,
"execution_count": 18,
"id": "d3a850df-7df0-485b-971d-381cfdc6be81",
"metadata": {},
"outputs": [
@@ -427,7 +448,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
"Cluster count diff: 87403 (0.43%)\n"
"Cluster count diff: 2716962 (2.99%)\n"
]
}
],
@@ -447,7 +468,7 @@
},
{
"cell_type": "code",
"execution_count": 50,
"execution_count": 19,
"id": "aff8db7e-8332-4228-857f-a9875cf940d3",
"metadata": {},
"outputs": [
@@ -467,14 +488,14 @@
},
{
"cell_type": "code",
"execution_count": 51,
"execution_count": 20,
"id": "9adeea2f-9309-4c0a-905b-7d1203ba1f4e",
"metadata": {},
"outputs": [
{
"data": {
"image/png":
"iVBORw0KGgoAAAANSUhEUgAAAxYAAAJOCAYAAAAqFJGJAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAtShJREFUeJzs3Qd4FNX6BvA3IY1ACITeRXpTlCIgCgqCCja8YgNREVQQRcSC2PCqWBCwoIgNL6ioF/GiIIIN5Q8IUkSkikgvAUJoIQlk/s971lk2m03Z7CbZ8v6eZ9lkdnZ29uxsmG/Od74TYVmWBRERERERER9E+vJkERERERERBRYiIiIiIuIX6rEQERERERGfKbAQERERERGfKbAQERERERGfKbAQERERERGfKbAQERERERGfKbAQERERERGfKbAQERERERGfKbAQEb9YvXo1brvtNtSrVw9xcXEoW7Yszj33XLz44os4ePCgc70uXbqYW1F54403MGXKlCLbvgSPUDwWfv75Z/Tp0wc1a9ZETEwMEhMT0bFjR7z55ps4duyYc70zzjgDERERzhu/j+eddx7+85//ZNse1+vVq5fH1/r111/NcwOlDQv6N8bb9/TUU09la6v4+HjUqlULPXr0wGuvvYYjR47kuk+ZmZmoVq2aed5///tfP79jkeCjwEJEfPb222+jdevWWLZsGR588EHMnTsXM2fOxHXXXYdJkyZhwIABxdbKoXgyKYUTasfCk08+iQsvvBA7d+7Ev//9b8yfPx/Tp09H165dzcnxY489lm39888/H4sXLzY3tgNPfvv372+CkGBTHH9juE22Fe/Hjh2LOnXq4KGHHkLz5s3x22+/eXzOV199hb1795qf3333XZ/3QSTYRZX0DohIcON/xHfffTcuueQSfPHFF4iNjXU+xmUPPPCA+Y86mFmWhRMnTqB06dIIBmlpaUGzr4GCV5554h0VFZj/LX722Wd4+umnzQk0T7K5r7bLLrvMnADzu+iqfPnyaN++vfP3bt26oW7duhg3bpz5zgaL4vobw8ClUqVKzt9vuOEG3HPPPejcuTOuvPJKbNy4Mdtr28EEe464zrx587Bjxw7T2yESrtRjISI+ee6558xJzuTJk3P8p0v8T5f/Kefmxx9/NM/nvau///47R8rCX3/9Zf6zr1GjhnmtqlWrmqu1q1atcqZA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 truncated
"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 truncated
"text/plain": [
"<Figure size 800x600 with 2 Axes>"
]