Repository navigation
feat(rpc): #470 Phase 2 — framed DECODE (0x43) rewrite + HTTP /v1/decode/{id} - #64
Conversation
…trieval Phase 2 of ddvnguyen/hydra_vortex#470 — merged P/D StatePut + Decode. The binary DECODE (0x43) RPC is rewritten from a streaming token-output path (which cannot carry model identity in its response) to a framed request/response that: 1. Reads [4B json_len][json][raw KV bytes] (32 KiB JSON cap) 2. Validates model identity via GGUF-derived getters (tokenizer, model_name, capabilities — hard-abort on MTP/VISION mismatch) 3. Restores KV into slot (reuses STATE_PUT path via llama_state_seq_set_data) 4. Returns immediately with decode_request_id (does NOT wait for generation) 5. Kicks off generation on the task queue asynchronously New HTTP routes: GET /v1/decode/:decode_request_id — SSE stream or buffered OAI JSON DELETE /v1/decode/:decode_request_id — cancel in-flight decode Result buffer bounded by HYDRA_DECODE_RESULT_TTL_S (default 300) and HYDRA_DECODE_RESULT_MAX (default 1024) env vars. The 9-field hydra_metrics sidecar is wired to the /v1/decode response (including model_identity, match, decode_ms, kv_bytes, t3_reloaded). t3_reloaded/t3_reload_ms remain stubbed (known gap). Engine INFO (0x41) now advertises 'merged_decode' capability. Files changed: tools/server/server-rpc.h — framing constants tools/server/server-task.h — hydra_action + result fields tools/server/server-task.cpp — to_json() for 0x43 tools/server/server-context.h — decode_result_entry, routes tools/server/server-context.cpp — rewritten hydra_handle_decode, new task handler with identity validation + KV restore + generation, HTTP route handlers, merged_decode cap tools/server/server-http.h/cpp — del() method for DELETE routes tools/server/server.cpp — route registration Build: cmake --build build --target llama llama-server-impl — clean
…s() generation Fixes two architectural issues identified in review of commit 5ba077f: Problem 1: The blocking generation loop in SERVER_TASK_TYPE_HYDRA_ENGINE_DECODE monopolized the shared task-processing thread, preventing any other requests from being processed during generation. Now the DECODE task handler only does validation + KV restore (fast, bounded), then posts a SERVER_TASK_TYPE_COMPLETION task with id_slot forced to the restored slot. update_slots() drives generation exactly as it does for every other completion in this server. Problem 2: The immediate 0x43 RPC response hardcoded valid:true instead of waiting for the identity validation to complete. Now hydra_handle_decode uses the same recv_with_timeout pattern as hydra_handle_configure (30s timeout) to synchronously wait for the validation+restore result before building the RPC response from real match/valid/restore_slot_ms data. Key changes: - SERVER_TASK_TYPE_HYDRA_ENGINE_DECODE handler: validation + KV restore only (removed generation loop, sampler setup, decode_results buffer storage) - hydra_handle_decode: two-phase flow: Phase 1 (sync): post validation task, wait for result, build real RPC response Phase 2 (in task handler): build SERVER_TASK_TYPE_COMPLETION task with flattened prompt data, force id_slot, post to queue — update_slots() takes over - Removed background consumer thread infrastructure (s_decode_consumer_*) - Removed completion_prompt/generation_started from decode_result_entry - decode_results buffer still populated by HTTP GET endpoint reading results Build: cmake --build build --target llama llama-server-impl — clean
The two-phase DECODE split (sync validate+restore, async generation via
a normal SERVER_TASK_TYPE_COMPLETION task) dropped the code that wrote
into decode_results when the hand-rolled generation loop was replaced.
Nothing then consumed the posted COMPLETION task's result, so
GET /v1/decode/{id} always returned not-found, even for successful
decodes.
Add a detached background thread (mirroring the existing STATE_GET
background-thread pattern) that waits on queue_results for the
completion task's final result and copies it into a decode_result_entry,
merging in the match/model-identity data already computed during the
sync validate+restore phase.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
QA Review — PR #64 (Phase 2: framed DECODE 0x43 rewrite)Reviewed Findings (ranked by severity)1. (Blocking — race condition, silent result loss)
These two operations (RPC thread's 2. (Blocking — orphaned request / resource leak on RPC-side timeout) The 30s 3. (Bug — messages path silently mistokenizes) Chat Verified as correct (matching the PR's claims / instructions' checklist)
Minor / non-blocking
VerdictFindings #1 and #2 are concurrency-correctness bugs in the core mechanism this PR exists to build (reliable async result delivery), not edge-of-scope nits — they directly undermine the "fixed, not left as a known gap" claims in the PR description. Recommend fixing before merge. REQUEST_CHANGES
|
- Completion task gets its own distinct completion_id (was reusing decode_request_id, causing race on waiting_task_ids between val_task and cmpl_task — results silently dropped under concurrent load) - Timeout error response now includes decode_request_id as JSON (was empty body, leaving caller with no way to track orphaned request) - DECODE endpoint rejects 'messages' field with HYDRA_STATUS_BAD_REQUEST (was silently tokenizing raw JSON instead of chat-template-formatted text) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
704686b added a 'messages' field rejection in the HYDRA_ENGINE_DECODE case that reused `res` (the RPC validation response) after it had already been moved into queue_results.send() a few lines above at the HYDRA_STATUS_OK success path. Any DECODE RPC whose prompt JSON contained a non-null `messages` field dereferenced a moved-from unique_ptr, crashing the engine process. By the time the messages check runs, the RPC request/response cycle for this decode_request_id is already over — the caller is polling GET /v1/decode/{id} for the result, not waiting on a second RPC response. Route the rejection through the same decode_results buffer that a normal completion would populate instead of touching `res`: add an `error` field to decode_result_entry, populate it under decode_results_mutex, and have GET /v1/decode/{id} return it as an error response (then drop the entry) instead of building an empty completion body. Verified no other queue_results.send(std::move(res)) reuse exists elsewhere in the diff introduced by 704686b. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Summary
Part of ggml-org#470 ("Merged P/D StatePut + Decode with GGUF-derived model identity"), Phase 2 of a 4-phase rollout. Depends on Phase 1 (#63, GGUF identity getters) — base branch is
feat/470-gguf-model-identity, nothydra-forkdirectly; rebase target after Phase 1 merges.Rewrites the binary DECODE (0x43) RPC. Today it sends the RPC response header before generation starts, then streams raw token frames — a real, already-hit wall documented in-code ("DECODE sends response header before task completes... cannot carry model identity in RPC response meta"). There were zero production callers of that shape (confirmed via Core-side audit), so this is a clean rewrite, not a back-compat-preserving change.
New design: merge STATE_PUT + decode-kickoff into one framed request that validates model identity, restores KV, and returns a
decode_request_idimmediately — generation runs async, retrieved via newGET/DELETE /v1/decode/{id}HTTP endpoints. This is the PREFILL → save slot → DECODE (get a request id) → HTTP streams result flow.Architecture (two-phase, went through 3 review rounds to get right)
Two structural issues came up in review and are fixed in this PR (not left as known gaps):
whileinsideprocess_single_task— the single thread shared by every opcode and every slot on the engine instance. That would have frozen the entire engine (no other PREFILL/STATE_GET/etc.) for the duration of any in-flight decode. Fixed by reusing the existingSERVER_TASK_TYPE_COMPLETION+update_slots()machinery — the same mechanism every other completion in this server already uses — instead of a hand-rolled loop.{"valid": true, ...}unconditionally from the RPC handler without waiting for actual validation. Fixed by havinghydra_handle_decodesynchronously wait (viarecv_with_timeout, same pattern ashydra_handle_configure) for the validate+restore phase, so the immediate response reflects the real result — callers can trustvalid/matchwithout polling.SERVER_TASK_TYPE_COMPLETIONtask, but nothing consumed that task's result —GET /v1/decode/{id}would 404 on every request, even successful ones. Added a detached background thread (mirrors the existingSTATE_GETbackground-thread pattern in this file) that drainsqueue_resultsfor the completion task and populatesdecode_results.Changes
tools/server/server-context.cpp—hydra_handle_decoderewritten for the framed wire format + synchronous validate+restore wait;SERVER_TASK_TYPE_HYDRA_ENGINE_DECODEcase does identity validation (via Phase 1's GGUF getters) + KV restore only, then posts aSERVER_TASK_TYPE_COMPLETIONtask; new background thread populates the result buffer;GET/DELETE /v1/decode/:decode_request_idhandlers;merged_decodeadded to INFO (0x41) capabilities.tools/server/server-task.h/.cpp—hydra_actiongainsdecode_json/kv_data/decode_request_id;server_task_result_hydra_enginegainsdecode_request_id/match_valid/match_json/restore_slot_ms/decode_init_ms/id_slot.tools/server/server-context.h—decode_result_entrystruct + bounded buffer (HYDRA_DECODE_RESULT_TTL_S/HYDRA_DECODE_RESULT_MAXenv vars, default 300s/1024 entries).tools/server/server-rpc.h—HYDRA_MAX_JSON_HEADER(32 KiB cap).tools/server/server-http.h/.cpp,server.cpp—del()HTTP method support, route registration.Test plan
cmake --build build --target llama llama-server-impl— clean compile, verified independently by me (reviewer) after every round of changes, not just agent self-reports.Known gaps (acceptable, documented in-code)
t3_reloaded/t3_reload_msin thehydra_metricssidecar remain stubbed (false/0.0) — real T3-reload-state plumbing is out of scope for this PR.DELETE /v1/decode/{id}cancellation is best-effort: removes the buffered result but does not interrupt in-flight generation.stoptype tracking has minor gaps in edge cases.Notes for reviewer
Does not touch STATE_PUT (0x31) — it's unchanged, still used by engines that don't advertise
merged_decode. Does not touchhydra_classify_config_key()or CONFIGURE (0x40) — out of scope, those are ggml-org#487's territory (already merged via PR #62).🤖 Generated with Claude Code
Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com