Skip to content

1bit serve: warm the backend before the first client request (the first request decodes 5-23% slower) - #492

Merged
bong-water-water-bong merged 2 commits into
mainfrom
1bit/serve-warmup
Oct 21, 2026
Merged

bong-water-water-bong merged 2 commits into
mainfrom
1bit/serve-warmup

Conversation

@bong-water-water-bong

@bong-water-water-bong bong-water-water-bong commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

What was measured

A loom agent saw requests 1 and 2 on Flash-Next decoding and prefilling 15-20 % slower than request 3. The same
thing exists on HRX with engine main, and it is a property of the first request after a load, not of the prompt:

Qwen3.8-27B on HRX, three fresh llama-server loads, three identical prompts then three different ones, all
with cache_prompt off, max_tokens 48 (llama-server's own timings):

request decode tok/s prefill of the same prompt
1 11.72 / 11.75 / 11.75 945 / 879 / 869 ms
2 12.41 / 12.46 / 12.42 730 / 731 / 733 ms
3 12.35 / 12.36 / 12.36 716 / 720 / 714 ms

Qwen3-4B, same protocol, three loads: the load whose model file was still cold decoded request 1 at 54.67 tok/s
against 71-72 later (−23 %); the two loads with the file in page cache showed −0.9 % and −1.4 %, so the 4B case
is dominated by first touch of the model.

What it is not

  • not llama.cpp's warmup: --no-warmup leaves request 1 at 11.66 tok/s (the warmup graph is not the shape
    the client uses);
  • not the GPU ramping: 9 s of continuous dispatch before the first request leaves request 1 at 11.74 tok/s
    (and a temperature sweep of the same kernel from 56 °C to 81 °C moves it 291.08 → 294.55 µs, i.e. nothing);
  • not prompt caching: every measured request was sent with cache_prompt: false.

What it is

HRX's per-shape command-program build plus the first touch of that shape's buffers, paid by whichever request
uses the shape first. A synthetic request of the measured prompt's length, sent before the six measured ones,
moved the penalty onto itself and left request 1 at 12.47 tok/s, the same as requests 2-3 — and request 5 (a
different prompt length) then paid a fresh ~200 ms prefill build of its own.

The fix

serve_child now sends one synthetic request per backend as soon as the backend answers /health, before the
service reports itself ready: a ~465-token prompt with 8 decode steps, temperature 0, cache_prompt false,
HRX backends only, ONEBIT_NO_WARMUP=1 to turn it off. It runs on the lazily started laya backends too.

A/B through 1bit serve, two loads per arm, same protocol (request 1 against the best of requests 2-3):

arm request 1 decode request 1 prefill
ONEBIT_NO_WARMUP=1 −5.1 % / −7.2 % 890 / 930 ms
warm-up on (default) +1.1 % / +1.1 % 872 / 930 ms

Cost: 2.1-3.3 s of load time on the 27B, logged as 1bit serve: warm-up request on port N took M ms.

What it does not fix

Only the decode shape and the warm-up's own prompt length are covered. A client prompt of another length still
builds its own prefill program once (~130-200 ms on the 27B — visible above as request 1 prefill 872-930 ms
against 720-726 ms for requests 2-3 of the same prompt). Removing that needs static prefill shapes (padding to
a fixed ubatch) in llama.cpp, which this change deliberately does not touch.

Checks

  • scripts/hw-gate.sh full on the binaries this engine ships (build-warm/hrx/llama/bin, llama.cpp
    5dcf6623418b, hrx-system 82804197bcbe, gpu-kernels 8ebe8da), commit 5914c13: PASS on all seven
    models — qwen3.8-27b kat/kld/placement (152 graphs), zaya1-8b + qwen3-coder-30b-a3b + qwen3-4b-iq4xs
    kat/kld/placement, qwen3-0.6b + -kq8 + -kvq8 with ops 663/663, kat, kld, needle (7082-token prompt) and
    placement; 0 amdgpu ring timeouts across the three runs.
  • the A/B above, and the pre-fix measurements, from ~/wt/mtpgap/{warm,warmreport,serve-warm}.{py,sh}.

🤖 Generated with Claude Code

bong-water-water-bong and others added 2 commits October 21, 2026 13:58
…st request decodes 5-23% slower)

The first request after a fresh load pays a one-time cost that is not the model's steady state. Measured
with engine main on strixhalo, Qwen3.8-27B on HRX, three fresh loads, three identical prompts then three
different ones with cache_prompt off:

- request 1 decoded 5 % slower than requests 2-3 (11.72/11.75/11.75 against 12.41/12.46/12.36 tok/s), in all
  three loads, and prefilled 20 % slower (869-945 against 714-733 ms)
- Qwen3-4B with a cold model file: request 1 decoded 23 % slower (54.67 against 71-72 tok/s)

It is not llama.cpp's warmup (--no-warmup leaves it at 11.66 tok/s), not the GPU ramping (9 s of continuous
dispatch before the first request: 11.74 tok/s) and not prompt caching (all requests sent with
cache_prompt off). It is HRX's per-shape command-program build and the first touch of that shape's buffers,
paid by whichever request uses the shape first: a synthetic request of the same prompt length before the
measured ones moved the penalty off request 1 (12.47 tok/s) and left requests 2-3 unchanged.

So serve now sends one synthetic request per backend as soon as the backend answers /health, before it
announces itself ready. Through 1bit serve, two loads per arm, requests 1-3 of the same prompt:

  warm-up off   request 1 decoded 5.1 % / 7.2 % slower than the best of requests 2-3
  warm-up on    request 1 decoded 1.1 % / 1.1 % FASTER than the best of requests 2-3

Cost: 2.1-3.3 s of load time on the 27B (logged as "warm-up request on port N took M ms"). HRX backends
only, and ONEBIT_NO_WARMUP=1 turns it off.

What it does not fix, stated so nobody expects it to: only the decode shape and the warm-up's own prompt
length are covered. A client prompt of another length still builds its own prefill program once, ~130-200 ms
on the 27B (the warm-up above left that excess in place: request 1 prefill 872-930 ms against 720-726 ms for
requests 2-3 of the same prompt). Making prefill shapes static (padding to a fixed ubatch) would remove it;
that is a llama.cpp change, not a serve one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@bong-water-water-bong
bong-water-water-bong merged commit 4d61f3e into main Oct 21, 2026
7 of 8 checks passed
@bong-water-water-bong
bong-water-water-bong deleted the 1bit/serve-warmup branch October 21, 2026 18:13
bong-water-water-bong added a commit that referenced this pull request Oct 21, 2026
…nt-backend carries the warm-up (#495)

Lemonade's standard benchmark had Lemonade's own Qwen3.8-27B entry drafting with the model's MTP
head (`--spec-type draft-mtp`, label "mtp") at 24-28 tok/s while the onebit recipe ran the same file
without a drafter at 11-12. And on the one-hop route lemond starts llama-server itself, so the
warm-up request serve sends a ready backend (#492) never ran.

- launch_for: a .gguf with <architecture>.nextn_predict_layers > 0 on hrx gets
  `--spec-type draft-mtp` (no -md: llama-server loads the head from -m), as Lemonade's llamacpp
  recipe does; --mtp-max / --mtp-p-min apply, recipes see drafter=mtp. Not with --mtp / --dflash,
  --parallel > 1, --embedding / --reranking, on cpu, or with --no-mtp / ONEBIT_NO_MTP=1. Both serve
  and the one-hop plan.
- --print-backend: an hrx chat plan carries "warmup": {path, body}, serve's own warm-up request
  (shared code now), for Lemonade's onebit recipe to send before it marks the model loaded
  (lemonade fork #16). ONEBIT_NO_WARMUP=1 leaves it out.
- tests/print_backend.sh: the default drafter, its opt-outs and the warmup key.
- docs/serve.md: both, with the Lemonade bench numbers (27B chat/code 11.1/11.9 -> 26.2/29.5 tok/s).

Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
bong-water-water-bong added a commit that referenced this pull request Oct 22, 2026
…yout at load (27B chat-short TTFT 2.64 -> 0.80 s) (#498)

Lemonade bench's chat-short TTFT on Qwen3.8-27B was 2.3-2.6 s after every reload against 0.5 s
later. Measured per prompt length on fresh servers (JIT cache warm): 1-12 tokens 0.1-0.5 s,
13-20 tokens 2.03-2.07 s, 21-512 tokens 0.45-1.23 s. The 13-20 class is not a program or JIT
build: perf shows ggml-hrx re-quantizing weights on the host
(materialize_symmetric_k32_eightgroups_shared4 from Q4_K/Q5_K/IQ4_XS, 6.1 s of CPU, +1.0 GiB
GTT) the first time its small-batch kernels run. llama-server splits a chat prompt at the last
user message and before its last 4 tokens, so chat-short (67 tokens) runs as 50 + 13 + 4.

- warmup_requests(): the chat request (#492) plus a 16-token /completion prompt (" a" x 16,
  n_predict 0), sent in order by serve and planned by --print-backend as "warmups" (with
  "warmup" = the first, for an older lemond). Token ids instead of text hit a 500 on
  Qwen3.5-4B (id 100 is a lone UTF-8 byte there).
- tests/print_backend.sh, docs/serve.md (the class map, the cause, the numbers).

Lemonade bench (P1, one hop, lemond on cores 0-3), TTFT ms before -> after: Qwen3.8-27B
chat-short 2643 -> 796, code-short 824 -> 818; Qwen3.5-4B 263 -> 138, 268 -> 142. Decode
unchanged. Load +2.1 s (27B), +0.2 s (4B). Greedy outputs identical with and without it.

Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg>
Co-authored-by: Claude Opus 5.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

Development

Successfully merging this pull request may close these issues.

1 participant