Skip to content

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

All triggers

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/dgpu are 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 (auto only). A surface is routed to the dGPU when ANY canonical signal fires: a WebGL/WebGPU context requested high-performance; a WebGPU requestAdapter({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/GpuRouter types and thresholds from koder_kit rather than defining its own.
  • R9 — Single vocabulary. Setting names, labels, preference values, and GPU-class names (igpu/dgpu) are defined once in koder_kit and 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)

EngineComponentMechanism (conforms to R8)
Chromium/CEFKruze (KRUZE-277)GPU-process pinning: __NV_PRIME_RENDER_OFFLOAD/DRI_PRIME (Linux), adapter LUID (Windows), native powerPreference (macOS)
wgpu/VelloKroma (engines/sdk/kroma) → all native appswgpu adapter selection (PowerPreference + enumeration)
HW decodevideo 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.