Compare commits
No commits in common. "407fe10a8c9a6715449bfc54721aacd080ac43f2" and "25ce7f87a817f85d9df8d161705b08dff59b9622" have entirely different histories.
407fe10a8c
...
25ce7f87a8
@ -503,7 +503,6 @@
|
|||||||
|
|
||||||
- 流程固定:ONNX → BIE → NEF,不支援跳過或只跑部分(後端限制)
|
- 流程固定:ONNX → BIE → NEF,不支援跳過或只跑部分(後端限制)
|
||||||
- 任何階段失敗即整體 FAILED,不自動重試
|
- 任何階段失敗即整體 FAILED,不自動重試
|
||||||
- **ONNX 階段輸出語意(2026-07-06 M 級變更)**:ONNX 優化後、存檔前對所有 platform 移除尾端 Softmax,故 NEF 最終輸出為 logits 而非機率。詳見 §11 註與 `../04-architecture/TDD.md` §12。
|
|
||||||
|
|
||||||
### 5.5 對外 API 端點預告(新增,2026-04-25,待 Architect 產出詳細規格)
|
### 5.5 對外 API 端點預告(新增,2026-04-25,待 Architect 產出詳細規格)
|
||||||
|
|
||||||
@ -803,18 +802,12 @@ RICE = (Reach x Impact x Confidence) / Effort
|
|||||||
|
|
||||||
| 平台代號 | 平台名稱 | 備註 |
|
| 平台代號 | 平台名稱 | 備註 |
|
||||||
|---------|---------|------|
|
|---------|---------|------|
|
||||||
| 520 | KDP520 | 含尾端 Softmax 的模型自 2026-07-06 起可轉(見下方註)|
|
| 520 | KDP520 | - |
|
||||||
| 720 | KDP720 | - |
|
| 720 | KDP720 | - |
|
||||||
| 530 | KDP530 | - |
|
| 530 | KDP530 | - |
|
||||||
| 630 | KDP630 | - |
|
| 630 | KDP630 | - |
|
||||||
| 730 | KDP730 | - |
|
| 730 | KDP730 | - |
|
||||||
|
|
||||||
> **輸出語意變更(2026-07-06,M 級)**:轉檔管線自 2026-07-06 起,對**所有 platform** 一律移除模型尾端的 Softmax 節點、NEF 輸出改為 **logits(未經 softmax 的原始分數)**,而非先前的機率輸出。
|
|
||||||
> - **為什麼**:含尾端 Softmax 的分類模型轉 KL520 時 batch_compile 會因不支援 Softmax CPU op 而失敗(exit 6);移除 Softmax 後可正常編譯,且與正式站(`converter.innovedus.com`)行為一致(正式站本就是這樣做)。
|
|
||||||
> - **對呼叫端的影響**:取分類結果(argmax)通常不受影響;若需要「機率值」,呼叫端(如 visionA)須在 host 端自行補 softmax。
|
|
||||||
> - **需跨團隊通知**:visionA 團隊。
|
|
||||||
> - 技術細節見 `../04-architecture/TDD.md` §12、`../04-architecture/design-doc.md` ADR-012。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 12. 風險與緩解
|
## 12. 風險與緩解
|
||||||
|
|||||||
@ -17,7 +17,6 @@
|
|||||||
| 2026-04-25 | 初版 Draft 1.0(OAuth resource server + promote) | Architect Agent |
|
| 2026-04-25 | 初版 Draft 1.0(OAuth resource server + promote) | Architect Agent |
|
||||||
| 2026-04-25 | Multipart 上傳路徑改 visionA → converter 直傳;移除 FAA GET/HEAD | Architect Agent |
|
| 2026-04-25 | Multipart 上傳路徑改 visionA → converter 直傳;移除 FAA GET/HEAD | Architect Agent |
|
||||||
| 2026-05-16 | **Phase 0.8b 重寫**:visionA → converter 改 API key;新增 `/result` endpoint;OAuth resource server 章節砍除;模組化拆分為索引 + 子檔案 | Architect Agent |
|
| 2026-05-16 | **Phase 0.8b 重寫**:visionA → converter 改 API key;新增 `/result` endpoint;OAuth resource server 章節砍除;模組化拆分為索引 + 子檔案 | Architect Agent |
|
||||||
| 2026-07-06 | **Worker 行為變更(M 級)**:onnx worker 對**所有 platform** 一律移除尾端 Softmax、輸出 logits(與正式站對齊、修 520 batch_compile exit 6);pre-check regex 收窄。新增 §12 | Architect Agent |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -38,7 +37,6 @@
|
|||||||
| `observability.md` | Log 格式 + 敏感資料保護 + 告警 | Backend |
|
| `observability.md` | Log 格式 + 敏感資料保護 + 告警 | Backend |
|
||||||
| `security.md` | Trust boundary + Input validation + Auth security | 全部 |
|
| `security.md` | Trust boundary + Input validation + Auth security | 全部 |
|
||||||
| `design-doc.md` | 架構決策 + ADR | 全部 |
|
| `design-doc.md` | 架構決策 + ADR | 全部 |
|
||||||
| 本檔 §12 | **Worker 轉檔管線行為變更**(尾端 Softmax 移除 + pre-check regex 收窄,2026-07-06 M 級)| Backend、Reviewer、Testing |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -293,94 +291,4 @@ apps/task-scheduler/
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 12. Worker 轉檔管線行為變更(2026-07-06,M 級)
|
**附註**:本 TDD 從 1390 行單檔重組為 ~180 行索引 + 8 個子檔案。每個子檔案 < 500 行(單一職責),可獨立給 Backend / Reviewer / Testing 不同角色讀對應檔案、減少 context 負擔。
|
||||||
|
|
||||||
> **範圍界定**:本 TDD 前 11 章聚焦 Task Scheduler 對外 API / auth 層(Phase 0.8b)。本章新增,記錄 **Python Worker 轉檔管線** 的一個行為變更 —— 與 API / auth 無關,但同屬本 repo 的架構決策、故收在同一份 TDD 索引,方便 Backend / Reviewer / Testing 讀。詳細實作插入點見 §12.2。
|
|
||||||
|
|
||||||
### 12.1 尾端 Softmax 自動移除(輸出 logits)
|
|
||||||
|
|
||||||
#### 為什麼(背景)
|
|
||||||
|
|
||||||
- **問題**:含尾端 Softmax 的分類模型(如 MobileNet classifier)轉 **KL520** 時,`batch_compile -T 520` 撞 `UnimplementedFeature: undefined CPU op [Softmax]`、**exit 6** 失敗。
|
|
||||||
- **根因**:runtime image(`/app/ktc/onnx_optimizer.py`,kneronnxopt 版)的 `eliminate_tail` 是**閹割版 no-op**(container 內印 `WRANING: eliminate_tail is not available in current conda environment`),所以即使 `onnx2onnx_flow(eliminate_tail=True)` 也砍不掉尾端 Softmax、Softmax 一路帶進 batch_compile 撞 520 不支援的 CPU op。
|
|
||||||
- **對齊正式站**:正式站 `converter.innovedus.com`(另一套 code base)對**同一顆** fixture 能編出真 520 .nef。staging 實測比對正式站 .nef:output = `(1,3,1,1)` 定點量化 logits、**無 Softmax**,且權重段(`wt` 0xc0530)與我們砍掉 Softmax 後產的 .nef **逐 byte 相同**、compiler 版本字串完全相同(`v0.9.1(6d7a863)`)→ 證明正式站就是「模型階段移除 Softmax、logits 落 host 後處理」,這是 Kneron 對 520 的標準做法。
|
|
||||||
|
|
||||||
#### 決策(拍板)
|
|
||||||
|
|
||||||
**onnx 階段(`onnx2onnx_flow` 之後、`onnx.save` 之前)對所有 platform 一律移除尾端 Softmax、輸出 logits,與正式站行為對齊。**
|
|
||||||
|
|
||||||
- **不限 520**:雖然只有 520 會因 Softmax exit 6,但為了「所有 platform 輸出一致 = logits」、與正式站對齊、避免 platform 分歧造成呼叫端困惑,決定**所有 platform 統一移除**。(若日後有「某 platform 需保留 Softmax」的需求,再走 platform 分支,屆時另開 ADR。)
|
|
||||||
- **觸發條件(實作對齊,2026-07-06 backend 實測修訂)**:**只移除 terminal(graph 尾端、無下游節點)的 Softmax;中間層的 Softmax 不移除**。理由見下方「怎麼做」的 `cut_nodes` 說明——`cut_types` 是 cut-from-node 語意,遇到中間層 Softmax 會把整段下游靜默切掉,故改用「terminal-only + `cut_nodes`」精準移除。移除後 logits 自動接成新 graph output。
|
|
||||||
|
|
||||||
#### 怎麼做(實作方式,2026-07-06 backend 實測修訂)
|
|
||||||
|
|
||||||
- **插入點**:`services/workers/onnx/core.py:36-37` 之間(`onnx2onnx_flow(...)` 之後、`onnx.save(model, output_path)` 之前)。
|
|
||||||
- **呼叫方式(backend 實作 + staging 實測可行)**:
|
|
||||||
```python
|
|
||||||
# 注意:runtime 的 ktc.onnx_optimizer 沒有 editor API(remove_nodes_with_types 等)
|
|
||||||
# 必須改呼叫底層 tools.other.remove_nodes
|
|
||||||
import sys
|
|
||||||
sys.path.insert(0, "libs/ONNX_Convertor/optimizer_scripts") # 依 container 掛載點調整
|
|
||||||
from tools import other
|
|
||||||
|
|
||||||
# 只挑「terminal(無下游節點)且 op_type == Softmax」的節點名,用 cut_nodes 精準移除
|
|
||||||
terminal_softmax = [
|
|
||||||
n.name for n in model.graph.node
|
|
||||||
if n.op_type == "Softmax" and _is_terminal(n, model.graph)
|
|
||||||
]
|
|
||||||
if terminal_softmax:
|
|
||||||
other.remove_nodes(model.graph, cut_nodes=terminal_softmax)
|
|
||||||
```
|
|
||||||
(`_is_terminal` 判斷該節點的 output 不是任何其他節點的 input,即 graph 尾端;實際 helper 命名以 backend 實作為準。)
|
|
||||||
- **不要用** `cut_types=["Softmax"]`(原 sketch 寫法,已棄用):`cut_types` 是 cut-from-node 語意,遇到**中間層** Softmax 會把其整段下游節點**靜默切掉**(危險)。改用 `cut_nodes`(明確指定要移除的節點名)+ 只挑 terminal Softmax。對已驗證情境(fixture、尾端 Softmax)兩者行為**逐 byte 等價**,但 `cut_nodes` + terminal-only 在有中間層 Softmax 的模型上才安全。
|
|
||||||
- **不要用**:`ktc.onnx_optimizer.remove_nodes_with_types(...)` —— runtime image 的 `ktc.onnx_optimizer`(kneronnxopt 版)**沒有任何 editor API**;`ktc/onnx_optimizer_1_7.py` 雖存在但不可 import(hardcode `/workspace` 路徑 + 依賴 container 沒有的 `onnx.optimizer`)。
|
|
||||||
- **不要依賴** `eliminate_tail=True` 做尾端清理(runtime 是 no-op)。
|
|
||||||
- job metadata 應標注「Softmax 已移除、需 host 端後處理」提醒呼叫端。
|
|
||||||
|
|
||||||
#### 行為影響(重要,必須傳達給呼叫端)
|
|
||||||
|
|
||||||
- **所有 platform 的 NEF 輸出從「含 Softmax(機率)」變「logits」**。
|
|
||||||
- 呼叫端(visionA-backend)若對轉檔結果做分類後處理(取 argmax 通常不受影響、但取「機率值」會受影響),**需自行在 host 端補 softmax**。
|
|
||||||
- **⚠️ 需通知 visionA 團隊**:這是對外可觀察的輸出語意變更,即使 argmax 分類結果不變、機率數值會變。列為交付前跨團隊溝通項。
|
|
||||||
|
|
||||||
#### 證據連結(個人層,per-branch)
|
|
||||||
|
|
||||||
- 實驗規劃(bug 反推、只讀 code):`.autoflow/05-implementation/tflite-520-experiment-plan-2026-07-06.md`
|
|
||||||
- staging 實測驗證(砍 Softmax → 520 產真 .nef、與正式站逐 byte 比對):`.autoflow/06-testing/reports/tflite-520-softmax-removal-verify-2026-07-06.md`(H1/H2/H3 全綠、含正式站 .nef 比對閉環、backend 實作注意事項)
|
|
||||||
|
|
||||||
### 12.2 pre-check regex 收窄(設計修訂)
|
|
||||||
|
|
||||||
#### 原設計 → 修訂
|
|
||||||
|
|
||||||
`services/backends/precheck.py` 的「不支援 op 早期失敗」pre-check,原設計是「掃到 marker 字樣(regex 命中)就擋」,實測發現會**誤擋**——`hw_not_support_col` 是**欄位名**、被 `hw_not_support` regex 誤命中,導致能編出真 .nef 的乾淨模型也可能被擋。
|
|
||||||
|
|
||||||
**修訂為「訊號 + 具體 op 名稱才擋,抽不到 op 名稱時放行 + warning(fail-open)」:**
|
|
||||||
|
|
||||||
| 項目 | 原設計 | 修訂後 |
|
|
||||||
|------|--------|--------|
|
|
||||||
| `hw_not_support_col`(欄位名誤命中) | 會命中、造成誤擋 | **刪除 / 大幅收窄**該 regex |
|
|
||||||
| `hw_not_support` 的 op capture group | optional(抽不到 op 名也擋) | **改必須**(capture group 必抽到具體 op 名才算命中) |
|
|
||||||
| 抽不到具體 op 名稱時 | 擋(fail-closed) | **放行 + warning**(真 fail-open) |
|
|
||||||
|
|
||||||
#### 為什麼 fail-open
|
|
||||||
|
|
||||||
pre-check 的定位是「早期快速失敗、省 batch_compile 時間」的**優化**,不是**授權邊界**。誤擋(把能轉的擋掉)比漏擋(放行後 batch_compile 自己 fail)代價高。抽不到具體 op 名稱 = 訊號不明確 = 寧可放行讓後段真正的 batch_compile 判定。
|
|
||||||
|
|
||||||
#### 證據連結
|
|
||||||
|
|
||||||
- rootcause 報告 §4a(pre-check 誤擋分析):見 `.autoflow/06-testing/reports/tflite-520-softmax-removal-verify-2026-07-06.md` §5(R3 誤擋風險實測數據點)+ 舊 rootcause 報告 §4a
|
|
||||||
- **同 PR 收窄**:本 regex 修訂與 §12.1 的 Softmax 移除**同一個 PR** 進,避免「Softmax 砍了但 pre-check 還誤擋」的半套狀態。
|
|
||||||
|
|
||||||
### 12.3 實作任務(給 Backend)
|
|
||||||
|
|
||||||
| # | 任務 | 檔案 | 驗收標準 |
|
|
||||||
|---|------|------|---------|
|
|
||||||
| W1 | onnx worker 加「移除尾端 Softmax」步驟 | `services/workers/onnx/core.py:36-37` | 用 `tools.other.remove_nodes(graph, cut_nodes=<terminal Softmax 節點名清單>)`(**只移除 terminal Softmax、中間層不動**;不可用 `cut_types=["Softmax"]`,會誤砍中間層下游);砍後模型過 `onnx.checker`;520 e2e 不再 exit 6、產真 .nef;job metadata 標注 Softmax 已移除 |
|
|
||||||
| W2 | pre-check regex 收窄 | `services/backends/precheck.py` | 刪 / 收窄 `hw_not_support_col`;`hw_not_support` op capture group 改必須;抽不到 op 名改放行 + warning;既有測試不 broken |
|
|
||||||
| W3 | 回歸驗證 | — | 720/530/630/730 既有可轉模型仍能轉(輸出改 logits 但 .nef 有效);含 Softmax 的 520 模型能轉成 |
|
|
||||||
|
|
||||||
> W1 + W2 **同 PR**。W3 交 Testing 做回歸(Prove-It:先寫「520+Softmax 應轉成」的 failing test → W1 修 → 轉綠)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**附註**:本 TDD 從 1390 行單檔重組為 ~180 行索引 + 8 個子檔案。每個子檔案 < 500 行(單一職責),可獨立給 Backend / Reviewer / Testing 不同角色讀對應檔案、減少 context 負擔。§12(2026-07-06 新增)為 worker 轉檔管線行為變更,與 API/auth 層獨立。
|
|
||||||
|
|||||||
@ -17,7 +17,6 @@
|
|||||||
| 2026-04-25 | 初版 Draft;OAuth resource server + promote 設計 | Architect Agent |
|
| 2026-04-25 | 初版 Draft;OAuth resource server + promote 設計 | Architect Agent |
|
||||||
| 2026-04-25 | 原始模型上傳路徑改 multipart 直傳;移除 FAA GET/HEAD 相關 | Architect Agent |
|
| 2026-04-25 | 原始模型上傳路徑改 multipart 直傳;移除 FAA GET/HEAD 相關 | Architect Agent |
|
||||||
| 2026-05-16 | **Phase 0.8b 重寫**:visionA → converter 改 API key;新增 `GET /api/v1/jobs/:id/result` streaming endpoint;保留 converter → FAA OAuth client(promote 用) | Architect Agent |
|
| 2026-05-16 | **Phase 0.8b 重寫**:visionA → converter 改 API key;新增 `GET /api/v1/jobs/:id/result` streaming endpoint;保留 converter → FAA OAuth client(promote 用) | Architect Agent |
|
||||||
| 2026-07-06 | **M 級 worker 行為變更**:onnx worker 對所有 platform 移除尾端 Softmax、輸出 logits(新增 ADR-012);pre-check regex 收窄。影響「輸出語意」(機率 → logits),需通知 visionA | Architect Agent |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -253,7 +252,7 @@ visionA-backend Converter Scheduler MinIO
|
|||||||
| Task Scheduler — routes | **小改**:`POST /jobs` / `GET /jobs` / `GET /jobs/:id` / `POST /jobs/:id/promote` 改掛 `requireApiKey()`;新增 `/jobs/:id/result` 路由 + handler |
|
| Task Scheduler — routes | **小改**:`POST /jobs` / `GET /jobs` / `GET /jobs/:id` / `POST /jobs/:id/promote` 改掛 `requireApiKey()`;新增 `/jobs/:id/result` 路由 + handler |
|
||||||
| Task Scheduler — config | 移除 `MEMBER_CENTER_ISSUER` / `MEMBER_CENTER_JWKS_URL` / `KNERON_CONVERTER_AUDIENCE` / `JWKS_*`;保留 `MEMBER_CENTER_TOKEN_URL` / `KNERON_CONVERTER_CLIENT_ID` / `KNERON_CONVERTER_CLIENT_SECRET` / `FILE_ACCESS_AGENT_*`(promote 用);新增 `CONVERTER_API_KEY` |
|
| Task Scheduler — config | 移除 `MEMBER_CENTER_ISSUER` / `MEMBER_CENTER_JWKS_URL` / `KNERON_CONVERTER_AUDIENCE` / `JWKS_*`;保留 `MEMBER_CENTER_TOKEN_URL` / `KNERON_CONVERTER_CLIENT_ID` / `KNERON_CONVERTER_CLIENT_SECRET` / `FILE_ACCESS_AGENT_*`(promote 用);新增 `CONVERTER_API_KEY` |
|
||||||
| Redis 資料模型 | **不動** |
|
| Redis 資料模型 | **不動** |
|
||||||
| Workers | Phase 0.8b **不動**;**2026-07-06 M 級變更**:onnx worker 加「移除尾端 Softmax、輸出 logits」步驟(所有 platform),詳見 ADR-012 + `TDD.md` §12 |
|
| Workers | **不動** |
|
||||||
| MinIO(Converter Bucket) | **不動** |
|
| MinIO(Converter Bucket) | **不動** |
|
||||||
| FAA / MC | **不動**(converter → FAA 仍走 OAuth client_credentials) |
|
| FAA / MC | **不動**(converter → FAA 仍走 OAuth client_credentials) |
|
||||||
|
|
||||||
@ -526,37 +525,6 @@ Web UI 仍走 `/jobs/*` 路徑、無 auth。Phase 0.8b 不動。
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### ADR-012:onnx worker 對所有 platform 移除尾端 Softmax、輸出 logits(**2026-07-06 新增,M 級**)
|
|
||||||
|
|
||||||
**狀態**:Accepted(2026-07-06 使用者拍板;staging 實測 + 正式站 .nef 逐 byte 比對佐證)
|
|
||||||
|
|
||||||
**背景**:
|
|
||||||
1. 含尾端 Softmax 的分類模型轉 **KL520** 時 `batch_compile -T 520` 撞 `undefined CPU op [Softmax]`、**exit 6** 失敗。
|
|
||||||
2. runtime image 的 `eliminate_tail` 是閹割版 no-op(`WRANING: eliminate_tail is not available in current conda environment`),砍不掉尾端 Softmax。
|
|
||||||
3. 正式站(另一套 code base)對**同一顆** fixture 能編出真 520 .nef —— staging 實測比對正式站 .nef:output 是 `(1,3,1,1)` 定點量化 logits、**無 Softmax**,權重段與我們砍掉 Softmax 後產的 .nef **逐 byte 相同**、compiler 版本字串完全相同 → 正式站就是「模型階段砍 Softmax、logits 落 host」。
|
|
||||||
|
|
||||||
**決定**:onnx 階段(`onnx2onnx_flow` 之後、`onnx.save` 之前,`services/workers/onnx/core.py:36-37`)對**所有 platform** 一律移除尾端 Softmax、輸出 logits。實作用 `libs/ONNX_Convertor/optimizer_scripts/tools/other.remove_nodes(graph, cut_types=["Softmax"])`(runtime `ktc.onnx_optimizer` 無 editor API、不可用 `remove_nodes_with_types`)。同 PR 收窄 `services/backends/precheck.py` 的不支援 op regex(`hw_not_support_col` 欄位名誤命中 → 刪 / 收窄;op capture group 改必須;抽不到 op 名改放行 + warning = fail-open)。
|
|
||||||
|
|
||||||
**理由**:
|
|
||||||
1. 修 520 exit 6(使用者目標「讓 520+Softmax 轉成」)。
|
|
||||||
2. 與正式站行為對齊(正式站就是這樣做,Kneron 對 520 的標準做法)。
|
|
||||||
3. 所有 platform 統一輸出 logits,避免 platform 分歧造成呼叫端困惑。
|
|
||||||
|
|
||||||
**代價 / 行為影響**:
|
|
||||||
- **所有 platform 的 NEF 輸出從「含 Softmax(機率)」變「logits」** —— 對外可觀察的輸出語意變更。
|
|
||||||
- 呼叫端(visionA-backend)若取「機率值」需自行在 host 端補 softmax(argmax 分類結果通常不受影響)。
|
|
||||||
- **需通知 visionA 團隊**(交付前跨團隊溝通項)。
|
|
||||||
|
|
||||||
**替代方案**:
|
|
||||||
- A. 只對 520 移除 Softmax(其他 platform 保留)— 排除:造成 platform 輸出分歧、與正式站不一致、呼叫端要判斷 platform。
|
|
||||||
- B. 換 toolchain 版本讓 520 容忍 Softmax(H3 假設)— 排除:正式站 compiler 版本與我們相同,證明不是版本問題;且換 toolchain 成本高。
|
|
||||||
- C. 調 `eliminate_tail` 讓它砍 Softmax — 排除:runtime 是 no-op,改不動;且 editor API 不可用。
|
|
||||||
- D. onnx 階段用 `tools.other.remove_nodes` 明確移除(**選擇**)。
|
|
||||||
|
|
||||||
**證據**:`.autoflow/06-testing/reports/tflite-520-softmax-removal-verify-2026-07-06.md`(H1/H2/H3 全綠 + 正式站比對閉環)、`.autoflow/05-implementation/tflite-520-experiment-plan-2026-07-06.md`。實作細節見 `TDD.md` §12。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. 風險與待確認事項
|
## 9. 風險與待確認事項
|
||||||
|
|
||||||
| # | 風險 / 議題 | 影響 | 行動 |
|
| # | 風險 / 議題 | 影響 | 行動 |
|
||||||
|
|||||||
@ -11,10 +11,8 @@ op_type:`` 等訊號,toolchain 會把這些寫進 ``model_fx_report``。因此
|
|||||||
(不需要跑完 quantization + batch_compile)就偵測到不支援的 op,fail-fast 並回一個
|
(不需要跑完 quantization + batch_compile)就偵測到不支援的 op,fail-fast 並回一個
|
||||||
使用者看得懂的錯誤,而不是讓 job 跑到 nef 階段撞 C++ backtrace(exit 6)。
|
使用者看得懂的錯誤,而不是讓 job 跑到 nef 階段撞 C++ backtrace(exit 6)。
|
||||||
|
|
||||||
設計原則(真 fail-open、避免誤擋):
|
設計原則(避免誤擋):
|
||||||
- 「明確訊號」= 不支援 marker **加上一個具體的 operator 名稱**。只出現 marker 字樣
|
- 只有在 evaluator 報告中出現「明確的」不支援訊號時才擋。
|
||||||
(例如報告表頭的 "HW not support" 欄位名)但抽不出 op 名稱時 → 放行 + warning,
|
|
||||||
不擋。誤擋能轉的模型(false positive)的代價高於晚一點在 nef 階段失敗。
|
|
||||||
- evaluator 分析失敗(例如環境問題、transient error)而沒有明確不支援訊號時 → 放行,
|
- evaluator 分析失敗(例如環境問題、transient error)而沒有明確不支援訊號時 → 放行,
|
||||||
讓後續流程照舊跑,不因為 pre-check 有疑慮就擋掉本來能跑的模型。
|
讓後續流程照舊跑,不因為 pre-check 有疑慮就擋掉本來能跑的模型。
|
||||||
"""
|
"""
|
||||||
@ -39,74 +37,53 @@ class UnsupportedOperatorError(Exception):
|
|||||||
# 對照來源:vendor/sys_flow*/test_case.py::check_compiler_HardwareNotSupport 與
|
# 對照來源:vendor/sys_flow*/test_case.py::check_compiler_HardwareNotSupport 與
|
||||||
# check_knerex_error 寫入 model_fx_report 的字串。
|
# check_knerex_error 寫入 model_fx_report 的字串。
|
||||||
#
|
#
|
||||||
# 每個 pattern 都帶一個「必須命中」的 capture group 抓 op 名稱片段。
|
# 每個 pattern 盡量帶一個 capture group 用來抓「op 名稱」(抓不到也沒關係,會退回泛用訊息)。
|
||||||
# marker 後面抽不出具體 op 名稱 → 視為弱訊號、放行(見 find_unsupported_operators)。
|
|
||||||
#
|
|
||||||
# 注意:這裡刻意**沒有**「HW not support」欄位名的泛用 pattern —— 那是
|
|
||||||
# model_fx_report 的欄位分類名稱(vendor/sys_flow/test_case.py::model_fx_report),
|
|
||||||
# 會出現在正常報告的表頭,光是出現不代表模型有不支援 op(R3 誤擋根因,
|
|
||||||
# 見 .autoflow/05-implementation/tflite-520-rootcause-2026-07-06.md §3a/§4a)。
|
|
||||||
# 「HW not support: <Op>」帶具體 op 的強訊號仍由 hw_not_support pattern
|
|
||||||
# 涵蓋(IGNORECASE + [_ ] 分隔)。
|
|
||||||
_UNSUPPORTED_MARKERS: tuple[tuple[str, str], ...] = (
|
_UNSUPPORTED_MARKERS: tuple[tuple[str, str], ...] = (
|
||||||
# "creating an EmptyNode instance for op_type: Softmax"
|
# "creating an EmptyNode instance for op_type: Softmax"
|
||||||
("empty_node", r"creating an EmptyNode instance for op_type:\s*([A-Za-z0-9_]+)"),
|
("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: Softmax" / "HardwareNotSupport: Softmax" / "Hardware not support: Softmax"
|
||||||
# op capture group 為必須:marker 後面一定要跟具體 op 名稱才算訊號。
|
("hw_not_support", r"(?:HW[_ ]NOT[_ ]SUPPORT|Hardware\s*not\s*support|HardwareNotSupport)\s*[:\-]?\s*([A-Za-z0-9_]+)?"),
|
||||||
# marker 帶 (?:ED)? 字尾容忍 + \b 詞界:報告若寫 "Hardware not supported: ...",
|
|
||||||
# 字尾 "ed" 必須被 marker 吃掉、不能漏進 capture group 被當成 op 名(誤擋)。
|
|
||||||
("hw_not_support", r"(?:HW[_ ]NOT[_ ]SUPPORT(?:ED)?|Hardware\s*not\s*support(?:ed)?|HardwareNotSupport)\b\s*[:\-]?\s*([A-Za-z0-9_]+)"),
|
|
||||||
# "UNIMPLEMENTED_FEATURE: ..." / "UnimplementedFeature: undefined CPU op [Softmax]"
|
# "UNIMPLEMENTED_FEATURE: ..." / "UnimplementedFeature: undefined CPU op [Softmax]"
|
||||||
("unimplemented", r"(?:UNIMPLEMENTED[_ ]FEATURE|UnimplementedFeature)\s*[:\-]?\s*(.*)"),
|
("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 名稱,例如 "undefined CPU op [Softmax]"。
|
||||||
_OP_IN_BRACKETS = re.compile(r"[\[\(]\s*([A-Za-z][A-Za-z0-9_]*)\s*[\]\)]")
|
_OP_IN_BRACKETS = re.compile(r"[\[\(]\s*([A-Za-z][A-Za-z0-9_]*)\s*[\]\)]")
|
||||||
# 報告表格常見的「非 op」值:欄位名(如 "HW not support")後面接的可能是這些字,
|
# 常見 ONNX op 名稱(PascalCase / 首字母大寫),用來在自由文字裡挑一個像 op 的字。
|
||||||
# 不能把它們當成 operator 名稱(否則又變回誤擋)。
|
_OP_TOKEN = re.compile(r"\b([A-Z][A-Za-z0-9]{2,})\b")
|
||||||
_NON_OP_TOKENS = frozenset({"none", "null", "nan", "na", "n", "true", "false", "yes", "no"})
|
|
||||||
|
|
||||||
|
|
||||||
def _extract_op_name(fragment: Optional[str]) -> Optional[str]:
|
def _extract_op_name(fragment: Optional[str]) -> Optional[str]:
|
||||||
"""從訊息片段中抽出 operator 名稱。抓不到(或抓到的不像 op)就回 None。
|
"""盡力從訊息片段中抽出 operator 名稱。抓不到就回 None。"""
|
||||||
|
|
||||||
只接受兩種**明確**形式:
|
|
||||||
1. 中括號 / 括號包住的 token(例如 "undefined CPU op [Softmax]");
|
|
||||||
2. fragment 本身就是單一 token(例如 hw_not_support capture 到的 "Softmax")。
|
|
||||||
|
|
||||||
刻意**不**在自由文字裡猜「首字母大寫的字」——自由文字(如
|
|
||||||
"UnimplementedFeature: Not supported in this mode")猜出來的字
|
|
||||||
("Not"、"CPU")不是 op 或不是對的 op,會造成誤擋 / 錯誤訊息;
|
|
||||||
抓不到就交給呼叫端 fail-open。
|
|
||||||
"""
|
|
||||||
if not fragment:
|
if not fragment:
|
||||||
return None
|
return None
|
||||||
fragment = fragment.strip()
|
fragment = fragment.strip()
|
||||||
if not fragment:
|
if not fragment:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
candidate: Optional[str] = None
|
|
||||||
|
|
||||||
m = _OP_IN_BRACKETS.search(fragment)
|
m = _OP_IN_BRACKETS.search(fragment)
|
||||||
if m:
|
if m:
|
||||||
candidate = m.group(1)
|
return m.group(1)
|
||||||
elif re.fullmatch(r"[A-Za-z][A-Za-z0-9_]*", fragment):
|
|
||||||
# 直接就是一個 op token(例如 "Softmax")
|
|
||||||
candidate = fragment
|
|
||||||
|
|
||||||
if candidate and candidate.lower() in _NON_OP_TOKENS:
|
# 直接就是一個 op token(例如 "Softmax")
|
||||||
return None
|
if re.fullmatch(r"[A-Za-z][A-Za-z0-9_]*", fragment):
|
||||||
return candidate
|
return fragment
|
||||||
|
|
||||||
|
m = _OP_TOKEN.search(fragment)
|
||||||
|
if m:
|
||||||
|
return m.group(1)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def find_unsupported_operators(report: Optional[str]) -> List[str]:
|
def find_unsupported_operators(report: Optional[str]) -> List[str]:
|
||||||
"""掃描 evaluator 報告字串,回傳偵測到的「不支援 operator 名稱」清單。
|
"""掃描 evaluator 報告字串,回傳偵測到的「不支援 operator 名稱」清單。
|
||||||
|
|
||||||
- 沒有偵測到任何明確不支援訊號 → 回空 list(呼叫端應放行)。
|
- 沒有偵測到任何明確不支援訊號 → 回空 list(呼叫端應放行)。
|
||||||
- 偵測到 marker 字樣但抽不出具體 op 名稱 → **放行**(回空 list)+ warning log。
|
- 偵測到訊號但抽不出 op 名稱 → 回含一個占位符 ``"<unknown>"`` 的 list,
|
||||||
marker 字樣可能只是報告表頭的欄位名(例如 "HW not support" 欄),
|
讓呼叫端仍能 fail-fast(但訊息較泛用)。
|
||||||
光是出現不足以斷定模型有不支援 op;擋錯的代價(誤擋能轉的模型)
|
|
||||||
比晚一點在 nef 階段失敗更高(真 fail-open)。
|
|
||||||
"""
|
"""
|
||||||
if not report:
|
if not report:
|
||||||
return []
|
return []
|
||||||
@ -125,17 +102,14 @@ def find_unsupported_operators(report: Optional[str]) -> List[str]:
|
|||||||
if found:
|
if found:
|
||||||
return found
|
return found
|
||||||
if saw_marker:
|
if saw_marker:
|
||||||
logger.warning(
|
# 有明確不支援訊號、但抽不到 op 名稱:仍回報(用占位符)。
|
||||||
"Pre-check saw an unsupported-op marker but could not extract a concrete "
|
return ["<unknown>"]
|
||||||
"operator name; treating as inconclusive and allowing the job to proceed "
|
|
||||||
"(fail-open)."
|
|
||||||
)
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def build_error_message(platform: str, ops: Iterable[str]) -> str:
|
def build_error_message(platform: str, ops: Iterable[str]) -> str:
|
||||||
"""產生使用者看得懂的錯誤訊息。"""
|
"""產生使用者看得懂的錯誤訊息。"""
|
||||||
op_list = [o for o in ops if o]
|
op_list = [o for o in ops if o and o != "<unknown>"]
|
||||||
if op_list:
|
if op_list:
|
||||||
ops_text = ", ".join(op_list)
|
ops_text = ", ".join(op_list)
|
||||||
op_clause = f"operator {ops_text}"
|
op_clause = f"operator {ops_text}"
|
||||||
|
|||||||
@ -1,165 +1,8 @@
|
|||||||
import logging
|
|
||||||
import os
|
import os
|
||||||
import sys
|
from typing import Dict, Any
|
||||||
from typing import Any, Dict, List
|
|
||||||
|
|
||||||
import onnx
|
import onnx
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
# libs/ONNX_Convertor/optimizer_scripts 的路徑(repo 與 container /app 的 layout 相同,
|
|
||||||
# 都是「repo root 底下的 libs/...」,所以用 core.py 的相對位置回推即可兩邊通用)。
|
|
||||||
#
|
|
||||||
# 為什麼直接用 libs 底層的 tools.other、而不是 ktc.onnx_optimizer 的 editor API:
|
|
||||||
# runtime image 的 ktc.onnx_optimizer 是 kneronnxopt 版、沒有任何 editor API
|
|
||||||
# (remove_nodes_with_types / cut_graph_from_nodes 都不存在);ktc/onnx_optimizer_1_7.py
|
|
||||||
# 則因 hardcode /workspace 路徑 + onnx>=1.9 移除 onnx.optimizer 而無法 import。
|
|
||||||
# 實測驗證:.autoflow/06-testing/reports/tflite-520-softmax-removal-verify-2026-07-06.md §4
|
|
||||||
_REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
|
|
||||||
_OPTIMIZER_SCRIPTS_DIR = os.path.join(_REPO_ROOT, "libs", "ONNX_Convertor", "optimizer_scripts")
|
|
||||||
|
|
||||||
|
|
||||||
def _ensure_optimizer_scripts_path() -> None:
|
|
||||||
"""把 optimizer_scripts 加進 sys.path(只加一次、避免重複 append)。"""
|
|
||||||
if _OPTIMIZER_SCRIPTS_DIR not in sys.path:
|
|
||||||
sys.path.insert(0, _OPTIMIZER_SCRIPTS_DIR)
|
|
||||||
|
|
||||||
|
|
||||||
def _find_terminal_softmax_nodes(graph) -> List[Any]:
|
|
||||||
"""回傳 graph 中所有「尾端(terminal)」的 Softmax 節點。
|
|
||||||
|
|
||||||
terminal 定義:該節點的所有 output 都沒有被任何其他節點當 input 消費
|
|
||||||
(沒有 children;典型情境是 Softmax output 直接綁 graph output)。
|
|
||||||
|
|
||||||
已知限制:只掃描頂層 graph 節點的 input,不掃 If/Loop 等 control-flow
|
|
||||||
節點的子圖(subgraph)內的引用;若 Softmax output 只被子圖消費會被誤判為
|
|
||||||
terminal。目前轉檔管線(tflite2onnx / onnx2onnx_flow 產物)不會產生
|
|
||||||
control-flow 節點,暫不處理。
|
|
||||||
"""
|
|
||||||
consumed_names = set()
|
|
||||||
for node in graph.node:
|
|
||||||
consumed_names.update(node.input)
|
|
||||||
return [
|
|
||||||
node
|
|
||||||
for node in graph.node
|
|
||||||
if node.op_type == "Softmax"
|
|
||||||
and not any(out in consumed_names for out in node.output)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def remove_tail_softmax(model: onnx.ModelProto) -> List[str]:
|
|
||||||
"""移除 graph 尾端的 Softmax 節點,讓模型輸出 logits(Softmax 落 host 端後處理)。
|
|
||||||
|
|
||||||
背景:KL520 的 batch_compile 對 Softmax 是硬 reject(exit 6、無 CPU fallback),
|
|
||||||
而 runtime 的 onnx2onnx_flow(eliminate_tail=True) 是閹割版 no-op(log 會印
|
|
||||||
"eliminate_tail is not available"),不會清掉尾端 Softmax。正式站的行為是在
|
|
||||||
模型階段移除尾端 Softmax、NEF 輸出 logits——這裡對所有 platform 一律對齊該行為。
|
|
||||||
|
|
||||||
只移除 terminal 的 Softmax:底層 tools.other.remove_nodes 是「cut-from-node」語意
|
|
||||||
(砍掉指定節點與其後所有下游節點),對中間層的 Softmax 使用會把後半個模型一起
|
|
||||||
切掉,因此非 terminal 的 Softmax 不動(留給 pre-check / batch_compile 決定)。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
model: 已經過 onnx2onnx_flow 的 ONNX model。會被就地修改。
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
被移除的 Softmax 節點名稱清單(無尾端 Softmax 時回空 list、模型不變)。
|
|
||||||
"""
|
|
||||||
terminal_nodes = _find_terminal_softmax_nodes(model.graph)
|
|
||||||
total_softmax = sum(1 for n in model.graph.node if n.op_type == "Softmax")
|
|
||||||
|
|
||||||
if not terminal_nodes:
|
|
||||||
if total_softmax:
|
|
||||||
logger.warning(
|
|
||||||
"Graph contains %d non-terminal Softmax node(s); not removed "
|
|
||||||
"(cut-from-node removal would truncate downstream nodes).",
|
|
||||||
total_softmax,
|
|
||||||
)
|
|
||||||
return []
|
|
||||||
|
|
||||||
# remove_nodes 的 cut_nodes 用 node.name 比對;防禦性處理沒有名字的節點,
|
|
||||||
# 並確保 fallback 名稱不與既有節點撞名(撞名會誤砍其他節點)。
|
|
||||||
existing_names = {n.name for n in model.graph.node if n.name}
|
|
||||||
fallback_index = 0
|
|
||||||
for node in terminal_nodes:
|
|
||||||
if not node.name:
|
|
||||||
while f"tail_softmax_{fallback_index}" in existing_names:
|
|
||||||
fallback_index += 1
|
|
||||||
node.name = f"tail_softmax_{fallback_index}"
|
|
||||||
existing_names.add(node.name)
|
|
||||||
|
|
||||||
_ensure_optimizer_scripts_path()
|
|
||||||
from tools import other # libs/ONNX_Convertor/optimizer_scripts/tools/other.py
|
|
||||||
|
|
||||||
# 防呆(silent import shadow):sys.path.insert(0) 讓通用名 "tools" 進程級
|
|
||||||
# 最優先;若進程先前已 import 過別的 "tools" package,這裡會拿到快取的錯誤
|
|
||||||
# 模組。明確驗證來源路徑、不對就 fail loud。
|
|
||||||
other_file = os.path.abspath(getattr(other, "__file__", "") or "")
|
|
||||||
if not other_file.startswith(_OPTIMIZER_SCRIPTS_DIR + os.sep):
|
|
||||||
raise RuntimeError(
|
|
||||||
"Imported 'tools.other' does not come from the expected optimizer_scripts "
|
|
||||||
f"directory (got: {other_file!r}, expected under: {_OPTIMIZER_SCRIPTS_DIR!r}); "
|
|
||||||
"another 'tools' package is shadowing it."
|
|
||||||
)
|
|
||||||
|
|
||||||
# 移除前:記錄每個被移除節點「餵進 graph output 的 output 名」與其 input 名,
|
|
||||||
# 供移除後驗證 output 重接沒有靜默遺失。remove_nodes 的重接依賴 value_info
|
|
||||||
# 內有 logits 條目(onnx2onnx_flow 的 shape inference 會補);缺失時該 output
|
|
||||||
# 會被直接 Abandon(other.py 的 output reorder 邏輯)——多 output 模型只掉
|
|
||||||
# 一個 output 時 graph.output 仍非空、checker 也照過,必須逐一驗證。
|
|
||||||
graph_output_names = {o.name for o in model.graph.output}
|
|
||||||
expected_rewires = [
|
|
||||||
(node.name, out_name, list(node.input))
|
|
||||||
for node in terminal_nodes
|
|
||||||
for out_name in node.output
|
|
||||||
if out_name in graph_output_names
|
|
||||||
]
|
|
||||||
|
|
||||||
removed_names = [n.name for n in terminal_nodes]
|
|
||||||
# 防呆(同名節點):cut_nodes 靠名字比對;若 graph 內有其他節點與被移除的
|
|
||||||
# Softmax 同名,remove_nodes 會把它(與其下游)一併砍掉。逐名驗證唯一性。
|
|
||||||
for name in removed_names:
|
|
||||||
matches = sum(1 for n in model.graph.node if n.name == name)
|
|
||||||
if matches != 1:
|
|
||||||
raise RuntimeError(
|
|
||||||
f"Cannot remove tail Softmax: node name {name!r} matches "
|
|
||||||
f"{matches} nodes in the graph (names must be unique for "
|
|
||||||
"cut_nodes-based removal); aborting to avoid cutting unrelated nodes."
|
|
||||||
)
|
|
||||||
# remove_nodes 會把被砍節點的輸入(logits)自動接成新的 graph output
|
|
||||||
#(find_first_sequential_output + output_mapping 邏輯,staging 已實測)。
|
|
||||||
other.remove_nodes(model.graph, cut_nodes=list(removed_names))
|
|
||||||
|
|
||||||
if not model.graph.output:
|
|
||||||
# 極端情況:所有 output 都被 Abandon。模型已壞,直接 fail loud。
|
|
||||||
raise RuntimeError(
|
|
||||||
"Tail Softmax removal produced a graph with no outputs "
|
|
||||||
f"(removed nodes: {removed_names}); aborting instead of saving a broken model."
|
|
||||||
)
|
|
||||||
# 逐一驗證:原本由被移除 Softmax 餵的 graph output,其 input(logits)必須
|
|
||||||
# 出現在新的 graph output,否則該 output 被靜默丟掉了 → fail loud。
|
|
||||||
new_output_names = {o.name for o in model.graph.output}
|
|
||||||
for node_name, original_output, input_names in expected_rewires:
|
|
||||||
if not any(name in new_output_names for name in input_names):
|
|
||||||
raise RuntimeError(
|
|
||||||
f"Tail Softmax removal silently dropped graph output "
|
|
||||||
f"'{original_output}' (removed node: {node_name}; expected one of its "
|
|
||||||
f"inputs {input_names} to become a graph output). This usually means "
|
|
||||||
"the logits tensor is missing from value_info; aborting instead of "
|
|
||||||
"saving a broken model."
|
|
||||||
)
|
|
||||||
|
|
||||||
# 砍後模型必須仍是合法 ONNX(TDD §12.3 W1 驗收標準)。不合法就直接失敗,
|
|
||||||
# 不要把壞模型往 bie/nef 送。只在真的有移除時檢查(no-op 路徑零成本)。
|
|
||||||
try:
|
|
||||||
onnx.checker.check_model(model)
|
|
||||||
except Exception as exc:
|
|
||||||
raise RuntimeError(
|
|
||||||
"Model failed ONNX validation after tail-Softmax removal "
|
|
||||||
f"(removed: {removed_names}): {exc}"
|
|
||||||
) from exc
|
|
||||||
return removed_names
|
|
||||||
|
|
||||||
|
|
||||||
def process_onnx_core(
|
def process_onnx_core(
|
||||||
input_paths: Dict[str, str],
|
input_paths: Dict[str, str],
|
||||||
@ -191,33 +34,18 @@ def process_onnx_core(
|
|||||||
raise ValueError("Unsupported file type. Only ONNX and TFLite files are supported.")
|
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)
|
model = ktc.onnx_optimizer.onnx2onnx_flow(model, eliminate_tail=True, opt_matmul=True)
|
||||||
|
|
||||||
# 尾端 Softmax 移除(所有 platform 一律;與正式站行為對齊、輸出 logits)。
|
|
||||||
# 詳見 remove_tail_softmax docstring 與
|
|
||||||
# .autoflow/06-testing/reports/tflite-520-softmax-removal-verify-2026-07-06.md
|
|
||||||
removed_softmax = remove_tail_softmax(model)
|
|
||||||
if removed_softmax:
|
|
||||||
logger.info(
|
|
||||||
"Removed %d tail Softmax node(s) %s; model outputs are now logits "
|
|
||||||
"(apply Softmax as host-side post-processing).",
|
|
||||||
len(removed_softmax),
|
|
||||||
removed_softmax,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.info("Tail Softmax removal: no-op (no terminal Softmax in graph).")
|
|
||||||
|
|
||||||
onnx.save(model, output_path)
|
onnx.save(model, output_path)
|
||||||
|
|
||||||
# Platform / operator 相容性 pre-check(fail-fast)。
|
# Platform / operator 相容性 pre-check(fail-fast)。
|
||||||
# 在送 bie/nef 之前,用 IP Evaluator 偵測目標 platform 不支援的 op,
|
# 在送 bie/nef 之前,用 IP Evaluator 偵測目標 platform 不支援的 op(如 Softmax@520),
|
||||||
# 避免白跑完 bie + nef 最後才在 batch_compile 撞 C++ backtrace(exit 6)。
|
# 避免白跑完 bie + nef 最後才在 batch_compile 撞 C++ backtrace(exit 6)。
|
||||||
# 預設開啟;偵測到「明確不支援」才擋,偵測不到則放行(不誤擋本來能跑的模型)。
|
# 預設開啟;偵測到「明確不支援」才擋,偵測不到則放行(不誤擋本來能跑的模型)。
|
||||||
#
|
#
|
||||||
# pre-check 內部會跑一次 evaluate()(compiler frontend 分析、成本高)。若下方
|
# pre-check 內部會跑一次 evaluate()(compiler frontend 分析、成本高)。若下方
|
||||||
# enable_evaluate 也開,直接重用這份報告,避免對同一模型跑兩次 evaluate()。
|
# enable_evaluate 也開,直接重用這份報告,避免對同一模型跑兩次 evaluate()。
|
||||||
precheck_enabled = parameters.get("enable_precheck", True)
|
precheck_ran = parameters.get("enable_precheck", True)
|
||||||
precheck_report = None
|
precheck_report = None
|
||||||
if precheck_enabled:
|
if precheck_ran:
|
||||||
from services.backends.precheck import run_precheck
|
from services.backends.precheck import run_precheck
|
||||||
|
|
||||||
precheck_report = run_precheck(
|
precheck_report = run_precheck(
|
||||||
@ -229,7 +57,7 @@ def process_onnx_core(
|
|||||||
|
|
||||||
eval_result = ""
|
eval_result = ""
|
||||||
if parameters.get("enable_evaluate", False):
|
if parameters.get("enable_evaluate", False):
|
||||||
if precheck_enabled:
|
if precheck_ran:
|
||||||
# 重用 pre-check 已經跑過的 evaluate() 報告,不再重跑。
|
# 重用 pre-check 已經跑過的 evaluate() 報告,不再重跑。
|
||||||
# precheck_report 為 None 代表 evaluate() 當時 raise 但無不支援訊號
|
# precheck_report 為 None 代表 evaluate() 當時 raise 但無不支援訊號
|
||||||
# (已在 pre-check 放行)→ 沒有可用報告,維持空字串。
|
# (已在 pre-check 放行)→ 沒有可用報告,維持空字串。
|
||||||
@ -250,9 +78,6 @@ def process_onnx_core(
|
|||||||
"file_path": output_path,
|
"file_path": output_path,
|
||||||
"file_size": os.path.getsize(output_path),
|
"file_size": os.path.getsize(output_path),
|
||||||
"eval_report": eval_result,
|
"eval_report": eval_result,
|
||||||
# 尾端 Softmax 被移除時,這裡列出節點名稱,提示呼叫端「模型輸出是 logits、
|
|
||||||
# Softmax 需在 host 端後處理」。沒移除時為空 list。
|
|
||||||
"removed_tail_softmax": removed_softmax,
|
|
||||||
"model_info": {
|
"model_info": {
|
||||||
"model_id": parameters["model_id"],
|
"model_id": parameters["model_id"],
|
||||||
"version": parameters["version"],
|
"version": parameters["version"],
|
||||||
|
|||||||
@ -1,270 +0,0 @@
|
|||||||
"""Unit tests for tail-Softmax removal in the ONNX worker.
|
|
||||||
|
|
||||||
這些測試需要 ``onnx``(建小型手工 graph)但**不需要 ktc / toolchain**:
|
|
||||||
`remove_tail_softmax` 走的是 repo 內 libs/ONNX_Convertor/optimizer_scripts 的
|
|
||||||
``tools.other.remove_nodes``(純 python + onnx),可以在本機直接驗。
|
|
||||||
|
|
||||||
模型結構模擬 staging 實測的情境(.autoflow/06-testing/reports/
|
|
||||||
tflite-520-softmax-removal-verify-2026-07-06.md):Gemm(logits) → Softmax(terminal)。
|
|
||||||
注意:remove_nodes 的 output 重接依賴 value_info 內有 logits 條目(production
|
|
||||||
flow 由 onnx2onnx_flow 的 shape inference 補齊),因此手工 graph 要自帶 value_info。
|
|
||||||
"""
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
onnx = pytest.importorskip("onnx")
|
|
||||||
|
|
||||||
from onnx import TensorProto, helper # noqa: E402 (after importorskip)
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
|
||||||
sys.path.insert(0, str(ROOT))
|
|
||||||
|
|
||||||
from services.workers.onnx.core import remove_tail_softmax # noqa: E402
|
|
||||||
|
|
||||||
|
|
||||||
def _make_model_with_tail_softmax(softmax_name="tail_softmax"):
|
|
||||||
"""input -> Gemm(logits) -> Softmax -> output,Softmax 為 terminal。"""
|
|
||||||
gemm = helper.make_node(
|
|
||||||
"Gemm", inputs=["x", "w"], outputs=["logits"], name="gemm0"
|
|
||||||
)
|
|
||||||
softmax = helper.make_node(
|
|
||||||
"Softmax", inputs=["logits"], outputs=["probs"], name=softmax_name
|
|
||||||
)
|
|
||||||
graph = helper.make_graph(
|
|
||||||
nodes=[gemm, softmax],
|
|
||||||
name="tail_softmax_graph",
|
|
||||||
inputs=[
|
|
||||||
helper.make_tensor_value_info("x", TensorProto.FLOAT, [1, 4]),
|
|
||||||
helper.make_tensor_value_info("w", TensorProto.FLOAT, [4, 3]),
|
|
||||||
],
|
|
||||||
outputs=[helper.make_tensor_value_info("probs", TensorProto.FLOAT, [1, 3])],
|
|
||||||
value_info=[
|
|
||||||
# logits 的 value_info:remove_nodes 重接 output 時需要(production
|
|
||||||
# 由 shape inference 補、這裡手工補)。
|
|
||||||
helper.make_tensor_value_info("logits", TensorProto.FLOAT, [1, 3]),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
return helper.make_model(graph, opset_imports=[helper.make_opsetid("", 13)])
|
|
||||||
|
|
||||||
|
|
||||||
def _make_model_without_softmax():
|
|
||||||
"""input -> Gemm -> Relu -> output,完全沒有 Softmax。"""
|
|
||||||
gemm = helper.make_node("Gemm", inputs=["x", "w"], outputs=["logits"], name="gemm0")
|
|
||||||
relu = helper.make_node("Relu", inputs=["logits"], outputs=["y"], name="relu0")
|
|
||||||
graph = helper.make_graph(
|
|
||||||
nodes=[gemm, relu],
|
|
||||||
name="no_softmax_graph",
|
|
||||||
inputs=[
|
|
||||||
helper.make_tensor_value_info("x", TensorProto.FLOAT, [1, 4]),
|
|
||||||
helper.make_tensor_value_info("w", TensorProto.FLOAT, [4, 3]),
|
|
||||||
],
|
|
||||||
outputs=[helper.make_tensor_value_info("y", TensorProto.FLOAT, [1, 3])],
|
|
||||||
value_info=[
|
|
||||||
helper.make_tensor_value_info("logits", TensorProto.FLOAT, [1, 3]),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
return helper.make_model(graph, opset_imports=[helper.make_opsetid("", 13)])
|
|
||||||
|
|
||||||
|
|
||||||
def _make_model_with_tail_softmax_missing_logits_value_info():
|
|
||||||
"""同 _make_model_with_tail_softmax、但刻意不給 logits 的 value_info。
|
|
||||||
|
|
||||||
remove_nodes 的 output 重接依賴 value_info;缺失時該 output 會被 Abandon
|
|
||||||
→ 單一 output 模型變成 0 個 output → guard 必須 raise。
|
|
||||||
"""
|
|
||||||
gemm = helper.make_node("Gemm", inputs=["x", "w"], outputs=["logits"], name="gemm0")
|
|
||||||
softmax = helper.make_node(
|
|
||||||
"Softmax", inputs=["logits"], outputs=["probs"], name="tail_softmax"
|
|
||||||
)
|
|
||||||
graph = helper.make_graph(
|
|
||||||
nodes=[gemm, softmax],
|
|
||||||
name="missing_value_info_graph",
|
|
||||||
inputs=[
|
|
||||||
helper.make_tensor_value_info("x", TensorProto.FLOAT, [1, 4]),
|
|
||||||
helper.make_tensor_value_info("w", TensorProto.FLOAT, [4, 3]),
|
|
||||||
],
|
|
||||||
outputs=[helper.make_tensor_value_info("probs", TensorProto.FLOAT, [1, 3])],
|
|
||||||
# 刻意不給 value_info
|
|
||||||
)
|
|
||||||
return helper.make_model(graph, opset_imports=[helper.make_opsetid("", 13)])
|
|
||||||
|
|
||||||
|
|
||||||
def _make_multi_output_model_with_tail_softmax(with_logits_value_info=True):
|
|
||||||
"""雙 output 模型:
|
|
||||||
branch 1: x -> Gemm(logits) -> Softmax -> probs(graph output 1)
|
|
||||||
branch 2: x2 -> Relu -> y(graph output 2、與 Softmax 無關)
|
|
||||||
"""
|
|
||||||
gemm = helper.make_node("Gemm", inputs=["x", "w"], outputs=["logits"], name="gemm0")
|
|
||||||
softmax = helper.make_node(
|
|
||||||
"Softmax", inputs=["logits"], outputs=["probs"], name="tail_softmax"
|
|
||||||
)
|
|
||||||
relu = helper.make_node("Relu", inputs=["x2"], outputs=["y"], name="relu0")
|
|
||||||
value_info = (
|
|
||||||
[helper.make_tensor_value_info("logits", TensorProto.FLOAT, [1, 3])]
|
|
||||||
if with_logits_value_info
|
|
||||||
else []
|
|
||||||
)
|
|
||||||
graph = helper.make_graph(
|
|
||||||
nodes=[gemm, softmax, relu],
|
|
||||||
name="multi_output_graph",
|
|
||||||
inputs=[
|
|
||||||
helper.make_tensor_value_info("x", TensorProto.FLOAT, [1, 4]),
|
|
||||||
helper.make_tensor_value_info("w", TensorProto.FLOAT, [4, 3]),
|
|
||||||
helper.make_tensor_value_info("x2", TensorProto.FLOAT, [1, 3]),
|
|
||||||
],
|
|
||||||
outputs=[
|
|
||||||
helper.make_tensor_value_info("probs", TensorProto.FLOAT, [1, 3]),
|
|
||||||
helper.make_tensor_value_info("y", TensorProto.FLOAT, [1, 3]),
|
|
||||||
],
|
|
||||||
value_info=value_info,
|
|
||||||
)
|
|
||||||
return helper.make_model(graph, opset_imports=[helper.make_opsetid("", 13)])
|
|
||||||
|
|
||||||
|
|
||||||
def _make_model_with_softmax_output_also_consumed():
|
|
||||||
"""Softmax 的 output 同時是 graph output、又被另一個節點消費:
|
|
||||||
x -> Softmax -> probs(graph output 1)、probs -> Relu -> y(graph output 2)。
|
|
||||||
有 children → 非 terminal → 不可移除。
|
|
||||||
"""
|
|
||||||
softmax = helper.make_node("Softmax", inputs=["x"], outputs=["probs"], name="softmax0")
|
|
||||||
relu = helper.make_node("Relu", inputs=["probs"], outputs=["y"], name="relu0")
|
|
||||||
graph = helper.make_graph(
|
|
||||||
nodes=[softmax, relu],
|
|
||||||
name="output_also_consumed_graph",
|
|
||||||
inputs=[helper.make_tensor_value_info("x", TensorProto.FLOAT, [1, 3])],
|
|
||||||
outputs=[
|
|
||||||
helper.make_tensor_value_info("probs", TensorProto.FLOAT, [1, 3]),
|
|
||||||
helper.make_tensor_value_info("y", TensorProto.FLOAT, [1, 3]),
|
|
||||||
],
|
|
||||||
value_info=[
|
|
||||||
helper.make_tensor_value_info("probs", TensorProto.FLOAT, [1, 3]),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
return helper.make_model(graph, opset_imports=[helper.make_opsetid("", 13)])
|
|
||||||
|
|
||||||
|
|
||||||
def _make_model_with_mid_graph_softmax():
|
|
||||||
"""input -> Softmax -> Relu -> output,Softmax 不是 terminal(有 children)。"""
|
|
||||||
softmax = helper.make_node(
|
|
||||||
"Softmax", inputs=["x"], outputs=["probs"], name="mid_softmax"
|
|
||||||
)
|
|
||||||
relu = helper.make_node("Relu", inputs=["probs"], outputs=["y"], name="relu0")
|
|
||||||
graph = helper.make_graph(
|
|
||||||
nodes=[softmax, relu],
|
|
||||||
name="mid_softmax_graph",
|
|
||||||
inputs=[helper.make_tensor_value_info("x", TensorProto.FLOAT, [1, 3])],
|
|
||||||
outputs=[helper.make_tensor_value_info("y", TensorProto.FLOAT, [1, 3])],
|
|
||||||
value_info=[
|
|
||||||
helper.make_tensor_value_info("probs", TensorProto.FLOAT, [1, 3]),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
return helper.make_model(graph, opset_imports=[helper.make_opsetid("", 13)])
|
|
||||||
|
|
||||||
|
|
||||||
def test_remove_tail_softmax_removes_terminal_softmax_and_rewires_logits():
|
|
||||||
model = _make_model_with_tail_softmax()
|
|
||||||
|
|
||||||
removed = remove_tail_softmax(model)
|
|
||||||
|
|
||||||
assert removed == ["tail_softmax"]
|
|
||||||
# Softmax 已不在 graph
|
|
||||||
assert all(n.op_type != "Softmax" for n in model.graph.node)
|
|
||||||
# graph output 自動重接成 logits(Softmax 的輸入)
|
|
||||||
assert [o.name for o in model.graph.output] == ["logits"]
|
|
||||||
# 砍完的模型仍是合法 ONNX
|
|
||||||
onnx.checker.check_model(model)
|
|
||||||
|
|
||||||
|
|
||||||
def test_remove_tail_softmax_is_noop_when_no_softmax():
|
|
||||||
model = _make_model_without_softmax()
|
|
||||||
nodes_before = [(n.name, n.op_type) for n in model.graph.node]
|
|
||||||
outputs_before = [o.name for o in model.graph.output]
|
|
||||||
|
|
||||||
removed = remove_tail_softmax(model)
|
|
||||||
|
|
||||||
assert removed == []
|
|
||||||
# graph 完全不變(絕不能壞既有 onnx/bie 輸入的 flow)
|
|
||||||
assert [(n.name, n.op_type) for n in model.graph.node] == nodes_before
|
|
||||||
assert [o.name for o in model.graph.output] == outputs_before
|
|
||||||
|
|
||||||
|
|
||||||
def test_remove_tail_softmax_keeps_mid_graph_softmax():
|
|
||||||
# 非 terminal 的 Softmax 不移除:remove_nodes 是 cut-from-node 語意,
|
|
||||||
# 對中間層 Softmax 使用會把下游一併切掉。
|
|
||||||
model = _make_model_with_mid_graph_softmax()
|
|
||||||
nodes_before = [(n.name, n.op_type) for n in model.graph.node]
|
|
||||||
|
|
||||||
removed = remove_tail_softmax(model)
|
|
||||||
|
|
||||||
assert removed == []
|
|
||||||
assert [(n.name, n.op_type) for n in model.graph.node] == nodes_before
|
|
||||||
assert any(n.op_type == "Softmax" for n in model.graph.node)
|
|
||||||
|
|
||||||
|
|
||||||
def test_remove_tail_softmax_handles_unnamed_softmax_node():
|
|
||||||
# 節點沒有名字時要有 fallback 命名(cut_nodes 靠 node.name 比對)。
|
|
||||||
model = _make_model_with_tail_softmax(softmax_name="")
|
|
||||||
|
|
||||||
removed = remove_tail_softmax(model)
|
|
||||||
|
|
||||||
assert len(removed) == 1
|
|
||||||
assert removed[0] # 有給 fallback 名稱
|
|
||||||
assert all(n.op_type != "Softmax" for n in model.graph.node)
|
|
||||||
assert [o.name for o in model.graph.output] == ["logits"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_remove_tail_softmax_raises_when_all_outputs_dropped():
|
|
||||||
# logits 缺 value_info → remove_nodes 把唯一的 output Abandon
|
|
||||||
# → graph 剩 0 個 output → 必須 raise、不能存壞模型。
|
|
||||||
model = _make_model_with_tail_softmax_missing_logits_value_info()
|
|
||||||
|
|
||||||
with pytest.raises(RuntimeError, match="no outputs"):
|
|
||||||
remove_tail_softmax(model)
|
|
||||||
|
|
||||||
|
|
||||||
def test_remove_tail_softmax_multi_output_rewires_and_keeps_unrelated_output():
|
|
||||||
# 多 output 模型:Softmax 餵的 output 重接成 logits、無關的 output 保留。
|
|
||||||
model = _make_multi_output_model_with_tail_softmax(with_logits_value_info=True)
|
|
||||||
|
|
||||||
removed = remove_tail_softmax(model)
|
|
||||||
|
|
||||||
assert removed == ["tail_softmax"]
|
|
||||||
assert all(n.op_type != "Softmax" for n in model.graph.node)
|
|
||||||
assert {o.name for o in model.graph.output} == {"logits", "y"}
|
|
||||||
onnx.checker.check_model(model)
|
|
||||||
|
|
||||||
|
|
||||||
def test_remove_tail_softmax_multi_output_raises_on_silently_dropped_output():
|
|
||||||
# 多 output 模型 + logits 缺 value_info:Softmax 餵的 output 被 Abandon、
|
|
||||||
# 但另一個 output 還在(graph.output 非空、checker 也會過)——
|
|
||||||
# 逐 output 驗證的 guard 必須抓到並 raise(Major-3)。
|
|
||||||
model = _make_multi_output_model_with_tail_softmax(with_logits_value_info=False)
|
|
||||||
|
|
||||||
with pytest.raises(RuntimeError, match="silently dropped"):
|
|
||||||
remove_tail_softmax(model)
|
|
||||||
|
|
||||||
|
|
||||||
def test_remove_tail_softmax_raises_on_duplicate_node_name():
|
|
||||||
# cut_nodes 靠名字比對:病態的同名節點(Softmax 與其他節點撞名)
|
|
||||||
# 會導致 remove_nodes 誤砍無關節點 → 必須 raise 而非靜默砍。
|
|
||||||
model = _make_model_with_tail_softmax(softmax_name="dup")
|
|
||||||
model.graph.node[0].name = "dup" # gemm0 改成與 Softmax 同名
|
|
||||||
|
|
||||||
with pytest.raises(RuntimeError, match="matches 2 nodes"):
|
|
||||||
remove_tail_softmax(model)
|
|
||||||
|
|
||||||
|
|
||||||
def test_remove_tail_softmax_keeps_softmax_whose_output_is_also_consumed():
|
|
||||||
# Softmax output 同時是 graph output 又被其他節點消費 → 非 terminal、不移除。
|
|
||||||
model = _make_model_with_softmax_output_also_consumed()
|
|
||||||
nodes_before = [(n.name, n.op_type) for n in model.graph.node]
|
|
||||||
outputs_before = [o.name for o in model.graph.output]
|
|
||||||
|
|
||||||
removed = remove_tail_softmax(model)
|
|
||||||
|
|
||||||
assert removed == []
|
|
||||||
assert [(n.name, n.op_type) for n in model.graph.node] == nodes_before
|
|
||||||
assert [o.name for o in model.graph.output] == outputs_before
|
|
||||||
@ -64,39 +64,10 @@ def test_find_unsupported_detects_hw_not_support_with_op():
|
|||||||
assert find_unsupported_operators(report) == ["Softmax"]
|
assert find_unsupported_operators(report) == ["Softmax"]
|
||||||
|
|
||||||
|
|
||||||
def test_find_unsupported_hw_not_supported_suffix_variants():
|
def test_find_unsupported_signal_without_op_returns_placeholder():
|
||||||
# marker 的 "supported" 字尾要被 marker 本身吃掉:
|
# 有明確訊號但抽不到 op 名稱 → 回占位符,讓呼叫端仍能 fail-fast。
|
||||||
# 後面跟真 op → 照擋;後面跟表格值 → 放行("ed" 不可被誤捕成 op 名)。
|
report = "kdp520/ERROR, HW not support"
|
||||||
assert find_unsupported_operators("HW_NOT_SUPPORTED: Softmax") == ["Softmax"]
|
assert find_unsupported_operators(report) == ["<unknown>"]
|
||||||
assert find_unsupported_operators("Hardware not supported: None") == []
|
|
||||||
assert find_unsupported_operators("hardware not supported") == []
|
|
||||||
|
|
||||||
|
|
||||||
def test_find_unsupported_free_text_fragment_passes_fail_open():
|
|
||||||
# unimplemented 的自由文字片段不做「首字母大寫字」猜測:
|
|
||||||
# "Not" 是真 ONNX op、"CPU" 不是對的 op —— 猜錯(誤擋 / 錯訊息)比放行更糟。
|
|
||||||
assert (
|
|
||||||
find_unsupported_operators("UnimplementedFeature: Not supported in this mode")
|
|
||||||
== []
|
|
||||||
)
|
|
||||||
# 無中括號的自由文字(抓不到明確 op)→ 放行,而不是抓錯成 "CPU"。
|
|
||||||
assert (
|
|
||||||
find_unsupported_operators("UnimplementedFeature: undefined CPU op Softmax")
|
|
||||||
== []
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_find_unsupported_marker_without_op_passes_fail_open():
|
|
||||||
# marker 字樣但抽不到具體 op 名稱 → 放行(真 fail-open)。
|
|
||||||
# "HW not support" 很可能只是 model_fx_report 的欄位分類名(表頭),
|
|
||||||
# 光是出現不代表模型有不支援 op(R3 誤擋根因)。
|
|
||||||
assert find_unsupported_operators("kdp520/ERROR, HW not support") == []
|
|
||||||
# 欄位名後面接表格值(非 op)也不能擋。
|
|
||||||
assert find_unsupported_operators("HW not support: 0") == []
|
|
||||||
assert find_unsupported_operators("Hardware not support: None") == []
|
|
||||||
assert find_unsupported_operators("kdp520/ip_eval/HW not support: N/A") == []
|
|
||||||
# 光禿禿的 "UnimplementedFeature" 欄位名(無後續內容)也放行。
|
|
||||||
assert find_unsupported_operators("UnimplementedFeature") == []
|
|
||||||
|
|
||||||
|
|
||||||
def test_find_unsupported_does_not_false_positive_on_supported_wording():
|
def test_find_unsupported_does_not_false_positive_on_supported_wording():
|
||||||
@ -162,8 +133,8 @@ def test_build_error_message_with_op_names():
|
|||||||
assert "720" in msg or "730" in msg
|
assert "720" in msg or "730" in msg
|
||||||
|
|
||||||
|
|
||||||
def test_build_error_message_without_op_names_falls_back_to_generic():
|
def test_build_error_message_with_unknown_op():
|
||||||
msg = build_error_message("520", [])
|
msg = build_error_message("520", ["<unknown>"])
|
||||||
assert "520" in msg
|
assert "520" in msg
|
||||||
assert "operator" in msg
|
assert "operator" in msg
|
||||||
|
|
||||||
@ -218,22 +189,6 @@ def test_run_precheck_raises_when_evaluate_raises_with_signal():
|
|||||||
assert "Softmax" in str(exc.value)
|
assert "Softmax" in str(exc.value)
|
||||||
|
|
||||||
|
|
||||||
def test_run_precheck_passes_on_marker_only_report():
|
|
||||||
# 報告帶「HW not support」欄位名但無具體 op → 放行不 raise(真 fail-open)。
|
|
||||||
evaluator = _FakeEvaluator(
|
|
||||||
report="kdp520/ip_eval/HW not support: N/A, kdp520/FPS: 1200"
|
|
||||||
)
|
|
||||||
# 不應 raise
|
|
||||||
run_precheck(
|
|
||||||
"/tmp/out.onnx",
|
|
||||||
model_id=6,
|
|
||||||
version="v1",
|
|
||||||
platform="520",
|
|
||||||
evaluator=evaluator,
|
|
||||||
)
|
|
||||||
assert len(evaluator.calls) == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_run_precheck_passes_when_evaluate_raises_without_signal():
|
def test_run_precheck_passes_when_evaluate_raises_without_signal():
|
||||||
# evaluate() raise,但訊息不含不支援訊號(transient / 環境問題)→ 放行不誤擋。
|
# evaluate() raise,但訊息不含不支援訊號(transient / 環境問題)→ 放行不誤擋。
|
||||||
evaluator = _FakeEvaluator(
|
evaluator = _FakeEvaluator(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user