Lab 01 — A GPU HAL with dlopen Backends (C)
Phase: 09 — Hardware Abstraction (Keystone) | Difficulty: ⭐⭐⭐⭐⭐ | Time: 6–10 hours
Language: C11 + POSIX dlopen | Hardware: none
Concept primer:
../WARMUP.mdCh. 2–7.
Run
make # builds the HAL core, 3 backend .so files, and the demo
./hal_demo
0. The mission
Build the literal architecture of "decouple software from hardware" (WARMUP
Ch. 1): a stable API, a versioned backend ABI, and three dlopen-loaded
vendor backends. The core links none of the backends — it loads them at
runtime, queries their capabilities, and routes work to a capable one.
| File | Role |
|---|---|
hal.h | the contract: stable API + the hal_backend ABI (vtable + caps + version) |
hal.c | the loader (dlopen/dlsym), version validation, capability router |
backend_cpu_ref.c | reference backend: FP32 only |
backend_fast.c | "fast" backend: FP32 + FP16, higher perf hint |
backend_limited.c | "limited" backend: FP32 only, lower perf |
hal_demo.c | loads all, runs conformance, routes a FP16 request, checks ABI version |
1. What the output shows
loaded backend: cpu_ref (abi v2, dtypes=F32, perf=1)
loaded backend: fast (abi v2, dtypes=F32|F16, perf=10)
loaded backend: limited (abi v2, dtypes=F32, perf=2)
rejected backend: stale (abi v1 != core v2) — ABI version guard works
== conformance: matmul FP32 vs reference, all backends ==
cpu_ref : PASS
fast : PASS
limited : PASS
== capability routing ==
FP32 matmul -> fast (highest perf among F32-capable)
FP16 matmul -> fast (only F16-capable backend)
FP8 matmul -> (none) cleanly refused: no loaded backend supports FP8
- Three backends loaded from
.sofiles the core never linked against — that's the decoupling, mechanically (WARMUP Ch. 3). - ABI version guard rejects a backend built against an incompatible ABI (WARMUP Ch. 2, 6) — the discipline that lets a year-old vendor binary still load (or be cleanly refused).
- Conformance: one test, all backends pass (WARMUP Ch. 7).
- Capability routing: FP16 work goes only to the FP16-capable backend; FP8 is refused cleanly instead of silently downgraded (WARMUP Ch. 4 — beating the lowest common denominator).
2. Reading order
hal.h— the ABI is the contract. Studyhal_backend(version first, reserved fields last) andhal_capsagainst WARMUP Ch. 2.hal.c—hal_load_backend(dlopen → dlsym → version check) andhal_select(capability routing).- The three backends — note they share no source with the core, only
hal.h's ABI. hal_demo.c— trace onematmulfrom API call → router → backend vtable.
3. Extensions (the heart of the keystone lab)
- Add a 4th backend with NO core recompile (
backend_exp.c): build just the new.so, drop it in the load list, run. It loads and passes conformance without rebuildinghal.c/hal_demo— this is what "decoupled from hardware" means. Prove it:make backend_exp.so && ./hal_demoafter adding only the path. - ABI evolution (WARMUP Ch. 6): add a new optional op at the end of the vtable behind a capability flag; show an old backend (without it) still loads and runs, while a new backend exposes the new op. Demonstrate append-only + reserved-field discipline.
- Native escape hatch (WARMUP Ch. 5): add an optional
native_oppointer a backend may provide for a fast path; the router uses it when present, falls back otherwise. - Performance conformance (WARMUP Ch. 7): time each backend's matmul; gate "recommended" status on a perf threshold.
4. Common pitfalls
- Inserting a struct field in the middle — breaks every already-built
backend's field offsets (WARMUP Ch. 2). Only append, behind a version guard.
Try it: reorder
hal_capsfields, rebuild only the core, and watch a not-rebuilt backend misbehave. - Forgetting the version check — a stale backend's misaligned struct reads
as garbage; check
abi_versionfirst, before any other field. dlsymreturning NULL — the backend didn't exporthal_backend_register(symbol visibility); handle it, don't crash.- Comparing float results with
==across backends — different operation order/rounding (Phase 03's FMA caveat applies cross-vendor); conformance uses a tolerance.
5. What this lab proves about you
You've built the JD's central artifact with your own hands: a versioned, dlopen-based, capability-routing hardware-abstraction layer — the mechanical meaning of "a platform that operates independently of the underlying hardware." You can now design this for real (CUDA/ROCm/Level Zero backends), version it so vendors trust it, and explain why it's the platform's moat.