From 9ab4f3eca372a03b5e21082657d48f6fcc60a47c Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Tue, 7 Jul 2026 19:46:02 +0800 Subject: [PATCH 1/3] gate auto based on metal simdgroup, no -> cpu, yes -> metal --- src/CMakeLists.txt | 11 ++ src/transcribe-load-common.cpp | 73 ++++++++ src/transcribe-load-common.h | 15 ++ tests/CMakeLists.txt | 17 ++ tests/backend_metal_simdgroup_gate_unit.cpp | 174 ++++++++++++++++++++ 5 files changed, 290 insertions(+) create mode 100644 tests/backend_metal_simdgroup_gate_unit.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e8b9ef13..4fcc5b62 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -151,6 +151,17 @@ if(TRANSCRIBE_GGML_BACKEND_DL) endif() endif() +# Metal is Apple-only and, in the default build, statically linked into ggml. +# Expose a define so the loader can call the Metal-specific capability query +# that gates AUTO device selection against GPUs with no simdgroup matrix +# multiply (pre-Apple7 Intel/AMD GPUs), which otherwise produce garbage +# transcripts (Handy issue #1608). Under GGML_BACKEND_DL the metal symbols +# live in a loadable module and are not link-visible, so the loader compiles +# the query out (keyed off TRANSCRIBE_GGML_BACKEND_DL). +if(GGML_METAL) + target_compile_definitions(transcribe PRIVATE TRANSCRIBE_HAS_METAL=1) +endif() + if(TRANSCRIBE_ENABLE_VALIDATION_HOOKS) target_compile_definitions(transcribe PRIVATE TRANSCRIBE_ENABLE_VALIDATION_HOOKS) endif() diff --git a/src/transcribe-load-common.cpp b/src/transcribe-load-common.cpp index f1205905..2913cf57 100644 --- a/src/transcribe-load-common.cpp +++ b/src/transcribe-load-common.cpp @@ -9,6 +9,13 @@ #include "ggml-backend.h" #include "ggml.h" #include "gguf.h" +// Metal-specific capability query, used only to gate AUTO device selection +// (see metal_backend_lacks_simdgroup_mm). Available only when the Metal +// backend is statically linked; under GGML_BACKEND_DL the symbol lives in a +// loadable module and is not link-visible here, so the query is compiled out. +#if defined(TRANSCRIBE_HAS_METAL) && !defined(TRANSCRIBE_GGML_BACKEND_DL) +# include "ggml-metal.h" +#endif #include "transcribe-backend.h" #include "transcribe-log.h" #include "transcribe-path.h" @@ -24,6 +31,40 @@ namespace transcribe::load_common { +// Some Metal devices cannot do simdgroup matrix multiply — every GPU below +// MTLGPUFamilyApple7, which on macOS means Intel integrated GPUs and AMD GPUs +// on Intel Macs. On whisper-class, matmul-heavy graphs these devices run the +// vector fallback matmul kernels, which on this hardware silently produce +// garbage: the decoder collapses onto token 0 and emits an unbroken run of +// "!" at ~0.01x realtime (Handy issue #1608). The signal is Metal-specific — +// no generic ggml capability exposes it, since MUL_MAT's supports_op gates on +// simdgroup *reduction*, which these GPUs do report — so the query is compiled +// in only for a statically-linked Metal backend and is a no-op otherwise. +// +// has_simdgroup_mm is `supportsFamily:MTLGPUFamilyApple7`; the public metal +// query indexes Apple families 1-based, so Apple7 is family 7. +bool metal_backend_lacks_simdgroup_mm(ggml_backend_t be, ggml_backend_dev_t dev) { + // Test hook: force the "missing" verdict for a device whose name matches, + // so the fallback path can be exercised on hardware that actually has + // simdgroup mm. Inert when unset/empty; "*" matches any device. + if (const char * match = std::getenv("TRANSCRIBE_TEST_METAL_NO_SIMDGROUP_MM"); + match != nullptr && match[0] != '\0') { + const char * name = ggml_backend_dev_name(dev); + if (std::strcmp(match, "*") == 0 || (name != nullptr && std::strstr(name, match) != nullptr)) { + return true; + } + } +#if defined(TRANSCRIBE_HAS_METAL) && !defined(TRANSCRIBE_GGML_BACKEND_DL) + // ggml_backend_metal_supports_family asserts the backend is Metal; guard + // so a classify/is_metal disagreement can never abort the process. + if (ggml_backend_is_metal(be)) { + return !ggml_backend_metal_supports_family(be, /*MTLGPUFamilyApple7=*/7); + } +#endif + (void) be; + return false; +} + namespace { // Device init can throw from GPU drivers. Treat that like nullptr so AUTO can @@ -87,6 +128,28 @@ ggml_backend_t try_init_kind(BackendKind wanted, const char * error_tag, Backend } const BackendKind kind = classify_device(dev); + + // Reject a Metal device that can't do simdgroup matrix multiply: it + // yields garbage transcripts on whisper-class graphs (see + // metal_backend_lacks_simdgroup_mm). Under AUTO ("any GPU") skip it so + // selection falls through to CPU; on an explicit Metal request, honour + // the device the caller named but warn loudly. + if (kind == BackendKind::Metal && metal_backend_lacks_simdgroup_mm(be, dev)) { + const char * dname = ggml_backend_dev_name(dev); + if (wanted == BackendKind::OtherGpu) { + log_msg(TRANSCRIBE_LOG_LEVEL_WARN, + "%s: skipping Metal device \"%s\": no simdgroup matrix multiply " + "(pre-Apple7 GPU) — falling back to CPU", + error_tag, dname != nullptr ? dname : "?"); + safe_backend_free(be); + continue; + } + log_msg(TRANSCRIBE_LOG_LEVEL_WARN, + "%s: Metal device \"%s\" has no simdgroup matrix multiply " + "(pre-Apple7 GPU); transcription may be incorrect and very slow", + error_tag, dname != nullptr ? dname : "?"); + } + log_msg(TRANSCRIBE_LOG_LEVEL_INFO, "%s: using %s backend: %s", error_tag, kind_name(kind), ggml_backend_dev_name(dev)); out_kind = kind; @@ -200,6 +263,16 @@ transcribe_status init_backends_explicit_index(transcribe_backend_request reques log_msg(TRANSCRIBE_LOG_LEVEL_INFO, "%s: using %s backend (gpu_device %d): %s", error_tag, kind_name(got), dev_index, ggml_backend_dev_name(dev)); + // Same gate as try_init_kind, but an explicit device index — like an + // explicit Metal request — is always honored: warn only. + if (got == BackendKind::Metal && metal_backend_lacks_simdgroup_mm(gpu_be, dev)) { + const char * dname = ggml_backend_dev_name(dev); + log_msg(TRANSCRIBE_LOG_LEVEL_WARN, + "%s: Metal device \"%s\" has no simdgroup matrix multiply " + "(pre-Apple7 GPU); transcription may be incorrect and very slow", + error_tag, dname != nullptr ? dname : "?"); + } + out.primary = gpu_be; out.primary_kind = got; out.scheduler_list.push_back(gpu_be); diff --git a/src/transcribe-load-common.h b/src/transcribe-load-common.h index 1d548fef..e7fa1051 100644 --- a/src/transcribe-load-common.h +++ b/src/transcribe-load-common.h @@ -26,12 +26,27 @@ struct ggml_backend; typedef struct ggml_backend * ggml_backend_t; struct ggml_backend_buffer; typedef struct ggml_backend_buffer * ggml_backend_buffer_t; +struct ggml_backend_device; +typedef struct ggml_backend_device * ggml_backend_dev_t; struct ggml_context; struct ggml_tensor; struct gguf_context; namespace transcribe::load_common { +// True when `dev` is a Metal device with no simdgroup matrix multiply +// (below MTLGPUFamilyApple7 — Intel iGPUs and AMD dGPUs on Intel Macs). +// Such devices silently produce garbage transcripts (Handy issue #1608), +// so init_backends skips them under AUTO and warns on any explicit +// selection. `be` must be the initialized backend for `dev`. +// +// Honors the TRANSCRIBE_TEST_METAL_NO_SIMDGROUP_MM hook (device-name +// substring match; "*" matches all). Returns false for non-Metal backends +// and when the Metal query is compiled out (non-Metal or GGML_BACKEND_DL +// builds). Exposed so the unit test can align its baseline expectations +// with the real hardware verdict instead of assuming Metal always wins. +bool metal_backend_lacks_simdgroup_mm(ggml_backend_t be, ggml_backend_dev_t dev); + // Resolve a BackendPlan from a caller's backend request by walking // ggml's device registry. // diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b35bb291..dcf5c11f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -267,6 +267,23 @@ transcribe_apply_warnings(transcribe_backend_init_throw_unit) add_test(NAME transcribe_backend_init_throw_unit COMMAND transcribe_backend_init_throw_unit) +# ----------------------------------------------------------------------------- +# Pins the AUTO-selection gate that skips a Metal device with no simdgroup +# matrix multiply (pre-Apple7 GPUs) and falls back to CPU (Handy issue #1608). + +add_executable(transcribe_backend_metal_simdgroup_gate_unit + backend_metal_simdgroup_gate_unit.cpp) + +target_link_libraries(transcribe_backend_metal_simdgroup_gate_unit PRIVATE transcribe ggml) + +target_include_directories(transcribe_backend_metal_simdgroup_gate_unit PRIVATE + ${CMAKE_SOURCE_DIR}/src) + +transcribe_apply_warnings(transcribe_backend_metal_simdgroup_gate_unit) + +add_test(NAME transcribe_backend_metal_simdgroup_gate_unit + COMMAND transcribe_backend_metal_simdgroup_gate_unit) + # ----------------------------------------------------------------------------- # No-throw teardown wrappers (safe_backend_free & co.) # ----------------------------------------------------------------------------- diff --git a/tests/backend_metal_simdgroup_gate_unit.cpp b/tests/backend_metal_simdgroup_gate_unit.cpp new file mode 100644 index 00000000..fddb93a1 --- /dev/null +++ b/tests/backend_metal_simdgroup_gate_unit.cpp @@ -0,0 +1,174 @@ +// backend_metal_simdgroup_gate_unit.cpp - AUTO device selection skips a Metal +// device that lacks simdgroup matrix multiply. +// +// A pre-Apple7 Metal GPU (Intel integrated / AMD on Intel Macs) runs +// whisper-class matmul graphs through vector fallback kernels that silently +// produce garbage ("!!!!!") at ~0.01x realtime (Handy issue #1608). The loader +// gates it via metal_backend_lacks_simdgroup_mm; here we force that verdict +// with the TRANSCRIBE_TEST_METAL_NO_SIMDGROUP_MM hook and pin the behavior: +// +// AUTO -> skip the gated Metal device and fall back to CPU. +// explicit METAL -> honor the device the caller named (warn only). +// hook unset / non-matching -> selection matches the real hardware verdict. +// +// The baselines can't assume "Metal always wins": on an actual pre-Apple7 +// Intel Mac the real gate fires with the hook unset, so AUTO lands on CPU +// there. main() probes the hardware verdict once (hook cleared) and the +// baselines assert against it. +// +// Every case is a no-op on machines with no Metal device (e.g. Linux CI), so +// the test passes trivially where the gate can't apply. + +#include "ggml-backend.h" +#include "transcribe-backend.h" +#include "transcribe-load-common.h" +#include "transcribe.h" + +#include +#include + +namespace { + +int g_failures = 0; + +#define CHECK(cond) \ + do { \ + if (!(cond)) { \ + std::fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + ++g_failures; \ + } \ + } while (0) + +void set_env(const char * key, const char * value) { +#if defined(_WIN32) + _putenv_s(key, value); +#else + setenv(key, value, 1); +#endif +} + +void unset_env(const char * key) { +#if defined(_WIN32) + _putenv_s(key, ""); +#else + unsetenv(key); +#endif +} + +void free_plan(transcribe::BackendPlan & plan) { + for (auto it = plan.scheduler_list.rbegin(); it != plan.scheduler_list.rend(); ++it) { + ggml_backend_free(*it); + } + plan = transcribe::BackendPlan{}; +} + +// The real hardware verdict for the first Metal device, probed once in +// main() with the hook cleared. On simdgroup-mm hardware (Apple Silicon) +// an unhooked AUTO must pick Metal; on gated hardware (pre-Apple7 GPUs) +// the real gate fires and AUTO must land on CPU — the same outcome the +// forced-hook case pins. +enum class MetalProbe { None, HasSimdgroupMM, LacksSimdgroupMM }; + +MetalProbe g_probe = MetalProbe::None; + +MetalProbe probe_metal_device() { + const size_t n = ggml_backend_dev_count(); + for (size_t i = 0; i < n; ++i) { + ggml_backend_dev_t dev = ggml_backend_dev_get(i); + if (transcribe::classify_device(dev) != transcribe::BackendKind::Metal) { + continue; + } + ggml_backend_t be = ggml_backend_dev_init(dev, nullptr); + if (be == nullptr) { + // A Metal device that can't init never reaches the gate; the + // loader's AUTO probe skips it the same way. Treat as absent. + return MetalProbe::None; + } + const bool lacks = transcribe::load_common::metal_backend_lacks_simdgroup_mm(be, dev); + ggml_backend_free(be); + return lacks ? MetalProbe::LacksSimdgroupMM : MetalProbe::HasSimdgroupMM; + } + return MetalProbe::None; +} + +transcribe::BackendKind expected_auto_kind() { + return g_probe == MetalProbe::LacksSimdgroupMM ? transcribe::BackendKind::Cpu : transcribe::BackendKind::Metal; +} + +// Baseline: with no hook, AUTO selection matches the hardware verdict. +void test_hook_unset_matches_hardware_verdict() { + if (g_probe == MetalProbe::None) { + return; + } + unset_env("TRANSCRIBE_TEST_METAL_NO_SIMDGROUP_MM"); + transcribe::BackendPlan plan; + CHECK(transcribe::load_common::init_backends(TRANSCRIBE_BACKEND_AUTO, 0, "test", plan) == TRANSCRIBE_OK); + CHECK(plan.primary_kind == expected_auto_kind()); + free_plan(plan); +} + +// A non-matching hook value leaves selection unchanged. +void test_nonmatching_hook_is_inert() { + if (g_probe == MetalProbe::None) { + return; + } + set_env("TRANSCRIBE_TEST_METAL_NO_SIMDGROUP_MM", "no-such-device-name-xyzzy"); + transcribe::BackendPlan plan; + CHECK(transcribe::load_common::init_backends(TRANSCRIBE_BACKEND_AUTO, 0, "test", plan) == TRANSCRIBE_OK); + CHECK(plan.primary_kind == expected_auto_kind()); + free_plan(plan); + unset_env("TRANSCRIBE_TEST_METAL_NO_SIMDGROUP_MM"); +} + +// The fix: AUTO skips a simdgroup-mm-less Metal device and lands on CPU. +void test_auto_skips_gated_metal_and_falls_back_to_cpu() { + if (g_probe == MetalProbe::None) { + return; + } + set_env("TRANSCRIBE_TEST_METAL_NO_SIMDGROUP_MM", "*"); + transcribe::BackendPlan plan; + const transcribe_status st = transcribe::load_common::init_backends(TRANSCRIBE_BACKEND_AUTO, 0, "test", plan); + CHECK(st == TRANSCRIBE_OK); + CHECK(plan.primary != nullptr); + CHECK(plan.primary_kind == transcribe::BackendKind::Cpu); + free_plan(plan); + unset_env("TRANSCRIBE_TEST_METAL_NO_SIMDGROUP_MM"); +} + +// An explicit Metal request is an override: the gated device is honored +// (the loader warns) rather than turning into a hard failure. +void test_explicit_metal_is_honored_despite_gate() { + if (g_probe == MetalProbe::None) { + return; + } + set_env("TRANSCRIBE_TEST_METAL_NO_SIMDGROUP_MM", "*"); + transcribe::BackendPlan plan; + const transcribe_status st = transcribe::load_common::init_backends(TRANSCRIBE_BACKEND_METAL, 0, "test", plan); + CHECK(st == TRANSCRIBE_OK); + CHECK(plan.primary_kind == transcribe::BackendKind::Metal); + free_plan(plan); + unset_env("TRANSCRIBE_TEST_METAL_NO_SIMDGROUP_MM"); +} + +} // namespace + +int main() { + transcribe_init_backends_default(); + + // The hook may leak in from the caller's environment; clear it so the + // probe and the baselines see the real hardware verdict. + unset_env("TRANSCRIBE_TEST_METAL_NO_SIMDGROUP_MM"); + g_probe = probe_metal_device(); + + test_hook_unset_matches_hardware_verdict(); + test_nonmatching_hook_is_inert(); + test_auto_skips_gated_metal_and_falls_back_to_cpu(); + test_explicit_metal_is_honored_despite_gate(); + + if (g_failures != 0) { + std::fprintf(stderr, "%d check(s) failed\n", g_failures); + return 1; + } + std::printf("ok\n"); + return 0; +} From 23c13ce12259084de55167adc4e755b5a8147044 Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Wed, 8 Jul 2026 16:24:33 +0800 Subject: [PATCH 2/3] comment cleanup pass --- src/CMakeLists.txt | 9 ++--- src/transcribe-load-common.cpp | 40 ++++++++------------- src/transcribe-load-common.h | 17 ++++----- tests/backend_metal_simdgroup_gate_unit.cpp | 36 +++++++------------ 4 files changed, 35 insertions(+), 67 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4fcc5b62..3a2753b1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -151,13 +151,8 @@ if(TRANSCRIBE_GGML_BACKEND_DL) endif() endif() -# Metal is Apple-only and, in the default build, statically linked into ggml. -# Expose a define so the loader can call the Metal-specific capability query -# that gates AUTO device selection against GPUs with no simdgroup matrix -# multiply (pre-Apple7 Intel/AMD GPUs), which otherwise produce garbage -# transcripts (Handy issue #1608). Under GGML_BACKEND_DL the metal symbols -# live in a loadable module and are not link-visible, so the loader compiles -# the query out (keyed off TRANSCRIBE_GGML_BACKEND_DL). +# Lets the loader call the Metal capability query behind the AUTO-selection +# gate (see metal_backend_lacks_simdgroup_mm in transcribe-load-common.cpp). if(GGML_METAL) target_compile_definitions(transcribe PRIVATE TRANSCRIBE_HAS_METAL=1) endif() diff --git a/src/transcribe-load-common.cpp b/src/transcribe-load-common.cpp index 2913cf57..f7efac57 100644 --- a/src/transcribe-load-common.cpp +++ b/src/transcribe-load-common.cpp @@ -9,10 +9,8 @@ #include "ggml-backend.h" #include "ggml.h" #include "gguf.h" -// Metal-specific capability query, used only to gate AUTO device selection -// (see metal_backend_lacks_simdgroup_mm). Available only when the Metal -// backend is statically linked; under GGML_BACKEND_DL the symbol lives in a -// loadable module and is not link-visible here, so the query is compiled out. +// Under GGML_BACKEND_DL the Metal symbols live in a loadable module and are +// not link-visible here, so the capability query below is compiled out. #if defined(TRANSCRIBE_HAS_METAL) && !defined(TRANSCRIBE_GGML_BACKEND_DL) # include "ggml-metal.h" #endif @@ -31,22 +29,14 @@ namespace transcribe::load_common { -// Some Metal devices cannot do simdgroup matrix multiply — every GPU below -// MTLGPUFamilyApple7, which on macOS means Intel integrated GPUs and AMD GPUs -// on Intel Macs. On whisper-class, matmul-heavy graphs these devices run the -// vector fallback matmul kernels, which on this hardware silently produce -// garbage: the decoder collapses onto token 0 and emits an unbroken run of -// "!" at ~0.01x realtime (Handy issue #1608). The signal is Metal-specific — -// no generic ggml capability exposes it, since MUL_MAT's supports_op gates on -// simdgroup *reduction*, which these GPUs do report — so the query is compiled -// in only for a statically-linked Metal backend and is a no-op otherwise. -// -// has_simdgroup_mm is `supportsFamily:MTLGPUFamilyApple7`; the public metal -// query indexes Apple families 1-based, so Apple7 is family 7. +// GPUs below MTLGPUFamilyApple7 (Intel iGPUs, AMD dGPUs on Intel Macs) have +// no simdgroup matrix multiply; ggml's fallback matmul kernels silently +// produce garbage transcripts there (Handy issue #1608). No generic ggml +// capability exposes this — MUL_MAT's supports_op only needs simdgroup +// *reduction*, which these GPUs do report — so ask Metal directly. bool metal_backend_lacks_simdgroup_mm(ggml_backend_t be, ggml_backend_dev_t dev) { - // Test hook: force the "missing" verdict for a device whose name matches, - // so the fallback path can be exercised on hardware that actually has - // simdgroup mm. Inert when unset/empty; "*" matches any device. + // Test hook: force the "missing" verdict by device-name substring + // ("*" matches any) so the gate can be exercised on healthy hardware. if (const char * match = std::getenv("TRANSCRIBE_TEST_METAL_NO_SIMDGROUP_MM"); match != nullptr && match[0] != '\0') { const char * name = ggml_backend_dev_name(dev); @@ -129,11 +119,9 @@ ggml_backend_t try_init_kind(BackendKind wanted, const char * error_tag, Backend const BackendKind kind = classify_device(dev); - // Reject a Metal device that can't do simdgroup matrix multiply: it - // yields garbage transcripts on whisper-class graphs (see - // metal_backend_lacks_simdgroup_mm). Under AUTO ("any GPU") skip it so - // selection falls through to CPU; on an explicit Metal request, honour - // the device the caller named but warn loudly. + // A Metal device without simdgroup matmul yields garbage transcripts + // (see metal_backend_lacks_simdgroup_mm): skip it under AUTO, honor + // an explicit Metal request with a warning. if (kind == BackendKind::Metal && metal_backend_lacks_simdgroup_mm(be, dev)) { const char * dname = ggml_backend_dev_name(dev); if (wanted == BackendKind::OtherGpu) { @@ -263,8 +251,8 @@ transcribe_status init_backends_explicit_index(transcribe_backend_request reques log_msg(TRANSCRIBE_LOG_LEVEL_INFO, "%s: using %s backend (gpu_device %d): %s", error_tag, kind_name(got), dev_index, ggml_backend_dev_name(dev)); - // Same gate as try_init_kind, but an explicit device index — like an - // explicit Metal request — is always honored: warn only. + // An explicit device index is always honored: warn only (same gate as + // try_init_kind). if (got == BackendKind::Metal && metal_backend_lacks_simdgroup_mm(gpu_be, dev)) { const char * dname = ggml_backend_dev_name(dev); log_msg(TRANSCRIBE_LOG_LEVEL_WARN, diff --git a/src/transcribe-load-common.h b/src/transcribe-load-common.h index e7fa1051..fd0f72f2 100644 --- a/src/transcribe-load-common.h +++ b/src/transcribe-load-common.h @@ -34,17 +34,12 @@ struct gguf_context; namespace transcribe::load_common { -// True when `dev` is a Metal device with no simdgroup matrix multiply -// (below MTLGPUFamilyApple7 — Intel iGPUs and AMD dGPUs on Intel Macs). -// Such devices silently produce garbage transcripts (Handy issue #1608), -// so init_backends skips them under AUTO and warns on any explicit -// selection. `be` must be the initialized backend for `dev`. -// -// Honors the TRANSCRIBE_TEST_METAL_NO_SIMDGROUP_MM hook (device-name -// substring match; "*" matches all). Returns false for non-Metal backends -// and when the Metal query is compiled out (non-Metal or GGML_BACKEND_DL -// builds). Exposed so the unit test can align its baseline expectations -// with the real hardware verdict instead of assuming Metal always wins. +// True when `dev` is a Metal device with no simdgroup matrix multiply (below +// MTLGPUFamilyApple7), which silently produces garbage transcripts (Handy +// issue #1608). `be` must be the initialized backend for `dev`. Honors the +// TRANSCRIBE_TEST_METAL_NO_SIMDGROUP_MM hook; returns false when the Metal +// query is compiled out (non-Metal or GGML_BACKEND_DL builds). Exposed for +// the gate unit test. bool metal_backend_lacks_simdgroup_mm(ggml_backend_t be, ggml_backend_dev_t dev); // Resolve a BackendPlan from a caller's backend request by walking diff --git a/tests/backend_metal_simdgroup_gate_unit.cpp b/tests/backend_metal_simdgroup_gate_unit.cpp index fddb93a1..b3485ba2 100644 --- a/tests/backend_metal_simdgroup_gate_unit.cpp +++ b/tests/backend_metal_simdgroup_gate_unit.cpp @@ -1,23 +1,16 @@ // backend_metal_simdgroup_gate_unit.cpp - AUTO device selection skips a Metal -// device that lacks simdgroup matrix multiply. +// device that lacks simdgroup matrix multiply (Handy issue #1608). // -// A pre-Apple7 Metal GPU (Intel integrated / AMD on Intel Macs) runs -// whisper-class matmul graphs through vector fallback kernels that silently -// produce garbage ("!!!!!") at ~0.01x realtime (Handy issue #1608). The loader -// gates it via metal_backend_lacks_simdgroup_mm; here we force that verdict -// with the TRANSCRIBE_TEST_METAL_NO_SIMDGROUP_MM hook and pin the behavior: +// Forces the gate's verdict via the TRANSCRIBE_TEST_METAL_NO_SIMDGROUP_MM +// hook and pins the behavior: // // AUTO -> skip the gated Metal device and fall back to CPU. // explicit METAL -> honor the device the caller named (warn only). -// hook unset / non-matching -> selection matches the real hardware verdict. +// hook unset / non-matching -> selection matches the real hardware verdict, +// probed once in main() (on a real pre-Apple7 +// GPU the gate fires with the hook unset). // -// The baselines can't assume "Metal always wins": on an actual pre-Apple7 -// Intel Mac the real gate fires with the hook unset, so AUTO lands on CPU -// there. main() probes the hardware verdict once (hook cleared) and the -// baselines assert against it. -// -// Every case is a no-op on machines with no Metal device (e.g. Linux CI), so -// the test passes trivially where the gate can't apply. +// Every case is a no-op on machines with no Metal device (e.g. Linux CI). #include "ggml-backend.h" #include "transcribe-backend.h" @@ -62,11 +55,9 @@ void free_plan(transcribe::BackendPlan & plan) { plan = transcribe::BackendPlan{}; } -// The real hardware verdict for the first Metal device, probed once in -// main() with the hook cleared. On simdgroup-mm hardware (Apple Silicon) -// an unhooked AUTO must pick Metal; on gated hardware (pre-Apple7 GPUs) -// the real gate fires and AUTO must land on CPU — the same outcome the -// forced-hook case pins. +// Real hardware verdict for the first Metal device, probed once in main() +// with the hook cleared. Baselines assert against it because on a real +// pre-Apple7 GPU the gate sends AUTO to CPU even with the hook unset. enum class MetalProbe { None, HasSimdgroupMM, LacksSimdgroupMM }; MetalProbe g_probe = MetalProbe::None; @@ -80,8 +71,8 @@ MetalProbe probe_metal_device() { } ggml_backend_t be = ggml_backend_dev_init(dev, nullptr); if (be == nullptr) { - // A Metal device that can't init never reaches the gate; the - // loader's AUTO probe skips it the same way. Treat as absent. + // A Metal device that can't init never reaches the gate; treat + // as absent. return MetalProbe::None; } const bool lacks = transcribe::load_common::metal_backend_lacks_simdgroup_mm(be, dev); @@ -155,8 +146,7 @@ void test_explicit_metal_is_honored_despite_gate() { int main() { transcribe_init_backends_default(); - // The hook may leak in from the caller's environment; clear it so the - // probe and the baselines see the real hardware verdict. + // Clear any inherited hook so the probe sees the real hardware verdict. unset_env("TRANSCRIBE_TEST_METAL_NO_SIMDGROUP_MM"); g_probe = probe_metal_device(); From 4c4559ba657a88778ddaaf771e30129b533e7f5b Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Wed, 8 Jul 2026 16:29:37 +0800 Subject: [PATCH 3/3] theoretical multigpu fix for metal --- src/transcribe-load-common.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transcribe-load-common.cpp b/src/transcribe-load-common.cpp index f7efac57..df148232 100644 --- a/src/transcribe-load-common.cpp +++ b/src/transcribe-load-common.cpp @@ -127,7 +127,7 @@ ggml_backend_t try_init_kind(BackendKind wanted, const char * error_tag, Backend if (wanted == BackendKind::OtherGpu) { log_msg(TRANSCRIBE_LOG_LEVEL_WARN, "%s: skipping Metal device \"%s\": no simdgroup matrix multiply " - "(pre-Apple7 GPU) — falling back to CPU", + "(pre-Apple7 GPU)", error_tag, dname != nullptr ? dname : "?"); safe_backend_free(be); continue;