使用 Workbench 对 LLM 进行 Fine-tuning
本指南演示如何使用从 Alauda AI Workbench 启动的 LLaMA-Factory 对 LLM(示例:Qwen3-0.6B)进行 Fine-tuning。Notebook 会向集群提交一个 VolcanoJob,因此 GPU 任务会在集群节点上运行,而你可以继续在 JupyterLab 中迭代。
当你需要交互式控制、自定义训练脚本,以及按实验进行 YAML 调整时,请使用本指南。对于可复用模板和配额管理,建议改用 Kubeflow Trainer v2。
适用范围
- Alauda AI 1.3 及以上版本。
- 在 x86_64 + NVIDIA GPUs 上进行 LLM Fine-tuning。其他模型家族(例如 YOLOv5)需要各自的镜像、脚本和数据集格式。
- NPU 集群需要与厂商栈兼容的运行时镜像——请参见下方的 在非 NVIDIA GPUs 上运行,或参见 Ascend NPU 配方。
前提条件
- 已安装 Alauda AI Workbench 插件(或 Kubeflow Base + Notebook)。
- 已安装 MLflow 插件用于实验跟踪。
1. 创建 Notebook / VSCode 实例
在 Alauda AI → Workbench(或 Advanced → Kubeflow → Notebook)中创建一个 workbench。workbench 本身只应请求 CPU——GPU 由其提交的 VolcanoJob 请求。参见 创建 Workbench。
2. 准备基础模型
下载 Qwen/Qwen3-0.6B(或任意 HF model),并将其推送到平台模型仓库。参见 使用 Notebook 上传模型。
3. 准备输出模型占位符
在模型仓库中创建一个空的模型条目,用于接收 Fine-tuning 后的输出,并记下其 Git URL。
4. 准备数据集
使用 示例 identity 数据集,它会教模型回答 “Who are you?”。先在 Datasets → Dataset Repository 下创建一个空的数据集仓库,然后对解压后的文件执行 git lfs push。刷新后,仓库文件列表中应显示已上传内容。
数据集格式必须与 Fine-tuning 框架的预期格式一致。
HuggingFace datasets 格式
import datasets
print(datasets.get_dataset_infos("<dataset directory>"))
print(datasets.load_dataset("<dataset directory>"))
LLaMA-Factory 格式
如果你使用 LLaMA-Factory,请使用其期望的目录结构——参见 data_preparation。
5. 运行时镜像
可直接使用预构建镜像 alaudadockerhub/fine_tune_with_llamafactory:v0.1.1,也可以自行构建。镜像必须包含 git lfs,以便能够拉取和推送模型 / 数据集。
Containerfile
ARG LLAMA_FACTORY_VERSION="v0.9.4"
FROM <registry-address>/mlops/nvidia/pytorch:24.12-py3
RUN sed -i 's@//.*archive.ubuntu.com@//mirrors.ustc.edu.cn@g' /etc/apt/sources.list.d/ubuntu.sources && \
sed -i 's/security.ubuntu.com/mirrors.ustc.edu.cn/g' /etc/apt/sources.list.d/ubuntu.sources && \
apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
git git-lfs unzip curl ffmpeg default-libmysqlclient-dev build-essential pkg-config && \
apt clean && rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple -U pip setuptools && \
cd /opt && \
git clone --depth 1 https://github.com/hiyouga/LLaMA-Factory.git && \
cd LLaMA-Factory && git checkout ${LLAMA_FACTORY_VERSION} && \
sed -i '/torch>=2.4.0/d;/torchvision>=0.19.0/d;/torchaudio>=2.4.0/d' pyproject.toml && \
pip install --no-cache-dir -e ".[metrics,awq,modelscope]" -i https://pypi.tuna.tsinghua.edu.cn/simple
RUN pip install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple \
"transformers>=4.51.1,<=4.53.3" "tokenizers>=0.21.1" \
"sqlalchemy~=2.0.30" "pymysql~=1.1.1" "loguru~=0.7.2" "mysqlclient~=2.2.7" \
"deepspeed~=0.18.8" "mlflow>=3.1"
WORKDIR /opt
6. 提交 Fine-tuning VolcanoJob
创建一个 YAML,并在 notebook 终端中使用 kubectl create -f vcjob_sft.yaml 提交。(使用 JupyterLab 上传器将 kubectl 二进制文件放入 workbench —— 该镜像不包含它。)
VolcanoJob YAML
apiVersion: batch.volcano.sh/v1alpha1
kind: Job
metadata:
generateName: vcjob-sft-qwen3-
spec:
minAvailable: 1
schedulerName: volcano
maxRetry: 1
queue: default
volumes:
# Workspace PVC (temporary; deleted after the job)
- mountPath: "/mnt/workspace"
volumeClaim:
accessModes: [ "ReadWriteOnce" ]
storageClassName: "sc-topolvm"
resources:
requests:
storage: 5Gi
tasks:
- name: "train"
replicas: 1 # >= 2 for distributed training
template:
metadata:
name: train
spec:
restartPolicy: Never
securityContext:
runAsNonRoot: true
runAsUser: 65534
runAsGroup: 65534
fsGroup: 65534
volumes:
- name: dshm
emptyDir: { medium: Memory, sizeLimit: 2Gi }
# PVC for models and datasets. For distributed jobs, prefer NFS / Ceph
# for simplicity, or local storage pre-cached via kserve local model cache.
- name: models-cache
persistentVolumeClaim:
claimName: wy-model-cache
initContainers:
- name: prepare
image: alaudadockerhub/fine_tune_with_llamafactory:v0.1.1
imagePullPolicy: IfNotPresent
env:
- { name: BASE_MODEL_URL, value: "https://<git-host>/<ns>/amlmodels/qwen3-0.6b" }
- { name: DATASET_URL, value: "https://<git-host>/<ns>/amldatasets/identity-alauda" }
- name: GIT_USER
valueFrom: { secretKeyRef: { name: aml-image-builder-secret, key: MODEL_REPO_GIT_USER } }
- name: GIT_TOKEN
valueFrom: { secretKeyRef: { name: aml-image-builder-secret, key: MODEL_REPO_GIT_TOKEN } }
resources:
requests: { cpu: 100m, memory: 128Mi }
limits: { cpu: 2, memory: 4Gi }
securityContext:
allowPrivilegeEscalation: false
capabilities: { drop: [ALL] }
runAsNonRoot: true
seccompProfile: { type: RuntimeDefault }
volumeMounts:
- { name: models-cache, mountPath: /mnt/models }
command: [ /bin/bash, -c ]
args:
- |
set -ex
cd /mnt/models
gitauth="${GIT_USER}:${GIT_TOKEN}"
BASE_MODEL_NAME=$(basename ${BASE_MODEL_URL})
if [ ! -d ${BASE_MODEL_NAME} ]; then
GIT_LFS_SKIP_SMUDGE=1 git -c http.sslVerify=false -c lfs.activitytimeout=36000 \
clone "https://${gitauth}@${BASE_MODEL_URL#https://}"
(cd ${BASE_MODEL_NAME} && git -c http.sslVerify=false -c lfs.activitytimeout=36000 lfs pull)
fi
DATASET_NAME=$(basename ${DATASET_URL})
rm -rf ${DATASET_NAME} data
git -c http.sslVerify=false -c lfs.activitytimeout=36000 \
clone "https://${gitauth}@${DATASET_URL#https://}"
containers:
- name: train
image: alaudadockerhub/fine_tune_with_llamafactory:v0.1.1
imagePullPolicy: IfNotPresent
volumeMounts:
- { mountPath: /dev/shm, name: dshm }
- { name: models-cache, mountPath: /mnt/models }
env:
- { name: BASE_MODEL_URL, value: "https://<git-host>/<ns>/amlmodels/qwen3-0.6b" }
- { name: DATASET_URL, value: "https://<git-host>/<ns>/amldatasets/identity-alauda" }
- { name: OUTPUT_MODEL_URL, value: "https://<git-host>/<ns>/amlmodels/wy-sft-output" }
- { name: HF_HOME, value: /mnt/workspace/hf_cache }
- { name: DO_MERGE, value: "true" }
- name: GIT_USER
valueFrom: { secretKeyRef: { name: aml-image-builder-secret, key: MODEL_REPO_GIT_USER } }
- name: GIT_TOKEN
valueFrom: { secretKeyRef: { name: aml-image-builder-secret, key: MODEL_REPO_GIT_TOKEN } }
- { name: MLFLOW_TRACKING_URI, value: "http://mlflow-tracking-server.kubeflow:5000" }
- { name: MLFLOW_EXPERIMENT_NAME, value: "<your-namespace>" }
command: [ bash, -c ]
args:
- |
set -ex
if [ "${VC_WORKER_HOSTS}" != "" ]; then
export N_RANKS=$(echo "${VC_WORKER_HOSTS}" | awk -F',' '{print NF}')
export RANK=$VC_TASK_INDEX
export MASTER_HOST=$(echo "${VC_WORKER_HOSTS}" | awk -F',' '{print $1}')
export WORLD_SIZE=$N_RANKS NNODES=$N_RANKS NODE_RANK=$RANK
export MASTER_ADDR=${MASTER_HOST} MASTER_PORT="8888"
else
export N_RANKS=1 RANK=0 NNODES=1 MASTER_HOST=""
fi
cd /mnt/workspace
BASE_MODEL_NAME=$(basename ${BASE_MODEL_URL})
DATASET_NAME=$(basename ${DATASET_URL})
cat >lf-sft.yaml <<EOL
model_name_or_path: /mnt/models/${BASE_MODEL_NAME}
stage: sft
do_train: true
finetuning_type: lora
lora_target: all
lora_rank: 8
lora_alpha: 16
lora_dropout: 0.1
dataset: identity_alauda
dataset_dir: /mnt/models/${DATASET_NAME}
template: qwen
cutoff_len: 1024
max_samples: 1000
overwrite_cache: true
preprocessing_num_workers: 8
output_dir: output_models
logging_steps: 10
save_steps: 500
plot_loss: true
overwrite_output_dir: true
per_device_train_batch_size: 2
gradient_accumulation_steps: 2
learning_rate: 2.0e-4
num_train_epochs: 4.0
bf16: false
fp16: true
ddp_timeout: 180000000
val_size: 0.1
per_device_eval_batch_size: 1
eval_strategy: steps
eval_steps: 500
report_to: mlflow
EOL
if [ ${NNODES} -gt 1 ]; then
echo "deepspeed: ds-z3-config.json" >> lf-sft.yaml
FORCE_TORCHRUN=1 llamafactory-cli train lf-sft.yaml
else
unset NNODES NODE_RANK MASTER_ADDR MASTER_PORT
llamafactory-cli train lf-sft.yaml
fi
if [ "${DO_MERGE}" = "true" ]; then
cat >lf-merge-config.yaml <<EOL
model_name_or_path: /mnt/models/${BASE_MODEL_NAME}
adapter_name_or_path: output_models
template: qwen
finetuning_type: lora
export_dir: output_models_merged
export_size: 4
export_device: cpu
export_legacy_format: false
EOL
llamafactory-cli export lf-merge-config.yaml
else
mv output_models output_models_merged
fi
cd /mnt/workspace/output_models_merged
touch README.md
PUSH_URL="https://${GIT_USER}:${GIT_TOKEN}@${OUTPUT_MODEL_URL#https://}"
push_branch=$(date +'%Y%m%d-%H%M%S')
git init && git checkout -b sft-${push_branch}
git lfs track *.safetensors
git add .
git -c user.name='AMLSystemUser' -c user.email='aml_admin@cpaas.io' commit -am "fine tune push auto commit"
git -c http.sslVerify=false -c lfs.activitytimeout=36000 push -u ${PUSH_URL} sft-${push_branch}
resources:
requests: { cpu: "1", memory: "2Gi" }
limits: { cpu: "8", memory: "16Gi", nvidia.com/gpu: 1 }
securityContext:
allowPrivilegeEscalation: false
capabilities: { drop: [ALL] }
runAsNonRoot: true
seccompProfile: { type: RuntimeDefault }
提交前需要修改的内容:
- 将
BASE_MODEL_URL、DATASET_URL、OUTPUT_MODEL_URL 改为你的仓库 Git URL。
models-cache PVC —— 需要提前创建。可在不同实验之间复用它,以避免重复下载基础模型。
- 共享内存
dshm —— 多 GPU 场景下至少 4 GiB。
- CPU / memory / GPU 的
requests 和 limits —— 需要与集群的 device-plugin 匹配(例如 nvidia.com/gpu、nvidia.com/gpualloc)。
- 超参数 —— LLaMA-Factory 的 YAML 直接内联在脚本中。建议将常调参数提取为环境变量。
NFS workspace PVC 说明
如果 PVC 后端是 NFS:
7. 管理任务
kubectl get vcjob
kubectl get vcjob <name> -o yaml
kubectl get pod && kubectl logs <pod>
kubectl describe vcjob <name> # if pods aren't scheduling
kubectl get podgroups # Volcano scheduling view
kubectl delete vcjob <name>
成功后,合并后的模型会被推送到输出仓库中一个带日期时间戳的分支(sft-YYYYMMDD-HHMMSS)——发布时请选择该分支。
8. 实验跟踪
在 LLaMA-Factory 配置中设置 report_to: mlflow,并配合 MLFLOW_TRACKING_URI / MLFLOW_EXPERIMENT_NAME 环境变量,即可将指标路由到 MLflow。在 Alauda AI → Tools → MLFlow 中查找运行记录,对比 loss 曲线,并固定表现最佳的运行。
在启用了安全机制(SSO + multi-tenant)的 MLflow 安装中,任务还必须进行认证——请提供 MLFLOW_TRACKING_TOKEN 并选择 workspace。有关如何获取令牌并配置客户端,请参见 Using the MLflow Python SDK with Authentication and RBAC。
9. 发布 Fine-tuned 模型
该示例使用 LoRA,并在推送前将 adapter 合并到基础模型中。目前尚不支持来自 base + adapter 组合的推理服务。
- 在 Model Repository → Fine-tuned 输出模型 → Model Info → File Management → Edit Metadata 中,将 Task Type 设置为 文本分类,Framework 设置为 Transformers。
- 选择 Publish Inference API → Custom Publishing。
- 选择与集群 CUDA 版本匹配的 vLLM runtime,填写 storage / resource / GPU 设置,然后点击 Publish。
- 启动后,点击 Experience 即可与模型对话(仅当模型包含
chat_template 时)。
在非 NVIDIA GPUs 上运行
适用于 Huawei Ascend NPU、Intel Gaudi、AMD 等。关于使用 PyTorch CANN + MindSpeed-LLM 的 Ascend NPU 配方,请参见 在 Ascend NPU 上 Fine-tune 和 Pretrain LLMs。
通用步骤如下:
- 前提条件:已部署厂商驱动和 Kubernetes device plugin,并且设备对 pod 可见。请记录资源名(例如
huawei.com/Ascend910: "1")。
- 收集厂商方案——文档、Fine-tuning 镜像、受支持模型、示例数据,以及启动命令 / 参数。
- (可选)先端到端验证厂商方案,以排除方案本身的问题。
- (可选)先将其封装为基本 Kubernetes Job,在添加 Volcano 之前确认 device plugin 在 K8s 下可正常工作。
- 使用 VolcanoJob 运行,以下面的 YAML 作为起点。
VolcanoJob YAML(厂商模板)
apiVersion: batch.volcano.sh/v1alpha1
kind: Job
metadata:
generateName: vcjob-sft-
spec:
minAvailable: 1
schedulerName: volcano
maxRetry: 1
queue: default
volumes:
- mountPath: "/mnt/workspace"
volumeClaim:
accessModes: [ "ReadWriteOnce" ]
storageClassName: "sc-topolvm"
resources:
requests:
storage: 5Gi
tasks:
- name: "train"
replicas: 1
template:
metadata: { name: train }
spec:
restartPolicy: Never
volumes:
- name: dshm
emptyDir: { medium: Memory, sizeLimit: 2Gi }
- name: models-cache
persistentVolumeClaim:
claimName: sft-qwen3-volume
containers:
- name: train
image: "<vendor-fine-tuning-image>"
imagePullPolicy: IfNotPresent
volumeMounts:
- { mountPath: /dev/shm, name: dshm }
- { name: models-cache, mountPath: /mnt/models }
env:
- { name: MLFLOW_TRACKING_URI, value: "http://mlflow-tracking-server.aml-system.svc.cluster.local:5000" }
- { name: MLFLOW_EXPERIMENT_NAME, value: kubeflow-admin-cpaas-io }
command: [ bash, -c ]
args:
- |
set -ex
echo "job workers list: ${VC_WORKER_HOSTS}"
# vendor-specific launch command goes here
resources:
requests: { cpu: "1", memory: "8Gi" }
limits:
cpu: "8"
memory: "16Gi"
nvidia.com/gpualloc: "1"
nvidia.com/gpucores: "50"
nvidia.com/gpumem: "8192"
其他设备上的实验跟踪
LLaMA-Factory 和 Transformers 可直接与 MLflow / wandb 集成。在框架配置中设置目标位置(例如 LLaMA-Factory 使用 report_to: mlflow),并提供 MLFLOW_TRACKING_URI 和 MLFLOW_EXPERIMENT_NAME 环境变量(在启用了安全机制的安装中还需提供 MLFLOW_TRACKING_TOKEN——参见 Using the MLflow Python SDK with Authentication and RBAC)。在 Alauda AI → Tools → MLFlow 下查看结果。