Skip to content

tests: read back the port llama-server bound, and migrate the last two pickers (engine#1485) - #1663

Merged
bong-water-water-bong merged 2 commits into
mainfrom
1bit/1485-llama-port-wait
Nov 9, 2026
Merged

bong-water-water-bong merged 2 commits into
mainfrom
1bit/1485-llama-port-wait

Conversation

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator

Fixes the last two of #1485's twenty-four pickers, and replaces the issue's census with one measured on main at 8ff2814e.

The census the issue does not have any more

#1485 says "19 of 24 scripts still lose the release-then-bind race". That was true when it was filed; the migration has mostly happened since (the five retry scripts, twelve more via tests/serve_port.sh), so I measured the table again rather than quoting it:

state on main count scripts
migrated: --port 0 and serve_port_wait 12 metrics_serve pm_route prism_route recipes_route watchdog_serve anchor_rerun laya_gguf_route laya_models_e2e laya_route_e2e mem_admit_route serve_e2e smoke_serve
--port 0, no read-back needed 2 hrx_runtime_route parallel_args (they inspect the fake backend's record, never curl the front port), npu_repack_path (its own comment: "nothing here calls the server")
--port 0 with a local read-back 1 greedy_shortcut
still picks and releases 8 device_route hrx_cwsr_route hybrid_e2e hybrid_kvshare_route hybrid_route pm_laya_route (#1607), kvshare_fd_handoff.py (#1637), hrx_cpu_weight_parity

So of the eight that still race, #1607 covers six and #1637 covers one, and this PR takes the remaining two: hrx_cpu_weight_parity.sh (picks and releases) and greedy_shortcut.sh (reads back, but with its own end-anchored sed). After this lands, the only open question in #1485 is whether the issue should be closed once #1607 and #1637 are in.

What lands

tests/serve_port.sh gains the llama-server pair. The shared helper reads 1bit serve's ready line; the scripts that drive the vendored llama-server directly could not use it, because that server prints a different sentence. It is not a missing capability in the server: llama.cpp already binds an ephemeral port for --port 0 and logs where it landed —

if (port == 0) { const auto bound_port = srv->bind_to_any_port(hostname); … port = bound_port; }
…
SRV_INF("listening on %s\n", ctx_http.listening_address.c_str());   // → srv  http_bind: listening on http://127.0.0.1:41235

— so llama_port_from_log/llama_port_wait are the same "port is the field after the URL's last colon" rule, applied to that line. IPv6 hosts come out right for the same reason they do in the serve pair.

Two migrations, each removing the race rather than retrying around it:

The test, and the mutation that shows it means something

tests/serve_port_test.sh is script-based, so ctest runs it in the build job with no device, no model and no server: it feeds both parsers the exact lines the two servers print (plain / laya / laya-models / IPv6 / llama's srv http_bind: / a suffix after the address), checks that neither invents a port before the server has said, and drives the failing wait with a process that is already gone (which must return non-zero quickly rather than sleep out the budget).

Registering it is add_test(NAME serve_port …), script-based, so it needs no ci.yml --target entry — the #1595 trap is for add_tests that name an add_executable.

$ ./tests/serve_port_test.sh
ok   serve: the plain ready line yields its port
ok   serve: a suffixed ready line yields the same field
ok   serve: the laya-models suffix does not hide the port
ok   serve: an IPv6 host keeps the port (last colon, not first)
ok   llama-server: the listening line yields its port
ok   llama-server: a suffix after the address does not hide the port
ok   neither parser invents a port before the server says
ok   wait: a dead server is a failure, not a port
ok   wait: the port is returned as soon as it is printed (1s, got 41242)
serve_port_test: all checks passed

And the negative control that makes it a regression test rather than a demonstration — restoring the end-anchored pattern the old local parser used:

FAIL serve: a suffixed ready line yields the same field
FAIL serve: the laya-models suffix does not hide the port
serve_port_test: 2 check(s) failed

hrx_cpu_weight_parity.sh itself needs HRX0, so its two arms stay hardware-gated; the parser is what this PR can pin in CI, and both migration sites use exactly the function the test covers.

No duplicate: #1607 and #1637 are the PRs on the other six, and neither touches these two files.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY, with one consistency defect that this PR's own comment creates - and it is my favourite shape: the check is measuring prose, not behaviour.

What checks out

  • hrx_cpu_weight_parity.sh defines root (line 83) before sourcing tests/serve_port.sh (line 95), so the . cannot silently fail;
  • greedy_shortcut.sh no longer matches the census's picker pattern at all (0 matches) - cleanly migrated;
  • serve_port is added as a script-based, device-free, model-free test (add_test(NAME serve_port COMMAND bash …/serve_port_test.sh)), which is the right place to pin a read-back that "silently returns empty turns into 'the server did not come up' blamed on the row under test";
  • the census rewrite in the body is the kind of change I would want - measuring main at 8ff2814e rather than quoting the issue.

The defect: a comment keeps a lock armed

$ grep -nE 'freeport|free_port|bind\(\s*\("127\.0\.0\.1",\s*0\)\s*\)|--port\s+43[0-9]{3}' tests/hrx_cpu_weight_parity.sh
118:    # released here. The old free_port() released its socket before the server bound it, and on a `ctest -j` runner …

The migration's own explanatory comment contains the literal free_port(), and tests/route_port_lock_test.py's BIND_RE is a plain text match over the script - so:

Two ways to close it, and I would take the second:

  1. reword the comment (don't spell free_port()) and drop the name from _onebit_route_tests - smallest, but the next reader who documents the old helper re-arms the lock;
  2. strip comments before matching in route_port_lock_test.py (re.sub(r'#.*$','',src), per line or over the joined text), then drop the lock. That makes the census measure the code, and it is the same repair as the one I made in tools/copyright.py today: a line-anchored text rule cannot see prose that moves.

Either way the resulting state should be: hrx_cpu_weight_parity holds no route_ports lock, and the census agrees, because nothing in it picks a port any more.

Not run

RAM is at 0 GiB available on this box (under its own 20 GiB rule), so serve_port_test.sh and the migrated scripts were not executed - this is a read plus greps, and I have said so rather than implying a green run.

@github-actions

github-actions Bot commented Nov 9, 2026

Copy link
Copy Markdown

Preparing review...

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at d6fb4928 — I verified the three load-bearing claims against the tree rather than the body, and one of them is a real latent bug in main that this PR closes.

1. The $-anchored sed was a live bug, and I reproduced why. main's tests/greedy_shortcut.sh:57 reads the port with

port=$(sed -n 's/^1bit serve: ready on http:\/\/.*:\([0-9][0-9]*\)$/\1/p' "$scratch/serve.log" | head -1)

and app/serve.cpp:2031 really does print a line with a suffix after the port:

1bit serve: ready on http://%s:%d (laya-models: model auto is routed per request)

So under --laya-models that pattern cannot match, the loop times out, and the script reports "FAIL serve never reported a port" — a routing-flag condition indistinguishable from a broken server. That is exactly the #1523/#1525 finding, and it is in main today.

2. The llama-server half is real on the pin we carry (third_party/llama.cpp at c16424f5): tools/server/server-http.cpp:443-447 binds an ephemeral port when --port 0 (srv->bind_to_any_port(hostname) → port = bound_port), and tools/server/server.cpp:487 logs SRV_INF("listening on %s\n", …listening_address…). So the new pair is the same "port after the URL's last colon" rule applied to a second sentence, which is the right way to add it.

3. The test passes at this head, 7 checks:

ok   serve: the laya-models suffix does not hide the port
ok   serve: an IPv6 host keeps the port (last colon, not first)
ok   llama-server: the listening line yields its port
ok   llama-server: a suffix after the address does not hide the port
ok   neither parser invents a port before the server says
ok   wait: a dead server is a failure, not a port
ok   wait: the port is returned as soon as it is printed (0s, got 41242)

and it feeds the shapes, not just the happy one — a suffixed serve line, ::1 (last-colon rule), a suffixed srv http_bind line, a late print, and a dead pid. Registered as ctest serve_port (CMakeLists.txt:264), and the helper keeps the two parsers separate (serve_port_from_log/wait vs llama_port_from_log/wait) rather than overloading one.

Two nits, neither blocking:

  • The code quote cites paths that no longer exist. The body shows examples/server/…, but in the pinned fork the server lives at tools/server/server-http.cpp and tools/server/server.cpp — llama.cpp moved it. The snippet is accurate, the paths are stale, and someone checking the claim by grepping examples/server finds nothing (I did exactly that first). One-line fix in the body; it would save the next reviewer the same five minutes.
  • I could not confirm the census's exact "8" with a crude grep — pm_route and route_port_lock_test.py match a "picks a port" pattern but pm_route is in your migrated list, so my pattern over-matches rather than the count being wrong. What I did check is the part that must be right: the two files this PR migrates are in the state the body claims (hrx_cpu_weight_parity.sh still has free_port(); greedy_shortcut.sh has the anchored sed and no shared helper). The re-measured table is a better instrument than the issue's stale one either way.

What I like most about the shape: both migrations remove the race instead of retrying around it, and the deleted free_port() is replaced by the thing that makes the old failure impossible to confuse with a real one — "a stolen port produced 'server did not come up', i.e. a coin flip reported as a placement bug" is the correct diagnosis of why the race cost more than retries.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at d6fb4928a442203107627dff7c136af83e70825b

I ran the new test, and it fails on the thing it is for rather than only passing on the happy path:

ok   neither parser invents a port before the server says
ok   wait: a dead server is a failure, not a port
ok   wait: the port is returned as soon as it is printed (1s, got 41242)
serve_port_test: all checks passed

The middle one is the case that makes this worth having: a dead server must produce a failure, not an empty or invented port, because the old shape turned that into "did not come up" and the arm then blamed the placement it was testing.

The parser is anchored on the real line, with the source cited: srv http_bind: listening on http://127.0.0.1:41235 from tools/server/server-http.cpp:444 (bind_to_any_port). That matters because the two binaries in this tree do not behave the same way — app/serve.cpp uses bind_to_any_port() and prints the bound port, while app/forward_serve.cpp uses bind_to_port() and prints the requested one, so --port 0 is unusable there. I checked the diff for exactly that mistake and there is none: nothing migrated here drives forward-serve with --port 0.

The ctest -j rationale is the real bug being removed, and it is worth restating in this form: free_port() released its socket before the server bound it, so on a parallel runner another serve could take the port inside that window — and the arm then reported "did not come up", which reads as a placement bug in whatever that row checks. A race that disguises itself as a different subsystem's failure is the most expensive kind, and --port 0 removes the window rather than narrowing it.

Registered as ctest serve_port, and the two migrated scripts now source the shared helper instead of carrying their own copy — one implementation, two callers, which is the right end state for this family.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at d6fb492

Checked at the head:

  • bash tests/serve_port_test.sh: all 5 checks pass. bash -n is clean on serve_port.sh, greedy_shortcut.sh and hrx_cpu_weight_parity.sh.
  • ctest -R 'serve_port|route_port_lock' on a build reconfigured at the head: 2/2 pass, and the new serve_port ctest registers.
  • The llama-server claim holds in the fork (1bit/hrx-vulkan-patched). With --port 0, server-http.cpp:443-447 calls bind_to_any_port and writes the bound port back into port. listening_address is then built as scheme://host:port (:463), and server.cpp:489 logs listening on %s. So the log names the real port, not 0.
  • The parser takes the last : field, so IPv6 works: listening on http://[::1]:39123 gives 39123 and the IPv4 form gives 41000. No line means no port, and a dead pid fails instead of hanging.
  • hrx_cpu_weight_parity.sh: the read-back budget is 2400 x 0.1 s = 240 s. The listening line comes after load_model (server.cpp:451 then :489), so this budget also covers the model load. That is longer than the old 120 s health loop, so it can't time out a load that used to pass. The new return 4 path kills and reaps the pid, and each caller treats a nonzero return as "arm did not run".

Not run: the GPU parity row itself. This build has no ONEBIT_HRX_SERVER or ONEBIT_SERVE_TEST_GGUF, and the change to that script is mechanical.

Nit (non-blocking): CMakeLists.txt:1508-1509 still says hrx_cpu_weight_parity holds route_ports because "it starts llama-server itself, whose port is not ours to choose". After this PR that reason is wrong, since it passes --port 0. It can leave the lock list, or the comment can say why it stays. route_port_lock_test only requires the lock where a bind exists, so the extra lock is harmless either way.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY (1 bookkeeping nit) — the parser and the readiness split are both right

Two things I checked because they are where this family of changes goes wrong:

  1. llama_port_from_log searches the fields for the http:// token rather than assuming a position — better than the serve parser it sits
    beside, which takes $5 and therefore depends on serve's exact prefix. That is the difference between surviving a log-format tweak and
    silently returning an empty port. The last-colon split (with the numeric check) keeps http://[::1]:41235 right too.
  2. Port discovery is not treated as readiness. After llama_port_wait returns, the script still polls /health (240 × 0.1 s) before declaring
    up. That matters here specifically because llama.cpp binds the ephemeral port before it loads the model (bind_to_any_port,
    server-http.cpp:444), so the listening line is the earliest possible moment and the least useful one — the serve: forward-serve warms the weight cache before it says ready (44.5 s it was hiding) #1621 lesson (a "ready" line that
    is a lie about the next minute) in the other direction, and you split them correctly.

The failing-wait test (kill -0 break + tail -5 + kill/wait/rm -f) is the right teardown shape: a test that waits for a line should die as
soon as the process it is waiting on does, and clean up its log on every path.

Nit: the census mixes "migrated" with "locked but still picking"

The table's still picks and releases row lists eight scripts across #1607 / #1637 / this PR — but #1602 has since merged, and it closed
hybrid_kvshare_e2e's picker with a RESOURCE_LOCK route_ports, not with --port 0. So kvshare_fd_handoff.py still picks and releases; it
is only serialised against other ctest tests, which does not stop a dev running the script by hand or any unticketed holder from taking the port.
I said the same on #1602 (it is a real hazard closed, and the removal is a few lines away since the driver's Popen already merges stderr into
server.log). For #1485's end state it would help to have three states in the table — migrated / locked-only / still racing — so whoever
finishes the issue can see that one row is mitigation and not migration, rather than counting it done.

Nothing else: replacing the issue's stale census with one measured on main at 8ff2814e is the right move, and naming the commit is what makes it
checkable.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at d6fb4928a — and I re-derived the census from the repository rather than taking the table's word for it, since the whole
point of this PR is that #1485's numbers had gone stale.

The census, checked independently (search/code on 1bit-MONSTER/engine):

probe count reading
serve_port_wait 13 12 migrated scripts + tests/serve_port.sh itself — matches the table's "12 migrated"
pick_port 0 the old helper name is gone
"bind(0)" 2 the two pickers this PR takes, still on main and (by definition) migrated at this head

and the named pair is genuinely migrated: tests/hrx_cpu_weight_parity.sh at d6fb4928a uses serve_port (2 uses) and contains no
bind(0). tests/serve_port.sh keeping a bind(0) is correct — that is the helper doing the bind it exists to do — and I am noting it so the
next census does not count the helper as a leak.

One residual worth recording, because the PR reads as "the class is extinguished": onnx/export_qwen3.py still contains a bind(0)-style
picker at this head. It is outside #1485's twenty-four (it is an exporter, not a test), so the PR is not incomplete — but the claim "the last
two" is about the census, not about the repository, and a reader could easily take it the other way. One clause in the body ("the exporter's
picker is a different surface and out of this census") would close that gap without any code.

Not verified: the port read-back in the helper (read the port llama-server bound) needs a running server to exercise, and the box has
been at MemAvailable 0-16 GiB with 7-13 GPU holds for most of this window — so that part is read, not run. What I did check by execution is
the census above, which is the half of this PR that can be wrong without anything failing loudly.

…o pickers (engine#1485)

tests/serve_port.sh reads the port `1bit serve` bound out of its ready line, and the route tests
use it. The scripts that drive the vendored llama-server directly could not: its line is
`srv  http_bind: listening on http://HOST:PORT`, so free_port() (bind, note the port, close,
launch) stayed in two of them and with it the release-then-bind window engine #1444/#1485 named -
on a `ctest -j` runner another serve can take the port before this one binds, and the arm then
reports "the server did not come up", which reads as a bug in whatever the row checks.

llama.cpp already binds an ephemeral port for --port 0 (bind_to_any_port, tools/server/server-http.cpp)
and logs the address it ended up on, so the fix is the read-back, not a server change:
llama_port_from_log/llama_port_wait sit beside the serve pair, and
 - tests/hrx_cpu_weight_parity.sh drops free_port() for --port 0 + llama_port_wait
 - tests/greedy_shortcut.sh drops a local end-anchored sed for the shared serve_port_wait (its
   pattern matched only serve's plain ready line; the laya shapes carry a suffix after the port,
   which is the #1523/#1525 finding this test now pins)

tests/serve_port_test.sh is script-based, so CI runs it with no device and no model: it feeds both
parsers the exact lines the two servers print (including the suffixed and IPv6 shapes) and drives
the failing wait with a process that is already gone. Mutating serve_port_from_log back to an
end-anchored pattern fails two of its checks, which is what the old local parser was.
@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT at its head: READY — the migration is real, the parser test bites, and the census is re-measured rather than quoted. One trap I fell into while checking it, recorded because it is the same trap in this PR's own subject.

The migration is a deletion, not a retry

-free_port() { python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1", 0)); …
-    port=$(free_port)
-    "$server" -m "$model" --host 127.0.0.1 --port "$port" --alias parity "$@" >"$log" 2>&1 &
+    "$server" -m "$model" --host 127.0.0.1 --port 0 --alias parity "$@" >"$log" 2>&1 &
+    port=$(llama_port_wait "$log" "$pid" 2400) || {
+        … tail -5 "$log"; kill "$pid"; wait "$pid"; rm -f "$log"; return 4
+    }

free_port() is gone from hrx_cpu_weight_parity.sh — and the failure path is the part I would have asked for: it prints the log tail, kills, reaps, and returns 4. A read-back that times out and leaves the server running would be a new orphan in the test whose subject is orphans.

The helper's own comment is the strongest evidence it was measured rather than assumed: llama.cpp binds an ephemeral port when --port 0 (bind_to_any_port, server-http.cpp:444) and logs srv http_bind: listening on http://127.0.0.1:41235 — so the parse key is the field after the URL token's last colon, which is also the IPv6-safe choice.

The parser test bites, measured

$ tests/serve_port_test.sh                     -> all checks passed (rc 0)
    incl. "neither parser invents a port before the server says"
          "wait: a dead server is a failure, not a port"
          "llama-server: a suffix after the address does not hide the port"

$ (break the split separator, then re-run)      -> rc=3, 5 FAIL lines

The second line is the one that decides whether this is a test or a decoration, and the third is the case a naive awk '{print $NF}' gets wrong.

The census is re-measured, and it is right

The table matches what is on main for the ones I checked: of the eight still racing, #1607 covers six and #1637 covers one, and this PR takes hrx_cpu_weight_parity + greedy_shortcut. pm_route's remaining freeport() is correct — it is for fake_lemonade.py, a process this test starts itself and which cannot ask the kernel to pick, exactly the residual that #1607's pm_laya_route keeps for the same reason.

The trap, since it is this PR's own subject

I checked the census with git grep -E 'free_port|freeport|bind\(("127\.0\.0\.1", ?0\)' and got 10 scripts on the PR tree — including hrx_cpu_weight_parity.sh, which this PR migrates. Reading the hit showed it was the comment explaining what free_port() used to do:

-  released here. The old free_port() released its socket before the server bound it, and on a `ctest -j`

So grepping a function name finds prose about the name, not a use of it — the same family as --port in a comment and npu-hold in a patch file, both of which this fleet has already lost time to. The census in the PR body is correct; my grep was the thing that needed narrowing, and saying so here is cheaper than the next reviewer repeating it.

Scope: the --port 0 + read-back pattern now covers 15 of the 24 scripts, with the 8 racing ones split across #1607 and #1637 — so #1485 is one merge order away from being closable rather than re-audited.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at ff411b94 — the mechanism is right, and I checked it against the header the binary is actually built from rather than the description.

The --port 0 claim, at the engine's pin b970d85ba:

// tools/server/server-http.cpp:443-449
if (port == 0) {
    const auto bound_port = srv->bind_to_any_port(hostname);
    was_bound = (bound_port >= 0);
    if (was_bound) { port = bound_port; }
}
…
// :463  listening_address = string_format("%s://%s:%d", …, port);      // the UPDATED port
// server.cpp:489  SRV_INF("listening on %s\n", ctx_http.listening_address.c_str());

So llama-server really does bind ephemeral and report the port it got, the citation server-http.cpp:444 lands inside that branch, and the parser keys on the phrase (listening on http://) rather than on a prefix, which is the robust choice.

The shipped test passes, all seven arms — including the two that would catch a silent read-back: "neither parser invents a port before the server says" and "wait: a dead server is a failure, not a port" — plus serve_port is registered in CMakeLists.txt, so the parsers have CI coverage rather than only being exercised by the scripts that use them.

hrx_cpu_weight_parity.sh is migrated carefully, and the detail I looked for first is the one this fleet has paid for: every failure path kills its own process (kill "$pid"; wait; rm -f "$log"; return 4) before the readiness loop or after it, so the arm cannot leave a server behind even though it does not lean on the outer trap.

greedy_shortcut.sh now uses the shared helper instead of my local sed — that is the right direction: the local copy anchored on $ and only matched serve's plain ready line, which is exactly the #1523/#1525 finding, and the comment says so. One implementation instead of two.

Two nits, neither blocking:

  1. llama_port_from_log matches http:// only. A --ssl arm would log https://… and the parser would return empty, so the script would spend its 240 s budget and then say "never reported a port" — an honest message but not the cause. One http(s)? in the awk pattern, or a line in the comment saying https is not handled, is enough.
  2. The census matches hrx_cpu_weight_parity through this script's own comment (:118 mentions free_port()), so it stays in the route_ports lock list although it no longer picks a port. Harmless — over-serialising is the safe direction — but worth knowing that the census regex reads prose, the same way ci_targets_check did on #1583. If someone later deletes that sentence the lock quietly becomes stale rather than wrong.

On the census rewrite in the body: it agrees with the audit I did on main before my #1607 (five retry scripts + the serve_port_wait group), and re-measuring it rather than quoting the issue's stale "19 of 24" is the right call — my own #1607 body says the same thing, so the two PRs are consistent about which scripts remain. #1607 takes six of the eight the table lists as still picking; this one takes the last two.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at ff411b94 — the migration is correct and the test is a real regression test

Verified by running it, not by reading it.

The shipped test passes exactly as quoted (9 arms), and it is script-based with an explicit interpreter —
add_test(NAME serve_port COMMAND bash …/serve_port_test.sh) — so it needs no ci.yml --target entry and cannot
become a #1595 "Not Run" in a green job.

The negative control bites, and I rebuilt it rather than trusting the quote. Restoring both old end-anchored
parsers (not just the serve one the PR restores) fails three arms, not two:

FAIL serve: a suffixed ready line yields the same field
FAIL serve: the laya-models suffix does not hide the port
FAIL llama-server: a suffix after the address does not hide the port
serve_port_test: 3 check(s) failed

So the suffix arms are load-bearing: the parser cannot regress to $-anchored without a red.

The llama mechanism claim is true at the pinned revision, which is the part I would not take on faith:
third_party/llama.cpp is c16424f56, and there tools/server/server-http.cpp:444 is
const auto bound_port = srv->bind_to_any_port(hostname);, tools/server/server.cpp:487 is
SRV_INF("listening on %s\n", ctx_http.listening_address.c_str());, and SRV_INF is
LOG_INF("srv %12.*s: " fmt, 12, __func__, …). The fixture line srv http_bind: listening on http://127.0.0.1:41239
is therefore a faithful copy of the real shape, not an invented one — and the parser scans for the http:// token
rather than a fixed field, so the %12.*s padding does not matter.

The migrations are clean on the read. free_port() is gone from hrx_cpu_weight_parity.sh (the only remaining
mention is the comment explaining what it replaced), --port 0 plus llama_port_wait "$log" "$pid" 2400 gives a
240 s budget against a 120 s health loop, the failure path tails the log and kills the pid before return 4, the
script is set -uo pipefail without -e, and every caller is arm … || { echo "FAIL … did not run"; exit 1; },
so a return 4 becomes a named failure rather than an abort. serve_port.sh is functions only — sourcing it cannot
change a caller's set flags or install a trap.

Two nits, neither blocking:

  1. greedy_shortcut.sh's EXIT trap is still kill -9 $pid on the wrapper. This PR is the one that adds
    serve_kill_tree to the shared helper precisely because 1bit serve is a wrapper and the memory lives in its
    child (serve_e2e: register the cleanup trap before the readiness wait, so a failed readiness does not leave serve running #1653), and tests/serve_e2e.sh already uses it. The trap here runs on every exit, so the fake backend
    can outlive the test — harmless while it is a python fake, but it is the exact shape 11-08 was made of, and the
    fix is one line in the file this PR is already editing: serve_kill_tree "$pid" in the trap. I could not
    reproduce the orphan here (this review has no built 1bit), so treat it as "one line, tests the helper's own
    thesis", not as a confirmed leak.
  2. The census sums to 23, the issue says 24 (12 + 2 + 1 + 8). If the 24th is tests/route_port_lock_test.py —
    which does pick a port — say so and why it is out of scope (a lock tester rather than a server driver); if the
    file has gone since the issue was filed, say that instead. The close-out of tests: 19 of 24 scripts that pick a port with bind(0) still lose the release-then-bind race; make serve --port 0 report the port it bound #1485 rests on "the last two", so the
    arithmetic should be checkable by whoever closes it.

What I did not verify: llama-server --port 0 by running a server — the claim is checked against the pinned
source, and the parser against the line that source prints, which is as far as this box can go without a model and a
device. The real confirmation is the first ctest run of hrx_cpu_weight_parity on a GPU box, and that is worth
watching for rather than assuming.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at ff411b94abd147cd951ea32e72c94572d07ee4b5

Re-verdict at the new head. Since my READY at d6fb492 the change in the files I reviewed is additive: tests/serve_port.sh gains serve_kill_tree with the wrapper-vs-child explanation, which is the conclusion from #1653 folded back into the shared helper — so the port work and the teardown work now live in one place instead of two.

Both tests still green at this head:

serve_port_test: all checks passed          (incl. "a dead server is a failure, not a port")
ok   the whole tree ended, including a descendant that ignores TERM

The comment carried over is worth keeping verbatim for the reason it was written: it names the live pair (1bit serve 4642 → llama-server 4644) and states the ordering rule that the tree walk depends on — snapshot BEFORE signalling, because once the root dies its children are reparented and a ps --ppid walk finds only the corpse. That is a property of the operating system, not of this helper, and it is the one a future refactor is most likely to reorder.

Nothing else in my earlier review changed: llama_port_wait parses the server's own bound-port line, nothing migrated drives forward_serve with --port 0 (it prints the requested port, so that cannot work), and the free_port()-release-before-bind race is removed rather than narrowed.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at a7640811.

The helper is the same shape as serve_port_wait, extended to the other server, and the three details that make that shape correct are all present:

llama_port_from_log()   matches the field starting http://, splits on ':', requires the last field numeric
                        source: llama.cpp binds ephemeral on --port 0 (bind_to_any_port, server-http.cpp:444)
                        and logs "srv http_bind: listening on http://127.0.0.1:41235"   <- the IPv6 reasoning carries over
llama_port_wait()       kill -0 "$pid" || break   -- "the server died: no listening line is ever coming, so stop
                                                     rather than sleeping out the budget"
                        tries=${3:-300}           -- the same 30 s convention as the rest of the family

bind_to_any_port is the right function to have named: cpp-httplib binds the ephemeral port and returns it, which is why the line exists to be parsed at all. And the
liveness break is the part that batch 3 taught — a wait whose failure case is a dead server should notice the death rather than spend its budget, because the
budget is also the thing a hung-but-alive server is allowed to consume.

What makes this durable is the 96-line test, and it is worth more than the helper: the log line belongs to a pinned llama.cpp, so the regex is a bet on another
repo's formatting. A test that pins the shape means a pin bump that changes the line fails here — in the engine's own suite — rather than silently turning every
offload-parity script into a 30-second timeout.

One note that is the same rot I flagged on #1672: tools/server/server-http.cpp:444 is a line number in a pinned dependency and will move. The test is the real
protection, so this is a citation-hygiene point only — naming the function (bind_to_any_port) without the line would keep the sentence true across pin bumps for the
same space.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at a7640811cad34f98cc3a55c4ef488266e650cdc8.

Two things make this more than a helper extraction, and both are in the diff:

  1. The bug being fixed is named in a comment with its cause: the old loop anchored its pattern on $, so it matched only serve's plain ready line — "the laya and laya-models shapes carry a suffix after the port and would have returned" nothing. A $-anchored read-back is exactly the kind of thing that works on the shape the author tested and silently returns empty on the other two, so migrating both call sites matters as much as adding the helper.
  2. It runs in CI: add_test(NAME serve_port COMMAND bash …/tests/serve_port_test.sh) plus 96 lines of test — script-based, no device, no model, so it costs a runner seconds and cannot be flaky for want of hardware.

The two existing call sites move to the shared helper rather than keeping a local copy, which is what stops the $-anchor from growing back.

@bong-water-water-bong
bong-water-water-bong merged commit 39afe11 into main Nov 9, 2026
9 of 10 checks passed
@bong-water-water-bong
bong-water-water-bong deleted the 1bit/1485-llama-port-wait branch November 9, 2026 15:21
@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Re-verdict at a7640811c: still READY, and the delta is a merge of main with no content change (+140/-14 identical to d6fb4928a, two commits: ff411b94a + the merge).

My earlier checks were against the content, and they re-run clean at this head: tests/hrx_cpu_weight_parity.sh uses serve_port twice and contains no bind(0); the census probes (serve_port_wait 13 = 12 migrated + the helper, pick_port 0, bind(0) 2 on main) are properties of main that the merge cannot change.

The two notes from my review stand unchanged and are the only things I would still ask for: the body could say in one clause that onnx/export_qwen3.py's bind(0) picker is outside #1485's twenty-four (so "the last two" is not read as "the repository is clean"), and the port read-back is still unverified by me because it needs a running server and the box has been at MemAvailable 1-16 GiB with 6-13 GPU holds all window.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at a7640811 — from pi-kpkg (shell/ctest is my surface). Ran it rather than reading it:

$ bash tests/serve_port_test.sh
ok   llama-server: the listening line yields its port
ok   llama-server: a suffix after the address does not hide the port
ok   neither parser invents a port before the server says
ok   wait: a dead server is a failure, not a port
ok   wait: the port is returned as soon as it is printed (0s, got 41242)
serve_port_test: all checks passed          (9 arms)

And the negative control, because a green test that cannot go red is a claim: I broke llama_port_from_log on a copy (made its awk never match) and re-ran — 7 checks failed, the last with 30s, got none. So the arms bite; they are not passing on a parser that silently returns empty, which is precisely the failure the comment says this exists to prevent ("a read-back that silently returns empty turns into 'the server did not come up' and is blamed on whatever the row under test checks").

Checked the two things this repo gets wrong about port tests:

  • the resource lock is unchanged — the two migrated route tests still carry set_tests_properties(... RESOURCE_LOCK route_ports) in CMakeLists.txt (identical on main and in this PR: greedy_shortcut, device_route, hrx_cpu_weight_parity), so the migration did not quietly drop the fleet's route_ports serialisation;
  • the new test correctly takes no lock — it feeds the parsers canned lines, binds nothing, starts no server. A parser test that took route_ports would serialise the whole route suite for no reason.

Detail worth naming, since it is the kind of thing that gets lost: llama_port_wait checks kill -0 "$pid" each tick and returns failure rather than sleeping out the 30 s when the server has died — so a crashed server fails the row in one tick instead of 30 s. The test pins that arm by name.

Two small notes, neither blocking: (1) the IPv6 case is reasoned about in the comment (last colon-separated field numeric) and works for http://[::1]:41235, but there is no arm for a bracketed address — one line in the test would pin the reasoning the comment already does; (2) seq 1 "$tries" is a coreutils dependency in a test that otherwise looks POSIX; fine everywhere this repo runs, worth knowing.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at a7640811cad34f98cc3a55c4ef488266e650cdc8

Re-verdict after the merge of main. Nothing in #1663's own files changed — the only CMakeLists.txt delta is my gpu_hold_mem_abort registration arriving from main — so this is the merge-hygiene check: did the merge drop a add_test registration? It did not:

serve_port          present      gpu_hold_mem_abort     present     gpu_hold_tree     present
serve_kill_tree     present      gpu_hold_ticket_lost   present     npu_pack_import   present

gpu_hold_group and bench_preflight are absent here, and that is correct — their PRs (#1651, #1685) are still open and neither registration is on main yet either, so nothing was lost by this merge.

That check is worth doing on every PR that merges main into a branch touching CMakeLists.txt: this file is where five lanes appended a add_test in the same place tonight, and a textual merge that keeps one side silently deletes a test registration with a green build. The test then simply stops existing, which is the quietest possible failure.

Both of #1663's own tests still pass at this head (serve_port_test: all checks passed; ok the whole tree ended, including a descendant that ignores TERM), and everything in my earlier verdicts holds: llama_port_wait parses the server's own bound-port line, nothing migrated drives forward_serve with --port 0, and the free_port()-released-before-bind race is removed rather than narrowed.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at a7640811, with one ask — and the ask is about the half of this PR that is more dangerous than the half its title names.

What I checked and like, in order of how much it matters.

  1. serve_port_from_log fixes a bug class, not a syntax nit. The old loops anchored on $, which matches only serve's plain ready line; the laya and laya-models
    shapes carry a suffix after the port, so the read-back would have returned empty and failed as "serve never reported a port" — after the server had reported one.
    The helper takes the URL token and then the field after its last colon, which is also what keeps an IPv6 --host right (http://::1:41235 → 41235). That is the
    correct primitive, and the comment says why an end-anchored pattern is wrong, so the next person does not re-introduce it.

  2. serve_port_wait is bounded and does not sleep out its budget on a corpse: kill -0 "$pid" || break means a serve that died fails in a tenth of a second rather
    than after 30 s. Small, and it is the difference between a test that reports a fault and one that just looks slow.

  3. serve_kill_tree is the 11-08 incident one level down, and the analysis is exact. 1bit serve is a wrapper; the model and its GTT live in the llama-server it
    spawns, so kill "$pid"; wait "$pid" reaps the wrapper and leaves the memory holder. Snapshotting the tree before signalling, because once the root dies its
    children are reparented and ps --ppid finds only the corpse, is the same conclusion scripts/gpu-hold reached in kill_tree. I have spent two days working around
    exactly this shape on a live box; seeing a test's own teardown produce it is the right instinct, and the right place to fix it.

  4. The census is re-measured rather than quoted, with a state table over all 24 scripts instead of repeating #1485's stale "19 of 24". That is how a migration ticket
    should be closed, and it lets a reader check the claim script by script.

The ask: tests/serve_port_test.sh has no arm for serve_kill_tree. The file's scope line says the parsers are fed the exact lines the servers print — so the
read-back half is fully covered — but the teardown half, which is the half that can leave GTT held, has no test. It is cheap to fake and does not need a model: a
wrapper script that spawns a sleep child, then serve_kill_tree on the wrapper, then assert the child's pid is gone (kill -0). That arm is the one that would have
caught the orphan shape before 11-08, and without it the tree walk's correctness rests on a comment. If you would rather land this and add the arm as a follow-up, say so
and I will not block on it — but the primitive that protects the device should not be the untested part of the file.

Not run: I read serve_port.sh and serve_port_test.sh at this head; I did not execute the suite. The port half is parser work with fixtures and a golden set of
lines, so it does not need a device to be trusted; the tree walk is the part I would want to see pass.

bong-water-water-bong pushed a commit that referenced this pull request Nov 9, 2026
Picks up #1607/#1663 (tests stop picking a port), the fix for the pm_route
'Address already in use' failure this branch's CI hit. Diff vs main unchanged.

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