docs(training-guides): daily fine-tuning pipeline with MLflow + TrustyAI#280
docs(training-guides): daily fine-tuning pipeline with MLflow + TrustyAI#280typhoonzero wants to merge 5 commits into
Conversation
…cking and TrustyAI evaluation Full solution guide under docs/en/training_guides/ that stitches together the smaller Kubeflow Pipelines + MLflow primer and TrustyAI LM-Eval reference into one end-to-end recipe: SFT Qwen3-0.6B with the HF Trainer + MLflow autologging, register the model in the MLflow Model Registry, deploy it as a KServe InferenceService, evaluate it with a TrustyAI LMEvalJob, log the results back into the same MLflow experiment, clean up the temporary serving resources, schedule the whole thing as a KFP Recurring Run, and compare day-over-day evaluation runs in MLflow. Also wires the new page into the training_guides Pick-a-path index and adds a See-also from pipelines-mlflow-integration.mdx. TrustyAI-slice smoke on the x86 dev cluster surfaced three gotchas that are now folded back into the guide: - ta-lmes-job runs as UID 65532 and needs pod.securityContext.fsGroup so the operator-managed output PVC is writable. - state=Complete is reached on both success and failure; check status.reason to distinguish them. - tokenized_requests: "False" does NOT skip AutoTokenizer.from_pretrained, so air-gapped clusters must use the offline PVC path in the LM-Eval docs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Deploying alauda-ai with
|
| Latest commit: |
80e87c7
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://8acab34e.alauda-ai.pages.dev |
| Branch Preview URL: | https://docs-finetune-pipeline-mlflo.alauda-ai.pages.dev |
… x86 + DSPO Fed back real findings from getting the pipeline to reach `Succeeded::9/9` on the x86 dev cluster (DSPO KFP backend, MLflow Cluster Plugin v3.10.0, HAMi vGPU): - KFP-v2 pods hit `runAsNonRoot` on argoexec/kfp-launcher; document the `spec.podSpecPatch` workaround and the auto-recycle-of-CreateContainerConfigError pods trick. - Shared PVC root dir is root-owned; either add `fsGroup: 1001` to the same podSpecPatch or bootstrap-chown once. - `python:3.12-slim` has `HOME=/`, so ModelScope/HF caches trip `PermissionError: '/.modelscope'` on non-root pods — set `MODELSCOPE_CACHE`, `HF_HOME`, `HOME` env vars in the component. - `torch>=2.3` pulls the full CUDA stack; use `torch==2.5.1+cpu` from Aliyun's pytorch-wheels mirror (Tsinghua does not host `+cpu` variants; pytorch.org is bandwidth-throttled to <100KB/s from mainland-China dev clusters). - KFP v2 `dsl.ExitHandler` fails DSPO compile with `unknown component implementation` — use `cleanup(...).after(evaluate)` and make cleanup idempotent (delete-by-label). - KServe vLLM runs as UID 1000; fine-tune writes as UID 1001 with umask 0660, so vLLM crashes with `FileNotFoundError: model.safetensors`. `chmod` the save directory to 0755/0644 after `save_pretrained`. - `mlflow.set_experiment(...)` must be called in every component that opens a nested run — otherwise MLflow defaults to experiment id 0 which the multi-tenant server rejects. - The Alauda MLflow plugin's default `/mlflow/artifacts` is emptyDir on the server pod, so client `log_artifacts` / `log_model` hits `PermissionError: '/mlflow'` and `create_model_version` rejects `file://` sources outside a run's artifact dir. Document the two options: reconfigure MLflow for S3, or skip the registry and coordinate via parent-run tags. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Full E2E now green on
|
Correction: earlier smoke run used a bypass Service; the doc's proxy-based auth is now what's verifiedAn earlier comment described the smoke run using Re-ran the smoke with the canonical path from the SDK guide:
MLflow API confirms the fine-tune component wrote the parent run through the proxy: Also worth noting: The doc's example is unchanged and correct; the earlier PR commentary was misleading. 🤖 Generated with Claude Code |
…o_install Replaces base_image=python:3.11-slim + packages_to_install on all four KFP components with a single pre-built image reference. This removes the last runtime `pip install` step, so the pipeline works on air-gapped clusters once the image is mirrored into an internal registry. Image: docker.io/alaudadockerhub/finetune-pipeline-runtime-cu126-amd64:v0.1.0 Derived from alaudadockerhub/llamafactory0.9-cu126-amd64:v0.1.0 (part of the published training-runtimes catalog) with one thin layer that pins `mlflow>=3.10` (for `set_workspace`), `kserve>=0.13`, and `kubernetes>=29`. Doc changes: - Prerequisites: new row calling out the runtime image + air-gap flow. - Pipeline: introduce `RUNTIME_IMAGE = "docker.io/alaudadockerhub/…"` and swap every `@dsl.component(base_image=..., packages_to_install=...)` to `@dsl.component(base_image=RUNTIME_IMAGE)`. - Troubleshooting: remove now-impossible rows (`mlflow.transformers AttributeError`, `torch>=2.3 pulls 4 GiB`). Add row on mirroring the image into an air-gapped registry. - New "Bake a custom image" section with a one-layer Containerfile users can copy to roll their own runtime. - Related guides: add training-runtimes catalog cross-link.
|
Follow-up: dropped the runtime Image: Derivation (one layer): ARG BASE_IMAGE=docker.io/alaudadockerhub/llamafactory0.9-cu126-amd64:v0.1.0
FROM ${BASE_IMAGE}
USER 0
RUN uv pip install --no-cache-dir "mlflow>=3.10" "kubernetes>=29" "kserve>=0.13"
USER 1000
WORKDIR /workspaceBase is Commit: 5825011. Also covered in the new Bake a custom image section for readers who need to swap the CUDA line or add private deps. |
…dataset section
The example previously called `load_dataset("tatsu-lab/alpaca")` which
requires HuggingFace egress at pod-start — the same air-gap problem the
last commit fixed for pip installs.
Now the `fine_tune` component's default is a small **synthetic**
in-process instruction corpus (arithmetic Q/A pairs, `Dataset.from_list`),
generated fully offline. Users can swap in a real dataset by passing
`dataset_path` — either a JSONL file on the shared PVC or an `s3://…`
URI to seaweedfs / MinIO.
Code changes:
- Rename `dataset_id` → `dataset_path` in both the component and the
pipeline function; default is empty string (= synthetic).
- Inline synthetic dataset generation via `operator` + `Dataset.from_list`.
- Loader dispatch: empty → synthetic, "s3://…" → `load_dataset("json",
data_files=..., ...)` reading credentials from AWS_* env, otherwise
local JSONL via `load_dataset("json", data_files=<path>)`.
Doc changes:
- New "Using a real dataset on air-gapped clusters" section (anchor
`#using-a-real-dataset`) with four sub-steps: download from HF or
ModelScope, convert to JSONL with a `text` column, upload to the
shared PVC OR to seaweedfs/S3 (with an `mlflow-s3` Secret + a
`use_secret_as_env` snippet for the pipeline), and how to submit
the pipeline with `dataset_path`.
- Note on air-gapping the base model itself (mirroring
`Qwen/Qwen3-0.6B` into the PVC and pointing `BASE_MODEL` at it).
- Prerequisites: reworded the Hugging Face egress row to name both
network paths (base model + LMEvalJob tokenizer) and cross-link the
new section. New "Training dataset (optional)" row clarifies the
synthetic default.
- Step 3 submission example: commented-out `dataset_path` argument
showing the shape.
Doom lint clean.
|
Follow-up: dropped the last runtime network dependency — Default is now a synthetic in-process instruction corpus (arithmetic Q/A pairs generated with New section:
Commit: 24574e0. |
…d by e2e
Running the pipeline as-written on the x86 dev cluster with the pre-built
runtime image (llamafactory 0.9.4 → TRL 0.12+, mlflow 3.10+) surfaced two
pre-existing bugs in the example code, plus one operator-side gotcha:
1. `TypeError: SFTTrainer.__init__() got an unexpected keyword argument
'tokenizer'`. TRL 0.12 removed the `tokenizer=` alias — pass the
tokenizer as `processing_class=tokenizer` instead.
2. `mlflow.transformers.log_model` fails with `MlflowException: The task
could not be inferred from the model. If you are saving a custom
local model that is not available in the Hugging Face hub, please
provide the 'task' argument ...`. Add `task="text-generation"` — the
fine-tune pod's model is loaded from a local PVC path, not from the
Hub, so MLflow cannot auto-infer the task.
3. Operator-side: MLflow client-side calls return `UNAUTHENTICATED:
Authentication with the Kubernetes API failed`. The mlflow-plugin
controller reconciles the mlflow-tracking-server Deployment and
periodically strips the `OAUTH2_PROXY_SKIP_JWT_BEARER_TOKENS=true`
env from the `oauth2-proxy` sidecar. Documented as a fresh
troubleshooting row so operators know to re-apply after any
operator upgrade or manual re-install.
E2E verification (on the x86 dev cluster's DSPO + MLflow plugin):
- Runtime image pulls via docker-mirrors.alauda.cn.
- Synthetic dataset codepath: `Dataset.from_list` → TRL SFTTrainer.
- MLflow bearer auth through the OAuth-proxy accepts the id_token.
- Parent run `pipeline-<run_id>` (FINISHED) gets `pipeline_run_id`,
`base_model`, `pvc_path` tags.
- Nested `train-<run_id>` streams `train_loss=10.32`, `train_runtime`,
`train_samples_per_second`, `epoch`, `num_tokens`, `max_steps` via
the HF Trainer MLflow callback.
Code changes:
- fine_tune component: `SFTTrainer(..., processing_class=tokenizer)`.
- fine_tune component: `mlflow.transformers.log_model(...,
task="text-generation")`.
Doc changes:
- Three new troubleshooting rows for the two code-side issues and the
one operator-side OAuth-proxy env issue.
|
E2E on x86 dev cluster (DSPO + MLflow plugin). Ran the fine_tune component end-to-end against the pre-built runtime image; the smoke uses this doc's synthetic-data + pre-mirrored-base-model path exactly as documented. Verified working:
Pre-existing doc bugs the e2e uncovered — pushed as commit 80e87c7:
The smoke run's evidence: parent |
Summary
Adds
docs/en/training_guides/fine-tuning-pipeline-with-mlflow-trustyai.mdx— an end-to-end solution guide that stitches Kubeflow Pipelines, MLflow tracking + Model Registry, KServe, and TrustyAI LM-Eval into one recipe:Qwen/Qwen3-0.6Bwith the HFTrainer+report_to="mlflow", register the fine-tuned model in the MLflow Model Registry.InferenceService.LMEvalJoband log the metrics back into the same MLflow experiment as a nested run + a tag on the model version.dsl.ExitHandler.mlflow.search_runssnippet for a CI gate and aset_registered_model_alias("champion", …)snippet for promotion.Also wires the new page into the
training_guidesPick-a-path index and adds a "See also" line frompipelines-mlflow-integration.mdx.What's been validated
TrustyAI-slice smoke on the
x86dev cluster (only TrustyAI was installed; KFP + MLflow are not present, so the KFP orchestration is not exercised):ceph-rbdPVC.InferenceServiceon the HAMi vGPU with the platform'saml-vllm-0.20.2-cuda-13.0runtime;/v1/completionsreturns the expected Qwen3 answer.LMEvalJobCRD accepts the exact spec the guide'sevaluatecomponent builds and launchespython -m lm_evalwith the rightmodel_args.Three real gotchas surfaced by the smoke are now folded into the guide (see Prereqs + Troubleshooting):
ta-lmes-jobruns as UID 65532, sospec.pod.securityContext.fsGroup: 65532is required or the driver dies withopen …/output/stdout.log: permission denied.state == "Complete"is reached on both success and failure — the guide's wait loop now also checksstatus.reason == "Succeeded".tokenized_requests: "False"does not skipAutoTokenizer.from_pretrained(...), so air-gapped clusters must switch to the offline PVC path documented intrustyai/lm-eval.mdx.Test plan
yarn lintclean (verified locally).0 2 * * *and confirm two daily runs land as separate rows in the MLflow experiment.🤖 Generated with Claude Code