LeetLLM
My PlanLearnGlossaryTracksPracticeBlog
LeetLLM

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

Product

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

Legal

  • Terms of Service
  • Privacy Policy

© 2026 LeetLLM. All rights reserved.

All Topics
Your Progress
0%

0 of 196 articles completed

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

GPU Compilers and Kernel Languages

Trace a GPU kernel from CUDA C++ or a tile language through compiler IR, PTX, cubin, and native instructions, then choose an authoring model by control, portability, and inspectable evidence.

37 min read
Learning path
Step 148 of 196 in the full curriculum
GPU Parallel Primitives LabGEMM From Naive CUDA to Tensor Cores

Personalize this lesson

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

Your vector-add kernel passed every correctness check yesterday. Suppose the same source now takes 300 milliseconds on its first call, or fails on a newer GPU with "no kernel image is available." Reading the source again won't explain either symptom. The GPU never executes CUDA C++ or Python directly.

The accelerator field guide mapped execution and memory owners across hardware. The parallel primitives lab then made synchronization scope, reduction trees, and prefix dependencies measurable. Keep those habits. Here we follow compiler artifacts between source and hardware, because each boundary owns a different class of bugs.

One addition, several representations

For 1,000 pairs of numbers, add each pair into a separate output slot. At index zero, 0+(−3)=−30 + (-3) = -30+(−3)=−3; at index one, 0.25+(−2)=−1.750.25 + (-2) = -1.750.25+(−2)=−1.75. In general, write outi=xi+yiout_i = x_i + y_iouti​=xi​+yi​ for each valid index iii.

CUDA C++ expresses the work from one thread's point of view. With 256 threads per block, 1,000 elements require four blocks: 1,024 threads, of which 24 must skip memory access. The same fixture below also checks lengths immediately around a block boundary.

Save this as vector_add.cu. It requires a CUDA toolkit to build and compatible NVIDIA hardware to run. The checks use small, exactly representable FP32 values; exact equality here isn't a general tolerance policy for reductions or matrix multiplication.

vector_add.cu
1#include <cuda_runtime.h> 2 3#include <cmath> 4#include <cstdlib> 5#include <iostream> 6#include <limits> 7#include <stdexcept> 8#include <vector> 9 10void check(cudaError_t status) { 11 if (status != cudaSuccess) { 12 std::cerr << cudaGetErrorString(status) << "\n"; 13 std::exit(1); 14 } 15} 16 17__global__ void add_kernel( 18 const float* x, 19 const float* y, 20 float* out, 21 int n 22) { 23 int i = blockIdx.x * blockDim.x + threadIdx.x; 24 if (i < n) { 25 out[i] = x[i] + y[i]; 26 } 27} 28 29void launch_add(const float* x, const float* y, float* out, int n) { 30 if (n < 0) throw std::invalid_argument("negative length"); 31 if (n == 0) return; // A zero-block GPU launch is invalid. 32 constexpr int threads = 256; 33 int blocks = (n + threads - 1) / threads; 34 add_kernel<<<blocks, threads>>>(x, y, out, n); 35} 36 37bool matches(const std::vector<float>& actual, 38 const std::vector<float>& expected) { 39 if (actual.size() != expected.size()) return false; 40 for (std::size_t i = 0; i < actual.size(); ++i) { 41 if (!std::isfinite(actual[i]) || actual[i] != expected[i]) return false; 42 } 43 return true; 44} 45 46bool run_case(int n) { 47 std::vector<float> x(n), y(n), expected(n), out(n); 48 for (int i = 0; i < n; ++i) { 49 x[i] = 0.25F * i; 50 y[i] = float(i % 7 - 3); 51 expected[i] = x[i] + y[i]; 52 } 53 54 float *d_x, *d_y, *d_out; 55 check(cudaMalloc(&d_x, n * sizeof(float))); 56 check(cudaMalloc(&d_y, n * sizeof(float))); 57 check(cudaMalloc(&d_out, n * sizeof(float))); 58 check(cudaMemcpy(d_x, x.data(), n * sizeof(float), cudaMemcpyHostToDevice)); 59 check(cudaMemcpy(d_y, y.data(), n * sizeof(float), cudaMemcpyHostToDevice)); 60 61 // All-one FP32 bit patterns are NaNs: skipped writes must fail the check. 62 check(cudaMemset(d_out, 0xff, n * sizeof(float))); 63 launch_add(d_x, d_y, d_out, n); 64 check(cudaGetLastError()); 65 check(cudaDeviceSynchronize()); 66 check(cudaMemcpy(out.data(), d_out, n * sizeof(float), cudaMemcpyDeviceToHost)); 67 68 check(cudaFree(d_x)); 69 check(cudaFree(d_y)); 70 check(cudaFree(d_out)); 71 72 bool correct = matches(out, expected); 73 std::cout << (correct ? "PASS" : "FAIL") << " n=" << n << "\n"; 74 return correct; 75} 76 77int main() { 78 // Test the checker itself: NaNs and wrong values must fail. 79 if (matches({std::numeric_limits<float>::quiet_NaN()}, {4.0F}) || 80 matches({3.0F}, {4.0F}) || matches({}, {4.0F})) return 1; 81 launch_add(nullptr, nullptr, nullptr, 0); // No GPU access. 82 for (int n : {1, 255, 256, 257, 1000}) { 83 if (!run_case(n)) return 1; 84 } 85 return 0; 86}

Successful execution prints PASS n=1, PASS n=255, PASS n=256, PASS n=257, and PASS n=1000. These are expected results, not measurements from this review's CPU-only environment. Distinct input values help catch misplaced writes; checking finiteness prevents a NaN from disappearing inside a maximum-error reduction. This fixture doesn't test large-index overflow, arbitrary strides, or aliasing.

At source level, you can inspect the bounds check and the index formula. You can't yet prove which machine instructions were emitted, how many physical registers they require, or whether a deployed binary contains code for the target GPU.

NVIDIA's offline compiler driver, nvcc, coordinates separate host and device compilation. For this SIMT device kernel, the compilation path reaches Parallel Thread Execution (PTX), a versioned virtual instruction set architecture (ISA). PTX acts as a stable, hardware-agnostic intermediate representation. Crucially, PTX operates over an unbounded virtual register space (such as %r0, %r1, %f0, %f1). It doesn't know how many physical registers exist on the target GPU, nor does it concern itself with warp scheduler stalls or instruction issue slots.[1]Reference 1NVIDIA CUDA Compiler Driver NVCChttps://docs.nvidia.com/cuda/cuda-compiler-driver-nvcc/

The optimizing assembler, ptxas, transforms that virtual PTX into a CUDA binary, called a cubin, for a specific streaming multiprocessor target such as sm_90. A cubin is a standard ELF container holding encoded GPU machine instructions plus symbols, relocations, and resource metadata.[2]Reference 2CUDA Binary Utilitieshttps://docs.nvidia.com/cuda/cuda-binary-utilities/ In this translation, ptxas isn't a passive 1-to-1 translator:

  • It performs physical register allocation, mapping unbounded virtual registers into the SM's finite register file through graph coloring and live-range analysis.
  • It performs instruction scheduling, reordering instructions to interleave independent math between memory load issues and their arrival, hiding memory and arithmetic latency.
  • It injects hardware control codes, setting per-instruction stall counts, yield flags, and scoreboard dependency barriers that direct hardware warp schedulers.
  • It manages register spilling, inserting spill and fill instructions to thread-private .local memory if register demand exceeds physical constraints.

Engineers commonly call the human-readable native instruction listing SASS. SASS isn't an executable artifact or a portable source format. It's a disassembly of target-specific machine instructions stored in a cubin.

To see this transformation concretely, compare the virtual PTX emitted for the core vector add against the native SASS disassembled from the resulting sm_90 cubin:

ptx
1// Virtual PTX: unbounded virtual registers (%f1, %f2, %f3) 2ld.global.f32 %f1, [%rd3]; 3ld.global.f32 %f2, [%rd4]; 4add.f32 %f3, %f1, %f2; 5st.global.f32 [%rd5], %f3;
text
1// Native SASS (sm_90): physical hardware registers (R2, R3, R4, R6) 2LDG.E.SYS R2, [R2.64] ; 3LDG.E.SYS R3, [R4.64] ; 4FADD R2, R2, R3 ; 5STG.E.SYS [R6.64], R2 ;

Notice the register reuse in SASS: ptxas reused physical register R2 for both the first loaded float and the final addition result, freeing up registers for other concurrent warps.

There can also be an earlier compiler layer. NVVM intermediate representation (IR) is NVIDIA's GPU-oriented subset and extension of LLVM IR. Language frontends can generate NVVM IR, and libNVVM compiles compatible NVVM IR into PTX.[3]Reference 3NVVM IR Specificationhttps://docs.nvidia.com/cuda/nvvm-ir-spec/[4]Reference 4libNVVM APIhttps://docs.nvidia.com/cuda/libnvvm-api/ That makes this conceptual path useful:

source→LLVM/NVVM IR→PTX→ptxas→cubin (SASS)\text{source} \rightarrow \text{LLVM/NVVM IR} \rightarrow \text{PTX} \rightarrow \text{ptxas} \rightarrow \text{cubin (SASS)}source→LLVM/NVVM IR→PTX→ptxas→cubin (SASS)

Don't turn a conceptual compiler layer into a deployment promise. nvcc publicly documents source-to-PTX and PTX-to-cubin stages; it doesn't promise a stable, human-readable NVVM file for every build. Another frontend may use different IRs before it reaches PTX, and a non-NVIDIA backend won't target PTX at all. CUDA Tile, introduced later, has a separate Tile IR path; the SIMT path here isn't a universal pipeline for every CUDA language.

The GPU compilation pipeline shows CUDA C++ and Triton converging through frontend IR and MLIR into PTX virtual ISA. ptxas performs physical register allocation and instruction scheduling to emit the cubin ELF binary, which the CUDA driver loads onto GPU silicon, while nvdisasm disassembles it into SASS for inspection.
CUDA C++ and Triton converge at the PTX virtual ISA. The ptxas assembler then maps virtual registers into physical hardware registers to produce the executable cubin ELF. SASS is an inspection disassembly produced by nvdisasm, not an executable artifact.

You find an FFMA instruction in nvdisasm output. Which layer has supplied the strongest evidence that this native operation exists?

Answer

The target cubin. nvdisasm reads encoded machine instructions from a cubin and renders them as SASS. A source expression or PTX instruction can suggest an FFMA, but a later compiler stage may still fuse, split, remove, or replace it.

nvcc builds a package, not one universal binary

A CUDA translation unit may contain CPU host code and GPU device code. nvcc separates those paths, sends the host path to a supported C++ compiler, compiles device code for requested virtual and real architectures, and embeds device images into the host object. The container of one or more device images is a fatbinary, usually shortened to fatbin.[1]Reference 1NVIDIA CUDA Compiler Driver NVCChttps://docs.nvidia.com/cuda/cuda-compiler-driver-nvcc/

The distinction between compute_90 and sm_90 records intent:

Target spellingArtifact meaningRuntime consequence
compute_90PTX using the virtual feature set associated with compute capability 9.0Driver may just-in-time compile it for a compatible current or later GPU
sm_90Cubin containing native code assembled for the 9.0 hardware targetCompatible GPU can load it without PTX code generation
Both in a fatbinNative code for known deployment plus PTX fallbackLoader prefers compatible binary and retains a forward-compatibility path

Ordinary PTX targets such as compute_90 provide a forward path to compatible later NVIDIA targets, not to older GPUs missing required features. The driver must also understand the emitted PTX ISA version. A toolkit upgrade can therefore raise the required driver version even when compute_90 stays unchanged.

Target suffixes matter: architecture-specific PTX such as compute_90a doesn't have the same forward-compatibility guarantee, and family-specific targets such as compute_100f restrict compatibility to their documented family. Cubins are narrower still, with compatibility rules within supported major compute-capability families, not across major versions. Check the exact target, not just the word “PTX.”[1]Reference 1NVIDIA CUDA Compiler Driver NVCChttps://docs.nvidia.com/cuda/cuda-compiler-driver-nvcc/

The runtime flow branches after packaging:

Diagram showing 1. Host launches kernel, 2. Compatible cubin present?, 3a. Load target code, and 3b. Compatible PTX present?.
1. Host launches kernel, 2. Compatible cubin present?, 3a. Load target code, and 3b. Compatible PTX present?.

This branch explains two otherwise confusing observations. A cold process may pay compilation or lazy-loading cost before the first kernel. A package may run on the build machine yet fail on another GPU because neither a compatible cubin nor usable PTX was embedded.

Produce artifacts you can inspect

On a supported CUDA development system, compile the CUDA source for one native Hopper target and keep PTX for future targets. --keep asks nvcc to retain supported intermediate files, while the two --generate-code entries package both sm_90 cubin and compute_90 PTX. Offline compilation and disassembly don't require a GPU, but they do require these NVIDIA tools and a supported host toolchain. Apple Clang alone can't run this lab. Use your deployment targets instead of copying sm_90 onto unrelated hardware.

terminal
1mkdir -p build/keep 2 3nvcc -O3 -lineinfo \ 4 --generate-code arch=compute_90,code=sm_90 \ 5 --generate-code arch=compute_90,code=compute_90 \ 6 --keep --keep-dir build/keep \ 7 vector_add.cu -o build/vector_add

Create standalone PTX and cubin files as well. Keeping both makes the boundary explicit and gives nvdisasm a cubin it can read directly.

terminal
1nvcc -O3 --ptx \ 2 --gpu-architecture=compute_90 \ 3 vector_add.cu -o build/vector_add.compute_90.ptx 4 5nvcc -O3 -lineinfo --cubin \ 6 --generate-code arch=compute_90,code=sm_90 \ 7 vector_add.cu -o build/vector_add.sm_90.cubin

Now inspect the package before judging performance. cuobjdump accepts a standalone cubin or a host executable, object, library, or external fatbin. nvdisasm accepts standalone cubins and adds richer control-flow and register-liveness views.[2]Reference 2CUDA Binary Utilitieshttps://docs.nvidia.com/cuda/cuda-binary-utilities/

terminal
1# What did the host executable package? 2cuobjdump --list-elf build/vector_add 3cuobjdump --list-ptx build/vector_add 4 5# Read virtual and native instruction forms. 6cuobjdump --dump-ptx build/vector_add > build/embedded.ptx 7cuobjdump --dump-sass build/vector_add > build/embedded.sass 8 9# Check per-kernel registers, local memory, shared memory, and stack use. 10cuobjdump --dump-resource-usage build/vector_add 11 12# Read one standalone cubin with source lines. 13nvdisasm --print-code \ 14 --print-line-info \ 15 build/vector_add.sm_90.cubin > build/vector_add.lines.sass 16 17# Generate a separate register-liveness view. 18nvdisasm --print-code \ 19 --print-life-ranges \ 20 build/vector_add.sm_90.cubin > build/vector_add.liveness.sass

Read these outputs as a chain of evidence:

  1. PTX header: Check .version, .target, address size, and kernel entry name. A mismatched .target can explain a deployment failure before launch.
  2. PTX body: Look for global loads, bounds predication, arithmetic, and the store. This confirms virtual operations, not final scheduling.
  3. Resource report: Record registers, static shared memory, local memory, and stack per kernel. Local memory can indicate spills, although not every local-memory use is a spill.
  4. SASS listing: Confirm native loads, arithmetic, stores, predicates, and architecture-specific instructions. Compare against compiler flags and source line info.
  5. Profiler trace: Measure executed instructions, memory behavior, and time on the target GPU. Static disassembly can't tell you which path dominates wall time.

⚠️ Common mistake: A shorter SASS listing isn't automatically faster. Instruction latency, issue rate, dependency chains, memory transactions, occupancy, and input-dependent control flow still decide runtime behavior.

Register pressure, occupancy cliffs, and spilling

When you inspect cuobjdump --dump-resource-usage or parse compiler logs, the most critical number is the register count per thread. That single metric dictates whether your kernel saturates GPU execution units or falls off a severe occupancy cliff.

Every modern NVIDIA Streaming Multiprocessor (SM), across architectures from Ampere (sm_80) and Ada (sm_89) to Hopper (sm_90) and Blackwell (sm_100), houses a physical register file of 64K (65,536) 32-bit registers. At the same time, an SM can host up to 2,048 active concurrent threads (organized as 64 warps of 32 threads).

To achieve 100% theoretical occupancy, all 2,048 threads must fit within the SM's register budget simultaneously:

65,536 registers2,048 threads=32 registers/thread\frac{65{,}536 \text{ registers}}{2{,}048 \text{ threads}} = 32 \text{ registers/thread}2,048 threads65,536 registers​=32 registers/thread

When each thread uses 32 registers or fewer, all 64 warps reside on the SM at once. The hardware warp schedulers have maximum freedom to swap out a stalled warp (waiting on global memory or math pipelines) and issue independent instructions from ready warps.

What happens when register demand creeps upward? The SM can't dynamically allocate fractions of registers. Instead, hardware allocates registers in fixed warp granularities, producing steep occupancy cliffs:

Registers per threadActive threads per SMActive warps per SMTheoretical occupancyHardware impact
≤\le≤ 322,04864100%Maximum warp concurrency to hide instruction latency
33 to 401,5364875%First occupancy cliff; 16 fewer warps to hide memory stalls
41 to 641,0243250%Half SM warp capacity
65 to 1285121625%Severe latency exposure; pipeline stalls become visible
129 to 255256812.5%Minimal latency hiding; memory stalls stall execution units
> 255000%Exceeds hardware limit; compilation fails or spills completely

These transitions aren't gentle slopes. Adding a single local variable or unrolling an inner loop that pushes register count from 32 to 33 immediately drops thread occupancy by 25%.

The spilling disaster: local memory is DRAM

When a complex kernel needs more registers than the hardware allows, or when you force a low register limit via compiler flags, ptxas doesn't abort. It spills excess registers into Local Memory (.local).

Don't let the name mislead you: local memory is not fast on-chip SRAM. Local memory is thread-private memory backed by off-chip DRAM, cached in L1 and L2.

When 1,000 or more active threads on an SM spill variables to .local memory simultaneously, the SM's 128KB to 256KB L1 cache lines thrash relentlessly. Spilled values get evicted down to off-chip DRAM. Every register read and write that turned into a spill now incurs memory controller roundtrips, triggering a catastrophic 10x to 50x latency penalty that ruins kernel throughput.

Parsing ptxas -v reports

To catch register pressure and spills before running a kernel, pass -Xptxas -v (or --ptxas-options=-v) to nvcc:

terminal
1nvcc -O3 -Xptxas -v --gpu-architecture=sm_90 vector_add.cu -o build/vector_add

The compiler outputs an exact resource receipt for every compiled kernel:

ptxas verbose output
1ptxas info : Compiling entry function 'add_kernel' for 'sm_90' 2ptxas info : Function properties for 'add_kernel' 3ptxas info : Used 32 registers, 0 bytes smem, 0 bytes cmem[0] 4ptxas info : Spilled 0 bytes to local memory, 0 bytes read back

Every field in this report delivers an operational truth:

  • Used 32 registers: Physical 32-bit registers allocated per thread. At exactly 32 registers, the kernel hits the 100% occupancy threshold for 2,048 threads per SM.
  • 0 bytes smem: Statically allocated shared memory per block.
  • 0 bytes cmem[0]: Constant memory bank 0 usage (kernel parameters and pointer addresses passed from the CPU host).
  • Spilled 0 bytes to local memory, 0 bytes read back: Zero is the target. If you see Spilled 16 bytes to local memory, 16 bytes read back, each thread spilled 4 32-bit registers (16 bytes). Across a full grid of 100,000 threads, those 4 words translate into hundreds of thousands of unnecessary memory bus accesses.

You have two primary compiler knobs to manage this tradeoff:

  1. --maxrregcount=N: Forces ptxas to cap registers per thread at NNN. If the kernel requires more than NNN registers, the compiler spills to .local memory to preserve target occupancy.
  2. __launch_bounds__(maxThreadsPerBlock, minBlocksPerMultiprocessor): A function qualifier placed in CUDA C++ source above __global__. It promises the compiler your launch bounds, allowing ptxas to calculate the exact register ceiling for that block size without accidentally triggering an occupancy drop.

Keep an artifact manifest

A benchmark result becomes easier to reproduce when its compiler state travels beside it. This is a template, not a captured build. Replace every placeholder and record at least these fields:

kernel-artifact-manifest.json
1{ 2 "source_revision": "git-sha", 3 "compiler": "nvcc", 4 "toolkit_version": "capture nvcc --version", 5 "driver_version": "capture nvidia-smi", 6 "virtual_target": "compute_90", 7 "native_targets": ["sm_90"], 8 "compile_flags": ["-O3", "-lineinfo"], 9 "embedded_images": ["compute_90 PTX", "sm_90 cubin"], 10 "kernel": "add_kernel", 11 "correctness": "record actual PASS/FAIL for n=1,255,256,257,1000" 12}

Hash the source, compile options, target, compiler version, and any compile-time constants into a JIT cache key. Shape-specialized kernel systems also need shape, stride, data type, alignment, and algorithmic mode when those values change generated code. Caching only by function name can silently load a valid cubin for the wrong contract.

For example, suppose two variants have 1,000 elements but strides 1 and 2. They address different memory locations, so a specialization that assumes stride 1 mustn't be reused for stride 2. Similarly, changing the target architecture suffix from sm_90 to sm_90a unlocks architecture-specific Hopper features like Tensor Memory Accelerator (TMA) asynchronous copies and Warpgroup Matrix Multiply-Accumulate (WGMMA) instructions. A cached binary compiled for sm_90 won't use those instructions, while an sm_90a cubin will crash if loaded on a GPU without the architecture-accelerated feature set.

This CPU exercise builds a canonical key from an explicit compilation recipe, including launch geometry and pipeline stages. It doesn't compile a kernel or implement a production artifact store.

compilation_identity.py
1from dataclasses import asdict, dataclass, replace 2import hashlib 3import json 4 5@dataclass(frozen=True) 6class Recipe: 7 source_sha256: str 8 toolchain: str 9 target: str 10 flags: tuple[str, ...] 11 shape: tuple[int, ...] 12 strides: tuple[int, ...] 13 dtype: str 14 block: int 15 num_warps: int = 4 16 num_stages: int = 2 17 18 def key(self): 19 encoded = json.dumps(asdict(self), sort_keys=True, separators=(",", ":")) 20 return hashlib.sha256(encoded.encode()).hexdigest() 21 22base = Recipe( 23 source_sha256=hashlib.sha256(b"vector-add-example-v1").hexdigest(), 24 toolchain="illustrative-compiler-build-A", target="sm_90", 25 flags=("-O3",), shape=(1000,), strides=(1,), dtype="float32", block=256, 26) 27assert base.key() == replace(base).key() 28variants = [ 29 replace(base, source_sha256=hashlib.sha256(b"v2").hexdigest()), 30 replace(base, toolchain="illustrative-compiler-build-B"), 31 replace(base, target="sm_100"), replace(base, flags=("-O3", "--use_fast_math")), 32 replace(base, shape=(257,)), replace(base, strides=(2,)), 33 replace(base, dtype="float16"), replace(base, block=128), 34 replace(base, num_warps=8), replace(base, num_stages=3), 35] 36assert len({base.key(), *(variant.key() for variant in variants)}) == 11 37cache = {base.key(): "fixture-artifact-handle"} 38assert cache.get(replace(base).key()) == "fixture-artifact-handle" 39assert all(cache.get(variant.key()) is None for variant in variants) 40print("same recipe: hit; 10 code-generation changes: miss")
Output
1same recipe: hit; 10 code-generation changes: miss

The marker string stands in for source bytes only in this exercise. A real cryptographic cache key must cover transitive includes, linked device libraries, compiler components, backend options, and any assumed pointer alignment (since 16-byte alignment enables 128-bit vector loads). Preserve flag order because repeated flags can override earlier ones. Runtime tensor values needn't enter the key unless compilation specializes on them. Store the artifact's hash and compatibility metadata separately; a cache hit isn't evidence that an untrusted binary is safe to load.

JIT is part of request latency

Just-in-time (JIT) compilation moves work from build time to runtime. NVIDIA's runtime compiler, NVRTC, accepts CUDA C++ strings and can emit PTX, cubin, CUDA Tile IR, or link-time optimization IR depending on target and options. The CUDA Driver API can load the result, while nvJitLink can combine PTX or link-time IR and produce a linked cubin.[5]Reference 5NVRTChttps://docs.nvidia.com/cuda/nvrtc/

PTX loaded at runtime is compiled by the device driver's JIT compiler. Generated cubins enter a compute cache, and a driver upgrade invalidates that cache so the new JIT compiler can regenerate code.[6]Reference 6CUDA Programming Guide.https://docs.nvidia.com/cuda/cuda-programming-guide/

That runtime behavior creates an operational contract:

ContractEvidence to captureFailure when omitted
Compilation identitySource or IR hash, compiler version, flags, target, specialization valuesWrong artifact reused or rollout can't be reproduced
Load compatibilityGPU compute capability, driver version, PTX target, cubin targetsMissing image or unsupported PTX at module load
Cold-start budgetCompile time, module-load time, cache state, first launch timeFirst request violates latency objective while steady state looks healthy
CorrectnessReference outputs, tolerances, edge shapes, alignment and mask casesFast specialization returns wrong values on a boundary case
PerformanceWarmups, synchronization, target GPU, input shapes, precision, profiler evidenceJIT or lazy-load time is mistaken for kernel execution time

Multi-process compilation races in distributed clusters

In production distributed training and serving (such as 8 GPUs on an HGX node running PyTorch DistributedDataParallel, DeepSpeed, Megatron-LM, or vLLM), all 8 worker processes execute in parallel.

When all 8 ranks encounter an uncompiled kernel or a new sequence length at the exact same millisecond, they all detect a cache miss simultaneously. If all 8 processes attempt to compile and write to the same disk location (such as ~/.triton/cache/<hash>.so or ~/.nv/ComputeCache) without coordination, severe race conditions occur:

  • Corrupted binary loads: Process A is halfway through writing the compiled ELF shared object when Process B checks the directory, finds the file name, and immediately calls dlopen() or cuModuleLoad(). Process B crashes with an invalid ELF header, truncated file error, or segmentation fault.
  • Overlapping clobbers: Multiple processes open the same file descriptor for writing simultaneously, corrupting disk blocks and producing a broken cache entry that breaks all future runs until manually purged.

Production GPU engines resolve this race through three complementary strategies:

  1. Atomic temporary write and replace: Each worker writes its compiled binary to a unique temporary file on the exact same filesystem (for example, <hash>.tmp.<pid>.<uuid>), flushes it to disk with os.fsync(), and performs an atomic rename via os.replace(). Under POSIX semantics, rename is guaranteed to be atomic within the same filesystem. Any competing reader sees either the complete existing file or the complete newly replaced file, never a half-written fragment.
  2. Advisory file locking: Before initiating compilation, the worker acquires an exclusive advisory lock using fcntl.flock or a cross-platform lock file (<hash>.lock). The first process to acquire the lock performs the compile. The remaining 7 processes block. Once the lock releases, the waiting processes wake up, see the populated cache file, and take an immediate cache hit without recompiling.
  3. Rank-0 compilation barrier: In managed distributed setups, rank 0 is assigned compilation responsibility during an explicit initialization phase. All workers wait at a torch.distributed.barrier(). Once rank 0 finishes populating the on-disk cache, workers 1 through 7 safely load the compiled artifacts in read-only mode.

Use the driver's switches to test both deployment branches. The first command forces embedded PTX through JIT and disables its disk cache for the run. The second disables PTX JIT, so success requires a compatible embedded cubin.[7]Reference 7CUDA Environment Variableshttps://docs.nvidia.com/cuda/cuda-programming-guide/05-appendices/environment-variables.html

terminal
1CUDA_FORCE_PTX_JIT=1 CUDA_CACHE_DISABLE=1 ./build/vector_add 2CUDA_DISABLE_PTX_JIT=1 ./build/vector_add

Run each mode in a fresh process. These shell assignments affect only that invocation; unset any conflicting JIT variables you previously exported. These are packaging tests, not kernel benchmarks.

CUDA module loading is lazy by default in current documentation. First use can therefore include module or kernel loading even when a compatible cubin exists. Warm the exact kernel or use eager loading when a latency-sensitive service needs that work outside the request path.[7]Reference 7CUDA Environment Variableshttps://docs.nvidia.com/cuda/cuda-programming-guide/05-appendices/environment-variables.html

A service packages sm_90 cubin plus compute_90 PTX. Its first request on a future compatible GPU is slow, but later requests are fast. What should you separate in the trace?

Answer

Separate process initialization, module loading, PTX-to-cubin JIT, cache write, first kernel launch, and steady-state execution. A fast warm kernel doesn't disprove expensive compilation or loading on the first request.

Same kernel, different unit of thought

CUDA C++ asks you to describe one thread, then organize threads into blocks. Triton asks you to describe one blocked program instance operating on vectors of offsets. Both versions below compute the same masked vector add.

The Triton kernel forms a block of 256 indices, loads two blocks under a mask, adds them, and stores the result. The compiler maps that blocked program to GPU threads and memory operations. The kernel follows Triton's vector-add tutorial; the wrapper below adds explicit input checks.[8]Reference 8Triton Programming Guide: Introductionhttps://triton-lang.org/main/programming-guide/chapter-1/introduction.html[9]Reference 9Vector Additionhttps://triton-lang.org/main/getting-started/tutorials/01-vector-add.html

vector_add_triton.py
1import torch 2import triton 3import triton.language as tl 4 5@triton.jit 6def add_kernel(x_ptr, y_ptr, out_ptr, n, BLOCK: tl.constexpr): 7 offsets = tl.program_id(axis=0) * BLOCK + tl.arange(0, BLOCK) 8 mask = offsets < n 9 x = tl.load(x_ptr + offsets, mask=mask, other=0.0) 10 y = tl.load(y_ptr + offsets, mask=mask, other=0.0) 11 tl.store(out_ptr + offsets, x + y, mask=mask) 12 13def add(x: torch.Tensor, y: torch.Tensor) -> torch.Tensor: 14 if not (x.is_cuda and y.is_cuda and x.device == y.device): 15 raise ValueError("inputs must share a CUDA device") 16 if x.ndim != 1 or y.ndim != 1 or x.shape != y.shape: 17 raise ValueError("inputs must be matching one-dimensional vectors") 18 if x.dtype != torch.float32 or y.dtype != torch.float32: 19 raise ValueError("this fixture supports float32 only") 20 if x.requires_grad or y.requires_grad: 21 raise ValueError("this wrapper has no autograd rule") 22 if not (x.is_contiguous() and y.is_contiguous()): 23 raise ValueError("strided views require a different address calculation") 24 out = torch.empty_like(x) 25 n = x.numel() 26 if n: 27 with torch.cuda.device(x.device): 28 add_kernel[(triton.cdiv(n, 256),)](x, y, out, n, BLOCK=256) 29 return out 30 31if __name__ == "__main__": 32 for n in (0, 1, 255, 256, 257, 1000): 33 i = torch.arange(n, device="cuda", dtype=torch.float32) 34 x, y = 0.25 * i, i.remainder(7) - 3 35 actual = add(x, y) 36 torch.cuda.synchronize(x.device) 37 torch.testing.assert_close(actual, x + y, rtol=0, atol=0, equal_nan=False) 38 print(f"PASS n={n}") 39 try: 40 add(x[::2], y[::2]) 41 except ValueError: 42 print("PASS rejected strided views") 43 else: 44 raise AssertionError("strided views were accepted")

Run this on a supported Linux/PyTorch/Triton CUDA environment. It should print six shape passes and one strided-view rejection. The GPU code wasn't executed in this review; don't treat the CPU checks below as Triton compilation or memory-safety evidence. Inputs requiring autograd aren't supported by this wrapper: a custom backward rule would be separate work.

The arithmetic stayed fixed. Ownership changed:

QuestionCUDA C++ answerTriton answer
What does one source instance own?One scalar threadOne block of values
How is the global index formed?blockIdx, blockDim, threadIdxprogram_id plus arange
How are edge elements protected?Scalar branch or predicateVector mask on load and store
Who chooses thread mapping?Programmer chooses block size and per-thread workCompiler maps blocked program; author supplies block and launch meta-parameters
What evidence closes the loop?Cubin resource report, SASS, profiler, correctness testCompiler artifacts, generated target code, profiler, correctness test

Triton removes some thread-level bookkeeping. It doesn't remove the need to reason about coalescing, block size, occupancy, specialization, or numerical behavior. A blocked expression is an optimization hypothesis until generated artifacts and measurements support it.

How Triton compiles: the MLIR lowering pipeline

Triton isn't an interpreter or a simple string-template emitter. It's an optimizing compiler built on the Multi-Level Intermediate Representation (MLIR) framework. It lowers abstract block mathematics into native GPU machine code through a sequence of explicit intermediate dialects:

  1. Triton IR (tt dialect): Parsed directly from the Python AST and @triton.jit decorator. This level captures high-level tile operations (tt.load, tt.store, tt.add, tt.dot). Operations at this stage are hardware-agnostic: they express math over multidimensional tiles without committing to thread counts, warp allocations, or physical memory layouts.
  2. TritonGPU IR (ttg dialect): This stage contains Triton's core GPU optimization intelligence. The compiler takes the abstract tiles and maps them to physical GPU resources:
    • Distributed layout assignment: The compiler assigns formal layout encodings (such as #triton_gpu.blocked, #triton_gpu.shared, and #triton_gpu.dot_op) that dictate how each element in a tile is distributed across warps and individual thread lanes.
    • Shared memory allocation and XOR swizzling: Matrix tiles loaded into shared memory often suffer from shared memory bank conflicts when threads access columns across regular power-of-two strides. Triton automatically allocates shared memory buffers and applies an XOR address-swizzling pattern to the indices. This scatters memory accesses across all 32 independent banks, completely eliminating bank serialization without manual programmer padding.
    • Asynchronous software pipelining: For reduction or GEMM loops, Triton automatically constructs circular multi-stage buffers in shared memory (num_stages = 3 or 4). It emits asynchronous global-to-shared copy instructions (cp.async on Ampere/Hopper), so that while the SM computes iteration kkk on Tensor Cores, it prefetches iteration k+1k+1k+1 from global memory in the background.
    • Vectorized memory coalescing: When the compiler inspects offsets = pid * BLOCK + tl.arange(0, BLOCK), it proves that the memory slice is contiguous and aligned. It automatically synthesizes wide 128-bit vector load instructions (LDG.E.128 in SASS) that fetch 16 bytes per thread in a single transaction. This guarantees coalesced memory bus saturation without requiring manual float4 pointer casting in user code.
  3. LLVM IR / NVVM IR: The optimized TritonGPU dialect lowers into LLVM IR annotated with target GPU intrinsics (such as barrier synchronization, special registers, and warp shuffles).
  4. PTX virtual ISA: The LLVM NVPTX backend compiles the LLVM IR into PTX virtual assembly.
  5. Native SASS via ptxas: Finally, the NVIDIA driver invokes ptxas to allocate physical registers from the SM's 64K register file, schedule instructions, and assemble the final .cubin binary.

Check ownership without a GPU

A CPU enumeration can verify the index arithmetic independently of either compiler. It can't detect generated-code bugs, GPU races, or invalid device memory access. Predict the counts before running it: length 257 requires two 256-lane blocks, with 255 lanes masked out.

check_index_coverage.py
1def covered_indices(n, block=256, *, include_equal=False): 2 if n < 0 or block <= 0: 3 raise ValueError("length must be nonnegative and block positive") 4 blocks = (n + block - 1) // block 5 active = [] 6 for program in range(blocks): 7 for lane in range(block): 8 i = program * block + lane 9 if i < n or (include_equal and i == n): 10 active.append(i) 11 return blocks, active 12 13for n in (0, 1, 255, 256, 257, 1000): 14 blocks, indices = covered_indices(n) 15 assert indices == list(range(n)) # No duplicates, omissions, or excess. 16 print(f"n={n}: blocks={blocks}, active={len(indices)}, masked={blocks * 256 - n}") 17 18# Deliberately change < to <=. It only looks correct on full blocks. 19assert covered_indices(256, include_equal=True)[1] == list(range(256)) 20bad = covered_indices(257, include_equal=True)[1] 21assert bad[-1] == 257 and bad != list(range(257)) 22print("off-by-one mask: full block passes; n=257 exposes index 257")
Output
1n=0: blocks=0, active=0, masked=0 2n=1: blocks=1, active=1, masked=255 3n=255: blocks=1, active=255, masked=1 4n=256: blocks=1, active=256, masked=0 5n=257: blocks=2, active=257, masked=255 6n=1000: blocks=4, active=1000, masked=24 7off-by-one mask: full block passes; n=257 exposes index 257

Now change the block size to 128 and repeat. Length 257 needs three blocks, with 127 masked lanes. A passing multiple-of-block test alone misses the off-by-one error; that's why both GPU fixtures include 255 and 257.

Six authoring surfaces, six control contracts

A kernel language isn't a ranking from easy to powerful. Each surface chooses which decisions belong to author, compiler, library, and runtime.

CUDA C++

CUDA C++ uses single instruction, multiple threads (SIMT) semantics. You control thread and block geometry, address calculations, synchronization, memory spaces, and low-level intrinsics. That directness fits irregular algorithms, architecture experiments, and cases where a higher layer hides a critical decision. Its portability surface is NVIDIA GPUs, with source, PTX, and fatbins handling generations inside that ecosystem.[6]Reference 6CUDA Programming Guide.https://docs.nvidia.com/cuda/cuda-programming-guide/

Evidence should reach the cubin. Keep ptxas resource output, disassemble native code, profile target hardware, and compare results against a trusted implementation.

Triton block programs

Triton is a Python-embedded domain-specific language (DSL) whose program instances operate on blocks. Its compiler performs block-level data-flow analysis and can automate coalescing, vectorization, shared-memory management, synchronization, and instruction selection.[8]Reference 8Triton Programming Guide: Introductionhttps://triton-lang.org/main/programming-guide/chapter-1/introduction.html Current project documentation lists NVIDIA and AMD GPU support, but backend support and generated code still vary by hardware and Triton release.[10]Reference 10Triton Language and Compilerhttps://github.com/triton-lang/triton

Triton fits custom fusion and dense or structured kernels where block ownership is natural. Inspect specialized IR and target assembly when available, because one Python function can compile into many shape, type, and meta-parameter variants.

CUTLASS and CuTe

CUTLASS supplies CUDA C++ templates for high-performance linear algebra. CuTe, used inside modern CUTLASS, models hierarchical tensor layouts, tensors, copy atoms, and matrix multiply-accumulate atoms. Its C++ surface keeps detailed control over thread-data mapping and architecture features. CuTe DSL brings the same low-level concepts into a Python JIT stack while preserving explicit memory, thread, and data hierarchy.[11]Reference 11CUTLASS and CuTe Documentationhttps://docs.nvidia.com/cutlass/latest/overview.html

Consider this family when matrix multiplication, convolution, attention building blocks, or unusual layouts need explicit layout and hardware-atom control. Whether it exposes a needed capability depends on the specific APIs and releases being compared. Expect a steeper layout-algebra learning curve and a larger specialization space. Use CUTLASS profiler or an equivalent harness, inspect PTX and SASS, and keep compile-time policies in the benchmark manifest.

CUDA Tile

CUDA Tile changes CUDA's unit of thought from one SIMT thread to one block operating collectively on immutable multidimensional tiles. The compiler chooses the number of threads per block and maps tile operations onto registers, shared memory, tensor cores, and other hardware resources. SIMT and tile kernels can coexist in one application.[12]Reference 12Writing Tile Kernelshttps://docs.nvidia.com/cuda/cuda-programming-guide/02-basics/writing-tile-kernels.html

Tooling snapshot, verified September 2, 2026: NVIDIA's CUDA 13.3 documentation lists CUDA Tile in Python through cuda.tile and CUDA Tile C++ in the toolkit from 13.3 onward. Tile shapes must be compile-time powers of two in the documented model. Treat earlier toolkits and different language releases as separate environments, not compatible assumptions.[12]Reference 12Writing Tile Kernelshttps://docs.nvidia.com/cuda/cuda-programming-guide/02-basics/writing-tile-kernels.html

CUDA Tile offers source portability across NVIDIA generations by hiding thread mapping, not cross-vendor portability. Inspect Tile IR or emitted device images where supported, record compiler version, then profile on each architecture you claim to support.

Pallas

Pallas extends JAX with custom kernels for GPU and tensor processing unit (TPU) backends. Kernels use references (Refs) to memory, launch grids, block specifications (BlockSpecs), and backend-specific pipelining or hardware APIs. A kernel is embedded in a larger JAX program through an API such as pl.kernel, pl.pallas_call, or a backend-specific wrapper; their signatures and supported transformations differ.[13]Reference 13Pallas: a JAX Kernel Languagehttps://docs.jax.dev/en/latest/pallas/

Pallas is useful when JAX composition or GPU and TPU integration matters. A custom kernel doesn't automatically inherit every JAX transformation; check the chosen API's support and supply derivative rules when needed. Shared concepts don't guarantee one source or tuning configuration performs equally across backends. JAX describes Pallas as under active development, and its quickstart still warns that the API is experimental. Hardware coverage is operation-specific: JAX 0.11 added Ampere matrix instructions and asynchronous copies to Mosaic GPU. The older Pallas Triton backend is deprecated in JAX 0.11 and scheduled for removal.[14]Reference 14Pallas Changeloghttps://docs.jax.dev/en/latest/pallas/CHANGELOG.html Pin JAX and backend versions, test interpret mode where supported, inspect lowering, and profile each target.

NKI

Neuron Kernel Interface (NKI) targets AWS Trainium, Inferentia2, Trainium2, and Trainium3 NeuronCores. Its high-level nki.language API handles tensor indexing and placement, while lower-level nki.isa exposes hardware operations for computation, data movement, dynamic control flow, and cross-core communication. The documented execution pattern moves inputs from high-bandwidth memory (HBM) into the on-chip state buffer (SBUF), computes on NeuronCore engines, then stores outputs to HBM.[15]Reference 15About Neuron Kernel Interfacehttps://awsdocs-neuron.readthedocs-hosted.com/en/latest/nki/get-started/about/index.html

NKI fits workloads committed to AWS Neuron hardware that need custom operations or tighter control than framework compilation supplies. It isn't a CUDA portability layer. Keep Neuron compiler and instance type in the artifact record, use framework-level correctness comparisons, and collect Neuron profiler evidence.

The comparison is easier to use after each model has a concrete meaning:

SurfaceAuthor's main unitExplicit controlPortability boundaryMinimum convincing evidence
CUDA C++Thread, warp, blockExplicit thread-level controlNVIDIA CUDA GPUsPTX, cubin resources, SASS, target profile
TritonBlocked programTile shape, loads, masks, launch meta-parametersSupported Triton GPU backendsSpecialization key, compiler IR or assembly, target profile
CUTLASS/CuTeHierarchical layout and hardware atomExplicit layout, copy, pipeline, and MMA controlNVIDIA CUDA GPUsPolicy manifest, profiler, PTX/SASS, numeric check
CUDA TileBlock-level immutable tilesTile partition and operations; compiler owns threadsCUDA Tile-capable NVIDIA stackTile/compiler artifact, version pin, per-target profile
PallasGrid program over Refs and blocksMemory movement, block specs, backend pipeline APIsJAX GPU and TPU backends, with backend-specific constraintsJAX/backend pin, lowering evidence, per-target profile
NKITile over Neuron memory and enginesSBUF placement plus optional low-level ISA controlAWS Inferentia2 and Trainium familiesNeuron compiler record, profiler, framework comparison

A Triton kernel and a CuTe rewrite pass the same correctness fixture. Triton is faster on H100, while CuTe is faster on B200. What must the review artifact contain before either implementation becomes the default?

Answer

Keep both source revisions, compiler and backend versions, specialization keys, compile flags, native targets, correctness tolerances, cold and warm timings, emitted-code evidence, and per-target profiler results. Choose against the deployment GPU mix and latency objective. One architecture's timing or a PTX instruction count can't establish a portable winner.

Diagnose the first failing boundary

Compiler failures become manageable when you ask which representation last satisfied its contract.

SymptomLikely boundaryEvidence to collectNext action
Source compile rejects type, address space, or intrinsicSource to frontend IRFull compiler log, minimal source, language and compiler versionsReduce to smallest rejected construct; check supported language subset
ptxas rejects target or instructionPTX to cubinPTX header, ptxas log, toolkit version, requested smAlign PTX feature set, toolkit, and target architecture
"No kernel image" at load or launchFatbin selectionGPU compute capability, cuobjdump --list-elf, --list-ptxPackage compatible cubin or usable PTX fallback
Forced PTX JIT fails, normal mode worksDriver JIT pathEmbedded PTX, driver version, JIT error logFix PTX compatibility or driver floor; don't treat cubin success as forward-compatibility proof
First call is slow, warm calls are fastCompile or lazy-load pathFresh-process timeline, cache state, module-loading modePrecompile, warm exact variants, or move eager loading outside request path
Register count rises and throughput fallsIR or target-code optimizationOld and new resource reports, SASS, occupancy and stall profileFind specialization or code change that extended live ranges or caused spills
Edge shape returns wrong valuesSource mask or specialization contractFailing shape, strides, alignment, generated variant, reference outputFix boundary mask and include shape or stride in specialization key
nvdisasm rejects host executableInspection tool inputFile type and packaged image listUse cuobjdump on host file or extract cubin before nvdisasm

Don't skip directly from source to profiler. A profiler identifies an executed bottleneck, but it can't explain a missing image that never loaded. Disassembly proves emitted code, but it can't prove correct answers. Correctness tests prove values for their fixtures, but they can't establish speed. The evidence layers complement one another.

Choose by the decision you must own

Start with a framework or vendor library if it meets the need. For a custom kernel, choose by the decision you need to express:

  1. Use a framework or vendor library when an existing operation already meets correctness and performance needs.
  2. Use Triton when custom block structure, fusion, or masking is central and compiler-managed thread mapping is acceptable.
  3. Use Pallas when the kernel must compose naturally with JAX or span its GPU and TPU ecosystem.
  4. Use NKI when NeuronCore memory placement or engines are the target, not an afterthought.
  5. Use CUDA Tile when block-level tile semantics fit and NVIDIA's compiler should own intra-block threads.
  6. Use CUTLASS/CuTe when tensor layouts, asynchronous copies, and matrix hardware atoms need explicit control.
  7. Use CUDA C++ when per-thread behavior, synchronization, special instructions, or unsupported irregularity requires it.

These are alternatives, not a ranked ladder. A prototype may begin in Triton, expose a compiler limitation, and move one hot kernel to CuTe or CUDA C++. Another kernel may move upward after a compiler learns the missing transformation.

If you have a compatible NVIDIA environment, practice the full artifact path before moving on. Build the vector-add executable with an sm_90 cubin and compute_90 PTX, save its manifest and cuobjdump image inventory, then run forced-PTX and cubin-only checks in fresh processes. Capture the resource report, SASS, correctness output, and separate cold and warm timings in one review artifact. Run the Triton version on the same five nonempty shapes. Both implementations should match the same reference exactly, and the strided-view test must reject before launch. For a memory-safety check, run the CUDA executable under compute-sanitizer --tool memcheck ./build/vector_add. Capture failures as well as passes. On CPU-only machines, complete the indexing and cache-identity exercises without claiming a GPU build or benchmark.

Mastery check

Complete the lesson

Mastery Check

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

1.Which artifact shows the native instructions encoded for add_kernel in the target binary?

Correct answer: The SASS disassembly of the target cubin

Source and PTX describe earlier representations that later passes can rewrite. Disassembling the target cubin reads the native instructions encoded for that architecture. Identify the intended kernel symbol; an add elsewhere in the binary is not proof about this operation.

2.A build embeds only an sm_90 cubin and must run on a GPU with a new major compute capability. Which deployment property is missing?

Correct answer: Compatible PTX that the newer driver can JIT for its target

Cubins aren't binary compatible across major compute-capability versions. Embedded PTX supplies the forward path because a newer driver can compile compatible virtual code for the new target.

3.Which command path is valid when you want register liveness from a CUDA host executable?

Correct answer: Extract a cubin with cuobjdump, then pass that cubin to nvdisasm

cuobjdump accepts host binaries and can list or extract their embedded cubins. nvdisasm accepts standalone cubins and provides the richer register-liveness view.

4.A kernel takes 250 ms on its first call and 20 microseconds afterward. Which experiment best separates JIT and loading from execution?

Correct answer: Trace a fresh process with cache state recorded, then warm and time synchronized launches separately

Fresh-process tracing exposes compilation and module loading, while warmed synchronized launches isolate steady-state kernel time. Mixing the phases produces a misleading latency number.

5.For the vector-add examples, which ownership change correctly describes CUDA C++ versus Triton?

Correct answer: CUDA describes one GPU thread, while Triton describes one blocked program instance

CUDA's ordinary SIMT model starts from per-thread work and explicit launch geometry. Triton starts from vectorized blocks of values and lets its compiler map each program instance onto GPU threads.

6.A JAX team needs one custom operation on TPU and Hopper GPU, accepts an actively developed API, and wants it to compose with JAX transformations. Which surface fits best?

Correct answer: Pallas, with separate backend validation and tuning for GPU and TPU

Pallas integrates custom kernels with JAX and provides GPU and TPU backends. Its shared programming ideas don't erase backend-specific APIs, constraints, profiling, or tuning.

7.After a specialization change, register use rises and throughput drops. Which evidence set most directly tests a compiler-codegen cause?

Correct answer: Before-and-after cubin resource reports, target SASS, and profiler data

Register allocation happens in target compilation. Resource reports and SASS show the emitted change, while profiler data connects it to occupancy, stalls, and measured throughput.

8.Which JIT cache key is safest for a shape-specialized kernel whose layout and compiler flags change generated code?

Correct answer: Source or IR hash, compiler and flags, target, shape, strides, dtype, and codegen meta-parameters

Every value that can alter generated code or its validity belongs in compilation identity. Function-name-only and shape-only caches can return a well-formed artifact for the wrong layout, type, target, or algorithm.

9.An SM has 65,536 physical registers and supports up to 2,048 active threads. A developer unrolls a loop, increasing register usage from 32 to 40 registers per thread. What is the immediate impact on SM occupancy?

Correct answer: Active threads drop from 2,048 (100% occupancy) to 1,536 (75% occupancy) because 2,048 threads would require 81,920 registers

With 65,536 registers per SM, 2,048 threads require at most 32 registers each. At 40 registers per thread, 2,048 threads would require 81,920 registers, exceeding the SM's physical register file. The SM abruptly throttles active threads to 1,536 (48 warps), dropping theoretical occupancy from 100% to 75%.

10.During distributed training across 8 GPUs on a single node, worker processes crash intermittently during startup with 'invalid ELF header' or corrupt shared library errors in ~/.triton/cache. What is the root cause?

Correct answer: Multiple worker processes simultaneously compiled and wrote to the same cache file without atomic renaming or advisory locking

When 8 worker processes encounter an uncompiled kernel simultaneously, they all experience a cache miss and begin writing to the same disk path. Without atomic renaming (such as os.replace from a temporary file) or file locking, one worker reads a partially written file, triggering an invalid ELF header or dynamic linker crash.

10 questions remaining.

Next Step
Continue to GEMM From Naive CUDA to Tensor Cores

You can now trace source choices into PTX, cubin resources, and native instructions. Next, use that evidence while transforming one matrix multiplication from a correct scalar-thread baseline into tiled, pipelined Tensor Core code.

PreviousGPU Parallel Primitives Lab
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

NVIDIA CUDA Compiler Driver NVCC

NVIDIA · 2026 · Official documentation

https://docs.nvidia.com/cuda/cuda-compiler-driver-nvcc/

CUDA Binary Utilities

NVIDIA · 2026 · Official documentation

https://docs.nvidia.com/cuda/cuda-binary-utilities/

NVVM IR Specification

NVIDIA · 2026 · Official documentation

https://docs.nvidia.com/cuda/nvvm-ir-spec/

libNVVM API

NVIDIA · 2026 · Official documentation

https://docs.nvidia.com/cuda/libnvvm-api/

NVRTC

NVIDIA · 2026 · Official documentation

https://docs.nvidia.com/cuda/nvrtc/

CUDA Programming Guide.

NVIDIA · 2026

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

CUDA Environment Variables

NVIDIA · 2026 · Official documentation

https://docs.nvidia.com/cuda/cuda-programming-guide/05-appendices/environment-variables.html

Triton Programming Guide: Introduction

Triton Project · 2026 · Official documentation

https://triton-lang.org/main/programming-guide/chapter-1/introduction.html

Vector Addition

Triton Project · 2026

https://triton-lang.org/main/getting-started/tutorials/01-vector-add.html

Triton Language and Compiler

Triton Project · 2026 · Official repository

https://github.com/triton-lang/triton

CUTLASS and CuTe Documentation

NVIDIA · 2026 · Official documentation

https://docs.nvidia.com/cutlass/latest/overview.html

Writing Tile Kernels

NVIDIA · 2026 · Official documentation

https://docs.nvidia.com/cuda/cuda-programming-guide/02-basics/writing-tile-kernels.html

Pallas: a JAX Kernel Language

JAX Authors · 2026 · Official documentation

https://docs.jax.dev/en/latest/pallas/

Pallas Changelog

JAX Authors · 2026

https://docs.jax.dev/en/latest/pallas/CHANGELOG.html

About Neuron Kernel Interface

Amazon Web Services · 2026 · Official documentation

https://awsdocs-neuron.readthedocs-hosted.com/en/latest/nki/get-started/about/index.html

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.