jim800121chen 1c53253e6e feat(worker): onnx 階段自動移除尾端 Softmax + pre-check regex 收窄
問題:同一顆含 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>
2026-07-06 05:54:13 +08:00

21 KiB
Raw Blame History

TDD 索引 — Kneron Model Converter 對外 API

作者Architect Agent

狀態DraftPhase 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.mdUX 回饋)。


變更歷程

日期 變更 作者
2026-04-25 初版 Draft 1.0OAuth 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 endpointOAuth resource server 章節砍除;模組化拆分為索引 + 子檔案 Architect Agent
2026-07-06 Worker 行為變更M 級)onnx worker 對所有 platform 一律移除尾端 Softmax、輸出 logits與正式站對齊、修 520 batch_compile exit 6pre-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 /jobsGET /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.jsOAuth 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 limit60 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-backendGo 服務Converter 對外 API 的唯一 caller
  • Member CenterMCOAuth authorization server — Phase 0.8b 後給 Converter → FAA promote 用
  • File Access AgentFAANAS 邊界檔案閘道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 keycrypto.timingSafeEqualPhase 0.8b 新)
認證promote OAuth client_credentialsjose / 自寫 fetch
資料庫 Redis 7
物件儲存 MinIOConverter Bucket
Worker Python 3.10+
反向代理 Nginx
測試 Jest

詳見 design-doc.md §3.5。


5. 專案結構

apps/task-scheduler/
├── server.js                 ← Entry
├── src/
│   ├── config.js             ← 集中讀 envPhase 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 schemeOAuth → 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不 throwOAuth resource server env 移除後 server 仍能啟動
A3 src/routes/v1/index.js / jobs.js / promote.jsrequireAuth(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 importsearch code base 沒有 reference 殘留
A5 .env.exampledocs/openapi.yamlREADME.md:移 OAuth resource server 段、加 CONVERTER_API_KEY A4 0.5d docs lint 過OpenAPI security scheme 改 bearer / api_key
A6 Integration testAPI 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(含 extractNefObjectKeybuildFilename、stream handler B1, A1 1.5d unit test 過filename 各情境、雙路徑 key 解析、stream error / client close handling
B3 Wire /resultsrc/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 → BBackend 單人):

  • 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 覆蓋率目標

  • apiKeyMiddleware100%(少量 code、必須全 cover
  • result.js90%
  • 既有 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 驗:/healthPOST /jobsGET /jobs/:idPOST /promoteGET /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 boundaryvisionA 一旦被 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 已定義 410visionA 端處理
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-06M 級)

範圍界定:本 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 classifierKL520 時,batch_compile -T 520UnimplementedFeature: undefined CPU op [Softmax]exit 6 失敗。
  • 根因runtime image/app/ktc/onnx_optimizer.pykneronnxopt 版)的 eliminate_tail閹割版 no-opcontainer 內印 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 實測比對正式站 .nefoutput = (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 實測修訂)只移除 terminalgraph 尾端、無下游節點)的 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 實測可行)
    # 注意runtime 的 ktc.onnx_optimizer 沒有 editor APIremove_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_optimizerkneronnxopt 版)沒有任何 editor APIktc/onnx_optimizer_1_7.py 雖存在但不可 importhardcode /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.mdH1/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 名稱時放行 + warningfail-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 報告 §4apre-check 誤擋分析):見 .autoflow/06-testing/reports/tflite-520-softmax-removal-verify-2026-07-06.md §5R3 誤擋風險實測數據點)+ 舊 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.checker520 e2e 不再 exit 6、產真 .nefjob metadata 標注 Softmax 已移除
W2 pre-check regex 收窄 services/backends/precheck.py 刪 / 收窄 hw_not_support_colhw_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 負擔。§122026-07-06 新增)為 worker 轉檔管線行為變更,與 API/auth 層獨立。