Skip to content
Open
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
13 changes: 11 additions & 2 deletions src/arch/qwen3_asr/model.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ constexpr const char k_default_variant[] = "qwen3-asr";

// Generation reserve: what the input gate keeps free, and the decode-budget floor.
constexpr int k_gen_reserve = 256;
// Qwen3-ASR's tokenizer fragments dense-script languages: measured 6.0
// tokens/word for Bengali and 4.6 for Hindi, vs 1.2 for English. Fast speech
// in those scripts reaches ~21 tokens/s, so the shared 12 tok/s estimate
// truncates mid-clip (~25 s of continuous Bengali). 45 covers the fastest
// measured speech with 2x margin; the context ceiling still clamps runaway
// decodes.
constexpr int k_transcript_tokens_per_sec = 45;

// Effective decoder context ceiling, in tokens: the model's trained maximum,
// optionally lowered — never raised — by the caller's session n_ctx knob.
Expand Down Expand Up @@ -853,7 +860,8 @@ transcribe_status run(transcribe_session * session,
}

const int max_new = transcribe::pick_decode_budget(
transcribe::predict_transcript_tokens(T_enc, cm->limits.ms_per_audio_token), k_gen_reserve, T_prompt, ceiling);
transcribe::predict_transcript_tokens(T_enc, cm->limits.ms_per_audio_token, k_transcript_tokens_per_sec),
k_gen_reserve, T_prompt, ceiling);

// KV cache init (grow-to-fit, clamped to the context ceiling). Size to
// hold prompt + decode budget, rounded up to a power of two (the step
Expand Down Expand Up @@ -1739,7 +1747,8 @@ transcribe_status run_batch(transcribe_session * session,
return TRANSCRIBE_OK;
}
const int max_new =
transcribe::pick_decode_budget(transcribe::predict_transcript_tokens(max_T_enc, cm->limits.ms_per_audio_token),
transcribe::pick_decode_budget(transcribe::predict_transcript_tokens(max_T_enc, cm->limits.ms_per_audio_token,
k_transcript_tokens_per_sec),
k_gen_reserve, max_T_prompt, ceiling);
int max_n_kv = 1024;
while (max_n_kv < max_T_prompt + max_new) {
Expand Down
12 changes: 9 additions & 3 deletions src/transcribe-decode-budget.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,25 @@

namespace transcribe {

// Conservative multilingual transcript estimate, in tokens per second.
// Default transcript estimate, in tokens per second. Calibrated on
// Latin-script languages: English realizes ~1.2 tokens per word (~2.6 tok/s
// of speech), so 12 leaves generous headroom. Dense-script languages
// fragment far more in multilingual tokenizers (measured on Qwen3-ASR's
// tokenizer: Bengali 6.0 tokens/word, Hindi 4.6, vs English 1.2), so
// families that transcribe them pass a higher rate.
constexpr int k_transcript_tokens_per_sec = 12;

// Fall back to the encoder-token count when its duration is unknown.
inline int predict_transcript_tokens(int audio_tokens, double ms_per_audio_token) {
inline int predict_transcript_tokens(int audio_tokens, double ms_per_audio_token,
int tokens_per_sec = k_transcript_tokens_per_sec) {
if (audio_tokens <= 0) {
return 0;
}
if (!(ms_per_audio_token > 0.0)) {
return audio_tokens;
}
const double seconds = static_cast<double>(audio_tokens) * ms_per_audio_token / 1000.0;
const double predicted = seconds * k_transcript_tokens_per_sec;
const double predicted = seconds * tokens_per_sec;
if (predicted <= 0.0) {
return 0;
}
Expand Down
19 changes: 19 additions & 0 deletions tests/decode_budget_unit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ void check_predict(const char * what, int audio_tokens, double ms_per_audio_toke
}
}

void check_predict_rate(const char * what, int audio_tokens, double ms_per_audio_token, int tokens_per_sec,
int expected) {
const int got = transcribe::predict_transcript_tokens(audio_tokens, ms_per_audio_token, tokens_per_sec);
if (got != expected) {
std::fprintf(stderr, "FAIL %s: predict_transcript_tokens(%d, %.3f, %d) = %d, expected %d\n", what, audio_tokens,
ms_per_audio_token, tokens_per_sec, got, expected);
++g_failures;
}
}

void check_budget(const char * what, int predicted, int floor_tokens, int t_prompt, int ceiling, int expected) {
const int got = transcribe::pick_decode_budget(predicted, floor_tokens, t_prompt, ceiling);
if (got != expected) {
Expand All @@ -37,6 +47,15 @@ int main(void) {
check_predict("480 ms LFR encoder, same 197 s", /*audio_tokens=*/410, /*ms_per_audio_token=*/480.0,
/*expected=*/2361);

// Families that transcribe dense-script languages pass a higher rate:
// Qwen3-ASR's tokenizer realizes ~6 tokens/word for Bengali and ~4.6 for
// Hindi vs ~1.2 for English, so fast speech in those scripts (~21 tok/s)
// truncates at the default 12 tok/s estimate (~25 s of continuous
// Bengali). 45 covers the fastest measured speech with 2x margin.
check_predict_rate("dense-script rate, 197 s", 2463, 80.0, 45, 8866);
check_predict_rate("26 s Bengali at the default rate truncates", 329, 80.0, 12, 315);
check_predict_rate("26 s Bengali at the dense-script rate completes", 329, 80.0, 45, 1184);

// An unpublished rate falls back to the audio-token count.
check_predict("unknown rate falls back", 2463, 0.0, 2463);
check_predict("negative rate falls back", 2463, -1.0, 2463);
Expand Down