LeetLLM
My PlanLearnGlossaryTracksPracticeBlog
LeetLLM

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

Product

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

Legal

  • Terms of Service
  • Privacy Policy

© 2026 LeetLLM. All rights reserved.

All Topics
Your Progress
0%

0 of 196 articles completed

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

39 min read
Learning path
Step 145 of 196 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 identical 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. Now we turn those mechanics into an uncompromising measurement discipline built around five verification gates:

  1. Gate 1: Reference Verification. Prove numerical output matches an independent double-precision CPU reference within declared tolerances (∣yi−ri∣≤atol+rtol∣ri∣|y_i - r_i| \leq \text{atol} + \text{rtol}|r_i|∣yi​−ri​∣≤atol+rtol∣ri​∣), testing both power-of-two and arbitrary unaligned lengths.
  2. Gate 2: Compute Sanitizer. Verify runtime memory safety and synchronization integrity (memcheck, racecheck, initcheck, synccheck), ensuring illegal memory accesses don't hide behind lucky allocations.
  3. Gate 3: Clean Synchronized Timing. Measure steady-state GPU elapsed time using stream-bound CUDA events, warmups, and explicit L2 cache invalidation, isolating clean runs from profiler overhead.
  4. Gate 4: Hardware Roofline Bound. Compare measured memory traffic and compute throughput against the theoretical hardware ceilings, verifying the kernel doesn't violate physical bandwidth limits.
  5. Gate 5: Reproducible Receipt. Capture hardware state, GPU UUID, clocks, compiler flags, and raw sample hashes in an immutable record so any peer can reproduce the result.

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 a candidate replaces an existing triad_f32 implementation. Before opening a profiler, write the claim so another engineer could disprove it. The starter supplies a baseline and a deliberately incorrect variant, not two valid implementations with a measured speedup.

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 leaves 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:

Ialgorithmic=2 FLOPs12 bytes≈0.167 FLOPs/byteI_{algorithmic} = \frac{2\ \text{FLOPs}}{12\ \text{bytes}} \approx 0.167\ \text{FLOPs/byte}Ialgorithmic​=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.

Gate 1: Reference verification and numerical tolerance

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 to float, and compare every element. A fused multiply-add (FMA) rounds the multiplication and addition together rather than rounding their intermediate product. The CPU reference rounds in double and then converts to float; the GPU's fmaf rounds directly to float. Higher precision helps here, but two-stage rounding isn't a universal bitwise oracle.[1]Reference 1Floating Point and IEEE 754 Compliance for NVIDIA GPUshttps://docs.nvidia.com/cuda/floating-point/index.html

A checksum is too weak: two wrong values can cancel, and a single out-of-bounds write may miss sampled positions. Initialize output to a recognizable poison value before checking, so a skipped store can't inherit a previous correct result. Allocate each test's logical length, not a larger buffer that hides its tail overrun.

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

This CPU-only indexing exercise isolates the allocation trap. Python bounds checks stand in for the illegal access that a CUDA sanitizer should report; they don't simulate GPU execution. Predict why the same wrong guard behaves differently with spare allocation space.

expose-the-tail-allocation-trap.py
1import math 2 3def write_indexes(n, allocated, inclusive=False): 4 output = [math.nan] * allocated 5 launched = ((n + 255) // 256) * 256 6 for i in range(launched): 7 if i < n or (inclusive and i == n): 8 output[i] = float(i) # Python raises on an allocation overrun. 9 return output 10 11n = 257 12correct = write_indexes(n, n) 13oversized = write_indexes(n, 512, inclusive=True) 14print("logical output matches:", oversized[:n] == correct) 15print("illegal extra store:", oversized[n]) 16try: 17 write_indexes(n, n, inclusive=True) 18except IndexError: 19 print("exact allocation: tail overrun caught") 20print("empty input:", write_indexes(0, 0))
Output
1logical output matches: True 2illegal extra store: 257.0 3exact allocation: tail overrun caught 4empty input: []

Gate 2: Compute Sanitizer and runtime memory safety

Reference comparison catches wrong values that reach output. Runtime tools catch illegal execution that happens to leave checked values unchanged. NVIDIA Compute Sanitizer provides four specialized checkers:[3]Reference 3Compute Sanitizerhttps://docs.nvidia.com/compute-sanitizer/ComputeSanitizer/

  • memcheck: Detects out-of-bounds global, shared, and local memory accesses, misaligned memory instructions, and illegal device pointers.
  • racecheck: Detects shared-memory data hazards (RAW, WAR, WAW hazards where threads in a block access the same shared memory location without synchronization).
  • initcheck: Flags reads of uninitialized global memory, catching kernels that assume memory allocations come pre-zeroed.
  • synccheck: Catches illegal barrier synchronization, such as calling __syncthreads() inside divergent branches where not all warps arrive.

Run memcheck first. NVIDIA specifically recommends running memcheck before tools such as racecheck and synccheck, because race checking doesn't validate underlying pointer validity.[3]Reference 3Compute Sanitizerhttps://docs.nvidia.com/compute-sanitizer/ComputeSanitizer/ Always configure --padding 32 to detect memory overruns that would otherwise land safely inside allocator alignment padding, and pass --error-exitcode 99 so automated test runners fail immediately when errors occur.

Diagram showing Candidate + fixed workload, Gate 1: Reference passes?, No, and Reject and repair.
Candidate + fixed workload, Gate 1: Reference passes?, No, and Reject and repair.

This sequence prevents an embarrassing trap. A profiler can report record-breaking memory throughput for a kernel that reads past buffer bounds. High hardware utilization never upgrades undefined behavior into a valid optimization.

Profiler scope: Nsight Systems versus Nsight Compute

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 suppliesWhat it can't establish alone
Why is the 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 system-level bottlenecks

Open a system trace before assuming the hottest-looking kernel owns application delay. Nsight Systems attaches lightweight runtime hooks (CUPTI and OS scheduler tracing) with low overhead (typically 1% to 5%). Look for large host-to-device transfers, CPU preparation gaps, stream serialization, blocking scalar reads, and GPU idle bubbles. 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 reveals that pattern immediately. It's correct for the isolated kernel-latency question, but it would distort a throughput experiment that expects multiple kernels or requests in flight. Scope determines whether synchronization is a measurement boundary or an application bottleneck.

Nsight Compute diagnoses kernel microarchitecture

Once triad_f32 is a known hotspot on the critical path, collect kernel metrics. Nsight Compute hooks directly into hardware performance monitoring units (PMUs) on the streaming multiprocessors. Its report sections cover launch configuration, memory workload analysis, GPU Speed of Light, and Roofline analysis.[5]Reference 5Nsight Compute Profiling Guidehttps://docs.nvidia.com/nsight-compute/ProfilingGuide/

Its baseline feature compares two kernel runs side-by-side. However, microarchitectural profiling changes the physical execution conditions.

Profiler distortions: multi-pass replay and counter perturbation

Hardware performance counters on GPU streaming multiprocessors are physically limited. An SM can't record hundreds of architectural counters in a single execution pass. To assemble a comprehensive report across instructions, cache hierarchies, and pipeline stalls, Nsight Compute uses multi-pass kernel replay.

During replay, the profiler saves the GPU memory state and re-executes the exact same kernel dozens of times under different hardware counter configurations. By default, Nsight Compute also enforces --cache-control all, explicitly flushing L1 and L2 caches before every pass to ensure deterministic counter collection.

This mechanism introduces massive execution distortion:

  • A 15-microsecond kernel can take hundreds of milliseconds to execute under profiling.
  • Cache lines that would remain resident in a tight loop get evicted before each replay pass.
  • Warp scheduling interleaving and memory controller queues get serialized to capture fine-grained stalls.

Never quote elapsed time from an instrumented profiler report as a clean benchmark. Profilers explain why code behaves as it does; clean, un-instrumented benchmark runs measure how fast it actually finishes.

Sanitizers run outside timing

Compute Sanitizer instruments memory instructions and barrier synchronizations, adding substantial overhead and serializing concurrent warps. A clean sanitizer report is correctness evidence, not a latency sample. Run sanitizers to verify safety, then close them completely before taking benchmark measurements.

⚠️ Common mistake: Running every tool simultaneously produces an expensive, distorted execution whose timing answers no valid question. Keep correctness verification, microarchitectural diagnosis, and clean benchmark timing as separate, linked runs.

Gate 3: Synchronized timing and benchmark state control

“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 device intervalOne kernel on resident buffers, plus any gaps inside event boundariesAllocation 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

Host versus device asynchrony: timing streams, not launch queues

CUDA kernel launches (<<<grid, block, shared, stream>>> or cudaLaunchKernel) are non-blocking asynchronous calls. When host code launches a kernel, the CPU constructs a launch descriptor packet, writes it into the driver's ring buffer (push-buffer in pinned memory), and returns control to the CPU thread immediately. This handoff typically takes 2 to 5 microseconds.

If an engineer wraps a host timer (std::chrono::high_resolution_clock or Python's time.perf_counter()) around a kernel launch without stream synchronization, the timer stops the instant the CPU finishes enqueuing the packet. The GPU hasn't even scheduled the blocks on SMs! The resulting "speedup" is merely a measurement of how fast the CPU pushed a descriptor into a queue.

CUDA events solve this by placing timestamp markers into the execution stream:

cpp
1cudaEvent_t start, stop; 2CUDA_CHECK(cudaEventCreate(&start)); 3CUDA_CHECK(cudaEventCreate(&stop)); 4 5CUDA_CHECK(cudaEventRecord(start, stream)); 6launch_triad<<<blocks, threads, 0, stream>>>(...); 7CUDA_CHECK(cudaEventRecord(stop, stream)); 8 9CUDA_CHECK(cudaEventSynchronize(stop)); 10float elapsed_ms = 0.0f; 11CUDA_CHECK(cudaEventElapsedTime(&elapsed_ms, start, stop));

Here is what happens in hardware:

  1. cudaEventRecord(start, stream) writes an event marker into the stream FIFO.
  2. When the GPU execution engine reaches that marker, it records the hardware clock timestamp into the event object.
  3. The kernel executes on the SMs.
  4. When the kernel completes all thread blocks, the GPU reaches cudaEventRecord(stop, stream) and records the stop timestamp.
  5. cudaEventSynchronize(stop) halts the host thread until the GPU signals that the stop event has retired.
  6. cudaEventElapsedTime(&elapsed_ms, start, stop) calculates the true elapsed time directly from the hardware timestamps.

Coarse device synchronization (cudaDeviceSynchronize()) stalls every stream on the GPU, draining concurrent queues and killing pipeline overlap. Stream-specific event synchronization cleanly measures the targeted boundary without penalizing independent streams.

Constructed trace: CPU submission takes 3 microseconds, the event interval runs from 5 to 75 microseconds, and the kernel runs from 10 to 75. The event interval includes a 5-microsecond gap plus 65 microseconds of kernel execution.
Five intervals answer different questions in this constructed trace: 3 microseconds to submit to the queue, a 5-microsecond driver queue gap, 65 microseconds of actual SM compute, 70 microseconds between stream events, and 78 microseconds for a synchronized host wall-clock timer. Stream events measure completed work without masking queue gaps.

For tiny sub-microsecond kernels, time a batch of launches and divide by iteration count to amortize event overhead, keeping the batch size identical across comparisons. CUDA events provide roughly 0.5-microsecond resolution; zero or sub-microsecond measurements signal an insufficient measurement window rather than infinite speed.[6]Reference 6CUDA Runtime API: Event Managementhttps://docs.nvidia.com/cuda/cuda-runtime-api/group__CUDART__EVENT.html

L2 cache invalidation: preventing residency bias

Modern GPU cache hierarchies feature substantial L2 caches:

  • NVIDIA A100: 40 MB L2 cache
  • NVIDIA H100: 50 MB L2 cache
  • NVIDIA RTX 4090: 72 MB L2 cache
  • NVIDIA B200: 60 MB L2 cache

Now consider the triad working set for n=1,000,003n = 1{,}000{,}003n=1,000,003 single-precision floats. Each buffer (XXX, ZZZ, YYY) consumes 444 MB. Three buffers require 121212 MB total.

That 12 MB working set fits entirely inside the GPU's L2 cache!

In a naive benchmark loop that repeatedly launches on the same buffer addresses:

  • Launch 1 fetches data from HBM3 (yielding ~2.0 to 3.35 TB/s).
  • Launches 2 through 200 find every single cache line already resident in L2!
  • L2 cache delivers up to 12 TB/s of aggregate bandwidth on an H100.
  • The benchmark prints an elapsed time of ~3 microseconds, implying an impossible 4,0004{,}0004,000 GB/s of bandwidth.

An engineer rejoices: "Our kernel hit 4 TB/s on H100!" In reality, the kernel never touched high-bandwidth memory. It merely benchmarked L2 cache latency.

In production LLM serving or training pipelines, incoming user prompts and fresh activations stream from DRAM. They don't arrive pre-warmed in L2 cache. Benchmarking an un-rotated small buffer creates an artificial residency bias that vanishes in production.

Two techniques prevent L2 residency bias:

  1. Buffer Rotation: Allocate an array of SSS distinct buffer triplets such that total memory footprint exceeds 2×2\times2× the L2 cache capacity (e.g. S×12 MB≥100 MBS \times 12\ \text{MB} \geq 100\ \text{MB}S×12 MB≥100 MB). Cycle through slot = i % slots on every iteration, forcing every launch to fetch from DRAM.
  2. Cache Invalidation Sweep: If memory constraints prevent allocating multiple buffers, launch an explicit cache-clearing sweep kernel over a separate dummy buffer sized to 2×2\times2× L2 capacity between timed runs, evicting all cached lines back to DRAM.
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 overhead inflates early samples, while reusing one small buffer creates an artificial L2 cache-resident workload. Warmup stabilizes initial driver overhead; buffer rotation forces true device-memory traffic. Choose the regime that matches the production path and document it explicitly.

Warm until mechanism is steady

Early iterations absorb CUDA context initialization, memory allocator pool growth, library loading, just-in-time compilation, page table mapping, and GPU clock frequency ramping.

Warmup iterations aren't arbitrary. Monitor latency, clocks, and temperature until they stabilize, then freeze the warmup count. Never discard slow samples that occur later in the run: thermal or power throttling during sustained execution is a physical hardware constraint, not an outlier.

Record clocks, power, and thermal state

GPU clocks dynamically adjust based on thermal headroom, power limits, and concurrent workloads. A candidate kernel measured after the card has heated up can appear slower even when its instruction stream is superior. NVIDIA System Management Interface (nvidia-smi) provides scriptable state queries:[7]Reference 7nvidia-smi documentationhttps://docs.nvidia.com/deploy/nvidia-smi/index.html

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

On dedicated hardware with administrator privileges, lock clocks during benchmarking to eliminate frequency jitter:

terminal
1sudo nvidia-smi --lock-gpu-clocks=1980,1980

On shared cloud VMs where clock locking is restricted, query and log the actual observed clocks before and after every benchmark round. Ensure both baseline and candidate run within the same clock and thermal envelope.

Preserve the distribution with alternating paired rounds (ABBA)

Reporting only the minimum execution time rewards random noise. Reporting a single mean obscures bimodal behavior and tail latency. Preserve raw samples, reporting sample count, median, p10, p90, and the p10-to-p90 spread.

To prevent thermal drift or background host activity from favoring one implementation, alternate rounds in an A, B, B, A sequence. Within each round rrr, compute the paired speedup:

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 evaluate the distribution of round-level speedups. If a candidate wins round 1 (1.2×1.2\times1.2×) but loses round 2 (0.9×0.9\times0.9×), pooling all samples into a single average hides that the candidate regressed once the GPU reached thermal equilibrium.

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.

Gate 4: Hardware Roofline and Speed-of-Light bounds

The Roofline model bounds attainable performance by the smaller of the compute ceiling and the bandwidth ceiling scaled by arithmetic intensity:[8]Reference 8Roofline: 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. Compare byte counts and bandwidth at the same memory level: an L2 hit is still an input load, but not an HBM read. More than 12 HBM bytes per element can indicate transaction inefficiency or redundant traffic; fewer can indicate cache reuse or another traffic-reducing mechanism. Under a genuinely streaming regime, approaching both the useful-byte floor and the bandwidth roof leaves little for shared-memory staging to remove.

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, test this hypothesis:

  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.

If the measurements don't support that sequence, revise the hypothesis before changing the kernel.

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.

Run the triad investigation: five gates in code

The CUDA lab requires an NVIDIA GPU, a compatible driver/toolkit, and the profiling tools named below. It wasn't compiled or run on CUDA during this review; the displayed output is an acceptance shape, not a hardware result. CPU-only checks can validate the arithmetic, host helper functions, and receipt parser, but can't establish device correctness or speed.

The file includes buggy and fixed kernels, deterministic inputs, a CPU reference, event timing, same-buffer and buffer-rotated modes, and raw-sample CSV output. Read it in three parts: kernels and checking helpers, setup and test modes, then the timed loop. --profile launches one fixed kernel as an isolated diagnostic, not a warmed benchmark.

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

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 2set -o pipefail 3nvcc -O3 -lineinfo -std=c++17 triad_bench.cu -o triad_bench && \ 4 ./triad_bench --check | tee receipts/correctness.txt

A correct run prints the target device and a complete elementwise check. The output below describes the required shape, not a recorded CUDA run. Inspect the actual error values rather than requiring the zeros shown here. This fixed-size starter isn't a general allocator or launch planner; larger shapes need device-limit and integer-overflow checks too.

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 --padding 32 ./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. Padding improves detection when adjacent allocations might otherwise hide an overrun. Without the sanitizer, the launch may appear to complete. Use the fixed mode with i < n, then require a clean report.[3]Reference 3Compute Sanitizerhttps://docs.nvidia.com/compute-sanitizer/ComputeSanitizer/

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

Run initcheck when a candidate changes which global data is initialized or read; it doesn't require shared memory. Add racecheck for shared-memory communication and synccheck for synchronization changes. None replaces memcheck or numerical comparison.

Capture the system timeline next. Timings printed while the 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 full trace includes allocations, setup copies, the initial correctness readback, warmup, 200 timed launches, and final per-slot verification copies. Only the event-bounded sample region claims to exclude copies. Inside it, expect one launch and one event wait per sample; investigate long host gaps rather than mistaking setup or verification for timed work.

List the installed sections before collecting a kernel profile. Section sets vary by installation; don't assume a set named roofline exists. The command names four standard sections and makes cache flushing explicit. --clock-control none leaves existing clock policy unchanged.[9]Reference 9Nsight Compute CLIhttps://docs.nvidia.com/nsight-compute/NsightComputeCli/

terminal
1ncu --list-sets 2ncu --list-sections 3ncu \ 4 --section LaunchStats \ 5 --section SpeedOfLight \ 6 --section MemoryWorkloadAnalysis \ 7 --section SpeedOfLight_RooflineChart \ 8 --cache-control all \ 9 --clock-control none \ 10 --launch-count 1 \ 11 --export receipts/triad_compute \ 12 ./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 the achieved point, device-memory bytes, cache behavior, and SpeedOfLight sections together. Compare measured traffic per element with the 12-byte algorithmic count. Useful bandwidth above the device's HBM specification can reflect data served by cache, rather than impossible hardware performance.

This isolated, cache-flushed profile doesn't establish the residency of the clean same-buffer benchmark. Nsight Compute normally flushes caches between replay passes and may control clocks; replay mode also changes execution. For a cache-primed application, a separately designed experiment can use application replay with --cache-control none so its setup is repeated for each pass. Record these choices and never mix a cold profile's byte count with a hot run's elapsed time in one Roofline point.[5]Reference 5Nsight Compute Profiling Guidehttps://docs.nvidia.com/nsight-compute/ProfilingGuide/

Finally collect clean timing outside tools:

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

The post-timing check validates each slot's final output, not every intermediate launch. Keep adversarial correctness runs separate so an intermittent omitted store can't hide behind a previous correct value. Record nvcc, nsys, ncu, and compute-sanitizer versions alongside the exact commands; profiler section availability and supported metrics vary by tool and GPU.

--bench-hot is a convenient flag name, not evidence of cache residency: the program reports it as same-buffer. Percentiles use linear interpolation at index q(N−1)q(N-1)q(N−1), so an even-count median averages the middle pair. Samples remain in acquisition order in the CSV. Match the selected CUDA device to its UUID, especially when CUDA_VISIBLE_DEVICES remaps ordinals; a GPU name alone isn't enough on a multi-GPU host.

Don't compare a hot baseline with a rotated candidate. Run both versions in the 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.

Gate 5: Reproducible receipts and machine-readable evidence

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 "binary_sha256": "<candidate binary SHA-256>", 8 "compiler": "nvcc <version>", 9 "flags": ["-O3", "-lineinfo", "-std=c++17"] 10 }, 11 "baseline_source": { 12 "commit": "<baseline full commit SHA>", 13 "file_sha256": "<baseline source SHA-256>", 14 "binary_sha256": "<baseline binary SHA-256>" 15 }, 16 "hardware": { 17 "gpu_uuid": "<GPU UUID>", 18 "gpu_name": "<exact model>", 19 "compute_capability": "<major.minor>", 20 "driver": "<driver version>", 21 "cuda_runtime": "<runtime version>", 22 "power_limit_w": "<watts>", 23 "clock_policy": "observed or locked", 24 "isolation": "<exclusive host, MIG slice, or competing-work policy>" 25 }, 26 "workload": { 27 "operation": "y[i] = fmaf(1.25f, x[i], z[i])", 28 "dtype": "float32", 29 "n": 1000003, 30 "threads_per_block": 256, 31 "cache_regime": "buffer-rotated", 32 "rotation_slots": "<measured run value>" 33 }, 34 "correctness": { 35 "reference": "CPU double FMA, one cast to float", 36 "atol": 0.000002, 37 "rtol": 0.00002, 38 "failures": "<measured count>", 39 "memcheck_errors": "<parsed count>", 40 "sanitizer_exit_status": "<actual exit status>", 41 "sanitizer_version": "<version>" 42 }, 43 "measurement": { 44 "timer": "CUDA events in kernel stream", 45 "warmup": "<count and stability rule>", 46 "samples": 200, 47 "rounds": "<count>", 48 "order": "ABBA within each round", 49 "raw_samples": ["<round/block-specific baseline CSVs>", "<round/block-specific candidate CSVs>"], 50 "raw_sample_sha256": "<digest for each artifact>", 51 "quantiles": "linear interpolation at q*(N-1)", 52 "profiler_attached": false 53 }, 54 "artifacts": { 55 "systems": "triad_systems.nsys-rep", 56 "compute": "triad_compute.ncu-rep", 57 "sanitizer": "memcheck-fixed.txt", 58 "gpu_state_before": "gpu-state-before.csv", 59 "gpu_state_after": "gpu-state-after.csv" 60 }, 61 "result": { 62 "baseline_median_us": "<value>", 63 "candidate_median_us": "<value>", 64 "round_speedup_median": "<value>", 65 "round_speedup_interval": "<method and bounds>", 66 "end_to_end_effect": "<measured separately or explicitly out of scope>" 67 } 68}

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 Python fixture below reads real temporary CSV files containing synthetic timings. It checks saved-byte hashes, sample order, positive finite durations, and matching comparison conditions before computing one round's ratio. It deliberately doesn't print “claim ready”: artifact integrity and a favorable ratio don't prove correctness, representative hardware conditions, or a repeatable improvement.

check-receipt-gate.py
1import csv 2import hashlib 3import io 4import math 5from pathlib import Path 6from statistics import median 7from tempfile import TemporaryDirectory 8 9def read_samples(path, expected_sha256, slots): 10 if type(slots) is not int or slots < 1: 11 raise ValueError("positive slot count required") 12 data = Path(path).read_bytes() 13 if hashlib.sha256(data).hexdigest() != expected_sha256: 14 raise ValueError("sample artifact changed") 15 rows = csv.DictReader(io.StringIO(data.decode("utf-8"))) 16 if rows.fieldnames != ["sample", "slot", "elapsed_us"]: 17 raise ValueError("unexpected CSV schema") 18 samples = [] 19 for index, row in enumerate(rows): 20 if None in row or any(value is None for value in row.values()): 21 raise ValueError("malformed row") 22 if int(row["sample"]) != index or int(row["slot"]) != index % slots: 23 raise ValueError("sample sequence or rotation mismatch") 24 elapsed = float(row["elapsed_us"]) 25 if not math.isfinite(elapsed) or elapsed <= 0: 26 raise ValueError("duration must be finite and positive") 27 samples.append(elapsed) 28 if len(samples) < 2: 29 raise ValueError("at least two samples required") 30 return samples 31 32def compare_pair(baseline, candidate): 33 required = {"n", "dtype", "cache_regime", "slots", "gpu_uuid", "timer"} 34 if set(baseline["conditions"]) != required or baseline["conditions"] != candidate["conditions"]: 35 raise ValueError("comparison conditions differ or are incomplete") 36 a, b = [read_samples(r["path"], r["sha256"], r["conditions"]["slots"]) 37 for r in (baseline, candidate)] 38 if len(a) != len(b): 39 raise ValueError("different sample counts") 40 a_median, b_median = median(a), median(b) 41 ratio = a_median / b_median 42 if not all(math.isfinite(value) and value > 0 for value in (a_median, b_median, ratio)): 43 raise ValueError("invalid derived statistic") 44 return a_median, b_median, ratio 45 46conditions = {"n": 1_000_003, "dtype": "float32", "cache_regime": "buffer-rotated", 47 "slots": 2, "gpu_uuid": "fixture-only", "timer": "synthetic"} 48with TemporaryDirectory() as directory: 49 records = [] 50 for name, values in (("baseline", [102, 101, 104, 100, 103]), 51 ("candidate", [82, 81, 83, 80, 82])): 52 path = Path(directory) / f"{name}.csv" 53 text = "sample,slot,elapsed_us\n" + "".join( 54 f"{i},{i % 2},{value}\n" for i, value in enumerate(values)) 55 path.write_text(text, encoding="utf-8") 56 records.append({"path": path, "sha256": hashlib.sha256(path.read_bytes()).hexdigest(), 57 "conditions": conditions.copy()}) 58 a, b, ratio = compare_pair(*records) 59 print(f"synthetic medians: {a:g} us and {b:g} us; ratio {ratio:.2f}x") 60 print("timing direction:", "improvement" if ratio > 1 else "no improvement") 61 records[1]["path"].write_text("truncated", encoding="utf-8") 62 try: 63 compare_pair(*records) 64 except ValueError as error: 65 print("after artifact change:", error)
Output
1synthetic medians: 102 us and 82 us; ratio 1.24x 2timing direction: improvement 3after artifact change: sample artifact changed

A hash binds a receipt to bytes; it doesn't prove those bytes came from a GPU. Collect hardware state, source identity, correctness logs, sanitizer exit status, and timing artifacts through a trusted harness. This small parser checks only timing-file integrity and declared conditions, not that entire collection process. Run it per matched round, then examine the round-level distribution.

The five verification gates in production review

Before submitting or merging any GPU optimization patch, run through the complete verification sequence:

  1. Gate 1: Reference Verification. Test numerical equivalence against a higher-precision reference on power-of-two, prime, and unaligned lengths (n=0,1,255,256,257,1,000,003n = 0, 1, 255, 256, 257, 1{,}000{,}003n=0,1,255,256,257,1,000,003) with explicit poison patterns.
  2. Gate 2: Compute Sanitizer. Run compute-sanitizer --tool memcheck --padding 32 --error-exitcode 99 to prove that no out-of-bounds reads or writes occur on any exercised path.
  3. Gate 3: Synchronized Timing. Time completed GPU execution via stream-bound CUDA events (cudaEventRecord), warm the card past initial JIT and driver overhead, invalidate L2 cache lines across iterations, and run alternating ABBA rounds.
  4. Gate 4: Hardware Roofline Bound. Calculate arithmetic intensity and verify that measured memory bandwidth doesn't violate physical HBM limits. If bandwidth looks impossibly high, check for unevicted L2 cache hits.
  5. Gate 5: Reproducible Receipt. Persist the git commit SHA, binary hash, GPU UUID, driver version, clocks, power limits, raw sample CSVs, and sanitizer output in a structured receipt.json.

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.

Follow-up questions

Why can an i <= n kernel pass an elementwise check for n=257 when its allocation has 512 elements?

Answer

It writes all 257 logical outputs correctly and also writes element 257. A comparison restricted to the logical output misses that extra store, while a memory checker sees an allocation-valid access. Use exact-sized allocations, output poisoning, and sanitizer padding to expose different parts of the failure.

A warm same-buffer run is fast, but a cache-flushed Nsight Compute report shows high HBM traffic. Can you divide that report's bytes by the warm event duration?

Answer

No. The numerator and denominator describe different cache regimes. Use traffic and duration from a consistent profiling experiment for its Roofline point, and report the clean benchmark separately. Record replay, cache, and clock controls so the difference is interpretable.

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 tool and interpretation fit this evidence?

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

The problem crosses CPU and GPU scheduling. Nsight Systems preserves that whole-run timeline. Nsight Compute can explain a selected kernel after the system trace establishes that it matters.

2.A candidate must support arbitrary positive lengths. It passes a divisible-by-256 input, but Compute Sanitizer reports invalid global access for n = 1,000,003. Sampled values still match. What is the correct release decision?

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

Illegal access is a correctness failure even when sampled values survive. Divisible shapes hid the extra tail thread. Silently narrowing the supported workload doesn't satisfy the original contract.

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.An unsynchronized host timer reports 3 microseconds around a CUDA call; a same-stream event pair reports 70 microseconds after completion. Which number describes the completed device interval?

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

The host timer can stop after enqueue. The event pair measures the completed device interval, which can include submission gaps or interference inside its boundaries. It isn't necessarily 70 microseconds of kernel instructions alone.

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

Having built the five verification gates to prove correctness and isolate clean streaming timing, apply this discipline to optimize shared-memory tiling, register pressure, and warp divergence in production kernels.

PreviousAccelerator Architecture Field Guide
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

Floating Point and IEEE 754 Compliance for NVIDIA GPUs

NVIDIA · 2026

https://docs.nvidia.com/cuda/floating-point/index.html

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/

CUDA Runtime API: Event Management

NVIDIA · 2026

https://docs.nvidia.com/cuda/cuda-runtime-api/group__CUDART__EVENT.html

nvidia-smi documentation

NVIDIA · 2026

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

Roofline: An Insightful Visual Performance Model for Multicore Architectures

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

https://doi.org/10.1145/1498765.1498785

Nsight Compute CLI

NVIDIA · 2026

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

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.