Repository navigation
server /train: max_slowdown_ppm bounds her decode slowdown, measured - #36
Conversation
… instead of a time share The share paced HOW OFTEN a window ran, not what it cost her: measured on Kimi's lane, her decode fell from 52 to 23 tok/s while a 25% share trained. The caller means a bound on her slowdown (Joel: "negligible impact on inference latency ... a slowdown if clever can be unnoticed"), so the engine now takes that bound and measures against it. - serving publishes the tokens it has decoded (one atomic beside n_busy_slots) and how long its slots have been idle; the trainer reads them as one serving_view. - after a window of d ms the yield owed is closed form on her measured rates: y = d * (r0 * (1 - T) - r_w) / (r0 * T), 0 when the window costs her less than T, the worst case (r_w = 0) before r0 has a sample. No loop gains to tune. - r0 (no window running) is sampled between consecutive 10 ms looks that both saw a slot working, so bursty turns still measure it; r_w over windows that started and ended busy. Recent evidence (60 s time constant) so a load change moves the bound. - a bound that stops yielding stops measuring r0: when it has gone 30 s without a probe's worth (500 ms) of samples, 500 ms is owed (about 1.6% of the trainer's time). - idle releases a yield only after 250 ms: a turn ending is not serving going idle. Measured: two lockstep requests ended together and 8.6 s windows were taken in the gap, holding the next turns behind them; the same gap exists between an agent's acts. - status: decode_tps_no_window / _in_window (run), *_recent (what the bound acts on), slowdown_ppm_realized (the receipt). share_ppm stays for callers that send it; sending both is refused. Measured on Qwen3.5-0.8B Q8_0, Metal (M5, beside a live 27B lane), 2 slots decoding, T = 10%: r0 93-120 tok/s, r_w 40-58, client throughput 92.1 tok/s before training and 94.4 during; the engine's realized figure over the run was 17% (one 13.5 s window, her rate moving), and the trainer still held about 20% of the lane. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
354332c to
000152b
Compare
|
APPROVED at 000152b. The yield is the right algebra: holding the cycle's average rate at r0(1-T) gives y = d(r0(1-T) - r_w)/(r0 T), which is yield_for_slowdown. The three measured fixes are real: an inter-request gap isn't idle (the 250 ms release), a bound that stops yielding still gets a no-window probe every 30 s, and the first look doesn't cancel the probe. The status reports the realized figure (17% here vs a 10% target) rather than the target, which is the honest receipt. One note for the NEXT step, not this PR: T bounds the AVERAGE over a cycle. Joel's bar ('brief ... unnoticed') is per turn. A turn arriving at a window's start still runs at the in-window rate (here 40-58 vs 93-120 tok/s) for the whole window, up to 13.5 s in your run. So the worst turn is bounded by window LENGTH, which you already named. Capping a window's duration (chunk size, or a ms ceiling the engine enforces per window) and reporting p95 turn slowdown beside the average would make both bounds hold. |
…al training restored (#38) (#4821) * fix(engine): pin 98f8df4ba — the decode-rate controller (llama.cpp #36) Fable's CambrianTech/llama.cpp#36 (approved by Cormac), on top of #35 already pinned by #4819. Training dreams in downtime: idle slots run windows back to back at full speed (after a 250 ms idle grace, so the gap between two of a citizen's acts is not downtime), and a busy lane yields to keep her measured decode rate within max_slowdown_ppm (continuum #4820 sends it). Measured on the 0.8B beside Kimi's live lane: client throughput 92.1 tok/s before training, 94.4 during. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc * fix(engine): pin 68c2ec729 — #36 plus #38 (refuse only a node no backend can run: Metal trains again) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…d a bare lambda) #36 (000152b) changed server_trainer's ctor to take a serving_view, and test-chat.cpp still passed the busy_slots lambda directly, which broke test-chat on every PR on feat/props-weight-residency (Cormac). The lambda is now serving_view's busy_slots; idle_ms and tokens_generated are optional and left empty, which keeps what the test drives. test-chat compiles (CPU build). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc
The share paced HOW OFTEN a window ran, not what it cost her: measured on Kimi's lane, her
decode fell from 52 to 23 tok/s while a 25% share trained. The caller means a bound on her
slowdown (Joel: "negligible impact on inference latency ... a slowdown if clever can be
unnoticed"), so the engine now takes that bound and measures against it.
its slots have been idle; the trainer reads them as one serving_view.
y = d * (r0 * (1 - T) - r_w) / (r0 * T), 0 when the window costs her less than T, the worst
case (r_w = 0) before r0 has a sample. No loop gains to tune.
working, so bursty turns still measure it; r_w over windows that started and ended busy.
Recent evidence (60 s time constant) so a load change moves the bound.
worth (500 ms) of samples, 500 ms is owed (about 1.6% of the trainer's time).
two lockstep requests ended together and 8.6 s windows were taken in the gap, holding the
next turns behind them; the same gap exists between an agent's acts.
slowdown_ppm_realized (the receipt). share_ppm stays for callers that send it; sending both
is refused.
Measured on Qwen3.5-0.8B Q8_0, Metal (M5, beside a live 27B lane), 2 slots decoding, T = 10%:
r0 93-120 tok/s, r_w 40-58, client throughput 92.1 tok/s before training and 94.4 during;
the engine's realized figure over the run was 17% (one 13.5 s window, her rate moving), and
the trainer still held about 20% of the lane.
Core side (sends max_slowdown_ppm, reads the rates into training.run.share) follows as a continuum PR.
🤖 Generated with Claude Code
https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo