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
6 changes: 6 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,12 @@ if(TRANSCRIBE_GGML_BACKEND_DL)
endif()
endif()

# 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()

if(TRANSCRIBE_ENABLE_VALIDATION_HOOKS)
target_compile_definitions(transcribe PRIVATE TRANSCRIBE_ENABLE_VALIDATION_HOOKS)
endif()
Expand Down
61 changes: 61 additions & 0 deletions src/transcribe-load-common.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
#include "ggml-backend.h"
#include "ggml.h"
#include "gguf.h"
// 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
#include "transcribe-backend.h"
#include "transcribe-log.h"
#include "transcribe-path.h"
Expand All @@ -24,6 +29,32 @@

namespace transcribe::load_common {

// 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 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);
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
Expand Down Expand Up @@ -87,6 +118,26 @@ ggml_backend_t try_init_kind(BackendKind wanted, const char * error_tag, Backend
}

const BackendKind kind = classify_device(dev);

// 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) {
log_msg(TRANSCRIBE_LOG_LEVEL_WARN,
"%s: skipping Metal device \"%s\": no simdgroup matrix multiply "
"(pre-Apple7 GPU)",
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;
Expand Down Expand Up @@ -200,6 +251,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));

// 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,
"%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);
Expand Down
10 changes: 10 additions & 0 deletions src/transcribe-load-common.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,22 @@ 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), 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
// ggml's device registry.
//
Expand Down
17 changes: 17 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.)
# -----------------------------------------------------------------------------
Expand Down
164 changes: 164 additions & 0 deletions tests/backend_metal_simdgroup_gate_unit.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// backend_metal_simdgroup_gate_unit.cpp - AUTO device selection skips a Metal
// device that lacks simdgroup matrix multiply (Handy issue #1608).
//
// 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,
// probed once in main() (on a real pre-Apple7
// GPU the gate fires with the hook unset).
//
// Every case is a no-op on machines with no Metal device (e.g. Linux CI).

#include "ggml-backend.h"
#include "transcribe-backend.h"
#include "transcribe-load-common.h"
#include "transcribe.h"

#include <cstdio>
#include <cstdlib>

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{};
}

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

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; 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();

// 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();

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