k4bench.regression.engine¶
k4bench.regression.engine ¶
Statistical step-change detector for nightly benchmark metrics.
Detection is deliberately conservative — the goal is a report developers trust, so every gate errs toward not flagging:
- Baseline: the trailing :data:
BASELINE_WINDOW_RUNSreliable runs strictly before the night under test. Runs failing the conservative host reliability check (:mod:k4bench.results.reliability) never enter the baseline and are never themselves evaluated — contention is not a regression. Below :data:MIN_BASELINE_RUNSreliable points the verdict isUNKNOWN, never a flag (no evidence ⇒ no verdict). - Robust statistics: the baseline center/spread are the median and the
normal-consistent MAD (:data:
MAD_NORMAL_CONSISTENCY× MAD), not mean/stddev, so one contaminated night that slipped past the reliability filter cannot distort the threshold. - Robust z-gate: flag only beyond :data:
Z_THRESHOLD(the Iglewicz–Hoaglin robust-outlier threshold). - Practical-effect floor (:data:
EFFECT_FLOOR), ANDed with the z-gate: a metric that is normally rock-steady has a tiny MAD, so the z-gate alone would trip on practically irrelevant wobbles. - Two-strike confirmation: the first night crossing both gates is
WATCH; only when the next reliable night repeats it in the same direction does it becomeCONFIRMED. This is the single highest-leverage lever against false positives (the pattern used by Chromium's and Firefox's perf-CI bots) — more effective than tightening the z-gate, which just trades false positives for missed regressions. The confirming night may belong to the same Key4hep release as the WATCH night (the nightly stack does not publish daily, so consecutive runs often re-measure one release): a second run of the same binary tripping the same way is independent evidence that rules out a one-night machine fluke. - Change-point re-anchoring at release boundaries: the unit of change
is the release; nights are repeat measurements of it. All nights
sharing a
run_date(the release date) are judged against one frozen baseline snapshot, and a confirmation is sticky for the rest of its release — every later night of the release tripping the same way is alsoCONFIRMED, with the same onset window, because the regression is a property of the release and every re-measurement of it should say so. Sticky, but not unconditional: the confirmed state holds only while the median of the release's judged nights still clears the gates. One quiet night cannot outvote the two that confirmed, but once quiet nights drag the release median back inside the band, the confirming nights are best explained as noise — the confirmation is revoked, and the baseline is never re-anchored onto a fluke level. Only when the walk leaves a release that confirmed a change is the baseline re-anchored on that release's values: the confirmed level becomes the new accepted one, so an expected regression (say, a deliberate physics change) alerts once per release transition instead of being re-judged against the pre-change median for weeks. While the new segment is short, judging continues against its median with the pre-change spread as the noise proxy, so a second change arriving right away is still caught. The state is recomputed from the history on every walk, so there is no state file to manage. - No good/bad judgment: a confirmed change is reported as
UPorDOWN— a plain sign, not an evaluation. Faster is not "improved" any more than slower is "regressed" in the colloquial sense: either can be a deliberate change, an optimization, or a bug, and the report leaves that call to a human instead of asserting one.
Known v1 limitations (deliberate):
- Slow drift (a creeping regression too small to trip step detection) is out of scope — "Phase 5 (not built)". Gathering a track record on the step detector comes first; Mann-Kendall/EWMA can be layered on later.
robust_baseline ¶
Return (median, scaled_mad) of values.
The MAD is scaled by :data:MAD_NORMAL_CONSISTENCY so the z-scores built
from it are comparable to classical standard scores under normality.
Source code in k4bench/regression/engine.py
robust_change ¶
Return (pct_change, z_score) of x against baseline (med, mad).
Shared by the confirmation walk below and by
:mod:k4bench.regression.report_builder's one-shot region-contributor
lookup, so both use exactly the same robust-statistics math.
Source code in k4bench/regression/engine.py
release_key ¶
The release a night measured: its date, or its run id when the date is unknown.
The walk below groups on this, and so does every consumer that has to line
a night up with the software state it measured (see
:mod:k4bench.regression.history). One definition, because a night placed
in one release here and another release there would compare a metric
against a baseline it was never judged under. A row with no usable date
keys on its run id and therefore forms a single-night group of its own,
which is what makes the degradation explicit rather than silently pooling
every undated night together.
Source code in k4bench/regression/engine.py
evaluate_series ¶
evaluate_series(history: DataFrame, *, series: SeriesId) -> list[MetricVerdict]
Chronologically evaluate one metric history and return its verdict series.
history holds the full ordered history of one series (one
:class:SeriesId), with columns:
run_id— run identifier (the nightly date directory name),run_date— datetime-like, defines the evaluation order,value— the metric value (NaN/None rows are skipped),reliable— the per-run reliability tri-state (True/False/None). OnlyFalseexcludes a run: an unknown verdict (no machine info) is not evidence of contention, and excluding it would starve baselines on histories without machine info — the same policy as the dashboard's reliability filter.
The unit of change is the release: nights sharing a run_date are
repeat measurements of one software state, and all engine state
transitions happen at release boundaries. Rows whose release date is
unknown fall back to their run date and therefore form single-night
groups, where the walk degrades to a plain night-by-night pass. Within a
release, every judged night is compared against one baseline snapshot
(median, MAD) frozen on entering the release from state accumulated
under earlier releases only — so every report night of one release
agrees on what the release was judged against. The two-strike pending
state still moves night by night (a WATCH set by an earlier night of the
same release confirms on a later one — a second run of the same binary
is independent evidence against a machine fluke; a clean night clears an
unconfirmed WATCH). Unreliable runs are skipped entirely — they neither
confirm nor reset a pending WATCH, since there is no evidence either way
for that night. Returns the full verdict series (the dashboard
drill-down shades from it); callers wanting "tonight's" verdict take the
last element.
A confirmation is sticky while the release's evidence supports it:
once a change is confirmed in a direction, every later night of the
release tripping the same way is also CONFIRMED and carries the
same onset_*/last_accepted_* window (the regression is a
property of the release, and every night re-measuring it reports it
identically — identical windows also keep the blame sidecar's per-window
dedup stable). A single OK night inside the release does not clear the
confirmed state — it is reported OK with a note that the release median
still sits beyond the baseline. But retention is decided by that median:
when enough quiet nights pull the median of the release's judged values
back inside the gates, the confirmation is revoked (the confirming
nights were more likely noise), a later trip starts a fresh two-strike
cycle, and the release triggers no boundary re-anchor.
Leaving a release that confirmed a change is the change-point: the
baseline window is re-anchored there (cleared and re-seeded with all of
the release's reliable judged values), so an expected/accepted change
alerts on the release that introduced it and on every re-measurement of
that release, then falls quiet from the next release on. While the new
segment is still short (< :data:MIN_BASELINE_RUNS points) the walk
keeps judging — against the growing segment's median, with the
pre-change scaled MAD as the spread proxy (the noise level changes far
less than the level itself) — so a second change arriving right after a
confirmed one is still caught rather than falling into a blind window.
A release that confirmed nothing simply appends its judged values to the
baseline in night order and carries any still-pending WATCH forward.
Each CONFIRMED verdict also carries the window the change entered in:
onset_* (the WATCH night — where it first appeared, one reliable night
before it was confirmed) and last_accepted_* (the newest night before
that observed at the then-accepted level). Because confirmation trails
onset, the confirmed night is the wrong place to look for a cause; the
change landed in (last_accepted, onset]. Both ends may fall inside
one release (first night OK, later nights confirm): such a same-release
window proves the stack did not move between them. An unreliable night
inside the window is skipped, not judged, so it never narrows the
window — it is spanned by it.
Source code in k4bench/regression/engine.py
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 | |