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. Configs identified as failed are likewise removed by the report assembly before a metric series reaches this engine, so their partial measurements are gaps rather than baseline points. 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. - Workload boundaries are not software changes: a series is only a measurement of the software while the events being simulated stay the same. When the recorded ddsim seed changes — including the night a fixed seed is introduced over an unseeded history — the new level is a property of the new event sample, so judging it against the old baseline would report the changeover as a regression, and would do so reproducibly: the same fixed workload sits at the same offset every night, so the two-strike rule (gate 5) confirms it rather than protecting against it. The changed-over runs are left unjudged and re-anchor the baseline, the same treatment a confirmed change gets. A seed change starts a new segment wherever it lands, mid-release included, since nothing makes benchmark configuration follow the release schedule. This costs one segment of sensitivity per workload change, which is the honest price of no longer measuring the same thing.
Series reaching this engine are not always raw measurements. Common-mode
decomposition (:mod:k4bench.regression.common_mode) is applied by
:mod:k4bench.regression.report_builder before a history gets here: a night
where every config of a run group moves together is one event, not one per
config, so the group-wide shift is judged as its own series and divided out of
each config's. This is a decomposition, not a subtraction — nothing is
discarded, and a stack-wide regression is still caught.
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
workload_of ¶
The Monte-Carlo workload a run simulated, from its optional workload
column — the ddsim seed, or :data:WORKLOAD_UNKNOWN when there is none.
Compared by equality, so a history of unseeded nights is one workload and not a new one every night. Those nights genuinely simulate different events, but they scatter randomly, which is what the baseline spread and the two-strike gate already model. What they cannot model is a workload that changes and then stays changed — an unseeded history gaining a fixed seed lands at one offset and reproduces it every night — and that is exactly what a change in this value marks.
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.
Three further columns are optional annotations, each defaulting to absent:
workload— the ddsim seed this run simulated (see :func:workload_of). A release whose workload differs from the one the baseline was built under is left unjudged and re-anchors the baseline (gate 8), because its numbers measure a different event sample.raw_value/common_mode_shift— wherevalueis a residual after a group-wide shift was divided out, the measurement it came from and the shift that was removed. Carried onto the verdict untouched; the engine judgesvalueand never re-derives one from the other.
The unit of change is the release, as measured on one workload: nights
sharing a run_date and a workload are repeat measurements of one
software state on one event sample, and all engine state transitions happen
at those boundaries. A seed change splits a release rather than being
absorbed into it — benchmark configs are edited on their own schedule, so a
new seed usually lands mid-release. 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
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 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 | |