問題:同一顆含 Softmax 的 tflite,正式站能編出 KL520 真 .nef, 本專案 worker 在 nef 階段撞 UnimplementedFeature [Softmax] exit 6。 根因:runtime ktc 的 eliminate_tail 是 no-op(閹割版、只印警告), 尾端 Softmax 未被移除;正式站產物實測為 logits 輸出(Softmax 已砍)。 修法(staging 實測驗證、與正式站產物權重段逐 byte 相同): - onnx/core.py:新增 remove_tail_softmax(),onnx2onnx_flow 後對所有 platform 移除 terminal Softmax(cut_nodes、不用 cut_types 避免誤砍 中間層下游)、多 output rewire 逐項驗證 fail-loud、onnx.checker、 removed_tail_softmax metadata - precheck.py:R3 誤擋收窄——刪 hw_not_support_col 欄位名誤命中、 op capture 改必須、抽不到具體 op 名改放行+warning(真 fail-open)、 刪自由文字猜 op fallback、加噪音 token 過濾 - tests:+16(29 passed;terminal/no-op/多 output/fail-open 全覆蓋) - docs:TDD §12、design-doc ADR-012、PRD 輸出語意變更(機率→logits) 行為變更:所有 platform NEF 輸出改 logits(與正式站一致)、 分類後處理需呼叫端自行補 softmax。 Review:兩輪(0C/3M→0C/0M);W3 docker integration 6/6 PASS (520+Softmax e2e 產真 .nef 799,844 bytes、720 回歸、precheck 放行)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
387 lines
21 KiB
Markdown
387 lines
21 KiB
Markdown
# TDD 索引 — Kneron Model Converter 對外 API
|
||
|
||
## 作者:Architect Agent
|
||
## 狀態:Draft(Phase 0.8b 重寫 + 模組化)
|
||
## 最後更新:2026-05-16
|
||
|
||
> **auth 設計演進**:本 TDD 反映 Phase 0.8b 拍板後的「目標狀態」。完整歷史見 visionA repo `docs/autoflow/04-architecture/adr/adr-015-server-to-server-api-key.md` v2.1 + `adr-016-download-via-converter.md` v1.0。
|
||
>
|
||
> **配套**:`design-doc.md`(架構決策)、`../02-prd/PRD.md`(需求)、`../03-design/design-review.md`(UX 回饋)。
|
||
|
||
---
|
||
|
||
## 變更歷程
|
||
|
||
| 日期 | 變更 | 作者 |
|
||
|------|------|------|
|
||
| 2026-04-25 | 初版 Draft 1.0(OAuth resource server + promote) | 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-07-06 | **Worker 行為變更(M 級)**:onnx worker 對**所有 platform** 一律移除尾端 Softmax、輸出 logits(與正式站對齊、修 520 batch_compile exit 6);pre-check regex 收窄。新增 §12 | Architect Agent |
|
||
|
||
---
|
||
|
||
## 1. 文件結構
|
||
|
||
本 TDD 在 Phase 0.8b 重寫時拆分為模組化結構:
|
||
|
||
| 檔案 | 內容 | 目標讀者 |
|
||
|------|------|---------|
|
||
| `TDD.md`(本檔,索引)| 各章節摘要 + 子檔案連結 | 全部 |
|
||
| `auth.md` | API key middleware 設計 + 砍除 OAuth resource server 清單 + 保留 OAuth client | Backend |
|
||
| `api/api-jobs.md` | `POST/GET /jobs`、`GET /jobs/:id` 規格 | Backend、Reviewer、Testing |
|
||
| `api/api-promote.md` | `POST /jobs/:id/promote` 規格 | Backend、Reviewer、Testing |
|
||
| `api/api-result.md` | **新增** `GET /jobs/:id/result` 規格 | Backend、Reviewer、Testing |
|
||
| `database.md` | Redis schema + 索引 + Lua script | Backend |
|
||
| `infra.md` | Nginx / docker-compose / .env 變動 + 部署順序 | Backend、DevOps |
|
||
| `performance.md` | SLO + 延遲預算 + 負載測試 | Backend、Testing |
|
||
| `observability.md` | Log 格式 + 敏感資料保護 + 告警 | Backend |
|
||
| `security.md` | Trust boundary + Input validation + Auth security | 全部 |
|
||
| `design-doc.md` | 架構決策 + ADR | 全部 |
|
||
| 本檔 §12 | **Worker 轉檔管線行為變更**(尾端 Softmax 移除 + pre-check regex 收窄,2026-07-06 M 級)| Backend、Reviewer、Testing |
|
||
|
||
---
|
||
|
||
## 2. Phase 0.8b 改動摘要
|
||
|
||
### 2.1 對外 auth 改 API key
|
||
|
||
- 砍 `auth/middleware.js`(OAuth resource server)+ `auth/jwks.js`
|
||
- 加 `auth/apiKeyMiddleware.js`
|
||
- 4 個既有 endpoint 改掛 `requireApiKey()`
|
||
- 新加 `/result` endpoint 也用 `requireApiKey()`
|
||
|
||
詳見 `auth.md` §1 + §3(砍除清單)。
|
||
|
||
### 2.2 新增 `/result` endpoint
|
||
|
||
- `GET /api/v1/jobs/:id/result`
|
||
- Streaming proxy NEF from MinIO → caller
|
||
- 4 種 4xx + 2 種 5xx 情境
|
||
- 雙路徑 NEF key 解析(新格式 + 舊格式向後相容)
|
||
- **2026-05-17 補充**:rate limit(60 req/min,獨立 bucket)、Range header 防護(silently ignore)、audit log 8 個 action、Backend `source_filename` 寫入 acceptance criteria(§9-§14)
|
||
|
||
詳見 `api/api-result.md`。
|
||
|
||
### 2.3 保留不動
|
||
|
||
- Promote 流程(converter → FAA 仍走 OAuth client_credentials)
|
||
- Redis schema(除確認 `source_filename` 欄位存在)
|
||
- Worker、MinIO bucket、Nginx 結構
|
||
|
||
詳見 `auth.md` §2 + `api/api-promote.md`。
|
||
|
||
### 2.4 Config 變動
|
||
|
||
- 移除:`MEMBER_CENTER_ISSUER` / `MEMBER_CENTER_JWKS_URL` / `KNERON_CONVERTER_AUDIENCE` / `JWKS_*` / `JWT_CLOCK_TOLERANCE_SEC`
|
||
- 新增:`CONVERTER_API_KEY`
|
||
- 保留:`MEMBER_CENTER_TOKEN_URL` / `KNERON_CONVERTER_CLIENT_*` / `FILE_ACCESS_AGENT_*` / `OAUTH_*`
|
||
|
||
詳見 `infra.md` §3。
|
||
|
||
---
|
||
|
||
## 3. 系統概述
|
||
|
||
### 3.1 角色
|
||
|
||
- **Converter(本專案)**:Node.js Task Scheduler + Python Worker
|
||
- **visionA-backend**:Go 服務,Converter 對外 API 的**唯一** caller
|
||
- **Member Center(MC)**:OAuth authorization server — Phase 0.8b 後**只**給 Converter → FAA promote 用
|
||
- **File Access Agent(FAA)**:NAS 邊界檔案閘道,single-tenant per instance
|
||
|
||
### 3.2 API 端點清單(Phase 0.8b 後)
|
||
|
||
| 方法 | 路徑 | Auth | 說明 | 規格 |
|
||
|------|------|------|------|------|
|
||
| GET | `/health` | — | 健康檢查 | `api/api-jobs.md` §3 |
|
||
| POST | `/api/v1/jobs` | API key | 建立 job | `api/api-jobs.md` §4 |
|
||
| GET | `/api/v1/jobs` | API key | 列表 / Recovery | `api/api-jobs.md` §6 |
|
||
| GET | `/api/v1/jobs/:id` | API key | 單一 job 狀態 | `api/api-jobs.md` §5 |
|
||
| POST | `/api/v1/jobs/:id/promote` | API key | 搬檔到 FAA | `api/api-promote.md` |
|
||
| GET | `/api/v1/jobs/:id/result` | API key | **NEW** stream NEF | `api/api-result.md` |
|
||
| POST | `/api/v1/jobs/:id/download-tokens` | API key | Phase 2,回 501 | `api/api-jobs.md` §7 |
|
||
| DELETE | `/api/v1/jobs/:id` | API key | Phase 2,回 501 | `api/api-jobs.md` §7 |
|
||
|
||
### 3.3 既有路徑(Phase 0.8b 不動)
|
||
|
||
| 方法 | 路徑 | 用途 |
|
||
|------|------|------|
|
||
| POST | `/jobs` (multipart) | Web UI 既有上傳 |
|
||
| GET | `/jobs/:id` | Web UI 狀態查詢 |
|
||
| GET | `/jobs/:id/events` (SSE) | Web UI 進度 push |
|
||
| GET | `/jobs/:id/download/:filename` | Web UI 下載 |
|
||
| GET | `/queues/stats` | 內部監控 |
|
||
|
||
這些走 internal vhost,不對外、不加 auth。
|
||
|
||
---
|
||
|
||
## 4. 技術堆疊(不變)
|
||
|
||
| 層級 | 選擇 |
|
||
|------|------|
|
||
| 後端框架 | Node.js 18 + Express 4 |
|
||
| 認證(對外)| API key(`crypto.timingSafeEqual`,Phase 0.8b 新)|
|
||
| 認證(promote)| OAuth client_credentials(jose / 自寫 fetch)|
|
||
| 資料庫 | Redis 7 |
|
||
| 物件儲存 | MinIO(Converter Bucket) |
|
||
| Worker | Python 3.10+ |
|
||
| 反向代理 | Nginx |
|
||
| 測試 | Jest |
|
||
|
||
詳見 `design-doc.md` §3.5。
|
||
|
||
---
|
||
|
||
## 5. 專案結構
|
||
|
||
```
|
||
apps/task-scheduler/
|
||
├── server.js ← Entry
|
||
├── src/
|
||
│ ├── config.js ← 集中讀 env(Phase 0.8b 改)
|
||
│ ├── redis.js ← Redis client
|
||
│ ├── auth/
|
||
│ │ ├── apiKeyMiddleware.js ← 【新】Phase 0.8b
|
||
│ │ ├── oauthClient.js ← 【保留】promote 用
|
||
│ │ ├── middleware.js ← 【砍】OAuth resource server
|
||
│ │ └── jwks.js ← 【砍】
|
||
│ ├── fileAccessAgent/
|
||
│ │ ├── client.js ← FAA HTTP client(保留)
|
||
│ │ └── errors.js ← 錯誤翻譯(保留)
|
||
│ ├── routes/
|
||
│ │ ├── legacy.js ← 既有 /jobs/* 路由
|
||
│ │ └── v1/
|
||
│ │ ├── index.js ← v1 router 組裝(要改 wire result + 換 auth middleware)
|
||
│ │ ├── jobs.js ← POST/GET(要改換 requireApiKey)
|
||
│ │ ├── promote.js ← POST promote(要改換 requireApiKey)
|
||
│ │ └── result.js ← 【新】Phase 0.8b
|
||
│ ├── services/
|
||
│ │ ├── jobService.js ← Job CRUD
|
||
│ │ └── doneListener.js ← Worker done event
|
||
│ ├── middleware/
|
||
│ │ ├── errorHandler.js ← 統一錯誤
|
||
│ │ └── requestId.js
|
||
│ └── utils/
|
||
│ └── logger.js
|
||
├── docs/openapi.yaml ← 要改 security scheme(OAuth → bearer/api_key)
|
||
├── .env.example ← 要改(見 infra.md §4)
|
||
├── README.md ← 要改 auth 章節
|
||
└── package.json
|
||
```
|
||
|
||
---
|
||
|
||
## 6. 實作任務拆分(給 Backend)
|
||
|
||
按 Autoflow 增量式開發規範,每個任務 = 一個可獨立 review 的單位。
|
||
|
||
### Phase A — API key middleware + auth 切換(取代 OAuth)
|
||
|
||
| # | 任務 | 依賴 | 預估 | 驗收標準 |
|
||
|---|------|------|------|---------|
|
||
| A1 | 新建 `src/auth/apiKeyMiddleware.js` | — | 1d | unit test 全過:happy path、missing header、wrong key、constant-time、destroy socket、env 未設定 fail-fast |
|
||
| A2 | 修 `src/config.js`:新增 `converter.apiKey`、移除 OAuth resource server 相關 env、保留 promote 相關 | — | 0.5d | config.test.js 過;啟動時 `CONVERTER_API_KEY` 未設只 warn(不 throw);OAuth resource server env 移除後 server 仍能啟動 |
|
||
| A3 | 修 `src/routes/v1/index.js` / `jobs.js` / `promote.js`:`requireAuth(scope)` → `requireApiKey()` | A1, A2 | 0.5d | 既有 integration test 全過(401 行為改成 API key 模式驗);server 啟動正常 |
|
||
| A4 | 砍 `src/auth/middleware.js` + `src/auth/jwks.js` + 相關 test | A3 | 0.5d | `git rm` + test runner 沒 broken import;search code base 沒有 reference 殘留 |
|
||
| A5 | 修 `.env.example`、`docs/openapi.yaml`、`README.md`:移 OAuth resource server 段、加 `CONVERTER_API_KEY` | A4 | 0.5d | docs lint 過;OpenAPI security scheme 改 bearer / api_key |
|
||
| A6 | Integration test:API key 驗證 4 個情境(happy / missing / wrong / 503) | A1-A5 | 1d | 全部過;既有 jobs / promote integration test 仍過 |
|
||
|
||
**Phase A 總工時**:~4d
|
||
|
||
### Phase B — `/result` endpoint
|
||
|
||
| # | 任務 | 依賴 | 預估 | 驗收標準 |
|
||
|---|------|------|------|---------|
|
||
| B1 | 確認 `jobService.createJob` 寫入 `source_filename` 欄位(檢查既有 code、補上若缺)| — | 0.5d | unit test 過;既有 job record 結構不破壞 |
|
||
| B2 | 新建 `src/routes/v1/result.js`(含 `extractNefObjectKey`、`buildFilename`、stream handler)| B1, A1 | 1.5d | unit test 過:filename 各情境、雙路徑 key 解析、stream error / client close handling |
|
||
| B3 | Wire `/result` 到 `src/routes/v1/index.js`(含 `requireApiKey` + per-client rate limiter)| B2 | 0.5d | server 啟動 + route table 正確;mergeParams 取 :id 通 |
|
||
| B4 | Integration test:`/result` 8 個情境(200 happy / 401 / 404 job / 404 result / 409 / 410 expired / 410 minio miss / 502)| B2, B3 | 1d | 全部過 |
|
||
|
||
**Phase B 總工時**:~3.5d
|
||
|
||
### 任務排程建議
|
||
|
||
**順序執行 A → B**(Backend 單人):
|
||
|
||
- A1 + A2 可平行
|
||
- A3 等 A1 + A2
|
||
- A4 等 A3
|
||
- A5 等 A4
|
||
- A6 等 A5(整體 verify)
|
||
- B1 + B2 可平行(B1 簡單,B2 是主要工作)
|
||
- B3 等 B2
|
||
- B4 等 B3
|
||
|
||
預估總工時:~7.5 工作日(單人)。若可雙人並行,A 和 B 可分工,壓到 ~5d。
|
||
|
||
### 與 visionA 端的 dependency
|
||
|
||
| Backend 任務狀態 | visionA 端可以做什麼 |
|
||
|---------------|------------------|
|
||
| Phase A 完成、deploy stage | visionA 可以打 stage converter 的既有 endpoint 驗 API key 流程 |
|
||
| Phase B 完成、deploy stage | visionA 可以打 `/result` endpoint 驗 streaming |
|
||
| Phase A + B 都 deploy 完 | e2e 驗證(visionA repo commit 9e29ebf 已 ready) |
|
||
|
||
---
|
||
|
||
## 7. 測試策略
|
||
|
||
詳見 `performance.md` §7 + 各 `api/*.md` 的 test 章節。
|
||
|
||
### 7.1 Unit test 覆蓋率目標
|
||
|
||
- `apiKeyMiddleware`:100%(少量 code、必須全 cover)
|
||
- `result.js`:90%
|
||
- 既有 OAuth-related 改動:維持 ≥ 85%
|
||
|
||
### 7.2 Integration test 必跑
|
||
|
||
- API key 4 情境(happy / missing / wrong / 503)
|
||
- 既有 jobs / promote 在 API key 模式下仍過
|
||
- `/result` 8 情境(見 `api/api-result.md` §7.1)
|
||
|
||
### 7.3 Manual stage e2e(部署後)
|
||
|
||
- curl 驗:`/health`、`POST /jobs`、`GET /jobs/:id`、`POST /promote`、`GET /result`
|
||
- visionA 端 e2e:完整 upload → poll → promote → download
|
||
|
||
---
|
||
|
||
## 8. 安全注意事項
|
||
|
||
詳見 `security.md`。重點:
|
||
|
||
- `CONVERTER_API_KEY` 不進 git / log / Slack
|
||
- `constant-time compare`(防 timing attack)
|
||
- Sec C1 暫緩(`.env` history rewrite + secret rotation 在 Phase 1 ready 後做、含 CONVERTER_API_KEY)
|
||
- Trust boundary:visionA 一旦被 compromise 可冒充任意 user_id(接受、與 OAuth 模型一致)
|
||
|
||
---
|
||
|
||
## 9. 風險與待確認
|
||
|
||
| # | 風險 | 影響 | 行動 |
|
||
|---|------|------|------|
|
||
| R1 | CONVERTER_API_KEY rotation 流程未自動化 | 低 | Phase 1 接受手動 |
|
||
| R2 | `/result` 高並發 stream 壓力 | 低 | NEF 通常小、visionA 是唯一 caller、QPS 可控 |
|
||
| R3 | Sec C1 暫緩(.env 進 git history) | 中 | Phase 1 ready 收尾後 rewrite |
|
||
| R4 | NEF 7 天過期後 client 重新轉檔 | 低 | API spec 已定義 410,visionA 端處理 |
|
||
| R5 | Phase 0.8b 部署期間「OAuth → API key」短暫不可用 | 低 | 既有 stage OAuth 從未跑通、不會有 regression |
|
||
|
||
---
|
||
|
||
## 10. 後續步驟
|
||
|
||
1. 本 TDD 索引 + 子檔案送 PM / Design 三方互審
|
||
2. 使用者審核
|
||
3. Backend Agent 依 §6 的任務拆分增量開發
|
||
4. Reviewer 每個任務把關
|
||
5. Testing 整合測試 + e2e
|
||
6. DevOps 部署(converter 先 + visionA 後)
|
||
|
||
---
|
||
|
||
## 11. 變更記錄
|
||
|
||
| 日期 | 版本 | 變更 | 作者 |
|
||
|------|------|------|------|
|
||
| 2026-04-25 | Draft 1.0 | 初版,Phase 1 完整規格(單檔 1390 行) | Architect Agent |
|
||
| 2026-04-25 | Draft 1.1 | Multipart 上傳路徑改 | Architect Agent |
|
||
| 2026-05-16 | Draft 2.0 | **Phase 0.8b 重寫**:API key + /result + 模組化拆分為索引 + 8 個子檔案 | Architect Agent |
|
||
|
||
---
|
||
|
||
## 12. Worker 轉檔管線行為變更(2026-07-06,M 級)
|
||
|
||
> **範圍界定**:本 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 層獨立。
|