You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Bug]: BDONE replaces the request's accumulated decode_ms/generated — every finish=stop request reports a single-step decode time #1869
Requests that end with finish=stop report a single-step decode time instead of the request's accumulated decode time, so predicted_ms/predicted_per_second are wrong by ~100-200x. finish=length requests are fine, which is why this hides for a while.
decode_tok_s comes out sane by accident: it is computed as generated / (decode_ms/1000) = 1 / 0.0184, i.e. one token in one inter-token interval, which happens to be the right order of magnitude for a decode rate. Any client that computes completion_tokens / predicted_ms (the llama.cpp convention, and what the timings block is for) gets 7,337 tok/s for a 135-token answer at ~54 tok/s.
finish=length rows are consistent (256 tokens / 7925.5 ms -> 32.3 tok/s), so the bad value is specific to the stop path: /metrics rows for stop-finished requests are decode_ms 18.4 / 18.5 / 18.8 / 19.3 / 19.4 / 22.2 / 22.5 / 24.7 ms with engine_generated 1, while length-finished ones carry real totals.
Where it comes from
serve/server.py, the streaming loop (around line 1363 in 0.1.41) — the BDONE handler replaces the per-request stats instead of accumulating them:
serve/test_request_accounting.py is the contract here — it exercises thinking-budget continuations and asserts the totals are summed (assertEqual((row["prompt_ms"], row["decode_ms"]), (47.0, 280.0)), i.e. 40+7 / 200+80). The BDONE path overwrites, so a continuation's last segment wins over the request's totals.
Suggested fix / workaround
A segment must never shrink the request's totals — accumulate, or keep whichever value is larger:
We have been running exactly this as a local patch since 0.1.41 (with serve.test_request_accounting + serve.test_server + serve.test_prometheus green, 284 tests) and /v1/status then reports predicted_ms 714.0 / predicted_per_second 42.0 for a 30-token stop turn — consistent with the ~54 tok/s the engine is actually doing.
Thank you, this was an exact report: the two numbers from the same request, the finish=stop vs finish=length split, and the local patch made it easy to find.
Cause, in two parts. First, the one you pointed at: when the engine's answer ends on its stop token, the server stopped reading at that token and never read the BDONE line right behind it (a background wait ate it), so the request kept the figures of its admission step: one token, one step. finish=length reads on until the BDONE, which is why only stop rows were wrong. 0.1.43 reads the BDONE before it lets the slot go. Second, a request that moves between the solo path and a batch slot (another request arrives, or its neighbour finishes) has one DONE/BDONE per leg, and the server replaced the request's figures with the last leg's. It now keeps each earlier leg's decode time and tokens, and the prompt figures of the leg that read the prompt, and adds them up. It is not a max(), because the server keeps the last figures per thread and a previous request's larger numbers must not leak into the next one. predicted_ms, predicted_per_second and /metrics decode_ms are totals again.
There are unit tests for both (serve/test_request_accounting.py, serve/test_parallel.py), and a concurrent pair on a --batch 2 engine on an RTX 3060 was checked against /metrics. If you still see a stop row that reports a single step on 0.1.43, please tell us.
Activity
serve: a request handed from a batch slot back to the solo path keeps…
serve: a request moved from the solo path to a batch slot keeps the s…
serve: a slot request that ends at its stop token reads its BDONE bef…
Niko1221 commentedon Oct 11, 2026
Thank you, this was an exact report: the two numbers from the same request, the finish=stop vs finish=length split, and the local patch made it easy to find.
Cause, in two parts. First, the one you pointed at: when the engine's answer ends on its stop token, the server stopped reading at that token and never read the BDONE line right behind it (a background wait ate it), so the request kept the figures of its admission step: one token, one step. finish=length reads on until the BDONE, which is why only stop rows were wrong. 0.1.43 reads the BDONE before it lets the slot go. Second, a request that moves between the solo path and a batch slot (another request arrives, or its neighbour finishes) has one DONE/BDONE per leg, and the server replaced the request's figures with the last leg's. It now keeps each earlier leg's decode time and tokens, and the prompt figures of the leg that read the prompt, and adds them up. It is not a max(), because the server keeps the last figures per thread and a previous request's larger numbers must not leak into the next one. predicted_ms, predicted_per_second and /metrics decode_ms are totals again.
There are unit tests for both (serve/test_request_accounting.py, serve/test_parallel.py), and a concurrent pair on a --batch 2 engine on an RTX 3060 was checked against /metrics. If you still see a stop row that reports a single step on 0.1.43, please tell us.
merge upstream/main (v0.1.43 line) into feature/sampling-presets-1129…
serve: 単独の経路とバッチの枠の間を移った要求も、全区間の decode の時間とトークン数を報告し、timings.prompt_…
serve: a request handed from a batch slot back to the solo path keeps…
serve: a request moved from the solo path to a batch slot keeps the s…
serve: a slot request that ends at its stop token reads its BDONE bef…
Report batch MTP draft counts across slot handoffs
serve: a continued request's record keeps its first read's cache sour…