Repository navigation
Description
Is this a duplicate?
- I confirmed there appear to be no duplicate issues for this request and that I agree to the Code of Conduct
Area
CUB
Is your feature request related to a problem? Please describe.
Vectorized global loads in CUB
This note lists every place where a CUB device algorithm loads global memory with vector instructions. For each one
it names the implementation, and it traces how the loads reach algorithms that are built on other algorithms. It is
meant as the starting point for centralizing the vectorized-load code.
Source: a static read of cub/ at upstream main (4984d75517), with default tuning policies and no user-provided
policies.
Terms used below:
- Striped: thread
tloads vectorst,t + N,t + 2N, and so on, whereNis the number of threads in the
block or warp. - Blocked: each thread loads consecutive vectors.
- Async copies to shared memory (LDGSTS, TMA) are a separate mechanism and are listed at the end.
Summary
- Six separate implementations. CUB has four striped implementations of vectorized register loads:
AgentReduce,
AgentFind, theDeviceTransformvectorized kernel, and theDeviceFor::ForEachCopywrapper. It has two blocked
ones:BlockLoad/WarpLoadVECTORIZE, andAgentHistogram'sBlockLoadof vector-typed items. A helper in
agent_batch_memcpy.cuhalso builds vectors out of 32-bit loads so that it can issue vector stores. - No two agree. They differ in vector type, eligibility check, how and where alignment is checked, partial-tile
handling, and cache-modifier support (see Where they differ). - VECTORIZE is rare. Only
DeviceTopKusesBLOCK_LOAD_VECTORIZEin its default tunings, and no tuning uses
WARP_LOAD_VECTORIZE. Every otherBlockLoaduser defaults to DIRECT, STRIPED, TRANSPOSE or WARP_TRANSPOSE. - Reuse spreads the striped loads.
DeviceSegmentedReducegets them throughAgentReduceandAgentWarpReduce.
DeviceFind::LowerBound/UpperBoundandDeviceCopy::Copyget them throughDeviceTransform. DeviceTransformmostly uses async copies. On sm_80 and newer it defaults to LDGSTS (sm_80–89) or TMA (sm_90+).
Its vectorized kernel is the default only below sm_80 and a fallback above it.
Implementations
| Implementation | Where | Pattern | Vector type | Compile-time gate | Alignment check | Partial tiles | Cache modifier |
|---|---|---|---|---|---|---|---|
AgentReduceImpl::ConsumeFullTile |
cub/agent/agent_reduce.cuh |
Striped over the block (AgentReduce) or warp (AgentWarpReduce) |
CubVector<InputT, VECTOR_LOAD_LENGTH> |
vec_size > 1, ITEMS_PER_THREAD % vec_size == 0, thrust::is_contiguous_iterator_v, primitive or trivially copyable, sizeof(InputT) <= 8 |
Device: is_sufficiently_aligned<alignof(VectorT)> on the range start |
Scalar | Policy modifier, through CacheModifiedInputIterator<VectorT> |
AgentFind::ConsumeTile |
cub/agent/agent_find.cuh |
Striped over the block | CubVector<InputT, VecSize> |
VecSize > 1, ItemsPerThread % VecSize == 0, thrust::is_contiguous_iterator_v (a raw pointer by then), trivially copyable |
Device, per tile: cuda::is_aligned(ptr, sizeof(VectorT)) |
Scalar | Policy modifier, through CacheModifiedInputIterator<VectorT> |
transform_kernel_vectorized |
cub/device/dispatch/kernels/kernel_transform.cuh |
Striped over the block | load_store_type<sizeof(T) * VecSize>: int8_t to int64_t, int4, 32-byte longlong4_32a, otherwise cuda::std::array<int, N> |
The policy picked the vectorized algorithm; power-of-two value sizes. Non-contiguous inputs are read one item at a time in the same layout | Host, in CanVectorize: cuda::is_aligned(ptr, sizeof(T) * vec_size) for the output and every contiguous input |
Prefetch code path | None |
__op_wrapper_vectorized_t |
cub/device/device_for.cuh |
Striped over the block | CubVector<T, 4> |
ForEachCopy and ForEachCopyN only, thrust::is_contiguous_iterator_v |
Host: is_sufficiently_aligned<alignof(vector_t)> on the range start |
The last vector index loops over the remainder | None |
InternalLoadDirectBlockedVectorized |
cub/block/block_load.cuh |
Blocked | CubVector<UnitWord<T>::DeviceWord, 4, 2 or 1>, sized from sizeof(T) * ItemsPerThread |
BlockLoad: a CacheModifiedInputIterator or a non-volatile contiguous iterator whose value type is T; anything else uses LoadDirectBlocked. WarpLoad: raw pointers only |
Device: is_sufficiently_aligned<alignof(vector_t)> on the block start |
LoadDirectBlocked |
The CacheModifiedInputIterator's modifier, otherwise LOAD_DEFAULT |
AgentHistogram::LoadFullAlignedTile |
cub/agent/agent_histogram.cuh |
Whatever the policy's BlockLoad algorithm produces; blocked with the default DIRECT |
CubVector<SampleT, VEC_SIZE> for one channel, CubVector<SampleT, NumChannels> per pixel otherwise |
Single channel with samples_per_thread % vec_size == 0, or several channels; native pointer samples |
Device: row start and row stride aligned to the vector or pixel | Scalar BlockLoad of samples |
Policy modifier, through CacheModifiedInputIterator<VecT> |
LoadVector, LoadVectorAndFunnelShiftR |
cub/agent/agent_batch_memcpy.cuh |
Thread-striped within each buffer | Builds uint2 or uint4 from uint32_t loads |
DeviceMemcpy::Batched byte copies |
None needed: it loads from the source rounded down to 4 B and funnel-shifts | Byte-wise copy | None |
Where they differ
- Vector type. There are three conventions.
CubVector<T, N>over the value type (reduce, find, for, histogram). This is a native CUDA vector only for the
built-in arithmetic types. For any otherTit's a plain struct aligned likeT, so its "vector" load is issued
as narrower loads.CubVectorover a machine word (BlockLoad). It uses the largest word that dividesT
(UnitWord<T>::DeviceWord), so it works for anyT.- Raw integer words (
DeviceTransform). This is the only implementation that can emit 32-byte loads.
- Eligibility. All of them gate on
thrust::is_contiguous_iterator_v, but the type gates differ:- reduce: primitive or trivially copyable, and at most 8 B
- find: trivially copyable
- for: none
BlockLoad: the value type must equalTand must not be volatile- transform: power-of-two value sizes
- Alignment.
- Device-side
alignofchecks: reduce,BlockLoad, histogram. - A device-side
sizeofcheck on every tile: find. - Host-side checks: transform (
sizeof(T) * vec_size) and for (alignof).
- Device-side
- Vector width. It comes from the policy (reduce, find, histogram, transform), is hard-coded to 4 (for), or is
derived from the bytes per thread (BlockLoad). - Partial tiles. Every implementation loads them scalar, except
DeviceFor, which finishes the remainder in the
last vector slot. - Cache modifiers. Reduce, find and histogram apply them through
CacheModifiedInputIterator<VectorT>, and
BlockLoadthroughThreadLoad<MODIFIER>. Transform and for have none. AgentReducesizing (from reading the code, not run).vec_sizeis clamped toITEMS_PER_THREAD, but
VectorTis built from the unclampedVECTOR_LOAD_LENGTH. The default sm_60+ policy uses a vector length of 4 and
clamp(64 / sizeof(AccumT), 1, 32)items per thread. A 17–32 B accumulator over a small input type therefore gives
2 or 3 items per thread, yet each thread loads a 4-item vector. Those vectors startvec_sizeitems apart, so most
lanes' loads are misaligned, and the loads write past the per-thread array.
Device algorithms
| Algorithm | Load path | Global loads | Conditions and notes |
|---|---|---|---|
DeviceReduce::Reduce, Sum, Min, Max, TransformReduce |
AgentReduce (run-to-run and not-guaranteed determinism) |
Striped vectors, block-wide | Contiguous input of a primitive or trivially copyable type of at most 8 B, aligned to the vector; full tiles only. vec_size is 4 in most tunings; the sm_100 tunings use 1, 2 or 4 depending on the type. |
DeviceReduce with gpu-to-gpu determinism |
Deterministic RFA kernel | Scalar | Loads transform_op(d_in[idx]) one item at a time. |
DeviceReduce::ArgMin, ArgMax, ArgMinMax, ArgMinLastMax |
AgentReduce over ArgIndexInputIterator |
Scalar | The index wrapper is not a contiguous iterator, so the vector path is never taken. |
DeviceReduce::ReduceByKey |
AgentReduceByKey, BlockLoad DIRECT or WARP_TRANSPOSE |
Scalar | Vectorizes only with a custom policy that selects BLOCK_LOAD_VECTORIZE. |
DeviceSegmentedReduce::Reduce, Sum, Min, Max |
AgentReduce for large segments; AgentWarpReduce with 32 threads for medium segments and 1 thread for small segments. Reuses the DeviceReduce policy |
Striped vectors, block- or warp-wide | Per segment: aligned segment start, full tiles only. With one thread per small segment the vectors are consecutive, so that case is effectively blocked. |
DeviceSegmentedReduce::ArgMin, ArgMax |
ArgIndexInputIterator |
Scalar | Same as DeviceReduce::ArgMin. |
DeviceFind::FindIf |
AgentFind |
Striped vectors, block-wide | vec_size 4 in all tunings; contiguous input, aligned full tiles. |
DeviceFind::LowerBound, UpperBound |
DeviceTransform over the needles, binary search into the haystack |
Inherits DeviceTransform |
Needles load like any DeviceTransform input. Haystack probes are scalar. |
DeviceFind::LowerBoundSortedValues, UpperBoundSortedValues |
Own kernel, striped CacheModifiedInputIterator loads into shared memory |
Scalar | |
DeviceTransform::Transform |
One transform_kernel; the policy picks the algorithm per arch |
Async copies on sm_80+, striped vectors below sm_80 and as fallback | sm_90+ uses TMA bulk copies (ublkcp) and sm_80–89 uses LDGSTS. The vectorized kernel is the fallback when shared memory runs out, tiles lose alignment, there are no inputs, any input is non-contiguous, or a tuning finds the vector kernel faster (for example sm_90 with outputs smaller than 4 B and several inputs). It needs power-of-two value sizes, aligned contiguous inputs and output, and full tiles; otherwise it uses prefetch, which is scalar. zip_iterator and zip_function inputs are split into separate streams first. |
DeviceTransform::TransformIf |
Prefetch kernel | Scalar | A predicate makes the output non-dense, which forces prefetch on every arch. |
DeviceTransform::TransformStableArgumentAddresses |
Prefetch kernel | Scalar | Stable argument addresses force prefetch. |
DeviceTransform::Fill, Generate |
Transform kernel without inputs | No loads | Only the stores can be vectorized. |
DeviceFor::ForEachCopy, ForEachCopyN |
AgentFor over CubVector<T, 4> items |
Striped vectors, block-wide | Contiguous input aligned to alignof(CubVector<T, 4>); the last vector index finishes the tail one item at a time. Otherwise scalar. |
DeviceFor::ForEach, ForEachN |
AgentFor with the plain wrapper |
Scalar | The functor receives a reference, so CUB cannot load copies into vectors. |
DeviceFor::Bulk, ForEachInExtents, ForEachInLayout |
AgentFor over indices |
No loads | Any loads happen inside the user functor. |
DeviceCopy::Copy (mdspan) |
DeviceTransform if both mdspans are exhaustive with the same strides, otherwise DeviceFor::ForEachInExtents |
Inherits DeviceTransform |
The ForEachInExtents fallback is scalar. |
DeviceCopy::Batched |
agent_batch_memcpy, item-wise |
Scalar | |
DeviceMemcpy::Batched |
agent_batch_memcpy, byte copy |
Scalar loads, vector stores | Loads 4-byte aligned uint32_t words and funnel-shifts them into a uint4; only the 16 B stores are vectorized. |
DeviceHistogram::HistogramEven, HistogramRange |
AgentHistogram, BlockLoad of CubVector<SampleT, VEC_SIZE> items |
Blocked vectors | The default, sm_90 and sm_100 tunings use vec_size 4 with BLOCK_LOAD_DIRECT. The sm_107 tunings vary: vec_size 1 (scalar), 4 with STRIPED (striped vectors), or 2 with WARP_TRANSPOSE (warp-striped vectors). Needs pointer samples, rows aligned to the vector, and full tiles. |
DeviceHistogram::MultiHistogramEven, MultiHistogramRange |
BlockLoad of one CubVector<SampleT, NumChannels> per pixel |
Blocked vectors, one per pixel | A native vector only for 2 or 4 channels; rows must be aligned to the pixel. |
DeviceTopK::MinKeys, MaxKeys, MinPairs, MaxPairs |
AgentTopK, BlockLoad<KeyT> with the policy's algorithm |
Blocked vectors (keys) | BLOCK_LOAD_VECTORIZE on every arch except the sm_107 keys-only float and double tunings with 64-bit offsets, which use DIRECT. Full tiles of aligned pointers or CacheModifiedInputIterators take the vector path. Values are gathered one at a time. |
DeviceBatchedTopK::MinKeys, MaxKeys, MinPairs, MaxPairs |
Own worker agents, a cluster agent on sm_90+, and an internal DeviceScan over a transform_iterator |
Scalar, or TMA in the cluster backend | Workers use BLOCK_LOAD_WARP_TRANSPOSE. The cluster backend copies segments with cp.async.bulk. Does not call DeviceTopK. |
DeviceScan::InclusiveSum, ExclusiveSum, InclusiveScan, ExclusiveScan, InclusiveScanInit |
Lookback AgentScan, or the warpspeed lookahead kernel |
Scalar, or TMA in lookahead | Lookahead runs on sm_100+ for contiguous, trivially copyable input and output when its stages fit in shared memory. It is off on sm_120 before nvcc 13.4. Lookback uses BlockLoad DIRECT or WARP_TRANSPOSE. |
DeviceScan::*ByKey |
AgentScanByKey, BlockLoad DIRECT or WARP_TRANSPOSE |
Scalar | |
DeviceSegmentedScan (all) |
Segmented scan kernel, BlockLoad WARP_TRANSPOSE |
Scalar | |
DeviceSelect::If, Flagged, FlaggedIf, Unique; DevicePartition::Flagged, two-way If |
AgentSelectIf, BlockLoad DIRECT or WARP_TRANSPOSE |
Scalar | |
DeviceSelect::UniqueByKey |
AgentUniqueByKey, BlockLoad DIRECT or WARP_TRANSPOSE |
Scalar | |
DevicePartition::If (three-way) |
AgentThreeWayPartition, BlockLoad DIRECT or WARP_TRANSPOSE |
Scalar | |
DeviceRunLengthEncode::Encode |
Reduce-by-key lookback, or the warpspeed lookahead kernel | Scalar, or TMA in lookahead | Lookahead runs on sm_100 to sm_10x with PTX ISA 9.2+, contiguous inputs and outputs, and trivially copyable keys. |
DeviceRunLengthEncode::NonTrivialRuns |
AgentRle, BlockLoad DIRECT or WARP_TRANSPOSE |
Scalar | |
DeviceAdjacentDifference (all) |
AgentAdjacentDifference, BlockLoad WARP_TRANSPOSE |
Scalar | |
DeviceRadixSort (all) |
Onesweep (LoadDirectWarpStriped), histogram and upsweep (LoadDirectStriped), downsweep (BlockLoad DIRECT, TRANSPOSE or WARP_TRANSPOSE) |
Scalar | |
DeviceSegmentedRadixSort (all) |
AgentSegmentedRadixSort, BlockLoad TRANSPOSE or WARP_TRANSPOSE |
Scalar | |
DeviceSegmentedSort (all) |
Three-way partition over segment indices, segmented radix sort, and WarpLoad DIRECT or TRANSPOSE for the sub-warp merge sort |
Scalar | |
DeviceMergeSort (all) |
BlockLoad DIRECT or WARP_TRANSPOSE, then merging with gmem_to_reg |
Scalar | |
DeviceMerge::MergeKeys, MergePairs |
agent_merge: gmem_to_reg, or BlockLoadToShared |
Scalar, or async copies | BlockLoadToShared uses 16 B LDGSTS on sm_80–89 and cp.async.bulk on sm_90+. It needs contiguous, trivially relocatable keys or values whose size equals their alignment; the sm_80 and sm_90 tunings enable it only for some key and value sizes. |
DeviceSetOperations (all) |
agent_set_operations, gmem_to_reg |
Scalar |
Propagation through other algorithms
| Algorithm | Goes through | Load pattern it inherits |
|---|---|---|
DeviceSegmentedReduce::Reduce, Sum, Min, Max |
AgentReduce and AgentWarpReduce with the DeviceReduce policy |
Striped vectors |
DeviceFind::LowerBound, UpperBound |
DeviceTransform |
Whatever DeviceTransform picks for the arch |
DeviceCopy::Copy |
DeviceTransform (exhaustive, same strides) or DeviceFor::ForEachInExtents |
DeviceTransform's choice, or scalar |
DeviceReduce::ArgMin, ArgMax, ArgMinMax, ArgMinLastMax; DeviceSegmentedReduce::ArgMin, ArgMax |
AgentReduce through ArgIndexInputIterator |
Scalar, because the wrapper hides contiguity |
DeviceRunLengthEncode::Encode |
Reduce-by-key agent, or the lookahead kernel | Scalar, or TMA |
DeviceSegmentedSort |
Three-way partition and segmented radix sort agents | Scalar |
DeviceBatchedTopK |
DeviceScan over a transform_iterator |
Scalar (lookback scan) |
DevicePartition::Flagged, If; DeviceSelect::Unique |
select_if dispatch |
Scalar |
DeviceMemcpy::Batched, DeviceCopy::Batched |
Shared agent_batch_memcpy |
Vector stores only, for the memcpy flavor |
No device algorithm reaches DeviceTopK, so blocked VECTORIZE loads do not propagate.
Async copies to shared memory
These copy global memory straight into shared memory, so they're a different facility from register vector loads.
BlockLoadToShared is the only shared implementation.
| Mechanism | Where | Used by | Arch |
|---|---|---|---|
BlockLoadToShared |
cub/block/block_load_to_shared.cuh |
DeviceMerge, and the DeviceBatchedTopK cluster agent. The agent also inlines the internals for its pipelined path |
16 B LDGSTS on sm_80–89, cp.async.bulk on sm_90+ |
transform_kernel_ldgsts |
cub/device/dispatch/kernels/kernel_transform.cuh |
DeviceTransform |
sm_80–89 |
transform_kernel_ublkcp |
cub/device/dispatch/kernels/kernel_transform.cuh |
DeviceTransform |
sm_90+ |
Warpspeed squad load_store.cuh |
cub/detail/warpspeed/squad/load_store.cuh |
DeviceScan lookahead |
sm_100+ |
| RLE lookahead kernel | cub/device/dispatch/kernels/kernel_rle_encode_lookahead.cuh |
DeviceRunLengthEncode::Encode lookahead |
sm_100 to sm_10x |
Describe the solution you'd like
We should unify all loads to registers into a single facility probably under BlockLoad.
There should be two top level entry points
BlockLoadDirectVectorizedBlockLoadStripedVectorized
Every algorithm in CUB that utilizes vectorized loads should go through one of the two
Describe alternatives you've considered
No response
Additional context
No response
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsTodo
Activity