Repository navigation
Conversation
max_slowdown_ppm bounds the cycle's AVERAGE; Joel's bar is per turn (Cormac on #36). A turn that lands at a window's start decodes at the in-window rate for the whole window, which the average never shows. Serving now hands every finished turn to the trainer (its generation span and decode steps): a turn that overlapped no training window is her per-turn baseline, one that overlapped a window is a slowdown sample against it. A turn's rate is per slot, so it is measured against other turns, never against the lane's total rate. Turns under 16 decode steps time the scheduler, not her, and are skipped. A run's end closes its last window, so later turns never read as overlapped by training that no longer runs. /train status: turns_clean, turns_overlapped, turn_tps_clean, turn_slowdown_ppm_p50/p95/max. Measured on Qwen3.5-0.8B Q8_0, Metal (M5, beside a live 27B lane), 2 slots, T = 10%: - run A: client throughput fell 20% overall (the average bound about holds), but overlapped turns were slowed p50 28% / p95 86% (engine), p95 80% (client per-request rates). - run B: the engine's no-window rate read 39 tok/s against ~120 true, so the bound barely yielded; overlapped turns p50 54% / p95 89% (engine), 81% / 95% (client). Not reproduced in run A; this GPU's baseline moved 45-123 tok/s between runs with the 27B lane beside it. The receipt tracks the client within about 10 points at p95 in both, and shows what the average hides: a 6-12 s window nearly stalls the turns it overlaps. The cap on window duration (the walk's chunk size) is what makes the per-turn bar hold; this is its receipt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
|
APPROVED at 43dab86, with one question to answer before merge. It's the receipt Joel's bar needs: each finished turn is classed by its overlap with this run's windows; clean turns set her per-slot baseline (never the lane's total rate), overlapped ones give a slowdown sample, and the status reports p50/p95/max. The data already makes the case for the window cap (overlapped p95 86% against a 10% average). (1) QUESTION: serving passes slot.stats.t_prompt_last as gen_start_us. That's only right if t_prompt_last is a ggml_time_us TIMESTAMP (when generation began), not the prompt's duration in ms like upstream's t_prompt_processing. If it's a duration, every overlap test compares a timestamp with a duration and the samples are noise. Please confirm, or pass the decode-start timestamp explicitly. (2) Bounds, not blocking: window_spans and turn_slowdown_ppm grow without limit over a run, and on_turn scans every span per turn. Drop spans that ended before (now - the longest turn seen), and keep the samples in a fixed-size histogram or reservoir; every in-memory ring is bounded. (3) A partly overlapped turn gives a diluted sample (20% overlap reads as a small slowdown). That's honest, but say it in the status doc so a p95 isn't read as the in-window slowdown. |
… is (Cormac on #39) - window spans that ended before (now - the longest turn seen) can overlap no turn still to finish and are dropped, so a turn's overlap scan stays the size of the windows it could meet - slowdown samples live in a fixed histogram of 1% buckets (p50/p95 to the bucket, max exact) instead of a vector that grew with the run - a sample is the whole turn's slowdown: a turn a window partly overlapped reads diluted, and the status comment says so, so a p95 is never read as the slowdown inside a window - the call site states that t_prompt_last and t_gen_last are ggml_time_us timestamps (server-common.h), the decode span, not durations Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
|
(1) Confirmed: a timestamp. |
|
One change before approval, at 05a0692: the span pruning can misfile the longest turns as clean. Pruning uses
Either way Suggested fix: prune against the oldest start of any turn still in flight. Smaller points:
The timestamp comment and the dilution note answer my questions, thanks. |
…red (Cormac) On this base there is no yield inside a window, so recompute's time cost (+5-15% on CUDA, +33% on Metal) would land directly on a citizen's worst overlapped turn. A caller sends "recompute": true; the default turns on after the walk's per-chunk p95 with recompute is measured beside #39's receipt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc
…ight (Cormac on #39) Pruning against the longest FINISHED turn dropped spans that a longer-than-ever turn still in flight had overlapped; that turn then landed in the clean baseline and drifted it slow. The server loop knows which slots are working: send_final_response passes the earliest start of any other turn in flight, and the trainer drops only windows that ended before it (on every finished turn, measured or not). A bucketed percentile is clamped to the exact max, which 1% rounding could exceed. Smoke on the 0.8B (Metal): overlapped turns p95 35% (engine) vs 38% (client per-request). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
|
65b3ada: pruned against the earliest start of any turn still in flight (send_final_response scans the working slots; a slot not yet started begins after now, so it can't overlap a past span). Pruning runs on every finished turn, including ones too short to measure. Also clamped the bucketed percentiles to the exact max (1% rounding read p95 350000 over a max of 349025). Smoke: overlapped p95 35% engine vs 38% client. |
|
One change at 65b3ada: the pruning now erases the windows that overlapped the turn being measured. In Fix (either):
A test pins it: a span [100, 200], one turn [50, 300] with no other slot open. Its overlap must be 100 µs, not 0. The rest is right: pruning on every finished turn, measured or not, and the p50/p95 bucket clamped to the exact max. |
…uned (Cormac on #39) on_turn pruned the window spans against oldest_open_us before summing this turn's overlap, and with no other turn in flight the server passes this turn's own END as oldest_open_us, so every window that ended inside the turn was erased before it was counted: a turn a finished window slowed filed as clean, and the baseline drifted slow. The decision is now one pure function, server_train_turn_overlap_then_prune (new server-train-spans.h): it sums the overlap first, then drops windows that ended before the oldest turn still in flight (after this turn is measured, a window that ended before every open turn began can overlap no turn still to finish, and new turns start after now). It runs on every finished turn, a short one too. tests/test-server-train-spans.cpp pins it, starting with Cormac's case (span [100, 200], turn [50, 300], nothing else open: overlap 100, then the span goes), plus a running window, a window kept for another open turn, and an empty turn that still prunes. Known-positive: with the prune moved back in front of the sum, the first case FAILS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
|
bbdc2b3: measured, then pruned. The decision is one pure function (server-train-spans.h, server_train_turn_overlap_then_prune) with its own test, tests/test-server-train-spans.cpp, starting from your case: span [100,200], turn [50,300], nothing else open gives overlap 100, then the span is dropped. Also covered: a running window, a window kept for another open turn, and an empty turn that still prunes. Known-positive: with the prune moved back before the sum, your case FAILS. Server build clean. |
|
Reviewed at bbdc2b3. Approve (as a comment: shared account). Measuring before pruning is right, and the test pins Cormac's case: a window that ended inside the very turn being measured is counted, then dropped. Checked in the code, not assumed:
Worth one comment line: |
max_slowdown_ppm bounds the cycle's AVERAGE; Joel's bar is per turn (Cormac on #36). A turn
that lands at a window's start decodes at the in-window rate for the whole window, which the
average never shows. Serving now hands every finished turn to the trainer (its generation
span and decode steps): a turn that overlapped no training window is her per-turn baseline,
one that overlapped a window is a slowdown sample against it. A turn's rate is per slot, so
it is measured against other turns, never against the lane's total rate. Turns under 16 decode
steps time the scheduler, not her, and are skipped. A run's end closes its last window, so
later turns never read as overlapped by training that no longer runs.
/train status: turns_clean, turns_overlapped, turn_tps_clean, turn_slowdown_ppm_p50/p95/max.
Measured on Qwen3.5-0.8B Q8_0, Metal (M5, beside a live 27B lane), 2 slots, T = 10%:
turns were slowed p50 28% / p95 86% (engine), p95 80% (client per-request rates).
yielded; overlapped turns p50 54% / p95 89% (engine), 81% / 95% (client). Not reproduced in
run A; this GPU's baseline moved 45-123 tok/s between runs with the 27B lane beside it.
The receipt tracks the client within about 10 points at p95 in both, and shows what the
average hides: a 6-12 s window nearly stalls the turns it overlaps. The cap on window
duration (the walk's chunk size) is what makes the per-turn bar hold; this is its receipt.
🤖 Generated with Claude Code
https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo