這是什麼、解決什麼問題

Day 6 到 Day 13,我們一天認識一個工具。 但工具會不會用,跟能不能把它們接起來,是兩件事。

今天走一次完整的路:一份資料進去,一個能打的 API 出來。

重點不在每個工具怎麼用(前面講過了), 在於交接點——上一個工具的產出,怎麼變成下一個工具的輸入。 斷掉的地方幾乎都在交接點上。


什麼時候你會用到

  • 第一次要把整套跑通
  • 要跟別人解釋「我們的流程長什麼樣」
  • 要找出「哪一段是人工的」——這是評估自動化程度最快的方法

前置條件

前面十二天的東西都要能動:

oc get dsc default-dsc -o json | jq -r --argjson want '
  ["DashboardReady","KserveReady","WorkbenchesReady","AIPipelinesReady","ModelRegistryReady"]' '
  .status.conditions[] | select(.type as $t | $want|index($t)) | "\(.type)=\(.status)"'

⚠️ 別寫成 test("Dashboard|Kserve|...") 這種模糊比對。 新版多了 KserveLLMInferenceServiceDependenciesWorkbenchesV2Ready 這些同前綴的條件,會一起被撈進來(Day 5 踩過)。

False 不等於要停。 我寫這篇那天重跑,ModelRegistryReady=Falsereason: DeployFailed,operator 升版後少拿到一個 RBAC 權限), 可是 registry 的 pod 是 2/2、API 打得通、後面站 5 的註冊照樣成功。

元件 condition 說的是「operator 這次調和順不順」,不是「這個服務能不能用」。 往下走之前看的是實例:pod 起來沒、API 回不回。


全景:七站與六個交接點

七站與六個交接點:①Connection ②人工改寫 component ③Output[Model] 自動上傳 ④呼叫 registry API ⑤label 版本約定 ⑥ServiceMonitor,其中 ②④⑤ 平台不會幫你做

交接 從 → 到 靠什麼接 最容易斷在
資料 → Workbench Connection(Secret) 憑證沒驗過就用
Workbench → Pipeline 人工把 notebook 改寫成 component 這裡幾乎都是人工
Pipeline → 產物 Output[Model] 自動上傳 S3 沒檢查產物存不存在
產物 → Registry 人工或腳本呼叫 API 常常整段沒有
Registry → ISvc 只有約定,平台不管 版本對不起來
ISvc → 監控 ServiceMonitor / PodMonitor 指標沒接上

②④⑤ 這三個交接點,平台不會幫你做。 評估一套 MLOps 有多成熟,看的就是這三段是人工還是自動。

其中 ④⑤ 是可以自動化的(在 pipeline 後面多接一棒就好), 而 ② 只能靠人。先補 ④⑤——它們最便宜,而且是「線上這顆模型有沒有身分」的關鍵。 實際寫法在 Day 28


站 1–2:資料進來,開始探索

# 建 Connection(Day 7)
oc apply -f connection.yaml
# 建 Workbench 時勾選它(Day 8)

交接點 ① 的驗證——不要相信 dashboard 顯示正常,在 workbench 裡真的讀一次:

import os, boto3
s3 = boto3.client("s3", endpoint_url=os.environ["AWS_S3_ENDPOINT"],
    aws_access_key_id=os.environ["AWS_ACCESS_KEY_ID"],
    aws_secret_access_key=os.environ["AWS_SECRET_ACCESS_KEY"])
print([o["Key"] for o in
       s3.list_objects_v2(Bucket=os.environ["AWS_S3_BUCKET"]).get("Contents", [])][:5])

列得出東西才算接上。

⚠️ bucket 名字也是 Connection 給的(AWS_S3_BUCKET),別自己寫死。 寫死而那個 bucket 不存在時,錯誤是 NoSuchBucket—— 看起來像憑證問題,其實憑證是對的。


站 3:把 notebook 變成 pipeline(交接點 ②)

這一段是純人工的,而且是整條路上最花時間的一段。

notebook 裡的一格:

df = pd.read_csv("data.csv")
df = df.dropna()
df.to_csv("clean.csv")

變成 pipeline component:

@dsl.component(packages_to_install=["pandas"])
def prepare(raw: dsl.Input[dsl.Dataset], clean: dsl.Output[dsl.Dataset]):
    import pandas as pd                    # ← import 要搬進函式裡
    pd.read_csv(raw.path).dropna().to_csv(clean.path, index=False)

三件事變了

  1. 檔案路徑變成 InputOutput 物件
  2. import 要搬進函式(那段程式會被抽到另一個容器跑)
  3. notebook 裡的全域變數全部不能用了

⚠️ 這一步沒有工具能自動幫你做(有些工具宣稱可以, 但只處理最單純的情況)。排時程時要算進去。


站 4:產物與版本(交接點 ③)

跑完之後,不要只看 run 是不是綠的

oc get workflows -n <ns>     # Argo 的執行實體

⚠️ 這條有保存期限persistenceagent 把紀錄寫進資料庫之後, 預設 24 小時TTL_SECONDS_AFTER_WORKFLOW_FINISH=86400)就把 workflow 物件刪掉。 所以 oc get workflowsNo resources found 不代表沒跑過, 只代表過期了——事後要查,去 Runs 頁或走 API。 (要留久一點:DSPA 的 spec.apiServer.resourceTTL。)

去 dashboard 的 Artifacts 分頁,看:

  • 檔案在不在
  • 大小合不合理(一個 5 MB 的模型跟一個 32 MB 的模型不是同一個東西)
  • 時間戳是不是這次跑的

我寫這篇時真的送了一次 run 去看。上面那個 prepare 的產物落在:

pipelines/day14-handoff/<run-id>/make-raw/<執行 id>/raw     30 bytes
pipelines/day14-handoff/<run-id>/prepare/<執行 id>/clean    20 bytes

30 → 20 這個數字本身就是一次檢查:進去 4 列、有兩列帶空值, 出來剩 2 列,檔案該變小。如果 cleanraw 一樣大, dropna() 那行大概沒作用——而 run 一樣是綠的。 (每個 step 旁邊還會多一份 executor-logs-0,卡住時先看它。)

綠燈在 Runs 頁,證據在 Artifacts 頁。 這是我在這個平台上學到最有用的一句話。


站 5:註冊(交接點 ④)

TOKEN=$(oc whoami -t)
BASE=https://<registry route>/api/model_registry/v1alpha3
curl -sk -X POST "$BASE/model_versions" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"name":"v3","registeredModelId":"1","author":"pipeline"}'

這一段最常整個不存在。 因為跳過它,模型照樣上線。

要自動化的話,就在 pipeline 最後加一個 component 呼叫這個 API。 加上去的那天,你才真的有一份可信的清單。

⚠️ 版本名重複會回 409。pipeline 重跑一次就撞得到—— 版本名要帶 run id 之類會變的東西,不要寫死 v3


站 6:上線(交接點 ⑤)

apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  name: my-model
  labels:
    model-registry/version: "v3"      # ← 交接點 ⑤ 就靠這一行
spec:
  predictor:
    model:
      modelFormat: { name: onnx }
      storageUri: s3://models/my-model/v3/

⚠️ 平台不會檢查這個 label 跟 registry 對不對得上。 它只是個字串。要它有意義,得有人(或 CI)去驗。


站 7:監控(交接點 ⑥)

模型服務要暴露 metrics,然後要有東西去抓

先確認你有沒有 metrics Service——不是每個 ISvc 都會有

oc get svc | grep metrics

我這台三個 ISvc,只有走具名 ServingRuntime(sklearn/mlserver)的那個有 <isvc>-metrics;另外兩個是自己打包的容器、沒指定 runtime,連 metrics Service 都沒有。 沒有的話下面這份 ServiceMonitor 指誰都沒用,要先讓服務自己把 /metrics 開出來。

有的話,加一個 ServiceMonitor 指它:

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: my-model
spec:
  selector:
    matchLabels: { name: my-model-metrics }   # ← 選的是 Service 的 label
  endpoints:
  - port: mlserver-runtime-metrics            # ← port 名字由 runtime 決定

這兩行我原本都寫錯,而且錯得一點聲音都沒有。 原本寫的是 matchLabels: { serving.kserve.io/inferenceservice: my-model }port: metrics

  • serving.kserve.io/inferenceservicepredictor Service 的 label, 而 predictor Service 只有一個叫 http 的 port——選得到 Service,選不到 port
  • metrics Service 的 label 是 name: <isvc>-metrics, 而 port 名字是跟著 ServingRuntime 的名字走的:我這台 runtime 叫 mlserver-runtime,port 就叫 mlserver-runtime-metrics換一個 runtime 名字就變了——這行不能照抄,要自己 oc get svc -o yaml

oc apply 兩種寫法都會成功,沒有任何警告。 ServiceMonitor 寫錯,行為就是安靜地抓不到東西。

⚠️ 還有第三層:要有人讀它才算數。 OpenShift 內建監控預設不看使用者 namespace,那份要另外打開(cluster-monitoring-configenableUserWorkload)。 沒打開的話,這份 YAML 就只是叢集裡的一筆資料——我這台 lab 就是這樣, 真正在抓的是我自己裝的一顆 Prometheus,它用 static config,連 ServiceMonitor 都不讀

驗證方式只有一個:去 Prometheus 的 Targets 頁看它在不在, 不是看 YAML 有沒有套進去。

Day 17 會細講要看哪四個指標。


怎麼確認整條路通了

一條一條驗,不要只看最後有沒有回應。

一句話驗證
1–2 workbench 裡 list_objects_v2 列得出東西
3 oc get workflows 有這次的執行
4 Artifacts 頁有檔案,大小合理
5 registry API 查得到新版本
6 oc get isvc READY True + pod 2/2kserve-containerkube-rbac-proxy
7 Prometheus 的 Targets 頁看得到這個 Service 且 UP(不是「ServiceMonitor 有建」)

最後再做一次真正的端到端驗證

# 從叢集內打一次真的推論請求(這是我這台 iris 模型的實際輸入)
oc run t --rm -i --restart=Never -n <ns> --image=curlimages/curl -- \
  curl -s -X POST http://my-model-predictor.<ns>.svc.cluster.local/v2/models/my-model/infer \
  -H 'Content-Type: application/json' \
  -d '{"inputs":[{"name":"input-0","shape":[1,4],"datatype":"FP32",
       "data":[5.1,3.5,1.4,0.2]}]}'

叢集內走 Service 的 80 埠是直接進到模型容器的,不經過旁邊那個 kube-rbac-proxy(它聽 8443),所以這一發不用帶 token。

回來的東西長這樣:

{"model_name":"my-model","outputs":[{"name":"predict","shape":[1,1],
 "datatype":"INT64","data":[0]}]}

回應內容要用眼睛看過。 HTTP 200 不代表結果是對的。 這個 0 是 setosa,剛好是對的; 但一個壞掉的模型永遠回傳 0,長得一模一樣,也一樣給你 200


常見問題

Q:一定要照這個順序嗎? A:不用。很多團隊是先把模型上線(站 6), 之後才回頭補 pipeline 和 registry。這樣沒錯,但要知道自己欠了什麼。

Q:這條路跑一次要多久? A:第一次連通我花了好幾天,多數時間卡在交接點而不是工具本身。 熟了之後,換一個模型跑完全程大約半天——前提是站 3 那段程式碼不用重寫。

Q:哪一段最該先自動化? A:交接點 ④(註冊)。它最便宜(一支 API 呼叫), 而且是唯一能讓你回答「線上是哪版」的東西。 交接點 ② 最貴,但省下的是人力不是風險。


你們這條路上,哪一段是人工接的?

🧪 這篇的實驗環境與 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.2openshift-gitops-operator.v1.21.3

DataScienceCluster 開啟的元件

  • kserveaipipelinesdashboardworkbenchesmodelregistrykueue(Unmanaged)
  • 其餘(raytrustyaifeastaigateway…)為 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 檔案

⚠️ ODH ≠ RHOAI:元件同源,但 namespace 與部分名稱不同 (我這裡是 opendatahub,商用版是 redhat-ods-* 那一套)。 指令的邏輯可以照用,字串要自己對一次。