Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

.nornir/gpu-adapter-selection-design.md

GPU ADAPTER SELECTION — facett decides which device it renders on

nornir:class=design

Only the chosen design is named here (L8); the alternatives that were rejected are not. The measured evidence sits in §2, and it refutes the hypothesis that started this work — which is the point of measuring first.


1. The question

The OSM 2D map was reported slow on a 32-core Threadripper PRO 3975WX with a GeForce RTX 4090. The obvious first suspicion was that facett was not on the 4090 at all: the box also exposes an ASPEED BMC graphics core (2a:00.0 ASPEED Graphics Family, the server board's remote-console framebuffer) and Mesa's llvmpipe software rasteriser, and nothing in the workspace ever printed which adapter was actually opened. Every headless probe in the repo called

instance.request_adapter(&wgpu::RequestAdapterOptions {
    power_preference: wgpu::PowerPreference::default(),   // == LowPower
    ..
})

PowerPreference::default() is LowPower — on a machine with an integrated GPU that deliberately asks for the slower device — and eframe's own selection was left to wgpu's internal ordering. The choice was never a policy and never observable.

2. ESTABLISHING THE TRUTH FIRST — and the hypothesis is REFUTED

cargo run -p facett-core --features wgpu --example gpu_probe, on oden:

== ENUMERATED 3 adapter(s) ==
  [0] score=  5028  NVIDIA GeForce RTX 4090 [discrete/Vulkan] vendor=0x10de device=0x2684
  [1] score=     8  llvmpipe (LLVM 21.1.8, 256 bits) [cpu/Vulkan] vendor=0x10005 device=0x0000
  [2] score=  2021  NVIDIA GeForce RTX 4090/PCIe/SSE2 [other/Gl] vendor=0x10de device=0x0000
== OLD PATH request_adapter(PowerPreference::default()) -> NVIDIA GeForce RTX 4090 [discrete/Vulkan]
== OLD PATH request_adapter(HighPerformance)            -> NVIDIA GeForce RTX 4090 [discrete/Vulkan]
== OLD PATH request_adapter(LowPower)                   -> NVIDIA GeForce RTX 4090 [discrete/Vulkan]

facett was already on the RTX 4090. All three power preferences returned it. Three facts explain why the trap did not fire on this box:

So adapter selection was not the cause of the slow 2D map. The cause is in osm-2d-perf-design.md. The policy below still ships, for three reasons: the choice must be observable (it was not), it must be overridable (it was not), and the traps are real on the machines this code also runs on — a Dell/HPE board puts a Matrox G200 BMC in the enumeration, a laptop puts an iGPU there, and a container with a broken ICD leaves only llvmpipe.

3. The policy

facett-core/src/render/adapter.rs — pure and feature-free. It does not depend on wgpu, so the whole ranking is unit-tested on a machine with no GPU stack at all, and any facett crate can consume it whether or not it links a GPU.

AdapterFacts mirrors wgpu::AdapterInfo in plain types (name, vendor, device, AdapterKind, backend, driver). AdapterKind mirrors wgpu::DeviceType.

3.1 The score

adapter_score(&AdapterFacts) -> i64. The class tier dominates; the tie-breaks are small enough (max +30 against a 1000-wide tier gap) that they can never lift a lesser class over a better one.

rank class tier why
1 DiscreteGpu 5000 a real card with its own VRAM
2 IntegratedGpu 4000 real hardware; far better than software
3 VirtualGpu 3000 virtio-gpu / venus / passthrough shim
4 Other 2000 an unclassified real driver still beats software
5 software 0 llvmpipe / lavapipe / SwiftShader / WARP — explicit last resort
6 management display −1000 ASPEED / Matrox G200 BMC — below even software

Tie-breaks inside a tier: NVIDIA +20, AMD +10, Intel +5 (by PCI vendor id or name), and Vulkan/Metal/Dx12 +8 versus Gl +1 — so the same physical device is taken through its native driver rather than the GL wrapper. Ties beyond that keep enumeration order (stable sort), so a two-identical-GPU box is deterministic.

3.2 The two special cases, and why each is matched the way it is

Software rasterisers are matched by name as well as by device type:

hit("llvmpipe") || hit("lavapipe") || hit("swiftshader") || hit("softpipe")
    || hit("software rasterizer") || hit("microsoft basic render") || name.contains("warp")

Type alone is not enough. lavapipe is itself a Vulkan ICD, so restricting the instance to Backends::VULKAN does not exclude software — a common and expensive mistake — and an ICD that reports VirtualGpu (or Other) would sail straight past a device_type == Cpu check. The name test forces it into the software tier regardless.

BMC / management displays are matched by PCI vendor id first:

pub const VENDOR_ASPEED: u32 = 0x1a03;   // AST2400/2500/2600 — Supermicro, ASRock Rack
pub const VENDOR_MATROX: u32 = 0x102b;   // MGA G200 — Dell iDRAC, HPE iLO

with a name fallback (ASPEED, AST…, MGA G200, G200e, MATROX) for backends that report vendor 0. Vendor-first because the device name varies by driver — ASPEED Graphics Family, AST, MGA G200eW WPCM450 are all the same part. These cores exist to paint a 1024×768 remote IPMI console; they have no usable 3D engine, so selecting one is not "slow", it is "no UI". On a headless-managed server the BMC is often PCI device 0, which is exactly why a naive "first adapter wins" lands there while the real GPU idles. They therefore rank below even software: llvmpipe at least draws.

3.3 Override + observability (L4)

knob effect
FACETT_GPU_ADAPTER=<index> that adapter verbatim, even software/BMC — an escape hatch is an escape hatch
FACETT_GPU_ADAPTER=discrete|integrated|virtual|software best adapter of that class; falls back to the policy if none matches
FACETT_GPU_ADAPTER=<substring> case-insensitive match on name or driver (nvidia, 4090, llvmpipe)
FACETT_GPU_ADAPTER=auto / unset the policy
WGPU_POWER_PREF=low request LowPower (wgpu's own standard var, honoured for familiarity)

AdapterSelection is the decision as data: the chosen device, the index, the reason, a software flag, a forced flag, and every rejected candidate with its score and its software / management_display verdicts. record_selection prints a loud one-line banner to stderr (the demo installs no logger) plus an explicit warning when the chosen device is a CPU rasteriser, and stores the decision in a process-global. facett-demo's state_json publishes it under "gpu", so a robot asserts

"gpu": { "kind": "discrete", "name": "NVIDIA GeForce RTX 4090", "software": false, … }

instead of a human reading a terminal. The key is always present: null means "no GPU lane was brought up" (headless / wasm / CPU-painter), which is a different statement from "software": true.

4. The wgpu edge

facett-core/src/render/gpu/adapter_wgpu.rs (feature wgpu) is the only part that touches a GPU type:

Backends are PRIMARY | GL: the real drivers plus GL as a genuine last resort, so a machine with only a GL driver still gets a window. The policy ranks a device's Vulkan entry above its GL entry, so GL only wins when it is all there is.

facett-demo/src/gpu_select.rs keeps its public API (three binaries install it) but is now a thin adapter over the shared policy — the ranking is not twinned (L5).

5. RED-when-broken proofs (12 tests, no GPU required)

test goes red when
ranks_discrete_over_integrated_bmc_and_software the headline: given [ASPEED, llvmpipe, iGPU, RTX 4090] the ranker must return the 4090, and the full ranking must be [3, 2, 1, 0]
bmc_never_wins an ASPEED/Matrox entry outranks software or an iGPU; also covers vendor-id-only and name-only detection
software_never_beats_hardware llvmpipe beats any real GPU — including a lavapipe that lies and reports VirtualGpu
software_alone_is_selected_and_flagged software-only enumeration returns nothing, or fails to set software: true
tie_breaks_stay_inside_their_tier a vendor bonus lifts an iGPU over a discrete GPU, or GL beats Vulkan for the same device
env_override_forces_a_device any override form stops working, or a bad override fails to start instead of degrading to the policy
selection_json_is_robot_readable the state_json fragment loses the chosen device, its kind, or the rejected candidates
total_over_garbage empty names / unknown backends panic
oden_enumeration_resolves_to_the_discrete_gpu the same case expressed in wgpu types (catches a broken facts_of)
device_type_mapping_is_exact a wgpu upgrade adds a device type that silently lands in the wrong tier
wgpu_options_install_the_policy (×2, core + demo) someone drops the native_adapter_selector and hands the choice back to wgpu

6. What is still open