2026-08-10 21:44:42 +08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import io
|
|
|
|
|
import json
|
|
|
|
|
import re
|
|
|
|
|
import urllib.request
|
|
|
|
|
import zipfile
|
|
|
|
|
from typing import Any
|
|
|
|
|
from urllib.parse import urlparse
|
|
|
|
|
|
|
|
|
|
from failure_taxonomy import classify_failure_report
|
|
|
|
|
from llm_classifier import LLMAssistedClassifier
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
MAX_LOG_ARCHIVE_BYTES = 20_000_000
|
|
|
|
|
MAX_RUNTIME_LOG_BYTES = 20_000_000
|
|
|
|
|
MAX_ERROR_REPORT_BYTES = 1_000_000
|
|
|
|
|
ERROR_LINE_PATTERN = re.compile(
|
|
|
|
|
r"(?:\b(?:[A-Za-z_]*(?:Error|Exception)|PREFLIGHT_[A-Z_]+|OOM)\b|"
|
|
|
|
|
r"out of memory|not supported|unsupported|does not recognize|cannot|can.t|"
|
2026-08-12 08:02:51 +08:00
|
|
|
r"not found|no such file|failed to|invalid|traceback|"
|
|
|
|
|
r"找不到空闲卡|不支持|暂不支持|不兼容|请换用|请更换)",
|
2026-08-10 21:44:42 +08:00
|
|
|
re.IGNORECASE,
|
|
|
|
|
)
|
2026-08-12 08:19:53 +08:00
|
|
|
MODEL_TYPE_NOT_RECOGNIZED_PATTERN = re.compile(
|
|
|
|
|
r"model\s+type\s+[`'\"](?P<model_type>[A-Za-z0-9_.-]+)[`'\"]\s+but\s+"
|
|
|
|
|
r"(?:Transformers\s+)?does\s+not\s+recognize\s+this\s+architecture",
|
|
|
|
|
re.IGNORECASE,
|
|
|
|
|
)
|
|
|
|
|
MODEL_ARCHITECTURES_NOT_SUPPORTED_PATTERN = re.compile(
|
|
|
|
|
r"Model\s+architectures?\s*(?P<architectures>\[[^\]\n]{1,500}\])\s+"
|
|
|
|
|
r"(?:are|is)\s+not\s+supported\s+for\s+now",
|
|
|
|
|
re.IGNORECASE,
|
|
|
|
|
)
|
2026-08-12 08:41:07 +08:00
|
|
|
SUBMITTED_FRAMEWORK_PATTERN = re.compile(
|
|
|
|
|
r"\[submit\]\s*framework\s*:\s*(?P<framework>[A-Za-z0-9_.-]{1,80})",
|
|
|
|
|
re.IGNORECASE,
|
|
|
|
|
)
|
|
|
|
|
TARGET_DOCKER_IMAGE_PATTERN = re.compile(
|
|
|
|
|
r"\[submit\]\s*docker_image\s*:\s*(?P<image>\S{1,500})",
|
|
|
|
|
re.IGNORECASE,
|
|
|
|
|
)
|
|
|
|
|
FRAMEWORK_IMAGE_MARKERS = (
|
|
|
|
|
("vllm-customized", ("vllm-customized", "vllm_customized")),
|
|
|
|
|
("vllm_fix_tokenizer", ("vllm-fix-tokenizer", "vllm_fix_tokenizer")),
|
|
|
|
|
("sentence-transformers", ("sentence-transformers", "sentence_transformers")),
|
|
|
|
|
("sherpa-onnx", ("sherpa-onnx", "sherpa_onnx")),
|
|
|
|
|
("llamacpp", ("llamacpp", "llama-cpp", "llama.cpp")),
|
|
|
|
|
("vllm-mlu", ("vllm-mlu", "vllm_mlu")),
|
|
|
|
|
("vllm-016", ("vllm-016", "vllm_016")),
|
|
|
|
|
("diffusers", ("diffusers",)),
|
|
|
|
|
("transformers", ("transformers",)),
|
|
|
|
|
("sglang", ("sglang",)),
|
|
|
|
|
("funasr", ("funasr",)),
|
|
|
|
|
("vllm", ("vllm",)),
|
|
|
|
|
)
|
2026-08-10 21:44:42 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def fetch_and_classify_failure_log(
|
|
|
|
|
log_url: str,
|
|
|
|
|
*,
|
|
|
|
|
task_context: dict[str, Any] | None = None,
|
|
|
|
|
llm_classifier: LLMAssistedClassifier | None = None,
|
|
|
|
|
timeout_seconds: int = 20,
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
parsed_url = urlparse(log_url)
|
|
|
|
|
if parsed_url.scheme not in {"http", "https"} or not parsed_url.netloc:
|
|
|
|
|
raise ValueError("failure log URL must be HTTP(S)")
|
|
|
|
|
request = urllib.request.Request(
|
|
|
|
|
log_url,
|
|
|
|
|
headers={"User-Agent": "modelhub-submmit-failure-inspector/1"},
|
|
|
|
|
)
|
|
|
|
|
with urllib.request.urlopen(request, timeout=max(1, int(timeout_seconds))) as response:
|
|
|
|
|
archive = response.read(MAX_LOG_ARCHIVE_BYTES + 1)
|
|
|
|
|
if len(archive) > MAX_LOG_ARCHIVE_BYTES:
|
|
|
|
|
raise ValueError("failure log archive exceeds size limit")
|
|
|
|
|
return classify_failure_archive(
|
|
|
|
|
archive,
|
|
|
|
|
task_context=task_context,
|
|
|
|
|
llm_classifier=llm_classifier,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def classify_failure_archive(
|
|
|
|
|
archive: bytes,
|
|
|
|
|
*,
|
|
|
|
|
task_context: dict[str, Any] | None = None,
|
|
|
|
|
llm_classifier: LLMAssistedClassifier | None = None,
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
report: dict[str, Any] = {}
|
|
|
|
|
runtime_log = ""
|
|
|
|
|
with zipfile.ZipFile(io.BytesIO(archive)) as bundle:
|
|
|
|
|
names = set(bundle.namelist())
|
|
|
|
|
if "error_report_raw.json" in names:
|
|
|
|
|
info = bundle.getinfo("error_report_raw.json")
|
|
|
|
|
if info.file_size <= MAX_ERROR_REPORT_BYTES:
|
|
|
|
|
try:
|
|
|
|
|
parsed = json.loads(bundle.read("error_report_raw.json"))
|
|
|
|
|
if isinstance(parsed, dict):
|
|
|
|
|
report = parsed
|
|
|
|
|
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
|
|
|
report = {}
|
|
|
|
|
if "pod_runtime_log.txt" in names:
|
|
|
|
|
info = bundle.getinfo("pod_runtime_log.txt")
|
|
|
|
|
if info.file_size <= MAX_RUNTIME_LOG_BYTES:
|
|
|
|
|
runtime_log = bundle.read("pod_runtime_log.txt").decode("utf-8", "replace")
|
|
|
|
|
|
|
|
|
|
error_lines = _extract_error_lines(runtime_log)
|
|
|
|
|
report_code = str(report.get("code") or "").strip() or None
|
2026-08-12 08:02:51 +08:00
|
|
|
suggestion = str(report.get("suggestion") or "")[:500] or None
|
|
|
|
|
classification_inputs = [*error_lines]
|
|
|
|
|
if suggestion:
|
|
|
|
|
classification_inputs.append(suggestion)
|
|
|
|
|
classification = classify_failure_report(report_code, classification_inputs)
|
2026-08-10 21:44:42 +08:00
|
|
|
result: dict[str, Any] = {
|
|
|
|
|
"failureCode": report_code,
|
2026-08-12 08:02:51 +08:00
|
|
|
"failureSuggestion": suggestion,
|
2026-08-10 21:44:42 +08:00
|
|
|
"failureCategory": classification.category,
|
|
|
|
|
"failureScope": classification.scope,
|
|
|
|
|
"failureAction": classification.action,
|
|
|
|
|
"failureDeterministic": classification.deterministic,
|
|
|
|
|
"failureNeedsLlm": classification.needs_llm,
|
|
|
|
|
"failureClassificationReason": classification.reason,
|
|
|
|
|
"failureEvidence": error_lines[-12:],
|
|
|
|
|
}
|
|
|
|
|
observed_memory_gib = _extract_observed_gpu_memory_gib(error_lines)
|
|
|
|
|
if report_code == "PREFLIGHT_OOM" and observed_memory_gib is not None:
|
|
|
|
|
result["failureObservedGpuMemoryGiB"] = observed_memory_gib
|
2026-08-12 08:19:53 +08:00
|
|
|
unsupported_architectures, unsupported_model_types = _extract_unsupported_architectures(
|
|
|
|
|
error_lines
|
|
|
|
|
)
|
|
|
|
|
if unsupported_architectures:
|
|
|
|
|
result["failureUnsupportedArchitectures"] = unsupported_architectures
|
|
|
|
|
if unsupported_model_types:
|
|
|
|
|
result["failureUnsupportedModelTypes"] = unsupported_model_types
|
2026-08-12 08:41:07 +08:00
|
|
|
detected_framework, framework_source = _extract_submitted_framework(runtime_log)
|
|
|
|
|
if detected_framework:
|
|
|
|
|
result["failureDetectedFramework"] = detected_framework
|
|
|
|
|
result["failureDetectedFrameworkSource"] = framework_source
|
2026-08-10 21:44:42 +08:00
|
|
|
if classification.needs_llm and llm_classifier is not None and llm_classifier.enabled:
|
|
|
|
|
llm_decision = llm_classifier.classify_failure(
|
|
|
|
|
task_context=dict(task_context or {}),
|
|
|
|
|
report_code=report_code,
|
|
|
|
|
suggestion=result["failureSuggestion"],
|
|
|
|
|
error_lines=error_lines[-12:],
|
|
|
|
|
)
|
|
|
|
|
result["failureLlmDecision"] = llm_decision
|
|
|
|
|
# Promote only a confident semantic decision. Deterministic rules and
|
|
|
|
|
# platform signatures above never reach this branch, so an LLM cannot
|
|
|
|
|
# override the hard classifiers.
|
|
|
|
|
confidence = float(llm_decision.get("confidence") or 0.0)
|
|
|
|
|
scope = str(llm_decision.get("scope") or "unknown")
|
|
|
|
|
if confidence >= 0.80 and scope != "unknown":
|
|
|
|
|
result.update(
|
|
|
|
|
{
|
|
|
|
|
"failureCategory": llm_decision.get("category") or result["failureCategory"],
|
|
|
|
|
"failureScope": scope,
|
|
|
|
|
"failureAction": llm_decision.get("action") or result["failureAction"],
|
|
|
|
|
"failureClassificationReason": f"llm:{llm_decision.get('reason') or 'semantic_classification'}",
|
|
|
|
|
"failureNeedsLlm": False,
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
2026-08-12 08:41:07 +08:00
|
|
|
def _extract_submitted_framework(runtime_log: str) -> tuple[str | None, str | None]:
|
|
|
|
|
"""Recover only explicit or unambiguous target-framework evidence."""
|
|
|
|
|
explicit = SUBMITTED_FRAMEWORK_PATTERN.search(runtime_log)
|
|
|
|
|
if explicit:
|
|
|
|
|
return explicit.group("framework").strip(), "submit_framework"
|
|
|
|
|
|
|
|
|
|
image_match = TARGET_DOCKER_IMAGE_PATTERN.search(runtime_log)
|
|
|
|
|
if image_match is None:
|
|
|
|
|
return None, None
|
|
|
|
|
target_image = image_match.group("image").strip().casefold()
|
|
|
|
|
for framework, markers in FRAMEWORK_IMAGE_MARKERS:
|
|
|
|
|
if any(marker in target_image for marker in markers):
|
|
|
|
|
return framework, "target_docker_image"
|
|
|
|
|
return None, None
|
|
|
|
|
|
|
|
|
|
|
2026-08-10 21:44:42 +08:00
|
|
|
def _extract_error_lines(runtime_log: str) -> list[str]:
|
|
|
|
|
selected: list[str] = []
|
|
|
|
|
for raw_line in runtime_log.splitlines():
|
|
|
|
|
line = raw_line.strip()
|
|
|
|
|
if not line or not ERROR_LINE_PATTERN.search(line):
|
|
|
|
|
continue
|
|
|
|
|
line = re.sub(r"\x1b\[[0-9;]*m", "", line)
|
|
|
|
|
line = re.sub(r"\b[0-9a-f]{32,64}\b", "<id>", line, flags=re.IGNORECASE)
|
|
|
|
|
line = line[:1200]
|
|
|
|
|
if line not in selected:
|
|
|
|
|
selected.append(line)
|
|
|
|
|
return selected[-24:]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _extract_observed_gpu_memory_gib(error_lines: list[str]) -> float | None:
|
|
|
|
|
for line in error_lines:
|
|
|
|
|
match = re.search(
|
|
|
|
|
r"=\s*(?P<available>[0-9]+(?:\.[0-9]+)?)\s*GB\s+available\b",
|
|
|
|
|
line,
|
|
|
|
|
flags=re.IGNORECASE,
|
|
|
|
|
)
|
|
|
|
|
if match:
|
|
|
|
|
value = float(match.group("available"))
|
|
|
|
|
if 0 < value <= 1024:
|
|
|
|
|
return value
|
|
|
|
|
return None
|
2026-08-12 08:19:53 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def _extract_unsupported_architectures(
|
|
|
|
|
error_lines: list[str],
|
|
|
|
|
) -> tuple[list[str], list[str]]:
|
|
|
|
|
architectures: set[str] = set()
|
|
|
|
|
model_types: set[str] = set()
|
|
|
|
|
for line in error_lines:
|
|
|
|
|
for match in MODEL_TYPE_NOT_RECOGNIZED_PATTERN.finditer(line):
|
|
|
|
|
value = match.group("model_type").strip()
|
|
|
|
|
if value:
|
|
|
|
|
model_types.add(value)
|
|
|
|
|
for match in MODEL_ARCHITECTURES_NOT_SUPPORTED_PATTERN.finditer(line):
|
|
|
|
|
for value in re.findall(r"['\"]([^'\"]+)['\"]", match.group("architectures")):
|
|
|
|
|
value = value.strip()
|
|
|
|
|
if value:
|
|
|
|
|
architectures.add(value)
|
|
|
|
|
return sorted(architectures, key=str.casefold), sorted(model_types, key=str.casefold)
|