Skip to content

feat: continuous coverage evaluation + calibration-triggered retraining #19

Description

@gkneighb

Problem

continuous_training_loop retrains every RETRAINING_INTERVAL_SEC (default 1800s) regardless of model quality or workload-distribution change. Two things follow:

  1. Calibration is only computed inside train(). _calculate_metrics_on_test (training_server.py:717) runs once per retrain, populates ttft_coverage_scores / tpot_coverage_scores / ttft_quantile_loss_scores / tpot_quantile_loss_scores, and the /metrics endpoint serves whatever value was computed at the last retrain. Between retrains, /metrics is stale by construction.
  2. No mechanism reads the calibration signal back into the training cadence. All four metric deques are populated and exposed but never consumed for any decision in train() or continuous_training_loop.

So under workload drift the system has a window of RETRAINING_INTERVAL_SEC during which (a) predictions are served from the stale model, (b) /metrics shows pre-drift coverage, and (c) the operator has no signal anything is wrong.

Empirical characterization

I prototyped the proposed change against a synthetic-drift harness (training_server in isolation, no router/vLLM) — a workload script drives /add_training_data_bulk with a baseline regime that flips to a drift regime at a configurable time, and a separate script polls /metrics every 5s. Three experiments under RETRAINING_INTERVAL_SEC=300, MIN_SAMPLES_FOR_RETRAIN=1000:

(A) Production-like timing, 3× slope drift. Drift fires at t=600s; first post-drift retrain at t=910s (310s detection lag). Coverage drops from 88.5% pre-drift to 76.8% post-drift, violation rate from 11% to 23%. Between t=600 and t=910, /metrics shows healthy 89% coverage — drift is silent.

(B) 10× slope drift, 20s retrain interval. Coverage moves only ~5pp (88% → 81%) but quantile loss explodes 69× (0.55 → 39.27) and decays gradually. Coverage is a weaker drift signal than QL for sharp shifts because the test buffer is FIFO size 1000 and binary-above/below loses magnitude.

(C) Same as A but with continuous coverage evaluation + calibration trigger (described below). Drift-to-first-detection drops from 310s to 74s (4×). 9 retrains in the post-drift window vs A's 1.

Proposal

Two changes, both contained to training_server.py:

  1. continuous_coverage_loop — a background thread that runs every COVERAGE_EVAL_INTERVAL_SEC (default 30s), evaluates _calculate_metrics_on_test against the currently loaded model + test buffer, and appends to the existing *_coverage_scores and *_quantile_loss_scores deques. No training, just evaluation. This alone makes /metrics fresh between retrains.

  2. Calibration trigger on continuous_training_loop's inter-retrain wait. Track EMA of |coverage - quantile_alpha| and quantile_loss / qloss_baseline. When either exceeds its threshold for K consecutive evaluations, signal a threading.Event the training loop checks during its sleep — fires train() immediately instead of waiting out RETRAINING_INTERVAL_SEC.

New env vars matching existing style:

LATENCY_COVERAGE_EVAL_INTERVAL_SEC      (default: 30; 0 disables)
LATENCY_CALIBRATION_COVERAGE_THRESHOLD  (default: 5.0 pp deviation from quantile_alpha)
LATENCY_CALIBRATION_QLOSS_RATIO         (default: 3.0× rolling baseline)
LATENCY_CALIBRATION_TRIGGER_K           (default: 2 consecutive bad evals)
LATENCY_CALIBRATION_EMA_ALPHA           (default: 0.3)

Default RETRAINING_INTERVAL_SEC could be raised once calibration-triggered retraining is in (e.g. to 7200s) since drift events bypass the schedule.

Open design questions for review

  • Dual-signal vs coverage-only. Experiment B showed coverage moves ~5pp under 10× drift while QL moves 69×. C also showed coverage and QL diverging during recovery — model accuracy improves (QL ↓) while calibration degrades (coverage ↓) as predictions converge on the drift-regime mean. A coverage-only trigger underweights both ends. The proposal above uses OR-composition of two signals; happy to argue otherwise if there's a preference.

  • Warmup grace period. In experiment C the trigger fired twice during cold-start (t=46s, t=106s) on transient coverage swings while the model was still converging on its first thousand samples. Easy mitigations: skip triggers for the first WARMUP_RETRAINS retrains, relax the threshold for the first WARMUP_SAMPLES, or both. Worth picking one before code lands.

  • Threshold tuning under different quantile_alpha. Defaults above are for the 0.9 baseline. For 0.99 quantile the natural variance of quantile_coverage is much smaller — threshold should likely scale.

  • /metrics cardinality. Adding a quantile_coverage_deviation_ema and qloss_ratio_ema gauge per model (TTFT/TPOT) is 4 extra series. Trivial but flagging it.

Things I considered and am explicitly leaving out

  • RandomDropDeque (lines 63-88). Defined but never used — actual buckets and test buffer use plain deque(maxlen=...) which is FIFO. So the "buffer dilutes new-regime data" argument isn't real today. If it ever gets wired in, it deserves a separate issue.

  • Feature-distribution drift detection (1D KS-test on incoming samples). Would catch upstream regime change before it shows up as calibration drift. Separate issue — needs a snapshot reference distribution and lifecycle.

  • Test-buffer size and refresh rate. Experiment C showed coverage continues degrading for ~500s post-drift even with aggressive triggered retraining, because the test buffer takes that long to fully refresh (FIFO size 1000 @ 2/s test arrivals). A larger buffer slows refresh; a smaller one is noisy. Worth a separate discussion.

Happy to send a PR for the two-change proposal once the design direction lands. Prototype implementation (~80 lines) is on a local branch; can post a draft PR or wait for feedback first depending on preference.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions