GPU device selection & power policy (hybrid iGPU/dGPU routing, per-surface preference, dGPU power lifecycle)
gpu specs/gpu/device-selection.kmd
Normative contract for how ANY Koder component that renders or decodes on a GPU decides WHICH physical GPU to use on hybrid machines (integrated iGPU vs discrete dGPU) and how it manages dGPU power. Materializes stack-RFC-025 into testable rules. The contract (this spec + thin types in koder_kit) is the SHARED layer every component inherits; the MECHANISM is implemented per render engine (Kroma for native apps, Kruze for Chromium/CEF, the video path for decode) — each backend conforms to these rules. Policy is hoisted; mechanism stays at the engine. Per-element routing across two physical GPUs is explicitly out of scope (cross-PCIe per-frame copy negates the win); per-element routing within one GPU is already native to the engine and needs no work.
When this spec applies
Primary triggers
- Author or extend a per-engine GPU-selection backend
All triggers
- Implement physical-GPU selection (iGPU vs dGPU) in any Koder component
- Define or change the 'workload deserves the dGPU' heuristic or its thresholds
- Implement dGPU power-management (runtime-D3 sleep/wake) in any component
- Add a user-facing GPU-preference setting to any Koder UI
- Work on KRUZE-277 (Chromium backend) or a Kroma/video GPU backend
Specification body
GPU device selection & power policy
Normative materialization of stack-RFC-025. Three layers: (1) this shared
contract (policy) + thin types in koder_kit; (2) per-engine backends
(mechanism); (3) apps inherit via their engine. See the RFC for the rationale
(why neither "all in Kore/Kroma" nor "per-module" is correct).
1. Model
- Surface — the unit a preference attaches to: a tab/window (Kruze), a surface/view (Kroma), a media pipeline (video). Each surface has exactly one effective GPU at a time.
GpuPreference(tri-state, per-surface):auto(default) ·igpu·dgpu.- Effective GPU — the device a surface actually runs on, resolved from the
preference + (in
auto) the heuristic.
2. Normative rules
- R1 — Single-GPU no-op. On a machine with one usable GPU, the whole contract is a no-op: every surface uses that GPU, no power lifecycle, no UI. A backend MUST detect GPU count before doing anything.
- R2 — Default is
auto. A surface with no explicit preference resolves via the heuristic (R4).igpu/dgpuare hard overrides that skip the heuristic. - R3 — Preference precedence. Per-site/per-app stored override > session
user setting >
auto. A user override MUST persist (per-site for web, per-app for native) and survive restart. - R4 — Heuristic (
autoonly). A surface is routed to the dGPU when ANY canonical signal fires: a WebGL/WebGPU context requestedhigh-performance; a WebGPUrequestAdapter({powerPreference:'high-performance'}); a render canvas/surface ≥ the size threshold; video ≥4K; sustained GPU time over the time threshold. Thresholds are defined HERE (numeric values in koder_kit's constants, single source); signal COLLECTION is the backend's job (R8). - R5 — dGPU power lifecycle. The dGPU MUST stay in runtime-D3 (asleep) until ≥1 surface is routed to it, and MUST return to D3 within the idle-release window (default 30 s, defined in koder_kit) after the last dGPU surface closes/migrates away. Backends MUST NOT hold the dGPU awake for iGPU-only surfaces.
- R6 — Migration semantics. Changing a surface's effective GPU MAY require recreating its render context (GPU device cannot switch mid-context). A backend MUST do this without losing user state (web: reload acceptable; native: re-init surface). Migration MUST be gated behind R3 (explicit pref or heuristic crossing), never spontaneous.
- R7 — User visibility. When a surface runs on a non-default GPU, the UI MUST
expose it (indicator: "running on
") and MUST offer the tri-state setting (R2) + per-surface override (R3). Naming/labels come from the shared vocabulary (R9). - R8 — Backend contract. A per-engine backend MUST: (a) report GPU
inventory + count; (b) collect the R4 signals in its engine's terms;
(c) execute routing/spawn on the chosen device; (d) execute the R5 lifecycle;
(e) emit the surface's effective-GPU state to telemetry (R10) and the
indicator (R7). It MUST consume the shared
GpuPreference/GpuRoutertypes and thresholds fromkoder_kitrather than defining its own. - R9 — Single vocabulary. Setting names, labels, preference values, and
GPU-class names (
igpu/dgpu) are defined once inkoder_kitand reused by every backend and UI. No backend invents its own names. - R10 — Telemetry (observability-first). Each routing decision and power transition MUST emit a structured signal (surface id, chosen GPU, trigger reason, D3 enter/exit) with bounded cardinality; never log PII. Backend + reason are labels, not free text.
- R11 — Stability carve-outs (platform). Backends MUST honor host
stability constraints recorded for the platform — e.g. on the owner's Avell
laptop, NEVER force
nvidia-drm.modeset=1(freezes boot with external monitors; standing alert) and prefer the documented offload path. A backend MUST degrade to iGPU-only rather than risk a known-unstable path. - R12 — Out of scope (permanent). Per-element routing across two physical GPUs is forbidden (cross-PCIe per-frame copy negates the win). Per-element routing within one GPU is the engine's native compositor behavior and is not governed here.
3. Per-engine backends (informative — mechanism lives at the engine)
| Engine | Component | Mechanism (conforms to R8) |
|---|---|---|
| Chromium/CEF | Kruze (KRUZE-277) | GPU-process pinning: __NV_PRIME_RENDER_OFFLOAD/DRI_PRIME (Linux), adapter LUID (Windows), native powerPreference (macOS) |
| wgpu/Vello | Kroma (engines/sdk/kroma) → all native apps | wgpu adapter selection (PowerPreference + enumeration) |
| HW decode | video players (mpv etc.) | VA-API/NVDEC decode-device selection |
Note (verified 2026-06-27): on Linux/X11 the WebGL powerPreference page hint is
ignored by the browser — physical-GPU choice happens at GPU-process spawn, so the
Chromium backend MUST route via process env, not the page hint.
4. Tests
- T1 (R1): single-GPU host → no routing decisions emitted, no power transitions, no GPU UI shown.
- T2 (R4): a surface that fires a canonical signal (e.g. WebGPU
high-performance adapter) is routed to the dGPU under
auto; one that fires none stays on iGPU. - T3 (R5): dGPU is in D3 with only iGPU surfaces open; opening a dGPU surface wakes it; closing the last dGPU surface returns it to D3 within the idle window. (Run on the s.khost1 GPU seat, never on owner hardware — heavy-work-isolation.)
- T4 (R3): a per-surface override persists across restart and beats the heuristic.
- T5 (R9): two different backends report the same preference value/label strings (consumed from koder_kit), not divergent ones.
- T6 (R11): a backend on a host with a known-unstable dGPU path degrades to iGPU-only instead of taking it.
5. Adoption
S1 (this spec + koder_kit types/heuristic) → koder_kit#101. Backends: Kruze
(KRUZE-277), Kroma, video — each implements §3 against these rules. The contract
is normative now; backends conform as they land.