frank-medium: route by workload — spread batches, pin agent loops

The two replicas behind :7770 were on plain round-robin, which throws away
prefix-cache hits: an agent loop re-sends its system prompt and file context
every turn, and alternating replicas means every other turn pays a cold
prefill. Measured with 90k-token contexts, warm-turn TTFT was 2676 ms.

Pinning everything to one replica is not the answer either — a cold batch
(the shape a benchmark or a burst of parallel subagents produces) then loses a
card's worth of prefill: 16 requests took 30.1 s pinned against 18.6 s spread.

So the policy routes by request shape. A continuation (more than one message —
the agent-loop signature) is pinned to the replica that served its prefix;
anything else spreads across the cards by counter. Measured: 19.97 s for the
burst and 1095 ms warm-turn TTFT — the best of both, without a regression on
either workload.

The policy lives in proxy/routing.py, separate from the HTTP plumbing in
proxy/affinity-proxy.py, so it can be tested without aiohttp, a network or a
GPU. tests/frank_medium_proxy.bats covers it, and it earned its keep: the
first version hashed fresh prefixes to pick a card, which sends every member of
a same-prefix burst to one card (29.8 s, measured), and a stale index in the
pin path returned 500 on every continuation. Both are now assertions.

ROUTING=roundrobin and ROUTING=affinity stay available to re-measure the
choice on this host; the default is adaptive.
This commit is contained in:
Frank committed 2026-09-16 04:45:21 -07:00
1 parent 7ce6e96bc5
commit 83625e84a7
8 files changed
+474 -124

No files matched your search

+10 -13
View File
@@ -21,7 +21,7 @@ UNIT_PROXY="$REPO_ROOT/dankserver/frank-medium/quadlet/frank-medium-proxy.contai
UNIT_TP2="$REPO_ROOT/dankserver/frank-medium/quadlet/frank-medium.container"
ENTRYPOINT="$REPO_ROOT/dankserver/frank-medium/radiance-entrypoint.sh"
HEALTHCHECK="$REPO_ROOT/dankserver/frank-medium/frank-medium-healthcheck.py"
PROXY="$REPO_ROOT/dankserver/frank-medium/proxy/roundrobin-proxy.py"
PROXY="$REPO_ROOT/dankserver/frank-medium/proxy/affinity-proxy.py"
REMOTE_DEPLOY="$REPO_ROOT/scripts/deploy-remote.sh"
@test "the frank-medium units live under dankserver/frank-medium/quadlet/" {
@@ -109,13 +109,6 @@ REMOTE_DEPLOY="$REPO_ROOT/scripts/deploy-remote.sh"
grep -q '^Environment=PORT=7772$' "$UNIT_B"
}
@test "the proxy really is a round-robin over the replicas" {
[ -f "$PROXY" ]
python3 -m py_compile "$PROXY"
grep -q 'itertools.cycle' "$PROXY"
grep -q 'UPSTREAMS' "$PROXY"
}
@test "frank-medium entrypoint is a syntax-clean fork of the upstream launcher" {
[ -f "$ENTRYPOINT" ]
bash -n "$ENTRYPOINT"
@@ -134,12 +127,16 @@ REMOTE_DEPLOY="$REPO_ROOT/scripts/deploy-remote.sh"
done
}
@test "frank-medium units carry no secret material" {
for unit in "$UNIT_A" "$UNIT_B" "$UNIT_PROXY"; do
! grep -Eq '^(Environment|EnvironmentFile)=.*(TOKEN|PASSWORD|SECRET|KEY)' "$unit"
done
@test "the proxy unit mounts the routing policy and the server" {
# The policy lives in its own file so it can be tested without aiohttp;
# the server imports it by name from its own directory, so both must be
# mounted or the container dies on ModuleNotFoundError at start.
grep -q 'proxy/routing.py:/usr/local/bin/routing.py:ro' "$UNIT_PROXY"
grep -q 'proxy/affinity-proxy.py:/usr/local/bin/affinity-proxy.py:ro' "$UNIT_PROXY"
# Routing behaviour itself is covered by tests/frank_medium_proxy.bats,
# which drives the policy directly.
[ -f "$REPO_ROOT/tests/frank_medium_proxy.bats" ]
}
@test "the deploy handler installs the trio and retires the TP=2 unit" {
grep -q 'install_quadlet frank-medium-a dankserver/frank-medium/quadlet frank-medium-a' "$REMOTE_DEPLOY"
grep -q 'install_quadlet frank-medium-b dankserver/frank-medium/quadlet frank-medium-b' "$REMOTE_DEPLOY"