…e it (#390)
The llama.cpp side is already in the pin (#441, llama.cpp fork #102): ggml-hrx's scheduler
knows the backend of every node, prints one line when GGML_HRX_REPORT is set, and aborts on
the first node off HRX with its name, op, type and shape when GGML_HRX_STRICT is. The engine
asked for neither, so a graph that partly ran on the CPU was invisible.
- --placement-report: passes -lv 4 to the backend for that load (llama-server drops the
library's INFO lines below verbosity 4, common/log.cpp) and prints serve's own line at
load: "1bit serve: HRX placement: 34 of 512 nodes off HRX (GET_ROWS x 4, MUL_MAT x 30), 7
graph splits". The same numbers are the onebit_cpu_fallback_ops series serve already read.
- --strict-device: sets GGML_HRX_STRICT=1 and refuses to start. A backend that refuses the
graph is reported as that, not as "did not become ready", and a report that still names
off-HRX nodes after a backend reached ready is a refusal with the same numbers. It needs
--device hrx or auto; an auto run with no HRX backend says so instead of passing silently.
- A GGML_HRX_REPORT / GGML_HRX_STRICT the user sets in the environment wins over serve's.
- app/serve_metrics.h: read the report's own count ("N off HRX": for a clean graph the first
number after the phrase was the split count, 1 instead of 0) and the graph's node and split
counts, which the metric series and the new line both use.
- tests/hrx_placement_test.cpp (ctest hrx_placement, listed in the CI build step): the
observer's line in, the counts, the summary and the refusal out; no GPU and no backend.
- docs/serve.md: "Off-HRX placement: --placement-report, --strict-device".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Engine #390, engine side only (no Flash-Next perf work). The llama.cpp half is already in the pin (#441 pins 8d9ce2c1a, which has fork #102): ggml-hrx's scheduler knows the backend of every node and can report the placement or refuse the graph. Nothing in
1bit serveasked it to, and nothing told the user.What this adds
--placement-report: at load, serve prints one line with the op types HRX placed on the CPU, their counts and the graph split count:and
1bit serve: HRX placement: all 512 nodes on HRX, 1 graph splitswhen nothing was left behind. The same numbers are theonebit_cpu_fallback_ops{op}seriesapp/serve_metrics.halready derives from the backend's line. llama-server drops the library's INFO lines below-lv 4(it mapsGGML_LOG_LEVEL_INFOto its trace level,common/log.cpp), so the report never reaches serve at the default verbosity: this flag is what passes-lv 4to the backend for that load. Without the flag nothing changes.--strict-device: refuse to start instead of running part of the graph on the CPU. serve setsGGML_HRX_STRICT=1, the backend aborts at load on the first node off HRX naming its op, type and shape, and serve exits non-zero with its own line instead of "did not become ready". If a report does arrive and names off-HRX nodes anyway, serve refuses with the same numbers. It needs--device hrxorauto; with--device cpu|ds4|onnx|nputhe combination is refused, and anautorun that ends up with no HRX backend says so instead of passing silently.Both flags only set what the backend already reads, and a
GGML_HRX_REPORT/GGML_HRX_STRICTin the environment wins over what serve passes (0 turns either off), so an operator keeps control.Unit test, CPU only (
tests/hrx_placement_test.cpp, ctesthrx_placement, added to the CI build list): the observer's exact line in, then the node count, split count, off-HRX count and per-op counts out; the summary and the refusal text; unrelated lines ignored; a total-only "CPU fallback: N" line from another backend still counted; andServeMetrics::placement()as the refusal's input. It also covers two parser defects the report exposes in the existing metrics reader: the first number after "off HRX" was taken as the total (for a clean graph that is the split count - 1 instead of 0), and the graph size and split count were not read at all.Docs:
docs/serve.md, "Off-HRX placement:--placement-report,--strict-device" (both lines, both flags, the verbosity reason, the env override, and the test).Validation (CPU only, ryzen,
nice -n 19, no GPU work)hrx_placementis one of them;metrics_serve(the existing end-to-end check ofonebit_cpu_fallback_opsagainsttests/fake_backend.py, whose line has no "N off HRX" count) anddevice_routestill pass, so the parser change and the new flags change nothing for the routesthat were already there.
python3 tools/copyright.py --checkand actionlint onci.ymlare clean.Not verified here: a real HRX run - the placement line,
-lv 4and the strict abort all need HRX0, and this box's GPU is under the orchestrator's queue. The strict path depends on the pinned backend honouringGGML_HRX_STRICT, which is what #441's pin carries; the engine side fails loudly if a graph is refused and is a no-op when the backend does not know the variable.🤖 Generated with Claude Code
Hardware check (HRX0, strixhalo, 2026-10-18, 30 min slot):
llama.cpp 1a5dcba2(fork #102) and this patch,1bit serveon Qwen3-0.6B-Q4_K_M and MiniCPM5-1B-Q4_K_M.--placement-report: the backend printedplacement_observer: placement: 986 nodes, 0 off HRX (none), 1 graph splits(750/1 for the 1B) and serve printed1bit serve: HRX placement: all 986 nodes on HRX, 1 graph splits, then came ready. The line only appears with the flag, which is the-lv 4requirement holding in practice.--strict-device: ready on both, which is correct for a clean graph, and serve notes that it saw no report (it does not force-lv 4).Not exercised on hardware: the refusal itself. Both models on that box put every node on HRX, and the model #390 reports as falling back (Qwen3.8-Flash-Next UD-IQ4_XS) is not on strixhalo. The refusal is covered by the unit test (
hrx_placement: off-HRX counts, the exact refusal line) and by fork #102's ownGGML_ABORTpath, not by a live off-HRX graph.