Skip to content

[FEA]: Unify Infrastructure to load into registers #12419

Open

Description

@miscco

Is this a duplicate?

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 t loads vectors t, t + N, t + 2N, and so on, where N is 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, the DeviceTransform vectorized kernel, and the DeviceFor::ForEachCopy wrapper. It has two blocked
    ones: BlockLoad/WarpLoad VECTORIZE, and AgentHistogram's BlockLoad of vector-typed items. A helper in
    agent_batch_memcpy.cuh also 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 DeviceTopK uses BLOCK_LOAD_VECTORIZE in its default tunings, and no tuning uses
    WARP_LOAD_VECTORIZE. Every other BlockLoad user defaults to DIRECT, STRIPED, TRANSPOSE or WARP_TRANSPOSE.
  • Reuse spreads the striped loads. DeviceSegmentedReduce gets them through AgentReduce and AgentWarpReduce.
    DeviceFind::LowerBound/UpperBound and DeviceCopy::Copy get them through DeviceTransform.
  • DeviceTransform mostly 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 other T it's a plain struct aligned like T, so its "vector" load is issued
      as narrower loads.
    • CubVector over a machine word (BlockLoad). It uses the largest word that divides T
      (UnitWord<T>::DeviceWord), so it works for any T.
    • 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 equal T and must not be volatile
    • transform: power-of-two value sizes
  • Alignment.
    • Device-side alignof checks: reduce, BlockLoad, histogram.
    • A device-side sizeof check on every tile: find.
    • Host-side checks: transform (sizeof(T) * vec_size) and for (alignof).
  • 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
    BlockLoad through ThreadLoad<MODIFIER>. Transform and for have none.
  • AgentReduce sizing (from reading the code, not run). vec_size is clamped to ITEMS_PER_THREAD, but
    VectorT is built from the unclamped VECTOR_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 start vec_size items 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

  • BlockLoadDirectVectorized
  • BlockLoadStripedVectorized

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    • Status
      Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions