diff --git a/AGENTS.md b/AGENTS.md
index c1bf31ed..3c547af5 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -39,6 +39,25 @@ scripts/ci/clang-format.sh --check # verify, no changes
formatted. CI gates our C/C++ in
`.github/workflows/clang-format.yml`.
+## C ABI Exception Discipline
+
+No C++ exception may escape a public entry point.
+
+- A new public entry point must either route through an `api_guard_*`
+ wrapper (`src/transcribe.cpp`) or be nothrow by construction. Device and
+ registry queries are not pure reads; guard them.
+- Entry points with ownership out-params enforce "non-OK => `*out == NULL`,
+ nothing leaked" in their forwarders on every error return.
+- Teardown never uses raw `ggml_backend_free` / `ggml_backend_buffer_free` /
+ `ggml_backend_sched_free` in library code: use `transcribe::safe_*` from
+ `src/transcribe-backend.h`. `tests/lint_teardown.cmake` fails CI on
+ violations.
+- Host log callbacks are contained at the emission site
+ (`transcribe_log_invoke`).
+- The `TRANSCRIBE_TEST_*` fault hooks (`_DEV_INIT_THROW`, `_TEARDOWN_THROW`)
+ intentionally ship in release artifacts for wheel clean-install CI.
+ Present-but-empty values are inert.
+
## Verification
- End-to-end numerical checks:
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2191715f..dc4735af 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -387,6 +387,15 @@ if(TRANSCRIBE_GGML_BACKEND_DL)
set(GGML_NATIVE OFF CACHE BOOL "" FORCE)
endif()
+# MSVC: use /EHs so exceptions thrown by ggml's C-ABI frames unwind to
+# transcribe.cpp's public-entry guards. Apply before ggml and src are added.
+if(MSVC)
+ string(REPLACE "/EHsc" "/EHs" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
+ if(NOT CMAKE_CXX_FLAGS MATCHES "/EHs")
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /EHs")
+ endif()
+endif()
+
# ggml's CMake declares its own warning flags; let it.
add_subdirectory(ggml)
diff --git a/docs/environment-variables.md b/docs/environment-variables.md
index c1f21d78..40d00f27 100644
--- a/docs/environment-variables.md
+++ b/docs/environment-variables.md
@@ -15,6 +15,13 @@ copies and writes raw activations to disk, so it carries I/O cost and a
data-leak surface. Leave it unset in production unless you are deliberately
collecting dumps.
+The two `TRANSCRIBE_TEST_*_THROW` fault-injection hooks also ship in release
+binaries on purpose: wheel clean-install CI proves exception containment
+against the exact artifact users install (see `AGENTS.md`, "C ABI Exception
+Discipline"). Unlike the boolean convention above, **any** non-empty value
+arms them (including `0`); unset or empty is inert. Leave them unset outside
+of tests.
+
| Variable | Effect |
| --- | --- |
| `TRANSCRIBE_NO_FLASH` | Disable flash attention on encoder and decoder (forces the manual F32 path). |
@@ -24,6 +31,8 @@ collecting dumps.
| `TRANSCRIBE_DUMP_DIR=
` | Enable the per-stage tensor dumper; writes `.f32` + `.json` per dumped tensor into ``. The basis for the numerical-comparison harness (`scripts/compare_tensors.py`). |
| `TRANSCRIBE_PERF_DEBUG` | Print a per-stage timing breakdown to stderr (DEBUG log) on the families that profile (`cohere`, `granite`, `canary`, `canary_qwen`, `moonshine`, `moonshine_streaming`, `qwen3_asr`, `whisper`). For whisper, a value containing `cpu` or `all` additionally prints the CPU sub-section breakdown. |
| `TRANSCRIBE_VOXTRAL_REALTIME_STREAM_TIMING` | Print a per-component streaming wall-time breakdown at stream finalize (voxtral_realtime). |
+| `TRANSCRIBE_TEST_DEV_INIT_THROW=` | Fault injection: backend device init (`ggml_backend_dev_init`) throws for devices whose name contains `` (`*` matches every device). Exercises throw → skip → CPU-fallback in backend probing; an explicit backend request fails with `TRANSCRIBE_ERR_BACKEND`. Used by `backend_init_throw_unit` and `scripts/ci/vulkan_degradation_check.py`. |
+| `TRANSCRIBE_TEST_TEARDOWN_THROW` | Fault injection: any non-empty value injects a throw after each real free inside the `transcribe::safe_*` teardown wrappers, proving containment without leaking the handle. Used by `teardown_safety_unit`. |
## Tier 2 — validation hooks
diff --git a/include/transcribe.h b/include/transcribe.h
index 5cff1a5d..15bf1d67 100644
--- a/include/transcribe.h
+++ b/include/transcribe.h
@@ -52,6 +52,12 @@
* releases. Consumers should rebuild against matching headers and
* should not assume any layout, enum value, or symbol set is frozen.
*
+ * Exception safety (C ABI):
+ * - No C++ exception escapes a public entry point. Allocation failure maps
+ * to TRANSCRIBE_ERR_OOM, backend/driver failure to TRANSCRIBE_ERR_BACKEND,
+ * and free/teardown functions never fail. The log callback receives
+ * exception detail when available.
+ *
* Params (size-aware structs):
* - Every caller-owned public struct crossing the ABI carries `struct_size`
* as field 0, both for inputs (params, family extensions) and outputs
@@ -407,6 +413,10 @@ typedef void (*transcribe_log_callback)(transcribe_log_level level, const char *
* cb == NULL logging explicitly disabled: library and ggml
* messages are dropped, nothing goes to stderr.
*
+ * The callback must not throw. If it does anyway, the exception is contained
+ * at the emission site, the message is dropped, and a note is written to
+ * stderr.
+ *
* Call once at process startup (see the threading contract above).
*/
TRANSCRIBE_API void transcribe_log_set(transcribe_log_callback cb, void * userdata);
@@ -1433,6 +1443,10 @@ TRANSCRIBE_API const struct transcribe_model * transcribe_get_model(const struct
* On success, results are populated on the session and may be read via
* the accessors below. Calling transcribe_run() again replaces the
* previous result on the same session.
+ *
+ * TRANSCRIBE_ERR_BACKEND from this function (and batch/streaming run calls)
+ * is recoverable in-process: free the session and model, reload with
+ * transcribe_model_load_params::backend = TRANSCRIBE_BACKEND_CPU, and retry.
*/
TRANSCRIBE_API transcribe_status transcribe_run(struct transcribe_session * session,
const float * pcm,
diff --git a/scripts/ci/vulkan_degradation_check.py b/scripts/ci/vulkan_degradation_check.py
index 838a5343..9df69a13 100644
--- a/scripts/ci/vulkan_degradation_check.py
+++ b/scripts/ci/vulkan_degradation_check.py
@@ -15,7 +15,10 @@
loader A loader (+ driver, e.g. lavapipe or a real GPU) is present.
The SAME artifacts must discover a Vulkan device. With
- --model: a real transcription runs with backend="vulkan".
+ --model: a real transcription runs with backend="vulkan";
+ then, with TRANSCRIBE_TEST_DEV_INIT_THROW=* injecting a
+ device-init throw, explicit vulkan fails cleanly and default
+ backend falls back to CPU.
Used by: provider-dl-vulkan (python-bindings.yml, hand-assembled provider
dir), clean-install (python-wheels.yml, the shipped wheel — ships in the
@@ -29,6 +32,7 @@
import argparse
import array
+import os
import sys
import wave
@@ -97,6 +101,28 @@ def main() -> int:
text = transcribe(t, args.model, args.audio, backend="vulkan")
print("text (vulkan):", text.strip())
assert "country" in text.lower(), text
+
+ # Same shipped artifact, injected device-init failure.
+ os.environ["TRANSCRIBE_TEST_DEV_INIT_THROW"] = "*"
+ try:
+ try:
+ t.Model(args.model, backend="vulkan")
+ except t.TranscribeError as exc:
+ print("throwing vulkan device correctly rejected:", exc)
+ else:
+ raise AssertionError(
+ "backend='vulkan' must fail when device init throws")
+ with t.Model(args.model) as m:
+ # m.backend is the ggml backend name ("CPU", "Vulkan0"),
+ # not a lowercase kind string.
+ assert m.backend.lower() == "cpu", m.backend
+ with m.session() as s:
+ text = s.run(load_pcm(args.audio)).text
+ print("text (throwing devices, default backend):",
+ text.strip())
+ assert "country" in text.lower(), text
+ finally:
+ del os.environ["TRANSCRIBE_TEST_DEV_INIT_THROW"]
print("ok: loader present — the same artifacts discovered Vulkan")
return 0
diff --git a/src/arch/canary/canary.h b/src/arch/canary/canary.h
index 558fe85c..ee905c48 100644
--- a/src/arch/canary/canary.h
+++ b/src/arch/canary/canary.h
@@ -60,7 +60,7 @@ struct CanaryKvCache {
void free() {
if (buffer != nullptr) {
- ggml_backend_buffer_free(buffer);
+ safe_buffer_free(buffer);
buffer = nullptr;
}
if (ctx != nullptr) {
diff --git a/src/arch/canary/model.cpp b/src/arch/canary/model.cpp
index 072c3373..84e02976 100644
--- a/src/arch/canary/model.cpp
+++ b/src/arch/canary/model.cpp
@@ -46,7 +46,7 @@ static_assert(std::is_base_of_v);
CanarySession::~CanarySession() {
kv_cache.free();
if (sched != nullptr) {
- ggml_backend_sched_free(sched);
+ safe_sched_free(sched);
sched = nullptr;
}
if (compute_ctx != nullptr) {
@@ -183,7 +183,7 @@ CanaryModel::~CanaryModel() {
bn_fused_ctx = nullptr;
}
if (bn_fused_buffer != nullptr) {
- ggml_backend_buffer_free(bn_fused_buffer);
+ safe_buffer_free(bn_fused_buffer);
bn_fused_buffer = nullptr;
}
if (conv_pw_f32_ctx != nullptr) {
@@ -191,7 +191,7 @@ CanaryModel::~CanaryModel() {
conv_pw_f32_ctx = nullptr;
}
if (conv_pw_f32_buffer != nullptr) {
- ggml_backend_buffer_free(conv_pw_f32_buffer);
+ safe_buffer_free(conv_pw_f32_buffer);
conv_pw_f32_buffer = nullptr;
}
if (ctx_meta != nullptr) {
@@ -199,11 +199,11 @@ CanaryModel::~CanaryModel() {
ctx_meta = nullptr;
}
if (backend_buffer != nullptr) {
- ggml_backend_buffer_free(backend_buffer);
+ safe_buffer_free(backend_buffer);
backend_buffer = nullptr;
}
for (auto it = plan.scheduler_list.rbegin(); it != plan.scheduler_list.rend(); ++it) {
- ggml_backend_free(*it);
+ safe_backend_free(*it);
}
plan.scheduler_list.clear();
plan.primary = nullptr;
diff --git a/src/arch/canary_qwen/model.cpp b/src/arch/canary_qwen/model.cpp
index 35de0274..71ccdb01 100644
--- a/src/arch/canary_qwen/model.cpp
+++ b/src/arch/canary_qwen/model.cpp
@@ -61,7 +61,7 @@ CanaryQwenSession::~CanaryQwenSession() {
kv_cache.free();
kv_cache_batch.free();
if (sched != nullptr) {
- ggml_backend_sched_free(sched);
+ safe_sched_free(sched);
sched = nullptr;
}
if (compute_ctx != nullptr) {
@@ -76,11 +76,11 @@ CanaryQwenModel::~CanaryQwenModel() {
ctx_meta = nullptr;
}
if (backend_buffer != nullptr) {
- ggml_backend_buffer_free(backend_buffer);
+ safe_buffer_free(backend_buffer);
backend_buffer = nullptr;
}
if (bn_fused_buffer != nullptr) {
- ggml_backend_buffer_free(bn_fused_buffer);
+ safe_buffer_free(bn_fused_buffer);
bn_fused_buffer = nullptr;
}
if (bn_fused_ctx != nullptr) {
@@ -88,7 +88,7 @@ CanaryQwenModel::~CanaryQwenModel() {
bn_fused_ctx = nullptr;
}
if (conv_pw_f32_buffer != nullptr) {
- ggml_backend_buffer_free(conv_pw_f32_buffer);
+ safe_buffer_free(conv_pw_f32_buffer);
conv_pw_f32_buffer = nullptr;
}
if (conv_pw_f32_ctx != nullptr) {
@@ -96,7 +96,7 @@ CanaryQwenModel::~CanaryQwenModel() {
conv_pw_f32_ctx = nullptr;
}
if (linear_f32_buffer != nullptr) {
- ggml_backend_buffer_free(linear_f32_buffer);
+ safe_buffer_free(linear_f32_buffer);
linear_f32_buffer = nullptr;
}
if (linear_f32_ctx != nullptr) {
@@ -105,7 +105,7 @@ CanaryQwenModel::~CanaryQwenModel() {
}
packed_gate_up.free();
for (auto it = plan.scheduler_list.rbegin(); it != plan.scheduler_list.rend(); ++it) {
- ggml_backend_free(*it);
+ safe_backend_free(*it);
}
plan.scheduler_list.clear();
plan.primary = nullptr;
@@ -357,7 +357,7 @@ transcribe_status promote_conv_pw_to_f32_on_cpu(CanaryQwenModel & m) {
const auto * f16_traits = ggml_get_type_traits(GGML_TYPE_F16);
if (f16_traits == nullptr || f16_traits->to_float == nullptr) {
log_msg(TRANSCRIBE_LOG_LEVEL_WARN, "canary_qwen: no f16 to_float trait — skipping conv promotion");
- ggml_backend_buffer_free(m.conv_pw_f32_buffer);
+ safe_buffer_free(m.conv_pw_f32_buffer);
m.conv_pw_f32_buffer = nullptr;
ggml_free(m.conv_pw_f32_ctx);
m.conv_pw_f32_ctx = nullptr;
@@ -483,7 +483,7 @@ transcribe_status promote_linears_bf16_to_f32_on_cpu(CanaryQwenModel & m) {
const auto * bf16_traits = ggml_get_type_traits(GGML_TYPE_BF16);
if (bf16_traits == nullptr || bf16_traits->to_float == nullptr) {
log_msg(TRANSCRIBE_LOG_LEVEL_WARN, "canary_qwen: no bf16 to_float trait — skipping linear promotion");
- ggml_backend_buffer_free(m.linear_f32_buffer);
+ safe_buffer_free(m.linear_f32_buffer);
m.linear_f32_buffer = nullptr;
ggml_free(m.linear_f32_ctx);
m.linear_f32_ctx = nullptr;
diff --git a/src/arch/cohere/cohere.h b/src/arch/cohere/cohere.h
index 3b369405..319e6151 100644
--- a/src/arch/cohere/cohere.h
+++ b/src/arch/cohere/cohere.h
@@ -78,7 +78,7 @@ struct CohereKvCache {
void free() {
if (buffer != nullptr) {
- ggml_backend_buffer_free(buffer);
+ safe_buffer_free(buffer);
buffer = nullptr;
}
if (ctx != nullptr) {
diff --git a/src/arch/cohere/model.cpp b/src/arch/cohere/model.cpp
index b631a579..22e638d1 100644
--- a/src/arch/cohere/model.cpp
+++ b/src/arch/cohere/model.cpp
@@ -49,7 +49,7 @@ static_assert(std::is_base_of_v);
CohereSession::~CohereSession() {
kv_cache.free();
if (sched != nullptr) {
- ggml_backend_sched_free(sched);
+ safe_sched_free(sched);
sched = nullptr;
}
if (compute_ctx != nullptr) {
@@ -193,7 +193,7 @@ CohereModel::~CohereModel() {
bn_fused_ctx = nullptr;
}
if (bn_fused_buffer != nullptr) {
- ggml_backend_buffer_free(bn_fused_buffer);
+ safe_buffer_free(bn_fused_buffer);
bn_fused_buffer = nullptr;
}
if (conv_pw_f32_ctx != nullptr) {
@@ -201,7 +201,7 @@ CohereModel::~CohereModel() {
conv_pw_f32_ctx = nullptr;
}
if (conv_pw_f32_buffer != nullptr) {
- ggml_backend_buffer_free(conv_pw_f32_buffer);
+ safe_buffer_free(conv_pw_f32_buffer);
conv_pw_f32_buffer = nullptr;
}
if (ctx_meta != nullptr) {
@@ -209,11 +209,11 @@ CohereModel::~CohereModel() {
ctx_meta = nullptr;
}
if (backend_buffer != nullptr) {
- ggml_backend_buffer_free(backend_buffer);
+ safe_buffer_free(backend_buffer);
backend_buffer = nullptr;
}
for (auto it = plan.scheduler_list.rbegin(); it != plan.scheduler_list.rend(); ++it) {
- ggml_backend_free(*it);
+ safe_backend_free(*it);
}
plan.scheduler_list.clear();
plan.primary = nullptr;
diff --git a/src/arch/funasr_nano/model.cpp b/src/arch/funasr_nano/model.cpp
index 45f365f4..c1aa74c1 100644
--- a/src/arch/funasr_nano/model.cpp
+++ b/src/arch/funasr_nano/model.cpp
@@ -47,7 +47,7 @@ FunAsrNanoSession::~FunAsrNanoSession() {
kv_cache.free();
kv_cache_batch.free();
if (sched != nullptr) {
- ggml_backend_sched_free(sched);
+ safe_sched_free(sched);
sched = nullptr;
}
if (compute_ctx != nullptr) {
@@ -62,12 +62,12 @@ FunAsrNanoModel::~FunAsrNanoModel() {
ctx_meta = nullptr;
}
if (backend_buffer != nullptr) {
- ggml_backend_buffer_free(backend_buffer);
+ safe_buffer_free(backend_buffer);
backend_buffer = nullptr;
}
packed_gate_up.free();
for (auto it = plan.scheduler_list.rbegin(); it != plan.scheduler_list.rend(); ++it) {
- ggml_backend_free(*it);
+ safe_backend_free(*it);
}
plan.scheduler_list.clear();
plan.primary = nullptr;
diff --git a/src/arch/gigaam/model.cpp b/src/arch/gigaam/model.cpp
index 3675a36a..1935db87 100644
--- a/src/arch/gigaam/model.cpp
+++ b/src/arch/gigaam/model.cpp
@@ -43,7 +43,7 @@ static_assert(std::is_base_of_v);
GigaamSession::~GigaamSession() {
if (sched != nullptr) {
- ggml_backend_sched_free(sched);
+ safe_sched_free(sched);
sched = nullptr;
}
if (compute_ctx != nullptr) {
@@ -59,11 +59,11 @@ GigaamModel::~GigaamModel() {
ctx_meta = nullptr;
}
if (backend_buffer != nullptr) {
- ggml_backend_buffer_free(backend_buffer);
+ safe_buffer_free(backend_buffer);
backend_buffer = nullptr;
}
for (auto it = plan.scheduler_list.rbegin(); it != plan.scheduler_list.rend(); ++it) {
- ggml_backend_free(*it);
+ safe_backend_free(*it);
}
plan.scheduler_list.clear();
plan.primary = nullptr;
diff --git a/src/arch/granite/model.cpp b/src/arch/granite/model.cpp
index dc24f439..f7c1a3ec 100644
--- a/src/arch/granite/model.cpp
+++ b/src/arch/granite/model.cpp
@@ -41,7 +41,7 @@ GraniteSession::~GraniteSession() {
kv.free();
kv_batch.free();
if (sched != nullptr) {
- ggml_backend_sched_free(sched);
+ safe_sched_free(sched);
sched = nullptr;
}
if (compute_ctx != nullptr) {
@@ -56,7 +56,7 @@ GraniteModel::~GraniteModel() {
bn_fused_ctx = nullptr;
}
if (bn_fused_buffer != nullptr) {
- ggml_backend_buffer_free(bn_fused_buffer);
+ safe_buffer_free(bn_fused_buffer);
bn_fused_buffer = nullptr;
}
packed_gate_up.free();
@@ -65,11 +65,11 @@ GraniteModel::~GraniteModel() {
ctx_meta = nullptr;
}
if (backend_buffer != nullptr) {
- ggml_backend_buffer_free(backend_buffer);
+ safe_buffer_free(backend_buffer);
backend_buffer = nullptr;
}
for (auto it = plan.scheduler_list.rbegin(); it != plan.scheduler_list.rend(); ++it) {
- ggml_backend_free(*it);
+ safe_backend_free(*it);
}
plan.scheduler_list.clear();
plan.primary = nullptr;
diff --git a/src/arch/granite_nar/model.cpp b/src/arch/granite_nar/model.cpp
index 48699d8f..42fed264 100644
--- a/src/arch/granite_nar/model.cpp
+++ b/src/arch/granite_nar/model.cpp
@@ -54,7 +54,7 @@ static_assert(std::is_base_of_v);
GraniteNarSession::~GraniteNarSession() {
if (sched != nullptr) {
- ggml_backend_sched_free(sched);
+ safe_sched_free(sched);
sched = nullptr;
}
if (compute_ctx != nullptr) {
@@ -69,7 +69,7 @@ GraniteNarModel::~GraniteNarModel() {
bn_fused_ctx = nullptr;
}
if (bn_fused_buffer != nullptr) {
- ggml_backend_buffer_free(bn_fused_buffer);
+ safe_buffer_free(bn_fused_buffer);
bn_fused_buffer = nullptr;
}
if (ctx_meta != nullptr) {
@@ -77,11 +77,11 @@ GraniteNarModel::~GraniteNarModel() {
ctx_meta = nullptr;
}
if (backend_buffer != nullptr) {
- ggml_backend_buffer_free(backend_buffer);
+ safe_buffer_free(backend_buffer);
backend_buffer = nullptr;
}
for (auto it = plan.scheduler_list.rbegin(); it != plan.scheduler_list.rend(); ++it) {
- ggml_backend_free(*it);
+ safe_backend_free(*it);
}
plan.scheduler_list.clear();
plan.primary = nullptr;
diff --git a/src/arch/medasr/model.cpp b/src/arch/medasr/model.cpp
index a97b0f8d..0bd3f589 100644
--- a/src/arch/medasr/model.cpp
+++ b/src/arch/medasr/model.cpp
@@ -50,7 +50,7 @@ static_assert(std::is_base_of_v);
MedAsrSession::~MedAsrSession() {
if (sched != nullptr) {
- ggml_backend_sched_free(sched);
+ safe_sched_free(sched);
sched = nullptr;
}
if (compute_ctx != nullptr) {
@@ -66,11 +66,11 @@ MedAsrModel::~MedAsrModel() {
ctx_meta = nullptr;
}
if (backend_buffer != nullptr) {
- ggml_backend_buffer_free(backend_buffer);
+ safe_buffer_free(backend_buffer);
backend_buffer = nullptr;
}
for (auto it = plan.scheduler_list.rbegin(); it != plan.scheduler_list.rend(); ++it) {
- ggml_backend_free(*it);
+ safe_backend_free(*it);
}
plan.scheduler_list.clear();
plan.primary = nullptr;
diff --git a/src/arch/moonshine/model.cpp b/src/arch/moonshine/model.cpp
index 13e26f3d..438a5ae0 100644
--- a/src/arch/moonshine/model.cpp
+++ b/src/arch/moonshine/model.cpp
@@ -45,7 +45,7 @@ static_assert(std::is_base_of_v);
MoonshineSession::~MoonshineSession() {
kv_cache.free();
if (sched != nullptr) {
- ggml_backend_sched_free(sched);
+ safe_sched_free(sched);
sched = nullptr;
}
if (compute_ctx != nullptr) {
@@ -60,11 +60,11 @@ MoonshineModel::~MoonshineModel() {
ctx_meta = nullptr;
}
if (backend_buffer != nullptr) {
- ggml_backend_buffer_free(backend_buffer);
+ safe_buffer_free(backend_buffer);
backend_buffer = nullptr;
}
for (auto it = plan.scheduler_list.rbegin(); it != plan.scheduler_list.rend(); ++it) {
- ggml_backend_free(*it);
+ safe_backend_free(*it);
}
plan.scheduler_list.clear();
plan.primary = nullptr;
diff --git a/src/arch/moonshine/moonshine.h b/src/arch/moonshine/moonshine.h
index 23b91038..defd93a2 100644
--- a/src/arch/moonshine/moonshine.h
+++ b/src/arch/moonshine/moonshine.h
@@ -65,7 +65,7 @@ struct MoonshineKvCache {
void free() {
if (buffer != nullptr) {
- ggml_backend_buffer_free(buffer);
+ safe_buffer_free(buffer);
buffer = nullptr;
}
if (ctx != nullptr) {
diff --git a/src/arch/moonshine_streaming/model.cpp b/src/arch/moonshine_streaming/model.cpp
index 7f4ee4b9..84f3ec32 100644
--- a/src/arch/moonshine_streaming/model.cpp
+++ b/src/arch/moonshine_streaming/model.cpp
@@ -61,7 +61,7 @@ static_assert(std::is_base_of_v);
MoonshineStreamingSession::~MoonshineStreamingSession() {
kv_cache.free();
if (sched != nullptr) {
- ggml_backend_sched_free(sched);
+ safe_sched_free(sched);
sched = nullptr;
}
if (compute_ctx != nullptr) {
@@ -76,11 +76,11 @@ MoonshineStreamingModel::~MoonshineStreamingModel() {
ctx_meta = nullptr;
}
if (backend_buffer != nullptr) {
- ggml_backend_buffer_free(backend_buffer);
+ safe_buffer_free(backend_buffer);
backend_buffer = nullptr;
}
for (auto it = plan.scheduler_list.rbegin(); it != plan.scheduler_list.rend(); ++it) {
- ggml_backend_free(*it);
+ safe_backend_free(*it);
}
plan.scheduler_list.clear();
plan.primary = nullptr;
diff --git a/src/arch/moonshine_streaming/moonshine_streaming.h b/src/arch/moonshine_streaming/moonshine_streaming.h
index 7e2793b5..05517a25 100644
--- a/src/arch/moonshine_streaming/moonshine_streaming.h
+++ b/src/arch/moonshine_streaming/moonshine_streaming.h
@@ -61,7 +61,7 @@ struct MoonshineStreamingKvCache {
void free() {
if (buffer != nullptr) {
- ggml_backend_buffer_free(buffer);
+ safe_buffer_free(buffer);
buffer = nullptr;
}
if (ctx != nullptr) {
diff --git a/src/arch/parakeet/decoder.cpp b/src/arch/parakeet/decoder.cpp
index c020ef60..27a83c2f 100644
--- a/src/arch/parakeet/decoder.cpp
+++ b/src/arch/parakeet/decoder.cpp
@@ -119,7 +119,7 @@ bool build_joint_weight(HostJoint & j, const ggml_tensor * src_out_w) {
auto fail = [&]() -> bool {
if (j.w_buf != nullptr) {
- ggml_backend_buffer_free(j.w_buf);
+ safe_buffer_free(j.w_buf);
j.w_buf = nullptr;
}
if (j.w_ctx != nullptr) {
@@ -127,7 +127,7 @@ bool build_joint_weight(HostJoint & j, const ggml_tensor * src_out_w) {
j.w_ctx = nullptr;
}
if (j.w_backend != nullptr) {
- ggml_backend_free(j.w_backend);
+ safe_backend_free(j.w_backend);
j.w_backend = nullptr;
}
j.g_enc_w = nullptr;
@@ -210,7 +210,7 @@ bool build_pred_weights(HostPredictor & p) {
auto fail = [&]() -> bool {
if (p.lstm_w_buf != nullptr) {
- ggml_backend_buffer_free(p.lstm_w_buf);
+ safe_buffer_free(p.lstm_w_buf);
p.lstm_w_buf = nullptr;
}
if (p.lstm_w_ctx != nullptr) {
@@ -218,7 +218,7 @@ bool build_pred_weights(HostPredictor & p) {
p.lstm_w_ctx = nullptr;
}
if (p.lstm_w_backend != nullptr) {
- ggml_backend_free(p.lstm_w_backend);
+ safe_backend_free(p.lstm_w_backend);
p.lstm_w_backend = nullptr;
}
for (auto & lh : p.lstm) {
@@ -301,7 +301,7 @@ struct JointGraph {
~JointGraph() {
if (buf != nullptr) {
- ggml_backend_buffer_free(buf);
+ safe_buffer_free(buf);
}
if (ctx != nullptr) {
ggml_free(ctx);
@@ -328,7 +328,7 @@ bool build_joint_graph(JointGraph & g, const HostJoint & j, ggml_backend_t backe
auto fail = [&]() -> bool {
if (g.buf != nullptr) {
- ggml_backend_buffer_free(g.buf);
+ safe_buffer_free(g.buf);
g.buf = nullptr;
}
if (g.ctx != nullptr) {
@@ -435,14 +435,14 @@ struct PredGraph {
~PredGraph() {
if (buf != nullptr) {
- ggml_backend_buffer_free(buf);
+ safe_buffer_free(buf);
}
if (ctx != nullptr) {
ggml_free(ctx);
}
- free_cpu_threadpool(backend, tp); // before ggml_backend_free(backend)
+ free_cpu_threadpool(backend, tp); // before safe_backend_free(backend)
if (backend != nullptr) {
- ggml_backend_free(backend);
+ safe_backend_free(backend);
}
}
@@ -465,7 +465,7 @@ bool build_pred_graph(PredGraph & g, const HostPredictor & p, int n_threads) {
auto fail = [&]() -> bool {
if (g.buf != nullptr) {
- ggml_backend_buffer_free(g.buf);
+ safe_buffer_free(g.buf);
g.buf = nullptr;
}
if (g.ctx != nullptr) {
@@ -475,7 +475,7 @@ bool build_pred_graph(PredGraph & g, const HostPredictor & p, int n_threads) {
free_cpu_threadpool(g.backend, g.tp);
g.tp = nullptr; // before freeing backend
if (g.backend != nullptr) {
- ggml_backend_free(g.backend);
+ safe_backend_free(g.backend);
g.backend = nullptr;
}
g.x = nullptr;
@@ -585,25 +585,25 @@ bool build_pred_graph(PredGraph & g, const HostPredictor & p, int n_threads) {
HostJoint::~HostJoint() {
if (w_buf != nullptr) {
- ggml_backend_buffer_free(w_buf);
+ safe_buffer_free(w_buf);
}
if (w_ctx != nullptr) {
ggml_free(w_ctx);
}
if (w_backend != nullptr) {
- ggml_backend_free(w_backend);
+ safe_backend_free(w_backend);
}
}
HostPredictor::~HostPredictor() {
if (lstm_w_buf != nullptr) {
- ggml_backend_buffer_free(lstm_w_buf);
+ safe_buffer_free(lstm_w_buf);
}
if (lstm_w_ctx != nullptr) {
ggml_free(lstm_w_ctx);
}
if (lstm_w_backend != nullptr) {
- ggml_backend_free(lstm_w_backend);
+ safe_backend_free(lstm_w_backend);
}
}
@@ -928,14 +928,14 @@ bool precompute_enc_proj_ggml(const HostJoint & j,
ggml_cgraph * graph = ggml_new_graph(ctx);
ggml_build_forward_expand(graph, res);
if (ggml_backend_graph_compute(backend, graph) != GGML_STATUS_SUCCESS) {
- ggml_backend_buffer_free(buf);
+ safe_buffer_free(buf);
ggml_free(ctx);
return false;
}
ggml_backend_tensor_get(res, out.data(), 0, static_cast(T) * static_cast(joint_h) * sizeof(float));
- ggml_backend_buffer_free(buf);
+ safe_buffer_free(buf);
ggml_free(ctx);
return true;
}
diff --git a/src/arch/parakeet/model.cpp b/src/arch/parakeet/model.cpp
index 4da16b79..3b49742b 100644
--- a/src/arch/parakeet/model.cpp
+++ b/src/arch/parakeet/model.cpp
@@ -59,7 +59,7 @@ ParakeetSession::~ParakeetSession() {
// Tear down per-call compute state before the model's backend plan
// (which outlives the context): scheduler, then context.
if (sched != nullptr) {
- ggml_backend_sched_free(sched);
+ safe_sched_free(sched);
sched = nullptr;
}
if (compute_ctx != nullptr) {
@@ -71,7 +71,7 @@ ParakeetSession::~ParakeetSession() {
// Streaming cache tensors live in their own ggml_context + backend
// buffer. Free buffer first (may hold a backend ref), then the ctx.
if (stream_caches.buffer != nullptr) {
- ggml_backend_buffer_free(stream_caches.buffer);
+ safe_buffer_free(stream_caches.buffer);
stream_caches.buffer = nullptr;
}
if (stream_caches.ctx != nullptr) {
@@ -80,7 +80,7 @@ ParakeetSession::~ParakeetSession() {
}
// Rel-pos projection memo (own ctx + buffer; geometry-keyed).
if (stream_caches.pos_proj_buf != nullptr) {
- ggml_backend_buffer_free(stream_caches.pos_proj_buf);
+ safe_buffer_free(stream_caches.pos_proj_buf);
stream_caches.pos_proj_buf = nullptr;
}
if (stream_caches.pos_proj_ctx != nullptr) {
@@ -105,7 +105,7 @@ ParakeetModel::~ParakeetModel() {
bn_fused_ctx = nullptr;
}
if (bn_fused_buffer != nullptr) {
- ggml_backend_buffer_free(bn_fused_buffer);
+ safe_buffer_free(bn_fused_buffer);
bn_fused_buffer = nullptr;
}
// CPU conv_pw F32 promotion ctx + buffer (non-null only when
@@ -115,7 +115,7 @@ ParakeetModel::~ParakeetModel() {
conv_pw_f32_ctx = nullptr;
}
if (conv_pw_f32_buffer != nullptr) {
- ggml_backend_buffer_free(conv_pw_f32_buffer);
+ safe_buffer_free(conv_pw_f32_buffer);
conv_pw_f32_buffer = nullptr;
}
if (ctx_meta != nullptr) {
@@ -123,11 +123,11 @@ ParakeetModel::~ParakeetModel() {
ctx_meta = nullptr;
}
if (backend_buffer != nullptr) {
- ggml_backend_buffer_free(backend_buffer);
+ safe_buffer_free(backend_buffer);
backend_buffer = nullptr;
}
for (auto it = plan.scheduler_list.rbegin(); it != plan.scheduler_list.rend(); ++it) {
- ggml_backend_free(*it);
+ safe_backend_free(*it);
}
plan.scheduler_list.clear();
plan.primary = nullptr;
@@ -1645,7 +1645,7 @@ transcribe_status ensure_pos_proj_cache(ParakeetSession * pc, ParakeetModel * pm
auto & sc = pc->stream_caches;
if (sc.pos_proj_buf != nullptr) {
- ggml_backend_buffer_free(sc.pos_proj_buf);
+ safe_buffer_free(sc.pos_proj_buf);
sc.pos_proj_buf = nullptr;
}
if (sc.pos_proj_ctx != nullptr) {
diff --git a/src/arch/qwen3_asr/model.cpp b/src/arch/qwen3_asr/model.cpp
index f208e946..6e382fbe 100644
--- a/src/arch/qwen3_asr/model.cpp
+++ b/src/arch/qwen3_asr/model.cpp
@@ -44,7 +44,7 @@ QwenAsrSession::~QwenAsrSession() {
kv_cache.free();
kv_cache_batch.free();
if (sched != nullptr) {
- ggml_backend_sched_free(sched);
+ safe_sched_free(sched);
sched = nullptr;
}
if (compute_ctx != nullptr) {
@@ -59,12 +59,12 @@ QwenAsrModel::~QwenAsrModel() {
ctx_meta = nullptr;
}
if (backend_buffer != nullptr) {
- ggml_backend_buffer_free(backend_buffer);
+ safe_buffer_free(backend_buffer);
backend_buffer = nullptr;
}
packed_gate_up.free();
for (auto it = plan.scheduler_list.rbegin(); it != plan.scheduler_list.rend(); ++it) {
- ggml_backend_free(*it);
+ safe_backend_free(*it);
}
plan.scheduler_list.clear();
plan.primary = nullptr;
diff --git a/src/arch/sensevoice/model.cpp b/src/arch/sensevoice/model.cpp
index 6826029a..9316fc9e 100644
--- a/src/arch/sensevoice/model.cpp
+++ b/src/arch/sensevoice/model.cpp
@@ -44,7 +44,7 @@ static_assert(std::is_base_of_v);
SenseVoiceSession::~SenseVoiceSession() {
if (sched != nullptr) {
- ggml_backend_sched_free(sched);
+ safe_sched_free(sched);
sched = nullptr;
}
if (compute_ctx != nullptr) {
@@ -59,11 +59,11 @@ SenseVoiceModel::~SenseVoiceModel() {
ctx_meta = nullptr;
}
if (backend_buffer != nullptr) {
- ggml_backend_buffer_free(backend_buffer);
+ safe_buffer_free(backend_buffer);
backend_buffer = nullptr;
}
for (auto it = plan.scheduler_list.rbegin(); it != plan.scheduler_list.rend(); ++it) {
- ggml_backend_free(*it);
+ safe_backend_free(*it);
}
plan.scheduler_list.clear();
plan.primary = nullptr;
diff --git a/src/arch/voxtral/model.cpp b/src/arch/voxtral/model.cpp
index d9d755ed..5a8be148 100644
--- a/src/arch/voxtral/model.cpp
+++ b/src/arch/voxtral/model.cpp
@@ -52,7 +52,7 @@ VoxtralSession::~VoxtralSession() {
kv_cache.free();
kv_cache_batch.free();
if (sched != nullptr) {
- ggml_backend_sched_free(sched);
+ safe_sched_free(sched);
sched = nullptr;
}
if (compute_ctx != nullptr) {
@@ -67,12 +67,12 @@ VoxtralModel::~VoxtralModel() {
ctx_meta = nullptr;
}
if (backend_buffer != nullptr) {
- ggml_backend_buffer_free(backend_buffer);
+ safe_buffer_free(backend_buffer);
backend_buffer = nullptr;
}
packed_gate_up.free();
for (auto it = plan.scheduler_list.rbegin(); it != plan.scheduler_list.rend(); ++it) {
- ggml_backend_free(*it);
+ safe_backend_free(*it);
}
plan.scheduler_list.clear();
plan.primary = nullptr;
diff --git a/src/arch/voxtral_realtime/model.cpp b/src/arch/voxtral_realtime/model.cpp
index e2a985b7..2a342cc8 100644
--- a/src/arch/voxtral_realtime/model.cpp
+++ b/src/arch/voxtral_realtime/model.cpp
@@ -54,7 +54,7 @@ Session::~Session() {
enc_kv.free();
ada_scale.clear();
if (ada_buffer != nullptr) {
- ggml_backend_buffer_free(ada_buffer);
+ safe_buffer_free(ada_buffer);
ada_buffer = nullptr;
}
if (ada_ctx != nullptr) {
@@ -62,7 +62,7 @@ Session::~Session() {
ada_ctx = nullptr;
}
if (sched != nullptr) {
- ggml_backend_sched_free(sched);
+ safe_sched_free(sched);
sched = nullptr;
}
if (compute_ctx != nullptr) {
@@ -77,12 +77,12 @@ Model::~Model() {
ctx_meta = nullptr;
}
if (backend_buffer != nullptr) {
- ggml_backend_buffer_free(backend_buffer);
+ safe_buffer_free(backend_buffer);
backend_buffer = nullptr;
}
packed_gate_up.free();
for (auto it = plan.scheduler_list.rbegin(); it != plan.scheduler_list.rend(); ++it) {
- ggml_backend_free(*it);
+ safe_backend_free(*it);
}
plan.scheduler_list.clear();
plan.primary = nullptr;
@@ -365,7 +365,7 @@ transcribe_status compute_ada_scales(Session * cc, Model * cm, int num_delay) {
// Persistent ada_scale_all.
if (cc->ada_buffer != nullptr) {
- ggml_backend_buffer_free(cc->ada_buffer);
+ safe_buffer_free(cc->ada_buffer);
cc->ada_buffer = nullptr;
}
if (cc->ada_ctx != nullptr) {
diff --git a/src/arch/whisper/model.cpp b/src/arch/whisper/model.cpp
index 3cca310d..e8c6d711 100644
--- a/src/arch/whisper/model.cpp
+++ b/src/arch/whisper/model.cpp
@@ -56,11 +56,11 @@ WhisperModel::~WhisperModel() {
ctx_meta = nullptr;
}
if (backend_buffer != nullptr) {
- ggml_backend_buffer_free(backend_buffer);
+ safe_buffer_free(backend_buffer);
backend_buffer = nullptr;
}
for (auto it = plan.scheduler_list.rbegin(); it != plan.scheduler_list.rend(); ++it) {
- ggml_backend_free(*it);
+ safe_backend_free(*it);
}
plan.scheduler_list.clear();
plan.primary = nullptr;
@@ -71,7 +71,7 @@ WhisperSession::~WhisperSession() {
kv_cache.free();
enc_out.free();
if (sched != nullptr) {
- ggml_backend_sched_free(sched);
+ safe_sched_free(sched);
sched = nullptr;
}
if (compute_ctx != nullptr) {
diff --git a/src/arch/whisper/whisper.h b/src/arch/whisper/whisper.h
index d5b9a649..6066dd5a 100644
--- a/src/arch/whisper/whisper.h
+++ b/src/arch/whisper/whisper.h
@@ -87,7 +87,7 @@ struct WhisperKvCache {
void free() {
if (buffer != nullptr) {
- ggml_backend_buffer_free(buffer);
+ safe_buffer_free(buffer);
buffer = nullptr;
}
if (ctx != nullptr) {
@@ -121,7 +121,7 @@ struct WhisperEncOut {
void free() {
if (buffer != nullptr) {
- ggml_backend_buffer_free(buffer);
+ safe_buffer_free(buffer);
buffer = nullptr;
}
if (ctx != nullptr) {
diff --git a/src/causal_lm/causal_lm.cpp b/src/causal_lm/causal_lm.cpp
index 5f04ea68..f9510882 100644
--- a/src/causal_lm/causal_lm.cpp
+++ b/src/causal_lm/causal_lm.cpp
@@ -5,6 +5,7 @@
#include "ggml-backend.h"
#include "ggml.h"
+#include "transcribe-backend.h"
#include "transcribe-log.h"
#include "transcribe-session.h"
@@ -42,7 +43,7 @@ ggml_tensor * mul_mat_f32acc(ggml_context * ctx, ggml_tensor * w, ggml_tensor *
void KvCache::free() {
if (buffer != nullptr) {
- ggml_backend_buffer_free(buffer);
+ safe_buffer_free(buffer);
buffer = nullptr;
}
if (ctx != nullptr) {
@@ -746,7 +747,7 @@ ggml_tensor * block_prefill_batched(ggml_context * ctx,
void PackedGateUpHandles::free() {
if (buffer != nullptr) {
- ggml_backend_buffer_free(buffer);
+ safe_buffer_free(buffer);
buffer = nullptr;
}
if (ctx != nullptr) {
diff --git a/src/transcribe-backend.cpp b/src/transcribe-backend.cpp
index 2e6d2ab6..47a6497b 100644
--- a/src/transcribe-backend.cpp
+++ b/src/transcribe-backend.cpp
@@ -6,7 +6,11 @@
#include "transcribe-backend.h"
+#include "transcribe-log.h"
+
+#include
#include
+#include
namespace transcribe {
@@ -82,4 +86,46 @@ BackendKind classify_device(ggml_backend_dev_t dev) {
return classify_backend_type(dev_type, reg_name);
}
+namespace {
+
+// Shared body for safe_* teardown wrappers. The test hook fires after the
+// real free; present-but-empty is inert.
+template void contained_free(const char * what, Fn && do_free) noexcept {
+ try {
+ do_free();
+ if (const char * hook = std::getenv("TRANSCRIBE_TEST_TEARDOWN_THROW"); hook != nullptr && hook[0] != '\0') {
+ throw std::runtime_error("TRANSCRIBE_TEST_TEARDOWN_THROW fault injection");
+ }
+ } catch (const std::exception & e) {
+ log_msg(TRANSCRIBE_LOG_LEVEL_WARN, "%s threw during teardown (contained; resource may leak): %s", what,
+ e.what());
+ } catch (...) {
+ log_msg(TRANSCRIBE_LOG_LEVEL_WARN,
+ "%s threw an unknown exception during teardown (contained; resource may leak)", what);
+ }
+}
+
+} // namespace
+
+void safe_backend_free(ggml_backend_t backend) noexcept {
+ if (backend == nullptr) {
+ return;
+ }
+ contained_free("ggml_backend_free", [&] { ggml_backend_free(backend); });
+}
+
+void safe_buffer_free(ggml_backend_buffer_t buffer) noexcept {
+ if (buffer == nullptr) {
+ return;
+ }
+ contained_free("ggml_backend_buffer_free", [&] { ggml_backend_buffer_free(buffer); });
+}
+
+void safe_sched_free(ggml_backend_sched_t sched) noexcept {
+ if (sched == nullptr) {
+ return;
+ }
+ contained_free("ggml_backend_sched_free", [&] { ggml_backend_sched_free(sched); });
+}
+
} // namespace transcribe
diff --git a/src/transcribe-backend.h b/src/transcribe-backend.h
index c9a2d2f8..aaea589a 100644
--- a/src/transcribe-backend.h
+++ b/src/transcribe-backend.h
@@ -76,4 +76,14 @@ struct BackendPlan {
std::vector scheduler_list;
};
+// No-throw wrappers for ggml backend teardown. Family destructors are
+// implicitly noexcept, so raw backend frees must not appear in library code;
+// tests/lint_teardown.cmake enforces this. NULL is a no-op.
+//
+// Test hook: non-empty TRANSCRIBE_TEST_TEARDOWN_THROW injects an internal
+// throw after the real free, proving containment without leaking the handle.
+void safe_backend_free(ggml_backend_t backend) noexcept;
+void safe_buffer_free(ggml_backend_buffer_t buffer) noexcept;
+void safe_sched_free(ggml_backend_sched_t sched) noexcept;
+
} // namespace transcribe
diff --git a/src/transcribe-load-common.cpp b/src/transcribe-load-common.cpp
index 52e18123..fbaf1be5 100644
--- a/src/transcribe-load-common.cpp
+++ b/src/transcribe-load-common.cpp
@@ -14,15 +14,44 @@
#include
#include
+#include
#include
#include
#include
+#include
#include
namespace transcribe::load_common {
namespace {
+// Device init can throw from GPU drivers. Treat that like nullptr so AUTO can
+// keep probing and explicit backend requests fail cleanly.
+//
+// Test hook: TRANSCRIBE_TEST_DEV_INIT_THROW is inert when unset or empty,
+// "*" matches every device, otherwise it matches by device-name substring.
+ggml_backend_t dev_init_checked(ggml_backend_dev_t dev, const char * error_tag) {
+ const char * name = "?";
+ try {
+ if (const char * n = ggml_backend_dev_name(dev); n != nullptr) {
+ name = n;
+ }
+ if (const char * match = std::getenv("TRANSCRIBE_TEST_DEV_INIT_THROW"); match != nullptr && match[0] != '\0') {
+ if (std::strcmp(match, "*") == 0 || std::strstr(name, match) != nullptr) {
+ throw std::runtime_error("TRANSCRIBE_TEST_DEV_INIT_THROW fault injection");
+ }
+ }
+ return ggml_backend_dev_init(dev, nullptr);
+ } catch (const std::exception & e) {
+ log_msg(TRANSCRIBE_LOG_LEVEL_WARN, "%s: device \"%s\" init threw: %s - skipping it", error_tag, name, e.what());
+ return nullptr;
+ } catch (...) {
+ log_msg(TRANSCRIBE_LOG_LEVEL_WARN, "%s: device \"%s\" init threw an unknown exception - skipping it", error_tag,
+ name);
+ return nullptr;
+ }
+}
+
// 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
@@ -51,7 +80,7 @@ ggml_backend_t try_init_kind(BackendKind wanted, const char * error_tag, Backend
continue;
}
- ggml_backend_t be = ggml_backend_dev_init(dev, nullptr);
+ ggml_backend_t be = dev_init_checked(dev, error_tag);
if (be == nullptr) {
continue;
}
@@ -77,7 +106,7 @@ void append_accel_backends(std::vector & out, const char * error
if (ggml_backend_dev_type(dev) != GGML_BACKEND_DEVICE_TYPE_ACCEL) {
continue;
}
- ggml_backend_t be = ggml_backend_dev_init(dev, nullptr);
+ ggml_backend_t be = dev_init_checked(dev, error_tag);
if (be == nullptr) {
continue;
}
@@ -161,7 +190,7 @@ transcribe_status init_backends_explicit_index(transcribe_backend_request reques
return TRANSCRIBE_ERR_INVALID_ARG;
}
- ggml_backend_t gpu_be = ggml_backend_dev_init(dev, nullptr);
+ ggml_backend_t gpu_be = dev_init_checked(dev, error_tag);
if (gpu_be == nullptr) {
log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: failed to initialize gpu_device %d (%s)", error_tag, dev_index,
ggml_backend_dev_name(dev));
@@ -457,7 +486,7 @@ transcribe_status promote_conv_pw_f16_to_f32_on_cpu(const BackendPlan &
// Partial success: the ctx + buffer are already allocated but
// unused. Free them so the caller's outparams stay nullptr,
// matching the "do nothing" contract.
- ggml_backend_buffer_free(buffer);
+ safe_buffer_free(buffer);
ggml_free(ctx);
return TRANSCRIBE_OK;
}
diff --git a/src/transcribe-loader.h b/src/transcribe-loader.h
index 1f18eb78..44b4496c 100644
--- a/src/transcribe-loader.h
+++ b/src/transcribe-loader.h
@@ -9,6 +9,7 @@
#pragma once
+#include "transcribe-model.h" // transcribe::MetaMap (must match the model's type)
#include "transcribe.h"
#include