在 OpenShift AI 上搭一條四棒的模型交付鏈
模型能上線之後,下一個問題是:它是怎麼來的?
如果答案是「某個人在某台機器上跑了一個 notebook」,那你沒有交付鏈, 你有的是一個口述歷史。
這篇是在 OpenShift AI 上把它變成 pipeline 的最小可用版本。
平台這邊要有什麼
OpenShift AI 的 pipeline 引擎是 Kubeflow Pipelines v2,
在 DSC 裡的元件名叫 aipipelines(2.x 叫 datasciencepipelines,改名了)。
開了之後,每個 project 要建一個 DataSciencePipelinesApplication(DSPA),
它會生出一整組東西:
oc get deploy -n llm-serve-demo | grep ds-pipeline
ds-pipeline-dspa 1/1 ← API server
ds-pipeline-metadata-envoy-dspa 1/1 ← lineage 的 proxy
ds-pipeline-metadata-grpc-dspa 1/1 ← lineage 儲存
ds-pipeline-persistenceagent-dspa 1/1 ← 把 run 狀態寫回 DB
ds-pipeline-scheduledworkflow-dspa 1/1 ← 排程
ds-pipeline-workflow-controller-dspa 1/1 ← 實際執行(Argo Workflows)
mariadb-dspa 1/1 ← metadata DB
七個 pod。 你只是要跑四個步驟,平台幫你把「誰跑過什麼、產出了什麼、 現在跑到哪」這些事管起來——代價是這七個東西。
⚠️ 那個內建的 MariaDB 有一個字元集陷阱,但不是你以為的那個。 我實際去查過:資料表與欄位是
utf8mb4_unicode_ci,中文存得進去、 dashboard 也顯示得正確(我的 run 描述就是中文)。陷阱在連線端:
SHOW VARIABLES LIKE 'character_set_%'; # character_set_client latin1 # character_set_connection latin1 # character_set_results latin1所以你自己寫的查詢、報表、或
mysqldump沒指定--default-character-set=utf8mb4就會拿到?????——而且不會報錯。備份尤其危險:你會得到一份看起來成功、 但中文全毀的 dump。(只有 8 個欄位真的是
latin1,全部是 UUID 欄位——那些只存十六進位字元, 用 latin1 是刻意的省空間,不影響中文。)
Pipeline 本身:用 Python 寫
KFP v2 的寫法是宣告容器步驟,然後宣告它們的順序:
from kfp import dsl
@dsl.container_component
def prepare_data(run_id: str, sample_mb: str):
return dsl.ContainerSpec(image=IMAGE, command=["bash", "-c", PREPARE, "bash"],
args=[run_id, sample_mb])
@dsl.container_component
def train_model(run_id: str, max_iters: str): ...
@dsl.container_component
def evaluate_model(run_id: str): ...
@dsl.container_component
def promotion_gate(run_id: str, max_val_loss: str): ...
@dsl.pipeline(name="llm-lifecycle")
def llm_lifecycle(run_id: str = "run1", sample_mb: str = "2",
max_iters: str = "300", max_val_loss: str = "6.0"):
p = prepare_data(run_id=run_id, sample_mb=sample_mb)
t = train_model(run_id=run_id, max_iters=max_iters).after(p)
e = evaluate_model(run_id=run_id).after(t)
g = promotion_gate(run_id=run_id, max_val_loss=max_val_loss).after(e)
.after() 就是 DAG 的邊。編譯:
python pipeline_llm.py # → llm-lifecycle.yaml
然後在 ODH dashboard 上傳那個 YAML,或用 kfp SDK 推上去。

三個當初卡住我的設定
1. 關掉 caching,不然你會以為它跑了
task.set_caching_options(False)
KFP 預設會 cache——輸入沒變就直接沿用上次的結果,秒回成功。
在 CI/CD 的情境這是優點。在「我要驗證這條鏈真的會跑」的情境, 這是災難:你會看到一條全綠的 run,實際上什麼都沒執行。
(這跟我在另一篇講的假綠是同一類問題。 做 lab 或驗收時,先把所有 cache 關掉。)
2. S3 憑證用 secret 注入,不要進 image
kubernetes.use_secret_as_env(
task, secret_name="minio-s3",
secret_key_to_env={"AWS_ACCESS_KEY_ID": "AWS_ACCESS_KEY_ID",
"AWS_SECRET_ACCESS_KEY": "AWS_SECRET_ACCESS_KEY"})
task.set_env_variable("S3_ENDPOINT", S3_ENDPOINT)
理由很直接:image 會進 registry,密碼不該跟著走。 image 可能被複製到別的環境、被別人拉、被掃描工具展開—— 任何進了 image layer 的東西都要當成已經公開。
3. 資源要一步一步給,不要整條開大
t.set_cpu_request("1").set_cpu_limit("3") \
.set_memory_request("2Gi").set_memory_limit("6Gi")
只有 train 這一棒吃資源,其他三棒是複製檔案和算數字。
整條 pipeline 開大會在資源緊的叢集上直接排不進去(我的 CRC 只有 10 CPU)。
讓 lineage 有東西可查
最後一棒有一行值得單獨講:
task.set_env_variable("PIPELINE_IMAGE", IMAGE)
因為容器裡沒有 git 可以問。你想在台帳裡記「這顆模型是哪一版程式碼訓練的」, 就得在編譯 pipeline 的時候把身份帶進去。
gate 那一棒收到之後寫進台帳:
entry["lineage"]["code_image"] = os.environ.get("PIPELINE_IMAGE", "unknown")
entry["lineage"]["run_id"] = run_id
這是 lineage 最容易漏掉的一段:大家都會記「用了哪份資料」, 但「用了哪一版程式碼」常常沒人記——而它同樣會改變模型。 image digest 是最誠實的答案,因為它不會被改寫。
跑起來長這樣
四棒依序執行,每一格綠了才走下一格:
prepare-data → train-model → evaluate-model → promotion-gate
參數在建立 run 的時候填:
run_id=run1 sample_mb=2 max_iters=300 max_val_loss=6.0
⚠️ 最後那個 max_val_loss 是 run 參數——誰都能填。
這件事的後果我另外寫了一篇,
簡短版是:三顆能力幾乎相同的模型,只因為門檻從 6.0 改成 8.0,一顆上線兩顆被擋。
你們的模型訓練有沒有進 pipeline?還是還在 notebook 或某台機器上手動跑?
🧪 這篇的實驗環境與 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-* 那一套)。
指令的邏輯可以照用,字串要自己對一次。
留言與指正
我特別想知道:你們的模型訓練有沒有進 pipeline?還是還在 notebook 或某台機器上手動跑?