From ec1a54d23d47f43d1e51a376d0b2cf981b649dfd Mon Sep 17 00:00:00 2001 From: Alex Coelho <160228115+alexxxcoelho@users.noreply.github.com> Date: Tue, 1 Sep 2026 23:13:54 +0200 Subject: [PATCH] cohere: stop degenerate repetition in the greedy decoder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Cohere decoder picks each token by plain argmax with no repetition penalty, no no_repeat_ngram and no coverage term, so nothing can break a self-reinforcing state: when the most likely continuation of a phrase is that same phrase, it is emitted until the token budget runs out. The transcript comes back with TRANSCRIBE_OK and no diagnostic beyond the "output truncated at N tokens" warning, which fires because of the loop rather than before it. Measured on 60 problem recordings from a French dictation corpus, Q8_0 and Q4_K_M loop token for token — 6 repetitions, 43 words, identical output — which rules out quantisation and points at the search. Adds a tail-repetition guard on both decode paths (static-graph and dynamic-graph). A block counts as a loop only if it recurs at least 3 times and spans at least 12 tokens in total, so a 1-token block needs 12 repeats while a 4-token block needs 3; that floor keeps an emphatic "no no no" from being mistaken for a loop. Every copy but the first is trimmed, a warning is logged, and decoding stops as if EOS had been reached. Only the tail is examined, so deliberate repetition earlier in an utterance survives. Result on the same 60 files: 1 -> 0 looping files, maximum repetition 6 -> 2, and only 4 of 60 outputs change at all. Verified independently on a second corpus of 7,724 speech units (28.9 h of French meeting audio): 36 loops stopped at the source, none residual. Co-Authored-By: Claude Opus 5 (1M context) --- src/arch/cohere/model.cpp | 86 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/src/arch/cohere/model.cpp b/src/arch/cohere/model.cpp index a305756f..80c2243d 100644 --- a/src/arch/cohere/model.cpp +++ b/src/arch/cohere/model.cpp @@ -634,6 +634,70 @@ transcribe_status init_context(transcribe_model * model, return TRANSCRIBE_OK; } +// Degenerate-repetition guard for the greedy decoder. +// +// Token selection here is a plain argmax with no repetition penalty, no +// no-repeat-ngram and no coverage term, so there is nothing to pull the model +// out of a self-reinforcing state: if the most likely continuation of a phrase +// is that same phrase, it emits it until the token budget runs out. Measured on +// real French dictation, one 21-second recording produced "Je suis allé à la +// médiation." twelve times over — and Q8_0 and Q4_K_M did it identically, token +// for token, which rules out quantisation and points at the search. +// +// The test looks at the TAIL only, so a deliberate repetition earlier in the +// utterance survives untouched. A block must also cover at least +// kMinLoopTokens tokens in total before it counts, which is what keeps an +// emphatic "non non non" from being mistaken for a loop. +namespace { + +constexpr int kMaxLoopBlock = 16; // longest repeating unit considered +constexpr int kMinLoopRepeat = 3; // a block must recur at least this often +constexpr int kMinLoopTokens = 12; // …and span at least this many tokens + +// Length of the repeating block at the tail, or 0 when the tail is not looping. +int looping_tail_block(const std::vector & ids) { + const int n = static_cast(ids.size()); + for (int block = 1; block <= kMaxLoopBlock; ++block) { + const int repeats = std::max(kMinLoopRepeat, (kMinLoopTokens + block - 1) / block); + if (n < block * repeats) { + continue; + } + bool same = true; + for (int r = 1; r < repeats && same; ++r) { + for (int i = 0; i < block; ++i) { + if (ids[n - 1 - i] != ids[n - 1 - i - r * block]) { + same = false; + break; + } + } + } + if (same) { + return block; + } + } + return 0; +} + +// Drop every copy of the repeating block but the first. What precedes the loop +// is usually correct; only the runaway tail is discarded. +void trim_looping_tail(std::vector & ids, int block) { + while (static_cast(ids.size()) >= 2 * block) { + const int n = static_cast(ids.size()); + bool same = true; + for (int i = 0; i < block && same; ++i) { + if (ids[n - 1 - i] != ids[n - 1 - i - block]) { + same = false; + } + } + if (!same) { + break; + } + ids.resize(static_cast(n - block)); + } +} + +} // namespace + transcribe_status run(transcribe_session * session, const float * pcm, int n_samples, @@ -1212,6 +1276,17 @@ transcribe_status run(transcribe_session * session, if (next_token != eos_id) { generated_ids.push_back(next_token); } + + if (const int block = looping_tail_block(generated_ids); block > 0) { + trim_looping_tail(generated_ids, block); + transcribe::log_msg(TRANSCRIBE_LOG_LEVEL_WARN, + "cohere run: degenerate repetition stopped at step %d " + "(block=%d tokens); the runaway tail was dropped and the " + "transcript kept up to the loop.", + step, block); + next_token = eos_id; + break; + } } } else { // ---------- Dynamic-graph step path (CPU) ---------- @@ -1283,6 +1358,17 @@ transcribe_status run(transcribe_session * session, if (next_token != eos_id) { generated_ids.push_back(next_token); } + + if (const int block = looping_tail_block(generated_ids); block > 0) { + trim_looping_tail(generated_ids, block); + transcribe::log_msg(TRANSCRIBE_LOG_LEVEL_WARN, + "cohere run: degenerate repetition stopped at step %d " + "(block=%d tokens); the runaway tail was dropped and the " + "transcript kept up to the loop.", + step, block); + next_token = eos_id; + break; + } } }