diff --git a/bindings/python/src/transcribe_cpp/__init__.py b/bindings/python/src/transcribe_cpp/__init__.py index 2398aaf4..375c7fd3 100644 --- a/bindings/python/src/transcribe_cpp/__init__.py +++ b/bindings/python/src/transcribe_cpp/__init__.py @@ -270,7 +270,8 @@ class BackendDevice: # comparable across device kinds. memory_free: int # Registry index of this device — the value to pass as ``Model(..., - # gpu_device=index)`` to select it (0 selects the auto / first device). + # gpu_device=index)`` to select it (0 means auto: discrete GPUs are + # probed before integrated). # None when the device came from Model.device, since the underlying # transcribe_model_get_device() does not expose an index; correlate such a # device back to backends() by device_id / name instead. The index is diff --git a/bindings/rust/transcribe-cpp/src/backend.rs b/bindings/rust/transcribe-cpp/src/backend.rs index f28e4bb8..74941855 100644 --- a/bindings/rust/transcribe-cpp/src/backend.rs +++ b/bindings/rust/transcribe-cpp/src/backend.rs @@ -75,11 +75,11 @@ pub struct Device { pub memory_free: u64, /// Registry index of this device — the value to pass as /// [`ModelOptions::gpu_device`](crate::ModelOptions) to select it (0 - /// selects the auto / first device). `None` when this `Device` came from - /// [`crate::Model::device`], since `transcribe_model_get_device` does not - /// expose an index; correlate such a device back to [`devices`] by - /// `device_id` / `name` instead. Order-dependent and not stable across - /// driver updates or hosts. + /// means auto: discrete GPUs are probed before integrated). `None` when + /// this `Device` came from [`crate::Model::device`], since + /// `transcribe_model_get_device` does not expose an index; correlate such + /// a device back to [`devices`] by `device_id` / `name` instead. + /// Order-dependent and not stable across driver updates or hosts. pub index: Option, } diff --git a/bindings/rust/transcribe-cpp/src/model.rs b/bindings/rust/transcribe-cpp/src/model.rs index 5a54260d..10c14cd9 100644 --- a/bindings/rust/transcribe-cpp/src/model.rs +++ b/bindings/rust/transcribe-cpp/src/model.rs @@ -32,7 +32,8 @@ use crate::version; pub struct ModelOptions { /// Which backend to request. Default [`Backend::Auto`]. pub backend: Backend, - /// GPU device registry index. 0 means auto / first matching device. + /// GPU device registry index. 0 means auto: the first device that + /// initializes, probing discrete GPUs before integrated. pub gpu_device: i32, } diff --git a/bindings/typescript/src/types.ts b/bindings/typescript/src/types.ts index 12d65131..a16e2b3a 100644 --- a/bindings/typescript/src/types.ts +++ b/bindings/typescript/src/types.ts @@ -104,18 +104,20 @@ export interface BackendInfo { * to refresh; backend-defined and not comparable across device kinds. */ memoryFree: number; /** Registry index of this device — the value to pass as - * {@link ModelOptions.gpuDevice} to select it (0 selects the auto / first - * device). `null` when this came from `model.device`, since - * `transcribe_model_get_device` does not expose an index; correlate such a - * device back to {@link getAvailableBackends} by `deviceId` / `name` - * instead. Order-dependent and not stable across driver updates or hosts. */ + * {@link ModelOptions.gpuDevice} to select it (0 means auto: discrete + * GPUs are probed before integrated). `null` when this came from + * `model.device`, since `transcribe_model_get_device` does not expose an + * index; correlate such a device back to {@link getAvailableBackends} by + * `deviceId` / `name` instead. Order-dependent and not stable across + * driver updates or hosts. */ index: number | null; } export interface ModelOptions { /** "auto" (default), or an explicit backend. */ backend?: Backend; - /** GPU device registry index. 0 means auto / first matching device. */ + /** GPU device registry index. 0 means auto: the first device that + * initializes, probing discrete GPUs before integrated. */ gpuDevice?: number; } diff --git a/include/transcribe.h b/include/transcribe.h index c79b02e9..40db25d5 100644 --- a/include/transcribe.h +++ b/include/transcribe.h @@ -634,15 +634,17 @@ TRANSCRIBE_API bool transcribe_model_accepts_ext_kind(const struct transcribe_mo /* * Backend request. * - * AUTO Pick the best available backend. Takes the first GPU/IGPU - * device that successfully initializes in ggml's device - * registry order — which is build-time prioritized (Metal on - * Apple, Vulkan / CUDA / SYCL on Linux, …). Host-memory - * accelerators (BLAS, AMX, …) are additionally layered onto - * the scheduler when present — they run on the same memory - * as the CPU backend and are orthogonal to the GPU/CPU split. - * Always succeeds: CPU is the final fallback when no GPU - * initializes. + * AUTO Pick the best available backend. Takes the first GPU device + * that successfully initializes, probing every discrete GPU + * before any integrated GPU; within a tier, devices are tried + * in ggml's device registry order — which is build-time + * prioritized (Metal on Apple, Vulkan / CUDA / SYCL on + * Linux, …). An integrated GPU is selected only when no + * discrete GPU initializes. Host-memory accelerators (BLAS, + * AMX, …) are additionally layered onto the scheduler when + * present — they run on the same memory as the CPU backend + * and are orthogonal to the GPU/CPU split. Always succeeds: + * CPU is the final fallback when no GPU initializes. * * CPU Strict CPU only. No GPU, no IGPU, and no host-memory * accelerators (BLAS/AMX). This is the right choice for @@ -875,9 +877,11 @@ TRANSCRIBE_API transcribe_status transcribe_model_get_device(const struct transc * for the semantics of each value. Default is AUTO. * * gpu_device: Multi-GPU selector. 0 (the default) means "auto / the first - * device of the chosen kind": AUTO picks the first GPU/IGPU in - * ggml's registry order, and explicit METAL/VULKAN/CUDA requests - * pick the first matching device, as before. + * device of the chosen kind": AUTO picks the first GPU that + * initializes, and explicit METAL/VULKAN/CUDA requests pick the + * first matching device — in both cases probing every discrete + * GPU before any integrated GPU, in ggml's registry order + * within each tier. * * A value > 0 selects the GPU/IGPU device at that global ggml * registry index — the same index space transcribe_get_backend_device() @@ -895,8 +899,9 @@ TRANSCRIBE_API transcribe_status transcribe_model_get_device(const struct transc * device whose vendor doesn't match an explicit GPU request, or * is non-zero alongside a CPU / CPU_ACCEL request (there is no * GPU to select). Note there is no way to explicitly select the - * device at registry index 0; that is exactly what AUTO / - * first-of-kind already picks. + * device at registry index 0 — 0 is the auto sentinel. An + * integrated GPU sitting at index 0 is therefore reachable only + * via the probe order, when no discrete GPU initializes. */ struct transcribe_model_load_params { uint64_t struct_size; diff --git a/src/transcribe-backend.cpp b/src/transcribe-backend.cpp index 47a6497b..1be28c45 100644 --- a/src/transcribe-backend.cpp +++ b/src/transcribe-backend.cpp @@ -86,6 +86,22 @@ BackendKind classify_device(ggml_backend_dev_t dev) { return classify_backend_type(dev_type, reg_name); } +std::vector gpu_probe_order(const std::vector & dev_types) { + std::vector order; + order.reserve(dev_types.size()); + for (size_t i = 0; i < dev_types.size(); ++i) { + if (dev_types[i] == GGML_BACKEND_DEVICE_TYPE_GPU) { + order.push_back(i); + } + } + for (size_t i = 0; i < dev_types.size(); ++i) { + if (dev_types[i] == GGML_BACKEND_DEVICE_TYPE_IGPU) { + order.push_back(i); + } + } + return order; +} + namespace { // Shared body for safe_* teardown wrappers. The test hook fires after the diff --git a/src/transcribe-backend.h b/src/transcribe-backend.h index aaea589a..dbea8aed 100644 --- a/src/transcribe-backend.h +++ b/src/transcribe-backend.h @@ -52,6 +52,15 @@ BackendKind classify_backend_type(enum ggml_backend_dev_type dev_type, const cha // vendor. Never returns Unknown for a valid device pointer. BackendKind classify_device(ggml_backend_dev_t dev); +// Probe order for GPU device selection. `dev_types` holds the ggml +// device type of every registry device, indexed by registry position; +// the returned indices are the candidates to try: every discrete GPU +// (registry order) before any integrated GPU (registry order), non-GPU +// devices excluded. Discrete-first matters because Vulkan enumeration +// on hybrid-graphics machines often lists the display iGPU before the +// dGPU. Unit-testable core for try_init_kind. +std::vector gpu_probe_order(const std::vector & dev_types); + // A resolved backend plan. Produced by load_common::init_backends // from a transcribe_backend_request and consumed by every helper // that needs to know where the graph will run. diff --git a/src/transcribe-load-common.cpp b/src/transcribe-load-common.cpp index df148232..74fc0f61 100644 --- a/src/transcribe-load-common.cpp +++ b/src/transcribe-load-common.cpp @@ -85,30 +85,31 @@ ggml_backend_t dev_init_checked(ggml_backend_dev_t dev, const char * error_tag) } // Try to discover and initialize the first device whose classified -// BackendKind matches `wanted`. On success returns the initialized -// backend and writes the classified kind of the device that actually -// succeeded to out_kind (which may differ from `wanted` when -// `wanted == BackendKind::OtherGpu`). Returns nullptr if no matching -// device initializes. +// BackendKind matches `wanted`, visiting candidates in gpu_probe_order +// (every discrete GPU before any integrated GPU). On success returns +// the initialized backend and writes the classified kind of the device +// that actually succeeded to out_kind (which may differ from `wanted` +// when `wanted == BackendKind::OtherGpu`). Returns nullptr if no +// matching device initializes. // -// When `wanted == BackendKind::OtherGpu`, this acts as a "first GPU -// or IGPU of any vendor" probe — used by the AUTO path. Critically, -// out_kind is derived from the device that actually initialized, not -// from a separate post-hoc registry walk, so a failing first-GPU -// followed by a succeeding second-GPU yields the correct kind. +// When `wanted == BackendKind::OtherGpu`, this acts as an "any GPU of +// any vendor" probe — used by the AUTO path. Critically, out_kind is +// derived from the device that actually initialized, not from a +// separate post-hoc registry walk, so a failing first-GPU followed by +// a succeeding second-GPU yields the correct kind. ggml_backend_t try_init_kind(BackendKind wanted, const char * error_tag, BackendKind & out_kind) { const size_t n = ggml_backend_dev_count(); + + std::vector dev_types; + dev_types.reserve(n); for (size_t i = 0; i < n; ++i) { + dev_types.push_back(ggml_backend_dev_type(ggml_backend_dev_get(i))); + } + + for (const size_t i : gpu_probe_order(dev_types)) { ggml_backend_dev_t dev = ggml_backend_dev_get(i); - if (wanted == BackendKind::OtherGpu) { - // "Any GPU" probe: accept the first GPU/IGPU device, - // regardless of vendor. - const auto dev_type = ggml_backend_dev_type(dev); - if (dev_type != GGML_BACKEND_DEVICE_TYPE_GPU && dev_type != GGML_BACKEND_DEVICE_TYPE_IGPU) { - continue; - } - } else if (classify_device(dev) != wanted) { + if (wanted != BackendKind::OtherGpu && classify_device(dev) != wanted) { continue; } @@ -389,12 +390,10 @@ transcribe_status init_backends(transcribe_backend_request requested, case TRANSCRIBE_BACKEND_AUTO: { - // AUTO: take the first GPU/IGPU device that successfully - // initializes, regardless of vendor. ggml registers devices - // in build-time priority order (Metal on Apple, Vulkan on - // Linux, etc.), which matches the documented preference. - // If every GPU fails init or none is compiled in, fall - // through to CPU + ACCEL. + // AUTO: take the first GPU device that successfully + // initializes, regardless of vendor, in gpu_probe_order + // (discrete before integrated). If every GPU fails init + // or none is compiled in, fall through to CPU + ACCEL. // // try_init_kind yields the classified kind of the device // that actually succeeded, so a failing-then-succeeding probe diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index dcf5c11f..41b23efc 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -101,6 +101,27 @@ transcribe_apply_warnings(transcribe_backend_classification_unit) add_test(NAME transcribe_backend_classification_unit COMMAND transcribe_backend_classification_unit) +# ----------------------------------------------------------------------------- +# Backend GPU probe order unit test +# ----------------------------------------------------------------------------- +# +# Covers the pure discrete-before-integrated GPU ordering used by the +# runtime backend selector, so the policy stays tested without requiring +# a hybrid-graphics (iGPU + dGPU) machine in CI. + +add_executable(transcribe_backend_probe_order_unit + backend_probe_order_unit.cpp) + +target_link_libraries(transcribe_backend_probe_order_unit PRIVATE transcribe ggml) + +target_include_directories(transcribe_backend_probe_order_unit PRIVATE + ${CMAKE_SOURCE_DIR}/src) + +transcribe_apply_warnings(transcribe_backend_probe_order_unit) + +add_test(NAME transcribe_backend_probe_order_unit + COMMAND transcribe_backend_probe_order_unit) + # ----------------------------------------------------------------------------- # Backend init_backends() runtime behavior # ----------------------------------------------------------------------------- diff --git a/tests/backend_probe_order_unit.cpp b/tests/backend_probe_order_unit.cpp new file mode 100644 index 00000000..2ce026af --- /dev/null +++ b/tests/backend_probe_order_unit.cpp @@ -0,0 +1,64 @@ +// backend_probe_order_unit.cpp - unit tests for GPU probe ordering. +// +// The runtime backend selector probes GPU candidates in gpu_probe_order: +// every discrete GPU before any integrated GPU, preserving registry order +// within each tier. This test covers the pure ordering core so the +// discrete-before-integrated policy stays tested without requiring a +// hybrid-graphics machine in CI. + +#include "transcribe-backend.h" + +#include +#include +#include + +namespace { + +int g_failures = 0; + +#define CHECK_ORDER(types, expected) \ + do { \ + const std::vector _got = transcribe::gpu_probe_order(types); \ + const std::vector _exp = (expected); \ + if (_got != _exp) { \ + std::fprintf(stderr, "FAIL %s:%d: got {", __FILE__, __LINE__); \ + for (size_t v : _got) { \ + std::fprintf(stderr, " %zu", v); \ + } \ + std::fprintf(stderr, " }, expected {"); \ + for (size_t v : _exp) { \ + std::fprintf(stderr, " %zu", v); \ + } \ + std::fprintf(stderr, " }\n"); \ + ++g_failures; \ + } \ + } while (0) + +} // namespace + +int main() { + using DevTypes = std::vector; + using Order = std::vector; + + // No devices, and no GPU devices at all. + CHECK_ORDER(DevTypes{}, Order{}); + CHECK_ORDER((DevTypes{ GGML_BACKEND_DEVICE_TYPE_CPU, GGML_BACKEND_DEVICE_TYPE_ACCEL }), Order{}); + + // Single-GPU machines: the sole GPU is the only candidate, + // whichever tier it is in. + CHECK_ORDER((DevTypes{ GGML_BACKEND_DEVICE_TYPE_GPU, GGML_BACKEND_DEVICE_TYPE_CPU }), (Order{ 0 })); + CHECK_ORDER((DevTypes{ GGML_BACKEND_DEVICE_TYPE_CPU, GGML_BACKEND_DEVICE_TYPE_IGPU }), (Order{ 1 })); + + // The hybrid-graphics case that motivated the tiering: the iGPU + // enumerates first, but the discrete GPU must be probed first. + CHECK_ORDER((DevTypes{ GGML_BACKEND_DEVICE_TYPE_IGPU, GGML_BACKEND_DEVICE_TYPE_GPU }), (Order{ 1, 0 })); + CHECK_ORDER((DevTypes{ GGML_BACKEND_DEVICE_TYPE_IGPU, GGML_BACKEND_DEVICE_TYPE_CPU, GGML_BACKEND_DEVICE_TYPE_GPU }), + (Order{ 2, 0 })); + + // Registry order is preserved within each tier. + CHECK_ORDER((DevTypes{ GGML_BACKEND_DEVICE_TYPE_GPU, GGML_BACKEND_DEVICE_TYPE_IGPU, GGML_BACKEND_DEVICE_TYPE_GPU, + GGML_BACKEND_DEVICE_TYPE_CPU, GGML_BACKEND_DEVICE_TYPE_IGPU }), + (Order{ 0, 2, 1, 4 })); + + return g_failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE; +}