From 5954788ea8f2f0080dd1a09f396914fb5610048f Mon Sep 17 00:00:00 2001 From: Ali Hamdi Ali Fadel Date: Sat, 24 Oct 2026 18:41:35 +0000 Subject: [PATCH] session: reject n_threads above ggml's thread limit --- include/transcribe.h | 4 +++- include/transcribe/diarize.h | 2 +- include/transcribe/langid.h | 2 +- include/transcribe/vad.h | 2 +- src/transcribe-abi.h | 8 ++++++++ src/transcribe-asr.cpp | 11 ++++++----- src/transcribe-diarize.cpp | 3 ++- src/transcribe-langid.cpp | 3 ++- src/transcribe-vad.cpp | 3 ++- tests/diarize_dispatch_unit.cpp | 8 +++++--- tests/langid_dispatch_unit.cpp | 10 ++++++---- tests/silero_vad_smoke.cpp | 9 ++++++++- tests/tokenizer_smoke.cpp | 24 +++++++++++++++++++++--- 13 files changed, 66 insertions(+), 23 deletions(-) diff --git a/include/transcribe.h b/include/transcribe.h index ec4ed5474..582c6d4de 100644 --- a/include/transcribe.h +++ b/include/transcribe.h @@ -1085,7 +1085,9 @@ TRANSCRIBE_API void transcribe_model_load_params_init(struct transcribe_model_lo * Session init params. * * n_threads: number of CPU threads for ops that run on CPU. 0 means - * "library picks a sensible default". + * "library picks a sensible default". At most 512 (ggml's + * thread limit); negative or larger values are rejected with + * TRANSCRIBE_ERR_INVALID_ARG. * * kv_type: data type for K/V activations in flash attention. * AUTO (default) uses f16 for quantized models, f32 for f32. diff --git a/include/transcribe/diarize.h b/include/transcribe/diarize.h index 4a4c2793a..e85f5dfd3 100644 --- a/include/transcribe/diarize.h +++ b/include/transcribe/diarize.h @@ -30,7 +30,7 @@ struct transcribe_diarize_info { struct transcribe_diarize_session_params { uint64_t struct_size; - int32_t n_threads; /* 0 = library default */ + int32_t n_threads; /* 0 = library default; at most 512 */ }; struct transcribe_diarize_params { diff --git a/include/transcribe/langid.h b/include/transcribe/langid.h index 0a8b9c07a..3f6f3380f 100644 --- a/include/transcribe/langid.h +++ b/include/transcribe/langid.h @@ -34,7 +34,7 @@ struct transcribe_langid_info { struct transcribe_langid_session_params { uint64_t struct_size; - int32_t n_threads; /* 0 = library default */ + int32_t n_threads; /* 0 = library default; at most 512 */ }; struct transcribe_langid_params { diff --git a/include/transcribe/vad.h b/include/transcribe/vad.h index bea5eb23b..3faf78419 100644 --- a/include/transcribe/vad.h +++ b/include/transcribe/vad.h @@ -34,7 +34,7 @@ struct transcribe_vad_info { struct transcribe_vad_session_params { uint64_t struct_size; /* struct size in bytes; set by the init function */ - int32_t n_threads; /* number of processing threads; 0 uses the default */ + int32_t n_threads; /* number of processing threads; 0 uses the default, at most 512 */ }; /* Settings for transcribe_vad_run. Durations are in milliseconds and diff --git a/src/transcribe-abi.h b/src/transcribe-abi.h index dfe9fe163..998263f8c 100644 --- a/src/transcribe-abi.h +++ b/src/transcribe-abi.h @@ -7,6 +7,7 @@ #pragma once +#include "ggml.h" #include "transcribe.h" #include @@ -66,6 +67,13 @@ inline transcribe_status check_input_struct_size(uint64_t got, uint64_t want) { return check_struct_size(got, want); } +// Session n_threads (ASR, VAD, langid, diarize): 0 selects the library +// default; the maximum is ggml's GGML_MAX_N_THREADS. Larger counts stall the +// CPU backend or fail at thread creation, reported as out-of-memory. +inline bool valid_n_threads(int32_t n_threads) { + return n_threads >= 0 && n_threads <= GGML_MAX_N_THREADS; +} + // Copy min(caller_size, library_size) bytes from a library-staged struct // into the caller's output buffer. The min() is the overflow guard: the // library never writes past what the caller declared, and never reads diff --git a/src/transcribe-asr.cpp b/src/transcribe-asr.cpp index ffbfa0c2a..fac1c335d 100644 --- a/src/transcribe-asr.cpp +++ b/src/transcribe-asr.cpp @@ -42,6 +42,7 @@ using transcribe::api_guard_value; using transcribe::api_guard_void; using transcribe::enum_field_raw; using transcribe::pcm_is_finite; +using transcribe::valid_n_threads; namespace { @@ -771,11 +772,11 @@ static transcribe_status transcribe_session_init_impl(struct transcribe_model * return st; } // n_threads contract from include/transcribe.h: 0 means "library - // picks a sensible default", positive means "use this many." A - // negative value is undefined input, not a documented sentinel — - // reject it here so individual family handlers don't have to - // re-check the same condition. - if (params->n_threads < 0) { + // picks a sensible default", positive means "use this many", up to + // ggml's thread limit. Anything else is undefined input, not a + // documented sentinel — reject it here so individual family handlers + // don't have to re-check the same condition. + if (!valid_n_threads(params->n_threads)) { return TRANSCRIBE_ERR_INVALID_ARG; } // Raw-validate kv_type before the families' first enum-typed load of it diff --git a/src/transcribe-diarize.cpp b/src/transcribe-diarize.cpp index 370e38fa7..66d2cd300 100644 --- a/src/transcribe-diarize.cpp +++ b/src/transcribe-diarize.cpp @@ -16,6 +16,7 @@ using transcribe::api_guard_void; using transcribe::check_input_struct_size; using transcribe::check_struct_size; using transcribe::copy_out_prefix; +using transcribe::valid_n_threads; namespace { @@ -106,7 +107,7 @@ static transcribe_status diarize_session_init_impl(transcribe_model * if (const auto st = check_input_struct_size(params->struct_size, k_min_session_params_size); st != TRANSCRIBE_OK) { return st; } - if (params->n_threads < 0) { + if (!valid_n_threads(params->n_threads)) { return TRANSCRIBE_ERR_INVALID_ARG; } *out = ops->new_session(); diff --git a/src/transcribe-langid.cpp b/src/transcribe-langid.cpp index cebe04bf2..a7ab1dee7 100644 --- a/src/transcribe-langid.cpp +++ b/src/transcribe-langid.cpp @@ -22,6 +22,7 @@ using transcribe::check_struct_size; using transcribe::copy_out_prefix; using transcribe::LangidCandidateEntry; using transcribe::LangidLabels; +using transcribe::valid_n_threads; namespace { @@ -223,7 +224,7 @@ static transcribe_status langid_session_init_impl(transcribe_model * if (const auto st = check_input_struct_size(params->struct_size, k_min_session_params_size); st != TRANSCRIBE_OK) { return st; } - if (params->n_threads < 0) { + if (!valid_n_threads(params->n_threads)) { return TRANSCRIBE_ERR_INVALID_ARG; } *out = ops->new_session(); diff --git a/src/transcribe-vad.cpp b/src/transcribe-vad.cpp index 74d0d18c1..8104579f2 100644 --- a/src/transcribe-vad.cpp +++ b/src/transcribe-vad.cpp @@ -27,6 +27,7 @@ using transcribe::check_struct_size; using transcribe::copy_out_prefix; using transcribe::VadSegmentEntry; using transcribe::VadSegmentParams; +using transcribe::valid_n_threads; namespace { @@ -349,7 +350,7 @@ static transcribe_status vad_session_init_impl(transcribe_model * if (const auto st = check_input_struct_size(params->struct_size, k_min_session_params_size); st != TRANSCRIBE_OK) { return st; } - if (params->n_threads < 0) { + if (!valid_n_threads(params->n_threads)) { return TRANSCRIBE_ERR_INVALID_ARG; } std::unique_ptr s(ops->new_session()); diff --git a/tests/diarize_dispatch_unit.cpp b/tests/diarize_dispatch_unit.cpp index 7c8d9ecc7..685ad7d33 100644 --- a/tests/diarize_dispatch_unit.cpp +++ b/tests/diarize_dispatch_unit.cpp @@ -135,9 +135,11 @@ void test_role_checks() { transcribe_diarize_session_params sp; transcribe_diarize_session_params_init(&sp); - sp.n_threads = -1; - CHECK(transcribe_diarize_session_init(&model, &sp, &s) == TRANSCRIBE_ERR_INVALID_ARG); - CHECK(s == nullptr); + for (const int n_threads : { -1, 513 }) { + sp.n_threads = n_threads; + CHECK(transcribe_diarize_session_init(&model, &sp, &s) == TRANSCRIBE_ERR_INVALID_ARG); + CHECK(s == nullptr); + } sp.n_threads = 2; CHECK(transcribe_diarize_session_init(&model, &sp, &s) == TRANSCRIBE_OK); CHECK(s != nullptr); diff --git a/tests/langid_dispatch_unit.cpp b/tests/langid_dispatch_unit.cpp index 5adb80625..1e24ddd00 100644 --- a/tests/langid_dispatch_unit.cpp +++ b/tests/langid_dispatch_unit.cpp @@ -147,12 +147,14 @@ void test_role_checks_and_labels() { CHECK(transcribe_langid_label_index(&model, "xx") == 0); CHECK(transcribe_langid_label_index(&model, "zz") == -1); - // A negative thread count is rejected up front. + // A negative thread count, or one above ggml's limit, is rejected up front. transcribe_langid_session_params sp; transcribe_langid_session_params_init(&sp); - sp.n_threads = -1; - CHECK(transcribe_langid_session_init(&model, &sp, &s) == TRANSCRIBE_ERR_INVALID_ARG); - CHECK(s == nullptr); + for (const int n_threads : { -1, 513 }) { + sp.n_threads = n_threads; + CHECK(transcribe_langid_session_init(&model, &sp, &s) == TRANSCRIBE_ERR_INVALID_ARG); + CHECK(s == nullptr); + } } void test_ranking() { diff --git a/tests/silero_vad_smoke.cpp b/tests/silero_vad_smoke.cpp index 683a8e972..2ed0dc35b 100644 --- a/tests/silero_vad_smoke.cpp +++ b/tests/silero_vad_smoke.cpp @@ -94,7 +94,14 @@ void test_model() { CHECK(transcribe_vad_get_info(m, &info) == TRANSCRIBE_OK); CHECK(info.sample_rate == 16000 && info.frame_samples == 512); - transcribe_vad_session * s = nullptr; + transcribe_vad_session * s = nullptr; + transcribe_vad_session_params sp; + transcribe_vad_session_params_init(&sp); + for (const int n_threads : { -1, 513 }) { + sp.n_threads = n_threads; + CHECK(transcribe_vad_session_init(m, &sp, &s) == TRANSCRIBE_ERR_INVALID_ARG); + CHECK(s == nullptr); + } CHECK(transcribe_vad_session_init(m, nullptr, &s) == TRANSCRIBE_OK); // Crosses the encoder's 256-frame block boundary with a partial tail. diff --git a/tests/tokenizer_smoke.cpp b/tests/tokenizer_smoke.cpp index 9d21f4736..87857c1fd 100644 --- a/tests/tokenizer_smoke.cpp +++ b/tests/tokenizer_smoke.cpp @@ -298,9 +298,9 @@ void test_v3_fixture() { // n_threads validation in transcribe_session_init (Finding 3) // --------------------------------------------------------------------------- // -// Negative n_threads is undefined input per the public header (0 means -// "library picks", positive means "use this many"). The central -// dispatcher rejects it before the family handler runs. +// Negative n_threads, or more than 512, is undefined input per the public +// header (0 means "library picks", positive means "use this many"). The +// central dispatcher rejects it before the family handler runs. void test_n_threads_validation() { struct transcribe_model * model = load_or_fail("tokenizer_minimal.gguf", "tdt-0.6b-v2"); if (model == nullptr) { @@ -322,6 +322,24 @@ void test_n_threads_validation() { ++g_failures; } + // Above ggml's thread limit is rejected the same way; the limit itself is accepted. + cp.n_threads = 513; + ctx = (struct transcribe_session *) 0xdeadbeef; + const transcribe_status st_above = transcribe_session_init(model, &cp, &ctx); + if (st_above != TRANSCRIBE_ERR_INVALID_ARG || ctx != nullptr) { + std::fprintf(stderr, "FAIL n_threads=513: expected INVALID_ARG and cleared out_ctx, got %s\n", + transcribe_status_string(st_above)); + ++g_failures; + } + cp.n_threads = 512; + ctx = nullptr; + const transcribe_status st_limit = transcribe_session_init(model, &cp, &ctx); + if (st_limit != TRANSCRIBE_OK || ctx == nullptr) { + std::fprintf(stderr, "FAIL n_threads=512: expected OK, got %s\n", transcribe_status_string(st_limit)); + ++g_failures; + } + transcribe_session_free(ctx); + // Sanity: n_threads == 0 (library default) succeeds. cp.n_threads = 0; ctx = nullptr;