Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion bindings/python/src/transcribe_cpp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions bindings/rust/transcribe-cpp/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<usize>,
}

Expand Down
3 changes: 2 additions & 1 deletion bindings/rust/transcribe-cpp/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

Expand Down
14 changes: 8 additions & 6 deletions bindings/typescript/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
33 changes: 19 additions & 14 deletions include/transcribe.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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;
Expand Down
16 changes: 16 additions & 0 deletions src/transcribe-backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,22 @@ BackendKind classify_device(ggml_backend_dev_t dev) {
return classify_backend_type(dev_type, reg_name);
}

std::vector<size_t> gpu_probe_order(const std::vector<enum ggml_backend_dev_type> & dev_types) {
std::vector<size_t> 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
Expand Down
9 changes: 9 additions & 0 deletions src/transcribe-backend.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<size_t> gpu_probe_order(const std::vector<enum ggml_backend_dev_type> & 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.
Expand Down
47 changes: 23 additions & 24 deletions src/transcribe-load-common.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<enum ggml_backend_dev_type> 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;
}

Expand Down Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
# -----------------------------------------------------------------------------
Expand Down
64 changes: 64 additions & 0 deletions tests/backend_probe_order_unit.cpp
Original file line number Diff line number Diff line change
@@ -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 <cstdio>
#include <cstdlib>
#include <vector>

namespace {

int g_failures = 0;

#define CHECK_ORDER(types, expected) \
do { \
const std::vector<size_t> _got = transcribe::gpu_probe_order(types); \
const std::vector<size_t> _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<enum ggml_backend_dev_type>;
using Order = std::vector<size_t>;

// 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;
}
Loading