Skip to content

serve: Qwen3.5 chat turn 2 re-prefills from position 0; the session extend never engages via /v1/chat/completions #4274

Open

Description

@noahgift

Measured by aprender-f5 on 0.69.3 RC1 7ff50ec (receipt /mnt/nvme-raid0/agent-wt/f5-g1/receipt-rc1-7ff50ec2a.md). Setup: Qwen3.5-4B Q4_K_M, RTX 4090.

What happens: on turn 2, serve logs [qwen35] batched prefill: 1076 tokens … from position 0. The 1076 tokens are the whole conversation, not just the new turn, so the KV cache from turn 1 is not reused.

Likely cause, not yet verified: the rendered chat history does not match the token ids generated in turn 1, so the common-prefix check fails.

Output is correct: the batched and one-token-at-a-time paths produce the same text on both turns. Only time is lost. At 32k context, turn 2's time to first token is 15.5 s, against 13.9 s for turn 1.

Done when: turn 2 extends from kv_len > 0, shown by the trace line, and the text is identical to a full re-prefill.

Found by aprender-f5; filed by aprender-36.

Activity

  1. added this to the 0.69.4 milestone on Sep 24, 2026
  2. noahgift commented on Sep 24, 2026

    @noahgift
    MemberAuthor

    Claimed by aprender-60 (#4252 dogfood owner). It will land in the next serve like-batch, not as its own PR (10-PR cap).

  3. noahgift commented on Sep 25, 2026

    @noahgift
    MemberAuthor

    Root cause + fix on batch/0.69.4-epic-4249 @90f8ec7a5 (aprender-ont).

    Root cause: ensure_capacity sized the first state to exactly the turn whenever that turn was bigger than MIN_CAPACITY (4096). At p4k, turn 1 needed 4197 positions, so it got capacity 4197. Turn 2 needed 4279, so it reallocated. Session::reserve drops the checkpoint on any reallocation, so turn 2 re-prefilled from position 0. p850 fit inside 4096, which is why it extended.

    Fix: pure grown_capacity(). It doubles MIN_CAPACITY until the turn fits, then clamps to the context. One-call states (min_capacity == 0) stay exact.

    • Table test a_state_that_outgrows_min_capacity_leaves_headroom_for_the_next_turn: 9 rows plus the e2e shape. Mutant (old formula restored) → RED left: 4197 right: 8192; source restored.
    • e2e, Qwen3.5-4B-Q4_K_M, CUDA, --context-length 8192, apr 0.69.4 (0833bad06, same diff pre-rebase) sha256 53fe2a16f817ffe8, run under gpu-q:
      • before (ec4cd7f): turn 2 4208 tokens … from position 0
      • after: turn 2 82 tokens … from position 4126 (checkpoint resume), then 7 tokens from position 4208
      • turn-2 text is byte-identical to a full re-prefill on a fresh server.

    Residual (not in this fix): a turn that crosses a doubling boundary still reallocates and re-prefills once. Keeping state across growth would mean copying the KV and recurrent state into the bigger buffers, which is a follow-up if wanted. Issue stays open until the batch merges.

  4. modified the milestones: 0.69.4, 0.69.5 on Sep 25, 2026
  5. noahgift commented on Sep 25, 2026

    @noahgift
    MemberAuthor

    #4274 e2e receipt — batch/0.69.4-epic-4249 @90f8ec7a5 (apr 0.69.4 (90f8ec7), exe-sha256 3f67ab9d1d09…, --features cuda)

    Qwen3.5-4B Q4_K_M (/mnt/nvme-raid0/lqw/models/Qwen3.5-4B-Q4_K_M.gguf), RTX 4090 lambda, apr serve run --backend cuda --gpu-layers all --context-length 40000, one gpu-q hold.
    Warm = one server, two /v1/chat/completions turns (f5's serve_client.py, same prompts + follow-up as receipt-rc1-7ff50ec2a).
    Cold = fresh server fed the full turn-2 messages (turn-1 assistant text from warm) = full re-prefill reference. temperature 0.

    prompt warm turn-2 prefill (trace) cold turn-2 prefill turn-2 text warm vs cold turn-2 TTFT warm / cold (RC1 was)
    p850 70 tokens from position 999 1069 from 0 IDENTICAL (28 tok) 0.194 s / 1.593 s (0.391)
    p4k 82 tokens from position 4126 4208 from 0 IDENTICAL (64 tok) 0.204 s / 1.652 s (1.31)
    p32k 61 tokens from position 31841 31902 from 0 IDENTICAL (17 tok) 0.384 s / 14.564 s (15.48)

    Backend line: "Backend: GPU (CUDA, NVIDIA GeForce RTX 4090 …) [qwen35 hybrid forward, #3090]". p850 turn-1 TTFT 96.8 s = one-time F2 guard (95.4 s), not prefill.
    Raw: warm-.{json,err}, cold-.{json,err}, runs.log in this directory.

  6. modified the milestones: 0.69.5, 0.71.0 on Oct 4, 2026
  7. noahgift commented on Oct 14, 2026

    @noahgift
    MemberAuthor

    merged into #5305 (master ticket for epic #3598; triage rule 7b, 2026-10-14). It is a checklist row there; reopen this issue when work on it starts or its PR opens.

  8. noahgift commented on Oct 15, 2026

    @noahgift
    MemberAuthor

    Evidence (apr-4918): the #4274 fix is on main since the #4655 fold (989cb01).

    • A re-rendered chat history resumes from the automatic recurrent-state checkpoint. The checkpoint is placed just before the last <|im_start|>. See Checkpoint / checkpoint_at in crates/aprender-serve/src/gguf/inference/forward/qwen35_session.rs.
    • Test second_chat_turn_resumes_from_the_first_turns_checkpoint_4274 (crates/aprender-serve/src/api/qwen35_chat_backend_tests.rs) runs the 0.8B model on the official template, with thinking on and off. Turn 2 does not extend turn 1, reuses more than 0 tokens, and answers the same as a fresh full prefill.
    • Falsifier a_device_argmax_from_below_the_checkpoint_drops_it (crates/aprender-serve/src/session_tests.rs).

    No further PR is planned for #4274. The prompt_n accounting is #4214's (perf/4214-prefix-reuse-main), which ships on its own. Closing is the cop's call.

  9. modified the milestones: 0.71.0, 0.73.0 on Oct 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High priorityowner:la-0.71Owned by the 0.71 look-ahead slotsize:MK̂ 3-4 h

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions