diff --git a/CMakeLists.txt b/CMakeLists.txt index f14eaca..f85e934 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -56,6 +56,10 @@ option(AARE_CUSTOM_ASSERT "Use custom assert" OFF) option(AARE_INSTALL_PYTHONEXT "Install the python extension in the install tree under CMAKE_INSTALL_PREFIX/aare/" OFF) option(AARE_ASAN "Enable AddressSanitizer" OFF) +option(AARE_CUDA "Build CUDA cluster finder backend" OFF) +set(AARE_CUDA_ARCHITECTURES "native" CACHE STRING + "CUDA architectures to compile for (used when AARE_CUDA=ON)") + # Configure which of the dependencies to use FetchContent for option(AARE_FETCH_FMT "Use FetchContent to download fmt" ON) option(AARE_FETCH_PYBIND11 "Use FetchContent to download pybind11" ON) @@ -79,6 +83,25 @@ if(AARE_SYSTEM_LIBRARIES) # since these are not available on conda-forge endif() +if(AARE_CUDA) + enable_language(CUDA) + find_package(CUDAToolkit REQUIRED) + + set(CMAKE_CUDA_STANDARD 17) + set(CMAKE_CUDA_STANDARD_REQUIRED ON) + set(CMAKE_CUDA_EXTENSIONS OFF) + set(CMAKE_CUDA_SEPARABLE_COMPILATION ON) + + if(NOT CMAKE_CUDA_ARCHITECTURES) + set(CMAKE_CUDA_ARCHITECTURES ${AARE_CUDA_ARCHITECTURES}) + endif() + + message(STATUS "AARE: CUDA support ENABLED (archs=${CMAKE_CUDA_ARCHITECTURES}, " + "toolkit=${CUDAToolkit_VERSION})") +else() + message(STATUS "AARE: CUDA support DISABLED (CPU-only build)") +endif() + if(AARE_BENCHMARKS) add_subdirectory(benchmarks) endif() @@ -307,22 +330,24 @@ else() endif() # Common flags for GCC and Clang +# The strict host warnings (especially -Wold-style-cast and -Wdouble-promotion) +# trip on NVIDIA's CUDA runtime headers when they get preprocessed by nvcc's +# host pass, so we gate them behind $. target_compile_options( aare_compiler_flags INTERFACE - -Wall - -Wextra - -pedantic - -Wshadow - -Wold-style-cast - -Wnon-virtual-dtor - -Woverloaded-virtual - -Wdouble-promotion - -Wformat=2 - -Wredundant-decls - -Wvla - -Wdouble-promotion - -Werror=return-type #important can cause segfault in optimzed builds + $<$:-Wall> + $<$:-Wextra> + $<$:-pedantic> + $<$:-Wshadow> + $<$:-Wold-style-cast> + $<$:-Wnon-virtual-dtor> + $<$:-Woverloaded-virtual> + $<$:-Wdouble-promotion> + $<$:-Wformat=2> + $<$:-Wredundant-decls> + $<$:-Wvla> + $<$:-Werror=return-type> #important can cause segfault in optimzed builds ) endif() #GCC/Clang specific @@ -392,6 +417,14 @@ set(PUBLICHEADERS include/aare/VarClusterFinder.hpp include/aare/utils/task.hpp ) + +if(AARE_CUDA) +list(APPEND PUBLICHEADERS + include/aare/ClusterFinderCUDA.hpp + include/aare/clusterfinder_kernel.cuh + include/aare/utils/cuda_check.cuh +) +endif() set(SourceFiles @@ -466,6 +499,22 @@ set_target_properties(aare_core PROPERTIES PUBLIC_HEADER "${PUBLICHEADERS}" ) +if(AARE_CUDA) + add_library(aare_cuda INTERFACE) + target_link_libraries(aare_cuda INTERFACE + aare_core + CUDA::cudart + ) + target_compile_features(aare_cuda INTERFACE cuda_std_17) + target_compile_definitions(aare_cuda INTERFACE + AARE_HAS_CUDA + _GLIBCXX_USE_CXX11_ABI=1 + ) + # Usage example downstream: + # set_source_files_properties(bind_ClusterFinderCUDA.cpp PROPERTIES LANGUAGE CUDA) + # target_link_libraries(my_pymodule PRIVATE aare_cuda) +endif() + if(AARE_TESTS) set(TestSources ${CMAKE_CURRENT_SOURCE_DIR}/src/algorithm.test.cpp @@ -500,7 +549,11 @@ endif() if(AARE_MASTER_PROJECT) - install(TARGETS aare_core aare_compiler_flags + set(AARE_INSTALL_TARGETS aare_core aare_compiler_flags) + if(AARE_CUDA) + list(APPEND AARE_INSTALL_TARGETS aare_cuda) + endif() + install(TARGETS ${AARE_INSTALL_TARGETS} EXPORT "${TARGETS_EXPORT_NAME}" LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} @@ -563,5 +616,8 @@ add_custom_target( if(AARE_MASTER_PROJECT) set(CMAKE_INSTALL_DIR "share/cmake/${PROJECT_NAME}") set(PROJECT_LIBRARIES aare-core aare-compiler-flags ) + if(AARE_CUDA) + list(APPEND PROJECT_LIBRARIES aare-cuda) + endif() include(cmake/package_config.cmake) endif() diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index aefbd95..3e56252 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -15,20 +15,50 @@ else() find_package(pybind11 2.13 REQUIRED) endif() -# Add the compiled python extension -pybind11_add_module( - _aare # name of the module - src/module.cpp # source file - ) - -set_target_properties(_aare PROPERTIES - LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR} -) +# ---- Main CPU module -------------------------------------------------------- +# module.cpp is the only source for the main module. When AARE_CUDA=ON, the +# CUDA bindings live in a *separate* Python extension (_aare_cuda.so) loaded +# independently at runtime. This isolates the nvcc-compiled translation unit +# into its own ELF image so pybind11's type registry cannot be corrupted by +# weak-symbol collisions between gcc-emitted and nvcc-emitted template +# instantiations. +pybind11_add_module(_aare NO_EXTRAS src/module.cpp) + target_link_libraries(_aare PRIVATE aare_core aare_compiler_flags) - + target_include_directories(_aare SYSTEM PRIVATE $ ) + +set_target_properties(_aare PROPERTIES + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/aare + INTERPROCEDURAL_OPTIMIZATION FALSE +) + +# ---- CUDA module (separate .so) -------------------------------------------- +if(AARE_CUDA) + pybind11_add_module(_aare_cuda NO_EXTRAS src/cuda_bindings.cu) + + target_link_libraries(_aare_cuda PRIVATE aare_cuda aare_compiler_flags) + + target_include_directories(_aare_cuda SYSTEM PRIVATE + $ + ) + + set_target_properties(_aare_cuda PROPERTIES + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/aare + INTERPROCEDURAL_OPTIMIZATION FALSE + CUDA_RESOLVE_DEVICE_SYMBOLS ON + CUDA_SEPARABLE_COMPILATION ON + ) + + target_compile_options(_aare_cuda PRIVATE + $<$:--expt-relaxed-constexpr> + $<$:--extended-lambda> + $<$:-Xcompiler=-fvisibility=hidden> + $<$:-Xcompiler=-fPIC> + ) +endif() # List of python files to be copied to the build directory set( PYTHON_FILES @@ -51,9 +81,9 @@ foreach(FILE ${PYTHON_FILES}) configure_file(${FILE} ${CMAKE_BINARY_DIR}/${FILE} ) endforeach(FILE ${PYTHON_FILES}) -set_target_properties(_aare PROPERTIES - LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/aare -) +# set_target_properties(_aare PROPERTIES +# LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/aare +# ) set(PYTHON_EXAMPLES examples/play.py @@ -69,8 +99,13 @@ endforeach(FILE ${PYTHON_EXAMPLES}) if(AARE_INSTALL_PYTHONEXT) + set(AARE_PY_INSTALL_TARGETS _aare) + if(AARE_CUDA) + list(APPEND AARE_PY_INSTALL_TARGETS _aare_cuda) + endif() + install( - TARGETS _aare + TARGETS ${AARE_PY_INSTALL_TARGETS} EXPORT "${TARGETS_EXPORT_NAME}" LIBRARY DESTINATION aare COMPONENT python @@ -80,5 +115,5 @@ if(AARE_INSTALL_PYTHONEXT) FILES ${PYTHON_FILES} DESTINATION aare COMPONENT python - ) + ) endif() \ No newline at end of file diff --git a/python/aare/ClusterFinder.py b/python/aare/ClusterFinder.py index eae7ad7..87aee70 100644 --- a/python/aare/ClusterFinder.py +++ b/python/aare/ClusterFinder.py @@ -49,6 +49,44 @@ def ClusterFinderMT(image_size, cluster_size = (3,3), dtype=np.int32, n_sigma=5, return cls(image_size, n_sigma=n_sigma, capacity=capacity, n_threads=n_threads) +def _cuda_available(): + """True if this build of aare was compiled with -DAARE_CUDA=ON.""" + return hasattr(_aare, "ClusterFinderCUDA_Cluster3x3i") + + +def ClusterFinderCUDA(image_size, cluster_size=(3,3), n_sigma=5, dtype=np.int32, + capacity=1024, n_streams=1): + """ + Factory function to create a ClusterFinderCUDA object. Provides a cleaner + syntax for the templated ClusterFinderCUDA in C++. API mirrors + ClusterFinder() plus CUDA-specific knobs (n_streams). + + .. code-block:: python + + from aare import ClusterFinderCUDA + + cf = ClusterFinderCUDA(image_size=(512, 1024), + cluster_size=(3, 3), + n_sigma=5, + n_streams=4) + for frame in pedestal_frames: + cf.push_pedestal_frame(frame) + for i, frame in enumerate(data_frames): + cf.find_clusters(frame, frame_number=i) + clusters = cf.steal_clusters() + """ + if not _cuda_available(): + raise RuntimeError( + "ClusterFinderCUDA is not available in this build of aare. " + "Rebuild with -DAARE_CUDA=ON (and -DAARE_PYTHON_BINDINGS=ON)." + ) + + cls = _get_class("ClusterFinderCUDA", cluster_size, dtype) + return cls(image_size, + n_sigma=n_sigma, + capacity=capacity, + n_streams=n_streams) + def ClusterCollector(clusterfindermt, dtype=np.int32): """ Factory function to create a ClusterCollector object. Provides a cleaner syntax for diff --git a/python/aare/__init__.py b/python/aare/__init__.py index 7684df8..5077562 100644 --- a/python/aare/__init__.py +++ b/python/aare/__init__.py @@ -2,6 +2,23 @@ # Make the compiled classes that live in _aare available from aare. from . import _aare +# ---- CUDA module (optional) ------------------------------------------------ +# When the package was built with AARE_CUDA=ON, a sibling extension +# _aare_cuda contains the ClusterFinderCUDA_* classes. We re-export them +# onto _aare so user code can do `from aare import ClusterFinderCUDA_*` +# regardless of which .so physically hosts the class. On a CPU-only build +# the import fails silently and ClusterFinderCUDA_* classes simply aren't +# present; the factory in ClusterFinder.py handles that case with a clear +# RuntimeError. +try: + from . import _aare_cuda as _aare_cuda_mod + for _name in dir(_aare_cuda_mod): + if _name.startswith("ClusterFinderCUDA"): + setattr(_aare, _name, getattr(_aare_cuda_mod, _name)) + del _name +except ImportError: + pass + from . import transform from ._aare import File, RawMasterFile, RawSubFile, JungfrauDataFile @@ -14,6 +31,7 @@ from ._aare import corner # from ._aare import ClusterFinderMT, ClusterCollector, ClusterFileSink, ClusterVector_i from .ClusterFinder import ClusterFinder, ClusterCollector, ClusterFinderMT, ClusterFileSink, ClusterFile +from .ClusterFinder import ClusterFinderCUDA, _cuda_available from .ClusterVector import ClusterVector from .Cluster import Cluster diff --git a/python/src/bind_ClusterFinderCUDA.hpp b/python/src/bind_ClusterFinderCUDA.hpp new file mode 100644 index 0000000..cb4afbe --- /dev/null +++ b/python/src/bind_ClusterFinderCUDA.hpp @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: MPL-2.0 +#pragma once +#include "aare/ClusterFinderCUDA.hpp" +#include "aare/ClusterVector.hpp" +#include "aare/NDView.hpp" +#include "aare/Pedestal.hpp" +#include "np_helper.hpp" + +#include +#include +// #include +#include + +namespace py = pybind11; +using pd_type = double; + +using namespace aare; + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-parameter" + +namespace aare { + +template +void define_ClusterFinderCUDA(py::module &m, const std::string &typestr) { + auto class_name = fmt::format("ClusterFinderCUDA_{}", typestr); + + using ClusterType = Cluster; + using CF = ClusterFinderCUDA; + + py::class_(m, class_name.c_str()) + .def(py::init, pd_type, size_t, int>(), + py::arg("image_size"), + py::arg("n_sigma") = 5.0, + py::arg("capacity") = 1'000'000, + py::arg("n_streams") = 1) + + .def_property( + "nSigma", + &CF::get_nSigma, + &CF::set_nSigma, + R"(Number of sigma above the pedestal to consider a photon during cluster finding.)") + + .def("push_pedestal_frame", + [](CF &self, py::array_t frame) { + auto view = make_view_2d(frame); + self.push_pedestal_frame(view); + }) + + .def("clear_pedestal", &CF::clear_pedestal) + + .def_property_readonly( + "pedestal", + [](CF &self) { + auto pd = new NDArray{}; + *pd = self.pedestal(); + return return_image_data(pd); + }) + + .def_property_readonly( + "noise", + [](CF &self) { + auto arr = new NDArray{}; + *arr = self.noise(); + return return_image_data(arr); + }) + + .def( + "steal_clusters", + [](CF &self, bool realloc_same_capacity) { + ClusterVector clusters = + self.steal_clusters(realloc_same_capacity); + return clusters; + }, + py::arg("realloc_same_capacity") = false) + + .def( + "find_clusters", + [](CF &self, py::array_t frame, uint64_t frame_number) { + auto view = make_view_2d(frame); + self.find_clusters(view, frame_number); + }, + py::arg("frame"), py::arg("frame_number") = 0) + + .def( + "find_clusters_batched", + [](CF &self, py::array_t frames, uint64_t first_frame) { + // frames is expected as a 3D numpy array (n_frames, nrows, ncols) + auto view = make_view_3d(frames); + return self.find_clusters_batched(view, first_frame); + }, + py::arg("frames"), py::arg("first_frame") = 0, + R"(Process a 3D array of frames (n_frames, nrows, ncols) in parallel +across the configured CUDA streams. Returns a list of ClusterVector, one per +input frame.)"); +} + +} // namespace aare + +#pragma GCC diagnostic pop \ No newline at end of file diff --git a/python/src/cuda_bindings.cu b/python/src/cuda_bindings.cu new file mode 100644 index 0000000..a30cd95 --- /dev/null +++ b/python/src/cuda_bindings.cu @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MPL-2.0 +// +// CUDA-only Python extension module. Registers ClusterFinderCUDA along with +// the ClusterVector and Cluster types it exposes in its return values, so +// the module is self-contained — users can call steal_clusters() and get +// back a usable ClusterVector without _aare needing to be imported first. + +#include "bind_Cluster.hpp" +#include "bind_ClusterVector.hpp" +#include "bind_ClusterFinderCUDA.hpp" + +#include + +namespace py = pybind11; + +// Register the Cluster + ClusterVector pair for one (T, N, M) combination. +// Subset of DEFINE_CLUSTER_BINDINGS from module.cpp: we register what +// ClusterFinderCUDA actually returns, nothing more. File I/O, eta and +// reduce_to_2x2 stay on the CPU side. +#define DEFINE_CUDA_CLUSTER_TYPES(T, N, M, U, TYPE_CODE) \ + define_ClusterVector(m, "Cluster" #N "x" #M #TYPE_CODE); \ + define_Cluster(m, #N "x" #M #TYPE_CODE); + +#define DEFINE_BINDINGS_CLUSTERFINDER_CUDA(T, N, M, U, TYPE_CODE) \ + aare::define_ClusterFinderCUDA( \ + m, "Cluster" #N "x" #M #TYPE_CODE); + +PYBIND11_MODULE(_aare_cuda, m) { + + // Types first — finders reference them in their signatures. + // SFINAE excludes 2x2 on ClusterFinderCUDA, so we skip it here too. + DEFINE_CUDA_CLUSTER_TYPES(int, 3, 3, uint16_t, i); + DEFINE_CUDA_CLUSTER_TYPES(double, 3, 3, uint16_t, d); + DEFINE_CUDA_CLUSTER_TYPES(float, 3, 3, uint16_t, f); + + DEFINE_CUDA_CLUSTER_TYPES(int, 5, 5, uint16_t, i); + DEFINE_CUDA_CLUSTER_TYPES(double, 5, 5, uint16_t, d); + DEFINE_CUDA_CLUSTER_TYPES(float, 5, 5, uint16_t, f); + + DEFINE_CUDA_CLUSTER_TYPES(int, 7, 7, uint16_t, i); + DEFINE_CUDA_CLUSTER_TYPES(double, 7, 7, uint16_t, d); + DEFINE_CUDA_CLUSTER_TYPES(float, 7, 7, uint16_t, f); + + DEFINE_CUDA_CLUSTER_TYPES(int, 9, 9, uint16_t, i); + DEFINE_CUDA_CLUSTER_TYPES(double, 9, 9, uint16_t, d); + DEFINE_CUDA_CLUSTER_TYPES(float, 9, 9, uint16_t, f); + + // Finders + DEFINE_BINDINGS_CLUSTERFINDER_CUDA(int, 3, 3, uint16_t, i); + DEFINE_BINDINGS_CLUSTERFINDER_CUDA(double, 3, 3, uint16_t, d); + DEFINE_BINDINGS_CLUSTERFINDER_CUDA(float, 3, 3, uint16_t, f); + + DEFINE_BINDINGS_CLUSTERFINDER_CUDA(int, 5, 5, uint16_t, i); + DEFINE_BINDINGS_CLUSTERFINDER_CUDA(double, 5, 5, uint16_t, d); + DEFINE_BINDINGS_CLUSTERFINDER_CUDA(float, 5, 5, uint16_t, f); + + DEFINE_BINDINGS_CLUSTERFINDER_CUDA(int, 7, 7, uint16_t, i); + DEFINE_BINDINGS_CLUSTERFINDER_CUDA(double, 7, 7, uint16_t, d); + DEFINE_BINDINGS_CLUSTERFINDER_CUDA(float, 7, 7, uint16_t, f); + + DEFINE_BINDINGS_CLUSTERFINDER_CUDA(int, 9, 9, uint16_t, i); + DEFINE_BINDINGS_CLUSTERFINDER_CUDA(double, 9, 9, uint16_t, d); + DEFINE_BINDINGS_CLUSTERFINDER_CUDA(float, 9, 9, uint16_t, f); +} + +#undef DEFINE_CUDA_CLUSTER_TYPES +#undef DEFINE_BINDINGS_CLUSTERFINDER_CUDA \ No newline at end of file diff --git a/python/tests/ClusterFinderCUDA.ipynb b/python/tests/ClusterFinderCUDA.ipynb new file mode 100644 index 0000000..612cbac --- /dev/null +++ b/python/tests/ClusterFinderCUDA.ipynb @@ -0,0 +1,422 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "789d5aab-0c75-4ed3-94bf-e66172f6737c", + "metadata": {}, + "outputs": [], + "source": [ + "import sys; sys.path.append('/home/ferjao_k/aare/build')\n", + "\n", + "from pathlib import Path\n", + "import matplotlib.pyplot as plt\n", + "from mpl_toolkits.axes_grid1 import make_axes_locatable\n", + "import numpy as np\n", + "import boost_histogram as bh\n", + "import time\n", + "\n", + "from aare import File, ClusterFinder, ClusterFinderMT, ClusterCollector, ClusterFinderCUDA" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "3f6089d6-2245-4aca-aad3-2417c16afae2", + "metadata": {}, + "outputs": [], + "source": [ + "def make_hist(clusters):\n", + " h = bh.Histogram(bh.axis.Regular(100, -2, 4000))\n", + " h.fill(clusters.sum())\n", + " return h" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "415b09d4-d0d0-4166-8601-9d84302617bd", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Image size: (400, 400)\n", + "Pedestal frames: 1000\n", + "Data frames: 40000\n" + ] + } + ], + "source": [ + "base = Path('/mnt/sls_det_storage/matterhorn_data/aare_test_data/')\n", + "f = File(base / 'Moench03new/cu_half_speed_master_4.json')\n", + "\n", + "n_frames_pd = 1000\n", + "N = 40000\n", + "cluster_size = (3, 3)\n", + "image_size = (f.rows, f.cols)\n", + "capacity = 100_000 #3_000_000\n", + "\n", + "print(f'Image size: {image_size}')\n", + "print(f'Pedestal frames: {n_frames_pd}')\n", + "print(f'Data frames: {N}')" + ] + }, + { + "cell_type": "markdown", + "id": "5531b79d-12ad-4de6-84b7-0224b10d13c3", + "metadata": {}, + "source": [ + "## Pedestal (both finders trained on identical frames)" + ] + }, + { + "cell_type": "markdown", + "id": "e33b421f-969a-41d6-8aa6-5bd859a88fb3", + "metadata": {}, + "source": [ + "- Modify the boolean `SERIAL` to choose between the sequential CPU version (ClusterFinder) and its multi-threaded homologue (ClusterFinderMT)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "6ee7c469-5a23-421a-9bb4-a76e8f57a0c1", + "metadata": {}, + "outputs": [], + "source": [ + "SERIAL = True" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "9d8f69bf-27f6-48c8-b2da-4157d67e6b04", + "metadata": {}, + "outputs": [], + "source": [ + "if(SERIAL):\n", + " cf_cpu = ClusterFinder(image_size, cluster_size, capacity=capacity)\n", + "else:\n", + " cf_cpu = ClusterFinderMT(image_size, cluster_size, capacity=capacity, n_threads=24)\n", + " sink = ClusterCollector(cf_cpu)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "cbdcb805-708b-4205-bda9-2aa163d0e81f", + "metadata": {}, + "outputs": [], + "source": [ + "N_STREAMS = 1 \n", + "cf_cuda = ClusterFinderCUDA(image_size, cluster_size, capacity=capacity, n_streams=N_STREAMS)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "1546f405-1bf6-4073-ab35-8134be695a6c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Pedestal (1000 frames): 0.498s\n" + ] + } + ], + "source": [ + "t0 = time.perf_counter()\n", + "for _ in range(n_frames_pd):\n", + " img = f.read_frame()\n", + " cf_cpu.push_pedestal_frame(img.copy())\n", + " cf_cuda.push_pedestal_frame(img.copy())\n", + "print(f'Pedestal ({n_frames_pd} frames): {time.perf_counter() - t0:.3f}s')" + ] + }, + { + "cell_type": "markdown", + "id": "5df035f0-7a27-4d5a-8f8d-c94e745bcd1a", + "metadata": {}, + "source": [ + "## Read all data frames into memory (I/O out of the timing loop)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "4573be3d-5ba8-4e18-bab0-c874f2b7dcb2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Reading 40000 frames: 2.002s (19985 FPS, 6098.804 GB/s)\n" + ] + } + ], + "source": [ + "f.seek(n_frames_pd)\n", + "t0 = time.perf_counter()\n", + "data = f.read_n(N)\n", + "t_io = time.perf_counter() - t0\n", + "print(f'Reading {N} frames: {t_io:.3f}s ({N/t_io:.0f} FPS, '\n", + " f'{f.bytes_per_frame * N / 1024**2 / t_io:.3f} GB/s)')" + ] + }, + { + "cell_type": "markdown", + "id": "c1e9362e-dcc2-41af-bfa1-c62d0968477e", + "metadata": {}, + "source": [ + "## CPU clustering" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "fbb14fda-2852-4b73-ba2c-9952abac1d99", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU clustering: 73.978s (541 FPS, 55449596 clusters, 1386.24/frame)\n" + ] + } + ], + "source": [ + "t0 = time.perf_counter()\n", + "for frame in data:\n", + " cf_cpu.find_clusters(frame)\n", + "t_cpu = time.perf_counter() - t0\n", + "\n", + "if(SERIAL):\n", + " clusters_cpu = cf_cpu.steal_clusters(realloc_same_capacity=False)\n", + " n_clusters_cpu = clusters_cpu.size\n", + " \n", + " hist_cpu = make_hist(clusters_cpu)\n", + "else:\n", + " cf_cpu.stop()\n", + " sink.stop()\n", + " \n", + " clusters_cpu = sink.steal_clusters() #cf_cpu.steal_clusters(realloc_same_capacity=False)\n", + " \n", + " hist_cpu = bh.Histogram(bh.axis.Regular(100, -2, 4000))\n", + " n_clusters_cpu = 0\n", + " for cv in clusters_cpu:\n", + " hist_cpu.fill(cv.sum())\n", + " n_clusters_cpu += cv.size\n", + " \n", + "print(f'CPU clustering: {t_cpu:.3f}s ({N/t_cpu:.0f} FPS, '\n", + " f'{n_clusters_cpu} clusters, {n_clusters_cpu/N:.2f}/frame)')" + ] + }, + { + "cell_type": "markdown", + "id": "8cfd7091-8020-49ff-b033-28bf773983d8", + "metadata": {}, + "source": [ + "## CUDA clustering" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "4b8df93b-9a1b-41a5-9fed-9cda295fb523", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CUDA clustering: 3.880s (10310 FPS, 55810704 clusters, 1395.27/frame)\n", + "Speedup (CPU / CUDA): 19.07×\n" + ] + } + ], + "source": [ + "# Warmup: first kernel launch pays CUDA context + pedestal H2D upload cost\n", + "cf_cuda.find_clusters(data[0])\n", + "_ = cf_cuda.steal_clusters(realloc_same_capacity=False)\n", + "\n", + "t0 = time.perf_counter()\n", + "for frame in data:\n", + " cf_cuda.find_clusters(frame)\n", + "t_cuda = time.perf_counter() - t0\n", + "clusters_cuda = cf_cuda.steal_clusters(realloc_same_capacity=False)\n", + "n_clusters_cuda = clusters_cuda.size\n", + "print(f'CUDA clustering: {t_cuda:.3f}s ({N/t_cuda:.0f} FPS, '\n", + " f'{n_clusters_cuda} clusters, {n_clusters_cuda/N:.2f}/frame)')\n", + "print(f'Speedup (CPU / CUDA): {t_cpu / t_cuda:.2f}×')\n", + "\n", + "hist_cuda = make_hist(clusters_cuda)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "1fda99ee-db54-40c1-8e5b-51168a33fb96", + "metadata": {}, + "outputs": [], + "source": [ + "# BATCH_SIZE = 500\n", + "\n", + "# # warmup\n", + "# _ = cf_cuda.find_clusters_batched(data[:1], first_frame=0)\n", + "\n", + "# t0 = time.perf_counter()\n", + "# clusters_cuda_per_frame = []\n", + "# for start in range(0, N, BATCH_SIZE):\n", + "# stop = min(start + BATCH_SIZE, N)\n", + "# clusters_cuda_per_frame.extend(\n", + "# cf_cuda.find_clusters_batched(data[start:stop], first_frame=start)\n", + "# )\n", + "# t_cuda = time.perf_counter() - t0\n", + "\n", + "# n_clusters_cuda = sum(cv.size for cv in clusters_cuda_per_frame)\n", + "\n", + "# print(f'CUDA clustering: {t_cuda:.3f}s ({N/t_cuda:.0f} FPS, '\n", + "# f'{n_clusters_cuda} clusters, {n_clusters_cuda/N:.2f}/frame)')\n", + "# print(f'Speedup (CPU / CUDA): {t_cpu / t_cuda:.2f}×')" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "b7df7b9d-6873-46c4-b6bc-7e4238a5bdf7", + "metadata": {}, + "outputs": [], + "source": [ + "# def make_hist_from_batch(result_list):\n", + "# h = bh.Histogram(bh.axis.Regular(100, -2, 4000))\n", + "# energies = [np.asarray(cv.sum()).ravel() for cv in result_list if cv.size > 0]\n", + "# if energies:\n", + "# h.fill(np.concatenate(energies))\n", + "# return h\n", + "\n", + "# hist_cuda = make_hist_from_batch(clusters_cuda_per_frame)" + ] + }, + { + "cell_type": "markdown", + "id": "b966cce1-0f73-4565-a707-1aec2a69f75e", + "metadata": {}, + "source": [ + "## Agreement check: \n", + "- Cluster counts should match closely.\n", + "- However, as the CUDA CF updates the pedestal once per frame rather than per-pixel, a small divergence after the first few frames is expected." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "d3a850df-7df0-485b-971d-381cfdc6be81", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Cluster count diff: 361108 (0.65%)\n" + ] + } + ], + "source": [ + "diff = abs(n_clusters_cpu - n_clusters_cuda)\n", + "rel = diff / max(n_clusters_cpu, 1)\n", + "print(f'Cluster count diff: {diff} ({rel:.2%})')" + ] + }, + { + "cell_type": "markdown", + "id": "81638ad3-6112-4bd7-a93a-f4d97f1f94cf", + "metadata": {}, + "source": [ + "## Plots" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "9adeea2f-9309-4c0a-905b-7d1203ba1f4e", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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ly7mQukvvfSSW5+HIA8A4Jfyo5vL3fmjzNJb5NVrLVflPWKWAKp2Eq4By3fffSfPPfecCU6++eYb6d+/v4waNapaVXneeefJ3r17zbG//vprcywNjC644AIpKChw2Xfnzp3y888/y+jRo2XKlCmVOv6rr74qV1xxhURFRZX5vM7bvHnzSj32pptuctnnzTffdNl+/vnnmzJqfaxYscIEcVpuDYSdNFi74YYbZNiwYWWWLz093QS2TZs2lV9//VX+7//+T1544QV58cUXi/Z5+eWXXcqxa9cuiYuLM6/LSetFn0Ozlr/99ptZXnnllfK///2v1HPq87z11lvStWtX8Vf5+fnH3Of666+X119/XQoLC91WDoI8AIBfshXkSAtbsll6DcshoZJvlgAq7/bbbzeZll9++UUuv/xyadeunckmjR07ttKZtZI06xQfH28yWD169DBdKr/88ksT8E2bNs1l36lTp5og6rbbbpNPPvlEjhw5UuGxNRv46aefykUXXVTu8zpvGjSVVKdOHZd9YmL+7pFw4MABSUpKkgceeMAES23btpV//etfkpWVJevXry/a75VXXjEBsGY+y/Lhhx9KTk6Oea2dO3eWSy+91NSBBnnObJ4+b/FyLF++XA4fPmyCFKdJkyaZYHH8+PHSoUMHszznnHPM+uIyMzPl6quvlrfffltiY2OlMmbPni29evWS8PBwk7XUMlY2E5eammrWLVq0yNzXcuvzN2zY0GQutd70fVWtWrUyy5NOOsk8pnjW97333jPZSS2Dvr7JkyeXet7//Oc/5jG6zwcffCA7duwwWWZ9ndolU9tq8WB+0KBBcvDgQVm8eLG4C0EeAMAvhaVulcVhY83SW4QfXC9bwkeYJYDKOXTokMnaacBS1himevXq1VhVnn322dKtWzf5/PPPi9ZpwKPBwDXXXGNO8jXA1JP6Y3XV1CBDA5SSNOjQMbt6HM3YpaSklBmAaVCjwcG9994rGRkZRdvq169vxvlNnz7dBJua0dNMn3Y51WxnZWkGTrtqatBZPPjYs2ePCV7KolnMc889V1q0aOFyHO3KWpwe56effnJZp++fZiD18ZWhXV01qNPHrFq1ynTLLas+K+uRRx6RDRs2mCB+48aNJpOmdaz0xwO1cOFCk7F0vv8akD766KPy1FNPmcc888wz5jga+BV3//33y5133mn2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eeaZRfWt70d+fr7LPnqszp07855UQk3V8c8//2y6Opx66qlF+2j3Q13HZ6NsixYtMt3c2rVrJzfddJOkpKQUbaPOj09aWppZxsXF0c49VOdOtHP3KCwslBkzZsiRI0dMF0K+y2u/zp1o4+4xatQoOf/88+Xcc891We/PbT3YI88K48CBA+ZD3rhxY5ca0fva4FB1+uGaPn26OdHdt2+fPPXUU3LaaaeZvtXOOi2rvnfs2GH+1n1CQ0MlNjaW96QaaqqOdakBS0m6js9GaYMHD5YrrrhCWrRoYf7DeuSRR+Tss882/zHpf1bUefVp8nTs2LHSt29f8x867dwzda5o5zVv7dq1JsDIycmRyMhImTVrlpx44olFJ6V8l9denSvauHvMmDFDVq5cKb/++mtAnbcQ5HkBHQBa8j+4kutQOfoF6dSlSxfzRXrCCSfIe++9VzQRRXXqm/ekamqijsvan/ehbMOGDSv6W0+Ke/XqZQK+uXPnyqWXXkqdH4fRo0fLmjVrZOnSpaW20c5rt85p5zWvffv2snr1ajM5xMyZM2XEiBGyePHiou208dqrcw30aOM1b9euXXLXXXfJ/PnzzQSH5fHHtk53TQ/SGXqCgoJKRfjazarkLwqoHp0BSYM97cLpnGWzovrWfbQbrc6QxHtSdTVVx7qPZmJL2r9/P5+NSmjSpIkJ8rTdU+fVpzOpzZ49W77//ntp1qxZ0Xraee3XeVlo58dPsxNt2rQxPwzprIPdunWTl19+mTbugTovC238+K1YscKcX+js9cHBweamQfUrr7xi/naed/jjeQtBnoc/6NroFixY4LJe72sXQxw/nfJ248aN5otSx+jph7B4feuHVj/szvrW9yMkJMRln71798q6det4TyqhpupYM7A6JueXX34p2ud///ufWcdn49gOHjxofr3Udk+dV53+8qrZpM8//1y+++47065p556t87LQzt3zPuj/m3yX136dl4U2fvzOOecc00VWs6fOmwbYV199tfm7devW/nve4pHpXlDqEgpTpkwxM/OMGTPGXEJh+/bt1FI13HPPPdaiRYusP/74w8xwpFPX6rS4zvrU2ZN0xqTPP//cTJP7j3/8o8xpcps1a2YtXLjQTJN79tlncwmFEjNUrVq1ytz0K+TFF180fzsv+1FTdaxTEes0xzo7ld66dOkSsJdQqKjOdZu2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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, (ax_spec, ax_ratio) = plt.subplots(\n", + " 2, 1, figsize=(9, 6), sharex=True,\n", + " gridspec_kw={'height_ratios': [3, 1]}\n", + ")\n", + "\n", + "edges = hist_cpu.axes[0].edges\n", + "cpu_vals = hist_cpu.values()\n", + "cuda_vals = hist_cuda.values()\n", + "\n", + "ax_spec.stairs(cpu_vals, edges, label=f'CPU ({n_clusters_cpu} clusters)')\n", + "ax_spec.stairs(cuda_vals, edges, label=f'CUDA ({n_clusters_cuda} clusters)', linestyle='--')\n", + "ax_spec.set_ylabel('Counts')\n", + "ax_spec.set_title('Cluster energy spectrum: CPU vs CUDA')\n", + "ax_spec.legend()\n", + "ax_spec.grid(alpha=0.3)\n", + "\n", + "with np.errstate(divide='ignore', invalid='ignore'):\n", + " ratio = np.where(cpu_vals > 0, cuda_vals / cpu_vals, np.nan)\n", + "\n", + "ax_ratio.stairs(ratio, edges, color='k')\n", + "ax_ratio.axhline(1.0, color='gray', linewidth=0.5)\n", + "ax_ratio.set_ylabel('CUDA / CPU')\n", + "ax_ratio.set_xlabel('Energy [ADU]')\n", + "ax_ratio.set_ylim(0.5, 3.5)\n", + "ax_ratio.grid(alpha=0.3)\n", + "\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a6ca4581-bb29-4c74-a34a-bd427551a39b", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "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 +}