Daily Fine-Tuning Pipeline with MLflow Tracking and TrustyAI Evaluation
A complete Kubeflow Pipeline that fine-tunes an LLM (Qwen3-0.6B in this example), tracks the run in MLflow on Kubeflow, registers the resulting model in the MLflow Model Registry, deploys it as a KServe InferenceService, evaluates it with a TrustyAI LMEvalJob, writes the evaluation numbers back into the same MLflow experiment, and cleans the temporary serving resources up. The pipeline is then wired to a KFP Recurring Run so it fires once a day — giving you a growing history of train → eval runs that MLflow's compare view turns into a regression signal.
This guide assembles the pieces already documented individually — Use Kubeflow Pipelines, Kubeflow Pipeline + MLflow Integration, Evaluate LLM — into one working recipe. Read those first if any step is unfamiliar.
TOC
What the pipeline doesPrerequisitesStep 1 — Create the MLflow token Secret and the shared PVCStep 2 — The pipelineUsing a real dataset on air-gapped clusters1. Download the dataset on a machine with egress2. Convert to a JSONL file with atext column3a. Upload to the shared PVC (simplest)3b. Upload to S3 / seaweedfs (shared across many pipelines)4. A note on the base modelStep 3 — Submit a one-off runStep 4 — Visualize training metrics in MLflowStep 5 — Schedule it as a daily Recurring RunFrom the UIFrom the SDKStep 6 — Compare daily evaluation results in MLflowTroubleshootingBake a custom imageRelated guidesWhat the pipeline does
Each daily run adds one row to the MLflow experiment. Because the training and evaluation metrics live under the same parent run, MLflow's Compare and Chart views plot day-over-day loss and evaluation accuracy without extra glue code.
Prerequisites
Step 1 — Create the MLflow token Secret and the shared PVC
The pipeline components below assume both objects exist in the namespace the pipeline runs in.
Step 2 — The pipeline
Save the following as finetune_pipeline.py. The pipeline is split into four components so failures are localized and re-runs are cheap; components share state through the PVC and through MLflow tags on the model version.
Compile:
Using a real dataset on air-gapped clusters
The fine_tune component defaults to a synthetic in-process instruction corpus (a few hundred arithmetic Q/A pairs). That keeps the smoke run fully offline — no HuggingFace, no ModelScope, no S3 — but it will not produce a meaningful fine-tune. For real workloads, pre-stage the dataset onto the cluster once, then pass its path as dataset_path on every run. The fine_tune component accepts either a local JSONL path (simplest) or an s3://… URI (shared across many pipelines).
1. Download the dataset on a machine with egress
Any machine that can reach Hugging Face — a laptop, a jump-host, a Workbench in a namespace with egress — is fine:
If Hugging Face is blocked, ModelScope mirrors most popular datasets and works from mainland-China networks:
2. Convert to a JSONL file with a text column
TRL's SFTTrainer expects one text field per row. Adapt the raw dataset into the shape the pipeline consumes:
3a. Upload to the shared PVC (simplest)
If your cluster has a jump-pod with the finetune-shared PVC mounted, kubectl cp the JSONL into it:
Then submit the pipeline with dataset_path="/mnt/shared/datasets/alpaca.jsonl" (see Step 3 below).
3b. Upload to S3 / seaweedfs (shared across many pipelines)
For teams running many pipelines, host the dataset in the same S3-compatible bucket the MLflow plugin uses for its artifact store — the cluster-internal seaweedfs endpoint is reachable from every pipeline pod, and clients only need AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY env vars.
Upload from a machine that can reach the S3 endpoint (typically a Workbench in the same cluster):
(Adjust the endpoint to your seaweedfs / MinIO Service. Use mc alias set + mc cp for MinIO Client, or s3cmd, or Python's boto3 — any tool that speaks S3 works.)
Then create a Secret with the S3 credentials and inject it into fine_tune alongside mlflow-token:
Add the following to the pipeline function (right next to the existing use_secret_as_env block that mounts the MLflow token):
Then submit the pipeline with dataset_path="s3://mlops-datasets/alpaca/alpaca.jsonl". s3fs is already in the runtime image, and load_dataset("json", data_files="s3://…") uses fsspec under the hood — the AWS_* env is picked up automatically.
4. A note on the base model
AutoModelForCausalLM.from_pretrained("Qwen/Qwen3-0.6B") also hits Hugging Face on first run. For a fully air-gapped fine-tune, mirror the model once (same pattern: huggingface-cli download Qwen/Qwen3-0.6B --local-dir ./qwen3-0.6b, then copy to the PVC or S3) and set BASE_MODEL = "/mnt/shared/models/qwen3-0.6b". The MLflow model registry then registers the fine-tune under qwen3-0.6b-sft as before.
Step 3 — Submit a one-off run
Upload pipeline.yaml in Kubeflow Dashboard → Pipelines → Upload Pipeline, then Create Run — or, from a Workbench:
While the run is in progress:
- Open Alauda AI → Tools → MLFlow and pick the
finetuneworkspace. - The
qwen3-0.6b-daily-sftexperiment shows one parent run namedpipeline-<pipeline-run-id>, with two nested runs:train-<...>(streamed by the HF Trainer callback) and, later,eval-<...>(written by theevaluatestep). - Under Models, the
qwen3-0.6b-sftregistered model gains a new version.
Step 4 — Visualize training metrics in MLflow
- In the MLflow UI, open the parent run → Metrics tab.
- Select
loss,eval_loss, and any custom metrics you added (perplexity,learning_rate,grad_norm) → click Chart. - To watch the loss curve live, set the Refresh interval to
10son the chart panel — MLflow re-fetches the metric stream as the Trainer writes new steps.
For classification-style objectives where accuracy matters, add a compute_metrics callback that returns {"accuracy": ...} in your TrainingArguments; report_to="mlflow" streams whatever compute_metrics returns straight into the same chart.
Step 5 — Schedule it as a daily Recurring Run
KFP has native cron scheduling. Wire the same pipeline.yaml to a Recurring Run so it fires every day.
From the UI
- Pipelines → pick
qwen3-sft-mlflow-trustyai→ Create Run. - Choose Recurring Run as the run type.
- Trigger:
Cron, expression0 2 * * *(02:00 UTC every day). - Max concurrent runs:
1— the pipeline claims one GPU; overlapping runs will queue. - Catchup:
off— skip missed windows instead of running a backlog on Monday morning. - Parameters:
num_train_epochs=1,eval_limit=200for the daily job (higher-signal than the one-off default). - Start.
From the SDK
The MLflow token in Secret/mlflow-token expires (Dex id tokens live 24 h by default). For a schedule that runs longer than the token lifetime, either mint the token inside the fine_tune component from service-account credentials and refresh it there (see the SDK token flow), or run a small CronJob that rotates the mlflow-token Secret on a schedule slightly shorter than the token TTL.
Step 6 — Compare daily evaluation results in MLflow
After a few days of runs the experiment has a row per day. MLflow makes the comparison one click away:
- In the MLflow UI, open the
qwen3-0.6b-daily-sftexperiment. - In the runs table, filter to nested eval runs:
tags.mlflow.runName LIKE 'eval-%'. - Sort by
attributes.start_time DESC. - Tick the check-boxes of the runs you want to compare (last 7 days is a good starting point) → click Compare.
- In the Chart tab, plot
arc_easy/acc_noneandhellaswag/acc_nonegrouped bytags.mlflow.runName. Steady lines mean the fine-tune is stable; a sharp step down usually means the training dataset or the base checkpoint changed. - The Parallel Coordinates view groups training hyper-parameters and evaluation metrics so you can eyeball which hyper-parameters correlate with the biggest evaluation gains.
For programmatic comparison — for example, to feed into a Slack alert or a CI gate that fails the pipeline when the accuracy drops more than 2 % day-over-day — use the search API:
To promote the best-scoring daily version to a downstream serving stack, use the Model Registry aliasing API — the pipeline already tagged each version with eval_acc:
Troubleshooting
Bake a custom image
The reference image is derived from llamafactory0.9-cu126-amd64 and only adds mlflow>=3.10, kserve>=0.13, and kubernetes>=29. To roll your own — for example to swap the CUDA line, add proprietary data-processing libs, or FIPS-comply the base — write a one-layer Containerfile:
Build with buildctl (or docker buildx build) against your buildkitd, push to your internal registry, and set RUNTIME_IMAGE accordingly. The rest of the pipeline code is unchanged.
If instead your fleet already runs Kubeflow Trainer v2, you can reuse one of the published TrainingRuntime images — llamafactory0.9-cu126-amd64:v0.1.0 already ships torch / HF / trl / mlflow; only kserve and kubernetes need adding for the deploy_for_evaluation + evaluate + cleanup components. That is exactly what the reference image does.
Related guides
- Kubeflow Pipeline + MLflow Integration — the smaller primer this guide extends.
- Using the MLflow Python SDK with Authentication and RBAC — how the
MLFLOW_TRACKING_TOKENis obtained and refreshed. - Evaluate LLM — full
LMEvalJobreference, including offline / air-gapped mode. - Use Kubeflow Pipelines — Recurring Runs UI and object-storage setup.
- Training Runtime Images — catalog of pre-built framework images (torch, LLaMA-Factory, TrainingHub, MindSpeed-LLM) the reference image is derived from.
- Fine-Tuning with Kubeflow Trainer v2 — a
TrainJob-based alternative to the SFT component above.