feat(worker): 加 platform/operator pre-check(不支援 op 早期快速失敗)
轉檔時若 op 在目標 platform 不支援(如 Softmax@520),原本會白跑完 onnx+bie 兩階段、最後在 nef 的 batch_compile 拋難懂的 C++ backtrace(undefined CPU op)。 本次在 onnx 階段(拿到 out.onnx 後、進 bie 前)用 Kneron IP Evaluator (ktc ModelConfig.evaluate / ip_evaluator mode 0 會跑 compiler frontend) 早期偵測不支援 op,快速失敗並回清楚訊息。 - 新增 services/backends/precheck.py:run_precheck 掃 evaluate 的不支援訊號 (HardwareNotSupport / UnimplementedFeature / undefined CPU op / EmptyNode) 抽出 op 名、raise UnsupportedOperatorError;錯誤走既有 _push_done(fail) 回報 - services/workers/onnx/core.py:enable_precheck default-on;pre-check 與既有 evaluate 共用同一次 evaluate() 呼叫(避免跑兩次 compiler frontend) - error 訊息:「platform 520 不支援 operator Softmax,請改用支援的 platform (如 720/730)或將該 operator 移至 host 端後處理。」 - fail-open:只有明確訊號才擋、evaluate 環境問題無訊號則放行(不誤擋能跑的模型) - 17 tests pass(含多 op / dedup) follow-up(部署後、需 docker toolchain 環境):integration smoke 真觸發 Softmax@520 確認 onnx 階段就擋;依實測 frontend 耗時評估是否收窄 default-on 粒度。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
57ffc619f9
commit
5a9c1b0140
183
services/backends/precheck.py
Normal file
183
services/backends/precheck.py
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
"""Platform / operator compatibility pre-check.
|
||||||
|
|
||||||
|
在轉檔流程「早期」(ONNX 階段拿到 out.onnx 之後、送 bie/nef 之前)用 IP Evaluator
|
||||||
|
的分析結果判斷模型是否含有目標 platform 不支援的 operator(例如 Softmax@520)。
|
||||||
|
|
||||||
|
背景:Kneron toolchain 的 ``ktc.ModelConfig(...).evaluate()``(ip_evaluator, mode 0)
|
||||||
|
內部會跑 compiler frontend 做 node placement 分析。當某個 op 在目標 platform 無法
|
||||||
|
落在 NPU、且沒有對應的 CPU op 定義時,compiler frontend 會在 log 內留下
|
||||||
|
``HardwareNotSupport`` / ``UnimplementedFeature`` / ``creating an EmptyNode instance for
|
||||||
|
op_type:`` 等訊號,toolchain 會把這些寫進 ``model_fx_report``。因此我們可以在編譯前
|
||||||
|
(不需要跑完 quantization + batch_compile)就偵測到不支援的 op,fail-fast 並回一個
|
||||||
|
使用者看得懂的錯誤,而不是讓 job 跑到 nef 階段撞 C++ backtrace(exit 6)。
|
||||||
|
|
||||||
|
設計原則(避免誤擋):
|
||||||
|
- 只有在 evaluator 報告中出現「明確的」不支援訊號時才擋。
|
||||||
|
- evaluator 分析失敗(例如環境問題、transient error)而沒有明確不支援訊號時 → 放行,
|
||||||
|
讓後續流程照舊跑,不因為 pre-check 有疑慮就擋掉本來能跑的模型。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
from typing import Iterable, List, Optional
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class UnsupportedOperatorError(Exception):
|
||||||
|
"""目標 platform 不支援模型中的某個 operator。
|
||||||
|
|
||||||
|
這是一個「明確且可預期」的失敗,訊息會直接透過 job 失敗回報機制傳回給使用者。
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# Compiler frontend / ip_evaluator 在遇到不支援 op 時會在報告 / log 留下的訊號。
|
||||||
|
# 對照來源:vendor/sys_flow*/test_case.py::check_compiler_HardwareNotSupport 與
|
||||||
|
# check_knerex_error 寫入 model_fx_report 的字串。
|
||||||
|
#
|
||||||
|
# 每個 pattern 盡量帶一個 capture group 用來抓「op 名稱」(抓不到也沒關係,會退回泛用訊息)。
|
||||||
|
_UNSUPPORTED_MARKERS: tuple[tuple[str, str], ...] = (
|
||||||
|
# "creating an EmptyNode instance for op_type: Softmax"
|
||||||
|
("empty_node", r"creating an EmptyNode instance for op_type:\s*([A-Za-z0-9_]+)"),
|
||||||
|
# "HW_NOT_SUPPORT: Softmax" / "HardwareNotSupport: Softmax" / "Hardware not support: Softmax"
|
||||||
|
("hw_not_support", r"(?:HW[_ ]NOT[_ ]SUPPORT|Hardware\s*not\s*support|HardwareNotSupport)\s*[:\-]?\s*([A-Za-z0-9_]+)?"),
|
||||||
|
# "UNIMPLEMENTED_FEATURE: ..." / "UnimplementedFeature: undefined CPU op [Softmax]"
|
||||||
|
("unimplemented", r"(?:UNIMPLEMENTED[_ ]FEATURE|UnimplementedFeature)\s*[:\-]?\s*(.*)"),
|
||||||
|
# 泛用「HW not support」報告欄位(toolchain 報告字串)
|
||||||
|
("hw_not_support_col", r"HW\s+not\s+support\b"),
|
||||||
|
)
|
||||||
|
|
||||||
|
# 從一段訊息中撈出被中括號 / 引號包住的 op 名稱,例如 "undefined CPU op [Softmax]"。
|
||||||
|
_OP_IN_BRACKETS = re.compile(r"[\[\(]\s*([A-Za-z][A-Za-z0-9_]*)\s*[\]\)]")
|
||||||
|
# 常見 ONNX op 名稱(PascalCase / 首字母大寫),用來在自由文字裡挑一個像 op 的字。
|
||||||
|
_OP_TOKEN = re.compile(r"\b([A-Z][A-Za-z0-9]{2,})\b")
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_op_name(fragment: Optional[str]) -> Optional[str]:
|
||||||
|
"""盡力從訊息片段中抽出 operator 名稱。抓不到就回 None。"""
|
||||||
|
if not fragment:
|
||||||
|
return None
|
||||||
|
fragment = fragment.strip()
|
||||||
|
if not fragment:
|
||||||
|
return None
|
||||||
|
|
||||||
|
m = _OP_IN_BRACKETS.search(fragment)
|
||||||
|
if m:
|
||||||
|
return m.group(1)
|
||||||
|
|
||||||
|
# 直接就是一個 op token(例如 "Softmax")
|
||||||
|
if re.fullmatch(r"[A-Za-z][A-Za-z0-9_]*", fragment):
|
||||||
|
return fragment
|
||||||
|
|
||||||
|
m = _OP_TOKEN.search(fragment)
|
||||||
|
if m:
|
||||||
|
return m.group(1)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def find_unsupported_operators(report: Optional[str]) -> List[str]:
|
||||||
|
"""掃描 evaluator 報告字串,回傳偵測到的「不支援 operator 名稱」清單。
|
||||||
|
|
||||||
|
- 沒有偵測到任何明確不支援訊號 → 回空 list(呼叫端應放行)。
|
||||||
|
- 偵測到訊號但抽不出 op 名稱 → 回含一個占位符 ``"<unknown>"`` 的 list,
|
||||||
|
讓呼叫端仍能 fail-fast(但訊息較泛用)。
|
||||||
|
"""
|
||||||
|
if not report:
|
||||||
|
return []
|
||||||
|
|
||||||
|
found: List[str] = []
|
||||||
|
saw_marker = False
|
||||||
|
|
||||||
|
for _kind, pattern in _UNSUPPORTED_MARKERS:
|
||||||
|
for match in re.finditer(pattern, report, flags=re.IGNORECASE):
|
||||||
|
saw_marker = True
|
||||||
|
captured = match.group(1) if match.groups() else None
|
||||||
|
op = _extract_op_name(captured)
|
||||||
|
if op and op not in found:
|
||||||
|
found.append(op)
|
||||||
|
|
||||||
|
if found:
|
||||||
|
return found
|
||||||
|
if saw_marker:
|
||||||
|
# 有明確不支援訊號、但抽不到 op 名稱:仍回報(用占位符)。
|
||||||
|
return ["<unknown>"]
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def build_error_message(platform: str, ops: Iterable[str]) -> str:
|
||||||
|
"""產生使用者看得懂的錯誤訊息。"""
|
||||||
|
op_list = [o for o in ops if o and o != "<unknown>"]
|
||||||
|
if op_list:
|
||||||
|
ops_text = ", ".join(op_list)
|
||||||
|
op_clause = f"operator {ops_text}"
|
||||||
|
else:
|
||||||
|
op_clause = "某個 operator"
|
||||||
|
return (
|
||||||
|
f"platform {platform} 不支援 {op_clause},"
|
||||||
|
f"請改用支援的 platform(如 720/730)或將該 operator 移至 host 端後處理。"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_precheck(
|
||||||
|
onnx_path: str,
|
||||||
|
*,
|
||||||
|
model_id: int,
|
||||||
|
version: str,
|
||||||
|
platform: str,
|
||||||
|
evaluator=None,
|
||||||
|
) -> Optional[str]:
|
||||||
|
"""對 ``onnx_path`` 跑 platform/operator 相容性檢查。
|
||||||
|
|
||||||
|
偵測到「明確不支援」的 op → raise :class:`UnsupportedOperatorError`。
|
||||||
|
其餘情況(分析成功且全支援、或分析失敗但無明確訊號)→ 放行。
|
||||||
|
|
||||||
|
放行時回傳 ``evaluate()`` 的原始報告字串(成功分析)或 ``None``
|
||||||
|
(evaluate() raise 但無明確不支援訊號)。回傳值讓呼叫端可以**重用**這份
|
||||||
|
報告,避免 ``enable_precheck`` 與 ``enable_evaluate`` 同開時重複跑一次
|
||||||
|
``evaluate()``(compiler frontend 分析成本高、秒~數十秒級)。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
onnx_path: 已經過 onnx2onnx_flow 的 out.onnx 路徑。
|
||||||
|
model_id / version / platform: 目標編譯設定。
|
||||||
|
evaluator: 可注入的 evaluator backend(測試用)。None 時使用預設 Kneron backend。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
evaluate() 的報告字串(放行且分析成功),或 None(放行但分析失敗無訊號)。
|
||||||
|
"""
|
||||||
|
if evaluator is None:
|
||||||
|
from services.backends.evaluator import get_evaluator_backend
|
||||||
|
|
||||||
|
evaluator = get_evaluator_backend()
|
||||||
|
|
||||||
|
report: Optional[str] = None
|
||||||
|
try:
|
||||||
|
report = evaluator.evaluate(
|
||||||
|
onnx_path,
|
||||||
|
model_id=int(model_id),
|
||||||
|
version=str(version),
|
||||||
|
platform=str(platform),
|
||||||
|
)
|
||||||
|
except Exception as exc: # noqa: BLE001 - 需要檢查例外訊息內容
|
||||||
|
# evaluate() 在遇到不支援 op 時可能直接 raise(例如 AssertionError)。
|
||||||
|
# 用例外訊息掃描明確訊號;掃不到就當成「分析失敗但不確定」→ 放行(不誤擋)。
|
||||||
|
message = str(exc)
|
||||||
|
ops = find_unsupported_operators(message)
|
||||||
|
if ops:
|
||||||
|
raise UnsupportedOperatorError(build_error_message(platform, ops)) from exc
|
||||||
|
logger.warning(
|
||||||
|
"Compatibility pre-check inconclusive (evaluator raised, no explicit "
|
||||||
|
"unsupported-op signal); allowing job to proceed. platform=%s error=%s",
|
||||||
|
platform,
|
||||||
|
message,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
ops = find_unsupported_operators(report)
|
||||||
|
if ops:
|
||||||
|
raise UnsupportedOperatorError(build_error_message(platform, ops))
|
||||||
|
|
||||||
|
logger.info("Compatibility pre-check passed. platform=%s", platform)
|
||||||
|
return report
|
||||||
@ -36,8 +36,33 @@ def process_onnx_core(
|
|||||||
model = ktc.onnx_optimizer.onnx2onnx_flow(model, eliminate_tail=True, opt_matmul=True)
|
model = ktc.onnx_optimizer.onnx2onnx_flow(model, eliminate_tail=True, opt_matmul=True)
|
||||||
onnx.save(model, output_path)
|
onnx.save(model, output_path)
|
||||||
|
|
||||||
|
# Platform / operator 相容性 pre-check(fail-fast)。
|
||||||
|
# 在送 bie/nef 之前,用 IP Evaluator 偵測目標 platform 不支援的 op(如 Softmax@520),
|
||||||
|
# 避免白跑完 bie + nef 最後才在 batch_compile 撞 C++ backtrace(exit 6)。
|
||||||
|
# 預設開啟;偵測到「明確不支援」才擋,偵測不到則放行(不誤擋本來能跑的模型)。
|
||||||
|
#
|
||||||
|
# pre-check 內部會跑一次 evaluate()(compiler frontend 分析、成本高)。若下方
|
||||||
|
# enable_evaluate 也開,直接重用這份報告,避免對同一模型跑兩次 evaluate()。
|
||||||
|
precheck_ran = parameters.get("enable_precheck", True)
|
||||||
|
precheck_report = None
|
||||||
|
if precheck_ran:
|
||||||
|
from services.backends.precheck import run_precheck
|
||||||
|
|
||||||
|
precheck_report = run_precheck(
|
||||||
|
output_path,
|
||||||
|
model_id=int(parameters["model_id"]),
|
||||||
|
version=str(parameters["version"]),
|
||||||
|
platform=str(parameters["platform"]),
|
||||||
|
)
|
||||||
|
|
||||||
eval_result = ""
|
eval_result = ""
|
||||||
if parameters.get("enable_evaluate", False):
|
if parameters.get("enable_evaluate", False):
|
||||||
|
if precheck_ran:
|
||||||
|
# 重用 pre-check 已經跑過的 evaluate() 報告,不再重跑。
|
||||||
|
# precheck_report 為 None 代表 evaluate() 當時 raise 但無不支援訊號
|
||||||
|
# (已在 pre-check 放行)→ 沒有可用報告,維持空字串。
|
||||||
|
evaluate_result = precheck_report or ""
|
||||||
|
else:
|
||||||
from services.backends.evaluator import get_evaluator_backend
|
from services.backends.evaluator import get_evaluator_backend
|
||||||
|
|
||||||
evaluator = get_evaluator_backend()
|
evaluator = get_evaluator_backend()
|
||||||
|
|||||||
@ -49,6 +49,8 @@ def test_worker_flow_e2e_uses_single_workdir():
|
|||||||
"platform": "520",
|
"platform": "520",
|
||||||
"work_dir": str(work_dir),
|
"work_dir": str(work_dir),
|
||||||
"enable_evaluate": False,
|
"enable_evaluate": False,
|
||||||
|
# 此測試聚焦 workdir 隔離、非相容性檢查;關掉 pre-check 保持原行為與速度。
|
||||||
|
"enable_precheck": False,
|
||||||
}
|
}
|
||||||
onnx_result = process_onnx_core(
|
onnx_result = process_onnx_core(
|
||||||
{"file_path": str(work_input_file)},
|
{"file_path": str(work_input_file)},
|
||||||
|
|||||||
@ -49,6 +49,8 @@ def test_worker_flow_e2e_tflite_uses_single_workdir():
|
|||||||
"platform": "520",
|
"platform": "520",
|
||||||
"work_dir": str(work_dir),
|
"work_dir": str(work_dir),
|
||||||
"enable_evaluate": False,
|
"enable_evaluate": False,
|
||||||
|
# 此測試聚焦端到端流程、非相容性檢查;關掉 pre-check 保持原行為與速度。
|
||||||
|
"enable_precheck": False,
|
||||||
}
|
}
|
||||||
onnx_result = process_onnx_core(
|
onnx_result = process_onnx_core(
|
||||||
{"file_path": str(work_input_file)},
|
{"file_path": str(work_input_file)},
|
||||||
|
|||||||
@ -13,7 +13,7 @@ def test_process_onnx_core_creates_output():
|
|||||||
|
|
||||||
assert input_file.exists(), f"Missing input file: {input_file}"
|
assert input_file.exists(), f"Missing input file: {input_file}"
|
||||||
|
|
||||||
params = {"model_id": 1, "version": "1a2b", "platform": "520"}
|
params = {"model_id": 1, "version": "1a2b", "platform": "520", "enable_precheck": False}
|
||||||
|
|
||||||
result = process_onnx_core(
|
result = process_onnx_core(
|
||||||
{"file_path": str(input_file)},
|
{"file_path": str(input_file)},
|
||||||
|
|||||||
@ -13,7 +13,7 @@ def test_process_tflite_core_creates_output():
|
|||||||
|
|
||||||
assert input_file.exists(), f"Missing input file: {input_file}"
|
assert input_file.exists(), f"Missing input file: {input_file}"
|
||||||
|
|
||||||
params = {"model_id": 4, "version": "tflite", "platform": "520"}
|
params = {"model_id": 4, "version": "tflite", "platform": "520", "enable_precheck": False}
|
||||||
|
|
||||||
result = process_onnx_core(
|
result = process_onnx_core(
|
||||||
{"file_path": str(input_file)},
|
{"file_path": str(input_file)},
|
||||||
|
|||||||
219
tests/workers/test_precheck.py
Normal file
219
tests/workers/test_precheck.py
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
"""Unit tests for platform/operator compatibility pre-check.
|
||||||
|
|
||||||
|
這些測試刻意不 import onnx / ktc(本機環境沒有 toolchain),只透過 mock 的
|
||||||
|
evaluator backend 驗證 pre-check 的判斷邏輯。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
from services.backends.precheck import (
|
||||||
|
UnsupportedOperatorError,
|
||||||
|
build_error_message,
|
||||||
|
find_unsupported_operators,
|
||||||
|
run_precheck,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeEvaluator:
|
||||||
|
"""可注入的假 evaluator:回傳固定字串,或在呼叫時 raise。"""
|
||||||
|
|
||||||
|
def __init__(self, *, report=None, raises=None):
|
||||||
|
self._report = report
|
||||||
|
self._raises = raises
|
||||||
|
self.calls = []
|
||||||
|
|
||||||
|
def evaluate(self, onnx_path, **kwargs):
|
||||||
|
self.calls.append((onnx_path, kwargs))
|
||||||
|
if self._raises is not None:
|
||||||
|
raise self._raises
|
||||||
|
return self._report
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# find_unsupported_operators
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
def test_find_unsupported_empty_report_returns_empty():
|
||||||
|
assert find_unsupported_operators("") == []
|
||||||
|
assert find_unsupported_operators(None) == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_unsupported_clean_report_returns_empty():
|
||||||
|
report = "kdp520/FPS: 1200, cpu_node: None, docker_version: 1.0"
|
||||||
|
assert find_unsupported_operators(report) == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_unsupported_detects_empty_node_op():
|
||||||
|
report = "compiler: creating an EmptyNode instance for op_type: Softmax"
|
||||||
|
assert find_unsupported_operators(report) == ["Softmax"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_unsupported_detects_unimplemented_feature_with_bracket_op():
|
||||||
|
report = "UnimplementedFeature: undefined CPU op [Softmax]"
|
||||||
|
assert find_unsupported_operators(report) == ["Softmax"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_unsupported_detects_hw_not_support_with_op():
|
||||||
|
report = "HW_NOT_SUPPORT: Softmax"
|
||||||
|
assert find_unsupported_operators(report) == ["Softmax"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_unsupported_signal_without_op_returns_placeholder():
|
||||||
|
# 有明確訊號但抽不到 op 名稱 → 回占位符,讓呼叫端仍能 fail-fast。
|
||||||
|
report = "kdp520/ERROR, HW not support"
|
||||||
|
assert find_unsupported_operators(report) == ["<unknown>"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_unsupported_does_not_false_positive_on_supported_wording():
|
||||||
|
# "int16 is not supported in 520" 這種 bitwidth 說明不應被誤判為 op 不支援。
|
||||||
|
report = "note: int16 is not supported in 520; datapath int8 ok"
|
||||||
|
assert find_unsupported_operators(report) == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_unsupported_detects_multiple_ops_across_markers():
|
||||||
|
# 模型含多個不支援 op,且分散在不同 marker pattern(EmptyNode + HW_NOT_SUPPORT)。
|
||||||
|
# 應回傳兩者、順序穩定(依報告出現順序)。
|
||||||
|
report = (
|
||||||
|
"compiler: creating an EmptyNode instance for op_type: Softmax\n"
|
||||||
|
"HW_NOT_SUPPORT: LSTM"
|
||||||
|
)
|
||||||
|
assert find_unsupported_operators(report) == ["Softmax", "LSTM"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_unsupported_dedups_repeated_op():
|
||||||
|
# 同一個不支援 op 在報告中出現多次(多個節點)→ 只回一次(precheck.py 的 dedup)。
|
||||||
|
report = (
|
||||||
|
"creating an EmptyNode instance for op_type: Softmax\n"
|
||||||
|
"creating an EmptyNode instance for op_type: Softmax\n"
|
||||||
|
"HW_NOT_SUPPORT: Softmax"
|
||||||
|
)
|
||||||
|
assert find_unsupported_operators(report) == ["Softmax"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_precheck_raises_lists_multiple_ops_in_message():
|
||||||
|
# 多個不支援 op → 擋掉時錯誤訊息應同時列出(去重後)。
|
||||||
|
evaluator = _FakeEvaluator(
|
||||||
|
report=(
|
||||||
|
"creating an EmptyNode instance for op_type: Softmax\n"
|
||||||
|
"HW_NOT_SUPPORT: LSTM\n"
|
||||||
|
"creating an EmptyNode instance for op_type: Softmax"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
with pytest.raises(UnsupportedOperatorError) as exc:
|
||||||
|
run_precheck(
|
||||||
|
"/tmp/out.onnx",
|
||||||
|
model_id=6,
|
||||||
|
version="v1",
|
||||||
|
platform="520",
|
||||||
|
evaluator=evaluator,
|
||||||
|
)
|
||||||
|
msg = str(exc.value)
|
||||||
|
assert "Softmax" in msg
|
||||||
|
assert "LSTM" in msg
|
||||||
|
assert "520" in msg
|
||||||
|
# 去重:Softmax 只出現一次
|
||||||
|
assert msg.count("Softmax") == 1
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# build_error_message
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
def test_build_error_message_with_op_names():
|
||||||
|
msg = build_error_message("520", ["Softmax"])
|
||||||
|
assert "520" in msg
|
||||||
|
assert "Softmax" in msg
|
||||||
|
# 帶有引導使用者的補救建議
|
||||||
|
assert "720" in msg or "730" in msg
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_error_message_with_unknown_op():
|
||||||
|
msg = build_error_message("520", ["<unknown>"])
|
||||||
|
assert "520" in msg
|
||||||
|
assert "operator" in msg
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# run_precheck
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
def test_run_precheck_raises_on_unsupported_report():
|
||||||
|
evaluator = _FakeEvaluator(
|
||||||
|
report="compiler: creating an EmptyNode instance for op_type: Softmax"
|
||||||
|
)
|
||||||
|
with pytest.raises(UnsupportedOperatorError) as exc:
|
||||||
|
run_precheck(
|
||||||
|
"/tmp/out.onnx",
|
||||||
|
model_id=1,
|
||||||
|
version="v1",
|
||||||
|
platform="520",
|
||||||
|
evaluator=evaluator,
|
||||||
|
)
|
||||||
|
assert "Softmax" in str(exc.value)
|
||||||
|
assert "520" in str(exc.value)
|
||||||
|
assert len(evaluator.calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_precheck_passes_on_clean_report():
|
||||||
|
evaluator = _FakeEvaluator(report="kdp720/FPS: 900, cpu_node: None")
|
||||||
|
# 不應 raise
|
||||||
|
run_precheck(
|
||||||
|
"/tmp/out.onnx",
|
||||||
|
model_id=2,
|
||||||
|
version="v1",
|
||||||
|
platform="720",
|
||||||
|
evaluator=evaluator,
|
||||||
|
)
|
||||||
|
assert len(evaluator.calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_precheck_raises_when_evaluate_raises_with_signal():
|
||||||
|
# evaluate() 直接 raise,但例外訊息含明確不支援訊號 → 應轉成清楚錯誤。
|
||||||
|
evaluator = _FakeEvaluator(
|
||||||
|
raises=AssertionError("UnimplementedFeature: undefined CPU op [Softmax]")
|
||||||
|
)
|
||||||
|
with pytest.raises(UnsupportedOperatorError) as exc:
|
||||||
|
run_precheck(
|
||||||
|
"/tmp/out.onnx",
|
||||||
|
model_id=3,
|
||||||
|
version="v1",
|
||||||
|
platform="520",
|
||||||
|
evaluator=evaluator,
|
||||||
|
)
|
||||||
|
assert "Softmax" in str(exc.value)
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_precheck_passes_when_evaluate_raises_without_signal():
|
||||||
|
# evaluate() raise,但訊息不含不支援訊號(transient / 環境問題)→ 放行不誤擋。
|
||||||
|
evaluator = _FakeEvaluator(
|
||||||
|
raises=RuntimeError("Quantization model generation failed. See above message.")
|
||||||
|
)
|
||||||
|
# 不應 raise
|
||||||
|
run_precheck(
|
||||||
|
"/tmp/out.onnx",
|
||||||
|
model_id=4,
|
||||||
|
version="v1",
|
||||||
|
platform="530",
|
||||||
|
evaluator=evaluator,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_precheck_passes_target_platform_to_evaluator():
|
||||||
|
evaluator = _FakeEvaluator(report="")
|
||||||
|
run_precheck(
|
||||||
|
"/tmp/out.onnx",
|
||||||
|
model_id=5,
|
||||||
|
version="abc",
|
||||||
|
platform="730",
|
||||||
|
evaluator=evaluator,
|
||||||
|
)
|
||||||
|
_path, kwargs = evaluator.calls[0]
|
||||||
|
assert kwargs["platform"] == "730"
|
||||||
|
assert kwargs["model_id"] == 5
|
||||||
|
assert kwargs["version"] == "abc"
|
||||||
Loading…
x
Reference in New Issue
Block a user