Skip to content

Fix #47213: Prevent decode checkpoint reuse in deterministic mode - #47580

Open
yu-depend wants to merge 1 commit into
sgl-project:mainfrom
yu-depend:fix/issue-47213-deterministic-prefix-cache
Open

yu-depend wants to merge 1 commit into
sgl-project:mainfrom
yu-depend:fix/issue-47213-deterministic-prefix-cache

Conversation

@yu-depend

@yu-depend yu-depend commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fix non-deterministic logprobs when prefix cache is reused across requests in deterministic inference mode with mamba extra buffer.

Problem

With deterministic inference enabled, Qwen3.5-9B (and other GDN/SSM models) returned different logprobs for identical requests depending on whether an earlier request had populated the prefix cache. The issue:

  1. Decode-produced recurrent states were being saved to the cache
  2. These states were reused by subsequent requests
  3. However, decode-produced states are not numerically identical to prefill-produced states for the same token prefix
  4. This caused different logprobs even though the generated tokens were identical

The root cause is that floating-point computations in decode mode (one token at a time) differ from prefill mode (chunked processing), making their states incompatible for deterministic reuse.

Solution

In deterministic inference mode with mamba extra buffer enabled, prevent Decode from overwriting or advancing the reusable Prefill checkpoint by:

  1. Disabling tracking masks (mamba_track_mask_cpu and mamba_track_mask_next_cpu)
  2. Disabling lazy checkpoint preallocation at boundaries

This ensures that only prefill-produced checkpoints are reused, maintaining deterministic logprobs across requests.

Key changes in prepare_for_decode method:

  • Check enable_deterministic_inference flag
  • When True: set tracking masks to all-False, skip lazy preallocation
  • When False: preserve original behavior (normal cache reuse)

Testing

  • Syntax validation passed
  • Normal generation continues unaffected
  • Only cache reuse behavior changes in deterministic mode
  • Prefill-produced checkpoints remain reusable
  • Note: Full validation requires GPU with mamba extra buffer support

Impact

  • Fixes non-deterministic logprobs in deterministic inference mode
  • Minimal performance impact (only affects cache behavior)
  • No breaking changes to API
  • Normal (non-deterministic) mode unchanged

Related Work

This fix complements existing deterministic inference improvements:

Fixes #47213


CI States

Latest PR Test (Base): ❌ Run #41164608101
Latest PR Test (Extra): ❌ Run #41164608081
Latest PR Test (AMD ROCm 10): ❌ Run #41164608156

…ic mode

In deterministic inference mode with mamba extra buffer, Decode-produced
states were being saved and reused by other requests. However, these
states are not numerically identical to Prefill-produced states, causing
different logprobs for the same request depending on cache state.

Fixed by disabling tracking masks and lazy preallocation in deterministic
mode, preventing Decode from overwriting the reusable Prefill checkpoint.
Normal generation continues unaffected; only cache reuse behavior changes.

Fixes sgl-project#47213
@yu-depend
yu-depend requested a review from merrymercy as a code owner October 31, 2026 03:29

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@github-actions

Copy link
Copy Markdown
Contributor

Required checks failed on 03be2f3; this PR cannot be merged until they pass.

PR title (run)

PR title must use "[Type] scope: title" or "[Type] [Hardware] scope: title". Type must be one of: Feat, Fix, Refactor, Perf, Doc, Test, CI, Chore. Scope is required and may be any module or model name, with unrestricted letter case (letters, numbers, dots, underscores, slashes, or hyphens). Hardware is optional; use a single tag without spaces. Use uppercase hardware tags (AMD, NVIDIA, NPU, MI300X), except for exactly these four spellings: Intel, Apple, Huawei, Ascend. Reverts must use GitHub's default format: Revert "original title". Use one space between fields and after the colon, with a non-empty title and no leading or trailing whitespace. Examples: "[Fix] attention: Fix KV cache indexing", "[Feat] qwen3: Add model support", "[Perf] [AMD] flash-attn: Optimize decode performance".
Edit the title; the check reruns on its own.

Lint (run)
Run pre-commit run --all-files locally and push the result. The run log shows any other failing step.

@duerwuyi

Copy link
Copy Markdown

Thanks for working on #47213. The checkpoint policy matches the intended fix, but I noticed two implementation issues in prepare_for_decode at 03be2f3.

1. track_mask_cpu must remain a Tensor

Both branches now assign a Python list to the local variable:

if deterministic_mode:
    track_mask_cpu = [False] * len(self.seq_lens_cpu)
else:
    track_remainders_cpu = self.seq_lens_cpu % mamba_track_interval
    track_mask_cpu = (track_remainders_cpu == 0).tolist()

However, the unchanged code below still does:

self.mamba_track_mask = track_mask_cpu.pin_memory().to(
    device=self.device, non_blocking=True
)

This raises:

AttributeError: 'list' object has no attribute 'pin_memory'

when this extra-buffer Decode path is reached. It affects both deterministic and ordinary mode, because both branches produce a list.

The original code intentionally kept two representations: the local track_mask_cpu was a CPU boolean Tensor for the device transfer, while self.mamba_track_mask_cpu was its Python-list representation. Syntax validation alone will not catch this regression.

2. Keep mamba_track_mask_next_cpu=None when overlap is disabled

The new nested conditional effectively groups as:

false_list if deterministic_mode else (
    boundary_list if self.enable_overlap else None
)

Consequently, deterministic_mode=True and enable_overlap=False produces an all-False list instead of the previous None.

This is separate from the blocking type error above: it changes the existing field convention, but I am not claiming an independently demonstrated inference failure from this difference alone.

Suggested adjustment

Keep the mask computation tensor-valued, and apply the overlap condition to the entire next-mask expression:

track_decode = not deterministic_mode
track_remainders_cpu = self.seq_lens_cpu % mamba_track_interval

track_mask_cpu = (track_remainders_cpu == 0) & track_decode
self.mamba_track_mask_cpu = track_mask_cpu.tolist()

self.mamba_track_mask_next_cpu = (
    (
        (track_remainders_cpu == mamba_track_interval - 1)
        & track_decode
    ).tolist()
    if self.enable_overlap
    else None
)

The existing change to skip lazy checkpoint preallocation in deterministic mode can remain. The later track_mask_cpu.pin_memory().to(...) then continues to receive a Tensor.

Could you also add regression coverage for deterministic on/off, overlap on/off, and lazy preallocation on/off? In particular, it should check that the local mask remains a boolean Tensor, non-overlap keeps the next mask as None, deterministic Decode does not advance the reusable checkpoint, and ordinary-mode tracking remains unchanged.

After those changes, the two-request reproducer in #47213 and the ordinary Prefill cache-reuse control should be rerun on this PR head. The earlier GPU results in the Issue validate the attached candidate implementation; they should not be treated as validation of this rewritten version.

This branch has not been deployed

No deployments
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.

[Bug] Qwen3.5 prefix-cache reuse changes logprobs with deterministic inference enabled

2 participants