Skip to content

feat(inference): complete b10621 sampling semantics and windows - #1487

Merged
inureyes merged 3 commits into
mainfrom
feature/issue-1436-b10621-sampling-semantics
Aug 28, 2026
Merged

inureyes merged 3 commits into
mainfrom
feature/issue-1436-b10621-sampling-semantics

Conversation

@inureyes

@inureyes inureyes commented Aug 28, 2026 •

Copy link
Copy Markdown
Member

Summary

Completes the b10621 sampling semantics that issue #1436 owns in the compatibility manifest: the repeat_last_n penalty window finally reaches the sampler, the DRY sentinels and off-by-one tier are reconciled with upstream, the top-n-sigma / XTC / ignore-eos / reverse-prompt server and native surfaces land on both binaries, and the sampler-order flags accept exactly the fixed b10621 default chain. 26 manifest entries flip to supported, 3 to not_applicable with tested rejection, and the 31 entries this PR cannot honestly claim repoint to the new remainder issue #1485, added as a shard co-owner in pin.json.

Behavior changes to know about

  • Server-side generations that enable a repetition / frequency / presence penalty and omit repetition_context_size (alias repeat_last_n) are now bounded to the server-wide --repeat-last-n default of 64 recent tokens, matching b10621; before this PR the flag was parsed and echoed by /props but every penalty scanned the whole history. The CLI keeps its pre-existing full-history behavior via a -1 default on the new --repeat-last-n flag.
  • dry_penalty_last_n: 0 now disables DRY (upstream's sentinel); the pre-existing "0 = whole history" form moved to the explicit DRY_FULL_HISTORY, spelled -1 on the CLI flags. The DRY comparison changed from > to >=, adding upstream's base^0 penalty tier at exactly dry_allowed_length. A dry_base below 1.0 falls back to the server default on HTTP and keeps the CLI default silently, both matching b10621.
  • penalty_last_n == 0 makes the penalty stage inert regardless of penalty values, so such configs stay on the fused batch path (needs_token_history gates on the windows now).

Rust versus C++ placement

No C++ or bridge change. The penalty window slices the token history handed to the existing Rust penalty functions ahead of the fused dispatch; the incremental SamplerState continues to serve only the full-history form (byte-identical, proven by the covering-window arm below), and a positive window takes the bounded rebuild path over at most repeat_last_n tokens. ignore_eos reuses the shared token-bias map (a -inf bias on the merged end-of-generation set at enqueue time), exactly upstream's EOG-bias mechanism.

What changed

  • src/lib/mlxcel-core/src/generate.rs + sampling.rs: SamplingConfig::penalty_last_n, DRY_FULL_HISTORY, penalty_window, windowed penalty application, DRY sentinel arms and >= tier, needs_token_history window gating.
  • src/lib/mlxcel-xla/src/sampler.rs + src/server/batch/xla_worker_admission.rs: the OpenXLA parity sampler mirrors the same window and DRY semantics (note for reviewers: this PR adds fields consumed by xla-iree-gated code; the OpenXLA feature compile job is the gate that sees those arms, though the xla crate itself compiles and passes its suite locally).
  • Server flags on BOTH binaries: --top-nsigma/--top-n-sigma (default -1.0, sanitized), --xtc-probability, --xtc-threshold, --ignore-eos, repeatable -r/--reverse-prompt (merged into every request's stop set), --samplers/--sampler-seq/--sampling-seq (fixed-order validation failing startup with the accepted form in the diagnostic), plus --repeat-last-n semantics and --dry-penalty-last-n sentinel mapping; /props reports the new defaults with the key-set contract updated.
  • Native /completion: top_n_sigma, xtc_probability/xtc_threshold (schema limits enforced with a 400), ignore_eos, repeat_last_n wiring, and samplers (default order accepted in both upstream shapes, others refused with a 400); generation_settings echoes the new resolved values.
  • Wire protocol: SerializableSamplingState::penalty_last_n with a -1 serde default so an older peer's frame keeps its full-history penalties; round-trip and legacy-frame tests. Note: an old frame carrying dry_penalty_last_n: 0 (the old full-history spelling) is read as disabled by a new decode node during a mixed-version handoff window; recorded here rather than silently ignored.
  • Manifest: compat/llama-server/b10621/sampling-and-grammar.json (27 flips, 31 repoints, two now-fixed --dry-base divergence records cleared, field:seed kept deferred under feat(inference): complete the remaining b10621 sampling and grammar surfaces #1485 because its below-minus-one rejection divergence is real) and compat/llama-server/b10621/pin.json (adds feat(inference): complete the remaining b10621 sampling and grammar surfaces #1485 to the shard owner set, the mechanism the validator itself prescribes for splitting a remainder).
  • Docs: docs/python-client.md, docs/llama-server-compat.md owner table.

Numerical validation (Qwen3-4B-4bit, greedy decoding, every position decided)

Arm Result
Control: --repeat-last-n -1 (full-history sentinel) vs --repeat-last-n 100000 (window covering the whole run), repetition 1.3, 200 tokens byte-identical
Control: --repeat-last-n 64 with all penalties at identity vs no flags byte-identical
Control: --dry-multiplier 0.8 --dry-penalty-last-n 0 vs DRY off byte-identical (0 disables)
Treatment: --repeat-last-n 64 vs full history, repetition 1.3, 200 tokens diverges first at word 76 of 164 (53 percent of subsequent words), exactly where the history first exceeds the window
DRY full-history sentinel vs DRY off diverges (DRY active), arithmetic pinned by fixed-logit unit tests including the new base^0 tier

The teacher-forced forward path is untouched: no model or kernel file changes, so the logit-trace forward comparison is identically zero by construction; every change is post-forward sampling arithmetic, validated by the fixed-logit unit tests and the greedy real-checkpoint arms above.

Review follow-up (second commit)

A review pass against the pinned b10621 source corrected three claims the first commit got wrong about upstream, plus hardening: request stop strings now REPLACE the server-wide --reverse-prompt list when non-empty (upstream's antiprompt fallback semantics) instead of merging; the native XTC fields now CLAMP into b10621's soft 0..=1 limits instead of answering 400; both server binaries reject a negative --dry-penalty-last-n at parse time exactly as b10621's CLI does (the offline generate CLI keeps -1 as a documented mlxcel-only spelling); --dry-base is sanitized at startup and the core plus xla DRY implementations mirror upstream's below-1.0 early-out and exponent cap; the OpenXLA admission path rejects ignore_eos with a diagnostic instead of silently stopping at EOS and the disaggregated gap is recorded beside the existing XTC note; HTTP dry_penalty_last_n clamps to INT32_MAX so a wire value cannot collide with the internal full-history sentinel; allow_hyphen_values became allow_negative_numbers per the documented parser policy; check_sampler_order runs before the model load (fails in 21 ms); and two hijacked doc blocks were restored to their functions. The corresponding manifest notes were rewritten to describe upstream accurately.

Test plan

  • cargo test -p mlxcel-core --profile test-fast --features metal,accelerate --lib sampling::tests (98 passed, including the seven new window/DRY tests)
  • cargo test -p mlxcel-xla --profile test-fast --lib (280 passed; the breaker test now isolates the breaker from the new >= tier)
  • Root filters (penalty, dry, sampler_order, sanitize, request_options, native_completion, serving_protocol, props, validate): all green
  • cargo test --profile test-fast --features metal,accelerate --test llama_compat_manifest (4 passed against both rebuilt binaries) and the llama_compat lib suite
  • python3 scripts/ci/check_llama_compat_manifest.py (zero feat(inference): complete b10621 sampling and grammar semantics #1436 deferred entries remain, so closing this issue cannot redden the validator), check_cross_repo_refs.py
  • cargo clippy (-D warnings) on mlxcel-core, mlxcel-xla, and the root lib/tests/bins; cargo fmt --all -- --check
  • Real-checkpoint arms as tabled above

Closes #1436

Close the b10621 sampling-semantics gaps that #1436 owns in the compatibility manifest, and split the remainder to #1485.

Penalty window: `SamplingConfig::penalty_last_n` (b10621 `repeat_last_n`) now reaches the sampler. `-1` (the default, and the CLI default) scans the full history exactly as before, `0` disables the repetition/frequency/presence stage entirely (the config then stays fused-batch eligible via `needs_token_history`), and `N > 0` penalizes over only the last N tokens. The incremental `SamplerState` keeps serving the full-history form byte-identically; a positive window takes the bounded rebuild path. The server resolves the request `repetition_context_size` (alias `repeat_last_n`, previously parse-only) or falls back to the server-wide `--repeat-last-n` default of 64, matching b10621; this changes penalized server-side generations that omit the field, which previously scanned the whole history.

DRY sentinels: `dry_penalty_last_n: 0` now DISABLES DRY (b10621's sentinel) and the pre-existing "0 = full history" form moved to the explicit `DRY_FULL_HISTORY` (spelled `-1` on the CLI flags); the penalty comparison changed from `>` to `>=` so a repeat of exactly `dry_allowed_length` receives the `base^0` tier; a `dry_base` below 1.0 falls back to the server default on the HTTP path and keeps the CLI default silently, both matching upstream. The mlxcel-xla parity sampler mirrors all of it.

New server surfaces on both binaries (`mlxcel serve`, `mlxcel-server`): `--top-nsigma` / `--top-n-sigma` (default -1.0, the b10621 disabled sentinel, sanitized to the 0.0 disabled form), `--xtc-probability`, `--xtc-threshold`, `--ignore-eos`, repeatable `-r` / `--reverse-prompt` (server-wide stop strings merged with request stops), and `--samplers` / `--sampler-seq` / `--sampling-seq` which accept exactly b10621's default chain order (the inert configuration, since #1375/#1377 pin the filter positions to it) and fail startup with an actionable diagnostic otherwise. `/props` reports the new defaults.

Native /completion: `top_n_sigma` (<= 0 maps to the explicit disabled form), `xtc_probability` / `xtc_threshold` (b10621's 0..=1 schema limits enforced with a 400, an inert threshold above 0.5 accepted), `ignore_eos` (implemented as upstream does, a -inf logit bias on the merged end-of-generation set at enqueue time), `repeat_last_n` wiring, and `samplers` (default order accepted in both upstream shapes, any other order refused with a 400 naming the accepted forms).

Manifest: 26 entries flip to `supported`, 3 to `not_applicable` with tested rejection, and 31 entries this issue could not honestly claim (Mirostat, dynatemp, adaptive-p, grammar surfaces, logit-bias, dry-sequence-breaker strings, min_keep, n_probs, post_sampling_probs, backend_sampling, the temp/top-k/top-p generation_config default divergence, and the seed below-minus-one divergence) repoint to the new #1485, which pin.json now lists as a shard co-owner. The `--dry-base` divergence records are cleared because the behavior now matches.

Validation: 98 core sampling tests including window sentinels, window-vs-full byte identity when the window covers the history, zero-window inertness plus fused eligibility, DRY 0-disable, full-history sentinel, and the >= tier; xla parity suite; compat integration against both rebuilt binaries; real-checkpoint arms on Qwen3-4B-4bit with greedy decoding (every position decided): full-history sentinel vs covering window byte-identical, window-present-penalties-off byte-identical to baseline, DRY 0-sentinel byte-identical to DRY-off, and the window-64 treatment arm diverging from full history first at word 76 of 164 (53 percent of subsequent words) exactly as a forgetting window should. The traced forward path is untouched by this change; every edit is post-forward sampling arithmetic and plumbing.

Refs #1431.
@inureyes inureyes added status:review Under review type:enhancement New features, capabilities, or significant additions priority:high High priority area:core mlxcel-core: MLX FFI, primitives, KV cache, layers area:inference Generation, sampling, decoding (incl. speculative, DRY) labels Aug 28, 2026
Review findings against the pinned b10621 source, each one a place where the first commit's manifest claim described upstream wrongly:

- Stop strings: upstream seeds the task's stop set from the CLI antiprompt list and REPLACES it wholesale when the request carries any effective stop string, falling back to the CLI list only when the request supplied none; the resolution now does exactly that instead of concatenating, and the test asserts the replace semantics.

- XTC domains: b10621 declares SOFT 0..=1 schema limits on both XTC fields, clamping out-of-range values instead of rejecting them; the native route now clamps identically (no more 400), with the struct docs, manifest notes, and tests updated.

- DRY flag domain: b10621's CLI rejects a negative --dry-penalty-last-n outright, so both server binaries now reject negatives at parse time (the resolve fn keeps its negative arm as defense in depth); a full-history scan stays reachable through a window at least the context length, and the offline generate CLI keeps its -1 spelling as a documented mlxcel-only extension.

- dry_base: the startup value is sanitized like typical_p (a sub-1.0 flag folds to the 1.75 default with a warning, matching upstream's silent keep), and both the core and xla DRY implementations mirror upstream's below-1.0 early-out so the request-layer sanitization has a backstop. The DRY exponent now carries upstream's FLOAT_MAX_LOG / ln(base) cap so a very long full-history match cannot push the penalty to infinity, and the shared DEFAULT_DRY_BASE const keeps the CLI fallback and the clap default from drifting.

- ignore_eos scope: the OpenXLA admission path rejects an ignore_eos request with a diagnostic (it has no token-bias path) instead of silently stopping at EOS, the disaggregated handoff records the same gap beside the existing XTC note, and both manifest entries carry the scope qualifier.

- Hygiene: HTTP dry_penalty_last_n clamps to upstream's INT32_MAX ceiling so a wire value can never collide with the internal DRY_FULL_HISTORY sentinel; the lossy repetition-window cast uses a checked conversion; allow_hyphen_values became allow_negative_numbers on every new flag per the documented parser policy; the check_sampler_order call moved ahead of the multi-GB model load (verified failing in 21 ms); the preprocess_logits_for_sampling and resolve_default_max_tokens doc blocks that had been hijacked by inserted helpers were restored to their functions; the bench literal spells DRY_FULL_HISTORY.

Refs #1436.
@inureyes inureyes added status:done Completed and removed status:review Under review labels Aug 28, 2026
@inureyes
inureyes merged commit 22c1e13 into main Aug 28, 2026
6 checks passed
@inureyes
inureyes deleted the feature/issue-1436-b10621-sampling-semantics branch August 28, 2026 03:36
@inureyes inureyes self-assigned this Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core mlxcel-core: MLX FFI, primitives, KV cache, layers area:inference Generation, sampling, decoding (incl. speculative, DRY) priority:high High priority status:done Completed type:enhancement New features, capabilities, or significant additions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(inference): complete b10621 sampling and grammar semantics

1 participant