使用 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_URLDATASET_URLOUTPUT_MODEL_URL 改为你的仓库 Git URL。
  • models-cache PVC —— 需要提前创建。可在不同实验之间复用它,以避免重复下载基础模型。
  • 共享内存 dshm —— 多 GPU 场景下至少 4 GiB。
  • CPU / memory / GPU 的 requestslimits —— 需要与集群的 device-plugin 匹配(例如 nvidia.com/gpunvidia.com/gpualloc)。
  • 超参数 —— LLaMA-Factory 的 YAML 直接内联在脚本中。建议将常调参数提取为环境变量。

NFS workspace PVC 说明

如果 PVC 后端是 NFS:

  • 可能挂载该 PVC 的每个节点都需要安装 nfs-utilsyum install -y nfs-utils)。

  • 在 StorageClass 上设置 mountPermissions: "0757"

    apiVersion: storage.k8s.io/v1
    kind: StorageClass
    metadata:
      name: ai-nfs
    provisioner: nfs.csi.k8s.io
    parameters:
      mountPermissions: "0757"
      server: <nfs-server-address>
      share: <nfs-export-path>
    reclaimPolicy: Delete
    volumeBindingMode: Immediate
    mountOptions: [hard, nfsvers=4.1]

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 组合的推理服务。

  1. Model Repository → Fine-tuned 输出模型 → Model Info → File Management → Edit Metadata 中,将 Task Type 设置为 文本分类,Framework 设置为 Transformers。
  2. 选择 Publish Inference API → Custom Publishing
  3. 选择与集群 CUDA 版本匹配的 vLLM runtime,填写 storage / resource / GPU 设置,然后点击 Publish
  4. 启动后,点击 Experience 即可与模型对话(仅当模型包含 chat_template 时)。

在非 NVIDIA GPUs 上运行

适用于 Huawei Ascend NPU、Intel Gaudi、AMD 等。关于使用 PyTorch CANN + MindSpeed-LLM 的 Ascend NPU 配方,请参见 在 Ascend NPU 上 Fine-tune 和 Pretrain LLMs

通用步骤如下:

  1. 前提条件:已部署厂商驱动和 Kubernetes device plugin,并且设备对 pod 可见。请记录资源名(例如 huawei.com/Ascend910: "1")。
  2. 收集厂商方案——文档、Fine-tuning 镜像、受支持模型、示例数据,以及启动命令 / 参数。
  3. (可选)先端到端验证厂商方案,以排除方案本身的问题。
  4. (可选)先将其封装为基本 Kubernetes Job,在添加 Volcano 之前确认 device plugin 在 K8s 下可正常工作。
  5. 使用 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_URIMLFLOW_EXPERIMENT_NAME 环境变量(在启用了安全机制的安装中还需提供 MLFLOW_TRACKING_TOKEN——参见 Using the MLflow Python SDK with Authentication and RBAC)。在 Alauda AI → Tools → MLFlow 下查看结果。