使用 Workbench 微调 LLM

本指南介绍如何使用从 Alauda AI Workbench 启动的 LLaMA-Factory 微调 LLM(示例:Qwen3-0.6B)。Notebook 会向集群提交一个 VolcanoJob,因此 GPU 任务在集群节点上运行,而你可以继续在 JupyterLab 中迭代。

当你需要交互式控制、自定义训练脚本以及针对每次实验调整 YAML 时,可以使用此方法。对于可复用的模板和配额,建议改用 Kubeflow Trainer v2。

适用范围

  • Alauda AI 1.3 及更高版本。
  • 在 x86_64 + NVIDIA GPU 上微调 LLM。其他模型系列(例如 YOLOv5)需要使用各自的镜像、脚本和数据集格式。
  • NPU 集群需要与供应商软件栈兼容的运行时镜像 — 请参阅下方的在非 NVIDIA GPU 上运行或 Ascend NPU recipes。

前提条件

  • 已安装 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(或任何 Hugging Face 模型)下载到共享 PVC 或训练作业可访问的 S3 兼容对象存储中。如果希望通过控制台部署或复用基础模型,请在 Model Registry 中注册生成的 Model URI。请参阅使用 Workbench 上传模型和 Model Registry。

3. 准备输出模型位置

使用共享 PVC 目录保存微调输出,或为作业配置一个 S3 位置以写入输出。作业完成后,使用其 Model URI 将输出 artifact 注册为新的 Model Registry 版本。无需预先创建占位模型记录。

4. 准备数据集

使用示例身份数据集训练模型回答“你是谁?”。将解压后的文件上传到训练作业可访问的 PVC 或 S3 位置,并将 DATASET_URI 设置为该位置。数据集布局必须与微调框架一致。

数据集格式必须符合微调框架的要求。

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,或自行构建镜像。镜像必须包含读取所选模型和数据集存储所需的工具。不需要包含大型文件模型仓库客户端。

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 \
      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. 提交微调 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_URI, value: 's3://models/qwen3-0.6b' }
                - {
                    name: DATASET_URI,
                    value: 'pvc://dataset-pvc/identity-alauda',
                  }
              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
                  # Mount or download the base model and dataset using the
                  # storage mechanism configured for the job. The example
                  # assumes the PVC mount already contains these directories.
                  cd /mnt/models
                  BASE_MODEL_NAME=qwen3-0.6b
                  DATASET_NAME=identity-alauda
                  test -d "${BASE_MODEL_NAME}" || { echo "Mount or stage ${BASE_MODEL_URI} at /mnt/models/${BASE_MODEL_NAME}"; exit 1; }
                  test -d "${DATASET_NAME}" || { echo "Mount or stage ${DATASET_URI} at /mnt/models/${DATASET_NAME}"; exit 1; }
          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_URI, value: 's3://models/qwen3-0.6b' }
                - {
                    name: DATASET_URI,
                    value: 'pvc://dataset-pvc/identity-alauda',
                  }
                - { name: OUTPUT_MODEL_URI, value: 's3://models/wy-sft-output' }
                - { name: HF_HOME, value: /mnt/workspace/hf_cache }
                - { name: DO_MERGE, value: 'true' }
                - {
                    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=qwen3-0.6b
                  DATASET_NAME=identity-alauda
                  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
                  # The merged artifact is now available at
                  # /mnt/workspace/output_models_merged. Upload it to the
                  # configured S3/PVC location with the organization's storage
                  # tooling, then register OUTPUT_MODEL_URI as a new Model
                  # Registry version. The training job does not use a
                  # source-code repository for model storage.
              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_URI、DATASET_URI 和 OUTPUT_MODEL_URI 修改为环境所使用的受支持存储 URI。将基础模型和数据集挂载或暂存到示例所用的路径,并在作业完成后将输出目录上传到 OUTPUT_MODEL_URI。
  • 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:

  • 每个可能挂载 PVC 的节点都需要 nfs-utils(yum 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>

成功后,将合并后的目录上传到 OUTPUT_MODEL_URI,并将其注册为新的 Model Registry 版本。发布时选择该活动版本。

8. 实验跟踪

在 LLaMA-Factory 配置中设置 report_to: mlflow,并配置 MLFLOW_TRACKING_URI / MLFLOW_EXPERIMENT_NAME 环境变量,即可将指标路由到 MLflow。在 Alauda AI → Tools → MLFlow 中查找运行记录、比较损失曲线,并固定表现最佳的运行记录。

在安全(SSO + 多租户)的 MLflow 安装中,作业还必须进行身份验证 — 提供一个 MLFLOW_TRACKING_TOKEN 并选择一个 workspace。请参阅使用带身份验证和 RBAC 的 MLflow Python SDK,了解如何获取令牌并配置客户端。

9. 发布微调后的模型

示例使用 LoRA,并在上传输出 artifact 前将 adapter 合并到基础模型中。当前尚不支持由基础模型 + adapter 组合构成的推理服务。

  1. 打开 AI Hub > Model Registry,将微调后的输出 URI 注册为新的模型版本,并在其元数据中设置 Task Type 和 Framework(例如 text-generation 和 transformers)。
  2. 打开活动版本,然后选择 Create Inference API。
  3. 选择与集群 CUDA 匹配的 vLLM 运行时,填写存储/资源和 GPU 设置,然后点击 Publish。
  4. 运行后,点击 Experience 与模型聊天(仅当模型包含 chat_template 时可用)。

在非 NVIDIA GPU 上运行

适用于 Huawei Ascend NPU、Intel Gaudi、AMD 等设备。关于使用 PyTorch CANN + MindSpeed-LLM 的 Ascend NPU 配方,请参阅在 Ascend NPU 上微调和预训练 LLM。

一般步骤:

  1. 前提条件:已部署供应商驱动程序和 Kubernetes device plugin,并且 Pod 可以看到设备。记录资源名称(例如 huawei.com/Ascend910: "1")。
  2. 收集供应商解决方案 — 文档、微调镜像、支持的模型、示例数据以及启动命令/参数。
  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_URI 和 MLFLOW_EXPERIMENT_NAME 环境变量(在安全安装中还需提供 MLFLOW_TRACKING_TOKEN — 请参阅使用带身份验证和 RBAC 的 MLflow Python SDK)。在 Alauda AI → Tools → MLFlow 下查看结果。