Skip to content

Fix pipelined prefix reuse skipping the last sampled token - #237

Merged
stikves merged 7 commits into
apple:mainfrom
stikves:sukru/fix-pipelined-prefix-reuse
Sep 14, 2026
Merged

stikves merged 7 commits into
apple:mainfrom
stikves:sukru/fix-pipelined-prefix-reuse

Conversation

@stikves

@stikves stikves commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

After pipelined generation, history.count exceeds processedTokenCount by 1 because the last sampled token is yielded but never fed back through the model. On the next multi-turn call, resolve() matches that unprocessed token, placing new tokens at the wrong KV cache position.

Clamp commonPrefix to processedTokenCount before the backup-by-1 step so the unprocessed trailing token is re-included in the next prefill.

Fixes #234

After pipelined generation, history.count exceeds processedTokenCount
by 1 because the last sampled token is yielded but never fed back
through the model. On the next multi-turn call, resolve() matches that
unprocessed token, placing new tokens at the wrong KV cache position.

Clamp commonPrefix to processedTokenCount before the backup-by-1 step
so the unprocessed trailing token is re-included in the next prefill.

Fixes apple#234
@john-rocky

Copy link
Copy Markdown

Confirmed with the harness from #234: this fix passes on all three fixtures.

Tested 99c7d49 merged locally onto main df81198. The branch forks before #227, so I evaluated the merge; it differs from main in exactly this PR's three files. Control: main df81198 alone. Same pinned bundles as #234 (Qwen3.5 0.8B and Qwen3.8 27B GDN S=1, dynamic Qwen3 0.6B), greedy, 24-token prompt, 24-token budget, 68-token second turn, COREAI_CHUNK_THRESHOLD unset.

processedTokenCount after the continued turn vs a full replay:

  • main: 90 vs 91 on all three fixtures. The 27B continuation text also diverges from the replay.
  • this PR: 91 vs 91 on all three. The continuation is token-identical to the replay, the replay is repeatable, dynamic partial reset/replay passes, and hybrid partial reset is still rejected as documented.
  • Every phase is token-identical to my truncate-history variant on all three fixtures.

One observation: lastPrefixHitCount is assigned before the clamp, so after a pipelined turn it reports 48 where 47 tokens were reused and one was re-prefilled. The protocol doc describes it as tokens skipped. If that is the intended meaning, the assignment could move after the clamp. That expectation is the only failure of my opt-in regression test (budgets 1 and 4) on this PR. All count and token checks pass on both fixtures, against 7 and 9 failures on main.

Mac Studio M4 Max, macOS 27.0 26A5416b, Xcode 27 beta 5 27A5237l, SDK 26A5406c. Harness: consumer/Sources/main.swift. Coverage: these three fixtures, one prompt, greedy decoding, fixed budgets. Not covered: cancellation/early-EOS, long conversations, iPhone.

stikves and others added 5 commits September 8, 2026 12:49
Addresses review on apple#237:
- Move lastPrefixHitCount after the KV-range clamp so it reports tokens
  actually reused (was over-reporting by one -- the re-prefilled token).
- Truncate history to the clamped prefix so the re-prefilled trailing token
  is not duplicated when new tokens are appended, avoiding spurious
  divergence on the third and later turns.
- Add multi-turn and multi-token-gap (cancellation/early-EOS) regression tests.
The pipelined gap is structurally always 1: runCompletion has no early-EOS
break and increments processedTokenCount eagerly at launch, so no path yields
a gap >1. Remove the synthetic gap-of-3 test and its cancellation/early-EOS
comment, and note the remaining clamp tests validate the algorithm against a
MockEngine copy of the clamp, not the real engine's KV interaction.
@stikves

stikves commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

Confirmed with the harness from #234: this fix passes on all three fixtures.

Tested 99c7d49 merged locally onto main df81198. The branch forks before #227, so I evaluated the merge; it differs from main in exactly this PR's three files. Control: main df81198 alone. Same pinned bundles as #234 (Qwen3.5 0.8B and Qwen3.8 27B GDN S=1, dynamic Qwen3 0.6B), greedy, 24-token prompt, 24-token budget, 68-token second turn, COREAI_CHUNK_THRESHOLD unset.

processedTokenCount after the continued turn vs a full replay:

  • main: 90 vs 91 on all three fixtures. The 27B continuation text also diverges from the replay.
  • this PR: 91 vs 91 on all three. The continuation is token-identical to the replay, the replay is repeatable, dynamic partial reset/replay passes, and hybrid partial reset is still rejected as documented.
  • Every phase is token-identical to my truncate-history variant on all three fixtures.

One observation: lastPrefixHitCount is assigned before the clamp, so after a pipelined turn it reports 48 where 47 tokens were reused and one was re-prefilled. The protocol doc describes it as tokens skipped. If that is the intended meaning, the assignment could move after the clamp. That expectation is the only failure of my opt-in regression test (budgets 1 and 4) on this PR. All count and token checks pass on both fixtures, against 7 and 9 failures on main.

Mac Studio M4 Max, macOS 27.0 26A5416b, Xcode 27 beta 5 27A5237l, SDK 26A5406c. Harness: consumer/Sources/main.swift. Coverage: these three fixtures, one prompt, greedy decoding, fixed budgets. Not covered: cancellation/early-EOS, long conversations, iPhone.

Thanks, updated to post process. However we can take the reported engine stats as "best effort", as correctness and performance are higher priorities.

@stikves
stikves marked this pull request as ready for review September 14, 2026 16:57
@stikves stikves self-assigned this Sep 14, 2026
@stikves
stikves merged commit 62ff88b into apple:main Sep 14, 2026
3 checks passed
@stikves
stikves deleted the sukru/fix-pipelined-prefix-reuse branch September 14, 2026 19:40
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.

[Swift runtime] Pipelined prefix reuse skips the last sampled token

4 participants