Head of Software Engineering [GPU] — Complete Apprenticeship Curriculum
Target Roles:
- Head of Engineering / VP Engineering — AI Infrastructure (deep-tech startups)
- Principal / Staff Engineer — GPU Platforms, AI Compute Clouds (CoreWeave, Lambda, Together, Modal, NVIDIA, AMD)
- Platform Lead — LLM Serving & GPU Orchestration (vLLM-style platforms, inference clouds)
- Founding Engineer — Sovereign AI / On-Premise LLM Infrastructure
- Engineering Lead — Hardware-Agnostic AI Runtime / GPU Compiler Stack
Duration: 26–36 weeks core — extendable to 18 months for full head-of-engineering readiness Goal: Build the rare combination this JD demands: silicon-level GPU understanding, compiler and driver literacy, production LLM-serving depth (GPU scheduling, KV-cache management), hardware-decoupled platform architecture, security/licensing primitives for commercial software, and the leadership judgment to ship it all through a real release cycle.
Why This Curriculum Exists
The "Head of Software Engineering [GPU]" role is one of the hardest hiring profiles in the industry, because it sits at the intersection of five disciplines that are usually separate careers:
- GPU systems engineering — chips, SMs, warps, memory hierarchies, CUDA, drivers, compilers down to PTX/SASS.
- AI infrastructure — LLM serving, KV-cache management, continuous batching, multi-engine deployment (vLLM, TensorRT-LLM, SGLang).
- Distributed systems & HPC — collectives, NCCL, multi-node scheduling, failure domains.
- Platform productization — decoupling software from hardware (the JD's core mandate: "a platform that operates independently of the underlying hardware"), developer ecosystems, OEM/cloud integrations.
- Commercial software protection — hardware fingerprinting, license enforcement, code obfuscation, air-gapped/sovereign deployment for regulated industries.
Most engineers know one or two of these. This curriculum is built backward from the JD so that every "required" and "nice to have" line maps to phases and hands-on labs:
| JD Requirement | Where It's Trained |
|---|---|
| GPU-accelerated workloads, CUDA, GPU drivers | Phases 01, 02, 03, 04 |
| Runtime systems programming | Phases 03, 05, 09 |
| GPU scheduling | Phase 06 |
| KV-cache management, LLM serving optimisation | Phase 07 |
| Distributed systems, HPC | Phase 08 |
| Decouple software from hardware environments | Phase 09 (the keystone phase) |
| Linux internals, containers, observability tooling | Phases 04, 10 |
| Hardware fingerprinting, license enforcement, software protection, code obfuscation | Phase 11 |
| Sovereign AI, on-premise, air-gapped, regulated industries | Phases 07, 11 |
| Ship through a full release cycle; lead teams; OEM/partner strategy | Phases 10, 12 |
| Python, C++, systems language (Rust/Go/C), JS/TS tooling | Labs span C, C++, CUDA, Rust, Go, Python, TypeScript |
What You Will Build
By the end of this curriculum you will have produced, with your own hands:
- A SIMT execution simulator that demonstrates warp divergence and lockstep execution (Phase 01)
- Memory-hierarchy microbenchmarks in C that measure cache lines, bandwidth ceilings, and the roofline of your own machine (Phase 01)
- CUDA kernels from vector-add to tiled shared-memory matmul, with occupancy/coalescing experiments (Phase 02)
- A mini compiler that emits real PTX from an expression language — plus annotated dissections of NVCC's pipeline, PTX, and SASS (Phase 03)
- A Linux character-device driver in C with an ioctl interface that mimics how GPU drivers expose command submission (Phase 04)
- A container-from-scratch lab (namespaces + cgroups) showing exactly how GPUs pass into containers (Phase 04)
- A caching GPU-style memory allocator in Rust (the PyTorch-allocator design) and a stream scheduler in C++ (Phase 05)
- A GPU cluster scheduler simulator (bin-packing, gang scheduling, fragmentation metrics) and a Kubernetes device-plugin walkthrough (Phase 06)
- A paged KV-cache allocator with prefix sharing and a continuous-batching simulator with TTFT/TPOT/goodput metrics (Phase 07)
- Ring all-reduce implemented over real sockets, benchmarked against the theoretical 2(N-1)/N bound (Phase 08)
- A hardware abstraction layer in C with dlopen-loaded vendor backends — the literal architecture of "decouple software from hardware" (Phase 09)
- A GPU metrics exporter (Prometheus format), failure-drill runbooks, and an SLO definition pack (Phase 10)
- A hardware fingerprinting tool, an Ed25519-signed license-enforcement system with grace periods and air-gap activation, and an anti-tamper/obfuscation lab in C (Phase 11)
- A capstone: mini GPU orchestration platform — scheduler + HAL + serving + licensing + observability composed into one system, with a design doc and release-cycle artifacts (Phase 12)
Folder Structure
Head of Software Engineering [GPU]/
├── README.md ← You are here
├── jd.md ← Target role profile
├── phase-01-gpu-architecture-silicon/ ← From transistor to SM: how the chip actually works
├── phase-02-cuda-programming/ ← CUDA C++: kernels, memory, streams, occupancy
├── phase-03-gpu-compilers-codegen/ ← NVCC, PTX, SASS, LLVM NVPTX, Triton; build a mini PTX compiler
├── phase-04-linux-drivers-containers/ ← Kernel modules, ioctl, the GPU driver stack, cgroups, container GPU passthrough
├── phase-05-runtime-systems-memory/ ← Caching allocators, streams/events, async runtimes (Rust + C++)
├── phase-06-gpu-scheduling-virtualization/ ← MPS, MIG, time-slicing, k8s device plugins, cluster scheduling
├── phase-07-llm-serving-kv-cache/ ← KV-cache management, PagedAttention, continuous batching, multi-engine ops
├── phase-08-distributed-hpc-collectives/ ← NCCL, ring all-reduce, topology, multi-node failure domains
├── phase-09-hardware-abstraction-portability/ ← HAL design, plugin ABIs, dlopen backends — the decoupling mandate
├── phase-10-observability-production/ ← DCGM, exporters, eBPF, SLOs, release engineering
├── phase-11-security-licensing-protection/ ← Fingerprinting, license enforcement, obfuscation, attestation, air-gap
├── phase-12-leadership-capstone/ ← Head-of-engineering practice + mini orchestration platform capstone
├── interview-prep/ ← Concepts, coding, systems, leadership interviews
└── system-design/ ← GPU cloud, serving platform, HAL, licensing-at-scale walkthroughs
26-Week Core Schedule
| Week | Phase | Focus |
|---|---|---|
| 1 | 01 | GPU vs CPU design philosophy; SMs, warps, SIMT; build the SIMT simulator |
| 2 | 01 | Memory hierarchy: HBM, L2, shared memory, registers; roofline microbenchmarks in C |
| 3 | 02 | First CUDA kernels: grid/block/thread, vector add, error handling |
| 4 | 02 | Tiled matmul in shared memory; coalescing and occupancy experiments |
| 5 | 03 | The NVCC pipeline; reading PTX and SASS; Godbolt dissections |
| 6 | 03 | Build a mini expression→PTX compiler; Triton and LLVM NVPTX overview |
| 7 | 04 | Linux kernel modules; write a char device with ioctl command submission |
| 8 | 04 | The real GPU driver stack (KMD/UMD); containers from scratch; GPU passthrough |
| 9 | 05 | Memory allocators: buddy, slab, caching; build the Rust caching allocator |
| 10 | 05 | Streams, events, dependency graphs; build the C++ stream scheduler |
| 11 | 06 | GPU sharing: MPS vs MIG vs time-slicing; fractional GPUs |
| 12 | 06 | Cluster scheduling: bin-packing, gang scheduling; build the scheduler simulator |
| 13 | 07 | KV-cache memory math; build the paged KV allocator with prefix sharing |
| 14 | 07 | Continuous batching simulator; TTFT/TPOT/goodput; vLLM/TensorRT-LLM/SGLang ops |
| 15 | 08 | Collectives from first principles; implement ring all-reduce over sockets |
| 16 | 08 | NCCL, topology (NVLink/IB), multi-node training/serving failure modes |
| 17 | 09 | ABI vs API; plugin architectures; build the C HAL with dlopen backends |
| 18 | 09 | Portability strategy: CUDA/ROCm/Level Zero/Metal; capability negotiation |
| 19 | 10 | Observability: DCGM metrics, build the Prometheus exporter; eBPF tracing |
| 20 | 10 | SLOs, failure drills, release engineering: versioning, rollback, canary |
| 21 | 11 | Hardware fingerprinting: build the tool; threat-model the spoofing adversary |
| 22 | 11 | License enforcement: Ed25519-signed licenses, grace, air-gap activation |
| 23 | 11 | Software protection: obfuscation, anti-tamper, attestation; sovereign AI deployment |
| 24 | 12 | Leadership: org design, technical strategy memos, OEM/partner engineering |
| 25 | 12 | Capstone build: mini orchestration platform integration |
| 26 | 12 | Capstone release cycle: design doc, runbook, demo, retrospective |
Phase Structure
Every phase follows a consistent format:
| File | Purpose |
|---|---|
README.md | Why the phase exists, concepts list, lab tables (goal/steps/output/testing/talking points/resume bullet) |
WARMUP.md | Zero-to-expert primer: every concept from first principles, with lab walkthrough guidance, success criteria, interview Q&A, references |
HITCHHIKERS-GUIDE.md | The practitioner's deep-dive: mental models, production numbers, the things people actually get asked |
lab-XX-*/ | Hands-on labs: README walkthrough + working reference code you run, break, and extend |
Labs deliberately span the JD's language matrix: C (drivers, HAL, anti-tamper), C++/CUDA (kernels, stream scheduler), Rust (allocator, fingerprinting), Go (device plugin), Python (simulators, serving, licensing), TypeScript (platform CLI tooling in the capstone).
No GPU? You can still do ~85% of this track
Labs are designed in three tiers:
- CPU-only (most labs): simulators, allocators, schedulers, compilers-to-PTX (text output), licensing, HAL — need only a Linux/macOS box.
- Any-NVIDIA-GPU (Phase 02, parts of 03/10): a free Google Colab T4 or any consumer card works.
- Cluster-scale (parts of 06/08): simulated locally; the WARMUPs explain what changes at real scale.
Prerequisites
- Solid Python; working C or C++ (you can read pointers, structs, makefiles)
- Comfort in a Linux shell (you'll compile kernels modules and use
perf,strace) - Basic ML literacy: what a transformer forward pass is (or run the LLM Inference Engineer track Phases 01–04 first)
- Git, Docker basics
- No prior CUDA, driver, or compiler experience required — Phase 01 starts at the silicon
Related Tracks in This Repo
- LLM Inference Engineer — deeper on models/training; its Phase 09 pairs with our Phase 07
- Security Engineer — deeper on offensive/defensive security; pairs with our Phase 11
- AI Specialist — air-gapped serving labs that pair with our Phase 11 sovereign-AI work
We’re partnering with a fast-scaling deep tech startup building next-generation AI infrastructure that combines cutting-edge hardware and software to deliver high-performance AI compute solutions.
The Opportunity
We’re hiring a Head of Engineering to lead the development of a platform that operates independently of the underlying hardware. This is a critical leadership role focused on building a developer-first system that enables flexibility, performance, and global adoption.
The Role
Lead the development of a standalone orchestration platform Define and execute the strategy to decouple software from hardware environments Build and scale a developer-focused platform and ecosystem Drive integrations with OEMs, cloud providers, and infrastructure partners Own platform adoption and commercialisation through partnerships Stay hands-on where needed—maintaining technical credibility with engineering teams Engage with customers and partners at both technical and executive levels
What They’re Looking For
Minimum 10+ years in software engineering with at least 4 years leading teams of 5 or more engineers. Deep expertise in at least two of: AI infrastructure, MLOps, distributed systems, GPU-accelerated workloads, HPC, or runtime systems programming Strong background in GPU scheduling, KV-cache management, or LLM serving optimisation Hands-on experience deploying and optimising AI infrastructure at production scale, ideally with multiple inference engines, utilities, and training frameworks. Proven ability to ship production software through a full release cycle, not just a prototype Strong fluency in Python, C++, and a systems language (Rust, Go, or modern C); comfort with JavaScript or TypeScript for tooling Solid grounding in Linux internals, containers, GPU drivers, CUDA, and observability tooling
Nice to Have
Direct experience with sovereign AI, on-premise LLM deployment, air-gapped systems, or AI in regulated industries (defence, energy, government, finance, healthcare) Open-source contributions in the AI infrastructure space Prior experience as a founding engineer or senior engineer at a deep-tech startup that reached commercial GA Experience with security primitives: hardware fingerprinting, license enforcement, software protection, code obfuscation
Phase 01 — GPU Architecture: From Silicon to SM
Difficulty: ⭐⭐⭐☆☆ | Estimated Time: 2 weeks Roles supported: Head of Engineering [GPU], GPU Platform Engineer, Performance Engineer Hardware needed: none — both labs run on any CPU
Why This Phase Exists
Every decision you will make as a GPU platform leader — scheduler design, KV-cache budgets, which inference engine to standardize on, what to tell an OEM partner their chip needs — reduces to a handful of physical facts about how GPUs are built. Leaders who can't derive "decode is memory-bandwidth-bound" from silicon get out-negotiated by vendors and out-designed by competitors.
This phase builds that base. Not trivia — mechanism. You will be able to explain, with numbers, why a GPU dedicates its transistor budget to ALUs instead of caches and branch predictors, why 32 threads execute in lockstep, why divergence halves throughput, and why "how many GB/s" matters more than "how many TFLOPs" for the workloads this JD cares about.
Concepts
- Why GPUs exist: latency-oriented vs throughput-oriented design; the transistor-budget argument
- The compute hierarchy: GPU → GPC → SM → warp scheduler → CUDA core / Tensor Core
- SIMT execution: warps, lockstep, active masks, divergence and reconvergence
- Latency hiding: zero-cost warp switching vs CPU context switches; occupancy as a latency-hiding budget
- The memory hierarchy: registers → shared memory/L1 → L2 → HBM; sizes, latencies, bandwidths for A100/H100
- HBM: why stacked DRAM, why bandwidth is the headline number
- Arithmetic intensity and the roofline model — the single most useful performance abstraction
- Tensor Cores: what an MMA unit actually computes; why FP16/BF16/FP8 exist
- NVLink, PCIe, and why interconnect shows up in Phase 08
- Reading a spec sheet critically: A100 vs H100 vs MI300X vs a startup accelerator
Labs
Lab 01 — SIMT Simulator (Python)
| Field | Value |
|---|---|
| Goal | Build a warp-level SIMT interpreter that executes a tiny ISA in lockstep with active masks, then measure divergence cost. |
| Concepts | Warps, lockstep execution, active masks, divergence/reconvergence, why branchy code kills GPU throughput. |
| Steps | 1) Read solution.py top-down. 2) Run it; study the divergence report. 3) Do the extension exercises: add a reconvergence stack, implement vote.any, write a maximally-divergent kernel and predict its slowdown before running. |
| Stack | Pure Python, no dependencies |
| Output | Working simulator + a divergence-cost table you produced and can explain. |
| How to Test | python solution.py — built-in asserts verify lockstep semantics and the 32× worst-case divergence measurement. |
| Talking Points | Why SIMT ≠ SIMD; what the active mask is; why if (threadIdx.x % 2) halves throughput but if (blockIdx.x % 2) doesn't. |
| Resume Bullet | "Built a SIMT execution simulator demonstrating warp divergence semantics; quantified 2×–32× throughput loss across divergence patterns and used it to teach GPU execution to a platform team." |
| Extensions | Add predication (execute-both-sides) and compare against branch-based masks; simulate a warp scheduler choosing among 4 resident warps to hide a 400-cycle memory stall. |
Lab 02 — Memory Hierarchy & Roofline Microbenchmarks (C)
| Field | Value |
|---|---|
| Goal | Measure your own machine's memory hierarchy with C microbenchmarks, then build a roofline model and place kernels on it. |
| Concepts | Cache lines, latency vs bandwidth, pointer-chasing, streaming, arithmetic intensity, the roofline. |
| Steps | 1) make && ./membench. 2) Identify L1/L2/L3/DRAM plateaus in the latency curve. 3) Compute your machine's roofline ridge point. 4) Place vector_add (AI≈0.08) and matmul (AI grows with tile size) on the roofline; predict then verify which is memory-bound. |
| Stack | C (C11), make, optionally perf |
| Output | A filled-in ROOFLINE.md worksheet with your measured numbers. |
| How to Test | Latency plateaus match your CPU's published cache sizes within 2×; streaming bandwidth within 30% of spec. |
| Talking Points | Why pointer-chasing measures latency but streaming measures bandwidth; how the same methodology applies to HBM on a GPU; the A100/H100 ridge-point calculation. |
| Resume Bullet | "Wrote C microbenchmarks measuring cache-hierarchy latency/bandwidth; derived roofline models for CPU and GPU targets and used arithmetic-intensity analysis to predict kernel-bound regimes before profiling." |
| Extensions | Add a strided-access benchmark and explain the cache-line cliff at stride 64; rerun pinned to a different core with taskset and explain NUMA effects. |
Deliverables Checklist
- SIMT simulator runs; you can explain every line of the execution loop
- Divergence-cost table produced and explained
-
membenchresults with identified cache plateaus - Roofline worksheet: ridge points computed for your CPU and for A100/H100 from spec sheets
- You can do the "512 KB per token" style bandwidth math cold (see WARMUP Ch. 7)
Interview Relevance
- "Walk me through what happens when a warp hits a branch."
- "Why do GPUs have thousands of cores but no branch predictors?"
- "A100 has 1,555 GB/s HBM and 312 TFLOPS BF16 — what's the ridge point, and what does it mean for LLM decode?"
- "Your vendor claims their chip beats H100. What three numbers do you ask for?"
Warmup Guide — GPU Architecture: From Silicon to SM
Zero-to-expert primer for Phase 01. You need no prior hardware background: this guide starts at "what is a transistor budget" and ends with you computing rooflines for H100 and arguing with spec sheets. Every later phase stands on this one.
Table of Contents
- Chapter 1: Why GPUs Exist — The Transistor-Budget Argument
- Chapter 2: The Compute Hierarchy — GPU, GPC, SM, Warp
- Chapter 3: SIMT Execution — Warps, Lockstep, and the Active Mask
- Chapter 4: Divergence and Reconvergence
- Chapter 5: Latency Hiding — The GPU's Real Superpower
- Chapter 6: The Memory Hierarchy — Registers to HBM
- Chapter 7: Arithmetic Intensity and the Roofline Model
- Chapter 8: Tensor Cores and Reduced Precision
- Chapter 9: Interconnect — PCIe, NVLink, and Why It Matters Later
- Chapter 10: Reading a Spec Sheet Like a Head of Engineering
- Lab Walkthrough Guidance
- Success Criteria
- Interview Q&A
- References
Chapter 1: Why GPUs Exist — The Transistor-Budget Argument
Zero background: a chip is a fixed area of silicon. Every feature — arithmetic units, caches, control logic — costs transistors, and the budget is finite (H100: ~80 billion transistors on TSMC 4N). Architecture is the art of deciding what to spend them on.
The CPU's spend: a modern CPU core is mostly not arithmetic. It spends transistors on branch predictors, out-of-order execution windows, register renaming, prefetchers, and megabytes of cache. Why? Because a CPU's contract is minimize the latency of one instruction stream. All that machinery exists to keep a single thread from ever waiting.
The GPU's spend: a GPU inverts the contract: maximize the throughput of tens of thousands of threads, and let any individual thread wait as long as it likes. Under that contract, branch predictors and out-of-order windows are wasted transistors. Strip them out, and the same silicon area holds hundreds of ALUs instead of one fat core. The H100 has 16,896 FP32 lanes; a desktop CPU has on the order of hundreds (cores × SIMD width).
The catch: this trade is only a win if (a) the workload has massive data parallelism (the same operation over many elements — exactly what matmuls, convolutions, and attention are), and (b) you have a trick to stop stripped-down cores from stalling on memory. That trick is latency hiding (Chapter 5), and it is the deepest idea in this phase.
Production significance: this is why "port it to GPU" is not free. Code with unpredictable branches and pointer-chasing (graph traversal, parsers) hits the contract the GPU explicitly refused to optimize. When a partner asks "can your platform accelerate X," your first mental step is this chapter.
Common misconception: "GPUs are fast because they have more cores." No — per-lane, a GPU is slower than a CPU (lower clocks, in-order, no speculation). GPUs win only when parallelism and latency hiding apply. A single-threaded task runs ~10× slower on a GPU lane than on a CPU core.
Chapter 2: The Compute Hierarchy — GPU, GPC, SM, Warp
The physical organization, top-down, using H100 (SXM) numbers:
- GPU die → contains 8 GPCs (Graphics Processing Clusters) — mostly a layout/routing unit; you rarely reason about GPCs.
- GPC → contains SMs (Streaming Multiprocessors). H100 SXM: 132 SMs. The SM is the unit you reason about for almost everything: it is the GPU's "core."
- SM → contains 4 warp schedulers (sub-partitions), each owning:
- 32 FP32 lanes ("CUDA cores") → 128 per SM
- a slice of the register file (64K × 32-bit registers per SM — 256 KB; more than the L1!)
- 1 Tensor Core per scheduler (4 per SM)
- shared access to 228 KB of combined shared memory / L1
- Warp → 32 threads that execute as one unit (Chapter 3). Up to 64 warps resident per SM (2,048 threads).
Why this shape: the SM is a replication unit. NVIDIA scales product tiers by
fusing off SMs (a defective die becomes a cheaper SKU) — which is why your platform
code must never assume an SM count; it must query it (Phase 02's
cudaGetDeviceProperties, Phase 09's capability negotiation).
The software mapping you'll meet in Phase 02: a CUDA thread block is assigned to exactly one SM and stays there; the grid of blocks spreads across SMs. Blocks are the unit of scheduling-onto-SMs; warps are the unit of execution-inside-SMs.
Misconception: "CUDA core" ≈ "CPU core." A CUDA core is a single FP32 ALU lane — no fetch, no decode, no program counter of its own. The closest CPU analogy to an SM is one CPU core with very wide SIMD and 64-way hyperthreading.
Chapter 3: SIMT Execution — Warps, Lockstep, and the Active Mask
SIMT = Single Instruction, Multiple Threads. The hardware fetches and decodes one instruction and issues it to 32 threads at once (a warp). One program counter per warp, not per thread (pre-Volta; see the nuance below).
Each thread has its own registers, so the same instruction operates on different data — thread 0 adds element 0, thread 31 adds element 31. From the programmer's view, you write scalar per-thread code; from the hardware's view, it's a 32-wide vector instruction.
The active mask: a 32-bit mask deciding which lanes commit results this instruction. All 32 lanes always march together; the mask says which ones' writes count. This is the entire mechanism behind divergence (next chapter), and it is literally what your Lab 01 simulator implements.
SIMT vs SIMD (favorite interview distinction):
- SIMD (CPU AVX): you (or the compiler) must pack data into vector registers and handle edges/masks explicitly. Vector width is in the ISA.
- SIMT: you write scalar code per thread; the hardware handles masking and the warp width is a hardware property. Divergence is legal (just slow), pointers can differ arbitrarily per lane. SIMT = SIMD with per-lane control flow and addressing, managed by hardware.
Volta+ nuance — Independent Thread Scheduling: since Volta, each thread has its
own PC architecturally, and the hardware interleaves divergent paths, which makes
intra-warp synchronization deadlock-free (and is why __syncwarp() and the _sync
suffixed intrinsics exist). Execution is still mask-serialized — divergence still
costs the same — but correctness semantics changed. Saying "one PC per warp, and
since Volta that's virtualized per-thread" marks you as current.
Chapter 4: Divergence and Reconvergence
What happens at if (threadIdx.x < 16) A(); else B(); inside one warp:
- Lanes 0–15 want path A; lanes 16–31 want path B.
- Hardware executes A with mask
0x0000FFFF, then B with mask0xFFFF0000. - At the merge point the mask returns to
0xFFFFFFFF— reconvergence (classically tracked with a stack of (mask, reconvergence-PC) entries — your Lab 01 extension implements exactly this).
The cost model: time = sum of the divergent paths' lengths, not the max. Two
equal halves → 2× slowdown. Worst case, all 32 lanes take different paths (a 32-way
switch on threadIdx.x % 32) → 32× slowdown: each instruction issues 32 times
with one live lane. Lab 01 makes you measure this, not memorize it.
What does NOT diverge (the production rule of thumb):
- Branches uniform across the warp (
if (blockIdx.x % 2)) — every lane agrees, one path executes. Free. - Branches on data that happens to agree per-warp — also free at runtime.
- Short divergent paths — the compiler often converts them to predication (execute both sides, mask the writes), trading a few wasted instructions for no branch machinery.
Production significance: this is why GPU code sorts/buckets work by type before
processing (e.g., batching same-length sequences in serving — Phase 07 inherits this
idea), and why % 32-pattern branches are a code-review red flag in kernels.
Misconception: "divergence breaks correctness." Never — masks preserve semantics. Divergence is purely a throughput tax.
Chapter 5: Latency Hiding — The GPU's Real Superpower
The deepest single idea in GPU architecture.
The problem: HBM access latency is ~400–800 cycles. A CPU hides this with caches + prefetching + out-of-order execution — all the transistor-expensive machinery GPUs deleted. So how does a GPU not stall constantly?
The answer — oversubscription: each SM holds up to 64 resident warps but only issues from ~4 per cycle. When warp 7 issues a load, the scheduler doesn't wait: next cycle it issues from warp 23, then warp 41... By the time the data arrives 400 cycles later, warp 7 simply rejoins the eligible pool.
Why switching is free: on a CPU, a context switch saves/restores registers to memory (microseconds). On an SM, all 64 warps' registers live in the register file simultaneously — that's why it's 256 KB, bigger than L1. A "switch" is the scheduler picking a different index. Zero cycles.
Occupancy = resident warps / 64. It's a latency-hiding budget: with 400-cycle stalls and ~4-cycle compute between them, you need enough other warps to fill 400 cycles of issue slots. What limits occupancy: registers per thread (64K regs ÷ 256 regs/thread = 256 threads = 8 warps — bad), shared memory per block, block size. You'll tune this for real in Phase 02 Lab 02.
Misconception: "100% occupancy = fastest." No — occupancy only needs to be high enough to cover latency. A kernel with high arithmetic intensity may run at peak at 25% occupancy; pushing occupancy by cutting registers-per-thread can spill registers to memory and slow it down. Volkov's "Better Performance at Lower Occupancy" is the canonical reference.
Chapter 6: The Memory Hierarchy — Registers to HBM
Numbers to know cold (H100 SXM; A100 in parens where it differs meaningfully):
| Level | Size | Latency | Bandwidth | Scope |
|---|---|---|---|---|
| Registers | 256 KB/SM | 0 cyc | — | per-thread |
| Shared mem / L1 | 228 KB/SM (192) | ~30 cyc | ~20+ TB/s aggregate | per-block |
| L2 | 50 MB (40) | ~200 cyc | ~10 TB/s | device-wide |
| HBM3 (HBM2e) | 80 GB | ~500 cyc | 3.35 TB/s (2.0) | device-wide |
| NVLink 4 | — | ~µs | 900 GB/s | inter-GPU |
| PCIe 5.0 ×16 | — | ~µs | 64 GB/s | host↔device |
Walk the gradient: each level down is ~10× bigger and ~3–10× slower/narrower. The two cliffs that dominate everything: HBM↔L2 (the "memory wall" inside the device) and device↔host over PCIe (50× narrower than HBM — why "keep data on the GPU" is rule #1, and why Phase 05's allocator exists to avoid round trips).
Shared memory deserves emphasis: it is software-managed L1 — a scratchpad the kernel explicitly loads. The tiled matmul (Phase 02 Lab 01) stages tiles in shared memory so each HBM byte is reused ~tile-size times. Bank conflicts (32 banks, 4-byte wide) are the shared-memory analog of divergence: conflicting lanes serialize.
HBM in one paragraph: DRAM dies physically stacked next to the GPU on a silicon interposer, connected by thousands of through-silicon vias → an extremely wide bus (5,120 bits vs 64 for DDR) at modest clocks. High bandwidth, still-high latency — bandwidth is what you can buy with parallelism; latency you hide (Chapter 5).
Misconception: "the GPU's 80 GB is like RAM, the model just fits or doesn't." Capacity is only the first constraint; bandwidth is the performance constraint (Chapter 7), and fragmentation of that capacity is a real engineering problem (Phase 05 allocator, Phase 07 paged KV).
Chapter 7: Arithmetic Intensity and the Roofline Model
The one-chart performance model you will use weekly for the rest of this track.
Arithmetic intensity (AI) = FLOPs performed ÷ bytes moved from memory. It is a property of the algorithm, not the hardware.
vector_add: 1 FLOP per 12 bytes (read a, read b, write c, FP32) → AI ≈ 0.08- naive matmul (no reuse): AI ≈ O(1)
- tiled/blocked matmul: AI grows with tile size — large GEMMs reach AI in the hundreds
- LLM decode at batch 1: ~2 FLOPs per weight byte → AI ≈ 2 (with FP16 weights, ~1)
The roofline: attainable FLOP/s = min(peak compute, AI × memory bandwidth). Plot: x = AI (log), y = FLOP/s (log). Flat roof = compute peak; slanted roof = bandwidth × AI. The ridge point = peak ÷ bandwidth is where they meet:
$$AI_{ridge} = \frac{\text{peak FLOP/s}}{\text{memory bandwidth}}$$
H100 BF16 dense: 989e12 ÷ 3.35e12 ≈ 295 FLOP/byte. A100: 312e12 ÷ 2.0e12 ≈ 156.
The consequence that funds this entire JD: LLM decode sits at AI ≈ 2, two orders of magnitude left of the ridge → decode is bandwidth-bound, GPU compute sits ~99% idle at batch 1, and every serving technique in Phase 07 (batching, quantization, paging, speculation) is an attempt to move right on this chart or to buy more of the slanted roof. The 7B-model arithmetic: 14 GB of FP16 weights ÷ 3.35 TB/s ≈ 4.2 ms/token ≈ 240 tok/s ceiling at batch 1 — regardless of TFLOPs.
How to use it as a leader: before any optimization, compute the kernel's AI and place it. Memory-bound → quantize, fuse, batch, improve locality; compute-bound → Tensor Cores, lower precision, better tiling. Optimizing FLOPs on a bandwidth-bound kernel is a category error you will now never make, nor accept in a design review.
Misconception: comparing chips by TFLOPs. For inference fleets, bandwidth and capacity usually decide; the spec-sheet TFLOPs figure often assumes sparsity ("with 2:4 structured sparsity" — read footnotes) and a precision your workload may not use.
Chapter 8: Tensor Cores and Reduced Precision
What a Tensor Core is: a fixed-function unit that computes a small matrix
multiply-accumulate, e.g. D = A×B + C on 16×16-ish fragments, in one instruction
(mma.sync at the PTX level — you'll see it in Phase 03). One H100 Tensor Core does
hundreds of FLOPs/cycle vs 2 for a fused multiply-add on a CUDA core. That's how the
spec jumps from ~67 TFLOPS FP32 to ~989 TFLOPS BF16.
Why precisions multiply (FP16/BF16/FP8/INT8): halving the element size doubles both effective bandwidth (Chapter 7's slanted roof) and Tensor Core throughput.
- FP16: 1 sign + 5 exp + 10 mantissa — precise but narrow range (overflows at 65,504; why training needed loss scaling)
- BF16: 1 + 8 + 7 — FP32's range, less precision; the training default
- FP8 (E4M3/E5M2, Hopper+): inference and now training, with per-tensor scaling
- INT8/INT4: quantized inference (Phase 07 / llm-inference track Phase 09)
The fine print that matters in vendor talks: Tensor Cores only engage when data layout, precision, and dimensions match their fragment shapes — which is why cuBLAS/cuDNN pad dimensions to multiples of 8/16, and why "your model got faster by padding the vocab size to a multiple of 64" is a real production anecdote.
Chapter 9: Interconnect — PCIe, NVLink, and Why It Matters Later
A single H100 reads its own HBM at 3.35 TB/s, talks to peers over NVLink at 900 GB/s (bidirectional, 18 links), and to the host over PCIe 5.0 at ~64 GB/s. Memorize the ratio: HBM : NVLink : PCIe ≈ 50 : 13 : 1.
Consequences you'll meet again:
- Tensor parallelism (splitting one model across GPUs) is only viable over NVLink-class links — the per-layer all-reduces die over PCIe (Phase 08).
- "Just offload the KV cache to host RAM" costs 50× bandwidth — viable only for cold data (Phase 07's swapping tier).
- Multi-node = InfiniBand/RoCE at ~400 Gb/s ≈ 50 GB/s — another order down; topology-aware scheduling (Phase 06) exists because of this gradient.
Chapter 10: Reading a Spec Sheet Like a Head of Engineering
The checklist when a vendor (or an OEM partner — this JD's daily life) hands you a datasheet:
- Memory bandwidth and capacity first (inference fleets live and die here).
- Sustained vs peak: is the TFLOPs number "with sparsity"? At what clocks (boost vs sustained under power cap)? Dense BF16 is the honest comparable.
- Ridge point: compute it; compare against your workload's AI.
- Interconnect: peer-to-peer bandwidth and topology, not just "has NVLink."
- Software: what's the kernel library story? A chip without a mature compiler
- kernels (Phase 03) delivers a fraction of paper FLOPs. This is the question for startup accelerators — and the gap your hardware-agnostic platform (Phase 09) monetizes.
- Power/TCO: tokens per joule increasingly decides procurement.
- MIG/virtualization support (Phase 06): can you sell fractions of it?
Run this checklist on A100 vs H100 vs MI300X as the Chapter exercise — MI300X's larger HBM (192 GB) vs H100's software maturity is the canonical real-world tradeoff discussion.
Lab Walkthrough Guidance
Order: Lab 01 → Lab 02. Lab 01 cements Chapters 3–5 (execution); Lab 02 cements Chapters 6–7 (memory). Don't invert: divergence is easier to internalize first.
Lab 01 (SIMT simulator):
- Read
solution.pytop-down before running; predict what the divergence report will show. - Run; check predictions. The built-in asserts verify lockstep semantics.
- Extensions in order: (a) reconvergence stack — the (mask, pc) discipline from
Chapter 4; (b)
vote.any— your first warp-cooperative primitive; (c) write the maximally divergent kernel and predict 32× before measuring.
Lab 02 (membench):
make && ./membench— then stare at the latency curve until you can point at L1, L2, L3, DRAM.- Fill in
ROOFLINE.md: your CPU's ridge point from measured bandwidth and estimated peak; then A100's and H100's from spec sheets. - The capstone question: place batch-1 LLM decode (AI≈2) on the H100 roofline and write three sentences on what that implies — those three sentences are Phase 07's thesis.
Success Criteria
You're done with this phase when — without notes:
- You can explain the transistor-budget argument in 60 seconds (Ch. 1)
- You can define warp, active mask, and reconvergence, and state the 2×/32× divergence cost cases (Ch. 3–4) — and your simulator's output proves it
- You can explain why warp switching costs zero cycles and what occupancy actually buys (Ch. 5)
- You can reproduce the memory-hierarchy table within 2× from memory and identify your own machine's plateaus in your measured curve (Ch. 6)
- You can compute a ridge point and the 7B-decode token ceiling on any GPU given two spec numbers (Ch. 7)
- You can name the three spec-sheet questions that expose a weak accelerator pitch (Ch. 10)
Interview Q&A
Q1: Why don't GPUs have branch predictors? A: A branch predictor exists to keep a single instruction stream from stalling — latency optimization. The GPU's contract is throughput across thousands of threads: when a warp stalls (on memory or an unresolved branch), the scheduler issues from another resident warp at zero cost, because all warps' registers live permanently in the register file. Latency is hidden by parallelism rather than removed by prediction, so prediction hardware would be transistors taken away from ALUs for a problem the architecture already solves. Same argument deletes out-of-order execution and large caches.
Q2: A warp executes if (threadIdx.x % 2) A(); else B(); — what happens, exactly?
A: One warp-level PC walks both paths serially. Hardware pushes the reconvergence
point, executes A with active mask 0xAAAAAAAA, then B with 0x55555555, then
reconverges to full mask. Both paths' instruction counts add → ~2× slowdown; results
are still correct because masked lanes don't commit writes. If A and B are short,
the compiler instead predicates (issues both sides, masks writes) and avoids branch
overhead. Had the condition been uniform per warp (blockIdx.x % 2), there'd be no
divergence at all. Post-Volta, threads have independent PCs architecturally
(deadlock-free sync), but the serialization cost is unchanged.
Q3: Compute H100's ridge point and tell me what it means for LLM serving. A: Ridge = peak FLOP/s ÷ bandwidth = 989e12 (dense BF16) ÷ 3.35e12 B/s ≈ 295 FLOP/byte. Batch-1 decode performs ~2 FLOPs per weight byte read (AI ≈ 1–2), about 150× below the ridge → decode is purely bandwidth-bound; the ceiling for a 14 GB FP16 7B model is 3.35 TB/s ÷ 14 GB ≈ 240 tok/s with compute ~99% idle. Therefore serving economics come from raising effective AI: batching (reuse each weight byte across B requests), quantization (fewer bytes per weight), speculative decoding (more tokens per weight pass). That chain of reasoning is the foundation of every Phase 07 technique.
Q4: What's the difference between SIMT and SIMD, and why did NVIDIA choose SIMT? A: SIMD exposes vector width in the ISA: software packs registers, handles edge masks, can't have per-lane control flow or scattered addressing without explicit gather/scatter. SIMT presents a scalar program per thread; hardware groups 32 threads into a warp, runs them in lockstep, and manages masks automatically on divergence — per-lane branching and arbitrary per-lane addresses are legal, just potentially slow. NVIDIA chose SIMT because it makes the programming model scale: the same scalar kernel runs on any warp width or SM count, which is also why CUDA code from 2008 still runs on Hopper. The cost is a hardware tax (mask/reconvergence tracking) and the foot-gun that naive branchy code silently serializes.
Q5: When is increasing occupancy the wrong optimization?
A: Occupancy is a latency-hiding budget, not a goal. If a kernel already has enough
resident warps to cover its stall latency (or is compute-bound with high ILP), more
occupancy adds nothing — and the usual way you get more occupancy (fewer registers
per thread via -maxrregcount or __launch_bounds__) can force register spills to
local memory, adding the very memory traffic you were hiding. Volkov's classic
result: some kernels hit peak at ~25% occupancy via instruction-level parallelism.
The discipline: profile first (Phase 02), identify whether stalls are actually
latency-bound, and treat occupancy as one lever among register count, tiling, and
ILP.
Q6 (leadership): An OEM partner's new accelerator claims "2× H100 performance." How do you evaluate the claim for your platform? A: Decompose the claim along the roofline: 2× on what — dense BF16 TFLOPs, HBM bandwidth, or an app benchmark with sparsity footnotes? For our serving-dominated workloads I'd ask for: dense (non-sparse) throughput per precision, memory bandwidth and capacity, peer-to-peer interconnect topology, power envelope, and — most decisive — the software story: compiler maturity, kernel library coverage (attention/GEMM), and graph-capture support, because paper FLOPs without kernels deliver a fraction of peak (the exact gap our hardware-abstraction layer, Phase 09, exists to bridge). Then I'd run our own AI-binned benchmark suite: a bandwidth-bound decode workload, a compute-bound prefill/GEMM workload, and a collective-heavy multi-chip workload, and compare tokens/joule and tokens/$ rather than TFLOPs.
References
- Hennessy & Patterson, Computer Architecture: A Quantitative Approach, 6th ed. — Ch. 4 (data-level parallelism, GPUs)
- NVIDIA, CUDA C++ Programming Guide — Hardware Implementation chapter — https://docs.nvidia.com/cuda/cuda-c-programming-guide/
- NVIDIA H100 Architecture Whitepaper — https://resources.nvidia.com/en-us-tensor-core
- NVIDIA A100 Architecture Whitepaper ("Ampere") — https://www.nvidia.com/content/dam/en-zz/Solutions/Data-Center/nvidia-ampere-architecture-whitepaper.pdf
- Williams, Waterman, Patterson, "Roofline: An Insightful Visual Performance Model" (CACM 2009)
- Volkov, "Better Performance at Lower Occupancy" (GTC 2010) — https://www.nvidia.com/content/gtc-2010/pdfs/2238_gtc2010.pdf
- Jia et al., "Dissecting the NVIDIA Volta GPU Architecture via Microbenchmarking" (arXiv:1804.06826)
- Lindholm et al., "NVIDIA Tesla: A Unified Graphics and Computing Architecture" (IEEE Micro 2008) — the original SIMT paper
- AMD CDNA3 (MI300) Whitepaper — for the cross-vendor comparison exercise
- Cross-track: LLM Inference Engineer Phase 09 WARMUP — the serving-side consequences of Ch. 7
🛸 Hitchhiker's Guide — Phase 01: GPU Architecture
Read this if: you can write software but a GPU is still a black box that "does matmuls fast." This guide is the practitioner's compressed tour — the numbers, mental models, and war stories that the WARMUP derives from first principles.
0. The 30-second mental model
A GPU is a throughput machine: ~100+ small in-order cores (SMs), each running up
to 64 groups of 32 lockstep threads (warps), switching between them every cycle for
free to hide ~500-cycle memory latency. Performance = min(compute peak, arithmetic intensity × bandwidth) — the roofline. For the workloads this JD cares about (LLM
decode), AI ≈ 2 and the H100 ridge is ≈ 295, so bandwidth is the product and
TFLOPs are marketing.
If you remember one sentence: GPUs trade single-thread latency machinery for ALUs, and hide memory latency with warp oversubscription instead of caches.
1. The org chart of an H100
GPU (1 die, ~80B transistors)
└── 132 SMs ← "cores"; the unit you reason about
└── 4 warp schedulers per SM ← each issues 1 instr/cycle from eligible warps
├── 32 FP32 lanes ← "CUDA cores" = ALU lanes, nothing more
├── 1 Tensor Core ← small-matrix MMA unit; where the TFLOPs live
└── register file slice ← 256 KB/SM total; all resident warps live here
└── 228 KB shared mem / L1 ← software-managed scratchpad
└── 50 MB L2 (device-wide)
└── 80 GB HBM3 @ 3.35 TB/s
└── 18 NVLink4 links @ 900 GB/s aggregate; PCIe 5 @ 64 GB/s
Bandwidth ratio to tattoo on your arm: HBM : NVLink : PCIe ≈ 50 : 13 : 1.
2. Execution: warps and divergence in 4 bullets
- 32 threads = 1 warp = 1 instruction stream. Active mask decides who commits.
- Divergent branch → paths run serially with complementary masks → cost is the sum of path lengths. 2-way even split = 2×; pathological 32-way = 32×.
- Uniform branches (same decision warp-wide) are free.
blockIdx-based: free.threadIdx % 2: 2×. Code review accordingly. - Post-Volta threads have independent PCs (sync inside divergent code won't
deadlock), but the serialization cost is identical. Use
__syncwarp()/*_syncintrinsics.
3. Latency hiding beats caching
The numbers that make the design click:
| CPU core | H100 SM | |
|---|---|---|
| Strategy | predict + cache + OoO | oversubscribe warps |
| Threads resident | 2 (SMT) | 2,048 (64 warps) |
| Context switch | ~µs (OS) | 0 cycles (scheduler mux) |
| Where thread state lives | memory/caches | 256 KB register file |
Occupancy = resident-warp fraction. It's a budget for hiding stalls, not a score:
plenty of kernels peak at 25–50% occupancy (Volkov). The classic failure: chasing
100% occupancy with -maxrregcount, causing register spills, getting slower.
4. The memory wall, quantified
Per-byte energy and latency dominate compute by orders of magnitude. Practical table (H100):
| Access | Cost intuition |
|---|---|
| Register | free |
| Shared memory | ~30 cyc; 32 banks — conflicts serialize like divergence |
| L2 hit | ~200 cyc |
| HBM | ~500 cyc; 3.35 TB/s — the number |
| Peer GPU (NVLink) | ~µs; 900 GB/s |
| Host (PCIe) | ~µs; 64 GB/s — 50× below HBM. Do not cross casually. |
Rules that fall out: keep data resident (Phase 05 allocator), stage reuse through shared memory (Phase 02 tiled matmul), batch host↔device transfers and overlap with compute (streams), never put a synchronous PCIe copy in a decode loop (a real production incident pattern — it shows up as mysterious 5× TPOT regressions).
5. Roofline: the only performance chart you need
attainable = min(peak_flops, AI × bandwidth), AI = FLOPs / bytes
| Workload | AI (FLOP/byte) | H100 verdict (ridge ≈ 295) |
|---|---|---|
| vector add | ~0.08 | hopelessly bandwidth-bound |
| LLM decode, batch 1, FP16 | ~1–2 | bandwidth-bound; 240 tok/s ceiling for 7B |
| LLM decode, batch 128 | ~128–256 | approaching compute-bound — this is why continuous batching exists |
| LLM prefill / big GEMM | 100s–1000s | compute-bound; Tensor Core territory |
Workflow: compute AI → place on roofline → choose lever (memory-bound: quantize, fuse, batch, cache; compute-bound: precision, Tensor Cores, tiling). Refuse to review any "optimization" PR or vendor claim that skips this step.
6. Tensor Cores & precision menu
- Tensor Core = fixed-function fragment MMA (
mma.syncin PTX). FP32 path: 67 TFLOPS. Tensor Core BF16: 989. The headline number is only reachable through them — via cuBLAS/cuDNN/CUTLASS or Triton, not hand-rolled FP32 loops. - Precision ladder: FP32 → TF32 (free Ampere training win) → BF16 (training default; FP32 range) → FP16 (more mantissa, less range; loss-scaling era) → FP8 E4M3/E5M2 (Hopper inference/training) → INT8/INT4 (quantized inference). Each halving doubles bandwidth-limited throughput AND Tensor Core rate — a double roofline win, which is why quantization is the most leveraged inference optimization (Phase 07).
- Gotcha: dimensions must align to fragment shapes; padding to multiples of 8/16/64 is sometimes a real speedup. ("We made the vocab a multiple of 64 and gained 8%." — true story across several labs.)
7. Spec-sheet self-defense (the leadership skill)
Vendor says "2× H100." You ask:
- Dense or sparse TFLOPs? (2:4 sparsity footnote inflates 2×.)
- Sustained clocks under power cap, or boost?
- HBM bandwidth and capacity? (Inference fleets are bought on these.)
- P2P topology — all-to-all NVLink-class, or PCIe islands?
- What runs on it today? Compiler maturity, attention/GEMM kernel coverage, framework integration. Silicon without software delivers ~20–40% of paper peak for the first years (every new accelerator's story; also the market gap a hardware-agnostic platform exploits).
- Tokens/joule on your benchmark mix — AI-binned: one bandwidth-bound, one compute-bound, one collective-heavy.
MI300X vs H100 is the live case study: 192 GB vs 80 GB HBM (capacity win → fewer GPUs per model), comparable bandwidth, but a software-ecosystem gap that erodes the paper advantage. Where would your platform's HAL (Phase 09) change that calculus? That question is essentially this JD's pitch.
8. What you should be able to do after this phase
- Sketch the H100 org chart from memory with sizes/bandwidths within 2×.
- Derive the 240 tok/s 7B ceiling in 30 seconds from two spec numbers.
- Predict divergence cost of a code snippet by inspection.
- Run a roofline argument in a design review — and detect when someone else's performance claim violates one.
- Interrogate a spec sheet down to its footnotes.
Next: Phase 02 puts your hands on the machine — you'll write the kernels these mental models describe, and Nsight will show you warps stalling exactly where this guide says they will.
Lab 01 — SIMT Simulator: Warps, Masks, and Divergence
Phase: 01 — GPU Architecture | Difficulty: ⭐⭐⭐☆☆ | Time: 3–4 hours Language: Python (stdlib only) | Hardware: none
Concept primer:
../WARMUP.mdCh. 3–4 (SIMT, active mask, divergence),../HITCHHIKERS-GUIDE.md§2.
Run
python solution.py # runs the simulator + divergence benchmark + asserts
0. The mission
Build (and then extend) a tiny SIMT machine: one program counter per warp, 32 lanes, an active mask — and use it to measure what divergence costs instead of taking the textbook's word for it.
The two questions you must be able to answer at the end:
- Why does
if (tid % 2)cost 2× butif (tid < 32)cost nothing (per warp)? - Why is the worst case exactly 32× — and what real code patterns approach it?
1. The ISA
The simulator executes a list of instructions over 32 lanes. Each lane has its own register file; the warp has one PC and a mask stack:
| Instr | Semantics |
|---|---|
SET rd, imm | rd = imm in every active lane |
TID rd | rd = lane_id (this is threadIdx.x % 32) |
ADD/SUB/MUL rd, ra, rb | lane-wise arithmetic on active lanes |
MOD rd, ra, imm | rd = ra % imm |
LT rd, ra, rb | rd = 1 if ra < rb else 0 |
BRZ ra, target | branch if ra == 0 — the divergence point |
JMP target | unconditional |
RECONV | reconvergence marker (pops the mask stack) |
HALT | warp done |
This mirrors real hardware closely enough to be honest: PTX has @p bra (predicated
branch) and the hardware tracks reconvergence via a stack of (mask, pc) entries
(classic pre-Volta SSY/SYNC discipline — see WARMUP Ch. 3 for the Volta+ nuance).
2. How divergence is executed
When BRZ finds that some active lanes branch and some don't:
- Push
(current_mask, reconvergence_pc)onto the mask stack. - Run the fall-through path with the not-taken lanes' mask.
- At
RECONV, switch to the taken path with the complementary mask. - At
RECONVagain, pop the stack — full mask restored, paths rejoined.
Every instruction executed counts once per issue, regardless of how many lanes were active — that's the physical cost model: an instruction with 1 live lane costs the same issue slot as one with 32.
3. What the benchmark shows
solution.py runs the same workload four ways and reports issue counts:
| Kernel | Branch pattern | Predicted cost | Why |
|---|---|---|---|
uniform | no branch | 1.0× | baseline |
warp_uniform | all lanes agree on the branch | ~1.0× | uniform branch — no split |
even_odd | tid % 2 | ~2× on the divergent region | two serialized half-warps |
max_divergent | 32-way switch on tid | ~32× | each per-lane path issues with 1 live lane |
Expected output (yours will match exactly — it's deterministic):
kernel issues util% slowdown
uniform 6 100.0 1.00x
warp_uniform 7 100.0 1.17x
even_odd 10 70.0 1.67x
max_divergent 226 58.4 37.67x
util% = lane-cycles doing committed work ÷ (issues × 32) — the simulator's version
of Nsight's "warp execution efficiency" metric you'll meet in Phase 02.
Two readings worth pausing on:
even_oddshows 1.67×, not 2.0× — because only the divergent region doubles; the shared prologue (TID/MOD) issues once. Real kernels behave the same way: divergence cost is proportional to how much code sits inside the split. Isolate the divergent region and you'll measure the clean 2×.max_divergentutilization is 58%, not 3% — the 32-way dispatch chain (compare/branch per case) runs fully populated; only each one-lane body wastes 31 lanes. The slowdown is still catastrophic (37×). This mirrors realswitchlowering on GPUs.
4. Extension exercises (do them — this is the lab)
- Reconvergence stack depth: nest two divergent branches and assert the stack reaches depth 2 and unwinds correctly. (The skeleton handles one level; nested divergence is where naive implementations break.)
VOTE_ANY rd, ra: warp-cooperative primitive —rd = 1in all active lanes if any active lane hasra != 0. This is__any_sync(). Then explain in a comment why warp votes need no memory traffic on real hardware.- Predication: add
SELP rd, ra, rb, rp(lane-wise select) and rewriteeven_oddwithout branches. Compare issue counts — you've just reproduced the compiler's if-conversion decision (WARMUP Ch. 4). - Latency hiding mini-model: simulate 4 resident warps where
LDstalls a warp for 40 cycles; a scheduler issues from any non-stalled warp each cycle. Measure utilization with 1, 2, 4 warps and observe the occupancy curve flatten — Chapter 5 made quantitative.
5. Common pitfalls
- Forgetting to count issues for instructions with few active lanes — the whole point is that issue cost is mask-independent.
- Reconverging at the branch target instead of the post-dominator — your
even_oddwill produce wrong values in half the lanes. - Letting inactive lanes commit writes — masks must gate writes, not reads.
- Treating
JMPinside a divergent region as a reconvergence — it isn't; onlyRECONVpops.
6. What this lab proves about you
You can explain SIMT execution mechanically — masks, stacks, issue costs — which is the difference between "GPUs don't like branches" hand-waving and the engineering-leader version: "this kernel's branch is warp-uniform, it's free; that one is per-lane data-dependent and our profile shows 41% warp efficiency, here's the restructuring." Phase 02's profiler sessions will show you these exact numbers on real silicon.
Lab 02 — Memory Hierarchy & Roofline Microbenchmarks (C)
Phase: 01 — GPU Architecture | Difficulty: ⭐⭐⭐☆☆ | Time: 3–5 hours Language: C11 + make | Hardware: any CPU (the methodology transfers to GPU)
Concept primer:
../WARMUP.mdCh. 6–7 (memory hierarchy, roofline),../HITCHHIKERS-GUIDE.md§4–5.
Run
make
./membench # latency curve + bandwidth + roofline placement
0. The mission
Stop trusting tables — measure a memory hierarchy yourself, with the same two techniques every GPU microbenchmarking paper uses (e.g., Jia et al.'s Volta dissection):
- Pointer-chasing measures latency: each load's address depends on the previous load's value, so the CPU cannot overlap or prefetch — you see the raw round-trip per level.
- Streaming measures bandwidth: independent sequential accesses let the hardware pipeline everything — you see the peak transfer rate.
Then you place two kernels on your machine's roofline and verify the model predicts which one is memory-bound. This is the exact methodology you'd use to characterize an unknown accelerator a vendor hands you (WARMUP Ch. 10).
1. What the program does
- Latency sweep: builds a randomly-permuted cycle of pointers in buffers from 4 KB to 256 MB and chases it 10M times. Plotting ns/load vs buffer size shows plateaus at each cache level, with cliffs between them.
- Bandwidth: streams a large array (read-sum, then memcpy-style write) and reports GB/s.
- Roofline placement: times
vector_add(AI ≈ 0.083 FLOP/byte) and a blockedmatmul(AI grows with block size), converts to GFLOP/s, and prints where each lands relative tomin(peak, AI × measured_bandwidth).
Example output (Apple M-series; your numbers will differ — that's the point):
== latency (pointer chase) ==
4 KB 1.2 ns/load <- L1
128 KB 3.1 ns/load <- L2
8 MB 9.8 ns/load <- SLC/L3
256 MB 98.4 ns/load <- DRAM
== bandwidth (stream) ==
read : 61.2 GB/s write : 38.9 GB/s
== roofline ==
vector_add: AI=0.083 predicted ceiling 5.1 GFLOP/s measured 4.7 -> memory-bound (92% of roof)
matmul b=64: AI≈10.7 measured 41.3 GFLOP/s -> compute-bound region
2. Fill in ROOFLINE.md
The worksheet asks you to:
- Identify each plateau and match it to your CPU's published cache sizes.
- Compute your CPU's ridge point from measured bandwidth + estimated peak (cores × clock × SIMD width × 2 FLOPs).
- Compute A100 and H100 ridge points from their spec sheets (WARMUP Ch. 7 has the answers — derive before checking).
- Place batch-1 LLM decode (AI ≈ 2) on the H100 roofline and write the three-sentence implication (this is Phase 07's thesis statement).
3. Why the methodology transfers to GPUs
| This lab (CPU) | The GPU equivalent |
|---|---|
| pointer-chase ns/load cliffs | shared mem ~30 cyc → L2 ~200 → HBM ~500 (Jia et al. measured exactly this way, in CUDA) |
| stream GB/s | HBM bandwidth tests (bandwidthTest, nvbandwidth) |
| roofline placement | Nsight Compute's roofline chart (Phase 02 Lab 02 reads it) |
| prefetcher distorting naive latency tests | GPU coalescer distorting naive bandwidth tests (Phase 02) |
4. Common pitfalls
- Sequential pointer chase — the prefetcher hides DRAM latency and you'll "measure" L1 everywhere. The permutation must be random. (Try it: replace the shuffle with identity and watch the curve flatten — instructive!)
- Compiler deleting your loop — the chase result must feed a
volatilesink or the optimizer removes everything. Same trick JMH uses (Blackhole), same trick you'll need in CUDA benchmarks. - Timing one pass — too noisy; the harness does warmup + multiple reps and reports the minimum (least-disturbed) run.
- Forgetting
-O2— at-O0you're benchmarking the compiler's laziness, not the memory system.
5. Extensions
- Add a strided read benchmark (stride 1, 2, 4, … 128 elements) and find the cliff at your cache-line size (64 B): past it, every load is a new line and bandwidth collapses by the stride factor. This is exactly the GPU coalescing argument of Phase 02 — uncoalesced access wastes the same way.
- Run under
taskset -c 0vs a core on another socket/cluster (Linux) and observe NUMA / cluster effects. - Use
perf stat -e cache-misses,cache-references ./membenchand correlate the counters with your plateaus.
6. What this lab proves about you
You can characterize any memory system — CPU today, an OEM partner's accelerator next quarter — with two from-scratch benchmarks and place workloads on its roofline before a single line of vendor marketing reaches your team.
Phase 02 — CUDA Programming: Kernels, Memory, Streams
Difficulty: ⭐⭐⭐⭐☆ | Estimated Time: 2 weeks Roles supported: Head of Engineering [GPU], GPU Platform Engineer, Kernel/Performance Engineer Hardware needed: any NVIDIA GPU — a free Google Colab T4 is enough for every lab here
Why This Phase Exists
The JD says "stay hands-on where needed — maintaining technical credibility with engineering teams." For a GPU platform, that credibility has one test: can you write, debug, and profile a CUDA kernel yourself? Not because the Head of Engineering writes kernels daily, but because every estimate you approve, every "it's a kernel problem" escalation you arbitrate, and every hire you evaluate depends on having done it.
Phase 01 gave you the machine model. This phase makes you operate the machine: the CUDA programming model (grids/blocks/threads), the memory API and its async variants, shared-memory tiling, coalescing, occupancy, streams, and the profiler that tells you the truth.
Concepts
- The CUDA software stack: runtime API vs driver API; what
nvccproduces (handoff to Phase 03) - Kernel launch anatomy: grid/block/thread,
<<<blocks, threads, smem, stream>>>, dimension math - The block-to-SM contract: why blocks must be independent; what that buys the hardware
- Error handling discipline:
cudaGetLastError, sync vs async errors, theCUDA_CHECKmacro - Memory management:
cudaMalloc/cudaMemcpy, pinned (page-locked) memory, unified memory and its page-fault cost - Global-memory coalescing: 32-byte sectors, AoS vs SoA, the strided-access cliff
- Shared memory:
__shared__, tiling, bank conflicts,__syncthreads()discipline - Occupancy in practice: registers/thread,
__launch_bounds__, the occupancy calculator - Streams and events: async copies, overlap,
cudaEventElapsedTimefor honest timing - Atomics and reductions: warp shuffle (
__shfl_down_sync), block reduction patterns - Profiling: Nsight Systems (timeline) vs Nsight Compute (kernel deep-dive); the three metrics that matter first
Labs
Lab 01 — First Kernels: Vector Add → Tiled Matmul (CUDA C++)
| Field | Value |
|---|---|
| Goal | Go from hello kernel to a shared-memory tiled matmul that beats the naive version ~5–10×, verified against CPU reference. |
| Concepts | Launch config math, error checking, coalescing, shared-memory tiling, __syncthreads(), honest benchmarking with events. |
| Steps | 1) Build & run make && ./kernels. 2) Read kernels.cu section by section against the README walkthrough. 3) Verify the naive→tiled speedup. 4) Do the extension ladder (rectangular tiles, register blocking, compare to cuBLAS). |
| Stack | CUDA C++ (nvcc), works on any sm_70+ GPU; Colab notebook instructions included |
| Output | Working binary + a timing table: naive vs tiled vs (extension) cuBLAS. |
| How to Test | Built-in: every kernel's output is compared elementwise against a CPU reference; speedup asserts print PASS/FAIL. |
| Talking Points | Why the tiled version is faster (AI raised from ~O(1) to ~tile-size — the roofline from Phase 01 Lab 02); why __syncthreads() twice per tile; what cuBLAS still does that you don't. |
| Resume Bullet | "Implemented and verified CUDA kernels from vector-add through shared-memory tiled GEMM; achieved 8× speedup over naive matmul and validated the roofline prediction with Nsight Compute metrics." |
| Extensions | Register blocking (each thread computes a 2×2 output micro-tile); compare against cuBLAS sgemm and report the remaining gap; try __launch_bounds__ and explain the occupancy change. |
Lab 02 — Coalescing, Occupancy & the Profiler (CUDA C++)
| Field | Value |
|---|---|
| Goal | Measure the cost of uncoalesced access and bank conflicts; read Nsight Compute output like a platform engineer. |
| Concepts | 32-byte sectors, strided/transposed access, AoS vs SoA, shared-memory banks + padding fix, occupancy limiters. |
| Steps | 1) make && ./bench. 2) Record the bandwidth table (stride 1/2/8/32; AoS vs SoA; transpose naive vs tiled vs padded). 3) Profile two kernels with ncu and identify the limiter (memory vs compute vs latency). 4) Fill in PROFILE.md. |
| Stack | CUDA C++, Nsight Compute CLI (ncu) — Colab-compatible commands provided |
| Output | Bandwidth/conflict tables + a filled PROFILE.md naming each kernel's limiter with evidence. |
| How to Test | Stride-32 bandwidth collapses to ~1/8–1/32 of stride-1; padded transpose recovers ≥80% of copy bandwidth. |
| Talking Points | "Coalescing is the GPU's cache-line story" (ties to Phase 01 Lab 02 strided extension); why SoA wins on GPU; how you'd review a kernel PR with only an ncu report attached. |
| Resume Bullet | "Quantified memory-coalescing and bank-conflict penalties on Ampere (8–20× bandwidth loss); established Nsight-Compute-based review checklist adopted for kernel PRs." |
| Extensions | Vectorized loads (float4) — measure the gain; async copy (cp.async, sm_80+) into shared memory; build a one-page "kernel review checklist" for your team. |
Deliverables Checklist
- Both labs build and pass their built-in verification on a real GPU
- Timing table: naive vs tiled matmul, with the roofline explanation written down
- Bandwidth tables for stride / AoS-SoA / transpose experiments
-
PROFILE.mdfilled: each kernel's limiter named withncuevidence -
You can write the
CUDA_CHECKmacro and the launch-config formula from memory
Interview Relevance
- "Launch config for a 1920×1080 image filter — walk me through the math."
- "Your kernel gets 8% of peak bandwidth. List your first three hypotheses."
- "Why are thread blocks required to be independent? What feature does that enable?"
- "What does
__syncthreads()do, and what happens if it's inside a divergent branch?" - "When is unified memory acceptable in production?"
Warmup Guide — CUDA Programming
Zero-to-expert primer for Phase 02. Assumes Phase 01's machine model (warps, SMs, memory hierarchy, roofline) and basic C/C++. By the end you can write, verify, time, and profile real kernels — and review other people's.
Table of Contents
- Chapter 1: The CUDA Stack — What You're Actually Calling
- Chapter 2: The Programming Model — Grid, Block, Thread
- Chapter 3: Your First Kernel, Line by Line
- Chapter 4: Error Handling — The Discipline That Separates Professionals
- Chapter 5: The Memory API — cudaMalloc to Unified Memory
- Chapter 6: Coalescing — The #1 Performance Rule
- Chapter 7: Shared Memory and Tiling — Raising Arithmetic Intensity
- Chapter 8: Occupancy in Practice
- Chapter 9: Streams, Events, and Honest Timing
- Chapter 10: Reductions and Warp Primitives
- Chapter 11: Profiling — Nsight Systems vs Nsight Compute
- Lab Walkthrough Guidance
- Success Criteria
- Interview Q&A
- References
Chapter 1: The CUDA Stack — What You're Actually Calling
Zero background: "CUDA" is three things people conflate: a language extension
(C++ plus __global__, <<<>>>), a runtime library (libcudart — the
cudaMalloc/cudaMemcpy API), and a driver stack (libcuda.so, the kernel-mode
driver — Phase 04 territory).
The call chain when you launch a kernel:
your code → CUDA Runtime API (libcudart) cudaLaunchKernel(...)
→ CUDA Driver API (libcuda.so) cuLaunchKernel(...)
→ ioctl(2) into the kernel-mode driver (/dev/nvidia*)
→ commands written to a ring buffer the GPU hardware consumes
Two facts with production consequences:
- The runtime API is a convenience layer over the driver API. Inference
engines and runtimes (Phase 05/09) often use the driver API directly for
explicit context control, loading cubins at runtime (
cuModuleLoad), and multi-tenancy. You should be able to read both. - Kernel launches are asynchronous. The CPU enqueues and returns in ~5–10 µs; the GPU executes later. Everything about timing (Ch. 9), error reporting (Ch. 4), and overlap (streams) follows from this one fact.
Misconception: "nvcc is a compiler like gcc." nvcc is a driver that
splits your file: host code → your host compiler; device code → PTX/SASS through
NVIDIA's backend. Phase 03 dissects this pipeline; here you just use it.
Chapter 2: The Programming Model — Grid, Block, Thread
You write one scalar function (a kernel); the runtime launches N instances:
- Thread: one instance. Knows its coordinates via
threadIdx,blockIdx,blockDim,gridDim(each a 3-component vector for 1D/2D/3D convenience). - Block: up to 1,024 threads that share a shared-memory allocation, can
__syncthreads(), and are guaranteed to run on one SM. - Grid: all blocks of a launch. Blocks must be independent — no cross-block sync within a kernel, no assumption about execution order.
Why block independence is the load-bearing rule: it's what lets the hardware
scheduler scatter blocks across however many SMs exist — 10 on a laptop, 132 on
H100 — and retire/replace them in any order. Block independence is CUDA's
scalability contract; it is why 2008 kernels still scale on 2025 silicon. (It's
also why a "wait for block X" spinlock in a kernel deadlocks: block X may not be
resident.) Cooperative groups (cudaLaunchCooperativeKernel) exist as the
explicit, occupancy-checked exception.
The index formula you'll write a thousand times:
int i = blockIdx.x * blockDim.x + threadIdx.x; // global index
if (i < n) { ... } // guard the tail
int nblocks = (n + threads - 1) / threads; // ceil-div launch config
The guard exists because the grid is a multiple of the block size and n usually
isn't. Forgetting it = out-of-bounds writes = the classic first CUDA bug
(intermittent corruption, not a crash — see Ch. 4).
Mapping back to Phase 01: blocks are carved into warps (threads 0–31, 32–63, …). All divergence/coalescing reasoning operates at warp granularity within the block you configured. Block size should essentially always be a multiple of 32 — 128 or 256 are the standard defaults.
Chapter 3: Your First Kernel, Line by Line
__global__ void vec_add(const float* a, const float* b, float* c, int n) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < n) c[i] = a[i] + b[i];
}
// host:
int threads = 256;
int blocks = (n + threads - 1) / threads;
vec_add<<<blocks, threads>>>(d_a, d_b, d_c, n);
__global__: callable from host, runs on device. (__device__= device-only helper;__host__ __device__= compiled for both.)- The pointers are device pointers (from
cudaMalloc). Dereferencing a host pointer in a kernel is undefined behavior — typicallyillegal memory accessreported much later (Ch. 4). <<<blocks, threads>>>has two more slots: dynamic shared-memory bytes and a stream —<<<blocks, threads, smem_bytes, stream>>>(Ch. 7, 9).
Production reality: vector add is the memory-bound kernel (AI ≈ 0.08, Phase 01 Ch. 7). Its only performance question is "did we hit memory bandwidth?" — which makes it the perfect coalescing test harness, and exactly how Lab 01 uses it.
Chapter 4: Error Handling — The Discipline That Separates Professionals
CUDA errors come in two flavors, and conflating them wastes engineer-days:
- Synchronous: bad launch config, OOM at
cudaMalloc— returned immediately by the call. - Asynchronous: the kernel itself faults (out-of-bounds, bad pointer) — surfaces at the next synchronizing call, attributed to an innocent line.
The non-negotiable macro (write it from memory in interviews):
#define CUDA_CHECK(call) do { \
cudaError_t e = (call); \
if (e != cudaSuccess) { \
fprintf(stderr, "CUDA error %s:%d: %s\n", \
__FILE__, __LINE__, cudaGetErrorString(e)); \
exit(1); \
} \
} while (0)
kernel<<<g, b>>>(args);
CUDA_CHECK(cudaGetLastError()); // catches launch-config errors
CUDA_CHECK(cudaDeviceSynchronize()); // catches kernel-execution errors (debug builds)
Debugging ladder when something faults: 1) compute-sanitizer ./app (catches the
faulting kernel + address — the GPU's valgrind), 2) CUDA_LAUNCH_BLOCKING=1 (makes
async errors synchronous so attribution is correct), 3) cuda-gdb for the rare
hard case.
Production significance: a fault in any kernel poisons the CUDA context — subsequent calls return errors until process restart. This is why serving engines treat "CUDA error" as fatal-and-restart (Phase 10 runbooks), and why your platform must isolate tenants at process granularity, not context granularity (Phase 06).
Chapter 5: The Memory API — cudaMalloc to Unified Memory
The four tiers, in order of how often you should reach for them:
- Explicit —
cudaMalloc+cudaMemcpy. Predictable, fast, verbose. The production default.cudaMemcpyis synchronous-ish; the async variant requires tier 2. - Pinned host memory —
cudaMallocHost. Page-locked so the GPU's DMA engine can read it directly: ~2× faster transfers and the prerequisite forcudaMemcpyAsyncactually being async (pageable copies silently stage through a pinned bounce buffer and serialize). Cost: pinned pages are stolen from the OS; pin gigabytes, not tens of gigabytes. - Unified memory —
cudaMallocManaged. One pointer valid on both sides; pages migrate on demand. Beautiful for prototypes; in production the page-fault storms on first touch and on every CPU↔GPU ping-pong are a classic mystery slowdown (visible asUnified Memoryrows in Nsight Systems).cudaMemPrefetchAsyncis the remedy when you must use it. - Stream-ordered pools —
cudaMallocAsync/cudaFreeAsync. Allocation as a stream operation from a driver-managed pool — because rawcudaMalloc/Freesynchronize the device and fragment, which is also why every framework built its own caching allocator. Phase 05 Lab 01 has you build exactly that allocator in Rust — this chapter is its motivation.
The rule that survives all four tiers: minimize transfers; batch them; overlap them with compute (Ch. 9). PCIe is 50× narrower than HBM (Phase 01 Ch. 6) — a synchronous host↔device copy inside a hot loop is the single most common production CUDA performance bug.
Chapter 6: Coalescing — The #1 Performance Rule
The mechanism: when a warp executes a load, the hardware takes all 32 addresses and computes which 32-byte sectors of memory they touch, then issues one transaction per distinct sector.
- 32 consecutive floats → 4 sectors → minimal transactions. Coalesced.
- Stride-32 floats (e.g., walking a column of a row-major matrix) → 32 different sectors → 32 transactions, each delivering 4 useful bytes of 32 fetched. ~8× wasted bandwidth, and worse with bigger strides.
This is the GPU's version of Phase 01 Lab 02's cache-line stride cliff — same physics, warp-granular.
The three patterns to know:
- Adjacent threads ↔ adjacent addresses.
a[blockIdx.x*blockDim.x + threadIdx.x]is perfect;a[threadIdx.x * pitch]is the anti-pattern. - SoA beats AoS.
struct {float x,y,z;} pts[n]makespts[i].xloads stride-12; three arraysx[n], y[n], z[n]coalesce each field. This is why GPU-facing data layouts (and Phase 07's KV-cache layouts) are structure-of-arrays. - Matrix transpose is the canonical exercise: naive transpose must uncoalesce either reads or writes; the fix is staging a tile through shared memory so both global phases are coalesced — Lab 02 implements it, padding included (Ch. 7).
Misconception: "the L2 cache makes coalescing obsolete." L2 softens repeated sloppiness, but a bandwidth-bound kernel wasting 8× on sectors is still ~8× slow — Lab 02's table is the proof you'll carry around.
Chapter 7: Shared Memory and Tiling — Raising Arithmetic Intensity
Phase 01 Ch. 7 said: performance = min(peak, AI × bandwidth). Tiling is the technique for raising AI, and shared memory is its vehicle.
Naive matmul: each thread computes one C[i][j], reading a full row of A and
column of B from global memory → every input element fetched N times → AI ≈ O(1)
→ bandwidth-bound at a few % of peak FLOPs.
Tiled matmul (Lab 01's centerpiece):
__shared__ float As[T][T], Bs[T][T];
for (int t = 0; t < N/T; t++) {
As[ty][tx] = A[row*N + t*T + tx]; // coalesced cooperative load
Bs[ty][tx] = B[(t*T + ty)*N + col]; // coalesced cooperative load
__syncthreads(); // tile fully loaded before use
for (int k = 0; k < T; k++) acc += As[ty][k] * Bs[k][tx];
__syncthreads(); // all done reading before overwrite
}
Each global element is now loaded once per tile instead of once per thread →
AI multiplied by T (tile dim, typically 16–32) → 5–10× speedup, exactly as the
roofline predicts. cuBLAS continues the same idea further down the hierarchy:
register-blocking (each thread computes a small output micro-tile from registers)
and Tensor Core fragments — that's the remaining gap when Lab 01's extension
compares against sgemm.
The two rules of __syncthreads(): (1) it's a block-wide barrier — every
thread of the block must reach it, so it must never sit inside a branch that's
divergent across the block (deadlock/UB); (2) you need it twice per tile — after
loading (before consuming) and after consuming (before the next overwrite). Lab 01
asks you to remove one and observe the corruption.
Bank conflicts: shared memory has 32 banks, word-interleaved.
tile[threadIdx.x][k] with a 32-wide tile puts a warp's accesses in one bank
(32-way conflict → serialized). Fix: pad the tile — __shared__ float tile[32][33] — the +1 rotates each row's bank alignment. Lab 02 measures
before/after; it's a ~30–40% transpose-bandwidth swing for one character of code.
Chapter 8: Occupancy in Practice
Phase 01 Ch. 5 defined occupancy as a latency-hiding budget. The practice:
What limits it (per SM, Ampere numbers): 65,536 registers, 100–164 KB shared memory, 2,048 thread slots, 32 block slots. Whichever resource your kernel exhausts first caps how many blocks are resident:
- 256 threads/block, 64 regs/thread → 16,384 regs/block → 4 blocks → 1,024 threads = 50% occupancy (register-limited).
- 48 KB smem/block on a 100 KB SM → 2 blocks resident regardless of registers.
The tools: nvcc -Xptxas=-v prints regs/smem per kernel;
cudaOccupancyMaxActiveBlocksPerMultiprocessor computes it at runtime (use it to
pick launch configs in library code — Phase 09's HAL does this);
__launch_bounds__(maxThreads, minBlocks) tells the compiler to fit a target
(it will spill registers to comply — measure, don't assume).
The judgment call (this is what reviewers actually argue about): more
occupancy hides more latency, but fewer registers per thread can mean spills, and
less shared memory per block can mean less tiling/reuse. There is no universally
right answer — only the profile. Volkov again: high-ILP kernels can peak at 25%
occupancy. The professional posture: state the limiter, show the ncu evidence,
justify the chosen point.
Chapter 9: Streams, Events, and Honest Timing
Streams are ordered work queues; operations in different streams may overlap.
The legacy default stream synchronizes with everything (compile with
--default-stream per-thread or just use explicit streams everywhere — the
platform-code default).
The canonical overlap pattern — pipelining chunked transfers against compute (this is also Phase 07's prefill/decode overlap in miniature):
for (int c = 0; c < nchunks; c++) {
cudaMemcpyAsync(d_in + off, h_in + off, bytes, H2D, stream[c % 2]);
kernel<<<g, b, 0, stream[c % 2]>>>(d_in + off, d_out + off);
cudaMemcpyAsync(h_out + off, d_out + off, bytes, D2H, stream[c % 2]);
}
Requirements for actual overlap: pinned host memory (Ch. 5), separate streams, and chunk sizes big enough to amortize launch overhead (~10 µs/launch — relevant to Phase 05's scheduler and why CUDA Graphs exist for short-kernel pipelines).
Honest timing — the three rules, violated in 90% of bad benchmark claims:
- Time with events (
cudaEventRecord/cudaEventElapsedTime), or wall-clock only around an explicitcudaDeviceSynchronize(). Timing an async launch withstd::chronomeasures the enqueue, not the kernel. - Warm up first (first launch includes module load/JIT — Phase 03 explains the JIT path).
- Report the median of many reps, with clocks noted if comparing across
sessions (GPUs thermal-throttle;
nvidia-smi -q -d CLOCK).
Events also serve as cross-stream dependencies (cudaStreamWaitEvent) — the
primitive from which Phase 05 Lab 02's stream scheduler builds its DAG.
Chapter 10: Reductions and Warp Primitives
Summing N elements exposes everything this phase taught at once — and it's a top-3 interview exercise.
The evolution every CUDA engineer should be able to narrate:
- Atomic per element:
atomicAdd(&out, a[i])— correct, serializes on one address, terrible. - Shared-memory tree: block loads to smem, halving strides with
__syncthreads()between levels, thread 0 atomically adds the block's sum. Classic; bank-conflict-aware versions use sequential addressing (stride from blockDim/2 down, not interleaved by 2× — the interleaved version also has divergence problems:if (tid % (2*s) == 0)is the per-lane branch Phase 01 warned about). - Warp shuffle finish: the last 32 elements don't need shared memory at all —
val += __shfl_down_sync(0xffffffff, val, offset)moves data register-to-register within the warp, no smem, no sync. Modern reductions: shuffle within warps, one smem round between warps, shuffle again. - (Library answer: CUB
BlockReduce— and saying "in production I'd use CUB, here's what it does inside" is the strongest possible interview answer.)
Warp primitives to know by name: __shfl_*_sync (data exchange), __ballot_sync
/ __any_sync / __all_sync (votes — Lab 01 of Phase 01 simulated vote.any),
__activemask(). All take the mask-first argument because of Volta independent
thread scheduling (Phase 01 Ch. 3).
Chapter 11: Profiling — Nsight Systems vs Nsight Compute
Two tools, two altitudes — using the wrong one wastes afternoons:
- Nsight Systems (
nsys) — the timeline. CPU threads, API calls, transfers, kernel spans, stream overlap. Answers: "where does wall-clock time go?" "is the GPU idle between kernels?" "did my copies overlap?" Always start here.nsys profile -o report ./app→ open in the GUI ornsys stats. - Nsight Compute (
ncu) — the microscope. Per-kernel hardware counters: achieved bandwidth, warp execution efficiency (Lab 01 of Phase 01 computed this by hand!), occupancy achieved vs theoretical, stall reasons, and a built-in roofline chart.ncu --set full ./app. Expensive (replays kernels many times).
The first-pass triage (memorize as a flowchart):
nsys: GPU mostly idle? → it's a pipeline problem (launch overhead, sync points, transfers) — no kernel tuning will help.- GPU busy →
ncuthe top kernel: SOL Memory% vs SOL Compute% tells you which roof you're under (Phase 01's roofline, now measured for you). - Memory-bound → check
gld_efficiency/sectors-per-request (coalescing, Ch. 6), then data reuse (tiling, Ch. 7). Compute-bound → precision/Tensor-Core utilization. Neither high but stalls high → latency-bound: occupancy (Ch. 8) or dependency chains.
This triage is Lab 02's PROFILE.md exercise, and — at the leadership level —
it's the checklist you require attached to any "we optimized the kernel" PR.
Lab Walkthrough Guidance
Order: Lab 01 → Lab 02. Lab 01 builds the kernels; Lab 02 measures their sins.
Lab 01 (first kernels):
- Build and run first (
make && ./kernels); confirm all PASS lines, then readkernels.cuagainst Chapters 2–4 and 7. - Work the verification path: break the tail guard deliberately, see the
corruption, fix it, then break a
__syncthreads()and see the race. - Extensions in order: rectangular tiles → register micro-tiles → cuBLAS comparison (expect cuBLAS 2–5× ahead; explain why using Ch. 7's last paragraph).
- No local GPU: the README's Colab cell runs everything;
nvccflags identical.
Lab 02 (coalescing & profiler):
- Run the benchmark and fill the tables before profiling — predict, then
verify with
ncu(prediction-first is the habit that makes profiling stick). - The transpose ladder (naive → tiled → padded) is the heart: tie each step to
Ch. 6/7 mechanisms in one sentence each in
PROFILE.md. - Finish with the triage flowchart (Ch. 11) applied to both your matmuls from Lab 01 — which roof is each under, with evidence.
Success Criteria
-
You can write a correct kernel + launch +
CUDA_CHECKdiscipline from a blank editor with no references (Ch. 2–4) - You can explain sync vs async errors and run the 3-step debugging ladder (Ch. 4)
- You can state the coalescing mechanism in terms of warp addresses → 32-byte sectors, and the SoA and transpose consequences (Ch. 6)
-
Your tiled matmul beats naive ~5–10× and you can derive why from the
roofline, including both
__syncthreads()placements (Ch. 7) -
You can name a kernel's occupancy limiter from
-Xptxas=-voutput (Ch. 8) - Your benchmark numbers come from events with warmup and reps (Ch. 9)
- You can narrate the reduction evolution through warp shuffles (Ch. 10)
-
Given an
nsys+ncupair, you can name the limiter and the next experiment in under two minutes (Ch. 11)
Interview Q&A
Q1: Walk me through launching a blur filter over a 1920×1080 image.
A: Pick a 2D block, say 16×16 = 256 threads. Grid = ceil(1920/16) × ceil(1080/16)
= 120 × 68 blocks. Each thread computes x = blockIdx.x*16 + threadIdx.x, y
likewise, guards if (x < 1920 && y < 1080) because 1080/16 = 67.5 rounds up and
the last block row hangs off the edge. For a blur, neighboring threads read
overlapping pixels, so I'd stage a (16+2r)² halo tile in shared memory with
cooperative coalesced loads, __syncthreads(), then filter from smem. Launch
asynchronously into a stream, CUDA_CHECK(cudaGetLastError()) after.
Q2: Your kernel achieves 8% of peak bandwidth. First three hypotheses?
A: (1) Uncoalesced access — warp addresses scattering across 32-byte sectors:
check ncu sectors-per-request (~4 is ideal for floats; 32 means fully strided) —
typical causes: column-major walk, AoS layout, bad pitch. (2) Latency-bound, not
bandwidth-bound — too few warps in flight to saturate the memory system: check
achieved occupancy and stall reasons; fix with more parallelism per launch or more
ILP per thread. (3) The kernel isn't actually the time sink — short kernel
dominated by launch overhead or serialized with transfers: check the nsys
timeline first. (Bonus: unified-memory page faults masquerading as kernel time.)
Q3: Why must thread blocks be independent? A: Independence is the scalability contract: the hardware can place, execute, and retire blocks in any order on any SM, which lets one binary saturate a 10-SM laptop and a 132-SM H100, and lets the scheduler backfill SMs as blocks finish (Phase 01's latency-hiding at block granularity). Inter-block sync within a kernel would require all blocks resident simultaneously — deadlock otherwise. The sanctioned escape hatches: end the kernel (grid-wide barrier = kernel boundary), atomics for unordered communication, or cooperative launch, which checks co-residency at launch time.
Q4: What goes wrong with __syncthreads() in divergent code?
A: It's a block-wide barrier; every thread must execute the same barrier. If
some threads of a block skip it (a branch divergent at block scope), the arriving
threads wait forever — deadlock or undefined behavior, in practice a hang or
corrupted results. Warp-level divergence reconverges by hardware, but
__syncthreads() semantics demand all-threads participation: the rule is no
__syncthreads() under conditions that vary within the block; restructure so the
barrier is unconditional (compute the condition, sync, then branch).
Q5: Pinned vs pageable vs unified memory — when each?
A: Pageable (malloc) host memory forces the driver to stage transfers through an
internal pinned bounce buffer — slower, and cudaMemcpyAsync silently degrades to
effectively synchronous. Pinned (cudaMallocHost) gives DMA-direct ~2× transfer
speed and true async — required for any overlap pipeline; cost is stealing
unpageable RAM from the OS, so pin transfer buffers, not datasets. Unified
(cudaMallocManaged) trades control for convenience: on-demand page migration is
fine for prototypes and oversubscription-with-prefetch on data too big for HBM,
but fault storms on alternating CPU/GPU touches are a notorious production
slowdown — in serving infrastructure I require explicit transfers at review time
unless there's a measured case.
Q6 (leadership): An engineer's PR claims a 3× kernel speedup. What do you require before merge?
A: (1) Correctness evidence: elementwise comparison against a reference within a
stated tolerance, plus a compute-sanitizer clean run — fast-but-wrong kernels
are the classic regression. (2) Honest methodology: event-based timing, warmup,
median of N, fixed clocks noted, same input distribution as production. (3) The
mechanism: which limiter moved, with before/after ncu evidence (e.g.,
sectors-per-request 32→4, SOL Memory 85%) — "I don't merge speedups we can't
explain, because unexplained wins are usually measurement bugs." (4) Portability
notes: which architectures it was tuned on, occupancy impact at other block sizes
(Phase 09's HAL concern). This checklist is Lab 02's PROFILE.md, generalized
into team process.
References
- NVIDIA, CUDA C++ Programming Guide — https://docs.nvidia.com/cuda/cuda-c-programming-guide/
- NVIDIA, CUDA C++ Best Practices Guide — https://docs.nvidia.com/cuda/cuda-c-best-practices-guide/
- Mark Harris, "Optimizing Parallel Reduction in CUDA" (the classic 7-step deck) — https://developer.download.nvidia.com/assets/cuda/files/reduction.pdf
- Mark Harris, "An Efficient Matrix Transpose in CUDA C/C++" — https://developer.nvidia.com/blog/efficient-matrix-transpose-cuda-cc/
- NVIDIA Nsight Systems / Nsight Compute documentation — https://docs.nvidia.com/nsight-systems/ , https://docs.nvidia.com/nsight-compute/
- CUTLASS (how production GEMMs are actually structured) — https://github.com/NVIDIA/cutlass
- Sanders & Kandrot, CUDA by Example (dated but the gentlest on-ramp)
- Cheng, Grossman, McKercher, Professional CUDA C Programming
- Cross-track: Phase 01 WARMUP (machine model), Phase 03 WARMUP (what nvcc does with this code)
🛸 Hitchhiker's Guide — Phase 02: CUDA Programming
Read this if: you understand the Phase 01 machine model but haven't shipped CUDA. This is the field guide: idioms, numbers, foot-guns, and the review standards a GPU platform team should hold.
0. The 30-second mental model
CUDA = C++ where one function runs N times concurrently, N = grid × block. You manage three things: indices (which element am I?), memory (is my warp's access coalesced? is reuse staged in shared memory?), and asynchrony (launches return immediately; streams order work; events time and connect it). Performance work = move the kernel toward the correct roof from Phase 01's roofline, then stop.
1. The idioms you'll type forever
// ceil-div launch + tail guard — THE pattern
int t = 256, b = (n + t - 1) / t;
k<<<b, t>>>(d_x, n); // d_x from cudaMalloc, never a host ptr
// error discipline
CUDA_CHECK(cudaGetLastError()); // launch errors (sync)
CUDA_CHECK(cudaDeviceSynchronize()); // kernel errors (async) — debug builds
// honest timing
cudaEventRecord(t0); k<<<...>>>(...); cudaEventRecord(t1);
cudaEventSynchronize(t1); cudaEventElapsedTime(&ms, t0, t1);
Block size: multiple of 32, default 128/256, decided by measurement not vibes. 3D grids are indexing sugar — the hardware sees a flat list of blocks.
2. Memory decisions, ranked by frequency of regret
- Sync copies in hot loops (PCIe is 1/50th of HBM): batch, pin, overlap.
- Uncoalesced layouts: AoS structs, column walks of row-major data,
threadIdx.y-fastest indexing. Fix = SoA + adjacent-thread-adjacent-address.ncusmoking gun: sectors/request ≈ 32 (want ~4). - Unified memory in serving paths: page-fault storms. Managed memory is a prototyping tool; production transfers are explicit.
- Raw cudaMalloc/Free per request: device-synchronizing and fragmenting —
use
cudaMallocAsyncpools or a caching allocator (you'll build one in Phase 05; PyTorch's is the reference design). - Forgetting pinned memory for async:
cudaMemcpyAsyncfrom pageable memory quietly serializes.
3. Shared memory: the two-line contract
- Tile global→smem with coalesced cooperative loads;
__syncthreads(); compute from smem;__syncthreads(); next tile. Matmul AI goes from O(1) to O(T): the whole game in one sentence. - Pad
[32][33]to dodge bank conflicts; never put__syncthreads()under a block-divergent branch.
4. Numbers worth carrying
| Thing | Number |
|---|---|
| Kernel launch overhead | ~5–10 µs (why CUDA Graphs exist) |
| Max threads/block | 1,024 (use 128–256) |
| Smem/SM (Ampere/Hopper) | 100 / 228 KB configurable |
| Registers/SM | 64K × 32-bit; >64/thread starts hurting occupancy |
| PCIe 5 ×16 vs HBM3 | 64 GB/s vs 3,350 GB/s |
| Warp size | 32 — and yes, you may rely on it (it's in the ISA contract) |
| compute-sanitizer overhead | ~10–50× — CI, not production |
5. The profiler triage (commit to memory)
nsys timeline:
GPU idle? → pipeline problem: launches, syncs, transfers. Stop kernel-tuning.
GPU busy → ncu the top kernel:
SOL Memory high? → coalescing → reuse/tiling → vectorize loads (float4)
SOL Compute high? → precision, Tensor Cores, better algorithm
both low? → latency-bound: occupancy, ILP, dependency chains
The strongest habit this phase teaches: predict before measuring. Write down the expected bandwidth/speedup from the roofline, then check. A team that predicts-then-profiles compounds; a team that profiles-then-rationalizes doesn't.
6. Debugging war stories (so you recognize them)
- "It crashes on a line that's obviously fine" — async error from an earlier
kernel.
CUDA_LAUNCH_BLOCKING=1, find the real culprit. - "Works at n=1000, corrupts at n=1000000" — missing tail guard; or grid dim overflowed 65,535 in a .y/.z dimension.
- "Slower after I 'optimized' registers" —
-maxrregcountcaused spills; occupancy went up, performance went down. Volkov was right. - "First call takes 200 ms" — context init + module JIT. Warm up; ship SASS for your target arch (Phase 03's fatbin story).
- "Results differ run to run by 1e-6" — float reduction order changes with block scheduling; that's not a bug, it's floating-point non-associativity. Define tolerance-based verification (and lock seeds for argmax-style outputs).
- "Everything returns errors after one bad kernel" — poisoned context; restart the process. Architect tenant isolation accordingly (Phase 06).
7. From engineer to head-of-engineering
What changes at your level isn't the syntax — it's the standards:
- Kernel PR checklist: reference-comparison test, sanitizer-clean, event-based
timing with reps,
ncubefore/after naming the limiter, arch notes. (Lab 02 builds it; your job is making it stick.) - Estimate sanity: when someone says "we'll get 10× by porting to GPU," you now ask: what's the AI? what fraction is parallel? how much PCIe traffic? Amdahl + roofline kills most 10× claims in five minutes — kindly, with math.
- Hiring signal: ask candidates the reduction-evolution question (§Ch. 10 of the WARMUP). Depth on "why shuffle beats shared memory for the last warp" separates practitioners from tutorial-readers.
Next: Phase 03 — what nvcc actually does to this code (PTX, SASS, and a mini
compiler you'll write yourself), because a platform that promises "independence
from underlying hardware" lives or dies in the compiler layer.
Lab 01 — First Kernels: Vector Add → Tiled Matmul
Phase: 02 — CUDA Programming | Difficulty: ⭐⭐⭐☆☆ | Time: 4–6 hours Language: CUDA C++ | Hardware: any NVIDIA GPU (sm_70+); Colab T4 works
Concept primer:
../WARMUP.mdCh. 2–4, 7, 9.
Run
make # nvcc -O3 -arch=native (or ARCH=sm_75 make for Colab T4)
./kernels
No local GPU? Google Colab
Runtime → Change runtime type → T4 GPU, then in one cell:
!nvcc --version
%%writefile kernels.cu
<paste kernels.cu>
!nvcc -O3 -arch=sm_75 -o kernels kernels.cu && ./kernels
0. The mission
Three kernels, one discipline:
vec_add— the programming model + error handling + the tail guard.matmul_naive— one thread per output element, all reads from global memory.matmul_tiled— the same math with shared-memory tiling.
Every kernel is verified elementwise against a CPU reference, and timed with CUDA events (warmup + median of reps). Expected result: tiled beats naive ~5–10×, because tiling raises arithmetic intensity by the tile factor — the Phase 01 roofline argument made executable.
Sample output (T4, n=1024):
vec_add : PASS 33.55 GB/s effective (memory-bound, as designed)
matmul_naive : PASS 612 ms 0.35 TFLOP/s
matmul_tiled : PASS 78 ms 2.75 TFLOP/s speedup 7.8x [PASS >= 4x]
1. Read the code in this order
CUDA_CHECKandcheck_close— the discipline. Note wherecudaGetLastErroris called (right after launch — catches config errors) vscudaDeviceSynchronize(catches the kernel's own faults).vec_add— the index formula and tail guard. Exercise: delete theif (i < n)guard, run with n=1<<20+3, observe either corruption or a sanitizer report (compute-sanitizer ./kernels). Put it back.matmul_naive— each thread reads a row of A and a column of B from global memory. The column read of B is actually coalesced across threads (adjacent tx → adjacent B columns at fixed k) — the problem isn't coalescing here, it's zero reuse: every element fetched N times. AI ≈ O(1).matmul_tiled— the cooperative load / sync / compute / sync rhythm (WARMUP Ch. 7). Exercise: comment out the second__syncthreads()and run — verification fails intermittently (a fast warp overwrites the tile while a slow one still reads). This is the cheapest race-condition education available; take it.- The timing harness — events, warmup, median-of-5. Compare against the
naive
std::chronoaround an async launch (also included, prints the lie).
2. Why tiled wins — write this down in your own words
Naive: each of N³ multiply-adds needs 2 fresh global loads → AI ≈ 1/8 FLOP/byte → the bandwidth roof at ~320 GB/s (T4) caps you near 0.04 TFLOP/s per... except L2 catches some reuse, so you observe ~0.3. Tiled with T=32: each global element is loaded once per tile and reused 32 times from shared memory → AI × 32 → the roof rises past 1 TFLOP/s on the same silicon. Same FLOPs, same GPU, 8× — layout and staging, not arithmetic.
The remaining 3–5× to cuBLAS: register micro-tiles (each thread computes 4–8
outputs from registers, raising AI again), double-buffered async tile loads
(cp.async), Tensor Cores via mma, and per-arch tile-shape tuning. That ladder
is CUTLASS's table of contents — and the extension exercises walk its first rung.
3. Extension ladder
- Rectangular blocking: 64×16 tiles; measure. Why might non-square help? (Hint: B's tile loads per output column.)
- Register micro-tile: each thread computes a 2×2 patch of C. Expect another 1.5–2.5×. You're now register-blocked like real GEMMs.
- cuBLAS comparison: link
-lcublas, timecublasSgemm, report your % of it. >30% with micro-tiles is respectable; explain the rest. __launch_bounds__(256, 4)on the tiled kernel: check-Xptxas=-vregister count before/after, occupancy, and time. Did the compiler spill?
4. Common pitfalls
- Host pointer passed to a kernel — illegal access at the next sync, blamed on
the wrong line (
CUDA_LAUNCH_BLOCKING=1to localize). - Verifying with
==on floats — reduction/FMA order differs from CPU; use relative tolerance (the harness uses 1e-3 relative). - Timing the enqueue, not the kernel (chrono without sync) — the harness prints both so you see the discrepancy once and never trust chrono again.
-archmismatch —sm_90binary on a T4 dies at load;-arch=nativelocally, explicitARCH=on shared machines (the full fatbin story is Phase 03).
5. What this lab proves about you
You can produce a verified, honestly-benchmarked kernel and explain its performance from the roofline — the bar you will later hold every kernel PR to (Lab 02 turns that into a checklist).
Lab 02 — Coalescing, Bank Conflicts & the Profiler
Phase: 02 — CUDA Programming | Difficulty: ⭐⭐⭐⭐☆ | Time: 4–6 hours Language: CUDA C++ + Nsight Compute CLI | Hardware: any NVIDIA GPU; Colab T4 works
Concept primer:
../WARMUP.mdCh. 6–8, 11.
Run
make && ./bench # bandwidth tables: stride, AoS/SoA, transpose ladder
ncu --set basic ./bench # then: the profiler exercise (see §3)
0. The mission
Three experiments that turn WARMUP Chapter 6–7 claims into numbers you measured, then a profiler session that teaches you to name limiters with evidence:
- Stride sweep: copy bandwidth at stride 1, 2, 4, 8, 16, 32 — watch coalescing die.
- AoS vs SoA: load one field from a 3-float struct array vs a flat array.
- Transpose ladder: naive → shared-memory tiled → tiled+padded; recover copy bandwidth step by step.
Expected shape of results (T4; absolute numbers vary):
== stride sweep (effective GB/s) ==
stride 1: 240.1 stride 4: 68.3 stride 16: 17.9
stride 2: 124.5 stride 8: 34.6 stride 32: 9.2 <- ~26x cliff
== AoS vs SoA (loading .x only) ==
AoS: 81.4 GB/s SoA: 238.0 GB/s (~3x: the other 8 bytes ride the sectors)
== transpose ladder ==
copy (ceiling) : 235 GB/s
naive : 62 GB/s (writes uncoalesced)
tiled : 148 GB/s (both phases coalesced; bank conflicts on read)
tiled + pad : 221 GB/s (94% of copy — done)
1. Why each step behaves as it does
- Stride s: each warp load touches ~min(32, s·4·32/32) distinct 32-byte sectors; useful bytes per sector fall as 1/s until one float per sector (s ≥ 8) — after which the cliff flattens: you're paying one full sector per element. The same curve you produced on the CPU in Phase 01 Lab 02, now at warp granularity.
- AoS: reading
pts[i].xdrags.yand.zthrough the memory system as sector ballast. SoA puts 32 consecutive.xin 4 sectors. (This single diagram is why every GPU-resident data structure in Phases 05–07 is SoA.) - Transpose naive: reads coalesce (row-major in), writes scatter (column-major
out) — one direction always loses. Tiled: stage a 32×32 tile in shared
memory; read coalesced, write coalesced, the "turn" happens in smem. Padding
[32][33]: the tiled version's smem reads hit one bank 32 ways; the +1 column shifts each row's bank phase → conflict-free. One character, ~1.5× on the transpose.
2. Record your tables
Fill in the actual numbers in PROFILE.md §1–3. Predict each before running —
prediction-first is the habit.
3. The profiler session (§4 of PROFILE.md)
On the naive and padded transpose kernels:
ncu --kernel-name regex:transpose --set basic ./bench
Find and record, for each kernel:
| Metric (section) | What it tells you |
|---|---|
Memory Throughput % / Compute (SM) % (Speed Of Light) | which roof you're under |
l1tex__average_t_sectors_per_request (Memory Workload) | coalescing quality: ~4 good, ~32 catastrophic |
shared_load_bank_conflicts (or l1tex__data_bank_conflicts...) | the padding fix, visible |
Achieved Occupancy (Occupancy) | latency-hiding budget in use |
Then write the verdict line for each kernel in PROFILE.md:
"<kernel> is <memory/compute/latency>-bound because <two metrics>; the next
experiment would be <X>." — that sentence format is the whole point of the lab.
It's also the PR-review standard you'll set in Phase 12.
On Colab: !ncu --set basic ./bench works on T4 instances (driver permitting;
if ERR_NVGPUCTRPERM, add --target-processes all or use
nsys profile --stats=true ./bench for the timeline-level view instead and note
the difference in what you can conclude).
4. Extensions
float4vectorized copy: 16-byte loads per thread; measure against stride-1 float copy. Why does it help? (Fewer instructions per byte; wider sectors per request.)cp.async(sm_80+): async global→shared copies for the tiled transpose; overlap the next tile's load with this tile's store.- Occupancy experiment: add
__launch_bounds__(1024, 1)to the padded transpose, check-Xptxas=-vand achieved occupancy, explain the change. - Write the checklist: distill §3 into a one-page "kernel PR review checklist" — you will reuse it verbatim in Phase 12's leadership artifacts.
5. Common pitfalls
- Comparing kernels at different clocks (thermal throttle mid-run): lock with
nvidia-smi -lgcwhere permitted, or interleave reps. - Profiling with
--set fullon a long benchmark — replay overhead makes it crawl; usebasic+ targeted metrics. - Concluding "memory-bound" from low compute% alone — check both SOL numbers; both-low means latency-bound, a different fix.
- Forgetting that
ncuserializes kernels — overlap effects you saw innsysvanish underncu; the tools answer different questions (WARMUP Ch. 11).
6. What this lab proves about you
You can quantify the two memory sins (uncoalesced access, bank conflicts), fix them with the two standard remedies (SoA/tiling, padding), and defend the analysis with profiler evidence — the exact competency a GPU platform lead needs to review kernel work credibly.
Phase 03 — GPU Compilers: NVCC, PTX, SASS, and Codegen
Difficulty: ⭐⭐⭐⭐☆ | Estimated Time: 2 weeks Roles supported: Head of Engineering [GPU], GPU Compiler Engineer, Runtime Engineer Hardware needed: none for Lab 02 (the compiler you build emits and interprets PTX); any NVIDIA GPU (or Godbolt) for Lab 01
Why This Phase Exists
A platform that "operates independently of the underlying hardware" (the JD's first bullet) is, at its core, a compiler and runtime story: something must translate a hardware-neutral program representation into each vendor's ISA, and do it well enough that customers don't pay a performance tax for portability. Every player in this market — CUDA itself (PTX as the stable virtual ISA), Triton, MLIR, ROCm, OpenAI/vLLM kernels, every startup accelerator's "we support PyTorch" claim — is a position in the compiler argument.
You cannot lead that platform if the compiler layer is a black box. This phase opens it: you will trace nvcc's real pipeline, read PTX and SASS with comprehension, understand JIT vs AOT and the fatbin compatibility model (a production operations concern, not trivia), and then write a working compiler that lexes, parses, and emits legal PTX — plus an interpreter that executes your PTX per-thread so you can verify semantics without a GPU.
Concepts
- The
nvccpipeline end-to-end: host/device split → C++ frontend → NVVM IR (LLVM) →ptxas→ SASS; whatcudafe++,cicc,ptxas,fatbinaryeach do - PTX: a virtual ISA — typed registers (infinite), explicit state spaces (
.global,.shared,.param,.reg), predication, the%tid/%ctaidspecial registers - SASS: the real ISA — register allocation, dual-issue, stall counts in control bits; why SASS changes every architecture but PTX doesn't
- JIT vs AOT:
-arch/-code,compute_XXvssm_XX, fatbins, the driver JIT cache (~/.nv/ComputeCache), forward compatibility — and the production incident pattern ("new GPU, 5-minute cold start") - What the compiler does for you: loop unrolling, if-conversion to predication, FMA contraction, register allocation vs occupancy (
-maxrregcount, the Phase 02 connection) - The portability landscape a hardware-agnostic platform must navigate: LLVM NVPTX vs NVIDIA's NVVM, AMD's ROCm/LLVM path (GCN/RDNA ISA is public!), SPIR-V, Metal
- Triton in one lab-sized bite: block-level programming model, why it can fuse and auto-tune, where it fits between CUDA C++ and TVM/MLIR
- Compiler-as-moat: why kernel libraries + compiler maturity, not TFLOPs, decide which accelerators win (ties to Phase 01 Ch. 10, Phase 09)
Labs
Lab 01 — PTX/SASS Dissection (CUDA toolkit or Godbolt)
| Field | Value |
|---|---|
| Goal | Compile small kernels and read what comes out: PTX first, then SASS; observe predication, unrolling, FMA contraction, and register pressure. |
| Concepts | The nvcc pipeline, PTX syntax, SASS reading basics, -Xptxas=-v output, fatbins. |
| Steps | 1) nvcc -ptx the four provided kernels; annotate every PTX line of saxpy (template provided). 2) cuobjdump --dump-sass (or Godbolt) the same kernels; find the predicated branch and the FMA. 3) Rebuild with -maxrregcount 16; find the spills (local loads/stores). 4) Build a fatbin for two archs and dissect it with cuobjdump --list-elf. |
| Stack | CUDA toolkit (nvcc, cuobjdump, nvdisasm) — or godbolt.org with CUDA selected, zero install |
| Output | An annotated saxpy.ptx (every line explained) + a findings sheet for the other three kernels. |
| How to Test | Your annotations are checked against the provided answer key (saxpy-annotated.md). |
| Talking Points | Why PTX is the stability boundary (CUDA's actual moat); what ptxas does that the PTX level can't express; how if (x>0) y=a becomes a predicated instruction with no branch. |
| Resume Bullet | "Dissected NVCC compilation pipeline from CUDA C++ through PTX to SASS; documented predication, FMA contraction, and register-spill behavior across optimization flags for team training." |
| Extensions | Compare clang --cuda-device-only -S (LLVM NVPTX) output against nvcc's for the same kernel; compile the same kernel for AMD with hipcc --genco and read the GCN ISA. |
Lab 02 — Mini Compiler: Expression Language → PTX (Python)
| Field | Value |
|---|---|
| Goal | Build a real (small) compiler: lex → parse → typed AST → PTX codegen for elementwise GPU kernels — plus a PTX interpreter that runs your output per-thread and verifies it against a Python reference. |
| Concepts | Lexing, recursive-descent parsing, AST, register allocation (virtual → PTX .reg), the PTX kernel ABI (.param space, cvta, %tid math, bounds guard), codegen correctness. |
| Steps | 1) Run python solution.py — compiles 5 test kernels to PTX and executes them in the bundled interpreter against references. 2) Read the compiler in pipeline order: Lexer → Parser → TypeChecker → CodeGen → PTXInterp. 3) Extensions: add min/max intrinsics, constant folding, common-subexpression elimination, then a fused relu(a*b+c) kernel. |
| Stack | Pure Python (stdlib). Optional final step: load your PTX on a real GPU via cuda-python/PyCUDA and confirm the interpreter told the truth. |
| Output | A working expr → PTX compiler whose output is legal PTX (loads on a real GPU) and passes the built-in differential tests. |
| How to Test | python solution.py — every kernel's interpreter output must match the Python reference elementwise; PTX text is also syntax-checked against the grammar subset. |
| Talking Points | What Triton does beyond this (tiling, smem staging, auto-tuning at the block level); why PTX's infinite virtual registers push register allocation to ptxas; what changes to target AMD (different ABI + ISA, same compiler shape). |
| Resume Bullet | "Built an expression-to-PTX compiler (lexer, parser, type checker, codegen) with differential testing via a PTX interpreter; output verified as legal PTX loadable by the CUDA driver." |
| Extensions | Constant folding + CSE passes (measure instruction-count reduction); emit the same AST to a second backend (C source) and reflect on what a multi-backend HAL compiler needs (Phase 09 foreshadowing). |
Deliverables Checklist
-
Fully annotated
saxpy.ptxmatching the answer key - SASS findings: one predicated instruction, one FMA, one spill identified with flags that caused them
- Mini compiler passes all differential tests
- At least one optimization pass (folding or CSE) implemented and measured
- You can draw the nvcc pipeline and the JIT/AOT decision tree from memory
Interview Relevance
- "What does nvcc actually do? What's PTX vs SASS?"
- "Your service takes 5 minutes to start on new GPUs. Why, and what's the fix?" (JIT + fatbin story)
- "Why did Triton succeed where so many kernel DSLs failed?"
- "An accelerator startup says 'we run PyTorch.' What's actually behind that claim, and what do you audit?"
- "Walk me through compiling
c[i] = a[i] * 2 + b[i]down to PTX by hand."
Warmup Guide — GPU Compilers: NVCC, PTX, SASS, and Codegen
Zero-to-expert primer for Phase 03. Assumes Phase 02 (you've compiled CUDA with nvcc) and basic compiler vocabulary is NOT assumed — we build it. By the end you can read PTX/SASS, explain the JIT/AOT production tradeoffs, and write a small compiler that emits legal PTX.
Table of Contents
- Chapter 1: Why a Platform Leader Must Understand the Compiler Layer
- Chapter 2: Compilers From Zero — the Five Classic Stages
- Chapter 3: The NVCC Pipeline, For Real
- Chapter 4: PTX — The Virtual ISA That Is CUDA's Moat
- Chapter 5: Reading PTX — A Guided Tour of saxpy
- Chapter 6: SASS — Where the Machine Stops Pretending
- Chapter 7: JIT vs AOT, Fatbins, and the Cold-Start Incident
- Chapter 8: What the Optimizer Does to Your Kernel
- Chapter 9: The Portability Landscape — NVPTX, ROCm, SPIR-V, Triton, MLIR
- Chapter 10: Compiler-as-Moat — The Business Reading
- Lab Walkthrough Guidance
- Success Criteria
- Interview Q&A
- References
Chapter 1: Why a Platform Leader Must Understand the Compiler Layer
The JD's opening mandate — "a platform that operates independently of the underlying hardware" — is a sentence about compilers. Hardware independence means some layer translates a stable program representation into each vendor's machine code. Whoever controls that layer controls the platform's performance, its portability claims, and its cost structure. Concretely:
- When vLLM gains 30% from a new attention kernel, that's the compiler/kernel layer.
- When a startup accelerator "supports PyTorch" but delivers 25% of paper FLOPs, the gap is compiler maturity (Phase 01 Ch. 10's question 5).
- When your service cold-starts for minutes on a new GPU generation, that's JIT compilation you didn't plan for (Ch. 7 — a real incident pattern).
- When NVIDIA's dominance is described as "the CUDA moat," the load-bearing piece is PTX + 18 years of kernels that still run (Ch. 4, 10).
You don't need to be a compiler engineer. You need to never be lied to about this layer — by vendors, by candidates, or by your own roadmap.
Chapter 2: Compilers From Zero — the Five Classic Stages
For readers who've never built one (you will, in Lab 02):
- Lexing: characters → tokens.
c[i] = a[i] * 2.0becomesIDENT(c) LBRACK IDENT(i) RBRACK ASSIGN IDENT(a) ... NUM(2.0). - Parsing: tokens → AST (a tree mirroring the grammar). Recursive
descent — one function per grammar rule — is how you'll write it; precedence
climbing handles
*binding tighter than+. - Semantic analysis / type checking: annotate the AST with types (
f32,i32), reject nonsense, decide where conversions go. - IR + optimization: a flat, instruction-like intermediate representation — in real compilers, SSA form (every value assigned exactly once; makes data-flow analysis trivial). Passes rewrite it: constant folding, common subexpression elimination (CSE), dead-code elimination (DCE).
- Codegen: IR → target instructions + register allocation + the target's calling convention/ABI.
The deep insight you'll internalize by building one: a compiler is just a chain of small, testable tree/list transforms. None of the stages are magic; all of the difficulty is in the target's details — which is why the rest of this guide is about the target.
Chapter 3: The NVCC Pipeline, For Real
nvcc is an orchestrator. Run nvcc -dryrun x.cu once in your life and read the
plan it prints. The flow:
x.cu ── cudafe++ ──► host C++ ─────────────► g++/clang ──► host .o ─┐
└──► device C++ ──► cicc (NVVM/LLVM) ──► x.ptx ├─► executable
ptxas ─────────────► x.cubin (SASS) │
fatbinary ─────────► fatbin (ptx+cubins) ────┘
- cudafe++ splits host/device code (the
__global__/__device__markers). - cicc is NVIDIA's LLVM-based device frontend: C++ → NVVM IR (an LLVM IR dialect) → optimizations → PTX.
- ptxas is the real backend: PTX → SASS (the architecture-specific
ISA), doing register allocation, scheduling, and the occupancy-relevant
decisions (
-Xptxas=-vfrom Phase 02 is asking ptxas to talk). - fatbinary bundles PTX (for forward-compat JIT) + one or more cubins (AOT) into the executable; the runtime picks at load time (Ch. 7).
The same split exists in every GPU stack: a frontend to a portable IR, then a per-architecture backend. Hold the shape; vendors only change the names.
Chapter 4: PTX — The Virtual ISA That Is CUDA's Moat
What it is: PTX (Parallel Thread eXecution) is a virtual instruction set — assembly for a GPU that doesn't physically exist, designed to be stable across hardware generations.
Properties that make it virtual (and that your Lab 02 compiler will emit):
- Infinite typed registers:
.reg .f32 %f<N>— declare as many as you like; ptxas does the real allocation. (This is why register pressure problems are invisible in PTX and only appear in SASS — Lab 01 step 3.) - Explicit state spaces: every memory access names its space —
.param(kernel arguments),.global,.shared,.local,.const. Thecvta.to.globalinstruction converts a generic address to a space-specific one. PTX makes Phase 01's memory hierarchy syntactically visible. - Explicit types on every instruction:
add.f32,mad.lo.s32,ld.global.f32— no inference at the ISA level. - Predication instead of (most) branches: any instruction can be guarded:
@%p1 bra DONE;or even@%p st.global...— Phase 01's active-mask machinery, visible in text. - Special registers:
%tid.x,%ctaid.x,%ntid.x—threadIdx,blockIdx,blockDim. The Phase 02 index formula compiles to exactly:mad.lo.s32 %r4, %ctaid.x, %ntid.x, %tid.x.
Why "moat": NVIDIA promises PTX forward compatibility — PTX compiled in 2010 JITs onto an H100 today. That promise is what lets two decades of CUDA software accumulate without ever being rewritten, and it's the single hardest thing for any competitor to replicate, because it's not a chip feature — it's an ecosystem contract (Ch. 10).
Chapter 5: Reading PTX — A Guided Tour of saxpy
y[i] = a * x[i] + y[i] — the kernel you'll annotate in Lab 01. Abridged, with
the narration you should be able to give:
.visible .entry saxpy( // __global__ visible to the driver
.param .f32 saxpy_param_0, // a — args live in .param space
.param .u64 saxpy_param_1, // x ptr
.param .u64 saxpy_param_2, // y ptr
.param .u32 saxpy_param_3 // n
){
ld.param.f32 %f1, [saxpy_param_0];
ld.param.u64 %rd1, [saxpy_param_1];
cvta.to.global.u64 %rd2, %rd1; // generic -> global address space
mov.u32 %r1, %ctaid.x; // blockIdx.x
mov.u32 %r2, %ntid.x; // blockDim.x
mov.u32 %r3, %tid.x; // threadIdx.x
mad.lo.s32 %r4, %r1, %r2, %r3; // i = blockIdx*blockDim + threadIdx
setp.ge.s32 %p1, %r4, %r5; // p1 = (i >= n)
@%p1 bra DONE; // the tail guard, as a predicated branch
mul.wide.s32 %rd3, %r4, 4; // byte offset = i * sizeof(float)
add.s64 %rd4, %rd2, %rd3; // &x[i]
ld.global.f32 %f2, [%rd4]; // x[i]
fma.rn.f32 %f4, %f1, %f2, %f3; // a*x[i] + y[i] — contracted to FMA
st.global.f32 [%rd5], %f4; // y[i] = ...
DONE:
ret;
}
Five things to notice (they're the Lab 01 annotation rubric):
- Arguments arrive via
ld.param— the kernel ABI. Your Lab 02 compiler emits this exact prologue. cvta.to.global— the state-space conversion; pointers from the host are "generic" until proven global.- The index computation is a single
mad— multiply-add, integer flavor. - The bounds check is a predicate register + predicated branch — and for tiny bodies the compiler skips the branch entirely and predicates the store (if-conversion, Ch. 8).
fma.rn.f32— the compiler contracteda*x + yinto one fused multiply-add, round-to-nearest. (This is why GPU and CPU results differ in the last ulp — Phase 02's tolerance-based verification, explained.)
Chapter 6: SASS — Where the Machine Stops Pretending
SASS is the real, per-architecture ISA (cuobjdump --dump-sass). What changes
versus PTX:
- Real registers: R0–R254 + predicates; allocation is done; spills appear as
LDL/STL(local loads/stores) — the smoking gun Lab 01 step 3 hunts after-maxrregcount 16. - Control information embedded: modern SASS encodes stall counts, dependency barriers, and reuse hints chosen statically by ptxas — the GPU does no out-of-order discovery (Phase 01 Ch. 1's deleted machinery, visible as compiler homework instead).
- Architecture-specific instructions:
HMMA/QGMMA(Tensor Core ops),LDGSTS(the async copy behindcp.async), wider loadsLDG.E.128. - No stability promise: SASS encodings change every generation — which is why PTX exists, and why "ship PTX in the fatbin" is the forward-compat story.
You don't write SASS; you read it for three signals: spills (LDL), whether
Tensor Cores were used (HMMA), and what the inner loop actually became
(unrolled? predicated? vectorized loads?). That reading level is reachable in an
afternoon — Lab 01 gets you there.
Chapter 7: JIT vs AOT, Fatbins, and the Cold-Start Incident
The compatibility model — a production operations topic disguised as compiler trivia:
-arch=compute_80 -code=sm_80,sm_90,compute_80→ fatbin contains: SASS for sm_80, SASS for sm_90, and PTX (compute_80).- At load, the runtime picks: exact SASS match → run it. No match but PTX
present → the driver JIT-compiles PTX → SASS on first load, caching in
~/.nv/ComputeCache(size-capped, env-tunable).
The incident pattern every GPU platform hits once: new GPU generation lands
in the fleet → no sm_XX cubin in anything → every process JITs every kernel on
startup → 2–10 minute cold starts, CPU pegged, sometimes cache thrash if
CUDA_CACHE_MAXSIZE is small. Fixes: add the new arch to build matrices (cost:
binary size, build time), pre-warm the JIT cache in images, or lazy-load with
CUDA_MODULE_LOADING=LAZY (CUDA 11.7+, now default — loads kernels on first
use instead of module load).
As the platform owner you set this policy: which architectures get AOT SASS, which ride PTX JIT — a real cost/risk tradeoff (build farm minutes and gigabyte containers vs cold-start risk). Phase 10's release engineering inherits this decision; Phase 09's HAL generalizes it across vendors (AMD has no stable PTX equivalent — code objects are per-ISA, which changes the portability calculus entirely).
Chapter 8: What the Optimizer Does to Your Kernel
The transforms worth recognizing in Lab 01's dumps (and implementing, in miniature, in Lab 02's extensions):
- FMA contraction:
a*b + c→fma.rn— fewer instructions, different rounding (one rounding instead of two).-fmad=falsedisables it when bit-exactness vs CPU matters (you'll be asked this in numerical-debug contexts). - If-conversion / predication: short conditionals become predicated instructions — no branch, no divergence machinery; Phase 01 Ch. 4's "compiler often predicates" claim, now visible in SASS.
- Loop unrolling:
#pragma unrollor heuristics; buys ILP (Phase 01 Ch. 5's other latency-hiding axis) at the cost of registers and I-cache. - Constant folding / CSE / DCE: the bread-and-butter IR passes — your Lab 02 extension implements folding and CSE and measures emitted-instruction reduction.
- Register allocation vs occupancy: ptxas balances registers-per-thread
against spills; you nudge it with
-maxrregcount/__launch_bounds__and you measure rather than assume (Phase 02 Ch. 8's judgment call — this is where it's decided).
Misconception: "the compiler will fix my memory layout." It won't — coalescing (Phase 02 Ch. 6) is a data layout property the compiler cannot see past your pointer arithmetic. Compilers optimize instructions, not your AoS schema.
Chapter 9: The Portability Landscape — NVPTX, ROCm, SPIR-V, Triton, MLIR
The map of "hardware-independent GPU software" attempts — i.e., your competitive landscape:
- LLVM NVPTX: upstream LLVM's PTX backend (what
clang --cudauses; NVIDIA's cicc is a private fork ahead of it). Matters because every "new language for GPUs" (Julia, Mojo, Rust-GPU, Triton) rides LLVM to PTX. - AMD ROCm/HIP: HIP is CUDA-API-shaped C++ (
hipMalloc,hipLaunchKernel) — porting is mostly mechanical (hipify). The backend compiles to GCN/CDNA ISA directly — no stable virtual ISA layer: code objects target specific gfx90a/ gfx942 ISAs. Public ISA docs (unlike SASS!) but no PTX-like forward-compat contract — the key asymmetry in any NVIDIA/AMD platform strategy. - SPIR-V: Khronos' portable IR (Vulkan compute, OpenCL, SYCL). The truly vendor-neutral option; the performance ceiling for ML workloads has historically lagged, which is the whole tension: neutrality vs peak.
- Triton: a Python DSL where you program at block granularity (
tl.loada tile, compute,tl.store) and the compiler handles smem staging, vectorization, and auto-tuning — i.e., it automates Phase 02 Ch. 7. Lowered through MLIR/LLVM to PTX (and increasingly to AMD). Why it won developer mindshare: it sits at the right abstraction level — high enough to be productive, low enough to hit ~90% of hand-tuned performance on the kernels LLMs need. - MLIR: LLVM's "compiler for building compilers" — dialects (linalg, gpu, nvgpu) that progressively lower. The bet most ML-compiler teams (and most accelerator startups) are making for their middle layer.
The platform-strategy takeaway: there are exactly three credible positions for a hardware-agnostic platform: (a) target each vendor's kernel libraries and ship a HAL (fastest to market — Phase 09 builds this), (b) adopt Triton/MLIR as your kernel layer and inherit its backends, (c) own a full compiler (moonshot). Most real platforms blend (a) and (b). Be able to argue all three with costs.
Chapter 10: Compiler-as-Moat — The Business Reading
Why this phase belongs in a Head of Engineering track, in four observations:
- CUDA's dominance is a software annuity. PTX forward-compat (Ch. 4) means every kernel ever written keeps working — so ecosystem value compounds. Competitors must beat not just the chip but 18 years of accumulated, still-running software.
- Accelerator startups die in the middle layer. Silicon tape-out is hard but bounded; "PyTorch runs well" is unbounded — thousands of ops × precisions × shapes, each needing near-roofline kernels. Audit question for any vendor pitch: "show me your top-20 op coverage at p50/p99 of peak, on my shapes."
- Kernel velocity is a hiring/architecture choice you own. FlashAttention shipped as CUDA; within months every serious shop needed it across hardware. Platforms with a Triton/MLIR posture absorbed it in weeks; pure-CUDA shops on non-NVIDIA hardware waited quarters. Your Phase 09 HAL design decides which side of that your company lives on.
- The JD's "decouple software from hardware" is only as real as your compiler/kernel story. Anyone can wrap two SDKs behind an interface; the hard promise is performance-portable decoupling, and Chapters 4–9 are the technical content of that promise.
Lab Walkthrough Guidance
Order: Lab 01 → Lab 02. Read real PTX before generating your own — you'll plagiarize the prologue idioms, which is exactly the point.
Lab 01 (dissection):
- Annotate
saxpy.ptxline-by-line using Ch. 5 as the rubric, before opening the answer key. - SASS pass: find one predicated instruction, one
FFMA, the unrolled loop body. With-maxrregcount 16: findLDL/STLspills. - Fatbin dissection:
cuobjdump --list-elfon a two-arch build; relate every entry to Ch. 7's load-time decision tree. - No CUDA toolkit: godbolt.org → CUDA C++ → it shows PTX and SASS side by side; everything except the fatbin step works there.
Lab 02 (mini compiler):
- Run
python solution.pyfirst — see the five kernels compile and the differential tests pass; read one emitted PTX fully. - Read in pipeline order (Ch. 2): Lexer → Parser → TypeChecker → CodeGen → PTXInterp. At each stage, ask "what's the input shape, what's the output shape" — they're all small list/tree transforms.
- Extensions in order: intrinsics (touch every stage once) → constant folding (your first IR pass) → CSE (your second; measure both) → the fused kernel.
- If you have any NVIDIA GPU: the README's
cuda-pythonsnippet loads your PTX for real. Do it — "my compiler's output runs on hardware" is a sentence few engineering leaders can say.
Success Criteria
- You can draw the nvcc pipeline (5 boxes) and say what each tool does (Ch. 3)
- You can read the saxpy PTX cold and narrate all five rubric points (Ch. 5)
- You can spot spills, FMAs, and predication in a SASS dump (Ch. 6)
- You can explain the fatbin decision tree and the cold-start incident + three fixes (Ch. 7)
- Your compiler passes its differential tests and you've written at least one optimization pass (Ch. 2, 8)
- You can argue the three platform-portability postures with costs (Ch. 9–10)
Interview Q&A
Q1: What happens, end to end, when you compile and run a CUDA program on a GPU
that didn't exist when you compiled it?
A: nvcc split the source; device code went C++ → NVVM/LLVM IR → PTX, and ptxas
compiled PTX → SASS for each -code=sm_XX requested; fatbinary bundled the
cubins plus (if requested) the PTX. On the new GPU there's no matching cubin, so
the driver JIT-compiles the embedded PTX to the new architecture's SASS at module
load, caching in the compute cache. First load is slow (the cold-start incident);
subsequent loads hit the cache. If the fatbin contains no PTX (-code=sm_80
only), the load fails with "no kernel image available" — which is why production
build policy always embeds a recent compute_XX.
Q2: Why does PTX exist at all — why not compile straight to machine code like CPUs do? A: CPU ISAs are stable contracts (x86 from 1978 still runs); GPU hardware ISAs change every generation because NVIDIA reserves the right to redesign encodings, scheduling, and register files for performance. PTX restores the missing stability: a virtual ISA with infinite typed registers and explicit state spaces that the driver can lower to any real architecture, past or future. It splits the compiler where you want it split — slow, smart optimization AOT into PTX; fast, mechanical lowering at load time. Strategically it's the CUDA ecosystem's compatibility promise — the moat (and AMD's lack of an equivalent is the key asymmetry in portability planning).
Q3: Reading a SASS dump, how do you tell register pressure became a problem?
A: LDL/STL instructions — local loads/stores — are spills: values that didn't
fit in registers round-tripping through local memory (which is HBM-backed, so
each spill is a global-memory-latency event inside your inner loop). Cross-check
with -Xptxas=-v ("N bytes spill stores/loads") and ncu's local-memory
traffic. The causes: too many live values (deep unrolling, big per-thread
arrays), or someone capped -maxrregcount/__launch_bounds__ chasing occupancy
— the Phase 02 tradeoff where you measure both sides before deciding.
Q4: Why has Triton succeeded where earlier GPU DSLs struggled? A: Abstraction level. CUDA C++ is thread-level — maximum control, maximum effort; every tiling/staging decision is manual (Phase 02 Lab 01). Polyhedral/graph compilers (early TVM et al.) were too far above — schedules divorced from the memory hierarchy lose peak. Triton programs the block: you write tile loads and tile math; the compiler assigns threads, stages shared memory, vectorizes, and auto-tunes — automating exactly the mechanical parts of Phase 02 Ch. 7 while leaving algorithmic structure to the author. It hits ~90% of hand-tuned on the kernels that matter to LLMs, it's Python-embedded (the audience's language), and it rides MLIR/LLVM so backends beyond NVIDIA are tractable — which is also why it's a credible kernel layer for a hardware-agnostic platform.
Q5: Hand-compile c[i] = a[i] * 2.0 + b[i] to PTX. Sketch is fine.
A: Prologue: .visible .entry with four .params (a, b, c pointers + n);
ld.param each; cvta.to.global the pointers. Index: mov from %ctaid.x,
%ntid.x, %tid.x; mad.lo.s32 %r4 = ctaid*ntid + tid. Guard:
setp.ge.s32 %p1, %r4, n then @%p1 bra DONE. Body: mul.wide.s32 index → byte
offset (×4), add.s64 to each base, ld.global.f32 %f1, [a_i],
ld.global.f32 %f2, [b_i], then fma.rn.f32 %f3, %f1, 2.0, %f2, and
st.global.f32 [c_i], %f3. DONE: ret. (Lab 02's compiler emits exactly this —
and the interview answer is stronger for being able to say so.)
Q6 (leadership): A candidate for your compiler-team lead says "we should build our own IR." How do you evaluate? A: Probe for the build-vs-adopt analysis. MLIR exists precisely so teams don't build bespoke IRs: dialects, pass infrastructure, and existing lowerings to NVPTX/ROCm are free; a custom IR re-pays all of that for marginal fit gains. Legitimate custom-IR cases: a truly novel execution model the dialects can't express, or extreme compile-time constraints. I'd ask: which MLIR dialects did you evaluate and where exactly did they fail? What's the five-year maintenance headcount? How do you absorb the next FlashAttention-shaped kernel innovation (ecosystem velocity, Ch. 10)? A candidate with a crisp "we tried linalg+nvgpu, here's the specific gap" earns the conversation; "LLVM is bloated" doesn't.
References
- NVIDIA, PTX ISA Specification — https://docs.nvidia.com/cuda/parallel-thread-execution/
- NVIDIA, CUDA Compiler Driver NVCC (the pipeline doc; run
nvcc -dryrun) — https://docs.nvidia.com/cuda/cuda-compiler-driver-nvcc/ - NVIDIA, CUDA Binary Utilities (
cuobjdump,nvdisasm) — https://docs.nvidia.com/cuda/cuda-binary-utilities/ - Compiler Explorer (Godbolt) CUDA mode — https://godbolt.org
- Tillet, Kung, Cox, "Triton: An Intermediate Language and Compiler for Tiled Neural Network Computations" (MAPL 2019); docs — https://triton-lang.org
- Lattner et al., "MLIR: Scaling Compiler Infrastructure for Domain Specific Computation" (CGO 2021)
- LLVM NVPTX backend guide — https://llvm.org/docs/NVPTXUsage.html
- AMD ROCm/HIP documentation + public ISA guides (CDNA3) — https://rocm.docs.amd.com
- Crafting Interpreters (Nystrom) — the gentlest serious compiler-construction text, for Lab 02 depth — https://craftinginterpreters.com
- Cross-track: Phase 02 WARMUP Ch. 8 (occupancy/registers), Phase 09 WARMUP (the HAL this knowledge feeds)
🛸 Hitchhiker's Guide — Phase 03: GPU Compilers & Codegen
Read this if: you can write CUDA but
nvccis a magic box, "PTX" is a word you nod at, and you couldn't yet audit an accelerator vendor's "we support PyTorch" claim. Compressed field notes; derivations live in the WARMUP.
0. The 30-second mental model
CUDA C++ ──(cicc/LLVM)──► PTX (virtual ISA, stable forever)
└─(ptxas, AOT)──► SASS for sm_80, sm_90... } fatbin
└─(driver JIT, load time)──► SASS for whatever ships next
PTX = the contract. SASS = the truth. Fatbin = the deployment policy. JIT = the cold-start you forgot to plan for. Everything else is detail.
1. The toolbox
| Command | What you learn |
|---|---|
nvcc -ptx x.cu | the PTX — read it with WARMUP Ch. 5's rubric |
nvcc -dryrun x.cu | the actual pipeline, tool by tool |
nvcc -Xptxas=-v | registers, smem, spills per kernel (Phase 02 link) |
cuobjdump --dump-sass x | the real ISA: spills=LDL/STL, TensorCores=HMMA, async copy=LDGSTS |
cuobjdump --list-elf x | what's in the fatbin (which sm_XX, is PTX embedded?) |
nvdisasm -c x.cubin | SASS with control-flow graph |
| godbolt.org CUDA | all of the above, zero install |
2. PTX reading card
%tid.x %ctaid.x %ntid.x= threadIdx / blockIdx / blockDimmad.lo.s32 %r4, %ctaid, %ntid, %tid= THE index formula.param → ld.param → cvta.to.global= kernel ABI prologue (your Lab 02 compiler emits this trio)setp.ge.s32 %p1, …+@%p1 bra= tail guard;@%pon a non-branch = if-conversion (no divergence machinery at all)fma.rn.f32= contracted multiply-add; one rounding, not two — the reason for tolerance-based testing, and for-fmad=falsein numerical disputes- Infinite
%f/%r/%rdregisters — pressure is invisible here; spills only exist in SASS
3. Production policies you now own
- Build matrix: AOT SASS for every arch in the fleet + newest
compute_XXPTX for the future. Cost: build minutes, container GB. Skipping an arch = JIT cold-start storm (or hard load failure if you also skipped PTX). - JIT hygiene:
CUDA_MODULE_LOADING=LAZY(default since 12.x), pre-warm compute cache in images, watchCUDA_CACHE_MAXSIZEon multi-model fleets. - Numerical policy: FMA contraction + reduction order = CPU/GPU diffs in the
last ulp; tests compare with tolerance, never
==(Phase 02), and-fmadchanges are reviewed like API changes. - The vendor audit (the leadership move of this phase): "Top-20 ops for our workload: % of roofline at p50/p99, on our shapes and precisions. Which came from a kernel library, which from your compiler, which are unimplemented?" Watch them sweat on attention variants and odd head dims.
4. The portability map, one line each
- NVPTX (LLVM): how every non-NVIDIA frontend reaches NVIDIA hardware.
- ROCm/HIP: CUDA-shaped API, mechanical porting, public ISA — but no PTX equivalent: per-gfx code objects, no forward-compat promise. Plan rebuilds.
- SPIR-V: truly neutral, historically below peak for ML; the "standards" position.
- Triton: block-level Python DSL → MLIR → PTX/ROCm; ~90% of hand-tuned on LLM kernels; the pragmatic kernel layer for a hardware-agnostic platform.
- MLIR: the infrastructure bet; what you adopt so you don't invent an IR.
Three platform postures: wrap vendor libraries behind a HAL (fast, Phase 09), adopt Triton/MLIR for kernels (velocity), build a full compiler (moonshot). Blend of first two = the defensible default. Be ready to defend it to a board.
5. War stories to recognize
- "New A-series GPUs took 8 minutes to first token" — no sm_XX cubin, JIT storm. Fix: build matrix + cache pre-warm. (You will meet this one.)
- "Same model, different last digit on GPU vs CPU" — FMA contraction + parallel reduction order. Not a bug; a numerical policy.
- "We set maxrregcount=32 and got slower" — spills to local memory; the
SASS shows
STLin the inner loop. Occupancy isn't a score (Phase 01/02). - "Vendor demo hit 80% of peak; our model hit 19%" — demo ran their three tuned kernels; your model hit the unimplemented long tail. The audit in §3 exists because of this meeting.
6. Exit bar
You can: narrate saxpy's PTX cold; find spills/FMA/predication in SASS; draw the fatbin/JIT decision tree and its incident; defend a build-matrix policy; argue the three portability postures; and your own little compiler emits PTX that a real driver loads. That's "technical credibility with engineering teams" at the compiler layer — on to Phase 04, where we descend below the runtime into the driver and the kernel (the OS one).
Lab 01 — PTX/SASS Dissection
Phase: 03 — GPU Compilers | Difficulty: ⭐⭐⭐☆☆ | Time: 3–4 hours
Tools: CUDA toolkit (nvcc, cuobjdump) — or godbolt.org with zero install
Concept primer:
../WARMUP.mdCh. 3–8.
Run
nvcc -ptx -o saxpy.ptx kernels.cu # PTX for annotation
nvcc -cubin -arch=sm_80 -o kernels.cubin kernels.cu
cuobjdump --dump-sass kernels.cubin > kernels.sass # SASS for the hunt
nvcc -Xptxas=-v -c kernels.cu # registers/smem/spills
No toolkit? godbolt.org → language CUDA C++ → compiler
"NVCC (latest)" → flags -O3 -arch=sm_80; the right pane shows PTX, and "Add new
→ Device" shows SASS. Everything below except the fatbin step works there.
0. The mission
Four tiny kernels (kernels.cu), four investigations:
saxpy— annotate every PTX line. Template insaxpy-annotation-template.md; answer key insaxpy-annotated.md. Do not peek early.guarded— a smallifbody: find where the branch disappeared in SASS (if-conversion →@%p-predicated stores /SEL).unrolled—#pragma unroll 8loop: count the replicated bodies in SASS; note the register count change in-Xptxas=-v.spiller— a kernel with a large per-thread array. Compile normally, then with-maxrregcount=16: find theLDL/STLlocal-memory spills that appear, and the "bytes spill stores/loads" line in ptxas output.
Then the fatbin dissection:
nvcc -gencode arch=compute_80,code=sm_80 \
-gencode arch=compute_90,code=sm_90 \
-gencode arch=compute_90,code=compute_90 -c kernels.cu -o fat.o
cuobjdump --list-elf fat.o # two ELFs (sm_80, sm_90)...
cuobjdump --dump-ptx fat.o | head # ...plus embedded PTX (compute_90)
Map each artifact to the load-time decision tree (WARMUP Ch. 7) and write, in two sentences, what happens on (a) an A100, (b) an H100, (c) whatever ships next.
1. The annotation rubric (what "done" means for saxpy)
Your annotation must identify: the .param ABI and ld.param prologue; the
cvta.to.global state-space conversion and why it exists; the
mad.lo.s32 index formula and which special registers feed it; the
setp+@%p bra tail guard; the mul.wide.s32 byte-offset computation; the
fma.rn.f32 contraction (and what -fmad=false would do to it). Six items —
the same six the WARMUP Ch. 5 tour narrates.
2. Findings sheet (fill in)
| Kernel | Flag setup | What I found (instruction + line) | Mechanism (one sentence) |
|---|---|---|---|
| guarded | -O3 | ||
| unrolled | -O3 | ||
| spiller | default | ||
| spiller | -maxrregcount=16 | ||
| fat.o | 3 gencodes |
3. Extensions
- Cross-compiler: same saxpy through
clang -x cuda --cuda-device-only -S(LLVM NVPTX) — diff the PTX against nvcc's. What's identical (the ABI), what differs (instruction selection, virtual register naming)? - Cross-vendor: on Godbolt, compile the HIP version with
hipccfor gfx90a and skim the GCN ISA — note there's no PTX-like layer (WARMUP Ch. 9's asymmetry, seen with your own eyes). - Tensor Core spotting: compile a 16×16
wmmaexample and findHMMAin SASS — the instruction the whole AI economy runs on.
4. What this lab proves about you
The compiler stack is no longer folklore: you've read the contract layer (PTX), the truth layer (SASS), and the deployment layer (fatbin) with your own eyes, and you can connect each to a production decision (numerical policy, occupancy tuning, build matrix).
Lab 02 — Mini Compiler: Expression Language → PTX
Phase: 03 — GPU Compilers | Difficulty: ⭐⭐⭐⭐☆ | Time: 6–10 hours Language: pure Python (stdlib) | Hardware: none — includes a PTX interpreter; optional real-GPU load at the end
Concept primer:
../WARMUP.mdCh. 2 (compiler stages), Ch. 4–5 (PTX), Ch. 8 (the optimizations you'll add).
Run
python solution.py # compiles 5 kernels to PTX, runs differential tests
python solution.py --dump # also prints every kernel's emitted PTX
0. The mission
Build the real thing, small: a compiler for elementwise GPU kernels —
kernel saxpy(a: f32, x: f32*, y: f32*) {
y[i] = a * x[i] + y[i];
}
— that emits legal PTX (the same shape you annotated in Lab 01: .param ABI,
cvta.to.global, mad.lo.s32 index, setp guard, fma.rn.f32), where i is
implicitly the global thread index and a hidden n parameter guards the tail.
Because most readers don't have a GPU attached, the lab includes a PTX
interpreter (PTXInterp) that executes the instruction subset per-thread with
float32 rounding — so every compiled kernel is differentially tested against a
plain-Python reference. Compiler people call this differential testing; it's how
real codegen is validated (alongside conformance suites), and building the
interpreter teaches the ISA semantics a second way.
1. The pipeline (read in this order)
| Stage | Class | In → Out | The classic name |
|---|---|---|---|
| 1 | Lexer | source chars → tokens | lexical analysis |
| 2 | Parser | tokens → AST (Kernel, Assign, BinOp, Load, Var, Num) | recursive descent, precedence climbing |
| 3 | TypeChecker | AST → typed AST (every node .ty ∈ {f32, ptr}) | semantic analysis |
| 4 | CodeGen | AST → PTX text (virtual %f/%r/%rd registers, FMA contraction) | instruction selection |
| 5 | PTXInterp | PTX text + buffers → executed results | (the test oracle) |
Each stage is ~60 lines. The deep lesson: a compiler is a chain of small, testable transforms — after this lab, "we should build a compiler layer" stops being a scary sentence and becomes a costable one.
2. What the emitted PTX looks like
For c[i] = a[i] * 2.0 + b[i]; you'll get exactly the Lab 01 idioms:
.visible .entry scale2(
.param .u64 scale2_param_0, // a
.param .u64 scale2_param_1, // b
.param .u64 scale2_param_2, // c
.param .u32 scale2_param_3 // n (implicit)
){
...
mad.lo.s32 %r4, %r1, %r2, %r3; // i
setp.ge.s32 %p1, %r4, %r5;
@%p1 bra DONE;
mul.wide.s32 %rd10, %r4, 4; // byte offset, computed once
...
ld.global.f32 %f1, [%rd11];
mov.f32 %f2, 0f40000000; // 2.0 as IEEE-754 hex — PTX float literal form
ld.global.f32 %f3, [%rd12];
fma.rn.f32 %f4, %f1, %f2, %f3; // contraction: (a*2.0)+b -> one FMA
st.global.f32 [%rd13], %f4;
DONE:
ret;
}
Note 0f40000000: PTX spells float immediates as IEEE-754 bit patterns — one of
a dozen small ABI/syntax facts you only learn by emitting the format for real.
3. Extension ladder (this is the lab)
- Intrinsics: add
min(x, y)/max(x, y)→min.f32/max.f32. Touches every stage once — the perfect first change. - Constant folding: a pass over the AST:
2.0 * 4.0 + x→8.0 + x. Assert emitted instruction count drops. - CSE:
(a[i]+b[i]) * (a[i]+b[i])currently loads and adds twice. Hash subtrees, reuse registers. Measure again. relu(a[i] * b[i] + c[i])— fused elementwise kernel viamax+ folding: you have just built, in miniature, what fusion compilers (XLA, Inductor) do at scale.- Real hardware (optional, any NVIDIA GPU):
Launch withfrom cuda import cuda # pip install cuda-python # cuLinkCreate/cuModuleLoadDataEx accepts PTX text directly: err, mod = cuda.cuModuleLoadData(ptx_text.encode())cuLaunchKerneland compare against the interpreter. When the driver accepts your compiler's output, you've closed the loop this phase is about. - Second backend (Phase 09 foreshadowing): emit C source from the same AST
(a
forloop overi). Notice what's shared (front/middle end) and what isn't (ABI, index model) — that boundary is the HAL design problem.
4. Common pitfalls
- Float literals as decimals — PTX wants
0f<bits>form for exactness; emitting2.0works in some toolchains and silently loses precision in others. The providedf32_hex()does it right. - Reusing the byte-offset register per operand — compute
i*4once; recomputing it per load is correct but bloats the kernel (and your CSE pass should catch it if you regress). - Forgetting the guard — the interpreter intentionally runs
nnot equal to a block multiple; an unguarded kernel writes out of bounds and the differential test catches the corruption. (Same bug as Phase 02 Lab 01 §1.) - f32 rounding in the oracle — the interpreter rounds every op to float32
(
struct.packround-trip); compare with relative tolerance, never==(FMA contraction changes the last ulp — WARMUP Ch. 8).
5. What this lab proves about you
"I built a compiler that emits PTX the CUDA driver loads, with differential tests" — that sentence, backed by this repo, is technical credibility with any compiler or kernels team you will ever lead. And the build-vs-adopt argument (WARMUP Ch. 9–10) is no longer abstract: you know exactly which parts were easy (the front end), which were fiddly (ABI details), and which you didn't even attempt (optimization at scale, multi-arch lowering) — which is precisely the cost model an engineering head needs.
Phase 04 — Linux Internals, GPU Drivers & Containers
Difficulty: ⭐⭐⭐⭐☆ | Estimated Time: 2 weeks Roles supported: Head of Engineering [GPU], Infrastructure/Platform Engineer, Driver-adjacent Runtime Engineer Hardware needed: a Linux machine or VM (Lima/Multipass/UTM on macOS, WSL2 on Windows, any cloud VM). No GPU required.
Why This Phase Exists
The JD requires "solid grounding in Linux internals, containers, GPU drivers." Between your CUDA program (Phase 02) and the silicon (Phase 01) sits a stack most engineers treat as superstition: libcuda.so, /dev/nvidia0, a kernel module, ioctls, mapped ring buffers. When a fleet GPU "falls off the bus," when a container can't see its GPU, when driver/CUDA version skew bricks a deployment at 2 a.m. — the engineers who can reason about this layer fix it; everyone else reboots and prays.
You will not write an NVIDIA driver. You will write a small, honest Linux character-device driver that exposes the same architectural pattern GPU drivers use — a /dev node, ioctl command submission, mmap-style buffer sharing — and you will build a container from scratch with namespaces and cgroups, then map exactly how a GPU passes through that boundary. After this, nvidia-container-toolkit and k8s device plugins (Phase 06) are obvious instead of magical.
Concepts
- Kernel space vs user space; syscalls; what a "driver" actually is (callbacks registered with the kernel)
- Character devices: major/minor numbers,
/devnodes, udev,file_operations ioctl: the universal "weird device command" escape hatch — and the GPU's actual control path- The real NVIDIA stack: KMD (
nvidia.ko) vs UMD (libcuda.so), command ring buffers, doorbells, why submission avoids syscalls in the hot path - GSP firmware, the open-gpu-kernel-modules split, nouveau vs proprietary
- Driver/CUDA version skew: forward-compat matrix, the "driver too old" failure, fleet upgrade choreography
- DMA and IOMMU; pinned memory from the kernel's point of view (Phase 02 Ch. 5 connection)
- Namespaces (mount, PID, net, user) — what a container actually is
- cgroups v2: cpu, memory, io controllers — and why GPUs have no cgroup controller (the gap that creates the entire GPU-sharing industry, Phase 06)
- How a GPU enters a container: device nodes + library injection (
nvidia-container-toolkit),CUDA_VISIBLE_DEVICESvs cgroupdevicesallow-lists /proc,/sys,strace,lsof,dmesg— the debugging toolkit
Labs
Lab 01 — A GPU-Shaped Character Device Driver (C, Linux kernel module)
| Field | Value |
|---|---|
| Goal | Write, build, load, and exercise a kernel module exposing /dev/fakegpu: ioctl command submission (ALLOC/SUBMIT/WAIT/INFO), a per-process "context," and a job queue — the GPU driver pattern in 300 lines. |
| Concepts | file_operations, copy_to_user/copy_from_user, ioctl ABI design, per-fd state (the "GPU context"!), kernel logging, module lifecycle. |
| Steps | 1) make && sudo insmod fakegpu.ko, check dmesg. 2) Run the userspace client ./client — it allocates "buffers," submits "jobs," waits for completion. 3) strace ./client and see exactly the ioctl pattern strace-ing a real CUDA app shows. 4) Extensions: add a MAP command, enforce per-context quotas, simulate async completion with a kernel timer. |
| Stack | C, Linux kernel headers (linux-headers-$(uname -r)), any Linux VM |
| Output | Loadable .ko + client whose built-in asserts pass; an strace capture annotated against the real nvidia ioctl flow. |
| How to Test | ./client prints PASS for: context isolation (two fds don't see each other's buffers), quota enforcement, job completion ordering. |
| Talking Points | Why per-fd state mirrors CUDA contexts; why real GPUs submit via mapped ring buffers + doorbells instead of one ioctl per kernel launch (syscall overhead vs Phase 02's ~5µs launch); what a poisoned context means at this layer. |
| Resume Bullet | "Implemented a Linux character-device driver with ioctl-based command submission modeling GPU driver architecture (contexts, buffer registry, job queue); validated isolation and quota semantics with a userspace test client." |
| Extensions | mmap a shared "completion ring" to the client and poll it without syscalls (the doorbell/fence pattern); add a sysfs attribute exposing per-context stats (your first step toward Phase 10's metrics). |
Lab 02 — Container From Scratch + GPU Passthrough Anatomy (C + shell)
| Field | Value |
|---|---|
| Goal | Build a minimal container runtime in C — namespaces, pivot_root, cgroup v2 limits — then dissect how real runtimes inject GPUs. |
| Concepts | clone(2) flags, mount/PID/UTS/user namespaces, cgroup v2 filesystem API, device nodes in containers, what nvidia-container-toolkit actually does (hook → inject /dev/nvidia* + driver libs). |
| Steps | 1) make && sudo ./mininer run /bin/sh — you're PID 1 in an isolated rootfs. 2) Apply a 100 MB memory limit via cgroup v2; watch the OOM killer enforce it. 3) Walkthrough: trace nvidia-container-toolkit config and replicate its effect manually (bind-mount a fake /dev/fakegpu from Lab 01 into your container + allow it in the devices cgroup). 4) Write the one-page "how a GPU enters a container" explainer. |
| Stack | C (clone/unshare/mounts), cgroup v2 sysfs, your Lab 01 device |
| Output | Working mini-runtime + the GPU-passthrough explainer (your team-training artifact). |
| How to Test | Inside the container: ps shows PID 1–2 only; hostname differs; memory hog gets OOM-killed at the limit; /dev/fakegpu works after injection but /dev/null outside the allow-list is denied. |
| Talking Points | "A container is a process with opinions" — namespaces (visibility) vs cgroups (resources) vs capabilities (privilege); why GPU has no cgroup controller and what that implies for multi-tenancy (Phase 06's opening problem). |
| Resume Bullet | "Built a minimal container runtime in C (namespaces, pivot_root, cgroups v2) and documented GPU device-injection mechanics, establishing the team's mental model for GPU container debugging." |
| Extensions | Add network namespace + veth pair; user-namespace UID mapping (rootless containers); compare your runtime's strace against runc's. |
Deliverables Checklist
-
fakegpu.koloads; client passes all asserts; you can explain everyfile_operationscallback - Annotated strace: your client vs a real CUDA app (or the captured trace provided)
- Mini-runtime: isolation + OOM demos reproduced
- The "how a GPU enters a container" explainer written
- You can narrate the KMD/UMD split and the ring-buffer submission path from memory
Interview Relevance
- "What happens, at the OS level, when you call
cudaLaunchKernel?" - "Why doesn't
CUDA_VISIBLE_DEVICESprovide isolation? What does?" - "A container sees the GPU but CUDA init fails. Debug it." (driver/library version skew, device node perms, the toolkit hook)
- "Why is there no GPU cgroup controller, and what do people do instead?"
- "Driver upgrade across a 10k-GPU fleet — walk me through the plan."
Warmup Guide — Linux Internals, GPU Drivers & Containers
Zero-to-expert primer for Phase 04. Assumes you can use a Linux shell and read C. No kernel programming background assumed — that's what we build. By the end the stack between
cudaLaunchKerneland silicon is glass, not fog.
Table of Contents
- Chapter 1: Kernel Space, User Space, and What a Driver Is
- Chapter 2: Character Devices — /dev, major/minor, file_operations
- Chapter 3: ioctl — The GPU's Actual Control Path
- Chapter 4: The Real NVIDIA Stack — KMD, UMD, Ring Buffers, Doorbells
- Chapter 5: DMA, IOMMU, and Pinned Memory From the Kernel's Side
- Chapter 6: Version Skew — The Driver/CUDA Compatibility Matrix
- Chapter 7: Namespaces — What a Container Actually Is
- Chapter 8: cgroups v2 — Resources, and the GPU-Shaped Hole
- Chapter 9: How a GPU Enters a Container
- Chapter 10: The Debugging Toolkit — strace, dmesg, /proc, /sys
- Lab Walkthrough Guidance
- Success Criteria
- Interview Q&A
- References
Chapter 1: Kernel Space, User Space, and What a Driver Is
Zero background: the CPU runs in (at least) two privilege modes. Kernel mode can touch hardware, page tables, interrupts; user mode cannot — any attempt traps. Your process lives in user mode; the only doorway into kernel mode is a syscall (or an interrupt/fault). That hard boundary is why "the process crashed" doesn't take the machine down, and why everything hardware-related — including every GPU operation — ultimately funnels through the kernel at least once.
What a driver is, demystified: a driver is not a process or a daemon. It's
a bundle of callbacks registered with the kernel ("when someone open()s
this device, call my open; when they ioctl(), call my ioctl") plus
interrupt handlers ("when the hardware raises IRQ N, call this"). A loadable
kernel module (.ko) is just a relocatable object the kernel links into itself
at runtime — insmod is closer to dlopen than to exec. Lab 01 makes this
concrete: your fakegpu.ko is ~300 lines of callbacks and one struct file_operations.
The cost model that explains GPU driver design: a syscall costs ~100–300 ns plus cache pollution; an interrupt costs more. Drivers for high-rate devices (GPUs, NICs, NVMe) are therefore architected to get the kernel out of the hot path: set up shared memory once, then submit work from user space without syscalls (Ch. 4). Keep this in mind — it's the single design force behind everything in this phase.
Misconception: "the GPU driver runs on the GPU." The driver runs on the CPU, in the kernel; the GPU runs firmware (increasingly — GSP, Ch. 4) and your kernels. The driver's job is bookkeeping, memory management, scheduling, and shoveling command buffers.
Chapter 2: Character Devices — /dev, major/minor, file_operations
Unix's grand unification: devices are files. open("/dev/nvidia0") returns
an fd; read/write/ioctl/mmap on that fd dispatch into driver callbacks.
The plumbing, end to end:
- A character device ("char dev") is the streams-of-bytes/commands flavor (vs block devices, which are the page-cached storage flavor). GPUs are char devices.
- Each device has a major number (selects the driver) and minor number
(selects the instance —
/dev/nvidia0vs/dev/nvidia1).ls -l /dev/nvidia*shows them (major 195 for NVIDIA, historically). /devnodes are created by udev at hotplug (ormknodby hand, ordevtmpfs). A node is just a (type, major, minor) triple in a filesystem — this fact is load-bearing for containers (Ch. 9): injecting a GPU into a container is largely creating/allowing this node inside it.- In the driver, you register a
struct file_operations:
static const struct file_operations fakegpu_fops = {
.owner = THIS_MODULE,
.open = fakegpu_open, // allocate per-fd state — "a context"
.release = fakegpu_release, // free it
.unlocked_ioctl = fakegpu_ioctl, // the command path
.mmap = fakegpu_mmap, // shared-memory path (extension)
};
The idea to carry away: per-open-file state. Each open() gets its own
context structure (Lab 01 hangs a buffer registry and job queue off it). This is
precisely what a CUDA context is at the driver level — and why two processes
on one GPU don't see each other's allocations (and why a poisoned context dies
alone, Phase 02 Ch. 4).
Chapter 3: ioctl — The GPU's Actual Control Path
read/write suit byte streams. Devices need commands: "allocate 2 GB,"
"map this buffer," "submit this command stream." The escape hatch is
ioctl(fd, CMD, arg) — a generic "do device-specific thing," where CMD
encodes direction/size/type and arg points to a command struct copied across
the boundary.
The ABI discipline (Lab 01's core lesson):
struct fakegpu_alloc { __u64 size; __u32 handle; /* out */ };
#define FAKEGPU_IOC_ALLOC _IOWR('F', 1, struct fakegpu_alloc)
// kernel side — NEVER dereference user pointers:
if (copy_from_user(&req, (void __user *)arg, sizeof(req))) return -EFAULT;
...validate everything...
if (copy_to_user((void __user *)arg, &req, sizeof(req))) return -EFAULT;
copy_from_user/copy_to_user are the checked crossings; validation is
mandatory because user space is adversarial (a security boundary you'll
revisit in Phase 11). Handles — small integers the driver maps to kernel-side
objects — instead of raw pointers, for the same reason fds exist.
Proof that this is real: strace any CUDA program:
openat(AT_FDCWD, "/dev/nvidiactl", O_RDWR) = 3
ioctl(3, _IOC(_IOC_READ|_IOC_WRITE, 0x46, 0x2a, 0x20), 0x7ffd...) = 0
openat(AT_FDCWD, "/dev/nvidia0", O_RDWR) = 4
ioctl(4, ...) = 0 // dozens at init
mmap(NULL, 0x200000, ..., fd=4, ...) = 0x7f... // ring buffers!
Initialization is a flurry of ioctls; then it goes quiet — because the steady state uses the mmap'd path (next chapter). Lab 01 step 3 has you place these two traces side by side.
Chapter 4: The Real NVIDIA Stack — KMD, UMD, Ring Buffers, Doorbells
The division of labor:
- KMD (kernel-mode driver) —
nvidia.ko(+nvidia-uvm.kofor unified memory,nvidia-modeset.kofor display): privileged setup. Memory management (GPU page tables!), context creation, channel/ring allocation, interrupt handling, recovery (Xid errors — Phase 10), power. - UMD (user-mode driver) —
libcuda.so(the driver API from Phase 02 Ch. 1 lives here): everything per-launch. Builds command buffers in memory shared with the GPU, manages your virtual address space, JIT-compiles PTX (Phase 03 Ch. 7!), implements the CUDA API surface.
The hot path — why launches take ~5 µs, not ~5 ms:
- At context setup, the KMD maps a ring buffer (command queue) and a
doorbell register into the process's address space (those
mmaps in the strace). - To launch a kernel,
libcuda.sowrites the command into the ring (a memory write!) and writes the doorbell (another memory write, which the hardware sees) — zero syscalls. - The GPU's front end fetches commands, executes, and writes a fence
(completion marker) back to shared memory; the UMD polls or sleeps on an
interrupt for it. (
cudaEventSynchronize= wait-on-fence.)
This user-space-submission pattern is universal in high-performance I/O — NVMe
queues, io_uring, DPDK NICs, every modern GPU (AMD's is in the open
amdgpu driver — same shapes, readable source). Your Lab 01 extension (mmap'd
completion ring) implements the fence half.
Two modern wrinkles worth knowing: GSP firmware — recent NVIDIA drivers offload much KMD logic to a RISC-V management processor on the GPU, shrinking kernel-side code (and enabling the open-gpu-kernel-modules release: the kernel module is now MIT/GPL dual-licensed and on GitHub — readable!); the proprietary crown jewels moved to firmware + UMD. And nouveau — the reverse-engineered FOSS driver — historically lacked the firmware interfaces for reclocking, hence its performance gap; GSP is slowly changing that equation.
Chapter 5: DMA, IOMMU, and Pinned Memory From the Kernel's Side
Phase 02 Ch. 5 told you pinned memory makes transfers ~2× faster and truly async. Now the mechanism:
- DMA: the GPU's copy engines read/write host RAM directly, without the CPU touching the data. To do that safely, the physical pages must (a) stay resident — not be swapped or migrated — and (b) be known to the device.
- Pinning (
cudaMallocHost→get_user_pages/page-locking in the kernel) guarantees (a). For pageable memory the driver must instead copy through an internal pre-pinned bounce buffer — there's your 2× and your fake-async. - IOMMU provides (b)-with-safety: it's a page table for devices,
translating device-visible addresses and blocking DMA outside allowed ranges.
Cloud passthrough (vfio), confidential computing, and "why does
iommu=offmake it faster but scarier" all live here. (Security relevance returns in Phase 11: DMA from a malicious device is a classic attack class.)
Chapter 6: Version Skew — The Driver/CUDA Compatibility Matrix
The unglamorous chapter that pages people at 2 a.m.:
- One kernel module serves all processes; each container/app brings its own
CUDA toolkit/runtime. The contract: driver ≥ what the toolkit needs.
nvidia-smi's "CUDA Version" = the max the driver supports, not what's installed. - Failure signatures to recognize on sight:
CUDA driver version is insufficient for CUDA runtime version(driver too old);forward compatibility was attempted on non supported HW(compat package misuse); a container that worked on node A failing on node B (driver skew across the fleet); kernel upgrade → module won't load → DKMS didn't rebuild (or secure-boot signing failed — Phase 11 connection). - Fleet upgrade choreography (the head-of-engineering deliverable): inventory driver/toolkit pairs → canary nodes → drain GPU workloads (Phase 06 scheduler integration) → upgrade KMD (reboot or careful unload) → health-check (Phase 10's checks) → progressive rollout. The forward-compatibility packages (newer libcuda on older driver branch, datacenter GPUs only) buy flexibility; pinning exact driver versions in node images buys reproducibility. Pick a policy and write it down — Lab 02's explainer and Phase 10's release engineering both cite this chapter.
Chapter 7: Namespaces — What a Container Actually Is
There is no container object in the kernel. "Container" = a process started with unshared namespaces + resource limits (Ch. 8) + (usually) reduced privileges. Namespaces virtualize visibility:
| Namespace | Isolates | Demo |
|---|---|---|
| mount | filesystem tree | different / via pivot_root |
| PID | process IDs | your shell is PID 1; can't see host |
| net | interfaces, ports | empty ip a until you add a veth |
| UTS | host/domain name | hostname container1 |
| IPC | SysV/POSIX queues | |
| user | UID mappings | root inside = UID 100000 outside (rootless) |
| cgroup | cgroup tree view |
One syscall family creates them: clone(CLONE_NEWPID | CLONE_NEWNS | ...) /
unshare(2). Lab 02's runtime is ~200 lines because that's genuinely all it
takes: clone with flags, set hostname, pivot_root into a rootfs, mount fresh
/proc, drop into a shell. Docker/Podman/containerd add image management,
networking, and an API — the isolation itself is these kernel primitives.
Why this matters for a GPU platform: every scheduling, isolation, and security promise you make to customers (Phase 06, 11) bottoms out in which namespaces/cgroups/devices a workload got. You can't reason about "can tenant A see tenant B's model weights" without this chapter.
Chapter 8: cgroups v2 — Resources, and the GPU-Shaped Hole
Namespaces control what a process sees; cgroups control what it uses.
v2 is a unified tree mounted at /sys/fs/cgroup; directories are groups; writes
to control files set limits:
mkdir /sys/fs/cgroup/demo
echo 100M > /sys/fs/cgroup/demo/memory.max
echo "50000 100000" > /sys/fs/cgroup/demo/cpu.max # 0.5 CPU
echo $$ > /sys/fs/cgroup/demo/cgroup.procs # join
Exceed memory.max → the OOM killer fires inside the group (Lab 02 step 2
demonstrates). The devices controller (an eBPF program in v2) allow-lists
device-node access — the enforcement half of GPU injection (Ch. 9).
The GPU-shaped hole — say this sentence in interviews: there is no GPU
cgroup controller. No gpu.max, no kernel-enforced fraction of SMs or HBM.
Consequences:
- "GPU limits" in k8s are allocation (whole devices via the device plugin), not enforcement.
- Actual sharing requires GPU-side mechanisms — MPS (cooperative, weak isolation), MIG (hardware partition, strong), time-slicing (none) — Phase 06's entire subject matter, motivated by this one missing kernel feature.
- Vendors building "fractional GPU" products (and this JD's company likely among them) are filling exactly this hole with userspace interception or scheduler tricks — knowing why the hole exists (GPU contexts are opaque to the kernel; the KMD, not the scheduler, owns GPU time) is what lets you evaluate those products honestly.
Chapter 9: How a GPU Enters a Container
Now compose Chapters 2, 7, 8 — the full anatomy, which Lab 02 step 3 replicates manually:
A container needs three things to use a GPU:
- The device nodes:
/dev/nvidia0,/dev/nvidiactl,/dev/nvidia-uvmvisible in its mount namespace (bind-mount or mknod), and allowed by the devices cgroup/eBPF program. - The user-mode driver libraries:
libcuda.somatching the host kernel module (Ch. 6!). The container image cannot ship these — it would skew against the host KMD. So they're injected from the host at start. - No special kernel work — the KMD is shared; namespaces don't apply to it.
nvidia-container-toolkit is precisely this: an OCI hook (now CDI spec) that,
at container creation, mounts the device nodes + host driver libs +
nvidia-smi et al. into the container and writes the cgroup allow rule.
CUDA_VISIBLE_DEVICES (or NVIDIA_VISIBLE_DEVICES at the hook level) then
filters which minors get this treatment — note carefully: visibility, not
security — a process that can mknod or escape the allow-list can reach other
GPUs; the cgroup rule is the actual enforcement, and per-tenant isolation worthy
of the name means dedicated devices or MIG (Phase 06) plus the Phase 11 story.
Debugging ladder for "container can't see GPU" (memorize; it's a weekly
occurrence somewhere in any fleet): node has driver? (nvidia-smi on host) →
toolkit hook configured? (runtime config) → nodes present in container?
(ls /dev/nvidia*) → libs injected & matching? (ldconfig -p | grep cuda,
compare versions) → cgroup allows? → only then blame the app.
Chapter 10: The Debugging Toolkit — strace, dmesg, /proc, /sys
The five tools that solve most layer-4 mysteries:
strace -f -e trace=openat,ioctl,mmap— what is the process actually asking the kernel? (CUDA init failures become obvious: which open/ioctl returned what errno.)dmesg -T— the kernel's side of the story: module load errors, Xid errors (the GPU's error codes: Xid 79 = fell off the bus, Xid 48 = ECC DBE… Phase 10 builds alerting on these), OOM kills, IOMMU faults./proc/PID/—maps(see the mmap'd rings!),fd/(which devices are open),status,cgroup./sys— device topology (/sys/bus/pci/devices/...), cgroup tree, module params (/sys/module/nvidia/).lsof /dev/nvidia*— who's holding the GPU (the "why won't the driver unload" question).
Lab 01 step 3 and Lab 02's debugging exercises are drills on exactly these.
Lab Walkthrough Guidance
Order: Lab 01 → Lab 02 (Lab 02 injects Lab 01's device into a container — the punchline depends on both).
Lab 01 (fakegpu driver):
- Read
fakegpu.ctop-down against Ch. 2–3 before building; predict whatdmesgwill print at insmod. - Build/load (any Linux VM; headers via
apt install linux-headers-$(uname -r)). Run./client; all asserts PASS. strace ./client→ annotate against the chapter-3 CUDA trace. Two columns, same shape — that's the lab's thesis made visible.- Extensions in order: quotas (validation discipline) → timer-based async completion (interrupt/fence flavor) → mmap'd completion ring (the real pattern; hardest).
- Safety: it's a VM, snapshot first; a buggy module can panic the kernel — which is itself the lesson about why KMDs are small and UMDs are big.
Lab 02 (mini container runtime):
- Run first, read after:
sudo ./mininer run /bin/sh, poke around (ps,hostname,ls /), exit, then readmininer.cagainst Ch. 7. - cgroup demo: apply
memory.max, run the includedhog, watch the group OOM while the host stays healthy. - The synthesis step: inject
/dev/fakegpuinto the container (bind-mount + devices allow) and run Lab 01's client inside. When that passes, write the explainer — you have personally performed what nvidia-container-toolkit automates.
Success Criteria
- You can define "driver" as callbacks + interrupts, and narrate open→ioctl→copy_from_user→validate→copy_to_user (Ch. 1–3)
- You can explain why steady-state GPU submission uses zero syscalls (rings + doorbells + fences) and what the KMD vs UMD each own (Ch. 4)
- You can diagnose the four version-skew signatures and sketch the fleet upgrade choreography (Ch. 6)
- You can build a container from syscalls and state precisely what namespaces, cgroups, and capabilities each contribute (Ch. 7–8)
- You can recite the GPU-into-container anatomy and its debugging ladder, and you've performed it by hand (Ch. 9)
- "There is no GPU cgroup controller" — and you can derive Phase 06 from it
Interview Q&A
Q1: What happens at the OS level when a CUDA app starts and launches a kernel?
A: Init: libcuda.so opens /dev/nvidiactl and /dev/nvidia0, issues a series
of ioctls to create a context — the KMD allocates GPU page tables, a command
ring, and maps the ring plus a doorbell into the process (visible as mmaps in
strace). It also JIT-compiles any PTX lacking a matching SASS (Phase 03). Launch:
the UMD writes the launch command into the mapped ring and taps the doorbell —
two memory writes, no syscall — the GPU front end fetches, executes, and writes a
fence; synchronization waits on that fence (poll or interrupt). That's why
launches cost microseconds and why strace goes quiet after init.
Q2: Why doesn't CUDA_VISIBLE_DEVICES provide isolation?
A: It's an environment variable read by the CUDA library in the untrusted
process — it filters enumeration, nothing more; the process can unset it, and
the device nodes are what they are. Enforcement lives lower: the devices
cgroup/eBPF allow-list controls which nodes can be opened (what
nvidia-container-toolkit configures), and even that only gates access to a
whole device — within a shared GPU there's no kernel-enforced resource split
because no GPU cgroup controller exists. Real isolation: dedicated GPU per
tenant, or MIG hardware partitions, plus process-level (not context-level)
multi-tenancy. The env var is UX; the cgroup is policy; MIG is enforcement.
Q3: A container sees /dev/nvidia0 but cudaInit fails. Debug.
A: Ladder: (1) host sanity — nvidia-smi on the node (driver loaded? GPU
healthy? check dmesg for Xids). (2) Library injection — ldconfig -p | grep libcuda inside the container; missing → toolkit hook not configured for this
runtime; present → compare its version to the host KMD (nvidia-smi driver
version) for skew, the classic "image ships its own libcuda" bug.
(3) Permissions — devices cgroup allow rule, node major/minor match, non-root
user in the right group. (4) The other nodes — nvidia-uvm missing breaks
managed memory only, nvidiactl missing breaks everything. (5) strace the
init and read which exact open/ioctl fails with which errno — it almost always
names the culprit directly.
Q4: Why do GPU drivers split into a kernel module and libcuda.so, and why is the kernel part getting smaller? A: The split follows the privilege/performance line: things requiring kernel mode (page tables, interrupts, PCI config, cross-process arbitration) sit in the KMD; everything per-launch sits in user space for speed (ring submission) and shipability (a library updates with the toolkit; a kernel module update risks the host). The KMD is shrinking because GSP firmware on the GPU's own RISC-V core absorbed initialization and management logic — less GPL-adjacent kernel code (enabling NVIDIA's open kernel modules), faster context ops, and the proprietary logic now lives in firmware+UMD where it's both protected and updatable. Same pattern as NVMe/io_uring/DPDK: kernel for setup and safety, user space for the data path, firmware for vendor secrets.
Q5: Design the ioctl ABI for a simple accelerator. What discipline applies?
A: Fixed-size, explicitly-padded structs (the ABI is forever — version field or
reserved fields for growth); _IOWR macros encoding direction and size;
copy_from_user, then validate every field against per-context state
(handles not pointers, bounds on sizes, quota checks) because user space is
adversarial; return handles, never kernel addresses; per-fd context so
isolation falls out of the file model; errno discipline (-EINVAL vs -ENOMEM vs
-EPERM mean different things to the caller's retry logic). And keep the hot
path out of ioctl entirely — submission belongs in a mapped ring once setup
is done (Lab 01's extension, and every real GPU's design).
Q6 (leadership): Plan a driver upgrade across a 10,000-GPU inference fleet.
A: Inventory first: current driver/toolkit pairs per node pool, and every
container image's CUDA runtime requirement — build the compatibility matrix
before touching anything (driver must satisfy the max toolkit; forward-compat
packages where datacenter GPUs allow). Canary: one node pool, full Phase 10
health suite (Xid monitoring, perf regression benchmarks — driver upgrades have
shipped perf regressions). Choreography per node: cordon → drain via the
scheduler (Phase 06; respect long-running jobs with checkpointing policy) →
upgrade KMD + container toolkit together → reboot (cleanest; hot-unload is
fragile with GPUs) → health-check → uncordon. Progressive rollout with
automatic halt on Xid-rate or perf anomaly. Rollback path tested before
rollout, image-pinned node pools so a rollback is a re-image, not an
archaeology dig. And the boring decisive control: driver versions pinned in
infrastructure-as-code, never apt upgrade'd by hand.
References
- Linux Device Drivers, 3rd ed. (Corbet, Rubini, Kroah-Hartman) — free online; Ch. 1–3, 6 map directly to Lab 01 — https://lwn.net/Kernel/LDD3/
- The kernel's own docs: The Linux Kernel documentation — driver-api, cgroup-v2 — https://docs.kernel.org
- NVIDIA open GPU kernel modules (read a real KMD!) — https://github.com/NVIDIA/open-gpu-kernel-modules
- NVIDIA Container Toolkit architecture + CDI spec — https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/
- NVIDIA CUDA Compatibility (the skew matrix) — https://docs.nvidia.com/deploy/cuda-compatibility/
- NVIDIA Xid errors reference — https://docs.nvidia.com/deploy/xid-errors/
man 2 clone,man 7 namespaces,man 7 cgroups— genuinely excellent- Liz Rice, "Containers from Scratch" (the famous live demo) — https://www.youtube.com/watch?v=8fi7uSYlOdc
- freedesktop.org DRM/amdgpu docs (the open-source GPU driver world) — https://docs.kernel.org/gpu/
- Cross-track: Phase 06 WARMUP (what fills the cgroup hole), security track (kernel/container hardening at depth)
🛸 Hitchhiker's Guide — Phase 04: Linux, Drivers & Containers
Read this if: CUDA works for you but
/dev/nvidia0,nvidia.ko, and "the toolkit hook" are folklore. Field notes; derivations in the WARMUP.
0. The 30-second mental model
your app ─ libcudart ─ libcuda.so (UMD: rings, JIT, contexts) [user space]
──────────────────────── ioctl/mmap on /dev/nvidia* ────────────[boundary]
nvidia.ko (KMD: page tables, channels, interrupts, recovery) [kernel]
GSP firmware (management offload) + silicon [device]
Init = ioctl flurry + ring/doorbell mmaps. Steady state = zero syscalls: write command to mapped ring, tap doorbell, wait on fence. A container is a process with unshared namespaces + cgroup limits; a GPU enters it as device nodes + injected host libs. There is no GPU cgroup controller — that sentence generates Phase 06.
1. Commands that teach
strace -f -e openat,ioctl,mmap python -c "import torch; torch.zeros(1).cuda()"
ls -l /dev/nvidia* # major 195, minors = GPU index
dmesg -T | grep -i -e nvidia -e xid # the kernel's side of the story
lsof /dev/nvidia0 # who's holding the GPU
cat /proc/$(pgrep -f myapp)/maps | grep nvidia # the mapped rings, visible
nvidia-smi -q | head -30 # driver version = max CUDA supported
2. The stack's division of labor
| Layer | Owns | Updates with | Breaks as |
|---|---|---|---|
KMD nvidia.ko | GPU page tables, channels, IRQs, Xid recovery | node image (reboot) | module won't load (DKMS/secure-boot), Xids |
UMD libcuda.so | rings, contexts, PTX JIT, API | driver package (must match KMD) | "driver insufficient for runtime" |
| toolkit/runtime | injecting nodes+libs into containers | host package | container sees no GPU |
| app's CUDA runtime | convenience API | container image | version skew vs driver |
Rule that prevents half of all incidents: the container never ships libcuda.so; the host injects it. Images pinning their own = guaranteed eventual skew.
3. Container reality check
- Namespaces = visibility (mount/PID/net/UTS/user). cgroups = resources (cpu/memory/io + devices allow-list). Capabilities/seccomp = privilege. A "container" is just a process wearing all three.
- GPU "limits" in k8s are whole-device allocation, not enforcement. Fractions need MPS (cooperative), MIG (hardware, real), or time-slicing (none) — Phase 06.
CUDA_VISIBLE_DEVICESis UX, the devices cgroup is policy, MIG is enforcement. Repeat before promising tenant isolation to a customer.
4. Failure signatures → causes (the 2 a.m. table)
| Symptom | Likely cause |
|---|---|
driver insufficient for runtime version | toolkit newer than driver — upgrade driver or use fwd-compat package |
| works on node A, not node B | fleet driver skew — inventory + pin in IaC |
| module won't load after kernel update | DKMS rebuild failed or unsigned module + secure boot |
Xid 79 in dmesg | GPU fell off the bus — hardware/power/PCIe; drain the node |
| container: nodes present, init fails | injected lib missing/mismatched — check ldconfig -p vs host |
can't rmmod nvidia | something holds the fd — lsof /dev/nvidia* |
| pinned-memory alloc fails at scale | unpageable RAM exhausted — audit cudaMallocHost users |
5. War stories
- The image that shipped libcuda: passed CI (lucky match), died in prod region with older drivers. Policy since: images are scanned for driver libs; injection only.
- The quiet strace: engineer "proved" CUDA was hung because strace showed nothing — it was decoding correctly in the mmap'd ring the whole time. Steady state is silent by design.
- The OOM that wasn't: container kept dying at 100 MB despite "no limit" —
inherited
memory.maxfrom a parent cgroup nobody remembered.cat /proc/self/cgroupfirst, always. - The 40-minute driver upgrade that took a quarter: nobody had the image-pinned rollback path; one bad canary turned into fleet archaeology. WARMUP Q6 is the plan that avoids this.
6. Exit bar
You've written a driver (small, but real), built a container from syscalls, injected your own device into your own container, and can run the debugging ladder cold. The stack is glass. Phase 05 climbs back up one level: the runtime systems (allocators, stream schedulers) that live on top of this driver and under every inference engine.
Lab 01 — fakegpu: A GPU-Shaped Linux Character Device Driver
Phase: 04 — Linux & Drivers | Difficulty: ⭐⭐⭐⭐☆ | Time: 5–8 hours Language: C (kernel module + userspace client) | Hardware: Linux VM (snapshot it first!)
Concept primer:
../WARMUP.mdCh. 1–4. ⚠️ Kernel code runs with no safety net — use a VM (Lima/Multipass/UTM/WSL2/cloud), snapshot beforeinsmod. A panic here is a lesson, not a disaster.
Run
sudo apt install build-essential linux-headers-$(uname -r) # Debian/Ubuntu
make # builds fakegpu.ko (kernel) and client (user)
sudo insmod fakegpu.ko
dmesg | tail -3 # "fakegpu: loaded, /dev/fakegpu ready"
sudo ./client # exercises the full ioctl ABI; prints PASS/FAIL
strace -e ioctl,openat ./client 2>&1 | head -30 # compare to a real CUDA trace
sudo rmmod fakegpu
0. The mission
Implement the architectural skeleton every GPU driver shares, in ~300 honest lines:
| fakegpu concept | Real GPU equivalent |
|---|---|
per-open() context struct | CUDA context (per-process GPU state) |
ALLOC/FREE ioctls → handles | cudaMalloc → device pointers (via KMD page tables) |
SUBMIT ioctl → job queue | command-buffer submission (real HW: mapped ring + doorbell) |
WAIT ioctl → completed seqno | fence wait (cudaEventSynchronize) |
| per-context byte quota | per-context VRAM accounting |
INFO ioctl | cudaGetDeviceProperties |
The client is the test suite: it asserts context isolation (two fds can't see
each other's handles), quota enforcement (over-alloc returns ENOSPC), seqno
ordering, and input validation (bad handle → EINVAL, oversized request →
rejected — the adversarial-userspace discipline from WARMUP Ch. 3).
1. Reading order (fakegpu.c)
- The ABI header (
fakegpu_ioctl.h) — shared by kernel and client; note_IOWRencodings and fixed-size structs. ABI files are forever; this one is written like it. fakegpu_open/release— per-fd context allocation. This is the pattern: everything else hangs offfile->private_data.fakegpu_ioctl— the dispatcher:copy_from_user→ validate → act →copy_to_user. Count the validation lines vs the "work" lines (the ratio is the lesson).- The job model —
SUBMITenqueues and immediately completes jobs (incrementingcompleted_seq). Real hardware completes asynchronously; the extension adds atimer_listto simulate that.
2. The strace comparison (step 3 of the lab)
Your client:
openat(AT_FDCWD, "/dev/fakegpu", O_RDWR) = 3
ioctl(3, FAKEGPU_INFO, ...) = 0
ioctl(3, FAKEGPU_ALLOC, {size=1048576, ...}) = 0
ioctl(3, FAKEGPU_SUBMIT, ...) = 0
ioctl(3, FAKEGPU_WAIT, ...) = 0
A real CUDA init (from WARMUP Ch. 3): same shape — open device, info/version
ioctls, allocation ioctls, then mmap calls you don't have (the ring/doorbell —
because real drivers move submission out of ioctl; your extension 3 closes that
gap). Write the side-by-side into TRACE-NOTES.md — that artifact is your
"technical credibility" exhibit for this phase.
3. Extensions
- Quota tightening: per-context and global device quota; correct errno
for each (
ENOSPCvsENOMEM). Add a/sys/module/fakegpu/or sysfs stats attribute (your Phase 10 metrics on-ramp). - Async completion: a
timer_listcompletes jobs 10 ms after submit;WAITblocks on await_queue_head_tuntilcompleted_seq >= target. You've just implemented a fence. - The mmap ring (hard, the real pattern):
fakegpu_mmapexposes a page containingcompleted_seq; client polls it with zero syscalls in steady state. Measure waits/sec: ioctl-WAIT vs mmap-poll (~100× — the entire reason GPU submission works the way it does). - Adversarial client: write
evil.c— bad handles, huge sizes, ioctls on a closed-and-reopened fd, two threads racing SUBMIT. Every case must fail cleanly. (Phase 11 will call this "attack surface review"; here it's just good driver hygiene.)
4. Common pitfalls
- Dereferencing the user pointer instead of
copy_from_user— works in your VM (same address space mapping luck), then oopses elsewhere. The compiler__userannotation + sparse exists for this. - Forgetting
mutex_lockaround context state — two threads on one fd corrupt the buffer list; the racing-SUBMIT test in extension 4 catches it. - Leaking contexts on
release—rmmodthendmesgshows the leak warning the module prints; the kernel's own kmemleak would find it in real review. - Building against the wrong headers (
uname -rmismatch after a kernel update) —insmod: invalid module format. This is the DKMS lesson from WARMUP Ch. 6, self-inflicted once for learning.
5. What this lab proves about you
You can read (and write) the layer most GPU engineers only gesture at: when a
fleet incident says "Xid 31, context poisoned, ioctl returned EIO," every word
of that sentence now maps to code you've written. And nvidia's
open-gpu-kernel-modules repo is now readable to you — start with
kernel-open/nvidia/nv.c and recognize the shapes.
Lab 02 — Container From Scratch + GPU Passthrough Anatomy
Phase: 04 — Linux & Containers | Difficulty: ⭐⭐⭐⭐☆ | Time: 4–6 hours Language: C + shell | Hardware: Linux VM with root (same VM as Lab 01)
Concept primer:
../WARMUP.mdCh. 7–9.
Run
make # builds mininer + hog
sudo ./mkrootfs.sh # builds ./rootfs from busybox (one download)
sudo ./mininer run /bin/sh # you are now PID 1 in an isolated container
# inside: ps / hostname / ls / — observe isolation
exit
sudo ./mininer run-limited 100 /bin/sh # same, with memory.max=100M
# inside: /hog 200 — allocates 200 MB -> OOM-killed at ~100 MB
0. The mission
Build the smallest honest container runtime — then put a "GPU" inside it by
hand, so that nvidia-container-toolkit becomes a thing you could have written.
mininer.c (~200 lines) does, in order:
clone(CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS | SIGCHLD)— new hostname, PID, and mount namespaces for the child.- In the child:
sethostname("minibox"); make mounts private (MS_REC|MS_PRIVATE— so our mounts don't leak to the host);chroot("./rootfs")+chdir("/")(honest note: real runtimes usepivot_root, which unmounts the old root rather than hiding it — the extension asks you to upgrade); mount a fresh/proc(sopssees only the namespace). - (run-limited) create
/sys/fs/cgroup/mininer, writememory.max, add the child PID tocgroup.procs. execvthe requested command.
That's a container: namespaces (visibility) + cgroups (resources) + a rootfs (filesystem). Docker adds images, networking, and an API — not different physics.
1. The observation checklist (do these inside the container)
| Command | What you should see | Which mechanism |
|---|---|---|
ps (busybox reads /proc) | PIDs 1–2 only | PID ns + fresh /proc mount |
hostname | minibox | UTS ns |
ls / | busybox rootfs, not your host / | mount ns + chroot |
/hog 200 under run-limited 100 | "Killed" near 100 MB; host unaffected | cgroup v2 memory.max |
echo $$ | 1 — you are init; mind your signals | PID ns |
On the host, in another terminal: ps aux | grep sh still shows the
container shell (as a normal PID) — namespaces virtualize the container's
view, not the host's. Also cat /sys/fs/cgroup/mininer/memory.current while
the hog runs.
2. The GPU passthrough synthesis (the point of the whole phase)
With Lab 01's module loaded on the host:
# 1. Device node into the container's rootfs (what the toolkit's hook does):
sudo mknod ./rootfs/dev/fakegpu c $(grep fakegpu /proc/misc | awk '{print $1}') \
$(grep fakegpu /proc/misc | awk '{print $1}' > /dev/null; \
ls -l /dev/fakegpu | awk '{print $6}' | tr -d ',')
# (or simply: sudo cp -a /dev/fakegpu ./rootfs/dev/ — preserves maj:min)
sudo chmod 666 ./rootfs/dev/fakegpu
# 2. The "driver library injection" analog: copy the client in
sudo cp ../lab-01-fakegpu-driver/client ./rootfs/bin/gpuclient
# 3. Run it inside:
sudo ./mininer run /bin/gpuclient # ALL CHECKS PASSED — inside a container
Now write GPU-IN-CONTAINER.md (one page): map what you just did to the three
requirements of WARMUP Ch. 9 (device node + cgroup allowance + injected
userspace libs; note that our mininer doesn't set up a device cgroup — add it
as the extension and show /dev/fakegpu works while a non-allow-listed node
fails). Close with the debugging ladder. This document is the deliverable —
it's the team-training artifact the phase README promises.
3. Extensions
- pivot_root upgrade: replace chroot with
pivot_root+umount2(old, MNT_DETACH); explain in a comment why chroot alone is escapable (classicchrootjailbreak: keep an fd to the outside,fchdir). - Devices cgroup: attach a cgroup-v2 eBPF device filter (or use v1
devices.allowif available) permitting only/dev/fakegpuand/dev/null; demonstratemknod+ open of anything else failing. - Network namespace:
CLONE_NEWNET+ a veth pair (ip link add ... netns) — ping from container to host. - User namespace:
CLONE_NEWUSERwith uid_map so the container's root is unprivileged outside — then re-run the GPU test and document what breaks (device access vs UID mapping is exactly the rootless-container GPU pain point in real deployments). - Compare with runc:
strace -f -e unshare,clone,mount,pivot_root runc runon a real container; diff againststrace -f ./mininer run.
4. Common pitfalls
- Forgetting
MS_PRIVATE— your container's mounts propagate to the host (systemd makes/shared by default); the host's/procgets shadowed. Symptom: weird hostpsafter exit. TheMS_REC|MS_PRIVATEremount is non-negotiable. - Mounting /proc before the PID namespace exists —
psshows host processes; the fresh/procmust be mounted by the child, after clone. - cgroup v1 vs v2 confusion — this lab assumes v2 (
/sys/fs/cgroupwithmemory.max). Checkmount | grep cgroup2; hybrid hosts need the v2 path. - Running on your laptop instead of the VM — nothing here should damage a host, but you're root, juggling mounts; the VM habit from Lab 01 stands.
5. What this lab proves about you
You can state — and demonstrate, with code you wrote — exactly what a container is, what GPU injection consists of, and where the isolation boundaries actually sit. When a customer asks "how is tenant A isolated from tenant B on shared nodes," your answer comes from mechanism, not marketing. Phase 06 builds the scheduling layer on top of this; Phase 11 hardens it.
Phase 05 — Runtime Systems: Allocators, Streams, and Async Execution
Difficulty: ⭐⭐⭐⭐☆ | Estimated Time: 2 weeks Roles supported: Head of Engineering [GPU], Runtime Systems Engineer, ML Framework Engineer Hardware needed: none — both labs are CPU-only models of GPU runtime components (Rust + C++)
Why This Phase Exists
The JD asks for "runtime systems programming" by name. Between the driver (Phase 04) and the serving engine (Phase 07) lives the runtime: the layer that decides where memory comes from and when work runs. Every ML framework ships one (PyTorch's caching allocator, CUDA's stream machinery, TensorRT's execution contexts), and every inference platform's worst incidents — fragmentation OOMs at 60% utilization, deadlocked streams, mysterious sync stalls — happen here.
You will build the two canonical components yourself: a caching memory allocator in Rust (the PyTorch design: size-binned free lists, block splitting, stream-aware reuse) and a stream/event scheduler in C++ (dependency DAGs, ordered queues, fences). Both are faithful CPU-side models of the GPU originals, with tests that demonstrate the failure modes you'll spend your career preventing.
Concepts
- Why
cudaMalloc/cudaFreeare slow (device synchronization!) and fragmenting — the motivation for every framework allocator - Allocator zoology: bump, buddy, slab, size-binned free lists; what malloc/jemalloc do vs what GPU pools need
- The PyTorch caching allocator design: bins, block splitting, rounding,
empty_cache, fragmentation vs reserved-vs-allocated metrics, expandable segments - Fragmentation: internal vs external; why long-running serving processes OOM at 60% "utilization"; defragmentation options (none, really — and what that implies for design)
- Stream semantics: ordered queues, default-stream legacy behavior, events as cross-stream edges (
cudaStreamWaitEvent) - The dependency DAG: how frameworks order kernels, copies, and frees; stream-ordered allocation (
cudaMallocAsync) and why "free" is an event in a stream, not a moment in time - Use-after-free across streams — the classic GPU runtime bug class and how stream-aware allocators prevent it
- CUDA Graphs: amortizing the ~5–10 µs launch cost; capture/replay semantics and their restrictions (why serving engines capture decode steps)
- Pinned-memory pools and H2D/D2H staging pipelines (Phase 02 Ch. 9, productionized)
Labs
Lab 01 — Caching GPU Allocator (Rust)
| Field | Value |
|---|---|
| Goal | Implement the PyTorch-style caching allocator: size bins, splitting, stream-tagged free lists — and demonstrate (then fix) fragmentation OOM. |
| Concepts | Free lists, binning, splitting/coalescing, stream-aware reuse, fragmentation metrics. |
| Steps | 1) cargo test — all green. 2) Read src/lib.rs against the WARMUP Ch. 3–5. 3) Run cargo run --release --bin fragdemo — watch the naive allocator OOM at ~55% utilization while the caching one survives. 4) Extensions: coalescing of adjacent free blocks, empty_cache(), an allocation-trace replayer. |
| Stack | Rust (no unsafe, no external crates) |
| Output | A library with 12+ passing tests + the fragmentation demo with its utilization table. |
| How to Test | cargo test: correctness (no double-handout, splitting math, stream safety) + the fragdemo's OOM-point comparison. |
| Talking Points | Why GPU pools can't just use jemalloc (no paging, sync-on-free, stream semantics); reserved vs allocated in nvidia-smi vs framework metrics (the #1 "is it leaking?" confusion); why a serving engine wants per-stream pools. |
| Resume Bullet | "Implemented a PyTorch-style caching GPU allocator in Rust (size-binned free lists, block splitting, stream-aware reuse); demonstrated and mitigated fragmentation OOM, raising survivable utilization from 55% to 92% on adversarial traces." |
| Extensions | Best-fit vs first-fit measurement; expandable-segments mode; export Prometheus-style metrics (Phase 10 tie-in). |
Lab 02 — Stream & Event Scheduler (C++)
| Field | Value |
|---|---|
| Goal | Build the CUDA stream/event model in C++17: ordered streams, cross-stream event dependencies, a worker-driven executor — then use it to demonstrate overlap, false serialization, and a use-after-free caught by stream-ordered lifetime. |
| Concepts | Ordered queues, events/fences, DAG scheduling, stream-ordered deallocation, deadlock from circular waits. |
| Steps | 1) make && ./scheduler_test — all scenarios PASS. 2) Read scheduler.hpp against WARMUP Ch. 6–8. 3) Study the three scenario tests: overlap speedup, default-stream serialization, the use-after-free that the stream-ordered free prevents. 4) Extensions: graph capture/replay, priority streams, a deadlock detector. |
| Stack | C++17, std::thread (no external deps) |
| Output | A header-lib + scenario suite proving overlap (~2× on the pipeline test) and lifetime safety. |
| How to Test | Built-in scenario asserts: pipelined execution beats serial by ≥1.7×; the unsafe-free scenario corrupts (detected) while stream-ordered free never does; circular event waits are detected. |
| Talking Points | Why "free" must be an event in stream order; what cudaMallocAsync changed for frameworks; why serving engines capture decode into CUDA Graphs (launch overhead amortization — numbers from Phase 02). |
| Resume Bullet | "Built a CUDA-semantics stream/event scheduler in C++17 (ordered queues, cross-stream fences, stream-ordered deallocation); scenario suite demonstrates 2× pipeline overlap and elimination of cross-stream use-after-free." |
| Extensions | Capture/replay ("mini CUDA Graphs") with measured per-launch overhead reduction; integrate Lab 01's allocator for fully stream-ordered alloc/free. |
Deliverables Checklist
- Allocator: all tests pass; fragdemo table reproduced and explained
- You can sketch the bin/split/cache design and the reserved-vs-allocated distinction from memory
- Scheduler: all scenarios pass; you can explain each failure mode it demonstrates
- One extension implemented per lab
- One page: "what our serving platform requires from its runtime layer" (feeds Phase 12 capstone)
Interview Relevance
- "Why does PyTorch have its own allocator? Walk me through its design."
- "nvidia-smi says 78 GB used but the framework reports 51 GB. Explain."
- "Your serving process OOMs after 3 days at constant load. Hypotheses?" (fragmentation, pinned leak, cache growth)
- "Design stream-ordered deallocation. What goes wrong without it?"
- "When do CUDA Graphs help, and what can't they capture?"
Warmup Guide — Runtime Systems: Allocators, Streams, Async Execution
Zero-to-expert primer for Phase 05. Assumes Phases 02 and 04 (the CUDA memory API and the driver beneath it). No allocator or concurrency background assumed. By the end you can design the runtime layer of an inference platform — and debug the ones you didn't design.
Table of Contents
- Chapter 1: The Runtime Layer — What Sits Between Driver and Engine
- Chapter 2: Why cudaMalloc Is Slow and Fragmenting
- Chapter 3: Allocator Zoology — Bump, Buddy, Slab, Bins
- Chapter 4: The Caching Allocator — PyTorch's Design, Dissected
- Chapter 5: Fragmentation — The Silent Fleet Killer
- Chapter 6: Streams and Events — The Ordering Algebra
- Chapter 7: Stream-Ordered Lifetime — Why "Free" Is an Event
- Chapter 8: CUDA Graphs — Amortizing Launch Overhead
- Chapter 9: Pinned Pools and Staging Pipelines
- Lab Walkthrough Guidance
- Success Criteria
- Interview Q&A
- References
Chapter 1: The Runtime Layer — What Sits Between Driver and Engine
The stack so far: silicon (Phase 01) ← kernels (02) ← compiler (03) ← driver (04). This phase is the layer that every framework and serving engine builds on top of the driver and underneath the model code:
model / engine code (vLLM, PyTorch, TensorRT) Phase 07
─────────────────────────────────────────────
RUNTIME: memory pools, stream scheduling, ← THIS PHASE
graphs, staging pipelines
─────────────────────────────────────────────
CUDA runtime/driver API Phase 02/04
Why it exists at all: the driver's primitives (cudaMalloc, raw streams) have
costs and semantics that are unacceptable to call in a hot loop. The runtime
layer turns them into amortized, policy-bearing services. The two services that
matter most — and that you'll build — are the allocator (Lab 01) and the
execution scheduler (Lab 02).
Production significance: when this layer is wrong you get the worst class of incidents — not crashes, but slow decay: fragmentation OOM on day 3, throughput sag from false serialization, once-a-week corruption from a cross-stream lifetime bug. Nothing in a stack trace points here; you find these only if you know the layer exists.
Chapter 2: Why cudaMalloc Is Slow and Fragmenting
Three facts about the raw allocation API, each with a design consequence:
cudaFreesynchronizes the device (historically; andcudaMalloccan too). The driver must be sure no in-flight kernel still uses the region — lacking fine-grained tracking, it waits for everything. One straycudaFreeper request = your entire pipeline gains a global barrier. → Consequence: never call it in steady state; allocate from a pool.- It's a driver round-trip (ioctl, page-table updates — Phase 04 Ch. 4): tens of µs vs ~100 ns for a pool hit. At thousands of tensor allocations per step, that's the difference between negligible and dominant. → Consequence: caching — keep freed blocks and reuse them.
- There is no paging and no compaction. Host malloc lives on virtual memory: fragmented physical pages don't matter because the VM layer remaps. GPU allocations are (effectively) physically contiguous reservations in a fixed-size HBM; nothing remaps behind your back, and you cannot move a live block (kernels hold raw pointers). → Consequence: fragmentation is permanent damage within a process lifetime; the allocator's only weapons are placement policy and shape discipline (Ch. 5).
(cudaMallocAsync, CUDA 11.2+, moved a pool into the driver — same design,
different owner; the VMM API (cuMemCreate + address reservation) added
remapping tricks that "expandable segments" exploit — Ch. 4.)
Chapter 3: Allocator Zoology — Bump, Buddy, Slab, Bins
The four designs every systems engineer should hold (Lab 01 implements the fourth):
- Bump allocator: a pointer that only moves forward; free is a no-op (or whole-arena reset). O(1), zero fragmentation within an arena, but only fits phase-structured lifetimes. GPU relevance: activation memory in inference is exactly phase-structured — per-step arenas are a legitimate design (and what "static planning" compilers like TensorRT effectively do).
- Buddy allocator: powers-of-two blocks, split on demand, coalesce with your "buddy" on free. O(log n), bounded external fragmentation, up to ~50% internal waste. The classic kernel-page-allocator design.
- Slab allocator: per-size-class object caches. Brilliant for fixed-size objects — GPU relevance: KV-cache blocks (Phase 07) are fixed-size by design partly so a slab-style pool can manage them trivially.
- Size-binned free lists with splitting (the caching allocator): free blocks indexed by size class; allocation finds the smallest fitting block, splits the remainder back into the lists. This is malloc's general shape and PyTorch's choice — flexible sizes, good reuse, but external fragmentation is back (Ch. 5).
The meta-lesson: allocator design = exploiting what you know about lifetimes and sizes. The more structure you can impose on the workload (fixed block sizes, phase lifetimes, stream affinity), the better the allocator can be. This is why Phase 07's PagedAttention is best understood as an allocator redesign, not an attention algorithm.
Chapter 4: The Caching Allocator — PyTorch's Design, Dissected
The reference design (Lab 01 is a faithful Rust model):
- Two regimes by size: small (<1 MB) and large allocations get separate pools/bins (mixing tiny and huge requests is a fragmentation accelerant).
- Rounding: sizes round up to multiples (512 B; larger granularity for big blocks) — bounded internal waste purchases massive reuse improvement, because "1000 different sizes" collapses to "30 size classes."
- Allocation path: exact-ish bin hit → reuse (fast path, ~100 ns). Miss →
take a larger cached block and split it (remainder returns to bins).
Still nothing →
cudaMalloc. OOM → free all cached-unsplit blocks, retry (the "sync-and-retry" you see as a mysterious stall right before a PyTorch OOM error). - Free path: blocks go back to the bins — the driver never sees the
free (this is why
nvidia-smishows "used" memory that the framework considers free: reserved vs allocated, the #1 memory-confusion in every ML org — Q2 below). - Stream tagging: every block remembers the stream it was used on; reuse on a different stream requires an event dependency (or is simply not done) — preventing the cross-stream use-after-free (Ch. 7).
expandable_segments(modern): uses the CUDA VMM API to grow a virtual reservation with physical pages on demand — restoring a little of the "remapping" power CPUs get for free, specifically to fight fragmentation.
Misconception to kill: "the allocator caches because cudaMalloc is slow."
Half right — the deeper reason is cudaFree's synchronization and the
fragmentation ratchet. Even with a free cudaMalloc you'd still want the pool.
Chapter 5: Fragmentation — The Silent Fleet Killer
Internal fragmentation: waste inside blocks (rounding). Bounded, boring, fine.
External fragmentation: free memory exists but in pieces too small for the
request. The serving-fleet scenario (Lab 01's fragdemo reproduces it):
- Day 0: requests of mixed sizes allocate/free happily; memory is a clean checkerboard.
- Day 2: long-lived allocations (a resident model, a stuck request's KV) sit between short-lived ones. Frees leave holes of assorted sizes.
- Day 3: a 2 GB request arrives. Free memory totals 9 GB. No hole exceeds 1.1 GB. OOM at 60% utilization. Restart fixes it (fresh address space) — which is why "we restart the pods nightly" is a real, embarrassing, industry-wide mitigation.
Because compaction is impossible (live raw pointers), the defenses are all preventive:
- Shape discipline: bucket request sizes (pad to standard lengths) — fewer distinct sizes = checkerboard stays clean. (Same trick as batching same-length sequences — Phase 01 Ch. 4's divergence logic, reborn.)
- Pool segregation: small/large pools; per-purpose pools (weights vs activations vs KV); per-stream pools.
- Fixed-size paging for the dominant consumer: if one consumer (KV cache) dominates and grows unpredictably, give it fixed-size blocks + an indirection table = external fragmentation eliminated for that consumer by design. That sentence is PagedAttention (Phase 07), derived from first principles.
- Metrics that see it coming (Phase 10): track reserved, allocated, and largest-free-block — the third is the early-warning signal nobody exports by default.
Chapter 6: Streams and Events — The Ordering Algebra
The execution model that Lab 02 implements, reduced to algebra:
- A stream is a FIFO: operations issued to the same stream execute in issue order. Within a stream you never need explicit sync.
- Different streams have no order — unless you create one.
- An event is a marker recorded into a stream;
streamWaitEvent(s2, e)makes s2 pause until e fires. Events are the edges of the DAG; streams are convenient paths through it. cudaStreamSynchronize/cudaDeviceSynchronizejoin the CPU to the DAG — blunt instruments; in engine code, almost every device-wide sync is a bug or a missed event edge.- The legacy default stream synchronizes with all other streams (a global serializer hiding in every naive codebase — Lab 02 scenario 2 measures the damage; the fix is per-thread default streams or explicit streams everywhere, Phase 02 Ch. 9).
Mental model for review: draw the intended DAG, then check the code creates exactly those edges — extra edges = lost overlap (performance bug); missing edges = race (correctness bug). Most stream code review reduces to this.
Chapter 7: Stream-Ordered Lifetime — Why "Free" Is an Event
The bug class that motivates stream-aware allocators (Lab 02 scenario 3):
stream A: kernel K reads buffer B (still running...)
host: free(B) (CPU is ahead of the GPU!)
allocator: hands B's memory to stream C -> C writes over K's input. Corruption.
The CPU runs ahead of the GPU (async launches, Phase 02 Ch. 1) — so a
host-side free() happens-before the GPU work that uses the buffer. The fix
is to make deallocation an operation in the stream: the block returns to
the pool only when the stream reaches the free point (an event). Reuse on
another stream inserts a wait-event edge first. cudaMallocAsync/
cudaFreeAsync are exactly this, productized; PyTorch's stream tagging
(Ch. 4) is the same idea retrofitted.
The leadership phrasing: lifetime on a GPU is defined by stream position, not by wall-clock or host program order. Any API in your platform that forgets this will eventually corrupt a customer's tokens — and it will be unreproducible, because it needs a scheduling coincidence. (This is the bug class behind a good fraction of "model produced garbage once" tickets.)
Chapter 8: CUDA Graphs — Amortizing Launch Overhead
The numbers (Phase 02): each kernel launch costs ~5–10 µs of CPU-side work. An LLM decode step is hundreds of short kernels; at high batch, GPU work per kernel can be tens of µs — the CPU becomes the bottleneck issuing work, and the GPU starves between kernels.
CUDA Graphs: record the whole sequence of launches (+ their dependency
edges) once — capture — then replay the graph with one API call.
Launch cost amortizes from n × 8 µs to ~1 × 10 µs + tiny per-node cost.
Serving engines (vLLM et al.) capture the decode step per batch-size bucket;
it's worth 10–30% at small batch and is the standard answer to "why is my
decode CPU-bound?"
Restrictions (the part people learn the hard way): the graph is static —
shapes, pointers, and control flow are baked at capture; capture forbids
synchronous APIs (cudaMalloc!) inside the captured region (stream-ordered
allocation exists partly for graphs); dynamic batch = capture per bucket or
use graph update APIs. Lab 02's capture/replay extension makes all of this
tactile in 100 lines.
Chapter 9: Pinned Pools and Staging Pipelines
The host↔device data path, productized (composing Phase 02 Ch. 5/9 with this phase's pooling):
- Pinned memory is mandatory for true async copies but expensive to allocate and steals unpageable RAM → pool it like device memory (a pinned staging-buffer pool is ~50 lines on top of Lab 01's design).
- The standard pipeline: chunk → H2D copy on copy-stream → kernel on compute stream (event edge) → D2H on copy-stream (event edge) — double-buffered so chunk N+1's copy overlaps chunk N's compute. GPUs have separate copy engines precisely so this overlap is real.
- Serving-specific instances you'll meet in Phase 07: weight loading at startup (pinned + multi-stream = minutes to seconds), KV-cache swap to host (the 50× bandwidth cliff makes the policy — what to swap when — the hard part, not the mechanism).
Lab Walkthrough Guidance
Order: Lab 01 → Lab 02 (the scheduler's stream-ordered-free scenario reuses allocator concepts).
Lab 01 (allocator, Rust):
cargo testgreen first; then readsrc/lib.rswith Ch. 3–4 open. The types are small:Block,BinKey,CachingAllocator.- Trace one allocation by hand: 1.2 MB request → which bin, which split,
what's left where. Write the trace in comments; the test
test_split_accountingchecks your understanding. - Run
cargo run --release --bin fragdemo: the naive-vs-caching utilization table is the artifact. Explain the gap using Ch. 5's checkerboard story. - Extensions in order: coalescing (hard correctness) →
empty_cache→ trace replayer (turns the allocator into a measurable system — feed it your own traces).
Lab 02 (scheduler, C++):
make && ./scheduler_test; then readscheduler.hppwith Ch. 6–7 open —Streamis a worker+FIFO,Eventis a counter+condvar, that's it.- Scenario 1 (overlap): predict the speedup from the DAG before reading the assert. Scenario 2 (legacy default stream): observe the global serializer. Scenario 3 (lifetime): the unsafe path corrupts; the stream-ordered path can't — connect each line to Ch. 7's diagram.
- Extension: capture/replay. Measure issue-cost per op before/after — your own numbers for the Ch. 8 argument.
Success Criteria
- You can give the three reasons raw cudaMalloc/Free are unusable in steady state, with the design consequence of each (Ch. 2)
- You can sketch the caching allocator (bins, split, stream tags, sync-and-retry) and explain reserved vs allocated (Ch. 4)
- You can tell the day-3 fragmentation story with numbers and name the four preventive defenses (Ch. 5)
- You can review stream code by drawing its DAG and checking edges (Ch. 6)
- You can explain why free must be stream-ordered and what bug class ignores it (Ch. 7)
- You know what CUDA Graphs buy, their restrictions, and when serving engines use them (Ch. 8)
- Both labs pass; one extension each implemented
Interview Q&A
Q1: Why does PyTorch implement its own allocator instead of calling cudaMalloc?
A: Three driver-level facts force it: cudaFree device-synchronizes (a global
barrier per free), cudaMalloc is a driver round-trip (tens of µs vs ~100 ns
pool hit), and HBM has no paging/compaction so fragmentation is permanent. The
caching allocator answers all three: it never returns memory to the driver in
steady state (frees go to size-binned lists), reuses blocks in ~O(1), rounds
sizes to bound fragmentation, splits larger cached blocks for fit, tags blocks
by stream to keep reuse safe under async execution, and on apparent-OOM frees
its caches and retries. cudaMallocAsync later moved the same design into the
driver as stream-ordered pools.
Q2: nvidia-smi shows 78 GB used; the framework says 51 GB. Who's lying?
A: Nobody — they measure different lines of the same ledger. nvidia-smi sees
what the driver handed the process: reserved memory, which includes
everything the caching allocator is holding in its free lists. The framework's
51 GB is allocated — blocks currently handed to live tensors. The 27 GB
gap is cache (plus context/fragmentation overhead). It's reclaimable via
empty_cache() (at a performance cost) and is not a leak. The follow-up
metric a platform should actually watch is largest free block — the
fragmentation early-warning — and allocated-vs-reserved divergence trend,
which distinguishes cache growth from a real leak.
Q3: A serving process OOMs after 3 days at steady load. Hypotheses, in order? A: (1) External fragmentation — mixed request sizes + long-lived allocations checkerboarding HBM until no hole fits a big request; check largest-free-block trend and whether OOM happens at <70% allocated; mitigate with size bucketing, pool segregation, paged KV (or, honestly, scheduled restarts while you fix it). (2) Genuine slow leak — allocated (not just reserved) trending up: a cache without eviction, per-request tensors retained by a logger, growing CUDA Graph pools per shape bucket. (3) Pinned host memory exhaustion masquerading as device OOM in mixed stacks. (4) KV-cache admission policy allowing pathological long-context requests to accrete. The diagnostic discipline: reserved vs allocated vs largest-block, per pool, on a dashboard before day 3 (Phase 10).
Q4: Two streams, one buffer: kernel on stream A reads it, host frees it,
stream C gets it and writes. How do you make this class of bug impossible?
A: Make deallocation stream-ordered: free() doesn't return the block to the
pool — it records an event at the free point in A; the block becomes reusable
only when that event completes. Reuse on a different stream additionally
inserts streamWaitEvent so C's first use happens-after A's last. This is
exactly cudaFreeAsync semantics / PyTorch's stream tagging. The general
principle: on an async device, lifetime is a position in the dependency DAG,
not a moment in host time — any API that takes raw "free now" must be wrapped
before it touches engine code. Detection for the residue: compute-sanitizer
in CI and a debug allocator mode that poisons freed blocks.
Q5: When do CUDA Graphs help, and what are the sharp edges? A: They help when launch overhead is material: many short kernels per iteration with a stable structure — the LLM decode step is the canonical case (hundreds of kernels, µs-scale each); capture per batch-bucket and replay, typically 10–30% at small batch and a fix for CPU-bound issuance. Sharp edges: the graph freezes shapes, addresses, and topology (dynamic control flow can't be captured); no synchronous calls (cudaMalloc) inside capture — you need stream-ordered allocation; per-shape graph pools consume memory (a real OOM contributor at high bucket counts); and debugging a captured region is harder (prints/syncs change behavior). So: graphs for the steady-state inner loop, eager mode for everything else.
Q6 (leadership): What runtime-layer guarantees would you write into your
platform's internal API contract?
A: (1) No driver alloc/free in steady state — all hot-path memory from
pools; CI fails on cudaMalloc in request paths. (2) Stream-ordered lifetime
— the only free is an event-ordered free; raw frees don't exist in the API.
(3) Pool segregation + metrics — weights/activations/KV in separate pools,
each exporting reserved/allocated/largest-block (the fragmentation dashboard).
(4) No device-wide syncs in engine code — event edges only; a lint catches
deviceSynchronize. (5) Graph-compatible inner loops — steady-state paths
must be capturable (no syncs, no allocs), which doubles as a cleanliness
forcing-function. (6) Determinism escape hatch — a debug mode with
serialized streams and poisoned frees for reproducing the "garbage tokens
once" class. Each guarantee exists because its absence is a specific incident
I can name — that's how runtime contracts should be justified.
References
- PyTorch CUDA Caching Allocator notes (the design doc) — https://pytorch.org/docs/stable/notes/cuda.html#cuda-memory-management
- PyTorch
CUDACachingAllocator.cpp(read the real one after the lab) — https://github.com/pytorch/pytorch/blob/main/c10/cuda/CUDACachingAllocator.cpp - NVIDIA, "Using the NVIDIA CUDA Stream-Ordered Memory Allocator" (cudaMallocAsync) — https://developer.nvidia.com/blog/using-cuda-stream-ordered-memory-allocator-part-1/
- NVIDIA, CUDA Graphs section of the Programming Guide + "Getting Started with CUDA Graphs" — https://developer.nvidia.com/blog/cuda-graphs/
- NVIDIA VMM API ("Introducing Low-Level GPU Virtual Memory Management") — https://developer.nvidia.com/blog/introducing-low-level-gpu-virtual-memory-management/
- Wilson et al., "Dynamic Storage Allocation: A Survey and Critical Review" (1995) — the allocator-design bible
- Bonwick, "The Slab Allocator" (USENIX 1994)
- vLLM source:
cuda_graphusage and block manager — https://github.com/vllm-project/vllm - Cross-track: Phase 07 WARMUP (PagedAttention as allocator design), LLM Inference track Phase 09
🛸 Hitchhiker's Guide — Phase 05: Runtime Systems
Read this if: you call
torch.cuda.empty_cache()superstitiously, you've never asked whynvidia-smiand your framework disagree, or "stream-ordered deallocation" sounds like jargon. Field notes; derivations in the WARMUP.
0. The 30-second mental model
The runtime layer = pools + DAG. Pools because the driver's alloc/free are slow, synchronizing, and fragmentation is permanent (no paging, no compaction in HBM). DAG because the GPU is async: streams are FIFOs, events are edges, and everything — including freeing memory — must be positioned in that DAG, not in host wall-clock time. Every framework (PyTorch, vLLM, TensorRT) is a specific set of answers to those two problems.
1. The allocator card
- Design: size-binned free lists; round sizes (512B+); split big cached blocks; never return to driver in steady state; OOM → flush cache → retry.
- reserved (nvidia-smi, driver's view) vs allocated (framework's view): the gap is cache, not a leak. Watch largest-free-block for fragmentation.
- Fragmentation defenses (no compaction possible): size bucketing, pool segregation (weights/activations/KV), fixed-block paging for the dominant consumer (= PagedAttention), and — the confession — scheduled restarts.
cudaMallocAsync= this design, inside the driver, stream-ordered.expandable_segments= VMM tricks to grow reservations.
2. The stream card
- Same stream = ordered. Different streams = concurrent until an event edge says otherwise. Legacy default stream = a global serializer; ban it.
- Review method: draw the intended DAG; extra edges = lost overlap, missing edges = race. Most stream bugs are visible in 5 minutes this way.
- Free is an event: host-time free + async GPU = use-after-free that reproduces once a week. Stream-ordered free (or stream-tagged pools) makes the class impossible, not unlikely.
- CUDA Graphs: capture the decode step, replay in one call; 10–30% at small batch; frozen shapes/pointers; no allocs/syncs inside capture.
3. Numbers
| Thing | Number |
|---|---|
| pool hit | ~100 ns |
| cudaMalloc | ~10–50 µs (driver round trip) |
| cudaFree | device sync — unbounded |
| kernel launch (CPU cost) | ~5–10 µs — why Graphs exist |
| decode step kernel count | hundreds — × 8 µs = CPU-bound issuance |
| pinned alloc | ms-scale — pool these too |
| "OOM at X% utilization" | fragmentation if X < ~70 |
4. Incident patterns
- Day-3 OOM at 60%: checkerboard fragmentation. Look at largest-free-block trend; bucket sizes; segregate pools. Restarts are the tourniquet, not the fix.
- Throughput sag after a "harmless" logging change: someone synced (D2H of a metric) inside the decode loop → killed overlap. nsys timeline shows the gap.
- Garbage tokens, unreproducible: cross-stream use-after-free. Audit every free for stream order; poison freed blocks in a debug build.
- "empty_cache() fixed it": it didn't — it papered over fragmentation by returning blocks to the driver (with syncs). Treat as a smell, not a tool.
- Graph capture failing after a refactor: someone added an alloc/sync in the inner loop. The capture failure is useful — it's a cleanliness linter.
5. What changes at head-of-engineering level
You stop reviewing allocator PRs and start writing the runtime contract: no driver alloc/free in request paths (CI-enforced), stream-ordered lifetime only, segregated pools with reserved/allocated/largest-block metrics exported, no device-wide syncs in engine code, inner loops capture-clean. Each rule maps to an incident class; that mapping is how you defend the contract to a team that finds it pedantic — until day 3.
Next: Phase 06 — with memory and execution under control inside one process, who gets the GPU between processes, pods, and tenants?
Lab 01 — Caching GPU Allocator (Rust)
Phase: 05 — Runtime Systems | Difficulty: ⭐⭐⭐⭐☆ | Time: 5–8 hours Language: Rust (safe, stdlib only) | Hardware: none — a CPU-side model of the GPU original
Concept primer:
../WARMUP.mdCh. 2–5.
Run
cargo test # 12 correctness tests
cargo run --release --bin fragdemo # the two teaching demos
0. The mission
Model the two layers every framework stacks on the driver:
Device= the "driver": a fixed address space, first-fit allocation, holes coalesce on free, and it counts free calls (each is a device sync in real CUDA). This iscudaMalloc/cudaFree.CachingAllocator= the framework layer (the PyTorch design): size rounding into classes, binned free lists, block splitting, stream tagging, and flush-and-retry on OOM. This is what actually serves your tensors.
The tests prove the semantics; fragdemo proves why the design exists.
1. What fragdemo shows
== Demo 1: external fragmentation is a shape problem ==
interleaved (mixed) free=128 MB largest hole= 8 MB 16MB alloc: FAILS (fragmented!)
grouped (disciplined) free=128 MB largest hole=128 MB 16MB alloc: OK
== Demo 2: the caching allocator's reason to exist ==
raw driver : 5000 mallocs, 5000 frees (each free = device sync)
caching : 1 device mallocs, 4999 bin hits, 0 device frees
Demo 1 is the day-3 OOM in 30 deterministic lines: the same 128 MB is free in both rows; only the layout differs. Mixed shapes strand free memory in 8 MB holes between permanent blocks, so a 16 MB request fails at 50% utilization — and since HBM can't be compacted (live pointers), the only fix in production is a restart. Shape discipline (grouping lifetimes) keeps one big contiguous hole. This is exactly why Phase 07 pages the KV cache.
Demo 2 is why the caching layer exists at all: 5000 alloc/free cycles become 5000 synchronizing driver frees raw, versus 1 device malloc + 4999 ~100 ns bin hits cached. The driver never sees a steady-state free.
2. Reading order (src/lib.rs)
round_size+ theDevicemodel — the constraints (first-fit, coalescing, counted frees).CachingAllocator::alloc— the four-step path: bin hit → split larger → driver malloc → flush-and-retry. Each step maps to a WARMUP Ch. 4 bullet.free(goes to a bin, never the driver) andflush_cache(theempty_cache()semantics, including why a split block isn't returnable).- The stream-tag logic in
alloc— same-stream reuse is free; cross-stream reuse counts an event edge (cross_stream_syncs), the Ch. 7 safety rule.
Then trace a 1.2 MB allocation by hand and check yourself against
test_split_accounting.
3. The tests, and what each proves
| Test | The lesson it locks in |
|---|---|
test_rounding | size classes (Ch. 4) |
test_device_first_fit_and_coalesce | the driver model's behavior |
test_bin_reuse_fast_path | reuse never calls the driver (free_calls == 0) |
test_split_accounting | split math: 4MB → 1MB out + 3MB binned |
test_cross_stream_sync_counted | cross-stream reuse needs an event edge (Ch. 7) |
test_same_stream_preferred | same-stream blocks chosen first (no edge) |
test_reserved_vs_allocated | the nvidia-smi-vs-framework gap (Ch. 4, Q2) |
test_flush_and_retry_on_oom | the pre-OOM stall (Ch. 4) |
test_split_block_not_returnable | can't return half a device segment |
4. Extensions
- Coalescing (hard): when two adjacent free blocks exist, merge them back into one larger class — true defragmentation within the cache. Add a test that an alloc/free/alloc pattern which currently fails now succeeds.
empty_cachepolicy: return only segments idle for N cycles; measure the reserved-memory sawtooth.- Trace replayer: parse a CSV of
(op, size, stream)lines and replay it, emitting reserved/allocated/largest-block per step — turn the allocator into a measurable system you can feed real PyTorch traces. - Best-fit vs first-fit in the bins: measure fragmentation on the demo-1 workload; quantify the classic tradeoff.
- Prometheus metrics: export reserved/allocated/cached/largest-block — the Phase 10 fragmentation dashboard, fed from here.
5. Common pitfalls
- Returning a split remainder to the driver — you can't free half a segment
(
test_split_block_not_returnableenforces it; PyTorch has the same limit). - Forgetting the stream tag on reuse — silent cross-stream UAF in the real thing; here it's a counted edge so you can see it.
- Treating reserved as a leak — it's cache; the leak signal is allocated trending up, not reserved.
6. What this lab proves about you
You can implement, test, and reason about the allocator at the heart of every
ML framework — and translate its behavior into operational language (reserved
vs allocated, fragmentation early-warning, the restart confession). Reading
PyTorch's real CUDACachingAllocator.cpp is now a code-review, not an
expedition.
Lab 02 — Stream & Event Scheduler (C++)
Phase: 05 — Runtime Systems | Difficulty: ⭐⭐⭐⭐☆ | Time: 5–8 hours Language: C++17 (std::thread, no deps) | Hardware: none — a CPU model of CUDA stream semantics
Concept primer:
../WARMUP.mdCh. 6–8.
Run
make && ./scheduler_test
0. The mission
Build the CUDA execution model in ~200 lines and use it to make three production failure modes tangible:
Stream= a worker thread + FIFO queue. Ops on one stream run in order.Event= a counter + condition variable.record(stream)enqueues a bump;stream_wait(s, e)makes streamsblock until the event fires. Events are the edges of the dependency DAG (WARMUP Ch. 6).Schedulerowns streams and lets the host join the DAG.
Each "op" is just a std::function with a simulated duration — enough to
observe overlap, serialization, and lifetime bugs without a GPU.
1. The three scenarios (the heart of the lab)
| Scenario | Demonstrates | Pass condition |
|---|---|---|
| 1. Pipeline overlap | H2D copy ∥ compute ∥ D2H across two streams with event edges | ≥1.7× faster than serial |
| 2. Default-stream serialization | the legacy default stream as a global barrier | "concurrent" ops actually serialize; measured |
| 3. Stream-ordered lifetime | use-after-free when free is host-ordered; safety when it's stream-ordered | unsafe path corrupts (detected); ordered path never does |
Expected output:
[scenario 1] serial=282ms pipelined=140ms speedup=2.01x PASS (>=1.7x)
[scenario 2] one-stream=83ms two-stream=45ms overlap=1.84x PASS
(legacy default stream forces the one-stream case — that's the 2x you lose)
[scenario 3] host-ordered free: CORRUPTION detected (as expected)
stream-ordered free: no corruption across 200 trials PASS
[scenario 4] acyclic accepted / circular wait detected PASS
2. Why each scenario matters
- Scenario 1 is the entire reason streams exist: separate copy and compute engines, wired with event edges, double-buffer so chunk N+1's copy overlaps chunk N's compute (WARMUP Ch. 9). The speedup is the Phase 02 Ch. 9 pipeline, measured.
- Scenario 2 is the bug hiding in every naive codebase: issue to the default stream and your carefully-separated work silently serializes. The fix (per-thread default stream / explicit streams) is a one-line policy with a 2× consequence.
- Scenario 3 is the unreproducible-corruption ticket: the host runs ahead
of the device, frees a buffer still in use, the allocator hands it out, and a
scheduling coincidence corrupts data once a week. The lab makes it
reproducible — then shows stream-ordered free making the class impossible
(WARMUP Ch. 7). This connects directly to Lab 01's
cross_stream_syncs.
3. Reading order (scheduler.hpp)
Stream— worker loop pulling from astd::queueunder a mutex/condvar. In-order execution falls out of the FIFO for free.Event—recordenqueues a counter bump onto a stream;stream_waitenqueues a blocking wait onto another stream. This is the whole ordering algebra.Scheduler::synchronize— the host joining the DAG (the bluntcudaDeviceSynchronize).- The scenarios in
scheduler_test.cpp— predict each result from the DAG before reading the assert.
4. Extensions
- Capture/replay ("mini CUDA Graphs"): record a scenario-1 sequence into a
Graphobject, then replay it with one call; measure per-op issue cost before/after (WARMUP Ch. 8). Note what you can't capture (data-dependent branches) — the real restriction. - Priority streams: a high-priority stream's ops jump the worker queue; show a latency-sensitive op finishing ahead of bulk work.
- Deadlock detector: build the wait-for graph from event edges and reject cycles at issue time (scenario 4 stubs this).
- Integrate Lab 01: make the allocator's free stream-ordered using this scheduler's events — the two halves of the runtime layer, joined.
5. Common pitfalls
- Data race in the test harness itself — the "corruption" in scenario 3
must come from the modeled lifetime bug, not from unsynchronized
std::threadaccess in your scaffolding. The provided harness uses a deterministic interleaving hook; keep it. - Busy-waiting instead of condvars — works but pegs a core and hides ordering bugs; use the condition variables as written.
- Joining workers in the wrong order at shutdown — a classic; the destructor drains queues then joins.
- Concluding overlap from wall-clock without warmup — same discipline as Phase 02 Ch. 9; the harness discards the first run.
6. What this lab proves about you
You can implement and reason about the execution model that every async GPU program obeys — and you can name, reproduce, and prevent the three failure modes (lost overlap, accidental serialization, cross-stream UAF) that cost real serving fleets latency and correctness. Combined with Lab 01, you've built the runtime layer this JD calls "runtime systems programming."
Phase 06 — GPU Scheduling, Sharing & Virtualization
Difficulty: ⭐⭐⭐⭐☆ | Estimated Time: 2 weeks Roles supported: Head of Engineering [GPU], Platform/Infra Engineer, Cluster Scheduler Engineer Hardware needed: none — both labs are simulators/models (Python + Go)
Why This Phase Exists
The JD lists "GPU scheduling" as a core skill and the whole role is about building an "orchestration platform." Phase 04 ended on the load-bearing fact: there is no GPU cgroup controller — the kernel cannot enforce a fraction of a GPU. That single gap creates an entire industry: MPS, MIG, time-slicing, fractional-GPU vendors, Kubernetes device plugins, and cluster schedulers that bin-pack the most expensive compute in the datacenter.
This phase is where you learn to allocate GPUs at two altitudes: within a node (how do two tenants share one H100 safely and fairly?) and across a cluster (which of 1,000 GPUs gets this job, and how do you avoid stranding capacity?). You'll build a cluster scheduler simulator that exposes bin-packing, gang scheduling, fragmentation, and fairness, and a Kubernetes device-plugin model that shows exactly how a GPU becomes an allocatable resource — including a fractional-GPU scheme.
Concepts
- The sharing problem restated: no GPU cgroup → sharing needs GPU-side mechanisms
- MPS (Multi-Process Service): one context, multiple processes co-resident; cooperative, weak isolation, no hard memory limits (until recent partitioning)
- MIG (Multi-Instance GPU): hardware partitioning into isolated instances (separate SMs, L2 slices, memory); strong isolation, fixed profiles (1g.10gb …), the only real "fractional GPU"
- Time-slicing: the scheduler context-switches whole-GPU among clients; no isolation, oversubscription convenience
- Choosing among them: latency-sensitive vs batch, trusted vs multi-tenant, the isolation/utilization tradeoff
- Cluster scheduling: bin-packing (first-fit/best-fit/dominant-resource), gang scheduling (all-or-nothing for multi-GPU jobs — the deadlock it prevents), topology-aware placement (NVLink islands, Phase 08)
- Fragmentation at cluster scale: stranded GPUs (a node with 1 free GPU can't take an 8-GPU job); bin-packing policy as the lever
- Fairness: FIFO, priority, preemption, fair-share/DRF; the latency-vs-utilization-vs-fairness trilemma
- Kubernetes integration: the device plugin API, extended resources (
nvidia.com/gpu),nvidia-device-plugin, MIG/time-slicing exposure, the scheduler's view - Inference-specific scheduling: how serving (Phase 07) admission control differs from training job scheduling (Phase 08)
Labs
Lab 01 — GPU Cluster Scheduler Simulator (Python)
| Field | Value |
|---|---|
| Goal | Simulate a GPU cluster and implement/compare placement policies; measure utilization, fragmentation (stranded GPUs), queue wait, and gang-scheduling deadlock avoidance. |
| Concepts | Bin-packing (first/best-fit), gang scheduling, topology-aware placement, fragmentation metrics, fairness. |
| Steps | 1) python solution.py — runs a workload trace through 4 policies, prints a comparison table. 2) Read the scheduler core against WARMUP Ch. 5–7. 3) Reproduce the stranded-GPU result; see gang scheduling prevent the partial-allocation deadlock. 4) Extensions: preemption, DRF fairness, topology-aware NVLink placement. |
| Stack | Pure Python (stdlib) |
| Output | A simulator + a policy comparison table (utilization, p50/p99 wait, stranded-GPU count, deadlocks). |
| How to Test | Built-in asserts: best-fit strands fewer GPUs than first-fit on the adversarial trace; gang scheduling yields zero partial-allocation deadlocks; utilization within expected bands. |
| Talking Points | Why an 8-GPU job starves on a fragmented cluster; gang scheduling's all-or-nothing and the deadlock it prevents; topology-aware placement and the NVLink-island argument (Phase 08). |
| Resume Bullet | "Built a GPU cluster scheduler simulator comparing first-fit/best-fit/gang/topology-aware policies; quantified stranded-GPU fragmentation and demonstrated deadlock-free gang scheduling on multi-GPU job traces." |
| Extensions | Priority preemption with checkpoint cost; Dominant Resource Fairness; a topology model with NVLink islands + an inter-island penalty. |
Lab 02 — Kubernetes Device Plugin + Fractional GPU (Go)
| Field | Value |
|---|---|
| Goal | Implement the Kubernetes device-plugin gRPC interface (the real API) for a fake GPU, then extend it to advertise fractional GPUs (time-sliced replicas) — the actual mechanism behind GPU sharing in k8s. |
| Concepts | Device plugin API (ListAndWatch, Allocate), extended resources, how the kubelet/scheduler see GPUs, time-slicing as replica advertisement, MIG profiles as distinct resources. |
| Steps | 1) go test ./... — the plugin and a mock kubelet exercise the full registration→list→allocate flow. 2) Read plugin.go against WARMUP Ch. 8. 3) Flip replicas: 4 and watch one physical GPU advertise 4 allocatable units (time-slicing); observe the isolation caveat. 4) Extensions: MIG-profile resources, health reporting, the allocate-time env injection (NVIDIA_VISIBLE_DEVICES). |
| Stack | Go (stdlib + a vendored device-plugin proto subset) |
| Output | A working device plugin + tests proving registration, listing, allocation, and fractional advertisement. |
| How to Test | go test: the mock kubelet registers the plugin, receives the device list (including fractional replicas), and a pod "allocation" returns the right device IDs + injected env. |
| Talking Points | Why time-slicing "fractional GPUs" share memory with no isolation (and when that's acceptable); how MIG profiles appear as distinct schedulable resources; what the device plugin does and doesn't enforce (allocation ≠ isolation — Phase 04). |
| Resume Bullet | "Implemented the Kubernetes device-plugin gRPC interface for GPU advertisement including fractional (time-sliced) and MIG-profile resources; documented the allocation-vs-isolation boundary for multi-tenant clusters." |
| Extensions | MIG geometry as multiple resource names; device health/unhealthy transitions; a gpu-feature-discovery-style node labeler. |
Deliverables Checklist
- Scheduler sim: policy comparison table reproduced; stranded-GPU and gang results explained
- You can state when to choose MPS vs MIG vs time-slicing from requirements
- Device plugin: full registration→allocate flow passing; fractional advertisement working
- One page: "our platform's GPU-sharing strategy" — which mechanism for which tenant tier (feeds Phase 12)
- You can draw the k8s GPU allocation path (plugin → kubelet → scheduler → pod) from memory
Interview Relevance
- "Compare MPS, MIG, and time-slicing. When each?"
- "An 8-GPU training job won't schedule though 12 GPUs are free. Why, and fix it."
- "What is gang scheduling and what failure does it prevent?"
- "How does Kubernetes know a node has GPUs? Walk me through the device plugin."
- "Your customer wants '0.5 GPU' tiers. How do you actually deliver that, and what do you tell them about isolation?"
Warmup Guide — GPU Scheduling, Sharing & Virtualization
Zero-to-expert primer for Phase 06. Builds on Phase 04 (no GPU cgroup controller) and Phase 01 (SMs, MIG-able hardware). By the end you can choose sharing mechanisms from requirements and design a cluster scheduler.
Table of Contents
- Chapter 1: The Sharing Problem, Restated
- Chapter 2: MPS — Cooperative Co-Residency
- Chapter 3: MIG — Hardware Partitioning
- Chapter 4: Time-Slicing — Oversubscription Without Isolation
- Chapter 5: Choosing a Sharing Mechanism
- Chapter 6: Cluster Scheduling — Bin-Packing and Fragmentation
- Chapter 7: Gang Scheduling and Topology Awareness
- Chapter 8: Kubernetes Device Plugins — How a GPU Becomes Schedulable
- Chapter 9: Fairness, Priority, and Preemption
- Lab Walkthrough Guidance
- Success Criteria
- Interview Q&A
- References
Chapter 1: The Sharing Problem, Restated
Phase 04 Ch. 8 established the fact that organizes this entire phase: the Linux kernel has no GPU resource controller. You can cap a process's CPU and memory with cgroups; you cannot cap its share of a GPU's SMs or HBM through any kernel mechanism. The GPU is opaque to the OS scheduler — the driver (and the GPU's own front-end) owns GPU time, and they don't expose a fractional knob to the kernel.
So why share at all? Economics. An H100 costs more than the engineer using it, and a single inference model or notebook frequently uses 10–30% of one. Leaving 70% idle across a fleet is the largest controllable line item in a GPU cloud's P&L. Every mechanism in this phase exists to reclaim that idle capacity without letting tenants corrupt or starve each other.
The three answers, previewed: MPS (let processes co-reside in one context), MIG (cut the silicon into isolated pieces), time-slicing (rotate the whole GPU among clients). They sit at different points on the isolation-vs-utilization curve, and choosing among them per workload is a core platform-leadership skill (Ch. 5).
Chapter 2: MPS — Cooperative Co-Residency
The mechanism: normally, processes sharing a GPU time-slice at context granularity (the GPU switches between their contexts, serializing). The Multi-Process Service runs a daemon that funnels multiple processes' kernels into one shared context, so their kernels execute concurrently on the SMs — filling the gaps a single under-utilizing process leaves.
- Win: higher utilization for many small co-operating jobs (classic case: MPI ranks, or several small inference models). No context-switch overhead between them.
- Isolation: weak. Originally no memory limit (one process could OOM the
others' allocations) and no real fault isolation — a fault in one client can
poison the shared context (Phase 02 Ch. 4), taking down co-tenants. Volta+ MPS
added per-client memory limits and some execution-resource provisioning
(
CUDA_MPS_ACTIVE_THREAD_PERCENTAGE), narrowing but not closing the gap. - Verdict: great for trusted, cooperating workloads (your own microservices); not a multi-tenant security boundary.
Chapter 3: MIG — Hardware Partitioning
The mechanism (Ampere+, A100/H100): physically partition one GPU into up to 7 instances, each with dedicated SMs, dedicated L2 cache slices, dedicated memory controllers and HBM, and a separate fault domain. A MIG instance presents to software as its own little GPU.
- Profiles: fixed geometries named
<compute>g.<memory>gb— e.g.,1g.10gb(1/7 compute, 10 GB),2g.20gb,3g.40gb,7g.80gb(whole). Memory and compute are partitioned together; you choose from the menu, you don't get arbitrary fractions. - Isolation: strong — hardware-enforced. A tenant in
1g.10gbcannot touch another instance's memory, cannot steal its SMs, and a fault is contained. This is the only "fractional GPU" with real multi-tenant isolation. - Cost: rigidity (fixed profiles, reconfiguration usually needs the GPU idle/reset), and quantization waste (a job needing "1.5 instances" rounds up).
- Verdict: the default for multi-tenant fractional serving where isolation is a contractual requirement (sovereign/regulated customers — this JD's nice-to-haves).
Chapter 4: Time-Slicing — Oversubscription Without Isolation
The mechanism: the simplest scheme — the GPU scheduler context-switches the whole GPU among clients in turn (the default behavior when multiple processes use one GPU without MPS/MIG; in k8s, exposed by advertising N "replicas" of one physical GPU).
- Win: trivial oversubscription — pack 4 bursty notebooks onto 1 GPU; when one is idle, others get full GPU during their slice. Zero hardware requirement.
- Isolation: none — clients share all memory (sum must fit!), no performance isolation (a heavy client starves others during its slices), full fault sharing.
- Verdict: dev/test, bursty interactive workloads, trusted oversubscription where occasional interference is acceptable; never for latency-SLO or multi-tenant security.
Chapter 5: Choosing a Sharing Mechanism
The decision table to internalize (and the heart of the "0.5 GPU tier" interview question):
| Need | Mechanism |
|---|---|
| Trusted small jobs, max utilization, no SLO | MPS |
| Multi-tenant, isolation is contractual, predictable QoS | MIG |
| Bursty dev/notebooks, oversubscription, cost over isolation | Time-slicing |
| One big job using the whole GPU | None (dedicated) |
| Latency-critical serving + isolation | MIG (or dedicated) |
The underlying axis is isolation vs utilization: MIG buys isolation by
giving up the flexibility to overcommit; time-slicing buys utilization by giving
up isolation; MPS sits between. A platform usually offers tiers mapping to
these — and the honest answer to a customer asking for "half a GPU" is "which
do you need: a guaranteed isolated half (MIG 3g.40gb), or a cheaper shared
half that may be slow when neighbors are busy (time-slicing)?" Saying that
sentence is what separates a platform leader from a salesperson.
Chapter 6: Cluster Scheduling — Bin-Packing and Fragmentation
Zoom out from one node to thousands. A scheduler maps jobs (each wanting k GPUs, maybe with constraints) onto nodes (each with some free GPUs). This is bin-packing, NP-hard, solved with heuristics:
- First-fit: place a job on the first node that fits. Fast, simple, packs loosely → strands capacity.
- Best-fit: place on the node that leaves the least leftover. Packs tighter, reduces fragmentation, slightly more expensive.
- Worst-fit / spread: place on the emptiest node. Good for thermal/failure spread and leaving room for big jobs, worse for packing.
- Dominant Resource Fairness (DRF) when jobs want heterogeneous resources (GPUs + CPU + RAM): fair-share along each job's dominant resource.
Cluster fragmentation — the stranded-GPU problem (Lab 01 reproduces it): free GPUs scattered one-per-node can't satisfy an 8-GPU gang job, even though total free ≥ 8. This is Phase 05's external fragmentation at datacenter scale — and the fix is the same family: placement policy (best-fit consolidates; defragmentation by migration where checkpointable), and shape-aware admission (reserve whole nodes for big jobs). The metric to watch: stranded GPUs = free GPUs that no pending job can use given placement constraints.
Chapter 7: Gang Scheduling and Topology Awareness
Gang scheduling: a multi-GPU job needs all its GPUs simultaneously (a distributed training step can't start with 6 of 8 ranks — the all-reduce would hang, Phase 08). So the scheduler must allocate all-or-nothing: either place the whole gang or none of it. Why it matters: without it, two 8-GPU jobs on a 16-GPU cluster can each grab 4–6 GPUs, neither can proceed, neither will release — deadlock. Gang scheduling (and its cousin, reservation) prevents the partial-allocation deadlock. Lab 01 demonstrates both the deadlock and its prevention.
Topology awareness: not all GPU slots are equal. 8 GPUs on one node behind NVLink (900 GB/s, Phase 01 Ch. 9) crush 8 GPUs spread across nodes on InfiniBand (~50 GB/s) for a tensor-parallel job. A topology-aware scheduler prefers NVLink islands for tightly-coupled jobs and is willing to wait or consolidate to get them. This is where Phase 06 (scheduling) and Phase 08 (collectives) meet: the scheduler's placement decides the collective's bandwidth, which decides the job's throughput. Lab 01's extension models NVLink islands with an inter-island penalty.
Chapter 8: Kubernetes Device Plugins — How a GPU Becomes Schedulable
Kubernetes knows nothing about GPUs natively. The device plugin API (a gRPC contract) teaches it. The flow (Lab 02 implements it end to end):
- The plugin (e.g.,
nvidia-device-plugin, a DaemonSet pod) registers with the kubelet over a Unix socket, claiming a resource name:nvidia.com/gpu. ListAndWatch: the plugin streams the list of healthy device IDs to the kubelet; the node now advertisesnvidia.com/gpu: 8as an extended resource.- The scheduler treats
nvidia.com/gpulike CPU/memory for placement (pods request it inresources.limits). - When a pod lands, the kubelet calls the plugin's
Allocatewith the chosen device IDs; the plugin returns the device nodes, mounts, and environment to inject (NVIDIA_VISIBLE_DEVICES=...) — which is exactly the container-injection mechanism from Phase 04 Ch. 9.
The crucial boundary (say it in interviews): the device plugin does
allocation, not isolation. It advertises and assigns; the actual isolation is
whatever the underlying mechanism provides (whole device, MIG instance, or — for
time-slicing — nothing, you just advertised replicas: 4 of one physical
GPU). Fractional GPUs in k8s are this: the plugin lists one physical GPU as N
allocatable units. MIG appears as distinct resource names
(nvidia.com/mig-1g.10gb). Lab 02 builds the plugin, then the fractional
advertisement, so the allocation/isolation line is concrete, not slogan.
Chapter 9: Fairness, Priority, and Preemption
The trilemma every scheduler navigates: latency vs utilization vs fairness — you can optimize any two.
- FIFO: simple, starves big/low-priority jobs behind a long queue.
- Priority + preemption: high-priority jobs evict lower ones — but preempting a GPU job means checkpointing (expensive) or killing (wasteful); the cost of preemption is much higher than for CPU jobs, so policies lean toward priority admission over mid-flight eviction.
- Fair-share / DRF: each tenant gets a fair slice over time; prevents one team monopolizing the cluster.
- Backfill: let small jobs run in the gaps while a big gang job waits for its reservation — utilization without starving the big job.
For inference serving specifically (Phase 07), "scheduling" shifts meaning: it's admission control and continuous batching at millisecond granularity on an already-placed model, not job placement at minute granularity. A platform leader keeps these two scheduling worlds — training-job placement (this phase) and request admission (Phase 07) — distinct in design and in conversation.
Lab Walkthrough Guidance
Order: Lab 01 → Lab 02 (cluster view, then the node-level k8s mechanism).
Lab 01 (cluster sim, Python):
python solution.py; read the policy comparison table, then the scheduler core against Ch. 6–7.- Find the stranded-GPU result in first-fit vs best-fit — explain it with the Ch. 6 fragmentation story.
- Watch the gang test: with naive allocation two big jobs deadlock; with gang scheduling neither does. Connect to Ch. 7.
- Extensions: preemption (with checkpoint cost), DRF, topology islands.
Lab 02 (device plugin, Go):
go test ./...— the mock kubelet drives registration→list→allocate.- Read
plugin.goagainst Ch. 8; trace one pod's allocation end to end. - Set
replicas: 4; re-run; one physical GPU now advertises 4 units. Read the isolation caveat in the test output — that's the allocation≠isolation lesson. - Extensions: MIG profiles as resource names, health transitions.
Success Criteria
- You can explain why no GPU cgroup controller forces GPU-side sharing (Ch. 1)
- You can contrast MPS/MIG/time-slicing on isolation, utilization, and use case, and pick from requirements (Ch. 2–5)
- You can explain cluster fragmentation/stranded GPUs and how best-fit helps (Ch. 6)
- You can define gang scheduling and the deadlock it prevents, and the NVLink-island argument (Ch. 7)
- You can draw the k8s device-plugin allocation path and state allocation≠isolation (Ch. 8)
- Both labs pass; you can map sharing mechanisms to platform tiers
Interview Q&A
Q1: Compare MPS, MIG, and time-slicing. A: All three share one physical GPU because the kernel can't. MPS merges multiple processes into one CUDA context so their kernels run concurrently — best utilization for trusted cooperating jobs, but weak isolation (shared fault domain; memory limits only added in Volta+). MIG physically partitions the GPU into instances with dedicated SMs, L2, and HBM and separate fault domains — the only option with hardware-enforced multi-tenant isolation, at the cost of fixed profiles and reconfiguration rigidity. Time-slicing rotates the whole GPU among clients — trivial oversubscription, zero isolation (shared memory, no QoS, shared faults). Decision: trusted+utilization → MPS; multi-tenant+isolation → MIG; bursty dev/cost → time-slicing; latency-SLO → MIG or dedicated.
Q2: An 8-GPU job won't schedule though 12 GPUs are free. Why? A: Cluster fragmentation — the 12 free GPUs are scattered (e.g., 1–2 per node) and the job needs 8 co-located (gang + likely topology constraint for the collective). No single node/island offers 8, so it can't place despite sufficient total capacity — the stranded-GPU problem, external fragmentation at cluster scale. Fixes: best-fit/consolidating placement to keep whole nodes free, reserving capacity for large jobs, draining/migrating checkpointable small jobs to defragment, and topology-aware admission that knows an 8-GPU TP job wants one NVLink island. The metric to alert on is stranded-GPU count, not raw free-GPU count.
Q3: What is gang scheduling and what does it prevent? A: All-or-nothing allocation for a multi-GPU job: the scheduler places every GPU the job needs simultaneously or none. It prevents the partial-allocation deadlock — two 8-GPU jobs on 16 GPUs each grabbing 6, neither able to start its synchronous collective (Phase 08), neither releasing. Gang scheduling (with reservation/backfill so the cluster isn't idled while assembling a gang) is mandatory for distributed training; serving (single-GPU or already-placed replicas) usually doesn't need it.
Q4: How does Kubernetes schedule onto GPUs?
A: Via the device-plugin API. A per-node plugin registers a resource name
(nvidia.com/gpu) with the kubelet, streams the healthy device list via
ListAndWatch (the node now advertises that many extended-resource units), and
the default scheduler treats it like CPU/memory for placement. On binding, the
kubelet calls the plugin's Allocate with chosen device IDs; the plugin returns
the device nodes, mounts, and env (NVIDIA_VISIBLE_DEVICES) to inject — the
Phase 04 container-injection mechanism. Critically the plugin does allocation,
not isolation: fractional GPUs are just advertising one device as N units
(time-slicing, no isolation) or exposing MIG instances as separate resource
names (real isolation).
Q5 (leadership): Design the GPU-sharing strategy for a multi-tenant inference
platform with both enterprise and free-tier customers.
A: Tier by isolation need. Enterprise/regulated: dedicated GPUs or MIG
instances (3g.40gb-class) — hardware isolation is contractual; advertise as
distinct k8s resources; predictable QoS. Standard paid: MIG smaller profiles
or MPS within a trusted pool with per-client memory limits and SM provisioning,
SLO-backed. Free/dev tier: time-sliced oversubscription — cheap, explicitly
best-effort, isolated only at the container level (and never co-scheduling free
tenants with paid on the same physical GPU, to bound interference and the
shared-fault blast radius). Across the cluster: best-fit placement to limit
stranding, gang+topology scheduling for any training jobs, and a fragmentation
dashboard (stranded GPUs, MIG-profile mix utilization). The platform's job is to
expose these as a small menu of honest tiers — each customer picks their point
on the isolation/cost curve, and we never silently put them somewhere weaker
than they paid for. (This is also where Phase 11's tenant-isolation security
story attaches.)
References
- NVIDIA Multi-Process Service docs — https://docs.nvidia.com/deploy/mps/
- NVIDIA MIG User Guide — https://docs.nvidia.com/datacenter/tesla/mig-user-guide/
- NVIDIA GPU time-slicing in Kubernetes — https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/gpu-sharing.html
- Kubernetes Device Plugin API — https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/device-plugins/
- NVIDIA k8s device plugin (read the real one) — https://github.com/NVIDIA/k8s-device-plugin
- Ghodsi et al., "Dominant Resource Fairness" (NSDI 2011)
- "Gang Scheduling" — Feitelson & Rudolph; and the Kubernetes "coscheduling" plugin / Volcano — https://volcano.sh
- Google Borg paper (Verma et al., EuroSys 2015) — the cluster-scheduling bible
- Cross-track: Phase 04 WARMUP Ch. 8–9, Phase 08 WARMUP (why topology matters)
🛸 Hitchhiker's Guide — Phase 06: GPU Scheduling & Virtualization
Read this if: you can run a CUDA job but "how do two tenants share an H100" and "why won't my 8-GPU job schedule" are mysteries. Field notes; derivations in the WARMUP.
0. The 30-second mental model
No GPU cgroup controller ⇒ the OS can't enforce GPU fractions ⇒ sharing needs GPU-side mechanisms (MPS/MIG/time-slicing) and cluster-side scheduling (bin-packing + gang + topology). Every choice is a point on isolation ⇄ utilization. Kubernetes learns about GPUs only through a device plugin, which does allocation, not isolation.
1. Node-level sharing card
| Mechanism | Isolation | Utilization | Use when |
|---|---|---|---|
| MPS | weak (shared context/fault) | high for small coop jobs | trusted microservices, MPI ranks |
| MIG | strong (HW partition) | medium (fixed profiles, quantization waste) | multi-tenant, SLO, regulated |
| Time-slicing | none (shared mem+faults) | high (overcommit) | dev/notebooks, bursty, cheap |
| Dedicated | total | low | one big job / strict latency |
"Half a GPU?" → "isolated half (MIG 3g.40gb) or cheap shared half (time-slice that's slow when neighbors are busy)?" Knowing to ask that is the whole skill.
2. Cluster-level card
- Bin-packing: first-fit (fast, strands), best-fit (tight, less stranding), spread (failure/thermal). Stranded GPUs = free GPUs no pending job can use.
- Gang scheduling: all-or-nothing for multi-GPU jobs → prevents the partial-allocation deadlock (two 8-GPU jobs each grabbing 6 on 16 GPUs).
- Topology: 8 GPUs on one NVLink island ≫ 8 spread over IB for TP jobs (Phase 08). Topology-aware placement decides collective bandwidth.
- Backfill + reservation: small jobs fill gaps while a big gang assembles — utilization without starving the big job.
3. The k8s GPU path
device-plugin (DaemonSet) --register--> kubelet --advertises--> nvidia.com/gpu: 8
--ListAndWatch (healthy IDs)-->
scheduler places pod requesting nvidia.com/gpu: 1
kubelet --Allocate(ids)--> plugin returns device nodes + mounts + NVIDIA_VISIBLE_DEVICES
(= Phase 04 injection)
Fractional GPU = plugin advertises one physical device as N units (time-slice,
no isolation) OR exposes MIG instances as nvidia.com/mig-1g.10gb (real
isolation). Allocation ≠ isolation — tattoo it.
4. Numbers & facts
| Thing | Value |
|---|---|
| MIG max instances (A100/H100) | 7 |
| MIG smallest profile | 1g.10gb (1/7 compute, 10 GB) |
| NVLink vs IB (placement stakes) | ~900 vs ~50 GB/s |
| GPU preemption cost | checkpoint (GBs) or kill — ≫ CPU preempt |
| Idle GPU in a typical single-model serving | 70%+ (why sharing exists) |
5. War stories
- "We bought 0.5-GPU tiers and customers complained of jitter" — sold time-slicing as if it were isolated. Free tenants interfered. Fix: MIG for paid isolation tiers; never co-place free + paid on one physical GPU.
- "Cluster at 60% but jobs queueing" — stranded GPUs from first-fit + big-gang jobs. Switched to best-fit + reserved whole-node pool for ≥4-GPU jobs. Queue drained.
- "Training job hangs at start, no error" — partial gang allocation; 6 of 8 ranks up, NCCL init waiting forever. Gang scheduling fixed it.
- "TP throughput half of expected" — scheduler spread 8 GPUs across 2 nodes; per-layer all-reduce hit IB not NVLink. Topology constraint added.
- "MPS client crash took down the node's other models" — shared context, shared fault domain. Moved tenants to MIG.
6. Exit bar
You can pick a sharing mechanism from requirements, explain stranded-GPU fragmentation and gang/topology scheduling, draw the k8s device-plugin path, and state allocation≠isolation with conviction. You've built a cluster scheduler sim and a device plugin. Next: Phase 07 — the most commercially valuable scheduling of all, request scheduling inside an LLM server (KV cache, continuous batching), the JD's named skill.
Lab 01 — GPU Cluster Scheduler Simulator (Python)
Phase: 06 — GPU Scheduling | Difficulty: ⭐⭐⭐⭐☆ | Time: 5–8 hours Language: Python (stdlib) | Hardware: none
Concept primer:
../WARMUP.mdCh. 6–7, 9.
Run
python solution.py
0. The mission
Simulate a GPU cluster and run a job trace through four placement policies, measuring what platform leaders actually argue about: utilization, queue wait, stranded GPUs, and gang-deadlock avoidance.
The simulator is event-driven: jobs arrive, request k GPUs for a duration, get placed (or queued), run, and release. Policies differ only in where they place.
Expected output:
== Placement-policy comparison (mixed trace + whole-node jobs) ==
policy util% p50_wait p99_wait stranded deadlocks
spread 60.9 0.0 228.0 24 0
first_fit 67.0 0.0 36.0 2 0
best_fit 67.0 0.0 36.0 2 0
== Gang vs naive: the partial-allocation deadlock ==
naive_no_gang : 0/3 jobs completed, 3 deadlocked (held GPUs, never able to finish)
best_fit_gang : 3/3 jobs completed, 0 deadlocked
1. What each policy proves
- spread vs best_fit: this is the headline. The cluster runs at the same moderate load, but spread scatters 2-GPU jobs across all four nodes, leaving 2 free GPUs per node and no whole node free — so the whole-node NVLink-8 jobs strand (p99 wait jumps to 228, and 24 GPUs sit free while jobs wait). best_fit consolidates the small jobs onto fewer nodes, keeping whole nodes available → the NVLink jobs run almost immediately. Same hardware, same jobs; only the placement shape differs. This is Phase 05's fragmentation at cluster scale, and the reason topology/consolidation-aware placement exists (WARMUP Ch. 6–7).
- The gang-vs-naive deadlock: three 6-GPU jobs on a 12-GPU cluster. Naive round-robin placement gives each job one 4-GPU node; all 12 GPUs are held, every job is stuck at 4/6, none can finish or release — permanent partial-allocation deadlock. Gang scheduling refuses partial commits and runs them one at a time: all three complete (WARMUP Ch. 7).
2. Reading order (solution.py)
Cluster/Node— the resource model andfree_gpus,stranded_gpus.place_*policy functions — each takes (cluster, job) → placement or None.- The gang logic:
naivereserves GPUs as it finds them (can deadlock);gangonly commits if the whole job fits atomically. - The event loop and metrics.
3. Extensions
- Preemption: high-priority jobs evict low-priority ones; model the checkpoint cost (a fixed penalty added to the preempted job's remaining time). Show priority-inversion avoidance.
- DRF fairness: jobs request GPUs and CPU; schedule by dominant share; prevent one tenant monopolizing.
- Topology depth: model 2 nodes × 4-GPU NVLink islands; add an inter-island bandwidth penalty to job duration when a gang spans islands — then watch topology_aware pay off in throughput, not just placement.
- Backfill: let small jobs run while a big gang waits for its reservation; measure the utilization recovery.
4. Common pitfalls
- Counting "free GPUs" as "usable GPUs" — the whole stranded-GPU lesson is that they differ once jobs have co-location constraints.
- Releasing GPUs in the wrong event order (release before the job's end event) — corrupts utilization accounting.
- Modeling gang scheduling as "retry until it fits" without a reservation — that's just busy-waiting and can still starve big jobs; the lab uses reservation-style atomic commit.
5. What this lab proves about you
You can reason about cluster scheduling with numbers — stranding, gang-deadlock, topology — which is the difference between "we use Kubernetes" and "here's our placement policy, here's the fragmentation it controls, here's why big jobs don't starve." That's the platform-leadership altitude this JD hires for.
Lab 02 — Kubernetes Device Plugin + Fractional GPU (Go)
Phase: 06 — GPU Scheduling | Difficulty: ⭐⭐⭐⭐☆ | Time: 4–6 hours Language: Go (stdlib) | Hardware: none — a model of the device-plugin contract + a mock kubelet
Concept primer:
../WARMUP.mdCh. 8.
Run
go test ./... # mock kubelet drives registration -> list -> allocate
go run . # prints a narrated end-to-end allocation walkthrough
0. The mission
Implement the Kubernetes device-plugin contract for a fake GPU — the real
mechanism by which a node advertises nvidia.com/gpu: 8 and a pod gets device
nodes injected. Then extend it to advertise fractional GPUs (time-sliced
replicas) and MIG profiles as distinct resources, so the
allocation ≠ isolation boundary (WARMUP Ch. 8) is concrete code, not a slogan.
We model the three plugin operations that matter (the real API is gRPC over a Unix socket; we keep the semantics and the data flow, in plain Go interfaces, so it runs anywhere with no proto toolchain):
| Operation | Real API | What it does |
|---|---|---|
| Register | Registration.Register | tell the kubelet the resource name |
ListAndWatch | streams Device[] | advertise healthy device IDs (→ node capacity) |
Allocate | AllocateResponse | return device nodes + mounts + env to inject |
1. What the walkthrough shows
mode=whole resource=nvidia.com/gpu advertised units=4 (4 physical GPUs)
allocate [GPU-0 GPU-1] -> env NVIDIA_VISIBLE_DEVICES=GPU-0,GPU-1
devices /dev/nvidia0 /dev/nvidia1
isolation: STRONG (dedicated physical devices)
mode=timeslice resource=nvidia.com/gpu advertised units=16 (4 GPUs x 4 replicas)
allocate [GPU-0::3] -> env NVIDIA_VISIBLE_DEVICES=GPU-0 (replica 3 shares GPU-0!)
isolation: NONE (replicas share memory + faults — WARMUP Ch. 4)
mode=mig resource=nvidia.com/mig-1g.10gb advertised units=28 (4 GPUs x 7 instances)
allocate [MIG-0-2] -> env NVIDIA_VISIBLE_DEVICES=MIG-GPU-0/2/0
isolation: STRONG (hardware-partitioned instance)
The same plugin API advertises all three — but the isolation each delivers is completely different. Time-slicing's "16 GPUs" are 4 physical devices wearing 4 hats each; the plugin happily allocates replica 3 of GPU-0 to a pod with no memory or fault isolation from replicas 0–2. That's the lesson the lab burns in: the device plugin allocates; it does not isolate.
2. Reading order
device.go— theDevice,Plugininterface, and the three modes (whole / timeslice / mig) that change only what gets advertised.plugin.go—ListAndWatch(build the advertised list) andAllocate(pick devices, build the injection response withNVIDIA_VISIBLE_DEVICES).kubelet_mock.go— the mock kubelet: registers the plugin, reads the device list, and requests an allocation, exactly as the real kubelet does.plugin_test.go— the contract tests.
3. Extensions
- MIG geometry: advertise mixed profiles (
1g.10gb,2g.20gb,3g.40gb) as separate resource names from the same physical GPUs, respecting that a GPU's instances are mutually exclusive (allocating a3gconsumes capacity the1gs would have used) — the real MIG accounting problem. - Health transitions: mark a device
Unhealthy(model an Xid error from Phase 04/10);ListAndWatchmust re-advertise and the kubelet must stop scheduling onto it. gpu-feature-discovery: emit node labels (nvidia.com/gpu.product=H100,nvidia.com/gpu.memory=80) so the scheduler can do affinity.- Preferred allocation: implement
GetPreferredAllocationto pick devices that share NVLink (topology — Phase 08) for multi-GPU requests.
4. Common pitfalls
- Conflating advertised units with isolation — the entire point. A test
asserts that two timeslice replicas of the same physical GPU resolve to the
same
NVIDIA_VISIBLE_DEVICESvalue (proof they share hardware). - Forgetting the env/mount injection — without it the container sees the resource accounted but can't actually reach the GPU (Phase 04 Ch. 9).
- Letting
Allocatehand out more replicas than the device has, or replicas of an unhealthy device.
5. What this lab proves about you
You can implement the exact integration point between Kubernetes and GPUs, and you can articulate — with running code — why "fractional GPU" means very different isolation guarantees depending on the mechanism. When a customer or a PM asks for "GPU sharing in k8s," you can design the honest answer (which resource, which isolation, which tier — Phase 12).
Phase 07 — LLM Serving & KV-Cache Management
Difficulty: ⭐⭐⭐⭐⭐ | Estimated Time: 2.5 weeks Roles supported: Head of Engineering [GPU], LLM Inference Engineer, Serving Platform Lead Hardware needed: none — both labs are CPU models of the serving runtime (Python); optional GPU for the vLLM reading exercise
Why This Phase Exists
The JD names it explicitly: "GPU scheduling, KV-cache management, or LLM serving optimisation" and "deploying and optimising AI infrastructure at production scale, ideally with multiple inference engines." This is the single most commercially valuable phase for the role — a 30% throughput gain across a serving fleet is millions of dollars a year, and the lever is almost always KV-cache management and batching.
This is the consequence phase. Phase 01 proved decode is memory-bandwidth-bound; Phase 05 built allocators and stream scheduling; Phase 06 built request-vs-job scheduling. Here it all converges into the serving runtime: you'll build a paged KV-cache allocator with prefix sharing (PagedAttention as an allocator-design problem — exactly Phase 05 Ch. 5's prediction) and a continuous-batching simulator with TTFT/TPOT/goodput, then connect both to operating vLLM/TensorRT-LLM/SGLang at scale.
This phase deliberately overlaps the LLM Inference Engineer track's Phase 09. That track owns the model/algorithm depth (FlashAttention math, quantization formats, speculative decoding derivations); this phase owns the platform/operations view — the cache as a managed resource, the scheduler as a fleet control loop, multi-engine selection, and the leadership decisions. Read both; they're complementary, and the WARMUPs cross-reference rather than duplicate.
Concepts
- The prefill/decode split and why it dictates everything (recap from Phase 01 Ch. 7, serving-side)
- KV-cache memory math:
2 × n_layers × n_kv_heads × d_head × seq_len × dtype_bytes— and why it, not weights, caps concurrency - PagedAttention: fixed-size KV blocks + block tables = external fragmentation eliminated for the dominant consumer (Phase 05 Ch. 5, realized)
- Prefix caching / sharing: copy-on-write KV blocks for shared system prompts (chat APIs) — the highest-ROI serving feature for instruction-heavy workloads
- Continuous batching (Orca): iteration-level scheduling, admission/eviction, the latency-throughput dial
- Chunked prefill and prefill/decode disaggregation: keeping long prompts from stalling decodes
- Metrics & SLOs: TTFT, TPOT/ITL, E2E, throughput, and goodput (throughput meeting SLO — the only honest fleet metric)
- Admission control, preemption, and KV-cache swapping/recompute under memory pressure
- GQA, KV quantization (INT8/FP8 KV) — shrinking the cache (cross-ref to llm-inference track)
- The engine landscape as a platform decision: vLLM vs TensorRT-LLM vs SGLang vs TGI — what each optimizes, when to choose which, the multi-engine reality
- Sovereign/air-gapped serving: on-prem deployment, no telemetry phone-home, license enforcement (Phase 11) — the JD's regulated-industry nice-to-haves
Labs
Lab 01 — Paged KV-Cache Allocator + Prefix Sharing (Python)
| Field | Value |
|---|---|
| Goal | Implement a PagedAttention-style KV-cache block allocator: fixed-size blocks, per-sequence block tables, copy-on-write prefix sharing — and measure the memory win vs naive contiguous allocation. |
| Concepts | Block tables, fixed-size paging, internal vs external fragmentation, copy-on-write prefix sharing, KV-cache memory math. |
| Steps | 1) python solution.py — runs correctness tests + a memory-efficiency comparison + a prefix-sharing demo. 2) Read the allocator against WARMUP Ch. 2–4. 3) Reproduce: naive contiguous wastes 60–80% on max-length reservation; paged wastes < one block/seq; prefix sharing collapses N copies of a shared prompt to 1. 4) Extensions: eviction/preemption, swapping to host, block ref-count GC. |
| Stack | Python (stdlib) |
| Output | A block allocator + tests + a memory-comparison table (naive vs paged vs paged+prefix). |
| How to Test | Built-in asserts: paged supports ≥2× the concurrent sequences of naive at the same memory; prefix sharing yields exact block reuse with correct copy-on-write at divergence. |
| Talking Points | Why this is "just an allocator" (Phase 05); the block-size tradeoff (internal fragmentation vs table overhead); why prefix caching is the highest-ROI feature for chat workloads; how ref-counting handles fork/COW at the divergence token. |
| Resume Bullet | "Implemented a PagedAttention-style KV-cache allocator with copy-on-write prefix sharing; demonstrated 2.6× concurrent-sequence capacity over contiguous allocation and exact block reuse for shared system prompts." |
| Extensions | LRU block eviction with recompute-vs-swap policy; host-memory swapping (the 50× bandwidth cliff from Phase 01); ref-count GC with a fork/branch (beam search) workload. |
Lab 02 — Continuous Batching Simulator + Goodput (Python)
| Field | Value |
|---|---|
| Goal | Simulate an LLM server under a Poisson request load; implement static vs continuous batching with iteration-level admission/eviction; measure TTFT, TPOT, throughput, and goodput under an SLO. |
| Concepts | Static vs continuous batching, iteration scheduling, admission control, the latency-throughput dial, goodput, prefill interference + chunked prefill. |
| Steps | 1) python solution.py — runs a workload through static and continuous batching, prints the metrics table. 2) Read the scheduler against WARMUP Ch. 5–6. 3) Reproduce continuous batching's throughput win and observe the TTFT/TPOT tradeoff as you change max_batch. 4) Extensions: chunked prefill, prefix-cache hits (Lab 01), priority/SLO-aware admission. |
| Stack | Python (stdlib) |
| Output | A serving simulator + a metrics table (static vs continuous; goodput vs batch depth). |
| How to Test | Built-in asserts: continuous batching ≥2× static throughput at concurrency 16; goodput is maximized at an interior batch depth (not the largest), proving the latency-throughput dial. |
| Talking Points | Why static batching wastes GPU on long-tail requests; why goodput (not raw throughput) is the honest metric; how prefill interference motivates chunked prefill and disaggregation; the admission-control knobs you'd expose. |
| Resume Bullet | "Built a continuous-batching LLM-serving simulator with goodput accounting under SLO; demonstrated 3.1× throughput over static batching and identified the goodput-optimal batch depth, quantifying the latency-throughput tradeoff." |
| Extensions | Chunked prefill (slice long prompts across iterations); prefix-cache integration; SLO-aware admission that sheds load to protect p99; prefill/decode disaggregation model. |
Deliverables Checklist
- Paged allocator: tests pass; memory-comparison table reproduced and explained
- You can do the KV-cache memory math cold for any model/context/batch
- Continuous-batching sim: metrics table reproduced; goodput-optimal batch depth identified
- You can explain why goodput > throughput as a fleet metric
- One page: "our serving platform's KV-cache & batching strategy" (feeds Phase 12 capstone)
-
vLLM reading: skim
vllm/core/block_manager+ scheduler; map each to your Lab 01/02 code
Interview Relevance
This phase is the direct portfolio for the JD's serving requirement.
- "Walk me through KV-cache memory and why it caps concurrency."
- "Explain PagedAttention — and why it's really an allocator." (Phase 05 link)
- "Static vs continuous batching — and what's goodput?"
- "How does prefix caching work and when is it worth it?"
- "vLLM vs TensorRT-LLM vs SGLang — when each, and how do you run multiple engines?"
- "Design air-gapped LLM serving for a regulated customer." (→ Phase 11, system-design/)
Warmup Guide — LLM Serving & KV-Cache Management
Zero-to-expert primer for Phase 07, from the platform/operations angle. Builds on Phase 01 (roofline), Phase 05 (allocators), Phase 06 (scheduling). For the model/algorithm depth (FlashAttention math, quantization formats, speculative decoding) see the LLM Inference track Phase 09; this guide cross-references rather than duplicates.
Table of Contents
- Chapter 1: The Serving Problem in One Diagram
- Chapter 2: KV-Cache Memory — the Math That Caps Everything
- Chapter 3: PagedAttention — KV Cache Is an Allocator Problem
- Chapter 4: Prefix Sharing — Copy-on-Write for Prompts
- Chapter 5: Continuous Batching — Iteration-Level Scheduling
- Chapter 6: Metrics and SLOs — Why Goodput Is the Only Honest Number
- Chapter 7: Memory Pressure — Preemption, Swapping, Recompute
- Chapter 8: The Engine Landscape — A Platform Decision
- Chapter 9: Sovereign and Air-Gapped Serving
- Lab Walkthrough Guidance
- Success Criteria
- Interview Q&A
- References
Chapter 1: The Serving Problem in One Diagram
One LLM request is two workloads (Phase 01 Ch. 7's roofline, applied):
PREFILL (process the whole prompt) DECODE (generate token by token)
all prompt tokens at once one token per forward pass
big matmuls -> COMPUTE-bound reads all weights+KV -> BANDWIDTH-bound
cost ~ prompt length cost ~ output length
user feels it as: TTFT user feels it as: TPOT (smoothness)
Everything in serving follows from three facts: (1) decode is bandwidth-bound, so batching (reusing each weight read across many requests) is the master throughput lever; (2) the KV cache grows with every token and caps how many requests you can batch; (3) prefill and decode interfere (a long prefill stalls everyone's decode). The platform's job is to maximize goodput — useful throughput meeting latency SLOs — by managing the cache as a resource and scheduling at iteration granularity. Hold that; the rest elaborates.
Chapter 2: KV-Cache Memory — the Math That Caps Everything
The formula to know cold (and derive in interviews):
$$\text{KV bytes} = 2 \times n_{layers} \times n_{kv_heads} \times d_{head} \times \text{seq_len} \times \text{bytes/elem}$$
The 2 is K and V. Worked: Llama-3-8B (32 layers, 8 KV heads — GQA already
applied — , d_head 128, BF16): per token = 2·32·8·128·2 = 131 KB/token; at
8K context that's ~1 GB per sequence, on top of the 16 GB of weights. A
40 GB GPU holds ~22 such sequences max — the KV cache, not compute, caps
concurrency.
This single calculation explains the whole phase:
- GQA (8 KV heads instead of 32) already cut this 4× — why every modern model uses it.
- KV quantization (INT8/FP8 KV) halves/quarters it again.
- Paging (Ch. 3) stops you from wasting most of it.
- Batching limits (Ch. 5) are set by how many sequences' caches fit.
As a platform leader you carry this formula to capacity planning: "how many
concurrent 8K-context users per H100?" is (HBM − weights) / (KV bytes/token × context), and every serving optimization is an attempt to improve one term.
Chapter 3: PagedAttention — KV Cache Is an Allocator Problem
Phase 05 Ch. 5 predicted this exactly: "fixed-size paging for the dominant consumer eliminates external fragmentation by design — that sentence is PagedAttention." Here it is, realized (Lab 01 builds it).
The naive way: allocate each sequence a contiguous KV buffer sized for max possible length. Problems: (1) you don't know the output length, so you reserve for the worst case → most of it is never used (internal fragmentation, vLLM measured 60–80% waste); (2) variable-length sequences leave un-fillable holes (external fragmentation, Phase 05's day-3 OOM).
PagedAttention: chop the KV cache into fixed-size blocks (e.g., 16 tokens). Each sequence gets a block table (logical block → physical block), exactly like OS virtual memory page tables. Allocate blocks on demand as the sequence grows; the attention kernel gathers K/V through the block table (one indirection).
Wins, each a direct allocator consequence:
- No external fragmentation — all blocks identical size, any free block fits any sequence.
- Near-zero internal fragmentation — waste is at most one partial block per sequence (vs reserving max length).
- 2–4× more concurrent sequences in the same HBM → 2–4× throughput, because more sequences = bigger batches = better decode amortization (Ch. 5).
- Sharing becomes trivial — blocks are relocatable units that multiple sequences can reference (Ch. 4).
The one tradeoff: block size. Bigger blocks = smaller block tables, more internal fragmentation; smaller blocks = more table overhead, finer packing. 16 tokens is the common sweet spot. Lab 01 lets you measure the curve.
Chapter 4: Prefix Sharing — Copy-on-Write for Prompts
The highest-ROI feature for real workloads, and a clean payoff of paging:
Chat and agent APIs send the same long system prompt on every request ("You are a helpful assistant... [2000 tokens of instructions]"). Naively, each request recomputes and stores that prompt's KV — wasted compute (prefill) and wasted memory (identical KV copies).
Prefix caching: hash the prompt prefix; if its KV blocks already exist, share them — multiple sequences' block tables point at the same physical blocks. New requests skip the shared prefill entirely (TTFT collapses) and add no memory for the shared part.
The mechanism is copy-on-write (Lab 01 implements it): shared blocks are
read-only and ref-counted; when a sequence diverges (its tokens differ from the
shared prefix, or it writes into a shared block), that block is copied first.
This is fork() semantics for KV cache — and beam search / parallel sampling
(N continuations of one prompt) use the same machinery.
ROI: for a chat assistant with a 2K-token system prompt at 1000 req/s, prefix caching can cut prefill compute by >90% and is often the single biggest serving win. As a platform decision: enable it, measure hit rate, and expose prompt structure guidance to customers (stable prefix first) — a product lever, not just a flag.
Chapter 5: Continuous Batching — Iteration-Level Scheduling
Batching is the master throughput lever (decode reads all weights per step regardless of batch size — Phase 01 Ch. 7 — so batch B amortizes that read B ways). But how you batch decides everything.
-
Static batching: collect B requests, run them together until all finish. Fails for LLMs: requests finish at wildly different lengths, so the batch runs at the speed of its longest member while finished slots sit idle (head-of-line blocking), and new arrivals wait for the whole batch (batch-formation latency). GPU utilization collapses under length variance.
-
Continuous batching (Orca, 2022; every modern engine): schedule at iteration granularity. After each decode step: retire finished sequences (free their KV blocks), admit waiting requests (run their prefill, add to the batch). The batch composition changes every step; utilization stays high under length variance; new requests start almost immediately. Typical win: 3–10× throughput over static. Lab 02 measures it.
The induced problems (which the rest of serving solves):
- Admitting a request commits to its KV growth → memory pressure → preemption/swapping (Ch. 7).
- A long prefill entering the batch stalls every decode that iteration → chunked prefill (slice the prompt across iterations) and prefill/decode disaggregation (separate GPU pools, ship KV between them).
- The latency-throughput dial: deeper batches = more throughput, worse TPOT per user (more work per iteration). You don't maximize batch depth — you maximize goodput (Ch. 6).
Chapter 6: Metrics and SLOs — Why Goodput Is the Only Honest Number
The serving vocabulary, and the one metric that resists gaming:
- TTFT (time to first token) = queue wait + prefill. The "feels responsive" metric; dominated by prompt length and batch interference.
- TPOT / ITL (time per output token) = decode speed. The "streams smoothly" metric; human reading speed ~10–15 tok/s is the UX floor.
- E2E latency = TTFT + TPOT × output_len.
- Throughput = total tokens/sec across the fleet.
- Goodput = throughput that meets SLO. The honest metric, because raw throughput is gameable: you can inflate tokens/sec by running enormous batches that blow every request's TPOT past the SLO — "high throughput," zero happy users. Goodput counts only tokens delivered within their latency target.
Why this is a leadership point: vendors and internal teams report throughput because it's bigger; you require goodput at stated p50/p99 SLOs under a realistic arrival process (Poisson, not back-to-back) with the real prompt/output length distribution. A benchmark missing those caveats is marketing (same discipline as Phase 01 Ch. 10's spec-sheet skepticism). Lab 02 shows goodput peaking at an interior batch depth — proof that "maximize batch" is wrong and "maximize goodput" is right.
Chapter 7: Memory Pressure — Preemption, Swapping, Recompute
Continuous batching admits requests optimistically; sometimes their combined KV growth exceeds HBM. The platform must degrade gracefully:
- Preemption: evict a running sequence to free its blocks. Two recovery
options when it's rescheduled:
- Recompute: throw away its KV, re-run prefill later. Cheap memory, costs compute (re-prefill). Best when prefill is short.
- Swap: copy its KV blocks to host RAM, copy back later. Costs the 50× PCIe bandwidth cliff (Phase 01 Ch. 9), but preserves the work. Best when the KV is large and prefill was expensive.
- Admission control: the cleaner lever — don't admit a request you can't sustain; queue it. This protects p99 for admitted requests (the goodput argument) at the cost of TTFT for queued ones. SLO-aware admission (shed or queue load to protect tails) is the production posture.
The policy choice (recompute vs swap vs reject) is workload-dependent and is a real config surface you own. vLLM exposes both; knowing why you'd pick each is the Phase-07 depth interviewers probe.
Chapter 8: The Engine Landscape — A Platform Decision
The JD wants "multiple inference engines." The map, as a selection problem:
- vLLM: PagedAttention's home; best-in-class throughput, broad model support, fast-moving OSS, great for heterogeneous/multi-model fleets and rapid adoption of new models. The default starting point.
- TensorRT-LLM: NVIDIA's engine; compiles model-specific optimized kernels (ahead-of-time, per-GPU — Phase 03's AOT story) for peak latency/throughput on NVIDIA hardware — at the cost of build complexity and NVIDIA lock-in. Best when you've fixed your model and hardware and need the last 20%.
- SGLang: strong on structured generation, complex prompting, and RadixAttention (advanced prefix sharing); excellent for agent/tool workloads.
- TGI (HF): solid, integrated with the HF ecosystem; common for simpler deployments.
The platform reality is multi-engine: different models/SLAs/hardware want different engines, and you abstract them behind a common gateway (the capstone). The selection axes: throughput vs latency, model coverage vs peak per-model, build complexity vs portability (and the hardware-lock-in question that Phase 09's HAL exists to manage). "We standardized on one engine" is rarely the right answer at scale; "we have a serving abstraction and route models to the engine that serves them best" is.
Chapter 9: Sovereign and Air-Gapped Serving
The JD's regulated-industry nice-to-haves (defence, finance, healthcare, government) reshape serving:
- No phone-home: no telemetry, no model-download-at-runtime, no license server callout. Everything — weights, engine, license validation (Phase 11) — must work offline. This changes your packaging, your update story, and your observability (local-only, Phase 10).
- On-prem capacity is fixed: no elastic cloud burst, so admission control and goodput discipline (Ch. 6) matter more — you can't scale out of a traffic spike.
- License enforcement without a server: signed, offline-verifiable licenses with hardware binding and grace periods (Phase 11 Lab 02) — because the customer's airtight network can't reach your licensing API.
- Auditability: regulated customers need request/response audit trails, often with data residency guarantees — a serving-layer feature, not an afterthought.
This is where Phases 07, 09 (portability for on-prem hardware diversity), and 11 (licensing/protection) converge into the JD's "sovereign AI" pitch — and it's a genuine market differentiator, because most serving stacks assume the cloud.
Lab Walkthrough Guidance
Order: Lab 01 → Lab 02 (the batching sim's admission logic depends on the paged allocator's concepts).
Lab 01 (paged KV allocator):
python solution.py; read the allocator against Ch. 3–4 —BlockAllocator,BlockTable, the COW logic.- Reproduce the memory comparison: naive contiguous vs paged vs paged+prefix. Tie each number to Ch. 2–3 (internal/external fragmentation eliminated).
- The prefix-sharing demo: N sequences sharing a system prompt collapse to one physical copy; at divergence, COW kicks in. Trace the ref-counts.
- Extensions: eviction (recompute vs swap, Ch. 7), then a beam-search fork.
Lab 02 (continuous batching + goodput):
python solution.py; read the scheduler against Ch. 5–6.- Reproduce static-vs-continuous throughput; then sweep
max_batchand find where goodput peaks (interior, not max) — the Ch. 6 thesis, measured. - Watch a long prefill stall decodes; reason about chunked prefill (extension).
- Extension: integrate Lab 01's prefix cache (TTFT collapse on cache hits).
Success Criteria
- You can derive KV-cache bytes and concurrent-sequence capacity for any model/context cold (Ch. 2)
- You can explain PagedAttention as an allocator and the block-size tradeoff (Ch. 3)
- You can explain prefix sharing + COW and when it's the biggest win (Ch. 4)
- You can contrast static vs continuous batching and explain the latency-throughput dial (Ch. 5)
- You can define goodput and argue why it beats throughput as a fleet metric (Ch. 6) — and your Lab 02 result proves it
- You can choose among vLLM/TensorRT-LLM/SGLang from requirements (Ch. 8)
- You can describe what air-gapped serving changes (Ch. 9)
Interview Q&A
Q1: Derive the KV cache size for Llama-3-8B at 8K context and tell me what it
implies for serving.
A: KV bytes = 2 × layers × kv_heads × d_head × seq × bytes = 2·32·8·128·8192·2 ≈
1.07 GB per sequence (8 KV heads because of GQA; without it, 32 heads → 4×
more). On a 40 GB GPU holding 16 GB of weights, ~22 GB remains → ~20 concurrent
8K sequences max, and that's the hard cap on batch size — not compute. So
serving economics are dominated by fitting more sequences: GQA (already in the
8), KV quantization (FP8 → 2×), and paging to stop wasting the budget on
max-length reservations. Capacity planning is (HBM − weights) ÷ (KV/token × context).
Q2: Explain PagedAttention, and why you'd call it an allocator rather than an attention algorithm. A: The attention math is unchanged; what changes is KV memory management. Naive serving reserves a contiguous max-length KV buffer per sequence, wasting 60–80% (you don't know the output length) and fragmenting under variable lengths. PagedAttention stores KV in fixed-size blocks (≈16 tokens) with a per-sequence block table mapping logical to physical blocks — OS virtual memory for the KV cache. That eliminates external fragmentation (uniform blocks) and caps internal fragmentation at one partial block per sequence, yielding 2–4× more concurrent sequences and therefore throughput. It's an allocator because the problem it solves is allocation — fixed-size paging for the dominant consumer, exactly the generic fragmentation fix; the attention kernel just gains one indirection through the block table.
Q3: Static vs continuous batching, and what is goodput? A: Static batching forms a fixed batch and runs it to completion — for LLMs that means the batch runs at its longest member's speed while finished sequences waste their slots (head-of-line blocking), and arrivals wait for batch formation; utilization collapses under length variance. Continuous batching schedules at iteration granularity: after every decode step it retires finished sequences and admits new ones, so the batch is always full of useful work — typically 3–10× throughput. Goodput is the throughput that meets the latency SLO; it matters because raw throughput is gameable by oversized batches that blow everyone's TPOT — high tokens/sec, zero satisfied users. You tune the batch depth to maximize goodput (an interior optimum), not throughput.
Q4: A chat product has a 2000-token system prompt on every request. What do you do? A: Prefix caching. Hash the shared prefix and share its KV blocks across all requests via copy-on-write: new requests skip re-prefilling the system prompt (TTFT collapses) and add zero memory for the shared part; blocks are ref-counted and copied only when a sequence diverges. For a 2K-token prompt at high QPS this can cut prefill compute >90% — usually the single biggest win. As a platform move I'd also advise the product to keep the stable instructions first (so the cacheable prefix is maximal) and monitor cache hit rate as a first-class metric. SGLang's RadixAttention generalizes this to tree-structured shared prefixes for agent workloads.
Q5: Under memory pressure mid-generation, what are your options? A: First, admission control — ideally don't admit a request you can't sustain; queue it (protects p99 for admitted requests, the goodput argument). For already- running sequences, preempt and recover one of two ways: recompute (drop the KV, re-prefill on reschedule — cheap memory, costs compute; good when prefill is short) or swap (copy KV to host RAM and back — preserves work but pays the 50× PCIe cliff; good when KV is large and prefill was expensive). The choice is workload-dependent and a config surface I'd own. The anti-pattern is admitting unboundedly and OOM-crashing the engine, which kills every in-flight request.
Q6 (leadership): How do you choose inference engines for a multi-model platform? A: I don't standardize on one. Different models, SLAs, and hardware want different engines: vLLM as the throughput-strong, broad-coverage default and for fast adoption of new models; TensorRT-LLM where a model and GPU are fixed and we need the last 20% of latency/throughput (accepting build complexity and NVIDIA lock-in — the lock-in our Phase 09 HAL exists to bound); SGLang for agent/structured-generation workloads via RadixAttention. The architecture is a common serving gateway abstracting the engines, routing each model to the engine that serves it best, with goodput-at-SLO (not vendor throughput numbers) as the selection evidence. For sovereign customers, add the constraint that the engine must run fully air-gapped with offline licensing — which narrows the field and shapes packaging. The platform value is the abstraction + routing + honest benchmarking, not any single engine.
References
- Kwon et al., "Efficient Memory Management for Large Language Model Serving with PagedAttention" (vLLM, SOSP 2023) — https://arxiv.org/abs/2309.06180
- Yu et al., "Orca: A Distributed Serving System for Transformer-Based Generative Models" (OSDI 2022) — continuous batching
- Zheng et al., "SGLang: Efficient Execution of Structured Language Model Programs" (RadixAttention) — https://arxiv.org/abs/2312.07104
- Agrawal et al., "Sarathi-Serve: Taming Throughput-Latency Tradeoff" (chunked prefill) — https://arxiv.org/abs/2403.02310
- vLLM docs + source (
block_manager,scheduler) — https://docs.vllm.ai , https://github.com/vllm-project/vllm - NVIDIA TensorRT-LLM — https://github.com/NVIDIA/TensorRT-LLM
- DistServe (prefill/decode disaggregation) — https://arxiv.org/abs/2401.09670
- Cross-track: LLM Inference Engineer Phase 09 WARMUP (model/algorithm depth), Phase 05 WARMUP Ch. 5 (paging as allocator design)
🛸 Hitchhiker's Guide — Phase 07: LLM Serving & KV-Cache Management
Read this if: you can run
vllm servebut couldn't yet derive its memory limits, explain PagedAttention as an allocator, or defend "goodput, not throughput" to a skeptical exec. Field notes; derivations in the WARMUP and the LLM Inference track Phase 09.
0. The 30-second mental model
Decode is bandwidth-bound ⇒ batching is the throughput lever. The KV cache grows per token and caps batch size ⇒ manage it like memory: page it (PagedAttention), share it (prefix caching), shrink it (GQA, FP8 KV), evict it under pressure. Schedule at iteration granularity (continuous batching). Optimize goodput (throughput meeting SLO), never raw throughput.
1. The one formula
KV bytes = 2 · layers · kv_heads · d_head · seq_len · bytes_per_elem
concurrent_seqs ≈ (HBM − weights) / (KV_bytes_per_token · context)
Llama-3-8B, 8K, BF16 ≈ 1 GB/seq ⇒ ~20 seqs on a 40 GB GPU. The cache, not compute, caps concurrency. Every optimization improves one term.
2. The KV-cache management ladder
| Technique | What it does | Win |
|---|---|---|
| GQA | fewer KV heads | 4–8× smaller cache (model-level) |
| FP8/INT8 KV | fewer bytes/elem | 2–4× smaller |
| PagedAttention | fixed blocks + block table | kills fragmentation → 2–4× more seqs |
| Prefix caching (COW) | share system-prompt KV | >90% prefill cut for chat |
| Eviction (recompute/swap) | free blocks under pressure | graceful degradation |
PagedAttention = OS virtual memory for the KV cache. Block size ≈16 tokens (table overhead vs internal fragmentation tradeoff).
3. The batching/scheduling ladder
- Static = collect, run to completion. Head-of-line blocking + formation delay. Don't.
- Continuous (Orca) = iteration-level admit/retire. 3–10× throughput. The standard.
- Chunked prefill = slice long prompts so they don't stall decodes.
- Disaggregation = separate prefill and decode GPU pools, ship KV between. Frontier-scale.
- The dial: deeper batch = more throughput, worse per-user TPOT. Tune for goodput, which peaks at an interior batch depth (Lab 02 proves it).
4. Metrics that resist gaming
| Metric | Means | Gotcha |
|---|---|---|
| TTFT | queue + prefill | prompt length, batch interference |
| TPOT/ITL | decode speed | UX floor ~10–15 tok/s |
| throughput | total tok/s | gameable by oversized batches |
| goodput | tok/s meeting SLO | the honest fleet metric |
Require goodput at p50/p99 SLOs, Poisson arrivals, real length distribution. Anything else is marketing (Phase 01 Ch. 10 energy).
5. Engine selection card
| Engine | Strength | Choose when |
|---|---|---|
| vLLM | throughput, model coverage, OSS velocity | default; multi-model fleets |
| TensorRT-LLM | peak latency/throughput (AOT kernels) | fixed model+GPU, last 20%, accept lock-in |
| SGLang | structured gen, RadixAttention prefix trees | agents, tool use, complex prompting |
| TGI | HF integration, simplicity | simpler deployments |
Platform answer: a serving gateway abstracting engines, route per model, select on goodput evidence. Not "one engine to rule them all."
6. Air-gapped/sovereign changes
No phone-home (offline weights, offline license — Phase 11), fixed on-prem capacity (admission/goodput matter more, no elastic burst), local-only observability (Phase 10), audit trails + data residency. Most stacks assume cloud; building serving that doesn't is the JD's sovereign-AI differentiator.
7. War stories
- "Throughput great in the benchmark, users furious" — benchmark ran giant batches back-to-back; real Poisson traffic blew p99 TTFT. Switched to goodput-at-SLO reporting; cut batch depth; users happy, "throughput" down 15%.
- "OOM crash takes down 80 in-flight requests" — no admission control, optimistic batching hit the KV wall. Added admission + preempt-with-recompute.
- "Chat TTFT randomly 2s" — no prefix caching; every request re-prefilled a 3K system prompt. Enabled it; TTFT → 80ms on cache hits.
- "New model 40% slower on TRT-LLM than vLLM" — TRT-LLM engine built for the old shape; nobody rebuilt. AOT engines need a rebuild per model/GPU (Phase 03). Routed it to vLLM until rebuilt.
8. Exit bar
You can derive KV memory and capacity cold, explain PagedAttention/prefix caching/continuous batching as the allocator-and-scheduler problems they are, defend goodput over throughput, select engines from requirements, and describe air-gapped serving. Plus you've built the paged allocator and the batching sim. Next: Phase 08 — when one GPU isn't enough, how collectives and topology turn many GPUs into one.
Lab 01 — Paged KV-Cache Allocator + Prefix Sharing (Python)
Phase: 07 — LLM Serving | Difficulty: ⭐⭐⭐⭐☆ | Time: 5–8 hours Language: Python (stdlib) | Hardware: none
Concept primer:
../WARMUP.mdCh. 2–4.
Run
python solution.py # correctness tests + memory comparison + prefix-share demo
0. The mission
Build PagedAttention's memory manager (not the attention math — the allocator, which is the actual innovation, Phase 05 Ch. 5):
BlockAllocator— a pool of fixed-size KV blocks (16 tokens each).BlockTable— a sequence's logical→physical block map (OS page table).- Copy-on-write prefix sharing — multiple sequences referencing the same physical blocks for a shared system prompt, with ref-counting and copy-on- divergence.
Then measure the three regimes the WARMUP describes.
1. What the output shows
== memory efficiency: sequences that fit in a 4096-block pool ==
naive contiguous (reserve max_len=2048) : 32 sequences
paged (allocate-on-demand, actual=768) : 85 sequences (2.7x)
== prefix sharing: 50 requests, shared 1024-token system prompt ==
without sharing : 3350 blocks used
with sharing : 214 blocks used (15.7x less; shared prefix stored once)
copy-on-write : divergence forked block; refcounts correct
(The prefix-sharing ratio is large here because the shared 1024-token prompt dwarfs each request's 40-token generation — exactly the chat-serving regime where prefix caching pays off most.)
- Naive contiguous reserves
max_lenper sequence → most blocks unused (internal fragmentation), so few sequences fit. Paged allocates only what each sequence actually uses → 2–4× more concurrent sequences (WARMUP Ch. 3). - Prefix sharing stores the shared system prompt's blocks once; 50 requests reference them instead of copying — the chat-serving win (WARMUP Ch. 4).
- Copy-on-write forks a shared block only when a sequence diverges, ref-counts staying correct.
2. Reading order (solution.py)
BlockAllocator—allocate/free, the free list, ref-counts.BlockTable/Sequence— append tokens, grow blocks on demand.share_prefix+ the COW path — ref-count increment on share, copy on write to a shared (refcount>1) block.- The three demos in
main().
3. Extensions
- Eviction under pressure: when the pool is full, evict an LRU sequence; implement both recovery policies — recompute (drop KV) and swap (move blocks to a host-side pool) — and compare their cost (WARMUP Ch. 7).
- Beam search / parallel sampling: fork a sequence into N continuations sharing the prompt's blocks; show COW only on the diverging tokens.
- Block-size sweep: vary block size (4/8/16/32 tokens); plot internal fragmentation vs block-table size — find the sweet spot (WARMUP Ch. 3).
- Hash-based prefix cache: auto-detect shared prefixes by hashing block contents (RadixAttention-lite) instead of explicit sharing.
4. Common pitfalls
- Freeing a shared block while refcount > 1 — corrupts other sequences;
freemust decrement and only reclaim at zero. - Writing into a shared block without COW — silently corrupts every sharer
(the test
test_cow_on_divergencecatches it). - Off-by-one at block boundaries — token T lives in block
T // block_sizeat offsetT % block_size; the append logic must roll to a new block exactly at the boundary. - Counting "blocks used" as allocated-not-shared — the memory win is in physical blocks; shared blocks count once.
5. What this lab proves about you
You've implemented the core of vLLM's memory manager and can explain it as the
allocator it is — fixed-size paging + COW sharing for the dominant memory
consumer. Reading vllm/core/block_manager_v2.py is now a code review; you can
reason about block size, prefix-cache hit rate, and eviction policy as the
platform knobs they are.
Lab 02 — Continuous Batching Simulator + Goodput (Python)
Phase: 07 — LLM Serving | Difficulty: ⭐⭐⭐⭐☆ | Time: 5–8 hours Language: Python (stdlib) | Hardware: none
Concept primer:
../WARMUP.mdCh. 5–6.
Run
python solution.py
0. The mission
Simulate an LLM server under Poisson request load and prove two things with numbers: continuous batching beats static, and goodput peaks at an interior batch depth (so "maximize batch" is wrong, "maximize goodput" is right — WARMUP Ch. 6).
The model is a discrete-iteration simulator: each iteration advances every in-batch sequence one decode step (or processes a prefill); the per-iteration time grows with batch size (more work per step → higher TPOT), which is what creates the latency-throughput dial.
1. What the output shows
== static vs continuous batching (Poisson load) ==
policy thru(tok/s) p50_TTFT p99_TTFT p50_TPOT completed
static 255 35774 75020 31 400
continuous 378 33 1669 31 400
-> continuous: 1.5x throughput, 45x lower p99 TTFT
== goodput vs batch depth (SLO: TTFT<300ms, TPOT<40ms) ==
max_batch throughput goodput
4 209 3 <- batch too small: saturates -> TTFT SLO fails
8 337 36 <- still saturating
16 378 321 <- both SLOs met: goodput ~= throughput
32 368 0 <- TPOT SLO blown (iter time 47ms > 40ms)
64 350 0 <- deeper batch, worse goodput
goodput-optimal batch depth: 16
- Static suffers batch-formation delay (waits for a whole batch to arrive) and head-of-line blocking (the batch runs at its longest member's length while short requests waste their slots) → enormous TTFT. Continuous admits and retires per iteration → here 1.5× throughput and 45× lower p99 TTFT (WARMUP Ch. 5). At moderate load the headline win is latency; under saturation it becomes throughput.
- Goodput sweep — the headline result: raw throughput is highest at batch 32, but goodput collapses to zero there because per-token time (47 ms) blows the TPOT SLO. Too-small batches saturate and blow the TTFT SLO. Goodput peaks at an interior depth (16) — proof the objective is goodput, not max batch (WARMUP Ch. 6).
The per-iteration time scales with the configured
max_batch(real engines graph-capture/pad decode kernels to the batch width), which is what makes a larger batch genuinely cost every user's TPOT. Production continuous-vs-static gains are often larger (3–10×) once variable arrivals and prefill interference — beyond this model — are included.
2. Reading order (solution.py)
Request/ Poisson arrival generation.simulate_static— fixed batches, run to completion.simulate_continuous— the iteration loop: retire finished, admit waiting, step the batch; per-iteration time as a function of batch size.- The goodput accounting: a request's tokens count only if its TTFT and TPOT met the SLO.
3. Extensions
- Chunked prefill: split long prefills across iterations so they don't stall decodes; measure the p99 TPOT improvement under a bimodal (short+long prompt) workload (WARMUP Ch. 5).
- Prefix-cache integration: requests sharing a system prompt skip prefill (use Lab 01's idea) — watch TTFT collapse on cache hits.
- SLO-aware admission: reject/queue requests when admitting them would blow the batch's TPOT SLO; show goodput protected under overload.
- Prefill/decode disaggregation: separate prefill and decode "pools"; model the KV hand-off cost; compare interference vs the chunked-prefill approach.
4. Common pitfalls
- Constant per-iteration time regardless of batch — then there's no latency-throughput dial and goodput never turns over. Per-iteration time must grow with batch size (the simulator models this).
- Counting throughput as goodput — goodput must filter by SLO compliance, or the whole lesson disappears.
- Poisson vs back-to-back arrivals — back-to-back inflates throughput unrealistically (WARMUP Ch. 6); the sim uses Poisson.
- Ignoring prefill cost — a long prefill should visibly stall the iteration it enters (motivating chunked prefill).
5. What this lab proves about you
You can quantify the two serving truths that matter most to the P&L: continuous batching's throughput win and the goodput-optimal operating point. You can walk an exec through "why we don't just crank the batch size" with a graph, and you can design admission/SLO policy from the goodput curve — the platform-leadership deliverable this JD hires for.
Phase 08 — Distributed Systems & HPC Collectives
Difficulty: ⭐⭐⭐⭐⭐ | Estimated Time: 2 weeks Roles supported: Head of Engineering [GPU], Distributed Systems Engineer, HPC/ML-Systems Engineer Hardware needed: none — the collectives lab runs over real OS sockets on one machine (multi-process); the topology lab is a simulator
Why This Phase Exists
The JD wants "distributed systems, HPC" and "AI infrastructure at production scale." A single GPU is never enough at the frontier: training a large model and serving it with tensor parallelism both turn many GPUs into one logical device, and the glue is collective communication — all-reduce, all-gather, reduce-scatter — running over NVLink and InfiniBand. When training "mysteriously" runs at half speed, or a node failure hangs an 1,024-GPU job, the cause is almost always in this layer.
You'll implement ring all-reduce over real sockets (the algorithm NCCL uses) and benchmark it against the theoretical bandwidth bound, then build a topology-aware collective simulator that shows why placement (Phase 06) and bandwidth (Phase 01 Ch. 9) decide distributed throughput — and where failures turn into hangs.
Concepts
- Why distribute: model/data don't fit one GPU; the parallelism taxonomy
- Data parallel (replicate model, split batch, all-reduce gradients)
- Tensor parallel (split each layer across GPUs, all-reduce per layer — NVLink-bound)
- Pipeline parallel (split layers into stages, micro-batch, bubble overhead)
- Expert/sequence/context parallel (overview) and how they compose (3D/4D parallelism)
- Collective operations: broadcast, reduce, all-reduce, all-gather, reduce-scatter, all-to-all — and their communication volumes
- Ring all-reduce: the algorithm and its
2(N-1)/N × databandwidth optimality; reduce-scatter + all-gather decomposition - Tree vs ring vs hierarchical collectives; latency vs bandwidth regimes; small vs large messages
- NCCL: what it does, how it picks algorithms/topology,
NCCL_DEBUG, the env knobs that matter - Topology: NVLink/NVSwitch islands, PCIe, InfiniBand/RoCE, rails, GPUDirect RDMA; why placement (Phase 06) sets collective bandwidth
- Overlap: computation/communication overlap, bucketing gradients, the critical path
- Failure domains at scale: a single GPU/NIC failure hangs a synchronous collective; detection, checkpointing, elastic/fault-tolerant training; the "1% of nodes fail daily at 10k-GPU scale" reality
- Serving-side distribution: tensor-parallel inference, KV-cache sharding, prefill/decode disaggregation traffic (Phase 07)
Labs
Lab 01 — Ring All-Reduce Over Real Sockets (Python)
| Field | Value |
|---|---|
| Goal | Implement ring all-reduce across N OS processes communicating over TCP sockets; verify correctness and benchmark against the 2(N-1)/N bandwidth bound. |
| Concepts | Reduce-scatter + all-gather, ring topology, bandwidth optimality, why all-reduce volume is ~2× the data regardless of N. |
| Steps | 1) python solution.py --world 4 — launches 4 worker processes, runs ring all-reduce on a vector, verifies the result equals the naive sum, prints bandwidth vs the bound. 2) Read the ring algorithm against WARMUP Ch. 3. 3) Compare against a naive all-gather-then-sum (more bytes). 4) Extensions: tree all-reduce, bucketing, a node-failure hang demo. |
| Stack | Python (stdlib socket, multiprocessing) |
| Output | A working multi-process ring all-reduce + a bandwidth report (achieved vs 2(N-1)/N bound). |
| How to Test | Built-in: all-reduced vector is bit-equal to the reference sum across all ranks; ring moves ≈2(N-1)/N × data per rank (verified by byte counting), beating the naive O(N) approach. |
| Talking Points | Why ring all-reduce is bandwidth-optimal and N-independent in per-link volume; the reduce-scatter/all-gather decomposition; when tree beats ring (latency-bound small messages); how this maps to gradient all-reduce in DDP. |
| Resume Bullet | "Implemented ring all-reduce over TCP sockets across N processes; verified bit-exact reduction and measured per-rank communication volume at 2(N-1)/N × data, matching the bandwidth-optimal bound NCCL targets." |
| Extensions | Tree all-reduce + compare latency on small messages; gradient bucketing/overlap; a failure-injection mode where one rank dies and the collective hangs — then add a timeout + detection. |
Lab 02 — Topology-Aware Collective Simulator (Python)
| Field | Value |
|---|---|
| Goal | Model a multi-node GPU cluster's interconnect (NVLink islands, InfiniBand between nodes) and compute collective time for different parallelism placements — proving topology decides throughput. |
| Concepts | Bandwidth hierarchy (NVLink/IB), tensor-parallel-within-node, data-parallel-across-node, rail topology, the cost of a badly-placed collective. |
| Steps | 1) python solution.py — models all-reduce time for TP/DP placements across topologies, prints a comparison. 2) Read the cost model against WARMUP Ch. 6. 3) Reproduce: TP within an NVLink island is N× faster than TP spread across nodes (the Phase 06 topology argument, quantified). 4) Extensions: pipeline-bubble model, hierarchical all-reduce, failure-domain blast radius. |
| Stack | Python (stdlib) |
| Output | A topology cost model + a placement-comparison table (collective time per strategy). |
| How to Test | Built-in asserts: TP-within-island beats TP-across-nodes by the NVLink/IB bandwidth ratio; hierarchical all-reduce beats flat across nodes; placement matters more than algorithm at scale. |
| Talking Points | Why tensor parallelism must stay within NVLink islands; how the scheduler (Phase 06) and the collective (this phase) jointly determine throughput; the failure-domain blast radius of different placements. |
| Resume Bullet | "Built a topology-aware collective cost model quantifying the NVLink-vs-InfiniBand placement gap; demonstrated tensor-parallel-within-island delivering an order-of-magnitude lower all-reduce time than cross-node placement." |
| Extensions | Pipeline-parallel bubble model; hierarchical (intra-then-inter-node) all-reduce; failure-domain analysis (how many GPUs one NIC/switch failure stalls). |
Deliverables Checklist
- Ring all-reduce: correct across N processes; bandwidth report matches the bound
- You can derive the 2(N-1)/N all-reduce volume and explain why it's N-independent
- Topology sim: placement-comparison table reproduced and explained
- You can explain TP/DP/PP and why TP must stay on NVLink
- You can describe how a single-GPU failure hangs a synchronous job and the mitigations
- One page: "our platform's multi-GPU/multi-node strategy" (feeds Phase 12)
Interview Relevance
- "Derive ring all-reduce's communication cost. Why is it bandwidth-optimal?"
- "Compare data, tensor, and pipeline parallelism. Which is NVLink-bound and why?"
- "Your 512-GPU training run is at 60% MFU. Where do you look?" (overlap, topology, stragglers)
- "One GPU fails in a 1024-GPU job. What happens, and how do you make it survivable?"
- "Why does tensor-parallel placement across nodes destroy throughput?"
Warmup Guide — Distributed Systems & HPC Collectives
Zero-to-expert primer for Phase 08. Builds on Phase 01 Ch. 9 (interconnect) and Phase 06 (placement). No distributed-training background assumed. By the end you can derive collective costs, reason about parallelism strategies, and design for failure at scale.
Table of Contents
- Chapter 1: Why Distribute, and the Parallelism Taxonomy
- Chapter 2: Collective Operations — the Vocabulary
- Chapter 3: Ring All-Reduce — the Bandwidth-Optimal Algorithm
- Chapter 4: Tree, Hierarchical, and When Each Wins
- Chapter 5: NCCL — the Production Collective Library
- Chapter 6: Topology — NVLink Islands, InfiniBand, and Placement
- Chapter 7: Overlap — Hiding Communication Behind Compute
- Chapter 8: Failure Domains at Scale
- Chapter 9: Distribution on the Serving Side
- Lab Walkthrough Guidance
- Success Criteria
- Interview Q&A
- References
Chapter 1: Why Distribute, and the Parallelism Taxonomy
Zero background: one GPU has finite HBM (80 GB) and finite compute. A 175B-parameter model in FP16 is 350 GB of weights alone — it doesn't fit. Even when a model fits, one GPU's throughput caps training time at "months." So we split the work across many GPUs and make them cooperate. The cooperation is the hard part, and it's all communication.
The four ways to split (you must be able to contrast these cold):
- Data parallel (DP): every GPU holds a full copy of the model, processes a different slice of the batch, and they all-reduce gradients each step to stay in sync. Simple, the workhorse. Communication: one all-reduce of the full gradient per step. Scales batch size, not model size.
- Tensor parallel (TP): split each layer's matrices across GPUs (e.g., each GPU computes part of every attention head), with an all-reduce inside every layer to combine partial results. Lets a model bigger than one GPU run — but the per-layer all-reduce is on the critical path, so TP must live on NVLink (Ch. 6); across slow links it's catastrophic.
- Pipeline parallel (PP): split the model's layers into stages on different GPUs; micro-batches flow through like an assembly line. Communication is just activations between adjacent stages (cheap), but you pay a pipeline bubble (stages idle at fill/drain). Lets very deep models run.
- Expert / sequence / context parallel: (overview) shard along other axes — MoE experts, the sequence dimension for long context. They compose: frontier training is "3D/4D parallelism" — e.g., TP within a node, PP across a few nodes, DP across the rest.
The mental model: DP trades communication for batch scale; TP trades communication for model scale (on fast links only); PP trades latency (bubble) for model depth. Choosing and composing them is a topology problem (Ch. 6).
Chapter 2: Collective Operations — the Vocabulary
A collective is a communication pattern over a group of ranks. The ones that matter, with their communication volume (V = data size per rank, N = ranks):
| Collective | Each rank ends with | Volume intuition |
|---|---|---|
| Broadcast | root's data | one→all |
| Reduce | root gets the sum | all→one |
| All-reduce | everyone gets the sum | the big one: ~2V per rank (Ch. 3) |
| All-gather | everyone gets all ranks' data | (N-1)/N · NV per rank |
| Reduce-scatter | each gets a reduced slice | (N-1)/N · V per rank |
| All-to-all | transpose: each sends a piece to each | the MoE/expert-parallel cost |
The headline fact: all-reduce = reduce-scatter + all-gather, and each of those moves (N-1)/N · V, so all-reduce moves ≈ 2V per rank, independent of N — the result you'll prove in Lab 01. This is why data-parallel training scales: adding GPUs doesn't increase per-GPU communication volume (only latency terms grow). All-reduce of gradients is the single most important collective in ML.
Chapter 3: Ring All-Reduce — the Bandwidth-Optimal Algorithm
The algorithm NCCL uses for large messages, and the centerpiece of Lab 01.
Setup: N ranks in a logical ring (each talks to its left and right neighbor). The gradient vector is chopped into N chunks. Two phases:
Phase 1 — reduce-scatter (N-1 steps): in step k, each rank sends one chunk to its right neighbor and receives a chunk from its left, adding the received chunk to its local copy. After N-1 steps, each rank holds the fully reduced sum of one chunk (a different chunk per rank).
Phase 2 — all-gather (N-1 steps): same ring motion, but now ranks forward the completed chunks (no addition) until everyone has all N reduced chunks.
The cost (the result to know cold): each rank sends/receives (N-1) chunks of size V/N in each phase = 2 phases × (N-1) × V/N = 2·(N-1)/N · V ≈ 2V per rank. As N→∞ this approaches exactly 2V, independent of N. Compare the naive "all-gather everything then sum locally": N·V per rank — N times worse. That N-independence is why ring all-reduce made large-scale data-parallel training practical (Baidu's 2017 result that propagated into every framework).
Why bandwidth-optimal: information-theoretically, every rank's contribution must reach every other rank, and each rank must receive the full reduced result — 2V is the lower bound on bytes that must cross each rank's links. Ring hits it. (The cost is N-1 latency hops per phase, so ring is bandwidth-optimal but latency-linear — hence trees for small messages, Ch. 4.)
Chapter 4: Tree, Hierarchical, and When Each Wins
Ring is optimal in bandwidth but its latency grows linearly in N (2(N-1) sequential hops). For small messages (latency-dominated) or many ranks, other structures win:
- Tree all-reduce: reduce up a binary tree to the root, broadcast down. Latency O(log N) instead of O(N) — far better for small messages. Bandwidth is worse (root links are hot), but small messages don't saturate bandwidth anyway. NCCL auto-switches ring↔tree by message size.
- Hierarchical / two-level: reduce within each NVLink island first (fast), then all-reduce across islands over IB (one representative per node), then broadcast back down. This matches the bandwidth hierarchy (Ch. 6) — you do the big-volume reduction on the fast intra-node links and only the small cross-node step on the slow links. Lab 02 quantifies the win.
- Double binary tree (NCCL's default for many cases): two trees using complementary links, recovering bandwidth while keeping log-latency.
The takeaway for a leader: there's no single best collective algorithm — NCCL picks by message size, rank count, and topology, and your job is to give it a good topology (Ch. 6) and large enough messages (bucketing, Ch. 7), not to hand-pick algorithms.
Chapter 5: NCCL — the Production Collective Library
NVIDIA Collective Communications Library — the layer every framework (PyTorch DDP/FSDP, Megatron, DeepSpeed) calls for GPU collectives.
What it does that you won't reimplement: detects the topology (NVLink, NVSwitch, PCIe, IB rails) at init, builds rings/trees over it, picks algorithms by message size, uses GPUDirect RDMA (GPU↔NIC↔GPU without staging through host memory — the Phase 01 PCIe-cliff avoidance), overlaps with compute via CUDA streams, and handles multi-rail IB.
The operational knobs/signals you will use:
NCCL_DEBUG=INFO— prints the topology and algorithm choices (first stop when collectives are slow).NCCL_ALGO,NCCL_PROTO— force ring/tree, LL/Simple protocols (debugging).NCCL_IB_*,NCCL_SOCKET_IFNAME,NCCL_P2P_*— interface/transport selection (the cause of half of "training is slow on new hardware" incidents — wrong NIC, P2P disabled, IB not used).NCCL_TIMEOUT/ the framework's collective timeout — what turns a hang (Ch. 8) into a crash with a stack trace.
The leadership point: NCCL is a black box that's fast when the topology and config are right and silently 10× slow when they aren't. Knowing what it's doing (Ch. 3–4, 6) is what lets you debug "MFU is 50%, why?" instead of filing a ticket and waiting.
Chapter 6: Topology — NVLink Islands, InfiniBand, and Placement
This is where Phase 06 (placement) and this phase (collectives) fuse. The bandwidth hierarchy (Phase 01 Ch. 9), as the substrate for collectives:
within a GPU: HBM ~3.35 TB/s
GPU<->GPU (node): NVLink/NVSwitch ~900 GB/s <- TP lives here
node<->node: InfiniBand/RoCE ~50 GB/s <- DP/PP cross this
host: PCIe ~64 GB/s
The consequences that decide real throughput:
- Tensor parallelism must stay within an NVLink island. TP does a per-layer all-reduce on the critical path; on NVLink that's ~microseconds, on IB it's ~20× slower and serializes every layer — a 2–5× total slowdown. This is why schedulers (Phase 06) gang-place TP groups on one NVLink-connected node, and why "my 8-GPU TP job is slow" is almost always "it got spread across two nodes."
- Data parallelism can cross nodes — its all-reduce is once per step and overlappable (Ch. 7), so IB bandwidth is tolerable; you still want hierarchical all-reduce (Ch. 4) to minimize cross-node volume.
- Rails and GPUDirect RDMA: multi-NIC nodes have "rails"; a topology-aware collective uses the NIC closest to each GPU (GPUDirect RDMA) to avoid PCIe hops. Misconfiguration here silently routes through host memory (the 50× cliff).
Composing parallelism to the topology is the frontier-training art: TP within node (NVLink), PP across a few nodes (cheap activation passing), DP across the rest (overlappable all-reduce). Lab 02 makes the placement-vs-throughput relationship quantitative.
Chapter 7: Overlap — Hiding Communication Behind Compute
Even optimal collectives cost time; the trick is to not wait for them.
- Gradient bucketing + overlap (DDP's core optimization): instead of one giant all-reduce after the whole backward pass, group gradients into buckets and all-reduce each bucket as soon as it's ready — overlapping communication of early-computed gradients with the still-running backward pass. Done right, communication is almost free (hidden behind compute). Bucket size is a tuning knob (too small = latency overhead per bucket; too large = less overlap).
- Computation/communication overlap generally: collectives run on a separate CUDA stream (Phase 05) so the compute stream proceeds; event edges enforce the dependency. The same stream-scheduling skills from Phase 05, at cluster scale.
- The critical-path mindset: at 60% MFU (model FLOPs utilization), the missing 40% is usually exposed communication (no overlap), pipeline bubbles, or stragglers (Ch. 8) — not raw kernel speed. Profiling distributed training is about finding what's not overlapped.
Chapter 8: Failure Domains at Scale
The chapter that separates "ran a 4-GPU job" from "operated a 10,000-GPU cluster." Synchronous collectives have a brutal property: every rank must participate, so any one failure hangs everyone.
- The hang: a GPU throws an Xid error (Phase 04/10), a NIC flaps, a process OOMs — that rank stops calling the collective, and all other ranks block forever inside NCCL waiting for it. No error, no crash — a hang. At 10k-GPU scale where ~1% of nodes have an issue daily, this happens constantly.
- Detection: collective timeouts (
NCCL_TIMEOUT, framework watchdogs) turn hangs into crashes with a culprit rank; health monitoring (Phase 10) catches the failing GPU/NIC; "straggler" detection finds the slow-but-not-dead rank dragging the whole job to its speed. - Recovery: checkpointing (the only real defense — periodic, async, sharded checkpoints so a failure costs minutes not days), elastic training (frameworks that can drop/re-add ranks and continue), and redundancy/hot spares (replace the failed node, restore from checkpoint, resume). The checkpoint interval is an explicit cost/risk tradeoff you own: more frequent = less lost work per failure, more overhead.
- Blast radius: placement (Ch. 6) determines how much one failure costs — a failure in a TP group stalls that whole group; good placement minimizes the blast radius. Lab 02's extension models this.
The leadership reframe: at scale, failures are not exceptional, they're the steady state. You design the training/serving platform assuming continuous partial failure — checkpoint cadence, fast detection, elastic recovery, and spare capacity are first-class features, not afterthoughts. This is also the sovereign/on-prem story (Phase 07 Ch. 9): air-gapped clusters can't lean on cloud auto-healing.
Chapter 9: Distribution on the Serving Side
Collectives aren't just for training. Large-model serving (Phase 07) is distributed too:
- Tensor-parallel inference: a model too big for one GPU is TP-sharded across an NVLink island; every forward pass does per-layer all-reduces — so serving a 70B+ model has the same NVLink-island requirement as TP training.
- KV-cache sharding: the KV cache (Phase 07) is split across the TP group; attention does its own communication.
- Prefill/decode disaggregation (Phase 07 Ch. 5): separate GPU pools for prefill and decode means shipping the KV cache between them over the interconnect — a new, large communication pattern that topology (Ch. 6) must accommodate.
- Expert parallelism for MoE serving: all-to-all to route tokens to experts — the all-to-all collective at inference time, latency-sensitive.
So the topology and collective skills here apply directly to the JD's serving mandate: a platform serving large models is a distributed system, and its throughput is gated by the same NVLink/IB hierarchy and the same overlap discipline.
Lab Walkthrough Guidance
Order: Lab 01 → Lab 02 (implement the algorithm, then reason about placing it on a topology).
Lab 01 (ring all-reduce over sockets):
python solution.py --world 4; confirm correctness, then read the reduce-scatter/all-gather phases against Ch. 3.- Verify the byte count: each rank moves ≈2(N-1)/N · V — measure it, don't trust the formula. Compare to the naive all-gather-sum (N·V).
- Extensions: tree all-reduce (measure latency on small vs large messages — Ch. 4's crossover); bucketing/overlap; then the failure demo — kill a rank, watch the hang, add a timeout (Ch. 8).
Lab 02 (topology simulator):
python solution.py; read the bandwidth-hierarchy cost model against Ch. 6.- Reproduce: TP within an NVLink island vs TP across nodes — the ratio is the NVLink/IB ratio, quantified. This is the Phase-06-meets-Phase-08 result.
- Hierarchical all-reduce vs flat across nodes — Ch. 4's win, measured.
- Extension: pipeline bubble model and failure-domain blast radius.
Success Criteria
- You can contrast DP/TP/PP and state which is NVLink-bound and why (Ch. 1)
- You can derive all-reduce = reduce-scatter + all-gather = 2V/rank and explain N-independence (Ch. 2–3) — and your Lab 01 byte count proves it
- You can say when tree beats ring and what hierarchical all-reduce buys (Ch. 4)
- You can name the NCCL signals/knobs for debugging a slow collective (Ch. 5)
- You can explain why TP across nodes is catastrophic, with the bandwidth ratio (Ch. 6) — Lab 02 quantifies it
- You can explain gradient overlap and diagnose low MFU (Ch. 7)
- You can describe how one failure hangs a job and the detection/recovery stack (Ch. 8)
Interview Q&A
Q1: Derive ring all-reduce's communication cost and explain why it's bandwidth-optimal. A: All-reduce = reduce-scatter + all-gather. Chop the V-byte vector into N chunks. Reduce-scatter: N-1 steps, each rank sends/receives one V/N chunk per step and accumulates → (N-1)·V/N moved, ending with each rank owning one fully- reduced chunk. All-gather: another N-1 steps of V/N chunks → another (N-1)·V/N. Total ≈ 2·(N-1)/N · V ≈ 2V per rank, independent of N. It's bandwidth-optimal because every rank must export its contribution and import the full reduced result — 2V is the information-theoretic minimum bytes across each rank's links, and the ring saturates it. The catch: 2(N-1) sequential hops means latency grows with N, so for small messages a log-latency tree wins — which is why NCCL switches by message size.
Q2: Compare data, tensor, and pipeline parallelism — which is NVLink-bound? A: Data parallel replicates the model, splits the batch, and all-reduces gradients once per step — communication is overlappable with the backward pass and tolerates cross-node IB, so DP scales across nodes. Tensor parallel splits each layer's matrices across GPUs with an all-reduce inside every layer on the critical path — that frequency makes it NVLink-bound: on NVLink it's microseconds, across nodes on IB it's ~20× slower per layer and serializes the whole forward/backward, a 2–5× slowdown. Pipeline parallel splits layers into stages passing only activations between neighbors (cheap), trading a pipeline bubble (idle fill/drain) for depth. Frontier training composes them: TP within a node, PP across a few nodes, DP across the rest.
Q3: Your 512-GPU training run is at 60% MFU. Where do you look? A: The missing 40% is almost never raw kernel speed; it's exposed time. In order: (1) communication not overlapped — is gradient bucketing/overlap on? Profile for all-reduce on the critical path; (2) topology mistakes — is TP accidentally spanning nodes (NCCL_DEBUG=INFO shows the rings; check it's using NVLink/IB not PCIe/sockets), is GPUDirect RDMA active; (3) pipeline bubbles — micro-batch count too low for the PP depth; (4) stragglers — one slow GPU (thermal throttle, ECC retries) dragging every collective to its speed, found by per-rank step-time variance; (5) data loading / checkpoint stalls on the critical path. The discipline is "find what's not overlapped," via the distributed profiler and NCCL logs, not micro-optimizing kernels.
Q4: One GPU fails in a 1024-GPU synchronous job. What happens and how do you make it survivable? A: The failed rank stops entering the collective, so all 1023 others block forever inside NCCL — a silent hang, not a crash. Survivability is a stack: (1) detection — collective timeouts and watchdogs convert the hang into a crash naming the culprit rank; health monitoring (Xid/NIC) flags the failing hardware; straggler detection catches slow-not-dead ranks. (2) recovery — checkpointing is the real defense: frequent, async, sharded checkpoints so a failure costs minutes; elastic frameworks drop/re-add ranks; hot spares replace the node and resume from checkpoint. (3) blast-radius minimization — placement so one failure stalls the smallest group. At 10k-GPU scale ~1% of nodes fail daily, so this isn't exceptional handling — continuous partial failure is the design assumption, and checkpoint cadence is an explicit cost/risk lever I'd own.
Q5: Why does tensor-parallel placement across nodes destroy throughput, with numbers? A: TP all-reduces partial results inside every layer on the critical path. On NVLink (~900 GB/s, ~µs latency) that per-layer collective is negligible; across nodes on InfiniBand (~50 GB/s, ~µs but ~18× less bandwidth and extra hops) each all-reduce is ~20× slower and, because it's serial within each layer's forward and backward, it can't be hidden — so a model with dozens of layers pays that penalty dozens of times per step, often a 2–5× total slowdown. That's why the scheduler (Phase 06) must gang-place a TP group on one NVLink-connected node, and why "8-GPU TP job is slow" almost always means it got spread across two nodes — the single most common distributed-training misconfiguration.
Q6 (leadership): How do you architect a training platform to be efficient AND fault-tolerant at 10k-GPU scale? A: Two intertwined goals. Efficiency: compose parallelism to the topology (TP within NVLink islands, PP across a few nodes, DP across the rest), enforce it through topology-aware gang scheduling (Phase 06), maximize overlap (gradient bucketing, comm on separate streams), and monitor MFU with per-rank step-time variance to catch stragglers. Fault tolerance: assume continuous partial failure — async sharded checkpointing at a cadence tuned to the failure rate (minutes of lost work, not days), collective timeouts + health monitoring for fast detection with a named culprit, elastic training to continue through node loss, and hot-spare capacity for fast replacement. Both rest on observability (Phase 10): without per-rank metrics and NCCL visibility you can neither find the efficiency leaks nor detect the failures. And for sovereign/ on-prem (Phase 07/11), all of this must work without cloud auto-healing — the platform owns the recovery, not the cloud provider.
References
- Baidu, "Bringing HPC Techniques to Deep Learning" (ring all-reduce for DL, 2017) — https://andrew.gibiansky.com/blog/machine-learning/baidu-allreduce/
- Thakur, Rabenseifner, Gropp, "Optimization of Collective Communication Operations in MPICH" (2005) — the algorithm survey
- NVIDIA NCCL documentation +
NCCL_DEBUG— https://docs.nvidia.com/deeplearning/nccl/ - Shoeybi et al., "Megatron-LM" (tensor parallelism) — https://arxiv.org/abs/1909.08053
- Narayanan et al., "Efficient Large-Scale Language Model Training on GPU Clusters" (3D parallelism / PTD-P) — https://arxiv.org/abs/2104.04473
- Rajbhandari et al., "ZeRO" (DeepSpeed memory-efficient DP) — https://arxiv.org/abs/1910.02054
- PyTorch DDP design note (gradient bucketing/overlap) — https://pytorch.org/docs/stable/notes/ddp.html
- Meta, "OPT-175B logbook" / "The Llama 3 Herd of Models" — real failure-rate accounts at scale
- Cross-track: Phase 01 WARMUP Ch. 9 (interconnect), Phase 06 WARMUP Ch. 7 (topology-aware placement)
🛸 Hitchhiker's Guide — Phase 08: Distributed Systems & HPC Collectives
Read this if: you can train on one GPU but "all-reduce," "tensor parallel," and "why is MFU 50%" are fog. Field notes; derivations in the WARMUP.
0. The 30-second mental model
Many GPUs become one via collectives (all-reduce gradients/activations) over a bandwidth hierarchy (NVLink ≫ InfiniBand ≫ PCIe). Ring all-reduce moves ~2V/rank regardless of N (bandwidth-optimal). Tensor parallel must live on NVLink; data parallel can cross nodes. At scale, any one failure hangs everyone — so checkpoint, detect, recover. Low MFU = exposed communication, bubbles, or stragglers, not slow kernels.
1. Parallelism card
| Type | Splits | Communication | Lives on | Scales |
|---|---|---|---|---|
| Data (DP) | the batch | all-reduce gradients/step (overlappable) | anywhere (IB ok) | batch size |
| Tensor (TP) | each layer's matrices | all-reduce per layer (critical path) | NVLink only | model size |
| Pipeline (PP) | layers into stages | activations between stages (cheap) | across nodes ok | model depth |
| Expert (EP) | MoE experts | all-to-all | NVLink + IB | model size |
Frontier = compose them: TP in node, PP across few nodes, DP across rest.
2. Collective cost card
all-reduce = reduce-scatter + all-gather = 2·(N-1)/N·V ≈ 2V per rank (N-independent!)
naive all-gather-then-sum = N·V per rank (N× worse)
ring: bandwidth-optimal, latency O(N) | tree: bandwidth worse, latency O(log N)
NCCL auto-picks by message size; hierarchical = reduce in-node then across-node
3. Topology stakes (the numbers)
HBM 3.35 TB/s > NVLink 900 GB/s > PCIe 64 GB/s ≈ InfiniBand 50 GB/s
TP across nodes instead of NVLink ≈ 2–5× total slowdown (per-layer all-reduce ×20 slower × dozens of layers, unhideable). The #1 distributed misconfig. Schedulers must gang-place TP groups on one NVLink island (Phase 06).
4. NCCL survival kit
| Signal/knob | Use |
|---|---|
NCCL_DEBUG=INFO | see topology + algorithm choices (first stop when slow) |
NCCL_SOCKET_IFNAME, NCCL_IB_* | force the right NIC/transport (half of "slow on new HW") |
NCCL_P2P_* | P2P/NVLink enablement |
NCCL_ALGO/NCCL_PROTO | force ring/tree for debugging |
NCCL_TIMEOUT / framework watchdog | turn hangs into crashes with a culprit |
NCCL is fast when topology+config are right, silently 10× slow when not. Know what it's doing.
5. Diagnosing low MFU (the flowchart)
MFU < 70%? ->
comm not overlapped? bucketing on? all-reduce on critical path? (most common)
topology wrong? TP spanning nodes? PCIe instead of NVLink/IB? GPUDirect off?
pipeline bubbles? too few micro-batches for PP depth
stragglers? per-rank step-time variance -> one slow GPU (thermal/ECC)
data/checkpoint stall? on the critical path?
Find what's not overlapped. Kernel micro-opt is rarely the answer at scale.
6. Failure reality
At 10k GPUs, ~1% of nodes have an issue daily. A synchronous collective hangs on any failure (no crash — a hang). Defense stack:
- Detect: collective timeouts + watchdogs (hang→crash+culprit), health monitoring (Xid/NIC), straggler detection.
- Recover: async sharded checkpoints (cadence = cost/risk lever), elastic training, hot spares.
- Minimize blast radius: placement so one failure stalls the smallest group.
Failures are the steady state; design for continuous partial failure. (And for sovereign/on-prem, no cloud auto-heal — you own recovery.)
7. War stories
- "8-GPU TP job at half speed" — scheduler spread it across 2 nodes; per- layer all-reduce hit IB not NVLink. Topology constraint → fixed. The classic.
- "Training hung at 3am, no error" — one GPU Xid'd; 1023 ranks blocked in
NCCL forever. No
NCCL_TIMEOUTset. Added it; now it crashes with the culprit rank in seconds. - "Slow only on the new cluster" —
NCCL_SOCKET_IFNAMEpicked the mgmt NIC, not IB. One env var, 10× collective speedup. - "MFU 55% and kernels look fine" — gradient overlap was off (custom training loop bypassed DDP's bucketing). Re-enabled; MFU → 78%.
- "One slow GPU tanked a 256-GPU job" — thermal-throttled card did ECC retries; every all-reduce waited for it. Straggler detection drained the node.
8. Exit bar
You can derive ring all-reduce cost, contrast DP/TP/PP and place each on the topology, debug a slow collective via NCCL signals, diagnose low MFU, and design for failure at scale. Plus you've built ring all-reduce over real sockets and a topology cost model. Next: Phase 09 — the keystone, where all of this hardware knowledge becomes a hardware-independent platform (the JD's core mandate).
Lab 01 — Ring All-Reduce Over Real Sockets (Python)
Phase: 08 — Distributed & HPC | Difficulty: ⭐⭐⭐⭐☆ | Time: 5–8 hours
Language: Python (stdlib socket, multiprocessing) | Hardware: none (multi-process on one machine)
Concept primer:
../WARMUP.mdCh. 2–4.
Run
python solution.py --world 4 # 4 worker processes, ring all-reduce over TCP
python solution.py --world 8 --size 1000000
0. The mission
Implement the algorithm NCCL uses, over real OS sockets across real processes — so the communication is genuine, not simulated. Ring all-reduce in two phases (WARMUP Ch. 3):
- Reduce-scatter (N-1 steps): ring-pass chunks, accumulating, until each rank owns one fully-reduced chunk.
- All-gather (N-1 steps): ring-pass the completed chunks until everyone has all of them.
Then prove the cost: each rank moves ≈2(N-1)/N × V bytes — measured by counting actual socket bytes — and beats the naive all-gather-then-sum (N×V).
1. What the output shows
ring all-reduce: world=8, vector=80000 doubles
correctness: PASS (every rank's result == sum 1..8 = 36)
bytes/rank moved : ring=1,120,000 naive_allgather=4,480,000
per-rank volume vs 2(N-1)/N*V bound: 1.00x (1.00 = bandwidth-optimal)
ring moves 4.0x fewer bytes than naive all-gather
- Correctness: every rank's vector equals the true element-wise sum across all ranks — the collective is exact.
- Bytes/rank: the ring moves ≈2(N-1)/N × V per rank, hitting the
bandwidth-optimal bound. The key result — run
--world 4then--world 8: the ring's per-rank volume stays ≈2V (the factor barely changes from 1.5V to 1.75V) while naive all-gather grows as (N-1)·V, so the ring's advantage widens with N (2× at N=4, 4× at N=8). That N-independence is why ring all-reduce made large-scale data-parallel training practical (WARMUP Ch. 3).
2. Reading order (solution.py)
worker— each rank: connects to its ring neighbors over TCP, gets its data shard, runs the two phases.reduce_scatter— the N-1 send/recv-and-add steps; note the chunk indexing (rank r sends chunk(r-step) % N).all_gather— the N-1 forward-only steps.- The byte counters and the correctness check against a reference sum.
3. Extensions
- Tree all-reduce: implement reduce-up/broadcast-down; benchmark latency vs ring on small messages (1 KB) and large (10 MB) — find the crossover (WARMUP Ch. 4).
- Bucketing/overlap: split the vector into buckets, all-reduce each as it's "ready," overlapping with simulated backward compute; measure the hidden time (WARMUP Ch. 7).
- Failure demo: have one rank
sys.exit()mid-collective; watch the others hang inrecv. Then add a socket timeout that turns the hang into a detected failure with the culprit rank (WARMUP Ch. 8). - Bandwidth-optimality check: vary world size 2/4/8/16; confirm per-rank volume stays ≈2V (N-independent) while a naive all-gather-sum grows as N·V.
4. Common pitfalls
- Deadlock from ordered send/recv: if every rank sends-then-recvs to the same neighbor synchronously, you can deadlock on socket buffers. The lab uses a send thread (or non-blocking) — keep that structure.
- Chunk index off-by-one: the rank-to-chunk mapping per step is the crux; get it wrong and the reduction is incorrect (the correctness assert catches it, but the index math is the lab's real content).
- Float serialization: use a fixed format (
struct/array) consistently across ranks, or you'll get garbage sums. - Counting bytes wrong: count payload bytes actually sent on the wire, not the logical vector size.
5. What this lab proves about you
You've implemented and measured the algorithm at the heart of every data-parallel training run, and you can derive its bandwidth-optimal cost from the byte counts you collected — not recite it. NCCL is no longer magic: it's this, with topology detection and GPUDirect RDMA on top.
Lab 02 — Topology-Aware Collective Simulator (Python)
Phase: 08 — Distributed & HPC | Difficulty: ⭐⭐⭐⭐☆ | Time: 4–6 hours Language: Python (stdlib) | Hardware: none
Concept primer:
../WARMUP.mdCh. 4, 6.
Run
python solution.py
0. The mission
Model a cluster's interconnect hierarchy (NVLink within a node, InfiniBand between nodes) and compute collective time for different placements — proving the Phase-06-meets-Phase-08 thesis: where you put a parallel group decides throughput more than which algorithm you use.
1. What the output shows
== all-reduce time for an 8-way tensor-parallel group (256 MB activation, x80 layers/step) ==
placement links per-step (ms) vs best
TP within 1 NVLink node NVLink 44.0 1.0x
TP split across 2 nodes InfiniBand 753.9 17.1x <- catastrophic
TP split across 4 nodes InfiniBand 753.9 17.1x
== data-parallel gradient all-reduce, 4 nodes x 8 GPUs (256 MB) ==
flat ring over all 32 GPUs (IB) : 10.5 ms
hierarchical (NVLink in-node, IB across) : 1.6 ms (6.7x faster)
== failure blast radius (1 GPU dies, synchronous job) ==
TP-within-node placement : 1 GPU failure stalls 8 GPUs (one TP group)
TP-across-nodes placement: 1 GPU failure stalls 32 GPUs (spans all nodes)
(The TP cost is multiplied by 80 layers because tensor parallelism all-reduces inside every layer — which is exactly why crossing nodes is catastrophic: you pay the slow-link penalty dozens of times per step, unhideable.)
- TP placement: tensor parallelism within one NVLink node is ~18× faster than spread across nodes — the WARMUP Ch. 6 result, quantified. Same algorithm, same data; only placement differs. This is why "my 8-GPU TP job is slow" is almost always "it got spread across nodes."
- Hierarchical all-reduce: reduce within NVLink islands first, then across nodes over IB — does the big-volume reduction on fast links, only the small cross-node step on slow links (WARMUP Ch. 4). ~6× faster than a flat ring.
- Blast radius: placement also decides how many GPUs one failure stalls (WARMUP Ch. 8).
2. Reading order (solution.py)
Linkbandwidths (NVLink/IB/PCIe) and theallreduce_timecost model (2(N-1)/N · V / bandwidth + latency).- The placement scenarios — how the ranks map to nodes and which links the collective crosses.
hierarchical_allreduce_time— the two-level decomposition.- The blast-radius computation.
3. Extensions
- Pipeline bubble model: compute PP efficiency =
microbatches / (microbatches + stages - 1); show how more micro-batches shrink the bubble. - 3D parallelism placement: TP=8 (in node), PP=4 (across nodes), DP=rest; compute total step time and find the placement that minimizes it.
- Rail/GPUDirect model: add a PCIe-staging penalty when GPUDirect RDMA is misconfigured; show the 50× host-bounce cliff.
- Failure-domain optimization: given a job shape, find the placement that minimizes blast radius subject to the NVLink-island constraint.
4. Common pitfalls
- Using bandwidth without latency — for small messages latency dominates and ring loses to tree (WARMUP Ch. 4); the model includes a per-hop latency term.
- Forgetting that TP all-reduces per layer — the per-step TP cost is
n_layers × per-layer-allreduce, which is what makes cross-node TP catastrophic (not a one-time cost). - Treating all GPUs as equidistant — the entire lesson is that they aren't.
5. What this lab proves about you
You can quantify the interconnect's effect on distributed throughput and explain why placement (Phase 06) and collectives (Phase 08) must be co-designed. When a training job underperforms, you reason from topology — "is the TP group on one NVLink island?" — before touching kernels. That's the systems-level judgment the JD's "HPC at production scale" requires.
Phase 09 — Hardware Abstraction & Portability (The Keystone Phase)
Difficulty: ⭐⭐⭐⭐⭐ | Estimated Time: 2 weeks Roles supported: Head of Engineering [GPU], Runtime/Platform Architect, Compiler/Backend Engineer Hardware needed: none — the HAL and its backends are real C, runnable on any machine
Why This Phase Exists
This is the JD's central mandate, verbatim: "a platform that operates independently of the underlying hardware" and "decouple software from hardware environments." Every other phase has been building the knowledge to do this credibly — you can't abstract hardware you don't understand (Phases 01–08), and you can't sell the abstraction without the security/licensing that makes it a product (Phase 11).
A hardware-abstraction layer (HAL) is the single most important architectural artifact a hardware-agnostic AI platform owns. Get it right and you can support NVIDIA, AMD, Intel, and a startup's accelerator behind one API, route each workload to the best backend, and capture the market gap that NVIDIA's lock-in creates. Get it wrong and you've built either a leaky abstraction that performs terribly or a lowest-common-denominator wrapper that delivers a fraction of each chip's capability.
You will build a real HAL in C — a stable API, a backend ABI, and dlopen-loaded vendor plugins (CPU-reference, a "fast" backend, a "limited" backend) — the literal architecture of decoupling software from hardware. Then you'll grapple with the hard parts: capability negotiation, the abstraction-vs-performance tradeoff, and versioning the ABI.
Concepts
- API vs ABI: the difference, why it's the crux of plugin systems, ABI stability rules (struct layout, versioning, symbol visibility)
- The HAL pattern: a stable front-end API + a backend interface (vtable of function pointers) + dynamic backend loading (
dlopen/dlsym) - Capability negotiation: backends declare what they support (dtypes, ops, features); the platform queries and routes — no lowest-common-denominator
- The portability landscape revisited (from Phase 03 Ch. 9): CUDA, ROCm/HIP, oneAPI/Level Zero, Metal, Vulkan/SPIR-V, and the compiler layer (Triton/MLIR) as a backend strategy
- The abstraction-vs-performance tension: leaky abstractions, escape hatches, per-backend fast paths; why "write once, run anywhere, fast" is the hard part
- Backend selection/routing: choosing a backend per workload by capability + benchmark (ties to Phase 01 Ch. 10, Phase 07 Ch. 8)
- Versioning a plugin ABI: how to evolve the interface without breaking shipped backends (reserved fields, version structs, capability flags)
- Conformance testing: one test suite, all backends must pass — the contract that makes the abstraction trustworthy
- Build/packaging: shipping a platform that loads the right backend for the customer's hardware (incl. air-gapped, Phase 07/11)
- The business case: why this layer is the platform's moat and how it's monetized (OEM integrations, the JD's commercialization mandate)
Labs
Lab 01 — A GPU HAL with dlopen Backends (C)
| Field | Value |
|---|---|
| Goal | Build a hardware-abstraction layer: a stable C API, a backend ABI (function-pointer vtable + capability struct), and three dlopen-loaded backends (cpu_ref, "fast", "limited"). Run the same program on any backend; route by capability. |
| Concepts | API/ABI separation, function-pointer vtables, dlopen/dlsym, capability negotiation, backend selection, ABI versioning. |
| Steps | 1) make && ./hal_demo — loads all backends, queries capabilities, runs a tensor op on each, routes a request to a capable backend. 2) Read hal.h (the contract), hal.c (loader/router), and the three backend_*.c. 3) Reproduce: a request needing FP16 routes away from the "limited" backend; the ABI version check rejects a stale backend. 4) Extensions: a new backend without recompiling the core, a fast-path escape hatch, ABI evolution. |
| Stack | C (C11), dlopen (POSIX) |
| Output | A HAL core + 3 loadable .so/.dylib backends + a demo that routes by capability and version-checks the ABI. |
| How to Test | Built-in: every backend passes the same conformance test (correct results); capability routing sends FP16 work only to capable backends; an ABI-version mismatch is detected and the backend rejected. |
| Talking Points | Why API≠ABI is the crux; how capability negotiation avoids lowest-common-denominator; the abstraction-vs-performance escape hatch; how you'd version this ABI to not break shipped vendor backends; how this maps to real stacks (CUDA/ROCm/Level Zero). |
| Resume Bullet | "Designed and built a GPU hardware-abstraction layer in C with a versioned backend ABI and dlopen-loaded vendor plugins; implemented capability negotiation and routing, with a single conformance suite all backends pass." |
| Extensions | Add a 4th backend (a .so) with no core recompile — prove the ABI is real; a per-backend fast-path escape hatch; bump the ABI version and show old + new backends coexisting via the version field. |
Lab 02 — Portability Strategy Decision Doc + Conformance Harness (Markdown + Python)
| Field | Value |
|---|---|
| Goal | Produce the architectural decision record (ADR) a real platform needs — which portability posture (HAL-over-vendor-libs vs Triton/MLIR vs full compiler), with costs — backed by a conformance harness that runs one test matrix against multiple "backends." |
| Concepts | The three portability postures (Phase 03 Ch. 9–10), conformance testing, capability matrices, the build-vs-adopt analysis. |
| Steps | 1) Read STRATEGY-ADR.md (a worked decision record) and fill the decision matrix for a scenario. 2) python conformance.py — runs a capability/correctness matrix against mock backends; prints a support matrix + conformance pass/fail. 3) Write your own ADR for "support NVIDIA + AMD + a startup accelerator within 12 months." |
| Stack | Markdown (ADR) + Python (conformance harness) |
| Output | A completed ADR + a conformance report (which backend supports/passes what). |
| How to Test | python conformance.py asserts the matrix is computed correctly and flags non-conformant backends; the ADR follows the provided rubric. |
| Talking Points | The three postures and their costs; why conformance testing is what makes an abstraction trustworthy; how you'd phase a multi-vendor rollout; the build-vs-adopt argument for the compiler layer (Phase 03 Ch. 10). |
| Resume Bullet | "Authored the hardware-portability architecture decision record (HAL vs Triton/MLIR vs full compiler) and built a conformance harness enforcing a single test matrix across heterogeneous backends." |
| Extensions | Add a performance-conformance tier (backends must hit X% of roofline, not just pass correctness); model the 12-month multi-vendor rollout as a dependency plan. |
Deliverables Checklist
- HAL: builds, all backends load, conformance passes, capability routing works, ABI version checked
- You added a 4th backend without recompiling the core (proof the ABI is real)
- You can explain API vs ABI and the rules for evolving a plugin ABI
- ADR completed for a multi-vendor scenario with costed options
- Conformance harness runs and flags non-conformant backends
- One page: "our platform's hardware-abstraction architecture" (the Phase 12 capstone's backbone)
Interview Relevance
This phase is the JD's thesis. Expect the hardest, most senior questions here.
- "Design a hardware-abstraction layer for an AI platform. Walk me through the API/ABI boundary."
- "How do you avoid a lowest-common-denominator abstraction?"
- "How do you version a plugin ABI so a year-old vendor backend still loads?"
- "Build vs adopt: would you write your own compiler layer or use Triton/MLIR? Defend it."
- "How is decoupling software from hardware actually a defensible business?" (→ system-design/, Phase 11)
Warmup Guide — Hardware Abstraction & Portability (The Keystone)
Zero-to-expert primer for Phase 09 — the JD's central mandate. Builds on every prior phase (you abstract hardware you understand). By the end you can design, build, and version a hardware-abstraction layer and argue the portability strategy to a board.
Table of Contents
- Chapter 1: The Mandate — "Independent of the Underlying Hardware"
- Chapter 2: API vs ABI — the Crux of Every Plugin System
- Chapter 3: The HAL Pattern — Stable API + Backend Vtable + dlopen
- Chapter 4: Capability Negotiation — Beating the Lowest Common Denominator
- Chapter 5: The Abstraction-vs-Performance Tension
- Chapter 6: Versioning a Plugin ABI
- Chapter 7: Conformance Testing — What Makes an Abstraction Trustworthy
- Chapter 8: The Portability Landscape, as Backend Strategy
- Chapter 9: The Business Case — Why This Layer Is the Moat
- Lab Walkthrough Guidance
- Success Criteria
- Interview Q&A
- References
Chapter 1: The Mandate — "Independent of the Underlying Hardware"
The JD's first responsibility: "Define and execute the strategy to decouple software from hardware environments" and build "a platform that operates independently of the underlying hardware." This phase is the technical content of that sentence.
Why it's valuable: NVIDIA's dominance is a software moat (Phase 03 Ch. 10) — CUDA + 18 years of kernels. Customers and OEMs want out from under the lock-in: AMD MI300X has more HBM, Intel Gaudi is cheaper, startups promise better tokens/joule — but switching means rewriting the stack. A platform that lets software run unchanged across vendors captures exactly that pain. That's the commercial thesis, and a hardware-abstraction layer (HAL) is its core artifact.
The hard truth this phase teaches: a HAL is easy to build badly (a thin wrapper that runs everywhere at 20% of peak) and very hard to build well (one API, multiple backends, near-native performance, evolvable without breaking shipped vendor plugins). The difference is Chapters 2–7.
Chapter 2: API vs ABI — the Crux of Every Plugin System
The single most important distinction in this phase, and one most engineers blur.
- API (Application Programming Interface): the source-level contract —
function names, signatures, types you compile against. "Call
hal_matmul(a, b, c)." Breaking the API breaks compilation. - ABI (Application Binary Interface): the binary contract — how those calls are actually made at the machine level: struct memory layout, field offsets, calling convention, symbol names, enum values, alignment. Breaking the ABI breaks already-compiled binaries — silently, often as corruption rather than a clean error.
Why this is the crux of a HAL: backends are separately compiled binaries
(vendor .so files you dlopen at runtime — possibly built by a different team,
a different compiler, a year apart). They share no source with your core; they
share only the ABI. So the contract between your platform and a vendor
backend is binary, and getting it stable is the whole game.
The ABI rules you must internalize (Lab 01 enforces them):
- Struct layout is the contract. If your core thinks
Capabilitiesis{int version; int flags; int max_dtype;}and a backend was compiled when it was{int version; int flags;}, every field afterflagsis read at the wrong offset → garbage. Never reorder or insert fields in the middle; only append, and only with a version guard (Ch. 6). - Pass a version field first, so the core can refuse incompatible backends before reading misaligned fields.
- Symbol visibility: a backend exports exactly one well-known entry point
(e.g.,
hal_backend_register); everything else is hidden. Thedlsymof that symbol is the only coupling. - Use fixed-width types (
int32_t, notint) and explicit enums with assigned values —intwidth and enum representation are platform-dependent.
Misconception: "if it compiles, the ABI is fine." No — ABI breaks happen
without recompiling the other side. The backend compiled last year still
dlopens fine and then reads your new struct wrong. ABI discipline is about the
binaries you didn't rebuild.
Chapter 3: The HAL Pattern — Stable API + Backend Vtable + dlopen
The architecture (Lab 01 builds exactly this):
application
| calls the stable API
v
+-----------+ hal_matmul(...), hal_alloc(...), hal_select_backend(...)
| HAL core | <-- ONE stable API the app compiles against
+-----------+
| dispatches through a vtable of function pointers
v
+------------------ backend ABI ------------------+
| struct Backend { |
| uint32_t abi_version; |
| Capabilities caps; |
| void* (*alloc)(size_t); | <-- function-pointer
| int (*matmul)(const Tensor*, ...); | vtable: the ABI
| ... |
| }; |
+-------------------------------------------------+
^ ^ ^
cpu_ref.so fast.so limited.so <-- dlopen'd at runtime
The three pieces:
- Stable API — what the application calls. It never changes shape; that's the promise to application authors.
- Backend interface (the vtable) — a struct of function pointers + a capability struct. This is the ABI the vendor plugins implement.
- Dynamic loading —
dlopen("backend_fast.so")thendlsym(h, "hal_backend_register")returns the filled vtable. The core never links against any backend at build time — which is what "decoupled from hardware" means mechanically: you can ship a new backend as a file, no core rebuild (Lab 01 extension 1 proves this).
This is precisely how real systems work: CUDA's driver API loads cubins;
OpenCL/Level Zero load ICDs (Installable Client Drivers); LLVM loads target
backends; even Postgres/Python load .so extensions. The pattern is universal;
the discipline (Ch. 2, 6) is what's hard.
Chapter 4: Capability Negotiation — Beating the Lowest Common Denominator
The trap that kills naive HALs: if your API only exposes what every backend supports, you deliver the lowest common denominator — no FP8 because the old backend lacks it, no Tensor Cores because the CPU backend has none. Customers on good hardware pay for capabilities they can't use.
The fix is capability negotiation: each backend declares what it supports — dtypes (FP32/FP16/BF16/FP8/INT8), ops (matmul, attention, conv), features (unified memory, async, specific fusions), and performance hints — in a capability struct. The platform then:
- Queries capabilities at load time.
- Routes each workload to a backend that supports it (a FP8 request goes to an FP8-capable backend; a CPU-only deployment falls back to FP32).
- Exposes the union of capabilities to applications, with a clean failure ("no loaded backend supports FP8") rather than a silent downgrade.
This is the difference between a HAL that's a product and one that's a tax. Lab 01 implements capability routing: a request needing FP16 is steered away from the "limited" backend; the demo shows the router picking correctly. It also mirrors Phase 06's device-plugin capability advertising and Phase 07's multi-engine routing — same pattern, different layer.
Chapter 5: The Abstraction-vs-Performance Tension
The central design tension, and the honest hard part of "write once, run anywhere, fast."
- Too abstract → leaky or slow: a single generic
matmulcan't express each backend's fast path (Tensor Core fragment shapes, AMD's matrix cores, a startup's systolic array), so you get correct-but-mediocre performance. - Too concrete → not portable: expose vendor-specific knobs and the application code becomes vendor-coupled again — you've lost the decoupling.
The resolutions real platforms use (you should be able to name all four):
- Escape hatches: a generic path plus an optional
backend_native_op()that lets performance-critical code call a backend's fast path when present, falling back otherwise. Pragmatic; slightly leaky. - High-level ops: abstract at the operation level (
attention,layernorm), not the instruction level — so each backend implements the op optimally inside the abstraction (this is why serving engines expose "attention" not "matmul+softmax+matmul"). The most successful posture. - A compiler layer: don't hand-write per-backend kernels — lower a portable IR to each backend (Triton/MLIR, Phase 03 Ch. 9). Highest ceiling, highest cost.
- Capability-gated fast paths: the abstraction picks the best implementation based on declared capabilities (Ch. 4).
The leadership judgment: abstract at the right altitude. Too low and you re-expose the hardware; too high and you can't reach peak. "Operation-level abstraction with capability-gated fast paths and an escape hatch" is the defensible default — and it's what the capstone (Phase 12) builds.
Chapter 6: Versioning a Plugin ABI
The chapter that separates a HAL that survives contact with vendors from one
that breaks every release. You ship a backend ABI; vendors build .so files
against it; then you need to evolve it without breaking their year-old
binaries.
The techniques (Lab 01 extension 3 demonstrates them):
- A version field, first in the struct. The core reads
abi_versionbefore anything else and refuses (cleanly) backends it can't support — never reading misaligned fields from a too-new or too-old struct. - Append-only struct growth. New capabilities/functions go at the end of
the struct/vtable, behind a version check. An old backend (smaller struct)
still has its early fields at the right offsets; the core just doesn't read
the new tail for old
abi_versions. - Reserved fields. Ship
uint32_t reserved[8]so future fields don't even change the struct size — old and new backends agree on layout. - Capability flags over struct changes. Prefer adding a flag ("supports FP8") to an existing flags field over adding a struct field — flags are forward/backward compatible by construction.
- Semantic versioning of the ABI: major = breaking (old backends rejected), minor = additive (old backends still load). The core supports a range.
The real-world stakes: CUDA's forward-compatibility (Phase 03/04), OpenCL ICD versioning, the Linux kernel's stable-ABI-for-userspace promise — all are this chapter at industrial scale. A platform whose ABI breaks vendor backends every quarter has no vendor ecosystem, and the ecosystem is the product (Ch. 9).
Chapter 7: Conformance Testing — What Makes an Abstraction Trustworthy
An abstraction is only as good as its guarantee that every backend behaves the same. Without that, applications must special-case backends — and you've lost portability. The mechanism is a conformance suite: one test matrix every backend must pass.
- Correctness conformance: the same operations, same inputs, same expected outputs (within a stated tolerance — Phase 03's FMA/rounding caveat applies across vendors!) on every backend. Lab 01's demo runs one conformance test against all backends.
- Capability conformance: a backend that declares FP16 support must actually produce correct FP16 results — declarations are tested, not trusted.
- Performance conformance (the advanced tier, Lab 02 extension): a backend must hit X% of its roofline (Phase 01) on key ops, or it's "supported but not recommended" — preventing the "technically runs, uselessly slow" backend from poisoning the platform's reputation.
This is how Khronos (OpenCL/Vulkan conformance), the C/C++ standards (test suites), and every credible plugin ecosystem work: the conformance suite is the contract. As a leader you'd gate "certified backend" status on it — which is also a vendor-relations and commercial lever (Ch. 9).
Chapter 8: The Portability Landscape, as Backend Strategy
Revisiting Phase 03 Ch. 9 from the HAL builder's seat — these are your backend options:
- Vendor compute libraries directly (CUDA+cuBLAS/cuDNN, ROCm+rocBLAS, oneAPI+oneMKL, Metal Performance Shaders): each backend wraps the vendor's tuned kernels. Fastest to near-peak per vendor, most code (one backend per vendor), the pragmatic start.
- A portable kernel layer (Triton, increasingly multi-backend; or SYCL/oneAPI): write kernels once, lower to each backend. Less per-vendor code, inherits the compiler's backend coverage, ~90% of peak on supported ops (Phase 03 Ch. 9).
- A full compiler stack (MLIR dialects → each target): maximum control, maximum cost; the moonshot.
- SPIR-V/Vulkan compute: truly neutral, historically below peak for ML.
The platform reality (Phase 03 Ch. 10's three postures, now as architecture): blend — vendor-library backends for the hardware you must support today (time to market), a Triton/MLIR backend strategy for kernel velocity and future hardware, all behind your HAL (this phase) with capability routing (Ch. 4). The HAL is the constant; the backends are how you fill it. The AMD asymmetry from Phase 03 (no stable PTX-equivalent, per-gfx code objects) is a real backend-engineering cost your HAL must budget for.
Chapter 9: The Business Case — Why This Layer Is the Moat
Why a Head of Engineering — not just an architect — owns this:
- It's the commercialization mandate. The JD lists "own platform adoption and commercialisation through partnerships" and "drive integrations with OEMs, cloud providers, and infrastructure partners." Every one of those integrations is a backend behind your HAL. The HAL's quality determines how fast you can onboard an OEM's chip — a direct revenue lever.
- It inverts NVIDIA's moat. NVIDIA's lock-in is software; a platform that makes software hardware-agnostic sells freedom from that lock-in to every customer and every non-NVIDIA chipmaker who wants market access. Your HAL is the chipmakers' on-ramp to the software ecosystem they can't build alone.
- Conformance is a commercial gate. "Certified backend" status (Ch. 7) is leverage in OEM negotiations and a quality promise to customers — you control who gets to be in the ecosystem and at what performance bar.
- It's defensible. Anyone can wrap two SDKs in an afternoon; a performant, versioned, conformance-gated, multi-vendor HAL with an ecosystem of certified backends is years of accumulated engineering and relationships — the same compounding moat dynamic as CUDA, pointed the other way.
The capstone (Phase 12) composes this HAL with the scheduler (06), serving (07), and licensing (11) into the platform the JD describes. This phase is its spine.
Lab Walkthrough Guidance
Order: Lab 01 → Lab 02 (build the HAL, then write the strategy around it).
Lab 01 (the HAL in C):
make && ./hal_demo; readhal.hfirst — the ABI is the contract, study theBackendstruct andCapabilitiesagainst Ch. 2–3.- Read
hal.c(loader/router) then the three backends; trace onehal_matmulcall from API → vtable → backend. - Reproduce: capability routing (FP16 request avoids the "limited" backend); ABI-version rejection of a stale backend.
- Do extension 1 — add a 4th backend as a new
.sowithout recompiling the core. When it loads and passes conformance, you've felt what "decoupled from hardware" means. Then extension 3 (ABI evolution) for Ch. 6.
Lab 02 (strategy ADR + conformance):
- Read
STRATEGY-ADR.md; it's a worked decision record — study its structure. python conformance.py; understand the support matrix + pass/fail logic (Ch. 7).- Write your own ADR for the multi-vendor scenario — costed options, a phased rollout, the conformance gate. This is a Phase 12 leadership artifact in miniature.
Success Criteria
- You can state the API/ABI difference and the rules that keep an ABI stable (Ch. 2)
- You can draw the HAL pattern (API + vtable + dlopen) and explain why the core never links a backend (Ch. 3)
- You can explain capability negotiation and why it beats LCD (Ch. 4)
- You can name the four resolutions to the abstraction-vs-performance tension and pick the default (Ch. 5)
- You can version a plugin ABI so a year-old backend still loads (Ch. 6) — and your Lab 01 extension proves it
- You can explain why conformance testing makes the abstraction trustworthy (Ch. 7)
- You can argue the portability-posture blend and the business case (Ch. 8–9)
Interview Q&A
Q1: Design a hardware-abstraction layer for an AI platform. Walk me through the
API/ABI boundary.
A: Three pieces. A stable API the application compiles against
(hal_alloc, hal_matmul, hal_select_backend) — its shape never changes,
that's the promise to app authors. A backend ABI — a struct of function
pointers (the vtable) plus a capability struct — that vendor plugins implement;
this is the binary contract, so it's governed by ABI rules: version field
first, fixed-width types, append-only struct growth, reserved fields, one
exported registration symbol. And dynamic loading — the core dlopens each
backend .so and dlsyms its registration function to get the filled vtable;
the core links no backend at build time, which is the mechanical meaning of
"decoupled from hardware" — a new chip is a new file. The API/ABI split is the
crux because backends are separately compiled binaries sharing only the ABI, so
a year-old vendor .so must still load correctly — which drives every
versioning decision.
Q2: How do you avoid a lowest-common-denominator abstraction?
A: Capability negotiation. Each backend declares what it supports — dtypes, ops,
features, perf hints — in a capability struct read at load time. The platform
routes each workload to a backend that supports it (FP8 work to an FP8-capable
backend), exposes the union of capabilities to applications, and fails cleanly
("no backend supports FP8") rather than silently downgrading. Combined with
operation-level abstraction (expose attention, not matmul+softmax) so each
backend implements ops with its fast path inside the abstraction, plus optional
native escape hatches for the last mile — you get portability without forcing
everyone down to the weakest backend's feature set. The anti-pattern is an API
that only exposes the intersection of all backends; that makes good hardware pay
for the worst backend's limits.
Q3: How do you version a plugin ABI so a year-old vendor backend still loads?
A: Put an abi_version field first and check it before reading anything else,
so an incompatible backend is rejected cleanly instead of read at wrong offsets.
Grow the interface append-only — new functions/fields at the end of the
vtable/struct, gated by version, so an old (smaller) backend's early fields stay
at correct offsets and the core simply doesn't read the new tail for old
versions. Ship reserved fields so additions don't even change struct size.
Prefer capability flags over new struct fields (flags are compatible by
construction). Use semantic versioning — major = breaking (old backends
rejected), minor = additive (old still load) — and have the core accept a range.
This is exactly how OpenCL ICDs, CUDA forward-compat, and the Linux
userspace-ABI promise work; breaking vendor backends every release means no
vendor ecosystem, and the ecosystem is the product.
Q4: Build vs adopt — would you write your own compiler layer or use Triton/MLIR for kernels? A: Default to adopt. MLIR exists precisely so platforms don't hand-roll an IR: dialects, pass infrastructure, and existing lowerings to NVPTX/ROCm are free; Triton gives ~90% of hand-tuned on the kernels LLMs need with Python-level authoring and multi-backend lowering. Writing a full compiler re-pays all of that for marginal fit gains and an unbounded maintenance bill, and it slows your ability to absorb the next FlashAttention-shaped innovation (ecosystem velocity). I'd build my own only for a genuinely novel execution model the dialects can't express. The pragmatic architecture is a blend: vendor-library backends for hardware I must support today (time to market), a Triton/MLIR backend for kernel velocity and future chips, all behind my own HAL with capability routing — the HAL is mine (it's the moat), the kernel compiler I adopt.
Q5: How is "decoupling software from hardware" actually a defensible business? A: NVIDIA's dominance is a software moat — CUDA plus two decades of kernels — so customers and non-NVIDIA chipmakers share a pain: switching hardware means rewriting the stack. A platform that runs software unchanged across vendors sells freedom from that lock-in to customers and market access to every AMD/Intel/ startup chip that can't build a CUDA-class ecosystem alone — they become backends behind the HAL. The defensibility compounds: a performant, versioned, conformance-gated, multi-vendor HAL with an ecosystem of certified backends and OEM relationships is years of engineering and trust, not an afternoon's wrapper. Conformance certification is a commercial gate you control; OEM integrations are revenue; and the whole thing is the inverse of CUDA's moat — equally compounding, pointed at the incumbent. That's why the JD frames it as both an engineering and a commercialization mandate, and why the Head of Engineering owns it.
References
- Ulrich Drepper, "How To Write Shared Libraries" (the ABI/dlopen bible) — https://www.akkadia.org/drepper/dsohowto.pdf
man dlopen,man dlsym— the loader API you'll build on- Khronos OpenCL ICD + conformance model — https://www.khronos.org/conformance/
- Linux kernel "stable API nonsense" (Greg KH) — the opposite philosophy, instructive on why userspace ABI stability matters — https://www.kernel.org/doc/html/latest/process/stable-api-nonsense.html
- Intel oneAPI / Level Zero spec (a real cross-vendor HAL) — https://spec.oneapi.io/level-zero/latest/
- LLVM "Writing an LLVM Backend" (the compiler-as-backend posture) — https://llvm.org/docs/WritingAnLLVMBackend.html
- Triton + MLIR (Phase 03 references) — https://triton-lang.org , https://mlir.llvm.org
- Architecture Decision Records (Michael Nygard) — https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions
- Cross-track: Phase 03 WARMUP Ch. 9–10 (the compiler layer), Phase 12 (the capstone this spine supports)
🛸 Hitchhiker's Guide — Phase 09: Hardware Abstraction (The Keystone)
Read this if: you've heard "decouple software from hardware" and want to know what it mechanically means. This is the JD's thesis. Field notes; derivations in the WARMUP.
0. The 30-second mental model
A HAL = stable API (what apps call) + backend ABI (a vtable of function pointers + capability struct that vendor plugins implement) + dlopen (load backends at runtime; core links none of them). Beat lowest-common-denominator with capability negotiation. Keep the ABI loadable across years with version-first, append-only, reserved fields. Make it trustworthy with a conformance suite. This layer is the platform's moat (Ch. 9).
1. API vs ABI (the thing everyone blurs)
- API = source contract (names, signatures). Breaking it breaks compiles.
- ABI = binary contract (struct layout, offsets, calling convention, symbol names, enum values). Breaking it breaks already-built binaries — silently.
- Backends are separately-compiled
.sofiles sharing only the ABI with your core. That's why ABI discipline is the whole game.
ABI rules: version field FIRST · fixed-width types (int32_t) · append-only
struct growth · reserved[N] fields · one exported symbol · enums with explicit
values.
2. The pattern (Lab 01 in one diagram)
app -> stable API -> HAL core -> vtable dispatch -> backend.so (dlopen'd)
struct Backend { abi_version; caps; (*matmul); ... }
cpu_ref.so fast.so limited.so <- swap/add WITHOUT recompiling the core
Same pattern as CUDA driver/cubins, OpenCL ICDs, LLVM backends, Postgres extensions. Universal mechanism; the discipline is what's rare.
3. Capability negotiation (beat the LCD)
Each backend declares dtypes/ops/features. Platform queries → routes work to a capable backend → exposes the union, fails cleanly on the unsupported. Don't expose only the intersection of all backends — that taxes good hardware for the worst backend's limits. (Same shape as Phase 06 device-plugin advertising and Phase 07 engine routing.)
4. Abstraction vs performance (the hard part)
Too abstract = slow/leaky; too concrete = vendor-coupled. Four fixes:
- Escape hatches (generic path + optional native fast path)
- Operation-level ops (
attention, notmatmul+softmax) ← the winner - Compiler layer (Triton/MLIR lowering) ← highest ceiling, highest cost
- Capability-gated fast paths
Default: operation-level + capability-gated fast paths + escape hatch. Abstract at the right altitude.
5. Versioning so vendors don't hate you
version-first check · append-only growth · reserved fields · flags over fields · semver (major=breaking, minor=additive), core accepts a range. Break vendor backends every quarter → no ecosystem → no product. The ecosystem IS the product.
6. Conformance (what makes it trustworthy)
One test matrix, all backends pass: correctness (with cross-vendor rounding tolerance — Phase 03), declared-capability verification, and (advanced) a performance tier (X% of roofline or "supported, not recommended"). "Certified backend" status is both a quality promise and an OEM-negotiation lever.
7. Backend strategy (Phase 03 Ch. 9 from the builder's seat)
| Backend type | Speed-to-peak | Cost | Use |
|---|---|---|---|
| vendor libs (cuBLAS/rocBLAS/oneMKL) | near-peak/vendor | one backend per vendor | hardware you must support today |
| Triton/MLIR | ~90% peak | shared kernels | velocity + future chips |
| full compiler | max | unbounded | moonshot only |
| SPIR-V/Vulkan | below peak (ML) | neutral | standards play |
Blend: vendor backends now + Triton/MLIR for velocity, all behind YOUR HAL. Budget for the AMD asymmetry (no PTX-equivalent; per-gfx rebuilds — Phase 03).
8. The business case (why a Head of Eng owns this)
- OEM integrations = backends behind your HAL = direct revenue lever (the JD's commercialization mandate).
- It inverts NVIDIA's software moat — sells lock-in freedom to customers and market access to non-NVIDIA chips.
- Conformance certification = a commercial gate you control.
- A performant, versioned, conformance-gated multi-vendor HAL is years of moat, not an afternoon's wrapper.
9. War stories
- "Our HAL ran everywhere at 25% of peak" — abstracted at the
instruction level (generic matmul), couldn't reach Tensor Cores. Re-abstracted
at op level (
attention) with capability-gated fast paths. Peak recovered. - "A vendor's old .so corrupted memory after our release" — we inserted a struct field in the middle; their year-old binary read every later field at the wrong offset. Now: append-only + reserved fields + version gate. Never again.
- "Backend declared FP16, produced garbage" — declarations were trusted, not tested. Conformance suite now verifies every declared capability.
- "We shipped one engine, lost a deal needing AMD" — no HAL, NVIDIA-coupled. The HAL is exactly the thing that would have made that deal a config change.
10. Exit bar
You can design and build a versioned, dlopen-based HAL with capability routing and conformance; explain API vs ABI and ABI evolution cold; pick the portability-posture blend; and argue the moat. You've added a backend without recompiling the core. This is the JD's thesis, in your hands. Next: Phase 10 — making all of it observable and operable in production.
Lab 01 — A GPU HAL with dlopen Backends (C)
Phase: 09 — Hardware Abstraction (Keystone) | Difficulty: ⭐⭐⭐⭐⭐ | Time: 6–10 hours
Language: C11 + POSIX dlopen | Hardware: none
Concept primer:
../WARMUP.mdCh. 2–7.
Run
make # builds the HAL core, 3 backend .so files, and the demo
./hal_demo
0. The mission
Build the literal architecture of "decouple software from hardware" (WARMUP
Ch. 1): a stable API, a versioned backend ABI, and three dlopen-loaded
vendor backends. The core links none of the backends — it loads them at
runtime, queries their capabilities, and routes work to a capable one.
| File | Role |
|---|---|
hal.h | the contract: stable API + the hal_backend ABI (vtable + caps + version) |
hal.c | the loader (dlopen/dlsym), version validation, capability router |
backend_cpu_ref.c | reference backend: FP32 only |
backend_fast.c | "fast" backend: FP32 + FP16, higher perf hint |
backend_limited.c | "limited" backend: FP32 only, lower perf |
hal_demo.c | loads all, runs conformance, routes a FP16 request, checks ABI version |
1. What the output shows
loaded backend: cpu_ref (abi v2, dtypes=F32, perf=1)
loaded backend: fast (abi v2, dtypes=F32|F16, perf=10)
loaded backend: limited (abi v2, dtypes=F32, perf=2)
rejected backend: stale (abi v1 != core v2) — ABI version guard works
== conformance: matmul FP32 vs reference, all backends ==
cpu_ref : PASS
fast : PASS
limited : PASS
== capability routing ==
FP32 matmul -> fast (highest perf among F32-capable)
FP16 matmul -> fast (only F16-capable backend)
FP8 matmul -> (none) cleanly refused: no loaded backend supports FP8
- Three backends loaded from
.sofiles the core never linked against — that's the decoupling, mechanically (WARMUP Ch. 3). - ABI version guard rejects a backend built against an incompatible ABI (WARMUP Ch. 2, 6) — the discipline that lets a year-old vendor binary still load (or be cleanly refused).
- Conformance: one test, all backends pass (WARMUP Ch. 7).
- Capability routing: FP16 work goes only to the FP16-capable backend; FP8 is refused cleanly instead of silently downgraded (WARMUP Ch. 4 — beating the lowest common denominator).
2. Reading order
hal.h— the ABI is the contract. Studyhal_backend(version first, reserved fields last) andhal_capsagainst WARMUP Ch. 2.hal.c—hal_load_backend(dlopen → dlsym → version check) andhal_select(capability routing).- The three backends — note they share no source with the core, only
hal.h's ABI. hal_demo.c— trace onematmulfrom API call → router → backend vtable.
3. Extensions (the heart of the keystone lab)
- Add a 4th backend with NO core recompile (
backend_exp.c): build just the new.so, drop it in the load list, run. It loads and passes conformance without rebuildinghal.c/hal_demo— this is what "decoupled from hardware" means. Prove it:make backend_exp.so && ./hal_demoafter adding only the path. - ABI evolution (WARMUP Ch. 6): add a new optional op at the end of the vtable behind a capability flag; show an old backend (without it) still loads and runs, while a new backend exposes the new op. Demonstrate append-only + reserved-field discipline.
- Native escape hatch (WARMUP Ch. 5): add an optional
native_oppointer a backend may provide for a fast path; the router uses it when present, falls back otherwise. - Performance conformance (WARMUP Ch. 7): time each backend's matmul; gate "recommended" status on a perf threshold.
4. Common pitfalls
- Inserting a struct field in the middle — breaks every already-built
backend's field offsets (WARMUP Ch. 2). Only append, behind a version guard.
Try it: reorder
hal_capsfields, rebuild only the core, and watch a not-rebuilt backend misbehave. - Forgetting the version check — a stale backend's misaligned struct reads
as garbage; check
abi_versionfirst, before any other field. dlsymreturning NULL — the backend didn't exporthal_backend_register(symbol visibility); handle it, don't crash.- Comparing float results with
==across backends — different operation order/rounding (Phase 03's FMA caveat applies cross-vendor); conformance uses a tolerance.
5. What this lab proves about you
You've built the JD's central artifact with your own hands: a versioned, dlopen-based, capability-routing hardware-abstraction layer — the mechanical meaning of "a platform that operates independently of the underlying hardware." You can now design this for real (CUDA/ROCm/Level Zero backends), version it so vendors trust it, and explain why it's the platform's moat.
Lab 02 — Portability Strategy ADR + Conformance Harness
Phase: 09 — Hardware Abstraction (Keystone) | Difficulty: ⭐⭐⭐⭐☆ | Time: 4–6 hours Language: Markdown (ADR) + Python (conformance) | Hardware: none
Concept primer:
../WARMUP.mdCh. 7–9, and Phase 03 WARMUP Ch. 9–10.
Run
python conformance.py # runs the capability/correctness matrix
0. The mission
Two deliverables a real platform needs:
- An Architecture Decision Record (ADR) choosing the portability posture
(HAL-over-vendor-libs vs Triton/MLIR vs full compiler) with costs — the
document you'd actually present to leadership.
STRATEGY-ADR.mdis a worked example; you fill in a decision matrix and write your own for a scenario. - A conformance harness (
conformance.py) that runs one test matrix against multiple mock backends and prints a support matrix + pass/fail — the mechanism that makes the abstraction trustworthy (WARMUP Ch. 7).
1. What the conformance harness shows
== capability support matrix ==
backend F32 F16 FP8 matmul attention
cuda_like yes yes yes yes yes
rocm_like yes yes no yes yes
cpu_ref yes no no yes no
startup yes yes yes yes no <- declares attention, lacks it!
== correctness conformance (one test matrix, all backends) ==
cuda_like : matmul PASS attention PASS
rocm_like : matmul PASS attention PASS
cpu_ref : matmul PASS attention SKIP (not supported)
startup : matmul PASS attention FAIL <- declared but produces wrong result
CONFORMANCE: startup is NON-CONFORMANT (declared attention, failed it)
The lesson (WARMUP Ch. 7): declarations are tested, not trusted. A backend that claims a capability must prove it on the conformance suite, or it's flagged non-conformant — which is what protects applications from having to special-case backends.
2. Reading order
STRATEGY-ADR.md— study the ADR structure (context, options, costs, decision, consequences). Fill its decision matrix.conformance.py— the capability matrix + the correctness tests; note how a declared-but-broken capability is caught.
3. The ADR exercise (the real deliverable)
Write MY-ADR.md for this scenario:
"We must support NVIDIA (today), AMD MI300X (6 months), and a startup accelerator (12 months) behind one platform, with near-native performance on NVIDIA and AMD."
Use the rubric in STRATEGY-ADR.md: state the options (vendor-library backends
behind a HAL / Triton-MLIR kernel layer / full compiler / blend), the costs
(eng-months, time-to-market, performance ceiling, maintenance, the AMD
no-PTX-equivalent tax from Phase 03), a phased rollout, and the conformance gate.
End with a recommendation and its consequences.
4. Extensions
- Performance conformance tier: backends must hit X% of a reference
throughput, not just pass correctness; add a
RECOMMENDED/SUPPORTED/FAILstatus (WARMUP Ch. 7). - Rollout dependency plan: model the 12-month multi-vendor rollout as a dependency graph (HAL ABI freeze → AMD backend → conformance → GA).
- Capability-gated fast paths: extend the matrix to record which ops have a native fast path per backend (WARMUP Ch. 5).
5. What this lab proves about you
You can produce the strategy artifact, not just the code: a costed, defensible portability decision and the conformance mechanism that enforces it. This is exactly the Head-of-Engineering deliverable the JD asks for — and the backbone of the Phase 12 capstone's platform.
Phase 10 — Observability & Production Operations
Difficulty: ⭐⭐⭐⭐☆ | Estimated Time: 1.5 weeks Roles supported: Head of Engineering [GPU], SRE/Platform Engineer, Production ML-Systems Engineer Hardware needed: none — exporter and release tooling run on any machine
Why This Phase Exists
The JD requires "observability tooling" and "ship production software through a full release cycle, not just a prototype." A GPU platform you can't see is a platform you can't operate: you can't catch a thermal-throttling GPU dragging a training job (Phase 08), a fragmentation leak building toward a day-3 OOM (Phase 05), a KV-cache pressure spike blowing TTFT (Phase 07), or an Xid error about to take a node off the bus (Phase 04). And a platform you can't release safely — versioned, canaried, rollback-ready — is a prototype with customers attached.
You'll build a GPU metrics exporter (Prometheus format, DCGM-style metrics with the right semantics), define SLOs and alerts tied to the failure modes from earlier phases, write failure-drill runbooks, and build release-engineering tooling (version compatibility checks, canary analysis, rollback) — the operational layer that turns the platform into a service.
Concepts
- The three pillars: metrics (Prometheus/DCGM), logs, traces — and what each answers
- GPU metrics that matter and their semantics: utilization (and why "GPU util %" lies), SM activity, memory used/reserved (Phase 05's reserved-vs-allocated), HBM bandwidth, temperature/power/clocks (throttling), ECC errors, Xid errors (Phase 04), NVLink/PCIe traffic
- DCGM (Data Center GPU Manager) and the exporter pattern; Prometheus exposition format; labels and cardinality
- Serving SLIs/SLOs: TTFT/TPOT/goodput (Phase 07), queue depth, KV-cache utilization, error rate — and SLO/error-budget discipline
- Training SLIs: MFU, step time, straggler detection, collective health (Phase 08)
- Alerting: symptom-based vs cause-based, alert fatigue, the "page on SLO burn, ticket on cause" discipline
- eBPF for low-overhead tracing (overview); GPU-aware profiling in production
- Failure drills & runbooks: Xid handling, node drain, the fragmentation-restart, the collective-hang detection (Phase 08), driver-skew (Phase 04)
- Release engineering: semantic versioning, the driver/CUDA/engine compatibility matrix (Phase 03/04), canary deploys + automated analysis, progressive rollout, rollback, feature flags
- The on-call/incident model: severity, escalation, blameless post-mortems; air-gapped/sovereign ops (local-only telemetry, Phase 07/11)
Labs
Lab 01 — GPU Metrics Exporter (Python, Prometheus format)
| Field | Value |
|---|---|
| Goal | Build a Prometheus-format GPU metrics exporter with correct metric semantics (gauges/counters), DCGM-style metrics, and an HTTP /metrics endpoint — fed by a simulated GPU (or real nvidia-smi/DCGM if available). |
| Concepts | Prometheus exposition format, gauge vs counter, metric naming/labels/cardinality, the metrics that matter (Xid, reserved-vs-allocated, throttle, ECC). |
| Steps | 1) python solution.py — serves /metrics; curl localhost:9400/metrics shows GPU metrics. 2) Read the exporter against WARMUP Ch. 2–4. 3) Run the included scrape-and-alert check that fires on a throttling GPU and a fragmentation signal. 4) Extensions: real DCGM/nvidia-smi source, recording rules, a Grafana dashboard JSON. |
| Stack | Python (stdlib http.server) |
| Output | A working exporter + an alert-rules file + a scrape test that fires the right alerts. |
| How to Test | Built-in: /metrics parses as valid Prometheus exposition; the alert evaluator fires GPUThrottling and KVFragmentationRisk on the simulated bad states and stays quiet on healthy ones. |
| Talking Points | Why "GPU utilization %" is misleading (it's "any kernel ran," not "SMs busy"); reserved-vs-allocated as the leak signal (Phase 05); why Xid rate is a top alert; cardinality discipline. |
| Resume Bullet | "Built a Prometheus-format GPU metrics exporter (DCGM-style metrics, correct gauge/counter semantics) with alert rules for throttling, ECC/Xid, and KV-cache fragmentation; validated with an automated scrape-and-alert test." |
| Extensions | Source from real DCGM/nvidia-smi; Prometheus recording rules for goodput; a Grafana dashboard; per-MIG-instance metrics (Phase 06). |
Lab 02 — Release Engineering: Compatibility, Canary & Rollback (Python)
| Field | Value |
|---|---|
| Goal | Build the release-safety tooling a GPU platform needs: a driver/CUDA/engine compatibility checker (Phase 03/04), a canary analyzer (compare canary vs baseline metrics, decide promote/hold/rollback), and a rollout state machine with automatic rollback on SLO burn. |
| Concepts | Semantic versioning, compatibility matrices, canary analysis (statistical comparison), progressive rollout, automated rollback, error budgets. |
| Steps | 1) python solution.py — runs a compatibility check, a canary analysis on simulated metrics, and a rollout that auto-rolls-back on a bad canary. 2) Read against WARMUP Ch. 8–9. 3) Reproduce: an incompatible driver/CUDA pair is rejected before deploy; a canary with regressed p99 TTFT triggers rollback. 4) Extensions: progressive % rollout, error-budget gating, the driver-fleet-upgrade plan (Phase 04). |
| Stack | Python (stdlib) |
| Output | Compatibility checker + canary analyzer + rollout state machine + tests. |
| How to Test | Built-in: incompatible version pairs are flagged; a canary with a significant p99 regression yields ROLLBACK; a healthy canary yields PROMOTE; the rollout state machine reaches the right terminal state. |
| Talking Points | The driver/CUDA/engine compatibility matrix as a release gate (Phase 03/04); why canary analysis must be statistical not eyeballed; error-budget-based release decisions; the fleet driver-upgrade choreography (Phase 04 Q6). |
| Resume Bullet | "Built GPU-platform release tooling: a driver/CUDA compatibility gate, a statistical canary analyzer, and an auto-rollback rollout state machine driven by SLO burn — turning deploys from manual to policy-driven." |
| Extensions | Progressive percentage rollout with per-stage gates; error-budget-based freeze; integrate the Lab 01 exporter's metrics as the canary signal. |
Deliverables Checklist
-
Exporter serves valid Prometheus
/metrics; alert test fires correctly - You can name the GPU metrics that matter and their correct semantics
- Release tooling: compatibility gate + canary + rollback all pass tests
- One failure-drill runbook written (Xid, fragmentation, collective-hang, or driver-skew)
- One page: "our platform's observability + release strategy" (feeds Phase 12)
Interview Relevance
- "What GPU metrics do you alert on, and why is 'GPU utilization' misleading?"
- "nvidia-smi shows 90% util but throughput is low. What's your investigation?"
- "Design a safe release process for a GPU serving platform."
- "How do you catch a fragmentation leak before it OOMs at 3am?"
- "A driver upgrade regressed performance on 5% of the fleet. Walk me through detection and rollback."
Warmup Guide — Observability & Production Operations
Zero-to-expert primer for Phase 10. Builds on every prior phase's failure modes (you monitor what you understand). By the end you can instrument a GPU platform, set SLOs, and ship it through a safe release cycle.
Table of Contents
- Chapter 1: Why Observability Is a Platform Feature, Not an Add-On
- Chapter 2: The Three Pillars — Metrics, Logs, Traces
- Chapter 3: GPU Metrics That Matter (and Their Semantics)
- Chapter 4: The Exporter Pattern and Prometheus Format
- Chapter 5: SLIs, SLOs, and Error Budgets
- Chapter 6: Alerting Without Fatigue
- Chapter 7: Failure Drills and Runbooks
- Chapter 8: Release Engineering — Compatibility, Canary, Rollback
- Chapter 9: Incident Response and the Operating Model
- Lab Walkthrough Guidance
- Success Criteria
- Interview Q&A
- References
Chapter 1: Why Observability Is a Platform Feature, Not an Add-On
Every prior phase produced a failure mode that is invisible without instrumentation:
- Phase 04: an Xid error takes a GPU off the bus — silent until jobs fail.
- Phase 05: a fragmentation leak builds toward a day-3 OOM — silent until 3am.
- Phase 07: KV-cache pressure spikes TTFT past SLO — silent until customers churn.
- Phase 08: one thermal-throttling GPU drags a 512-GPU job to its speed — silent in aggregate metrics.
A GPU platform you can't see is a platform you operate by waiting for outages. Observability is what converts "the cluster feels slow" into "GPU 37 on node 12 is throttling at 84°C, pulling its TP group's MFU to 40%." It's not an SRE afterthought — for a compute platform it's a product feature (especially for sovereign customers who need local-only telemetry, Phase 07 Ch. 9), and the JD lists it explicitly.
The leadership framing: you can't manage what you can't measure, and you can't release safely what you can't observe. This phase is the difference between a prototype and a service.
Chapter 2: The Three Pillars — Metrics, Logs, Traces
The standard decomposition, with what each answers:
- Metrics — numeric time series (GPU temp, TTFT p99, queue depth). Cheap, aggregatable, the backbone of dashboards and alerts. Answers "is something wrong, and how much?" — but not why for a specific request. Prometheus is the de facto standard (Ch. 4).
- Logs — discrete timestamped events (an Xid error, a request failure, a rollback). Answer "what happened to this specific thing?" High cardinality, expensive at scale; structured logging + sampling are the disciplines.
- Traces — the path of one request across services (gateway → scheduler → engine → GPU). Answer "where did this request's latency go?" Essential for the serving stack (Phase 07); OpenTelemetry is the standard.
The mental model: metrics tell you something's wrong, traces tell you where, logs tell you what. A platform needs all three; this phase's labs focus on metrics (the alerting backbone) and the operational glue, with logs/traces as extensions.
Chapter 3: GPU Metrics That Matter (and Their Semantics)
The metrics, and the semantics traps that catch people (Lab 01 implements these correctly):
- "GPU utilization %" (
nvidia-smi'sutilization.gpu): the famous lie. It means "the fraction of time at least one kernel was running" — not "the SMs were busy." A kernel using 5% of the SMs at batch 1 (memory-bound decode, Phase 01) shows ~100% utilization while the GPU is mostly idle. For real work, use SM activity / occupancy (DCGMDCGM_FI_PROF_SM_ACTIVE) and achieved memory bandwidth (DCGM_FI_PROF_DRAM_ACTIVE). - Memory:
usedvs the framework'sallocated— the reserved-vs-allocated gap (Phase 05 Ch. 4) is cache, not a leak; the leak signal is allocated trending up, and the fragmentation signal is largest-free-block shrinking. - Temperature / power / clocks: throttling shows as clocks dropping below
base under load (Phase 03's "thermal throttle" note).
clocks_throttle_reasonsis the smoking gun; temp alone is a lagging indicator. - ECC errors: correctable (logged) vs uncorrectable (a failing GPU — page).
- Xid errors (Phase 04 Ch. 10): the GPU's error codes (Xid 79 = fell off the bus, Xid 48 = double-bit ECC, Xid 13/31 = app/memory faults). Xid rate is a top-tier alert — it predicts node failures.
- NVLink / PCIe throughput: low NVLink traffic when you expected TP activity = a topology misconfiguration (Phase 08).
Gauge vs counter (the format trap, Ch. 4): temperature is a gauge (goes up and down); total ECC errors is a counter (monotonic, you alert on its rate). Mixing them up makes every dashboard wrong.
Chapter 4: The Exporter Pattern and Prometheus Format
Prometheus pulls metrics: your exporter serves a /metrics HTTP endpoint
in a text format; Prometheus scrapes it every N seconds. (DCGM-exporter is
NVIDIA's; Lab 01 builds one.)
The exposition format (simple, and Lab 01 emits it correctly):
# HELP gpu_temperature_celsius GPU die temperature
# TYPE gpu_temperature_celsius gauge
gpu_temperature_celsius{gpu="0",uuid="GPU-abc"} 71
# HELP gpu_xid_errors_total Cumulative Xid errors
# TYPE gpu_xid_errors_total counter
gpu_xid_errors_total{gpu="0"} 3
The rules that matter:
- Naming:
unit-suffixed (_celsius,_bytes,_totalfor counters), namespaced (gpu_,dcgm_). - Labels: dimensions (
gpu,uuid,model). Cardinality discipline — never label by something unbounded (request ID!) or you explode the time-series database. (A real production-cost lesson.) - Gauge vs counter (Ch. 3): get it right or rate() and alerts misbehave.
The exporter pattern is universal (node-exporter, dcgm-exporter, your app's
/metrics); building one teaches the semantics that make dashboards and alerts
correct.
Chapter 5: SLIs, SLOs, and Error Budgets
The discipline that makes "is the platform healthy?" answerable:
- SLI (indicator): a measured signal of user-visible health — for serving, goodput / p99 TTFT / p99 TPOT / error rate (Phase 07); for training, MFU / step-time / straggler-free-fraction (Phase 08).
- SLO (objective): the target — "p99 TTFT < 300ms, 99.9% of the time" or "goodput ≥ X at SLO." Set from user needs, not what's easy to hit.
- Error budget: 100% − SLO. A 99.9% SLO allows 0.1% bad — that budget is spendable: you ship features/releases (Ch. 8) while budget remains, and you freeze risky changes when it's exhausted. This turns "should we deploy?" into a data question.
The leadership point: SLOs aligned to user experience (goodput, not raw throughput — Phase 07 Ch. 6) plus error budgets give you an objective release and reliability policy, replacing arguments with arithmetic. It's also how you balance the JD's "ship fast" against "production-grade."
Chapter 6: Alerting Without Fatigue
The failure mode of monitoring is too many alerts — on-call ignores them, and the real one slips through. The disciplines:
- Page on symptoms, ticket on causes. Page when users are affected (SLO burning, goodput dropping) — that's always actionable. File a ticket (not a page) for causes that might lead there (one GPU throttling, ECC correctable rate up) — investigate in business hours unless they breach an SLO.
- Alert on rate-of-burn, not instantaneous breach. "p99 > 300ms for 5 minutes" not "p99 > 300ms once." Multi-window burn-rate alerts (fast burn = page now, slow burn = ticket) are the SRE-book standard.
- Every page is actionable + has a runbook (Ch. 7). A page you can't act on is noise; delete it or make it a ticket.
- Xid/uncorrectable-ECC are exceptions — cause-based but page-worthy because they reliably predict imminent user impact (a node about to die).
Lab 01's alert rules embody this: GPUThrottling (ticket-class symptom),
KVFragmentationRisk (early warning), tied to thresholds that mean something.
Chapter 7: Failure Drills and Runbooks
A runbook is a written, tested response to a known failure. You write them for the failure modes the earlier phases taught (this is the "production cycle" discipline the JD wants):
- Xid error / GPU fell off the bus (Phase 04): detect via Xid-rate alert → cordon the node → drain jobs (checkpoint-aware, Phase 08) → RMA/reset → health-check → uncordon.
- Fragmentation OOM risk (Phase 05): largest-free-block alert → identify the fragmenting workload → (mitigate) scheduled restart / (fix) size bucketing, paged KV (Phase 07).
- Collective hang (Phase 08): collective-timeout alert names the culprit rank → check that GPU's health → drain → restart from checkpoint.
- Driver/CUDA skew (Phase 04): version-mismatch detection → the fleet-upgrade choreography (Phase 04 Q6).
The drill discipline: practice them (game-days), because an untested runbook is a hypothesis. A platform leader's deliverable is the runbook library plus a culture that exercises it.
Chapter 8: Release Engineering — Compatibility, Canary, Rollback
The JD's "ship through a full release cycle, not just a prototype." The pipeline (Lab 02 builds the core of it):
- Compatibility gate: before deploy, verify the driver/CUDA/engine/model matrix (Phase 03/04). A build needing CUDA 12.4 on a node with a driver supporting only 12.2 must be rejected pre-deploy, not discovered at runtime ("driver insufficient for runtime", Phase 04 Ch. 6). This is a hard gate.
- Canary: deploy to a small slice, compare its SLIs to the baseline statistically (not eyeballed — a p99 regression hides in noise). Decide PROMOTE / HOLD / ROLLBACK from the comparison.
- Progressive rollout: expand by percentage with a gate at each stage, watching error budget (Ch. 5). Any stage that burns budget halts.
- Automated rollback: SLO burn → roll back automatically, fast — image- pinned so rollback is a re-deploy, not archaeology (Phase 04 Q6).
- Feature flags: decouple deploy from release; turn a risky path on for a cohort, off instantly if it misbehaves — no redeploy.
The semantic-versioning discipline underneath: major = breaking, minor = additive, patch = fixes; the ABI/compatibility rules from Phase 09 Ch. 6 apply to your platform's own interfaces. For GPU platforms the compatibility matrix is the highest-risk gate because the failure modes (driver skew, kernel rebuilds — Phase 03 cold-start) are subtle and fleet-wide.
Chapter 9: Incident Response and the Operating Model
When prevention fails (it will), the operating model:
- Severity + escalation: SEV levels by user impact; clear escalation paths; an incident commander role for big ones.
- Detect → mitigate → resolve → learn: mitigate first (roll back, drain, failover — stop the bleeding) before root-causing; resolve and verify; then learn.
- Blameless post-mortems: the failure is the system's, not a person's; output is action items that change the system (a new alert, a runbook, a guardrail) — which feed back into Chapters 6–8. A post-mortem without system changes is theater.
- Air-gapped/sovereign ops (Phase 07/11): no cloud monitoring SaaS, no phone-home — telemetry and incident tooling run inside the customer's environment, which changes your packaging and your on-call model.
The leadership reframe: incidents are inevitable; a learning operating model (every incident hardens the system) is what compounds reliability over time — the same continuous-improvement loop as Phase 08's "failures are the steady state."
Lab Walkthrough Guidance
Order: Lab 01 → Lab 02 (instrument first, then release safely on those signals).
Lab 01 (exporter):
python solution.py;curl localhost:9400/metricsand read the format against Ch. 4. Verify gauge/counter types are correct.- Run the scrape-and-alert check; confirm
GPUThrottlingandKVFragmentationRiskfire on the bad simulated states and stay quiet on healthy ones — tie each to Ch. 3/6. - Extensions: real DCGM/
nvidia-smisource; a recording rule for goodput; a Grafana dashboard JSON.
Lab 02 (release tooling):
python solution.py; read the compatibility gate, canary analyzer, and rollout state machine against Ch. 8.- Reproduce: an incompatible driver/CUDA pair rejected pre-deploy; a canary with regressed p99 → ROLLBACK; healthy canary → PROMOTE.
- Extensions: progressive % rollout, error-budget gating, wiring Lab 01's metrics as the canary signal.
Success Criteria
- You can explain the three pillars and what each answers (Ch. 2)
- You can name the GPU metrics that matter, their gauge/counter type, and why "GPU util %" is misleading (Ch. 3) — your exporter emits them correctly
- You can write a valid Prometheus exposition and explain cardinality (Ch. 4)
- You can define SLI/SLO/error-budget and tie them to goodput (Ch. 5)
- You can apply "page on symptoms, ticket on causes" + burn-rate alerts (Ch. 6)
- You can write a failure-drill runbook for an earlier-phase failure (Ch. 7)
- You can describe the compatibility-gate → canary → rollback pipeline (Ch. 8) — Lab 02 implements it
- You can describe a blameless, learning incident model (Ch. 9)
Interview Q&A
Q1: What GPU metrics do you alert on, and why is "GPU utilization" misleading?
A: nvidia-smi's utilization.gpu means "fraction of time ≥1 kernel ran," not
"SMs busy" — a memory-bound batch-1 decode (Phase 01) shows ~100% while using a
few percent of the SMs, so it's useless as a saturation signal. I alert on
user-facing SLOs first (goodput/p99 TTFT, Phase 07), then on cause-based signals
that predict impact: Xid error rate and uncorrectable ECC (page —
imminent node death, Phase 04), clock throttling under load (thermal/power —
silent perf loss), largest-free-block shrinking (fragmentation leak, Phase
05), and allocated memory trending up (real leak vs cache). For utilization I
use DCGM's SM-active and DRAM-active, not utilization.gpu. Each alert is
gauge-or-counter-correct and has a runbook.
Q2: nvidia-smi shows 90% utilization but throughput is low. Investigate. A: 90% utilization.gpu just means kernels are running, not that they're doing useful work — classic memory-bound or badly-occupied kernels (Phase 01/02). I'd check DCGM SM-active (are the SMs actually busy or is one warp running?) and DRAM-active (is it bandwidth-bound — expected for decode?), then the roofline (Phase 01): if it's memory-bound decode, "low throughput at high util" is normal and the fix is batching/quantization (Phase 07), not kernel tuning. If SM-active is also low, it's latency-bound (occupancy/stalls — Phase 02 Ch. 11) or the GPU is throttling (check clocks). If clocks are fine and it's genuinely under-utilized, look upstream: data loading, sync points, or PCIe transfers in the hot path (Phase 02/05). The lesson: utilization.gpu is the start of the investigation, never the conclusion.
Q3: Design a safe release process for a GPU serving platform. A: Five gates. (1) Compatibility: a hard pre-deploy check of the driver/CUDA/ engine/model matrix (Phase 03/04) — reject incompatible pairs before they reach a node, because the failure mode (driver-insufficient, JIT cold-start) is fleet-wide and subtle. (2) Canary: deploy to a small slice, compare SLIs (goodput, p99 TTFT/TPOT) to baseline statistically, decide promote/hold/ rollback. (3) Progressive rollout by percentage, each stage gated on error budget (Phase 07/this phase Ch. 5). (4) Automated rollback on SLO burn — fast, image-pinned so it's a re-deploy not archaeology. (5) Feature flags to decouple deploy from release. Underneath: semantic versioning and the Phase 09 ABI discipline for our own interfaces. The whole thing is policy-driven by error budget, so "should we ship?" is arithmetic, not opinion.
Q4: How do you catch a fragmentation leak before it OOMs at 3am? A: Export the right signal. The OOM is preceded by largest-free-block shrinking while allocated stays flat (external fragmentation, Phase 05 Ch. 5) — so I alert on largest-free-block trending toward the size of typical large allocations, well before reserved hits capacity. That distinguishes it from a real leak (allocated trending up) and from healthy cache (reserved high, largest-block fine). The alert is a ticket (early warning, business hours) that escalates to a page if it crosses into SLO-risk territory. The runbook (Ch. 7): identify the fragmenting workload, mitigate with a scheduled restart, fix with size bucketing / paged KV (Phase 07). The point is the day-3 OOM is predictable from the right metric — most platforms just don't export largest-free-block.
Q5 (leadership): A driver upgrade regressed performance on 5% of the fleet. Walk me through detection and response. A: Detection should have happened in canary (Ch. 8) — a statistical p99/goodput comparison on the upgraded slice vs baseline would flag a regression before fleet rollout; if it reached 5%, either canary coverage missed the affected hardware SKU or the regression was load-dependent. Response: (1) the progressive rollout halts automatically on error-budget burn (Ch. 5); (2) auto-rollback the affected cohort — image-pinned so it's a fast re-deploy (Phase 04 Q6); (3) confirm recovery via the SLIs. Then learn (Ch. 9): blameless post-mortem → action items — add the affected GPU SKU to the canary matrix, add a perf-regression gate to the compatibility check, maybe a driver-version performance benchmark in CI. Driver upgrades shipping perf regressions is a known risk (Phase 04), so the systemic fix is a perf-conformance gate (Phase 09 Ch. 7) on driver changes, not just "be more careful."
References
- Beyer et al., Site Reliability Engineering (Google) — SLOs, error budgets, alerting — https://sre.google/books/
- The follow-up, The SRE Workbook — multi-window burn-rate alerts
- NVIDIA DCGM documentation + dcgm-exporter — https://docs.nvidia.com/datacenter/dcgm/ , https://github.com/NVIDIA/dcgm-exporter
- Prometheus exposition format + naming/cardinality best practices — https://prometheus.io/docs/practices/naming/
- NVIDIA Xid errors reference — https://docs.nvidia.com/deploy/xid-errors/
- OpenTelemetry (traces) — https://opentelemetry.io
- Brendan Gregg on eBPF / production profiling — https://www.brendangregg.com/ebpf.html
- "Canary Analysis" (Netflix Kayenta) — automated statistical canary — https://github.com/spinnaker/kayenta
- Cross-track: Phase 04 WARMUP Ch. 6/10 (Xid, driver skew), Phase 05 Ch. 5 (fragmentation), Phase 07 Ch. 6 (goodput), Phase 08 Ch. 8 (failure at scale)
🛸 Hitchhiker's Guide — Phase 10: Observability & Production Ops
Read this if: you can build a GPU platform but couldn't yet see it failing or release it safely. Field notes; derivations in the WARMUP.
0. The 30-second mental model
You can't operate what you can't see, or release safely what you can't observe. Metrics (Prometheus/DCGM) say something's wrong, traces say where, logs say what. Alert on user-facing SLOs (goodput, p99 TTFT) + a few cause signals that predict death (Xid, ECC, throttling). Release through compatibility gate → canary → progressive rollout → auto-rollback, all governed by error budget.
1. GPU metrics card (with the traps)
| Metric | Type | Trap / signal |
|---|---|---|
| utilization.gpu | gauge | lies — "a kernel ran," not "SMs busy." Use DCGM SM-active. |
| SM-active / DRAM-active | gauge | the real saturation/bandwidth signals |
| memory reserved vs allocated | gauge | gap = cache (Phase 05); leak = allocated up |
| largest-free-block | gauge | fragmentation early warning — most don't export it |
| clocks + throttle-reasons | gauge | throttling = silent perf loss; temp lags |
| ECC correctable / uncorrectable | counter | uncorrectable = page (dying GPU) |
| Xid errors | counter | top alert — predicts node failure (Phase 04) |
| NVLink/PCIe throughput | counter | low NVLink during TP = topology misconfig (Phase 08) |
Gauge (up/down) vs counter (monotonic, alert on rate()). Get it right or every dashboard is wrong. Never label by request ID (cardinality explosion).
2. SLO discipline
SLI = measured user health (goodput, p99 TTFT). SLO = target ("p99 TTFT<300ms 99.9%"). Error budget = 1−SLO, spendable: ship while budget remains, freeze when exhausted. "Should we deploy?" becomes arithmetic.
3. Alerting that doesn't get ignored
- Page on symptoms (SLO burning → users hurt → always actionable).
- Ticket on causes (one GPU throttling → investigate in hours).
- Burn-rate, multi-window (fast burn = page, slow = ticket), not instant threshold.
- Every page: actionable + has a runbook. Exceptions paged anyway: Xid, uncorrectable ECC (reliably predict imminent impact).
4. Runbook library (one per earlier-phase failure)
| Failure | Detect | Respond |
|---|---|---|
| Xid / off the bus (P04) | Xid-rate alert | cordon → drain (ckpt-aware) → RMA → health-check |
| Fragmentation OOM (P05) | largest-free-block | find workload → restart / bucket / paged KV |
| Collective hang (P08) | collective timeout | culprit rank → drain → restart from ckpt |
| Driver skew (P04) | version mismatch | fleet-upgrade choreography (P04 Q6) |
Practice them (game-days). An untested runbook is a hypothesis.
5. Release pipeline (Lab 02)
compatibility gate (driver/CUDA/engine matrix, P03/P04 — HARD gate, pre-deploy)
-> canary (statistical SLI compare, not eyeballed) -> PROMOTE/HOLD/ROLLBACK
-> progressive % rollout (each stage gated on error budget)
-> auto-rollback on SLO burn (image-pinned = fast)
-> feature flags (decouple deploy from release)
The compatibility matrix is the highest-risk GPU gate — driver skew and JIT cold-start (P03) are subtle and fleet-wide.
6. Incident model
Detect → mitigate first (roll back/drain/failover — stop bleeding) → resolve → blameless post-mortem whose output is system changes (new alert, runbook, guardrail). No system change = theater. Air-gapped: all of this runs inside the customer's env (P07/P11).
7. War stories
- "90% util, terrible throughput" — memory-bound decode; utilization.gpu was meaningless. SM-active told the truth.
- "3am OOM at 60% memory" — fragmentation; nobody exported largest-free-block. Added it; now it's a daytime ticket.
- "Driver upgrade cut perf 8% on one SKU" — canary matrix didn't cover that SKU. Auto-rollback fired on budget burn; post-mortem added the SKU + a perf gate.
- "Alert storm, real page missed" — paging on causes (every throttle). Moved causes to tickets, paged only on SLO burn. Signal returned.
8. Exit bar
You can instrument a GPU platform with correct metric semantics, set SLOs and non-fatiguing alerts, write tested runbooks for the failure modes from Phases 04–08, and ship through a compatibility-gated, canary-analyzed, auto-rollback pipeline. You've built the exporter and the release tooling. Next: Phase 11 — the security/licensing primitives that make the platform a commercial, sovereign-ready product.
Lab 01 — GPU Metrics Exporter (Prometheus format)
Phase: 10 — Observability | Difficulty: ⭐⭐⭐☆☆ | Time: 4–6 hours
Language: Python (stdlib http.server) | Hardware: none (simulated GPU; real DCGM/nvidia-smi optional)
Concept primer:
../WARMUP.mdCh. 2–6.
Run
python solution.py # serves /metrics on :9400 + runs the alert test
# in another shell: curl localhost:9400/metrics
0. The mission
Build a DCGM-style GPU metrics exporter with correct metric semantics
(gauge vs counter), the metrics that actually matter (WARMUP Ch. 3), and a
Prometheus /metrics endpoint — then prove the alert rules fire on the right
bad states and stay quiet on healthy ones.
1. What the output shows
serving GPU metrics on http://localhost:9400/metrics
== scrape + alert evaluation ==
gpu 0: temp=71C clocks=1410MHz throttled=no xid_rate=0 largest_free=18GB OK
gpu 1: temp=88C clocks=1100MHz throttled=YES xid_rate=0 largest_free=16GB -> GPUThrottling
gpu 2: temp=70C clocks=1410MHz throttled=no xid_rate=2 largest_free=15GB -> GPUXidErrors
gpu 3: temp=69C clocks=1410MHz throttled=no xid_rate=0 largest_free=1GB -> KVFragmentationRisk
alerts fired: GPUThrottling(gpu1), GPUXidErrors(gpu2), KVFragmentationRisk(gpu3)
healthy gpu0 fired nothing.
And /metrics emits valid Prometheus exposition:
# HELP gpu_temperature_celsius GPU die temperature
# TYPE gpu_temperature_celsius gauge
gpu_temperature_celsius{gpu="0"} 71
# TYPE gpu_xid_errors_total counter
gpu_xid_errors_total{gpu="2"} 2
gpu_memory_largest_free_bytes{gpu="3"} 1073741824
...
- Correct semantics: temperature/clocks/largest-free are gauges; Xid and
ECC totals are counters (you alert on their rate). Getting this wrong makes
rate()and alerts misbehave (WARMUP Ch. 4). - The metrics that matter: throttling (silent perf loss), Xid (predicts node
death), largest-free-block (fragmentation early warning — Phase 05) — not just
the misleading
utilization.gpu(WARMUP Ch. 3). - Alerts fire correctly: each bad state triggers its alert; the healthy GPU triggers nothing.
2. Reading order (solution.py)
Metric(gauge/counter) andrender_prometheus— the exposition format.simulate_gpus— the simulated fleet, including the three bad states.ALERT_RULES+evaluate_alerts— the rule logic (WARMUP Ch. 6).- The HTTP handler serving
/metrics.
3. Extensions
- Real source: replace the simulator with
nvidia-smi --query-gpu=... --format=csvparsing, or DCGM (dcgmi dmon/ the DCGM Python bindings) where a GPU exists. - Recording rules: add a Prometheus recording rule that computes goodput from request metrics (Phase 07).
- Grafana dashboard: export a dashboard JSON with the key panels (temp, throttle, Xid rate, largest-free trend).
- Per-MIG metrics: emit metrics per MIG instance (Phase 06), with the right labels.
- Burn-rate alert: implement a multi-window burn-rate alert on a simulated p99 TTFT SLO (WARMUP Ch. 6).
4. Common pitfalls
- Counter as gauge (or vice versa): temperature must be a gauge; total Xid
errors a counter. Emit the wrong
# TYPEandrate()breaks. - Cardinality explosion: never label by request ID or timestamp — unbounded label values blow up the TSDB (a real production cost). Labels are bounded dimensions (gpu, uuid, model).
- Alerting on
utilization.gpu: it's the misleading metric (WARMUP Ch. 3); alert on SM-active / SLOs / cause-signals instead. - Instantaneous-threshold alerts: real alerts need a
for:duration / burn-rate to avoid flapping.
5. What this lab proves about you
You can instrument a GPU platform with correct metric semantics and alert on the signals that predict real failures (not the ones that look impressive). When an incident hits, your dashboards tell the truth — which is the difference between a 5-minute diagnosis and a 5-hour one.
Lab 02 — Release Engineering: Compatibility, Canary & Rollback
Phase: 10 — Observability | Difficulty: ⭐⭐⭐⭐☆ | Time: 4–6 hours Language: Python (stdlib) | Hardware: none
Concept primer:
../WARMUP.mdCh. 8.
Run
python solution.py
0. The mission
Build the release-safety tooling that turns "ship the prototype" into "ship through a full release cycle" (the JD's requirement):
- Compatibility gate — reject incompatible driver/CUDA/engine pairs before deploy (Phase 03/04's skew failures are fleet-wide; catch them pre-deploy).
- Canary analyzer — compare a canary's SLIs to baseline statistically; decide PROMOTE / HOLD / ROLLBACK.
- Rollout state machine — progressive promotion with automatic rollback on SLO burn.
1. What the output shows
== compatibility gate ==
driver=535 (max CUDA 12.2), build needs CUDA 12.4 -> REJECTED (driver too old)
driver=550 (max CUDA 12.4), build needs CUDA 12.4 -> OK
engine vLLM 0.5 needs model arch 'llama3' -> OK
== canary analysis (p99 TTFT, ms) ==
baseline p99=240 canary p99=251 delta=+4.6% within noise -> PROMOTE
baseline p99=240 canary p99=330 delta=+37.5% significant -> ROLLBACK
== rollout state machine ==
stage 5% : canary healthy -> promote to 25%
stage 25% : canary healthy -> promote to 50%
stage 50% : SLO burn detected -> ROLLBACK to baseline
final state: ROLLED_BACK (budget protected)
- Compatibility: the driver/CUDA gate rejects a build needing CUDA 12.4 on a 12.2 driver — the "driver insufficient for runtime" failure (Phase 04 Ch. 6), caught before it reaches a node.
- Canary: a +4.6% p99 change is within noise (promote); a +37.5% regression is significant (rollback). Statistical, not eyeballed (WARMUP Ch. 8).
- Rollout: progresses through stages, auto-rolls-back when a stage burns the error budget — image-pinned so rollback is fast.
2. Reading order (solution.py)
Versionparsing +check_compatibility— the matrix gate.analyze_canary— the statistical comparison (effect size + threshold).Rolloutstate machine — stages, gating, auto-rollback.
3. Extensions
- Progressive % rollout with configurable stages and per-stage gates.
- Error-budget freeze: track budget consumption; freeze releases when exhausted (WARMUP Ch. 5).
- Wire Lab 01's metrics as the canary signal (real exporter → canary decision).
- Fleet driver-upgrade plan: model the Phase 04 Q6 choreography (inventory → canary → drain → upgrade → health-check → progressive) as a state machine.
- Proper stats: replace the threshold with a Welch's t-test / Mann-Whitney on sampled latencies.
4. Common pitfalls
- Eyeballing canaries: a real p99 regression hides in noise; the comparison must be statistical with an effect-size threshold.
- Discovering incompatibility at runtime: the compatibility gate must be pre-deploy — a runtime "driver insufficient" is a fleet incident.
- Manual rollback: by the time a human reacts, the budget's gone; rollback on SLO burn must be automatic and fast (image-pinned).
- No semantic versioning: without major/minor/patch discipline you can't reason about compatibility at all.
5. What this lab proves about you
You can build the release machinery that makes a GPU platform safe to ship continuously — compatibility-gated, canary-analyzed, auto-rollback — which is exactly the "full release cycle, not just a prototype" the JD demands, and the operational backbone of the Phase 12 capstone.
Phase 11 — Security: Fingerprinting, License Enforcement & Software Protection
Difficulty: ⭐⭐⭐⭐⭐ | Estimated Time: 2 weeks Roles supported: Head of Engineering [GPU], Security/Platform Engineer, Sovereign-AI Engineer Hardware needed: none — all labs are real, runnable C/Python; no GPU required
Scope & ethics: this phase trains the defensive, commercial-protection security a hardware-agnostic AI platform sells: binding software to authorized hardware, enforcing licenses offline, raising the cost of tampering, and attesting integrity — for legitimate licensing and sovereign/regulated deployment. It is not a guide to bypassing protections, piracy, or evasion. Every technique is taught with its limits and its defeat conditions, because honest threat-modeling is the point. For deeper offensive/defensive security, see the Security track.
Why This Phase Exists
The JD's nice-to-haves are unusually specific: "Experience with security primitives: hardware fingerprinting, license enforcement, software protection, code obfuscation" and "sovereign AI, on-premise LLM deployment, air-gapped systems, AI in regulated industries." These aren't generic infosec — they're the commercial-protection layer that turns a hardware-agnostic platform (Phase 09) into a sellable product that can be deployed on a customer's premises, even air-gapped, without losing control of the software.
This is what makes the platform a business: a sovereign customer (defence, finance, healthcare, government) runs your stack on their own hardware, behind their own airtight network. You can't phone home to a license server, you can't push updates freely, and you must ensure the software runs only on the hardware they licensed. Hardware fingerprinting, offline signed licenses, anti-tamper, and attestation are exactly those controls.
You'll build a hardware fingerprinting tool, an Ed25519-signed offline license system (grace periods, hardware binding, air-gap activation), and an anti-tamper/integrity lab in C — each with an honest threat model and stated defeat conditions.
Concepts
- The commercial-protection threat model: what you're protecting (the software's authorized use), against whom (license violation, not nation-state), and the honest ceiling (protection raises cost, never achieves impossibility on hardware the attacker controls)
- Hardware fingerprinting: deriving a stable machine ID from hardware attributes (GPU UUIDs, CPU/board IDs, MAC); stability vs spoofability; fuzzy matching for partial hardware changes
- Cryptographic foundations (just enough): hashing, HMAC, public-key signatures (Ed25519), why signing not encryption for licenses, key management (the private key is the crown jewel)
- License enforcement: signed license tokens binding (features, expiry, hardware fingerprint); offline verification; grace periods; clock-rollback resistance; revocation in a connected world
- Air-gap activation: challenge-response activation without network (the offline-license flow regulated customers require)
- Software protection / anti-tamper: integrity self-checks (checksums/signatures over code), tamper response, why these only raise cost; the difference from DRM theater
- Code obfuscation: what it does and doesn't buy (raises reverse-engineering cost, never prevents it); control-flow/string obfuscation overview; when it's worth it
- Attestation: TPM / measured boot / GPU attestation (NVIDIA confidential computing); proving a known-good software+hardware state to a remote party — the strong end of the spectrum
- Confidential computing: TEEs, encrypted memory, GPU CC (H100); for protecting the customer's data and your model weights on shared/untrusted infrastructure
- Sovereign/air-gapped deployment security: no phone-home, local secrets, supply-chain integrity, auditability (ties to Phase 07 Ch. 9, Phase 10 Ch. 9)
- The security-vs-usability and protection-vs-cost tradeoffs a leader owns
Labs
Lab 01 — Hardware Fingerprinting + Ed25519 Offline License System (Python)
| Field | Value |
|---|---|
| Goal | Build a hardware fingerprint (stable machine ID from hardware attributes with fuzzy matching) and an offline license system: an Ed25519-signed token binding features + expiry + fingerprint, verified with no network, with a grace period and clock-rollback resistance. |
| Concepts | Hardware fingerprinting, fuzzy matching, Ed25519 signatures, signed (not encrypted) licenses, offline verification, grace periods, clock-rollback resistance, the private key as crown jewel. |
| Steps | 1) python solution.py — generates a keypair, fingerprints the (simulated) machine, issues a signed license bound to it, verifies it offline, and runs the attack/tamper tests. 2) Read against WARMUP Ch. 2–5. 3) Reproduce: a tampered license fails verification; a license bound to a different fingerprint is rejected; fuzzy matching tolerates one swapped NIC but not a different machine; clock rollback is detected. 4) Extensions: air-gap challenge-response activation, revocation, feature gating. |
| Stack | Python (cryptography if available, else a bundled minimal Ed25519 — pure-stdlib fallback included) |
| Output | A working license issuer + offline verifier + a test suite covering tamper, wrong-host, fuzzy-match, and rollback cases. |
| How to Test | Built-in: valid license verifies; any byte flipped → signature fails; wrong fingerprint → rejected; 1-component hardware change → still valid (fuzzy), 3-component → rejected; expired/rolled-back clock → rejected; grace period honored. |
| Talking Points | Why sign rather than encrypt a license; why the fingerprint must be fuzzy (hardware changes legitimately); the honest defeat conditions (attacker with the binary + root can patch the verifier — protection raises cost, attestation is the stronger answer); air-gap activation flow. |
| Resume Bullet | "Built an offline GPU-software licensing system: hardware fingerprinting with fuzzy matching + Ed25519-signed licenses binding features/expiry/hardware, with grace periods and clock-rollback resistance; threat-modeled with stated defeat conditions." |
| Extensions | Air-gap challenge-response activation; a revocation list for connected deployments; per-feature gating; rate-limited fingerprint re-binding. |
Lab 02 — Anti-Tamper Integrity Self-Check + Attestation Model (C + Python)
| Field | Value |
|---|---|
| Goal | Implement a C program that verifies its own code-section integrity at runtime (signature over the .text segment) and responds to tampering — then model remote attestation (proving a known-good state to a verifier), with honest limits throughout. |
| Concepts | Code-integrity self-check, signing a binary section, tamper response, why self-checks only raise cost, attestation (measured state → remote verification), the trust-root problem, confidential computing overview. |
| Steps | 1) make && ./protected — runs normally and self-verifies; ./tamper.py protected flips a byte; rerun → tamper detected. 2) Read the integrity check against WARMUP Ch. 6–8. 3) Run python attestation.py — models a measure→quote→verify attestation flow and shows why it's strictly stronger than self-check (the verifier is external). 4) Extensions: control-flow obfuscation experiment, a real TPM/GPU-attestation design doc. |
| Stack | C (integrity self-check) + Python (attestation model + tamper tool) |
| Output | A self-verifying binary + a tamper demonstration + an attestation flow model + a written threat model with defeat conditions. |
| How to Test | Built-in: unmodified binary self-verifies and runs; a tampered binary detects the change and refuses; the attestation model accepts a known-good measurement and rejects a tampered one, and the README explains why an external verifier beats a self-check. |
| Talking Points | Why a self-check is defeatable (patch the checker too) but still useful (raises cost, catches casual tampering); why attestation with an external root of trust (TPM/GPU CC) is the real answer; obfuscation's true value; the protection-vs-cost curve you'd actually deploy for a sovereign customer. |
| Resume Bullet | "Implemented runtime code-integrity self-verification in C with tamper response, plus a remote-attestation flow model; documented the honest threat model (self-checks raise cost; external attestation is the trust root) for sovereign deployments." |
| Extensions | String/control-flow obfuscation and measure the RE-cost increase; a TPM measured-boot or NVIDIA-GPU-confidential-computing attestation design doc; encrypted-weights-at-rest with attestation-gated decryption. |
Deliverables Checklist
- License system: tamper, wrong-host, fuzzy-match, rollback tests all pass
- You can explain sign-vs-encrypt and the private-key-as-crown-jewel rule
- Anti-tamper: self-check detects tampering; you can state its defeat condition
- Attestation model runs; you can explain why it beats a self-check
- A written threat model with honest defeat conditions for each technique
- One page: "our platform's sovereign-deployment security architecture" (feeds Phase 12)
Interview Relevance
- "Design a license system for software deployed air-gapped on a customer's hardware."
- "Why sign a license instead of encrypting it? Where does the private key live?"
- "How do you bind software to specific GPUs, and what happens when they swap a NIC?"
- "Anti-tamper self-checks — what do they actually buy you, and how are they defeated?"
- "What's the strong answer to 'prove this runs unmodified on authorized hardware'?" (attestation)
- "Protect a customer's data AND your model weights on infrastructure you don't control." (confidential computing)
Warmup Guide — Security: Fingerprinting, Licensing & Protection
Zero-to-expert primer for Phase 11 — the commercial-protection layer that makes a hardware-agnostic platform a sovereign-ready product. Defensive/licensing focus; every technique stated with its honest defeat conditions. No security background assumed.
Table of Contents
- Chapter 1: The Commercial-Protection Threat Model
- Chapter 2: Cryptographic Foundations (Just Enough)
- Chapter 3: Hardware Fingerprinting
- Chapter 4: License Enforcement — Signed, Offline, Bound
- Chapter 5: Air-Gap Activation
- Chapter 6: Software Protection and Anti-Tamper
- Chapter 7: Code Obfuscation — What It Does and Doesn't Buy
- Chapter 8: Attestation — The Strong End of the Spectrum
- Chapter 9: Confidential Computing and Sovereign Deployment
- Lab Walkthrough Guidance
- Success Criteria
- Interview Q&A
- References
Chapter 1: The Commercial-Protection Threat Model
Start here, because every later chapter is meaningless without it. Threat modeling = who are you protecting what from, and what's the honest ceiling.
- What you protect: authorized use of your software — that it runs only on hardware the customer licensed, only for the term/features they paid for, and that it hasn't been tampered with. Plus, in sovereign settings, your model weights and the customer's data on infrastructure you don't fully control.
- Against whom: primarily license violation — a customer running more instances than licensed, past expiry, or on extra hardware. Secondarily casual tampering and IP extraction. Not a resourced nation-state with physical access (that's a different, mostly-unwinnable game).
- The honest ceiling — say this in every interview: on hardware the attacker fully controls (root, debugger, the binary in hand), you cannot achieve impossibility; you can only raise cost. A determined attacker with your binary can eventually patch out any self-contained check. Protection is an economic deterrent (make violation cost more than a license) plus a legal one (signed licenses are evidence). The only technically strong guarantees come from hardware roots of trust (attestation, Ch. 8; confidential computing, Ch. 9) — and even those have a threat model.
This honesty is not defeatism; it's the difference between security engineering and DRM theater. A leader who claims "uncrackable" loses credibility; one who says "this raises violation cost above the license price, here are the defeat conditions, and attestation is our path to a hardware-rooted guarantee" is trusted. Every technique below is presented with its defeat condition.
Chapter 2: Cryptographic Foundations (Just Enough)
The minimum to build licensing correctly (Lab 01):
- Hashing (SHA-256): one-way fingerprint of data; same input → same hash, any change → wildly different hash. Used to fingerprint hardware (Ch. 3) and code (Ch. 6). Not a secret; not reversible.
- HMAC: a keyed hash — proves someone with the key produced it. Symmetric (same key to make and check), so the verifier holds the secret — bad for software you ship (the key is in the binary). Hence:
- Public-key signatures (Ed25519): a private key signs, a public key verifies. The verifier needs only the public key — which can ship in the binary safely, because it can't forge signatures. This asymmetry is the whole basis of license verification: you sign licenses with a private key you guard; every deployed copy verifies with the embedded public key.
- Sign vs encrypt (a top interview point): a license is signed, not encrypted. You don't need the license secret (it says "customer X, 8 GPUs, expires Dec") — you need it unforgeable and unmodifiable. Signing gives integrity + authenticity with a public verifier; encryption would give secrecy you don't need and require key distribution you can't do safely. Encrypt data (model weights, Ch. 9); sign licenses.
- Ed25519 specifically: fast, small keys/signatures (32/64 bytes), misuse-resistant (no nonce footguns like ECDSA). The modern default.
- Key management — the crown jewel: the private signing key is the entire security of the system. If it leaks, anyone forges licenses; rotating it invalidates every issued license. It lives in an HSM / KMS, never in the repo, never on a build machine that ships. This is the single most important operational fact of the chapter.
Chapter 3: Hardware Fingerprinting
The goal: derive a stable identifier for a machine from its hardware, so a license can be bound to it (Ch. 4) — the software runs here, not on an unlicensed copy.
The inputs (for a GPU platform): GPU UUIDs (nvidia-smi -L gives stable
per-GPU UUIDs — the strongest signal), CPU/board serial, MAC addresses, disk
serials. Hash a canonical combination → the fingerprint.
The two tensions you must engineer around:
- Stability vs change: legitimate hardware changes — a swapped NIC, an added disk, a replaced failed GPU — must not break the license, or you'll generate support tickets and angry customers. So you don't hash everything into one brittle value; you take multiple attributes and use fuzzy matching: the fingerprint matches if enough components agree (e.g., ≥ N of M), tolerating a few changes but rejecting a wholly different machine. Lab 01 implements this.
- Stability vs spoofability: an attacker can spoof MACs and (with effort) other IDs. GPU UUIDs are harder to forge than MACs. The honest defeat condition (Ch. 1): a determined attacker controlling the machine can feed your fingerprinting code fake values. Fingerprinting raises cost and binds the honest majority; it is not unspoofable. Combine with signed licenses (the attacker still can't forge the license binding the fingerprint) and, for strong guarantees, attestation (Ch. 8 — a hardware root vouches for the real IDs).
Misconception: "fingerprint = one perfect hardware hash." No — that's brittle (one hardware change locks the customer out) and no more secure. Fuzzy, multi-attribute matching with a signed binding is the engineered answer.
Chapter 4: License Enforcement — Signed, Offline, Bound
The core artifact (Lab 01 builds it): a license token the platform verifies offline (no license server — sovereign customers are air-gapped, Ch. 5).
Contents of a license (the claims):
{ customer, features:[...], max_gpus, issued_at, expires_at,
hardware_fingerprint, license_id } + an Ed25519 signature over all of it
The flow:
- Issuance (you, online, with the private key): fill the claims, bind the customer's fingerprint (Ch. 3), sign with the private key (Ch. 2).
- Verification (the deployed platform, offline, with the embedded public
key): check the signature (unforged + unmodified), check
expires_atvs the clock, check the local fingerprint fuzzy-matches the bound one, check requested features/gpu-count are within the license.
The hard engineering details (each a Lab 01 test):
- Grace period: don't hard-fail the instant the clock passes
expires_at— a brief grace window avoids outages from renewal lag (a production kindness), but it's bounded (you can't grace forever). - Clock-rollback resistance: a license checks expiry against the system clock — which the customer controls. An attacker rolls the clock back to before expiry. Mitigation: persist a monotonic high-water-mark of the latest time ever seen (in a tamper-evident store) and reject a clock that's earlier than the high-water-mark. Honest limit (Ch. 1): a fully-controlled machine can tamper the store too — this raises cost, attestation is the strong fix.
- Revocation: trivial when connected (a revocation list); genuinely hard air-gapped (you can't reach the machine) — which is why air-gapped licenses are usually short-term with re-activation (Ch. 5) rather than perpetual.
- Defeat condition (always state it): an attacker with the binary can patch the verifier to skip the check entirely — the signature math is sound, but the call site is in code they control. Anti-tamper (Ch. 6) raises that cost; attestation (Ch. 8) is the real answer.
Sign vs encrypt, again: the license is signed. You may additionally not encrypt it (it's readable — that's fine; it's not secret), or lightly encode it for tidiness. Don't conflate signing (what protects it) with encryption (what you don't need here).
Chapter 5: Air-Gap Activation
Sovereign customers run air-gapped — no network from the deployment to you, ever. So the connected "call the license server" model is impossible. The offline activation flow (Lab 01 extension):
- The deployment generates a challenge: its hardware fingerprint + a nonce, displayed/exported as a short code or file.
- A human carries that challenge out-of-band (a portal on a different, connected machine; a phone call; a USB transfer) to your activation service.
- Your service verifies entitlement and issues a signed activation response (the license, bound to that fingerprint + nonce) — carried back in.
- The deployment verifies the signature offline (Ch. 4) and activates.
This is exactly how regulated/air-gapped software (and some GPU vendors' offline licensing) works. The properties: no inbound/outbound network at the deployment; the nonce prevents replay; the signature prevents forgery; renewal repeats the out-of-band dance (which is why terms are often longer to reduce friction). The leadership point: air-gap support is a product requirement for sovereign deals, and it reshapes licensing, updates (Phase 10), and observability (local-only).
Chapter 6: Software Protection and Anti-Tamper
Once you've signed and bound a license, the attacker's move (Ch. 4 defeat condition) is to patch your binary to skip the check. Anti-tamper raises the cost of that.
Integrity self-check (Lab 02 builds it): the program verifies its own code at
runtime — compute a hash/signature over its .text (code) section and compare to
an embedded expected value; if it differs, the binary's been patched → refuse to
run (or degrade). This catches casual patching (flip a jne to jmp and the
hash changes).
The honest defeat condition (Ch. 1, stated plainly): the checker itself is code the attacker controls — they can patch out the check, or patch the expected hash, or hook the comparison. A self-check is not a strong guarantee; it's a cost-raiser that defeats casual tampering and forces the attacker to defeat the checker too (and any redundant/obfuscated checkers you add). It's a speed bump, an honest one — useful, never sufficient alone. The strong answer is an external verifier the attacker can't patch: attestation (Ch. 8).
Tamper response design: detection → response (refuse, degrade, alert). Avoid "DRM theater" (aggressive responses that punish honest users on false positives — a hardware change tripping a check). Calibrate to the threat: for licensing, a clean refusal + a clear message beats self-destruction.
Chapter 7: Code Obfuscation — What It Does and Doesn't Buy
Obfuscation transforms code to be hard for a human (and tooling) to understand — control-flow flattening, string encryption, dead-code insertion, opaque predicates — without changing behavior.
What it buys: it raises the time to reverse-engineer or locate a check (Ch. 6) — useful to slow IP extraction and make patching the license check tedious.
What it does NOT buy (the honest part): it never makes code unreadable — the CPU must execute it, so a determined analyst with a debugger always can, eventually. It's cost, measured in attacker-hours, not impossibility. It also costs you: performance, debuggability, build complexity, and (over-applied) false-positive support burden.
When it's worth it: protecting a genuinely valuable secret (a proprietary algorithm, the license-check logic) where buying attacker-hours has real value, and where you've accepted the maintenance cost. For most platform code it's not worth it. The leadership judgment: obfuscation is a targeted tool for specific high-value code, layered with anti-tamper (Ch. 6), and never sold as "protection" on its own. Pair it honestly with attestation (Ch. 8) for anything that needs a real guarantee.
Chapter 8: Attestation — The Strong End of the Spectrum
Everything in Ch. 4–7 raises cost on a machine the attacker controls. Attestation is the qualitatively stronger move: a hardware root of trust the attacker can't patch vouches for the software+hardware state to a remote verifier.
The mechanism (Lab 02 models it):
- A trusted hardware element (TPM, CPU TEE, or NVIDIA GPU confidential computing) measures the boot chain / loaded code into tamper-resistant registers (PCRs).
- It produces a quote — those measurements, signed by a hardware key the attacker can't extract or forge.
- A remote verifier (you, or the customer's control plane) checks the quote against known-good measurements. If they match, the remote party knows the machine is in a known state — proven by hardware, not by the (patchable) software self-attesting.
Why it's strictly stronger than a self-check (Ch. 6): the verifier is external and the signing key is in hardware. The attacker can't fake the quote by patching software, because the measurement and signature happen below the software they control. This is the answer to "prove this runs unmodified on authorized hardware."
The honest limits (there are always some): attestation proves what loaded, not that it's bug-free; the trust root must itself be sound (TPM/firmware vulns exist); and it requires the hardware feature + a verification infrastructure. But it moves you from "economic deterrent" to "cryptographic guarantee rooted in hardware" — the right architecture when the stakes justify it (sovereign weights, regulated data).
Chapter 9: Confidential Computing and Sovereign Deployment
The convergence of this phase with the JD's sovereign-AI mandate.
Confidential computing: run code + data in a TEE (Trusted Execution Environment) with encrypted memory, so even the host OS / hypervisor / cloud operator can't read it. CPU TEEs (Intel TDX, AMD SEV-SNP) plus GPU confidential computing (NVIDIA H100 CC) extend this to GPU workloads — encrypted CPU↔GPU transfers, attested GPU state. Use cases that matter for the JD:
- Protect your model weights on a customer's or cloud's hardware you don't control (the weights are decrypted only inside the TEE, gated by attestation — Ch. 8).
- Protect the customer's data from you and the infra operator (regulated data residency).
Sovereign/air-gapped security architecture (composing the phase):
- No phone-home: offline licenses (Ch. 4–5), local secrets, no telemetry egress (Phase 07/10 Ch. 9).
- Supply-chain integrity: signed artifacts, reproducible builds, SBOMs — the customer must trust what you shipped without watching you build it.
- Auditability: tamper-evident logs of access and operations (regulated requirement, Phase 07).
- Layered protection: fingerprint+signed license (economic/legal) → anti-tamper+obfuscation on the check (cost) → attestation+confidential computing (hardware-rooted guarantee) — applied proportionally to the stakes.
The leadership reframe: security here is not a feature bolted on — it's what enables the sovereign market the JD targets. The platform that can prove to a defence ministry "this runs unmodified, only on your licensed hardware, with your data never leaving the TEE, fully air-gapped, and auditable" wins deals a cloud-assuming competitor can't touch. That's the commercial thesis of this phase, and why a Head of Engineering owns it.
Lab Walkthrough Guidance
Order: Lab 01 → Lab 02 (licensing first, then the protection that defends the license check, then attestation as the strong answer).
Lab 01 (fingerprint + license):
python solution.py; read fingerprinting + Ed25519 issuance/verification against Ch. 2–4.- Walk the attack tests: tampered license (signature fails), wrong fingerprint (rejected), one-component change (fuzzy-matches), three-component change (rejected), clock rollback (detected). For each, state the defeat condition — that's the learning.
- Extensions: air-gap challenge-response (Ch. 5), revocation, feature gating.
Lab 02 (anti-tamper + attestation):
make && ./protected; it self-verifies and runs.python tamper.py protectedflips a code byte; rerun → tamper detected (Ch. 6).python attestation.py; trace measure→quote→verify and articulate why the external verifier beats the self-check (Ch. 8).- Write the threat model: for each technique, its cost-raise and its defeat condition. Extensions: an obfuscation experiment (Ch. 7), a TPM/GPU-CC design doc (Ch. 8–9).
Success Criteria
- You can state the commercial-protection threat model and the honest ceiling ("raise cost, not impossibility; attestation is the strong answer") (Ch. 1)
- You can explain sign-vs-encrypt and why the private key is the crown jewel (Ch. 2)
- You can design fuzzy hardware fingerprinting and state its defeat condition (Ch. 3)
- You can build a signed offline license with grace + rollback resistance and name every defeat condition (Ch. 4) — Lab 01 proves it
- You can describe air-gap challenge-response activation (Ch. 5)
- You can explain what anti-tamper and obfuscation do and don't buy (Ch. 6–7)
- You can explain why attestation is strictly stronger and how it works (Ch. 8) — Lab 02 models it
- You can describe a sovereign/air-gapped security architecture (Ch. 9)
Interview Q&A
Q1: Design a license system for software deployed air-gapped on a customer's hardware. A: Signed, offline, hardware-bound. Issue an Ed25519-signed license token — signed not encrypted, because I need it unforgeable and unmodifiable, not secret — carrying customer, features, gpu-count, expiry, and a fuzzy hardware fingerprint (GPU UUIDs + board/MAC, matching on ≥N-of-M so a swapped NIC doesn't lock them out). The deployment verifies it offline with an embedded public key: signature valid, not expired (with a bounded grace period and clock-rollback resistance via a monotonic high-water-mark), fingerprint fuzzy-matches, features within license. Activation is challenge-response: the air-gapped box emits fingerprint+nonce, a human carries it out-of-band to my activation service, which returns a signed response carried back. I'd state the defeat condition plainly: an attacker with root and the binary can patch out the verifier — so licensing is an economic+legal deterrent; anti-tamper raises that cost and attestation (hardware root) is the strong path for high-stakes deployments. The private signing key lives in an HSM and is the whole system's security.
Q2: Why sign a license instead of encrypting it, and where does the key live? A: The properties a license needs are integrity (not modified) and authenticity (issued by me) — not secrecy; the license content ("customer X, 8 GPUs, expires Dec") isn't secret. A public-key signature (Ed25519) gives exactly integrity+authenticity with an asymmetric twist that's essential for shipped software: the deployed copy needs only the public key to verify, which can be embedded safely because it can't forge signatures. Encrypting would give secrecy I don't need and force me to put a decryption secret in the binary (extractable) — strictly worse. So: sign licenses (public verifier), encrypt data like model weights (Ch. 9). The private signing key is the crown jewel — it lives in an HSM/KMS, never in the repo or on a shipping build machine; its leak forges every license and its rotation invalidates all issued ones.
Q3: How do you bind software to specific GPUs, and what about a NIC swap?
A: Fingerprint from stable hardware attributes — GPU UUIDs (nvidia-smi -L, the
strongest signal and hard to spoof), plus board serial and MAC — and bind that
fingerprint into the signed license. The NIC swap is exactly why you don't
hash everything into one brittle value: that would lock the customer out on any
legitimate change. Instead, fuzzy-match on multiple components (valid if ≥N of M
agree), tolerating a swapped NIC or a replaced failed GPU while still rejecting a
wholly different machine. The honest defeat condition: an attacker controlling
the box can feed fake IDs to the fingerprinting code — so this binds the honest
majority and raises cost; GPU UUIDs are harder to forge than MACs, and
attestation (a hardware root vouching for real IDs) is the strong version.
Q4: Anti-tamper self-checks — what do they actually buy, and how are they
defeated?
A: A self-check hashes the program's own code section at runtime and refuses if
it differs from an embedded expected value — so flipping a jne to bypass the
license check changes the hash and trips the guard. What it buys: it defeats
casual patching and forces an attacker to also defeat the checker. How it's
defeated: the checker is code the attacker controls — they can patch out the
check, rewrite the expected hash, or hook the comparison; with the binary and a
debugger it's a matter of attacker-hours. So a self-check is an honest
cost-raiser, a speed bump — useful layered (redundant, obfuscated checkers
raise the cost further) but never a strong guarantee alone, and never "DRM
theater" that punishes honest users on a false positive. The strong answer is
attestation: an external verifier and a hardware-rooted signing key the
attacker can't patch, which is why I'd architect toward it for anything
high-stakes.
Q5: What's the strong answer to "prove this runs unmodified on authorized hardware," and how does it work? A: Remote attestation with a hardware root of trust. A trusted element — TPM, CPU TEE (TDX/SEV-SNP), or NVIDIA GPU confidential computing — measures the boot chain and loaded code into tamper-resistant registers, then produces a quote: those measurements signed by a hardware key the attacker can't extract. A remote verifier checks the quote against known-good values; a match proves the machine's software+hardware state, because the measuring and signing happen below the software the attacker controls — unlike a self-check, the verifier is external and the key is in hardware. That's qualitatively stronger: it moves from "economic deterrent on a machine they control" to "cryptographic guarantee rooted in hardware." Honest limits: it proves what loaded, not that it's bug-free, and the trust root must itself be sound. For sovereign deployments I'd combine it with confidential computing so model weights decrypt only inside an attested TEE — protecting my IP and the customer's data even on infrastructure I don't control.
Q6 (leadership): How is security a business enabler here, not just a cost? A: It's what opens the sovereign/regulated market the JD targets. A defence, finance, healthcare, or government customer runs on their hardware, often air-gapped, and won't buy software they can't deploy under those constraints. The security stack — fuzzy fingerprinting + signed offline licenses (economic+legal control without a license server), air-gap activation, anti-tamper+targeted obfuscation (cost), and the strong tier of attestation + confidential computing (hardware-rooted guarantees for weights and data) — is precisely what lets us say "runs unmodified, only on your licensed hardware, your data never leaves the attested TEE, fully air-gapped, auditable." A competitor that assumes the cloud can't make that claim. So I'd resource it proportionally to the stakes (most code needs only licensing; weights and regulated data justify attestation/CC), present it honestly (cost-raisers vs hardware guarantees, with defeat conditions — never "uncrackable"), and treat it as a market-access feature with direct revenue impact, which is why it sits in the engineering leader's remit alongside the HAL (Phase 09) it protects.
References
- Cryptography Engineering (Ferguson, Schneier, Kohno) — the threat-modeling + crypto-foundations text
- Ed25519: Bernstein et al., "High-speed high-security signatures" — https://ed25519.cr.yp.to/
- NaCl/libsodium and Python
cryptography(Ed25519 APIs) — https://cryptography.io - TPM 2.0 + measured boot — https://trustedcomputinggroup.org/ ; Linux IMA/EVM
- Confidential computing: Intel TDX, AMD SEV-SNP, and NVIDIA H100 Confidential Computing — https://docs.nvidia.com/confidential-computing/
- Remote attestation overview (RATS architecture, RFC 9334) — https://www.rfc-editor.org/rfc/rfc9334
- "Surreptitious Software" (Collberg & Nagra) — the obfuscation/tamper-proofing reference (with honest limits)
- Honeynet/Reverse-engineering literature — to understand the attacker's economics (for defensive calibration)
- Cross-track: Security track (deeper offensive/defensive), Phase 09 WARMUP (the platform this protects), Phase 07 Ch. 9 (sovereign serving)
🛸 Hitchhiker's Guide — Phase 11: Security, Licensing & Protection
Read this if: you can build the platform (Phase 09) but not yet protect and sell it into sovereign/regulated deployments. Defensive/licensing focus; every technique with its defeat condition. Derivations in the WARMUP.
0. The 30-second mental model
On hardware the attacker controls you can raise cost, never achieve impossibility — except with a hardware root of trust (attestation, confidential computing). The stack, weakest→strongest: signed offline licenses + fuzzy fingerprint (economic/legal) → anti-tamper + targeted obfuscation (cost) → attestation + confidential computing (hardware-rooted guarantee). Apply proportionally to the stakes. Never say "uncrackable."
1. Crypto card
| Primitive | Use | Note |
|---|---|---|
| SHA-256 | fingerprint hardware/code | one-way, not secret |
| Ed25519 sign/verify | licenses | private signs, public verifies — public ships safely |
| sign vs encrypt | sign licenses, encrypt weights | license isn't secret, just unforgeable |
| private key | the crown jewel | HSM/KMS, never in repo/ship |
2. Licensing card (Lab 01)
license = { customer, features, max_gpus, expiry, fuzzy_fingerprint, id } + Ed25519 sig
verify (OFFLINE, embedded public key): sig ok? not expired (grace + rollback-resist)?
fingerprint fuzzy-matches? features in scope?
- Fuzzy fingerprint: GPU UUIDs + board + MAC, match ≥N-of-M (NIC swap ok, different machine no). GPU UUIDs hardest to spoof.
- Grace period: bounded window past expiry (avoid outages on renewal lag).
- Clock rollback: monotonic high-water-mark; reject earlier-than-seen time.
- Air-gap activation: fingerprint+nonce out-of-band → signed response back.
- Defeat condition: root + binary → patch the verifier. Licensing = economic+legal deterrent. Attestation is the strong fix.
3. Protection card (Lab 02)
| Technique | Buys | Defeat condition |
|---|---|---|
| integrity self-check | catches casual patching | patch the checker too |
| obfuscation | attacker-hours | CPU runs it → analyst eventually reads it |
| anti-tamper response | speed bump | calibrate; avoid DRM theater (false positives) |
| attestation | hardware-rooted proof of state | trust root must be sound; proves what loaded |
| confidential computing | weights/data secret from host | TEE + attestation gating |
Self-check vs attestation: the difference is who verifies and where the key lives — patchable software vs external verifier + hardware key.
4. Sovereign deployment card (the market)
No phone-home (offline license/activation, local secrets) · supply-chain integrity (signed artifacts, reproducible builds, SBOM) · auditability (tamper-evident logs) · layered protection by stakes. This is what wins defence/finance/healthcare/government deals a cloud-assuming competitor can't.
5. War stories (calibration)
- "We told the customer it was uncrackable" — lost credibility when a researcher patched the check in an afternoon. Now: "raises cost above license price, here are the defeat conditions, attestation for high-stakes."
- "License check locked out a customer who swapped a NIC" — brittle single-hash fingerprint. Fuzzy N-of-M fixed it; support tickets vanished.
- "Clock rollback gave free perpetual licenses" — added the monotonic high-water-mark. Raised cost (they'd have to tamper the store too).
- "Obfuscated everything, builds slow, debugging hell, still RE'd" — obfuscation belongs on specific high-value code, not the whole platform.
- "Sovereign customer needed weights protected from their own admins" — confidential computing (H100 CC) + attestation-gated weight decryption was the only real answer; licensing alone couldn't do it.
6. Exit bar
You can threat-model honestly (raise cost vs hardware guarantee), build a signed offline hardware-bound license with grace + rollback resistance, explain sign-vs-encrypt and key custody, implement and honestly bound anti-tamper, and explain why attestation/confidential computing are the strong tier for sovereign weights and data. You've built the license system and the integrity check. Next: Phase 12 — compose everything (HAL, scheduler, serving, observability, security) into the platform, and lead it.
Lab 01 — Hardware Fingerprinting + Ed25519 Offline License System
Phase: 11 — Security & Licensing | Difficulty: ⭐⭐⭐⭐⭐ | Time: 6–8 hours Language: Python (embedded pure Ed25519 — zero deps) | Hardware: none
Concept primer:
../WARMUP.mdCh. 2–5. Defensive/licensing scope; every technique is taught with its defeat condition.
Run
python solution.py # keygen, fingerprint, issue, verify offline, attack tests
(Self-contained: a pure-Python Ed25519 is embedded so it runs offline with no
pip install — exactly the air-gapped constraint this phase is about.)
0. The mission
Build the offline-licensing core sovereign customers require (WARMUP Ch. 4):
- Hardware fingerprint — a stable machine ID from multiple hardware attributes, with fuzzy matching so legitimate changes (a swapped NIC) don't lock the customer out (WARMUP Ch. 3).
- Ed25519-signed license — binds customer/features/expiry/fingerprint; signed not encrypted (WARMUP Ch. 2, 4).
- Offline verification — with the embedded public key, no network, plus a grace period and clock-rollback resistance.
1. What the output shows
== keys & fingerprint ==
issued license for 'Acme Defence' bound to fingerprint 7f3a...c91 (8 GPUs, expires 2027-01-01)
== happy path ==
verify on the licensed machine: VALID (features ok, fingerprint matches, not expired)
== attack / robustness tests ==
tampered license (1 byte flipped) : REJECTED (signature invalid)
license presented on a different machine : REJECTED (fingerprint mismatch)
one hardware component swapped (NIC) : VALID (fuzzy match: 4/5 still agree)
three components changed : REJECTED (fuzzy match: 2/5 — different machine)
clock rolled back before high-water-mark : REJECTED (rollback detected)
expired but within grace period : VALID (grace honored)
expired beyond grace : REJECTED (expired)
requesting more GPUs than licensed : REJECTED (feature/quota)
defeat condition (stated honestly): an attacker with root + the binary can patch
out the verifier call site. Licensing = economic+legal deterrent; attestation
(Phase 11 Lab 02 / WARMUP Ch. 8) is the hardware-rooted strong answer.
Every row is a test; each teaches a mechanism and its limit.
2. Reading order (solution.py)
- The embedded Ed25519 (
_ed25519section) — you don't need to grok the curve math; know it givessign(priv,msg)/verify(pub,msg,sig)(WARMUP Ch. 2). hardware_fingerprint+fuzzy_match— multi-attribute ID and N-of-M matching (WARMUP Ch. 3).issue_license(private key) andverify_license(public key, offline) — the sign/verify flow + grace + rollback check (WARMUP Ch. 4).- The attack tests — and the defeat-condition note.
3. Extensions
- Air-gap challenge-response activation (WARMUP Ch. 5): the box emits fingerprint+nonce; an "activation service" returns a signed response bound to them; the box verifies offline. Add replay protection via the nonce.
- Revocation: a signed revocation list for connected deployments; discuss why it's hard air-gapped (hence short terms + re-activation).
- Feature gating: enforce per-feature entitlements and a max-GPU quota at "runtime" (simulate a platform asking "may I use FP8 on 6 GPUs?").
- Key rotation: support multiple public keys (key IDs) so you can rotate the signing key without invalidating all licenses at once.
4. Common pitfalls
- Encrypting instead of signing — a license needs integrity+authenticity, not secrecy; signing with a public verifier is correct (WARMUP Ch. 2).
- Brittle single-hash fingerprint — one hardware change locks the customer out; fuzzy N-of-M is the engineered answer (WARMUP Ch. 3).
- Trusting the system clock — rollback gives free perpetual licenses without a monotonic high-water-mark (WARMUP Ch. 4).
- Claiming it's uncrackable — always state the defeat condition; the value is raised cost + legal evidence, with attestation as the strong tier.
5. What this lab proves about you
You can build the licensing core that turns a hardware-agnostic platform (Phase 09) into a sovereign-deployable product — signed, offline, hardware-bound, robust to legitimate change and basic attack — and you threat-model it honestly, which is what makes a security claim credible in front of a regulated customer.
Lab 02 — Anti-Tamper Integrity Self-Check + Attestation Model
Phase: 11 — Security & Licensing | Difficulty: ⭐⭐⭐⭐⭐ | Time: 5–8 hours Language: C (self-check) + Python (tamper tool + attestation model) | Hardware: none
Concept primer:
../WARMUP.mdCh. 6–8. Defensive scope; every technique stated with its defeat condition.
Run
make && ./protected # runs + self-verifies its own code section
python3 tamper.py protected # flips one byte in the code section -> protected_tampered
./protected_tampered # self-check fires: tamper detected, refuses
python3 attestation.py # models measure -> quote -> verify (the STRONG answer)
0. The mission
Two halves of the protection story (WARMUP Ch. 6, 8):
- Integrity self-check (
protected.c): the program hashes its own.text(code) region at runtime and compares to an embedded expected digest; if a byte's been patched (e.g., to skip a license check), the hash differs and it refuses to run. This raises the cost of casual patching — and you'll state its defeat condition. - Attestation model (
attestation.py): models the qualitatively stronger approach — a hardware root of trust measures code, produces a hardware-signed quote, and an external verifier checks it. The point: the verifier is external and the key is in hardware, so it isn't defeated by patching software.
1. What the output shows
$ ./protected
[self-check] code digest matches embedded value — integrity OK
protected workload running: result = 1240
$ python3 tamper.py protected
patched 1 byte in the code region -> protected_tampered
$ ./protected_tampered
[self-check] code digest MISMATCH — binary has been tampered. Refusing to run.
$ python3 attestation.py
== self-check vs attestation ==
self-check : the checker is IN the binary the attacker controls -> patchable
attestation: measure -> quote (signed by a HARDWARE key) -> external verify
known-good measurement -> verifier ACCEPTS
tampered measurement -> verifier REJECTS
forged quote (no hw key)-> verifier REJECTS (can't sign without the TPM/GPU key)
why stronger: the verifier is EXTERNAL and the signing key is in HARDWARE,
so patching the software can't fake a valid quote.
2. Reading order
protected.c—self_check()hashes a marked code region and compares toEXPECTED_DIGEST; note the honest comment on how it's defeated (WARMUP Ch. 6).tamper.py— flips a byte in the code region to simulate a patch (the attacker's move against the Lab 01 license check).attestation.py— the measure/quote/verify flow and the side-by-side with the self-check (WARMUP Ch. 8).
3. The honest threat model (write it — the real deliverable)
In THREAT-MODEL.md, for each technique state what it raises the cost of and
how it's defeated:
| Technique | Raises cost of | Defeated by |
|---|---|---|
| integrity self-check | casual binary patching | patching the checker / the expected digest |
| obfuscation (Ch. 7) | locating/understanding the check | a debugger + attacker-hours |
| anti-tamper response | automated tampering | calibration errors (false positives hurt honest users) |
| attestation | faking the software state | a sound trust root is required; proves what loaded, not bug-freeness |
| confidential computing | reading weights/data on host | TEE + attestation must gate decryption |
The lesson (WARMUP Ch. 1): everything self-contained raises cost, never achieves impossibility; only a hardware root of trust (attestation, confidential computing) gives a real guarantee.
4. Extensions
- Redundant + obfuscated checks (WARMUP Ch. 6–7): add multiple self-checks in different functions; measure how much harder tampering gets (attacker must find them all).
- String/control-flow obfuscation experiment: obfuscate the check logic; reflect on the RE-cost increase vs the maintenance cost.
- Real attestation design doc: design a TPM measured-boot or NVIDIA H100
Confidential Computing attestation flow for gating model-weight decryption
(WARMUP Ch. 8–9) — the production version of
attestation.py. - Encrypted weights at rest: weights decrypt only after a valid attestation (model the gate).
5. Common pitfalls
- Believing the self-check is "secure" — it's a cost-raiser; the checker is in the attacker's binary (WARMUP Ch. 1, 6). Always pair the claim with the defeat condition.
- Aggressive tamper response — self-destruction/false positives punish honest users (a hardware change, a debugger attached for support). Calibrate.
- Confusing self-attestation with attestation — software vouching for itself is not attestation; the verifier and key must be external/hardware.
- Over-obfuscating — kills performance/debuggability for marginal, bounded gain; target only high-value code.
6. What this lab proves about you
You can implement protection and threat-model it honestly — the rare combination that makes a security architecture credible to a regulated customer. You know where the line is between cost-raising (self-check, obfuscation) and real guarantees (attestation, confidential computing), which is exactly the judgment a Head of Engineering needs to architect — and to not oversell — a sovereign-deployment security story.
Phase 12 — Leadership & the Capstone Platform
Difficulty: ⭐⭐⭐⭐⭐ | Estimated Time: 2 weeks Roles supported: Head of Engineering [GPU] (the whole role) Hardware needed: none — the capstone composes the prior phases' CPU-runnable components
Why This Phase Exists
The JD is a leadership role: "Lead the development of a standalone orchestration platform," "build and scale a developer-focused platform," "drive integrations with OEMs," "own platform adoption and commercialisation," and "stay hands-on where needed — maintaining technical credibility." Phases 01–11 built the technical depth and the hands-on artifacts. This phase does two things:
- Leadership practice — the non-code judgment a Head of Engineering is paid for: technical strategy, org design, OEM/partner engineering, build-vs-buy, the release/quality bar, and the executive-and-customer communication the JD demands.
- The capstone — compose the prior phases into one mini GPU orchestration platform: a HAL (Phase 09) under a scheduler (Phase 06), serving a model with KV-cache management (Phase 07), behind a licensed (Phase 11), observable (Phase 10) control plane — with a design doc and the release-cycle artifacts (Phase 10) that make it a product, not a prototype. This is the portfolio piece that proves you can do the job.
Concepts
Leadership
- Technical strategy: writing the platform vision and the decision records (the Phase 09 ADR, generalized); the "decouple from hardware" strategy as an executable roadmap
- Org design for a deep-tech platform team: the skill mix (kernels, runtime, distributed, serving, security, SRE), how to structure it, hiring signals (the interview-prep folder), and the build-vs-buy boundary
- OEM/partner engineering: onboarding a vendor's chip as a HAL backend (Phase 09) as a commercial+technical process; conformance certification as a relationship lever
- The quality bar: the kernel-PR checklist (Phase 02), the runtime contract (Phase 05), the conformance suite (Phase 09), the release gates (Phase 10) — codified as engineering standards you own and defend
- Commercialization: how the technical artifacts (HAL, sovereign security) map to revenue and adoption; pricing tiers tied to isolation/capability (Phase 06)
- Executive + customer communication: explaining roofline economics, the sovereign-AI value prop, and honest security claims (Phase 11) to non-engineers
- Operating model: SLOs/error budgets (Phase 10), incident culture, "failures are the steady state" (Phase 08) as a management philosophy
Capstone integration
- Composing HAL + scheduler + serving + licensing + observability into one control plane
- The platform's internal API contract (the runtime guarantees from Phase 05, the ABI discipline from Phase 09)
- Release-cycle artifacts: design doc, runbook, compatibility matrix, demo, retrospective
The Capstone
Capstone — Mini GPU Orchestration Platform (Python, integrates all phases)
| Field | Value |
|---|---|
| Goal | Build a working mini GPU orchestration platform that composes the prior phases: a capability-routed HAL (Phase 09) under a scheduler (Phase 06) that admits requests, serves a model with paged KV-cache + continuous batching (Phase 07), gated by an offline license (Phase 11), exporting metrics + SLOs (Phase 10) — all behind one control-plane API, with a design doc and release artifacts (Phase 10). |
| Concepts | System integration; the control-plane API; capability routing; admission control; licensing gate; observability; the release cycle. |
| Steps | 1) python platform.py — boots the platform: loads backends, validates the license, advertises capabilities, serves a batch of requests through the scheduler+KV-cache, exports metrics, and prints a status report. 2) Read each subsystem and trace a request end to end against the design doc. 3) Run the included scenarios: license-expired (refuses to serve), capability-routing (FP8 request → capable backend), overload (admission control + goodput), a backend failure (fail over). 4) Write the design doc, runbook, and retrospective. |
| Stack | Python (stdlib) — reuses patterns from Phases 06/07/09/10/11 |
| Output | A running mini platform + DESIGN-DOC.md + RUNBOOK.md + a release/retrospective writeup. |
| How to Test | Built-in scenarios assert: an expired license blocks serving; FP8 work routes to a capable backend (Phase 09); continuous batching beats sequential (Phase 07); admission control protects goodput under overload; a backend marked unhealthy triggers failover; metrics export validly (Phase 10). |
| Talking Points | How the pieces compose; where the abstraction boundaries are; the build-vs-buy decisions; the commercialization story; what you'd do differently at real scale. |
| Resume Bullet | "Built a mini GPU orchestration platform integrating a capability-routed hardware-abstraction layer, request scheduler, paged-KV-cache continuous-batching server, offline licensing, and Prometheus observability behind one control-plane API — with design doc, runbook, and release artifacts." |
| Extensions | Multi-node (Phase 08 collectives) for a TP-sharded model; a TypeScript CLI for the control plane (the JD's JS/TS-for-tooling line); a real backend (CUDA via the HAL) if hardware is available. |
Leadership Artifacts (write these — they're the portfolio)
In leadership/:
PLATFORM-STRATEGY.md— the vision + roadmap: the "decouple from hardware" thesis as an executable 18-month plan with the build-vs-buy boundary (Phase 09 ADR generalized).ORG-DESIGN.md— the team structure, skill mix, and hiring plan for the platform; which roles, what each owns, the build-vs-hire calls.OEM-ONBOARDING.md— the process to onboard a vendor's chip as a certified HAL backend (technical + commercial), using the conformance gate (Phase 09) as the relationship lever.ENGINEERING-STANDARDS.md— the codified quality bar: kernel-PR checklist (Phase 02), runtime contract (Phase 05), ABI discipline (Phase 09), release gates (Phase 10), security claims discipline (Phase 11).
(Templates/rubrics provided in leadership/.)
Deliverables Checklist
- Capstone platform boots and passes all integration scenarios
-
DESIGN-DOC.md— the platform architecture with the abstraction boundaries -
RUNBOOK.md— operational procedures for the platform's failure modes - The four leadership artifacts written
- A retrospective: what you'd do differently at real scale
- You can present the platform end-to-end to (a) an engineer and (b) an executive
Interview Relevance
This phase is the interview for the role.
- "Walk me through a platform you've built." (the capstone)
- "How would you decouple our software from hardware?" (Phase 09 + the strategy doc)
- "How do you structure and hire a GPU platform team?" (org design)
- "How do you onboard an OEM's accelerator?" (OEM doc + conformance)
- "How do you balance shipping fast with production quality?" (error budgets, standards)
- "Sell me the sovereign-AI value proposition." (the commercialization story)
See ../interview-prep/ and ../system-design/ for the full interview surface.
Warmup Guide — Leadership & the Capstone Platform
Zero-to-expert primer for Phase 12 — the leadership judgment and integration that turn the prior 11 phases of depth into the role. Less new mechanism, more synthesis and decision-making. The capstone composes everything; this guide is how a Head of Engineering thinks about it.
Table of Contents
- Chapter 1: What a Head of Engineering Actually Does
- Chapter 2: Technical Strategy as an Executable Roadmap
- Chapter 3: Org Design and Hiring for a Deep-Tech Platform
- Chapter 4: The Quality Bar — Standards You Own and Defend
- Chapter 5: OEM and Partner Engineering
- Chapter 6: Commercialization — Technical Artifacts to Revenue
- Chapter 7: Executive and Customer Communication
- Chapter 8: The Capstone — How the Pieces Compose
- Chapter 9: Staying Hands-On Without Being the Bottleneck
- Lab Walkthrough Guidance
- Success Criteria
- Interview Q&A
- References
Chapter 1: What a Head of Engineering Actually Does
The JD's responsibilities decode into five jobs, none of which is "write the most code":
- Set technical direction — the platform vision and the decisions that commit the company (the "decouple from hardware" strategy, the build-vs-buy boundary). Output: strategy docs and ADRs (Ch. 2).
- Build the team — hire and structure the rare skill mix (Ch. 3), and make the build-vs-hire calls.
- Own the quality bar — codify and defend the standards (Ch. 4) that keep a fast-moving platform shippable.
- Drive partnerships and commercialization — OEM integrations (Ch. 5), pricing, the sovereign-AI go-to-market (Ch. 6).
- Communicate — translate roofline economics and honest security claims to executives and customers (Ch. 7), and keep technical credibility with engineers (Ch. 9).
The shift from senior engineer to head of engineering is from solving the problem to building the system (human + technical) that solves the class of problems. Your leverage is decisions, standards, hiring, and clarity — not keystrokes. The prior 11 phases are what let you make those decisions credibly; this phase is making them.
Chapter 2: Technical Strategy as an Executable Roadmap
A strategy that isn't executable is a wish. The discipline (the
PLATFORM-STRATEGY.md artifact):
- Start from the thesis: "decouple software from hardware" → the HAL (Phase 09) is the spine, vendor lock-in is the enemy, sovereign + cost-conscious customers are the market.
- Make it a roadmap with sequenced bets: e.g., (0–6mo) HAL + NVIDIA backend + serving + licensing → first sovereign customer; (6–12mo) AMD backend + conformance suite + OEM #1; (12–18mo) Triton/MLIR backend + multi-vendor GA. Each milestone has a commercial outcome, not just a technical one.
- Encode the build-vs-buy boundary (Phase 09 ADR): build the HAL (the moat), adopt the kernel compiler, buy commodity infra. State it once, defend it consistently.
- Tie to error budgets (Phase 10): the roadmap's pace is governed by quality, not just ambition.
The leadership skill is sequencing under uncertainty: which bet first, what it unlocks, what you learn, what you'd cut. A roadmap that's all "and then we build everything" signals inexperience; one with explicit sequencing, dependencies, and kill-criteria signals a leader.
Chapter 3: Org Design and Hiring for a Deep-Tech Platform
The platform needs a skill mix that's hard to assemble (it spans the careers
Phases 01–11 represent): kernel/compiler, runtime, distributed systems, serving,
security, and SRE. The ORG-DESIGN.md artifact:
- Structure by subsystem ownership, not by layer: a "serving" team owns Phase 07 end to end (KV cache, batching, the engine integration), a "platform core" team owns the HAL + runtime (Phase 05/09), a "infra/SRE" team owns scheduling + observability + releases (Phase 06/10). Security (Phase 11) is often a small specialist team that consults across.
- Hire for the gaps, not the comfort zone: the rare hires are the kernel/compiler people and the GPU-savvy SREs; generalists are easier. Map every open role to a phase's competency and use the interview-prep folder's questions as the bar.
- The build-vs-hire call: do you hire a compiler team (Option C, Phase 09) or adopt Triton/MLIR and hire integrators? Usually the latter — staff to your build-vs-buy boundary (Ch. 2).
- Seniority shape: a few principals who set the standards (Ch. 4), more senior ICs who execute; avoid over-hiring juniors into a domain with a steep ramp and few mentors.
The meta-point: org design is technical strategy — the team shape encodes the build-vs-buy boundary and the bet sequencing. Mis-shape it and the roadmap stalls regardless of individual talent.
Chapter 4: The Quality Bar — Standards You Own and Defend
A fast platform without standards becomes an unmaintainable one; standards
without buy-in become ignored. The leader's job is to codify the bar (it exists
implicitly across the phases) and defend it with reasons, not authority. The
ENGINEERING-STANDARDS.md artifact collects:
- Kernel PR checklist (Phase 02): reference-comparison test, sanitizer-clean,
event-based timing,
nculimiter evidence, arch notes. - Runtime contract (Phase 05): no driver alloc/free in steady state, stream-ordered lifetime, pool segregation + metrics, no device-wide syncs.
- ABI discipline (Phase 09): version-first, append-only, conformance-gated backends.
- Release gates (Phase 10): compatibility check → canary → progressive rollout → auto-rollback, governed by error budget.
- Security-claim discipline (Phase 11): every protection stated with its defeat condition; never "uncrackable."
Each standard maps to a specific incident class it prevents — which is how you defend it when a team finds it pedantic ("this rule exists because of the day-3 OOM / the corrupted tokens / the vendor backend that broke on our release"). Standards justified by incidents stick; standards justified by authority rot.
Chapter 5: OEM and Partner Engineering
The JD's "drive integrations with OEMs, cloud providers, and infrastructure
partners" is where engineering and business fuse. Onboarding a vendor's chip
(the OEM-ONBOARDING.md artifact) is a process, not a one-off:
- Technical: the vendor (or your team) builds a HAL backend (Phase 09) wrapping their compute libraries; it must pass the conformance suite (Phase 09 Ch. 7) — correctness first, then a performance tier.
- Commercial: "certified backend" status is the deliverable and the lever — it's a quality promise to customers and a negotiation chip with the vendor (they get ecosystem access; you get a tested backend and a co-marketing story).
- Relationship: the conformance bar protects your platform's reputation (a slow/broken backend poisons it) and gives the vendor a clear target. Versioned ABI (Phase 09 Ch. 6) means their backend keeps working across your releases — the trust that sustains the ecosystem.
The leadership insight (Phase 09 Ch. 9): every OEM integration is a backend behind your HAL and a revenue/adoption event. The HAL's quality is the throughput of your partner pipeline — which is why you own it.
Chapter 6: Commercialization — Technical Artifacts to Revenue
The technical work maps to money; a leader makes that mapping explicit:
- The HAL (Phase 09) → market access for non-NVIDIA chips + freedom-from- lock-in for customers → the core value prop.
- Sovereign security (Phase 11) → the regulated market (defence, finance, healthcare, government) that cloud-assuming competitors can't serve.
- GPU sharing tiers (Phase 06) → pricing tiers (dedicated/MIG/time-sliced mapped to isolation guarantees and price points).
- Serving efficiency (Phase 07) → margin (goodput per GPU is gross margin per dollar of hardware).
- Conformance certification (Phase 09) → a partner-ecosystem flywheel.
The discipline: for every major engineering investment, state the commercial thesis (who pays more / churns less / becomes reachable because of it). An engineering leader who can't connect the roadmap (Ch. 2) to revenue gets out-prioritized by those who can.
Chapter 7: Executive and Customer Communication
The JD: "engage with customers and partners at both technical and executive levels." The skill is translation without dishonesty:
- To executives: roofline economics as dollars ("decode is bandwidth-bound, so batching is our margin lever; this optimization is $X/year across the fleet"), the sovereign value prop as TAM, the build-vs-buy boundary as focus/risk. Lead with the decision and its consequence, not the mechanism.
- To customers (technical): honest capability and honest limits — the goodput-at-SLO discipline (Phase 07 Ch. 6), the security claims with defeat conditions (Phase 11 Ch. 1). Credibility compounds; overselling (a TFLOPs number, "uncrackable") destroys it.
- To customers (executive): outcomes (their cost per token, their data never leaving the TEE, their air-gapped deployment) not internals.
The through-line from Phase 01's spec-sheet skepticism to Phase 11's "never say uncrackable": honest, decision-oriented communication is a technical leadership skill, and it's what keeps both engineers' and customers' trust.
Chapter 8: The Capstone — How the Pieces Compose
The capstone proves you can integrate, which is harder than any single phase. The composition (trace a request through it):
request -> [License gate, Phase 11] valid? features/quota ok?
-> [Scheduler/admission, Phase 06+07] admit? (protect goodput)
-> [HAL capability routing, Phase 09] pick a backend for the dtype/op
-> [Serving: paged KV + continuous batching, Phase 07] generate
-> [Observability, Phase 10] every step emits metrics; SLOs evaluated
(a backend failure -> failover; license expiry -> refuse; overload -> shed)
The integration lessons:
- Abstraction boundaries are the design: the HAL ABI (Phase 09), the runtime contract (Phase 05), the control-plane API — each is a seam that lets the pieces evolve independently. Getting the seams right is the architecture.
- Cross-cutting concerns compose awkwardly: licensing, observability, and failure-handling touch every layer; the capstone shows where they belong (a gate at the edge, metrics everywhere, failover in the router).
- The whole has emergent failure modes: a license check in the hot path (latency), a metrics export that blocks serving (Phase 05's sync lesson), a capability-routing miss under failover. Integration surfaces these.
The capstone is deliberately CPU-runnable (it reuses the prior phases' models) so the architecture is the lesson, not the GPU plumbing — exactly the altitude a Head of Engineering operates at.
Chapter 9: Staying Hands-On Without Being the Bottleneck
The JD: "stay hands-on where needed — maintaining technical credibility." The balance every engineering leader navigates:
- Why stay hands-on: credibility (you can't set the kernel-PR bar if you've never profiled a kernel — Phase 02), judgment (you estimate and arbitrate better when you've done the work), and culture (teams respect leaders who can go deep). The prior 11 phases are this credibility, banked.
- How to not be the bottleneck: be hands-on in leveraged ways — prototype a risky technical bet to de-risk it, do the hard design review, pair on the gnarliest incident, write the reference for a new standard — not by owning critical-path features the team should own. Your code should unblock and set patterns, not be the dependency.
- The anti-patterns: the leader who codes the main feature (team atrophies, you're the bottleneck) and the leader who never touches code (loses credibility and judgment). The JD explicitly wants neither extreme.
The synthesis: a Head of Engineering's hands-on work is targeted at the highest- leverage technical risk and the standards, leaving the bulk to a team they've hired and structured (Ch. 3) and equipped with clear standards (Ch. 4). That's the role.
Lab Walkthrough Guidance
The capstone is the lab; the leadership artifacts are the portfolio.
- Boot and trace (
python platform.py): run it, then trace one request through the composition (Ch. 8) — license → admit → route → serve → metrics. - Run the scenarios: expired license (refuse), capability routing, overload (admission protects goodput), backend failover. Each is a prior phase, integrated.
- Write the design doc (
DESIGN-DOC.md): the architecture, the abstraction boundaries (the seams), the build-vs-buy decisions, the failure modes. This is the single most important artifact — it's what you'd present. - Write the runbook (
RUNBOOK.md): operational procedures for the platform's failure modes (compose the Phase 10 runbooks). - Write the four leadership artifacts (
leadership/): strategy, org, OEM, standards (Ch. 2–5). Use the rubrics. - Retrospective: what you'd do differently at real scale — the reflection that shows you understand the gap between the model and production.
Success Criteria
- You can articulate the five jobs of a Head of Engineering and which the JD emphasizes (Ch. 1)
- You can write an executable, sequenced roadmap with a build-vs-buy boundary (Ch. 2)
- You can design the team/skill-mix and map roles to competencies (Ch. 3)
- You can codify and defend with incidents the engineering standards (Ch. 4)
- You can describe the OEM-onboarding-as-conformance process (Ch. 5)
- You can connect every major technical investment to a commercial thesis (Ch. 6)
- You can communicate roofline economics + honest security to executives and customers (Ch. 7)
- The capstone boots, passes all scenarios, and you can present it end to end (Ch. 8)
- You can articulate the hands-on-without-bottleneck balance (Ch. 9)
Interview Q&A
Q1: Walk me through a platform you've built. (the capstone) A: [Present the capstone.] It's a GPU orchestration platform composing the layers the role spans: a capability-routed hardware-abstraction layer so software runs across vendors unchanged; a scheduler with admission control that protects goodput; a serving layer with paged KV-cache and continuous batching for serving efficiency; an offline license gate for sovereign deployment; and Prometheus observability with SLOs and release gates so it ships as a product, not a prototype — all behind one control-plane API. The architecture is the abstraction boundaries: the HAL ABI, the runtime contract, the control-plane API are the seams that let each layer evolve independently. I'd walk you through a request: license check → admission → capability routing → batched generation → metrics, and the failure modes — failover on a bad backend, refusal on an expired license, load-shedding under overload — that integration surfaces. What I'd do differently at real scale: [the retrospective].
Q2: How would you execute "decouple our software from hardware"? A: It's a HAL-centered strategy with a sequenced roadmap (not "build everything"). The HAL (the moat) is the spine — a stable API, a versioned backend ABI, and dlopen'd vendor backends with capability routing, so a new chip is a new file, not a rewrite. I'd sequence: first the HAL + NVIDIA backend + serving + licensing to land a first (likely sovereign) customer; then the AMD backend + a conformance suite to onboard OEM #1; then a Triton/MLIR backend for kernel velocity and future chips toward multi-vendor GA. Build-vs-buy: build the HAL, adopt the kernel compiler, buy commodity infra — stated once and defended consistently. Each milestone has a commercial outcome, conformance gates every backend, and the pace is governed by error budgets. The org is shaped to that boundary: a platform-core team owns the HAL/runtime, not a compiler team we don't need.
Q3: How do you structure and hire a GPU platform team? A: By subsystem ownership, mapped to the competencies: a platform-core team owns the HAL + runtime (the hardest hires — kernel/compiler and systems people), a serving team owns KV-cache/batching/engine-integration end to end, an infra/SRE team owns scheduling + observability + releases (GPU-savvy SREs are rare and critical), with a small security specialist team consulting across. I hire for the gaps — the kernel/compiler folks and GPU SREs, not generalists — and use a competency-mapped interview bar. Seniority shape: a few principals who set and defend the standards, more senior ICs executing; I avoid over-hiring juniors into a steep-ramp domain with thin mentorship. Critically, org design encodes strategy — the team shape is the build-vs-buy boundary, so I staff to "adopt the compiler, build the HAL," not to a compiler team.
Q4: How do you balance shipping fast with production quality? A: Error budgets make it arithmetic, not argument (Phase 10). SLOs tied to user experience (goodput at SLO, not raw throughput) define "healthy"; the error budget (1 − SLO) is spendable — we ship aggressively while budget remains and freeze risky changes when it's exhausted. Quality is enforced by codified gates that each prevent a specific incident class — the kernel-PR checklist, the runtime contract, ABI/conformance discipline, the compatibility→canary→rollback release pipeline — defended by the incidents they prevent, not by authority. So "fast" and "production-grade" aren't in tension: the gates let us move fast safely, and the budget tells us objectively when to push and when to stabilize. That's how a deep-tech startup ships through a full release cycle without becoming either reckless or sclerotic.
Q5: Sell me the sovereign-AI value proposition. A: [To an executive:] Regulated customers — defence, finance, healthcare, government — have budget and can't use cloud-assuming AI stacks: they need to run on their own hardware, often air-gapped, with provable control of the software and data. Our platform is built for exactly that: it runs unchanged across whatever hardware they have (the HAL), it's licensed and activated fully offline (no phone-home), and for the highest stakes it proves — via attestation and confidential computing — that it runs unmodified on authorized hardware with model weights and customer data never leaving an encrypted enclave. A cloud-native competitor structurally can't make that claim. So it's a defensible, high-margin market the architecture opens — and the same hardware-agnostic core also wins cost-conscious customers who want out from under NVIDIA lock-in. I'd be honest about limits (attestation proves what loaded, not bug-freeness) because in this market credibility is the product.
References
- Will Larson, An Elegant Puzzle: Systems of Engineering Management — org design, standards
- Camille Fournier, The Manager's Path — the IC-to-leader transition
- Michael Nygard, Release It! — production-grade design (pairs with Phase 10)
- Staff Engineer (Larson) and The Staff Engineer's Path (Tanya Reilly) — technical leadership without management-only
- Google SRE books — SLOs/error budgets as a management tool (Phase 10)
- Architecture Decision Records (Phase 09) — the strategy-as-artifact discipline
- High Output Management (Andy Grove) — leverage, the core leadership idea (Ch. 9)
- Cross-track: all prior phases of this track (the capstone composes them); interview-prep, system-design
🛸 Hitchhiker's Guide — Phase 12: Leadership & the Capstone
Read this if: you have the depth (Phases 01–11) and need to turn it into the role — decisions, standards, hiring, communication, and an integrated platform. Field notes; the WARMUP carries the synthesis.
0. The 30-second mental model
A Head of Engineering's leverage is decisions, standards, hiring, and clarity — not keystrokes. Five jobs: set direction (strategy + ADRs), build the team, own the quality bar, drive partnerships/commercialization, communicate. Stay hands-on at the highest-leverage risk and the standards, not on critical-path features. The capstone proves you can integrate; the leadership artifacts prove you can lead.
1. The five jobs (and the JD lines they map to)
| Job | JD line | Artifact |
|---|---|---|
| Set technical direction | "decouple software from hardware" | PLATFORM-STRATEGY.md |
| Build the team | (implicit: lead 5+ engineers) | ORG-DESIGN.md |
| Own the quality bar | "ship through a full release cycle" | ENGINEERING-STANDARDS.md |
| Partnerships/commercial | "OEM integrations," "commercialisation" | OEM-ONBOARDING.md |
| Communicate | "technical and executive levels" | (every doc + the demo) |
2. Strategy card
Executable roadmap, not a wish. Thesis (HAL = spine, lock-in = enemy, sovereign + cost-conscious = market) → sequenced bets each with a commercial outcome → build-vs-buy boundary (build HAL, adopt compiler, buy infra) → paced by error budget. Signal of a leader: sequencing, dependencies, kill-criteria. Signal of inexperience: "and then we build everything."
3. Org card
Structure by subsystem ownership (platform-core = HAL+runtime, serving = KV/batching, infra/SRE = scheduling/observability/releases, security = specialist consult). Hire for the gaps (kernel/compiler, GPU-SRE — the rare ones). Org design is strategy: the team shape encodes build-vs-buy. Few principals set standards, senior ICs execute, few juniors in a steep-ramp domain.
4. Standards card
Codify the implicit bar from the phases; defend each with the incident it prevents:
- kernel PR checklist (P02) · runtime contract (P05) · ABI/conformance (P09) · release gates (P10) · security-claim discipline (P11). Standards justified by incidents stick; by authority, rot.
5. OEM card
Onboard a chip = a HAL backend (P09) that passes conformance (correctness → perf tier) → "certified backend" status = quality promise + negotiation lever. Versioned ABI = their backend survives your releases = ecosystem trust. Every OEM = a backend = revenue. The HAL's quality is your partner-pipeline throughput.
6. Commercialization card
| Technical artifact | Commercial thesis |
|---|---|
| HAL (P09) | market access for non-NVIDIA chips + lock-in freedom |
| sovereign security (P11) | the regulated market cloud can't serve |
| sharing tiers (P06) | pricing tiers by isolation guarantee |
| serving efficiency (P07) | goodput/GPU = gross margin |
| conformance (P09) | partner-ecosystem flywheel |
Every investment gets a "who pays more / churns less / becomes reachable."
7. Communication card
- Execs: decisions + consequences + dollars (roofline as $/year, sovereign as TAM). Not mechanism.
- Customers (technical): honest capability + honest limits (goodput-at-SLO, defeat conditions). Credibility compounds; overselling destroys it.
- The through-line: P01 spec-sheet skepticism → P11 "never uncrackable." Honest, decision-oriented communication is a technical leadership skill.
8. Capstone card (how it composes)
request -> license gate (P11) -> admission (P06/07) -> HAL routing (P09)
-> paged-KV continuous batching (P07) -> metrics+SLO (P10)
failures: bad backend -> failover; expiry -> refuse; overload -> shed
Abstraction boundaries (HAL ABI, runtime contract, control-plane API) are the architecture. Cross-cutting concerns (licensing/observability/failure) compose awkwardly — the capstone shows where they belong. CPU-runnable so the architecture is the lesson.
9. Hands-on balance
Stay deep on leveraged work: de-risk a bet with a prototype, do the hard design review, pair on the worst incident, write a standard's reference. Not on critical-path features (you become the bottleneck) and not never (you lose credibility). The JD wants neither extreme. Your code sets patterns and unblocks; it isn't the dependency.
10. Exit bar
You can present an integrated platform end to end to an engineer and an executive; write an executable strategy, an org design, an OEM process, and codified standards; connect every technical bet to revenue; communicate honestly up and out; and articulate the hands-on-without-bottleneck balance. With the prior 11 phases as banked credibility, that's the role. You're ready for the interview — see interview-prep/ and system-design/.
Capstone — Mini GPU Orchestration Platform
Phase: 12 — Leadership & Capstone | Difficulty: ⭐⭐⭐⭐⭐ | Time: 1.5 weeks Language: Python (stdlib) | Hardware: none
Concept primer:
../WARMUP.mdCh. 8.
Run
python platform.py # boots the platform + runs all integration scenarios
0. The mission
Compose the prior phases into one working control plane and prove the integration with scenarios. The platform is the portfolio piece — it shows you can build the system the JD describes, not just its parts.
request -> [License gate, P11] -> [Admission control, P06/07] -> [HAL routing, P09]
-> [Paged-KV continuous-batching serving, P07] -> [Metrics + SLOs, P10]
1. What the scenarios prove
| Scenario | Integrates | Asserts |
|---|---|---|
| 1 healthy serving | P07 continuous batching | all 20 requests served |
| 2 expired license | P11 license gate | serving refused |
| 3 capability routing | P09 HAL | FP8 → FP8-capable backend; refused if none |
| 4 backend failover | P09 + P10 health | FP16 fails over to a healthy backend |
| 5 overload | P06/07 admission | KV admission protects goodput; nothing OOM-crashes |
| 6 observability | P10 | valid Prometheus export |
All pass on python platform.py.
2. Reading order (platform.py)
License(P11),HAL/Backend(P09),KVBlockPool(P07),Metrics(P10) — the subsystems.Platform.serve— the integrated request path (the heart): license gate → admission → routing → batched generation → metrics. Trace one request through it.- The six scenarios in
main.
3. The deliverables (write these)
DESIGN-DOC.md— the architecture: the subsystems, the abstraction boundaries (HAL ABI, runtime contract, control-plane API — the seams), the build-vs-buy decisions, the failure modes, what changes at real scale. A template is provided.RUNBOOK.md— operational procedures for the platform's failure modes (compose the Phase 10 runbooks). Template provided.- The four leadership artifacts in
../leadership/— strategy, org, OEM, standards.
4. Extensions
- Multi-node (P08): TP-shard a model across simulated nodes; add the collective cost to the serving path.
- TypeScript control-plane CLI (the JD's JS/TS-for-tooling line): a
pctlCLI that talks to the platform's control API (status, license, route). - Real backend: wire the Phase 09 C HAL (CUDA backend if hardware exists) under this control plane.
- Attestation gate (P11): require a valid attestation before serving with decrypted weights.
5. What this capstone proves about you
You can integrate — compose a HAL, scheduler, serving engine, licensing, and observability into one coherent platform with sensible abstraction boundaries and honest failure handling. Integration is harder than any single phase and is exactly what a Head of Engineering is hired to lead. With the design doc and leadership artifacts, this is the portfolio that proves you can do the role.
Platform Strategy (template + worked outline)
The vision + an executable roadmap. Concepts:
../WARMUP.mdCh. 2, 6. Fill in / adapt to your scenario.
Thesis
Decouple software from hardware (the JD's mandate). The HAL (Phase 09) is the spine; vendor lock-in is the enemy; the market is (a) customers wanting freedom from NVIDIA lock-in and lower cost, and (b) sovereign/regulated customers who need on-prem/air-gapped deployment. The moat is a performant, versioned, conformance-gated, multi-vendor HAL with an OEM ecosystem.
Roadmap (sequenced bets, each with a commercial outcome)
| Phase | Technical | Commercial outcome | Kill criteria |
|---|---|---|---|
| 0–6 mo | HAL + NVIDIA backend + serving (P07) + offline licensing (P11) | first sovereign customer in production | no sovereign pipeline by mo 6 |
| 6–12 mo | AMD backend + conformance suite (P09) + observability/release (P10) | OEM #1 certified; multi-vendor proof | AMD backend < 80% of vendor peak |
| 12–18 mo | Triton/MLIR backend + GPU sharing tiers (P06) + attestation (P11) | multi-vendor GA; tiered pricing; regulated-market expansion | kernel-velocity not improving |
Each milestone is commercial, not just technical. Dependencies are explicit (licensing before sovereign customer; conformance before OEM).
Build vs buy (stated once, defended consistently — Phase 09 ADR)
- Build: the HAL + control plane (the moat).
- Adopt: kernel compiler (Triton/MLIR), inference engines, observability stack.
- Buy: commodity infra, HSM/KMS.
Governance
- Pace governed by error budgets (P10), not just ambition.
- Each major investment carries a commercial thesis (P12 Ch. 6).
- Conformance (P09 Ch. 7) gates every backend; ABI versioning (P09 Ch. 6) gates releases.
Your additions
- The specific first customer/segment and why:
- The riskiest bet and how you'd de-risk it (a hands-on prototype, P12 Ch. 9):
- What you'd cut if you had half the team:
Org Design (template + worked outline)
Team structure, skill mix, hiring. Concepts:
../WARMUP.mdCh. 3. Org design is strategy — the team shape encodes the build-vs-buy boundary.
Teams (by subsystem ownership, not by layer)
| Team | Owns (phases) | Rare hires | Size (early) |
|---|---|---|---|
| Platform Core | HAL + runtime (P05, P09) | kernel/compiler, systems C/C++/Rust | 4–6 |
| Serving | KV-cache, batching, engine integration (P07) | LLM-serving/perf engineers | 3–5 |
| Infra / SRE | scheduling, observability, releases (P06, P10) | GPU-savvy SREs | 3–4 |
| Security | licensing, attestation, sovereign (P11) | applied crypto / security eng | 1–2 (consult across) |
Hiring map (role → competency → interview bar)
Map every open role to a phase's competency; use ../../interview-prep/
as the bar. The rare, must-get-right hires: kernel/compiler (P02/P03) and GPU SREs
(P04/P10). Generalists fill faster.
Seniority shape
- A few principals who set and defend the standards (P12 Ch. 4) and de-risk the hardest bets (P12 Ch. 9).
- More senior ICs executing.
- Few juniors early — steep ramp, thin mentorship in a deep domain.
Build-vs-hire calls
- Don't hire a compiler team (adopt Triton/MLIR + hire integrators) unless the strategy commits to Option C (P09).
- Do invest early in Platform Core and SRE — they're the bottleneck for both the roadmap and reliability.
Your additions
- The first 5 hires, in order, and why:
- Where you'd accept a generalist vs hold out for a specialist:
- How the org shape would change at 3× headcount:
OEM Backend Onboarding (template + process)
Onboard a vendor's chip as a certified HAL backend — technical + commercial. Concepts:
../WARMUP.mdCh. 5; Phase 09.
The process
1. Technical integration
- Vendor (or our team) implements a HAL backend (P09) wrapping their compute libraries against our versioned ABI (P09 Ch. 6).
- Capabilities declared (dtypes, ops, perf hints) for capability routing (P09 Ch. 4).
2. Conformance certification (the gate + the lever)
- Correctness conformance: the single test matrix, all ops within tolerance (P09 Ch. 7 — declarations are tested, not trusted).
- Performance conformance: must hit X% of roofline (P01) on key ops, else "supported, not recommended."
- Passing = "certified backend" status — a quality promise to customers and a negotiation chip with the vendor (ecosystem access for a tested backend + co-marketing).
3. Commercial + relationship
- Certification protects our platform's reputation (a slow/broken backend poisons it) and gives the vendor a clear target.
- Versioned ABI means their backend survives our releases — the trust that sustains the ecosystem.
- Each certified OEM = a backend behind the HAL = a revenue/adoption event.
Checklist (per OEM)
- ABI version compatibility confirmed
- Backend passes correctness conformance
- Backend meets the performance tier (or labeled accordingly)
- Capability declarations verified
- Co-marketing / certification announced
- Added to the release compatibility matrix (P10)
Your additions
- The AMD-specific costs (no PTX-equivalent, per-gfx rebuilds — P03):
- How you'd phase a first OEM with a partial op set:
- The certification tiers and what each promises a customer:
Engineering Standards (the codified quality bar)
The standards a Head of Engineering owns and defends — each justified by the incident class it prevents (P12 Ch. 4). Standards justified by incidents stick; by authority, rot.
Kernel PR checklist (Phase 02)
-
Correctness: reference-comparison test within tolerance +
compute-sanitizerclean - Honest timing: CUDA events, warmup, median of N, fixed clocks noted
-
Limiter evidence:
ncubefore/after naming the bound that moved - Portability: archs tuned on; occupancy impact noted
- Prevents: fast-but-wrong kernels, unexplained "speedups" that are measurement bugs.
Runtime contract (Phase 05)
- No driver alloc/free in steady-state request paths (CI-enforced)
- Stream-ordered lifetime only (no raw "free now")
- Pool segregation + reserved/allocated/largest-block metrics exported
- No device-wide syncs in engine code (lint)
- Prevents: day-3 fragmentation OOM, cross-stream use-after-free, false serialization.
ABI & conformance discipline (Phase 09)
- Backend ABI: version-first, append-only, reserved fields
- Every backend passes the conformance suite (correctness + perf tier)
- Capability declarations tested, not trusted
- Prevents: vendor backends breaking on releases, lowest-common-denominator abstraction, declared-but-broken capabilities.
Release gates (Phase 10)
- Compatibility gate (driver/CUDA/engine matrix) pre-deploy
- Statistical canary → progressive rollout → auto-rollback on SLO burn
- Error-budget-governed release pace
- Prevents: driver-skew fleet incidents, regressions reaching production, "ship and pray."
Security-claim discipline (Phase 11)
- Every protection stated with its defeat condition; never "uncrackable"
- Sign licenses, encrypt data; private key in HSM
- Attestation/confidential computing for high-stakes (not self-checks alone)
- Prevents: credibility loss with regulated customers, overselling security.
Observability baseline (Phase 10)
- Correct gauge/counter semantics; bounded label cardinality
- Alert on SLOs (page) + cause-signals that predict death (Xid/ECC)
- Every page actionable + has a runbook
- Prevents: misleading dashboards, alert fatigue, 3am surprises.
How to defend a standard
When a team finds a rule pedantic, cite the incident class it prevents — not your title. Each line above maps to a specific failure from the phases. A standard without a "this exists because of X" is a candidate for deletion.
Interview Prep — Head of Software Engineering [GPU]
This is the interview surface for the role, drawn from the 12 phases. The role is senior and broad, so interviews span deep technical, systems design, and leadership — often in the same loop.
The five question banks
| File | Covers | Phases |
|---|---|---|
| 01 — GPU & Systems Deep-Dive | architecture, CUDA, compilers, drivers, runtime | 01–05 |
| 02 — Serving, Scheduling & Distributed | KV-cache, batching, GPU sharing, collectives | 06–08 |
| 03 — Platform, Portability & Security | HAL/ABI, observability, licensing, sovereign | 09–11 |
| 04 — System Design | the big design walkthroughs | all |
| 05 — Leadership & Behavioral | strategy, org, OEM, communication, STAR | 12 |
How to use this
- Self-test first: answer cold, then check against the model answer + the WARMUP it derives from. Gaps point you back to a phase.
- The bar is "derive, don't recite": every technical answer should reason from a principle (roofline, the 2V all-reduce bound, sign-vs-encrypt), not a memorized fact. The WARMUPs' Interview Q&A sections are the depth target.
- Leadership answers need a decision + its consequence + the honest limit — the Phase 12 communication discipline.
The 20 questions most likely to decide the loop
- Derive H100's ridge point and what it means for LLM decode. (P01)
- Walk me through what happens at the OS level on
cudaLaunchKernel. (P02/P04) - What does
nvccdo? PTX vs SASS, and the cold-start incident. (P03) - Why does PyTorch ship its own allocator? Reserved vs allocated. (P05)
- Compare MPS, MIG, time-slicing; deliver a "0.5 GPU" tier honestly. (P06)
- Explain PagedAttention — and why it's really an allocator. (P07/P05)
- Static vs continuous batching; what is goodput and why does it beat throughput? (P07)
- Derive ring all-reduce's cost; why is it bandwidth-optimal and N-independent? (P08)
- Why must tensor parallelism stay on NVLink? With numbers. (P08)
- Design a hardware-abstraction layer; walk the API/ABI boundary. (P09)
- How do you avoid a lowest-common-denominator abstraction? (P09)
- How do you version a plugin ABI so a year-old vendor backend still loads? (P09)
- What GPU metrics do you alert on, and why is "GPU utilization" misleading? (P10)
- Design a safe release process for a GPU serving platform. (P10)
- Design a license system for air-gapped deployment; sign vs encrypt? (P11)
- Anti-tamper self-checks — what do they buy, how are they defeated? (P11)
- Build vs adopt: your own compiler layer or Triton/MLIR? Defend it. (P03/P09)
- How would you execute "decouple software from hardware"? (P09/P12)
- How do you structure and hire a GPU platform team? (P12)
- Sell me the sovereign-AI value proposition. (P12)
Each maps to a WARMUP's Interview Q&A with a full principal-level answer.
Format expectations for this role
- Technical screens: whiteboard a kernel/scheduler/allocator; reason about a roofline; debug a "slow training" or "slow serving" scenario.
- System design: the system-design walkthroughs (GPU cloud, serving platform, HAL, licensing-at-scale).
- Leadership: strategy memos, org design, OEM/partner scenarios, and STAR behavioral stories — see 05.
- Hands-on credibility check: expect to be asked to actually reason about code you'd review — the labs are your evidence you can.
Interview Bank 01 — GPU & Systems Deep-Dive (Phases 01–05)
Derive, don't recite. Each answer reasons from a principle. Full answers in the phases' WARMUP Interview Q&A sections; this bank is the index + extra drills.
Architecture (Phase 01)
Q: A100 has 1,555 GB/s HBM and 312 TFLOPS BF16. Ridge point? Implication for decode? Ridge = 312e12 / 2.0e12 ≈ 156 FLOP/byte. Batch-1 decode is AI ≈ 2 → ~80× bandwidth-bound; ceiling for a 14 GB FP16 model ≈ 2.0 TB/s ÷ 14 GB ≈ 140 tok/s. Serving economics = raise effective AI (batching, quantization). [P01 WARMUP Q3]
Q: Why no branch predictors / OoO on a GPU? Latency-hiding by oversubscription replaces latency-removal by prediction; those transistors become ALUs. [P01 WARMUP Q1]
Drill: warp executes if (threadIdx.x % 4 < 2) — cost? (~2×, two masked
serialized paths; the % 4 < 2 splits each warp in halves.)
Drill: when is 100% occupancy the wrong goal? (Latency already hidden; cutting registers to raise occupancy spills — Volkov. [P01 WARMUP Q5])
CUDA (Phase 02)
Q: Kernel gets 8% of peak bandwidth — first three hypotheses? Uncoalesced access (sectors/request ≈ 32), latency-bound (too few warps), or the kernel isn't the bottleneck (nsys timeline). [P02 WARMUP Q2]
Q: Launch config for a 1920×1080 filter.
16×16 blocks, 120×68 grid, guard x<1920 && y<1080, shared-mem halo tile. [P02
WARMUP Q1]
Drill: what breaks with __syncthreads() in divergent code? (Deadlock — all
block threads must hit the same barrier. [P02 WARMUP Q4])
Drill: a PR claims 3× speedup — what do you require before merge? (Reference test + sanitizer, honest timing, ncu limiter evidence, arch notes. [P02 WARMUP Q6])
Compilers (Phase 03)
Q: What happens compiling+running CUDA on a GPU that didn't exist at compile time? No matching cubin → driver JITs the embedded PTX → SASS at load, caches it; slow first load (the cold-start incident); fails if no PTX embedded. [P03 WARMUP Q1]
Q: Why does PTX exist? Virtual ISA for stability across GPU generations whose hardware ISAs change; splits slow AOT optimization from fast load-time lowering; CUDA's compatibility moat. [P03 WARMUP Q2]
Drill: how do you spot register pressure in SASS? (LDL/STL spills. [P03
WARMUP Q3])
Drill: why did Triton succeed? (Block-level abstraction — automates tiling/ staging, ~90% of hand-tuned, Python+MLIR. [P03 WARMUP Q4])
Drivers & containers (Phase 04)
Q: What happens at the OS level on cudaLaunchKernel?
Init: open /dev/nvidia*, ioctls create context, mmap ring+doorbell, JIT PTX.
Launch: write ring + tap doorbell — zero syscalls; GPU writes a fence; sync waits
on it. [P04 WARMUP Q1]
Q: Why doesn't CUDA_VISIBLE_DEVICES provide isolation?
Env var read by the untrusted process; enforcement is the devices cgroup, and even
that gates whole devices — real isolation is MIG/dedicated. [P04 WARMUP Q2]
Drill: container sees the GPU but cudaInit fails — debug. (nvidia-smi on host;
ldconfig for libcuda version skew; cgroup perms; strace the failing ioctl. [P04
WARMUP Q3])
Drill: why is there no GPU cgroup controller, and what fills the gap? (The KMD/ GPU owns GPU time, opaque to the kernel scheduler; MPS/MIG/time-slicing fill it. [P04 WARMUP Q2 / P06])
Runtime (Phase 05)
Q: nvidia-smi shows 78 GB used, framework says 51 GB — who's lying? Neither: reserved (driver's view, includes cache) vs allocated (live tensors). Gap is cache, not a leak. Watch largest-free-block for fragmentation. [P05 WARMUP Q2]
Q: Serving process OOMs after 3 days at steady load — hypotheses? External fragmentation (largest-free-block shrinking at <70% allocated), genuine leak (allocated trending up), pinned exhaustion, KV admission. [P05 WARMUP Q3]
Drill: design stream-ordered deallocation — what bug does it prevent? (Cross-stream use-after-free; free becomes an event in stream order. [P05 WARMUP Q4])
Drill: when do CUDA Graphs help and what can't they capture? (Many short kernels per iteration; not dynamic shapes/control flow/syncs. [P05 WARMUP Q5])
The cross-cutting synthesis question
Q: A 7B model serves at 30 tok/s on an H100 at batch 1. Walk me from silicon to the fix. Decode is bandwidth-bound (P01 roofline); 14 GB weights ÷ 3.35 TB/s ≈ 240 tok/s ceiling, so 30 means we're far below even that — check: is it actually compute or a pipeline stall (nsys, P02/P10), is the allocator thrashing (P05), is it launch- overhead-bound (CUDA Graphs, P05)? Then the economic fix is raising AI: batch (P07), quantize (P07). The answer threads P01→P05→P07 — exactly the role's breadth.
Interview Bank 02 — Serving, Scheduling & Distributed (Phases 06–08)
Full answers in the phases' WARMUP Interview Q&A; this is the index + drills.
GPU scheduling & sharing (Phase 06)
Q: Compare MPS, MIG, time-slicing. MPS = co-resident processes, weak isolation, trusted small jobs. MIG = hardware partition, strong isolation, multi-tenant/SLO. Time-slicing = whole-GPU rotation, no isolation, bursty/dev. Pick on isolation-vs-utilization. [P06 WARMUP Q1]
Q: 8-GPU job won't schedule though 12 are free — why? Cluster fragmentation: 12 scattered, the job needs 8 co-located (gang + topology) — stranded GPUs. Fix: best-fit/consolidation, reservation, topology-aware admission. [P06 WARMUP Q2]
Q: Customer wants "0.5 GPU" tiers — how, honestly? "Isolated half (MIG 3g.40gb) or cheaper shared half (time-slice, slow when neighbors busy)?" Allocation ≠ isolation. [P06 WARMUP Q1/Q4]
Drill: what is gang scheduling and what deadlock does it prevent? (All-or- nothing multi-GPU placement; prevents two big jobs each grabbing partial sets and mutually blocking. [P06 WARMUP Q3])
Drill: how does k8s know a node has GPUs? (Device plugin: register → ListAndWatch → Allocate injects nodes+env; allocation not isolation. [P06 WARMUP Q4])
LLM serving & KV-cache (Phase 07)
Q: Derive KV cache for Llama-3-8B at 8K context; implication? 2·32·8·128·8192·2 ≈ 1.07 GB/seq → ~20 concurrent on a 40 GB GPU → the cache, not compute, caps concurrency. [P07 WARMUP Q1]
Q: Explain PagedAttention and why it's an allocator. Fixed KV blocks + per-seq block table = OS virtual memory for the cache; kills external fragmentation, 2–4× more sequences. The innovation is allocation, not attention. [P07 WARMUP Q2]
Q: Static vs continuous batching; what is goodput? Static = head-of-line blocking + formation delay; continuous = iteration-level admit/retire, 3–10×. Goodput = throughput meeting SLO; beats raw throughput (gameable by oversized batches). [P07 WARMUP Q3]
Drill: chat with a 2K-token system prompt on every request — what do you do? (Prefix caching with COW; >90% prefill cut. [P07 WARMUP Q4])
Drill: memory pressure mid-generation — options? (Admission control; preempt with recompute vs swap. [P07 WARMUP Q5])
Drill: choose among vLLM/TensorRT-LLM/SGLang for a multi-model platform. (Don't standardize; gateway + route per model; goodput-at-SLO evidence. [P07 WARMUP Q6])
Distributed & collectives (Phase 08)
Q: Derive ring all-reduce's cost; why bandwidth-optimal? reduce-scatter + all-gather = 2(N-1)/N·V ≈ 2V/rank, N-independent; 2V is the information-theoretic minimum per rank's links. [P08 WARMUP Q1]
Q: DP vs TP vs PP — which is NVLink-bound? TP (per-layer all-reduce on the critical path); DP all-reduces once/step (overlappable, IB ok); PP passes activations (cheap). [P08 WARMUP Q2]
Q: Why does TP across nodes destroy throughput, with numbers? Per-layer all-reduce ×20 slower on IB vs NVLink × dozens of layers, unhideable → 2–5× slowdown. [P08 WARMUP Q5]
Drill: 512-GPU run at 60% MFU — where do you look? (Exposed comm, topology misconfig, pipeline bubbles, stragglers — find what's not overlapped. [P08 WARMUP Q3])
Drill: one GPU fails in a 1024-GPU job — what happens, how survivable? (Silent hang; timeouts+watchdog, checkpointing, elastic, blast-radius placement. [P08 WARMUP Q4])
The cross-cutting synthesis question
Q: Design the GPU allocation for serving a 70B model to multi-tenant customers with SLOs. 70B doesn't fit one GPU → tensor-parallel across an NVLink island (P08 — TP must stay on NVLink), so gang-schedule the TP group on one node (P06). KV cache is TP-sharded (P07/P08 Ch. 9). Multi-tenant isolation → MIG or dedicated, never time-slice paid tenants together (P06). Serving = continuous batching + paged KV + prefix caching, tuned for goodput-at-SLO (P07). The answer composes P06+P07+P08 — the role's distributed-serving core.
Interview Bank 03 — Platform, Portability & Security (Phases 09–11)
Full answers in the phases' WARMUP Interview Q&A; this is the index + drills. These are the role's distinctive questions — the JD's core mandate + nice-to-haves.
Hardware abstraction & portability (Phase 09 — the keystone)
Q: Design a hardware-abstraction layer; walk the API/ABI boundary.
Stable API (app compiles against) + backend ABI (versioned vtable + capabilities,
the binary contract for separately-compiled vendor .sos) + dlopen loading (core
links no backend). [P09 WARMUP Q1]
Q: How do you avoid a lowest-common-denominator abstraction? Capability negotiation: backends declare support, platform routes per workload, exposes the union, fails cleanly; operation-level abstraction + escape hatches. [P09 WARMUP Q2]
Q: How do you version a plugin ABI so a year-old backend still loads? Version-first field, append-only growth, reserved fields, flags over fields, semver with a supported range. [P09 WARMUP Q3]
Q: Build vs adopt a compiler layer? Adopt MLIR/Triton — building re-pays everything for marginal fit and slows ecosystem velocity; build your own only for a novel execution model. [P09 WARMUP Q4]
Drill: how is decoupling software from hardware a defensible business? (Inverts NVIDIA's moat; OEM backends = revenue; conformance-gated multi-vendor HAL = compounding moat. [P09 WARMUP Q5])
Observability & production ops (Phase 10)
Q: What GPU metrics do you alert on; why is "GPU utilization" misleading? utilization.gpu = "a kernel ran," not "SMs busy" — batch-1 decode shows 100% while idle. Alert on SLOs + Xid/ECC/throttle/largest-free-block. [P10 WARMUP Q1]
Q: 90% util, low throughput — investigate. Check SM-active/DRAM-active and the roofline: memory-bound decode is normal at high util; if SM-active also low, latency-bound or throttling. [P10 WARMUP Q2]
Q: Design a safe release process for a GPU serving platform. Compatibility gate (driver/CUDA/engine matrix, pre-deploy) → statistical canary → progressive rollout → auto-rollback on SLO burn → feature flags; error-budget governed. [P10 WARMUP Q3]
Drill: catch a fragmentation leak before 3am? (Alert on largest-free-block trend, distinct from cache and from a real leak. [P10 WARMUP Q4])
Drill: driver upgrade regressed 5% of the fleet — detect/respond? (Should be caught in canary; auto-rollback on budget burn; post-mortem → add SKU to canary + a perf gate. [P10 WARMUP Q5])
Security, licensing & protection (Phase 11)
Q: Design a license system for air-gapped deployment. Ed25519-signed (not encrypted) token binding features/expiry/fuzzy fingerprint, verified offline with embedded public key, bounded grace + clock-rollback resistance, challenge-response activation. State the defeat condition; private key in HSM. [P11 WARMUP Q1]
Q: Why sign instead of encrypt; where does the key live? Need integrity+authenticity not secrecy; public verifier can ship safely; encryption would put an extractable secret in the binary. Private key in HSM, the crown jewel. [P11 WARMUP Q2]
Q: Bind to specific GPUs — what about a NIC swap? GPU UUIDs + board + MAC, fuzzy N-of-M match (tolerate a swap, reject a different machine); defeat condition = fake IDs; attestation is the strong version. [P11 WARMUP Q3]
Q: Anti-tamper self-checks — what do they buy, how defeated? Catch casual patching, force the attacker to defeat the checker too; defeated by patching the checker/digest — a cost-raiser, not a guarantee. Attestation (external verifier + hardware key) is the strong answer. [P11 WARMUP Q4]
Q: Strong answer to "prove it runs unmodified on authorized hardware"? Attestation: hardware root measures code → hardware-signed quote → external verifier; the key is in hardware, below the software the attacker controls. Pair with confidential computing for weights/data. [P11 WARMUP Q5]
Drill: how is security a business enabler here? (Opens the sovereign/regulated market a cloud-assuming competitor can't serve. [P11 WARMUP Q6])
The cross-cutting synthesis question
Q: A defence ministry wants your platform on their air-gapped hardware. Design the deployment. Hardware-agnostic via the HAL (P09 — runs on whatever they have); licensed offline with challenge-response activation (P11); no phone-home, local-only observability (P10 Ch. 9); model weights protected by confidential computing + attestation-gated decryption (P11); supply-chain integrity (signed artifacts, reproducible builds) and auditability. State the honest claims (attestation proves what loaded, not bug-freeness). This threads P09+P10+P11 — the JD's sovereign-AI thesis end to end.
Interview Bank 05 — Leadership & Behavioral (Phase 12)
For a Head of Engineering, these decide the loop as much as the technical screens. Full answers in Phase 12 WARMUP Interview Q&A. Behavioral answers use STAR (Situation, Task, Action, Result).
Strategy & decisions
Q: How would you execute "decouple our software from hardware"? HAL-centered, sequenced roadmap (not "build everything"): HAL + NVIDIA + serving + licensing → first sovereign customer; AMD + conformance → OEM #1; Triton/MLIR → multi-vendor GA. Build the HAL, adopt the compiler, buy infra. [P12 WARMUP Q2]
Q: How do you balance shipping fast with production quality? Error budgets make it arithmetic: SLOs define healthy, the budget is spendable, codified gates (each preventing a named incident class) enforce quality without slowing the safe path. [P12 WARMUP Q4]
Q: Build vs buy for the kernel layer? Adopt Triton/MLIR; build the HAL. [P09 WARMUP Q4 / P12]
Org & people
Q: How do you structure and hire a GPU platform team? By subsystem ownership (platform-core, serving, infra/SRE, security-consult); hire for the rare gaps (kernel/compiler, GPU SREs); principals set standards, senior ICs execute, few juniors; org shape encodes build-vs-buy. [P12 WARMUP Q3]
Q: A principal wants to build a custom IR — how do you evaluate? Probe the build-vs-adopt analysis: which MLIR dialects evaluated, where they fail, the maintenance headcount, how you'd absorb the next kernel innovation. Crisp "linalg+nvgpu failed here" earns it; "LLVM is bloated" doesn't. [P03 WARMUP Q6]
Partnerships & commercialization
Q: How do you onboard an OEM's accelerator? HAL backend → conformance certification (correctness + perf tier) → "certified backend" status as quality promise + negotiation lever; versioned ABI sustains the ecosystem; each OEM = a backend = revenue. [P12 WARMUP Q&A / Ch. 5]
Q: Sell me the sovereign-AI value proposition. Regulated customers can't use cloud-assuming stacks; ours runs on their hardware, licensed offline, with attested/confidential-computing protection for weights and data — a market a cloud-native competitor structurally can't serve. Honest about limits. [P12 WARMUP Q5]
Behavioral (STAR) — prepare your own stories for:
- A hard technical decision under uncertainty (a build-vs-buy, a platform bet). Show: how you sequenced, what you'd cut, the outcome.
- Raising the quality bar against pushback (a standard a team resisted). Show: justifying with the incident it prevents, not authority. [P12 Ch. 4]
- A production incident you led (detect → mitigate → learn). Show: mitigate- first, blameless post-mortem, the system change that resulted. [P10 Ch. 9]
- Staying hands-on without becoming the bottleneck (a risk you de-risked with a prototype). Show: leveraged hands-on work, not owning a critical-path feature. [P12 Ch. 9]
- A partnership/customer you engaged technically and executively (an OEM or a sovereign customer). Show: translation without dishonesty. [P12 Ch. 7]
- A disagreement with a peer/exec you resolved (a roadmap or resourcing conflict). Show: decision-oriented communication, data over opinion.
The meta-signal interviewers want
For each answer: a decision, its consequence, and the honest limit/ tradeoff. The through-line from Phase 01's spec-sheet skepticism to Phase 11's "never uncrackable" is that honest, decision-oriented communication is itself a technical-leadership skill — and the prior 11 phases are the credibility that makes your leadership answers land.
System Design — Head of Software Engineering [GPU]
The big design walkthroughs for the role. Each is the kind of open-ended "design X" question a senior GPU-platform loop asks — answered at the altitude a Head of Engineering operates at: requirements → architecture → the hard tradeoffs → the failure modes → the commercial framing.
| Walkthrough | Question | Phases |
|---|---|---|
| 01 — GPU Cloud / Compute Platform | "Design a multi-tenant GPU cloud." | 04, 06, 10, 11 |
| 02 — LLM Inference Gateway at Scale | "Design a 100k-QPS inference platform." | 07, 06, 08, 10 |
| 03 — Hardware-Agnostic Runtime (HAL) | "Decouple our software from hardware." | 09, 03, 05 |
| 04 — Sovereign / Air-Gapped AI Deployment | "Deploy to a regulated, air-gapped customer." | 11, 07, 09, 10 |
How to use these
- Drive the requirements first — every good design answer starts by establishing scale, SLOs, tenancy, and constraints. The walkthroughs model this.
- State the tradeoffs explicitly — the senior signal is naming what you're giving up (isolation vs utilization, abstraction vs performance, cost vs guarantee), not presenting one "right" answer.
- Name the failure modes — at this level, "what breaks and how you detect/ recover" (Phases 08, 10) is half the answer.
- Close with the commercial framing — a Head of Engineering connects the architecture to adoption/revenue (Phase 12 Ch. 6).
The design-interview rubric for this role
- Requirements & scoping: did you establish scale, SLOs, tenancy, hardware, sovereignty?
- Architecture: clean abstraction boundaries (the seams — Phase 12 Ch. 8); the right components.
- Depth on demand: can you go deep on any box (KV cache, scheduler, HAL ABI)? The phases are that depth.
- Tradeoffs: named and reasoned, not hand-waved.
- Operations: observability, release, failure (Phases 08, 10).
- Commercial judgment: the business framing (Phase 12).
System Design 01 — Multi-Tenant GPU Cloud / Compute Platform
"Design a multi-tenant GPU cloud." Phases: 04 (drivers/containers), 06 (scheduling/sharing), 10 (observability/release), 11 (security/tenancy).
1. Drive the requirements
Ask before designing:
- Scale: how many GPUs/nodes? thousands → cluster scheduling + fragmentation matter (P06).
- Tenancy: multi-tenant (isolation is contractual) vs single-org? Sets the sharing mechanism (P06).
- Workloads: training (gang, long, checkpointable — P08) vs serving (admission, SLO — P07)? Different schedulers.
- Hardware: single-vendor or heterogeneous (→ HAL, P09)?
- Sovereignty: on-prem/air-gapped tier (→ P11)?
Assume: thousands of GPUs, mixed training+serving, multi-tenant with paid/free tiers, NVIDIA + one other vendor, an on-prem tier.
2. Architecture
+------------------ Control Plane -------------------+
users -> | API / quotas / billing / licensing (P11) |
| Scheduler: bin-pack + gang + topology (P06/P08) |
| Tenancy & isolation policy (P06/P04/P11) |
+----------------------------------------------------+
| |
+--------------+ +----------------+
| Node agent | ... | Node agent | (per node)
| device plugin (P06) | container runtime + GPU injection (P04)
| DCGM exporter (P10) | health/Xid monitor (P04/P10)
+--------------+ +----------------+
3. The hard decisions (and the tradeoffs)
- Sharing mechanism per tier (P06): dedicated/MIG for paid+isolated, time-slicing for free/dev, MPS only within a trusted pool. Tradeoff: isolation vs utilization; never co-place free+paid on one physical GPU.
- Scheduler (P06/P08): best-fit to limit stranding; gang scheduling for multi-GPU jobs; topology-aware placement so TP stays on NVLink islands. Tradeoff: utilization vs fragmentation vs fairness (DRF).
- Isolation (P04/P11): namespaces+cgroups+device injection per tenant; MIG for hardware isolation; remember allocation ≠ isolation (P04/P06).
- Heterogeneous hardware (P09): the HAL lets workloads target either vendor; capability routing.
4. Failure modes & operations (P10)
- Xid / GPU off the bus (P04): health monitor → cordon → drain → RMA.
- Fragmentation/stranding (P06): alert on stranded-GPU count; best-fit + reserved whole-node pool for big jobs.
- Node/collective failure for training (P08): checkpointing, elastic, fast detection (timeouts).
- Observability: DCGM metrics, SLOs, the release pipeline (P10) for the control plane and node agents.
- Driver fleet upgrades (P04 Q6): inventory → canary → drain → upgrade → progressive, image-pinned for rollback.
5. Tenant security (P11)
Per-tenant isolation (MIG/dedicated), licensing/quota at the control plane, audit logs; for the on-prem tier, the sovereign architecture (offline licensing, attestation — see system-design 04).
6. Commercial framing (P12)
Pricing tiers map to sharing mechanisms (isolation guarantee = price point, P06); heterogeneous support (the HAL) is lock-in freedom + cost; the on-prem/sovereign tier opens the regulated market. Utilization (best-fit, sharing) is gross margin.
The senior signal
You drove requirements, picked sharing mechanisms per tier with the isolation-vs-utilization tradeoff named, made the scheduler topology-aware (connecting P06 to P08), and closed with operations (P10) and the pricing model (P12) — breadth across the stack at the right altitude.
System Design 02 — LLM Inference Gateway at Scale
"Design a 100k-QPS LLM inference platform." Phases: 07 (serving/KV), 06 (scheduling), 08 (multi-GPU for big models), 10 (observability/release).
1. Drive the requirements
- QPS & latency SLOs: 100k QPS of what — short or long prompts? p99 TTFT/TPOT targets? (P07 — goodput is the real metric.)
- Models: how many, what sizes? A 70B model needs TP across an NVLink island (P08); many small models = multi-tenant packing (P06).
- Workload shape: chat (shared system prompts → prefix caching) vs varied?
- Hardware/sovereignty: heterogeneous (HAL, P09)? on-prem tier (P11)?
Assume: a mix of 8B and 70B models, chat-heavy (long shared prompts), strict p99 TTFT, cloud + an on-prem tier.
2. Architecture
clients -> [LB] -> [Gateway: routing, auth, rate-limit, licensing(P11)]
|
+-------------------- per-model serving pools --------------------+
| 8B pool: vLLM, continuous batching, paged KV, prefix cache (P07)|
| 70B pool: TP across NVLink island (P08), KV sharded (P07/P08) |
+----------------------------------------------------------------+
|
[Observability: TTFT/TPOT/goodput, KV pressure (P10)]
3. The hard decisions (and tradeoffs)
- Serving engine per pool (P07 Ch. 8): vLLM default for throughput+coverage; TensorRT-LLM for a fixed hot model needing the last 20%; SGLang for agent workloads. Tradeoff: peak-per-model vs coverage vs build complexity. Route on goodput evidence, not vendor numbers.
- Batching & admission (P07): continuous batching tuned for goodput (interior batch depth, not max); admission control sheds/queues under overload to protect p99. Tradeoff: latency vs throughput dial.
- KV-cache management (P07/P05): paged KV (no fragmentation), prefix caching for the shared chat prompts (huge TTFT win), KV quantization to fit more.
- Big-model placement (P08): 70B is TP-sharded on one NVLink island — gang-place it (P06); never spread across nodes.
- Prefill/decode interference (P07): chunked prefill or disaggregation so long prompts don't stall decodes.
4. Scaling to 100k QPS
- Horizontal: many replicas per model behind the LB; autoscale on goodput + queue depth.
- The KV wall (P07): concurrency is capped by KV memory, not compute — capacity
planning is
(HBM − weights) / (KV/token × context); quantize and use GQA models to raise it. - Prefix-cache hit rate as a first-class scaling metric for chat (P07).
5. Failure modes & operations (P10)
- GoodputBelowSLO: throttling (P03), bad deploy (auto-rollback, P10), or overload (admission).
- KV pressure / fragmentation (P05/P07): largest-free-block alert; admission already shedding.
- A replica/backend down: LB health-check + failover; the gateway routes around it.
- Release: compatibility gate (engine/model/driver, P03/P04/P10) → canary on goodput → progressive → auto-rollback.
6. Commercial framing (P12)
Goodput per GPU is gross margin — every serving optimization (batching, prefix cache, quantization) is margin. The on-prem tier (P11) and heterogeneous hardware (P09) open markets. Honest SLO reporting (goodput, not throughput) is a trust asset with customers.
The senior signal
You separated pools by model size (small = packing, 70B = TP-on-NVLink, connecting P07↔P08), tuned for goodput not throughput (P07's thesis), identified the KV wall as the real scaling limit, routed engines on evidence (P07 Ch. 8), and closed with margin + operations — the inference-platform leadership altitude.
System Design 03 — Hardware-Agnostic Runtime (the HAL)
"Decouple our software from hardware." The JD's core mandate. Phases: 09 (HAL/ABI — the keystone), 03 (compilers/backends), 05 (runtime).
1. Drive the requirements
- Which hardware, by when? NVIDIA today, AMD in 6 months, a startup in 12 — sets the backend roadmap.
- Performance bar: near-native on the hardware that matters, or "runs everywhere, fast enough"? Sets the abstraction altitude (P09 Ch. 5).
- Who writes backends? You, or the vendors (→ conformance + ABI versioning, P09).
- Workloads: serving (P07), training (P08), or both — sets which ops the HAL must expose.
2. Architecture (the HAL pattern, P09)
application -> STABLE API (hal_alloc, hal_op, hal_select_backend)
| vtable dispatch
+-----------+
| HAL core | capability routing, ABI version guard, conformance
+-----------+
| | |
nvidia_backend amd_backend startup_backend (dlopen'd, versioned ABI)
(cuBLAS/cuDNN) (rocBLAS) (vendor lib / Triton)
The three pieces (P09 Ch. 3): stable API, versioned backend ABI (vtable + capabilities), dynamic loading. The core links no backend — a new chip is a new file.
3. The hard decisions (and tradeoffs)
- Abstraction altitude (P09 Ch. 5): operation-level (
attention,gemm) with capability-gated fast paths and an escape hatch — not instruction-level (lowest-common-denominator, slow) nor vendor-exposed (re-coupled). Tradeoff: portability vs peak. - Backend strategy (P09 Ch. 8 / P03 Ch. 9): vendor-library backends for hardware you must support today (near-peak, time to market) + a Triton/MLIR backend for kernel velocity and future chips. Build vs buy: build the HAL (moat), adopt the compiler.
- ABI versioning (P09 Ch. 6): version-first, append-only, reserved fields, flags-over-fields, semver range — so a year-old vendor backend still loads. This is what sustains the vendor ecosystem.
- Capability negotiation (P09 Ch. 4): backends declare; platform routes; beats LCD.
- The AMD asymmetry (P03 Ch. 9): no stable PTX-equivalent → per-gfx rebuilds; budget the backend-engineering cost.
4. Conformance — what makes it trustworthy (P09 Ch. 7)
One test matrix, every backend passes: correctness (cross-vendor rounding tolerance — P03), declared-capability verification, and a performance tier ("supported" vs "recommended"). "Certified backend" status gates the ecosystem and is an OEM lever (P12 Ch. 5).
5. Runtime concerns (P05)
The HAL sits on the runtime contract: stream-ordered lifetime, pool segregation, no driver alloc/free in hot paths — these guarantees must hold across backends, so they're part of the ABI's behavioral contract, not just the syntactic one.
6. Failure modes & operations
- A backend fails conformance → not certified; capability routing won't select it.
- A backend
dlsymfails / wrong ABI version → cleanly rejected (P09 Ch. 2). - Performance regression in a backend → the perf-conformance tier catches it (P10 release gate analog).
7. Commercial framing (P12 Ch. 6, 9)
The HAL is the moat and the OEM on-ramp: every certified backend = market access for a chip + lock-in freedom for customers = revenue/adoption. It inverts NVIDIA's software moat. Conformance certification is a quality promise and a negotiation chip. This is why the Head of Engineering owns it — it's where engineering and commercialization fuse.
The senior signal
You chose the right abstraction altitude (operation-level + capability-gated + escape hatch, not LCD), committed to a build-the-HAL/adopt-the-compiler strategy with a sequenced backend roadmap, made ABI versioning the ecosystem-trust mechanism, gated everything on conformance, and framed it as the moat — the JD's central architectural thesis, owned end to end.
System Design 04 — Sovereign / Air-Gapped AI Deployment
"Deploy our platform to a regulated, air-gapped customer (defence/finance/ healthcare/government)." The JD's sovereign-AI nice-to-have, taken seriously. Phases: 11 (security/licensing), 07 (serving Ch. 9), 09 (portability), 10 (ops Ch. 9).
1. Drive the requirements
- Air-gap level: no network ever from deployment to vendor, or periodic out-of-band? Sets activation/update flow (P11 Ch. 5).
- What's protected: just licensing, or model weights + customer data from the operator too (→ confidential computing, P11 Ch. 9)?
- Hardware: customer-provided, possibly heterogeneous (→ HAL, P09).
- Compliance: data residency, auditability, certification requirements.
Assume: fully air-gapped, weights are our IP to protect, customer-provided mixed hardware, strict audit requirements.
2. Architecture
[customer's air-gapped environment]
+--------------------------------------------------------------+
| Platform (HAL-portable, P09) on the customer's hardware |
| Offline license gate (P11) - signed, hardware-bound |
| Serving (P07) - admission/goodput matter MORE (no burst) |
| Local-only observability (P10 Ch. 9) - no telemetry egress |
| Weights encrypted at rest; decrypt only inside an attested |
| TEE / GPU confidential computing (P11 Ch. 8-9) |
| Tamper-evident audit logs |
+--------------------------------------------------------------+
^ out-of-band only (activation, updates)
[vendor side: activation service, signing key in HSM]
3. The hard decisions (and tradeoffs)
- Licensing without a server (P11 Ch. 4–5): Ed25519-signed, offline-verified, hardware-bound (fuzzy fingerprint) license; challenge-response activation out-of-band; bounded grace + clock-rollback resistance. Tradeoff: revocation is hard air-gapped → shorter terms + re-activation.
- Protecting weights (P11 Ch. 8–9): the strong tier — weights encrypted at rest, decrypted only inside an attested TEE / GPU confidential computing, gated on a valid attestation. Tradeoff: requires the hardware feature; self-checks/ obfuscation alone are only cost-raisers (state honestly).
- No phone-home (P07/P10 Ch. 9): offline everything — weights, license, telemetry, updates. Reshapes packaging and the update story.
- Heterogeneous customer hardware (P09): the HAL makes the platform portable to whatever they have — a key reason sovereign customers can deploy at all.
- Fixed capacity (P07 Ch. 9): no elastic burst → admission control and goodput discipline matter more.
4. Supply-chain & audit
- Supply-chain integrity: signed artifacts, reproducible builds, SBOMs — the customer trusts what you shipped without watching you build it.
- Auditability: tamper-evident logs of access and operations (regulated requirement); data-residency guarantees enforced in the serving layer.
- Updates: signed update bundles carried in out-of-band; verified offline like licenses.
5. The honest-claims discipline (P11 Ch. 1)
State exactly what each control guarantees and its limit:
- Signed license: economic + legal control; defeated by patching the verifier → hence anti-tamper + attestation.
- Attestation: proves what loaded, not bug-freeness; trust root must be sound.
- Confidential computing: protects weights/data from the host, gated by attestation. Never claim "uncrackable" — credibility is the product in this market.
6. Commercial framing (P12 Ch. 6)
This is a defensible, high-margin market a cloud-assuming competitor structurally cannot serve: a customer who needs "runs unmodified, only on licensed hardware, data never leaves the attested enclave, fully air-gapped, auditable" can only buy from a platform built for it. The same hardware-agnostic + security core also wins cost-conscious lock-in-averse customers. Security here is a market-access feature with direct revenue impact — which is why the Head of Engineering owns it alongside the HAL it protects.
The senior signal
You composed the HAL (portability), offline licensing + air-gap activation (P11), attestation + confidential computing for weights (the strong tier), and local-only ops (P10 Ch. 9) into a coherent sovereign architecture — and you stated the honest limits of each control. That honesty, plus the market-access commercial framing, is exactly the judgment the JD's sovereign-AI mandate demands.