Day 12:Model Registry——模型的戶口名簿
這是什麼、解決什麼問題
Model Registry 是模型的清單:哪些模型存在、每個有哪些版本、 每個版本在哪、誰註冊的、現在是什麼狀態。
它不參與推論,不影響效能。你可以完全不裝它,模型照樣上線。
而這正是要講它的原因:因為它不是必經之路,所以很容易被跳過, 然後半年後有人問「線上這個模型是哪來的」,你只能去翻 S3 的檔案時間。
什麼時候你會用到
具體一點,出現以下任何一件事就該有:
- 模型超過三個
- 同一個模型有兩個以上版本在跑(例如 A/B)
- 有人會問「線上是哪版」(稽核、法遵、事故調查)
- 換版之後要能回滾到指定版本
什麼時候不需要:一個模型、一個版本、一個人維護、不受監理。 那用 git tag 加一份 README 也行——重點是有個地方能回答,不是一定要用這個工具。
前置條件
- DSC 裡
modelregistry是Managed - 一個 MySQL 資料庫(它把資料存在這裡)
spec:
components:
modelregistry:
managementState: Managed
registriesNamespace: odh-model-registries # ← registry 實例放這裡
⚠️ registriesNamespace 這個欄位很重要。 registry 實例只能建在
這個 namespace 裡,建在別的地方 operator 不會理你——
而且不會有錯誤訊息,CR 就只是一直不 Ready。
步驟一:建一個 registry 實例
apiVersion: modelregistry.opendatahub.io/v1beta1
kind: ModelRegistry
metadata:
name: demo-registry
namespace: odh-model-registries
spec:
mysql:
host: mr-mysql.odh-model-registries.svc.cluster.local
port: 3306
database: modelregistry
username: mruser
passwordSecret:
name: mr-db
key: MYSQL_PASSWORD
rest:
port: 8080
serviceRoute: disabled
grpc:
port: 9090
kubeRBACProxy:
port: 8443
routePort: 443
serviceRoute: enabled # ← 對外走這個
驗證這一步:
oc get modelregistries.modelregistry.opendatahub.io -n odh-model-registries
# NAME AVAILABLE AGE
# demo-registry True 12d
oc get pods -n odh-model-registries
# demo-registry-... 2/2 Running
# mr-mysql-... 1/1 Running
⚠️ 注意 CRD 有兩個同名的:
modelregistries.components.platform.opendatahub.io(元件的狀態)和
modelregistries.modelregistry.opendatahub.io(你的 registry 實例)。
oc get modelregistry 會拿到前者。要查實例得寫全名。
步驟二:認識三層資料模型
這是用它之前必須先搞懂的:
RegisteredModel 「詐欺偵測模型」——一個概念上的模型
└─ ModelVersion 「v1-production」「v2-snakecase」——具體版本
└─ ModelArtifact 「s3://models/fraud/v1/」——實際的檔案位置
| 層 | 回答什麼問題 |
|---|---|
| RegisteredModel | 我們有哪些模型? |
| ModelVersion | 這個模型有哪幾版?現在哪版在線上? |
| ModelArtifact | 那一版的檔案在哪? |
大部分人只建到第二層就停了,然後 registry 就變成一份沒有連結的清單。 第三層才是它跟真實世界的接點。
⚠️ 我自己的 lab 就是活教材。發文前對帳,我把兩個版本底下的 artifact 列出來:
version 2 (v1-production): ckpt -> s3://models/llm/ckpt.pt
version 3 (v2-snakecase): (沒有 artifact)
v2 那一版在 registry 裡查得到名字、查不到檔案。 它看起來被「登記」了,實際上沒有。這種紀錄的價值是負的—— 它讓你以為有人在管。
步驟三:註冊一個模型
REST API(v1alpha3):
TOKEN=$(oc whoami -t)
BASE=https://<你的 route>/api/model_registry/v1alpha3
# 1. 建 RegisteredModel
curl -sk -X POST "$BASE/registered_models" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"name":"llm-scratch","description":"自刻的小 GPT"}'
# 2. 建 ModelVersion
curl -sk -X POST "$BASE/model_versions" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"name":"v1-production","registeredModelId":"1","author":"ryan"}'
查回來:
curl -sk -H "Authorization: Bearer $TOKEN" "$BASE/registered_models?pageSize=10" \
| jq -r '.items[] | "\(.name) | id \(.id) | \(.state)"'
llm-scratch | id 1 | LIVE
curl -sk -H "Authorization: Bearer $TOKEN" "$BASE/model_versions?pageSize=10" \
| jq -r '.items[] | "\(.name) | model \(.registeredModelId) | \(.author)"'
v1-production | model 1 | ryan
v2-snakecase | model 1 | ryan
Python SDK 也有(pip install model-registry),日常用比 curl 順。
步驟四:⭐ 註冊完之後才是重點
registry 有紀錄 ≠ 線上跑的是那一版。
這兩件事之間沒有任何自動的連結。KServe 不會去問 registry, registry 也不知道 KServe 在跑什麼。是你要把它們對起來。
最小可行的做法——部署時把版本寫進 ISvc 的 label:
metadata:
labels:
model-registry/registered-model: "llm-scratch"
model-registry/version: "v1-production"
然後這個問題就有答案了:
oc get isvc -A -L model-registry/registered-model,model-registry/version
這不是平台給你的功能,是你要自己加的約定。 但沒有它,registry 就只是一份沒人維護的清單。
⚠️ 加這個 label 會重啟你的模型服務
這件事我是在寫下一篇的時候撞到的,值得回頭補在這裡。
KServe 會把 ISvc 上的 label 傳播到 predictor 的 pod template。 pod template 一變就是一次 rolling update——你只是加了一行註記, 模型服務會換一次 pod。
我的 lab 上更難看:節點 CPU 已經吃到 96%,
rolling update 的 maxSurge 在單副本上會進位成 1,
新 pod 要多一份資源才排得進去,於是它 Pending 了 22 分鐘,
最後 ProgressDeadlineExceeded。
而在那 22 分鐘裡:
$ oc get isvc llm-scratch
NAME READY
llm-scratch False ← 說壞了
$ curl .../health
{"status":"ok","model_loaded":true,...} ← 其實一直好好的
舊 pod 從頭到尾在服務,一次都沒斷。 是 READY 這一欄在說謊——
Day 10 那條(pod 不存在時它說 True)的另一面。兩個方向都會騙你。
所以:把 label 加上去要挑時間,當成一次部署處理,
不要在星期五下午順手 oc label 一下。
(我最後是把 label 拿掉讓它回到舊的 replicaset,30 秒就收斂了。)
但這樣還是回答不了「跑的是哪一版」
把 label 加上去之後我做了一次完整對帳,三個來源攤開來看:
1. registry 說 v1-production 的檔案是: s3://models/llm/ckpt.pt
2. ISvc 的 storageUri 是: s3://models/llm/
3. pod 裡實際檔案的 sha256: 4d694be9342d73c0...ba8c835
兩個問題浮出來。
第一,粒度對不起來。 registry 記的是單一檔案,KServe 要的是一個目錄前綴。 你沒辦法直接字串比對——這是第 4 項檢查實際做下去會撞到的第一件事。
第二,也是真正的問題:registry 記的是「檔案在哪」,不是「檔案是什麼」。
ModelArtifact 的欄位就這些:
{ "name": "ckpt", "uri": "s3://models/llm/ckpt.pt",
"modelFormatName": "pytorch", "customProperties": {} }
沒有 checksum、沒有 digest。 也就是說,S3 上那個路徑的內容被人換掉, registry 上什麼都不會變——它還是理直氣壯地告訴你「v1-production 在那裡」。
因此:自己把 checksum 補進去
customProperties 是唯一的地方(我實跑過,可用):
curl -sk -X PATCH "$BASE/model_artifacts/1" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"customProperties":{"sha256":{"metadataType":"MetadataStringValue",
"string_value":"4d694be9342d73c0...ba8c835"}}}'
配上 Day 10 那條 sha256sum /mnt/models/<檔名>,
「線上跑的是不是我以為的那一版」這個問題才第一次真的有答案。
平台把位置記給你了,內容要你自己記。
怎麼確認做對了
| 檢查 | 怎麼看 | |
|---|---|---|
| 1 | registry Ready | oc get modelregistries.modelregistry... |
| 2 | 兩個 pod Running | oc get pods -n odh-model-registries |
| 3 | API 查得到 | 上面那個 curl |
| 4 | artifact 指到真實位置 | 拿 registry 上的 URI 去 S3 對,檔案在不在 |
| 5 | 線上的 ISvc 對得回來 | 上面那個 oc get isvc -L |
| 6 | 內容也對得上 | registry 的 customProperties.sha256 vs pod 裡的 sha256sum |
第 4、5 項是 registry 有沒有價值的分水嶺。只做到 3 的 registry 沒有用。 而第 6 項平台不給你,位置對得上不代表內容沒被換過。
常見問題
Q:內建的 MySQL 能上正式環境嗎? A:不行。它沒有備份、沒有 HA。 registry 掛了不影響線上推論(KServe 不依賴它), 但你的紀錄會不見——而那是稽核唯一的憑據。 正式環境要接企業資料庫。
Q:我用 camelCase 送欄位,值默默不見了。 A:這是我實際撞到的行為。API 對不認識的欄位不報錯,直接忽略, 你會拿到一個看起來成功、但少了資料的物件。
發文前對帳時我又撞到它的另一面,而且更難看。
我試著 PATCH 一個不存在的 checksum 欄位,回應是整個物件的欄位全部變 null:
{"name": null, "uri": null, "checksum": null}
看起來像資料被我清空了。去 GET 回來看,資料庫其實一個字都沒動。 所以這個 API 的回應在兩個方向上都不能信: 送對了它不一定告訴你,送錯了它也不一定告訴你。
每次寫入之後 GET 回來對一次,這是唯一可靠的做法。 細節寫在這篇。
Q:跟 MLflow 比呢? A:MLflow 的 registry 綁在 MLflow 的 tracking 上,功能多(含實驗追蹤)。 這個比較輕、跟 OpenShift 的權限整合。 如果你們已經在用 MLflow,不需要為了「平台有」而換。
Q:可以強制「沒註冊的模型不准上線」嗎? A:平台沒有這個機制。 要做得自己寫 admission webhook 或在 pipeline 裡擋。 這是 Day 28 的主題——平台給的是能力,不是已經在擋的機制。
被問「線上跑的是哪個模型版本」,你們現在去哪裡查?
🧪 這篇的實驗環境與 lab 檔案(最後更新 2026-08-30)
⚠️ 上面寫的 v3.5.0 是這台叢集長期以來的版本;本篇所有指令與輸出是在自動升級後的 3.6.0-ea.1 上重跑的(升版經過寫在 Day 5 的補記)。
叢集
- 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-* 那一套)。
指令的邏輯可以照用,字串要自己對一次。
留言與指正
我特別想知道:被問「線上跑的是哪個模型版本」,你們現在去哪裡查?