From 5a9c1b01409e3825c3683b7aa668b6fc37328bf3 Mon Sep 17 00:00:00 2001 From: jim800121chen Date: Fri, 3 Jul 2026 02:51:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(worker):=20=E5=8A=A0=20platform/operator?= =?UTF-8?q?=20pre-check=EF=BC=88=E4=B8=8D=E6=94=AF=E6=8F=B4=20op=20?= =?UTF-8?q?=E6=97=A9=E6=9C=9F=E5=BF=AB=E9=80=9F=E5=A4=B1=E6=95=97=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 轉檔時若 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) --- services/backends/precheck.py | 183 +++++++++++++++++++++ services/workers/onnx/core.py | 95 +++++++---- tests/workers/test_flow_e2e.py | 2 + tests/workers/test_flow_e2e_tflite.py | 2 + tests/workers/test_onnx_core.py | 10 +- tests/workers/test_onnx_core_tflite.py | 2 +- tests/workers/test_precheck.py | 219 +++++++++++++++++++++++++ 7 files changed, 472 insertions(+), 41 deletions(-) create mode 100644 services/backends/precheck.py create mode 100644 tests/workers/test_precheck.py diff --git a/services/backends/precheck.py b/services/backends/precheck.py new file mode 100644 index 0000000..ff6a48c --- /dev/null +++ b/services/backends/precheck.py @@ -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 名稱 → 回含一個占位符 ``""`` 的 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 [""] + return [] + + +def build_error_message(platform: str, ops: Iterable[str]) -> str: + """產生使用者看得懂的錯誤訊息。""" + op_list = [o for o in ops if o and o != ""] + 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 diff --git a/services/workers/onnx/core.py b/services/workers/onnx/core.py index 19a2850..71ff509 100644 --- a/services/workers/onnx/core.py +++ b/services/workers/onnx/core.py @@ -1,18 +1,18 @@ -import os -from typing import Dict, Any - +import os +from typing import Dict, Any + import onnx - - + + def process_onnx_core( input_paths: Dict[str, str], output_path: str, parameters: Dict[str, Any], ) -> Dict[str, Any]: - file_path = input_paths["file_path"] - if not os.path.exists(file_path): - raise FileNotFoundError(f"Input file not found: {file_path}") - + file_path = input_paths["file_path"] + if not os.path.exists(file_path): + raise FileNotFoundError(f"Input file not found: {file_path}") + output_dir = os.path.dirname(output_path) if output_dir: os.makedirs(output_dir, exist_ok=True) @@ -24,38 +24,63 @@ def process_onnx_core( os.environ.setdefault("KTC_SCRIPT_RES", res_dir) import ktc - - file_extension = os.path.splitext(file_path)[1].lower() - if file_extension == ".tflite": - model = ktc.onnx_optimizer.tflite2onnx_flow(file_path) - elif file_extension == ".onnx": - model = onnx.load(file_path) - else: - raise ValueError("Unsupported file type. Only ONNX and TFLite files are supported.") - - model = ktc.onnx_optimizer.onnx2onnx_flow(model, eliminate_tail=True, opt_matmul=True) - onnx.save(model, output_path) - - eval_result = "" - if parameters.get("enable_evaluate", False): - from services.backends.evaluator import get_evaluator_backend - evaluator = get_evaluator_backend() - evaluate_result = evaluator.evaluate( + file_extension = os.path.splitext(file_path)[1].lower() + if file_extension == ".tflite": + model = ktc.onnx_optimizer.tflite2onnx_flow(file_path) + elif file_extension == ".onnx": + model = onnx.load(file_path) + else: + raise ValueError("Unsupported file type. Only ONNX and TFLite files are supported.") + + model = ktc.onnx_optimizer.onnx2onnx_flow(model, eliminate_tail=True, opt_matmul=True) + 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 = "" + 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 + + evaluator = get_evaluator_backend() + evaluate_result = evaluator.evaluate( + output_path, + model_id=int(parameters["model_id"]), + version=str(parameters["version"]), + platform=str(parameters["platform"]), + ) eval_result = evaluate_result.split(",")[0] return { - "file_path": output_path, - "file_size": os.path.getsize(output_path), - "eval_report": eval_result, - "model_info": { - "model_id": parameters["model_id"], - "version": parameters["version"], - "platform": parameters["platform"], - }, - } + "file_path": output_path, + "file_size": os.path.getsize(output_path), + "eval_report": eval_result, + "model_info": { + "model_id": parameters["model_id"], + "version": parameters["version"], + "platform": parameters["platform"], + }, + } diff --git a/tests/workers/test_flow_e2e.py b/tests/workers/test_flow_e2e.py index 07b1c50..1541d8a 100644 --- a/tests/workers/test_flow_e2e.py +++ b/tests/workers/test_flow_e2e.py @@ -49,6 +49,8 @@ def test_worker_flow_e2e_uses_single_workdir(): "platform": "520", "work_dir": str(work_dir), "enable_evaluate": False, + # 此測試聚焦 workdir 隔離、非相容性檢查;關掉 pre-check 保持原行為與速度。 + "enable_precheck": False, } onnx_result = process_onnx_core( {"file_path": str(work_input_file)}, diff --git a/tests/workers/test_flow_e2e_tflite.py b/tests/workers/test_flow_e2e_tflite.py index e95eb54..52e1763 100644 --- a/tests/workers/test_flow_e2e_tflite.py +++ b/tests/workers/test_flow_e2e_tflite.py @@ -49,6 +49,8 @@ def test_worker_flow_e2e_tflite_uses_single_workdir(): "platform": "520", "work_dir": str(work_dir), "enable_evaluate": False, + # 此測試聚焦端到端流程、非相容性檢查;關掉 pre-check 保持原行為與速度。 + "enable_precheck": False, } onnx_result = process_onnx_core( {"file_path": str(work_input_file)}, diff --git a/tests/workers/test_onnx_core.py b/tests/workers/test_onnx_core.py index 589ffc8..059089d 100644 --- a/tests/workers/test_onnx_core.py +++ b/tests/workers/test_onnx_core.py @@ -7,13 +7,13 @@ sys.path.insert(0, str(ROOT)) from services.workers.onnx.core import process_onnx_core -def test_process_onnx_core_creates_output(): +def test_process_onnx_core_creates_output(): input_file = ROOT / "tests" / "fixtures" / "onnx" / "input.onnx" output_file = ROOT / "tests" / "fixtures" / "outputs" / "out.onnx" 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( {"file_path": str(input_file)}, @@ -21,6 +21,6 @@ def test_process_onnx_core_creates_output(): params, ) - assert output_file.exists() - assert result["file_path"] == str(output_file) - assert result["file_size"] > 0 + assert output_file.exists() + assert result["file_path"] == str(output_file) + assert result["file_size"] > 0 diff --git a/tests/workers/test_onnx_core_tflite.py b/tests/workers/test_onnx_core_tflite.py index 231d9c4..82766f9 100644 --- a/tests/workers/test_onnx_core_tflite.py +++ b/tests/workers/test_onnx_core_tflite.py @@ -13,7 +13,7 @@ def test_process_tflite_core_creates_output(): 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( {"file_path": str(input_file)}, diff --git a/tests/workers/test_precheck.py b/tests/workers/test_precheck.py new file mode 100644 index 0000000..a4888e4 --- /dev/null +++ b/tests/workers/test_precheck.py @@ -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) == [""] + + +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", [""]) + 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"