beast reserves KV for eight tenants to serve one — max_in_flight is starving the single deep session it actually gets #291
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Measured while asking why a 27B on 64 GB of RTX 5090 struggles for room
when the same model surpasses expectation on a single 24 GB card.
The capacity is there. It is being spent on breadth we do not use.
Observed concurrency: one request at a time, 90% of the time
Seven days of beast's journal, start/done paired by
req_id(an earlierunpaired count drifted to a nonsense peak of 322 — 312 starts never log a
done, fromclient_gone, errors, and the window edge). 1,397 pairedrequests:
Utilisation: 4.9% — 8.3 busy hours in a week.
And most of the concurrency that exists is ours: helexa-bench's
concurrency:2/4/8scenarios are synthetic bursts, and they produce the4/8/9 spikes. Real multi-user demand on beast is close to nil.
(The peak of 9 against
max_in_flight = 8is the queue draining, not aviolation — but the effective ceiling is not exactly 8.)
What that costs the session we do get
At 32 KB/token/card that budget is 150,816 tokens, divided by
max_in_flight = 8→ 18,852 tokens per slot.Meanwhile we advertise a 131,072-token context. Per card:
A card holds 31 GB and already carries ~21 GB of weights. The
advertised context and the concurrency setting are arithmetically
incompatible by roughly 7×. An agentic session gets ~19k tokens of KV
while seven idle slots hold reservations for callers who are not there.
This is also why #290's
xhighblowout was so easy to hit: a modelinstructed to deliberate exhaustively, inside an output ceiling set
within a KV allocation sized for eight tenants.
Recommendation:
max_in_flight = 22keeps the batch engine meaningful — measured at 1.79× at twoslots, and 5.24× at eight is throughput we are not being asked for —
gives the 27B a genuinely deep window, and leaves admission to queue the
~6% above two slots, which it already does gracefully with
Retry-After(#53).
Config-only, on a file CI now ships, and reversible.
Reference implementation worth mining
https://github.com/syv-ai/qwen38-27b-rtx3090 — vLLM settings for
this exact model on a single 24 GB RTX 3090. It already encodes the
single-vs-multi-user split this issue is proposing, and several of its
choices map onto open issues here:
single-user/start_qwen.sh(latency, 64kcontext, MTP-4 speculation, ~120–124 tok/s greedy) versus
batch/start_qwen.sh(64 concurrent, ~1,035 tok/s aggregate, 8seats). Two named configurations, not one compromise.
CTX=fast(bf16, ~64k),CTX=long(fp8 via FlashInfer, 114–150k),CTX=huge(4/2-bit KVarN,240k+, lossy). That is #289 promoted from a tuning knob to a serving
profile, and it is how they buy context rather than by adding VRAM.
SPEC=mtp, k=4). That is #79, wherethe draft head is already confirmed present in our deployed weights
(849 MB,
mtp_num_hidden_layers = 1).SPEC=dflash2theymeasure 137 tok/s at 1 stream and 97 tok/s per-stream at 2, noting
"the limit is the pool rather than
MAX_SEQS". Slot count is notthe only thing that binds, and an adaptive scheduler that only moves
max_in_flightmay find the pool binds first.For scale: they get 120–124 tok/s single-user on one 3090; beast
measures 32 tok/s on two 5090s. Their weights are W4A16 with int8
lm_head against our q6k, and they speculate where we do not — so the gap
is configuration, not silicon. Same observation as the llama.cpp writeup
on #282.
The harder half: adapting at runtime
The obvious conclusion is that a single user should be able to use the
capacity idle tenants are reserving. One constraint has to be stated
before anyone designs it:
KV is planned at load.
max_in_flightis both the admission boundand the batch engine's slot count, and the KV budget is derived from it
when the model loads. So "use the spare capacity when nobody else is
here" is not a per-request knob — it implies re-planning the KV budget,
which today means a model reload (~5–6 min of 27B cold-load on beast).
Two shapes, in increasing cost:
Nearly free — it is the
max_in_flight = 2change above — andcaptures most of the benefit given the measured distribution. A third
caller waits rather than being served a shallow window.
re-plan KV for a deeper single session, and re-plan back under
contention. Needs: an idleness signal that cannot thrash, a way to
re-plan without dropping in-flight work, and an answer for what
happens to the advertised
limit.contextwhen it changes underneatha client that has already read it. The advertised context changing
mid-session is a client-visible contract change, not just an internal
one.
(2) is the interesting version and should not be attempted before (1) is
measured — if
max_in_flight = 2restores the deep window, theremaining upside from adaptivity is the 6% tail.
Suggested order
max_in_flight = 2on beast; re-measure the tower-defence runand
helexa-benchconcurrency:*. Cheap, reversible, testable.bench, so the distribution reflects users rather than our own
instrument (#288's lesson).
quantisation (#289) and speculation (#79) return more for the effort.
Related
#290 (the reasoning work that surfaced this), #289 (KV quantisation),
#79 (MTP speculation — draft head confirmed in our weights), #282
(single-stream decode gap vs llama.cpp), #98 (the batch engine these
slots feed), #53/#54 (admission and fair-share), #257 (KV budget).
Fixed in
43e873f8, deployed and verified (batch engine started max_slots=2on beast).00acd283corrects the docs.beast now runs
max_in_flight = 2. Each slot gets ~59,000 tokens of KVinstead of a fraction of the advertised context, so a session can
actually reach the window we publish.
The justification sits in
asset/neuron/beast.tomlbeside the value,because a number like this is worthless without the evidence that chose
it: seven days of paired request spans showed 89.6% of busy time was
one request in flight and 93.6% two or fewer. Eight slots were
sizing for traffic that does not arrive, and charging every real session
for the privilege.
00acd283fixes the docs example that advertised8— an operatorfollowing it would have reproduced this bug on their own hardware, which
makes the doc the more durable half of the fix.
Correction — reopening. The premise of this issue is wrong, and the
fix has been reverted (
21eefe7e).The issue and its closing comment both rest on:
That has not been true since
eca42510(2026-08-15), eleven daysbefore this issue was filed, which made admission reserve KV per
request:
kv_budgetis a semaphore of MiB permits. Each request takes what itsown prompt needs and waits for bytes when the pool is short — which is
precisely what
kv_max_wait_secsexists for.grepfinds nothing inthe tree dividing that budget by the slot count, and the engine's
max_slotsis only the batch width.So the arithmetic in the issue body — "the advertised context and the
concurrency setting are arithmetically incompatible by roughly 7×" — is
not a real constraint. Eight slots do not give a session a 19k window.
The session gets whatever it needs from a shared 118,000-token pool, and
concurrent sessions contend for bytes rather than being partitioned.
The measurement stands; the conclusion does not. Seven days of
paired spans really did show 89.6% of busy time at one request in flight
and 4.9% utilisation. But that data cannot demonstrate starvation that
the code does not implement — and it described traffic that no longer
exists. While it was being taken, the router's
helexa/balancedaliaspointed the authenticated web chat at
Qwen/Qwen3.6-27Bwhile beast'sresident model was the 3.8 (see
3b0b3a22), so per-model concurrencywas split across two models that were busy evicting each other. Both
public tiers now resolve to the flagship, so chat concurrency lands on
one model.
Two things worth keeping from this:
me, while editing them.
kv_max_wait_secswas documented as "howlong it waits for enough KV budget" twelve lines above the claim that
the budget is divided per slot. One of those describes a shared pool
and the other a partition. Both were written down and shipped.
verified mechanism. This one had seven days of production spans, a
trade table, and a live
batch engine started max_slots=2confirmation — every number correct, attached to a causal claim that
was never checked against the code it described.
21eefe7erestoresmax_in_flight = 8, corrects the operating docs,and fixes the prefix-cache trade table, whose "tokens per slot" column
had silently divided the pool by two.
What remains genuinely open here:
00acd283also changed the docs'example from 8 to 2 on this reasoning, and #295/#292's closing comments
quote per-slot figures. Those are corrected in the same commit. If
anyone acted on the 2-slot guidance on their own hardware, this is the
retraction.