LeetLLM
My PlanLearnGlossaryTracksPracticeBlog
LeetLLM

Your go-to resource for mastering AI & LLM systems.

Product

  • Learn
  • Glossary
  • Tracks
  • Practice
  • Blog
  • RSS

Legal

  • Terms of Service
  • Privacy Policy

© 2026 LeetLLM. All rights reserved.

All Topics
Your Progress
0%

0 of 196 articles completed

🛠️Computing Foundations0/9
Git, Shell, Linux for AIDocker for Reproducible AIPython for AI EngineeringNumPy and Tensor ShapesCUDA for ML TrainingMPS & Metal for ML on MacData Structures for AISQL and Data ModelingAlgorithms for ML Engineers
📊Math & Statistics0/9
Gradients and BackpropVectors, Matrices & TensorsLinear Algebra for MLAdam, Momentum, SchedulersProbability for Machine LearningBayesian Inference from EvidenceStatistics and UncertaintyDistributions and SamplingHypothesis Tests, Intervals, and pass@k
📚Preparation & Prerequisites0/13
Neural Networks from ScratchCNNs from ScratchTraining & BackpropagationSoftmax, Cross-Entropy & OptimizationRNNs, LSTMs, GRUs, and Sequence ModelingAutoencoders and VAEsThe Transformer Architecture End-to-EndLanguage Modeling & Next TokensFrom GPT to Modern LLMsPrompt Engineering FundamentalsCalling LLM APIs in ProductionFirst AI App End-to-EndThe LLM Lifecycle
🧮ML Algorithms & Evaluation0/15
Linear Regression from ScratchLogistic Regression and MetricsDecision Trees, Forests, and BoostingReinforcement Learning BasicsValidation and LeakageStatistical Learning and GeneralizationConformal Prediction and UncertaintyClustering and PCAProbabilistic Generative ModelsCore Retrieval AlgorithmsDecoding AlgorithmsExperiment Design and A/B TestingCausal Inference and CounterfactualsPyTorch Training LoopsDataset Pipelines and Data Quality
📦Production ML Systems0/6
Feature Engineering for Production MLBatch and Streaming Feature PipelinesGradient Boosted Trees in ProductionRanking and Recommendation SystemsForecasting and Anomaly DetectionMonitoring Predictive Models
🧪Core LLM Foundations0/8
The Bitter Lesson & ComputeBPE, WordPiece, and SentencePieceStatic to Contextual EmbeddingsPerplexity & Model EvaluationFile Ingestion for AIChunking StrategiesLLM Benchmarks & LimitationsInstruction Tuning & Chat Templates
🧰Applied LLM Engineering0/25
Dimensionality Reduction for EmbeddingsCoT, ToT & Self-Consistency PromptingFunction Calling & Tool UseMCP & Tool Protocol StandardsContext EngineeringPrompt Injection DefenseResponsible AI GovernancePrivacy-Preserving Machine LearningData Labeling and Human FeedbackEvaluating AI AgentsProduction RAG PipelinesHybrid Search: Dense + SparseReranking and Cross-Encoders for RAGRAG Evaluation for Reliable AnswersLLM-as-a-Judge EvaluationBias & Fairness in LLMsHallucination Detection & MitigationLLM Observability & MonitoringExperiment Tracking with MLflow and W&BPrompt Optimization with DSPyModel Versioning & DeploymentSemantic Caching & Cost OptimizationLLM Cost Engineering & Token EconomicsModel Gateways, Routing, and FallbacksDesign an Automated Support Agent
🎓Portfolio Capstones0/9
Capstone: Delivery ETA PredictionCapstone: Product RankingCapstone: Demand ForecastingCapstone: Image Damage ClassifierCapstone: Production ML PipelineCapstone: Document QACapstone: Eval DashboardCapstone: Fine-Tuned ClassifierCapstone: Reproducible ML Study
🧠Transformer Deep Dives0/8
Sentence Embeddings & Contrastive LossEmbedding Similarity & QuantizationScaled Dot-Product AttentionVision Transformers and Image EncodersPositional Encoding: RoPE & ALiBiLayer Normalization: Pre-LN vs Post-LNMechanistic InterpretabilityDecoding Strategies: Greedy to Nucleus
🧬Advanced Training & Adaptation0/17
Scaling Laws & Compute-Optimal TrainingPre-training Data at ScaleBuild GPT from Scratch LabJAX for PyTorch ResearchersContinued Pretraining for Domain ShiftSynthetic Data PipelinesSupervised Fine-Tuning PipelineMixed Precision TrainingDistributed Training: FSDP & ZeROLoRA & Parameter-Efficient TuningTraining Run OperationsReward Modeling from Preference DataRLHF & DPO AlignmentConstitutional AI & Red TeamingRLVR & Verifiable RewardsKnowledge Distillation for LLMsModel Merging and Weight Interpolation
🤖Advanced Agents & Retrieval0/16
Vector DB Internals: HNSW & IVFAdvanced RAG: HyDE & Self-RAGGraphRAG & Knowledge GraphsRAG Security & Access ControlStructured Output GenerationReAct & Plan-and-ExecuteGuardrails & Safety FiltersCode Generation & SandboxingComputer-Use / GUI / Browser AgentsHuman-in-the-Loop Agent ArchitectureAI Coding Workflow with AgentsAgent Memory & PersistenceAgent Failure & RecoveryRecursive Language Models (RLM)Multi-Agent OrchestrationCapstone: Production Agent
⚡Inference & Production Scale0/31
Inference: TTFT, TPS & KV CacheMulti-Query & Grouped-Query AttentionKV Cache & PagedAttentionPrefix Caching and Prompt CachingFlashAttention & Memory EfficiencyContinuous Batching & SchedulingScaling LLM InferenceModel Parallelism for LLM InferenceAccelerator Architecture Field GuideCorrect GPU Profiling and BenchmarkingGPU Kernel Performance Engineering LabGPU Parallel Primitives LabGPU Compilers and Kernel LanguagesGEMM From Naive CUDA to Tensor CoresLow-Precision GPU ProgrammingGPU Runtime Execution LabAI-Generated Kernel EngineeringModel Quantization: GPTQ, AWQ & GGUFLocal LLM DeploymentSLM Specialization & Edge DeploymentSpeculative DecodingLong Context Window ManagementMixture of Experts ArchitectureMamba & State Space ModelsReasoning & Test-Time ComputeAdvanced MLOps & DevOps for AIGPU Serving & AutoscalingGPU Failure OperationsDistributed Inference Data PlaneServing Benchmark EngineeringA/B Testing for LLMs
🏗️System Design Capstones0/9
Content Moderation SystemCode Completion SystemMulti-Tenant LLM PlatformLLM-Powered Search EngineVision-Language Models & CLIPMultimodal LLM ArchitectureDiffusion Models: Images & TextReal-Time Voice AI AgentReasoning Agent System Design
🎤AI Lab Interviewing0/4
AI Lab Coding Interview: Python SystemsAI Lab System Design InterviewAI Lab Behavioral InterviewAI Lab Technical Presentation
🔬Project Deep Dives0/17
Deep Dive - vLLMDeep Dive - SkyRLDeep Dive - FlashAttentionDeep Dive - FlashInferDeep Dive - DeepGEMMDeep Dive - NCCLDeep Dive - MegatronDeep Dive - DeepSpeedDeep Dive - RayDeep Dive - MLflowDeep Dive - PyTorchDeep Dive - TransformersDeep Dive - SGLangDeep Dive - slimeDeep Dive - DeepEPDeep Dive - TinkerDeep Dive - Light-PEFT
Back to Topics
LearnInference & Production ScaleGPU Parallel Primitives Lab
🚀HardInference Optimization

GPU Parallel Primitives Lab

Build reduction, prefix scan, arg reduction, and stable online softmax from one tensor, then decide when a tuned CUB primitive should replace custom CUDA.

22 min read
Learning path
Step 147 of 196 in the full curriculum
GPU Kernel Performance Engineering LabGPU Compilers and Kernel Languages

Personalize this lesson

Adapt explanations and teaching visuals to your background and preferred voice.

Eight numbers sit in GPU memory: 3, -1, 4, 1, 5, -9, 2, 6. A transpose can move every number to a new address. It can't answer three common questions: What is their total? How many values came before each position? Which value is largest, and where did it occur?

Those questions need parallel primitives, small algorithms that many larger kernels reuse. Reduction turns many inputs into fewer aggregates. Prefix scan returns an aggregate at every boundary. Arg reduction carries a value and its location together. Stable softmax combines reductions with exponentials without overflowing.

The GPU Kernel Performance Engineering Lab established coalescing, shared memory, barriers, exact checks, and event timing through one transpose. Keep those contracts. Here one stable tensor exposes new dependency shapes instead of a new memory layout.

Work eight values by hand

Start with sum. A serial loop visits all eight values in order and ends at 11. A balanced tree reaches the same integer through three parallel levels:

reduction-tree.txt
1input: [ 3, -1, 4, 1, 5, -9, 2, 6] 2level1: [ 2, 5, -4, 8 ] 3level2: [ 7, 4 ] 4output: [11]

Eight leaves require seven additions either way. Tree depth is only log28=3log_2 8=3log2​8=3, provided enough workers can execute each level together. That shrinking output is reduction's signature.

Prefix scan keeps every boundary instead. For addition, an inclusive scan includes current value at index iii. An exclusive scan stops before it:

Index iiiInput xix_ixi​Exclusive prefixInclusive prefixHow to check row
0303exclusive starts at additive identity 0
1-1323+(−1)=23+(-1)=23+(−1)=2
2426previous inclusive becomes next exclusive
31676+1=76+1=76+1=7
45712prefix retains position 4 boundary
5-9123negative input lowers running total
6235output count still equals input count
76511last inclusive equals full reduction

The relation is exact for every valid iii:

inclusive⁡[i]=exclusive⁡[i]+xi\operatorname{inclusive}[i]=\operatorname{exclusive}[i]+x_iinclusive[i]=exclusive[i]+xi​

Argmax needs another payload. Maximum value is 6 and its index is 7, so output is pair (6, 7). If tensor contained another 6, operator would need a tie rule such as "smaller index wins." Value, index, and tie rule form one reduction state.

Two aligned views of tensor [3, -1, 4, 1, 5, -9, 2, 6]. Reduction groups adjacent values into pair sums [2, 5, -4, 8], then [7, 4], then scalar 11, so only one final aggregate remains. Prefix scan keeps one output per input position: exclusive prefixes [0, 3, 2, 6, 7, 12, 3, 5] and inclusive prefixes [3, 2, 6, 7, 12, 3, 5, 11]. Horizontal position preserves each prefix boundary.
Read both panels from same input row. Reduction discards position after combining adjacent groups and returns scalar 11. Scan preserves each boundary, so output index still tells which prefix was summarized.

The runnable model below checks all four outputs and constructs stable softmax state used later. It uses Python's math module so mechanism stays visible.

primitive-model.py
1from math import exp, inf, isclose 2 3x = [3.0, -1.0, 4.0, 1.0, 5.0, -9.0, 2.0, 6.0] 4 5inclusive = [] 6running = 0.0 7for value in x: 8 running += value 9 inclusive.append(running) 10 11exclusive = [0.0] + inclusive[:-1] 12argmax = max(enumerate(x), key=lambda pair: (pair[1], -pair[0])) 13 14def merge_softmax_states(left, right): 15 left_max, left_denom = left 16 right_max, right_denom = right 17 if left_denom == 0.0: 18 return right 19 if right_denom == 0.0: 20 return left 21 merged_max = max(left_max, right_max) 22 merged_denom = ( 23 left_denom * exp(left_max - merged_max) 24 + right_denom * exp(right_max - merged_max) 25 ) 26 return merged_max, merged_denom 27 28state = (-inf, 0.0) 29for value in x: 30 state = merge_softmax_states(state, (value, 1.0)) 31 32maximum, denominator = state 33probabilities = [exp(value - maximum) / denominator for value in x] 34 35assert inclusive == [3.0, 2.0, 6.0, 7.0, 12.0, 3.0, 5.0, 11.0] 36assert exclusive == [0.0, 3.0, 2.0, 6.0, 7.0, 12.0, 3.0, 5.0] 37assert argmax == (7, 6.0) 38assert merge_softmax_states((-inf, 0.0), (-inf, 0.0)) == (-inf, 0.0) 39assert isclose(sum(probabilities), 1.0, rel_tol=0.0, abs_tol=1e-15) 40 41print(f"sum={inclusive[-1]:.0f}") 42print(f"exclusive={[int(value) for value in exclusive]}") 43print(f"inclusive={[int(value) for value in inclusive]}") 44print(f"argmax=(value={argmax[1]:.0f}, index={argmax[0]})") 45print(f"online_state=(max={maximum:.0f}, denominator={denominator:.6f})") 46print(f"softmax_sum={sum(probabilities):.12f}")
Output
1sum=11 2exclusive=[0, 3, 2, 6, 7, 12, 3, 5] 3inclusive=[3, 2, 6, 7, 12, 3, 5, 11] 4argmax=(value=6, index=7) 5online_state=(max=6, denominator=1.578968) 6softmax_sum=1.000000000000

Move sum into one warp

A naive GPU baseline assigns all eight additions, or all nnn additions, to one thread. That gives a correct, easy-to-check answer but preserves serial dependency depth and leaves rest of GPU idle. Hierarchical version begins by giving each lane a local sum.

A warp is CUDA's group of 32 lanes that executes instructions together. __shfl_down_sync lets one participating lane read a register from a higher-numbered lane without routing value through shared memory. Five offsets, 16, 8, 4, 2, 1, reduce 32 lane-local values to lane 0.[1]Reference 1CUDA Programming Guide.https://docs.nvidia.com/cuda/cuda-programming-guide/

The lab uses this helper:

primitives_lab.cu
1__device__ long long warp_sum(long long value) { 2 constexpr unsigned kFullMask = 0xffffffffu; 3 for (int offset = warpSize / 2; offset > 0; offset /= 2) { 4 value += __shfl_down_sync(kFullMask, value, offset); 5 } 6 return value; 7}

Full mask is correct only because every lane reaches each shuffle. Tail lanes load additive identity 0 instead of exiting. CUDA defines a shuffle read from an inactive source lane as undefined, and participating lanes named by mask must execute same intrinsic with same mask.[1]Reference 1CUDA Programming Guide.https://docs.nvidia.com/cuda/cuda-programming-guide/

That contract rules out a tempting edge shortcut:

broken-tail.cu
1if (index < n) { 2 value += __shfl_down_sync(0xffffffffu, value, 16); 3}

On partial warp, mask names lanes that skipped call. Repair by keeping all lanes active and substituting identity for missing input, or compute one valid mask with __ballot_sync before lanes diverge and ignore shuffle results whose source lane isn't valid. Calling __activemask() after divergence can give participating lanes different masks. Mask says who participates. It doesn't turn invalid source lane into zero.

Only 20 values remain in final warp. Why is full mask safe in supplied lab but unsafe when lanes 20 through 31 return early?

Answer

Lab keeps all 32 lanes at every shuffle and gives lanes 20 through 31 value 0, so every named lane participates. Early return violates mask contract because full mask still names absent lanes. Use uniform participation with identity values or a correctly formed active mask plus valid-source guards.

Join warps inside one block

A 256-thread block contains eight warps. Each warp first reduces registers. Lane 0 of every warp writes one partial to eight-element shared array. One block-wide barrier then makes those writes visible before warp 0 reduces eight partials.

block-reduction-core.cu
1sum = warp_sum(sum); 2if (lane == 0) { 3 warp_sums[warp] = sum; 4} 5__syncthreads(); 6 7if (warp == 0) { 8 long long block_sum = lane < warp_count ? warp_sums[lane] : 0; 9 block_sum = warp_sum(block_sum); 10 if (lane == 0) { 11 block_sums[blockIdx.x] = block_sum; 12 } 13}

__syncthreads() waits for every thread in block and makes earlier shared and global memory accesses visible to that block. Placing it under a condition that differs across block can hang or produce unintended behavior.[1]Reference 1CUDA Programming Guide.https://docs.nvidia.com/cuda/cuda-programming-guide/

Older teaching reductions begin with interleaved active threads, then replace divergent modulo branch with contiguous active lanes. They also give each thread several elements before shared tree begins. Those steps reduce branch divergence, address work, and synchronization while keeping O(n)O(n)O(n) total work.[2]Reference 2Optimizing Parallel Reduction in CUDAhttps://developer.download.nvidia.com/compute/cuda/1.1-Beta/x86_website/projects/reduction/doc/reduction.pdf Current NVIDIA samples still include a two-stage shared-memory reduction as runnable reference.[3]Reference 3CUDA Sampleshttps://github.com/NVIDIA/cuda-samples

One block can't synchronize with every other ordinary block inside kernel. Hierarchical reduction therefore writes one partial per block, then launches another reduction pass over partial array. Each launch on same stream forms device-wide ordering boundary:

Diagram showing 1. Coalesced input chunks, 2. Warp register sums, 3. Block partials, and 4. More than one?.
1. Coalesced input chunks, 2. Warp register sums, 3. Block partials, and 4. More than one?.

Two practical details matter:

  • Each thread accumulates two or more inputs before warp shuffle, balancing parallel depth against useful work per thread.
  • Accepted operator needs associative grouping. Commutativity is optional only for algorithms that preserve operand order, such as scan. CUB DeviceReduce doesn't support non-commutative reduction operators.[4]Reference 4cub::DeviceReducehttps://nvidia.github.io/cccl/unstable/cub/api/structcub_1_1DeviceReduce.html

For mathematical addition, (a+b)+c equals a+(b+c). Floating-point addition only approximates that law because each addition rounds. Tree can differ from serial sum without either kernel containing race.

Make floating-point reductions deliberate

Suppose float32 values include 100000000.0, 1.0, and -100000000.0. Serial grouping can lose 1 before cancellation, while another tree preserves it. Integer fixture avoided that ambiguity so CUDA lab could demand exact equality.

Use numerical contract suited to workload:

NeedReduction state or methodCheck
exact counts or offsetsinteger accumulator wide enough for totalexact equality and overflow boundary
ordinary float sumpairwise tree, often wider accumulatorcompare with higher-precision reference under stated tolerance
reproducible same-device runsfixed tree and fixed launch policyrepeated bitwise check on same build and GPU
cross-device reproducibilitylibrary guarantee or explicit portable treeverify documented guarantee and test every target
cancellation-sensitive sumcompensated or higher-precision schemeadversarial signed fixtures and error distribution

Reproducibility and accuracy answer different questions. A fixed wrong order can reproduce. A more accurate algorithm can still choose different trees on different architectures.

Current CUB DeviceReduce documentation states default run_to_run determinism. Same input, build, launch configuration, tuning, CCCL version, and GPU select same fixed tree. Another architecture, policy, or toolkit release can change floating-point combining order.[4]Reference 4cub::DeviceReducehttps://nvidia.github.io/cccl/unstable/cub/api/structcub_1_1DeviceReduce.html Pin CCCL version and verify installed API when reproducibility enters product contract.

Two float sum kernels pass abs(error) < 1e-4. One returns same bits on every run, while other has lower error against FP64 but changes last bit across runs. Which one is correct?

Answer

Both satisfy stated accuracy tolerance. First also satisfies same-run reproducibility; second doesn't. If product needs both properties, neither single observation is enough. Write separate accuracy and determinism gates, then choose operator, accumulator, and library policy that pass both.

Keep every prefix with scan

Reduction's tree can discard intermediate totals. Scan must send block prefix into every later output. A work-efficient block scan has two phases:

  1. Upsweep: combine leaves into block total.
  2. Downsweep: replace root with identity, then propagate left-prefix information back to leaves.

Supplied Blelloch-style kernel handles 512 items per 256-thread block. Every thread loads two values into shared memory, participates in both tree phases, and writes exclusive output. It also records block total before root becomes 0.

Arrays longer than one block need three layers:

LayerInputOutputInvariant
local block scanoriginal valueslocal exclusive prefixes plus one block totaleach output excludes current value
recursive total scanblock totalsexclusive offset for every blockblock 0 offset is identity
uniform addlocal prefixes plus block offsetdevice-wide exclusive prefixesevery element receives totals of earlier blocks

If first output of block 1 is 0 instead of sum of block 0, local tree probably passed and uniform add failed. That symptom points at propagation, not arithmetic inside block.

Blocks of four scan [3,-1,4,1 | 5,-9,2,6]. Local exclusive outputs are [0,3,2,6 | 0,5,-4,-2]. What offset repairs second block?

Answer

First block total is 7, so scan of block totals gives offsets [0,7]. Add 7 to every local output in second block, producing [7,12,3,5]. Combined output is [0,3,2,6,7,12,3,5].

Multi-pass scan is easy to inspect but moves block metadata in extra kernels. Merrill and Garland's decoupled look-back scan lets block compute local scan, publish aggregate and status, then look backward through earlier block states until global prefix is known. Small redundant work overlaps global prefix propagation with local work. Their report describes about nnn input reads plus nnn output writes, matching sequential scan's asymptotic data movement.[5]Reference 5Single-pass Parallel Prefix Scan with Decoupled Look-backhttps://research.nvidia.com/sites/default/files/pubs/2016-03_Single-pass-Parallel-Prefix/nvr-2016-002.pdf

CUB has used decoupled look-back for device-wide scan since early releases. DeviceScan docs expose inclusive and exclusive forms, in-place support, and temporary-storage contract. Classic stream overloads warn that pseudo-associative results may vary across runs. Newer execution-environment controls depend on version, type, and operator: current unstable docs default to not_guaranteed and permit run_to_run only for supported integral operators and floating-point cuda::std::plus.[6]Reference 6cub::DeviceScanhttps://nvidia.github.io/cccl/unstable/cub/api/structcub_1_1DeviceScan.html Check pinned CCCL docs or headers before turning a determinism mode into a release gate.

Decoupled look-back isn't invitation to improvise spin protocol. Published states, memory ordering, forward progress, fallback path, and architecture behavior all need proof. Use library for standalone device scan unless fusion or data layout gives custom kernel clear reason to exist.

Treat stable softmax as reduction state

Softmax turns logits xix_ixi​ into probabilities:

pi=exi∑jexjp_i=\frac{e^{x_i}}{\sum_j e^{x_j}}pi​=∑j​exj​exi​​

Direct exponentiation can overflow for large positive logit. Stable form subtracts row maximum mmm:

m=max⁡jxj,pi=exi−m∑jexj−mm=\max_j x_j, \qquad p_i=\frac{e^{x_i-m}}{\sum_j e^{x_j-m}}m=jmax​xj​,pi​=∑j​exj​−mexi​−m​

Subtracting same constant from all logits doesn't change probabilities, while largest exponent becomes e0=1e^0=1e0=1.

Safe implementation can read logits once for maximum, again for denominator, then again to write normalized outputs. Online normalizer combines first two reductions. State (m, d) represents partition maximum mmm and shifted exponential sum ddd. Merge two partitions aaa and bbb by:

m=max⁡(ma,mb)m=\max(m_a,m_b)m=max(ma​,mb​) d=daema−m+dbemb−md=d_a e^{m_a-m}+d_b e^{m_b-m}d=da​ema​−m+db​emb​−m

This operator is associative over finite logits in exact real arithmetic, so same warp and block reduction machinery can merge chunk states. Empty partition uses identity (-inf, 0), but merge must return other operand before evaluating formula. Otherwise two empty partitions evaluate -inf - -inf and poison denominator with NaN. Python model's two early returns enforce identity law. Milakov and Gimelshein derive online update, prove invariant, and reduce softmax memory accesses by combining maximum and denominator passes.[7]Reference 7Online normalizer calculation for softmax.https://arxiv.org/abs/1805.02867

Attention masks expose same edge. A masked logit contributes empty identity, not singleton state (-inf, 1). Fully masked row has no mathematical softmax distribution, so kernel contract must reject it or define explicit fallback before normalization. NaN and positive-infinity logits also need declared input contract because subtracting maximum doesn't make inf - inf defined.

Running tensor ends with m=6 and denominator near 1.578968. Python model then revisits logits only to write eight probabilities. Fusing next consumer can sometimes avoid writing full probability vector, but correctness contract expands to fused operation.

Online doesn't mean one sequential thread. Each lane can summarize local chunk into (m,d), then parallel tree merges those states. Intermediate maximum can rise, so old denominator must be rescaled before new contribution arrives.

Left chunk state is (5, 2) and right state is (6, 1). What merged denominator is measured relative to maximum 6?

Answer

Merged maximum is 6. Left denominator rescales by e5−6=e−1e^{5-6}=e^{-1}e5−6=e−1, while right stays at scale 1. Merged denominator is 2e−1+12e^{-1}+12e−1+1, about 1.7358. Adding 2 and 1 directly would mix different exponential scales.

Carry segments and indices when output needs them

Primitive choice follows output contract, not operator name.

  • Segmented reduction: reduce several variable-length ranges independently, such as one maximum per sequence in packed batch.
  • Arg reduction: return both winning value and index, with explicit tie and NaN rule.
  • Reduce by key: combine adjacent runs sharing key, often after grouping.
  • Scan by key: reset prefix when key changes.

For argmax, define state (value, index). Combine larger value; on tie, choose smaller index. Empty segment also needs documented identity or sentinel. Without those rules, parallel tree may return different valid-looking index after scheduling or tile change.

CUB provides device-wide, block-wide, and warp-wide collectives plus segmented variants. Scope controls who invokes operation and where output is valid. A BlockReduce result is meaningful only for designated thread; every lane writing it would duplicate or race output.[8]Reference 8CUB Developer Overviewhttps://nvidia.github.io/cccl/unstable/cub/developer_overview.html

Prefer library until fusion earns custom kernel

Current CUDA Core Compute Libraries (CCCL) CUB documentation layers reduction across thread, warp, block, and device scopes. Device APIs choose tuned policies by type, size, and architecture; block APIs expose reusable temporary storage and synchronization rules.[8]Reference 8CUB Developer Overviewhttps://nvidia.github.io/cccl/unstable/cub/developer_overview.html

Two-phase device reduction call looks like this. First call queries temporary bytes, second launches operation:

cub-sum.cu
1void* temporary = nullptr; 2std::size_t temporary_bytes = 0; 3 4cub::DeviceReduce::Sum( 5 temporary, temporary_bytes, device_input, device_output, count 6); 7cudaMalloc(&temporary, temporary_bytes); 8cub::DeviceReduce::Sum( 9 temporary, temporary_bytes, device_input, device_output, count 10);

Real code must check every CUDA return, own stream explicitly, reuse temporary allocation across hot calls, and free it after stream work completes. Newer CCCL execution-environment APIs can manage stream-ordered temporary storage, but version pin decides available surface.

Use this boundary:

SituationDefaultReason
standalone sum, min, max, argmax, or scanCUB device primitivetuned multi-pass policy, edge handling, and documented contract already exist
reduction inside custom epilogueCUB block or warp primitivekeep fusion while reusing collective machinery
unusual reduction state with associative, commutative operatorCUB custom reduction firstcustom state doesn't require custom scheduling
fuse load, transform, reduce, and writecustom kernel, benchmark against CUB compositionavoided intermediate traffic may justify ownership
unsupported layout or strict orderingcustom kernel with explicit testslibrary contract may not match semantics
teaching mechanismcustom lab plus library comparisoninspectable source clarifies invariants, not performance leadership

Custom kernel owns more than arithmetic: architecture policy, partial warps, temporary storage, stream semantics, deterministic order, sanitizer coverage, and future retuning.

Run downloadable CUDA lab

Download complete CUDA source. It contains four paths:

OperationBaselineHierarchical candidateCorrectness rule
sum reductionone GPU thread loops over all valuestwo items per thread, warp shuffles, block partials, recursive passesexact signed 64-bit total
exclusive scanone GPU thread emits running total512-item block scans, recursive block-total scan, uniform addevery signed 64-bit prefix matches CPU

Baselines are intentionally serial. They expose dependency and produce trusted integer reference, but they aren't competitive implementations.

Compile for installed GPU architecture. Attached local receipt used sm_120; command below keeps target explicit:

terminal
1GPU_ARCH=${GPU_ARCH:-sm_80} 2nvcc -O3 -std=c++17 -lineinfo -arch="$GPU_ARCH" \ 3 assets/primitives_lab.cu -o primitives_lab 4 5./primitives_lab 4194304 30

Program prints hardware identity, API versions, workload, warmups, iteration count, exact correctness status, CUDA-event time, and logical byte rate. Reduction rate counts input bytes; scan rate counts one input read plus one output write. It isn't physical DRAM bandwidth because hierarchical paths also move partials and offsets.

Run odd size through correctness tools before trusting power-of-two benchmark:

terminal
1compute-sanitizer --tool memcheck --error-exitcode=99 ./primitives_lab 4099 1 2compute-sanitizer --tool racecheck --error-exitcode=99 ./primitives_lab 4099 1 3compute-sanitizer --tool synccheck --error-exitcode=99 ./primitives_lab 4099 1

Compute Sanitizer distinguishes invalid addresses, shared-memory hazards, and synchronization misuse.[9]Reference 9Compute Sanitizerhttps://docs.nvidia.com/compute-sanitizer/ComputeSanitizer/ Numerical checks still matter because sanitizer can't prove prefix values.

Local benchmark receipt records full raw output from RTX 5070 Ti with CUDA 13.2, source hash, compiler command, warm-cache timing method, exact CPU checks, and clean sanitizer results. It doesn't claim CUB comparison, physical bandwidth, or portability. Re-run same receipt on target hardware before using any timing decision.

Test properties, not one fixture

One eight-value tensor makes mechanism visible. Property tests catch edge behavior.

Reduction properties

  • Empty input returns declared identity or documented error.
  • One value returns that value.
  • Split then merge matches full reduction for truly associative operator.
  • Permutation leaves output unchanged only when operator is commutative.
  • Integer accumulator doesn't overflow tested bound.
  • Float error stays within stated tolerance against higher-precision reference.

Scan properties

  • Output length equals input length.
  • Exclusive output at index 0 equals identity.
  • exclusive[i+1] == exclusive[i] op input[i] for exact operator.
  • Last inclusive output matches full reduction.
  • Segment reset occurs exactly at supplied boundary.
  • Sizes around block boundary pass: 0, 1, 31, 32, 33, 511, 512, 513.

Softmax properties

  • For an accepted finite row with at least one unmasked logit, every output is finite and nonnegative.
  • Row sums to 1 within tolerance.
  • Adding constant to every logit leaves probabilities unchanged within tolerance.
  • Probability argmax matches logit argmax under same tie rule.
  • Extreme fixtures such as [-10000, 0, 10000] don't produce NaN or Inf.
  • Masked positions contribute empty identity, and fully masked rows follow declared error or fallback contract.

Property that fails often identifies broken layer faster than output dump.

Diagnose by first broken invariant

SymptomLikely boundaryFirst evidenceRepair
wrong only for partial warpshuffle mask names absent lanes or invalid sourcetest 31, 32, 33 elements; inspect maskzero-fill active lanes or guard valid source
hang only on edge blocksome threads skipped block barriersynccheck plus edge shapekeep barrier participation uniform
one correct sum, later runs varyrace, atomic order, or uninitialized partialracecheck, initialized buffers, fixed-tree rerunremove race; state determinism contract
each block scan restarts at zeromissing recursive offset propagationinspect block totals and offsetsscan totals and uniform-add offsets
inclusive scan shifted one slotidentity or inclusive/exclusive convention mismatchtiny hand fixturewrite contract at API boundary
softmax contains Infexponentials used unshifted logitsprint row max and max shifted logitsubtract max or use online state
softmax finite but row sum wrongdenominator states merged without rescalinginspect (m,d) per chunkrescale both denominators to merged max
softmax is NaN only on fully masked rowempty row entered ordinary normalizationcount unmasked logits before normalizereject row or apply declared fallback
argmax value right, index unstabletie rule unspecifiedduplicate maxima at different indicesencode deterministic pair comparator
custom kernel loses after library updatecopied policy no longer fits targetsame-shape CUB benchmark and profileuse library or retune with current target

Stop timing at first correctness failure. Fast wrong prefix can corrupt allocation offsets far from kernel that produced them.

Keep complete benchmark receipt

Primitive timing is incomplete without five fields:

  1. Hardware and software: GPU, compute capability, driver, toolkit, compiler flags, source hash.
  2. Workload: count, segments, value distribution, alignment, warmups, iterations, cache state.
  3. Precision and algorithm: input, accumulator, output types, operator, hierarchy, deterministic policy.
  4. Baseline: exact serial, CUB, framework, or prior kernel path under equivalent work.
  5. Correctness: reference, tolerance, property sizes, sanitizer results.

Measure kernel execution with CUDA events in same stream. Separate allocation, first-use loading, and JIT cost unless latency objective includes them. Report distributions across process runs when decision is close. Attach profiler counters only after timing and correctness identify meaningful candidate.

No universal speedup follows from primitive name. Reduction shape, value type, segment distribution, architecture, library version, fusion, and launch overhead can reverse ranking.

Mastery check

Key concepts

  • Reduction shrinks output; scan preserves one prefix per position.
  • Warp shuffles move register values only among valid participating lanes.
  • Block collectives need shared-memory visibility and uniform barriers.
  • Device-wide collectives need inter-block propagation through more launches or proven look-back protocol.
  • Stable softmax merges (maximum, shifted denominator) states.
  • Masked softmax values contribute empty identity; fully masked rows need explicit contract.
  • CUB is default for standard standalone primitives; custom CUDA needs fusion or semantic reason.

Evaluation rubric

  • Foundational: Compute reduction, inclusive scan, exclusive scan, and argmax for eight-value tensor by hand.
  • Intermediate: Explain warp, block, and device hierarchy plus mask, barrier, identity, and offset invariants.
  • Advanced: Implement and benchmark custom fused primitive against CUB with numerical, determinism, sanitizer, and receipt gates.

Common pitfalls

  • Treating active mask as replacement for valid-source check.
  • Returning before block-wide barrier on tail data.
  • Assuming floating-point addition is exactly associative.
  • Forgetting to scan block totals before uniform add.
  • Merging online-softmax denominators measured against different maxima.
  • Calling demonstration baseline competitive without tuned library comparison.
Complete the lesson

Mastery Check

Answer every question, then check your score. Score 75% or higher to mark this lesson complete.

1.Tensor is [3, -1, 4, 1, 5, -9, 2, 6]. What are two level-2 partial sums in balanced reduction?

Correct answer: [7, 4]

First level gives [2, 5, -4, 8]. Combining adjacent pairs again gives 7 and 4, which then combine to final sum 11.

2.Exclusive scan of [3, -1, 4, 1] uses addition and identity 0. What is output?

Correct answer: [0, 3, 2, 6]

Each position receives sum before current input: 0, then 3, then 3 plus -1 equals 2, then 2 plus 4 equals 6.

3.Why does lab assign 0 to tail lanes before warp reduction?

Correct answer: It keeps every lane named by full mask participating with additive identity

Every named lane executes same shuffle. Identity contributes nothing, while an inactive source lane would make shuffle result undefined.

4.Each scan block produces correct local prefixes, but first output of every later block is 0. Which stage is missing?

Correct answer: Exclusive scan of block totals followed by uniform offset add

Local scan starts each block at identity. Device-wide scan requires prefix of earlier block totals to be added to every local output in later blocks.

5.Two float reductions both meet accuracy tolerance, but only one repeats bitwise on same GPU. Which statement is justified?

Correct answer: Accuracy and repeatability require separate checks

Tolerance measures closeness to reference. Bitwise repeatability measures combining-order stability. Either property can hold independently.

6.Online-softmax states are (5, 2) and (6, 1). Which merge is correct?

Correct answer: maximum 6 and denominator 2e^-1 plus 1

Both denominators must use merged maximum 6 as scale. Left state rescales by e^(5-6); right state already uses maximum 6.

7.Argmax sees equal maximum at indices 4 and 9. Comparator says smaller index wins. Which state is valid output?

Correct answer: (maximum value, index 4)

State carries value and index. Explicit tie rule is part of operator, so every reduction grouping must select smaller index 4.

8.A standard standalone exclusive scan already matches CUB's contract. When is custom kernel strongest next choice?

Correct answer: When fusion avoids materializing surrounding intermediates

Fusion can remove global traffic and launches that standalone library composition needs. Custom still must beat CUB composition under same correctness contract.

9.Which evidence package supports custom reduction timing claim?

Correct answer: Complete system, workload, baseline, and correctness receipt

Timing is interpretable only when reader can reconstruct system, work, algorithm, comparison, and proof that faster output remains valid.

10.A block scan hangs only at N=513. What is first likely invariant to inspect?

Correct answer: Whether every tail-block thread reached each __syncthreads()

N=513 creates partial second block. If out-of-range threads returned before block barrier, remaining threads can wait forever or observe incomplete shared state.

10 questions remaining.

Next Step
Continue to GPU Compilers and Kernel Languages

You can now state a primitive's dependency, synchronization, numerical, and benchmark contracts. Next trace how source choices become PTX, cubin resources, and native instructions that implement those collectives.

PreviousGPU Kernel Performance Engineering Lab
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

CUDA Programming Guide.

NVIDIA · 2026

https://docs.nvidia.com/cuda/cuda-programming-guide/

Optimizing Parallel Reduction in CUDA

Harris, M. · 2007 · NVIDIA Developer Technology

https://developer.download.nvidia.com/compute/cuda/1.1-Beta/x86_website/projects/reduction/doc/reduction.pdf

CUDA Samples

NVIDIA · 2026 · Official NVIDIA repository

https://github.com/NVIDIA/cuda-samples

cub::DeviceReduce

NVIDIA · 2026 · CUDA Core Compute Libraries documentation

https://nvidia.github.io/cccl/unstable/cub/api/structcub_1_1DeviceReduce.html

Single-pass Parallel Prefix Scan with Decoupled Look-back

Merrill, D. and Garland, M. · 2016 · NVIDIA Technical Report NVR-2016-002

https://research.nvidia.com/sites/default/files/pubs/2016-03_Single-pass-Parallel-Prefix/nvr-2016-002.pdf

cub::DeviceScan

NVIDIA · 2026 · CUDA Core Compute Libraries documentation

https://nvidia.github.io/cccl/unstable/cub/api/structcub_1_1DeviceScan.html

Online normalizer calculation for softmax.

Milakov, M. & Gimelshein, N. · 2018

https://arxiv.org/abs/1805.02867

CUB Developer Overview

NVIDIA · 2026 · CUDA Core Compute Libraries documentation

https://nvidia.github.io/cccl/unstable/cub/developer_overview.html

Compute Sanitizer

NVIDIA · 2026 · Official documentation

https://docs.nvidia.com/compute-sanitizer/ComputeSanitizer/

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.