Skip to content

fix: DRY sampler never applies during decoding (stale token history views)#1720

Open
Nakanokensetsu wants to merge 1 commit into
dphnAI:mainfrom
Nakanokensetsu:fix/dry-stale-token-history-views
Open

fix: DRY sampler never applies during decoding (stale token history views)#1720
Nakanokensetsu wants to merge 1 commit into
dphnAI:mainfrom
Nakanokensetsu:fix/dry-stale-token-history-views

Conversation

@Nakanokensetsu

Copy link
Copy Markdown
Contributor

Problem

The DRY sampler is effectively inert during real decoding while appearing fully configured. With any repetition-inducing prompt, dry_multiplier=5.0 and dry_multiplier=0.0 (same seed) produce bit-identical outputs.

Root cause

InputBatch._make_sampling_metadata() materializes the CPU-kernel inputs as

token_history_ids_cpu = self.token_ids_cpu_tensor[:num_reqs, :max_history_len]

with max_history_len computed at metadata build time. Sampling metadata is only rebuilt on batch-composition changes, so during steady-state decoding the view's width stays frozen at (roughly) the prompt length. Every generated token is written outside the view; dry_scan_penalties clamps each row's length to the tensor width and therefore rescans the prompt-only prefix forever (instrumentation shows per-row lens growing 36→49 while the history tensor shape stays (1, 36), and the kernel returns the identical penalty set every step).

Net effect: DRY yields either no penalty at all, or a frozen prompt-derived penalty, for the entire generation.

Why the obvious fix doesn't work

Widening the cached view to full width makes it live (row-only slices share storage), but at sampling time the tail of token_ids_cpu contains async-scheduling placeholder ids (-1). The kernel then matches the placeholder run and emits penalties for token index -1 (observed: token_indexes=[-1], match_lens=[65]), i.e. it penalizes garbage.

Fix

Stop feeding the CPU kernel and let Sampler.apply_dry fall through to the persistent-state python path, which is updated with the real sampled ids each step via update_dry_state and computes correct penalties.

Verification (TP=2, Qwen3.5-family 9B, optimization-level 3)

  • Repro prompt ("repeat this 10-char string 20 times"), seed fixed:
    • before: dry=0.0 → 25 verbatim repetitions, dry=5.0 → 25 verbatim repetitions (identical bytes)
    • after: dry=0.0 → 25 repetitions, dry=5.00 repetitions
  • Chat-template (thinking) requests now also diverge between dry=0/5 (previously bit-identical end to end).
  • Decode throughput unchanged within noise (69 → 66 tok/s single-stream).

Happy to adjust if you'd rather repair the CPU-kernel path instead (it would need per-step view refresh plus masking of the async placeholder tail).

🤖 Generated with Claude Code

@AlpinDale AlpinDale left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Hi, and thanks for the PR. However it seems that unrelated changes were included in this PR?

@@ -218,7 +218,22 @@ def _process_scanner_items(self, items: Sequence[LexerInput]) -> list[SemanticEv
events: list[SemanticEvent] = []
for item in items:
if isinstance(item, PreLexedTerminal):
events.extend(self._process_lex_tokens(self._lexer.flush()))
# The text lexer may be holding a buffered prefix (e.g. "<tool_")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is this change related to this PR?

Comment thread aphrodite/parser/qwen3.py

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same as above, seems unrelated.

@@ -709,9 +699,15 @@ def _cache_partial_tail_block(self, request: Request, num_tokens: int) -> None:
block_idx = boundary_tokens // self.block_size
if block_idx >= len(blocks):
return
block = blocks[block_idx]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same as above.

Comment thread aphrodite/v1/worker/gpu_input_batch.py Outdated
token_history_ids_cpu, token_history_lens_cpu = (
self._get_token_history_cpu_views(num_reqs) if not self.no_dry else (None, None)
)
# DRY CPU-kernel fast path disabled (2026-07-25): the cached

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

git blame already offers us with commit timestamps so no need to include it here.

Comment thread aphrodite/v1/worker/gpu_input_batch.py Outdated
@@ -1415,6 +1421,10 @@ def _make_output_token_ids_tensor(self, num_reqs: int) -> torch.Tensor:
return output_token_ids_cpu_tensor.to(device=self.device, non_blocking=True)

def _get_token_history_cpu_views(self, num_reqs: int) -> tuple[torch.Tensor, torch.Tensor]:
# NOTE (2026-07-25): unused — the DRY CPU-kernel path is disabled in

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Might as well delete this function instead of leaving dead code lying around.

@@ -438,3 +440,56 @@ def test_partial_block_promotes_to_direct_full_block_hash():
)
assert pool.get_cached_block(promoted_full_hash, [kv_cache_group_id]) == [blocks[1]]
assert pool.get_cached_block(partial_hash_10, [kv_cache_group_id]) is None


def test_manager_does_not_resurrect_partial_hash_after_full_promotion():

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I feel like this should be another PR, same as above.

@Nakanokensetsu
Nakanokensetsu force-pushed the fix/dry-stale-token-history-views branch from db1238f to 6ab8200 Compare July 26, 2026 01:51
…iews)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Nakanokensetsu
Nakanokensetsu force-pushed the fix/dry-stale-token-history-views branch from 6ab8200 to 6b533d5 Compare July 26, 2026 04:31
@Nakanokensetsu

Nakanokensetsu commented Jul 26, 2026

Copy link
Copy Markdown
Contributor Author

You were right — the branch was cut from my fork's main, which carries local patches. Rebased onto upstream main; the PR is now one commit touching only gpu_input_batch.py. The parser and prefix-cache changes will go in separate PRs if they prove generally useful.

Also dropped the date from the comment and deleted _get_token_history_cpu_views rather than leaving it as dead code.

If you would rather fix the CPU kernel path than bypass it, that means rebuilding the views at sampling time instead of metadata-build time — happy to do that instead, it was just a much larger change.

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.

2 participants