棘輪失效:每一步都合法,二十步之後系統爛掉
我的 promotion gate 有一條回歸檢查:
if new_bpc > base_bpc + tol:
reasons.append("回歸:比基準差")
base 是現行 production 那一顆。意思是:新模型不能比現在線上的那顆差超過 tol。
聽起來很合理。而且我前一篇才花力氣把 tol 量出依據
(σ = 0.0211,所以 2σ = 0.042)。
但這條規則有一個結構性的缺陷,而且改小 tol 治不好。
⭐ 用真的 gate 程式碼跑一次
假設每次都剛好退步 tol(= 0.042),連續二十次,每一次都送進 gate:
次 新模型 BPC 基準 BPC gate 距離原點
1 5.0366 4.9946 ✅ 通過 +0.0420
2 5.0786 5.0366 ✅ 通過 +0.0840
3 5.1206 5.0786 ✅ 通過 +0.1260
10 5.4146 5.3726 ✅ 通過 +0.4200
19 5.7926 5.7506 ✅ 通過 +0.7980
20 5.8346 5.7926 ✅ 通過 +0.8400
二十次之後,BPC 從 4.9946 退到 5.8346——累積退步 0.84。
而每一步都通過了 gate。
如果拿第 20 顆直接跟最初那顆比:
❌ 回歸:test_bpc 5.8346 比基準 4.9946 差(容忍 0.042)
同一個 gate、同一個 tol,只是換了比較對象,結論完全相反。
為什麼改小 tol 沒有用
直覺反應是「那把 tol 調嚴一點」。
沒有用,只是把二十次變成一百次。 因為問題不在容忍度的大小, 在比較的對象——每次都跟「剛剛才被放行的那一顆」比, 於是每一次退步都成為下一次的新基準。
這是棘輪,只是方向反了。 棘輪的設計是「只能往一個方向走,退不回去」。 而這個 gate 讓每一步的退步都變成不可逆的新起點—— 它棘住的是退步,不是進步。
用 Java 講:像 code review 規定每個 PR 最多只能多欠 5 行技術債。 每一次 review 都是對的,一年後系統爛掉, 而且你找不到哪一次該負責——因為每一次都符合規則。
修法:兩個基準,不是一個
擋下的條件 = (比上一筆差 > tol) OR (比凍結基準差 > tol_total)
| 基準 | 是什麼 | 容忍度 | 防的是 |
|---|---|---|---|
| 滾動基準 | 現行 production | tol(2σ) |
單次的大幅退步 |
| 凍結基準 | 人工釘住的某一版 | tol_total(例如 3×tol) |
累積漂移 |
凍結基準的重點是「人工」——它不會自動更新。 要動它,得有人明確做一次決定: 「我承認新的水準就是這樣了,把基準往下移。」
那一個動作應該留下紀錄,而且應該有人簽。 這就是它和滾動基準的差別:滾動基準是自動的,凍結基準是要有人負責的。
這個模式到處都是
一旦看懂,你會在很多地方看到同一個形狀:
| 領域 | 「跟上一版比」的規則 | 累積之後 |
|---|---|---|
| 模型治理 | 不能比現行版差 > tol | 效能慢慢退 |
| 效能測試 | 這次不能比上次慢 > 5% | 一年慢一倍 |
| 技術債 | 每個 PR 最多加 N 行 | 系統爛掉 |
| 資安例外 | 每次只放行一個例外 | 例外變常態 |
| 專案時程 | 每次只延一週 | 延了半年 |
共同點:每一次決定都是合理的,而總和是災難。
而且這類問題不會有人負責,因為責任被切成二十份, 每一份都小到不值得反對。
所以驗收要問什麼
看到任何「不能比上一版差超過 X」的規則,問一句:
「這條規則跑二十次之後會怎樣?」
如果對方沒想過,那條規則防的是單次的意外, 不是持續的退化——而後者才是真正會發生的那種。
再問第二句:
「有沒有一個不會自動更新的基準?誰有權改它?」
沒有的話,這條鏈上只有滾動基準,也就是沒有底線。
誠實標一下
我還沒實作這個修法。
上面那個二十次的演示是拿我真正的 gate_reasons() 跑出來的——
程式碼是真的,退化的過程是模擬的(我沒有真的訓練二十顆愈來愈爛的模型)。
而我的 gate 現在只有滾動基準。所以這篇的結論同樣適用於我自己: 我知道這個洞在哪裡,但它還開著。
你們有哪個規則是「跟上一版比」的?有沒有可能被這樣累積繞過?
🧪 這篇的實驗環境與 lab 檔案(最後更新 2026-08-30)
叢集
- CRC 2.63.0 · OpenShift 4.22.7 · Kubernetes v1.35.6
- 單節點:13 vCPU / 40 GiB RAM / 120 GB disk
- 宿主:Framework Laptop 16(Ryzen AI 7 350 · 8C/16T · 64 GB RAM · 1 TB NVMe · RTX 5070 顯卡模組)
- ⚠️ 叢集內看不到 GPU(CRC 是 VM,RTX 5070 未 passthrough)
Operator
opendatahub-operator.v3.5.0← 即 RHOAI 3.x 的上游開源版cert-manager-operator.v1.20.0(3.x 的必要相依;2.x 不需要)openshift-pipelines-operator-rh.v1.23.2、openshift-gitops-operator.v1.21.3
DataScienceCluster 開啟的元件
kserve、aipipelines、dashboard、workbenches、modelregistry、kueue(Unmanaged)- 其餘(
ray/trustyai/feast/aigateway…)為Removed
叢集外的依賴(跑在宿主的 podman 上,crc start 不會帶起來)
- Harbor v2.15.2(私有 registry)· MinIO(S3)
模型端
- Python 3.12.13 · PyTorch 2.11.0+cu128 · FastAPI + uvicorn · prometheus-client
lab 檔案
- YAML/Containerfile/腳本:github.com/ryanGTR/openshift-ai-30days
(含 Day 對照表;主機名是佔位符,跑
set-lab-host.sh換成你自己的) - 服務的那個模型:llm-from-scratch(從零手刻的小 GPT)
⚠️ ODH ≠ RHOAI:元件同源,但 namespace 與部分名稱不同
(我這裡是 opendatahub,商用版是 redhat-ods-* 那一套)。
指令的邏輯可以照用,字串要自己對一次。
留言與指正
我特別想知道:你們有哪個規則是「跟上一版比」的?有沒有可能被這樣累積繞過?