IIntroduction to AI Systems Performance EngineeringTechnical textbook
Chapter 7: GPU Profiling and Instruction Analysis
7.3

Source, PTX, and SASS analysis

Use PTX and target SASS to answer one compiler question and connect the changed instruction sequence to separate profiler and timing evidence

Before you begin

Follow a linked prerequisite when you cannot explain it from memory. Background skills without links are stated as abilities rather than hidden terminology.

  • Ability to recognize registers, load/store instructions, predicates, branches, and dependency order
  • Introductory distinction between source code, compiler intermediate code, and native machine instructions

C++ source specifies program semantics, but it does not specify every machine instruction. The compiler can inline functions, remove variables, vectorize or scalarize loads, unroll loops, predicate branches, and assign registers. A source array can become individually addressed registers or local-memory accesses. A matrix expression can use matrix instructions on one shape and a different path on another.

Use assembly when a specific source-level expectation disagrees with measurement. If a vectorized rewrite does not reduce load instructions, inspect the generated load sequence. If register reports show local storage, locate spill operations. If a matrix kernel has unexpectedly low tensor-pipeline activity, verify whether the expected matrix instruction was emitted.

CUDA compilation exposes two relevant instruction layers. PTX is a virtual instruction set used as a compilation target and compatibility layer. Architecture-specific assembly, commonly called SASS, is the native code executed by a target GPU. The driver or offline tools can assemble PTX differently for different architectures, so PTX is informative but not the final machine program.

Follow the compilation layers

nvcc separates host and device compilation and can embed PTX, native cubins, or both in the output. PTX describes operations for a virtual GPU architecture. ptxas or the driver lowers suitable PTX to native code for a specific compute capability. A fat binary can contain several target variants and a PTX fallback.

A compute capability identifies an NVIDIA GPU instruction and resource target with a major and minor version. In compiler options, a name such as compute_90 denotes a virtual PTX target, while sm_90 denotes native code for the corresponding real architecture. The example command uses sm_90 only as an example. Select targets from the GPUs that will run the binary, and verify which embedded or JIT-compiled image the driver loads.

cuobjdump can inspect host binaries and extract embedded PTX or disassemble embedded cubins. nvdisasm operates on cubin files and provides richer native disassembly and control-flow information. Preserve the exact binary used in measurement, because rebuilding can change compiler decisions even when the source is unchanged.

Compile an optimized build with line information when source correlation is needed. A debug build changes optimization and register allocation, so it may not explain release performance. Record compiler version, flags, target architecture, linked libraries, and whether runtime JIT compilation occurred.

Answer one compiler question at a time

For a suspected spill, use compiler resource output to establish that local storage exists. Then search SASS for local-memory loads and stores and correlate them with source. Determine whether they execute in the hot loop. Profiler local-memory traffic should change when the spill is removed.

For suspected vectorization, identify the load and store instructions for the relevant line. Check the instruction width and the number of issued operations. Confirm that pointer alignment and tail handling make the vector operation legal. For matrix acceleration, identify both the matrix instruction and the data-movement instructions that supply its fragments.

For control-flow analysis, inspect branches, predicates, and loop structure around the tail or divergent region. A compiler may predicate a short branch, unroll a loop, or retain a branch for a long path. Compare the generated path with the lane distribution and instruction metrics observed in the profiler.

Relate instructions to elapsed time

Fewer instructions do not guarantee a faster kernel. Instructions use different pipelines and have different latency and throughput. Independent instructions can overlap, while a dependency chain serializes operations. A memory instruction can spend most of its lifetime waiting outside the issue pipeline. Removing an instruction from a non-limiting pipeline may have no measurable effect.

Use source correlation and instruction-level metrics to establish frequency and location. Then predict a resource effect: fewer local-memory transactions, fewer integer address instructions, more vectorized memory operations, or use of the matrix pipeline. Recompile, verify that the native sequence changed, and measure the kernel with a separate timing method.

The same PTX can produce different SASS across GPU generations because native instructions, scheduling rules, and assembler decisions differ. Treat assembly findings as architecture- and toolchain-specific. Preserve a higher-level correctness path and repeat the analysis for supported targets.

Worked example: Identifying a missing vector-load instruction

A kernel was rewritten to load four FP16 values at once, but time and instruction count barely changed.

  1. Check alignment and aliasing assumptions in source. The pointer type alone may not prove to the compiler that every address meets the vector alignment.
  2. Inspect PTX and SASS around the load. Suppose four scalar operations remain, confirming the intended vector instruction was not selected.
  3. Express or enforce alignment safely, adjust layout if necessary, and handle tails with a separate path. Recompile and verify the native instruction rather than assuming success.
  4. Measure again. If time still does not change, inspect whether load-instruction issue was ever the limiter; device-memory bandwidth or downstream computation may already dominate.

Conclusion: Disassembly resolves the compiler question, while the performance model determines whether resolving it matters.

Common errors

  • Treating PTX as the final executed ISA. PTX is an important intermediate, but target assembly makes architecture-specific choices.
  • Assuming fewer instructions must be faster. Pipeline type, dependencies, memory behavior, and overlap determine elapsed time.
  • Reading a debug binary to explain optimized performance. Different optimization and allocation make it a different program.

Reference summary

CUDA device code can be represented as PTX and assembled into native instructions for a target compute capability. PTX is a virtual instruction set. SASS is the architecture-specific code executed by the GPU. The same PTX can produce different native code on different targets.

Inspect disassembly to answer a defined compiler question, such as whether a vector load was emitted or whether an array spilled. Then connect the instruction change to profiler evidence and separate timing. Instruction count alone does not determine elapsed time.

bash
# sm_90 is an example; select the compute capability of the target GPU
nvcc -lineinfo -O3 -arch=sm_90 kernel.cu -o kernel
cuobjdump --dump-ptx kernel
cuobjdump --dump-sass kernel
# or extract a cubin and use nvdisasm for richer control-flow output
Keep source line information for correlation without assuming debug builds represent optimized code.
  • Look for local-memory load/store instructions that indicate spills.
  • Confirm vectorized or tensor instructions were actually selected.
  • Count repeated address calculations in hot loops.
  • Inspect predication and branches around tails.
  • Compare architectures; PTX can lower differently as hardware evolves.

Knowledge check

Why can two binaries containing similar PTX have different performance on two GPU generations?

Show the answer guide
  • PTX is a virtual instruction representation that can be lowered again for a target architecture, while the final SASS or cubin contains the native instruction selection and scheduling for that target.
  • Different GPU generations provide different native instructions, latencies, cache behavior, resource limits, and compiler lowering choices even when the PTX operations look similar.
  • A valid comparison preserves the exact cubin or JIT output, compiler and driver versions, target architecture, resource report, and measured input shape.
Practice

Compare virtual and native code

Compile one CUDA kernel for two supported native architecture targets or compare an embedded cubin with a PTX JIT path. Extract PTX and native disassembly and predict one measurable consequence.

Evidence to keep: Build commands, compiler and target versions, preserved binaries, PTX and SASS excerpts, register and instruction differences, and timing for identical inputs on applicable hardware.

Combine the necessary measurements in a report that states the hypothesis, evidence, change, result, and reproduction procedure.

References

Sources and further reading

The lesson explanations synthesize primary papers and official references. Follow the originals for proofs, exact APIs, architecture details, and version-sensitive behavior.

  1. Nsight Compute Profiling Guide
  2. Parallel Thread Execution ISA
  3. CUDA Binary Utilities