Skip to content

fix(server): DECODE_APPLY KV restore check was inverted — success looked like failure - #93

Merged
ddvnguyen merged 3 commits into
hydra-forkfrom
fix/78-decode-apply-kv-restore-check
Aug 8, 2026
Merged

ddvnguyen merged 3 commits into
hydra-forkfrom
fix/78-decode-apply-kv-restore-check

Conversation

@ddvnguyen

@ddvnguyen ddvnguyen commented Aug 8, 2026 •

Copy link
Copy Markdown
Owner

Summary

llama_state_seq_set_data returns the number of bytes read on success and 0 on failure — per its own doc comment in include/llama.h:

// Returns:
//  - Positive: Ok
//  - Zero: Failed to load

server-context.cpp:4534's original check was if (status != 0), backwards — it treated every successful restore (nonzero = bytes read) as a failure. Confirmed against upstream ggml-org/llama.cpp directly (not just this fork's copy of the doc comment): src/llama-context.cpp's implementation is untouched by the fork, and every real call site upstream (tools/server/server-task.cpp, tests/test-save-load-state.cpp) checks the expected byte count, never != 0 as failure.

Correction during review: the first fix pushed here changed the check to if (status != kv_len). That is also wrong, for a subtler reason: status (llama_state_seq_set_data's return value) only counts bytes the KV-cache reader itself consumed (llama_context::state_seq_read_data returns io.n_bytes(), a running counter of actual read()/read_tensor() calls — it has no knowledge of the buffer size passed in beyond an overrun guard). kv_len here is KV-state bytes plus trailing logits: PREFILL_DONE unconditionally appends n_vocab * sizeof(float) logit bytes after the KV state whenever logits are available (~line 4098) — the normal case for merged-decode, not an edge case — and the coordinator forwards that full blob verbatim as the DECODE 0x43 payload. So status < kv_len is the expected, correct outcome whenever logits are present, and comparing against kv_len reintroduces a false failure on essentially every real restore.

Fixed to if (status == 0), matching the STATE_PUT sibling check exactly (server-context.cpp ~line 3395).

Also fixed: DECODE_APPLY was missing the trailing-logits extraction entirely. The STATE_PUT handler computes remaining = state_len - n_read and, when it equals n_vocab * sizeof(float), injects those floats into the per-slot restored_logits buffer so the first sample after restore doesn't read garbage (server-context.cpp ~line 3405-3417, comment: "without these, common_sampler_sample reads garbage after StatePut"). DECODE_APPLY restored the KV cache but silently dropped this step. Ported the same logic in.

Evidence this is the real bug behind #78

157626320 in #78's reported log line (KV restore failed (157626320)) is ≈150.3 MiB — matching almost exactly "the 150 MB KV blob" the issue itself describes as the input being restored. The restore was succeeding the whole time; the check just reported it as failed. Same shape independently observed in an earlier debug session (KV restore failed (36656)).

Provenance

The inverted check was introduced fresh in PR #64 ("feat(rpc): ggml-org#470 Phase 2 — framed DECODE (0x43) rewrite", merged 2026-07-24) and survived two subsequent PRs that touched adjacent lines for unrelated reasons — #66 (2026-07-28) and #69 (2026-07-29, Closes ggml-org#506) — without the polarity being caught in any of those review rounds. The missing logits-injection step also dates to the same PR #64 (DECODE_APPLY was written without porting the full STATE_PUT success path).

Verification

Not yet build- or live-rig-verified — no local build directory was available in this worktree. The changes are localized to the DECODE_APPLY success/failure branch, use only existing fields (slot->restored_logits, slot->logits_valid, vocab) already used identically in the STATE_PUT sibling a few hundred lines above, and introduce no new symbols. Live-rig E2E verification (the project's own required step before merge) is still outstanding.

Closes #78

ddvnguyen and others added 2 commits August 8, 2026 09:40
…ked like failure

llama_state_seq_set_data returns the number of bytes read on success (0
means failed to load), per its own doc comment in include/llama.h and
confirmed identical in upstream ggml-org/llama.cpp. Every real call site
upstream (tools/server/server-task.cpp, tests/test-save-load-state.cpp)
checks the full expected byte count, not "nonzero == error".

The DECODE_APPLY handler had it backwards: `if (status != 0)` treated
every successful restore (which returns the byte count) as a failure,
aborted the decode, and logged a bogus "KV restore failed (N)" where N
was actually the correct number of bytes restored. The sibling STATE_PUT
call site 20 lines above (server-context.cpp:3394-3397) already had the
correct check.

Fixes the symptom in #78 — 157626320 in that issue's log line is ~150.3
MiB, matching "the 150 MB KV blob" the issue itself describes as the
input; the restore was succeeding the whole time.

Closes #78

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…h, and never injected trailing logits

Correction to the previous commit on this branch: `status != kv_len` was
still a false-failure. llama_state_seq_set_data's return value only
counts bytes the KV-cache reader itself consumed (llama-context.cpp
state_seq_read_data -> io.n_bytes(), a running counter of actual reads)
-- it has no knowledge of the trailing logits block PREFILL_DONE appends
after the KV state (~line 4098, unconditional whenever logits are
available, which is the normal merged-decode case). So status < kv_len
whenever logits are present, and `status != kv_len` fails exactly the
case it needed to pass. Corrected to `status == 0`, matching the
STATE_PUT sibling check exactly (server-context.cpp ~line 3395).

Also: DECODE_APPLY was missing the trailing-logits extraction entirely.
STATE_PUT computes `remaining = state_len - n_read` and, when it matches
n_vocab*sizeof(float), injects those floats into the per-slot
restored_logits buffer so the first sample after restore doesn't read
garbage. DECODE_APPLY restored the KV cache but silently dropped this
step. Ported the same logic (kv_len - status / expected_logits /
slot->restored_logits / slot->logits_valid) into DECODE_APPLY.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…b checkpoint, align n_prompt_tokens_cache (llama#94)

- Failure path now clears tokens/checkpoints/cache and removes KV cells,
  mirroring STATE_PUT so the slot is not left poisoned (n_past > 0 with
  no KV cells -> pos_min == -1 abort on next decode).
- v2 header checkpoint is now captured (not skipped) and registered after
  restore, with a fabricated (0, n_past-1) fallback — fixes hybrid/
  recurrent models whose KV was restored without its memory checkpoint.
- n_prompt_tokens_cache/processed now read hdr_n_tok (blob_n_tok), the
  same header field STATE_PUT uses, instead of blob_n_past.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant