How does the CUDA kernel from §2.1 become instructions a GPU actually executes? The answer reveals something unusual about the GPU ecosystem: unlike x86 — backward compatible all the way to the Intel 8086 of 1976, so 40-year-old binaries can in principle still run — GPU vendors never promised hardware-level compatibility. What they promised instead shaped their entire compilation stack.
2.2.1 NVIDIA: a documented virtual ISA over an undocumented real one
With many vendors shipping many different GPU designs, early programmable
GPUs already relied on ISA virtualization (OpenGL’s shading language,
Microsoft’s HLSL). When NVIDIA introduced CUDA in 2007 they followed the same
path: kernels compile first to
Before running, PTX is lowered — by the driver or the standalone ptxas —
to cuobjdump and began listing opcode names (though, as of the book’s
writing, not operand formats or semantics).
Step through the same SAXPY kernel at three levels — watch how the logical structure survives while everything else changes:
PTX — virtual ISA (CUDA 8.0)
1.visible .entry _Z5saxpyifPfS_(2.param .u32 _Z5saxpyifPfS__param_0,3.param .f32 _Z5saxpyifPfS__param_1,4.param .u64 _Z5saxpyifPfS__param_2,5.param .u64 _Z5saxpyifPfS__param_36)7{8.reg .pred %p<2>;9.reg .f32 %f<5>;10.reg .b32 %r<6>;11.reg .b64 %rd<8>;12 13ld.param.u32 %r2, [_Z5saxpyifPfS__param_0];14ld.param.f32 %f1, [_Z5saxpyifPfS__param_1];15ld.param.u64 %rd1, [_Z5saxpyifPfS__param_2];16ld.param.u64 %rd2, [_Z5saxpyifPfS__param_3];17mov.u32 %r3, %ctaid.x;18mov.u32 %r4, %ntid.x;19mov.u32 %r5, %tid.x;20mad.lo.s32 %r1, %r4, %r3, %r5;21setp.ge.s32 %p1, %r1, %r2;22@%p1 bra BB0_2;23 24cvta.to.global.u64 %rd3, %rd2;25cvta.to.global.u64 %rd4, %rd1;26mul.wide.s32 %rd5, %r1, 4;27add.s64 %rd6, %rd4, %rd5;28ld.global.f32 %f2, [%rd6];29add.s64 %rd7, %rd3, %rd5;30ld.global.f32 %f3, [%rd7];31fma.rn.f32 %f4, %f2, %f1, %f3;32st.global.f32 [%rd7], %f4;33 34BB0_2:35ret;36}SASS — Fermi (sm_20)
1/*0000*/ MOV R1, c[0x1][0x100];2/*0008*/ S2R R0, SR_CTAID.X;3/*0010*/ S2R R2, SR_TID.X;4/*0018*/ IMAD R0, R0, c[0x0][0x8], R2;5/*0020*/ ISETP.GE.AND P0, PT, R0, c[0x0][0x20], PT;6/*0028*/ @P0 BRA.U 0x78;7/*0030*/ @!P0 MOV32I R5, 0x4;8/*0038*/ @!P0 IMAD R2.CC, R0, R5, c[0x0][0x28];9/*0040*/ @!P0 IMAD.HI.X R3, R0, R5, c[0x0][0x2c];10/*0048*/ @!P0 IMAD R4.CC, R0, R5, c[0x0][0x30];11/*0050*/ @!P0 LD.E R2, [R2];12/*0058*/ @!P0 IMAD.HI.X R5, R0, R5, c[0x0][0x34];13/*0060*/ @!P0 LD.E R0, [R4];14/*0068*/ @!P0 FFMA R0, R2, c[0x0][0x24], R0;15/*0070*/ @!P0 ST.E [R4], R0;16/*0078*/ EXIT;SASS — Pascal (sm_60)
1 /* control instruction */2/*0008*/ MOV R1, c[0x0][0x20];3/*0010*/ S2R R0, SR_CTAID.X;4/*0018*/ S2R R2, SR_TID.X;5 /* control instruction */6/*0028*/ XMAD.MRG R3, R0.reuse, c[0x0][0x8].H1, RZ;7/*0030*/ XMAD R2, R0.reuse, c[0x0][0x8], R2;8/*0038*/ XMAD.PSL.CBCC R0, R0.H1, R3.H1, R2;9 /* control instruction */10/*0048*/ ISETP.GE.AND P0, PT, R0, c[0x0][0x140], PT;11/*0050*/ @P0 EXIT;12/*0058*/ SHL R2, R0.reuse, 0x2;13 /* control instruction */14/*0068*/ SHR R0, R0, 0x1e;15/*0070*/ IADD R4.CC, R2.reuse, c[0x0][0x148];16/*0078*/ IADD.X R5, R0.reuse, c[0x0][0x14c];17 /* control instruction */18/*0088*/ IADD R2.CC, R2, c[0x0][0x150];19/*0090*/ IADD.X R3, R0, c[0x0][0x154];20/*0098*/ LDG.E R0, [R4];21 /* control instruction */22/*00a8*/ LDG.E R6, [R2];23/*00b0*/ FFMA R0, R0, c[0x0][0x144], R6;24/*00b8*/ STG.E [R2], R0;25 /* control instruction */26/*00c8*/ EXIT;27/*00d0*/ BRA 0xd0;28/*00d8*/ NOP;Kernel parameters: PTX gives parameters their own 'param' address space, loaded explicitly. SASS instead passes them via banked constant memory — c[bank][offset] operands readable directly by ordinary instructions (see the ISETP/FFMA/IADD lines). The MOV at the top just sets up the stack pointer from constant memory.
Three observations worth pausing on:
- Parameters live in a dedicated address space in PTX but in banked
(constant memory constant memoryBanked read-only memory (SASS c[bank][offset]) holding e.g. kernel parameters; readable by non-load/store instructions.Glossary →c[bank][offset]) in SASS — readable directly by ALU instructions, no load needed. - Pascal’s undisassembled rows are
(introduced with Kepler): one 64-bit word per three instructions carrying stall counts, yield hints, and dependency barriers per instruction — the compiler doing the dependency bookkeeping a hardware scoreboard would otherwise do, reminiscent of the Tera Computer System’s explicit-dependence lookahead.control instructions control instructionsKepler+ scheduling words (1 per 3 instructions on Maxwell/Pascal) carrying stall counts, yield hints and dependency barriers — replacing hardware scoreboard checks.Glossary → .reuseflags (Maxwell onward) mark operands servable from an without a fresh register-file read — an energy optimization foreshadowing the register-file research of §3.6.operand reuse cache operand reuse cacheMaxwell+ structure implied by .reuse flags: lets an operand be read multiple times per register-file access, saving energy.Glossary →
2.2.2 AMD: a fully documented hardware ISA — with a scalar unit
AMD went the opposite way: with Southern Islands (the first Graphics Core
Next generation), they published the complete hardware-level ISA
specification, which is why lower-level academic simulators favor AMD. (AMD
still has a virtual ISA too —
The architectural headline is GCN’s split into v_ instructions compute a
different 32-bit value per thread; s_ instructions compute a single
value shared by the whole wavefront. Where does that help? Control flow —
and any computation that’s the same regardless of thread ID (the
scalarization opportunity §3.5 explores).
The book’s if/else example makes the machinery concrete. The
OpenCL source
1float fn0(float a, float b)2{3 if (a > b)4 return (a * a - b);5 else6 return (b * b - a);7}GCN (Southern Islands) microcode
1// r0 = "a", r1 = "b"; result in r22 3v_cmp_gt_f32 r0, r1 // a > b -> vcc4s_mov_b64 s0, exec // save current exec mask5s_and_b64 exec, vcc, exec // do "if"6s_cbranch_vccz label0 // branch if all lanes fail7v_mul_f32 r2, r0, r0 // result = a * a8v_sub_f32 r2, r2, r1 // result = result - b9label0:10s_not_b64 exec, exec // do "else"11s_and_b64 exec, s0, exec // do "else"12s_cbranch_execz label1 // branch if all lanes fail13v_mul_f32 r2, r1, r1 // result = b * b14v_sub_f32 r2, r2, r0 // result = result - a15label1:16s_mov_b64 exec, s0 // restore exec maskVector compare → vcc: v_cmp_gt_f32 is a VECTOR instruction: every lane compares its own a and b, writing one bit each into vcc. In our toy wavefront the comparison holds in lanes 0, 2 and 5. All 8 lanes are still executing.
Both sides of the branch execute serially, each under a partial mask — divergence costs throughput even here, at the ISA level. NVIDIA hardware automates this same bookkeeping with a reconvergence stack, which is exactly where Chapter 3 picks up (§3.1.1).
One more GCN insight for later: instead of hardware interlocks for
long-latency dependences, GCN uses S_WAITCNT to hold the
wavefront until a counter drops below a threshold. Compare that with
NVIDIA’s control instructions above: two vendors, the same conclusion —
let the compiler manage dependences; spend the silicon on lanes.
The two philosophies, side by side
| Hardware ISA documentation | Partial (opcode names) | Complete public spec |
| Virtual ISA | PTX (fully documented) | HSAIL (HSA stack) |
| HW backward compatibility | None promised — ISA redesigned per generation | None promised |
| Scalar unit | No (through Volta) | 1 scalar + 4 vector units per CU |
| Long-latency dependences | Control instructions (Kepler+) | S_WAITCNT counters |
| SIMT group | warp = 32 threads | wavefront = 64 threads |
Check yourself
1. Why does NVIDIA deliberately under-document SASS?
2. A key difference between PTX and SASS registers:
3. Predict the mask (use the GCN animation): 8-lane wavefront, a>b in lanes 0, 2, 5, exec initially all-1. After s_not_b64 exec, exec then s_and_b64 exec, s0, exec — what is exec for the else-body?
4. What do the Kepler+ 'control instructions' (one per 3 instructions on Maxwell/Pascal) do?
5. AMD GCN's S_WAITCNT waits on three per-wavefront counters. Which set?