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 ScaleCorrect GPU Profiling and Benchmarking
🚀HardInference Optimization

Correct GPU Profiling and Benchmarking

Build trustworthy GPU performance evidence: prove kernel correctness, choose system or kernel profiling scope, control benchmark state, and preserve a reproducible receipt before claiming a speedup.

27 min read
Learning path
Step 145 of 192 in the full curriculum
Accelerator Architecture Field GuideGPU Kernel Performance Engineering Lab

Personalize this lesson

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

A patch arrives with one exciting line: triad_f32 is 1.8x faster. It passed on one input, and a host timer printed a smaller number. On another run, the advantage shrinks. On a length that isn't divisible by 256, one output near the end changes.

That isn't a performance result yet. A trustworthy claim must show that both kernels computed the same operation, timed completed GPU work under the same conditions, and improved the scope users care about. Profilers explain where time went. They don't repair a broken comparison.

The Accelerator Architecture Field Guide established streaming multiprocessors, high-bandwidth memory, caches, and hardware ceilings. CUDA for ML Training established grids, blocks, asynchronous launches, and event timing. Keep those mechanics. Now turn them into a measurement discipline.

One small kernel will carry the investigation. For every index iii, it computes a floating-point triad:

yi=αxi+ziy_i = \alpha x_i + z_iyi​=αxi​+zi​

It looks too small to fail. That makes it useful: if the evidence is weak here, a fused attention or quantization kernel will hide the same mistakes behind far more code.

Turn “faster” into a testable claim

Suppose the candidate triad_f32 replaces a scalar baseline. Before opening a profiler, write the claim so another engineer could disprove it.

Claim fieldConcrete contract for the running kernel
SemanticsFor each valid index, return fmaf(alpha, x[i], z[i]) within declared tolerance
Workloadfloat32, n=1,000,003n=1{,}000{,}003n=1,000,003, deterministic bounded inputs
ScopeOne completed kernel launch, excluding allocation and host-to-device copies
Cache regimeReport cache-hot and buffer-rotated measurements separately
BaselineSame compiler flags, device, inputs, output check, and timing method; record each version's launch shape
EnvironmentGPU UUID, driver, CUDA runtime, source revision, clocks, power limit, and temperature
DecisionKeep candidate only if correctness gates pass and measured scope improves

The odd length is intentional. With 256 threads per block, the last block contains threads whose indexes exceed n−1n-1n−1. A boundary guard must reject them. Testing only lengths divisible by block size can leave an off-by-one bug invisible.

The arithmetic also supplies an early hypothesis. One triad element performs one multiply and one add, counted as two floating-point operations (FLOPs). Its algorithmic minimum traffic is two float32 loads and one float32 store, or 12 bytes:

Imin=2 FLOPs12 bytes≈0.167 FLOPs/byteI_{min} = \frac{2\ \text{FLOPs}}{12\ \text{bytes}} \approx 0.167\ \text{FLOPs/byte}Imin​=12 bytes2 FLOPs​≈0.167 FLOPs/byte

That low arithmetic intensity suggests a bandwidth-sensitive kernel. It doesn't prove measured device-memory traffic equals 12 bytes per element, nor that high-bandwidth memory (HBM) is the current limit. Caches, transaction efficiency, launch overhead, and extra instructions can all move the measured point.

💡 Key insight: A performance hypothesis comes from operation and byte counts. A performance claim comes from a correctness-checked measurement on named hardware.

A cache-hot run processes 1,000,003 elements in 8.0 microseconds. Using the 12-byte algorithmic floor, what useful bandwidth does that imply, and why doesn't it prove HBM delivered that rate?

Answer

The calculation is 12×1,000,0038.0×10−6\frac{12 \times 1{,}000{,}003}{8.0 \times 10^{-6}}8.0×10−612×1,000,003​, or about 1,5001{,}5001,500 GB/s. That number counts algorithmic input and output bytes. It isn't a device-memory traffic counter, so cache hits can make useful bandwidth exceed the GPU's HBM specification without violating a hardware limit.

Prove output before studying speed

A trusted reference should be simpler than the candidate and independent enough to catch its mistakes. For triad, compute on the CPU in double, cast once to float, and compare every element. Both paths use a fused multiply-add (FMA), which rounds multiplication plus addition once rather than after each operation.[1]Reference 1CUDA Programming Guide.https://docs.nvidia.com/cuda/cuda-programming-guide/ A checksum is too weak: two wrong values can cancel, and a single out-of-bounds write may miss sampled positions.

Floating-point equality needs a numerical contract. PyTorch's allclose rule is a useful statement of it:[2]Reference 2torch.allclosehttps://docs.pytorch.org/docs/stable/generated/torch.allclose

∣yi−ri∣≤atol+rtol∣ri∣|y_i - r_i| \leq \text{atol} + \text{rtol}|r_i|∣yi​−ri​∣≤atol+rtol∣ri​∣

Here yiy_iyi​ is the candidate output and rir_iri​ is the reference. Relative tolerance scales with nonzero reference magnitude. Absolute tolerance protects values near zero, where relative error alone can explode. Neither constant is universal. Choose them from dtype, operation count, expected rounding, input range, and downstream quality requirements, then freeze them before comparing candidates.

For bounded float32 inputs and one fused multiply-add, this lab uses atol = 2e-6 and rtol = 2e-5. Those are acceptance settings for this workload, not recommended defaults for arbitrary kernels. A reduction over thousands of values needs different analysis because accumulation order changes rounding.

Exercise the contract across shapes and values that expose distinct failure modes:

TestWhy it existsFailure signature
n=0n=0n=0 and n=1n=1n=1empty and smallest legal worklaunch or guard assumption
n=255,256,257n=255,256,257n=255,256,257both sides of one block boundarylast-block indexing bug
n=1,000,003n=1{,}000{,}003n=1,000,003realistic rounded-up gridrare tail corruption
zeros and cancellationreference values near zeromissing absolute tolerance
largest lab magnitude, 101010^{10}1010large-value rounding and cancellationabsolute- or relative-error spike
repeated seedsvaried values with reproducible inputsinput-sensitive mismatch

Reference comparison catches wrong values that reach output. Runtime tools catch illegal execution that happens to leave checked values unchanged. NVIDIA Compute Sanitizer is a functional correctness suite: memcheck detects out-of-bounds and misaligned memory accesses, racecheck detects shared-memory hazards, initcheck detects uninitialized device-memory reads, and synccheck detects invalid synchronization use.[3]Reference 3Compute Sanitizerhttps://docs.nvidia.com/compute-sanitizer/ComputeSanitizer/

Run memcheck first. NVIDIA specifically recommends that order before tools such as racecheck and synccheck, because those tools don't replace memory-access checking.[3]Reference 3Compute Sanitizerhttps://docs.nvidia.com/compute-sanitizer/ComputeSanitizer/

Diagram showing Candidate + fixed workload, Reference + tolerance gate, Sanitizers clean?, and No.
Candidate + fixed workload, Reference + tolerance gate, Sanitizers clean?, and No.

The order prevents a familiar trap. A profiler can report excellent memory throughput for a kernel that reads one element past the buffer. High utilization never upgrades undefined behavior into a valid optimization.

Choose instrument by question

Start wide enough to see ownership of delay, then zoom into one proven hotspot. NVIDIA separates those scopes across Nsight Systems and Nsight Compute.[4]Reference 4Nsight Systems User Guidehttps://docs.nvidia.com/nsight-systems/UserGuide/[5]Reference 5Nsight Compute Profiling Guidehttps://docs.nvidia.com/nsight-compute/ProfilingGuide/

QuestionInstrumentEvidence it can supplyWhat it can't establish alone
Why is application waiting?Nsight SystemsCPU scheduling, CUDA API calls, transfers, kernels, streams, synchronization, idle gapsexact instruction or memory limit inside one kernel
Why is this kernel slow?Nsight Computelaunch resources, SpeedOfLight sections, cache and device-memory traffic, warp stalls, Roofline pointuser-visible end-to-end latency or output correctness
Is CUDA execution legal?Compute Sanitizerinvalid access, shared-memory hazard, uninitialized read, invalid barrier userepresentative performance under normal execution
How long did completed device work take?CUDA events or a synchronizing benchmark harnessdevice elapsed time for declared event boundariescause of the elapsed time

Nsight Systems finds scope

Open a system trace before assuming the hottest-looking kernel owns the application delay. Look for large host-to-device copies, CPU input gaps, many tiny launches, stream serialization, blocking scalar reads, and idle GPU regions. A long kernel is actionable only if it lies on the critical path for the claim.

For this lab, event synchronization after every sample intentionally serializes launches. Nsight Systems should reveal that pattern. It's correct for the isolated kernel-latency question, but it would distort a throughput experiment that expects several kernels or requests in flight. Scope determines whether synchronization is a measurement boundary or a workload bug.

Nsight Compute explains one kernel

Once triad_f32 is a known hotspot, collect kernel metrics. Nsight Compute's report sections cover launch configuration, memory workload, GPU Speed of Light, and Roofline analysis. Its baseline feature can compare reports, but metric collection and kernel replay add overhead.[5]Reference 5Nsight Compute Profiling Guidehttps://docs.nvidia.com/nsight-compute/ProfilingGuide/

Never quote elapsed time from a heavily instrumented profiler run as a clean benchmark. Profile to explain. Rerun outside the profiler to measure.

Sanitizers run outside timing

Compute Sanitizer changes execution cost and may serialize behavior. A clean sanitizer report is correctness evidence, not latency sample. Preserve its command, tool version, exit status, and report alongside performance artifacts.

⚠️ Common mistake: Running every tool at once produces one expensive, perturbed execution whose timing answers no clean question. Keep correctness, diagnosis, and measurement as linked but separate runs.

Read Roofline and SpeedOfLight without guessing

The Roofline model bounds attainable performance by the smaller of the compute ceiling and the bandwidth ceiling scaled by arithmetic intensity:[6]Reference 6Roofline: An Insightful Visual Performance Model for Multicore Architectureshttps://doi.org/10.1145/1498765.1498785

P≤min⁡(Ppeak, I×BW)P \leq \min(P_{peak},\ I \times BW)P≤min(Ppeak​, I×BW)

Nsight Compute plots the achieved point against precision- and device-specific ceilings. The sloped region represents the memory-bandwidth bound; the flat region represents the peak-compute bound. The ridge is their intersection.[5]Reference 5Nsight Compute Profiling Guidehttps://docs.nvidia.com/nsight-compute/ProfilingGuide/

For triad, algorithmic intensity begins near 0.167 FLOPs/byte. If the profiler reports substantially more than 12 bytes of device-memory traffic per element, the measured intensity is even lower than the algorithmic estimate. That gap points toward transaction inefficiency or redundant traffic. If measured traffic stays near the compulsory minimum and the point approaches the bandwidth roof, adding shared memory probably can't remove two input reads and one output write. It may add instructions instead.

Read several sections together:

Nsight Compute evidenceQuestion answeredSound next move
LaunchStatsDid grid, block, registers, or shared memory constrain launch?change launch resources only when metric identifies pressure
SpeedOfLightWhich compute or memory paths approach throughput ceiling?inspect saturated path and corroborating stalls
MemoryWorkloadAnalysisHow much traffic reaches cache levels and device memory?compare measured bytes with algorithmic minimum
Roofline chartDoes achieved point sit in bandwidth or compute region?target bytes/reuse on slope; target math pipeline near flat roof

One percentage isn't diagnosis. Low compute utilization can mean memory limit, too little parallel work, launch overhead, dependencies, or host starvation. High device-memory utilization can coexist with wasteful transactions. Roofline locates achieved point; system trace and detailed counters explain why it landed there.

For streaming triad, likely sequence is:

  1. Reference and sanitizer pass.
  2. Nsight Systems confirms kernel dominates selected scope rather than copies or host gaps.
  3. Nsight Compute shows low arithmetic intensity and strong device-memory pressure.
  4. Measured bytes per element are compared with 12-byte algorithmic floor.
  5. Optimization targets excess traffic or access order. Shared memory is rejected when it can't remove compulsory traffic.

That conclusion may sound uneventful. Avoiding a useless rewrite saves engineering time and keeps risk out of the hot path.

Nsight Compute places triad_f32 near the sloped bandwidth roof, but Nsight Systems shows that the GPU is idle for most of the request while the CPU prepares inputs. What can you conclude, and which scope should you optimize first?

Answer

You can conclude that the isolated kernel is probably bandwidth-limited under the profiled workload. You can't conclude that kernel tuning will materially improve request latency. The system trace identifies CPU preparation on the request's critical path, so measure and repair that wider bottleneck before spending effort on a kernel already near its roof.

Design benchmark around real regime

“Kernel time” still leaves choices. State whether the product reuses the same resident data, streams new buffers, overlaps launches, or includes transfers. Different scopes can all be legitimate, but they can't share one unlabeled number.

Measurement questionIncludeExcludeTiming method
isolated launch latencyone kernel on resident buffersallocation and copiesCUDA events around launch
steady pipeline throughputrepresentative sequence with normal overlapone-time setupevents around batch or end-to-end synchronized wall clock
first request latencycompile, lazy initialization, allocation, and transfers that user experiencesunrelated process startupsynchronized end-to-end clock
cache-sensitive streamrotated working set sized beyond relevant cacheaccidental same-buffer reuseevents plus cache counters

Synchronize boundaries, not every operation

CUDA launches are asynchronous with respect to the host. A CPU timer can stop after enqueue while the device still works. CUDA events record progress in a stream and can measure elapsed device time after the end event completes.[7]Reference 7CUDA semantics.https://docs.pytorch.org/docs/2.13/notes/cuda.html

For one launch:

  1. Finish unrelated setup before start event.
  2. Record start event in same stream as kernel.
  3. Launch kernel.
  4. Record end event.
  5. Wait for end event, then read elapsed time.

A device-wide cudaDeviceSynchronize() inside the timed region adds a boundary that the production path may not contain. Use an event dependency when it matches the question. For tiny kernels, time a batch of launches and divide when event overhead is large relative to the work, while preserving the same batch size for the baseline and candidate.

PyTorch's benchmark utilities address accelerator synchronization and warmup for similar reasons.[8]Reference 8PyTorch Benchmarkhttps://docs.pytorch.org/tutorials/recipes/recipes/benchmark.html A harness is helpful, but its defaults still need to match intended scope.

Warm until mechanism is steady

Early iterations can include CUDA context creation, allocator growth, library initialization, just-in-time compilation, page mapping, cache fill, and clock ramp. Warmup count isn't magic. Watch latency, clocks, temperature, and selected algorithm until they stabilize, then record count and stopping rule.

Don't silently discard inconvenient later samples. Thermal or power throttling after warmup is part of the environment, not an outlier by definition.

Name cache policy

Reusing one buffer can make data cache-resident. Rotating across a working set larger than the relevant cache better represents streaming data. Neither regime is universally honest.

Schematic normalized latency over six observations for three GPU benchmark regimes. An un-warmed run starts high and settles, a same-buffer run stays lower when its working set remains cache-resident, and warmed rotating buffers remain near the intended steady-state level. The values compare measurement regimes, not hardware results.
First-use work inflates early samples, while one reused buffer can create a different cache-hot workload. Warmup removes setup bias; buffer rotation changes data-residency regime. Pick regime from product path and label it.

The plotted values are schematic, not benchmark results. In an actual run, the total rotated footprint should exceed the target cache by a deliberate margin, and Nsight Compute cache metrics should confirm the assumed regime. Allocation size alone can't prove every access missed the cache.

Record clocks, power, and thermal state

GPU clocks respond to idle state, temperature, power limit, and other work. A candidate measured after card heats up can look slower even when its code is better. NVIDIA System Management Interface (nvidia-smi) exposes scriptable queries, supported clock controls, and power-limit state.[9]Reference 9nvidia-smi documentationhttps://docs.nvidia.com/deploy/nvidia-smi/index.html

Capture the state before and after each comparison:

terminal
1nvidia-smi --query-gpu=timestamp,uuid,name,driver_version,pstate,clocks.sm,clocks.mem,temperature.gpu,power.draw,power.limit --format=csv

On isolated, administrator-controlled hardware, supported clock locking can reduce variance. It requires privileges and differs by GPU. Users on a shared fleet should record observed clocks and power instead of changing machine policy. Either way, the baseline and candidate need the same power limit, thermal envelope, and competing-work policy.

Preserve distribution

Reporting only the minimum rewards noise. A single mean hides tails. Keep raw samples, then report at least the sample count, median, p10, p90, and p10-to-p90 spread. Compare the baseline and candidate in alternating rounds such as A, B, B, A so gradual clock or thermal drift doesn't always favor the same version.

Within each controlled round rrr, compute speedup from like-for-like medians:

speedupr=baseline median latency in round rcandidate median latency in round r\text{speedup}_r = \frac{\text{baseline median latency in round }r}{\text{candidate median latency in round }r}speedupr​=candidate median latency in round rbaseline median latency in round r​

Then summarize the distribution of round-level speedups. State the number of rounds and a spread or interval instead of treating 200 adjacent launches on one thermal trajectory as 200 independent comparisons. Also report absolute time. A 2x speedup from 2 microseconds to 1 microsecond may disappear in an application dominated by launch, copies, or queueing.

Two controlled rounds produce baseline and candidate medians of (12, 10) and (9, 10) microseconds. What are the round speedups, and why is one pooled speedup inadequate?

Answer

The round speedups are 12/10=1.2×12/10 = 1.2\times12/10=1.2× and 9/10=0.9×9/10 = 0.9\times9/10=0.9×. The candidate wins one round and loses the other, so pooling samples into one number would hide a reversal that may come from drift or an unstable effect. Report both round-level results, inspect clocks and temperature, and collect more alternating rounds before making a speed claim.

Run the triad investigation

The lab uses one complete CUDA file. It includes buggy and fixed kernels, deterministic seeded inputs, a CPU reference, a tolerance check, event timing, cache-hot and buffer-rotated modes, and ordered raw-sample CSV output. --profile launches one fixed kernel so Nsight Compute doesn't collect hundreds of samples.

triad_bench.cu
1#include <cuda_runtime.h> 2 3#include <algorithm> 4#include <cstddef> 5#include <cmath> 6#include <cstdlib> 7#include <fstream> 8#include <iomanip> 9#include <iostream> 10#include <string> 11#include <utility> 12#include <vector> 13 14static void cuda_check(cudaError_t status, const char* call) { 15 if (status != cudaSuccess) { 16 std::cerr << call << ": " << cudaGetErrorString(status) << "\n"; 17 std::exit(1); 18 } 19} 20 21#define CUDA_CHECK(call) cuda_check((call), #call) 22 23__global__ void triad_buggy( 24 const float* x, 25 const float* z, 26 float* y, 27 std::size_t n, 28 float alpha 29) { 30 const std::size_t i = blockIdx.x * blockDim.x + threadIdx.x; 31 if (i <= n) { 32 y[i] = fmaf(alpha, x[i], z[i]); 33 } 34} 35 36__global__ void triad_f32( 37 const float* x, 38 const float* z, 39 float* y, 40 std::size_t n, 41 float alpha 42) { 43 const std::size_t i = blockIdx.x * blockDim.x + threadIdx.x; 44 if (i < n) { 45 y[i] = fmaf(alpha, x[i], z[i]); 46 } 47} 48 49static void launch_triad( 50 bool buggy, 51 const float* x, 52 const float* z, 53 float* y, 54 std::size_t n, 55 float alpha, 56 cudaStream_t stream = nullptr 57) { 58 if (n == 0) { 59 return; 60 } 61 constexpr int threads = 256; 62 const int blocks = static_cast<int>((n + threads - 1) / threads); 63 if (buggy) { 64 triad_buggy<<<blocks, threads, 0, stream>>>(x, z, y, n, alpha); 65 } else { 66 triad_f32<<<blocks, threads, 0, stream>>>(x, z, y, n, alpha); 67 } 68 CUDA_CHECK(cudaGetLastError()); 69} 70 71struct Verification { 72 std::size_t failures; 73 double max_abs; 74 double max_rel; 75}; 76 77static Verification verify( 78 const std::vector<float>& x, 79 const std::vector<float>& z, 80 float* device_y, 81 std::size_t n, 82 float alpha 83) { 84 if (n == 0) { 85 return {0, 0.0, 0.0}; 86 } 87 88 std::vector<float> actual(n); 89 CUDA_CHECK(cudaMemcpy( 90 actual.data(), 91 device_y, 92 actual.size() * sizeof(float), 93 cudaMemcpyDeviceToHost 94 )); 95 96 constexpr double atol = 2e-6; 97 constexpr double rtol = 2e-5; 98 Verification result{0, 0.0, 0.0}; 99 100 for (std::size_t i = 0; i < n; ++i) { 101 const double reference64 = std::fma( 102 static_cast<double>(alpha), 103 static_cast<double>(x[i]), 104 static_cast<double>(z[i]) 105 ); 106 const float reference = static_cast<float>(reference64); 107 const double abs_error = std::abs( 108 static_cast<double>(actual[i]) - static_cast<double>(reference) 109 ); 110 const double rel_error = abs_error / std::max(std::abs(static_cast<double>(reference)), 1e-30); 111 const double allowed = atol + rtol * std::abs(static_cast<double>(reference)); 112 113 result.max_abs = std::max(result.max_abs, abs_error); 114 result.max_rel = std::max(result.max_rel, rel_error); 115 if (!std::isfinite(actual[i]) || abs_error > allowed) { 116 ++result.failures; 117 } 118 } 119 return result; 120} 121 122static void fill_inputs( 123 std::vector<float>& x, 124 std::vector<float>& z, 125 unsigned int seed, 126 float alpha 127) { 128 for (std::size_t i = 0; i < x.size(); ++i) { 129 const int x_code = static_cast<int>((i * 29 + seed * 17) % 251) - 125; 130 const int z_code = static_cast<int>((i * 31 + seed * 13) % 239) - 119; 131 x[i] = static_cast<float>(x_code) / 63.0f; 132 z[i] = static_cast<float>(z_code) / 71.0f; 133 if (i % 4096 == 0) { 134 x[i] = 0.0f; 135 z[i] = 0.0f; 136 } else if (i % 4096 == 1) { 137 x[i] = 1.0f; 138 z[i] = -alpha; 139 } else if (i % 4096 == 2) { 140 x[i] = 1e10f; 141 z[i] = -1e10f; 142 } 143 } 144} 145 146static double percentile(const std::vector<float>& sorted, double q) { 147 const auto index = static_cast<std::size_t>(q * (sorted.size() - 1)); 148 return sorted[index]; 149} 150 151static std::string cuda_version(int encoded) { 152 return std::to_string(encoded / 1000) + "." + std::to_string((encoded % 1000) / 10); 153} 154 155int main(int argc, char** argv) { 156 const std::string mode = argc > 1 ? argv[1] : "--check"; 157 const bool bench_hot = mode == "--bench-hot"; 158 const bool bench_rotate = mode == "--bench-rotate"; 159 const bool benchmark = bench_hot || bench_rotate; 160 161 if ( 162 argc > 3 || 163 ( 164 mode != "--check" && 165 mode != "--buggy" && 166 mode != "--profile" && 167 !benchmark 168 ) 169 ) { 170 std::cerr << "usage: ./triad_bench [--check|--buggy|--profile|--bench-hot|--bench-rotate] [samples.csv]\n"; 171 return 2; 172 } 173 if (argc == 3 && !benchmark) { 174 std::cerr << "a sample CSV path is valid only for benchmark modes\n"; 175 return 2; 176 } 177 178 constexpr std::size_t n = 1'000'003; 179 constexpr float alpha = 1.25f; 180 const std::size_t bytes = n * sizeof(float); 181 182 cudaDeviceProp properties{}; 183 CUDA_CHECK(cudaGetDeviceProperties(&properties, 0)); 184 int runtime_version = 0; 185 int driver_api_version = 0; 186 CUDA_CHECK(cudaRuntimeGetVersion(&runtime_version)); 187 CUDA_CHECK(cudaDriverGetVersion(&driver_api_version)); 188 std::cout << "device=" << properties.name 189 << " compute_capability=" << properties.major << "." << properties.minor 190 << " l2_bytes=" << properties.l2CacheSize 191 << " cuda_runtime=" << cuda_version(runtime_version) 192 << " cuda_driver_api=" << cuda_version(driver_api_version) << "\n"; 193 194 std::vector<float> host_x(n); 195 std::vector<float> host_z(n); 196 fill_inputs(host_x, host_z, 7, alpha); 197 198 int slots = 1; 199 if (bench_rotate) { 200 const std::size_t slot_bytes = 3 * bytes; 201 const std::size_t target_bytes = std::max( 202 8 * slot_bytes, 203 2 * static_cast<std::size_t>(properties.l2CacheSize) + slot_bytes 204 ); 205 const std::size_t desired_slots = (target_bytes + slot_bytes - 1) / slot_bytes; 206 207 std::size_t free_bytes = 0; 208 std::size_t total_bytes = 0; 209 CUDA_CHECK(cudaMemGetInfo(&free_bytes, &total_bytes)); 210 const std::size_t affordable_slots = std::max<std::size_t>(1, free_bytes / (4 * slot_bytes)); 211 if (desired_slots > affordable_slots) { 212 std::cerr << "buffer rotation needs " << desired_slots * slot_bytes 213 << " bytes but conservative allocation budget is " 214 << affordable_slots * slot_bytes << " bytes\n"; 215 return 1; 216 } 217 slots = static_cast<int>(desired_slots); 218 std::cout << "rotation_target_bytes=" << target_bytes 219 << " rotation_working_set_bytes=" << slots * slot_bytes 220 << " free_bytes=" << free_bytes 221 << " total_bytes=" << total_bytes << "\n"; 222 } 223 224 std::vector<float*> xs(slots, nullptr); 225 std::vector<float*> zs(slots, nullptr); 226 std::vector<float*> ys(slots, nullptr); 227 228 for (int slot = 0; slot < slots; ++slot) { 229 CUDA_CHECK(cudaMalloc(reinterpret_cast<void**>(&xs[slot]), bytes)); 230 CUDA_CHECK(cudaMalloc(reinterpret_cast<void**>(&zs[slot]), bytes)); 231 CUDA_CHECK(cudaMalloc(reinterpret_cast<void**>(&ys[slot]), bytes)); 232 CUDA_CHECK(cudaMemcpy(xs[slot], host_x.data(), bytes, cudaMemcpyHostToDevice)); 233 CUDA_CHECK(cudaMemcpy(zs[slot], host_z.data(), bytes, cudaMemcpyHostToDevice)); 234 } 235 236 if (mode == "--buggy") { 237 launch_triad(true, xs[0], zs[0], ys[0], n, alpha); 238 CUDA_CHECK(cudaDeviceSynchronize()); 239 std::cout << "buggy launch completed; inspect it under Compute Sanitizer\n"; 240 } else if (mode == "--profile") { 241 launch_triad(false, xs[0], zs[0], ys[0], n, alpha); 242 CUDA_CHECK(cudaDeviceSynchronize()); 243 std::cout << "profile launch complete n=" << n << "\n"; 244 } else if (mode == "--check") { 245 const std::vector<std::pair<std::size_t, unsigned int>> cases = { 246 {0, 7}, {1, 7}, {255, 7}, {256, 7}, {257, 7}, 247 {n, 7}, {n, 19}, {n, 101}, 248 }; 249 for (const auto& [test_n, seed] : cases) { 250 fill_inputs(host_x, host_z, seed, alpha); 251 CUDA_CHECK(cudaMemcpy(xs[0], host_x.data(), bytes, cudaMemcpyHostToDevice)); 252 CUDA_CHECK(cudaMemcpy(zs[0], host_z.data(), bytes, cudaMemcpyHostToDevice)); 253 launch_triad(false, xs[0], zs[0], ys[0], test_n, alpha); 254 CUDA_CHECK(cudaDeviceSynchronize()); 255 const Verification correctness = verify(host_x, host_z, ys[0], test_n, alpha); 256 std::cout << std::scientific 257 << "seed=" << seed 258 << " n=" << test_n 259 << " correctness=" << (correctness.failures == 0 ? "PASS" : "FAIL") 260 << " failures=" << correctness.failures 261 << " max_abs=" << correctness.max_abs 262 << " max_rel=" << correctness.max_rel << "\n"; 263 if (correctness.failures != 0) { 264 return 1; 265 } 266 } 267 } else { 268 launch_triad(false, xs[0], zs[0], ys[0], n, alpha); 269 CUDA_CHECK(cudaDeviceSynchronize()); 270 const Verification correctness = verify(host_x, host_z, ys[0], n, alpha); 271 std::cout << std::scientific 272 << "correctness=" << (correctness.failures == 0 ? "PASS" : "FAIL") 273 << " failures=" << correctness.failures 274 << " max_abs=" << correctness.max_abs 275 << " max_rel=" << correctness.max_rel << "\n"; 276 277 if (correctness.failures != 0) { 278 return 1; 279 } 280 281 const int warmup = std::max(20, 2 * slots); 282 constexpr int samples = 200; 283 for (int i = 0; i < warmup; ++i) { 284 const int slot = i % slots; 285 launch_triad(false, xs[slot], zs[slot], ys[slot], n, alpha); 286 } 287 CUDA_CHECK(cudaDeviceSynchronize()); 288 289 cudaEvent_t start{}; 290 cudaEvent_t stop{}; 291 CUDA_CHECK(cudaEventCreate(&start)); 292 CUDA_CHECK(cudaEventCreate(&stop)); 293 std::vector<float> sample_ms; 294 sample_ms.reserve(samples); 295 296 for (int i = 0; i < samples; ++i) { 297 const int slot = i % slots; 298 CUDA_CHECK(cudaEventRecord(start)); 299 launch_triad(false, xs[slot], zs[slot], ys[slot], n, alpha); 300 CUDA_CHECK(cudaEventRecord(stop)); 301 CUDA_CHECK(cudaEventSynchronize(stop)); 302 float elapsed_ms = 0.0f; 303 CUDA_CHECK(cudaEventElapsedTime(&elapsed_ms, start, stop)); 304 sample_ms.push_back(elapsed_ms); 305 } 306 307 const std::string sample_path = argc == 3 308 ? argv[2] 309 : (bench_hot ? "samples-hot.csv" : "samples-rotate.csv"); 310 std::ofstream sample_file(sample_path); 311 if (!sample_file) { 312 std::cerr << "could not open sample CSV: " << sample_path << "\n"; 313 return 1; 314 } 315 sample_file << "sample,slot,elapsed_us\n" << std::fixed << std::setprecision(6); 316 for (int i = 0; i < samples; ++i) { 317 sample_file << i << "," << i % slots << "," << sample_ms[i] * 1000.0 << "\n"; 318 } 319 320 std::vector<float> sorted_ms = sample_ms; 321 std::sort(sorted_ms.begin(), sorted_ms.end()); 322 const double p10_ms = percentile(sorted_ms, 0.10); 323 const double median_ms = percentile(sorted_ms, 0.50); 324 const double p90_ms = percentile(sorted_ms, 0.90); 325 const double useful_gb_s = (3.0 * bytes) / (median_ms / 1000.0) / 1e9; 326 327 std::cout << std::fixed << std::setprecision(3) 328 << "regime=" << (bench_hot ? "cache-hot" : "buffer-rotated") 329 << " slots=" << slots 330 << " warmup=" << warmup 331 << " samples=" << samples 332 << " raw_samples=" << sample_path << "\n" 333 << "p10_us=" << p10_ms * 1000.0 334 << " median_us=" << median_ms * 1000.0 335 << " p90_us=" << p90_ms * 1000.0 << "\n" 336 << "useful_gb_s=" << useful_gb_s 337 << " (12 algorithmic bytes per element)\n"; 338 339 CUDA_CHECK(cudaEventDestroy(start)); 340 CUDA_CHECK(cudaEventDestroy(stop)); 341 } 342 343 for (int slot = 0; slot < slots; ++slot) { 344 CUDA_CHECK(cudaFree(xs[slot])); 345 CUDA_CHECK(cudaFree(zs[slot])); 346 CUDA_CHECK(cudaFree(ys[slot])); 347 } 348 return 0; 349}

Compile with optimization and line information. Line information lets sanitizer and profiler reports point back to source without using debug build as performance binary:

terminal
1mkdir -p receipts 2nvcc -O3 -lineinfo -std=c++17 triad_bench.cu -o triad_bench 3./triad_bench --check | tee receipts/correctness.txt

A correct run prints the target device and a complete elementwise check. The exact device name varies. These bounded inputs should normally agree bit for bit with the chosen fused reference, while the tolerance gate still records the acceptance contract:

Expected output shape
1device=<your GPU> compute_capability=<major.minor> l2_bytes=<bytes> cuda_runtime=<major.minor> cuda_driver_api=<major.minor> 2seed=7 n=0 correctness=PASS failures=0 max_abs=0.000000e+00 max_rel=0.000000e+00 3seed=7 n=1 correctness=PASS failures=0 max_abs=0.000000e+00 max_rel=0.000000e+00 4seed=7 n=255 correctness=PASS failures=0 max_abs=0.000000e+00 max_rel=0.000000e+00 5seed=7 n=256 correctness=PASS failures=0 max_abs=0.000000e+00 max_rel=0.000000e+00 6seed=7 n=257 correctness=PASS failures=0 max_abs=0.000000e+00 max_rel=0.000000e+00 7seed=7 n=1000003 correctness=PASS failures=0 max_abs=0.000000e+00 max_rel=0.000000e+00 8seed=19 n=1000003 correctness=PASS failures=0 max_abs=0.000000e+00 max_rel=0.000000e+00 9seed=101 n=1000003 correctness=PASS failures=0 max_abs=0.000000e+00 max_rel=0.000000e+00

Now run the deliberately wrong i <= n guard under memcheck. --error-exitcode 99 makes a detected sanitizer error fail automation even when the target process exits cleanly, and pipefail keeps tee from hiding that status:

terminal
1set -o pipefail 2compute-sanitizer --error-exitcode 99 --tool memcheck ./triad_bench --buggy 2>&1 \ 3 | tee receipts/memcheck-buggy.txt

Because the grid rounds up and n=1,000,003n=1{,}000{,}003n=1,000,003, a thread with i == n executes. The report should identify an invalid global access in triad_buggy. Without the sanitizer, the launch may appear to complete. Replace the guard with i < n by using fixed mode, then require a clean report:

terminal
1set -o pipefail 2compute-sanitizer --error-exitcode 99 --tool memcheck ./triad_bench --check 2>&1 \ 3 | tee receipts/memcheck-fixed.txt
Required sanitizer ending
1ERROR SUMMARY: 0 errors

If next candidate adds shared memory or barriers, extend gate with racecheck, initcheck, and synccheck. Current triad has neither shared-memory communication nor block barriers, so those tools shouldn't be added as ceremony.

Capture system timeline next. Timings printed while profiler is attached are diagnostic only:

terminal
1nsys profile \ 2 --trace=cuda,osrt \ 3 --sample=none \ 4 --output=receipts/triad_systems \ 5 ./triad_bench --bench-rotate receipts/nsys-diagnostic-samples.csv

The trace should show warmup launches followed by one launch and one event wait per sample. Unexpected copies or long host gaps would contradict the isolated resident-buffer claim.

Collect one kernel profile with Roofline set:

terminal
1ncu \ 2 --set roofline \ 3 --launch-count 1 \ 4 --export receipts/triad_compute \ 5 ./triad_bench --profile

On a locked-down host, ncu may return ERR_NVGPUCTRPERM. Counter-access policy blocked metric collection; the kernel didn't fail. Use an approved profiling host or ask an administrator to enable performance counters rather than changing privilege policy on a shared machine.[5]Reference 5Nsight Compute Profiling Guidehttps://docs.nvidia.com/nsight-compute/ProfilingGuide/

Read achieved point, device-memory bytes, cache behavior, and SpeedOfLight sections together. Compare measured traffic per element with 12-byte algorithmic floor. A cache-hot run may report useful bandwidth above device HBM specification because useful-byte calculation counts algorithmic bytes while data is served by cache. That's evidence of different regime, not impossible hardware.

Finally collect clean timing outside tools:

terminal
1nvidia-smi --query-gpu=timestamp,uuid,name,driver_version,pstate,clocks.sm,clocks.mem,temperature.gpu,power.draw,power.limit --format=csv \ 2 | tee receipts/gpu-state-before.csv 3 4./triad_bench --bench-hot receipts/bench-hot-samples.csv | tee receipts/bench-hot.txt 5./triad_bench --bench-rotate receipts/bench-rotate-samples.csv | tee receipts/bench-rotate.txt 6 7nvidia-smi --query-gpu=timestamp,uuid,name,driver_version,pstate,clocks.sm,clocks.mem,temperature.gpu,power.draw,power.limit --format=csv \ 8 | tee receipts/gpu-state-after.csv

The starter warms for at least 20 launches and enough launches to touch every rotation slot twice. Treat that as an initial policy, not proof of steady state. Increase it if latency, clocks, temperature, or cache counters haven't stabilized on the target machine.

Don't compare hot baseline with rotated candidate. Run both versions in same regime and alternate order across rounds.

Diagnose contradictory evidence

GPU investigations often return signals that look inconsistent. Resolve by asking which scope each signal describes.

ObservationTempting conclusionBetter diagnosisNext check
host timer is tinykernel is fasttimer stopped after enqueueCUDA events or synchronized boundary
sanitizer fails but sampled outputs matcherror is harmlessillegal access missed sampled statereject candidate and repair bounds
nvidia-smi utilization is highkernel is efficientGPU stayed busy doing some workNsight Systems critical path, then kernel counters
SpeedOfLight memory percentage is highno optimization remainsone memory path is busy, perhaps wastefullybytes per element and transaction efficiency
cache-hot timing winscandidate is universally fasterbenchmark changed residency regimesame-buffer versus rotated A/B under same policy
profiler duration is sloweroptimization regressedinstrumentation or replay perturbed executionclean rerun outside profiler
median improves while p90 worsensclear windistribution changed or throttling emergedraw samples, clocks, power, temperature, paired order
kernel improves but request latency doesn'tprofiler is wrongoptimized scope isn't end-to-end bottleneckreturn to Nsight Systems and critical path

Failure diagnosis should end with one controlled experiment. Changing block size, vector width, memory layout, and dtype together destroys attribution even if final number improves.

Preserve evidence receipt

A chat message saying “A is faster” isn't reproducible. Save a machine-readable receipt plus raw artifacts. At minimum, another engineer should know exactly what ran, where it ran, what passed, and which files support the claim.

receipt.json
1{ 2 "claim": "candidate lowers buffer-rotated triad_f32 kernel latency versus baseline", 3 "source": { 4 "repository": "<repository URL>", 5 "commit": "<full commit SHA>", 6 "file_sha256": "<triad_bench.cu SHA-256>", 7 "compiler": "nvcc <version>", 8 "flags": ["-O3", "-lineinfo", "-std=c++17"] 9 }, 10 "hardware": { 11 "gpu_uuid": "<GPU UUID>", 12 "gpu_name": "<exact model>", 13 "compute_capability": "<major.minor>", 14 "driver": "<driver version>", 15 "cuda_runtime": "<runtime version>", 16 "power_limit_w": "<watts>", 17 "clock_policy": "observed or locked", 18 "isolation": "<exclusive host, MIG slice, or competing-work policy>" 19 }, 20 "workload": { 21 "operation": "y[i] = fmaf(1.25f, x[i], z[i])", 22 "dtype": "float32", 23 "n": 1000003, 24 "threads_per_block": 256, 25 "cache_regime": "buffer-rotated", 26 "rotation_slots": "<measured run value>" 27 }, 28 "correctness": { 29 "reference": "CPU double FMA, one cast to float", 30 "atol": 0.000002, 31 "rtol": 0.00002, 32 "failures": 0, 33 "memcheck_errors": 0 34 }, 35 "measurement": { 36 "timer": "CUDA events in kernel stream", 37 "warmup": "<count and stability rule>", 38 "samples": 200, 39 "rounds": "<count>", 40 "order": "ABBA within each round", 41 "raw_samples": ["baseline-rotate.csv", "candidate-rotate.csv"] 42 }, 43 "artifacts": { 44 "systems": "triad_systems.nsys-rep", 45 "compute": "triad_compute.ncu-rep", 46 "sanitizer": "memcheck-fixed.txt", 47 "gpu_state_before": "gpu-state-before.csv", 48 "gpu_state_after": "gpu-state-after.csv" 49 }, 50 "result": { 51 "baseline_median_us": "<value>", 52 "candidate_median_us": "<value>", 53 "round_speedup_median": "<value>", 54 "round_speedup_interval": "<method and bounds>", 55 "end_to_end_effect": "<measured separately or explicitly out of scope>" 56 } 57}

Store reports with the source revision, not in a personal profiler workspace that disappears. Hash the source and raw sample files. If the workload contains proprietary input, archive the deterministic generator, shape distribution, and a redacted data fingerprint so the receipt remains useful without leaking data.

The small Python fixture below applies a release gate to one receipt. Its numbers are labeled fixture data; they aren't GPU measurements.

check-receipt-gate.py
1from statistics import median 2 3receipt_fixture = { 4 "baseline_us": [102.0, 101.0, 104.0, 100.0, 103.0], 5 "candidate_us": [82.0, 81.0, 83.0, 80.0, 82.0], 6 "correctness_failures": 0, 7 "sanitizer_errors": 0, 8 "same_workload": True, 9 "same_environment": True, 10 "raw_samples_saved": True, 11} 12 13baseline = median(receipt_fixture["baseline_us"]) 14candidate = median(receipt_fixture["candidate_us"]) 15speedup = baseline / candidate 16gates = [ 17 receipt_fixture["correctness_failures"] == 0, 18 receipt_fixture["sanitizer_errors"] == 0, 19 receipt_fixture["same_workload"], 20 receipt_fixture["same_environment"], 21 receipt_fixture["raw_samples_saved"], 22] 23 24print("fixture baseline median:", baseline, "us") 25print("fixture candidate median:", candidate, "us") 26print(f"fixture speedup: {speedup:.2f}x") 27print("claim ready:", all(gates))
Output
1fixture baseline median: 102.0 us 2fixture candidate median: 82.0 us 3fixture speedup: 1.24x 4claim ready: True

Flip any gate to False. The speedup remains numerically the same, but the claim becomes unpublishable. That's intended behavior.

A defensible optimization loop

Keep the loop narrow:

  1. State claim. Name workload, baseline, scope, correctness, and environment.
  2. Prove semantics. Run reference comparison across adversarial shapes and values.
  3. Check execution. Require relevant Compute Sanitizer tools to pass.
  4. Find scope. Use Nsight Systems to locate delay on application critical path.
  5. Explain kernel. Use Nsight Compute and Roofline only for confirmed hotspot.
  6. Change one mechanism. Target measured traffic, launch cost, occupancy constraint, or math pipeline.
  7. Rerun clean. Warm, control cache regime, record clocks and power, preserve raw samples.
  8. Archive receipt. Link correctness, reports, environment, and result to exact source revision.

Optimization earns trust when the faster candidate remains the same program for approved inputs and the evidence survives another engineer's rerun. Kernel engineering starts after that foundation, not before it.

Mastery check

Evaluation rubric

  • Foundational: Separate reference checking, sanitizer evidence, profiler diagnosis, and clean timing into distinct runs.
  • Intermediate: Compute useful bandwidth from declared algorithmic bytes while distinguishing it from measured HBM traffic.
  • Advanced: Design alternating benchmark rounds, interpret system and kernel profiles at their proper scopes, and defend a speedup from a reproducible evidence receipt.

Common pitfalls

  • Treating a clean sampled output as proof that an illegal memory access is harmless.
  • Comparing a cache-hot candidate with a buffer-rotated baseline.
  • Publishing profiler duration as clean latency or pooling samples across drifting rounds.
Complete the lesson

Mastery Check

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

1.A model-serving trace has long CPU gaps between groups of short CUDA kernels, but no single kernel dominates GPU time. Which first tool and interpretation fit evidence?

Correct answer: Nsight Systems for cross-CPU/GPU launch cadence and idle gaps

Observed problem crosses CPU and GPU scheduling. Nsight Systems keeps that whole-run timeline. Nsight Compute explains internals of one selected kernel after system trace proves it matters.

2.A candidate passes one divisible-by-256 input but Compute Sanitizer reports invalid global access for n = 1,000,003. Sampled output values still match. What is correct release decision?

Correct answer: Reject it until bounds and all correctness gates pass

Illegal access is correctness failure even when sampled values survive. Divisible shapes hid tail thread, and profiler utilization can't validate undefined execution.

3.Reference value is close to zero. Why is an absolute-tolerance term needed alongside relative tolerance?

Correct answer: It gives a finite error scale when reference is near zero

The closeness rule adds atol to magnitude-scaled rtol * |reference|. Near zero, relative term becomes tiny, so declared absolute floor controls acceptable rounding.

4.CPU host timer reports 3 microseconds around CUDA call, while CUDA event reports 70 microseconds after completion. Which number answers isolated completed kernel latency?

Correct answer: 70 microseconds, because events bound completed device work

CUDA launch is asynchronous with host. Host timer can stop after enqueue. Recorded events in kernel stream, read after end event completes, measure selected device interval.

5.Same-buffer triad run is much faster than buffer-rotated run. Which conclusion is justified before accepting either as baseline?

Correct answer: They are different residency regimes; match A/B policy

Repeated buffer may be cache-hot, while rotation can represent streaming data. Either can be valid when it matches intended path, but comparing versions across regimes confounds implementation with residency.

6.Nsight Compute places correct triad kernel near sloped bandwidth roof and measured traffic is already close to 12-byte algorithmic minimum per element. Which next step has strongest support?

Correct answer: Skip staging unless it removes traffic; recheck wider scope

Triad must read two inputs and write one output. When measured traffic approaches that floor and point approaches bandwidth roof, shared-memory staging may add work without removing compulsory bytes.

7.Candidate runs second in every round and looks 6 percent slower while power draw reaches limit and clocks fall over time. Which comparison repair best controls drift?

Correct answer: Capture state and alternate paired A-B-B-A order

Fixed ordering confounds implementation with thermal and power history. Alternating paired order plus state capture exposes drift and gives both versions comparable conditions.

8.Nsight Compute reports candidate kernel 20 percent faster, but profiler used replay and clean event-timed run outside profiler shows no improvement. Which evidence should support speed claim?

Correct answer: Use clean timing; keep profiler metrics as diagnosis

Metric collection and replay perturb execution. Nsight Compute explains kernel behavior; clean rerun under declared benchmark protocol supplies performance number.

9.Receipt contains source hash, GPU UUID, raw timings, and 1.3x paired speedup, but omits reference tolerances and sanitizer outcome. Can it support a published speedup claim?

Correct answer: No, not until reference and sanitizer gates pass

Performance evidence contract includes correctness. Without reference acceptance and sanitizer outcome, faster timing may describe wrong or illegal execution.

9 questions remaining.

Next Step
Continue to GPU Kernel Performance Engineering Lab

You can now produce a defensible baseline and trace one bottleneck without mistaking profiler output for proof. The next lab holds launch geometry fixed while changing memory path and kernel structure one mechanism at a time.

PreviousAccelerator Architecture Field Guide
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

CUDA Programming Guide.

NVIDIA · 2026

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

torch.allclose

PyTorch Contributors · 2026 · Official documentation

https://docs.pytorch.org/docs/stable/generated/torch.allclose

Compute Sanitizer

NVIDIA · 2026 · Official documentation

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

Nsight Systems User Guide

NVIDIA · 2026 · Official documentation

https://docs.nvidia.com/nsight-systems/UserGuide/

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 semantics.

PyTorch Contributors · 2026

https://docs.pytorch.org/docs/2.13/notes/cuda.html

PyTorch Benchmark

PyTorch Contributors · 2026 · Official tutorial

https://docs.pytorch.org/tutorials/recipes/recipes/benchmark.html

nvidia-smi documentation

NVIDIA · 2026

https://docs.nvidia.com/deploy/nvidia-smi/index.html

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.