Skip to content

Commit 6ca331d

Browse files
authored
Merge pull request #637 from raylei50653/fix/538-f02-complete
fix(perception): count every WorkbenchPool overflow frame (#538)
2 parents aa69c3c + 39ddb39 commit 6ca331d

5 files changed

Lines changed: 745 additions & 39 deletions

File tree

‎docs/architecture/async_ownership_538.md‎

Lines changed: 97 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
<!-- doc-status: proposed -->
22
<!-- doc-promotion: none -->
3-
<!-- doc-date: 2026-10-25 -->
3+
<!-- doc-date: 2026-10-26 -->
44
<!-- doc-module: cross -->
55

66
# 非同步 edge 與 buffer ownership 盤點(#538 D1)
77

8-
本稿是 [#538](https://github.com/raylei50653/saccade/issues/538) 的 **D1**:依 #535 的 profiles,盤點主要的非同步 edge,逐條記下誰持有資料、何時算完成、何時可以重用或釋放、容量與溢出、失敗怎麼傳、取消與重啟時怎麼收尾,以及支撐這些說法的證據等級。**本稿不修改行為,也不選定政策**;新發現的缺口只列出並路由(§5)。Owner 對發布、路由與 #340 trace 的裁決見 §9(決策來源是 #538 的 comment)。閱讀入口是 [architecture README](README.md)。
8+
本稿的 §1~§9 是 [#538](https://github.com/raylei50653/saccade/issues/538) 的 **D1**:依 #535 的 profiles,盤點主要的非同步 edge,逐條記下誰持有資料、何時算完成、何時可以重用或釋放、容量與溢出、失敗怎麼傳、取消與重啟時怎麼收尾,以及支撐這些說法的證據等級。D1 只盤點與路由,不修改行為;Owner 對發布、路由與 #340 trace 的裁決見 §9(決策來源是 #538 的 comment)。[§10](#d3-f02) 另記 D3-F02 的候選修復與驗證。閱讀入口是 [architecture README](README.md)。
99

10-
**Source baseline**:`main@3375e7d4`(#629 merge,2026-10-25)。除 §5 標 `reproduced` 的兩項與 §1 註明的 DALI 建構觀察外,本稿新增的判讀都是 `source-inspected`;引用的既有證據保留原文件的等級與座標,本稿沒有重跑。
10+
**D1 source baseline(§1~§9)**:`main@3375e7d4`(#629 merge,2026-10-25)。除 §5 標 `reproduced` 的兩項與 §1 註明的 DALI 建構觀察外,D1 新增的判讀都是 `source-inspected`;引用的既有證據保留原文件的等級與座標,D1 沒有重跑。§10 的修復與驗證另列自己的基線。
1111

1212
**本稿不擁有的內容**(只引用):CAP/REQ 與支持裁決歸 [#535 ledger](capability_requirements_535.md);節點/邊 ID 歸 [跨 profile 總覽](system_overview_536.md)(本稿的 `AE-*` 掛在它的 `SN-*`/`SE-*` 上,見 [§6](system_overview_536.md#6-給-537538539-的共同邊界));失敗狀態與 run 轉換語義歸 [#537](failure_degradation_537.md);shipping 的排程、graph 與 parity 歸 #465([resolved config §14](../reference/native_runtime_resolved_config.md#14-native-double-buffercuda-graphpr-10u5)),本稿只引用、不重驗、不推廣成全 repo 的保證;VRAM 快照契約歸 [degradation §6](../modules/resource/architecture_degradation.md#6-vram-level-跨程序快照契約537-b0-f07);功能保留或退役歸 #539。
1313

@@ -131,7 +131,7 @@
131131
| ID | 發現 | 等級 | 影響的 D3 驗收項 | 路由(§9 已裁決者註明) |
132132
|:--|:--|:--|:--|:--|
133133
| D1-F01 | `mot17.py --processes`:子程序以非零碼結束時 parent 只印 `❌`,接著用剩下的輸出算 metrics 並 `exit(0)` | reproduced | error 不得偽裝成正常完成 | **已裁決**:#537 擁有修復契約,#538 只保留追溯連結;修復見 [failure_degradation_537.md](failure_degradation_537.md) §25 |
134-
| D1-F02 | `WorkbenchPool.put_frame` 溢出時清空整個 queue(最多丟 4 幀),`drops` 只加 1;`AsyncDispatcher` 則是丟 1 個新幀、加 1。兩邊的計數單位不同,`drops` 不是遺失的幀數 | source-inspected | overflow、丟幀與計數語義一致 | **已裁決**:#538 獨立 D3 切片;保留 drain-all,計數改為實際丟棄的幀數(§9) |
134+
| D1-F02 | `WorkbenchPool.put_frame` 溢出時清空整個 queue(最多丟 4 幀),`drops` 只加 1;`AsyncDispatcher` 則是丟 1 個新幀、加 1。兩邊的計數單位不同,`drops` 不是遺失的幀數 | source-inspected(D1);修復前重現與候選驗證見 §10 | overflow、丟幀與計數語義一致 | **已裁決**:#538 獨立 D3 切片;保留 drain-all,計數改為實際丟棄的幀數(§9);候選修復見 [§10](#d3-f02) |
135135
| D1-F03 | relink 背景寫入(`pipeline_relink`)的 executor 沒有 `shutdown()`;main 先出錯時,未取的 future 不會被等待,背景 thread 可能仍在改 relinker/bank | source-inspected | cancel/drain | #539(選用分支是否保留)後再決定是否修 |
136136
| D1-F04 | `main.py` 的 producer 以 `asyncio.create_task(media.watchdog_loop())` 啟動 watchdog(8stream 沒有啟動 watchdog),沒有保留引用、不在 `supervise` 內、producer 結束時不取消;它的例外不會被收集。重連路徑的 `time.sleep(1)` 與 `connect()`(最多等 3 s 第一幀)在 event loop 上阻塞,同時卡住所有 stream 與 dispatcher worker | source-inspected | cancel/drain;error 不得被吞 | **已裁決**:留在 #538(event loop 阻塞與 task ownership) |
137137
| D1-F05 | MediaMTX 的 latest-wins 與 smart sampling 丟幀沒有計數,`AsyncDispatcher` 的 `dropped_queue_full` 看不到 ingest 層的丟幀 | source-inspected | overflow 與計數語義 | 未個別裁決;F02 的計數原則是否涵蓋 ingest 層,待 F02 切片確認 |
@@ -182,7 +182,7 @@
182182
- [x] 主要 async edge 有 ownership/completion/release 規則,或具名的未知與後續 owner:§2~§3 已逐條列出;未知集中在 AE-E8(C++ eval pool)與 AE-O2/AE-E1b(DALI,documentation-supported),路由見 §5。Owner 已裁決 D1 inventory completed(§9)。
183183
- [x] WorkbenchPool 的有限狀態契約先於修復固定(D2-WB-0 → WB-1,已合併)。
184184
- [x] 至少一個故障注入重現了具體違約(D2);本稿另外重現 D1-F01、D1-F06,其餘標 source-inspected。
185-
- [~] overflow、丟幀與計數語義一致:**不一致**(D1-F02、D1-F05)。F02 的方向已裁決,修復尚未實作。
185+
- [~] overflow、丟幀與計數語義一致:F02 的候選修復與待完成門檻見 [§10](#d3-f02);D1-F05 的 ingest 丟幀仍未處理。
186186
- [~] error/cancel/timeout 不偽裝成正常完成:D1-F01、D1-F06 違反;D1-F03、D1-F04、D1-F07 待處理。
187187
- [x] host-only、GPU-validated、documentation-supported 與未驗證的證據分開記錄(§1、§3)。
188188
- [x] #465 的 native schedule、capture freeze 與 trace/measurement 邊界沒有改動;本稿只引用。G 的 contract/test/limit 映射尚未交付。
@@ -210,3 +210,95 @@
210210
全量的壓縮率與耗時以首次封存的實測為準。
211211
- **GE-07**:source/文件支持、GPU-unverified,不能取代 GPU 端到端測試(stream completion、PyTorch consumer、實際 pipeline 組態)。online qualification 維持未通過。
212212
- **執行順序**:D1 審核並合併 → #537 F01/F06、#538 F02 與 watchdog 分別處理 → 獨立執行 #340 封存。
213+
214+
<a id="d3-f02"></a>
215+
216+
## 10. D3-F02:WorkbenchPool 溢出計數與停止競態
217+
218+
**範圍與狀態**:依 §9 的 F02 裁決接續 `22c01fcd5a3ab5ff01c7f248f768c4be3eae9769` WIP;基線是 `main@aa69c3c1caf502ecc5aeeda92f90d8db6871a6c3`。這是候選實作,尚未合併。只修 `WorkbenchPool` 的 host queue admission、計數與對應停止競態;不修改 CUDA 排程、模型、tracker 參數、AsyncDispatcher 的丟幀策略或線上支持範圍。D1 表格保留原盤點座標。
219+
220+
### 10.1 計數與 identity
221+
222+
| 欄位 | 定義與範圍 |
223+
|:--|:--|
224+
| `drops` | 實際從滿 queue 清掉的幀數,從最近一次 `reset_stats()` 起累積 |
225+
| `overflow_events` | `put_frame()` 第一次 enqueue 遇到滿 queue 的次數;即使 consumer 搶先取走全部幀而清掉 0 幀,仍是一個事件 |
226+
| `workbench_drops`/`workbench_overflow_events` | 相同事件的 telemetry;由 telemetry 物件累積,pool 的 `reset_stats()` 不重設它 |
227+
| `streams[stream_id]` | pool 物件生命期的 `submitted`、`dequeued`、`dropped`、`overflow_events`、`discarded_on_stop`、`last_dequeued_seq`、`gaps`、`gap_frames` 及當下 `queued`;stop/start 與 `reset_stats()` 不清掉它們 |
228+
| `discarded_on_stop` | stop 清掉的 queued 幀,與 overflow drop 分開,重試 stop 不重複計數 |
229+
230+
每條 stream 在接受 frame 時配置從 1 開始、單調遞增的 `seq`。worker 取出 frame 時記 `last_dequeued_seq`;跳過的 identity 數量計入 `gap_frames`,每段缺口計入 `gaps`。這裡的 `dequeued` 表示交給處理,不表示成功處理或 callback 已交付;處理失敗與 late callback 仍走既有契約。現有 callback 的 stream ID/timestamp 介面不變。
231+
232+
所有 producer/worker 安靜後,每條 stream 滿足:
233+
234+
```text
235+
submitted = dequeued + dropped + discarded_on_stop + queued
236+
```
237+
238+
仍在執行中的 `get_stats()` 是逐 stream 的觀測,不是整個 pool 與 telemetry 的原子快照。比較總數時須保持相同統計區間:全域 `drops` 可 reset,per-stream 與 telemetry 則累積。尾端被丟棄的幀在下一個 frame 被 worker 取出之前,尚不形成已觀察的 gap。
239+
240+
### 10.2 並行 admission 與有界停止
241+
242+
- 保留 latest-wins/drain-all、每 stream queue 容量 4。per-stream lock 涵蓋第二次 RUNNING 檢查、identity 配置及完整 drain-and-enqueue;worker 仍可以取走舊幀。計數以實際 `get_nowait()` 取出的數量為準。
243+
- producer 在 lock 外已看過 RUNNING,取得 lock 後仍須再查一次;停止開始後才取得 lock 的 producer 會被拒絕,不增加 `submitted`。已通過第二次檢查的 admission 必須完成,再由 worker 或 stop 收帳。
244+
- `stop(timeout)` 的 worker joins 與等待 stream lock 共用 deadline。lock 逾時時維持 STOPPING、保留 batcher、增加 `stop_timeouts` 並拋 `WorkbenchPoolStopTimeout`;之後可以重試。每條 stream 一清完就更新全域與該 stream 的 `discarded_on_stop`,後面的 stream 逾時不會遺失前面的計數。batcher shutdown 仍另有既有上限([#537 §13](failure_degradation_537.md#13-538-d2-wb-1workbenchpool-有界停止worker-failure-與資源釋放))。
245+
- lock 會改變競爭下的交錯順序,因此可能改變實際交付哪些幀;保留的是上述有界 queue/latest-wins 契約,沒有宣稱修復前後的丟幀樣本、輸出、吞吐或精度等價。不同 stream 各有自己的 lock。
246+
247+
**審查發現與處置**:對原 WIP `22c01fcd` 重現一項停止回歸:producer 持有 stream lock 時,`stop(timeout=0.1)` 等到 1 秒後才返回,且回報正常停止。原因是 worker joins 之後的 lock acquisition 沒有上限。候選改為上述 deadline 內取得 lock,並補停止後拒絕、保留 batcher、重試及部分 drain 的測試。
248+
249+
### 10.3 證據與尚未完成項
250+
251+
契約測試:[test_workbench_pool_overflow_contract_538.py](../../tests/unit/test_workbench_pool_overflow_contract_538.py)。CPU 情境沿用 D2 的獨立子程序 harness 與外部 timeout;GPU 情境沿用 D2-GPU 的真實 CUDA Workbench/batcher,只替換 TRT inference。
252+
253+
- 修復前重現:送 6 幀,worker 收到 `[0.0, 5.0]`,實際丟 4 幀卻報 `drops=1`。重播使用的 dispatcher git blob 與基線 `aa69c3c1` 相同。
254+
- 已提交 head `a06f56abaefb6e5ea4ef86abad2ea282afffa6af`、本次新 build:四份相關契約測試 **43 passed/0 skipped**(30 個 CPU、13 個 GPU),RTX 5070 Ti Laptop。這是上述局部測試後,以新 native extension 做的完整相關回歸。
255+
- CPU 範圍:兩 stream/16 producer threads 的 6,400 次提交、overflow 與 telemetry 對帳、stop discard 分離、重啟 identity gap、admission/stop 競態及部分清理後的重試。
256+
- CUDA 範圍:既有 D2-GPU 契約與新增 F02 案例。新案例固定第一幀正在 inference,再排入 5 幀:`drops=4`、`overflow_events=1`、callback timestamps `[0.0, 5.0]`,並核對真實 CUDA 前處理交給 batcher 的兩份 input 值。
257+
- 6 個隔離 mutation 都被行為斷言攔住:把 drops 算成事件、telemetry 算成事件、取消 stop lock timeout、遺失部分 stop 計數、重啟重設 identity、移除 admission 的第二次狀態檢查。
258+
- 第一次失敗分類:新增的 stop 回歸測試在原 WIP 上失敗(`returned`,應為 `timeout`);修復後通過。沙箱內 NVML/CUDA 被作業系統阻擋;已在沙箱外核對裝置並執行上述 GPU 測試,沒有把 skip 當通過。
259+
- 完整 pre-push 首次執行於 combined head `756dde4ebe580f2f551ed3ea04162b54e818f1a4`:6,241 passed、195 skipped、105 deselected、5 xfailed、1 failed。唯一失敗是既有 `test_h2_import_witness.py::test_the_recorder_records_dependencies_rather_than_filtering_them`:驗證 checkout 的 `.venv` 是指向 repo 外的 symlink,`ImportRecorder.note_file()` 依既有契約先 `resolve()` 再判斷是否位於 repo 內,所以沒有記錄該依賴。該 recorder 與測試相對基線完全未改;同一測試在原工作區通過。改以同一 head 的 `.worktrees/550-f02-validation`、repo 內的實體 `.venv` 目錄(同磁碟 hard links,禁止 sync 與 bytecode 寫入)驗證後,整份 import-witness suite **34 passed**。此項分類為已定位的驗證環境問題,沒有改寫或跳過測試。
260+
- 修正環境後,**同一 combined head** 的完整 `scripts/pre_push.sh` exit 0:**6,242 passed、195 skipped、105 deselected、5 xfailed、0 failed**。格式、mypy(605 files)、文件與其餘適用檢查也通過;這是本機結果,沒有遠端 CI 結果。以下收尾只增加文件中的驗證紀錄,不改實作、測試或 identity 檔案內容。
261+
262+
本機原始輸出與 mutation runner 在 `/tmp/saccade-550-f02-evidence/`;來源與摘要檔 SHA-256:
263+
264+
重播指令(在候選 worktree;GPU 測試需要可存取裝置的 host):
265+
266+
```bash
267+
PYTHONPATH=build/runtime_identity:src .venv/bin/python -m pytest -q \
268+
tests/unit/test_workbench_pool_overflow_contract_538.py \
269+
tests/unit/test_workbench_pool_lifecycle_contract_538.py \
270+
tests/unit/test_workbench_pool_lifecycle_observed_538.py \
271+
tests/unit/test_workbench_pool_gpu_contract_538.py
272+
.venv/bin/python /tmp/saccade-550-f02-evidence/run_mutations.py
273+
```
274+
275+
| 檔案 | SHA-256 |
276+
|:--|:--|
277+
| `src/saccade/perception/dispatcher.py` | `c0b9a68eac09ef67b7a8e9df3380fc98cebdb909b6779459c6ae138a1af53afe` |
278+
| `tests/unit/test_workbench_pool_overflow_contract_538.py` | `e29c073237f4bbcd7937011132f30e7e1e386e57fdadc774e421a78a020849a4` |
279+
| `run_mutations.py` | `21a7ec01f720484a72070bdfa99bb5997169392108beb8c064d3fa9986dfb5d4` |
280+
| `mutations.json` | `2a0d106a6ff8db957a7ddad5b895aad240c0ae9aef1a03de96d13d786d68c25c` |
281+
| `final-contracts.log` | `3a69d4032550ea4bcc409ca9c2eb915b2a31a68ba407000de9e2de3823343b96` |
282+
| `final-contracts.xml` | `4c3b1f1daec87805add348a84c2c4fca3868b42d1c537d727838acc888e03e94` |
283+
| `pre-push.log`(首次失敗) | `f37f3683d01bcd4388c9d77ca14f8e0978501028b8ba54d4e8400536d4e05483` |
284+
| `pre-push-local-venv.log`(環境修正後通過) | `e28174382d9597fd174efa575ff7f5cc599987c3e580ab6f2b7bdbab17d1422e` |
285+
| `attested-strict.log` | `6f66d4d2da35c315d08ac546f9d406dc828bf9378a7619db41ebb586d58e750d` |
286+
287+
**Runtime identity 候選**:在 clean `a06f56ab` 上獨立 build `saccade_tracking_ext`/`saccade_scan_plugin`,依 runbook 重跑 MOT17-09 identity probe:
288+
289+
```bash
290+
PYTHONPATH=src .venv/bin/python scripts/tools/build_runtime_identity.py \
291+
--run-probe --build-dir build/runtime_identity \
292+
--emit /tmp/saccade-550-f02-evidence/runtime_identity.candidate.json \
293+
--require-complete
294+
```
295+
296+
候選 `publication_complete=true`;只有 implementation 改變(`11263f7c… → 0cb15155…`,唯一變動 member 是 `dispatcher.py`),其餘四軸相同。probe 是 fresh run,digest 仍為 `2dabed0b…`;`equivalence=unproven`。候選檔 SHA-256:`f36316235124b7edde843e262b46248299b99077f25aa47c50e1784d51bdb5b2`;capture log:`b52b5757db0b1d98785ad4ed2939e31fc2e4e27fd87c3d8bbd2d7cc396f8cd1e`。候選只寫入 scratch,publication 應以 canonical+archive 的獨立 review surface 與實作原子落地。
297+
298+
本機 publication 分支已準備 canonical+archive 的獨立兩檔 commit,archive 與 `main@aa69c3c1` 的原 canonical 逐位元組相同(SHA-256 `d740889a2fe15e58bd868a9af5b8e9ae23b033c76cca1caa068562e65407d85b`)。另以 `h2_behavioral_identity.py --identity-mode` 重跑驗證 probe、`h2_runtime_inputs.py --build-dir build/runtime_identity` 重算完整 manifest,再執行 `check_runtime_identity_staleness.py --mode attested --strict --probe-from <verification-probe.json> --runtime-inputs-from <runtime-inputs.json>`,exit 0;這不是只驗 portable axes 的結果。
299+
300+
完整 pre-push 的環境保護參數為 `UV_NO_SYNC=1`、`PYTHONDONTWRITEBYTECODE=1`;`UV_CACHE_DIR` 指向 `/tmp/saccade-550-uv-cache`。`PYTHONPATH` 依序指定本次新 build、原工作區其他 native extensions,以及驗證 checkout 的 `src`。這些參數不跳過任何檢查。
301+
302+
**尚未完成**:獨立 publication review、最終 head review、PR/required CI 與 atomic co-land。現有本機驗證不能替代這些門檻。WorkbenchPool online qualification、D1-F05 ingest 丟幀、watchdog(F04)、多 worker NMS capture 與其餘 D1/D2 殘留仍 OPEN;沒有清除 #340 trace。
303+
304+
回退需要同時回退實作並依 [runtime identity runbook](../reference/runbooks/runtime_identity_republication.md) 重發座標;不能只把 canonical publication 換回舊檔。

‎docs/reference/runtime_identity.generated.json‎

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

‎docs/reference/runtime_identity/archive/runtime_identity.2026-10-26.11263f7cae3f8bce.json‎

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)