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

220 lines
7.0 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.

"""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 patternEmptyNode + 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"