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 192 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/27
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 Compilers and Kernel LanguagesGEMM From Naive CUDA to Tensor CoresLow-Precision GPU ProgrammingModel 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 & AutoscalingDistributed 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 Kernel Performance Engineering Lab
🚀HardInference Optimization

GPU Kernel Performance Engineering Lab

Optimize one CUDA matrix-transpose kernel through a repeatable evidence loop. Diagnose coalescing, shared-memory bank conflicts, occupancy limits, correctness failures, and the point where a reduction or scan is the right primitive.

19 min read
Learning path
Step 146 of 192 in the full curriculum
Correct GPU Profiling and BenchmarkingGPU Compilers and Kernel Languages

Personalize this lesson

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

Four lines change, the timer drops, and the kernel looks faster. Then one output check finds stale values. The optimization didn't win anything. It made the program wrong more quickly.

This lab turns that failure into a repeatable engineering habit. You'll keep one square matrix, one data type, one launch geometry, and one timing harness while changing only the kernel's memory path. Every candidate must pass exact output checks and CUDA correctness tools before its timing counts.

Three prerequisites supply the vocabulary. CUDA for ML Training established grids, blocks, warps, asynchronous launches, and memory hierarchy. Accelerator Architecture Field Guide connected arithmetic intensity and resource ceilings to workload bounds. GPU Profiling, Correctness, and Benchmarking separated host timelines, kernel counters, and correctness evidence. Here those ideas control one kernel from first measurement through recovery.

Keep one experiment stable

The workload transposes an N×NN \times NN×N row-major float32 matrix. If input element A[r,c]A[r,c]A[r,c] lives at input[r * N + c], correct output satisfies:

B[c,r]=A[r,c]B[c,r] = A[r,c]B[c,r]=A[r,c]

No floating-point arithmetic changes a value. A correct result must therefore match bit for bit, so max_abs_error should be 0. The default matrix gives every location a distinct integer-valued float, which keeps a misplaced element from hiding behind repeated test data. That exact contract is stronger than a loose tolerance and simpler than checking only a checksum.

The supplied harness contains five variants:

VariantPurposeIncluded in normal benchmark?
copyreads and writes every element without transposing, giving a same-harness memory-movement referenceyes
naivereads adjacent input words but writes the transpose with stride NNNyes
tiledstages a 32×3232 \times 3232×32 tile in shared memory so global reads and writes are adjacentyes
paddedchanges shared tile to 32×3332 \times 3332×33 so column reads spread across banksyes
brokenremoves required block barrier to create a shared-memory raceno, run deliberately

copy isn't a transpose baseline. It performs a different operation with the same logical read and write volume. Treat it as context for the memory path, not as a denominator for a claimed transpose speedup.

Every iteration follows the same loop:

Diagram showing 1 · Measure fixed case, 2 · Name observed bound, 3 · Change one cause, and 4 · Correct?.
1 · Measure fixed case, 2 · Name observed bound, 3 · Change one cause, and 4 · Correct?.

Record hypothesis before editing. Otherwise every surprising counter becomes a story invented after result.

Build reproducible lab

You need an NVIDIA GPU, a CUDA Toolkit with nvcc and Compute Sanitizer, and Nsight Compute's ncu command. Tool versions and GPU identity belong beside every result because compiler decisions, architecture, clocks, and profiler metric availability can change evidence.

Save transpose lab source as transpose_lab.cu. Also save recovery record template for deliberate failure later.

First create one artifact directory and capture environment. These commands produce text files you can compare without relying on terminal scrollback:

terminal
1mkdir -p artifacts/run-01 2nvidia-smi -L | tee artifacts/run-01/gpu.txt 3nvidia-smi | tee artifacts/run-01/nvidia-smi.txt 4nvcc --version | tee artifacts/run-01/nvcc.txt 5ncu --version | tee artifacts/run-01/ncu.txt 6compute-sanitizer --version | tee artifacts/run-01/compute-sanitizer.txt 7sha256sum transpose_lab.cu | tee artifacts/run-01/source.sha256

Compile optimized code with line information for profiler and sanitizer attribution. -Xptxas=-v also prints per-kernel register and static shared-memory use:

terminal
1nvcc -O3 -lineinfo -Xptxas=-v \ 2 -o transpose_lab transpose_lab.cu \ 3 2>&1 | tee artifacts/run-01/build.txt

Expected build evidence is structural, not one universal register count:

  • compiler names each instantiated kernel;
  • transpose_tiled<0> reserves 32×32×4=409632 \times 32 \times 4 = 409632×32×4=4096 bytes of static shared memory;
  • transpose_tiled<1> reserves 32×33×4=422432 \times 33 \times 4 = 422432×33×4=4224 bytes;
  • register count is recorded instead of assumed;
  • no compilation error is present.

Run fixed 4096×40964096 \times 40964096×4096 matrix for 100 measured launches per normal variant:

terminal
1./transpose_lab all 4096 100 \ 2 | tee artifacts/run-01/benchmark.csv

Harness prints CSV columns variant, correct, mismatches, max_abs_error, average_ms, and effective_gb_s. Device and launch shape go to standard error, so tee keeps CSV machine-readable while terminal still names hardware. Pass conditions are:

FieldRequired evidence
correcttrue for copy, naive, tiled, and padded
mismatches0
max_abs_error0
average_mspositive CUDA-event time after five warmup launches
effective_gb_spositive logical read-plus-write rate, interpreted only with profiler evidence

Don't rank variants yet. A single process run can be affected by clocks, temperature, competing work, or startup state. Repeat separate processes under same conditions, alternate order by round, and preserve every row:

terminal
1{ 2 echo 'variant,correct,mismatches,max_abs_error,average_ms,effective_gb_s' 3 for round in 1 2 3 4 5; do 4 if (( round % 2 == 1 )); then 5 order=(naive tiled padded) 6 else 7 order=(padded tiled naive) 8 fi 9 for variant in "${order[@]}"; do 10 ./transpose_lab "$variant" 4096 100 | tail -n 1 11 done 12 done 13} | tee artifacts/run-01/repeated-benchmark.csv

Compare distributions, not only best row. If one candidate wins once and overlaps baseline in later runs, evidence doesn't support keeping it.

Read naive memory path

CUDA coalesces a warp's global-memory requests into transactions that cover requested addresses. Adjacent float32 words need fewer transactions than words spread across matrix rows.[1]Reference 1CUDA Programming Guide.https://docs.nvidia.com/cuda/cuda-programming-guide/ The naive kernel reads input with good spatial order, then loses that order on output.

Here is complete indexing logic from transpose_naive. A block has 32 threads in x and 8 in y; each thread handles four rows separated by kBlockRows:

transpose-naive.cu
1__global__ void transpose_naive(float* output, const float* input, int n) { 2 const int x = blockIdx.x * kTile + threadIdx.x; 3 const int y = blockIdx.y * kTile + threadIdx.y; 4 5 for (int offset = 0; offset < kTile; offset += kBlockRows) { 6 if (x < n && y + offset < n) { 7 const size_t input_index = static_cast<size_t>(y + offset) * n + x; 8 const size_t output_index = static_cast<size_t>(x) * n + y + offset; 9 output[output_index] = input[input_index]; 10 } 11 } 12}

Within one warp, threadIdx.x runs from 0 through 31 while threadIdx.y stays fixed. Input indexes therefore differ by one float. Output indexes differ by NNN floats because x became output row. Correct index mapping creates inefficient stores.

Transpose does almost no useful floating-point arithmetic. Its logical data volume per launch is one read and one write for every element:

logical bytes=2N2⋅sizeof⁡(float)\text{logical bytes} = 2N^2 \cdot \operatorname{sizeof}(\text{float})logical bytes=2N2⋅sizeof(float)

For N=4096N=4096N=4096, that's 134,217,728134{,}217{,}728134,217,728 bytes, or 128 MiB. Harness reports:

effective GB/s=2N2⋅4average time in seconds⋅109\text{effective GB/s} = \frac{2N^2 \cdot 4}{\text{average time in seconds} \cdot 10^9}effective GB/s=average time in seconds⋅1092N2⋅4​

Effective bandwidth isn't physical DRAM traffic. Cache hits, excess transactions, error-correction traffic, and architecture-specific paths can make hardware traffic differ. Use it as a stable workload-normalized rate for comparing same operation on same setup.

A 4096×40964096 \times 40964096×4096 transpose averages 0.2500.2500.250 ms. What effective rate should the ledger record, and what hardware claim remains unsupported?

Answer

The logical volume is 2×40962×4=134,217,7282 \times 4096^2 \times 4 = 134{,}217{,}7282×40962×4=134,217,728 bytes. Dividing by 0.0002500.0002500.000250 seconds and then by 10910^9109 gives about 536.9536.9536.9 GB/s. That is a workload-normalized effective rate. It doesn't establish physical DRAM traffic or percentage of peak bandwidth without profiler counters and the matching hardware conditions.

Profile before editing

Nsight Compute groups kernel evidence into named sections. LaunchStats reports grid, block, registers, and shared memory. SpeedOfLight compares compute and memory resource throughput with device ceilings. MemoryWorkloadAnalysis traces traffic through memory hierarchy. Occupancy reports active-warps capacity and its resource limiters.[2]Reference 2Nsight Compute Profiling Guidehttps://docs.nvidia.com/nsight-compute/ProfilingGuide/

Collect one naive launch. Full set may replay kernel several times to gather counters, so don't use profiler run as timing result:

terminal
1ncu --set full \ 2 --kernel-name-base function \ 3 --kernel-name transpose_naive \ 4 --launch-count 1 \ 5 --force-overwrite \ 6 -o artifacts/run-01/naive \ 7 ./transpose_lab naive 4096 1 8 9ncu --import artifacts/run-01/naive.ncu-rep --page details \ 10 > artifacts/run-01/naive.txt

Read report in this order:

  1. Confirm kernel name, matrix size, block 32 x 8, and one profiled launch.
  2. Check whether memory throughput is closer to its ceiling than compute throughput.
  3. Inspect global load and store requests, sectors, and reported access inefficiencies.
  4. Check static shared memory is zero for naive variant.
  5. Read occupancy limiter, but don't optimize it unless evidence connects limiter to latency or throughput.

Roofline reasoning predicts a memory-bound transpose because arithmetic intensity is near zero. Profiler still matters: it distinguishes useful memory traffic from inefficient transactions and verifies that observed kernel is one you meant to measure. Roofline is a ceiling model, not a promise that every low-intensity kernel reaches peak bandwidth.[3]Reference 3Roofline: An Insightful Visual Performance Model for Multicore Architectureshttps://doi.org/10.1145/1498765.1498785

Write one bound statement before editing:

artifacts/run-01/naive-bound.txt
1Workload: float32 transpose, N=4096, block=32x8 2Correctness: exact transpose, 0 mismatches 3Observed limiter: [fill from report] 4Source evidence: [section, counter, and source line] 5Hypothesis: make output stores adjacent by staging a tile in shared memory

If report doesn't support memory-path hypothesis, stop. A shared-memory rewrite would be guesswork.

Iteration one: coalesce both global directions

Shared memory is on-chip storage shared by threads in one block. Tiled transpose first copies input tile into shared memory with adjacent global reads. After block-wide barrier, threads read tile with swapped indexes and write adjacent output words. CUDA's official guidance uses shared memory for this exact reordering pattern.[4]Reference 4CUDA C++ Best Practices Guidehttps://docs.nvidia.com/cuda/cuda-c-best-practices-guide/

The core kernel below accepts Padding as compile-time tile-width change. Variant tiled instantiates Padding = 0:

transpose-tiled.cu
1template <int Padding> 2__global__ void transpose_tiled(float* output, const float* input, int n) { 3 __shared__ float tile[kTile][kTile + Padding]; 4 5 const int input_x = blockIdx.x * kTile + threadIdx.x; 6 const int input_y = blockIdx.y * kTile + threadIdx.y; 7 8 for (int offset = 0; offset < kTile; offset += kBlockRows) { 9 if (input_x < n && input_y + offset < n) { 10 tile[threadIdx.y + offset][threadIdx.x] = 11 input[static_cast<size_t>(input_y + offset) * n + input_x]; 12 } 13 } 14 15 __syncthreads(); 16 17 const int output_x = blockIdx.y * kTile + threadIdx.x; 18 const int output_y = blockIdx.x * kTile + threadIdx.y; 19 20 for (int offset = 0; offset < kTile; offset += kBlockRows) { 21 if (output_x < n && output_y + offset < n) { 22 output[static_cast<size_t>(output_y + offset) * n + output_x] = 23 tile[threadIdx.x][threadIdx.y + offset]; 24 } 25 } 26}

Barrier is a correctness boundary. Every thread must finish writes into tile before any thread reads values written by another warp. Stream order doesn't provide that block-internal guarantee.

Run correctness and event timing first, then collect profiler report:

terminal
1./transpose_lab tiled 4096 100 \ 2 | tee artifacts/run-01/tiled-benchmark.csv 3 4ncu --set full \ 5 --kernel-name-base function \ 6 --kernel-name 'regex:transpose_tiled' \ 7 --launch-count 1 \ 8 --force-overwrite \ 9 -o artifacts/run-01/tiled \ 10 ./transpose_lab tiled 4096 1

Expected evidence has two parts:

  • global reads and writes now use adjacent lane addresses;
  • shared-memory column read maps many lanes onto same bank.

Shared memory has 32 banks for the access model used here, and successive 32-bit words map to successive banks. Different words requested from same bank in one warp are serialized, except supported broadcast case where threads request same word.[1]Reference 1CUDA Programming Guide.https://docs.nvidia.com/cuda/cuda-programming-guide/

For unpadded tile, column element for lane iii has word offset 32i+c32i + c32i+c. Its bank is:

bank⁡(i)=(32i+c) mod 32=c\operatorname{bank}(i) = (32i + c) \bmod 32 = cbank(i)=(32i+c)mod32=c

All 32 lanes address different words in bank ccc. Global traffic improved, but shared-memory conflict is now visible.

Three-stage comparison of representative lanes in a 32-lane warp transposing a matrix. The naive kernel reads adjacent input words but stores rows separated by matrix stride N. A 32 by 32 shared-memory tile makes global stores adjacent but maps the shown column c equals 0 from every lane to bank 0. Padding the tile to 32 by 33 keeps global stores adjacent and maps lane i to bank i for c equals 0; other columns rotate the bank numbers while keeping all 32 banks distinct.
Follow one warp across three variants. For the shown column $c=0$, tiling repairs strided global writes but sends every lane to bank 0. One padding column spreads those lanes across all 32 banks; another $c$ rotates the bank numbers without creating a conflict.

Profiler versions and architectures can expose bank evidence with different metric names. Use report's Shared Memory tables and source correlation first. If you need raw name for scripted collection, query installed tool instead of copying a metric from another GPU:

terminal
1ncu --query-metrics | rg 'bank_conflicts|shared.*conflict' \ 2 | tee artifacts/run-01/bank-metrics.txt

Record metric name, value, and source line. A screenshot without workload identity or source version isn't enough.

Iteration two: pad shared tile

Candidate change is one token in tile width:

padded-shared-tile.cu
1__shared__ float tile[32][33];

Column element for lane iii now has word offset 33i+c33i + c33i+c:

bank⁡(i)=(33i+c) mod 32=(i+c) mod 32\operatorname{bank}(i) = (33i + c) \bmod 32 = (i + c) \bmod 32bank(i)=(33i+c)mod32=(i+c)mod32

Lanes 0 through 31 map to 32 distinct banks. Padding doesn't change matrix shape, output, or logical global bytes. It changes address stride inside shared memory. NVIDIA's best-practices transpose example uses same extra column to remove bank conflict.[4]Reference 4CUDA C++ Best Practices Guidehttps://docs.nvidia.com/cuda/cuda-c-best-practices-guide/

Would float tile[32][34] remove the column conflict as completely as float tile[32][33]? Derive the bank pattern before answering.

Answer

No. A 34-word row gives bank⁡(i)=(34i+c) mod 32=(2i+c) mod 32\operatorname{bank}(i) = (34i + c) \bmod 32 = (2i + c) \bmod 32bank(i)=(34i+c)mod32=(2i+c)mod32. Lanes 0 through 15 reach 16 distinct banks, then lanes 16 through 31 repeat those banks. Each bank in that set receives two different words, leaving a two-way conflict. A 33-word row works because its stride is one modulo 32.

Check padded result before timing or profiling it:

terminal
1./transpose_lab padded 4096 100 \ 2 | tee artifacts/run-01/padded-benchmark.csv 3 4ncu --set full \ 5 --kernel-name-base function \ 6 --kernel-name 'regex:transpose_tiled' \ 7 --launch-count 1 \ 8 --force-overwrite \ 9 -o artifacts/run-01/padded \ 10 ./transpose_lab padded 4096 1

Compare same evidence fields across tiled and padded:

Evidencetiled expectationpadded expectationDecision use
exact transpose0 mismatches0 mismatchesreject either candidate that fails
logical global bytessame 2N2⋅42N^2 \cdot 42N2⋅4same 2N2⋅42N^2 \cdot 42N2⋅4keeps rate comparable
global access orderadjacent reads and writesadjacent reads and writesconfirms padding didn't regress coalescing
shared bank conflictscolumn conflict presentconflict removed for shown mappingtests causal hypothesis
static shared memory4096 bytes4224 bytesfeeds resource-limit check
event timemeasured distributionmeasured distributionkeep change only if repeat evidence improves

Don't write “padding is X times faster” without your hardware, software, shape, precision, timing method, baseline, and correctness record. Architecture and toolchain decide actual size of benefit, so a universal speedup would be false.

A candidate is 6% faster, but its edit both pads the tile and changes the block from 32 x 8 to 32 x 16. What can the evidence ledger conclude, and how should the experiment recover?

Answer

The run shows that the combined candidate differs, but it can't attribute the change to padding or launch geometry. Restore 32 x 8 and measure padding alone against the fixed baseline. Then hold tile width fixed and test 32 x 16 separately. Each branch still needs the same exact-output, profiler, sanitizer, and repeated-timing gates.

Occupancy is constraint, not score

Occupancy is ratio of active warps on a streaming multiprocessor (SM) to hardware maximum. More resident warps can hide latency, but highest occupancy doesn't automatically deliver highest performance. Registers, shared memory, threads per block, and barriers can limit resident blocks, while forcing resource use down can introduce spills or extra instructions.[4]Reference 4CUDA C++ Best Practices Guidehttps://docs.nvidia.com/cuda/cuda-c-best-practices-guide/[2]Reference 2Nsight Compute Profiling Guidehttps://docs.nvidia.com/nsight-compute/ProfilingGuide/

Lab block has 32 x 8 = 256 threads, or eight 32-thread warps. Resource comparison stays concrete:

ResourcenaivetiledpaddedWhat to inspect
threads per block256256256LaunchStats
static shared memory per block0 B4096 B4224 Bcompiler output and LaunchStats
added shared memory from padding0 B0 B128 Barithmetic plus report
registers per threadcompiler-specificcompiler-specificcompiler-specific-Xptxas=-v and LaunchStats
resident blocks or warpsdevice-specificdevice-specificdevice-specificOccupancy limiter

Small 128-byte increase may leave occupancy unchanged, or cross allocation boundary on some architecture or a larger tile design. Read actual report. Don't infer resident blocks from source alone.

Use these diagnoses:

Profiler evidenceLikely constraintNext experiment
low occupancy limited by static shared memorytoo much per-block tile storagereduce tile footprint or test smaller blocks, one change at time
low occupancy limited by registers plus local-memory trafficregister pressure and spillingreduce live state or revisit unrolling, then check instruction count
adequate occupancy but high memory throughputworkload remains bandwidth-boundreduce transactions or bytes, not chase 100% occupancy
many waves but long barrier stallssynchronization or imbalanceinspect work per warp and barrier placement
too few blocks to fill SMsgrid lacks parallel worktest larger workload or different decomposition

Keep padded variant when it removes measured conflict and improves repeat timing without correctness or resource regression. Revert when counter improves but end-to-end kernel timing doesn't. Counter movement isn't product outcome by itself.

Deliberately remove barrier

Performance edits often break synchronization before they break memory bounds. Variant broken uses padded tile but omits __syncthreads() between block writes and reads. Some warps can read locations before producer warp writes them.

Run smaller matrix under tools. NVIDIA recommends memcheck before racecheck because racecheck doesn't check invalid memory addresses.[5]Reference 5Compute Sanitizerhttps://docs.nvidia.com/compute-sanitizer/ComputeSanitizer/

terminal
1compute-sanitizer --tool memcheck \ 2 ./transpose_lab broken 1024 1 \ 3 2>&1 | tee artifacts/run-01/broken-memcheck.txt 4 5compute-sanitizer --tool racecheck \ 6 ./transpose_lab broken 1024 1 \ 7 2>&1 | tee artifacts/run-01/broken-racecheck.txt 8 9compute-sanitizer --tool synccheck \ 10 ./transpose_lab broken 1024 1 \ 11 2>&1 | tee artifacts/run-01/broken-synccheck.txt

Expected diagnostic split:

  • memcheck may report no illegal address because every index stays in allocation bounds;
  • racecheck should report shared-memory read-after-write hazards around missing barrier;
  • synccheck may report no misuse because kernel omitted barrier instead of executing divergent or invalid barrier;
  • output checker may fail, but accidental correct output on one run doesn't clear race.

That last case is important. Scheduler timing can hide data race during ordinary run. Dynamic race evidence invalidates kernel even when sampled output happens to match.

Recover by restoring block-wide barrier, then rerun exact output, memcheck, and racecheck on padded variant:

terminal
1compute-sanitizer --tool memcheck \ 2 ./transpose_lab padded 1024 1 \ 3 2>&1 | tee artifacts/run-01/recovered-memcheck.txt 4 5compute-sanitizer --tool racecheck \ 6 ./transpose_lab padded 1024 1 \ 7 2>&1 | tee artifacts/run-01/recovered-racecheck.txt 8 9./transpose_lab padded 4096 100 \ 10 | tee artifacts/run-01/recovered-benchmark.csv

Recovery record must connect symptom to invariant:

FieldMinimum content
failuremissing block barrier between shared writes and cross-warp reads
detectionracecheck hazard plus output result, whether pass or fail
repairrestored __syncthreads() reached by every thread in block
proofexact transpose, memcheck clean, racecheck clean
performancesame event-timed shape and iteration count after repair
provenanceGPU, toolkit, source hash, compiler resource output, report paths

Fill recovery-template.md and keep it with raw reports. That artifact is stronger than “added synchronization” because reviewer can trace failure, repair, and post-recovery cost.

Decide between transpose, reduction, and scan

Shared-memory tile is right for layout reordering. It isn't universal answer for every memory-bound kernel. Ask what output dependency requires before choosing primitive.

PrimitiveOutput contractNeighbor interactionTypical use
transposepreserve every value, change two-dimensional index orderthreads exchange tile positionsswitch row-major and column-major access orientation
reductioncombine many values into fewer values with associative operatorpartial aggregates mergesum loss, maximum error, count mismatches, row sum
scanemit prefix aggregate for every input positioneach output depends on preceding rangeprefix offsets, stream compaction positions, cumulative token counts

Consider moving correctness check from CPU back to GPU. If you need only total mismatch count and maximum absolute error, use reductions. Both outputs collapse N2N^2N2 comparisons into one or few scalars. A scan would do extra work by producing prefix result for every comparison.

Now consider compacting indexes of mismatched elements. Each failing element needs destination position based on number of failures before it. That is prefix dependency, so scan computes offsets. A reduction can count total failures but can't assign each failure unique compacted slot.

Decision rule stays mechanical:

  1. Same number of values, different layout: transpose.
  2. Fewer summary values: reduction.
  3. Same number of prefix states or compaction offsets: scan.

Tiling can appear inside all three implementations, but dependency graph picks algorithm. Don't start by copying transpose tile into reduction or scan kernel.

Close evidence ledger

Complete comparison only after all variants share workload and validity contract:

RunHypothesisOne code changeCorrectnessProfiler evidenceEvent timingVerdict
naivedirect transpose wastes global-store transactionsnoneexactmemory-bound, strided store source linefive-run distributionbaseline
tiledshared tile coalesces output storesadd tile and block barrierexactglobal access improves; bank conflict appearsfive-run distributionkeep for next iteration or reject
paddedextra column changes bank mappingtile width 32 to 33exact plus sanitizerbank conflict falls; resource use recordedfive-run distributionkeep only with repeat benefit
brokenbarrier is required for cross-warp tile reuseremove barrierunreliableracecheck hazardtiming invalidreject and recover

One row isn't complete if its “correctness” cell says only “looks right.” Keep raw CSV, .ncu-rep files, sanitizer logs, compiler resource output, environment files, source hash, and filled recovery record.

You have finished when you can defend all six claims:

  • workload and timing method stayed fixed;
  • baseline bound came from profiler evidence;
  • each candidate changed one causal mechanism;
  • output check matched transpose exactly;
  • sanitizer cleared accepted kernel;
  • reported performance includes conditions and distribution, not unsupported headline speedup.
Complete the lesson

Mastery Check

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

1.A candidate transpose reports lower CUDA-event time but has three mismatched outputs. What is the correct verdict?

Correct answer: Reject candidate because correctness gate failed before performance comparison

Performance evidence counts only after same output contract passes. Faster wrong work isn't an optimization, even when profiler counter moves in expected direction.

2.Why does naive transpose produce inefficient global stores for one warp?

Correct answer: Each lane writes an address separated from next lane by matrix stride N

Within warp, x changes by one. Naive output index x * N + y therefore changes by N floats, scattering lane stores across rows instead of adjacent words.

3.For shared tile declared float tile[32][32], a warp reads one column. What bank mapping follows for lane i?

Correct answer: All lanes map different words to same bank because (32i + c) mod 32 = c

Row stride 32 disappears modulo 32. Addresses differ, so this isn't broadcast; accesses conflict on same bank.

4.What does changing shared tile from 32 by 32 to 32 by 33 accomplish?

Correct answer: It maps lane i to (i + c) mod 32, spreading column access across banks

Extra column changes shared-memory word stride to 33. Modulo 32, stride becomes one, so warp lanes reach distinct banks while output stays unchanged.

5.Which evidence best supports claim that naive transpose is memory-path limited?

Correct answer: Nsight reports memory pressure, inefficient stores, and little useful arithmetic

Bound claim needs kernel-level evidence tied to actual source and workload. Utilization, register headroom, or different copy operation doesn't isolate limiter.

6.Why isn't 100 percent occupancy an automatic optimization target?

Correct answer: Higher occupancy may not help, while forced resource cuts can cause spills

Occupancy helps hide latency, but highest value doesn't guarantee best throughput. Resource limits and instruction-level parallelism must be evaluated with timing and profiler evidence.

7.Broken variant omits block barrier, yet output happens to match once. Which next evidence decides validity?

Correct answer: Racecheck result, because sampled equality can't clear a shared-memory ordering hazard

Race can stay hidden under one schedule. Racecheck analyzes shared-memory hazards, while synccheck targets misuse of synchronization primitives rather than every omitted barrier.

8.You need one total mismatch count and one maximum error from N squared comparisons. Which primitive fits?

Correct answer: Reduction, because many comparison values collapse into few summary scalars

Count and maximum are associative aggregates. No per-element prefix result is required, so reduction matches output dependency.

9.You need compact output offsets for every mismatched element. Which primitive fits dependency?

Correct answer: Scan, because each offset depends on number of prior mismatches

Prefix count gives each selected element unique destination position. A reduction supplies only total, while transpose changes layout without computing prefix state.

10.Padded variant removes bank-conflict counter but event-time distributions overlap tiled variant. What should evidence ledger say?

Correct answer: Record mechanism gain, but report no measured timing win

Counter validates mechanism, while event timing decides observed kernel outcome. This protocol hasn't demonstrated a timing improvement, so the ledger should preserve the counter result without claiming a speedup.

10 questions remaining.

Next Step
Continue to GPU Compilers and Kernel Languages

You now know how to turn one kernel edit into a measured, correct, and recoverable result. Next trace how CUDA source becomes PTX and machine code, then compare language and compiler choices that expose same performance controls at different levels.

PreviousCorrect GPU Profiling and Benchmarking
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

CUDA Programming Guide.

NVIDIA · 2026

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

Nsight Compute Profiling Guide

NVIDIA · 2026 · Official documentation

https://docs.nvidia.com/nsight-compute/ProfilingGuide/

Roofline: An Insightful Visual Performance Model for Multicore Architectures

Williams, S., Waterman, A., & Patterson, D. · 2009

https://doi.org/10.1145/1498765.1498785

CUDA C++ Best Practices Guide

NVIDIA · 2026 · Official documentation

https://docs.nvidia.com/cuda/cuda-c-best-practices-guide/

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.