diff --git a/docs/ClusterFinderCUDA_optimizations.pptx b/docs/ClusterFinderCUDA_optimizations.pptx deleted file mode 100644 index 4dbdb834..00000000 Binary files a/docs/ClusterFinderCUDA_optimizations.pptx and /dev/null differ diff --git a/docs/Doxyfile.in b/docs/Doxyfile.in index bc9ebeaf..60abd60d 100644 --- a/docs/Doxyfile.in +++ b/docs/Doxyfile.in @@ -288,7 +288,7 @@ OPTIMIZE_OUTPUT_VHDL = NO # Note that for custom extensions you also need to set FILE_PATTERNS otherwise # the files are not read by doxygen. -EXTENSION_MAPPING = +EXTENSION_MAPPING = cu=C++ cuh=C++ # If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments # according to the Markdown format, which allows for more readable @@ -831,6 +831,8 @@ FILE_PATTERNS = *.c \ *.hxx \ *.hpp \ *.h++ \ + *.cu \ + *.cuh \ *.cs \ *.d \ *.php \ diff --git a/docs/src/ClusterFinderCUDA.rst b/docs/src/ClusterFinderCUDA.rst new file mode 100644 index 00000000..b8a93097 --- /dev/null +++ b/docs/src/ClusterFinderCUDA.rst @@ -0,0 +1,26 @@ +ClusterFinderCUDA +================= + +GPU cluster finder. Available only when aare is configured with +``-DAARE_CUDA=ON``; on a CPU-only build the class is not compiled and the +Python factory raises ``RuntimeError``. + +Single frames go through :cpp:func:`aare::ClusterFinderCUDA::find_clusters`, +which mirrors the CPU :cpp:class:`aare::ClusterFinder` interface. Throughput +comes from ``find_clusters_batched``, which distributes a batch of frames +round-robin over ``n_streams`` CUDA streams (4 by default) and overlaps H2D, +kernel and D2H work. Results are read back either with ``collect``, which +returns one ``ClusterVector`` per frame, or with ``collect_view``, which hands +back a ``BatchView`` reading clusters in place from the pinned host buffer with +no per-frame allocation or copy. + +.. doxygenclass:: aare::ClusterFinderCUDA + :members: + :undoc-members: + +Device kernel +------------- + +.. doxygennamespace:: aare::device + :members: + :undoc-members: diff --git a/docs/src/Installation.rst b/docs/src/Installation.rst index 2cd8c681..53dc3c50 100644 --- a/docs/src/Installation.rst +++ b/docs/src/Installation.rst @@ -109,6 +109,39 @@ Build the Python bindings. Default option is off. If you have a newer system Python compared to the one in your virtual environment, you might have to pass -DPython_FIND_VIRTUALENV=ONLY to cmake. +**AARE_CUDA "Build CUDA cluster finder backend" OFF** + +Build :doc:`ClusterFinderCUDA`, the GPU cluster finder. Default option is off, +in which case aare builds CPU-only and the class is not compiled at all. + +Requires the CUDA toolkit (nvcc) and a compiler combination nvcc accepts; see +:doc:`Requirements`. With ``AARE_PYTHON_BINDINGS=ON`` the CUDA bindings are +built as a separate extension module, ``_aare_cuda``, which avoids nvcc/gcc ABI +conflicts in the main module. The classes are re-exported onto ``aare``, so +imports stay the same either way: + +.. code-block:: bash + + cmake ../aare -DAARE_CUDA=ON -DAARE_PYTHON_BINDINGS=ON + make -j4 + +.. code-block:: python + + from aare import ClusterFinderCUDA, _cuda_available + +On a CPU-only build ``_cuda_available()`` returns False and the +``ClusterFinderCUDA`` factory raises ``RuntimeError``. + +**AARE_CUDA_ARCHITECTURES "CUDA architectures to compile for" native** + +Only used when ``AARE_CUDA=ON``. Defaults to ``native``, which targets the GPU +in the building machine. Set it explicitly to build portable binaries or to +cross-compile for a GPU you are not building on: + +.. code-block:: bash + + cmake ../aare -DAARE_CUDA=ON -DAARE_CUDA_ARCHITECTURES="80;89" + **AARE_TESTS "Build tests" OFF** Build unit tests. Default option is off. diff --git a/docs/src/Requirements.rst b/docs/src/Requirements.rst index 7c44dbdc..5d820976 100644 --- a/docs/src/Requirements.rst +++ b/docs/src/Requirements.rst @@ -19,6 +19,17 @@ To simplify deployment we build and statically link a few libraries. - pybind11 - ZeroMQ +**Extra dependencies for the CUDA backend (-DAARE_CUDA=ON)** + +Only needed to build :doc:`ClusterFinderCUDA`; aare builds CPU-only without them. + +- CUDA toolkit 11.0 or newer (nvcc), and a host compiler that nvcc supports. + The device code is compiled as CUDA C++17, which nvcc supports from CUDA 11. +- CMake 3.24+ if you keep the default ``AARE_CUDA_ARCHITECTURES=native``; + ``native`` is not understood by older CMake. With an explicit architecture + list the project minimum of 3.15 is enough. +- An NVIDIA GPU at runtime. + **Extra dependencies for building documentation** - Sphinx diff --git a/docs/src/index.rst b/docs/src/index.rst index ab22e57d..3891f222 100644 --- a/docs/src/index.rst +++ b/docs/src/index.rst @@ -49,6 +49,7 @@ AARE Cluster ClusterFinder ClusterFinderMT + ClusterFinderCUDA ClusterFile ClusterVector Interpolation diff --git a/docs/src/python/cluster/index.rst b/docs/src/python/cluster/index.rst index 7f218943..6a994c5c 100644 --- a/docs/src/python/cluster/index.rst +++ b/docs/src/python/cluster/index.rst @@ -7,5 +7,6 @@ Cluster & Interpolation pyCluster pyClusterVector + pyClusterFinderCUDA pyInterpolation pyVarClusterFinder diff --git a/docs/src/python/cluster/pyClusterFinderCUDA.rst b/docs/src/python/cluster/pyClusterFinderCUDA.rst new file mode 100644 index 00000000..5f4fb96b --- /dev/null +++ b/docs/src/python/cluster/pyClusterFinderCUDA.rst @@ -0,0 +1,11 @@ +ClusterFinderCUDA +================= + +.. py:currentmodule:: aare + +Requires a build configured with ``-DAARE_CUDA=ON``. On a CPU-only build the +factory raises ``RuntimeError``. + +.. autofunction:: ClusterFinderCUDA + +.. autofunction:: find_cluster_views_batched_iter diff --git a/include/aare/ClusterFinderCUDA_old.hpp b/include/aare/ClusterFinderCUDA_old.hpp deleted file mode 100644 index 81a528d8..00000000 --- a/include/aare/ClusterFinderCUDA_old.hpp +++ /dev/null @@ -1,435 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -#pragma once -#include "aare/ClusterFinder.hpp" -#include "aare/clusterfinder_kernel.cuh" -#include "aare/utils/cuda_check.cuh" -#include -#include -#include -#include -#include -#include -#include - -namespace aare { - -// Per-stream device resources -template -struct StreamContext { - cudaStream_t stream = nullptr; // handle to the stream - FRAME_TYPE *d_frame = nullptr; - PEDESTAL_TYPE *d_pd_mean = nullptr; - PEDESTAL_TYPE *d_pd_sum = nullptr; - PEDESTAL_TYPE *d_pd_sum2 = nullptr; - ClusterType *d_clusters = nullptr; - uint32_t *d_cluster_count = nullptr; - - // Pinned host staging buffers. These make cudaMemcpyAsync real async DMA - // transfers even when the caller's NDView points to pageable memory. - FRAME_TYPE *h_frame = nullptr; - uint32_t *h_cluster_count = nullptr; - ClusterType *h_clusters = nullptr; - - cudaEvent_t kernel_start = nullptr; - cudaEvent_t kernel_stop = nullptr; -}; - -template , - typename FRAME_TYPE = uint16_t, typename PEDESTAL_TYPE = double, - typename = std::enable_if_t::value>> -class ClusterFinderCUDA { - using COMPUTE_TYPE = - device::COMPUTE_TYPE; // match the kernel's internal precision - - static constexpr int BLOCK_X = 16; - static constexpr int BLOCK_Y = 16; - static constexpr int col_radius = ClusterType::cluster_size_x / 2; - static constexpr int row_radius = ClusterType::cluster_size_y / 2; - - Shape<2> m_shape; - size_t nrows; - size_t ncols; - size_t m_image_size; // nrows * ncols - int n_streams; - size_t m_capacity; - - size_t m_image_bytes; - size_t m_cluster_bytes; - - COMPUTE_TYPE m_nSigma; - Pedestal m_pedestal; - ClusterVector m_clusters; - bool m_pedestal_dirty = true; - - using SC = StreamContext; - std::vector v_sc; - - float m_total_kernel_ms = 0.0f; - size_t m_frames_processed = 0; - - // Kernel parameters - dim3 grid; - dim3 block; - size_t shmem_bytes; - - public: - /** - * @brief Construct a ClusterFinderCUDA - * - * @param m_image_size shape of the detector frame (rows, cols) - * @param nSigma threshold in units of per-pixel pedestal - * std - * @param capacity device-side cluster buffer size per stream - * @param n_streams number of CUDA streams for multi-frame - * overlap - */ - ClusterFinderCUDA(Shape<2> shape_, COMPUTE_TYPE nSigma = 5.0, - size_t capacity = 1000000, int n_streams_ = 1) - : m_shape(shape_), nrows(shape_[0]), ncols(shape_[1]), - m_image_size(nrows * ncols), n_streams(n_streams_), - m_capacity(capacity), m_nSigma(nSigma), - m_pedestal(shape_[0], shape_[1]), m_clusters(capacity) { - if (n_streams_ <= 0) { - throw std::invalid_argument( - "ClusterFinderCUDA: n_streams must be > 0"); - } - - if (capacity > - static_cast(std::numeric_limits::max())) { - throw std::invalid_argument( - "ClusterFinderCUDA: capacity must fit in uint32_t"); - } - - if (capacity == 0) { - throw std::invalid_argument( - "ClusterFinderCUDA: capacity must be > 0"); - } - - // Grid/Block dimensions - block = dim3(BLOCK_X, BLOCK_Y); - grid = dim3((static_cast(ncols) + BLOCK_X - 1) / BLOCK_X, - (static_cast(nrows) + BLOCK_Y - 1) / BLOCK_Y); - - // Shared memory: one tile of (BLOCK_X + 2*col_radius) x (BLOCK_Y + - // 2*row_radius) elements - // Mixed precision used -> shmem takes COMPUTE_TYPE = floats (not - // PEDESTAL_TYPE) - shmem_bytes = (BLOCK_X + 2 * col_radius) * (BLOCK_Y + 2 * row_radius) * - sizeof(COMPUTE_TYPE); - - m_image_bytes = m_image_size * sizeof(FRAME_TYPE); - m_cluster_bytes = m_capacity * sizeof(ClusterType); - - v_sc.resize(n_streams); - for (int k = 0; k < n_streams; ++k) { - auto &sc = v_sc[k]; - CUDA_CHECK( - cudaStreamCreateWithFlags(&sc.stream, cudaStreamNonBlocking)); - CUDA_CHECK(cudaEventCreate(&sc.kernel_start)); - CUDA_CHECK(cudaEventCreate(&sc.kernel_stop)); - CUDA_CHECK(cudaMalloc(&sc.d_frame, m_image_bytes)); - CUDA_CHECK(cudaMalloc(&sc.d_pd_mean, - m_image_size * sizeof(PEDESTAL_TYPE))); - CUDA_CHECK( - cudaMalloc(&sc.d_pd_sum, m_image_size * sizeof(PEDESTAL_TYPE))); - CUDA_CHECK(cudaMalloc(&sc.d_pd_sum2, - m_image_size * sizeof(PEDESTAL_TYPE))); - CUDA_CHECK(cudaMalloc(&sc.d_clusters, m_cluster_bytes)); - CUDA_CHECK(cudaMalloc(&sc.d_cluster_count, sizeof(uint32_t))); - - CUDA_CHECK(cudaMallocHost(reinterpret_cast(&sc.h_frame), - m_image_bytes)); - CUDA_CHECK( - cudaMallocHost(reinterpret_cast(&sc.h_cluster_count), - sizeof(uint32_t))); - if (m_cluster_bytes > 0) { - CUDA_CHECK( - cudaMallocHost(reinterpret_cast(&sc.h_clusters), - m_cluster_bytes)); - } - } - } - - ~ClusterFinderCUDA() { - for (auto &sc : v_sc) { - if (sc.stream) - cudaStreamSynchronize(sc.stream); - - if (sc.d_frame) - cudaFree(sc.d_frame); - if (sc.d_pd_mean) - cudaFree(sc.d_pd_mean); - if (sc.d_pd_sum) - cudaFree(sc.d_pd_sum); - if (sc.d_pd_sum2) - cudaFree(sc.d_pd_sum2); - if (sc.d_clusters) - cudaFree(sc.d_clusters); - if (sc.d_cluster_count) - cudaFree(sc.d_cluster_count); - - if (sc.h_frame) - cudaFreeHost(sc.h_frame); - if (sc.h_clusters) - cudaFreeHost(sc.h_clusters); - if (sc.h_cluster_count) - cudaFreeHost(sc.h_cluster_count); - - if (sc.kernel_start) - cudaEventDestroy(sc.kernel_start); - if (sc.kernel_stop) - cudaEventDestroy(sc.kernel_stop); - if (sc.stream) - cudaStreamDestroy(sc.stream); - } - } - - // Non-copyable, non-movable - ClusterFinderCUDA(const ClusterFinderCUDA &) = delete; - ClusterFinderCUDA &operator=(const ClusterFinderCUDA &) = delete; - ClusterFinderCUDA(ClusterFinderCUDA &&) = delete; - ClusterFinderCUDA &operator=(ClusterFinderCUDA &&) = delete; - - void set_nSigma(COMPUTE_TYPE nSigma) { m_nSigma = nSigma; } - COMPUTE_TYPE get_nSigma() const { return m_nSigma; } - - void push_pedestal_frame(NDView frame) { - m_pedestal.push(frame); - m_pedestal_dirty = true; - } - - void clear_pedestal() { - m_pedestal.clear(); - m_pedestal_dirty = true; - } - - NDArray pedestal() { return m_pedestal.mean(); } - NDArray noise() { return m_pedestal.std(); } - - /** - * @brief Move clusters out of the internal ClusterVector, optionally - * reallocating the internal one with the same capacity. - */ - ClusterVector - steal_clusters(bool realloc_same_capacity = false) { - ClusterVector tmp = std::move(m_clusters); - if (realloc_same_capacity) - m_clusters = ClusterVector(tmp.capacity()); - else - m_clusters = ClusterVector{}; - return tmp; - } - - /** - * @brief Find clusters in a single frame, appending them to the internal - * ClusterVector. - */ - void find_clusters(NDView frame, uint64_t frame_number = 0) { - if (m_pedestal_dirty) { // need to update the pedestal on the gpu - sync_pedestal_to_device(); - m_pedestal_dirty = false; - } - - auto &sc = v_sc[0]; - const uint32_t n_pd_samples = - static_cast(m_pedestal.n_samples()); - - // First, CPU copies frame into a reusable pinned buffer - std::memcpy(sc.h_frame, frame.data(), m_image_bytes); - - // Reset cluster counter - CUDA_CHECK(cudaMemsetAsync(sc.d_cluster_count, 0, sizeof(uint32_t), - sc.stream)); - - // Upload frame - CUDA_CHECK(cudaMemcpyAsync(sc.d_frame, sc.h_frame, m_image_bytes, - cudaMemcpyHostToDevice, sc.stream)); - - // Timed Kernel launch - CUDA_CHECK(cudaEventRecord(sc.kernel_start, sc.stream)); - device::find_clusters_in_single_frame - <<>>( - sc.d_frame, sc.d_pd_mean, sc.d_pd_sum, sc.d_pd_sum2, - n_pd_samples, m_nSigma, nrows, ncols, sc.d_clusters, - sc.d_cluster_count, static_cast(m_capacity)); - CUDA_CHECK(cudaEventRecord(sc.kernel_stop, sc.stream)); - CUDA_CHECK(cudaGetLastError()); - - // Read back cluster count into pinned buffer - CUDA_CHECK(cudaMemcpyAsync(sc.h_cluster_count, sc.d_cluster_count, - sizeof(uint32_t), cudaMemcpyDeviceToHost, - sc.stream)); - - // Synchronize to ensure count is available before the CPU reads - // clusters - CUDA_CHECK(cudaStreamSynchronize(sc.stream)); - - record_kernel_time(sc); - - // Clamp to max in case of overflow - uint32_t n_found = *sc.h_cluster_count; - n_found = std::min(n_found, static_cast(m_capacity)); - - // Read back clusters - m_clusters.set_frame_number(frame_number); - if (n_found > 0) { - append_device_clusters_to(m_clusters, sc, n_found); - } - } - - /** - * @brief Batched cluster finding across multiple frames, using n_streams - * CUDA streams to overlap H2D transfer, kernel, and D2H transfer. - * - * Returns one ClusterVector per input frame (with frame_number set to - * first_frame + i). - */ - std::vector> - find_clusters_batched(NDView frames, - uint64_t first_frame = 0) { - if (m_pedestal_dirty) { - sync_pedestal_to_device(); - m_pedestal_dirty = false; - } - - const size_t n_frames = frames.shape(0); - const uint32_t n_pd_samples = - static_cast(m_pedestal.n_samples()); - - std::vector> results; - results.reserve(n_frames); - for (size_t i = 0; i < n_frames; ++i) { - results.emplace_back(); - results.back().set_frame_number(first_frame + i); - } - - const size_t n_rounds = (n_frames + n_streams - 1) / n_streams; - for (size_t round = 0; round < n_rounds; ++round) { - - // Launch phase: fan out kernels on all streams for this round - for (int k = 0; k < n_streams; ++k) { - // OOB guard - const size_t frame_idx = round * n_streams + k; - if (frame_idx >= n_frames) - continue; - - auto &sc_k = v_sc[k]; - const FRAME_TYPE *h_src = - frames.data() + frame_idx * m_image_size; - - std::memcpy(sc_k.h_frame, h_src, m_image_bytes); - - CUDA_CHECK(cudaMemsetAsync(sc_k.d_cluster_count, 0, - sizeof(uint32_t), sc_k.stream)); - CUDA_CHECK( - cudaMemcpyAsync(sc_k.d_frame, sc_k.h_frame, m_image_bytes, - cudaMemcpyHostToDevice, sc_k.stream)); - - CUDA_CHECK(cudaEventRecord(sc_k.kernel_start, sc_k.stream)); - device::find_clusters_in_single_frame - <<>>( - sc_k.d_frame, sc_k.d_pd_mean, sc_k.d_pd_sum, - sc_k.d_pd_sum2, n_pd_samples, m_nSigma, nrows, ncols, - sc_k.d_clusters, sc_k.d_cluster_count, - static_cast(m_capacity)); - CUDA_CHECK(cudaEventRecord(sc_k.kernel_stop, sc_k.stream)); - CUDA_CHECK(cudaGetLastError()); - - // Queue count D2H immediately after the kernel - CUDA_CHECK(cudaMemcpyAsync( - sc_k.h_cluster_count, sc_k.d_cluster_count, - sizeof(uint32_t), cudaMemcpyDeviceToHost, sc_k.stream)); - } - - // Drain phase: fan in results from all streams - for (int k = 0; k < n_streams; ++k) { - const size_t frame_idx = round * n_streams + k; - if (frame_idx >= n_frames) - continue; - - auto &sc_k = v_sc[k]; - - // Wait for memset -> H2D -> kernel -> count D2H - CUDA_CHECK(cudaStreamSynchronize(sc_k.stream)); - - record_kernel_time(sc_k); - - uint32_t n_found = *sc_k.h_cluster_count; - n_found = std::min(n_found, - static_cast(m_capacity)); - - if (n_found > 0) { - append_device_clusters_to(results[frame_idx], sc_k, - n_found); - } - } - } - - return results; - } - - float avg_kernel_time_ms() const { - return m_frames_processed > 0 ? m_total_kernel_ms / m_frames_processed - : 0.0f; - } - - void reset_timers() { - m_total_kernel_ms = 0.0f; - m_frames_processed = 0; - } - - private: - /** - * Upload the current host pedestal (mean, sum, sum2) to every stream's - * device buffers. Called lazily before a find_clusters call when the - * host pedestal has been updated. - */ - void sync_pedestal_to_device() { - // These return-by-value NDArrays must stay alive until the async - // copies complete, so we synchronise at the end before they go out - // of scope. - NDArray h_mean = m_pedestal.mean(); - NDArray h_sum = m_pedestal.get_sum(); - NDArray h_sum2 = m_pedestal.get_sum2(); - - const size_t bytes = m_image_size * sizeof(PEDESTAL_TYPE); - for (auto &sc : v_sc) { - CUDA_CHECK(cudaMemcpyAsync(sc.d_pd_mean, h_mean.data(), bytes, - cudaMemcpyHostToDevice, sc.stream)); - CUDA_CHECK(cudaMemcpyAsync(sc.d_pd_sum, h_sum.data(), bytes, - cudaMemcpyHostToDevice, sc.stream)); - CUDA_CHECK(cudaMemcpyAsync(sc.d_pd_sum2, h_sum2.data(), bytes, - cudaMemcpyHostToDevice, sc.stream)); - } - for (auto &sc : v_sc) - CUDA_CHECK(cudaStreamSynchronize(sc.stream)); - } - - /** - * Copy n_found clusters from sc.d_clusters into the given ClusterVector - * and block on the transfer. - */ - void append_device_clusters_to(ClusterVector &cv, SC &sc, - uint32_t n_found) { - - CUDA_CHECK(cudaMemcpyAsync(sc.h_clusters, sc.d_clusters, - n_found * sizeof(ClusterType), - cudaMemcpyDeviceToHost, sc.stream)); - - CUDA_CHECK(cudaStreamSynchronize(sc.stream)); - - for (uint32_t i = 0; i < n_found; ++i) - cv.push_back(sc.h_clusters[i]); - } - - void record_kernel_time(SC &sc) { - float ms = 0.0f; - CUDA_CHECK(cudaEventElapsedTime(&ms, sc.kernel_start, sc.kernel_stop)); - m_total_kernel_ms += ms; - m_frames_processed++; - } -}; - -} // namespace aare diff --git a/include/aare/clusterfinder_kernel.cuh b/include/aare/clusterfinder_kernel.cuh index b3a2d34c..436ed576 100644 --- a/include/aare/clusterfinder_kernel.cuh +++ b/include/aare/clusterfinder_kernel.cuh @@ -6,14 +6,12 @@ namespace aare::device { -// Device arithmetic precision. -// COMPUTE_TYPE : per-frame stencil arithmetic (shared-memory tile, sums). -// DEVICE_PED_TYPE : device pedestal storage + running variance update. -// Shipped as double/double so the GPU result matches the double-precision CPU -// ClusterFinder to the floating-point floor. float/float is ~2x faster but -// reintroduces a small near-threshold mismatch; a build-time toggle for that -// trade-off is planned. +/// Stencil arithmetic precision. Set both aliases to double for an f64 build. +/// float is safe here because the device pedestal stores centered moments +/// against a host-computed baseline: f32 and f64 agree to 3e-7 in cluster +/// count, and f32 cuts the 9x9 kernel time by ~40%. using COMPUTE_TYPE = float; +/// Device pedestal storage and running variance update. See COMPUTE_TYPE. using DEVICE_PED_TYPE = float; template , diff --git a/include/aare/utils/batch.hpp b/include/aare/utils/batch.hpp deleted file mode 100644 index 4584a670..00000000 --- a/include/aare/utils/batch.hpp +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -#pragma once -#include "aare/NDArray.hpp" -#include -#include - -template -void pack_frame_batch(const std::vector> &frames, - size_t first_frame, size_t n_frames, - std::vector &batch) { - if (n_frames == 0) - return; - - const size_t rows = frames[first_frame].shape(0); - const size_t cols = frames[first_frame].shape(1); - const size_t image_size = rows * cols; - const size_t total_size = n_frames * image_size; - - if (batch.size() != total_size) { - batch.resize(total_size); - } - - for (size_t k = 0; k < n_frames; ++k) { - const FRAME_TYPE *src = frames[first_frame + k].data(); - FRAME_TYPE *dst = batch.data() + k * image_size; - std::memcpy(dst, src, image_size * sizeof(FRAME_TYPE)); - } -} \ No newline at end of file diff --git a/python/tests/ClusterFinderCUDA.ipynb b/python/tests/ClusterFinderCUDA.ipynb index 1522ba82..efd3415e 100644 --- a/python/tests/ClusterFinderCUDA.ipynb +++ b/python/tests/ClusterFinderCUDA.ipynb @@ -1,596 +1,595 @@ - { - "cells": - [ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "title", + "metadata": {}, + "source": [ + "# ClusterFinder — CPU vs CUDA\n", + "\n", + "Two finders over the same frames and the same pedestal:\n", + "\n", + "| | |\n", + "|---|---|\n", + "| **`ClusterFinderMT`** + `ClusterCollector` | multithreaded CPU baseline |\n", + "| **`ClusterFinderCUDA.find_clusters_batched`** | batched across CUDA streams, over a pinned input buffer, so H2D / kernel / D2H overlap |\n", + "\n", + "**Part 1** times them. **Part 2** compares what they found." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "imports", + "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 boost_histogram as bh\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "from tqdm import tqdm\n", + "\n", + "from aare import (File, ClusterFinderMT, ClusterCollector, ClusterFinderCUDA)" + ] + }, + { + "cell_type": "markdown", + "id": "md-config", + "metadata": {}, + "source": [ + "## Configuration" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "config", + "metadata": {}, + "outputs": [ { - "cell_type": "markdown", - "id": "title", - "metadata": {}, - "source": [ - "# ClusterFinder — CPU vs CUDA\n", - "\n", - "Two finders over the same frames and the same pedestal:\n", - "\n", - "| | |\n", - "|---|---|\n", - "| **`ClusterFinderMT`** + `ClusterCollector` | multithreaded CPU baseline |\n", - "| **`ClusterFinderCUDA.find_clusters_batched`** | batched across CUDA streams, over a pinned input buffer, so H2D / kernel / D2H overlap |\n", - "\n", - "**Part 1** times them. **Part 2** compares what they found." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "imports", - "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 boost_histogram as bh\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "from tqdm import tqdm\n", - "\n", - "from aare import (File, ClusterFinderMT, ClusterCollector, ClusterFinderCUDA)" - ] - }, - { - "cell_type": "markdown", - "id": "md-config", - "metadata": {}, - "source": [ - "## Configuration" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "config", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Image size: (400, 400) cluster (7, 7)\n", - "Pedestal frames: 1000\n", - "Data frames: 10000 of 100000 in file\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", - "pd = File(base / 'Cu_factor_10_pedestal_master_0.json')\n", - "\n", - "n_frames_pd = 1000 # pedestal frames, taken from the head of the file\n", - "N = 10000 # data frames, read after those\n", - "cluster_size = (7, 7)\n", - "rows, cols = f.rows, f.cols\n", - "image_size = (rows, cols)\n", - "\n", - "N_SIGMA = 5\n", - "N_STREAMS = 4 # H2D / kernel / D2H overlap\n", - "BATCH_SIZE = 2000 # frames per find_clusters_batched call\n", - "\n", - "# CPU workers. ClusterFinderMT gives each thread its own ClusterFinder with its\n", - "# own pedestal, and data frames are handed out round-robin -- so at high thread\n", - "# counts each pedestal tracks on only 1/n of the frames while the CUDA finder's\n", - "# single pedestal sees all of them, and the cluster counts drift apart. Keep it\n", - "# low when comparing counts; raise it to measure CPU throughput.\n", - "N_THREADS = 4\n", - "\n", - "# Fixed per-frame output slot the kernel writes into, copied WHOLE on every D2H.\n", - "# Too low does not error -- it TRUNCATES. The CUDA cell prints the peak actually\n", - "# reached, which is how you size this for a new dataset.\n", - "CAP = 3000\n", - "\n", - "capacity = 10_000 # initial ClusterVector capacity, CPU side\n", - "N_BINS = 200\n", - "E_MIN, E_MAX = -2, 4000 # spectrum range, ADU\n", - "\n", - "print(f'Image size: {image_size} cluster {cluster_size}')\n", - "print(f'Pedestal frames: {n_frames_pd}')\n", - "print(f'Data frames: {N} of {f.total_frames} in file')" - ] - }, - { - "cell_type": "markdown", - "id": "md-build", - "metadata": {}, - "source": [ - "## Build the two finders" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "build", - "metadata": {}, - "outputs": [], - "source": [ - "cf_cpu = ClusterFinderMT(image_size, cluster_size, n_sigma=N_SIGMA,\n", - " capacity=capacity, n_threads=N_THREADS)\n", - "sink = ClusterCollector(cf_cpu)\n", - "\n", - "cf_cuda = ClusterFinderCUDA(image_size, cluster_size, n_sigma=N_SIGMA,\n", - " max_clusters_per_frame=CAP, n_streams=N_STREAMS)" - ] - }, - { - "cell_type": "markdown", - "id": "md-ped", - "metadata": {}, - "source": [ - "## Pedestal\n", - "\n", - "Identical frames into both, so the implementation is the only thing that differs." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "train", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Pedestal (1000 frames): 0.32s\n" - ] - } - ], - "source": [ - "f.seek(0)\n", - "t0 = time.perf_counter()\n", - "for _ in range(n_frames_pd):\n", - " pd_img = pd.read_frame()\n", - " cf_cpu.push_pedestal_frame(pd_img.copy())\n", - " cf_cuda.push_pedestal_frame(pd_img.copy())\n", - "print(f'Pedestal ({n_frames_pd} frames): {time.perf_counter() - t0:.2f}s')" - ] - }, - { - "cell_type": "markdown", - "id": "md-io", - "metadata": {}, - "source": [ - "## Read the data frames\n", - "\n", - "Kept out of both timing loops: the two finders run over the same in-memory array." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "io", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Reading 10000 frames: 0.44s (22,750 FPS, 3.20 GB at 7.28 GB/s)\n" - ] - } - ], - "source": [ - "f.seek(0)\n", - "t0 = time.perf_counter()\n", - "data = f.read_n(N)\n", - "t_io = time.perf_counter() - t0\n", - "gb = f.bytes_per_frame * N / 1e9\n", - "print(f'Reading {N} frames: {t_io:.2f}s ({N / t_io:,.0f} FPS, '\n", - " f'{gb:.2f} GB at {gb / t_io:.2f} GB/s)')" - ] - }, - { - "cell_type": "markdown", - "id": "md-cpu", - "metadata": {}, - "source": [ - "## 1 — CPU, multithreaded" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "cpu", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "100%|██████████████████████████████████████████| 10000/10000 [00:16<00:00, 615.07it/s]\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU MT (4 threads): 16.26s 615 FPS 17,463,321 clusters (1746.3/frame)\n" - ] - } - ], - "source": [ - "t0 = time.perf_counter()\n", - "# Pass the frame number: find_clusters(frame) defaults it to 0, and every\n", - "# resulting cluster is then stamped frame 0. Part 2 groups by it.\n", - "for i, frame in enumerate(tqdm(data)):\n", - " cf_cpu.find_clusters(frame, i)\n", - "t_cpu = time.perf_counter() - t0\n", - "\n", - "cf_cpu.stop(); sink.stop()\n", - "clusters_cpu = sink.steal_clusters()\n", - "\n", - "hist_cpu = bh.Histogram(bh.axis.Regular(N_BINS, E_MIN, E_MAX))\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 MT ({N_THREADS} threads): {t_cpu:.2f}s {N / t_cpu:,.0f} FPS '\n", - " f'{n_clusters_cpu:,} clusters ({n_clusters_cpu / N:.1f}/frame)')" - ] - }, - { - "cell_type": "markdown", - "id": "md-cuda", - "metadata": {}, - "source": [ - "## 2 — CUDA, batched + multi-streamed\n", - "\n", - "`register_input_buffer` page-locks the input once so H2D runs at full DMA\n", - "bandwidth instead of through the driver's staging buffer. It is paid once,\n", - "outside the loop, and released when the dataset is done." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "cuda", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CUDA batched: 1.17s 8,574 FPS 17,463,350 clusters (1746.3/frame)\n", - " input pinned in 0.09s, once, outside the loop\n", - " vs CPU: 13.94x\n", - " peak clusters in one frame: 2,062 of CAP 3,000\n" - ] - } - ], - "source": [ - "t0 = time.perf_counter()\n", - "cf_cuda.register_input_buffer(data)\n", - "t_pin = time.perf_counter() - t0\n", - "\n", - "clusters_cuda = []\n", - "t0 = time.perf_counter()\n", - "for start in range(0, N, BATCH_SIZE):\n", - " stop = min(start + BATCH_SIZE, N)\n", - " clusters_cuda.extend(\n", - " cf_cuda.find_clusters_batched(data[start:stop], first_frame=start))\n", - "t_cuda = time.perf_counter() - t0\n", - "\n", - "cf_cuda.unregister_input_buffer()\n", - "\n", - "hist_cuda = bh.Histogram(bh.axis.Regular(N_BINS, E_MIN, E_MAX))\n", - "energies = [np.asarray(cv.sum()).ravel() for cv in clusters_cuda if cv.size > 0]\n", - "if energies:\n", - " hist_cuda.fill(np.concatenate(energies))\n", - "n_clusters_cuda = sum(cv.size for cv in clusters_cuda)\n", - "\n", - "print(f'CUDA batched: {t_cuda:.2f}s {N / t_cuda:,.0f} FPS '\n", - " f'{n_clusters_cuda:,} clusters ({n_clusters_cuda / N:.1f}/frame)')\n", - "print(f' input pinned in {t_pin:.2f}s, once, outside the loop')\n", - "print(f' vs CPU: {t_cpu / t_cuda:.2f}x')\n", - "\n", - "# One ClusterVector per frame, so this IS the per-frame maximum.\n", - "peak = max((cv.size for cv in clusters_cuda), default=0)\n", - "print(f' peak clusters in one frame: {peak:,} of CAP {CAP:,}')\n", - "if peak >= CAP:\n", - " print(' !! CAP REACHED -- clusters were dropped; raise CAP and re-run.')" - ] - }, - { - "cell_type": "markdown", - "id": "md-summary", - "metadata": {}, - "source": [ - "## Summary" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "summary", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " s FPS us/frame clusters\n", - "CPU MT (4 thr) 16.26 615 1626.1 17,463,321\n", - "CUDA batched 1.17 8,574 116.6 17,463,350\n", - "\n", - "speedup: 13.94x\n" - ] - } - ], - "source": [ - "print(f'{\"\":<22} {\"s\":>7} {\"FPS\":>10} {\"us/frame\":>10} {\"clusters\":>13}')\n", - "for name, t, n in (('CPU MT (%d thr)' % N_THREADS, t_cpu, n_clusters_cpu),\n", - " ('CUDA batched', t_cuda, n_clusters_cuda)):\n", - " print(f'{name:<22} {t:7.2f} {N / t:10,.0f} {t * 1e6 / N:10.1f} {n:>13,}')\n", - "print(f'\\nspeedup: {t_cpu / t_cuda:.2f}x')" - ] - }, - { - "cell_type": "markdown", - "id": "md-part2", - "metadata": {}, - "source": [ - "# Part 2 — Where the two disagree\n", - "\n", - "The two implementations are not bit-identical by construction: the CUDA finder\n", - "updates its pedestal once per frame, the CPU finder per pixel, so once the\n", - "pedestals drift apart a few pixels land on opposite sides of the threshold.\n", - "\n", - "Three views, cheapest first: the **spectra**, the **cluster sets**, and **where\n", - "in the run** the difference accumulates." - ] - }, - { - "cell_type": "markdown", - "id": "md-spec", - "metadata": {}, - "source": [ - "## Spectra\n", - "\n", - "Both curves should lie on top of each other; the ratio panel is the sensitive\n", - "view." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "spectra", - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": - "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EP38te5r/Z44caI5tv7oYdO/FQ0C9W9NPy87GF63bp2p31pv9e9Ig7hnnnnG/E3r9543/bvWz/3uu+82Xda1C6nWAf3RYurUqebz1wBOf5jQuquBpX5v2gGe/gig59W6fuaZZ5rupJpfrc+aZ6V/j3369JH+/fvLc889Z9LrDxOrV68O+L4BFLJsbXsAEKG2b99uukDdcMMNeX6Nf3fN7777zhxDb72tX7/e7J84caJ5/O+//5rHY8eOPaEueiNHjrRiYmKsJUuW+Oz/5JNPsnUN08eJiYnWnj17jvt+Nm3aZMXFxVn9+/f32X/gwAGrQoUKPt3TtHucHvujjz7ySXv55ZdbderU8Tz+4IMPTLqpU6f6pNO86/7x48d79lWvXt10l/3jjz+ydZlzuVzWqlWrfPa3b98+W3lrvk4//XTTtdb2zDPPmPLSzyEnef1MNI+al2XLlmXraleqVClP97i8fkbz5883j4cMGXJCdcGuc5deemm255KSksxz/rQe6n7v8tD3VaxYMWvLli2effoeNZ12Q/Tu9vfZZ5+Z/V988YUVDnLqrvnPP/+YfOpn4U+7Supz2kU7J/l5/YgRI8zjbdu2ZUurXThr167teVyrVi1Td/1t3brVHOOpp57K9f3an59/3dL6ofu//fbb43bZ1u688+bNM+l/++23bH/X3n+vmva0004z+3/55RfP/t27d5u/14EDB3r23XXXXdYpp5xibdy40eeczz33nHm9/Tfcr18/69RTT801nwBCh+6aAHACypQpY37h1hay0aNHm66Q2uKVV9qFTn891+592gXK3rR7VaDuotrdrHTp0sc97jfffGOOc8stt/gcV1u/tFXK/7h6ri5dumRrjdCWOu+8avdDTed9TM27toL5H1Nf79+yot0U9f16tyIqnajCn7ao7dy5Uz7++GPzWMtVW/86derk0931ZD6T+vXrm25m3m666SbT0mHPupjXz0gn3VDa/fRkXH311XKyNK/a3dOmrU1KW1y165//fu/PORDv953fLZgCdVnNy3Mn8vqc0uY1XV7zlBP9O9e/d3/a4qh1VP/mYmNjTQuj/k0rbTn3P7+2+tm0xVFn8tRum9qV1/tvRluM/f/edUIY7fLs/Xl27NjR87estLVPW8j1b/jzzz83reYAwgdBHgDHKFeunLmQ1S5oBU0vorT7m17wa9dA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n30avGi+7On2NY2e394eeOABqV+/vs++Bx98MM/nfPHFF+XWW2+VmJhjYYNO89+hQwcZPHhwwNdokON9Pt3uuOMOEwQ3btz4uOf87LPPTIBVqVKlgAGenrtdu3ayePFiEzT269fPJ3+tWrWSjz76SP744w+ZOnWq/PXXX3LNNdfkes7atWvLuHHjzGf7448/mrzqOXbt2uUpZ92ee+45k2bSpEmmDtx+++0+x9GF06+99lq55557cjyXlueECRPMrJ0Fxgqh8ePHWw0bNrRKlixptosvvtiaMWOG5/mePXtamkXvrUmTJj7HOHLkiNWvXz+rbNmyVvHixa0uXbpYmzdv9kmzZ88eq3v37lapUqXMpvf37t3rk2bjxo1W586dzTH0WP3797eOHj3qk2b58uXWpZdeahUtWtSqVKmSlZycbGVkZOTrPaekpJj3obfhQPOvZZHf9wHKONxQlyljJ6AeU85OrsuHDx+2Vq9ebW4jTceOHa3KlStbBw8ezPac9zVl9erVrTFjxvg8X6tWLeuGG24w95OSkqxGjRoFPIZeH3733Xc55qFFixbWPffcY65Tf/zxR7NPy/eJJ54w1796XD2+v5zOmRe7du2yXC6XtXLlyoDPa3413/7X1f5SU1Ot008/3Ro+fPhxz7llyxZT1nrOQOWpscBjjz2Wr/fx+eefm/eh+cjvNfvs2bOttLS0gNfKH330kZWQkGCe9zdx4kQrMTEx4LE1xihSpIj17bffBnw+t7+VvMYSIW3J0/6o2uT8888/m02bXbt16yarVq3ypNFI3ftXgBkzZvgcY8CAATJt2jSZMmWKiboPHjwonTt39omMb7rpJlm2bJmJtnXT+9psa9O02sytfWP1GHosjfr1lw/b/v37pW3btuYXBf3F4KWXXjKR/OjRowu8nAAAABAa2k1Prx+1xU678vkL1CXPm3YZPJmp8L0lJCTIzTff7NOa984775iWoYKg18Xa7bBu3bondZwvvvhC/v33X+nVq1eu6TIyMsw1+kMPPWRaH/3p2nHawqfrx2lrYfny5aVFixYmn7l9fpMnTzbp4+Pj85RfbY3TLqmJiYnSqFGjHNNpV03tshoXF5fvz1GP+8MPP0hBCeli6NqP2b8PqzZdLlq0yPPBap/nChUq5Fiwb775prz77rum36x67733TDP1nDlzpH379rJmzRrz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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.5), sharex=True, gridspec_kw={'height_ratios': [3, 1]})\n", - "\n", - "edges = hist_cpu.axes[0].edges\n", - "cpu_vals, cuda_vals = hist_cpu.values(), hist_cuda.values()\n", - "\n", - "ax_spec.stairs(cpu_vals, edges, label=f'CPU MT ({n_clusters_cpu:,})',\n", - " color='C0', linewidth=1.4)\n", - "ax_spec.stairs(cuda_vals, edges, label=f'CUDA ({n_clusters_cuda:,})',\n", - " color='C1', linestyle='--', linewidth=1.4)\n", - "ax_spec.set_ylabel('Counts')\n", - "ax_spec.set_title(f'Cluster energy spectrum — {N:,} frames')\n", - "ax_spec.legend()\n", - "ax_spec.grid(alpha=0.2)\n", - "\n", - "with np.errstate(divide='ignore', invalid='ignore'):\n", - " ratio = np.where(cpu_vals > 0, cuda_vals / cpu_vals, np.nan)\n", - "ax_ratio.stairs(ratio, edges, color='C1', linewidth=1.2)\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.9, 1.1)\n", - "ax_ratio.grid(alpha=0.3)\n", - "\n", - "plt.tight_layout()\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "md-sets", - "metadata": {}, - "source": [ - "## Cluster sets\n", - "\n", - "Every cluster is keyed by `(frame, x, y)`, so the two results can be differenced\n", - "directly — no per-frame Python loop.\n", - "\n", - "A cluster whose centre moved by one pixel was *relabelled*, not gained or lost,\n", - "so those are counted separately from genuine adds and drops." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "sets", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU 17,463,321 clusters\n", - "CUDA 17,463,350 clusters net +29 (0.0002%)\n", - "\n", - "exact-position mismatches CPU-only 25 CUDA-only 54\n", - " of which shifted by 1 px 1 4\n", - " genuine add / drop 24 50\n", - "\n", - "frames touched by a genuine difference: 72 of 10,000\n", - "[2.9s]\n" - ] - } - ], - "source": [ - "def cluster_keys(vectors):\n", - " \"\"\"One int64 key per cluster: frame * rows*cols + y * cols + x.\n", - "\n", - " Centres are unique within a frame, so keys are globally unique and the sets\n", - " below need only a sort, never a np.unique.\n", - " \"\"\"\n", - " n = len(vectors)\n", - " sizes = np.fromiter((cv.size for cv in vectors), np.int64, n)\n", - " frames = np.repeat(\n", - " np.fromiter((cv.frame_number for cv in vectors), np.int64, n), sizes)\n", - " xy = [np.asarray(cv) for cv in vectors if cv.size]\n", - " if not xy:\n", - " return np.empty(0, np.int64), frames\n", - " x = np.concatenate([v['x'] for v in xy]).astype(np.int64)\n", - " y = np.concatenate([v['y'] for v in xy]).astype(np.int64)\n", - " return frames * (rows * cols) + y * cols + x, frames\n", - "\n", - "\n", - "t0 = time.perf_counter()\n", - "k_cpu, fr_cpu = cluster_keys(clusters_cpu)\n", - "k_cuda, fr_cuda = cluster_keys(clusters_cuda)\n", - "\n", - "# Sort ONCE. np.setdiff1d and np.isin each re-sort their argument on every call,\n", - "# and the shift check below calls one eight times per direction -- about twenty\n", - "# sorts of a 27-million-element array. Two sorts plus binary search instead.\n", - "s_cpu = np.sort(k_cpu)\n", - "s_cuda = np.sort(k_cuda)\n", - "\n", - "\n", - "def present_in(vals, sorted_keys):\n", - " \"\"\"Which of `vals` appear in an already-sorted key array.\"\"\"\n", - " if sorted_keys.size == 0:\n", - " return np.zeros(vals.size, dtype=bool)\n", - " i = np.searchsorted(sorted_keys, vals)\n", - " np.clip(i, 0, sorted_keys.size - 1, out=i)\n", - " return sorted_keys[i] == vals\n", - "\n", - "\n", - "cpu_only = s_cpu[~present_in(s_cpu, s_cuda)]\n", - "cuda_only = s_cuda[~present_in(s_cuda, s_cpu)]\n", - "\n", - "\n", - "def drop_shifted(only, sorted_other):\n", - " \"\"\"Drop mismatches that have a counterpart within 1 px in the other set.\"\"\"\n", - " keep = np.ones(only.size, dtype=bool)\n", - " for dy in (-1, 0, 1):\n", - " for dx in (-1, 0, 1):\n", - " if dx or dy:\n", - " keep &= ~present_in(only + dy * cols + dx, sorted_other)\n", - " return only[keep]\n", - "\n", - "\n", - "cpu_real = drop_shifted(cpu_only, s_cuda)\n", - "cuda_real = drop_shifted(cuda_only, s_cpu)\n", - "touched = np.unique(np.concatenate([cpu_real, cuda_real]) // (rows * cols)).size\n", - "\n", - "print(f'CPU {k_cpu.size:>12,} clusters')\n", - "print(f'CUDA {k_cuda.size:>12,} clusters net {k_cuda.size - k_cpu.size:+,}'\n", - " f' ({abs(k_cuda.size - k_cpu.size) / k_cpu.size:.4%})')\n", - "print()\n", - "print(f'exact-position mismatches CPU-only {cpu_only.size:>8,} '\n", - " f'CUDA-only {cuda_only.size:>8,}')\n", - "print(f' of which shifted by 1 px {cpu_only.size - cpu_real.size:>17,} '\n", - " f'{cuda_only.size - cuda_real.size:>17,}')\n", - "print(f' genuine add / drop {cpu_real.size:>17,} {cuda_real.size:>17,}')\n", - "print(f'\\nframes touched by a genuine difference: {touched:,} of {N:,}')\n", - "print(f'[{time.perf_counter() - t0:.1f}s]')" - ] - }, - { - "cell_type": "markdown", - "id": "md-drift", - "metadata": {}, - "source": [ - "## Where in the run it accumulates\n", - "\n", - "Cumulative `CUDA − CPU` against frame index. A curve that rises and then flattens\n", - "means the two pedestals settled into agreement and the difference is a start-up\n", - "transient; a steadily sloping line would mean a persistent per-frame bias." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "drift", - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": - "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9Lug29S8oX7++WfzXdDj/vjjj5t9onlCvy96/tPP4U0/z0knnSTPPPOMyZvBzlvh+Omnn0z+0za23vQz2u9rGsu7/B9//CF79uzxTPefd9q0abJ3717Tpk+XUUcffbTPfJo39Htgvx+I5hc9Bpqfb7zxRunXr1+x74i+p+0VtR3xrl27zDT93mn7RZ2u7bk0r2ge0XOkHifdrv92dD3a9nDRokXm/KS/DXqu0en//Oc/zflJv/eaB4cNG2aWc7lc8o9//MP8pui5TvfN6tWrzbHW/D9//nzzXdP06Llff09efPFF085s3bp15ruh3/sqVaoE3Qd6HtS8ceSRR3qm/etf/zLntOuvv958v7t37y7Vq1eX0s4T+n268847zbn6v//9r8nThx12mJx66qkhf6djSX+X9Nxhf1d036lA502dpr85mldbt27tma6fQfOC5jv/PIkkFesoEyivjRs3mjteejczVGUtybPvrmk7grJUTdQ79qmpqcVKHd555x2zXr2DbtPXepc1lDYaeudc53/++eeDzvP555+bebTNiDetEqPTtU1jWUvytJTBm25Dp+vd4rJU19S7srr8E0884TNd74yXVpLnfcdXq8Z407TrXVxvehde53388cd9pmv7D72j7109S9Nv3x0vz3GtX7++lZeX55OHdXldj01LYfQOvv9dc29a2qClE6tXr/aZ/thjjxW7Q14avfurpRhaSqnL/vjjj573Dj30UPPQkuBgQklvuCV5uv+1Spt/6ZOWonuXEthtMD/88MOIlOT50zvmum9OO+00U0odSnXNYCV5Om3u3Lk+8x555JGmpNp2++23B6zup/OEUpKnbby0NDU/P9/nM2jbtyZNmnju9D/55JNmfVpCbtPzjZbUasmO93Z1Oe/qi+qGG24wJSf2Oco+j5566qlWWZRUkqclaN77yL/UTNtelyTU5bVET1+/8cYbxebVebxLW7Tdne6rQFq3bm1K/kpi/748+uijAb8n9957r1UaPa5aMqfnNe/zpZ3/brzxRp/5tX2XTh83bpzPdC0t1xIim35+nU9L27zpOU6nP/300z7nOP1uhktL1bSJhT87H7399ts+04OV5Gke9D4H6nlKSxUDlcaW9p0OpyTPXof90NJDXdZ7mj78S+n96e+2/++dLqOfQdPnTWvdaImszq/NVLz9/vvvZnpJNX6Q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E2lmcUqGQeIgX4F8hXjAuQrkK8SL6g67Zi+pBM/mjMqlAAAAAICIIMgDAAAAgARCkBcDmZmZct9995lngHwFJ+N8BfIU4gHnKpCvkqzjFQAAAABIJpTkAQAAAEACIcgDAAAAgARCkAcAAAAACYQgLwaefvppadGihVSuXFlOOOEE+e6772KRDDjM2LFjpUOHDpKdnS316tWT8847T5YvX+4zjzahHTlypBkAMysrS7p16ybLli3zmaegoEBuvPFGqVOnjlStWlXOPfdc+euvv3zmyc3NlQEDBphxVvSh/2/fvj0qnxOxz2cpKSkydOhQzzTyFcpi3bp10r9/f6ldu7ZUqVJF2rVrJwsWLCBfoUwKCwvlnnvuMddH+vvWsmVLuf/++8XlcpGnELJvv/1W+vTpY66T9Lfu/fff93k/mr93a9asMWnRdei6brrpJtm3b1/0jqZ2vILomTJlilWpUiXr+eeft37++Wfr5ptvtqpWrWqtXr2aw5DkzjjjDOull16yfvrpJ2vx4sXW2WefbR1yyCHWzp07PfM89NBDVnZ2tvW///3PWrp0qXXxxRdbDRs2tPLy8jzzXHvttVbjxo2tadOmWQsXLrS6d+9uHXvssVZhYaFnnjPPPNNq27atNXv2bPPQ/88555yof2ZE1w8//GA1b97cOuaYY8y5x0a+Qri2bdtmNWvWzBo0aJA1d+5ca+XKldaXX35prVixgnyFMhk9erRVu3Zt6+OPPzb56e2337aqVatmjR8/njyFkH366afWiBEjzHWShjnvvfeez/vR+r3TeXWaLqvr0HU1atTIuuGGG6J2NAnyouzEE080mcfb4Ycfbt15553RTgocbvPmzeYENWPGDPPa5XJZDRo0MCco2969e60aNWpYzzzzjHm9fft2cxNBbybY1q1bZ6Wmplqff/65ea03F3S9c+bM8czz/fffm2m//vprFD8hoik/P99q1aqV+aHp2rWrJ8gjX6Es7rjjDqtLly5B3ydfIVx6Y/PKK6/0mda3b1+rf//+5CmUifgFedE8L2mwqcvosrY33njDyszMtHbs2BGVI0p1zSjSIlqtytKrVy+f6fp69uzZ0UwK4sCOHTvMc61atczzypUrZePGjT75R8cF6tq1qyf/aP7av3+/zzxaJaFt27aeeb7//ntTtaBjx46eeTp16mSmkQ8T1/XXXy9nn3229OzZ02c6+Qpl8eGHH0r79u3loosuMtXLjzvuOHn++efJVyizLl26yFdffSW//fabef3jjz/KzJkz5ayzzuJchYhYGcXrKJ1Hl9FlbWeccYapCupdrb0ipUdlKzC2bNkiRUVFUr9+fZ89oq810wE2vQE1bNgw86OnJwll55FA+Wf16tWeeTIyMiQnJydoHtNnvSjzp9PIh4lpypQpsnDhQpk3b16x98hXKIs///xTJk6caM5Td999t/zwww+mvYleMF1++eXkK4TtjjvuMDc3Dz/8cElLSzPXS2PGjJFLL72UcxUiYmMUr6P02X87uk5dd7SutQjyYkAbgvpf0PtPQ3K74YYbZMmSJeYuZiTyj/88geYnHyamtWvXys033yxTp041nT0FQ75COLQzDC3Je/DBB81rLcnTzgs08NMgj3yFcL355psyadIkmTx5shx11FGyePFi00GUloQMHDiQPIWISYnSdVSsr7WorhlF2rOO3p3yj+A3b95cLNpH8tIenbQq1DfffCNNmjTxTG/QoIF5Lin/6DxaLVh7fSppnk2bNhXb7t9//00+TEBaLUSPv/bkm56ebh4zZsyQJ5980vxv5wvyFcLRsGFDOfLII32mHXHEEaY3OcX5CuG6/fbb5c4775RLLrlEjj76aNNb4S233GJ6BCZPIRIaRPE6Sufx346uU6uCRuuanyAvirSIVi+0pk2b5jNdX3fu3DmaSYED6d0dLcF799135euvvzbdSHvT13rS8M4/eiLSC3Y7/2j+qlSpks88GzZskJ9++skzz0knnWSqxGj1KtvcuXPNNPJh4jnttNNk6dKl5q64/dASmMsuu8z8r92Uk68QrpNPPrnYEC/alqpZs2bmf85XCNfu3bslNdX3slRvjNtDKJCnUF4tongdpfPoMrqsTWvUaJV23UZURKV7FxQbQuGFF14wvfMMHTrUDKGwatUq9lKSu+6660wPT9OnT7c2bNjgeezevdszj/YIpfO8++67puvfSy+9NGDXv02aNDHdmWu3vT169AjY9a92o6+9Qenj6KOPZgiFJOLdu6YiX6Esw3Gkp6dbY8aMsX7//Xfr9ddft6pUqWJNmjSJfIUyGThwoOm23h5CQX/n6tSpYw0fPpw8hbB6kl60aJF5aJgzbtw48789VFm0fu/sIRROO+00sw5dl66TIRQS3H/+8x8zvlBGRoZ1/PHHe7rIR3LTk1Ggh46d593973333We6ANZueE899VRzkvK2Z88ecxKpVauWlZWVZU46a9as8Zln69at1mWXXWbGitGH/p+bmxu1zwpnBXnkK5TFRx99ZC5i9FykQwE999xzPu+TrxAOvcjW85KOD1u5cmWrZcuWZryzgoIC8hRC9s033wS8lho4cGDUz0saWOrQILoOXZeuU4dsiJYU/ROdMkMAAAAAQEWjTR4AAAAAJBCCPAAAAABIIAR5AAAAAJBACPIAAAAAIIEQ5AEAAABAAiHIAwAAAIAEQpAHAAAAAAmEIA8AAAAAEghBHgAAAAAkEII8AEBSGjRokKSkpBR7rFixItZJAwCgXNLLtzgAAPHrzDPPlJdeeslnWt26dX1e79u3TzIyMqKcMgAAyo6SPABA0srMzJQGDRr4PE477TS54YYbZNiwYVKnTh05/fTTzbzjxo2To48+WqpWrSpNmzaVIUOGyM6dOz3revnll6VmzZry8ccfS5s2baRKlSpy4YUXyq5du+SVV16R5s2bS05Ojtx4441SVFTkE0QOHz5cGjdubNbdsWNHmT59ekz2BwAgMVCSBwCAHw3KrrvuOpk1a5ZYlmWmpaamypNPPmmCtZUrV5ogT4Ozp59+2rPc7t27zTxTpkyR/Px86du3r3lo8Pfpp5/Kn3/+KRdccIF06dJFLr74YrPMFVdcIatWrTLLNGrUSN577z1Twrh06VJp1aoVxwYAELYUy/71AgAgydrkTZo0SSpXruyZ1rt3b/n7779lx44dsmjRohKXf/vtt00guGXLFk9JngZs2qbv0EMPNdOuvfZaee2112TTpk1SrVo1M00DOA0Un3nmGfnjjz9MIPfXX3+ZAM/Ws2dPOfHEE+XBBx+soE8PAEhklOQBAJJW9+7dZeLEiZ7XWl3y0ksvlfbt2xeb95tvvjFB188//yx5eXlSWFgoe/fuNdUxdTmlVTTtAE/Vr1/fBHR2gGdP27x5s/l/4cKFpqSwdevWPtsqKCiQ2rVrV8hnBgAkPoI8AEDS0uDssMMOCzjd2+rVq+Wss84yJXMPPPCA1KpVS2bOnClXXXWV7N+/3zNfpUqVfJbT3joDTXO5XOZ/fU5LS5MFCxaYZ2/egSEAAOEgyAMAoBTz5883JXePP/64aZun3nrrrXLvt+OOO850wqIle6eccgrHAQAQEfSuCQBAKbQKpgZ5Tz31lOk8RdvZaZu68tJqmpdddplcfvnl8u6775oOXebNmycPP/yw6agFAICyIMgDAKAU7dq1M0MoaPDVtm1bef3112Xs2LER2W86Tp8GebfeeqsZeuHcc8+VuXPnmmEaAAAoC3rXBAAAAIAEQkkeAAAAACQQgjwAAAAASCAEeQAAAACQQAjyAAAAACCBEOQBAAAAQAIhyAMAAACABEKQBwAAAAAJhCAPAAAAABIIQR4AAAAAJBCCPAAAAABIIAR5AAAAAJBACPIAAAAAQBLH/wM501RwAhoUigAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "first half: +16 second half: +13\n" - ] - } - ], - "source": [ - "cnt_cpu = np.bincount(fr_cpu, minlength=N)\n", - "cnt_cuda = np.bincount(fr_cuda, minlength=N)\n", - "cum = np.cumsum(cnt_cuda.astype(np.int64) - cnt_cpu.astype(np.int64))\n", - "\n", - "fig, ax = plt.subplots(figsize=(9, 3.6))\n", - "ax.plot(cum, color='C1', linewidth=1.2)\n", - "ax.axhline(0, color='gray', linewidth=0.5)\n", - "ax.set_xlabel('Frame')\n", - "ax.set_ylabel('cumulative CUDA - CPU')\n", - "ax.set_title(f'Cluster-count difference accumulating over {N:,} frames '\n", - " f'(final {cum[-1]:+,})')\n", - "ax.grid(alpha=0.25)\n", - "plt.tight_layout()\n", - "plt.show()\n", - "\n", - "half = N // 2\n", - "print(f'first half: {cum[half - 1]:+,} second half: {cum[-1] - cum[half - 1]:+,}')" + "name": "stdout", + "output_type": "stream", + "text": [ + "Image size: (400, 400) cluster (3, 3)\n", + "Pedestal frames: 1000\n", + "Data frames: 10000 of 100000 in file\n" ] } ], - "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" - } + "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", + "pd = File(base / 'Cu_factor_10_pedestal_master_0.json')\n", + "\n", + "n_frames_pd = 1000 # pedestal frames, taken from the head of the file\n", + "N = 10000 # data frames, read after those\n", + "cluster_size = (3, 3)\n", + "rows, cols = f.rows, f.cols\n", + "image_size = (rows, cols)\n", + "\n", + "N_SIGMA = 5\n", + "N_STREAMS = 4 # H2D / kernel / D2H overlap\n", + "BATCH_SIZE = 2000 # frames per find_clusters_batched call\n", + "\n", + "# CPU workers. ClusterFinderMT gives each thread its own ClusterFinder with its\n", + "# own pedestal, and data frames are handed out round-robin -- so at high thread\n", + "# counts each pedestal tracks on only 1/n of the frames while the CUDA finder's\n", + "# single pedestal sees all of them, and the cluster counts drift apart. Keep it\n", + "# low when comparing counts; raise it to measure CPU throughput.\n", + "N_THREADS = 4\n", + "\n", + "# Fixed per-frame output slot the kernel writes into, copied WHOLE on every D2H.\n", + "# Too low does not error -- it TRUNCATES. The CUDA cell prints the peak actually\n", + "# reached, which is how you size this for a new dataset.\n", + "CAP = 3000\n", + "\n", + "capacity = 10_000 # initial ClusterVector capacity, CPU side\n", + "N_BINS = 200\n", + "E_MIN, E_MAX = -2, 4000 # spectrum range, ADU\n", + "\n", + "print(f'Image size: {image_size} cluster {cluster_size}')\n", + "print(f'Pedestal frames: {n_frames_pd}')\n", + "print(f'Data frames: {N} of {f.total_frames} in file')" + ] }, - "nbformat": 4, - "nbformat_minor": 5 - } + { + "cell_type": "markdown", + "id": "md-build", + "metadata": {}, + "source": [ + "## Build the two finders" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "build", + "metadata": {}, + "outputs": [], + "source": [ + "cf_cpu = ClusterFinderMT(image_size, cluster_size, n_sigma=N_SIGMA,\n", + " capacity=capacity, n_threads=N_THREADS)\n", + "sink = ClusterCollector(cf_cpu)\n", + "\n", + "cf_cuda = ClusterFinderCUDA(image_size, cluster_size, n_sigma=N_SIGMA,\n", + " max_clusters_per_frame=CAP, n_streams=N_STREAMS)" + ] + }, + { + "cell_type": "markdown", + "id": "md-ped", + "metadata": {}, + "source": [ + "## Pedestal\n", + "\n", + "Identical frames into both, so the implementation is the only thing that differs." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "train", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Pedestal (1000 frames): 0.86s\n" + ] + } + ], + "source": [ + "f.seek(0)\n", + "t0 = time.perf_counter()\n", + "for _ in range(n_frames_pd):\n", + " pd_img = pd.read_frame()\n", + " cf_cpu.push_pedestal_frame(pd_img.copy())\n", + " cf_cuda.push_pedestal_frame(pd_img.copy())\n", + "print(f'Pedestal ({n_frames_pd} frames): {time.perf_counter() - t0:.2f}s')" + ] + }, + { + "cell_type": "markdown", + "id": "md-io", + "metadata": {}, + "source": [ + "## Read the data frames\n", + "\n", + "Kept out of both timing loops: the two finders run over the same in-memory array." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "io", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Reading 10000 frames: 5.15s (1,942 FPS, 3.20 GB at 0.62 GB/s)\n" + ] + } + ], + "source": [ + "f.seek(0)\n", + "t0 = time.perf_counter()\n", + "data = f.read_n(N)\n", + "t_io = time.perf_counter() - t0\n", + "gb = f.bytes_per_frame * N / 1e9\n", + "print(f'Reading {N} frames: {t_io:.2f}s ({N / t_io:,.0f} FPS, '\n", + " f'{gb:.2f} GB at {gb / t_io:.2f} GB/s)')" + ] + }, + { + "cell_type": "markdown", + "id": "md-cpu", + "metadata": {}, + "source": [ + "## 1 — CPU, multithreaded" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "cpu", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|█████████████████████████████████████████| 10000/10000 [00:02<00:00, 3440.17it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU MT (4 threads): 2.91s 3,434 FPS 23,242,745 clusters (2324.3/frame)\n" + ] + } + ], + "source": [ + "t0 = time.perf_counter()\n", + "# Pass the frame number: find_clusters(frame) defaults it to 0, and every\n", + "# resulting cluster is then stamped frame 0. Part 2 groups by it.\n", + "for i, frame in enumerate(tqdm(data)):\n", + " cf_cpu.find_clusters(frame, i)\n", + "t_cpu = time.perf_counter() - t0\n", + "\n", + "cf_cpu.stop(); sink.stop()\n", + "clusters_cpu = sink.steal_clusters()\n", + "\n", + "hist_cpu = bh.Histogram(bh.axis.Regular(N_BINS, E_MIN, E_MAX))\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 MT ({N_THREADS} threads): {t_cpu:.2f}s {N / t_cpu:,.0f} FPS '\n", + " f'{n_clusters_cpu:,} clusters ({n_clusters_cpu / N:.1f}/frame)')" + ] + }, + { + "cell_type": "markdown", + "id": "md-cuda", + "metadata": {}, + "source": [ + "## 2 — CUDA, batched + multi-streamed\n", + "\n", + "`register_input_buffer` page-locks the input once so H2D runs at full DMA\n", + "bandwidth instead of through the driver's staging buffer. It is paid once,\n", + "outside the loop, and released when the dataset is done." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "cuda", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CUDA batched: 0.21s 48,578 FPS 23,244,600 clusters (2324.5/frame)\n", + " input pinned in 0.08s, once, outside the loop\n", + " vs CPU: 14.15x\n", + " peak clusters in one frame: 2,522 of CAP 3,000\n" + ] + } + ], + "source": [ + "t0 = time.perf_counter()\n", + "cf_cuda.register_input_buffer(data)\n", + "t_pin = time.perf_counter() - t0\n", + "\n", + "clusters_cuda = []\n", + "t0 = time.perf_counter()\n", + "for start in range(0, N, BATCH_SIZE):\n", + " stop = min(start + BATCH_SIZE, N)\n", + " clusters_cuda.extend(\n", + " cf_cuda.find_clusters_batched(data[start:stop], first_frame=start))\n", + "t_cuda = time.perf_counter() - t0\n", + "\n", + "cf_cuda.unregister_input_buffer()\n", + "\n", + "hist_cuda = bh.Histogram(bh.axis.Regular(N_BINS, E_MIN, E_MAX))\n", + "energies = [np.asarray(cv.sum()).ravel() for cv in clusters_cuda if cv.size > 0]\n", + "if energies:\n", + " hist_cuda.fill(np.concatenate(energies))\n", + "n_clusters_cuda = sum(cv.size for cv in clusters_cuda)\n", + "\n", + "print(f'CUDA batched: {t_cuda:.2f}s {N / t_cuda:,.0f} FPS '\n", + " f'{n_clusters_cuda:,} clusters ({n_clusters_cuda / N:.1f}/frame)')\n", + "print(f' input pinned in {t_pin:.2f}s, once, outside the loop')\n", + "print(f' vs CPU: {t_cpu / t_cuda:.2f}x')\n", + "\n", + "# One ClusterVector per frame, so this IS the per-frame maximum.\n", + "peak = max((cv.size for cv in clusters_cuda), default=0)\n", + "print(f' peak clusters in one frame: {peak:,} of CAP {CAP:,}')\n", + "if peak >= CAP:\n", + " print(' !! CAP REACHED -- clusters were dropped; raise CAP and re-run.')" + ] + }, + { + "cell_type": "markdown", + "id": "md-summary", + "metadata": {}, + "source": [ + "## Summary" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "summary", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " s FPS us/frame clusters\n", + "CPU MT (4 thr) 2.91 3,434 291.2 23,242,745\n", + "CUDA batched 0.55 18,074 55.3 23,244,269\n", + "\n", + "speedup: 5.26x\n" + ] + } + ], + "source": [ + "print(f'{\"\":<22} {\"s\":>7} {\"FPS\":>10} {\"us/frame\":>10} {\"clusters\":>13}')\n", + "for name, t, n in (('CPU MT (%d thr)' % N_THREADS, t_cpu, n_clusters_cpu),\n", + " ('CUDA batched', t_cuda, n_clusters_cuda)):\n", + " print(f'{name:<22} {t:7.2f} {N / t:10,.0f} {t * 1e6 / N:10.1f} {n:>13,}')\n", + "print(f'\\nspeedup: {t_cpu / t_cuda:.2f}x')" + ] + }, + { + "cell_type": "markdown", + "id": "md-part2", + "metadata": {}, + "source": [ + "# Part 2 — Where the two disagree\n", + "\n", + "The two implementations are not bit-identical by construction: the CUDA finder\n", + "updates its pedestal once per frame, the CPU finder per pixel, so once the\n", + "pedestals drift apart a few pixels land on opposite sides of the threshold.\n", + "\n", + "Three views, cheapest first: the **spectra**, the **cluster sets**, and **where\n", + "in the run** the difference accumulates." + ] + }, + { + "cell_type": "markdown", + "id": "md-spec", + "metadata": {}, + "source": [ + "## Spectra\n", + "\n", + "Both curves should lie on top of each other; the ratio panel is the sensitive\n", + "view." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "spectra", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": + "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eoUKFVzPeffdd/3u/91335nXpvv5M23aNJNfnTt3Dsq5jx8/bvKiatWq5v0866yzTP47nThxQoYOHSq1atWSIkWKSIMGDeSLL77wOMagQYPkmWeekQMHDki0IcgDAAAAcvDXX39J48aNTU3Y888/Lz/99JO54G/VqpXcd999+cq7K6+8UrZt22aOPWfOHHOsPn36SIcOHSQtzXOu1s2bN8vSpUvNuSZOnJiv8y1ZskR+//13ueGGG1zbFi1aZIK82bNny4oVK0waNHBdtWqVa59SpUqZYEjPv3btWrn99tvN4h0Mep/r3HPPlRkzZpjn3HHHHXLbbbfJp59+mm3flJQU89jll1/u93h///23PPLII3LxxRcH9DrPDeDcXbp0MQH6W2+9Jb/99ptMnTrV1HI6PfHEE/L666/LqFGjZN26ddK7d2+55pprPPJGz1OtWrUcA9hIcViWZUU6EdFGo/ESJUqYQqe/akQDfZs0PZou/RUD5LedRKp81xw4x9xueKZdtsc+G5S5rcPQzH3shO8T8tvOKN/Rm9/Hjh2TTZs2SfXq1aVw4cISS9q3b28CBg0GtHbI3f79+00tl9I8mDlzZrbaJq3JW716tallctbk6fO8a/00iNRg54033pCePXu6ticnJ8uvv/5qao6aNWsm//77r5x66ql5eg0PPvigCSo/+OCDHPerW7eudO3a1ZzLn0aNGslVV10lTz31VMDn1/21xtK9tkzdeOONUqNGDYmPjzf5ofnkLj09XS699FITWC5evNhnvuX13F988YU5759//mmCWF8qVqwojz/+uAnuNG3OWkTN9ylTpni8NxosasAcLDl9VgKNU6jJA1Bg/Zxwq1l8aeP4wSwAgIJt7969JijQWjTvAE85A7xguOyyy0yzQG0i6R5Ia+3drbfeamqaNCB6//3383xsDUK02WVOtKnowYMH/QY+mhYNaDTYveSSS/J0fg1KvI+rr+uPP/6QwYP994HXJpPanPTOO+/M0/lyOvcnn3xi8kJrZc844wypWbOmqSk8evSoR3NO7wBLm21+++23HtvOP/98+eGHH8z+0SQh0gkAgGiV5EiT1MGlXfdPSIIUTd4R0TQBgC09Vdb/Yz3ni1RokLk+7RaR3+f63u+S/4lc+mjm+sp3RD7v53u/8vVFen0dcNI2btxoghv3pnyhpOfRWkOn+fPn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fv3koosuMv/kKePhz29FGQ+un376yQQZx44dk1NPPVVmzpwp55xzjusCle/v8OS3omwH17Rp02TlypXy448/FqjrE4K8GKGdRL3/6XlvQ870S9Opfv365sv1rLPOkrfffts1IEV+8pn3InDByF9f+/MeZNe1a1fXul4YN2nSxAR8n3/+uVx77bXk90m4//77Ze3atfLtt99me4wyHr78powHV61atWT16tVmoIgZM2ZI9+7dZeHCha7HKdvhyW8N9CjbwbNlyxbp06ePzJ071wxe6I8dyzfNNaOcjuITHx+f7VcAbXbl/asD8kZHR9JgT5twOkfZzCmfdR9tPqujJ/Fe5E2w8lf30VpYb7t27eLzkIsKFSqYIE/LO/mdfzq62ieffCLffPONVKpUybWdMh7e/PaFMn5ytKbi7LPPNj8I6QiEDRo0kJdffpmyHeb89oWynX8rVqww1xE6Mn1CQoJZNJgePXq0WXdeX9jx+oQgLwa+BLRgzps3z2O73temhsg/HQ53/fr15stT++jpB9Q9n/UDrV8EznzW9yExMdFjn23btsnPP//Me5GLYOWv1r5q35wffvjBtc+yZcvMNj4POduzZ4/5RVPLO/mdd/prrNYoffTRR/L111+bMk0Zj1x++0IZD/57oP8n+f4Ob377QtnOv8svv9w0jdVaU+eigfUtt9xi1s8880z7Xp9EZLgX5GsKhbfeesuM3tO3b18zhcJff/1FTubBww8/bC1YsMD6888/zehHOqytDpnrzEcdWUlHU/roo4/MELo33XSTzyF0K1WqZM2fP98MoXvZZZcxhYLbyFWrVq0yi361jBgxwqw7p/oIVv7qEMU6/LGOWqVL/fr1C+QUCjnltz6m5V2HcN60aZP1zTffWM2bNzfDP5Pf+XPPPfeY8qvfIe7TUhw5csS1D2U8fPlNGQ+uAQMGWIsWLTLfF2vXrjVD+uvIgXPnzjWPU7bDl9+U7dC71G10TTuXb4K8GDF27FiratWqVlJSktWoUSOPYaQRGOe8JxowV6xY0br22mutdevWuR7XYXR1mgUdSrdQoULWJZdcYj7s7o4ePWrdf//9VqlSpawiRYqYD+/mzZt5CyzLBBIabHgvOhR0MPNX53q75ZZbTICui67v27evwL0HOeW3Xgi3adPGKlu2rCnvOr+YbvfOS/I7cL7yWhedy82JMh6+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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.5), sharex=True, gridspec_kw={'height_ratios': [3, 1]})\n", + "\n", + "edges = hist_cpu.axes[0].edges\n", + "cpu_vals, cuda_vals = hist_cpu.values(), hist_cuda.values()\n", + "\n", + "ax_spec.stairs(cpu_vals, edges, label=f'CPU MT ({n_clusters_cpu:,})',\n", + " color='C0', linewidth=1.4)\n", + "ax_spec.stairs(cuda_vals, edges, label=f'CUDA ({n_clusters_cuda:,})',\n", + " color='C1', linestyle='--', linewidth=1.4)\n", + "ax_spec.set_ylabel('Counts')\n", + "ax_spec.set_title(f'Cluster energy spectrum — {N:,} frames')\n", + "ax_spec.legend()\n", + "ax_spec.grid(alpha=0.2)\n", + "\n", + "with np.errstate(divide='ignore', invalid='ignore'):\n", + " ratio = np.where(cpu_vals > 0, cuda_vals / cpu_vals, np.nan)\n", + "ax_ratio.stairs(ratio, edges, color='C1', linewidth=1.2)\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.9, 1.1)\n", + "ax_ratio.grid(alpha=0.3)\n", + "\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "md-sets", + "metadata": {}, + "source": [ + "## Cluster sets\n", + "\n", + "Every cluster is keyed by `(frame, x, y)`, so the two results can be differenced\n", + "directly — no per-frame Python loop.\n", + "\n", + "A cluster whose centre moved by one pixel was *relabelled*, not gained or lost,\n", + "so those are counted separately from genuine adds and drops." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "sets", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU 23,242,745 clusters\n", + "CUDA 23,244,269 clusters net +1,524 (0.0066%)\n", + "\n", + "exact-position mismatches CPU-only 1,976 CUDA-only 3,500\n", + " of which shifted by 1 px 120 107\n", + " genuine add / drop 1,856 3,393\n", + "\n", + "frames touched by a genuine difference: 2,385 of 10,000\n", + "[4.3s]\n" + ] + } + ], + "source": [ + "def cluster_keys(vectors):\n", + " \"\"\"One int64 key per cluster: frame * rows*cols + y * cols + x.\n", + "\n", + " Centres are unique within a frame, so keys are globally unique and the sets\n", + " below need only a sort, never a np.unique.\n", + " \"\"\"\n", + " n = len(vectors)\n", + " sizes = np.fromiter((cv.size for cv in vectors), np.int64, n)\n", + " frames = np.repeat(\n", + " np.fromiter((cv.frame_number for cv in vectors), np.int64, n), sizes)\n", + " xy = [np.asarray(cv) for cv in vectors if cv.size]\n", + " if not xy:\n", + " return np.empty(0, np.int64), frames\n", + " x = np.concatenate([v['x'] for v in xy]).astype(np.int64)\n", + " y = np.concatenate([v['y'] for v in xy]).astype(np.int64)\n", + " return frames * (rows * cols) + y * cols + x, frames\n", + "\n", + "\n", + "t0 = time.perf_counter()\n", + "k_cpu, fr_cpu = cluster_keys(clusters_cpu)\n", + "k_cuda, fr_cuda = cluster_keys(clusters_cuda)\n", + "\n", + "# Sort ONCE. np.setdiff1d and np.isin each re-sort their argument on every call,\n", + "# and the shift check below calls one eight times per direction -- about twenty\n", + "# sorts of a 27-million-element array. Two sorts plus binary search instead.\n", + "s_cpu = np.sort(k_cpu)\n", + "s_cuda = np.sort(k_cuda)\n", + "\n", + "\n", + "def present_in(vals, sorted_keys):\n", + " \"\"\"Which of `vals` appear in an already-sorted key array.\"\"\"\n", + " if sorted_keys.size == 0:\n", + " return np.zeros(vals.size, dtype=bool)\n", + " i = np.searchsorted(sorted_keys, vals)\n", + " np.clip(i, 0, sorted_keys.size - 1, out=i)\n", + " return sorted_keys[i] == vals\n", + "\n", + "\n", + "cpu_only = s_cpu[~present_in(s_cpu, s_cuda)]\n", + "cuda_only = s_cuda[~present_in(s_cuda, s_cpu)]\n", + "\n", + "\n", + "def drop_shifted(only, sorted_other):\n", + " \"\"\"Drop mismatches that have a counterpart within 1 px in the other set.\"\"\"\n", + " keep = np.ones(only.size, dtype=bool)\n", + " for dy in (-1, 0, 1):\n", + " for dx in (-1, 0, 1):\n", + " if dx or dy:\n", + " keep &= ~present_in(only + dy * cols + dx, sorted_other)\n", + " return only[keep]\n", + "\n", + "\n", + "cpu_real = drop_shifted(cpu_only, s_cuda)\n", + "cuda_real = drop_shifted(cuda_only, s_cpu)\n", + "touched = np.unique(np.concatenate([cpu_real, cuda_real]) // (rows * cols)).size\n", + "\n", + "print(f'CPU {k_cpu.size:>12,} clusters')\n", + "print(f'CUDA {k_cuda.size:>12,} clusters net {k_cuda.size - k_cpu.size:+,}'\n", + " f' ({abs(k_cuda.size - k_cpu.size) / k_cpu.size:.4%})')\n", + "print()\n", + "print(f'exact-position mismatches CPU-only {cpu_only.size:>8,} '\n", + " f'CUDA-only {cuda_only.size:>8,}')\n", + "print(f' of which shifted by 1 px {cpu_only.size - cpu_real.size:>17,} '\n", + " f'{cuda_only.size - cuda_real.size:>17,}')\n", + "print(f' genuine add / drop {cpu_real.size:>17,} {cuda_real.size:>17,}')\n", + "print(f'\\nframes touched by a genuine difference: {touched:,} of {N:,}')\n", + "print(f'[{time.perf_counter() - t0:.1f}s]')" + ] + }, + { + "cell_type": "markdown", + "id": "md-drift", + "metadata": {}, + "source": [ + "## Where in the run it accumulates\n", + "\n", + "Cumulative `CUDA − CPU` against frame index. A curve that rises and then flattens\n", + "means the two pedestals settled into agreement and the difference is a start-up\n", + "transient; a steadily sloping line would mean a persistent per-frame bias." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "drift", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": + "iVBORw0KGgoAAAANSUhEUgAAA3kAAAFeCAYAAAA4xtZyAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAY95JREFUeJzt3Qd8FGX+x/FfQkIIEErovSi9SlGwHFUUFU+xVzjLqdhRsd3ZTuTUEz317F3s97frqaAoIr0piCAovfdQA0n2//o+44TNZhOSkLK7fN6v10B2dnZ2dubZ2fnN8zy/Jy4QCAQMAAAAABAT4st6AwAAAAAAxYcgDwAAAABiCEEeAAAAAMQQgjwAAAAAiCEEeQAAAAAQQwjyAAAAACCGEOQBAAAAQAwhyAMAAACAGEKQBwAAAAAxhCAPUe+nn36yv/zlL9asWTOrUKGCVa5c2bp06WIPPfSQbd68OXu53r17u6mkPPXUU/bKK6+U2PqjUXHtk7i4OLvnnnuyH2udmrd06dIcy/3tb3+zxo0bW0JCglWrVs3N27t3r1155ZVWr149K1eunHXu3PmgtwfRL7RMFdSuXbvc67799ttcz+VVLpHTa6+9Zueee661atXK4uPjrWnTpnnuoh07dtgNN9xg9evXd+d3fX/ffvvtAu/Swrx+1qxZ1r9/f/cbovPH4MGD7ffffw+77BNPPGGtW7e2pKQk99tz77332r59+wq0TV9//bV169bNKlWq5MrLhx9+aIcS7Sftu3/+85855r/zzjvWrl07S05Odvtlzpw57rumvyP1fFBQjz32mCtPKit6r8Jei+g14abQffj+++/beeedZ4cffrjbj/puXXDBBbZo0aJ81797925r2bKlW+e//vWvXOVV34lVq1YVaptR9hLKegOAg/H888/bsGHD3MXCLbfcYm3btnU/IDNmzLBnnnnGJk+ebB988EGpBTQ1a9a0oUOHlsr7RYOS2icnn3yyO7YK3HwfffSRjRw50u68804bOHCgu/iSp59+2p599ll3Uda1a1f3YwUUlYI8XdBL6IVauHKJ3F5//XVbu3atHXnkkZaVlZVvcKQL4+nTp7uLWV2Evvnmm+4iVq87//zzD7h7C/r6BQsWuOOpIPDdd9+1PXv22F133WXHHXecCzZq1aqVvazOM3//+9/ttttuswEDBrj16waTLoKfe+65fLcnEAjY2Wef7bbl448/doGefr8Otd+FLVu22LXXXps9b8OGDXbRRRfZiSee6J7X+Vv76LLLLnPzop2uR3Ss+/bta5988kmR1nHmmWfaTTfdlGOebmoGe/DBB61u3brud7B58+a2YsUKe+CBB9yN7ylTprggOhyV5507d4Z9rl+/fu67escdd9irr75apG1HGQkAUWrSpEmBcuXKBU488cTAnj17cj2fnp4e+Oijj7If9+rVy00lpV27dsW+/qysrMCuXbsC0aq49olOVXfffXe+y9x///1uuXXr1uWYf9lllwWSk5MDxWnnzp3Fuj6UvoKUqXA2bNhQ5NceKvbu3RvYt29fns9nZmZm/33yyScHmjRpEna5zz77zO3rN998M8f8448/PlC/fv1ARkZGvttRmNefddZZgZo1awa2bduWPW/p0qWBxMTEwIgRI7Lnbdy4MVChQoXAX//61xzrHDlyZCAuLi7w888/57tNK1eudNv04IMPBg7F84zKRYMGDQK33XZbjvkTJ050++Wdd94pk+0q6nf65Zdfdq89kOAyX5TfRb3H1VdffcDlQn//ZNWqVa4cX3rppWFfM3Xq1ED58uUD7733nnufhx9+ONcy//3vf9311vLlywu13ShbNNdE1NLdKTUt0J1Tv9YmWPny5e3UU0/N8/VqbqXXhza7UlMrzQ9uZqgmO2pepCY/eq86deq4u1u6wytqEvHzzz/bd999l92MIrgJUlpamt18882uqYa2q0GDBq4JUeidM73ummuucXf92rRp494rvztn6enpdt9997ll1RSpRo0a1qdPH5s0aVL2Mrojffvtt+d476uvvtq2bt1aoOYq+hzBNXF+k7Tx48fbVVdd5Wrq9L66Y7569eocr8tvn4Sj/XT55Ze79anGTXdwf/311wM2i9N6dSdddGz8z6L/X3jhBdcUxd8G/7jqd1N3jHXnXs1aqlev7u6UhjbP0t399u3b24QJE+zoo4+2ihUr2iWXXFKk46oaDB0rraNTp0726aef5vpsqlFQTYM+h46/7tRefPHF7lj7VAtyxRVXWMOGDd37+s3FMjIy7EDUJEq1D6pt0ufW9qhGItxd3KlTp9qgQYPc8VD5Ouyww9znK8z25tXcKlzTRh3HU045xe2XI444Inv7/P2k1+ix7ojrzrJq7EOPVbhmUCq/Byp7qklQqwC1BlDZq127trvr/v3332cvo231a3S0v/0y5X8/wn0mv/yotke1Qjr2usOumiXVJgXT90XHRsvoffQ9/eyzz8Kep8KZOHGiOy+lpKS4dai86vW+H3/80a3rxRdfzPXa//3vf+451S751MRLtV3aFzq22vf/+c9/wp5HVbZVy6DvgJZdvHhxntupJpoFoVYYOhZnnXVWjvlqnq9zjcpncbxe3xuVsTPOOMOqVKmSvVyTJk3c+TS4NcgXX3zhzqlaR+g6dU7Jr+mlvgv6zsqtt96a45zof0/UZFTnIZ2P9H0TlXP9/mhZvwmevnPLli3LsX6//H3zzTfZ51F9Hn0f9f3WeUO1iGqKqu+/zl2htahq3n7//fdnN0VVOdRn0/cjmN5DZVvvoW3S9177TzXd+VH5Uo2nau18+v4ce+yx7u9zzjknR3PGcOcP/zyhY6EaKr2/tvell14q9He6tBS0zB8sfcZQum5RuVOtXigdb/2e6VyjJsR50e+A9qFaTyF60FwTUSkzM9P9yKj5XaNGjUr8/U466ST3nurnpx+zjRs3ukDKD5R0EaAf5qpVq7rAQfzAUz96vXr1spUrV7rmDh07dnQXc2oKNHfuXBs3blyOHzFdJOhHSM+r2UW4k7Z/YaJmiVpWF9768dI8NclYvny5u8DTRcdpp53m2tQr0NNFpvow3n333a5ZmaZwAXJBqBmNmqep+ZN+PNRc9sILL3TH5UD7JBx/W7Vf9dm7d+9uP/zwg/uMB6L30sWnLl71w6/31I+agsR//OMfLiD1t8u/cFKQpIui6667zjVxUf9NBczab7oYVtDiW7NmjftsI0aMcDcX9INd2OOqi21d6Os99GOpsnT66afbwoUL3UW/6H11saPAWcu1aNHCvbcujPRjrP3nN3PTNui99Hl0HHVhpuDi5Zdfzndf6cJd5VllRsGSgjR9/mnTpmXvI/nyyy/dD7su7EePHu3Kvdb/1VdfZS9TkO0tLK1TZVXNjXQcFUzpBoLmqRz7N3d0kawLvSVLlriLvIPl99/Vd0PfO/XlUrnSxabeV//rwljlS+Xq0ksvdd8BCW7KF46OmfrFKAjS+rVefR5dfOkCXLTfVJ50TNTEWN/7t956y90cKAjdTDn++ONdOdT3QPte3zsdQ61HF8+6saDgWWVE2x9M3wW9p8qGzJ8/330XdNwfeeQRt09UJvR90flPnyOYPk/Pnj3dDSqVzbzOW4Uxb948V/7UxzaYPqP/vLbxYF//22+/uRtB/vzQZceOHesCO93o0GukQ4cOOZZT2dD3wH8+HJUXHQOVZzVVVAAd+h3Rcwro1I/Yv/Gi752adGp+amqqKysqIzpH6jjpfUPfR+tR38PZs2e785N+G3Su0fy//vWv7vyk773K4PDhw93rdNPhz3/+s/tN0blO+0aBpI61yr+CTX3XtD069+v3RIGVgkYFbvpu6HuvGwx50XlQZUOBV3BTQZ3TFGjo+63AOjjYzus8oe+TblDpXK2beSrT6ov2pz/9qcDf6Wii31p9t3WcdONI54bQmw3h6MaljqN+X0PpvK1ypt/J0EA+mG4m+jeN9BpEiTKuSQSKZO3ata5Zwbnnnlvg14Q21xw/frxbh/4PtmTJEjdfzTD85jl6/Nhjj+W7/ryaYIwaNSoQHx8fmD59eq7mD1rv559/nj1Pj6tWrRrYvHnzAT/Pa6+95pZ//vnn81zmiy++cMs89NBDOearSYzmP/fccwdsrqKmVEOGDMnVPGXYsGE5ltN7aP6aNWuK1Czlf//7n3v9v//971zNoEK3zd8GHSufntc8NacLpm2vVKlSjnmTJ092yz7yyCM55q9YscI17QxunqXt17Jff/31QR3XOnXqBNLS0nKUYb1e6/H17ds3UK1atcD69evz3E9XXHFFoHLlyoFly5blmP+vf/3Lvc+BmouFNgdW86nvvvvOvfbHH3/Mfu6www5z0+7du/N8fUG21z8uocIdQ5U17X81afPNmTPHLVevXr0czdc+/PBDN//jjz8+YJNslYHQJoEHap6lpnzaN/369QucfvrpBWquGe4z+eVHTaKCtW3bNnDCCSdkP77lllvCNvfTMuHOU6F69OgRqF27dmD79u05PkP79u0DDRs2dMdaHn/8cbe+hQsXZi+n801SUlLgpptuyvG+el1w80W55pprXHNF/xzln0f/9Kc/BYoiv+aaLVq0yLGPfKtXr3bv+cADD+S77oK+/ocffnCP33rrrVzLahk9p9fI5Zdf7vZVOC1btgwMGDAg323yf19Cm8T535O77rorcCA6rjt27HDnteDzpV/+rr322hzLn3baaW7+6NGjc8zv3LlzoEuXLtmP9fm13P/93//lWE7nOM1/6qmncpzj9N0srDZt2rguFqH8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+QjRgN81UI4QKbIi7Do7LS3NVTb58UheCPKKwG+iqQAvUoI8jamkbYmEwofoRDkC5QiRgvMRKEeIFFkRep19oC5jkbOlAAAAAICDRpAHAAAAADGEIA8AAAAAYghBHgAAAADEEII8AAAAAIghBHkAAAAAEEMI8gAAAAAghhDkAQAAAEAMiZggb8KECTZo0CCrX7++G9zvww8/zPH80KFD3fzgqUePHjmWSU9Pt2uvvdZq1qxplSpVslNPPdVWrlyZYxmNWH/RRRdZ1apV3aS/t27dWiqfEQAAAECUSN9h9ts3ZoGARZuICfJ27txpnTp1sieffDLPZU488URbs2ZN9vT555/neP6GG26wDz74wN5++22bOHGi7dixw0455RTLzMzMXub888+3OXPm2BdffOEm/a1ADwAAAACyzXjJ4t84wypOecSiTYJFiIEDB7opP0lJSVa3bt2wz23bts1efPFFe/31161///5u3pgxY6xRo0Y2btw4O+GEE+yXX35xgd2UKVPsqKOOcss8//zz1rNnT1u4cKG1atWqBD4ZAAAAgKiTvt39t69eV6tg0SVigryC+Pbbb6127dpWrVo169Wrl40cOdI9lpkzZ9q+fftswIAB2cur6Wf79u1t0qRJLsibPHmya6LpB3iiJp+ap2XyCvLUDFSTLy0tzf2flZXlprKmbQgEAhGxLYhelCNQjhApOB+BcoSIkFjRNXvMqpAaMdfZBd2OqAnyVMt31llnWZMmTWzJkiX297//3fr27euCO9XwrV271sqXL2/Vq1fP8bo6deq450T/+0FhMM3zlwln1KhRdu+99+aar/59GRkZFgkHe/v27S7Qi4+PmBa4iDKUI1COECk4H4FyhEiQvHuXVfqjW9m+zZsj4jpb1/wxFeSdc8452X+rdq5bt24u4Pvss89s8ODBeb5OgY+StPiC/85rmVC33367DR8+PEdNnpqBKqCsUqWKRcKPobZf2xMJhQ/RiXIEyhEiBecjUI4QEZIruv+U0DElNTUirrMTEhJiK8gLVa9ePRfkLVq0yD1WX729e/e62rXg2rz169fb0Ucfnb3MunXrcq1rw4YNrsYvL6op1BRKBzoSDrYoyIuk7UF0ohyBcoRIwfkIlCOUubi4iDsfFXQbyn5Li2jTpk22YsUKF+xJ165dLTEx0caOHZu9jDJwzps3LzvIU4IV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jHHZNJ988kn75ptvrHXr1taxY8eirA4AAAAAEAmDoVerVs2GDRtmM2bMcMMo9O7du7i2CwAAAABQGoOhb9u2zcaOHWtLly51/fGaN29u/fr1s86dO7uhFAAAAAAg6gUOgSEURE00r7nmGktLS8sxv2rVqvbMM8/YOeecU9zbBwAAAABlKC52m2uqOeZf/vIXO+2001xWzd27d7tx89RUc9CgQXbRRRfZnDlzSnZrAQAAAADFU5P3xBNPuABPmTSDdenSxV577TUX8Km55ksvvVTQVQIAAAAAyqom74cffrArrrgiz+evvPJKmzhxYnFtFwAAAACgJIM8DZfQsmXLPJ/Xc6tWrSrKNgAAAAAASjvIU3PMChUq5Pl8UlKS7dmzp7i2CwAAAADKUODQyK755Zdfukya4WzdurW4tgkAAAAAIkIgCrNrFirIGzJkSL7Pa9w8AAAAAEAUBHlZWVkluyUAAAAAgNLrkwcAAAAAiKEgT2PhhZs++ugjW7BgQclupZndc889rjlo8FS3bt3s5wOBgFumfv36lpycbL1797aff/45xzrS09Pt2muvtZo1a1qlSpXs1FNPtZUrV5b4tgMAAABAxDXXvP7668PO37Fjh2vKedJJJ9mbb75pKSkpVlLatWtn48aNy35crly57L8feughGz16tBusXcM53H///Xb88cfbwoULs7fphhtusE8++cTefvttq1Gjht100012yimn2MyZM3OsCwAAAABiviZvy5YtYSfVjk2ZMsWWL19u9957b4lubEJCgqu986datWpl1+I99thjduedd9rgwYOtffv29uqrr7phHxR4yrZt2+zFF1+0Rx55xPr3729HHHGEjRkzxubOnZsjcAQAAAAACwQO3T558fHx1r17dxc8qZasJC1atMg1x2zWrJmde+659vvvv7v5S5YssbVr19qAAQNyjNvXq1cvmzRpknus2rp9+/blWEbrUkDoLwMAAAAAOUThAAKFGkIhP4cffniJ9m876qijXB9ANcVct26da4559NFHu353CvCkTp06OV6jx8uWLXN/a5ny5ctb9erVcy3jvz4vqq3U5EtLS3P/q5lqJGQd1TaoNjMStgXRi3IEyhEiBecjUI4QCeICf4yQF4ickQYKuh3FFuT99ttv1rBhQyspAwcOzP67Q4cO1rNnTzvssMNcs8wePXqEHadPgc+Bxu4ryDKjRo0K2xRVzVUzMjIsEg729u3b3WdRzSpAOQLnI0QzftdAOUIkqLhnj1X8IwdJxubNEXGdrWv+UgnyFFjMnj3bJTEZNGiQlRZlx1Swpyacp512mpunGrl69eplL7N+/frs2j314du7d68LzIJr87SMagTzc/vtt9vw4cNz1OQ1atTIradKlSoWCT+GClS1PZFQ+BCdKEegHCFScD4C5QiRIK5CBfd/5cqVrUpqakRcZytHSYGWK+gKFUCEq/FSZJuZmWknnniiG8KgtKj55C+//GLHHXec66OnIG7s2LEuoYoooPvuu+/swQcfdI+7du1qiYmJbpmzzz7bzVuzZo3NmzfPZebMj/r3aQqlAx0JB1t0bCJpexCdKEegHCFScD4C5QhlLu6P2Ccucq77C7oNBQ7ylL0yHNVktW7d2tq0aWMl6eabb3Y1hY0bN3a1b+qTpxq1IUOGuB8CDY/wwAMPWIsWLdykvytWrGjnn3++e33VqlXt0ksvdTWOGj4hNTXVrVO1gcq2CQAAAAD7RW92zQIHeQqmypKSupx33nm2ceNGN3SC+uFp6IYmTZq450eMGGG7d++2YcOGuSaZStTy1Vdf5Ri379FHH3VVnKrJ07L9+vVz4+oxRh4AAACAWEmvGRdQp7oCWL16tRts/K677srVD01j0KlmTTVjoRkuY5FqEFUzqM8dKX3yNm/e7GonI6EaGdGJcgTKESIF5yNQjhARxt1jNvFR23zh11ateZeIuM4uaBxS4C1VgKeVhluZ3kiZXrQMAAAAAKDsFDjI++KLL+ziiy/O83k99+mnnxbXdgEAAAAASjLIW7JkiUt6kheNkbd06dKibAMAAAAAoLSDvOTk5HyDOD2nZQAAAAAAURDkKVvl66+/nufzr732mh155JHFtV0AAAAAUHYCh8AQCsqcefzxx7skK7fcckt2Fs1169a5wcQ1FIGGLAAAAACAmBsUPRaDvD59+th//vMfu/766914c8qyqUHIlb4zMTHRnnjiCevbt2/Jbi0AAAAAoHiCPLniiivslFNOsXfffdcWL15sGmKvZcuWduaZZ7rEKwAAAACAKArypEGDBnbjjTeWzNYAAAAAAA5K2Q/bDgAAAAAoNgR5AAAAAJBL9GbXJMgDAAAAgDxFX3ZNgjwAAAAAiCFFCvK2bt1qL7zwgt1+++22efNmN2/WrFm2atWq4t4+AAAAAEBJZtf86aefrH///m5Q9KVLl9rll19uqamp9sEHH9iyZcvstddeK+wqAQAAAABlVZM3fPhwGzp0qC1atMgqVKiQPX/gwIE2YcKE4touAAAAAEBpBHnTp093g6KHGz9v7dq1RdkGAAAAAEBZBXmqvUtLS8s1f+HChVarVq3i2i4AAAAAKDuBQ2gIhT//+c9233332b59+9zjuLg4W758ud122212xhlnlMQ2AgAAAEDZiDsEhlD417/+ZRs2bLDatWvb7t27rVevXnb44YdbSkqKjRw5smS2EgAAAABQMtk1q1SpYhMnTrRvvvnGDZuQlZVlXbp0cRk3AQAAAABRFuRp2ISmTZta37593QQAAAAAiOLmms2bN7djjz3Wnn322eyB0AEAAAAAURrkzZgxw3r27Gn333+/1a9f3yViee+99yw9Pb1kthAAAAAASl3g0Any1P/u4Ycfdhk1//e//7kELBo3T/9fcsklJbOVAAAAAFAm4mI/yPNp6IQ+ffrY888/b+PGjXPNOF999dXi3ToAAAAAQOkEeStWrLCHHnrIOnfubN27d7dKlSrZk08+WdTVAQAAAADKIrvmc889Z2+88Yb98MMP1qpVK7vgggvsww8/dBk3AQAAAABRFuT94x//sHPPPdf+/e9/u1o8AAAAAEAUB3lKuKL+eAAAAACAKA3yfvrpJ2vfvr3Fx8fb3Llz8122Y8eOxbVtAAAAAFA2AoHYDvLULHPt2rVumAT9rZq8QNCH9h/r/8zMzJLcXgAAAAAoRXGxGeQtWbLEatWqlf03AAAAACCKg7wmTZpk/71s2TI7+uijLSEh50szMjJs0qRJOZYFAAAAAET4OHkaAH3z5s255m/bts09BwAAAACIoiDP73sXatOmTW5AdAAAAABAFAyhMHjwYPe/AryhQ4daUlJS9nNKtqIMnGrGCQAAAACIgiCvatWq2TV5KSkplpycnP1c+fLlrUePHnb55ZeXzFYCAAAAQFmIi9HsmvLyyy+7/5s2bWo333wzTTMBAAAAIJqDPN/dd99dMlsCAAAAACj9IE/++9//2rvvvmvLly+3vXv35nhu1qxZB79VAAAAAIDSya75+OOP21/+8herXbu2zZ4924488kirUaOG/f777zZw4MCibQUAAAAAoGyCvKeeesqee+45e/LJJ13C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rhAGa926deC2224rs21C5Fi/fr07uX333XfucVZWVqBu3bru5Obbs2dPoGrVqoFnnnnGPd66dau7caAbCL5Vq1YF4uPjA1988YV7rBsKWu+UKVOyl5k8ebKbt2DBglL8hChJ27dvD7Ro0cL9KPXq1Ss7yKMcoSBuvfXWwLHHHpvn85QjFIRuVl5yySU55g0ePDhw4YUXUo5QIBYS5JXmuUfBpl6j1/reeuutQFJSUmDbtm0lfgRprhmlVNWrZi8DBgzIMV+PJ02aVGbbhcixbds2939qaqr7f8mSJbZ27docZUZjvvTq1Su7zKhM7du3L8cyas7Qvn377GUmT57smiUcddRR2cv06NHDzaPsxY6rr77aTj75ZOvfv3+O+ZQjFMTHH39s3bp1s7POOss1Hz/iiCPs+eefpxyhUI499lj7+uuv7ddff3WPf/zxR5s4caKddNJJnI9QJEtK8VpIy+g1eq3vhBNOcE1Bg5uul5SEEn8HlIiNGzdaZmam1alTJ8d8PVbhxaFNN6+GDx/ufiB1ghG/XIQrM8uWLctepnz58la9evVcy/iv1/+6aAuleZS92PD222/brFmzbPr06bmeoxyhIH7//Xd7+umn3XnojjvusGnTprm+KLqYuvjiiylHKJBbb73V3bBs3bq1lStXzl33jBw50s477zzORyiStaV4LaT/Q99H69S6S+N6iSAvyqlDaejFfeg8HHquueYa++mnn9wdz+IoM6HLhFueshcbVqxYYddff7199dVXLqFTXihHyI8SY6gm74EHHnCPVZOnxAYK/BTkUY5QEO+8846NGTPG3nzzTWvXrp3NmTPHJYFSzciQIUMoRyiyuFK6FirL6yWaa0YpZejRXa3QOwHr16/PddcAhxZlg1JTqfHjx1vDhg2z59etW9f9n1+Z0TJqCqyMUfkts27dulzvu2HDBspeDFATEh1vZetNSEhw03fffWePP/64+9svB5Qj5KdevXrWtm3bHPPatGnjMs0J5yMUxC233GK33XabnXvuudahQweXvfDGG290WX8pRyiKuqV4LaRlQt9H61RT0NK4VifIi1Kq6tVF2NixY3PM1+Ojjz66zLYLZUd3hlSD9/7779s333zjUk4H02OdcILLjE5iuoD3y4zKVGJiYo5l1qxZY/PmzctepmfPnq75jJpf+aZOnermUfaiX79+/Wzu3Lnujrk/qUbmggsucH8rhTnlCAdyzDHH5BrCRf2qmjRp4v7mfISC2LVrl8XH57xU1Q1ufwgFyhEKq1kpXgtpGb1Gr/WplYyares9SlyJp3ZBiQ+h8OKLL7osPzfccIMbQmHp0qXs9UPQVVdd5bJDffvtt4E1a9ZkT7t27cpeRtmktMz777/v0gafd955YdMGN2zY0KU7V8rfvn37hk0brLT6yiSlqUOHDgyhEMOCs2sK5QgFGX4jISEhMHLkyMCiRYsCb7zxRqBixYqBMWPGUI5QYEOGDHFp7P0hFPTbVbNmzcCIESMoR8g3O/Ts2bPdpFBn9OjR7m9/iLHS+g3zh1Do16+fW4fWpXUyhAIK5D//+Y8bi6h8+fKBLl26ZKfLx6FHJ7Jwk8bOC04dfPfdd7v0wUrh+6c//cmd4ILt3r3bnYBSU1MDycnJ7oS1fPnyHMts2rQpcMEFF7hxZjTp7y1btpTaZ0XZBnmUIxTEJ5984i5wdK7R8D7PPfdcjucpRzgQXXTr3KMxXytUqBBo3ry5G/8sPT2dcoQ8jR8/Puz1kG4alPa5R4GlhgLROrQurVNDNpSGOP1T8vWFAAAAAIDSQJ88AAAAAIghBHkAAAAAEEMI8gAAAAAghhDkAQAAAEAMIcgDAAAAgBhCkAcAAAAAMYQgDwAAAABiCEEeAAAAAMQQgjwAAAAAiCEEeQAA5GPo0KEWFxeXa1q8eDH7DQAQkRLKegMAAIh0J554or388ss55tWqVSvH471791r58uVLecsAAMiNmjwAAA4gKSnJ6tatm2Pq16+fXXPNNTZ8+HCrWbOmHX/88W7Z0aNHW4cOHaxSpUrWqFEjGzZsmO3YsSN7Xa+88opVq1bNPv30U2vVqpVVrFjRzjzzTNu5c6e9+uqr1rRpU6tevbpde+21lpmZmSOIHDFihDVo0MCt+6ijjrJvv/2WYwcAyIWaPAAAikhB2VVXXWU//PCDBQIBNy8+Pt4ef/xxF6wtWbLEBXkKzp566qns1+3atcst8/bbb9v27dtt8ODBblLw9/nnn9vvv/9uZ5xxhh177LF2zjnnuNf85S9/saVLl7rX1K9f3z744ANXwzh37lxr0aIFxxAAkC0u4P8qAQCAsH3yxowZYxUqVMieN3DgQNuwYYNt27bNZs+ene9ee++991wguHHjxuyaPAVs6tN32GGHuXlXXnmlvf7667Zu3TqrXLmym6cAToHiM888Y7/99psL5FauXOkCPF///v3tyCOPtAceeIAjBwDIRk0eAAAH0KdPH3v66aezH6u55HnnnWfdunXLtez48eNd0DV//nxLS0uzjIwM27Nnj2uOqdeJmmj6AZ7UqVPHBXR+gOfPW79+vft71qxZrqawZcuWOd4rPT3datSowfEDAORAkAcAwAEoODv88MPDzg+2bNkyO+mkk1zN3D/+8Q9LTU21iRMn2qWXXmr79u3LXi4xMTHH65StM9y8rKws97f+L1eunM2cOdP9Hyw4MAQAgCAPAIBiNGPGDFdz98gjj7i+efLuu+8e9HqPOOIIl4RFNXvHHXdcMWwpACCWkV0TAIBioiaYCvKeeOIJlzxF/ezUp+5gqZnmBRdcYBdffLG9//77LqHL9OnT7cEHH3SJWgAACEaQBwBAMencubMbQkHBV/v27e2NN96wUaNGFcu6NU6fgrybbrrJDb1w6qmn2tSpU90wDQAABCO7JgAAAADEEGryAAAAACCGEOQBAAAAQAwhyAMAAACAGEKQBwAAAAAxhCAPAAAAAGIIQR4AAAAAxBCCPAAAAACIIQR5AAAAABBDCPIAAAAAIIYQ5AEAAABADCHIAwAAAIAYQpAHAAAAABY7/h+OpBRFZXdhnQAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "first half: -263 second half: +1,787\n" + ] + } + ], + "source": [ + "cnt_cpu = np.bincount(fr_cpu, minlength=N)\n", + "cnt_cuda = np.bincount(fr_cuda, minlength=N)\n", + "cum = np.cumsum(cnt_cuda.astype(np.int64) - cnt_cpu.astype(np.int64))\n", + "\n", + "fig, ax = plt.subplots(figsize=(9, 3.6))\n", + "ax.plot(cum, color='C1', linewidth=1.2)\n", + "ax.axhline(0, color='gray', linewidth=0.5)\n", + "ax.set_xlabel('Frame')\n", + "ax.set_ylabel('cumulative CUDA - CPU')\n", + "ax.set_title(f'Cluster-count difference accumulating over {N:,} frames '\n", + " f'(final {cum[-1]:+,})')\n", + "ax.grid(alpha=0.25)\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "half = N // 2\n", + "print(f'first half: {cum[half - 1]:+,} second half: {cum[-1] - cum[half - 1]:+,}')" + ] + } + ], + "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 +}