Skip to content

Skip MinLength logits processor construction when eos_token_id is negative#29604

Merged
titaiwangms merged 4 commits into
microsoft:mainfrom
titaiwangms:fix/minlength-eos-guard
Jul 8, 2026
Merged

Skip MinLength logits processor construction when eos_token_id is negative#29604
titaiwangms merged 4 commits into
microsoft:mainfrom
titaiwangms:fix/minlength-eos-guard

Conversation

@titaiwangms

@titaiwangms titaiwangms commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

What

Skip constructing the MinLengthLogitsProcessor when eos_token_id is negative.

A negative eos_token_id is the "no-EOS" sentinel used by greedy/sampling
generation (it defaults to -1). With no EOS token there is nothing to demote,
so a MinLength processor built with a negative eos can only ever be a guaranteed
no-op. This change guards its construction at the list level in
LogitsProcessorInitImpl (logits_processor.h), so we do not build a processor
that would do nothing.

if (parameters.min_length > 0 && parameters.eos_token_id >= 0) {
  ...  // add MinLength processor
}

Why

This is a small defense-in-depth / code-clarity improvement, not a behavior
change and not a correctness or security fix:

  • For a valid eos_token_id >= 0, behavior is unchanged — the processor is
    still constructed and enforces the minimum length exactly as before.
  • For a negative eos, the added guard skips a processor that would be a no-op
    anyway (SetScore already ignores negative token ids), so this is a minor
    performance and clarity optimization.
  • It mirrors the existing conditional-adds in the same function (e.g.
    RepetitionPenalty), keeping the construction logic consistent.

CPU-only: the CUDA path is already inherently a no-op for a negative eos, so no
CUDA code is touched.

Tests

Adds 4 unit tests in min_length_logits_processor_test.cc (run via
onnxruntime_provider_test --gtest_filter='*MinLengthLogitsProcessorTest*'):

  • SetScoreIgnoresNegativeTokenId — documents the pre-existing inline backstop
    that ignores negative token ids.
  • ListInitSkipsProcessorForNegativeEosTokenId — drives LogitsProcessorList::Init
    with a negative eos and confirms a below-min-length run leaves scores unchanged
    (the processor is skipped as a guaranteed no-op).
  • ListInitDemotesEosBelowMinLength — positive control / enforcement path: with a
    valid eos the processor is constructed and demotes the eos score below min length.
  • ListInitLeavesScoresUnchangedAtMinLength — at the min length, no demotion.

The tests only reference exported (LogitsProcessorList::Init/Process) and
header-inline symbols so they link in both static and shared-library builds.

Only construct the MinLength logits processor when eos_token_id is a valid
(non-negative) token id. A negative eos_token_id is the documented "no eos"
sentinel used by greedy/sampling generation; in that case there is no token to
demote, so the processor would be a no-op. Skipping its construction makes the
call-site contract explicit and mirrors the conditional-add style of the
neighboring processors, while the existing SetScore bounds check remains the
runtime backstop.

Add unit tests covering the negative-sentinel no-op path and the existing
min-length enforcement behavior to guard against regressions.

Signed-off-by: Tita Wang <titaiwang@microsoft.com>
The existing tests construct MinLengthLogitsProcessor directly and thus exercise
the SetScore runtime backstop rather than the LogitsProcessorList::Init call site
that decides whether to add the processor. Add two list-level tests that drive
Init with a GreedySearchParameters configured to activate only the MinLength
path: one asserts a negative eos_token_id skips the processor (scores unchanged),
and a positive-control test asserts a valid eos_token_id adds it (eos demoted).
Together they cover the Init construction condition in both directions.

Signed-off-by: Tita Wang <titaiwang@microsoft.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves robustness and clarity in the CPU generation logits-processor initialization by avoiding construction of a no-op MinLengthLogitsProcessor when eos_token_id is the negative “no EOS” sentinel (e.g., -1). It also adds focused unit tests to lock in both the processor behavior and the list-level construction guard.

Changes:

  • Guard MinLengthLogitsProcessor construction in LogitsProcessorList::LogitsProcessorInitImpl behind eos_token_id >= 0.
  • Add unit tests covering: negative EOS no-op, valid EOS demotion behavior, and list-level init guard behavior.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
onnxruntime/contrib_ops/cpu/transformers/logits_processor.h Adds an eos_token_id >= 0 guard to skip constructing a no-op MinLength processor for the negative sentinel.
onnxruntime/test/contrib_ops/min_length_logits_processor_test.cc Adds unit tests for MinLengthLogitsProcessor behavior and for the new list-level construction guard.

Comment thread onnxruntime/test/contrib_ops/min_length_logits_processor_test.cc
The earlier tests constructed MinLengthLogitsProcessor<float> directly, which
references a template member defined in logits_processor.cc. That symbol is not
exported from the shared library, so onnxruntime_provider_test failed to link in
the shared-library build configurations (undefined reference to the constructor),
even though it linked in a static build.

Rework the tests to depend only on symbols available in both static and shared
builds: drive the processor through the public LogitsProcessorList::Init and
Process path (which is exactly the construction guard under test), and cover the
negative-token-id backstop through the header-inline NextTokenScores::SetScore.
Coverage is preserved: negative-sentinel skip, below-min-length demotion,
min-length boundary no-op, and the SetScore negative-id guard.

Signed-off-by: Tita Wang <titaiwang@microsoft.com>
@titaiwangms

Copy link
Copy Markdown
Contributor Author

Review — multi-agent pass (readability, correctness, adversarial, spec/deep, integration)

Verdict: LGTM. The guard change is correct, safe, and behavior-neutral. Reviewers confirmed the "negative-eos MinLength is a no-op" premise directly: MinLengthLogitsProcessor::Process writes only via NextTokenScores::SetScore, which early-returns on token_id < 0 (logits_processor.h:31), and the processor has no other side effect or list-ordering dependency. >= 0 is the spec-correct threshold — eos_token_id defaults to -1 in all three parsers (greedy_search_parameters.cc:13, sampling_parameters.cc:11, beam_search_parameters.cc:24). BeamSearch/Whisper are unaffected (they hard-reject eos_token_id < 0 in Validate(), so the new conjunct is always true there). No CPU/CUDA divergence: the CUDA kernel already no-ops for a negative demote_token_id (word_id == demote_token_id never matches). New test file is auto-wired via the contrib_ops/*.cc CMake glob.

Worth addressing

  • Major (test quality, non-blocking): the headline negative test doesn't actually guard the change. ListInitSkipsProcessorForNegativeEosTokenId asserts scores are unchanged with eos=-1 — but because SetScore already no-ops on negative ids (pre-existing, not in this diff), reverting the production guard leaves this test still passing. So it provides zero regression protection for the line it's meant to lock in, and the comment calling the guard "the sole observable behavior" is inaccurate. Either reframe the change honestly as a defensive/perf skip of a guaranteed no-op processor, or make the test discriminating (e.g. assert processor_list_ membership/size so removing the guard fails it). (via deep-reviewer)

  • Minor: value-initialize the params struct. GreedySearchParameters parameters; (test l.40) leaves inherited scalar fields indeterminate. Safe today since Init only reads assigned fields, but brittle — a future guard reading another scalar could make these tests flaky. Use GreedySearchParameters parameters{};. (raised independently by code-reviewer and critical-reviewer)

Minor / nits (optional)

  • PR description says "All 5 tests pass" but the file has 4 TEST(...) — reconcile the count. (readability)
  • SetScoreIgnoresNegativeTokenId lives in the MinLengthLogitsProcessorTest suite but tests NextTokenScores::SetScore, not the processor — a *MinLengthLogitsProcessorTest* filter silently also runs it. Rename the suite or note it. (readability)
  • kBatchBeamSize is assigned to parameters.batch_size; for the greedy/1-beam path it's really a batch size — consider kBatchSize. (readability)
  • Trim the second sentence of the production comment ("Matches the conditional-add style…") and the multi-sentence per-test narration; the test names are already self-describing. (readability)

Follow-ups (out of scope)

  • Sibling latent no-op: TemperatureLogitsProcessor. Same pattern as the one just fixed — with the default temperature == 1.0f the processor is constructed and added, but Process returns immediately for 1.0f. A follow-up could tighten the guard to temperature > 0 && temperature != 1.0f. (integration-reviewer)
  • Schema note: eos_token_id is declared as a required INT attr in the GreedySearch/Sampling schema (contrib_defs.cc), yet the parser tolerates its absence via GetAttrOrDefault(-1). The -1 sentinel is a code convention, not schema-blessed — worth confirming the exporter is expected to emit eos_token_id=-1 for eos-less models. Pre-existing. (deep-reviewer)

Reviewed by a 5-agent team (Claude / GPT / Gemini). Findings deduplicated and prioritized by the lead; the Major was verified against logits_processor.h:31 before posting.

Reword the production and test comments to describe the guard honestly as a
defensive/performance skip of a MinLength processor that would be a guaranteed
no-op for a negative (no-EOS) eos_token_id, rather than a correctness fix. The
eos >= 0 positive-control test is the discriminating case for the enforcement
path. Value-initialize GreedySearchParameters in the test helper so inherited
scalar fields are not left indeterminate.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Tita Wang <titaiwang@microsoft.com>
@titaiwangms titaiwangms requested a review from apsonawane July 7, 2026 23:51
@titaiwangms titaiwangms merged commit 4ce84a7 into microsoft:main Jul 8, 2026
87 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants