Back to home page

EIC code displayed by LXR

 
 

    


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

0001 /** TRACCC library, part of the ACTS project (R&D line)
0002  *
0003  * (c) 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 <cub/cub.cuh>
0011 
0012 #include "traccc/device/sort.hpp"
0013 
0014 namespace traccc::cuda {
0015 namespace kernels {
0016 /**
0017  * @brief Sort the cells that will serve as the input to the clustering
0018  * algorithm.
0019  *
0020  * @note This performs a sort per module, and while the sorting might
0021  * reorder modules within the input, there is no guarantee that the indices
0022  * of the modules will be sorted in any particular way.
0023  *
0024  * @note The permutation map may be filled even if `write_permutation` is
0025  * false.
0026  *
0027  * @tparam SORT_THREADS_PER_BLOCK The number of threads to use per block when
0028  *     launching this kernel.
0029  * @tparam PER_THREAD_MAX The maximum number of cells to sort per thread.
0030  *
0031  * @param[in] cells_per_thread The target number of cells to allocate per
0032  *     thread.
0033  * @param[in] num_cells The number of cells to sort.
0034  * @param[in] cells The original, potentially unordered cells.
0035  * @param[out] new_cells The new, sorted vector of cells.
0036  * @param[out] permutation_map_view The permutation map between new and old
0037  *     indices.
0038  * @param[in] write_permutation Boolean flag to enforce writing to the
0039  *     `permutation_map_view` vector.
0040  */
0041 template <std::size_t SORT_THREADS_PER_BLOCK, std::size_t PER_THREAD_MAX>
0042 __global__ __launch_bounds__(SORT_THREADS_PER_BLOCK) void sort_cells(
0043     const unsigned int cells_per_thread, const unsigned int num_cells,
0044     const edm::silicon_cell_collection::const_view cells,
0045     edm::silicon_cell_collection::view new_cells,
0046     vecmem::data::vector_view<unsigned int> permutation_map_view,
0047     const bool write_permutation) {
0048   /*
0049    * We start off defining some types which we will use in sorting. In
0050    * particular, we will be doing a key-value sort where the key is composed
0051    * of the channel0 and channel1 coordinates, as well as the module index.
0052    * Although this can, for many use cases, fit into 32 bits, we use 64 bits
0053    * to be sure. The value, then, is simply the index of the cell within the
0054    * block-local frame, which easily fits into 32 bits.
0055    */
0056   using key_t = std::uint64_t;
0057   using value_t = std::uint32_t;
0058   static_assert(sizeof(key_t) == 8);
0059   static_assert(sizeof(value_t) == 4);
0060 
0061   /*
0062    * The meat of the pudding here is CUB's block radix sort algorithm, which
0063    * is an exact fit for the job we are trying to do here. This struct
0064    * defines the thread allocation as well as the shared memory sizing.
0065    */
0066   using sort_t = cub::BlockRadixSort<key_t, SORT_THREADS_PER_BLOCK,
0067                                      PER_THREAD_MAX, value_t>;
0068 
0069   /*
0070    * Compute the partition target size based on the cells per thread
0071    * parameter and the block dimension. Note that we routinely overshoot
0072    * or undershoot this value because the partitioning algorithm is greedy.
0073    */
0074   unsigned int partition_target_size = cells_per_thread * blockDim.x;
0075 
0076   /*
0077    * Define a sum type of the storage that we will need in shared memory,
0078    * which will hold either the CUB sorting scratch space or a small cache
0079    * of cell coordinates and module indices. Throughout the computation, we
0080    * use the software cache first and then evict it to repurpose it as the
0081    * CUB scratchpad.
0082    *
0083    * Note that the cache always has size SORT_THREADS_PER_BLOCK times
0084    * PER_THREAD_MAX, which is also the maximum partition size that can be
0085    * sorted with the CUB algorithm.
0086    */
0087   union smem_union {
0088     typename sort_t::TempStorage cub_sorting_tmp;
0089     struct {
0090       unsigned short channel0[SORT_THREADS_PER_BLOCK * PER_THREAD_MAX];
0091       unsigned short channel1[SORT_THREADS_PER_BLOCK * PER_THREAD_MAX];
0092       unsigned int module_index[SORT_THREADS_PER_BLOCK * PER_THREAD_MAX];
0093     } cell_attribute_cache;
0094   };
0095 
0096   /*
0097    * Shared memory allocations. The union type is by far the largest, but we
0098    * also keep some integers for administrative purposes.
0099    */
0100   __shared__ smem_union smem;
0101   __shared__ unsigned int partition_start, partition_end;
0102   __shared__ unsigned int min_module_index, max_module_index, max_channel0,
0103       max_channel1;
0104 
0105   vecmem::device_vector<unsigned int> permutation_map(permutation_map_view);
0106   const edm::silicon_cell_collection::const_device cells_device(cells);
0107   edm::silicon_cell_collection::device new_cells_device(new_cells);
0108 
0109   /*
0110    * Compute the beginning and end of the partition boundary search. Note
0111    * that this employs a greedy algorithm similar to the CCL algorithm
0112    * (albeit with different partition points). The actual partition edges
0113    * will always be equal to or greater than these, never smaller.
0114    */
0115   const unsigned int search_start = blockIdx.x * partition_target_size;
0116   const unsigned int search_end =
0117       std::min(num_cells, search_start + partition_target_size);
0118 
0119   if (threadIdx.x == 0) {
0120     /*
0121      * We'll find the partition point using a parallel algorithm and rely
0122      * on atomic min operations, so we initialize the partition
0123      * boundaries by some sentinel values.
0124      */
0125     partition_start = std::numeric_limits<unsigned int>::max();
0126     partition_end = num_cells;
0127 
0128     /*
0129      * Similarly, the minimum and maximum module indices and channels are
0130      * initialized to reasonable sentinels.
0131      */
0132     min_module_index = std::numeric_limits<unsigned int>::max();
0133     max_module_index = 0;
0134     max_channel0 = 0;
0135     max_channel1 = 0;
0136   }
0137 
0138   __syncthreads();
0139 
0140   /*
0141    * First, perform the boundary search for the upper limit of the
0142    * partition. Simple parallel marching algorithm which modifies the
0143    * boundary using an atomic minimum operation in shared memory. Computing
0144    * the end first allows us to find a reasonable upper bound on the start.
0145    */
0146   {
0147     unsigned int i = search_end + threadIdx.x;
0148     bool valid_split = false;
0149 
0150     do {
0151       valid_split = (i > 0 && i < num_cells)
0152                         ? cells_device.module_index().at(i - 1) !=
0153                               cells_device.module_index().at(i)
0154                         : true;
0155 
0156       if (valid_split) {
0157         atomicMin(&partition_end, i);
0158       }
0159 
0160       i += blockDim.x;
0161     } while (!__syncthreads_or(valid_split));
0162   }
0163 
0164   /*
0165    * And then the same algorithm to find a reasonable cut-off point for the
0166    * start of the partition.
0167    *
0168    * NOTE: No synchronization is necessary here, the previous do-while loop
0169    * synchronizes as part of its loop condition.
0170    */
0171   {
0172     unsigned int i = search_start + threadIdx.x;
0173     bool valid_split = false;
0174 
0175     do {
0176       valid_split = (i > 0 && i < partition_end)
0177                         ? (cells_device.module_index().at(i - 1) !=
0178                            cells_device.module_index().at(i))
0179                         : true;
0180 
0181       if (valid_split) {
0182         atomicMin(&partition_start, i);
0183       }
0184 
0185       i += blockDim.x;
0186     } while (!__syncthreads_or(valid_split));
0187   }
0188 
0189   __syncthreads();
0190 
0191   /*
0192    * With the partition boundaries found, compute the partition size and
0193    * exit if it is 0, in which case there is no sorting to be done.
0194    */
0195   assert(partition_start <= partition_end);
0196 
0197   const unsigned int partition_size = partition_end - partition_start;
0198 
0199   if (partition_size == 0) {
0200     return;
0201   }
0202 
0203   /*
0204    * Recall that SORT_THREADS_PER_BLOCK * PER_THREAD_MAX is the maximum
0205    * number of cells that we can sort in one block, so we need a slow path
0206    * for larger partitions.
0207    */
0208   if (partition_size > SORT_THREADS_PER_BLOCK * PER_THREAD_MAX) [[unlikely]] {
0209     /*
0210      * We are unlikely to hit the slow path, but if we do we _need_ the
0211      * permutation map even if we don't explicitly need it as an output,
0212      * so we populate its values.
0213      */
0214     for (unsigned int i = partition_start + threadIdx.x; i < partition_end;
0215          i += blockDim.x) {
0216       permutation_map.at(i) = i;
0217     }
0218 
0219     __syncthreads();
0220 
0221     /*
0222      * Simple comparison function of cells that will put them in the
0223      * desired order.
0224      */
0225     auto compare_cells = [&cells_device](const unsigned int idx_a,
0226                                          const unsigned int idx_b) {
0227       const auto& a = cells_device.at(idx_a);
0228       const auto& b = cells_device.at(idx_b);
0229       if (a.module_index() < b.module_index()) {
0230         return true;
0231       } else if (a.module_index() == b.module_index()) {
0232         if (a.channel1() < b.channel1()) {
0233           return true;
0234         } else if (a.channel1() == b.channel1()) {
0235           return a.channel0() < b.channel0();
0236         } else {
0237           return false;
0238         }
0239 
0240       } else {
0241         return false;
0242       }
0243     };
0244 
0245     /*
0246      * Launch the (slow) odd-even sort to sort the indices.
0247      */
0248     traccc::cuda::details::thread_id1 thread_id;
0249     traccc::cuda::barrier barrier;
0250     device::blockOddEvenSort(thread_id, barrier,
0251                              &permutation_map.at(partition_start),
0252                              partition_size, compare_cells);
0253 
0254     /*
0255      * Using the permutation map which we have now written, put the cells
0256      * into the output vector.
0257      */
0258     for (unsigned int i = partition_start + threadIdx.x; i < partition_end;
0259          i += blockDim.x) {
0260       new_cells_device.at(i) = cells_device.at(permutation_map.at(i));
0261     }
0262   } else {
0263     /*
0264      * Then the fast path. Here, we will compute the maximum channel0 and
0265      * channel1 values, as well as the range of module indices, to find
0266      * the number of bits we need to radix sort over.
0267      *
0268      * We first compute the limits per thread to reduce atomic contention.
0269      */
0270     unsigned int local_max_channel_0 = 0;
0271     unsigned int local_max_channel_1 = 0;
0272     unsigned int local_min_module_index =
0273         std::numeric_limits<unsigned int>::max();
0274     unsigned int local_max_module_index = 0;
0275 
0276     /*
0277      * Perform a parallel march over the partition, recording the values
0278      * into our shared memory cache and then computing the minima and
0279      * maxima per thread.
0280      */
0281     for (unsigned int i = threadIdx.x; i < partition_size; i += blockDim.x) {
0282       const auto& cell = cells_device.at(partition_start + i);
0283       const auto channel0 = cell.channel0();
0284       const auto channel1 = cell.channel1();
0285       const auto module_index = cell.module_index();
0286 
0287       local_max_channel_0 = std::max(local_max_channel_0, channel0);
0288       local_max_channel_1 = std::max(local_max_channel_1, channel1);
0289       local_min_module_index = std::min(local_min_module_index, module_index);
0290       local_max_module_index = std::max(local_max_module_index, module_index);
0291 
0292       assert(channel0 <= 0xFFFF);
0293       smem.cell_attribute_cache.channel0[i] = channel0;
0294       assert(channel1 <= 0xFFFF);
0295       smem.cell_attribute_cache.channel1[i] = channel1;
0296       smem.cell_attribute_cache.module_index[i] = module_index;
0297     }
0298 
0299     /*
0300      * Aggregate the boundary values to find the partition-wide minima and
0301      * maxima.
0302      */
0303     atomicMax(&max_channel0, local_max_channel_0);
0304     atomicMax(&max_channel1, local_max_channel_1);
0305     atomicMin(&min_module_index, local_min_module_index);
0306     atomicMax(&max_module_index, local_max_module_index);
0307 
0308     __syncthreads();
0309 
0310     /*
0311      * Use the CLZ (count leading zero) operation to find the number of
0312      * bits required to store the range of module indices, channel0, and
0313      * channel1 values. Then add these up to compute the total key size.
0314      */
0315     const auto module_index_clz = __clz(max_module_index - min_module_index);
0316     const unsigned int module_index_bits =
0317         CHAR_BIT * sizeof(std::decay_t<decltype(module_index_clz)>) -
0318         module_index_clz;
0319 
0320     const auto channel0_clz = __clz(max_channel0);
0321     const unsigned int channel0_bits =
0322         CHAR_BIT * sizeof(std::decay_t<decltype(channel0_clz)>) - channel0_clz;
0323 
0324     const auto channel1_clz = __clz(max_channel1);
0325     const unsigned int channel1_bits =
0326         CHAR_BIT * sizeof(std::decay_t<decltype(channel1_clz)>) - channel1_clz;
0327 
0328     const unsigned int total_bits =
0329         module_index_bits + channel0_bits + channel1_bits;
0330 
0331     /*
0332      * Accounting for a single sentinel bit, these bits should fit into
0333      * the chosen key type.
0334      *
0335      * Note that the added bit (which any valid cell cannot write to and
0336      * leaves at 0) serves as the sentinel bit; only invalid cells set
0337      * this to 1 and are thus automatically sorted past valid cells.
0338      */
0339     assert((total_bits + 1) <= (CHAR_BIT * sizeof(key_t)));
0340 
0341     /*
0342      * Now, extract the cell values from the shared memory cache and use
0343      * them to populate thread-local arrays.
0344      */
0345     key_t keys[PER_THREAD_MAX];
0346     value_t values[PER_THREAD_MAX];
0347 
0348     for (unsigned int i = 0; i < PER_THREAD_MAX; ++i) {
0349       unsigned int eff = i * blockDim.x + threadIdx.x;
0350       if (eff < partition_size) {
0351         const auto module_index =
0352             smem.cell_attribute_cache.module_index[eff] - min_module_index;
0353         const auto channel0 = smem.cell_attribute_cache.channel0[eff];
0354         const auto channel1 = smem.cell_attribute_cache.channel1[eff];
0355 
0356         keys[i] = static_cast<key_t>(module_index)
0357                       << (channel0_bits + channel1_bits) |
0358                   static_cast<key_t>(channel1) << (channel0_bits) |
0359                   static_cast<key_t>(channel0);
0360         values[i] = eff;
0361       } else {
0362         keys[i] = std::numeric_limits<key_t>::max();
0363         values[i] = std::numeric_limits<value_t>::max();
0364       }
0365     }
0366 
0367     __syncthreads();
0368 
0369     /*
0370      * Finally, perform the sorting on the keys and values using an
0371      * algorithm provided by CUB.
0372      */
0373     sort_t(smem.cub_sorting_tmp)
0374         .SortBlockedToStriped(keys, values, 0, total_bits + 1);
0375 
0376     /*
0377      * Now, with the values (original indices) sorted, we can populate a
0378      * new, sorted cell vector.
0379      */
0380     for (unsigned int i = 0; i < PER_THREAD_MAX; ++i) {
0381       unsigned int eff = i * blockDim.x + threadIdx.x;
0382       if (eff < partition_size) {
0383         new_cells_device.at(partition_start + eff) =
0384             cells_device.at(partition_start + values[i]);
0385         if (write_permutation) {
0386           permutation_map.at(partition_start + eff) =
0387               partition_start + values[i];
0388         }
0389       }
0390     }
0391   }
0392 }
0393 }  // namespace kernels
0394 }  // namespace traccc::cuda