- CUDA Graph variant of ClusterFinderCUDA: one pre-recorded graph per stream
(memset + H2D + kernel + D2H), with per-frame src/dst pointers swapped via
cudaGraphExecMemcpyNodeSetParams to cut per-frame launch overhead.
- Exposed through the _aare_cuda bindings and a ClusterFinderCUDAGraph factory.
- DEVICE_PED_TYPE for device pedestal/variance (shipped double/double to match
the double CPU ClusterFinder).
- Local-max suppression in Test 1/Test 3: non-peak pixels no longer store or
update, mirroring ClusterFinder's `value < max -> continue`.
- device_pedestal()/device_noise() accessors (+ bindings) for the in-kernel
decision-time pedestal.
- Move Chi2.hpp from include/aare/ to src/ (private)
- Pimpl on FitModel<Model>: MnUserParameters/MnStrategy behind opaque
src/FitModelImpl.hpp, no Minuit2 includes in public headers
- Move fit_pixel/fit_3d bodies to Fit.cpp with explicit instantiations
for all 8 models; drop FCN template param from public API
- CMake: aare::Minuit2 wrapped in $<BUILD_INTERFACE:...> (hidden from
exported targets, same pattern as lmfit), MINUIT2_INSTALL OFF, Chi2.hpp
removed from PUBLICHEADERS
- Update python bindings and benchmark callsites accordingly
---------
Co-authored-by: Erik Fröjdh <erik.frojdh@psi.ch>
Co-authored-by: Alice <alice.mazzoleni@psi.ch>
Collection of small improvements in usability:
- NDView works for large arrays
- Direct subtraction of Pedestal from np.array
- len() support for python bindings of files
- reshape image directly in decoder such that first dimension is num
counters
- take into account chip artefact in decoder.
---------
Co-authored-by: Erik Fröjdh <erik.frojdh@psi.ch>
With C++20 `fmt::print(s)` expects a compile time format string and
otherwise fails complaining about consteval. To get runtime formatting
use `fmt::print(fmt::runtime(s))`
If AARE_FETCH_MINUIT is set to OFF we first look for a standalone
Minuit2 and if that is not found we try to find Minuit2 as a part of
ROOT.
In both cases we make an alias to allow for simpler use of the target
later.
It still doesn't solve the issue that we install Minuit to when we fetch
it but that can be addressed in a separate PR.
closes #316
Multi threaded filling of per pixel histograms for example for detector calibration
1. PixelHistogram - Generic variant expects already pedestal subtracted
data
2. PedestalTrackingHistogram - Terrible name, useful class. Keeps it's
own pedestal and does conversion and pedestal tracking in the worker
threads.
---------
Co-authored-by: Lars Erik Fröjd <froejdh_e@pc-jungfrau-02.psi.ch>
- Eliminate the ~200–300 µs inter-batch idle gap by allowing two batches
to be in-flight simultaneously:
- submit_batch() enqueues H2D+kernel+D2H without blocking
- collect() syncs via cudaEventSynchronize (not
cudaStreamSynchronize) so a queued second batch runs uninterrupted.
- Two ping-pong output slots (NUM_SLOTS=2) with per-slot pinned buffers
and cudaEventDisableTiming sync events.
- find_clusters_batched() keeps its direct implementation.
* Measured: 0.026 -> 0.022 ms/frame (~18%).
- Device pedestal arrays (mean/sum/sum2) are now float instead of
double: halves global-memory bandwidth for pedestal reads/writes and
eliminates FP64 arithmetic in the kernel (3.3x kernel speedup,
15µs -> 4.6µs).
- Replace the per-cluster push_back loop in the D2H drain with a
single resize()+memcpy().
Rework the multi-stream pipeline to eliminate per-frame sync barriers and
fix the D2H staging architecture.
Sync reduction:
- Replace one cudaStreamSynchronize per frame with one per stream per batch,
cutting synchronisation calls from O(n_frames x n_streams) to O(n_streams)
- Introduce a unified per-frame D2H output layout [uint32_t count | clusters[max]]
stored in a single class-level lazy-allocated pinned pool (h_output_pinned),
replacing the per-stream separate cluster/count device buffers
- Move CUDA event pool from per-stream fixed-size to per-frame-slot lazy-allocated,
enabling correct kernel timing across any batch size
Pinned H2D without CPU-side copy:
- Add register_input_buffer(ptr, bytes) / unregister_input_buffer() wrapping
cudaHostRegister so callers can pin their existing batch buffer once; all
find_clusters_batched() slices then transfer at DMA speed (~22 GB/s) instead
of ~15 GB/s for pageable, with no extra memcpy or WC-memory penalty
Result (RTX 4090, 400x400 uint16, 3x3 clusters, batch=2000, 5 streams):
Before: ~34 µs/frame -> After: ~28 µs/frame (−18 %)
Add options for var cluster_finder_X:
1. number of neighbors (for better segmentation of clusters)
2. option to empty the surrounding pixels
---------
Co-authored-by: xiangyu.xie <xiangyu.xie@psi.ch>
- Use per-stream pinned host staging buffers for truly async CUDA transfers.
- Avoid reserving full device capacity per result frame.
- Reduce kernel work by delaying cluster payload construction.
- Use squared comparisons and removing per-pixel sqrtf() ops.
- Stencil arithmetic and shared memory use float (COMPUTE_TYPE alias).
- Pedestal accumulation stays double to preserve variance accuracy.
Notes:
- On RTX 4090, FP32 throughput is ~64× higher than FP64, so moving
stencil math to float improves performance.
- Using float also avoids shared memory bank conflicts: stride-18 maps
to distinct banks for 32-bit values, but caused conflicts with 64-bit.
- Allowing the users more flexibility to play around with custom eta
functions without touching the c++ code
- passing vector of eta values to ``transform_eta_values``
```
from aare import Interpolator, ClusterVector, Etai, Cluster
import numpy as np
def custom_eta(cluster_pixel_coordinate_x, cluster_pixel_coordinate_y, cluster_data):
# dummy custom eta function that just returns the sum of the cluster data
eta = Etai()
eta.x = 0.1 # dummy x value
eta.y = 0.1 # dummy y value
eta.sum = np.sum(cluster_data) # sum of the cluster data as the "energy
return eta
# Create a dummy eta distribution and bins
eta_distribution = np.zeros((10, 10, 1)) # dummy eta distribution
etax_bins = np.linspace(0, 1.0, 11)
etay_bins = np.linspace(0, 1.0, 11)
e_bins = np.array([0., 10.]) # dummy energy bins
# Create the interpolator
interpolator = Interpolator(eta_distribution, etax_bins, etay_bins, e_bins)
# Create a dummy cluster vector
cluster_vector = ClusterVector()
cluster_vector.push_back(Cluster(10, 5, np.ones(shape=9, dtype = np.int32)))
cluster_vector.push_back(Cluster(20, 10, np.ones(shape=9, dtype = np.int32)))
# Create dummy etas for the clusters
cluster_array = np.array(cluster_vector)
etas = np.array([custom_eta(cluster["x"], cluster["y"], cluster["data"]) for cluster in cluster_array])
# transform eta values to uniform coordinates
uniform_coordinates = interpolator.transform_eta_values(etas)
# Interpolate to get the photon coordinates e.g. apply interpolation logic
photon_coordinates_x = cluster_array["x"] + uniform_coordinates["x"] # add to pixel coordinate
photon_coordinates_y = cluster_array["y"] + uniform_coordinates["y"] # add to pixel coordinate
```
advantage: full control over interpolation logic,
downside: inefficient quite some loops in python
- passing pre computed eta values to interpolate function
```
Interpolator.interpolate(cluster_vector, etas)
```
downside: less flexibility in interpolation logic.
downside: People might misuse it instead of using interpolate directly
with a pre compiled eta function implemented in c++
- After upgrading to pybind11 3, duplicate registration of cluster-related
types across `_aare` and `_aare_cuda` started failing.
- Mark the `Cluster` and `ClusterVector` bindings as `py::module_local()` so
each extension owns its local registration.
Note: cluster objects from CPU and CUDA bindings are now distinct Python types.
- Add bind_ClusterFinderCUDA.hpp with pybind11 bindings for
ClusterFinderCUDA
- Build CUDA bindings as separate _aare_cuda.so to avoid
segfaults from mixing nvcc and gcc compiled code in the
same shared object
- Re-export CUDA classes onto _aare in __init__.py so user
code uses `from aare import ClusterFinderCUDA` regardless
of which .so hosts the class
- Factory in ClusterFinder.py selects backend; RuntimeError
if GPU requested on CPU-only build
- Update python/CMakeLists.txt: _aare_cuda module gated
behind AARE_CUDA and AARE_PYTHON_BINDINGS
- Add validation notebook: ~20x speedup vs sequential ClusterFinder
To improve codebase quality and reduce human error, this PR introduces
the pre-commit framework. This ensures that all code adheres to project
standards before it is even committed, maintaining a consistent style
and catching common mistakes early.
Key Changes:
- Code Formatting: Automated C++ formatting using clang-format (based on
the project's .clang-format file).
- Syntax Validation: Basic checks for file integrity and syntax.
- Spell Check: Automated scanning for typos in source code and comments.
- CMake Formatting: Standardization of CMakeLists.txt and .cmake
configuration files.
- GitHub Workflow: Added a CI action that validates every Pull Request
against the pre-commit configuration to ensure compliance.
The configuration includes a [ci] block to handle automated fixes within
the PR. Currently, this is disabled. If we want the CI to automatically
commit formatting fixes back to the PR branch, this can be toggled to
true in .pre-commit-config.yaml.
```yaml
ci:
autofix_commit_msg: [pre-commit] auto fixes from pre-commit hooks
autofix_prs: false
autoupdate_schedule: monthly
```
The last large commit with the fit functions, for example, was not
formatted according to the clang-format rules. This PR would allow to
avoid similar mistakes in the future.
Python fomat with `ruff` for tests and sanitiser for `.ipynb` notebooks
can be added as well.
- Non-photon pixels now update pedestal (push_fast equivalent)
directly in the kernel, no atomics needed
- Commented out quadrant significance test (c2): absent from
sequential CPU code, was producing GPU-only clusters.
- Added d_pd_sum to device allocations and host upload
Build (sm_89): 46 registers, 0 spills, 100% occupancy.
Verified on 256x256 Jungfrau data, 5000 frames, nSigma=5.0:
CPU 8428 vs GPU 8471 clusters, 99.8% match
0.63 ms/frame CPU vs 0.04 ms/frame GPU (~16x)