Skip to content

Commit cb9f8ca

Browse files
authored
[autorevert] Add infra_issue AI advisor verdict (treated like not_related) (#8213)
Adds a new AI advisor verdict **`infra_issue`** alongside `garbage`, wired through every consumer here (ClickHouse schema, autorevert lambda, HUD). Companion prompt change: pytorch/pytorch#188042. **Why:** a 7-day sample of `misc.autorevert_advisor_verdicts` showed nearly all `garbage` verdicts were actually CI infrastructure failures, and ~13 infra failures were mislabeled `not_related`. `infra_issue` gives them one consistent home ("the environment failed, no valid code-level signal"); `garbage` is narrowed to genuinely corrupt/invalid signals. **Handling:** `infra_issue` is treated like `not_related` — it blocks the signal from autorevert (no revert), since it isn't the suspect's fault. `garbage` keeps its 2h suppress-then-recheck window. **Changes:** CH schema `verdict` Enum8 gains `'infra_issue' = 6`; lambda `AdvisorVerdict.INFRA_ISSUE` + `ADVISOR_INFRA_ISSUE`, handled like `not_related` in `_check_advisor_verdict` (+2 tests); HUD `AdvisorVerdictType`, badge pill ("infra issue"), and `hud_renderer` CSS. **⚠️ Rollout order:** the live table must be ALTERed **before** the workflow emits `infra_issue`, or ingestion drops those rows: ```sql ALTER TABLE misc.autorevert_advisor_verdicts MODIFY COLUMN verdict Enum8('revert'=1,'unsure'=2,'not_related'=3,'garbage'=4,'related'=5,'infra_issue'=6); ``` Order: (1) ALTER → (2) merge this PR → (3) merge pytorch/pytorch#188042. Reads are crash-safe throughout (unknown verdict → `unsure`).
1 parent 6d36e31 commit cb9f8ca

13 files changed

Lines changed: 127 additions & 20 deletions

File tree

aws/lambda/pytorch-auto-revert/pytorch_auto_revert/hud_renderer.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
.advisor-cell.adv-revert { color: #a40000; }
5858
.advisor-cell.adv-not_related { color: #1a73e8; }
5959
.advisor-cell.adv-garbage { color: #7a5a00; }
60+
.advisor-cell.adv-infra_issue { color: #57606a; }
6061
.advisor-cell.adv-unsure { color: #555; }
6162
.advisor-dispatch { font-size: 10px; display: block; margin-top: 2px;
6263
color: #1a73e8; font-style: italic; }

aws/lambda/pytorch-auto-revert/pytorch_auto_revert/signal.py

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,23 @@ class AdvisorVerdict(Enum):
1717
1818
`revert` is retained indefinitely for backward compatibility with
1919
historical CH rows produced before the rename.
20+
21+
`infra_issue` means the CI environment failed before producing a real
22+
code-level outcome (runner/container/GPU/network/checkout failure). Like
23+
`not_related`, it is not the suspect's fault, so the lambda treats it the
24+
same as `not_related`: block this signal from autorevert (no revert). It is
25+
kept as a distinct verdict so the reason is recorded faithfully.
26+
27+
`garbage` means the recorded signal itself is invalid/corrupt as data; it
28+
suppresses the signal for a 2h window, then falls through to re-confirm.
2029
"""
2130

2231
REVERT = "revert"
2332
UNSURE = "unsure"
2433
NOT_RELATED = "not_related"
2534
GARBAGE = "garbage"
2635
RELATED = "related"
36+
INFRA_ISSUE = "infra_issue"
2737

2838

2939
@dataclass(frozen=True)
@@ -118,6 +128,7 @@ class IneligibleReason(Enum):
118128
PENDING_GAP = "pending_gap" # unknown/pending commits present
119129
ADVISOR_NOT_RELATED = "advisor_not_related" # AI advisor says not related
120130
ADVISOR_GARBAGE = "advisor_garbage" # AI advisor says signal is garbage
131+
ADVISOR_INFRA_ISSUE = "advisor_infra_issue" # AI advisor says infra failure
121132

122133

123134
@dataclass
@@ -601,7 +612,7 @@ def _check_advisor_verdict(
601612
602613
Returns:
603614
- AutorevertPattern if advisor says "revert" or "related" with sufficient confidence
604-
- Ineligible if advisor says "not_related" or "garbage" (within 2h window)
615+
- Ineligible if advisor says "not_related"/"infra_issue", or "garbage" (within 2h window)
605616
- None if no verdict, verdict is "unsure", or confidence below threshold
606617
"""
607618
suspected = partition.failed[-1]
@@ -620,14 +631,24 @@ def _check_advisor_verdict(
620631
if result.verdict in (AdvisorVerdict.REVERT, AdvisorVerdict.RELATED):
621632
return self._build_autorevert_pattern(partition, advisor_result=result)
622633

623-
if result.verdict == AdvisorVerdict.NOT_RELATED:
634+
# `infra_issue` (CI environment failed before producing a real
635+
# code-level outcome) is not the suspect's fault, so it is handled
636+
# exactly like `not_related`: block this signal from autorevert. Kept as
637+
# a distinct IneligibleReason so the cause is recorded.
638+
if result.verdict in (AdvisorVerdict.NOT_RELATED, AdvisorVerdict.INFRA_ISSUE):
639+
if result.verdict == AdvisorVerdict.INFRA_ISSUE:
640+
reason, label = IneligibleReason.ADVISOR_INFRA_ISSUE, "infra issue"
641+
else:
642+
reason, label = IneligibleReason.ADVISOR_NOT_RELATED, "not related"
624643
return Ineligible(
625-
IneligibleReason.ADVISOR_NOT_RELATED,
626-
f"AI advisor says not related (confidence={result.confidence:.2f})",
644+
reason,
645+
f"AI advisor says {label} (confidence={result.confidence:.2f})",
627646
)
628647

629648
if result.verdict == AdvisorVerdict.GARBAGE:
630-
# Garbage verdict blocks the signal for 2 hours since the verdict timestamp
649+
# Garbage (invalid/corrupt signal) blocks the signal for 2 hours
650+
# since the verdict timestamp, then falls through so a fresh run can
651+
# re-confirm.
631652
from datetime import timezone
632653

633654
now = datetime.now(timezone.utc)
@@ -639,7 +660,7 @@ def _check_advisor_verdict(
639660
f"(confidence={result.confidence:.2f}, "
640661
f"age={int(verdict_age.total_seconds() / 60)}min)",
641662
)
642-
# Garbage verdict expired — fall through to normal processing
663+
# Garbage window expired — fall through to normal processing
643664

644665
# "unsure" or expired garbage → continue normally
645666
return None
@@ -655,7 +676,7 @@ def _handle_no_successes(self) -> Union[AutorevertPattern, Ineligible]:
655676
suspect commit is likely the one that introduced the test. Defer to the
656677
AI advisor:
657678
- If a prior advisor verdict exists on the suspect, act on it
658-
(revert/related → `AutorevertPattern`; not_related/garbage → blocked).
679+
(revert/related → `AutorevertPattern`; not_related/infra_issue/garbage → blocked).
659680
- Otherwise, dispatch a fresh advisor request alongside the `Ineligible`
660681
response. Next tick can act on the verdict.
661682

aws/lambda/pytorch-auto-revert/pytorch_auto_revert/tests/test_signal.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -973,6 +973,56 @@ def test_advisor_garbage_expires_after_2h(self):
973973
# Garbage expired — should proceed to AutorevertPattern
974974
self.assertIsInstance(res, AutorevertPattern)
975975

976+
def test_advisor_infra_issue_produces_ineligible(self):
977+
"""infra_issue is treated like not_related: block the signal, no revert."""
978+
s = self._make_signal_with_advisor(AdvisorVerdict.INFRA_ISSUE)
979+
res = s.process_valid_autorevert_pattern()
980+
self.assertIsInstance(res, Ineligible)
981+
self.assertEqual(res.reason, IneligibleReason.ADVISOR_INFRA_ISSUE)
982+
983+
def test_advisor_infra_issue_does_not_expire(self):
984+
"""Unlike garbage, infra_issue has no 2h window — an old verdict still blocks."""
985+
old_timestamp = datetime.now(tz=timezone.utc) - timedelta(hours=3)
986+
advisor_result = AIAdvisorResult(
987+
verdict=AdvisorVerdict.INFRA_ISSUE,
988+
confidence=0.95,
989+
timestamp=old_timestamp,
990+
signal_key="job",
991+
)
992+
c_newest = SignalCommit(
993+
head_sha="sha_newest",
994+
timestamp=ts(self.t0, 0),
995+
events=[self._ev("job", SignalStatus.FAILURE, 7)],
996+
)
997+
c_newer = SignalCommit(
998+
head_sha="sha_newer",
999+
timestamp=ts(self.t0, 0),
1000+
events=[self._ev("job", SignalStatus.FAILURE, 5)],
1001+
)
1002+
c_suspected = SignalCommit(
1003+
head_sha="sha_mid",
1004+
timestamp=ts(self.t0, 0),
1005+
events=[self._ev("job", SignalStatus.FAILURE, 4)],
1006+
advisor_result=advisor_result,
1007+
)
1008+
c_base = SignalCommit(
1009+
head_sha="sha_old",
1010+
timestamp=ts(self.t0, 0),
1011+
events=[
1012+
self._ev("job", SignalStatus.SUCCESS, 3),
1013+
self._ev("job", SignalStatus.SUCCESS, 6),
1014+
],
1015+
)
1016+
s = Signal(
1017+
key="job",
1018+
workflow_name="wf",
1019+
commits=[c_newest, c_newer, c_suspected, c_base],
1020+
)
1021+
res = s.process_valid_autorevert_pattern()
1022+
# infra_issue has no expiry window — still blocked as not-the-suspect's-fault
1023+
self.assertIsInstance(res, Ineligible)
1024+
self.assertEqual(res.reason, IneligibleReason.ADVISOR_INFRA_ISSUE)
1025+
9761026
def test_advisor_unsure_continues_normal_processing(self):
9771027
"""When advisor says 'unsure', continue with normal autorevert logic."""
9781028
s = self._make_signal_with_advisor(AdvisorVerdict.UNSURE)

clickhouse_db_schema/misc.autorevert_advisor_verdicts/schema.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ CREATE TABLE misc.autorevert_advisor_verdicts
99
`signal_key` String,
1010
`signal_source` LowCardinality(String),
1111
`workflow_name` String,
12-
`verdict` Enum8('revert' = 1, 'unsure' = 2, 'not_related' = 3, 'garbage' = 4, 'related' = 5),
12+
`verdict` Enum8('revert' = 1, 'unsure' = 2, 'not_related' = 3, 'garbage' = 4, 'related' = 5, 'infra_issue' = 6),
1313
`confidence` Float32,
1414
`summary` String,
1515
`causal_reasoning` String,

torchci/components/autorevert/AutorevertCell.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Tooltip } from "@mui/material";
22
import AdvisorSection from "components/job/AdvisorSection";
3-
import { AdvisorVerdict } from "lib/advisorVerdictUtils";
3+
import { AdvisorVerdict, AdvisorVerdictType } from "lib/advisorVerdictUtils";
44
import styles from "./autorevert.module.css";
55
import {
66
CellEvent,
@@ -33,17 +33,19 @@ const ADV_VERDICT_CLS: Record<string, string> = {
3333
revert: styles.advRevert,
3434
related: styles.advRevert,
3535
not_related: styles.advNotRelated,
36+
infra_issue: styles.advInfra,
3637
garbage: styles.advGarbage,
3738
unsure: styles.advUnsure,
38-
};
39+
} satisfies Record<AdvisorVerdictType, string>;
3940

4041
const ADV_VERDICT_SHORT: Record<string, string> = {
4142
revert: "REV",
4243
related: "REV",
4344
not_related: "OK",
45+
infra_issue: "INF",
4446
garbage: "JNK",
4547
unsure: "?",
46-
};
48+
} satisfies Record<AdvisorVerdictType, string>;
4749

4850
interface AutorevertCellProps {
4951
events: CellEvent[];

torchci/components/autorevert/AutorevertGrid.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,10 @@ const INELIGIBLE_REASONS: Record<string, string> = {
4444
"Some commits between the failure and baseline have pending CI — waiting for results.",
4545
advisor_not_related:
4646
"AI advisor determined this failure is not related to the suspect commit.",
47+
advisor_infra_issue:
48+
"AI advisor determined this failure is an infrastructure issue, not caused by the suspect commit.",
4749
advisor_garbage:
48-
"AI advisor flagged this signal as unreliable (infrastructure flake).",
50+
"AI advisor flagged this signal as invalid — it does not reflect a real CI outcome.",
4951
};
5052

5153
function outcomeTooltip(

torchci/components/autorevert/autorevert.module.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,11 @@
358358
color: #8d6e63;
359359
}
360360

361+
.advInfra {
362+
background: rgba(87, 96, 106, 0.2);
363+
color: #57606a;
364+
}
365+
361366
.advUnsure {
362367
background: rgba(117, 117, 117, 0.2);
363368
color: #757575;

torchci/components/autorevert/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
* The API merges multiple workflow-set rows into a unified response.
66
*/
77

8+
import { AdvisorVerdictType } from "lib/advisorVerdictUtils";
9+
810
// --- API Response ---
911

1012
export interface AutorevertStateResponse {
@@ -43,7 +45,7 @@ export interface CellEvent {
4345
}
4446

4547
export interface ColumnAdvisorResult {
46-
verdict: "revert" | "not_related" | "garbage" | "unsure";
48+
verdict: AdvisorVerdictType;
4749
confidence: number;
4850
signal_key: string;
4951
}

torchci/components/job/AdvisorSection.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
1-
import { AdvisorVerdict, advisorRunUrl } from "lib/advisorVerdictUtils";
1+
import {
2+
AdvisorVerdict,
3+
AdvisorVerdictType,
4+
advisorRunUrl,
5+
} from "lib/advisorVerdictUtils";
26
import { useState } from "react";
37

48
const VERDICT_COLORS: Record<string, { border: string; badge: string }> = {
59
revert: { border: "#d32f2f", badge: "#d32f2f" },
610
related: { border: "#d32f2f", badge: "#d32f2f" },
711
not_related: { border: "#388e3c", badge: "#2e7d32" },
12+
infra_issue: { border: "#57606a", badge: "#57606a" },
813
garbage: { border: "#8d6e63", badge: "#6d4c41" },
914
unsure: { border: "#757575", badge: "#616161" },
10-
};
15+
} satisfies Record<AdvisorVerdictType, { border: string; badge: string }>;
1116

1217
export default function AdvisorSection({
1318
verdict,

torchci/components/job/AiAdvisorIndicator.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,22 +68,27 @@ function isDispatched(
6868

6969
const VERDICT_CHIP_COLORS: Record<
7070
string,
71-
"error" | "warning" | "success" | "default"
71+
"error" | "warning" | "success" | "default" | "info"
7272
> = {
7373
revert: "error",
7474
related: "error",
7575
unsure: "warning",
7676
not_related: "success",
77+
infra_issue: "info",
7778
garbage: "default",
78-
};
79+
} satisfies Record<
80+
AdvisorVerdictType,
81+
"error" | "warning" | "success" | "default" | "info"
82+
>;
7983

8084
const VERDICT_LABELS: Record<string, string> = {
8185
revert: "Revert",
8286
related: "Related",
8387
unsure: "Unsure",
8488
not_related: "Not Related",
89+
infra_issue: "Infra Issue",
8590
garbage: "Garbage Signal",
86-
};
91+
} satisfies Record<AdvisorVerdictType, string>;
8792

8893
export default function AiAdvisorIndicator({
8994
jobName,

0 commit comments

Comments
 (0)