fix(llama-cpp): stop a generation whose stream is gone - #11860
Open
localai-org-maint-bot wants to merge 1 commit into
Open
fix(llama-cpp): stop a generation whose stream is gone#11860localai-org-maint-bot wants to merge 1 commit into
localai-org-maint-bot wants to merge 1 commit into
Conversation
grpc::ServerWriter::Write() returns false once the peer is gone, and PredictStream ignored that result at every call site. The handler kept pulling decoded tokens and writing them into a dead stream, so the llama.cpp slot stayed busy until the generation ended on its own terms. A model configured with max_tokens 0 and a large context ends on its own terms only at the context limit. On a 35B model at ~41 t/s a 120k context is about fifty minutes, and a slot held that long is a slot every other request for that model queues behind. Two abandoned requests were enough to make a node with free VRAM and a healthy control plane serve nothing: new requests timed out waiting for a slot, each timeout abandoned another generation, and the node fell further behind the longer it ran. Track the peer instead. The first failed write retires it for good, since a stream never recovers, and the RPC's own cancellation flag folds into the same predicate so the loop has one condition to test. Returning early is what frees the slot: ~server_response_reader() posts SERVER_TASK_TYPE_CANCEL for whatever is still decoding. TTSStream already checked Write(); this brings PredictStream in line. Cancellation stays cooperative and is checked between decoded results, so a batch already in flight may finish before the request stops. Assisted-by: Claude:claude-opus-5
2 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
grpc::ServerWriter::Write()returnsfalseonce the peer is gone, andPredictStreamignored that result at every call site. The handler kept pulling decoded tokens and writing them into a dead stream, so the llama.cpp slot stayed busy until the generation ended on its own terms.A model configured with
max_tokens: 0and a largecontext_sizeends on its own terms only at the context limit. On a 35B model at ~41 t/s a 120k context is about fifty minutes, and a slot held that long is a slot every other request for that model queues behind.This came out of a live incident on a distributed deployment. Two abandoned generations had each produced 18,000+ tokens and were still decoding, holding both slots, while
node_models.in_flightread0— the frontend had long since given up. The node had 41 GB VRAM free, answered its NATS control plane instantly, and served nothing. Every new request timed out waiting for a slot, and each timeout abandoned another generation, so the node fell further behind the longer it ran. Restarting the worker cleared it; the next request answered in 1.1s.The fix tracks the peer instead. The first failed write retires it for good, since a stream never recovers, and the RPC's own cancellation flag folds into the same predicate so the loop has one condition to test. Returning early is what frees the slot:
~server_response_reader()postsSERVER_TASK_TYPE_CANCELfor whatever is still decoding.TTSStreamalready checkedWrite()(lines 3240, 3261); this bringsPredictStreamin line. Cancellation stays cooperative and is checked between decoded results, so a batch already in flight may finish before the request stops.Same class of bug, and same remedy, as #11822 (
fix(ds4): cancel abandoned inference) — llama-cpp never got the equivalent treatment.Notes for Reviewers
The decision is extracted into
stream_peer.hso it is unit-testable without a full backend build, following thethread_params.h/request_lifecycle.hpattern.stream_peer_test.cppis picked up automatically bybackend/cpp/run-unit-tests.sh— no CI edit needed.Verify:
The sticky-retirement assertions are the ones that matter: I confirmed they fail if
observe_writeignores its argument, which is the exact bug being fixed.Worth knowing, because this fix does not cover it: cancellation only helps once a client has actually gone away. A client that waits still receives a full context worth of tokens, so a generation cap on the model config remains the real guard. The docs note this, and the incident config had
max_tokens: 0withrepeat_penalty: 1— no cap and no brake on a repetition loop.Not addressed here, found in the same incident and worth separate issues:
node_models.in_flightis persisted and never reconciled, so counters survive an OOM-killed frontend as permanent phantom load.POST /api/nodes/:id/models/unloadreturned{"message":"model unloaded"}/ HTTP 200 twice while the backend process kept running and holding 27 GB —StopBackendfails silently.Signed commits