« All Roles

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:

  1. GPU systems engineering — chips, SMs, warps, memory hierarchies, CUDA, drivers, compilers down to PTX/SASS.
  2. AI infrastructure — LLM serving, KV-cache management, continuous batching, multi-engine deployment (vLLM, TensorRT-LLM, SGLang).
  3. Distributed systems & HPC — collectives, NCCL, multi-node scheduling, failure domains.
  4. 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.
  5. 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 RequirementWhere It's Trained
GPU-accelerated workloads, CUDA, GPU driversPhases 01, 02, 03, 04
Runtime systems programmingPhases 03, 05, 09
GPU schedulingPhase 06
KV-cache management, LLM serving optimisationPhase 07
Distributed systems, HPCPhase 08
Decouple software from hardware environmentsPhase 09 (the keystone phase)
Linux internals, containers, observability toolingPhases 04, 10
Hardware fingerprinting, license enforcement, software protection, code obfuscationPhase 11
Sovereign AI, on-premise, air-gapped, regulated industriesPhases 07, 11
Ship through a full release cycle; lead teams; OEM/partner strategyPhases 10, 12
Python, C++, systems language (Rust/Go/C), JS/TS toolingLabs 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

WeekPhaseFocus
101GPU vs CPU design philosophy; SMs, warps, SIMT; build the SIMT simulator
201Memory hierarchy: HBM, L2, shared memory, registers; roofline microbenchmarks in C
302First CUDA kernels: grid/block/thread, vector add, error handling
402Tiled matmul in shared memory; coalescing and occupancy experiments
503The NVCC pipeline; reading PTX and SASS; Godbolt dissections
603Build a mini expression→PTX compiler; Triton and LLVM NVPTX overview
704Linux kernel modules; write a char device with ioctl command submission
804The real GPU driver stack (KMD/UMD); containers from scratch; GPU passthrough
905Memory allocators: buddy, slab, caching; build the Rust caching allocator
1005Streams, events, dependency graphs; build the C++ stream scheduler
1106GPU sharing: MPS vs MIG vs time-slicing; fractional GPUs
1206Cluster scheduling: bin-packing, gang scheduling; build the scheduler simulator
1307KV-cache memory math; build the paged KV allocator with prefix sharing
1407Continuous batching simulator; TTFT/TPOT/goodput; vLLM/TensorRT-LLM/SGLang ops
1508Collectives from first principles; implement ring all-reduce over sockets
1608NCCL, topology (NVLink/IB), multi-node training/serving failure modes
1709ABI vs API; plugin architectures; build the C HAL with dlopen backends
1809Portability strategy: CUDA/ROCm/Level Zero/Metal; capability negotiation
1910Observability: DCGM metrics, build the Prometheus exporter; eBPF tracing
2010SLOs, failure drills, release engineering: versioning, rollback, canary
2111Hardware fingerprinting: build the tool; threat-model the spoofing adversary
2211License enforcement: Ed25519-signed licenses, grace, air-gap activation
2311Software protection: obfuscation, anti-tamper, attestation; sovereign AI deployment
2412Leadership: org design, technical strategy memos, OEM/partner engineering
2512Capstone build: mini orchestration platform integration
2612Capstone release cycle: design doc, runbook, demo, retrospective

Phase Structure

Every phase follows a consistent format:

FilePurpose
README.mdWhy the phase exists, concepts list, lab tables (goal/steps/output/testing/talking points/resume bullet)
WARMUP.mdZero-to-expert primer: every concept from first principles, with lab walkthrough guidance, success criteria, interview Q&A, references
HITCHHIKERS-GUIDE.mdThe 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:

  1. CPU-only (most labs): simulators, allocators, schedulers, compilers-to-PTX (text output), licensing, HAL — need only a Linux/macOS box.
  2. Any-NVIDIA-GPU (Phase 02, parts of 03/10): a free Google Colab T4 or any consumer card works.
  3. 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
  • 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)

FieldValue
GoalBuild a warp-level SIMT interpreter that executes a tiny ISA in lockstep with active masks, then measure divergence cost.
ConceptsWarps, lockstep execution, active masks, divergence/reconvergence, why branchy code kills GPU throughput.
Steps1) 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.
StackPure Python, no dependencies
OutputWorking simulator + a divergence-cost table you produced and can explain.
How to Testpython solution.py — built-in asserts verify lockstep semantics and the 32× worst-case divergence measurement.
Talking PointsWhy 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."
ExtensionsAdd 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)

FieldValue
GoalMeasure your own machine's memory hierarchy with C microbenchmarks, then build a roofline model and place kernels on it.
ConceptsCache lines, latency vs bandwidth, pointer-chasing, streaming, arithmetic intensity, the roofline.
Steps1) 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.
StackC (C11), make, optionally perf
OutputA filled-in ROOFLINE.md worksheet with your measured numbers.
How to TestLatency plateaus match your CPU's published cache sizes within 2×; streaming bandwidth within 30% of spec.
Talking PointsWhy 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."
ExtensionsAdd 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
  • membench results 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

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:

  1. Lanes 0–15 want path A; lanes 16–31 want path B.
  2. Hardware executes A with mask 0x0000FFFF, then B with mask 0xFFFF0000.
  3. At the merge point the mask returns to 0xFFFFFFFFreconvergence (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):

LevelSizeLatencyBandwidthScope
Registers256 KB/SM0 cycper-thread
Shared mem / L1228 KB/SM (192)~30 cyc~20+ TB/s aggregateper-block
L250 MB (40)~200 cyc~10 TB/sdevice-wide
HBM3 (HBM2e)80 GB~500 cyc3.35 TB/s (2.0)device-wide
NVLink 4~µs900 GB/sinter-GPU
PCIe 5.0 ×16~µs64 GB/shost↔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.

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:

  1. Memory bandwidth and capacity first (inference fleets live and die here).
  2. Sustained vs peak: is the TFLOPs number "with sparsity"? At what clocks (boost vs sustained under power cap)? Dense BF16 is the honest comparable.
  3. Ridge point: compute it; compare against your workload's AI.
  4. Interconnect: peer-to-peer bandwidth and topology, not just "has NVLink."
  5. 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.
  6. Power/TCO: tokens per joule increasingly decides procurement.
  7. 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):

  1. Read solution.py top-down before running; predict what the divergence report will show.
  2. Run; check predictions. The built-in asserts verify lockstep semantics.
  3. 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):

  1. make && ./membench — then stare at the latency curve until you can point at L1, L2, L3, DRAM.
  2. Fill in ROOFLINE.md: your CPU's ridge point from measured bandwidth and estimated peak; then A100's and H100's from spec sheets.
  3. 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() / *_sync intrinsics.

3. Latency hiding beats caching

The numbers that make the design click:

CPU coreH100 SM
Strategypredict + cache + OoOoversubscribe warps
Threads resident2 (SMT)2,048 (64 warps)
Context switch~µs (OS)0 cycles (scheduler mux)
Where thread state livesmemory/caches256 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):

AccessCost intuition
Registerfree
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
WorkloadAI (FLOP/byte)H100 verdict (ridge ≈ 295)
vector add~0.08hopelessly bandwidth-bound
LLM decode, batch 1, FP16~1–2bandwidth-bound; 240 tok/s ceiling for 7B
LLM decode, batch 128~128–256approaching compute-bound — this is why continuous batching exists
LLM prefill / big GEMM100s–1000scompute-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.sync in 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:

  1. Dense or sparse TFLOPs? (2:4 sparsity footnote inflates 2×.)
  2. Sustained clocks under power cap, or boost?
  3. HBM bandwidth and capacity? (Inference fleets are bought on these.)
  4. P2P topology — all-to-all NVLink-class, or PCIe islands?
  5. 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).
  6. 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.md Ch. 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:

  1. Why does if (tid % 2) cost 2× but if (tid < 32) cost nothing (per warp)?
  2. 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:

InstrSemantics
SET rd, immrd = imm in every active lane
TID rdrd = lane_id (this is threadIdx.x % 32)
ADD/SUB/MUL rd, ra, rblane-wise arithmetic on active lanes
MOD rd, ra, immrd = ra % imm
LT rd, ra, rbrd = 1 if ra < rb else 0
BRZ ra, targetbranch if ra == 0the divergence point
JMP targetunconditional
RECONVreconvergence marker (pops the mask stack)
HALTwarp 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:

  1. Push (current_mask, reconvergence_pc) onto the mask stack.
  2. Run the fall-through path with the not-taken lanes' mask.
  3. At RECONV, switch to the taken path with the complementary mask.
  4. At RECONV again, 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:

KernelBranch patternPredicted costWhy
uniformno branch1.0×baseline
warp_uniformall lanes agree on the branch~1.0×uniform branch — no split
even_oddtid % 2~2× on the divergent regiontwo serialized half-warps
max_divergent32-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_odd shows 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_divergent utilization 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 real switch lowering on GPUs.

4. Extension exercises (do them — this is the lab)

  1. 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.)
  2. VOTE_ANY rd, ra: warp-cooperative primitive — rd = 1 in all active lanes if any active lane has ra != 0. This is __any_sync(). Then explain in a comment why warp votes need no memory traffic on real hardware.
  3. Predication: add SELP rd, ra, rb, rp (lane-wise select) and rewrite even_odd without branches. Compare issue counts — you've just reproduced the compiler's if-conversion decision (WARMUP Ch. 4).
  4. Latency hiding mini-model: simulate 4 resident warps where LD stalls 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

  1. Forgetting to count issues for instructions with few active lanes — the whole point is that issue cost is mask-independent.
  2. Reconverging at the branch target instead of the post-dominator — your even_odd will produce wrong values in half the lanes.
  3. Letting inactive lanes commit writes — masks must gate writes, not reads.
  4. Treating JMP inside a divergent region as a reconvergence — it isn't; only RECONV pops.

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.md Ch. 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):

  1. 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.
  2. 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 blocked matmul (AI grows with block size), converts to GFLOP/s, and prints where each lands relative to min(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:

  1. Identify each plateau and match it to your CPU's published cache sizes.
  2. Compute your CPU's ridge point from measured bandwidth + estimated peak (cores × clock × SIMD width × 2 FLOPs).
  3. Compute A100 and H100 ridge points from their spec sheets (WARMUP Ch. 7 has the answers — derive before checking).
  4. 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 cliffsshared mem ~30 cyc → L2 ~200 → HBM ~500 (Jia et al. measured exactly this way, in CUDA)
stream GB/sHBM bandwidth tests (bandwidthTest, nvbandwidth)
roofline placementNsight Compute's roofline chart (Phase 02 Lab 02 reads it)
prefetcher distorting naive latency testsGPU coalescer distorting naive bandwidth tests (Phase 02)

4. Common pitfalls

  1. 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!)
  2. Compiler deleting your loop — the chase result must feed a volatile sink or the optimizer removes everything. Same trick JMH uses (Blackhole), same trick you'll need in CUDA benchmarks.
  3. Timing one pass — too noisy; the harness does warmup + multiple reps and reports the minimum (least-disturbed) run.
  4. Forgetting -O2 — at -O0 you'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 0 vs a core on another socket/cluster (Linux) and observe NUMA / cluster effects.
  • Use perf stat -e cache-misses,cache-references ./membench and 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 nvcc produces (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, the CUDA_CHECK macro
  • 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, cudaEventElapsedTime for 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++)

FieldValue
GoalGo from hello kernel to a shared-memory tiled matmul that beats the naive version ~5–10×, verified against CPU reference.
ConceptsLaunch config math, error checking, coalescing, shared-memory tiling, __syncthreads(), honest benchmarking with events.
Steps1) 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).
StackCUDA C++ (nvcc), works on any sm_70+ GPU; Colab notebook instructions included
OutputWorking binary + a timing table: naive vs tiled vs (extension) cuBLAS.
How to TestBuilt-in: every kernel's output is compared elementwise against a CPU reference; speedup asserts print PASS/FAIL.
Talking PointsWhy 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."
ExtensionsRegister 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++)

FieldValue
GoalMeasure the cost of uncoalesced access and bank conflicts; read Nsight Compute output like a platform engineer.
Concepts32-byte sectors, strided/transposed access, AoS vs SoA, shared-memory banks + padding fix, occupancy limiters.
Steps1) 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.
StackCUDA C++, Nsight Compute CLI (ncu) — Colab-compatible commands provided
OutputBandwidth/conflict tables + a filled PROFILE.md naming each kernel's limiter with evidence.
How to TestStride-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."
ExtensionsVectorized 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.md filled: each kernel's limiter named with ncu evidence
  • You can write the CUDA_CHECK macro 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

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:

  1. 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.
  2. 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 — typically illegal memory access reported 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:

  1. ExplicitcudaMalloc + cudaMemcpy. Predictable, fast, verbose. The production default. cudaMemcpy is synchronous-ish; the async variant requires tier 2.
  2. Pinned host memorycudaMallocHost. Page-locked so the GPU's DMA engine can read it directly: ~2× faster transfers and the prerequisite for cudaMemcpyAsync actually 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.
  3. Unified memorycudaMallocManaged. 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 as Unified Memory rows in Nsight Systems). cudaMemPrefetchAsync is the remedy when you must use it.
  4. Stream-ordered poolscudaMallocAsync/cudaFreeAsync. Allocation as a stream operation from a driver-managed pool — because raw cudaMalloc/Free synchronize 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:

  1. Adjacent threads ↔ adjacent addresses. a[blockIdx.x*blockDim.x + threadIdx.x] is perfect; a[threadIdx.x * pitch] is the anti-pattern.
  2. SoA beats AoS. struct {float x,y,z;} pts[n] makes pts[i].x loads stride-12; three arrays x[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.
  3. 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:

  1. Time with events (cudaEventRecord/cudaEventElapsedTime), or wall-clock only around an explicit cudaDeviceSynchronize(). Timing an async launch with std::chrono measures the enqueue, not the kernel.
  2. Warm up first (first launch includes module load/JIT — Phase 03 explains the JIT path).
  3. 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:

  1. Atomic per element: atomicAdd(&out, a[i]) — correct, serializes on one address, terrible.
  2. 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).
  3. 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.
  4. (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 or nsys 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):

  1. nsys: GPU mostly idle? → it's a pipeline problem (launch overhead, sync points, transfers) — no kernel tuning will help.
  2. GPU busy → ncu the top kernel: SOL Memory% vs SOL Compute% tells you which roof you're under (Phase 01's roofline, now measured for you).
  3. 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):

  1. Build and run first (make && ./kernels); confirm all PASS lines, then read kernels.cu against Chapters 2–4 and 7.
  2. Work the verification path: break the tail guard deliberately, see the corruption, fix it, then break a __syncthreads() and see the race.
  3. Extensions in order: rectangular tiles → register micro-tiles → cuBLAS comparison (expect cuBLAS 2–5× ahead; explain why using Ch. 7's last paragraph).
  4. No local GPU: the README's Colab cell runs everything; nvcc flags identical.

Lab 02 (coalescing & profiler):

  1. Run the benchmark and fill the tables before profiling — predict, then verify with ncu (prediction-first is the habit that makes profiling stick).
  2. The transpose ladder (naive → tiled → padded) is the heart: tie each step to Ch. 6/7 mechanisms in one sentence each in PROFILE.md.
  3. 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_CHECK discipline 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=-v output (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+ncu pair, 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

  1. Sync copies in hot loops (PCIe is 1/50th of HBM): batch, pin, overlap.
  2. Uncoalesced layouts: AoS structs, column walks of row-major data, threadIdx.y-fastest indexing. Fix = SoA + adjacent-thread-adjacent-address. ncu smoking gun: sectors/request ≈ 32 (want ~4).
  3. Unified memory in serving paths: page-fault storms. Managed memory is a prototyping tool; production transfers are explicit.
  4. Raw cudaMalloc/Free per request: device-synchronizing and fragmenting — use cudaMallocAsync pools or a caching allocator (you'll build one in Phase 05; PyTorch's is the reference design).
  5. Forgetting pinned memory for async: cudaMemcpyAsync from 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

ThingNumber
Kernel launch overhead~5–10 µs (why CUDA Graphs exist)
Max threads/block1,024 (use 128–256)
Smem/SM (Ampere/Hopper)100 / 228 KB configurable
Registers/SM64K × 32-bit; >64/thread starts hurting occupancy
PCIe 5 ×16 vs HBM364 GB/s vs 3,350 GB/s
Warp size32 — 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"-maxrregcount caused 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, ncu before/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.md Ch. 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:

  1. vec_add — the programming model + error handling + the tail guard.
  2. matmul_naive — one thread per output element, all reads from global memory.
  3. 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

  1. CUDA_CHECK and check_close — the discipline. Note where cudaGetLastError is called (right after launch — catches config errors) vs cudaDeviceSynchronize (catches the kernel's own faults).
  2. vec_add — the index formula and tail guard. Exercise: delete the if (i < n) guard, run with n=1<<20+3, observe either corruption or a sanitizer report (compute-sanitizer ./kernels). Put it back.
  3. 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).
  4. 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.
  5. The timing harness — events, warmup, median-of-5. Compare against the naive std::chrono around 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

  1. Rectangular blocking: 64×16 tiles; measure. Why might non-square help? (Hint: B's tile loads per output column.)
  2. 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.
  3. cuBLAS comparison: link -lcublas, time cublasSgemm, report your % of it. >30% with micro-tiles is respectable; explain the rest.
  4. __launch_bounds__(256, 4) on the tiled kernel: check -Xptxas=-v register count before/after, occupancy, and time. Did the compiler spill?

4. Common pitfalls

  1. Host pointer passed to a kernel — illegal access at the next sync, blamed on the wrong line (CUDA_LAUNCH_BLOCKING=1 to localize).
  2. Verifying with == on floats — reduction/FMA order differs from CPU; use relative tolerance (the harness uses 1e-3 relative).
  3. Timing the enqueue, not the kernel (chrono without sync) — the harness prints both so you see the discrepancy once and never trust chrono again.
  4. -arch mismatch — sm_90 binary on a T4 dies at load; -arch=native locally, explicit ARCH= 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.md Ch. 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:

  1. Stride sweep: copy bandwidth at stride 1, 2, 4, 8, 16, 32 — watch coalescing die.
  2. AoS vs SoA: load one field from a 3-float struct array vs a flat array.
  3. 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].x drags .y and .z through the memory system as sector ballast. SoA puts 32 consecutive .x in 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

  • float4 vectorized 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=-v and 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

  1. Comparing kernels at different clocks (thermal throttle mid-run): lock with nvidia-smi -lgc where permitted, or interleave reps.
  2. Profiling with --set full on a long benchmark — replay overhead makes it crawl; use basic + targeted metrics.
  3. Concluding "memory-bound" from low compute% alone — check both SOL numbers; both-low means latency-bound, a different fix.
  4. Forgetting that ncu serializes kernels — overlap effects you saw in nsys vanish under ncu; 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 nvcc pipeline end-to-end: host/device split → C++ frontend → NVVM IR (LLVM) → ptxas → SASS; what cudafe++, cicc, ptxas, fatbinary each do
  • PTX: a virtual ISA — typed registers (infinite), explicit state spaces (.global, .shared, .param, .reg), predication, the %tid/%ctaid special 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_XX vs sm_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)

FieldValue
GoalCompile small kernels and read what comes out: PTX first, then SASS; observe predication, unrolling, FMA contraction, and register pressure.
ConceptsThe nvcc pipeline, PTX syntax, SASS reading basics, -Xptxas=-v output, fatbins.
Steps1) 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.
StackCUDA toolkit (nvcc, cuobjdump, nvdisasm) — or godbolt.org with CUDA selected, zero install
OutputAn annotated saxpy.ptx (every line explained) + a findings sheet for the other three kernels.
How to TestYour annotations are checked against the provided answer key (saxpy-annotated.md).
Talking PointsWhy 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."
ExtensionsCompare 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)

FieldValue
GoalBuild 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.
ConceptsLexing, recursive-descent parsing, AST, register allocation (virtual → PTX .reg), the PTX kernel ABI (.param space, cvta, %tid math, bounds guard), codegen correctness.
Steps1) 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: LexerParserTypeCheckerCodeGenPTXInterp. 3) Extensions: add min/max intrinsics, constant folding, common-subexpression elimination, then a fused relu(a*b+c) kernel.
StackPure Python (stdlib). Optional final step: load your PTX on a real GPU via cuda-python/PyCUDA and confirm the interpreter told the truth.
OutputA working expr → PTX compiler whose output is legal PTX (loads on a real GPU) and passes the built-in differential tests.
How to Testpython solution.py — every kernel's interpreter output must match the Python reference elementwise; PTX text is also syntax-checked against the grammar subset.
Talking PointsWhat 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."
ExtensionsConstant 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.ptx matching 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

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

  1. Lexing: characters → tokens. c[i] = a[i] * 2.0 becomes IDENT(c) LBRACK IDENT(i) RBRACK ASSIGN IDENT(a) ... NUM(2.0).
  2. 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 +.
  3. Semantic analysis / type checking: annotate the AST with types (f32, i32), reject nonsense, decide where conversions go.
  4. 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).
  5. 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=-v from 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. The cvta.to.global instruction 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.xthreadIdx, 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):

  1. Arguments arrive via ld.param — the kernel ABI. Your Lab 02 compiler emits this exact prologue.
  2. cvta.to.global — the state-space conversion; pointers from the host are "generic" until proven global.
  3. The index computation is a single mad — multiply-add, integer flavor.
  4. 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).
  5. fma.rn.f32 — the compiler contracted a*x + y into 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 behind cp.async), wider loads LDG.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 + cfma.rn — fewer instructions, different rounding (one rounding instead of two). -fmad=false disables 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 unroll or 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 --cuda uses; 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.load a 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:

  1. 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.
  2. 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."
  3. 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.
  4. 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):

  1. Annotate saxpy.ptx line-by-line using Ch. 5 as the rubric, before opening the answer key.
  2. SASS pass: find one predicated instruction, one FFMA, the unrolled loop body. With -maxrregcount 16: find LDL/STL spills.
  3. Fatbin dissection: cuobjdump --list-elf on a two-arch build; relate every entry to Ch. 7's load-time decision tree.
  4. 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):

  1. Run python solution.py first — see the five kernels compile and the differential tests pass; read one emitted PTX fully.
  2. 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.
  3. Extensions in order: intrinsics (touch every stage once) → constant folding (your first IR pass) → CSE (your second; measure both) → the fused kernel.
  4. If you have any NVIDIA GPU: the README's cuda-python snippet 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 nvcc is 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

CommandWhat you learn
nvcc -ptx x.cuthe PTX — read it with WARMUP Ch. 5's rubric
nvcc -dryrun x.cuthe actual pipeline, tool by tool
nvcc -Xptxas=-vregisters, smem, spills per kernel (Phase 02 link)
cuobjdump --dump-sass xthe real ISA: spills=LDL/STL, TensorCores=HMMA, async copy=LDGSTS
cuobjdump --list-elf xwhat's in the fatbin (which sm_XX, is PTX embedded?)
nvdisasm -c x.cubinSASS with control-flow graph
godbolt.org CUDAall of the above, zero install

2. PTX reading card

  • %tid.x %ctaid.x %ntid.x = threadIdx / blockIdx / blockDim
  • mad.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; @%p on 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=false in numerical disputes
  • Infinite %f/%r/%rd registers — 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_XX PTX 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, watch CUDA_CACHE_MAXSIZE on multi-model fleets.
  • Numerical policy: FMA contraction + reduction order = CPU/GPU diffs in the last ulp; tests compare with tolerance, never == (Phase 02), and -fmad changes 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 STL in 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.md Ch. 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:

  1. saxpy — annotate every PTX line. Template in saxpy-annotation-template.md; answer key in saxpy-annotated.md. Do not peek early.
  2. guarded — a small if body: find where the branch disappeared in SASS (if-conversion → @%p-predicated stores / SEL).
  3. unrolled#pragma unroll 8 loop: count the replicated bodies in SASS; note the register count change in -Xptxas=-v.
  4. spiller — a kernel with a large per-thread array. Compile normally, then with -maxrregcount=16: find the LDL/STL local-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)

KernelFlag setupWhat I found (instruction + line)Mechanism (one sentence)
guarded-O3
unrolled-O3
spillerdefault
spiller-maxrregcount=16
fat.o3 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 hipcc for 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 wmma example and find HMMA in 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.md Ch. 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)

StageClassIn → OutThe classic name
1Lexersource chars → tokenslexical analysis
2Parsertokens → AST (Kernel, Assign, BinOp, Load, Var, Num)recursive descent, precedence climbing
3TypeCheckerAST → typed AST (every node .ty ∈ {f32, ptr})semantic analysis
4CodeGenAST → PTX text (virtual %f/%r/%rd registers, FMA contraction)instruction selection
5PTXInterpPTX 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)

  1. Intrinsics: add min(x, y) / max(x, y)min.f32/max.f32. Touches every stage once — the perfect first change.
  2. Constant folding: a pass over the AST: 2.0 * 4.0 + x8.0 + x. Assert emitted instruction count drops.
  3. CSE: (a[i]+b[i]) * (a[i]+b[i]) currently loads and adds twice. Hash subtrees, reuse registers. Measure again.
  4. relu(a[i] * b[i] + c[i]) — fused elementwise kernel via max + folding: you have just built, in miniature, what fusion compilers (XLA, Inductor) do at scale.
  5. Real hardware (optional, any NVIDIA GPU):
    from cuda import cuda  # pip install cuda-python
    # cuLinkCreate/cuModuleLoadDataEx accepts PTX text directly:
    err, mod = cuda.cuModuleLoadData(ptx_text.encode())
    
    Launch with cuLaunchKernel and compare against the interpreter. When the driver accepts your compiler's output, you've closed the loop this phase is about.
  6. Second backend (Phase 09 foreshadowing): emit C source from the same AST (a for loop over i). Notice what's shared (front/middle end) and what isn't (ABI, index model) — that boundary is the HAL design problem.

4. Common pitfalls

  1. Float literals as decimals — PTX wants 0f<bits> form for exactness; emitting 2.0 works in some toolchains and silently loses precision in others. The provided f32_hex() does it right.
  2. Reusing the byte-offset register per operand — compute i*4 once; recomputing it per load is correct but bloats the kernel (and your CSE pass should catch it if you regress).
  3. Forgetting the guard — the interpreter intentionally runs n not 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.)
  4. f32 rounding in the oracle — the interpreter rounds every op to float32 (struct.pack round-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, /dev nodes, 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_DEVICES vs cgroup devices allow-lists
  • /proc, /sys, strace, lsof, dmesg — the debugging toolkit

Labs

Lab 01 — A GPU-Shaped Character Device Driver (C, Linux kernel module)

FieldValue
GoalWrite, 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.
Conceptsfile_operations, copy_to_user/copy_from_user, ioctl ABI design, per-fd state (the "GPU context"!), kernel logging, module lifecycle.
Steps1) 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.
StackC, Linux kernel headers (linux-headers-$(uname -r)), any Linux VM
OutputLoadable .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 PointsWhy 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."
Extensionsmmap 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)

FieldValue
GoalBuild a minimal container runtime in C — namespaces, pivot_root, cgroup v2 limits — then dissect how real runtimes inject GPUs.
Conceptsclone(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).
Steps1) 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.
StackC (clone/unshare/mounts), cgroup v2 sysfs, your Lab 01 device
OutputWorking mini-runtime + the GPU-passthrough explainer (your team-training artifact).
How to TestInside 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."
ExtensionsAdd network namespace + veth pair; user-namespace UID mapping (rootless containers); compare your runtime's strace against runc's.

Deliverables Checklist

  • fakegpu.ko loads; client passes all asserts; you can explain every file_operations callback
  • 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_DEVICES provide 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 cudaLaunchKernel and silicon is glass, not fog.

Table of Contents


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/nvidia0 vs /dev/nvidia1). ls -l /dev/nvidia* shows them (major 195 for NVIDIA, historically).
  • /dev nodes are created by udev at hotplug (or mknod by hand, or devtmpfs). 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.ko for unified memory, nvidia-modeset.ko for 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:

  1. 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).
  2. To launch a kernel, libcuda.so writes the command into the ring (a memory write!) and writes the doorbell (another memory write, which the hardware sees) — zero syscalls.
  3. 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 (cudaMallocHostget_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=off make 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:

NamespaceIsolatesDemo
mountfilesystem treedifferent / via pivot_root
PIDprocess IDsyour shell is PID 1; can't see host
netinterfaces, portsempty ip a until you add a veth
UTShost/domain namehostname container1
IPCSysV/POSIX queues
userUID mappingsroot inside = UID 100000 outside (rootless)
cgroupcgroup 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:

  1. The device nodes: /dev/nvidia0, /dev/nvidiactl, /dev/nvidia-uvm visible in its mount namespace (bind-mount or mknod), and allowed by the devices cgroup/eBPF program.
  2. The user-mode driver libraries: libcuda.so matching 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.
  3. 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):

  1. Read fakegpu.c top-down against Ch. 2–3 before building; predict what dmesg will print at insmod.
  2. Build/load (any Linux VM; headers via apt install linux-headers-$(uname -r)). Run ./client; all asserts PASS.
  3. strace ./client → annotate against the chapter-3 CUDA trace. Two columns, same shape — that's the lab's thesis made visible.
  4. Extensions in order: quotas (validation discipline) → timer-based async completion (interrupt/fence flavor) → mmap'd completion ring (the real pattern; hardest).
  5. 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):

  1. Run first, read after: sudo ./mininer run /bin/sh, poke around (ps, hostname, ls /), exit, then read mininer.c against Ch. 7.
  2. cgroup demo: apply memory.max, run the included hog, watch the group OOM while the host stays healthy.
  3. The synthesis step: inject /dev/fakegpu into 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

LayerOwnsUpdates withBreaks as
KMD nvidia.koGPU page tables, channels, IRQs, Xid recoverynode image (reboot)module won't load (DKMS/secure-boot), Xids
UMD libcuda.sorings, contexts, PTX JIT, APIdriver package (must match KMD)"driver insufficient for runtime"
toolkit/runtimeinjecting nodes+libs into containershost packagecontainer sees no GPU
app's CUDA runtimeconvenience APIcontainer imageversion 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_DEVICES is 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)

SymptomLikely cause
driver insufficient for runtime versiontoolkit newer than driver — upgrade driver or use fwd-compat package
works on node A, not node Bfleet driver skew — inventory + pin in IaC
module won't load after kernel updateDKMS rebuild failed or unsigned module + secure boot
Xid 79 in dmesgGPU fell off the bus — hardware/power/PCIe; drain the node
container: nodes present, init failsinjected lib missing/mismatched — check ldconfig -p vs host
can't rmmod nvidiasomething holds the fd — lsof /dev/nvidia*
pinned-memory alloc fails at scaleunpageable 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.max from a parent cgroup nobody remembered. cat /proc/self/cgroup first, 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.md Ch. 1–4. ⚠️ Kernel code runs with no safety net — use a VM (Lima/Multipass/UTM/WSL2/cloud), snapshot before insmod. 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 conceptReal GPU equivalent
per-open() context structCUDA context (per-process GPU state)
ALLOC/FREE ioctls → handlescudaMalloc → device pointers (via KMD page tables)
SUBMIT ioctl → job queuecommand-buffer submission (real HW: mapped ring + doorbell)
WAIT ioctl → completed seqnofence wait (cudaEventSynchronize)
per-context byte quotaper-context VRAM accounting
INFO ioctlcudaGetDeviceProperties

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)

  1. The ABI header (fakegpu_ioctl.h) — shared by kernel and client; note _IOWR encodings and fixed-size structs. ABI files are forever; this one is written like it.
  2. fakegpu_open/release — per-fd context allocation. This is the pattern: everything else hangs off file->private_data.
  3. 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).
  4. The job modelSUBMIT enqueues and immediately completes jobs (incrementing completed_seq). Real hardware completes asynchronously; the extension adds a timer_list to 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

  1. Quota tightening: per-context and global device quota; correct errno for each (ENOSPC vs ENOMEM). Add a /sys/module/fakegpu/ or sysfs stats attribute (your Phase 10 metrics on-ramp).
  2. Async completion: a timer_list completes jobs 10 ms after submit; WAIT blocks on a wait_queue_head_t until completed_seq >= target. You've just implemented a fence.
  3. The mmap ring (hard, the real pattern): fakegpu_mmap exposes a page containing completed_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).
  4. 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

  1. Dereferencing the user pointer instead of copy_from_user — works in your VM (same address space mapping luck), then oopses elsewhere. The compiler __user annotation + sparse exists for this.
  2. Forgetting mutex_lock around context state — two threads on one fd corrupt the buffer list; the racing-SUBMIT test in extension 4 catches it.
  3. Leaking contexts on releasermmod then dmesg shows the leak warning the module prints; the kernel's own kmemleak would find it in real review.
  4. Building against the wrong headers (uname -r mismatch 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.md Ch. 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:

  1. clone(CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS | SIGCHLD) — new hostname, PID, and mount namespaces for the child.
  2. 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 use pivot_root, which unmounts the old root rather than hiding it — the extension asks you to upgrade); mount a fresh /proc (so ps sees only the namespace).
  3. (run-limited) create /sys/fs/cgroup/mininer, write memory.max, add the child PID to cgroup.procs.
  4. execv the 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)

CommandWhat you should seeWhich mechanism
ps (busybox reads /proc)PIDs 1–2 onlyPID ns + fresh /proc mount
hostnameminiboxUTS ns
ls /busybox rootfs, not your host /mount ns + chroot
/hog 200 under run-limited 100"Killed" near 100 MB; host unaffectedcgroup v2 memory.max
echo $$1 — you are init; mind your signalsPID 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

  1. pivot_root upgrade: replace chroot with pivot_root + umount2(old, MNT_DETACH); explain in a comment why chroot alone is escapable (classic chroot jailbreak: keep an fd to the outside, fchdir).
  2. Devices cgroup: attach a cgroup-v2 eBPF device filter (or use v1 devices.allow if available) permitting only /dev/fakegpu and /dev/null; demonstrate mknod + open of anything else failing.
  3. Network namespace: CLONE_NEWNET + a veth pair (ip link add ... netns) — ping from container to host.
  4. User namespace: CLONE_NEWUSER with 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).
  5. Compare with runc: strace -f -e unshare,clone,mount,pivot_root runc run on a real container; diff against strace -f ./mininer run.

4. Common pitfalls

  1. Forgetting MS_PRIVATE — your container's mounts propagate to the host (systemd makes / shared by default); the host's /proc gets shadowed. Symptom: weird host ps after exit. The MS_REC|MS_PRIVATE remount is non-negotiable.
  2. Mounting /proc before the PID namespace existsps shows host processes; the fresh /proc must be mounted by the child, after clone.
  3. cgroup v1 vs v2 confusion — this lab assumes v2 (/sys/fs/cgroup with memory.max). Check mount | grep cgroup2; hybrid hosts need the v2 path.
  4. 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/cudaFree are 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)

FieldValue
GoalImplement the PyTorch-style caching allocator: size bins, splitting, stream-tagged free lists — and demonstrate (then fix) fragmentation OOM.
ConceptsFree lists, binning, splitting/coalescing, stream-aware reuse, fragmentation metrics.
Steps1) 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.
StackRust (no unsafe, no external crates)
OutputA library with 12+ passing tests + the fragmentation demo with its utilization table.
How to Testcargo test: correctness (no double-handout, splitting math, stream safety) + the fragdemo's OOM-point comparison.
Talking PointsWhy 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."
ExtensionsBest-fit vs first-fit measurement; expandable-segments mode; export Prometheus-style metrics (Phase 10 tie-in).

Lab 02 — Stream & Event Scheduler (C++)

FieldValue
GoalBuild 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.
ConceptsOrdered queues, events/fences, DAG scheduling, stream-ordered deallocation, deadlock from circular waits.
Steps1) 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.
StackC++17, std::thread (no external deps)
OutputA header-lib + scenario suite proving overlap (~2× on the pipeline test) and lifetime safety.
How to TestBuilt-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 PointsWhy "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."
ExtensionsCapture/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

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:

  1. cudaFree synchronizes the device (historically; and cudaMalloc can too). The driver must be sure no in-flight kernel still uses the region — lacking fine-grained tracking, it waits for everything. One stray cudaFree per request = your entire pipeline gains a global barrier. → Consequence: never call it in steady state; allocate from a pool.
  2. 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.
  3. 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-smi shows "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):

  1. Day 0: requests of mixed sizes allocate/free happily; memory is a clean checkerboard.
  2. Day 2: long-lived allocations (a resident model, a stuck request's KV) sit between short-lived ones. Frees leave holes of assorted sizes.
  3. 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/cudaDeviceSynchronize join 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):

  1. cargo test green first; then read src/lib.rs with Ch. 3–4 open. The types are small: Block, BinKey, CachingAllocator.
  2. 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_accounting checks your understanding.
  3. Run cargo run --release --bin fragdemo: the naive-vs-caching utilization table is the artifact. Explain the gap using Ch. 5's checkerboard story.
  4. 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++):

  1. make && ./scheduler_test; then read scheduler.hpp with Ch. 6–7 open — Stream is a worker+FIFO, Event is a counter+condvar, that's it.
  2. 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.
  3. 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_graph usage 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 why nvidia-smi and 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

ThingNumber
pool hit~100 ns
cudaMalloc~10–50 µs (driver round trip)
cudaFreedevice sync — unbounded
kernel launch (CPU cost)~5–10 µs — why Graphs exist
decode step kernel counthundreds — × 8 µs = CPU-bound issuance
pinned allocms-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.md Ch. 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 is cudaMalloc/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)

  1. round_size + the Device model — the constraints (first-fit, coalescing, counted frees).
  2. CachingAllocator::alloc — the four-step path: bin hit → split larger → driver malloc → flush-and-retry. Each step maps to a WARMUP Ch. 4 bullet.
  3. free (goes to a bin, never the driver) and flush_cache (the empty_cache() semantics, including why a split block isn't returnable).
  4. 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

TestThe lesson it locks in
test_roundingsize classes (Ch. 4)
test_device_first_fit_and_coalescethe driver model's behavior
test_bin_reuse_fast_pathreuse never calls the driver (free_calls == 0)
test_split_accountingsplit math: 4MB → 1MB out + 3MB binned
test_cross_stream_sync_countedcross-stream reuse needs an event edge (Ch. 7)
test_same_stream_preferredsame-stream blocks chosen first (no edge)
test_reserved_vs_allocatedthe nvidia-smi-vs-framework gap (Ch. 4, Q2)
test_flush_and_retry_on_oomthe pre-OOM stall (Ch. 4)
test_split_block_not_returnablecan't return half a device segment

4. Extensions

  1. 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.
  2. empty_cache policy: return only segments idle for N cycles; measure the reserved-memory sawtooth.
  3. 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.
  4. Best-fit vs first-fit in the bins: measure fragmentation on the demo-1 workload; quantify the classic tradeoff.
  5. Prometheus metrics: export reserved/allocated/cached/largest-block — the Phase 10 fragmentation dashboard, fed from here.

5. Common pitfalls

  1. Returning a split remainder to the driver — you can't free half a segment (test_split_block_not_returnable enforces it; PyTorch has the same limit).
  2. 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.
  3. 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.md Ch. 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 stream s block until the event fires. Events are the edges of the dependency DAG (WARMUP Ch. 6).
  • Scheduler owns 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)

ScenarioDemonstratesPass condition
1. Pipeline overlapH2D copy ∥ compute ∥ D2H across two streams with event edges≥1.7× faster than serial
2. Default-stream serializationthe legacy default stream as a global barrier"concurrent" ops actually serialize; measured
3. Stream-ordered lifetimeuse-after-free when free is host-ordered; safety when it's stream-orderedunsafe 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)

  1. Stream — worker loop pulling from a std::queue under a mutex/condvar. In-order execution falls out of the FIFO for free.
  2. Eventrecord enqueues a counter bump onto a stream; stream_wait enqueues a blocking wait onto another stream. This is the whole ordering algebra.
  3. Scheduler::synchronize — the host joining the DAG (the blunt cudaDeviceSynchronize).
  4. The scenarios in scheduler_test.cpp — predict each result from the DAG before reading the assert.

4. Extensions

  1. Capture/replay ("mini CUDA Graphs"): record a scenario-1 sequence into a Graph object, 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.
  2. Priority streams: a high-priority stream's ops jump the worker queue; show a latency-sensitive op finishing ahead of bulk work.
  3. Deadlock detector: build the wait-for graph from event edges and reject cycles at issue time (scenario 4 stubs this).
  4. 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

  1. Data race in the test harness itself — the "corruption" in scenario 3 must come from the modeled lifetime bug, not from unsynchronized std::thread access in your scaffolding. The provided harness uses a deterministic interleaving hook; keep it.
  2. Busy-waiting instead of condvars — works but pegs a core and hides ordering bugs; use the condition variables as written.
  3. Joining workers in the wrong order at shutdown — a classic; the destructor drains queues then joins.
  4. 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)

FieldValue
GoalSimulate a GPU cluster and implement/compare placement policies; measure utilization, fragmentation (stranded GPUs), queue wait, and gang-scheduling deadlock avoidance.
ConceptsBin-packing (first/best-fit), gang scheduling, topology-aware placement, fragmentation metrics, fairness.
Steps1) 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.
StackPure Python (stdlib)
OutputA simulator + a policy comparison table (utilization, p50/p99 wait, stranded-GPU count, deadlocks).
How to TestBuilt-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 PointsWhy 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."
ExtensionsPriority 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)

FieldValue
GoalImplement 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.
ConceptsDevice plugin API (ListAndWatch, Allocate), extended resources, how the kubelet/scheduler see GPUs, time-slicing as replica advertisement, MIG profiles as distinct resources.
Steps1) 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).
StackGo (stdlib + a vendored device-plugin proto subset)
OutputA working device plugin + tests proving registration, listing, allocation, and fractional advertisement.
How to Testgo 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 PointsWhy 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."
ExtensionsMIG 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

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.10gb cannot 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):

NeedMechanism
Trusted small jobs, max utilization, no SLOMPS
Multi-tenant, isolation is contractual, predictable QoSMIG
Bursty dev/notebooks, oversubscription, cost over isolationTime-slicing
One big job using the whole GPUNone (dedicated)
Latency-critical serving + isolationMIG (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):

  1. 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.
  2. ListAndWatch: the plugin streams the list of healthy device IDs to the kubelet; the node now advertises nvidia.com/gpu: 8 as an extended resource.
  3. The scheduler treats nvidia.com/gpu like CPU/memory for placement (pods request it in resources.limits).
  4. When a pod lands, the kubelet calls the plugin's Allocate with 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):

  1. python solution.py; read the policy comparison table, then the scheduler core against Ch. 6–7.
  2. Find the stranded-GPU result in first-fit vs best-fit — explain it with the Ch. 6 fragmentation story.
  3. Watch the gang test: with naive allocation two big jobs deadlock; with gang scheduling neither does. Connect to Ch. 7.
  4. Extensions: preemption (with checkpoint cost), DRF, topology islands.

Lab 02 (device plugin, Go):

  1. go test ./... — the mock kubelet drives registration→list→allocate.
  2. Read plugin.go against Ch. 8; trace one pod's allocation end to end.
  3. 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.
  4. 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

MechanismIsolationUtilizationUse when
MPSweak (shared context/fault)high for small coop jobstrusted microservices, MPI ranks
MIGstrong (HW partition)medium (fixed profiles, quantization waste)multi-tenant, SLO, regulated
Time-slicingnone (shared mem+faults)high (overcommit)dev/notebooks, bursty, cheap
Dedicatedtotallowone 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

ThingValue
MIG max instances (A100/H100)7
MIG smallest profile1g.10gb (1/7 compute, 10 GB)
NVLink vs IB (placement stakes)~900 vs ~50 GB/s
GPU preemption costcheckpoint (GBs) or kill — ≫ CPU preempt
Idle GPU in a typical single-model serving70%+ (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.md Ch. 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)

  1. Cluster / Node — the resource model and free_gpus, stranded_gpus.
  2. place_* policy functions — each takes (cluster, job) → placement or None.
  3. The gang logic: naive reserves GPUs as it finds them (can deadlock); gang only commits if the whole job fits atomically.
  4. The event loop and metrics.

3. Extensions

  1. 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.
  2. DRF fairness: jobs request GPUs and CPU; schedule by dominant share; prevent one tenant monopolizing.
  3. 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.
  4. Backfill: let small jobs run while a big gang waits for its reservation; measure the utilization recovery.

4. Common pitfalls

  1. Counting "free GPUs" as "usable GPUs" — the whole stranded-GPU lesson is that they differ once jobs have co-location constraints.
  2. Releasing GPUs in the wrong event order (release before the job's end event) — corrupts utilization accounting.
  3. 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.md Ch. 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):

OperationReal APIWhat it does
RegisterRegistration.Registertell the kubelet the resource name
ListAndWatchstreams Device[]advertise healthy device IDs (→ node capacity)
AllocateAllocateResponsereturn 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

  1. device.go — the Device, Plugin interface, and the three modes (whole / timeslice / mig) that change only what gets advertised.
  2. plugin.goListAndWatch (build the advertised list) and Allocate (pick devices, build the injection response with NVIDIA_VISIBLE_DEVICES).
  3. kubelet_mock.go — the mock kubelet: registers the plugin, reads the device list, and requests an allocation, exactly as the real kubelet does.
  4. plugin_test.go — the contract tests.

3. Extensions

  1. 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 a 3g consumes capacity the 1gs would have used) — the real MIG accounting problem.
  2. Health transitions: mark a device Unhealthy (model an Xid error from Phase 04/10); ListAndWatch must re-advertise and the kubelet must stop scheduling onto it.
  3. gpu-feature-discovery: emit node labels (nvidia.com/gpu.product=H100, nvidia.com/gpu.memory=80) so the scheduler can do affinity.
  4. Preferred allocation: implement GetPreferredAllocation to pick devices that share NVLink (topology — Phase 08) for multi-GPU requests.

4. Common pitfalls

  1. 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_DEVICES value (proof they share hardware).
  2. Forgetting the env/mount injection — without it the container sees the resource accounted but can't actually reach the GPU (Phase 04 Ch. 9).
  3. Letting Allocate hand 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)

FieldValue
GoalImplement 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.
ConceptsBlock tables, fixed-size paging, internal vs external fragmentation, copy-on-write prefix sharing, KV-cache memory math.
Steps1) 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.
StackPython (stdlib)
OutputA block allocator + tests + a memory-comparison table (naive vs paged vs paged+prefix).
How to TestBuilt-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 PointsWhy 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."
ExtensionsLRU 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)

FieldValue
GoalSimulate 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.
ConceptsStatic vs continuous batching, iteration scheduling, admission control, the latency-throughput dial, goodput, prefill interference + chunked prefill.
Steps1) 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.
StackPython (stdlib)
OutputA serving simulator + a metrics table (static vs continuous; goodput vs batch depth).
How to TestBuilt-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 PointsWhy 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."
ExtensionsChunked 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

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

  1. python solution.py; read the allocator against Ch. 3–4 — BlockAllocator, BlockTable, the COW logic.
  2. Reproduce the memory comparison: naive contiguous vs paged vs paged+prefix. Tie each number to Ch. 2–3 (internal/external fragmentation eliminated).
  3. The prefix-sharing demo: N sequences sharing a system prompt collapse to one physical copy; at divergence, COW kicks in. Trace the ref-counts.
  4. Extensions: eviction (recompute vs swap, Ch. 7), then a beam-search fork.

Lab 02 (continuous batching + goodput):

  1. python solution.py; read the scheduler against Ch. 5–6.
  2. Reproduce static-vs-continuous throughput; then sweep max_batch and find where goodput peaks (interior, not max) — the Ch. 6 thesis, measured.
  3. Watch a long prefill stall decodes; reason about chunked prefill (extension).
  4. 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 serve but 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

TechniqueWhat it doesWin
GQAfewer KV heads4–8× smaller cache (model-level)
FP8/INT8 KVfewer bytes/elem2–4× smaller
PagedAttentionfixed blocks + block tablekills fragmentation → 2–4× more seqs
Prefix caching (COW)share system-prompt KV>90% prefill cut for chat
Eviction (recompute/swap)free blocks under pressuregraceful 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

MetricMeansGotcha
TTFTqueue + prefillprompt length, batch interference
TPOT/ITLdecode speedUX floor ~10–15 tok/s
throughputtotal tok/sgameable by oversized batches
goodputtok/s meeting SLOthe 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

EngineStrengthChoose when
vLLMthroughput, model coverage, OSS velocitydefault; multi-model fleets
TensorRT-LLMpeak latency/throughput (AOT kernels)fixed model+GPU, last 20%, accept lock-in
SGLangstructured gen, RadixAttention prefix treesagents, tool use, complex prompting
TGIHF integration, simplicitysimpler 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.md Ch. 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_len per 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)

  1. BlockAllocatorallocate/free, the free list, ref-counts.
  2. BlockTable / Sequence — append tokens, grow blocks on demand.
  3. share_prefix + the COW path — ref-count increment on share, copy on write to a shared (refcount>1) block.
  4. The three demos in main().

3. Extensions

  1. 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).
  2. Beam search / parallel sampling: fork a sequence into N continuations sharing the prompt's blocks; show COW only on the diverging tokens.
  3. 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).
  4. Hash-based prefix cache: auto-detect shared prefixes by hashing block contents (RadixAttention-lite) instead of explicit sharing.

4. Common pitfalls

  1. Freeing a shared block while refcount > 1 — corrupts other sequences; free must decrement and only reclaim at zero.
  2. Writing into a shared block without COW — silently corrupts every sharer (the test test_cow_on_divergence catches it).
  3. Off-by-one at block boundaries — token T lives in block T // block_size at offset T % block_size; the append logic must roll to a new block exactly at the boundary.
  4. 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.md Ch. 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)

  1. Request / Poisson arrival generation.
  2. simulate_static — fixed batches, run to completion.
  3. simulate_continuous — the iteration loop: retire finished, admit waiting, step the batch; per-iteration time as a function of batch size.
  4. The goodput accounting: a request's tokens count only if its TTFT and TPOT met the SLO.

3. Extensions

  1. 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).
  2. Prefix-cache integration: requests sharing a system prompt skip prefill (use Lab 01's idea) — watch TTFT collapse on cache hits.
  3. SLO-aware admission: reject/queue requests when admitting them would blow the batch's TPOT SLO; show goodput protected under overload.
  4. Prefill/decode disaggregation: separate prefill and decode "pools"; model the KV hand-off cost; compare interference vs the chunked-prefill approach.

4. Common pitfalls

  1. 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).
  2. Counting throughput as goodput — goodput must filter by SLO compliance, or the whole lesson disappears.
  3. Poisson vs back-to-back arrivals — back-to-back inflates throughput unrealistically (WARMUP Ch. 6); the sim uses Poisson.
  4. 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 × data bandwidth 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)

FieldValue
GoalImplement ring all-reduce across N OS processes communicating over TCP sockets; verify correctness and benchmark against the 2(N-1)/N bandwidth bound.
ConceptsReduce-scatter + all-gather, ring topology, bandwidth optimality, why all-reduce volume is ~2× the data regardless of N.
Steps1) 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.
StackPython (stdlib socket, multiprocessing)
OutputA working multi-process ring all-reduce + a bandwidth report (achieved vs 2(N-1)/N bound).
How to TestBuilt-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 PointsWhy 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."
ExtensionsTree 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)

FieldValue
GoalModel a multi-node GPU cluster's interconnect (NVLink islands, InfiniBand between nodes) and compute collective time for different parallelism placements — proving topology decides throughput.
ConceptsBandwidth hierarchy (NVLink/IB), tensor-parallel-within-node, data-parallel-across-node, rail topology, the cost of a badly-placed collective.
Steps1) 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.
StackPython (stdlib)
OutputA topology cost model + a placement-comparison table (collective time per strategy).
How to TestBuilt-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 PointsWhy 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."
ExtensionsPipeline-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

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

CollectiveEach rank ends withVolume intuition
Broadcastroot's dataone→all
Reduceroot gets the sumall→one
All-reduceeveryone gets the sumthe big one: ~2V per rank (Ch. 3)
All-gathereveryone gets all ranks' data(N-1)/N · NV per rank
Reduce-scattereach gets a reduced slice(N-1)/N · V per rank
All-to-alltranspose: each sends a piece to eachthe 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.

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

  1. python solution.py --world 4; confirm correctness, then read the reduce-scatter/all-gather phases against Ch. 3.
  2. 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).
  3. 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):

  1. python solution.py; read the bandwidth-hierarchy cost model against Ch. 6.
  2. 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.
  3. Hierarchical all-reduce vs flat across nodes — Ch. 4's win, measured.
  4. 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

TypeSplitsCommunicationLives onScales
Data (DP)the batchall-reduce gradients/step (overlappable)anywhere (IB ok)batch size
Tensor (TP)each layer's matricesall-reduce per layer (critical path)NVLink onlymodel size
Pipeline (PP)layers into stagesactivations between stages (cheap)across nodes okmodel depth
Expert (EP)MoE expertsall-to-allNVLink + IBmodel 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/knobUse
NCCL_DEBUG=INFOsee 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_PROTOforce ring/tree for debugging
NCCL_TIMEOUT / framework watchdogturn 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_TIMEOUT set. Added it; now it crashes with the culprit rank in seconds.
  • "Slow only on the new cluster"NCCL_SOCKET_IFNAME picked 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.md Ch. 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):

  1. Reduce-scatter (N-1 steps): ring-pass chunks, accumulating, until each rank owns one fully-reduced chunk.
  2. 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 4 then --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)

  1. worker — each rank: connects to its ring neighbors over TCP, gets its data shard, runs the two phases.
  2. reduce_scatter — the N-1 send/recv-and-add steps; note the chunk indexing (rank r sends chunk (r-step) % N).
  3. all_gather — the N-1 forward-only steps.
  4. The byte counters and the correctness check against a reference sum.

3. Extensions

  1. 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).
  2. 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).
  3. Failure demo: have one rank sys.exit() mid-collective; watch the others hang in recv. Then add a socket timeout that turns the hang into a detected failure with the culprit rank (WARMUP Ch. 8).
  4. 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

  1. 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.
  2. 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).
  3. Float serialization: use a fixed format (struct/array) consistently across ranks, or you'll get garbage sums.
  4. 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.md Ch. 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)

  1. Link bandwidths (NVLink/IB/PCIe) and the allreduce_time cost model (2(N-1)/N · V / bandwidth + latency).
  2. The placement scenarios — how the ranks map to nodes and which links the collective crosses.
  3. hierarchical_allreduce_time — the two-level decomposition.
  4. The blast-radius computation.

3. Extensions

  1. Pipeline bubble model: compute PP efficiency = microbatches / (microbatches + stages - 1); show how more micro-batches shrink the bubble.
  2. 3D parallelism placement: TP=8 (in node), PP=4 (across nodes), DP=rest; compute total step time and find the placement that minimizes it.
  3. Rail/GPUDirect model: add a PCIe-staging penalty when GPUDirect RDMA is misconfigured; show the 50× host-bounce cliff.
  4. Failure-domain optimization: given a job shape, find the placement that minimizes blast radius subject to the NVLink-island constraint.

4. Common pitfalls

  1. 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.
  2. 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).
  3. 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)

FieldValue
GoalBuild 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.
ConceptsAPI/ABI separation, function-pointer vtables, dlopen/dlsym, capability negotiation, backend selection, ABI versioning.
Steps1) 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.
StackC (C11), dlopen (POSIX)
OutputA HAL core + 3 loadable .so/.dylib backends + a demo that routes by capability and version-checks the ABI.
How to TestBuilt-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 PointsWhy 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."
ExtensionsAdd 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)

FieldValue
GoalProduce 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."
ConceptsThe three portability postures (Phase 03 Ch. 9–10), conformance testing, capability matrices, the build-vs-adopt analysis.
Steps1) 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."
StackMarkdown (ADR) + Python (conformance harness)
OutputA completed ADR + a conformance report (which backend supports/passes what).
How to Testpython conformance.py asserts the matrix is computed correctly and flags non-conformant backends; the ADR follows the provided rubric.
Talking PointsThe 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."
ExtensionsAdd 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"

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 Capabilities is {int version; int flags; int max_dtype;} and a backend was compiled when it was {int version; int flags;}, every field after flags is 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. The dlsym of that symbol is the only coupling.
  • Use fixed-width types (int32_t, not int) and explicit enums with assigned values — int width 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:

  1. Stable API — what the application calls. It never changes shape; that's the promise to application authors.
  2. Backend interface (the vtable) — a struct of function pointers + a capability struct. This is the ABI the vendor plugins implement.
  3. Dynamic loadingdlopen("backend_fast.so") then dlsym(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 matmul can'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):

  1. 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.
  2. 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.
  3. 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.
  4. 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_version before 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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):

  1. make && ./hal_demo; read hal.h first — the ABI is the contract, study the Backend struct and Capabilities against Ch. 2–3.
  2. Read hal.c (loader/router) then the three backends; trace one hal_matmul call from API → vtable → backend.
  3. Reproduce: capability routing (FP16 request avoids the "limited" backend); ABI-version rejection of a stale backend.
  4. Do extension 1 — add a 4th backend as a new .so without 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):

  1. Read STRATEGY-ADR.md; it's a worked decision record — study its structure.
  2. python conformance.py; understand the support matrix + pass/fail logic (Ch. 7).
  3. 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 .so files 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:

  1. Escape hatches (generic path + optional native fast path)
  2. Operation-level ops (attention, not matmul+softmax) ← the winner
  3. Compiler layer (Triton/MLIR lowering) ← highest ceiling, highest cost
  4. 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 typeSpeed-to-peakCostUse
vendor libs (cuBLAS/rocBLAS/oneMKL)near-peak/vendorone backend per vendorhardware you must support today
Triton/MLIR~90% peakshared kernelsvelocity + future chips
full compilermaxunboundedmoonshot only
SPIR-V/Vulkanbelow peak (ML)neutralstandards 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.md Ch. 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.

FileRole
hal.hthe contract: stable API + the hal_backend ABI (vtable + caps + version)
hal.cthe loader (dlopen/dlsym), version validation, capability router
backend_cpu_ref.creference backend: FP32 only
backend_fast.c"fast" backend: FP32 + FP16, higher perf hint
backend_limited.c"limited" backend: FP32 only, lower perf
hal_demo.cloads 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 .so files 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

  1. hal.hthe ABI is the contract. Study hal_backend (version first, reserved fields last) and hal_caps against WARMUP Ch. 2.
  2. hal.chal_load_backend (dlopen → dlsym → version check) and hal_select (capability routing).
  3. The three backends — note they share no source with the core, only hal.h's ABI.
  4. hal_demo.c — trace one matmul from API call → router → backend vtable.

3. Extensions (the heart of the keystone lab)

  1. 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 rebuilding hal.c/hal_demothis is what "decoupled from hardware" means. Prove it: make backend_exp.so && ./hal_demo after adding only the path.
  2. 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.
  3. Native escape hatch (WARMUP Ch. 5): add an optional native_op pointer a backend may provide for a fast path; the router uses it when present, falls back otherwise.
  4. Performance conformance (WARMUP Ch. 7): time each backend's matmul; gate "recommended" status on a perf threshold.

4. Common pitfalls

  1. 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_caps fields, rebuild only the core, and watch a not-rebuilt backend misbehave.
  2. Forgetting the version check — a stale backend's misaligned struct reads as garbage; check abi_version first, before any other field.
  3. dlsym returning NULL — the backend didn't export hal_backend_register (symbol visibility); handle it, don't crash.
  4. 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.md Ch. 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:

  1. 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.md is a worked example; you fill in a decision matrix and write your own for a scenario.
  2. 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

  1. STRATEGY-ADR.md — study the ADR structure (context, options, costs, decision, consequences). Fill its decision matrix.
  2. 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

  1. Performance conformance tier: backends must hit X% of a reference throughput, not just pass correctness; add a RECOMMENDED/SUPPORTED/FAIL status (WARMUP Ch. 7).
  2. Rollout dependency plan: model the 12-month multi-vendor rollout as a dependency graph (HAL ABI freeze → AMD backend → conformance → GA).
  3. 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)

FieldValue
GoalBuild 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).
ConceptsPrometheus exposition format, gauge vs counter, metric naming/labels/cardinality, the metrics that matter (Xid, reserved-vs-allocated, throttle, ECC).
Steps1) 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.
StackPython (stdlib http.server)
OutputA working exporter + an alert-rules file + a scrape test that fires the right alerts.
How to TestBuilt-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 PointsWhy "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."
ExtensionsSource 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)

FieldValue
GoalBuild 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.
ConceptsSemantic versioning, compatibility matrices, canary analysis (statistical comparison), progressive rollout, automated rollback, error budgets.
Steps1) 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).
StackPython (stdlib)
OutputCompatibility checker + canary analyzer + rollout state machine + tests.
How to TestBuilt-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 PointsThe 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."
ExtensionsProgressive 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

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's utilization.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 (DCGM DCGM_FI_PROF_SM_ACTIVE) and achieved memory bandwidth (DCGM_FI_PROF_DRAM_ACTIVE).
  • Memory: used vs the framework's allocated — 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_reasons is 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, _total for 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):

  1. 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.
  2. 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.
  3. Progressive rollout: expand by percentage with a gate at each stage, watching error budget (Ch. 5). Any stage that burns budget halts.
  4. Automated rollback: SLO burn → roll back automatically, fast — image- pinned so rollback is a re-deploy, not archaeology (Phase 04 Q6).
  5. 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):

  1. python solution.py; curl localhost:9400/metrics and read the format against Ch. 4. Verify gauge/counter types are correct.
  2. Run the scrape-and-alert check; confirm GPUThrottling and KVFragmentationRisk fire on the bad simulated states and stay quiet on healthy ones — tie each to Ch. 3/6.
  3. Extensions: real DCGM/nvidia-smi source; a recording rule for goodput; a Grafana dashboard JSON.

Lab 02 (release tooling):

  1. python solution.py; read the compatibility gate, canary analyzer, and rollout state machine against Ch. 8.
  2. Reproduce: an incompatible driver/CUDA pair rejected pre-deploy; a canary with regressed p99 → ROLLBACK; healthy canary → PROMOTE.
  3. 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)

MetricTypeTrap / signal
utilization.gpugaugelies — "a kernel ran," not "SMs busy." Use DCGM SM-active.
SM-active / DRAM-activegaugethe real saturation/bandwidth signals
memory reserved vs allocatedgaugegap = cache (Phase 05); leak = allocated up
largest-free-blockgaugefragmentation early warning — most don't export it
clocks + throttle-reasonsgaugethrottling = silent perf loss; temp lags
ECC correctable / uncorrectablecounteruncorrectable = page (dying GPU)
Xid errorscountertop alert — predicts node failure (Phase 04)
NVLink/PCIe throughputcounterlow 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)

FailureDetectRespond
Xid / off the bus (P04)Xid-rate alertcordon → drain (ckpt-aware) → RMA → health-check
Fragmentation OOM (P05)largest-free-blockfind workload → restart / bucket / paged KV
Collective hang (P08)collective timeoutculprit rank → drain → restart from ckpt
Driver skew (P04)version mismatchfleet-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.md Ch. 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)

  1. Metric (gauge/counter) and render_prometheus — the exposition format.
  2. simulate_gpus — the simulated fleet, including the three bad states.
  3. ALERT_RULES + evaluate_alerts — the rule logic (WARMUP Ch. 6).
  4. The HTTP handler serving /metrics.

3. Extensions

  1. Real source: replace the simulator with nvidia-smi --query-gpu=... --format=csv parsing, or DCGM (dcgmi dmon / the DCGM Python bindings) where a GPU exists.
  2. Recording rules: add a Prometheus recording rule that computes goodput from request metrics (Phase 07).
  3. Grafana dashboard: export a dashboard JSON with the key panels (temp, throttle, Xid rate, largest-free trend).
  4. Per-MIG metrics: emit metrics per MIG instance (Phase 06), with the right labels.
  5. Burn-rate alert: implement a multi-window burn-rate alert on a simulated p99 TTFT SLO (WARMUP Ch. 6).

4. Common pitfalls

  1. Counter as gauge (or vice versa): temperature must be a gauge; total Xid errors a counter. Emit the wrong # TYPE and rate() breaks.
  2. 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).
  3. Alerting on utilization.gpu: it's the misleading metric (WARMUP Ch. 3); alert on SM-active / SLOs / cause-signals instead.
  4. 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.md Ch. 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):

  1. Compatibility gate — reject incompatible driver/CUDA/engine pairs before deploy (Phase 03/04's skew failures are fleet-wide; catch them pre-deploy).
  2. Canary analyzer — compare a canary's SLIs to baseline statistically; decide PROMOTE / HOLD / ROLLBACK.
  3. 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)

  1. Version parsing + check_compatibility — the matrix gate.
  2. analyze_canary — the statistical comparison (effect size + threshold).
  3. Rollout state machine — stages, gating, auto-rollback.

3. Extensions

  1. Progressive % rollout with configurable stages and per-stage gates.
  2. Error-budget freeze: track budget consumption; freeze releases when exhausted (WARMUP Ch. 5).
  3. Wire Lab 01's metrics as the canary signal (real exporter → canary decision).
  4. Fleet driver-upgrade plan: model the Phase 04 Q6 choreography (inventory → canary → drain → upgrade → health-check → progressive) as a state machine.
  5. Proper stats: replace the threshold with a Welch's t-test / Mann-Whitney on sampled latencies.

4. Common pitfalls

  1. Eyeballing canaries: a real p99 regression hides in noise; the comparison must be statistical with an effect-size threshold.
  2. Discovering incompatibility at runtime: the compatibility gate must be pre-deploy — a runtime "driver insufficient" is a fleet incident.
  3. Manual rollback: by the time a human reacts, the budget's gone; rollback on SLO burn must be automatic and fast (image-pinned).
  4. 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)

FieldValue
GoalBuild 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.
ConceptsHardware fingerprinting, fuzzy matching, Ed25519 signatures, signed (not encrypted) licenses, offline verification, grace periods, clock-rollback resistance, the private key as crown jewel.
Steps1) 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.
StackPython (cryptography if available, else a bundled minimal Ed25519 — pure-stdlib fallback included)
OutputA working license issuer + offline verifier + a test suite covering tamper, wrong-host, fuzzy-match, and rollback cases.
How to TestBuilt-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 PointsWhy 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."
ExtensionsAir-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)

FieldValue
GoalImplement 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.
ConceptsCode-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.
Steps1) 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.
StackC (integrity self-check) + Python (attestation model + tamper tool)
OutputA self-verifying binary + a tamper demonstration + an attestation flow model + a written threat model with defeat conditions.
How to TestBuilt-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 PointsWhy 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."
ExtensionsString/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

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:

  1. 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.
  2. 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_at vs 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):

  1. The deployment generates a challenge: its hardware fingerprint + a nonce, displayed/exported as a short code or file.
  2. 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.
  3. Your service verifies entitlement and issues a signed activation response (the license, bound to that fingerprint + nonce) — carried back in.
  4. 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):

  1. A trusted hardware element (TPM, CPU TEE, or NVIDIA GPU confidential computing) measures the boot chain / loaded code into tamper-resistant registers (PCRs).
  2. It produces a quote — those measurements, signed by a hardware key the attacker can't extract or forge.
  3. 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):

  1. python solution.py; read fingerprinting + Ed25519 issuance/verification against Ch. 2–4.
  2. 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.
  3. Extensions: air-gap challenge-response (Ch. 5), revocation, feature gating.

Lab 02 (anti-tamper + attestation):

  1. make && ./protected; it self-verifies and runs. python tamper.py protected flips a code byte; rerun → tamper detected (Ch. 6).
  2. python attestation.py; trace measure→quote→verify and articulate why the external verifier beats the self-check (Ch. 8).
  3. 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

PrimitiveUseNote
SHA-256fingerprint hardware/codeone-way, not secret
Ed25519 sign/verifylicensesprivate signs, public verifies — public ships safely
sign vs encryptsign licenses, encrypt weightslicense isn't secret, just unforgeable
private keythe crown jewelHSM/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)

TechniqueBuysDefeat condition
integrity self-checkcatches casual patchingpatch the checker too
obfuscationattacker-hoursCPU runs it → analyst eventually reads it
anti-tamper responsespeed bumpcalibrate; avoid DRM theater (false positives)
attestationhardware-rooted proof of statetrust root must be sound; proves what loaded
confidential computingweights/data secret from hostTEE + 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.md Ch. 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)

  1. The embedded Ed25519 (_ed25519 section) — you don't need to grok the curve math; know it gives sign(priv,msg) / verify(pub,msg,sig) (WARMUP Ch. 2).
  2. hardware_fingerprint + fuzzy_match — multi-attribute ID and N-of-M matching (WARMUP Ch. 3).
  3. issue_license (private key) and verify_license (public key, offline) — the sign/verify flow + grace + rollback check (WARMUP Ch. 4).
  4. The attack tests — and the defeat-condition note.

3. Extensions

  1. 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.
  2. Revocation: a signed revocation list for connected deployments; discuss why it's hard air-gapped (hence short terms + re-activation).
  3. Feature gating: enforce per-feature entitlements and a max-GPU quota at "runtime" (simulate a platform asking "may I use FP8 on 6 GPUs?").
  4. Key rotation: support multiple public keys (key IDs) so you can rotate the signing key without invalidating all licenses at once.

4. Common pitfalls

  1. Encrypting instead of signing — a license needs integrity+authenticity, not secrecy; signing with a public verifier is correct (WARMUP Ch. 2).
  2. Brittle single-hash fingerprint — one hardware change locks the customer out; fuzzy N-of-M is the engineered answer (WARMUP Ch. 3).
  3. Trusting the system clock — rollback gives free perpetual licenses without a monotonic high-water-mark (WARMUP Ch. 4).
  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.md Ch. 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):

  1. 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.
  2. 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

  1. protected.cself_check() hashes a marked code region and compares to EXPECTED_DIGEST; note the honest comment on how it's defeated (WARMUP Ch. 6).
  2. tamper.py — flips a byte in the code region to simulate a patch (the attacker's move against the Lab 01 license check).
  3. 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:

TechniqueRaises cost ofDefeated by
integrity self-checkcasual binary patchingpatching the checker / the expected digest
obfuscation (Ch. 7)locating/understanding the checka debugger + attacker-hours
anti-tamper responseautomated tamperingcalibration errors (false positives hurt honest users)
attestationfaking the software statea sound trust root is required; proves what loaded, not bug-freeness
confidential computingreading weights/data on hostTEE + 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

  1. 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).
  2. String/control-flow obfuscation experiment: obfuscate the check logic; reflect on the RE-cost increase vs the maintenance cost.
  3. 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.
  4. Encrypted weights at rest: weights decrypt only after a valid attestation (model the gate).

5. Common pitfalls

  1. 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.
  2. Aggressive tamper response — self-destruction/false positives punish honest users (a hardware change, a debugger attached for support). Calibrate.
  3. Confusing self-attestation with attestation — software vouching for itself is not attestation; the verifier and key must be external/hardware.
  4. 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:

  1. 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.
  2. 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)

FieldValue
GoalBuild 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).
ConceptsSystem integration; the control-plane API; capability routing; admission control; licensing gate; observability; the release cycle.
Steps1) 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.
StackPython (stdlib) — reuses patterns from Phases 06/07/09/10/11
OutputA running mini platform + DESIGN-DOC.md + RUNBOOK.md + a release/retrospective writeup.
How to TestBuilt-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 PointsHow 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."
ExtensionsMulti-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/:

  1. 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).
  2. ORG-DESIGN.md — the team structure, skill mix, and hiring plan for the platform; which roles, what each owns, the build-vs-hire calls.
  3. 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.
  4. 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

The JD's responsibilities decode into five jobs, none of which is "write the most code":

  1. 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).
  2. Build the team — hire and structure the rare skill mix (Ch. 3), and make the build-vs-hire calls.
  3. Own the quality bar — codify and defend the standards (Ch. 4) that keep a fast-moving platform shippable.
  4. Drive partnerships and commercialization — OEM integrations (Ch. 5), pricing, the sovereign-AI go-to-market (Ch. 6).
  5. 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, ncu limiter 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:

  1. 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.
  2. 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).
  3. 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.

  1. Boot and trace (python platform.py): run it, then trace one request through the composition (Ch. 8) — license → admit → route → serve → metrics.
  2. Run the scenarios: expired license (refuse), capability routing, overload (admission protects goodput), backend failover. Each is a prior phase, integrated.
  3. 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.
  4. Write the runbook (RUNBOOK.md): operational procedures for the platform's failure modes (compose the Phase 10 runbooks).
  5. Write the four leadership artifacts (leadership/): strategy, org, OEM, standards (Ch. 2–5). Use the rubrics.
  6. 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)

JobJD lineArtifact
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 artifactCommercial 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.md Ch. 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

ScenarioIntegratesAsserts
1 healthy servingP07 continuous batchingall 20 requests served
2 expired licenseP11 license gateserving refused
3 capability routingP09 HALFP8 → FP8-capable backend; refused if none
4 backend failoverP09 + P10 healthFP16 fails over to a healthy backend
5 overloadP06/07 admissionKV admission protects goodput; nothing OOM-crashes
6 observabilityP10valid Prometheus export

All pass on python platform.py.

2. Reading order (platform.py)

  1. License (P11), HAL/Backend (P09), KVBlockPool (P07), Metrics (P10) — the subsystems.
  2. Platform.servethe integrated request path (the heart): license gate → admission → routing → batched generation → metrics. Trace one request through it.
  3. The six scenarios in main.

3. The deliverables (write these)

  1. 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.
  2. RUNBOOK.md — operational procedures for the platform's failure modes (compose the Phase 10 runbooks). Template provided.
  3. The four leadership artifacts in ../leadership/ — strategy, org, OEM, standards.

4. Extensions

  1. Multi-node (P08): TP-shard a model across simulated nodes; add the collective cost to the serving path.
  2. TypeScript control-plane CLI (the JD's JS/TS-for-tooling line): a pctl CLI that talks to the platform's control API (status, license, route).
  3. Real backend: wire the Phase 09 C HAL (CUDA backend if hardware exists) under this control plane.
  4. 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.md Ch. 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)

PhaseTechnicalCommercial outcomeKill criteria
0–6 moHAL + NVIDIA backend + serving (P07) + offline licensing (P11)first sovereign customer in productionno sovereign pipeline by mo 6
6–12 moAMD backend + conformance suite (P09) + observability/release (P10)OEM #1 certified; multi-vendor proofAMD backend < 80% of vendor peak
12–18 moTriton/MLIR backend + GPU sharing tiers (P06) + attestation (P11)multi-vendor GA; tiered pricing; regulated-market expansionkernel-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.md Ch. 3. Org design is strategy — the team shape encodes the build-vs-buy boundary.

Teams (by subsystem ownership, not by layer)

TeamOwns (phases)Rare hiresSize (early)
Platform CoreHAL + runtime (P05, P09)kernel/compiler, systems C/C++/Rust4–6
ServingKV-cache, batching, engine integration (P07)LLM-serving/perf engineers3–5
Infra / SREscheduling, observability, releases (P06, P10)GPU-savvy SREs3–4
Securitylicensing, attestation, sovereign (P11)applied crypto / security eng1–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.md Ch. 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-sanitizer clean
  • Honest timing: CUDA events, warmup, median of N, fixed clocks noted
  • Limiter evidence: ncu before/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

FileCoversPhases
01 — GPU & Systems Deep-Divearchitecture, CUDA, compilers, drivers, runtime01–05
02 — Serving, Scheduling & DistributedKV-cache, batching, GPU sharing, collectives06–08
03 — Platform, Portability & SecurityHAL/ABI, observability, licensing, sovereign09–11
04 — System Designthe big design walkthroughsall
05 — Leadership & Behavioralstrategy, org, OEM, communication, STAR12

How to use this

  1. Self-test first: answer cold, then check against the model answer + the WARMUP it derives from. Gaps point you back to a phase.
  2. 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.
  3. Leadership answers need a decision + its consequence + the honest limit — the Phase 12 communication discipline.

The 20 questions most likely to decide the loop

  1. Derive H100's ridge point and what it means for LLM decode. (P01)
  2. Walk me through what happens at the OS level on cudaLaunchKernel. (P02/P04)
  3. What does nvcc do? PTX vs SASS, and the cold-start incident. (P03)
  4. Why does PyTorch ship its own allocator? Reserved vs allocated. (P05)
  5. Compare MPS, MIG, time-slicing; deliver a "0.5 GPU" tier honestly. (P06)
  6. Explain PagedAttention — and why it's really an allocator. (P07/P05)
  7. Static vs continuous batching; what is goodput and why does it beat throughput? (P07)
  8. Derive ring all-reduce's cost; why is it bandwidth-optimal and N-independent? (P08)
  9. Why must tensor parallelism stay on NVLink? With numbers. (P08)
  10. Design a hardware-abstraction layer; walk the API/ABI boundary. (P09)
  11. How do you avoid a lowest-common-denominator abstraction? (P09)
  12. How do you version a plugin ABI so a year-old vendor backend still loads? (P09)
  13. What GPU metrics do you alert on, and why is "GPU utilization" misleading? (P10)
  14. Design a safe release process for a GPU serving platform. (P10)
  15. Design a license system for air-gapped deployment; sign vs encrypt? (P11)
  16. Anti-tamper self-checks — what do they buy, how are they defeated? (P11)
  17. Build vs adopt: your own compiler layer or Triton/MLIR? Defend it. (P03/P09)
  18. How would you execute "decouple software from hardware"? (P09/P12)
  19. How do you structure and hire a GPU platform team? (P12)
  20. 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:

  1. A hard technical decision under uncertainty (a build-vs-buy, a platform bet). Show: how you sequenced, what you'd cut, the outcome.
  2. Raising the quality bar against pushback (a standard a team resisted). Show: justifying with the incident it prevents, not authority. [P12 Ch. 4]
  3. A production incident you led (detect → mitigate → learn). Show: mitigate- first, blameless post-mortem, the system change that resulted. [P10 Ch. 9]
  4. 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]
  5. A partnership/customer you engaged technically and executively (an OEM or a sovereign customer). Show: translation without dishonesty. [P12 Ch. 7]
  6. 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.

WalkthroughQuestionPhases
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

  1. Drive the requirements first — every good design answer starts by establishing scale, SLOs, tenancy, and constraints. The walkthroughs model this.
  2. 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.
  3. Name the failure modes — at this level, "what breaks and how you detect/ recover" (Phases 08, 10) is half the answer.
  4. 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 dlsym fails / 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.