Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-07-26 08:22:05

0001 /** TRACCC library, part of the ACTS project (R&D line)
0002  *
0003  * (c) 2022-2026 CERN for the benefit of the ACTS project
0004  *
0005  * Mozilla Public License Version 2.0
0006  */
0007 
0008 #pragma once
0009 
0010 #include <mutex>
0011 
0012 #include <vecmem/memory/device_atomic_ref.hpp>
0013 
0014 #include "traccc/clusterization/clustering_config.hpp"
0015 #include "traccc/clusterization/device/aggregate_cluster.hpp"
0016 #include "traccc/clusterization/device/ccl_kernel_definitions.hpp"
0017 #include "traccc/clusterization/device/reduce_problem_cell.hpp"
0018 #include "traccc/device/concepts/barrier.hpp"
0019 #include "traccc/device/concepts/thread_id.hpp"
0020 #include "traccc/device/mutex.hpp"
0021 #include "traccc/device/unique_lock.hpp"
0022 #include "traccc/edm/silicon_cell_collection.hpp"
0023 
0024 namespace traccc::device {
0025 
0026 /// Implementation of a FastSV algorithm with the following steps:
0027 ///   1) mix of stochastic and aggressive hooking
0028 ///   2) shortcutting
0029 ///
0030 /// The implementation corresponds to an adapted versiion of Algorithm 3 of
0031 /// the following paper:
0032 /// https://www.sciencedirect.com/science/article/pii/S0743731520302689
0033 ///
0034 ///                     This array only gets updated at the end of the iteration
0035 ///                     to prevent race conditions.
0036 /// @param[in] adjc     The number of adjacent cells
0037 /// @param[in] adjv     Vector of adjacent cells
0038 /// @param[in] tid      The thread index
0039 /// @param[in] blckDim  The block size
0040 /// @param[inout] f     array holding the parent cell ID for the current
0041 ///                     iteration.
0042 /// @param[inout] gf    array holding grandparent cell ID from the previous
0043 ///                     iteration.
0044 /// @param[in] barrier  A generic object for block-wide synchronisation
0045 ///
0046 template <device::concepts::barrier barrier_t,
0047           device::concepts::thread_id1 thread_id_t, typename index_t>
0048 TRACCC_HOST_DEVICE void fast_sv_1(const thread_id_t& thread_id,
0049                                   vecmem::device_vector<index_t>& f,
0050                                   vecmem::device_vector<index_t>& gf,
0051                                   unsigned char* adjc, index_t* adjv,
0052                                   unsigned int thread_cell_count,
0053                                   barrier_t& barrier) {
0054   /*
0055    * The algorithm finishes if an iteration leaves the arrays unchanged.
0056    * This varible will be set if a change is made, and dictates if another
0057    * loop is necessary.
0058    */
0059   bool gf_changed;
0060 
0061   do {
0062     /*
0063      * Reset the end-parameter to false, so we can set it to true if we
0064      * make a change to the gf array.
0065      */
0066     gf_changed = false;
0067 
0068     /*
0069      * The algorithm executes in a loop of three distinct parallel
0070      * stages. In this first one, a mix of stochastic and aggressive
0071      * hooking, we examine adjacent cells and copy their grand parents
0072      * cluster ID if it is lower than ours, essentially merging the two
0073      * together.
0074      */
0075     for (unsigned int tst = 0; tst < thread_cell_count; ++tst) {
0076       const unsigned int cid =
0077           tst * thread_id.getBlockDimX() + thread_id.getLocalThreadIdX();
0078 
0079       TRACCC_ASSUME(adjc[tst] <= 4);
0080       for (unsigned char k = 0; k < adjc[tst]; ++k) {
0081         const auto cid2 = adjv[4 * tst + k];
0082 
0083         index_t q2 = gf.at(cid2);
0084         index_t q1 = gf.at(cid);
0085 
0086         if (gf.at(cid) > q2) {
0087           f.at(f.at(cid)) = q2;
0088           f.at(cid) = q2;
0089         }
0090         if (gf.at(cid2) > q1) {
0091           f.at(f.at(cid2)) = q1;
0092           f.at(cid2) = q1;
0093         }
0094       }
0095     }
0096 
0097     /*
0098      * Each stage in this algorithm must be preceded by a
0099      * synchronization barrier!
0100      */
0101     barrier.blockBarrier();
0102 
0103     for (unsigned int tst = 0; tst < thread_cell_count; ++tst) {
0104       const unsigned int cid =
0105           tst * thread_id.getBlockDimX() + thread_id.getLocalThreadIdX();
0106       /*
0107        * The second stage is shortcutting, which is an optimisation that
0108        * allows us to look at any shortcuts in the cluster IDs that we
0109        * can merge without adjacency information.
0110        */
0111       if (f.at(cid) > gf.at(cid)) {
0112         f.at(cid) = gf.at(cid);
0113       }
0114     }
0115 
0116     /*
0117      * Synchronize before the final stage.
0118      */
0119     barrier.blockBarrier();
0120 
0121     for (unsigned int tst = 0; tst < thread_cell_count; ++tst) {
0122       const unsigned int cid =
0123           tst * thread_id.getBlockDimX() + thread_id.getLocalThreadIdX();
0124       /*
0125        * Update the array for the next generation, keeping track of any
0126        * changes we make.
0127        */
0128       if (gf.at(cid) != f.at(f.at(cid))) {
0129         gf.at(cid) = f.at(f.at(cid));
0130         gf_changed = true;
0131       }
0132     }
0133 
0134     /*
0135      * To determine whether we need another iteration, we use block
0136      * voting mechanics. Each thread checks if it has made any changes
0137      * to the arrays, and votes. If any thread votes true, all threads
0138      * will return a true value and go to the next iteration. Only if
0139      * all threads return false will the loop exit.
0140      */
0141   } while (barrier.blockOr(gf_changed));
0142 }
0143 
0144 template <device::concepts::barrier barrier_t,
0145           device::concepts::thread_id1 thread_id_t, typename index_t>
0146 TRACCC_HOST_DEVICE inline void ccl_core(
0147     const clustering_config& cfg, const thread_id_t& thread_id,
0148     std::size_t& partition_start, std::size_t& partition_end,
0149     vecmem::device_vector<index_t> f, vecmem::device_vector<index_t> gf,
0150     index_t* adjv, unsigned char* adjc,
0151     const edm::silicon_cell_collection::const_device& cells_device,
0152     const detector_design_description::const_device& det_desc,
0153     const detector_conditions_description::const_device& det_cond,
0154     edm::measurement_collection::device measurements_device,
0155     const barrier_t& barrier, vecmem::device_vector<unsigned int>& disjoint_set,
0156     vecmem::device_vector<unsigned int>& cluster_size) {
0157   const auto size = static_cast<unsigned int>(partition_end - partition_start);
0158 
0159   assert(size <= f.size());
0160   assert(size <= gf.size());
0161 
0162   const unsigned int thread_cell_count =
0163       (size - thread_id.getLocalThreadIdX() + thread_id.getBlockDimX() - 1) /
0164       thread_id.getBlockDimX();
0165 
0166   for (unsigned int tst = 0; tst < thread_cell_count; ++tst) {
0167     /*
0168      * Look for adjacent cells to the current one.
0169      */
0170     const unsigned int cid =
0171         tst * thread_id.getBlockDimX() + thread_id.getLocalThreadIdX();
0172     adjc[tst] = 0;
0173 
0174     reduce_problem_cell(
0175         cells_device, cid, static_cast<unsigned int>(partition_start),
0176         static_cast<unsigned int>(partition_end), adjc[tst], &adjv[4 * tst]);
0177 
0178     f.at(cid) = static_cast<index_t>(cid);
0179     gf.at(cid) = static_cast<index_t>(cid);
0180   }
0181 
0182   /*
0183    * Now that the data has initialized, we synchronize again before we
0184    * move onto the actual processing part.
0185    */
0186   barrier.blockBarrier();
0187 
0188   /*
0189    * Run FastSV algorithm, which will update the father index to that of
0190    * the cell belonging to the same cluster with the lowest index.
0191    */
0192   fast_sv_1(thread_id, f, gf, adjc, adjv, thread_cell_count, barrier);
0193 
0194   barrier.blockBarrier();
0195 
0196   /*
0197    * We'll now convert the parent array `f` into a linked list equivalent
0198    * stored in array `gf`. The point of this linked list is that the ID of
0199    * each cell is either:
0200    *
0201    * - An out-of-bounds value if there is no later cell in the same
0202    *   cluster; or
0203    * - An in-bounds index pointing to the next cell belonging to the same
0204    *   cluster.
0205    *
0206    * If, for example, f would look like this:
0207    *
0208    *        0  1  2  3  4  5  6  7  8
0209    * f  = [ 0, 0, 2, 0, 2, 5, 2, 5, 8]
0210    *
0211    * We would compute:
0212    *
0213    * gf = [ 1, 3, 4, 9, 6, 7, 9, 9, 9]
0214    *
0215    * This makes aggregating the clusters trivial.
0216    *
0217    * WARNING: After this point, the `gf` vector no longer contains the
0218    * grandparent information it held before. Do not use it as such!
0219    *
0220    * First, we start out by setting out-of-bounds values for all cells...
0221    */
0222   for (unsigned int i = thread_id.getLocalThreadIdX(); i < size;
0223        i += thread_id.getBlockDimX()) {
0224     gf.at(i) = static_cast<index_t>(partition_end - partition_start);
0225   }
0226 
0227   barrier.blockBarrier();
0228 
0229   /*
0230    * Now we construct the actual linked list. We move backwards here,
0231    * because we are much more likely to be able to exit our loop early
0232    * compared to looping forwards.
0233    */
0234   for (unsigned int i = thread_id.getLocalThreadIdX(); i < size;
0235        i += thread_id.getBlockDimX()) {
0236     const index_t effi =
0237         static_cast<index_t>((partition_end - partition_start) - (i + 1));
0238 
0239     const auto fid = f.at(effi);
0240 
0241     if (fid != effi) {
0242       for (unsigned int j = effi - 1; j < size; --j) {
0243         if (fid == f.at(j)) {
0244           gf.at(j) = effi;
0245           break;
0246         }
0247       }
0248     }
0249   }
0250 
0251   barrier.blockBarrier();
0252 
0253   for (details::index_t tst = 0; tst < thread_cell_count; ++tst) {
0254     const auto cid = static_cast<details::index_t>(
0255         tst * thread_id.getBlockDimX() + thread_id.getLocalThreadIdX());
0256 
0257     if (f.at(cid) == cid) {
0258       // Add a new measurement to the output buffer. Remembering its
0259       // position inside of the container.
0260       const edm::measurement_collection::device::size_type meas_pos =
0261           measurements_device.push_back_default();
0262       // Set up the measurement under the appropriate index.
0263       aggregate_cluster(
0264           cfg, cells_device, det_desc, det_cond, gf,
0265           static_cast<unsigned int>(partition_start),
0266           static_cast<unsigned int>(partition_end), cid,
0267           measurements_device.at(meas_pos), meas_pos, disjoint_set,
0268           (cluster_size.capacity()
0269                ? std::optional<
0270                      std::reference_wrapper<unsigned int>>{cluster_size.at(
0271                      meas_pos)}
0272                : std::nullopt));
0273     }
0274   }
0275 }
0276 
0277 template <device::concepts::barrier barrier_t,
0278           device::concepts::thread_id1 thread_id_t>
0279 TRACCC_HOST_DEVICE inline void ccl_kernel(
0280     const clustering_config cfg, const thread_id_t& thread_id,
0281     const edm::silicon_cell_collection::const_view& cells_view,
0282     const detector_design_description::const_view& det_desc_view,
0283     const detector_conditions_description::const_view& det_cond_view,
0284     std::size_t& partition_start, std::size_t& partition_end, std::size_t& outi,
0285     vecmem::data::vector_view<details::index_t> f_view,
0286     vecmem::data::vector_view<details::index_t> gf_view,
0287     vecmem::data::vector_view<details::fallback_index_t> f_backup_view,
0288     vecmem::data::vector_view<details::fallback_index_t> gf_backup_view,
0289     vecmem::data::vector_view<unsigned char> adjc_backup_view,
0290     vecmem::data::vector_view<details::fallback_index_t> adjv_backup_view,
0291     vecmem::device_atomic_ref<uint32_t> backup_mutex,
0292     vecmem::data::vector_view<unsigned int> disjoint_set_view,
0293     vecmem::data::vector_view<unsigned int> cluster_size_view,
0294     const barrier_t& barrier,
0295     edm::measurement_collection::view measurements_view) {
0296   // Construct device containers around the views.
0297   const edm::silicon_cell_collection::const_device cells_device(cells_view);
0298   const detector_design_description::const_device det_desc(det_desc_view);
0299   const detector_conditions_description::const_device det_cond(det_cond_view);
0300   edm::measurement_collection::device measurements_device(measurements_view);
0301   vecmem::device_vector<details::index_t> f_primary(f_view);
0302   vecmem::device_vector<details::index_t> gf_primary(gf_view);
0303   vecmem::device_vector<details::fallback_index_t> f_backup(f_backup_view);
0304   vecmem::device_vector<details::fallback_index_t> gf_backup(gf_backup_view);
0305   vecmem::device_vector<unsigned char> adjc_backup(adjc_backup_view);
0306   vecmem::device_vector<details::fallback_index_t> adjv_backup(
0307       adjv_backup_view);
0308   vecmem::device_vector<unsigned int> disjoint_set(disjoint_set_view);
0309   vecmem::device_vector<unsigned int> cluster_size(cluster_size_view);
0310 
0311   mutex<uint32_t> mutex(backup_mutex);
0312   unique_lock lock(mutex, std::defer_lock);
0313 
0314   const unsigned int num_cells = cells_device.size();
0315 
0316   /*
0317    * First, we determine the exact range of cells that is to be examined
0318    * by this block of threads. We start from an initial range determined
0319    * by the block index multiplied by the target number of cells per
0320    * block. We then shift both the start and the end of the block forward
0321    * (to a later point in the array); start and end may be moved different
0322    * amounts.
0323    */
0324   if (thread_id.getLocalThreadIdX() == 0) {
0325     unsigned int start = thread_id.getBlockIdX() * cfg.target_partition_size();
0326     assert(start < num_cells);
0327     unsigned int end = std::min(num_cells, start + cfg.target_partition_size());
0328     outi = 0;
0329 
0330     /*
0331      * Next, shift the starting point to a position further in the
0332      * array; the purpose of this is to ensure that we are not operating
0333      * on any cells that have been claimed by the previous block (if
0334      * any).
0335      */
0336     while (start != 0 && start < num_cells &&
0337            cells_device.module_index().at(start - 1) ==
0338                cells_device.module_index().at(start) &&
0339            cells_device.channel1().at(start) <=
0340                cells_device.channel1().at(start - 1) + 1) {
0341       ++start;
0342     }
0343 
0344     /*
0345      * Then, claim as many cells as we need past the naive end of the
0346      * current block to ensure that we do not end our partition on a
0347      * cell that is not a possible boundary!
0348      */
0349     while (end < num_cells &&
0350            cells_device.module_index().at(end - 1) ==
0351                cells_device.module_index().at(end) &&
0352            cells_device.channel1().at(end) <=
0353                cells_device.channel1().at(end - 1) + 1) {
0354       ++end;
0355     }
0356     partition_start = start;
0357     partition_end = end;
0358     assert(partition_start <= partition_end);
0359   }
0360 
0361   barrier.blockBarrier();
0362 
0363   // It seems that sycl runs into undefined behaviour when calling
0364   // group synchronisation functions when some threads have already run
0365   // into a return. As such, we cannot use returns in this kernel.
0366 
0367   // Get partition for this thread group
0368   const auto size = static_cast<unsigned int>(partition_end - partition_start);
0369 
0370   // If the size is zero, we can just retire the whole block.
0371   if (size == 0) {
0372     return;
0373   }
0374 
0375   /*
0376    * If our partition is too large, we need to handle this specific edge
0377    * case. The first thread of the block will attempt to enter a critical
0378    * section by obtaining a lock on a mutex in global memory. When this is
0379    * obtained, we can use some memory in global memory instead of the shared
0380    * memory. This can be done more efficiently, but this should be a very
0381    * rare edge case.
0382    */
0383   if (size > cfg.max_partition_size()) [[unlikely]] {
0384     if (thread_id.getLocalThreadIdX() == 0) {
0385       lock.lock();
0386     }
0387 
0388     barrier.blockBarrier();
0389 
0390     unsigned char* adjc = adjc_backup.data() + (thread_id.getLocalThreadIdX() *
0391                                                 cfg.max_cells_per_thread *
0392                                                 cfg.backup_size_multiplier);
0393     details::fallback_index_t* adjv =
0394         adjv_backup.data() +
0395         (thread_id.getLocalThreadIdX() * 4 * cfg.max_cells_per_thread *
0396          cfg.backup_size_multiplier);
0397     ccl_core(cfg, thread_id, partition_start, partition_end, f_backup,
0398              gf_backup, adjv, adjc, cells_device, det_desc, det_cond,
0399              measurements_device, barrier, disjoint_set, cluster_size);
0400   } else {
0401     /*
0402      * Vector of indices of the adjacent cells.
0403      */
0404     details::index_t adjv[details::CELLS_PER_THREAD_STACK_LIMIT * 4];
0405 
0406     /*
0407      * The number of adjacent cells for each cell must start at zero, to
0408      * avoid uninitialized memory. adjv does not need to be zeroed, as
0409      * we will only access those values if adjc indicates that the value
0410      * is set.
0411      */
0412     unsigned char adjc[details::CELLS_PER_THREAD_STACK_LIMIT];
0413 
0414     ccl_core(cfg, thread_id, partition_start, partition_end, f_primary,
0415              gf_primary, adjv, adjc, cells_device, det_desc, det_cond,
0416              measurements_device, barrier, disjoint_set, cluster_size);
0417   }
0418 
0419   barrier.blockBarrier();
0420 }
0421 }  // namespace traccc::device