fix: align repetition handling with vLLM (cutoff off by default) - #82
Merged
Merged
Conversation
The native repetition-cutoff heuristic was always on (max_consecutive=16, max_ngram_repeats=3, ngram_size=64), wrongly truncating legitimate repeated structure like large markdown tables. Align with vLLM, which ships no repetition-stop heuristic by default and relies on max_tokens plus logit penalties. Centralize the three defaults into named constants in sampling.rs (DEFAULT_MAX_CONSECUTIVE_TOKENS / DEFAULT_MAX_NGRAM_REPEATS / DEFAULT_NGRAM_SIZE, all 0) and reference them at every fallback site that previously hardcoded 16/3/64: params.rs extract_chat_params, the qwen3 / qwen3_5 / qwen3_5_moe / gemma4 / qianfan_ocr per-turn resolvers, and the napi GenerationConfig default. Zero already disables each detector via the existing check_consecutive / check_ngram guards, so the detection algorithm is unchanged and a positive value still re-enables it (opt-in preserved, per-request overrides still win). Update the stale field doc comments (generation.rs, types.rs, the hand-synced index.d.cts) and the lfm2_session test comment to state the new default. GRPO training defaults and paddleocr are intentionally left as-is. Add unit tests locking the new behavior: all three resolve to 0 when unset, and an explicit positive value passes through. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Task 1 disabled the native repetition cutoff by default, so the Qwen launch presets no longer need to loosen it (256/8/64). Remove maxConsecutiveTokens / maxNgramRepeats / ngramSize from all four QWEN_SAMPLING_DEFAULTS variants; repetition is now shaped by the sampling penalties and bounded by maxOutputTokens, with per-request opt-in still winning via ChatSession.mergeConfig. Update the rationale comment and flip the presets test to assert the fields are absent while preserving the opt-in/override coverage. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… + rename stale test block Final-review follow-ups. Task 1 corrected the hand-synced crates/mlx-core/index.d.cts but not the build-generated packages/core/index.d.cts (the artifact downstream TS imports), leaving the two declaration files disagreeing — one saying default 0, the other 16/3/64. Hand-sync the maxConsecutiveTokens / maxNgramRepeats / ngramSize doc comments on both ChatConfig and GenerationConfig so published types match runtime (it self-heals on the next native build from the already corrected napi source comments). Also rename the now-stale presets test describe block "cutoff survives ChatSession.mergeConfig" to "repetition-cutoff handling through ChatSession.mergeConfig" — its first test now asserts no cutoff survives by default. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The presets test passed under vitest (types stripped) but `tsc -b` (CI typecheck + build) rejected it: QWEN_SAMPLING_DEFAULTS is `as const`, so after removing the cutoff fields the literal type no longer has maxConsecutiveTokens / maxNgramRepeats / ngramSize, making the `toBeUndefined()` accesses TS2339 errors. Annotate the loop local as ChatConfig so they are optional-undefined accesses (the values are genuinely undefined at runtime; the assertion still holds). Verified with `yarn typecheck` (clean) and the vitest suite (7/7). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Qwen models generating large markdown tables (a
|----------------------------------|rule, or many byte-identical rows) get silently truncated. The nativecheck_repetition_cutoffruns every decode step and stops generation on N identical tokens in a row (Tier 1) or a repeating n-gram block (Tier 2). It fires on legitimate repeated structure — table rules, ASCII boxes, separators, repeated rows — and the"repetition"finish reason is then mapped to a cleanend_turn, so the cut looks like a normal answer.PR #81 only loosened the TS Qwen launch preset (256/8/64). Every other path — non-launch servers, Gemma4/LFM2, per-request configs that omit the fields, and the napi
GenerationConfig::default()— still fell back to the tight native defaults (16/3/64), so as few as ~6 repeated tokens still truncated tables.How vLLM handles it (the model for this change)
vLLM ships no repetition-stop heuristic by default. It shapes repetition only with logit-level penalties (
repetition_penalty1.0,frequency_penalty/presence_penalty0.0 — all no-ops by default) and stops generation only on real conditions (EOS, stop tokens, stop strings,max_tokens). Its opt-in loop detector (repetition_detection, recent main) defaults toNone, and when enabled surfaces its own distinctFINISHED_REPETITIONfinish reason rather than disguising the cut.Change: fully align with vLLM — cutoff OFF by default, opt-in
fix(sampling)— Introduce three named constants (all0) insampling.rsand route every former 16/3/64 default-fallback site through them (params.rs, qwen3 / qwen3.5 / qwen3.5-MoE / gemma4 / qianfan_ocr model files, and the napiGenerationConfig::default()). With the knobs unset the detectors resolve to disabled (0already trips the existingcheck_consecutive/check_ngramguards). The detection algorithm is unchanged; GRPO training (grpo/engine.rs) and paddleocr keep their explicit values. New unit tests lock default-off and prove a positive value still re-enables detection (opt-in).fix(server)— Remove the now-unnecessary loosened cutoff (256/8/64) from all four Qwen launch presets; repetition is now shaped by the sampling penalties and bounded bymaxOutputTokens, with per-request opt-in still winning viaChatSession.mergeConfig. Presets test flipped to assert the fields are absent while keeping the opt-in/override coverage.fix(types)— Sync the build-generatedpackages/core/index.d.ctscutoff doc comments to match the hand-syncedcrates/mlx-core/index.d.cts(both now saydefault: 0 = disabled), and rename a stale testdescribeblock.Behavior change / migration
A degenerate small model now runs to
max_tokens(defaultmax_new_tokens= 2048) instead of stopping at ~16 tokens. This is intended and vLLM-aligned —max_tokensis the backstop. Operators who want the old early repetition-stop must now setmaxConsecutiveTokens/maxNgramRepeats/ngramSizeexplicitly (per-request or via registeredsamplingDefaults).Testing
cargo test -p mlx-core --lib repetition→ 28 passing (TDD RED→GREEN; algorithm tests unchanged). Full lib suite 1878 pass; the 7 failures are pre-existing flaky f32/Metal numerical tests (banded-attn, attention-vjp, packed-affine embedding, sparse-moe gather, chunked-prefill parity) — verified to fail identically on base73f4a89e, no path to this change.vp test __test__/server/presets.test.ts→ 7/7.Out of scope (noted, not fixed here)
An adversarial review flagged that
/v1/responsesnever clampsmax_output_tokensagainst the registry's per-modeloutputTokenLimit(unlike/v1/messages). Independently verified as pre-existing (byte-identical at base73f4a89e; this branch doesn't touchresponses.ts/messages.ts) and not a regression of this change (the cutoff only ever caught exact-repeat loops, never bounded long coherent output). Worth a separate server-hardening PR.🤖 Generated with Claude Code
Note
Medium Risk
Changes default generation stopping behavior across all chat paths: legitimate long repeats no longer truncate early, but runaway loops rely on
max_new_tokensunless operators opt back in via config.Overview
Native repetition-stop heuristics are off by default, matching vLLM: unset
maxConsecutiveTokens,maxNgramRepeats, andngramSizenow resolve to 0 (disabled) instead of 16/3/64. SharedDEFAULT_*constants insampling.rsreplace scattered literal fallbacks across chat param extraction and Qwen/Gemma/Qianfan generation paths; the detection logic is unchanged and positive per-request values still opt in.Qwen launch presets drop the previous 256/8/64 “loosened” cutoff pins—repetition is left to sampling penalties and
maxOutputTokens, withChatSession.mergeConfigstill letting clients override. API docs (ChatConfig/GenerationConfig) and tests (presets,params.rsunit tests, LFM2 session comment) are updated for default-off behavior.Reviewed by Cursor Bugbot for commit 5686106. Bugbot is set up for automated code reviews on this repo. Configure here.