jim800121chen 5a9c1b0140 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>
2026-07-03 02:51:07 +08:00

184 lines
7.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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就偵測到不支援的 opfail-fast 並回一個
使用者看得懂的錯誤,而不是讓 job 跑到 nef 階段撞 C++ backtraceexit 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