2026-07-10 00:22:50 +08:00
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
from collections import defaultdict
|
2026-08-10 21:44:42 +08:00
|
|
|
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
2026-07-10 00:22:50 +08:00
|
|
|
|
from datetime import datetime, timedelta
|
2026-08-12 08:02:51 +08:00
|
|
|
|
import os
|
2026-07-10 00:22:50 +08:00
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
2026-08-12 08:02:51 +08:00
|
|
|
|
from architecture_compatibility import (
|
|
|
|
|
|
DEFAULT_ARCHITECTURE_BLOCK_TTL_DAYS,
|
|
|
|
|
|
EXPLICIT_ARCHITECTURE_FAILURE_CATEGORY,
|
|
|
|
|
|
EXPLICIT_ARCHITECTURE_FAILURE_REASON,
|
|
|
|
|
|
architecture_compatibility_key,
|
|
|
|
|
|
architecture_profile,
|
2026-08-12 08:19:53 +08:00
|
|
|
|
architecture_profiles,
|
2026-08-12 08:02:51 +08:00
|
|
|
|
)
|
2026-08-01 16:50:50 +08:00
|
|
|
|
from common import append_jsonl, parse_datetime, read_jsonl, update_jsonl, utc_now
|
2026-08-10 21:44:42 +08:00
|
|
|
|
from failure_log_inspector import fetch_and_classify_failure_log
|
2026-07-10 00:22:50 +08:00
|
|
|
|
from history_stats import classify_failure, is_failure, is_success
|
2026-08-10 21:44:42 +08:00
|
|
|
|
from llm_classifier import LLMAssistedClassifier
|
2026-07-10 02:02:08 +08:00
|
|
|
|
from modelhub_client import ModelHubClient, ModelHubClientPool
|
2026-08-12 08:41:07 +08:00
|
|
|
|
from task_registry import task_type_from_history_task
|
2026-07-10 00:22:50 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
DEFAULT_OUTCOMES_PATH = Path("outcomes/submissions.jsonl")
|
2026-08-10 21:44:42 +08:00
|
|
|
|
FAILURE_ENRICHMENT_LIMIT = 40
|
|
|
|
|
|
FAILURE_ENRICHMENT_WORKERS = 4
|
|
|
|
|
|
FAILURE_ENRICHMENT_MAX_ATTEMPTS = 3
|
2026-08-12 08:41:07 +08:00
|
|
|
|
TERMINAL_TASK_STATUSES = {"success", "failed", "error", "cancelled", "completed"}
|
2026-07-10 00:22:50 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _now_iso() -> str:
|
|
|
|
|
|
return utc_now().isoformat()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OutcomeTracker:
|
|
|
|
|
|
def __init__(self, path: Path | str) -> None:
|
|
|
|
|
|
self.path = Path(path)
|
|
|
|
|
|
self._records: list[dict[str, Any]] = []
|
|
|
|
|
|
self._by_task_id: dict[str, dict[str, Any]] = {}
|
|
|
|
|
|
self._by_model_gpu: dict[tuple[str, str], list[dict[str, Any]]] = defaultdict(list)
|
2026-08-02 17:45:21 +08:00
|
|
|
|
self._failed_model_gpus: dict[tuple[str, str], datetime] = {}
|
2026-08-10 21:44:42 +08:00
|
|
|
|
self._failure_llm_classifier: LLMAssistedClassifier | None = None
|
2026-07-10 00:22:50 +08:00
|
|
|
|
|
2026-08-01 16:50:50 +08:00
|
|
|
|
self._records = read_jsonl(self.path)
|
|
|
|
|
|
self._rebuild_indexes()
|
|
|
|
|
|
|
|
|
|
|
|
last_sync_times = [
|
|
|
|
|
|
parse_datetime(record.get("lastSyncTime"))
|
|
|
|
|
|
for record in self._records
|
|
|
|
|
|
if record.get("lastSyncTime")
|
|
|
|
|
|
]
|
|
|
|
|
|
self._last_sync_time: datetime = max(last_sync_times) if last_sync_times else utc_now() - timedelta(days=7)
|
|
|
|
|
|
|
2026-08-10 21:44:42 +08:00
|
|
|
|
def set_failure_llm_classifier(self, classifier: LLMAssistedClassifier | None) -> None:
|
|
|
|
|
|
self._failure_llm_classifier = classifier
|
|
|
|
|
|
|
2026-08-12 08:19:53 +08:00
|
|
|
|
def get_task_compatibility_contexts(self) -> dict[str, dict[str, Any]]:
|
|
|
|
|
|
"""Return locally known submit metadata needed for account queue cleanup."""
|
|
|
|
|
|
contexts: dict[str, dict[str, Any]] = {}
|
|
|
|
|
|
for task_id, record in self._by_task_id.items():
|
|
|
|
|
|
framework = str(record.get("framework") or "").strip()
|
|
|
|
|
|
task_type = str(record.get("taskType") or "").strip()
|
|
|
|
|
|
if not framework or not task_type:
|
|
|
|
|
|
continue
|
|
|
|
|
|
profile = record.get("modelProfile")
|
|
|
|
|
|
contexts[task_id] = {
|
|
|
|
|
|
"taskId": task_id,
|
|
|
|
|
|
"modelId": str(record.get("modelId") or ""),
|
|
|
|
|
|
"targetGpu": str(record.get("targetGpu") or ""),
|
|
|
|
|
|
"framework": framework,
|
|
|
|
|
|
"taskType": task_type,
|
|
|
|
|
|
"modelProfile": dict(profile) if isinstance(profile, dict) else {},
|
|
|
|
|
|
"submitTime": record.get("submitTime"),
|
|
|
|
|
|
}
|
|
|
|
|
|
return contexts
|
|
|
|
|
|
|
2026-08-12 10:18:09 +08:00
|
|
|
|
def get_strategy_history_records(self) -> list[dict[str, Any]]:
|
|
|
|
|
|
"""Expose only successes and evidence-attributable failures for GPU ranking."""
|
|
|
|
|
|
records: list[dict[str, Any]] = []
|
|
|
|
|
|
for record in self._records:
|
|
|
|
|
|
outcome = record.get("outcome")
|
|
|
|
|
|
if outcome == "success":
|
|
|
|
|
|
verify_result = 1
|
|
|
|
|
|
elif _is_attributable_failure(record):
|
|
|
|
|
|
verify_result = -1
|
|
|
|
|
|
else:
|
|
|
|
|
|
continue
|
|
|
|
|
|
records.append(
|
|
|
|
|
|
{
|
|
|
|
|
|
**record,
|
|
|
|
|
|
"gpuType": record.get("targetGpu"),
|
|
|
|
|
|
"status": "success",
|
|
|
|
|
|
"verifyResult": verify_result,
|
|
|
|
|
|
"updateTime": record.get("lastSyncTime") or record.get("submitTime"),
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
return records
|
|
|
|
|
|
|
2026-08-01 16:50:50 +08:00
|
|
|
|
def _rebuild_indexes(self) -> None:
|
|
|
|
|
|
self._by_task_id.clear()
|
|
|
|
|
|
self._by_model_gpu.clear()
|
|
|
|
|
|
for record in self._records:
|
2026-07-10 00:22:50 +08:00
|
|
|
|
task_id = record.get("taskId")
|
|
|
|
|
|
if task_id:
|
|
|
|
|
|
self._by_task_id[str(task_id)] = record
|
|
|
|
|
|
model_id = record.get("modelId") or ""
|
|
|
|
|
|
target_gpu = record.get("targetGpu") or ""
|
|
|
|
|
|
self._by_model_gpu[(model_id, target_gpu)].append(record)
|
|
|
|
|
|
|
|
|
|
|
|
self._rebuild_failed_index()
|
|
|
|
|
|
|
|
|
|
|
|
def record_submission(
|
|
|
|
|
|
self,
|
|
|
|
|
|
model_id: str,
|
|
|
|
|
|
target_gpu: str,
|
|
|
|
|
|
framework: str,
|
|
|
|
|
|
task_type: str,
|
|
|
|
|
|
task_id: str | None,
|
|
|
|
|
|
submit_time: str,
|
2026-08-10 21:44:42 +08:00
|
|
|
|
model_profile: dict[str, Any] | None = None,
|
2026-07-10 00:22:50 +08:00
|
|
|
|
) -> None:
|
|
|
|
|
|
record: dict[str, Any] = {
|
|
|
|
|
|
"modelId": model_id,
|
|
|
|
|
|
"targetGpu": target_gpu,
|
|
|
|
|
|
"framework": framework,
|
|
|
|
|
|
"taskType": task_type,
|
|
|
|
|
|
"taskId": task_id,
|
|
|
|
|
|
"submitTime": submit_time,
|
|
|
|
|
|
"lastSyncTime": None,
|
|
|
|
|
|
"status": "pending",
|
|
|
|
|
|
"verifyResult": None,
|
|
|
|
|
|
"outcome": "pending",
|
|
|
|
|
|
"failReason": None,
|
2026-08-10 21:44:42 +08:00
|
|
|
|
"modelProfile": dict(model_profile or {}),
|
2026-07-10 00:22:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
self._records.append(record)
|
|
|
|
|
|
if task_id:
|
|
|
|
|
|
self._by_task_id[task_id] = record
|
|
|
|
|
|
self._by_model_gpu[(model_id, target_gpu)].append(record)
|
|
|
|
|
|
append_jsonl(self.path, record)
|
|
|
|
|
|
|
2026-08-12 01:04:34 +08:00
|
|
|
|
def mark_policy_cancellations(self, decisions: list[dict[str, Any]]) -> int:
|
|
|
|
|
|
"""Persist intentional task stops so they never become failure evidence."""
|
|
|
|
|
|
marked = 0
|
|
|
|
|
|
marked_at = _now_iso()
|
|
|
|
|
|
for decision in decisions:
|
|
|
|
|
|
task_id_value = decision.get("taskId")
|
|
|
|
|
|
if task_id_value is None:
|
|
|
|
|
|
continue
|
|
|
|
|
|
task_id = str(task_id_value)
|
|
|
|
|
|
existing = self._by_task_id.get(task_id)
|
|
|
|
|
|
if existing is not None and existing.get("outcome") in {"success", "failed"}:
|
|
|
|
|
|
continue
|
|
|
|
|
|
reasons = list(decision.get("cleanupReasons") or [decision.get("reason")])
|
|
|
|
|
|
reasons = [str(reason) for reason in reasons if reason]
|
|
|
|
|
|
if existing is None:
|
|
|
|
|
|
existing = {
|
|
|
|
|
|
"modelId": decision.get("modelId") or "",
|
|
|
|
|
|
"targetGpu": decision.get("gpuType") or decision.get("targetGpu") or "",
|
|
|
|
|
|
"framework": decision.get("framework") or "",
|
|
|
|
|
|
"taskType": decision.get("taskType") or "",
|
|
|
|
|
|
"taskId": task_id,
|
|
|
|
|
|
"submitTime": decision.get("submitTime") or marked_at,
|
|
|
|
|
|
"verifyResult": None,
|
|
|
|
|
|
}
|
|
|
|
|
|
self._records.append(existing)
|
|
|
|
|
|
self._by_task_id[task_id] = existing
|
|
|
|
|
|
self._by_model_gpu[(existing["modelId"], existing["targetGpu"])].append(existing)
|
|
|
|
|
|
existing.update(
|
|
|
|
|
|
{
|
|
|
|
|
|
"lastSyncTime": marked_at,
|
|
|
|
|
|
"status": "cancellation_requested",
|
|
|
|
|
|
"outcome": "policy_cancelled",
|
|
|
|
|
|
"failReason": None,
|
|
|
|
|
|
"policyCancelled": True,
|
|
|
|
|
|
"policyCancellationReasons": reasons,
|
|
|
|
|
|
"policyCancelledAt": marked_at,
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
marked += 1
|
|
|
|
|
|
if marked:
|
|
|
|
|
|
self._rebuild_failed_index()
|
|
|
|
|
|
return marked
|
|
|
|
|
|
|
2026-07-10 02:02:08 +08:00
|
|
|
|
def sync_from_api(self, client: ModelHubClient | ModelHubClientPool) -> int:
|
2026-07-10 00:22:50 +08:00
|
|
|
|
try:
|
|
|
|
|
|
begin = self._last_sync_time
|
|
|
|
|
|
end = utc_now()
|
2026-07-10 02:02:08 +08:00
|
|
|
|
# Use fanout for pools to sync outcomes across all accounts
|
2026-07-10 00:22:50 +08:00
|
|
|
|
list_kwargs: dict[str, Any] = {"begin_time": begin, "end_time": end, "page_size": 100, "only_mine": True}
|
2026-07-10 02:02:08 +08:00
|
|
|
|
if isinstance(client, ModelHubClientPool):
|
|
|
|
|
|
list_kwargs["_fanout_all"] = True
|
2026-07-10 00:22:50 +08:00
|
|
|
|
tasks = client.list_tasks(**list_kwargs)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
updated_count = 0
|
2026-08-10 21:44:42 +08:00
|
|
|
|
enrichment_candidates: list[dict[str, Any]] = []
|
2026-07-10 00:22:50 +08:00
|
|
|
|
for task in tasks:
|
|
|
|
|
|
task_id = str(task.get("taskId")) if task.get("taskId") is not None else None
|
|
|
|
|
|
if not task_id:
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
existing = self._by_task_id.get(task_id)
|
|
|
|
|
|
if existing is not None:
|
2026-08-12 01:04:34 +08:00
|
|
|
|
if existing.get("outcome") in {"pending", "policy_cancelled"}:
|
2026-07-10 00:22:50 +08:00
|
|
|
|
self._update_record_from_task(existing, task)
|
2026-08-10 21:44:42 +08:00
|
|
|
|
if existing.get("outcome") == "failed" and existing.get("logCosUrl"):
|
|
|
|
|
|
enrichment_candidates.append(existing)
|
2026-07-10 00:22:50 +08:00
|
|
|
|
updated_count += 1
|
|
|
|
|
|
else:
|
|
|
|
|
|
status = str(task.get("status") or "").lower()
|
2026-08-12 08:41:07 +08:00
|
|
|
|
if status in TERMINAL_TASK_STATUSES:
|
2026-07-10 00:22:50 +08:00
|
|
|
|
record = self._create_record_from_task(task)
|
|
|
|
|
|
self._records.append(record)
|
|
|
|
|
|
self._by_task_id[task_id] = record
|
|
|
|
|
|
model_id = record["modelId"]
|
|
|
|
|
|
target_gpu = record["targetGpu"]
|
|
|
|
|
|
self._by_model_gpu[(model_id, target_gpu)].append(record)
|
|
|
|
|
|
updated_count += 1
|
|
|
|
|
|
|
2026-08-12 08:41:07 +08:00
|
|
|
|
# Retry a small bounded set of our own failed submissions. Newer logs
|
|
|
|
|
|
# contain the target image, so enrichment can recover the framework even
|
|
|
|
|
|
# when the history API omits it.
|
2026-08-10 21:44:42 +08:00
|
|
|
|
candidate_ids = {id(record) for record in enrichment_candidates}
|
|
|
|
|
|
for record in self._records:
|
|
|
|
|
|
if len(enrichment_candidates) >= FAILURE_ENRICHMENT_LIMIT:
|
|
|
|
|
|
break
|
|
|
|
|
|
if id(record) in candidate_ids:
|
|
|
|
|
|
continue
|
|
|
|
|
|
if (
|
|
|
|
|
|
record.get("outcome") == "failed"
|
|
|
|
|
|
and record.get("logCosUrl")
|
|
|
|
|
|
and not record.get("failureCategory")
|
|
|
|
|
|
and int(record.get("failureEnrichmentAttempts") or 0) < FAILURE_ENRICHMENT_MAX_ATTEMPTS
|
|
|
|
|
|
):
|
|
|
|
|
|
enrichment_candidates.append(record)
|
|
|
|
|
|
candidate_ids.add(id(record))
|
|
|
|
|
|
|
|
|
|
|
|
enrichment_attempts = self._enrich_failure_records(
|
|
|
|
|
|
enrichment_candidates[:FAILURE_ENRICHMENT_LIMIT]
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if updated_count or enrichment_attempts:
|
2026-07-10 00:22:50 +08:00
|
|
|
|
self._last_sync_time = end
|
|
|
|
|
|
self._rebuild_failed_index()
|
|
|
|
|
|
self.save()
|
|
|
|
|
|
|
|
|
|
|
|
return updated_count
|
|
|
|
|
|
|
2026-08-12 08:41:07 +08:00
|
|
|
|
def bootstrap_from_history_tasks(
|
|
|
|
|
|
self,
|
|
|
|
|
|
tasks: list[dict[str, Any]],
|
|
|
|
|
|
*,
|
|
|
|
|
|
task_contexts: dict[str, dict[str, Any]] | None = None,
|
|
|
|
|
|
enrichment_limit: int = 0,
|
|
|
|
|
|
enrichment_workers: int = 8,
|
|
|
|
|
|
log: Any = None,
|
|
|
|
|
|
) -> dict[str, int]:
|
|
|
|
|
|
"""Import terminal history and classify every usable historical failure."""
|
|
|
|
|
|
contexts = task_contexts if isinstance(task_contexts, dict) else {}
|
|
|
|
|
|
imported = 0
|
|
|
|
|
|
refreshed = 0
|
|
|
|
|
|
terminal_seen = 0
|
|
|
|
|
|
seen_task_ids: set[str] = set()
|
|
|
|
|
|
|
|
|
|
|
|
for raw_task in tasks:
|
|
|
|
|
|
if not isinstance(raw_task, dict):
|
|
|
|
|
|
continue
|
|
|
|
|
|
task_id_value = raw_task.get("taskId")
|
|
|
|
|
|
if task_id_value is None:
|
|
|
|
|
|
continue
|
|
|
|
|
|
task_id = str(task_id_value)
|
|
|
|
|
|
if not task_id or task_id in seen_task_ids:
|
|
|
|
|
|
continue
|
|
|
|
|
|
seen_task_ids.add(task_id)
|
|
|
|
|
|
status = str(raw_task.get("status") or "").strip().lower()
|
|
|
|
|
|
if status not in TERMINAL_TASK_STATUSES:
|
|
|
|
|
|
continue
|
|
|
|
|
|
terminal_seen += 1
|
|
|
|
|
|
|
|
|
|
|
|
task = self._enrich_history_task(raw_task, contexts.get(task_id))
|
|
|
|
|
|
existing = self._by_task_id.get(task_id)
|
|
|
|
|
|
if existing is None:
|
|
|
|
|
|
record = self._create_record_from_task(task)
|
|
|
|
|
|
self._records.append(record)
|
|
|
|
|
|
self._by_task_id[task_id] = record
|
|
|
|
|
|
self._by_model_gpu[(record["modelId"], record["targetGpu"])].append(record)
|
|
|
|
|
|
imported += 1
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
self._merge_record_metadata(existing, task)
|
|
|
|
|
|
if existing.get("outcome") in {"pending", "policy_cancelled"}:
|
|
|
|
|
|
self._update_record_from_task(existing, task)
|
|
|
|
|
|
refreshed += 1
|
|
|
|
|
|
elif task.get("logCosUrl") and not existing.get("failureCategory"):
|
|
|
|
|
|
existing["logCosUrl"] = task.get("logCosUrl")
|
|
|
|
|
|
|
|
|
|
|
|
candidates = [
|
|
|
|
|
|
record
|
|
|
|
|
|
for record in self._records
|
|
|
|
|
|
if (
|
|
|
|
|
|
record.get("outcome") == "failed"
|
|
|
|
|
|
and record.get("logCosUrl")
|
|
|
|
|
|
and not record.get("failureCategory")
|
|
|
|
|
|
and int(record.get("failureEnrichmentAttempts") or 0)
|
|
|
|
|
|
< FAILURE_ENRICHMENT_MAX_ATTEMPTS
|
|
|
|
|
|
)
|
|
|
|
|
|
]
|
|
|
|
|
|
candidates.sort(key=_outcome_record_timestamp, reverse=True)
|
|
|
|
|
|
if enrichment_limit > 0:
|
|
|
|
|
|
candidates = candidates[:enrichment_limit]
|
|
|
|
|
|
self._last_sync_time = utc_now()
|
|
|
|
|
|
self._rebuild_failed_index()
|
|
|
|
|
|
if imported or refreshed:
|
|
|
|
|
|
self.save()
|
|
|
|
|
|
|
|
|
|
|
|
candidate_task_ids = [
|
|
|
|
|
|
str(record["taskId"])
|
|
|
|
|
|
for record in candidates
|
|
|
|
|
|
if record.get("taskId") is not None
|
|
|
|
|
|
]
|
|
|
|
|
|
enrichment_attempts = 0
|
|
|
|
|
|
classified_total = 0
|
|
|
|
|
|
errors_total = 0
|
|
|
|
|
|
batch_size = 200
|
|
|
|
|
|
total_candidates = len(candidate_task_ids)
|
|
|
|
|
|
for offset in range(0, total_candidates, batch_size):
|
|
|
|
|
|
batch_ids = candidate_task_ids[offset : offset + batch_size]
|
|
|
|
|
|
batch_records = [
|
|
|
|
|
|
self._by_task_id[task_id]
|
|
|
|
|
|
for task_id in batch_ids
|
|
|
|
|
|
if task_id in self._by_task_id
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
def progress(
|
|
|
|
|
|
completed: int,
|
|
|
|
|
|
_total: int,
|
|
|
|
|
|
classified: int,
|
|
|
|
|
|
errors: int,
|
|
|
|
|
|
) -> None:
|
|
|
|
|
|
if log is None:
|
|
|
|
|
|
return
|
|
|
|
|
|
global_completed = offset + completed
|
|
|
|
|
|
if global_completed == total_candidates or global_completed % 50 == 0:
|
|
|
|
|
|
log(
|
|
|
|
|
|
f"[architecture-bootstrap] failure_logs={global_completed}/{total_candidates} "
|
|
|
|
|
|
f"classified={classified_total + classified} "
|
|
|
|
|
|
f"errors={errors_total + errors}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
enrichment_attempts += self._enrich_failure_records(
|
|
|
|
|
|
batch_records,
|
|
|
|
|
|
workers=max(1, int(enrichment_workers)),
|
|
|
|
|
|
progress=progress,
|
|
|
|
|
|
)
|
|
|
|
|
|
classified_total += sum(
|
|
|
|
|
|
1 for record in batch_records if record.get("failureCategory")
|
|
|
|
|
|
)
|
|
|
|
|
|
errors_total += sum(
|
|
|
|
|
|
1 for record in batch_records if record.get("failureEnrichmentError")
|
|
|
|
|
|
)
|
|
|
|
|
|
self._rebuild_failed_index()
|
|
|
|
|
|
self.save()
|
|
|
|
|
|
|
|
|
|
|
|
explicit_architecture_failures = sum(
|
|
|
|
|
|
1 for record in self._records if _is_explicit_architecture_failure(record)
|
|
|
|
|
|
)
|
|
|
|
|
|
recovered_frameworks = sum(
|
|
|
|
|
|
1
|
|
|
|
|
|
for record in self._records
|
|
|
|
|
|
if record.get("failureDetectedFramework") and record.get("framework")
|
|
|
|
|
|
)
|
|
|
|
|
|
return {
|
|
|
|
|
|
"recordsScanned": len(seen_task_ids),
|
|
|
|
|
|
"terminalRecords": terminal_seen,
|
|
|
|
|
|
"importedRecords": imported,
|
|
|
|
|
|
"refreshedRecords": refreshed,
|
|
|
|
|
|
"eligibleFailureLogs": total_candidates,
|
|
|
|
|
|
"enrichmentAttempts": enrichment_attempts,
|
|
|
|
|
|
"explicitArchitectureFailures": explicit_architecture_failures,
|
|
|
|
|
|
"recoveredFrameworks": recovered_frameworks,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _enrich_history_task(
|
|
|
|
|
|
task: dict[str, Any],
|
|
|
|
|
|
context: dict[str, Any] | None,
|
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
enriched = dict(task)
|
|
|
|
|
|
context = context if isinstance(context, dict) else {}
|
|
|
|
|
|
if not enriched.get("modelId") and context.get("modelId"):
|
|
|
|
|
|
enriched["modelId"] = context.get("modelId")
|
|
|
|
|
|
if not (enriched.get("gpuType") or enriched.get("targetGpu")) and context.get("targetGpu"):
|
|
|
|
|
|
enriched["targetGpu"] = context.get("targetGpu")
|
|
|
|
|
|
if not enriched.get("framework") and context.get("framework"):
|
|
|
|
|
|
enriched["framework"] = context.get("framework")
|
|
|
|
|
|
if not enriched.get("taskType"):
|
|
|
|
|
|
enriched["taskType"] = context.get("taskType") or task_type_from_history_task(enriched)
|
|
|
|
|
|
if context.get("modelProfile") and not enriched.get("modelProfile"):
|
|
|
|
|
|
enriched["modelProfile"] = context.get("modelProfile")
|
|
|
|
|
|
if context.get("submitTime") and not (
|
|
|
|
|
|
enriched.get("submitTime") or enriched.get("createTime")
|
|
|
|
|
|
):
|
|
|
|
|
|
enriched["submitTime"] = context.get("submitTime")
|
|
|
|
|
|
return enriched
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _merge_record_metadata(record: dict[str, Any], task: dict[str, Any]) -> None:
|
|
|
|
|
|
field_sources = {
|
|
|
|
|
|
"modelId": task.get("modelId") or task.get("model_id"),
|
|
|
|
|
|
"targetGpu": task.get("gpuType") or task.get("targetGpu"),
|
|
|
|
|
|
"framework": task.get("framework"),
|
|
|
|
|
|
"taskType": task.get("taskType") or task_type_from_history_task(task),
|
|
|
|
|
|
"modelProfile": task.get("modelProfile"),
|
|
|
|
|
|
}
|
|
|
|
|
|
for field, value in field_sources.items():
|
|
|
|
|
|
if not record.get(field) and value not in (None, "", "unknown"):
|
|
|
|
|
|
record[field] = dict(value) if field == "modelProfile" and isinstance(value, dict) else value
|
|
|
|
|
|
if task.get("logCosUrl") and not record.get("failureCategory"):
|
|
|
|
|
|
record["logCosUrl"] = task.get("logCosUrl")
|
|
|
|
|
|
|
|
|
|
|
|
def _enrich_failure_records(
|
|
|
|
|
|
self,
|
|
|
|
|
|
records: list[dict[str, Any]],
|
|
|
|
|
|
*,
|
|
|
|
|
|
workers: int = FAILURE_ENRICHMENT_WORKERS,
|
|
|
|
|
|
progress: Any = None,
|
|
|
|
|
|
) -> int:
|
2026-08-10 21:44:42 +08:00
|
|
|
|
if not records:
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
def inspect(record: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any] | None, str | None]:
|
|
|
|
|
|
try:
|
|
|
|
|
|
result = fetch_and_classify_failure_log(
|
|
|
|
|
|
str(record["logCosUrl"]),
|
|
|
|
|
|
task_context=record,
|
|
|
|
|
|
llm_classifier=self._failure_llm_classifier,
|
|
|
|
|
|
)
|
|
|
|
|
|
return record, result, None
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
return record, None, f"{type(exc).__name__}: {exc}"
|
|
|
|
|
|
|
|
|
|
|
|
attempted = 0
|
2026-08-12 08:41:07 +08:00
|
|
|
|
classified = 0
|
|
|
|
|
|
errors = 0
|
|
|
|
|
|
with ThreadPoolExecutor(max_workers=min(max(1, int(workers)), len(records))) as executor:
|
2026-08-10 21:44:42 +08:00
|
|
|
|
futures = [executor.submit(inspect, record) for record in records]
|
|
|
|
|
|
for future in as_completed(futures):
|
|
|
|
|
|
record, result, error = future.result()
|
|
|
|
|
|
attempted += 1
|
|
|
|
|
|
record["failureEnrichmentAttempts"] = int(record.get("failureEnrichmentAttempts") or 0) + 1
|
|
|
|
|
|
if result is None:
|
2026-08-12 08:41:07 +08:00
|
|
|
|
errors += 1
|
2026-08-10 21:44:42 +08:00
|
|
|
|
record["failureEnrichmentError"] = error
|
2026-08-12 08:41:07 +08:00
|
|
|
|
if progress is not None:
|
|
|
|
|
|
progress(attempted, len(records), classified, errors)
|
2026-08-10 21:44:42 +08:00
|
|
|
|
continue
|
|
|
|
|
|
record.update(result)
|
2026-08-12 08:41:07 +08:00
|
|
|
|
if not record.get("framework") and result.get("failureDetectedFramework"):
|
|
|
|
|
|
record["framework"] = result.get("failureDetectedFramework")
|
2026-08-10 21:44:42 +08:00
|
|
|
|
record["failReason"] = result.get("failureCategory") or record.get("failReason")
|
|
|
|
|
|
record["failureEnrichmentError"] = None
|
|
|
|
|
|
record.pop("logCosUrl", None)
|
2026-08-12 08:41:07 +08:00
|
|
|
|
classified += 1
|
|
|
|
|
|
if progress is not None:
|
|
|
|
|
|
progress(attempted, len(records), classified, errors)
|
2026-08-10 21:44:42 +08:00
|
|
|
|
return attempted
|
|
|
|
|
|
|
2026-08-02 17:45:21 +08:00
|
|
|
|
def is_model_gpu_failed(
|
|
|
|
|
|
self,
|
|
|
|
|
|
model_id: str,
|
|
|
|
|
|
target_gpu: str,
|
|
|
|
|
|
*,
|
|
|
|
|
|
cooldown_hours: int = 24,
|
|
|
|
|
|
now: datetime | None = None,
|
|
|
|
|
|
) -> bool:
|
|
|
|
|
|
failed_at = self._failed_model_gpus.get((model_id, target_gpu))
|
|
|
|
|
|
if failed_at is None:
|
|
|
|
|
|
return False
|
|
|
|
|
|
now = now or utc_now()
|
|
|
|
|
|
return failed_at >= now - timedelta(hours=max(0, int(cooldown_hours)))
|
2026-07-10 00:22:50 +08:00
|
|
|
|
|
|
|
|
|
|
def get_stats_report(self) -> dict[str, Any]:
|
2026-08-12 08:02:51 +08:00
|
|
|
|
now_datetime = utc_now()
|
|
|
|
|
|
now = now_datetime.isoformat()
|
2026-07-10 00:22:50 +08:00
|
|
|
|
terminal = [r for r in self._records if r.get("outcome") in {"success", "failed"}]
|
|
|
|
|
|
|
|
|
|
|
|
gpu_groups: dict[str, list[dict[str, Any]]] = defaultdict(list)
|
|
|
|
|
|
framework_groups: dict[str, list[dict[str, Any]]] = defaultdict(list)
|
|
|
|
|
|
combo_groups: dict[tuple[str, str, str], list[dict[str, Any]]] = defaultdict(list)
|
2026-08-10 21:44:42 +08:00
|
|
|
|
profile_groups: dict[tuple[str, str, str, str, str], list[dict[str, Any]]] = defaultdict(list)
|
2026-08-15 19:24:26 +08:00
|
|
|
|
sized_profile_groups: dict[tuple[str, str, str, str, str, int], list[dict[str, Any]]] = defaultdict(list)
|
2026-07-10 00:22:50 +08:00
|
|
|
|
|
|
|
|
|
|
for record in terminal:
|
|
|
|
|
|
gpu = record.get("targetGpu") or "unknown"
|
|
|
|
|
|
fw = record.get("framework") or "unknown"
|
|
|
|
|
|
tt = record.get("taskType") or "unknown"
|
|
|
|
|
|
gpu_groups[gpu].append(record)
|
|
|
|
|
|
framework_groups[f"{fw}"].append(record)
|
|
|
|
|
|
combo_groups[(gpu, fw, tt)].append(record)
|
2026-08-10 21:44:42 +08:00
|
|
|
|
profile = record.get("modelProfile") or {}
|
|
|
|
|
|
model_type = str(profile.get("modelType") or "").strip()
|
|
|
|
|
|
if model_type:
|
|
|
|
|
|
quantization = str(profile.get("quantizationMethod") or "none").strip()
|
|
|
|
|
|
profile_groups[(gpu, fw, tt, model_type, quantization)].append(record)
|
2026-08-15 19:24:26 +08:00
|
|
|
|
try:
|
|
|
|
|
|
load_bytes = max(1, int(profile.get("estimatedLoadBytes") or 0))
|
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
|
load_bytes = 0
|
|
|
|
|
|
if load_bytes > 0:
|
|
|
|
|
|
size_bucket = int(load_bytes).bit_length() - 1
|
|
|
|
|
|
sized_profile_groups[(gpu, fw, tt, model_type, quantization, size_bucket)].append(record)
|
2026-07-10 00:22:50 +08:00
|
|
|
|
|
|
|
|
|
|
gpu_summaries = {gpu: _summarize(records) for gpu, records in gpu_groups.items()}
|
|
|
|
|
|
framework_summaries = {fw: _summarize(records) for fw, records in framework_groups.items()}
|
|
|
|
|
|
combination_stats = {
|
|
|
|
|
|
f"{gpu}|{fw}|{tt}": {"targetGpu": gpu, "framework": fw, "taskType": tt, **_summarize(records)}
|
|
|
|
|
|
for (gpu, fw, tt), records in combo_groups.items()
|
|
|
|
|
|
}
|
2026-08-05 18:21:59 +08:00
|
|
|
|
recent_combination_stats: dict[str, dict[str, Any]] = {}
|
|
|
|
|
|
for (gpu, fw, tt), records in combo_groups.items():
|
|
|
|
|
|
recent = sorted(records, key=_outcome_record_timestamp, reverse=True)[:20]
|
2026-08-10 21:44:42 +08:00
|
|
|
|
consecutive_failures = _consecutive_attributable_failures(recent)
|
|
|
|
|
|
consecutive_platform_failures = _consecutive_platform_failures(recent)
|
2026-08-05 18:21:59 +08:00
|
|
|
|
last_terminal_at = None
|
|
|
|
|
|
if recent:
|
|
|
|
|
|
last_terminal_at = (
|
|
|
|
|
|
parse_datetime(recent[0].get("lastSyncTime"))
|
|
|
|
|
|
or parse_datetime(recent[0].get("submitTime"))
|
|
|
|
|
|
)
|
|
|
|
|
|
recent_combination_stats[f"{gpu}|{fw}|{tt}"] = {
|
|
|
|
|
|
"targetGpu": gpu,
|
|
|
|
|
|
"framework": fw,
|
|
|
|
|
|
"taskType": tt,
|
|
|
|
|
|
**_summarize(recent),
|
|
|
|
|
|
"consecutiveFailures": consecutive_failures,
|
2026-08-10 21:44:42 +08:00
|
|
|
|
"consecutivePlatformFailures": consecutive_platform_failures,
|
|
|
|
|
|
"lastPlatformFailureAt": _latest_platform_failure_at(recent),
|
|
|
|
|
|
"lastTerminalAt": last_terminal_at.isoformat() if last_terminal_at else None,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
profile_combination_stats: dict[str, dict[str, Any]] = {}
|
|
|
|
|
|
recent_profile_combination_stats: dict[str, dict[str, Any]] = {}
|
|
|
|
|
|
for (gpu, fw, tt, model_type, quantization), records in profile_groups.items():
|
|
|
|
|
|
key = f"{gpu}|{fw}|{tt}|{model_type}|{quantization}"
|
|
|
|
|
|
profile_combination_stats[key] = {
|
|
|
|
|
|
"targetGpu": gpu,
|
|
|
|
|
|
"framework": fw,
|
|
|
|
|
|
"taskType": tt,
|
|
|
|
|
|
"modelType": model_type,
|
|
|
|
|
|
"quantizationMethod": quantization,
|
|
|
|
|
|
**_summarize(records),
|
|
|
|
|
|
}
|
|
|
|
|
|
recent = sorted(records, key=_outcome_record_timestamp, reverse=True)[:20]
|
|
|
|
|
|
consecutive_failures = _consecutive_attributable_failures(recent)
|
|
|
|
|
|
last_terminal_at = None
|
|
|
|
|
|
if recent:
|
|
|
|
|
|
last_terminal_at = (
|
|
|
|
|
|
parse_datetime(recent[0].get("lastSyncTime"))
|
|
|
|
|
|
or parse_datetime(recent[0].get("submitTime"))
|
|
|
|
|
|
)
|
|
|
|
|
|
recent_profile_combination_stats[key] = {
|
|
|
|
|
|
**profile_combination_stats[key],
|
|
|
|
|
|
**_summarize(recent),
|
|
|
|
|
|
"consecutiveFailures": consecutive_failures,
|
2026-08-05 18:21:59 +08:00
|
|
|
|
"lastTerminalAt": last_terminal_at.isoformat() if last_terminal_at else None,
|
|
|
|
|
|
}
|
2026-07-10 00:22:50 +08:00
|
|
|
|
|
2026-08-15 19:24:26 +08:00
|
|
|
|
sized_profile_combination_stats: dict[str, dict[str, Any]] = {}
|
|
|
|
|
|
for (gpu, fw, tt, model_type, quantization, size_bucket), records in sized_profile_groups.items():
|
|
|
|
|
|
key = f"{gpu}|{fw}|{tt}|{model_type}|{quantization}|{size_bucket}"
|
|
|
|
|
|
sized_profile_combination_stats[key] = {
|
|
|
|
|
|
"targetGpu": gpu,
|
|
|
|
|
|
"framework": fw,
|
|
|
|
|
|
"taskType": tt,
|
|
|
|
|
|
"modelType": model_type,
|
|
|
|
|
|
"quantizationMethod": quantization,
|
|
|
|
|
|
"loadSizeLog2Bucket": size_bucket,
|
|
|
|
|
|
**_summarize(records),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-10 00:22:50 +08:00
|
|
|
|
warnings: list[str] = []
|
|
|
|
|
|
for gpu, summary in gpu_summaries.items():
|
2026-08-10 21:44:42 +08:00
|
|
|
|
if summary["decisionTotal"] >= 4 and summary["decisionFailureRate"] >= 0.5:
|
2026-07-10 00:22:50 +08:00
|
|
|
|
warnings.append(f"GPU {gpu} 本地统计失败率偏高(≥50%),建议重点关注。")
|
|
|
|
|
|
for key, stat in combination_stats.items():
|
2026-08-10 21:44:42 +08:00
|
|
|
|
if stat["decisionTotal"] >= 3 and stat["decisionFailureRate"] >= 0.6:
|
2026-07-10 00:22:50 +08:00
|
|
|
|
warnings.append(f"组合 {key} 近期失败集中,建议降低该 GPU+框架的提交优先级。")
|
|
|
|
|
|
|
|
|
|
|
|
pending_count = sum(1 for r in self._records if r.get("outcome") == "pending")
|
2026-08-12 01:04:34 +08:00
|
|
|
|
policy_cancelled_count = sum(
|
|
|
|
|
|
1 for record in self._records if record.get("outcome") == "policy_cancelled"
|
|
|
|
|
|
)
|
2026-08-10 21:44:42 +08:00
|
|
|
|
observed_gpu_memory: dict[str, float] = {}
|
|
|
|
|
|
for record in self._records:
|
|
|
|
|
|
gpu = str(record.get("targetGpu") or "")
|
|
|
|
|
|
try:
|
|
|
|
|
|
memory_gib = float(record.get("failureObservedGpuMemoryGiB"))
|
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
|
continue
|
|
|
|
|
|
if gpu and 0 < memory_gib <= 1024:
|
|
|
|
|
|
previous = observed_gpu_memory.get(gpu)
|
|
|
|
|
|
observed_gpu_memory[gpu] = min(previous, memory_gib) if previous else memory_gib
|
2026-07-10 00:22:50 +08:00
|
|
|
|
|
2026-08-12 08:02:51 +08:00
|
|
|
|
architecture_block_ttl_days = _architecture_block_ttl_days()
|
|
|
|
|
|
architecture_blocks = _build_architecture_compatibility_blocks(
|
|
|
|
|
|
terminal,
|
|
|
|
|
|
now=now_datetime,
|
|
|
|
|
|
ttl_days=architecture_block_ttl_days,
|
|
|
|
|
|
)
|
|
|
|
|
|
architecture_blocks_by_gpu_framework: dict[str, int] = defaultdict(int)
|
|
|
|
|
|
for block in architecture_blocks.values():
|
|
|
|
|
|
combination = f"{block['targetGpu']}|{block['framework']}"
|
|
|
|
|
|
architecture_blocks_by_gpu_framework[combination] += 1
|
|
|
|
|
|
|
2026-07-10 00:22:50 +08:00
|
|
|
|
return {
|
|
|
|
|
|
"generatedAt": now,
|
|
|
|
|
|
"totalRecords": len(self._records),
|
|
|
|
|
|
"pendingRecords": pending_count,
|
2026-08-12 01:04:34 +08:00
|
|
|
|
"policyCancelledRecords": policy_cancelled_count,
|
2026-07-10 00:22:50 +08:00
|
|
|
|
"terminalRecords": len(terminal),
|
|
|
|
|
|
"gpuSummaries": gpu_summaries,
|
|
|
|
|
|
"frameworkSummaries": framework_summaries,
|
|
|
|
|
|
"combinationStats": combination_stats,
|
2026-08-05 18:21:59 +08:00
|
|
|
|
"recentCombinationStats": recent_combination_stats,
|
2026-08-10 21:44:42 +08:00
|
|
|
|
"profileCombinationStats": profile_combination_stats,
|
|
|
|
|
|
"recentProfileCombinationStats": recent_profile_combination_stats,
|
2026-08-15 19:24:26 +08:00
|
|
|
|
"sizedProfileCombinationStats": sized_profile_combination_stats,
|
2026-08-12 08:02:51 +08:00
|
|
|
|
"architectureCompatibilityBlocks": architecture_blocks,
|
|
|
|
|
|
"architectureCompatibilitySummary": {
|
|
|
|
|
|
"activeBlockCount": len(architecture_blocks),
|
|
|
|
|
|
"ttlDays": architecture_block_ttl_days,
|
|
|
|
|
|
"byGpuFramework": dict(architecture_blocks_by_gpu_framework),
|
|
|
|
|
|
},
|
2026-08-10 21:44:42 +08:00
|
|
|
|
"observedGpuMemoryGiB": observed_gpu_memory,
|
2026-07-10 00:22:50 +08:00
|
|
|
|
"totals": _summarize(terminal),
|
|
|
|
|
|
"warnings": warnings,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def save(self) -> None:
|
2026-08-01 16:50:50 +08:00
|
|
|
|
local_records = list(self._records)
|
|
|
|
|
|
|
|
|
|
|
|
def merge(existing: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
|
|
|
|
merged = list(existing)
|
|
|
|
|
|
index_by_key = {_outcome_record_key(record): index for index, record in enumerate(merged)}
|
|
|
|
|
|
for record in local_records:
|
|
|
|
|
|
key = _outcome_record_key(record)
|
|
|
|
|
|
existing_index = index_by_key.get(key)
|
|
|
|
|
|
if existing_index is None:
|
|
|
|
|
|
index_by_key[key] = len(merged)
|
|
|
|
|
|
merged.append(record)
|
|
|
|
|
|
continue
|
|
|
|
|
|
merged[existing_index] = _prefer_newer_outcome(merged[existing_index], record)
|
|
|
|
|
|
return merged
|
|
|
|
|
|
|
|
|
|
|
|
self._records = update_jsonl(self.path, merge)
|
|
|
|
|
|
self._rebuild_indexes()
|
2026-07-10 00:22:50 +08:00
|
|
|
|
|
|
|
|
|
|
def _rebuild_failed_index(self) -> None:
|
|
|
|
|
|
self._failed_model_gpus.clear()
|
2026-08-02 17:45:21 +08:00
|
|
|
|
latest_by_combo: dict[tuple[str, str], tuple[datetime, dict[str, Any]]] = {}
|
2026-07-10 00:22:50 +08:00
|
|
|
|
for record in self._records:
|
2026-08-12 10:18:09 +08:00
|
|
|
|
# Platform, unresolved, and policy outcomes neither clear nor
|
|
|
|
|
|
# create a model/GPU cooldown. The full-history audit showed that
|
|
|
|
|
|
# more than half of failures lack enough evidence for attribution.
|
|
|
|
|
|
if _is_policy_cancelled(record) or (
|
|
|
|
|
|
record.get("outcome") == "failed"
|
|
|
|
|
|
and not _is_attributable_failure(record)
|
|
|
|
|
|
):
|
2026-08-10 21:44:42 +08:00
|
|
|
|
continue
|
2026-08-02 17:45:21 +08:00
|
|
|
|
model_id = record.get("modelId") or ""
|
|
|
|
|
|
target_gpu = record.get("targetGpu") or ""
|
2026-08-12 08:41:07 +08:00
|
|
|
|
event_time = parse_datetime(record.get("submitTime")) or parse_datetime(record.get("lastSyncTime"))
|
2026-08-02 17:45:21 +08:00
|
|
|
|
if not model_id or not target_gpu or event_time is None:
|
|
|
|
|
|
continue
|
|
|
|
|
|
key = (model_id, target_gpu)
|
|
|
|
|
|
current = latest_by_combo.get(key)
|
|
|
|
|
|
if current is None or event_time >= current[0]:
|
|
|
|
|
|
latest_by_combo[key] = (event_time, record)
|
|
|
|
|
|
|
|
|
|
|
|
for key, (event_time, record) in latest_by_combo.items():
|
2026-07-10 00:22:50 +08:00
|
|
|
|
if record.get("outcome") == "failed":
|
2026-08-02 17:45:21 +08:00
|
|
|
|
self._failed_model_gpus[key] = event_time
|
2026-07-10 00:22:50 +08:00
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _update_record_from_task(record: dict[str, Any], task: dict[str, Any]) -> None:
|
|
|
|
|
|
record["status"] = task.get("status")
|
|
|
|
|
|
record["verifyResult"] = task.get("verifyResult")
|
|
|
|
|
|
record["lastSyncTime"] = _now_iso()
|
2026-08-10 21:44:42 +08:00
|
|
|
|
if task.get("logCosUrl"):
|
|
|
|
|
|
record["logCosUrl"] = task.get("logCosUrl")
|
2026-08-12 01:04:34 +08:00
|
|
|
|
status = str(task.get("status") or "").strip().lower()
|
2026-07-10 00:22:50 +08:00
|
|
|
|
if is_success(task):
|
|
|
|
|
|
record["outcome"] = "success"
|
|
|
|
|
|
record["failReason"] = None
|
2026-08-12 01:04:34 +08:00
|
|
|
|
if record.get("policyCancelled"):
|
|
|
|
|
|
record["policyCancellationResolvedAsSuccess"] = True
|
|
|
|
|
|
record["policyCancelled"] = False
|
|
|
|
|
|
elif record.get("policyCancelled"):
|
|
|
|
|
|
# A successful stop may be observed as waiting/running briefly before
|
|
|
|
|
|
# the platform publishes its terminal cancellation state. None of
|
|
|
|
|
|
# those intermediate states should become failure evidence.
|
|
|
|
|
|
record["outcome"] = "policy_cancelled"
|
|
|
|
|
|
record["failReason"] = None
|
2026-07-10 00:22:50 +08:00
|
|
|
|
elif is_failure(task):
|
|
|
|
|
|
record["outcome"] = "failed"
|
|
|
|
|
|
record["failReason"] = classify_failure(task)
|
|
|
|
|
|
else:
|
|
|
|
|
|
record["outcome"] = "pending"
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _create_record_from_task(task: dict[str, Any]) -> dict[str, Any]:
|
2026-08-12 08:41:07 +08:00
|
|
|
|
create_time = (
|
|
|
|
|
|
parse_datetime(task.get("submitTime"))
|
|
|
|
|
|
or parse_datetime(task.get("createTime"))
|
|
|
|
|
|
or parse_datetime(task.get("updateTime"))
|
|
|
|
|
|
)
|
|
|
|
|
|
model_profile = task.get("modelProfile")
|
2026-07-10 00:22:50 +08:00
|
|
|
|
record: dict[str, Any] = {
|
|
|
|
|
|
"modelId": task.get("modelId") or task.get("model_id") or "",
|
|
|
|
|
|
"targetGpu": task.get("gpuType") or task.get("targetGpu") or "",
|
|
|
|
|
|
"framework": task.get("framework") or "",
|
2026-08-12 08:41:07 +08:00
|
|
|
|
"taskType": task.get("taskType") or task_type_from_history_task(task) or "",
|
2026-07-10 00:22:50 +08:00
|
|
|
|
"taskId": str(task.get("taskId")) if task.get("taskId") is not None else None,
|
|
|
|
|
|
"submitTime": create_time.isoformat() if create_time else _now_iso(),
|
|
|
|
|
|
"lastSyncTime": _now_iso(),
|
|
|
|
|
|
"status": task.get("status"),
|
|
|
|
|
|
"verifyResult": task.get("verifyResult"),
|
|
|
|
|
|
"outcome": "pending",
|
|
|
|
|
|
"failReason": None,
|
2026-08-10 21:44:42 +08:00
|
|
|
|
"logCosUrl": task.get("logCosUrl"),
|
2026-08-12 08:41:07 +08:00
|
|
|
|
"modelProfile": dict(model_profile) if isinstance(model_profile, dict) else {},
|
2026-07-10 00:22:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
if is_success(task):
|
|
|
|
|
|
record["outcome"] = "success"
|
|
|
|
|
|
elif is_failure(task):
|
|
|
|
|
|
record["outcome"] = "failed"
|
|
|
|
|
|
record["failReason"] = classify_failure(task)
|
|
|
|
|
|
return record
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _summarize(records: list[dict[str, Any]]) -> dict[str, Any]:
|
|
|
|
|
|
total = len(records)
|
|
|
|
|
|
success_count = sum(1 for r in records if r.get("outcome") == "success")
|
|
|
|
|
|
failure_count = sum(1 for r in records if r.get("outcome") == "failed")
|
|
|
|
|
|
pending_count = sum(1 for r in records if r.get("outcome") == "pending")
|
2026-08-10 21:44:42 +08:00
|
|
|
|
attributable_failure_count = sum(
|
2026-08-12 10:18:09 +08:00
|
|
|
|
1 for record in records if _is_attributable_failure(record)
|
|
|
|
|
|
)
|
|
|
|
|
|
platform_failure_count = sum(1 for record in records if _is_platform_failure(record))
|
|
|
|
|
|
unresolved_failure_count = max(
|
|
|
|
|
|
0,
|
|
|
|
|
|
failure_count - attributable_failure_count - platform_failure_count,
|
2026-08-10 21:44:42 +08:00
|
|
|
|
)
|
|
|
|
|
|
decision_total = success_count + attributable_failure_count
|
2026-07-10 00:22:50 +08:00
|
|
|
|
|
|
|
|
|
|
failure_breakdown: dict[str, int] = defaultdict(int)
|
|
|
|
|
|
for r in records:
|
2026-08-10 21:44:42 +08:00
|
|
|
|
reason = r.get("failureCategory") or r.get("failReason")
|
2026-07-10 00:22:50 +08:00
|
|
|
|
if reason:
|
|
|
|
|
|
failure_breakdown[reason] += 1
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
"total": total,
|
|
|
|
|
|
"successCount": success_count,
|
|
|
|
|
|
"failureCount": failure_count,
|
2026-08-10 21:44:42 +08:00
|
|
|
|
"attributableFailureCount": attributable_failure_count,
|
|
|
|
|
|
"platformFailureCount": platform_failure_count,
|
2026-08-12 10:18:09 +08:00
|
|
|
|
"unresolvedFailureCount": unresolved_failure_count,
|
2026-08-10 21:44:42 +08:00
|
|
|
|
"decisionTotal": decision_total,
|
2026-07-10 00:22:50 +08:00
|
|
|
|
"pendingCount": pending_count,
|
|
|
|
|
|
"successRate": round(success_count / total, 4) if total > 0 else 0.0,
|
|
|
|
|
|
"failureRate": round(failure_count / total, 4) if total > 0 else 0.0,
|
2026-08-10 21:44:42 +08:00
|
|
|
|
"decisionSuccessRate": round(success_count / decision_total, 4) if decision_total > 0 else 0.0,
|
|
|
|
|
|
"decisionFailureRate": round(attributable_failure_count / decision_total, 4) if decision_total > 0 else 0.0,
|
2026-07-10 00:22:50 +08:00
|
|
|
|
"pendingRate": round(pending_count / total, 4) if total > 0 else 0.0,
|
|
|
|
|
|
"failureBreakdown": dict(failure_breakdown),
|
|
|
|
|
|
}
|
2026-08-01 16:50:50 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-08-12 08:02:51 +08:00
|
|
|
|
def _architecture_block_ttl_days() -> int:
|
|
|
|
|
|
raw = os.getenv("MODELHUB_ARCHITECTURE_BLOCK_TTL_DAYS")
|
|
|
|
|
|
if raw is None:
|
|
|
|
|
|
return DEFAULT_ARCHITECTURE_BLOCK_TTL_DAYS
|
|
|
|
|
|
try:
|
|
|
|
|
|
value = int(raw)
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
return DEFAULT_ARCHITECTURE_BLOCK_TTL_DAYS
|
|
|
|
|
|
return min(365, max(1, value))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _build_architecture_compatibility_blocks(
|
|
|
|
|
|
records: list[dict[str, Any]],
|
|
|
|
|
|
*,
|
|
|
|
|
|
now: datetime,
|
|
|
|
|
|
ttl_days: int,
|
|
|
|
|
|
) -> dict[str, dict[str, Any]]:
|
|
|
|
|
|
successes: dict[str, list[tuple[datetime, dict[str, Any]]]] = defaultdict(list)
|
|
|
|
|
|
failures: dict[str, list[tuple[datetime, dict[str, Any], dict[str, Any]]]] = defaultdict(list)
|
|
|
|
|
|
cutoff = now - timedelta(days=max(1, int(ttl_days)))
|
|
|
|
|
|
|
|
|
|
|
|
for record in records:
|
|
|
|
|
|
target_gpu = str(record.get("targetGpu") or "").strip()
|
|
|
|
|
|
framework = str(record.get("framework") or "").strip()
|
|
|
|
|
|
task_type = str(record.get("taskType") or "").strip()
|
|
|
|
|
|
event_time = (
|
|
|
|
|
|
parse_datetime(record.get("submitTime"))
|
|
|
|
|
|
or parse_datetime(record.get("lastSyncTime"))
|
|
|
|
|
|
)
|
2026-08-12 08:19:53 +08:00
|
|
|
|
if not target_gpu or not framework or not task_type or event_time is None:
|
2026-08-12 08:02:51 +08:00
|
|
|
|
continue
|
|
|
|
|
|
if record.get("outcome") == "success":
|
2026-08-12 08:19:53 +08:00
|
|
|
|
for profile in _stored_architecture_profiles(record, include_model_type=True):
|
|
|
|
|
|
key = architecture_compatibility_key(
|
|
|
|
|
|
target_gpu,
|
|
|
|
|
|
framework,
|
|
|
|
|
|
task_type,
|
|
|
|
|
|
profile["signature"],
|
|
|
|
|
|
)
|
|
|
|
|
|
if key is not None:
|
|
|
|
|
|
successes[key].append((event_time, record))
|
2026-08-12 08:02:51 +08:00
|
|
|
|
continue
|
|
|
|
|
|
if not _is_explicit_architecture_failure(record) or event_time < cutoff:
|
|
|
|
|
|
continue
|
2026-08-12 08:19:53 +08:00
|
|
|
|
for profile in _failure_architecture_profiles(record):
|
|
|
|
|
|
key = architecture_compatibility_key(
|
|
|
|
|
|
target_gpu,
|
|
|
|
|
|
framework,
|
|
|
|
|
|
task_type,
|
|
|
|
|
|
profile["signature"],
|
|
|
|
|
|
)
|
|
|
|
|
|
if key is not None:
|
|
|
|
|
|
failures[key].append((event_time, record, profile))
|
2026-08-12 08:02:51 +08:00
|
|
|
|
|
|
|
|
|
|
blocks: dict[str, dict[str, Any]] = {}
|
|
|
|
|
|
for key, failure_events in failures.items():
|
|
|
|
|
|
failure_events.sort(key=lambda item: item[0])
|
|
|
|
|
|
latest_failure_at, latest_failure, latest_profile = failure_events[-1]
|
|
|
|
|
|
success_events = successes.get(key) or []
|
|
|
|
|
|
latest_success_at = max((item[0] for item in success_events), default=None)
|
|
|
|
|
|
if latest_success_at is not None and latest_success_at >= latest_failure_at:
|
|
|
|
|
|
continue
|
|
|
|
|
|
effective_failures = [
|
|
|
|
|
|
item
|
|
|
|
|
|
for item in failure_events
|
|
|
|
|
|
if latest_success_at is None or item[0] > latest_success_at
|
|
|
|
|
|
]
|
|
|
|
|
|
expires_at = latest_failure_at + timedelta(days=max(1, int(ttl_days)))
|
|
|
|
|
|
if expires_at <= now:
|
|
|
|
|
|
continue
|
|
|
|
|
|
blocks[key] = {
|
|
|
|
|
|
"targetGpu": str(latest_failure.get("targetGpu") or ""),
|
|
|
|
|
|
"framework": str(latest_failure.get("framework") or ""),
|
|
|
|
|
|
"taskType": str(latest_failure.get("taskType") or ""),
|
|
|
|
|
|
"matchType": latest_profile["matchType"],
|
|
|
|
|
|
"architectureSignature": latest_profile["signature"],
|
|
|
|
|
|
"architectures": latest_profile["architectures"],
|
|
|
|
|
|
"modelType": latest_profile["modelType"],
|
|
|
|
|
|
"evidenceCount": len(effective_failures),
|
|
|
|
|
|
"latestFailureAt": latest_failure_at.isoformat(),
|
|
|
|
|
|
"latestSuccessfulAt": latest_success_at.isoformat() if latest_success_at else None,
|
|
|
|
|
|
"expiresAt": expires_at.isoformat(),
|
|
|
|
|
|
"sourceTaskIds": _bounded_unique(
|
|
|
|
|
|
record.get("taskId") for _, record, _ in reversed(effective_failures)
|
|
|
|
|
|
),
|
|
|
|
|
|
"sourceModelIds": _bounded_unique(
|
|
|
|
|
|
record.get("modelId") for _, record, _ in reversed(effective_failures)
|
|
|
|
|
|
),
|
|
|
|
|
|
}
|
|
|
|
|
|
return blocks
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-12 08:19:53 +08:00
|
|
|
|
def _stored_architecture_profiles(
|
|
|
|
|
|
record: dict[str, Any],
|
|
|
|
|
|
*,
|
|
|
|
|
|
include_model_type: bool,
|
|
|
|
|
|
) -> list[dict[str, Any]]:
|
|
|
|
|
|
profile_data = record.get("modelProfile")
|
|
|
|
|
|
if not isinstance(profile_data, dict):
|
|
|
|
|
|
return []
|
|
|
|
|
|
if include_model_type:
|
|
|
|
|
|
return architecture_profiles(
|
|
|
|
|
|
profile_data.get("modelType"),
|
|
|
|
|
|
profile_data.get("architectures"),
|
|
|
|
|
|
)
|
|
|
|
|
|
profile = architecture_profile(
|
|
|
|
|
|
profile_data.get("modelType"),
|
|
|
|
|
|
profile_data.get("architectures"),
|
|
|
|
|
|
)
|
|
|
|
|
|
return [profile] if profile is not None else []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _failure_architecture_profiles(record: dict[str, Any]) -> list[dict[str, Any]]:
|
|
|
|
|
|
profiles: list[dict[str, Any]] = []
|
|
|
|
|
|
unsupported_architectures = record.get("failureUnsupportedArchitectures")
|
|
|
|
|
|
if isinstance(unsupported_architectures, list) and unsupported_architectures:
|
|
|
|
|
|
profile = architecture_profile(None, unsupported_architectures)
|
|
|
|
|
|
if profile is not None:
|
|
|
|
|
|
profiles.append(profile)
|
|
|
|
|
|
unsupported_model_types = record.get("failureUnsupportedModelTypes")
|
|
|
|
|
|
if isinstance(unsupported_model_types, list):
|
|
|
|
|
|
for model_type in unsupported_model_types:
|
|
|
|
|
|
profile = architecture_profile(model_type, None)
|
|
|
|
|
|
if profile is not None:
|
|
|
|
|
|
profiles.append(profile)
|
|
|
|
|
|
if not profiles:
|
|
|
|
|
|
profiles.extend(_stored_architecture_profiles(record, include_model_type=False))
|
|
|
|
|
|
deduped: dict[str, dict[str, Any]] = {}
|
|
|
|
|
|
for profile in profiles:
|
|
|
|
|
|
deduped[profile["signature"]] = profile
|
|
|
|
|
|
return list(deduped.values())
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-12 08:02:51 +08:00
|
|
|
|
def _is_explicit_architecture_failure(record: dict[str, Any]) -> bool:
|
|
|
|
|
|
return bool(
|
|
|
|
|
|
record.get("outcome") == "failed"
|
|
|
|
|
|
and record.get("failureCategory") == EXPLICIT_ARCHITECTURE_FAILURE_CATEGORY
|
|
|
|
|
|
and record.get("failureDeterministic") is True
|
|
|
|
|
|
and record.get("failureClassificationReason") == EXPLICIT_ARCHITECTURE_FAILURE_REASON
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _bounded_unique(values: Any, limit: int = 5) -> list[str]:
|
|
|
|
|
|
result: list[str] = []
|
|
|
|
|
|
for value in values:
|
|
|
|
|
|
rendered = str(value or "").strip()
|
|
|
|
|
|
if not rendered or rendered in result:
|
|
|
|
|
|
continue
|
|
|
|
|
|
result.append(rendered)
|
|
|
|
|
|
if len(result) >= limit:
|
|
|
|
|
|
break
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-10 21:44:42 +08:00
|
|
|
|
def _is_platform_failure(record: dict[str, Any]) -> bool:
|
|
|
|
|
|
if record.get("outcome") != "failed":
|
|
|
|
|
|
return False
|
|
|
|
|
|
category = str(record.get("failureCategory") or "").lower()
|
|
|
|
|
|
scope = str(record.get("failureScope") or "").lower()
|
|
|
|
|
|
return scope == "platform" or category.startswith("platform_")
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-12 10:18:09 +08:00
|
|
|
|
def _is_attributable_failure(record: dict[str, Any]) -> bool:
|
|
|
|
|
|
"""Require classified, non-platform evidence before penalizing a strategy."""
|
|
|
|
|
|
if record.get("outcome") != "failed" or _is_platform_failure(record):
|
|
|
|
|
|
return False
|
|
|
|
|
|
category = str(record.get("failureCategory") or "").strip().lower()
|
|
|
|
|
|
scope = str(record.get("failureScope") or "").strip().lower()
|
|
|
|
|
|
return bool(category and scope not in {"", "unknown", "platform"})
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-12 01:04:34 +08:00
|
|
|
|
def _is_policy_cancelled(record: dict[str, Any]) -> bool:
|
|
|
|
|
|
return bool(record.get("policyCancelled")) or record.get("outcome") == "policy_cancelled"
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-10 21:44:42 +08:00
|
|
|
|
def _consecutive_attributable_failures(records: list[dict[str, Any]]) -> int:
|
|
|
|
|
|
count = 0
|
|
|
|
|
|
for record in records:
|
|
|
|
|
|
if record.get("outcome") == "success":
|
|
|
|
|
|
break
|
2026-08-12 10:18:09 +08:00
|
|
|
|
if _is_attributable_failure(record):
|
2026-08-10 21:44:42 +08:00
|
|
|
|
count += 1
|
|
|
|
|
|
return count
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _consecutive_platform_failures(records: list[dict[str, Any]]) -> int:
|
|
|
|
|
|
count = 0
|
|
|
|
|
|
for record in records:
|
|
|
|
|
|
if not _is_platform_failure(record):
|
|
|
|
|
|
break
|
|
|
|
|
|
count += 1
|
|
|
|
|
|
return count
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _latest_platform_failure_at(records: list[dict[str, Any]]) -> str | None:
|
|
|
|
|
|
for record in records:
|
|
|
|
|
|
if not _is_platform_failure(record):
|
|
|
|
|
|
continue
|
2026-08-12 08:41:07 +08:00
|
|
|
|
timestamp = parse_datetime(record.get("submitTime")) or parse_datetime(record.get("lastSyncTime"))
|
2026-08-10 21:44:42 +08:00
|
|
|
|
return timestamp.isoformat() if timestamp else None
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-05 18:21:59 +08:00
|
|
|
|
def _outcome_record_timestamp(record: dict[str, Any]) -> float:
|
2026-08-12 08:41:07 +08:00
|
|
|
|
timestamp = parse_datetime(record.get("submitTime")) or parse_datetime(record.get("lastSyncTime"))
|
2026-08-05 18:21:59 +08:00
|
|
|
|
return timestamp.timestamp() if timestamp else 0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-01 16:50:50 +08:00
|
|
|
|
def _outcome_record_key(record: dict[str, Any]) -> str:
|
|
|
|
|
|
task_id = record.get("taskId")
|
|
|
|
|
|
if task_id is not None:
|
|
|
|
|
|
return f"task:{task_id}"
|
|
|
|
|
|
return "fallback:{model}|{gpu}|{time}".format(
|
|
|
|
|
|
model=record.get("modelId") or "",
|
|
|
|
|
|
gpu=record.get("targetGpu") or "",
|
|
|
|
|
|
time=record.get("submitTime") or "",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _outcome_version(record: dict[str, Any]) -> tuple[int, float, float]:
|
|
|
|
|
|
last_sync = parse_datetime(record.get("lastSyncTime"))
|
|
|
|
|
|
submit_time = parse_datetime(record.get("submitTime"))
|
2026-08-12 01:04:34 +08:00
|
|
|
|
outcome_rank = 1 if record.get("outcome") in {"success", "failed", "policy_cancelled"} else 0
|
2026-08-01 16:50:50 +08:00
|
|
|
|
return (
|
|
|
|
|
|
outcome_rank,
|
|
|
|
|
|
last_sync.timestamp() if last_sync else 0.0,
|
|
|
|
|
|
submit_time.timestamp() if submit_time else 0.0,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _prefer_newer_outcome(existing: dict[str, Any], candidate: dict[str, Any]) -> dict[str, Any]:
|
|
|
|
|
|
if _outcome_version(candidate) >= _outcome_version(existing):
|
|
|
|
|
|
return candidate
|
|
|
|
|
|
return existing
|