|
15 | 15 | * completion tokens (from ``usage.completion_tokens``) |
16 | 16 | * tokens/s for that turn |
17 | 17 | * server-side KV pool state, scraped from ``/metrics``:: |
18 | | - inference_engine_scheduler_active_sessions |
19 | | - inference_engine_scheduler_pool_in_use |
20 | | - inference_engine_scheduler_pool_size |
21 | | - inference_engine_scheduler_kv_live_bytes |
| 18 | + scheduler_active_sessions |
| 19 | + scheduler_pool_in_use |
| 20 | + scheduler_pool_total |
| 21 | + scheduler_pending |
| 22 | + scheduler_kv_live_bytes |
22 | 23 | * client-side RSS (best-effort; uses /proc/self/status on Linux, |
23 | 24 | psutil if installed, otherwise ``None``) |
24 | 25 |
|
|
35 | 36 | session can run for hours without OOM. A single agent driven for |
36 | 37 | 4 hours is therefore the cleanest evidence. |
37 | 38 |
|
| 39 | +A note on what this bench measures and what it doesn't (per the |
| 40 | +analysis of the 2026-05-30 Mac M4 run, ``bench_long_session_mac_ |
| 41 | +1780130542.aborted.json``): |
| 42 | +
|
| 43 | + * **KV memory** stays bounded across hours (the §2.3 claim). The |
| 44 | + ``scheduler_kv_live_bytes`` gauge is what proves it. |
| 45 | + * **Per-turn latency** does NOT stay bounded. The OpenAI |
| 46 | + chat-completions protocol is stateless: every turn the client |
| 47 | + sends the full history, the server tokenizes it from scratch |
| 48 | + and the verifier prefills the entire prompt, so prefill cost |
| 49 | + grows linearly with history length. Sink+window only bounds |
| 50 | + *generation-phase* memory, not prefill cost. A 30-min run on |
| 51 | + Mac M4 showed p50 turn latency growing from ~15 s to ~55 s as |
| 52 | + history grew from ~50 to ~3700 tokens. This is a **protocol- |
| 53 | + level limitation**, not a memory-stability failure. Cross- |
| 54 | + request KV reuse (a v0.4 feature) is the eventual fix; until |
| 55 | + then, agent applications should manage prompt length via |
| 56 | + summarization or sliding windows. |
| 57 | +
|
| 58 | +The bench reports both metrics independently — KV bounded check is |
| 59 | +a hard claim, latency drift is a measurement, not a gate. |
| 60 | +
|
38 | 61 | Usage |
39 | 62 | ----- |
40 | 63 |
|
@@ -131,11 +154,16 @@ def _client_rss_bytes() -> Optional[int]: |
131 | 154 | return None |
132 | 155 |
|
133 | 156 |
|
| 157 | +# Names match the ones registered by inference_engine.server.metrics |
| 158 | +# (Prometheus-client does NOT add a service prefix). Changing any of |
| 159 | +# these breaks the bench's KV-bounded check; if you rename a metric on |
| 160 | +# the server, update both ends in the same commit. |
134 | 161 | _METRIC_NAMES = ( |
135 | | - "inference_engine_scheduler_active_sessions", |
136 | | - "inference_engine_scheduler_pool_in_use", |
137 | | - "inference_engine_scheduler_pool_size", |
138 | | - "inference_engine_scheduler_kv_live_bytes", |
| 162 | + "scheduler_active_sessions", |
| 163 | + "scheduler_pool_in_use", |
| 164 | + "scheduler_pool_total", |
| 165 | + "scheduler_pending", |
| 166 | + "scheduler_kv_live_bytes", |
139 | 167 | ) |
140 | 168 |
|
141 | 169 |
|
@@ -345,7 +373,7 @@ def _print_progress( |
345 | 373 | ) -> None: |
346 | 374 | last = turns[-1] if turns else None |
347 | 375 | last_lat = f"{last['latency_s']:.2f}s" if last else "-" |
348 | | - kv = (metrics or {}).get("inference_engine_scheduler_kv_live_bytes") |
| 376 | + kv = (metrics or {}).get("scheduler_kv_live_bytes") |
349 | 377 | kv_str = f"{kv / (1024 * 1024):.1f} MiB" if kv is not None else "?" |
350 | 378 | print( |
351 | 379 | f"[bench] t={elapsed/60:6.1f} min | turns={turn_idx:5d} " |
@@ -384,7 +412,7 @@ def _bucketize( |
384 | 412 | bucket_turns = buckets[idx] |
385 | 413 | latencies = [b["latency_s"] for b in bucket_turns] |
386 | 414 | kv_vals = [ |
387 | | - (b["metrics"] or {}).get("inference_engine_scheduler_kv_live_bytes") |
| 415 | + (b["metrics"] or {}).get("scheduler_kv_live_bytes") |
388 | 416 | for b in bucket_turns |
389 | 417 | ] |
390 | 418 | kv_vals_clean = [v for v in kv_vals if v is not None] |
@@ -416,12 +444,12 @@ def _aggregate( |
416 | 444 | } |
417 | 445 | latencies = [t["latency_s"] for t in turns] |
418 | 446 | kv_series = [ |
419 | | - (t["metrics"] or {}).get("inference_engine_scheduler_kv_live_bytes") |
| 447 | + (t["metrics"] or {}).get("scheduler_kv_live_bytes") |
420 | 448 | for t in turns |
421 | 449 | ] |
422 | 450 | kv_clean = [v for v in kv_series if v is not None] |
423 | 451 | pool_in_use_series = [ |
424 | | - (t["metrics"] or {}).get("inference_engine_scheduler_pool_in_use") |
| 452 | + (t["metrics"] or {}).get("scheduler_pool_in_use") |
425 | 453 | for t in turns |
426 | 454 | ] |
427 | 455 | pool_in_use_clean = [v for v in pool_in_use_series if v is not None] |
|
0 commit comments