Skip to content

Commit 66745f9

Browse files
test: Case-1 stress ceilings + Case-2 measured cross-host WAN-penalty curve
Case 1 stress (Mac M4): FD not the limit (100k); memory scales with capacity x window (cap 2048 -> 11.5GB, node bound 61GB > 24GB RAM); single-tenant serialization caps heavy-context concurrency at ~8 (vs 256 light). Case 2 (H200 real models): injected per-block proposer<->verifier RTT -> measured WAN-penalty curve. Co-located 2.20x AR; break-even ~100ms/block; 150ms -> 0.77x (net loss). LAN (<=15ms) keeps 1.8-2.2x. Confirms WAN data plane infeasible. Updates ADR 0014 + README with measured curves + evidence JSONs. Co-authored-by: FluffyAIcode <FluffyAIcode@users.noreply.github.com>
1 parent cb29fbd commit 66745f9

4 files changed

Lines changed: 435 additions & 36 deletions

File tree

README.md

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -259,11 +259,18 @@ resident **window**, not the number of agents.
259259

260260
**Cross-host proposer/verifier.** A GPU proposer ⇄ Mac verifier *token-level
261261
draft* data plane is **design-only** (no `CapabilityService` / `ProposeBlock` /
262-
gossip) **and** ruled out by the WAN latency budget (per-block RTT ≫ block
263-
compute). The realizable split is **WAN = control + tool plane** (the Mac
264-
bridge) and **LAN = co-located data plane** (spec-decode is a same-host win:
265-
GPU 1.79× AR, Mac ≈ parity). See ADR 0014 for the full plan, evidence, and the
266-
served-MLX-gemma gap found during testing.
262+
gossip) **and** ruled out by the WAN latency budget — now **measured** on real
263+
H200 compute by injecting one proposer↔verifier round-trip per block:
264+
265+
| per-block RTT | 0 (co-located) | 15 ms (LAN) | 30 ms | 60 ms | 100 ms | 150 ms |
266+
| --- | --- | --- | --- | --- | --- | --- |
267+
| vs AR | **2.20×** | 1.81× | 1.50× | 1.22× | **0.98×** (break-even) | 0.77× (loss) |
268+
269+
**Break-even ≈100 ms/block**: a cloud↔desk WAN (30–150 ms) straddles/exceeds it,
270+
while a LAN (≤15 ms) keeps the 1.8–2.2× win. So the realizable split is **WAN =
271+
control + tool plane** (the Mac bridge) and **LAN = co-located data plane**. See
272+
[ADR 0014](docs/adr/0014-agent-connection-capacity-and-cross-host-topology-tests.md)
273+
for the full plan, evidence, and the served-MLX-gemma gap found during testing.
267274

268275
## Kakeya Inference Engine for Mac — MLX speculative-decode port (K3 beta baseline)
269276

docs/adr/0014-agent-connection-capacity-and-cross-host-topology-tests.md

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ runnable vs design-only and shaped the test plan below.
3737
`[self-hosted, macOS, ARM64, kakeya-mac-m4]`, reached via the **Mac bridge**
3838
git-bus plane (no inbound path; allowlisted presets only).
3939
- **Cloud agent**: Linux x86 VM (no Metal). Orchestrates via the bridge.
40-
- **GPU**: H200 (vast.ai) — used for the co-located CUDA reference (PR #119);
41-
**unavailable at test time** (instance recycled), so Case-2 GPU numbers are
42-
cited from the prior committed evidence.
40+
- **GPU**: H200 NVL (vast.ai) — runs the CUDA proposer + verifier for the
41+
co-located reference (fused **2.06–2.20× AR**, recall 1.0) and the §4.3
42+
cross-host WAN-penalty sweep.
4343
- **Engine**: gRPC `RuntimeService` (`scripts/start_grpc_runtime_server.py`),
4444
Python SDK clients (`sdks/python/kakeya`).
4545

@@ -82,6 +82,29 @@ behavior; the served **MLX gemma** path is a separate v0.4 gap (§6).
8282
- **Server RSS is flat** (3825 → 3850 MB across 1 → 256 agents): adding agents
8383
costs ~0 memory beyond the bounded slab; model weights dominate.
8484

85+
### 3.2b Stress beyond 256 — the real ceilings (preset `agent-capacity-stress`)
86+
87+
Pushing further with the FD limit raised (`RLIMIT_NOFILE` soft 100k, hard
88+
unlimited on the Mac) and a **per-agent context prefill** (window 256,
89+
`--context-len 256`, capacity 2048):
90+
91+
| agents | created | create p95 | per-session KV | server RSS |
92+
| --- | --- | --- | --- | --- |
93+
| 1 | 1/1 | 3.07 s | 29.8 MB | 11 477 MB |
94+
| 8 | 8/8 | 25.2 s | 29.8 MB | 11 343 MB |
95+
| 16 | 15/16 (1 `RpcCancelled`) | 44.6 s | 29.8 MB | 10 781 MB |
96+
97+
- **FD is not the ceiling** (raised to 100k; Mac hard limit is unlimited).
98+
- **Memory** scales with `capacity × window`: capacity 2048 @ window 256 →
99+
**~11.5 GB RSS**, and the theoretical node bound is **~61 GB > 24 GB RAM**
100+
so capacity must be **sized to RAM** (it is the memory knob, not agent count).
101+
- The binding constraint with real per-agent context is **single-tenant
102+
serialization**: create latency is purely linear (3 → 12 → 25 → 45 s as
103+
N = 1 → 4 → 8 → 16) because every session's prefill serializes through the one
104+
shared verifier, so clean concurrency tops out at **~8 heavy-context agents**
105+
before RPCs time out — versus **256 light-session agents** (§3.2). Per-session
106+
KV stays bounded (29.8 MB @ window 256) throughout.
107+
85108
### 3.3 Honest caveat — v0.3 is single-tenant
86109

87110
Create/generate latency scales **linearly** with `N` (256 agents → gen p95
@@ -119,22 +142,47 @@ a LAN** for the data plane.
119142
| --- | --- | --- | --- |
120143
| Discovery / capability advertise | yes (seconds-scale) | bridge proxy only | bridge dispatch ~10 s + queue; one Mac, serialized (`concurrency: mac-bridge`) |
121144
| Job/tool dispatch (eval/bench) | yes | implemented (bridge) | this ADR's Case-1 run is itself an instance |
122-
| **Token-level draft (data plane)** | **no — must be LAN** | not implemented | — (ruled out by §4.1) |
123-
| Co-located spec-decode (the feasible data plane) | n/a (same host) | implemented | **GPU H200 1.79× AR** (PR #119); **Mac 0.93× AR** (PR #118) |
145+
| **Token-level draft (data plane)** | **no — must be LAN** | not implemented | **measured penalty curve §4.3** (break-even ~100 ms/block) |
146+
| Co-located spec-decode (the feasible data plane) | n/a (same host) | implemented | **GPU H200 2.06–2.20× AR**; **Mac 0.93× AR** (PR #118) |
124147

125148
So the answers to Case 2's three metrics, under the **realizable** topology:
126149

127-
- **Token throughput**: spec-decode is a *co-located* win — **1.79× AR on the
128-
GPU** (28.94 vs 16.13 tok/s, recall 1.0) and **≈AR parity (0.93×) on the
129-
Mac**. A WAN GPU-proposer→Mac-verifier draft loop would be **slower than
130-
running either side alone** (§4.1), so it is not a throughput strategy.
150+
- **Token throughput**: spec-decode is a *co-located* win — **2.06–2.20× AR on
151+
the GPU** (recall 1.0) and **≈AR parity (0.93×) on the Mac**. The measured
152+
WAN-penalty curve (§4.3) shows the cross-host draft loop falls to **break-even
153+
at ~100 ms/block and a net loss at 150 ms**, i.e. slower than running AR
154+
locally — so it is not a throughput strategy.
131155
- **Max agent connections**: governed by the *serving* node (Case 1): **256+
132156
concurrent agents** on the Mac via `RuntimeService`.
133157
- **Mac KV upper bound**: bounded — **capacity × per-session `sink+window`**
134158
(≈2.0 GB at capacity 256 for Qwen3-0.6B; for the gemma S5 production config
135159
the per-agent resident KV is ~133 MB at 5.8k ctx, dominated by the 5 exact
136160
full-attention layers — see the README beta scorecard).
137161

162+
### 4.3 Measured WAN-penalty curve (H200, real models, `--rtt-sweep`)
163+
164+
Rather than rest on the latency *estimate*, we measured it: the fused engine was
165+
re-timed with one injected proposer↔verifier round-trip **per block** on the real
166+
Gemma-4-26B verifier + DFlash drafter (H200 NVL), sweeping per-block RTT across
167+
the cloud↔desk range (`scripts/research/k3_specdecode_gpu_bench.py --rtt-sweep`,
168+
`results/research/k3_crosshost_rtt_gpu.json`):
169+
170+
| per-block RTT | decode tok/s | vs AR | regime |
171+
| --- | --- | --- | --- |
172+
| 0 ms (co-located) | 52.4 | **2.20×** | the win |
173+
| 5 ms (LAN) | 47.0 | 1.97× | LAN keeps it |
174+
| 15 ms | 43.3 | 1.81× | LAN keeps it |
175+
| 30 ms | 35.9 | 1.50× | WAN edge |
176+
| 60 ms | 29.2 | 1.22× | shrinking |
177+
| 100 ms | 23.5 | **0.98×** | **break-even** |
178+
| 150 ms | 18.4 | 0.77× | net **loss** |
179+
180+
AR baseline = 23.8 tok/s. **Break-even is ~100 ms/block**: beyond it, cross-host
181+
spec-decode is *slower than running AR locally*. A cloud↔desk WAN (30–150 ms RTT)
182+
straddles or exceeds break-even, while a LAN/Thunderbolt link (≤15 ms) preserves
183+
the 1.8–2.2× win. This is the architecture's prediction (design doc §4.2),
184+
**now quantified on real compute** — and it is why the data plane must be LAN.
185+
138186
## 5. Decision
139187

140188
1. **Case 1 is validated**: the session-bound gRPC runtime admits and serves

results/research/k3_agent_capacity_stress_mac.json

Lines changed: 58 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@
2424
},
2525
"levels": [
2626
1,
27+
4,
28+
8,
29+
16,
30+
32,
31+
48,
2732
64,
28-
256,
29-
512,
30-
1024,
31-
2048
33+
96
3234
],
3335
"single_tenant_note": "v0.3 single-tenant: shared verifier, RPCs serialized on one asyncio loop; this measures connection/session admission scaling, not parallel inference."
3436
},
@@ -39,43 +41,77 @@
3941
"generate_ok": 1,
4042
"errors": {},
4143
"create_latency_s": {
42-
"p50": 3.0911,
43-
"p95": 3.0911
44+
"p50": 3.0748,
45+
"p95": 3.0748
4446
},
4547
"generate_latency_s": {
46-
"p50": 0.0297,
47-
"p95": 0.0297
48+
"p50": 0.0296,
49+
"p95": 0.0296
4850
},
4951
"per_session_kv_bytes": 29474816,
50-
"server_rss_mb": 11227.9,
51-
"wall_s": 3.12
52+
"server_rss_mb": 11476.5,
53+
"wall_s": 3.11
5254
},
5355
{
54-
"agents": 64,
55-
"created_ok": 14,
56-
"generate_ok": 14,
56+
"agents": 4,
57+
"created_ok": 4,
58+
"generate_ok": 4,
59+
"errors": {},
60+
"create_latency_s": {
61+
"p50": 9.255,
62+
"p95": 12.3516
63+
},
64+
"generate_latency_s": {
65+
"p50": 0.0867,
66+
"p95": 0.1155
67+
},
68+
"per_session_kv_bytes": 29818880,
69+
"server_rss_mb": 11474.8,
70+
"wall_s": 12.47
71+
},
72+
{
73+
"agents": 8,
74+
"created_ok": 8,
75+
"generate_ok": 8,
76+
"errors": {},
77+
"create_latency_s": {
78+
"p50": 15.7695,
79+
"p95": 25.2241
80+
},
81+
"generate_latency_s": {
82+
"p50": 0.1739,
83+
"p95": 0.2352
84+
},
85+
"per_session_kv_bytes": 29818880,
86+
"server_rss_mb": 11342.6,
87+
"wall_s": 25.46
88+
},
89+
{
90+
"agents": 16,
91+
"created_ok": 15,
92+
"generate_ok": 15,
5793
"errors": {
58-
"RpcCancelledError": 50
94+
"RpcCancelledError": 1
5995
},
6096
"create_latency_s": {
61-
"p50": 21.683,
62-
"p95": 40.7049
97+
"p50": 25.6965,
98+
"p95": 44.5888
6399
},
64100
"generate_latency_s": {
65-
"p50": 0.2375,
66-
"p95": 0.3998
101+
"p50": 0.2797,
102+
"p95": 0.4459
67103
},
68104
"per_session_kv_bytes": 29818880,
69-
"server_rss_mb": 10849.8,
70-
"wall_s": 67.58
105+
"server_rss_mb": 10780.7,
106+
"wall_s": 63.64
71107
}
72108
],
73109
"summary": {
74-
"max_concurrent_agents_clean": 1,
110+
"max_concurrent_agents_clean": 8,
75111
"per_session_kv_bytes": 29818880,
76112
"per_session_kv_mb": 29.8189,
77113
"node_kv_upper_bound_mb": 61069.07,
78114
"node_kv_upper_bound_note": "capacity * per-session bounded KV \u2014 the whole-node resident-KV ceiling, independent of context length or agent churn.",
79-
"server_peak_rss_mb": 11227.9
115+
"server_peak_rss_mb": 11476.5
80116
}
81117
}

0 commit comments

Comments
 (0)