📌 نظرة عامّة: MLOps حديث: من النموذج إلى الإنتاج
دَرَّبتَ نموذجًا، metrics تفوق نسخة الخدمة الحالية، والرغبة في دفع checkpoint إلى prod قويّة. تلك بالضبط لحظة فقدان الوقت. دون Registry منضبط، binary مَنسوخ يدويًّا على خادم، ولا أحد يعرف ماذا يخدم ماذا في الشهر الموالي.
MLflow Model Registry يحلّ هذه المشكلة بتحويل كلّ نسخة نموذج إلى artefact مُؤَرَّخ، قابل للوسم، وقابل للعنونة بـ URI مستقرّ. منذ MLflow 2.9 (RFC #10336)، الـ stages التاريخية (None، Staging، Production، Archived) مُستَبعَدة لصالح aliases القابلة للتغيير وtags المُهَيكَلَة.
المتطلّبات
- MLflow ≥ 3.3.0 (webhooks OSS) — نسخة مستقرّة: MLflow 3.12.0
- خادم MLflow remote (tracking + registry) بـ backend store علاقي وartefact store S3/MinIO/GCS
- Python 3.10+ و
pip install mlflow==3.12.0 - Docker Engine 24+ وregistry صور خاصّ
- Repo GitHub مع secrets، workspace Slack مع Incoming Webhook
الخطوة 1 — aliases وtags (مقابل stages المُستَبعَدة)
alias مؤشّر مُسَمَّى قابل للتغيير نحو نسخة دقيقة — champion، challenger، shadow. عدّة aliases يمكن أن تتعايش. URI models:/<name>@<alias> يحلّ إلى النسخة المُشار إليها. tag زوج مفتاح/قيمة (validation_status=passed، owner=ml-platform).
from mlflow import MlflowClient
client = MlflowClient()
# القديم (مُستَبعَد):
# client.transition_model_version_stage(name="fraud-classifier", version=7, stage="Production")
# الجديد (موصى به):
client.set_registered_model_alias(name="fraud-classifier", alias="champion", version=7)
client.set_model_version_tag(name="fraud-classifier", version=7,
key="validation_status", value="passed")
الخطوة 2 — تسجيل نموذج في registry
import mlflow, os
mlflow.set_tracking_uri(os.environ["MLFLOW_TRACKING_URI"])
RUN_ID = os.environ["RUN_ID"]
MODEL_NAME = "fraud-classifier"
model_uri = f"runs:/{RUN_ID}/model"
mv = mlflow.register_model(model_uri=model_uri, name=MODEL_NAME)
print(f"Registered {MODEL_NAME} version {mv.version} from run {RUN_ID}")
المُتَوَقَّع: Registered fraud-classifier version 12. إشارة النجاح: client.get_model_version(MODEL_NAME, mv.version).status == "READY".
الخطوة 3 — إسناد alias « challenger »
from mlflow import MlflowClient
client = MlflowClient()
client.set_registered_model_alias(name="fraud-classifier", alias="challenger", version=mv.version)
client.set_model_version_tag(name="fraud-classifier", version=mv.version,
key="validation_status", value="pending")
client.set_model_version_tag(name="fraud-classifier", version=mv.version,
key="git_sha", value=os.environ["GITHUB_SHA"])
الخطوة 4 — Workflow GitHub Actions: تقييم آلي
name: mlflow-promote
on:
workflow_dispatch:
inputs:
run_id:
description: "MLflow run_id"
required: true
env:
MODEL_NAME: fraud-classifier
MLFLOW_TRACKING_URI: ${{ secrets.MLFLOW_TRACKING_URI }}
MLFLOW_TRACKING_USERNAME: ${{ secrets.MLFLOW_TRACKING_USERNAME }}
MLFLOW_TRACKING_TOKEN: ${{ secrets.MLFLOW_TRACKING_TOKEN }}
jobs:
register-and-evaluate:
runs-on: ubuntu-24.04
outputs:
version: ${{ steps.register.outputs.version }}
passed: ${{ steps.eval.outputs.passed }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- run: pip install mlflow==3.12.0 scikit-learn==1.5.2 pandas==2.2.3
- id: register
run: |
VERSION=$(python ci/register_challenger.py "${{ inputs.run_id }}")
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- id: eval
run: |
if python ci/evaluate_challenger.py "${{ steps.register.outputs.version }}"; then
echo "passed=true" >> "$GITHUB_OUTPUT"
else
echo "passed=false" >> "$GITHUB_OUTPUT"
exit 1
fi
الخطوة 5 — ترقية challenger → champion
promote:
needs: register-and-evaluate
if: needs.register-and-evaluate.outputs.passed == 'true'
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.11" }
- run: pip install mlflow==3.12.0
- run: python ci/promote_to_champion.py "${{ needs.register-and-evaluate.outputs.version }}"
# ci/promote_to_champion.py
import sys
from mlflow import MlflowClient
MODEL_NAME = "fraud-classifier"
new_version = sys.argv[1]
client = MlflowClient()
try:
previous = client.get_model_version_by_alias(MODEL_NAME, "champion")
client.set_model_version_tag(MODEL_NAME, new_version,
"previous_champion_version", str(previous.version))
except Exception:
pass
client.set_registered_model_alias(MODEL_NAME, "champion", new_version)
client.set_model_version_tag(MODEL_NAME, new_version, "validation_status", "passed")
print(f"champion -> v{new_version}")
الخطوة 6 — بناء صورة Docker عبر mlflow models build-docker
build-image:
needs: promote
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.11" }
- run: pip install mlflow==3.12.0
- name: build docker image
env:
MLFLOW_TRACKING_URI: ${{ secrets.MLFLOW_TRACKING_URI }}
MLFLOW_TRACKING_TOKEN: ${{ secrets.MLFLOW_TRACKING_TOKEN }}
run: |
mlflow models build-docker \
--model-uri "models:/fraud-classifier@champion" \
--name "ghcr.io/${{ github.repository_owner }}/fraud-classifier:${{ github.sha }}"
إشارة النجاح: docker run --rm -p 8080:8080 ghcr.io/<owner>/fraud-classifier:<sha> ثم curl localhost:8080/ping يُرجع HTTP 200.
الخطوة 7 — نشر الصورة
deploy:
needs: build-image
runs-on: ubuntu-24.04
permissions:
packages: write
contents: read
steps:
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- run: docker push ghcr.io/${{ github.repository_owner }}/fraud-classifier:${{ github.sha }}
- uses: azure/setup-kubectl@v4
- run: echo "${{ secrets.KUBECONFIG_B64 }}" | base64 -d > $HOME/.kube/config
- run: |
kubectl set image deployment/fraud-classifier \
api=ghcr.io/${{ github.repository_owner }}/fraud-classifier:${{ github.sha }} \
-n inference
kubectl rollout status deployment/fraud-classifier -n inference --timeout=5m
الخطوة 8 — Webhook لإشعار الانتقالات
from mlflow.webhooks import create_webhook
create_webhook(
name="slack-champion-transition",
url="https://relay.internal/mlflow/slack",
events=["model_version.alias_created"],
secret="REPLACE_WITH_STRONG_SECRET",
description="Notify Slack on champion alias movement",
)
# relay Flask
import hmac, hashlib, os, requests
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["MLFLOW_WEBHOOK_SECRET"].encode()
SLACK = os.environ["SLACK_WEBHOOK_URL"]
@app.post("/mlflow/slack")
def mlflow_slack():
sig = request.headers.get("X-Mlflow-Signature", "")
expected = hmac.new(SECRET, request.data, hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expected):
abort(401)
payload = request.get_json()
if payload.get("alias") != "champion":
return "", 204
requests.post(SLACK, json={
"text": f":rocket: champion -> {payload['name']} v{payload['version']}"
}, timeout=5)
return "", 204
الخطوة 9 — rollback (إعادة إسناد alias)
# ci/rollback_champion.py
from mlflow import MlflowClient
MODEL_NAME = "fraud-classifier"
client = MlflowClient()
current = client.get_model_version_by_alias(MODEL_NAME, "champion")
previous_tag = next(
(t.value for t in client.get_model_version(MODEL_NAME, current.version).tags
if t.key == "previous_champion_version"),
None,
)
if not previous_tag:
raise SystemExit("Aucune version précédente connue, rollback impossible")
client.set_registered_model_alias(MODEL_NAME, "champion", previous_tag)
client.set_model_version_tag(MODEL_NAME, current.version, "validation_status", "rolled_back")
print(f"rollback : champion -> v{previous_tag}")
المُتَوَقَّع: rollback : champion -> v11. إن كانت pods تُخَزِّن النموذج في الذاكرة عند الإقلاع، أضف kubectl rollout restart deployment/fraud-classifier -n inference.
الخطوة 10 — التحقّق وإشارة النجاح
MODEL=fraud-classifier
# 1. Registry يكشف champion
mlflow client get-registered-model-by-alias $MODEL champion
# 2. tag تدقيق على النسخة الحالية
VERSION=$(mlflow client get-registered-model-by-alias $MODEL champion --jsonpath '$.version')
mlflow client get-model-version $MODEL $VERSION --jsonpath '$.tags'
# 3. صورة في prod تخدم نفس النسخة
curl -s https://api.example.com/fraud-classifier/version
# 4. webhook ACTIVE
python -c "from mlflow.webhooks import list_webhooks; \
print([w for w in list_webhooks() if w.name == 'slack-champion-transition'])"
# 5. استدعاء استنتاج
curl -s -X POST https://api.example.com/fraud-classifier/invocations \
-H "Content-Type: application/json" \
-d '{"inputs": [[0.1, 0.2, 0.3, 0.4, 0.5]]}'
أخطاء شائعة
| العَرَض | السبب المحتمل | التصحيح |
|---|---|---|
| DeprecationWarning: stage parameter is deprecated | كود legacy يستدعي transition_model_version_stage |
استبدل بـ set_registered_model_alias + tag |
| RESOURCE_DOES_NOT_EXIST: Alias ‘champion’ not found | أوّل نشر | غَلِّف في try/except |
build-docker يفشل على pip |
conda.yaml يُشير لـ index خاصّ | أعد log بـ extra_pip_requirements نظيف |
| signature HMAC غير صالح | secret مُقَطَّع | تحقّق من الطول |
| pod يخدم النسخة القديمة | النموذج مُحَمَّل في الذاكرة عند الإقلاع | kubectl rollout restart |
| UI يعرض N-1 | cache متصفّح أو LB | فَرِّغ cache |
| webhook مُشَغَّل عدّة مرّات | retry على timeout | idempotence بـ deduplication |
مصادر رسمية
- MLflow Model Registry — التوثيق الرسمي
- API
mlflow.client - MLflow Webhooks (OSS، منذ 3.3.0)
- RFC #10336 — Deprecating model registry stages
- MLflow CLI reference
الأدلّة المرتبطة
FAQ
هل stages MLflow ستختفي كلّيًّا؟ مُستَبعَدة منذ 2.9 لكنّها وظيفية في 3.x للتوافق. أيّ pipeline جديد يستند حصرًا إلى aliases وtags.
كم alias نضع على نسخة؟ لا حدّ صارم. عمليًّا: champion، challenger، أحيانًا shadow.
URI models:/<name>@<alias> مع كلّ flavors؟ نعم. حلّ URI يتمّ على مستوى client.
إعادة بناء صورة مع كلّ ترقية؟ موصى به إن غيّرت artefacts التبعيّات. البديل: صورة محايدة تُحَمِّل ديناميكيًّا.
webhook MLflow يُدير retries؟ نعم. يُفرض idempotence في relay.
عدّة بيئات مع aliases؟ aliases scoped (staging-champion) أو فصل في RegisteredModel متمايزة.
لماذا لا mlflow deployments مباشرة؟ يُغطّي targets مُدارة (SageMaker، Azure ML، Databricks Serving). لـ Kubernetes ذاتي، build-docker الخيار القياسي.