4 Commits

Author SHA1 Message Date
xvxuopop
ce9dc9e072 [docker] add Ascend 950PR&950DT images (#10851) 2026-09-28 17:10:59 +08:00
xvxuopop
c1dc0f51ab [model] add GLM-5.3-Flash training support (#10840) 2026-09-28 16:30:40 +08:00
kahlun
4d6c7cf03b Intel docker file (#10753) 2026-09-28 11:51:04 +08:00
shubham singhal
d4823eb10e [xpu] add Intel XPU support to test infrastructure and runs_on markers (#10835)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-28 11:47:33 +08:00
36 changed files with 900 additions and 130 deletions

View File

@@ -27,6 +27,12 @@ jobs:
- device: "npu-a3" - device: "npu-a3"
os: "openeuler" os: "openeuler"
base_image: "quay.io/ascend/cann:9.1.0-a3-openeuler24.03-py3.12" base_image: "quay.io/ascend/cann:9.1.0-a3-openeuler24.03-py3.12"
- device: "npu-a5"
os: "ubuntu"
base_image: "quay.io/ascend/cann:9.1.0-950-ubuntu22.04-py3.12"
- device: "npu-a5"
os: "openeuler"
base_image: "quay.io/ascend/cann:9.1.0-950-openeuler24.03-py3.12"
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -68,7 +74,7 @@ jobs:
base_image_tag="${BASE_IMAGE##*:}" base_image_tag="${BASE_IMAGE##*:}"
cann_version="${base_image_tag%%-*}" cann_version="${base_image_tag%%-*}"
torch_npu_version="$(sed -nE 's/^torch[-_]npu==([0-9]+(\.[0-9]+)*(\.post[0-9]+)?).*/\1/p' requirements/npu.txt)" torch_npu_version="$(sed -nE 's/^torch[-_]npu==([0-9]+(\.[0-9]+)*(\.post[0-9]+)?).*/\1/p' requirements/npu.txt)"
soc="$(grep -oE '(910b|a3)' <<< "${base_image_tag}" | head -n 1)" soc="$(grep -oE '(910b|a3|950)' <<< "${base_image_tag}" | head -n 1)"
operating_system="$(grep -oE '(ubuntu|openeuler)[0-9]+(\.[0-9]+)*' <<< "${base_image_tag}" | head -n 1)" operating_system="$(grep -oE '(ubuntu|openeuler)[0-9]+(\.[0-9]+)*' <<< "${base_image_tag}" | head -n 1)"
python_version="$(grep -oE 'py[0-9]+\.[0-9]+' <<< "${base_image_tag}" | head -n 1)" python_version="$(grep -oE 'py[0-9]+\.[0-9]+' <<< "${base_image_tag}" | head -n 1)"
@@ -85,6 +91,7 @@ jobs:
case "${MATRIX_DEVICE}" in case "${MATRIX_DEVICE}" in
npu-a2) expected_soc="910b" ;; npu-a2) expected_soc="910b" ;;
npu-a3) expected_soc="a3" ;; npu-a3) expected_soc="a3" ;;
npu-a5) expected_soc="950" ;;
*) *)
echo "Unsupported NPU device ${MATRIX_DEVICE}" >&2 echo "Unsupported NPU device ${MATRIX_DEVICE}" >&2
exit 1 exit 1

View File

@@ -613,14 +613,18 @@ You can also download the pre-built Docker images:
# Docker Hub # Docker Hub
docker pull hiyouga/llamafactory:latest-910b-ubuntu docker pull hiyouga/llamafactory:latest-910b-ubuntu
docker pull hiyouga/llamafactory:latest-a3-ubuntu docker pull hiyouga/llamafactory:latest-a3-ubuntu
docker pull hiyouga/llamafactory:latest-950-ubuntu
docker pull hiyouga/llamafactory:latest-910b-openeuler docker pull hiyouga/llamafactory:latest-910b-openeuler
docker pull hiyouga/llamafactory:latest-a3-openeuler docker pull hiyouga/llamafactory:latest-a3-openeuler
docker pull hiyouga/llamafactory:latest-950-openeuler
# quay.io # quay.io
docker pull quay.io/ascend/llamafactory:latest-910b-ubuntu docker pull quay.io/ascend/llamafactory:latest-910b-ubuntu
docker pull quay.io/ascend/llamafactory:latest-a3-ubuntu docker pull quay.io/ascend/llamafactory:latest-a3-ubuntu
docker pull quay.io/ascend/llamafactory:latest-950-ubuntu
docker pull quay.io/ascend/llamafactory:latest-910b-openeuler docker pull quay.io/ascend/llamafactory:latest-910b-openeuler
docker pull quay.io/ascend/llamafactory:latest-a3-openeuler docker pull quay.io/ascend/llamafactory:latest-a3-openeuler
docker pull quay.io/ascend/llamafactory:latest-950-openeuler
``` ```
#### Install BitsAndBytes #### Install BitsAndBytes

View File

@@ -613,14 +613,18 @@ pip install https://github.com/jllllll/bitsandbytes-windows-webui/releases/downl
# Docker Hub # Docker Hub
docker pull hiyouga/llamafactory:latest-910b-ubuntu docker pull hiyouga/llamafactory:latest-910b-ubuntu
docker pull hiyouga/llamafactory:latest-a3-ubuntu docker pull hiyouga/llamafactory:latest-a3-ubuntu
docker pull hiyouga/llamafactory:latest-950-ubuntu
docker pull hiyouga/llamafactory:latest-910b-openeuler docker pull hiyouga/llamafactory:latest-910b-openeuler
docker pull hiyouga/llamafactory:latest-a3-openeuler docker pull hiyouga/llamafactory:latest-a3-openeuler
docker pull hiyouga/llamafactory:latest-950-openeuler
# quay.io # quay.io
docker pull quay.io/ascend/llamafactory:latest-910b-ubuntu docker pull quay.io/ascend/llamafactory:latest-910b-ubuntu
docker pull quay.io/ascend/llamafactory:latest-a3-ubuntu docker pull quay.io/ascend/llamafactory:latest-a3-ubuntu
docker pull quay.io/ascend/llamafactory:latest-950-ubuntu
docker pull quay.io/ascend/llamafactory:latest-910b-openeuler docker pull quay.io/ascend/llamafactory:latest-910b-openeuler
docker pull quay.io/ascend/llamafactory:latest-a3-openeuler docker pull quay.io/ascend/llamafactory:latest-a3-openeuler
docker pull quay.io/ascend/llamafactory:latest-950-openeuler
``` ```
#### 安装 BitsAndBytes #### 安装 BitsAndBytes

View File

@@ -14,12 +14,14 @@ For installation and troubleshooting details, see the [English NPU installation
The following `latest` NPU image tags are available: The following `latest` NPU image tags are available:
| Hardware series | Operating system | Tag | | Product series | Operating system | Tag |
| --- | --- | --- | | --- | --- | --- |
| A2 | Ubuntu 22.04 | `latest-910b-ubuntu` | | Atlas A2 products | Ubuntu 22.04 | `latest-910b-ubuntu` |
| A3 | Ubuntu 22.04 | `latest-a3-ubuntu` | | Atlas A3 products | Ubuntu 22.04 | `latest-a3-ubuntu` |
| A2 | openEuler 24.03 | `latest-910b-openeuler` | | Atlas A2 products | openEuler 24.03 | `latest-910b-openeuler` |
| A3 | openEuler 24.03 | `latest-a3-openeuler` | | Atlas A3 products | openEuler 24.03 | `latest-a3-openeuler` |
| Ascend 950PR&950DT products | Ubuntu 22.04 | `latest-950-ubuntu` |
| Ascend 950PR&950DT products | openEuler 24.03 | `latest-950-openeuler` |
For historical NPU image tags and links to their corresponding archived Dockerfiles, see [Supported Tags](./supported_tags.md). For historical NPU image tags and links to their corresponding archived Dockerfiles, see [Supported Tags](./supported_tags.md).
@@ -53,7 +55,7 @@ latest-<chip>-<os>
| Field | Values | Description | | Field | Values | Description |
| --- | --- | --- | | --- | --- | --- |
| `chip` | `910b` or `a3` | Ascend chip model supported by the image | | `chip` | `910b`, `a3`, or `950` | Ascend chip model supported by the image |
| `os` | `ubuntu` or `openeuler` | Container operating system family | | `os` | `ubuntu` or `openeuler` | Container operating system family |
Future NPU release images will use the full tag format below. Historical release images used earlier naming formats and do not follow this rule. Future NPU release images will use the full tag format below. Historical release images used earlier naming formats and do not follow this rule.
@@ -67,7 +69,7 @@ Future NPU release images will use the full tag format below. Historical release
| `LlamaFactory-version` | `0.9.5` | LlamaFactory release version | | `LlamaFactory-version` | `0.9.5` | LlamaFactory release version |
| `CANN-version` | `9.1.0` | Parsed from the CANN base image tag | | `CANN-version` | `9.1.0` | Parsed from the CANN base image tag |
| `TorchNPU-version` | `2.10.0.post2` | Full TorchNPU version used by the image, including suffixes such as `.postN` | | `TorchNPU-version` | `2.10.0.post2` | Full TorchNPU version used by the image, including suffixes such as `.postN` |
| `chip` | `910b` or `a3` | Ascend chip model supported by the image | | `chip` | `910b`, `a3`, or `950` | Ascend chip model supported by the image |
| `os` | `ubuntu22.04` or `openeuler24.03` | Container operating system family and version | | `os` | `ubuntu22.04` or `openeuler24.03` | Container operating system family and version |
| `Python-version` | `py3.12` | Parsed from the CANN base image tag | | `Python-version` | `py3.12` | Parsed from the CANN base image tag |
@@ -148,7 +150,7 @@ Available build arguments:
### Start with Docker Compose ### Start with Docker Compose
The preceding `docker build` command invokes the Dockerfile directly. It builds an image but does not start a container. Docker Compose does not use a separate build implementation: it reads the presets in `docker-compose.yml`, reuses the same Dockerfile, and selects a hardware-series and operating-system combination through a profile. Each `up -d` command below starts the selected container in the background. If the image is not available locally, Docker Compose builds it first: The preceding `docker build` command invokes the Dockerfile directly. It builds an image but does not start a container. Docker Compose does not use a separate build implementation: it reads the presets in `docker-compose.yml`, reuses the same Dockerfile, and selects a product-series and operating-system combination through a profile. Each `up -d` command below starts the selected container in the background. If the image is not available locally, Docker Compose builds it first:
```bash ```bash
cd docker/docker-npu cd docker/docker-npu
@@ -164,16 +166,22 @@ docker compose --profile a2-openeuler up -d
# A3 with openEuler # A3 with openEuler
docker compose --profile a3-openeuler up -d docker compose --profile a3-openeuler up -d
# 950PR&950DT with Ubuntu
docker compose --profile 950-ubuntu up -d
# 950PR&950DT with openEuler
docker compose --profile 950-openeuler up -d
``` ```
To build an image with Docker Compose without starting a container, use `docker compose --profile <profile> build`. To build an image with Docker Compose without starting a container, use `docker compose --profile <profile> build`.
## Hardware Support and Compatibility Notes ## Hardware Support and Compatibility Notes
- A2 images use the `910b` CANN base image; A3 images use the `a3` CANN base image. - A2 images use the `910b` CANN base image; A3 images use the `a3` CANN base image; 950PR&950DT images use the `950` CANN base image.
- The image build targets both x86-64 (`linux/amd64`) and AArch64 (`linux/arm64`) hosts. The CPU architecture is independent of whether the hardware series is A2 or A3. - The image build targets both x86-64 (`linux/amd64`) and AArch64 (`linux/arm64`) hosts. The CPU architecture is independent of whether the product series is A2, A3, or 950PR&950DT.
- Ubuntu 22.04 and openEuler 24.03 refer to the operating system inside the container. - Ubuntu 22.04 and openEuler 24.03 refer to the operating system inside the container.
- Legacy NPU tags are replaced by the `latest-<910b|a3>-<ubuntu|openeuler>` format. - Legacy NPU tags are replaced by the `latest-<910b|a3|950>-<ubuntu|openeuler>` format.
- Validate the exact driver, firmware, CANN, and SoC combination before production deployment. - Validate the exact driver, firmware, CANN, and SoC combination before production deployment.
## License and Disclaimer ## License and Disclaimer

View File

@@ -14,12 +14,14 @@ LlamaFactory 昇腾 NPU 镜像面向华为昇腾 Atlas NPU,提供可直接使
当前提供以下 `latest` NPU 镜像 tag: 当前提供以下 `latest` NPU 镜像 tag:
| 硬件系列 | 操作系统 | Tag | | 产品系列 | 操作系统 | Tag |
| --- | --- | --- | | --- | --- | --- |
| A2 | Ubuntu 22.04 | `latest-910b-ubuntu` | | Atlas A2 系列产品 | Ubuntu 22.04 | `latest-910b-ubuntu` |
| A3 | Ubuntu 22.04 | `latest-a3-ubuntu` | | Atlas A3 系列产品 | Ubuntu 22.04 | `latest-a3-ubuntu` |
| A2 | openEuler 24.03 | `latest-910b-openeuler` | | Atlas A2 系列产品 | openEuler 24.03 | `latest-910b-openeuler` |
| A3 | openEuler 24.03 | `latest-a3-openeuler` | | Atlas A3 系列产品 | openEuler 24.03 | `latest-a3-openeuler` |
| Ascend 950PR&950DT 系列产品 | Ubuntu 22.04 | `latest-950-ubuntu` |
| Ascend 950PR&950DT 系列产品 | openEuler 24.03 | `latest-950-openeuler` |
历史版本的 NPU 镜像 Tag 及其对应的 Dockerfile 归档链接,请参阅 [Supported Tags](./supported_tags.md)。 历史版本的 NPU 镜像 Tag 及其对应的 Dockerfile 归档链接,请参阅 [Supported Tags](./supported_tags.md)。
@@ -53,7 +55,7 @@ latest-<芯片信息>-<操作系统>
| 字段 | 可选值 | 说明 | | 字段 | 可选值 | 说明 |
| --- | --- | --- | | --- | --- | --- |
| `芯片信息` | `910b` 或 `a3` | 镜像所适配的昇腾芯片型号 | | `芯片信息` | `910b`、`a3` 或 `950` | 镜像所适配的昇腾芯片型号 |
| `操作系统` | `ubuntu` 或 `openeuler` | 容器操作系统类型 | | `操作系统` | `ubuntu` 或 `openeuler` | 容器操作系统类型 |
后续发布的 NPU release 镜像将使用以下完整 tag 格式。历史 release 镜像使用旧的命名格式,不适用以下规则。 后续发布的 NPU release 镜像将使用以下完整 tag 格式。历史 release 镜像使用旧的命名格式,不适用以下规则。
@@ -67,7 +69,7 @@ latest-<芯片信息>-<操作系统>
| `LlamaFactory版本` | `0.9.5` | LlamaFactory release 版本号 | | `LlamaFactory版本` | `0.9.5` | LlamaFactory release 版本号 |
| `CANN版本` | `9.1.0` | 从 CANN 基础镜像 tag 中提取 | | `CANN版本` | `9.1.0` | 从 CANN 基础镜像 tag 中提取 |
| `TorchNPU版本` | `2.10.0.post2` | 镜像使用的 TorchNPU 完整版本,包含 `.postN` 等后缀 | | `TorchNPU版本` | `2.10.0.post2` | 镜像使用的 TorchNPU 完整版本,包含 `.postN` 等后缀 |
| `芯片信息` | `910b` 或 `a3` | 镜像所适配的昇腾芯片型号 | | `芯片信息` | `910b`、`a3` 或 `950` | 镜像所适配的昇腾芯片型号 |
| `操作系统` | `ubuntu22.04` 或 `openeuler24.03` | 容器操作系统类型和版本 | | `操作系统` | `ubuntu22.04` 或 `openeuler24.03` | 容器操作系统类型和版本 |
| `Python版本` | `py3.12` | 从 CANN 基础镜像 tag 中提取 | | `Python版本` | `py3.12` | 从 CANN 基础镜像 tag 中提取 |
@@ -148,7 +150,7 @@ docker build \
### 通过 Docker Compose 启动 ### 通过 Docker Compose 启动
前面的 `docker build` 命令直接调用 Dockerfile,只构建镜像,不启动容器。Docker Compose 不使用另一套构建逻辑:它读取 `docker-compose.yml` 中的预设配置,复用同一个 Dockerfile,并通过 profile 选择硬件系列和操作系统组合。下面的 `up -d` 会在后台启动容器;若本地镜像不存在,Docker Compose 会先构建镜像: 前面的 `docker build` 命令直接调用 Dockerfile,只构建镜像,不启动容器。Docker Compose 不使用另一套构建逻辑:它读取 `docker-compose.yml` 中的预设配置,复用同一个 Dockerfile,并通过 profile 选择产品系列和操作系统组合。下面的 `up -d` 会在后台启动容器;若本地镜像不存在,Docker Compose 会先构建镜像:
```bash ```bash
cd docker/docker-npu cd docker/docker-npu
@@ -164,16 +166,22 @@ docker compose --profile a2-openeuler up -d
# A3 + openEuler # A3 + openEuler
docker compose --profile a3-openeuler up -d docker compose --profile a3-openeuler up -d
# 950PR&950DT + Ubuntu
docker compose --profile 950-ubuntu up -d
# 950PR&950DT + openEuler
docker compose --profile 950-openeuler up -d
``` ```
如果只想通过 Docker Compose 构建镜像而不启动容器,请使用 `docker compose --profile <profile> build`。 如果只想通过 Docker Compose 构建镜像而不启动容器,请使用 `docker compose --profile <profile> build`。
## 硬件支持与兼容性说明 ## 硬件支持与兼容性说明
- A2 镜像使用标记为 `910b` 的 CANN 基础镜像,A3 镜像使用标记为 `a3` 的 CANN 基础镜像。 - A2 镜像使用标记为 `910b` 的 CANN 基础镜像,A3 镜像使用标记为 `a3` 的 CANN 基础镜像,950PR&950DT 镜像使用标记为 `950` 的 CANN 基础镜像。
- 镜像构建目标同时包含 x86-64(`linux/amd64`)和 AArch64(`linux/arm64`)宿主机。CPU 架构与硬件系列是 A2 还是 A3 无关。 - 镜像构建目标同时包含 x86-64(`linux/amd64`)和 AArch64(`linux/arm64`)宿主机。CPU 架构与产品系列是 A2、A3 还是 950PR&950DT 无关。
- Ubuntu 22.04 和 openEuler 24.03 指容器内部的操作系统。 - Ubuntu 22.04 和 openEuler 24.03 指容器内部的操作系统。
- 旧式 NPU tag 已由 `latest-<910b|a3>-<ubuntu|openeuler>` 格式取代。 - 旧式 NPU tag 已由 `latest-<910b|a3|950>-<ubuntu|openeuler>` 格式取代。
- 正式部署前,请验证具体驱动、固件、CANN 和 SoC 组合的兼容性。 - 正式部署前,请验证具体驱动、固件、CANN 和 SoC 组合的兼容性。
## 许可证与免责声明 ## 许可证与免责声明

View File

@@ -23,6 +23,13 @@ x-npu-common: &npu-common
- /dev/hisi_hdc - /dev/hisi_hdc
restart: unless-stopped restart: unless-stopped
x-npu-950: &npu-950
<<: *npu-common
devices:
- /dev/davinci0
- /dev/davinci_manager
- /dev/hisi_hdc
services: services:
llamafactory-a2-ubuntu: llamafactory-a2-ubuntu:
<<: *npu-common <<: *npu-common
@@ -79,3 +86,31 @@ services:
ports: ports:
- "7863:7860" - "7863:7860"
- "8003:8000" - "8003:8000"
llamafactory-950-ubuntu:
<<: *npu-950
profiles: ["950-ubuntu"]
build:
<<: *build
args:
<<: *build-args
BASE_IMAGE: quay.io/ascend/cann:9.1.0-950-ubuntu22.04-py3.12
container_name: llamafactory-950-ubuntu
image: llamafactory:npu-950-ubuntu
ports:
- "7864:7860"
- "8004:8000"
llamafactory-950-openeuler:
<<: *npu-950
profiles: ["950-openeuler"]
build:
<<: *build
args:
<<: *build-args
BASE_IMAGE: quay.io/ascend/cann:9.1.0-950-openeuler24.03-py3.12
container_name: llamafactory-950-openeuler
image: llamafactory:npu-950-openeuler
ports:
- "7865:7860"
- "8005:8000"

View File

@@ -0,0 +1,88 @@
# https://hub.docker.com/r/intel/deep-learning-essentials/tags
# Unlike the CUDA/ROCm bases this image ships no PyTorch, and its version must match the
# host Intel GPU driver — see README.md.
ARG BASE_IMAGE=intel/deep-learning-essentials:2026.1.0-devel-ubuntu24.04
FROM ${BASE_IMAGE}
# Installation arguments
ARG PIP_INDEX=https://pypi.org/simple
ARG PYTORCH_INDEX=https://download.pytorch.org/whl/xpu
# Must stay in the same compute-runtime series as the base image — see README.md ("ocloc").
ARG OCLOC_VERSION=26.18.38308.1
# Define environments
ENV DEBIAN_FRONTEND=noninteractive
ENV PIP_ROOT_USER_ACTION=ignore
# The base image's Python is distro-managed (PEP 668); nothing to protect in a container.
ENV PIP_BREAK_SYSTEM_PACKAGES=1
# expandable_segments trips a Level-Zero bug on XPU — see README.md ("Multi-GPU and OPTIM_TORCH").
ENV OPTIM_TORCH=0
# Use Bash instead of default /bin/sh
SHELL ["/bin/bash", "-c"]
# Set the working directory
WORKDIR /app
# Install pip, Python dev headers and a C/C++ toolchain — see README.md ("Python.h: No such file or directory").
RUN PY_MM="$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')" && \
apt-get update -y && \
apt-get install -y --no-install-recommends \
"python${PY_MM}-dev" python3-pip build-essential && \
apt-get clean && rm -rf /var/lib/apt/lists/*
# Install ocloc — pinned .deb rather than apt, see README.md ("ocloc and torch.compile").
RUN wget -q "https://github.com/intel/compute-runtime/releases/download/${OCLOC_VERSION}/intel-ocloc_${OCLOC_VERSION}-0_amd64.deb" \
-O /tmp/intel-ocloc.deb && \
dpkg -i /tmp/intel-ocloc.deb && \
rm -f /tmp/intel-ocloc.deb
# Change pip source — pip is deliberately not upgraded, see README.md ("How pip is installed").
RUN pip config set global.index-url "${PIP_INDEX}" && \
pip config set global.extra-index-url "${PIP_INDEX}" && \
pip install --no-cache-dir packaging wheel setuptools editables "hatchling>=1.18.0"
# Copy the application into the image
COPY . /app
# Install PyTorch for XPU — all three wheels pinned as a set, see README.md ("Pinned torch stack")
RUN pip install --no-cache-dir -r requirements/xpu.txt --index-url "${PYTORCH_INDEX}"
# Install LLaMA Factory
# metrics.txt is explicit because this project defines no extras, so `.[metrics]` would no-op.
RUN pip install --no-cache-dir -e . --no-build-isolation && \
pip install --no-cache-dir -r requirements/metrics.txt
# Optional accelerators — see README.md ("Optional accelerators").
# py-cpuinfo must land first: deepspeed's own setup.py imports deepspeed/ops/adam/cpu_adam.py
# during metadata generation (before pip installs deepspeed's declared deps), which needs it.
# deepspeed is pinned separately from requirements/deepspeed.txt's <=0.18.4: that range's XPU
# accelerator imports IPEX's DpcppBuildExtension unconditionally, which no longer exists on a
# post-IPEX torch. Fixed upstream in 0.18.7 (plain torch.utils.cpp_extension.BuildExtension).
RUN pip install --no-cache-dir py-cpuinfo && \
(DS_BUILD_OPS=0 pip install --no-cache-dir "deepspeed==0.19.6" \
|| echo "WARNING: deepspeed install failed - deepspeed training unavailable in this image")
RUN pip install --no-cache-dir -r requirements/bitsandbytes.txt \
|| echo "WARNING: bitsandbytes install failed - 4-bit quantization (QLoRA) unavailable in this image"
# Source the oneAPI environment in every interactive shell
RUN echo "source /opt/intel/oneapi/setvars.sh --force" >> /root/.bashrc
# Set up volumes
# VOLUME [ "/root/.cache/huggingface", "/app/shared_data", "/app/output" ]
# Expose port 7860 for LLaMA Board
ENV GRADIO_SERVER_PORT=7860
EXPOSE 7860
# Expose port 8000 for API service
ENV API_PORT=8000
EXPOSE 8000
# Reset pip config
RUN pip config unset global.index-url && \
pip config unset global.extra-index-url
# oneAPI must be on the loader path before torch can open its XPU backend
CMD ["bash", "-c", "source /opt/intel/oneapi/setvars.sh --force && exec llamafactory-cli webui"]

206
docker/docker-xpu/README.md Normal file
View File

@@ -0,0 +1,206 @@
# Docker Setup for Intel GPUs
This directory contains Docker configuration files for running LLaMA Factory with Intel GPU (XPU) support.
## Image Details
| Component | Version |
|---|---|
| Base OS | Ubuntu 24.04 LTS (x86_64) |
| Intel DLE base | [intel/deep-learning-essentials:2026.1.0-devel-ubuntu24.04](https://hub.docker.com/r/intel/deep-learning-essentials) |
| Python | 3.12 |
| PyTorch | 2.13.0+xpu |
| Intel GPU runtime | Bundled inside DLE (libze-intel-gpu 26.18.x, oneAPI 2026.1) |
The Intel compute runtime is bundled inside the DLE base image — no GPU compute packages are needed on the host, only the kernel driver. The bundled runtime must be compatible with the host kernel driver; verify with `dpkg -l libze-intel-gpu1 | grep -oP '\d+\.\d+\.\d+'`.
## Prerequisites
### 1. Docker & Docker Compose
```bash
# Ubuntu/Debian
sudo apt-get update && sudo apt-get install docker.io docker-compose-v2
```
See the [official Docker install docs](https://docs.docker.com/engine/install/) for other distros or newer versions.
### 2. Intel GPU Kernel Driver (host only)
```bash
# Add the Intel GPU PPA
sudo apt-get install -y gpg-agent wget
wget -qO - https://repositories.intel.com/gpu/intel-graphics.key | \
sudo gpg --dearmor -o /usr/share/keyrings/intel-graphics.gpg
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/intel-graphics.gpg] \
https://repositories.intel.com/gpu/ubuntu noble client" | \
sudo tee /etc/apt/sources.list.d/intel-graphics.list
sudo apt-get update
# Kernel driver only — no compute runtime packages needed on the host
sudo apt-get install -y intel-i915-dkms intel-fw-gpu
sudo reboot
```
After reboot, verify `/dev/dri` is populated: `ls /dev/dri/` (expect `card0`, `renderD128`, etc).
See the [Intel GPU Installation Guide](https://dgpu-docs.intel.com/installation-guides/installing-packages-from-the-intel-ppa.html) for details.
> [!IMPORTANT]
> Enable **Resizable BAR** in your system BIOS before proceeding, or you may see `Bus error (core dumped)` or degraded performance. See [Intel's guide](https://www.intel.com/content/www/us/en/support/articles/000090831/graphics.html).
### 3. Add your user to the GPU groups
```bash
sudo usermod -aG render,video $USER
# Log out and back in for group membership to take effect
```
(Optional) sanity-check the host side with `sudo apt-get install -y clinfo && clinfo --list | grep Device`. The check that actually matters — `torch.xpu.device_count()` — runs inside the container, in Usage below.
## Usage
### Using Docker Compose (Recommended)
```bash
cd docker/docker-xpu/
docker compose up -d
docker compose exec llamafactory bash
```
Verify GPU access inside the container:
```bash
python3 -c "import torch; print(torch.xpu.device_count(), 'XPU device(s) found')"
```
### Using Docker Run
```bash
# Build the image (from the repo root)
docker build -t llamafactory:xpu -f docker/docker-xpu/Dockerfile .
# Run the container
docker run -it --rm \
--device /dev/dri \
-v /dev/dri/by-path:/dev/dri/by-path \
--group-add $(getent group render | cut -d: -f3) \
--group-add $(getent group video | cut -d: -f3) \
--ipc=host \
-p 7860:7860 \
-p 8000:8000 \
-v ~/.cache/huggingface:/root/.cache/huggingface \
--name llamafactory \
llamafactory:xpu bash
```
## Build arguments
| Argument | Default | Purpose |
|---|---|---|
| `BASE_IMAGE` | `intel/deep-learning-essentials:2026.1.0-devel-ubuntu24.04` | oneAPI / GPU runtime version |
| `PIP_INDEX` | `https://pypi.org/simple` | PyPI mirror for everything except the torch wheels |
| `PYTORCH_INDEX` | `https://download.pytorch.org/whl/xpu` | where the `+xpu` torch wheels come from |
| `OCLOC_VERSION` | `26.18.38308.1` | pinned `intel-ocloc` build — must match `BASE_IMAGE`'s compute-runtime series |
```bash
docker build -t llamafactory:xpu -f docker/docker-xpu/Dockerfile . \
--build-arg PIP_INDEX=https://pypi.org/simple
```
## Design notes
Why the image is built the way it is — skip to [Troubleshooting](#troubleshooting) if you just want to run it.
### Pinned torch stack
`torch`, `torchvision` and `torchaudio` install together from `requirements/xpu.txt` as `+xpu` builds, via `--index-url https://download.pytorch.org/whl/xpu` (not `--extra-index-url`, which would let a plain PyPI build win). Pinning all three avoids `RuntimeError: operator torchvision::nms does not exist` from an ABI-mismatched pair.
No pip constraint file is needed: every other `torch` requirement in the dependency graph is a lower bound that `2.13.0+xpu` already satisfies, so nothing later swaps it out.
If the XPU wheels ever get replaced with plain ones, `torch.xpu.device_count()` returns `0`. Restore with:
```bash
pip install --force-reinstall -r requirements/xpu.txt \
--index-url https://download.pytorch.org/whl/xpu
```
### How pip is installed
The DLE base ships Python 3.12 with no pip and no `ensurepip` (Ubuntu strips it). pip comes from apt (`python3-pip`) and is **left at the distro version** rather than upgraded — apt's pip has no `RECORD` file, so `pip install --upgrade pip` fails with:
```
ERROR: Cannot uninstall pip 24.0, RECORD file not found. Hint: The package was installed by debian.
```
— taking `setuptools`/`wheel`/`hatchling` down with it in the same command.
### Multi-GPU and `OPTIM_TORCH`
The image sets `ENV OPTIM_TORCH=0`. The launcher's default (`OPTIM_TORCH=1`) sets `PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True`, whose allocator path trips a Level-Zero driver bug on 2-GPU XPU runs (DDP segfault, FSDP2 hang). Disabling it costs <1% throughput. Re-enable per-run with `OPTIM_TORCH=1 llamafactory-cli train ...` to test it. It is tracked in intel, will be update or remove this ENV when it is resolve with PyTorch or level zero etc fixes.
### Optional accelerators
`deepspeed` and `bitsandbytes` install on a best-effort basis — neither is required to train on XPU, so a build failure only prints a warning instead of failing the image:
```
WARNING: deepspeed install failed - deepspeed training unavailable in this image
WARNING: bitsandbytes install failed - 4-bit quantization (QLoRA) unavailable in this image
```
Both install and work on XPU today (verified: `torch.compile`, LoRA + DeepSpeed ZeRO-2 SFT, and bitsandbytes 4-bit all pass on real Arc Pro B60 hardware). `deepspeed` is pinned to `==0.19.6` here, separately from `requirements/deepspeed.txt`'s shared `<=0.18.4` cap: any version in that range detects XPU as the active accelerator and imports `torch.utils.cpp_extension.DpcppBuildExtension` — a class that used to be provided by `intel_extension_for_pytorch` (IPEX). IPEX is no longer part of this stack, so that import fails with `ImportError: cannot import name 'DpcppBuildExtension'`. deepspeed dropped the IPEX dependency and switched to the stock `BuildExtension` in `0.18.7`, so anything from there onward works; `0.19.6` is the latest release at time of writing.
Check the build log, or verify inside the container with `python3 -c "import deepspeed"` / `import bitsandbytes`. Install manually if missing:
```bash
DS_BUILD_OPS=0 pip install "deepspeed==0.19.6"
pip install -r requirements/bitsandbytes.txt
```
### `ocloc` and `torch.compile`
`torch.compile` lowers to Intel Triton, which shells out to `ocloc` (the Intel offline GPU compiler) — not bundled in the DLE base, so compiled paths fail with `FileNotFoundError: 'ocloc'` without it. The Dockerfile installs a single pinned `.deb` from Intel's GitHub releases (`ARG OCLOC_VERSION`) rather than `apt-get install intel-ocloc`, because the Intel graphics PPA keeps only its newest revision and would drag `libze-intel-gpu1` (the host-driver-facing package) up with it.
> [!IMPORTANT]
> If you override `ARG BASE_IMAGE`, move `OCLOC_VERSION` to match — check `docker run --rm <base-image> dpkg -l libze-intel-gpu1 | tail -1` and pick the nearest [compute-runtime release](https://github.com/intel/compute-runtime/releases).
## Troubleshooting
### GPU Not Detected (`torch.xpu.device_count()` returns 0)
1. **Kernel driver too old for the DLE runtime** — compare `dpkg -l libze-intel-gpu1` on the host vs. `docker run --rm llamafactory:xpu dpkg -l libze-intel-gpu1`. Update the host driver (`sudo apt-get install -y intel-i915-dkms intel-fw-gpu && sudo reboot`) or use an older `BASE_IMAGE`.
2. **Missing `/dev/dri` device** — pass `--device /dev/dri` (done automatically by `docker compose`).
3. **Missing group membership** — the container process needs the `render` and `video` groups; `docker compose` sets these via `group_add`. For manual `docker run`, pass `--group-add $(getent group render | cut -d: -f3) --group-add $(getent group video | cut -d: -f3)`.
4. **`by-path` mount missing** — required for multi-GPU Level-Zero IPC: `-v /dev/dri/by-path:/dev/dri/by-path` (included in `docker-compose.yml`).
### `fatal error: Python.h: No such file or directory`
Intel Triton JIT-compiles a C driver at the first XPU kernel launch, which needs matching `pythonX.Y-dev` headers and a C compiler present in the image — installed at build time even though the error would happen at runtime. If you've swapped the interpreter, reinstall matching headers:
```bash
PY_MM="$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')"
apt-get update && apt-get install -y "python${PY_MM}-dev" build-essential
```
### Permission Denied on `/dev/dri`
```bash
sudo usermod -aG render,video $USER
newgrp render # apply without logout
```
### `SYCL Backends mismatch` / `libsycl.so.N: cannot open shared object file`
Two SYCL runtimes exist in this image by design — torch's own `intel-sycl-rt` (pip) and the DLE base's oneAPI — and normally share the same `libsycl.so.9` SONAME, so both coexist fine. A mismatch shows up only after changing `torch` or `BASE_IMAGE` independently. Compare versions:
```bash
pip list | grep -E "intel-sycl-rt|dpcpp-cpp-rt"
ls /opt/intel/oneapi/*/lib/libsycl.so.*
```
The `libsycl.so.N` numbers on both sides must match. `ldconfig -p | grep libsycl` returning nothing is expected — neither runtime is in the ldconfig cache.
## Additional Notes
- The container automatically sources `/opt/intel/oneapi/setvars.sh` in every interactive shell (`~/.bashrc`). For non-interactive scripts, source it explicitly.
- For training, `llamafactory-cli train` dispatches automatically via `torchrun` for multi-GPU.

View File

@@ -0,0 +1,48 @@
services:
llamafactory:
build:
dockerfile: ./docker/docker-xpu/Dockerfile
context: ../..
args:
PIP_INDEX: https://pypi.org/simple
# The DLE base image determines which Intel GPU runtime (libze-intel-gpu)
# is shipped. The runtime version must be >= the host driver version,
# otherwise torch.xpu.device_count() returns 0:
# DLE 2025.3 → libze-intel-gpu 25.18.x (works on driver ≤26.09)
# DLE 2026.1 → libze-intel-gpu 26.18.x+ (works on driver 26.18/26.22)
# Verify host driver: dpkg -l libze-intel-gpu1 | grep -oP '\d+\.\d+\.\d+'
BASE_IMAGE: intel/deep-learning-essentials:2026.1.0-devel-ubuntu24.04
container_name: llamafactory
image: llamafactory:xpu
ports:
- "7860:7860"
- "8000:8000"
ipc: host
tty: true
# shm_size: "16gb" # ipc: host is set
stdin_open: true
command: bash
devices:
# Intel GPU character devices (renderD* and card* under /dev/dri).
# renderD* nodes are owned by group `render`; card* by group `video`.
- /dev/dri:/dev/dri
volumes:
# by-path symlinks are required for oneCCL's ze_fd_manager (2-GPU IPC).
# `devices:` alone does not carry sub-directories — this volume does.
- /dev/dri/by-path:/dev/dri/by-path
- ~/.cache/huggingface:/root/.cache/huggingface
group_add:
# Grant access to the Intel GPU device nodes.
# renderD* nodes are owned by group `render`; card* nodes by `video`.
# Use numeric GIDs here — Docker compose resolves group names against
# the CONTAINER's /etc/group (not the host), and the DLE base image
# does not carry a `render` group entry. Standard Linux GIDs:
# If your host uses different GIDs, run `getent group render video` and
# update these values accordingly.
# example output.
# getent group render video
# render:x:992:root
# video:x:44:support
- "992" # render — owns /dev/dri/renderD* nodes
- "44" # video — owns /dev/dri/card* nodes
restart: unless-stopped

3
requirements/xpu.txt Normal file
View File

@@ -0,0 +1,3 @@
torch==2.13.0+xpu
torchvision==0.28.0+xpu
torchaudio==2.11.0+xpu

View File

@@ -19,7 +19,7 @@ import inspect
import math import math
import os import os
import re import re
from copy import deepcopy from copy import copy, deepcopy
from dataclasses import dataclass from dataclasses import dataclass
from io import BytesIO from io import BytesIO
from types import SimpleNamespace from types import SimpleNamespace
@@ -56,6 +56,7 @@ if TYPE_CHECKING:
from transformers.feature_extraction_sequence_utils import SequenceFeatureExtractor from transformers.feature_extraction_sequence_utils import SequenceFeatureExtractor
from transformers.image_processing_utils import BaseImageProcessor from transformers.image_processing_utils import BaseImageProcessor
from transformers.video_processing_utils import BaseVideoProcessor from transformers.video_processing_utils import BaseVideoProcessor
from transformers.video_utils import VideoMetadata
class EncodedImage(TypedDict): class EncodedImage(TypedDict):
path: str | None path: str | None
@@ -2776,6 +2777,213 @@ class Qwen3VLPlugin(Qwen2VLPlugin):
return messages return messages
@dataclass
class Glm5NextPlugin(BasePlugin):
r"""GLM5-Next images and timestamped video frames using the native processors."""
@override
def _validate_input(
self,
processor: Optional["MMProcessor"],
images: list["ImageInput"],
videos: list["VideoInput"],
audios: list["AudioInput"],
) -> None:
if processor is None and not (images or videos or audios):
return # Preserve tokenizer-only text preprocessing.
super()._validate_input(processor, images, videos, audios)
def _decode_video(
self, video: "VideoInput", processor: "MMProcessor"
) -> tuple[list["ImageObject"], "VideoMetadata"]:
r"""Decode sampled frames and preserve their source timing for the native processor."""
from transformers.video_utils import VideoMetadata
fps = getattr(processor, "video_fps", 2.0)
maxlen = getattr(processor, "video_maxlen", 128)
temporal = processor.video_processor.temporal_patch_size
if not math.isfinite(fps) or fps <= 0 or maxlen < temporal:
raise ValueError("glm5_next requires video_fps > 0 and video_maxlen >= temporal_patch_size.")
# Native sampling can duplicate the last frame for temporal patching.
# Use a private sampler so frame limits never mutate the shared processor.
sampler = copy(processor.video_processor)
sampler.max_frames = min(sampler.max_frames, maxlen // temporal * temporal)
if _check_video_is_nested_images(video):
if not video:
raise ValueError("glm5_next received an empty video frame list.")
total = len(video)
indices = np.linspace(0, total - 1, min(total, sampler.max_frames), dtype=int).tolist()
frames = self._regularize_images(
[video[i] for i in indices], image_max_pixels=float("inf"), image_min_pixels=0
)["images"]
# Frame lists have no source timing; video_fps describes their spacing.
metadata = VideoMetadata(total_num_frames=total, fps=fps, frames_indices=indices)
else:
with av.open(video, "r") as container:
stream = next((s for s in container.streams if s.type == "video"), None)
if stream is None or not stream.average_rate or stream.average_rate <= 0:
raise ValueError("glm5_next requires a video stream with a valid source FPS.")
source_fps = float(stream.average_rate)
duration = float(stream.duration * stream.time_base) if stream.duration is not None else None
metadata = VideoMetadata(total_num_frames=stream.frames, fps=source_fps, duration=duration)
# Some containers omit frame counts. Count without retaining all
# decoded pixels, then seek back and retain only selected frames.
if not metadata.total_num_frames:
metadata.total_num_frames = sum(1 for _ in container.decode(stream))
container.seek(0)
if metadata.total_num_frames <= 0:
raise ValueError("glm5_next received a video with no decodable frames.")
indices = sampler.sample_frames(metadata, fps=fps).tolist()
if not indices: # Sub-second clips can round to zero in the native sampler.
indices = [0]
indices = indices[: sampler.max_frames]
selected = set(indices)
decoded = {}
for i, frame in enumerate(container.decode(stream)):
if i in selected:
decoded[i] = frame.to_image()
if i >= max(selected):
break
if selected - decoded.keys():
raise ValueError("glm5_next could not decode all sampled video frames.")
frames = [decoded[i] for i in indices]
metadata.frames_indices = indices
# Keep both pixels and timestamps aligned when repeating an odd last frame.
while len(frames) % temporal:
frames.append(frames[-1])
metadata.frames_indices.append(metadata.frames_indices[-1])
return frames, metadata
@override
def _get_mm_inputs(
self,
images: list["ImageInput"],
videos: list["VideoInput"],
audios: list["AudioInput"],
processor: "MMProcessor",
) -> dict[str, Any]:
self._validate_input(processor, images, videos, audios)
mm_inputs = {}
if images:
# Only decode/convert here: resizing twice changes the native pixels.
images = self._regularize_images(images, image_max_pixels=float("inf"), image_min_pixels=0)["images"]
image_processor = processor.image_processor
pixels_per_token = (image_processor.patch_size * image_processor.merge_size) ** 2
kwargs = {}
if hasattr(processor, "image_max_pixels"):
kwargs["max_image_tokens"] = max(1, processor.image_max_pixels // pixels_per_token)
if hasattr(processor, "image_min_pixels"):
kwargs["min_image_tokens"] = max(1, math.ceil(processor.image_min_pixels / pixels_per_token))
mm_inputs.update(image_processor(images=images, return_tensors="pt", **kwargs))
if videos:
video_processor = processor.video_processor
pixels_per_token = (video_processor.patch_size * video_processor.merge_size) ** 2
processed = []
for video in videos:
frames, metadata = self._decode_video(video, processor)
temporal_tokens = len(frames) // video_processor.temporal_patch_size
# LlamaFactory limits pixels per frame; native GLM5 budgets tokens
# across the whole video (after temporal merging).
kwargs = {
"min_image_tokens": max(
1, math.ceil(getattr(processor, "video_min_pixels", 256) / pixels_per_token)
)
* temporal_tokens,
"max_image_tokens": max(1, getattr(processor, "video_max_pixels", 65536) // pixels_per_token)
* temporal_tokens,
}
processed.append(
video_processor(
videos=[np.stack([np.asarray(frame) for frame in frames])],
video_metadata=[metadata],
do_sample_frames=False,
return_metadata=True,
return_tensors="pt",
**kwargs,
)
)
for key in ("pixel_values_videos", "video_grid_thw"):
mm_inputs[key] = torch.cat([item[key] for item in processed])
mm_inputs["video_metadata"] = [item["video_metadata"][0] for item in processed]
return mm_inputs
@override
def process_messages(
self,
messages: list[dict[str, str]],
images: list["ImageInput"],
videos: list["VideoInput"],
audios: list["AudioInput"],
processor: Optional["MMProcessor"],
) -> list[dict[str, str]]:
self._validate_input(processor, images, videos, audios)
self._validate_messages(messages, images, videos, audios)
messages = deepcopy(messages)
mm_inputs = self._get_mm_inputs(images, videos, audios, processor) if self.expand_mm_tokens else {}
image_idx, video_idx = 0, 0
for message in messages:
if message["role"] != "user" and any(
p in message["content"] for p in (IMAGE_PLACEHOLDER, VIDEO_PLACEHOLDER)
):
raise ValueError("glm5_next supports images and videos in user messages only.")
while IMAGE_PLACEHOLDER in message["content"]:
tokens = (
processor.replace_image_token(mm_inputs, image_idx) if self.expand_mm_tokens else self.image_token
)
message["content"] = message["content"].replace(
IMAGE_PLACEHOLDER, f"<|begin_of_image|>{tokens}<|end_of_image|>", 1
)
image_idx += 1
while VIDEO_PLACEHOLDER in message["content"]:
tokens = (
processor.replace_video_token(mm_inputs, video_idx) if self.expand_mm_tokens else self.video_token
)
message["content"] = message["content"].replace(
VIDEO_PLACEHOLDER, f"<|begin_of_video|>{tokens}<|end_of_video|>", 1
)
video_idx += 1
return messages
@override
def get_mm_inputs(
self,
images: list["ImageInput"],
videos: list["VideoInput"],
audios: list["AudioInput"],
imglens: list[int],
vidlens: list[int],
audlens: list[int],
batch_ids: list[list[int]],
processor: Optional["MMProcessor"],
) -> dict[str, Union[list[list[int]], "torch.Tensor"]]:
mm_inputs = self._get_mm_inputs(images, videos, audios, processor)
token_types = processor.create_mm_token_type_ids(batch_ids)
for count, ids in zip(vidlens, batch_ids):
boundaries = [token for token in ids if token in (processor.video_start_id, processor.video_end_id)]
if boundaries != [processor.video_start_id, processor.video_end_id] * count:
raise ValueError("glm5_next video boundaries do not match inputs; check cutoff_len.")
# Images and video frames share image_token_id: validate by modality,
# otherwise a truncated image could be hidden by extra video tokens.
for modality, lens, key, subprocessor in (
(1, imglens, "image_grid_thw", processor.image_processor),
(2, vidlens, "video_grid_thw", processor.video_processor),
):
grids = mm_inputs.get(key, [])
offset = 0
for count, types in zip(lens, token_types):
expected = sum(
int(grid.prod()) // subprocessor.merge_size**2 for grid in grids[offset : offset + count]
)
if types.count(modality) != expected:
raise ValueError(
"glm5_next image/video tokens do not match features; check cutoff_len and media limits."
)
offset += count
mm_inputs.pop("video_metadata", None) # Used for prompt timestamps, never forwarded to model.forward.
mm_inputs["mm_token_type_ids"] = token_types
return mm_inputs
@dataclass @dataclass
class GLM4VPlugin(Qwen2VLPlugin): class GLM4VPlugin(Qwen2VLPlugin):
@override @override
@@ -3249,6 +3457,7 @@ PLUGINS = {
"gemma3n": Gemma3nPlugin, "gemma3n": Gemma3nPlugin,
"gemma4": Gemma4Plugin, "gemma4": Gemma4Plugin,
"glm4v": GLM4VPlugin, "glm4v": GLM4VPlugin,
"glm5_next": Glm5NextPlugin,
"intern_vl": InternVLPlugin, "intern_vl": InternVLPlugin,
"kimi_vl": KimiVLPlugin, "kimi_vl": KimiVLPlugin,
"llama4": Llama4Plugin, "llama4": Llama4Plugin,

View File

@@ -339,6 +339,73 @@ class Template:
return modelfile return modelfile
@dataclass
class Glm5NextTemplate(Template):
r"""GLM-5.3-Flash template with preserved thinking and EOS only on the final assistant response."""
@override
def _encode(
self,
tokenizer: "PreTrainedTokenizer",
messages: list[dict[str, str]],
system: Optional[str],
tools: Optional[str],
) -> list[list[int]]:
if not messages or len(messages) % 2 or messages[0]["role"] != Role.USER:
raise ValueError(
"glm5_next expects alternating user/observation and assistant/function pairs, starting with user."
)
system = system or self.default_system
encoded = []
for i, message in enumerate(messages):
role, content = message["role"], message["content"]
expected = (Role.USER, Role.OBSERVATION) if i % 2 == 0 else (Role.ASSISTANT, Role.FUNCTION)
if role not in expected or not isinstance(content, str):
raise ValueError(
"glm5_next expects alternating user/observation and assistant/function text messages."
)
if message.get("tool_calls"):
raise ValueError("glm5_next expects tool calls as JSON in role=function content (v0 ShareGPT format).")
if i % 2 == 0 and i > 0:
after_function = messages[i - 1]["role"] == Role.FUNCTION
if (role == Role.OBSERVATION) != after_function:
raise ValueError("glm5_next requires an observation after each function turn.")
elements = []
if i == 0:
elements += self.format_prefix.apply()
if tools:
elements += self.format_tools.apply(content=tools)
if system:
elements += self.format_system.apply(content=system)
if role == Role.USER:
elements += self.format_user.apply(content=content)
elif role == Role.OBSERVATION:
elements += self.format_observation.apply(content=content)
else:
if role == Role.FUNCTION:
content = self.format_function.apply(
content=content,
thought_words=self.thought_words,
tool_call_words=self.tool_call_words,
)[0]
if "</think>" in content:
reasoning = content.split("</think>")[0].split("<think>")[-1]
content = content.split("</think>")[-1]
else:
reasoning = ""
elements += self.format_assistant.apply(content=reasoning + "</think>" + content.strip())
if role == Role.FUNCTION:
# Supervise the observation marker so the model learns to hand control to tools.
elements += ["<|observation|>"]
elif i == len(messages) - 1:
elements += [{"eos_token"}]
encoded.append(self._convert_elements_to_ids(tokenizer, elements))
return encoded
@dataclass @dataclass
class MossVLTemplate(Template): class MossVLTemplate(Template):
@override @override
@@ -1211,6 +1278,23 @@ register_template(
) )
register_template(
name="glm5_next",
format_user=StringFormatter(slots=["<|user|>{{content}}<|assistant|><think>"]),
format_assistant=StringFormatter(slots=["{{content}}"]),
format_system=StringFormatter(slots=["<|system|>{{content}}"]),
format_function=FunctionFormatter(slots=["{{content}}"], tool_format="glm5_next"),
format_observation=StringFormatter(slots=["<tool_response>{{content}}</tool_response><|assistant|><think>"]),
format_tools=ToolFormatter(tool_format="glm5_next"),
format_prefix=EmptyFormatter(slots=["[gMASK]<sop><|system|>Reasoning Effort: Max"]),
stop_words=["<|user|>", "<|observation|>"],
thought_words=("<think>", "</think>"),
preserve_thinking=True,
mm_plugin=get_mm_plugin(name="glm5_next", image_token="<|image|>", video_token="<|video|>"),
template_class=Glm5NextTemplate,
)
register_template( register_template(
name="glm4", name="glm4",
format_user=StringFormatter(slots=["<|user|>\n{{content}}<|assistant|>"]), format_user=StringFormatter(slots=["<|user|>\n{{content}}<|assistant|>"]),

View File

@@ -55,6 +55,14 @@ GLM4_MOE_TOOL_PROMPT = (
"\n...\n</tool_call>\n" "\n...\n</tool_call>\n"
) )
GLM5_NEXT_TOOL_PROMPT = (
"<|system|>\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\n"
"You are provided with function signatures within <tools></tools> XML tags:\n<tools>\n{tool_text}"
"</tools>\n\nFor each function call, output the function name and arguments within the following XML format:\n"
"<tool_call>{{function-name}}<arg_key>{{arg-key-1}}</arg_key><arg_value>{{arg-value-1}}</arg_value>"
"<arg_key>{{arg-key-2}}</arg_key><arg_value>{{arg-value-2}}</arg_value>...</tool_call>"
)
LLAMA3_TOOL_PROMPT = ( LLAMA3_TOOL_PROMPT = (
"Cutting Knowledge Date: December 2023\nToday Date: {date}\n\n" "Cutting Knowledge Date: December 2023\nToday Date: {date}\n\n"
"You have access to the following functions. To call a function, please respond with JSON for a function call. " "You have access to the following functions. To call a function, please respond with JSON for a function call. "
@@ -804,6 +812,44 @@ class GLM4MOEToolUtils(QwenToolUtils):
return "\n".join(function_texts) return "\n".join(function_texts)
class GLM5NextToolUtils(GLM4MOEToolUtils):
r"""GLM5-Next tool using template."""
@override
@staticmethod
def tool_formatter(tools: list[dict[str, Any]]) -> str:
if not isinstance(tools, list):
raise ValueError("glm5_next tools must be a JSON list.")
tool_text = ""
for tool in tools:
tool = tool.get("function", tool)
if tool.get("defer_loading", False):
continue
tool = {key: value for key, value in tool.items() if key not in {"strict", "defer_loading"}}
tool_text += json.dumps(tool, ensure_ascii=False) + "\n"
return GLM5_NEXT_TOOL_PROMPT.format(tool_text=tool_text)
@override
@staticmethod
def function_formatter(functions: list["FunctionCall"]) -> str:
if not functions:
raise ValueError("glm5_next function messages must contain at least one call.")
calls = []
for name, arguments in functions:
if not isinstance(name, str) or not re.fullmatch(r"[^\s<>]+", name):
raise ValueError("Invalid glm5_next function name.")
arguments = json.loads(arguments)
if not isinstance(arguments, dict):
raise ValueError("glm5_next function arguments must be a JSON object.")
text = "<tool_call>" + name
for key, value in arguments.items():
value = value if isinstance(value, str) else json.dumps(value, ensure_ascii=False)
text += f"<arg_key>{key}</arg_key><arg_value>{value}</arg_value>"
calls.append(text + "</tool_call>")
return "".join(calls)
class SeedToolUtils(ToolUtils): class SeedToolUtils(ToolUtils):
r"""Seed tool using template.""" r"""Seed tool using template."""
@@ -986,6 +1032,7 @@ TOOLS = {
"qwen3_5": Qwen35ToolUtils(), "qwen3_5": Qwen35ToolUtils(),
"qwen3_8": Qwen38ToolUtils(), "qwen3_8": Qwen38ToolUtils(),
"glm4_moe": GLM4MOEToolUtils(), "glm4_moe": GLM4MOEToolUtils(),
"glm5_next": GLM5NextToolUtils(),
"seed_oss": SeedToolUtils(), "seed_oss": SeedToolUtils(),
"ling": LingToolUtils(), "ling": LingToolUtils(),
} }

View File

@@ -261,6 +261,15 @@ _register_composite_model(
) )
_register_composite_model(
model_type="glm5_next",
projector_keys=["model.visual.merger", "model.visual.downsample"],
vision_model_keys=["model.visual.patch_embed", "model.visual.blocks", "model.visual.post_layernorm"],
language_model_keys=["model.language_model", "lm_head"],
lora_conflict_keys=["patch_embed"],
)
_register_composite_model( _register_composite_model(
model_type="glm_ocr", model_type="glm_ocr",
projector_keys=["visual.merger"], projector_keys=["visual.merger"],

View File

@@ -186,6 +186,8 @@ def get_process_group_backend() -> str:
return "hccl" return "hccl"
elif get_current_accelerator().type == DeviceType.CUDA: elif get_current_accelerator().type == DeviceType.CUDA:
return "nccl" return "nccl"
elif get_current_accelerator().type == DeviceType.XPU:
return "xccl"
else: else:
return "gloo" return "gloo"

View File

@@ -78,6 +78,8 @@ def _get_visible_devices_env() -> str | None:
return "CUDA_VISIBLE_DEVICES" return "CUDA_VISIBLE_DEVICES"
elif CURRENT_DEVICE == "npu": elif CURRENT_DEVICE == "npu":
return "ASCEND_RT_VISIBLE_DEVICES" return "ASCEND_RT_VISIBLE_DEVICES"
elif CURRENT_DEVICE == "xpu":
return "ZE_AFFINITY_MASK"
else: else:
return None return None
@@ -160,6 +162,8 @@ def _manage_distributed_env(request: FixtureRequest, monkeypatch: MonkeyPatch) -
monkeypatch.setattr(torch.cuda, "device_count", lambda: 1) monkeypatch.setattr(torch.cuda, "device_count", lambda: 1)
elif CURRENT_DEVICE == "npu": elif CURRENT_DEVICE == "npu":
monkeypatch.setattr(torch.npu, "device_count", lambda: 1) monkeypatch.setattr(torch.npu, "device_count", lambda: 1)
elif CURRENT_DEVICE == "xpu":
monkeypatch.setattr(torch.xpu, "device_count", lambda: 1)
@pytest.fixture @pytest.fixture

View File

@@ -43,7 +43,7 @@ TRAIN_ARGS = {
} }
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
@pytest.mark.parametrize("num_samples", [16]) @pytest.mark.parametrize("num_samples", [16])
def test_feedback_data(num_samples: int): def test_feedback_data(num_samples: int):
train_dataset = load_dataset_module(**TRAIN_ARGS)["train_dataset"] train_dataset = load_dataset_module(**TRAIN_ARGS)["train_dataset"]

View File

@@ -52,7 +52,7 @@ def _convert_sharegpt_to_openai(messages: list[dict[str, str]]) -> list[dict[str
return new_messages return new_messages
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
@pytest.mark.parametrize("num_samples", [16]) @pytest.mark.parametrize("num_samples", [16])
def test_pairwise_data(num_samples: int): def test_pairwise_data(num_samples: int):
train_dataset = load_dataset_module(**TRAIN_ARGS)["train_dataset"] train_dataset = load_dataset_module(**TRAIN_ARGS)["train_dataset"]

View File

@@ -18,7 +18,7 @@ import pytest
from llamafactory.data.processor.processor_utils import infer_seqlen from llamafactory.data.processor.processor_utils import infer_seqlen
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
@pytest.mark.parametrize( @pytest.mark.parametrize(
"test_input,test_output", "test_input,test_output",
[ [

View File

@@ -43,7 +43,7 @@ TRAIN_ARGS = {
} }
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
@pytest.mark.parametrize("num_samples", [16]) @pytest.mark.parametrize("num_samples", [16])
def test_supervised_single_turn(num_samples: int): def test_supervised_single_turn(num_samples: int):
train_dataset = load_dataset_module(dataset_dir="ONLINE", dataset=TINY_DATA, **TRAIN_ARGS)["train_dataset"] train_dataset = load_dataset_module(dataset_dir="ONLINE", dataset=TINY_DATA, **TRAIN_ARGS)["train_dataset"]
@@ -72,7 +72,7 @@ def test_supervised_single_turn(num_samples: int):
assert train_dataset["labels"][index] == ref_label_ids assert train_dataset["labels"][index] == ref_label_ids
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
@pytest.mark.parametrize("num_samples", [8]) @pytest.mark.parametrize("num_samples", [8])
def test_supervised_multi_turn(num_samples: int): def test_supervised_multi_turn(num_samples: int):
train_dataset = load_dataset_module(dataset_dir="REMOTE:" + DEMO_DATA, dataset="system_chat", **TRAIN_ARGS)[ train_dataset = load_dataset_module(dataset_dir="REMOTE:" + DEMO_DATA, dataset="system_chat", **TRAIN_ARGS)[
@@ -90,7 +90,7 @@ def test_supervised_multi_turn(num_samples: int):
assert train_dataset["input_ids"][index] == ref_input_ids assert train_dataset["input_ids"][index] == ref_input_ids
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
@pytest.mark.parametrize("num_samples", [4]) @pytest.mark.parametrize("num_samples", [4])
def test_supervised_train_on_prompt(num_samples: int): def test_supervised_train_on_prompt(num_samples: int):
train_dataset = load_dataset_module( train_dataset = load_dataset_module(
@@ -108,7 +108,7 @@ def test_supervised_train_on_prompt(num_samples: int):
assert train_dataset["labels"][index] == ref_input_ids assert train_dataset["labels"][index] == ref_input_ids
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
@pytest.mark.parametrize("num_samples", [4]) @pytest.mark.parametrize("num_samples", [4])
def test_supervised_mask_history(num_samples: int): def test_supervised_mask_history(num_samples: int):
train_dataset = load_dataset_module( train_dataset = load_dataset_module(

View File

@@ -47,7 +47,7 @@ TRAIN_ARGS = {
} }
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
@pytest.mark.parametrize("num_samples", [16]) @pytest.mark.parametrize("num_samples", [16])
def test_unsupervised_data(num_samples: int): def test_unsupervised_data(num_samples: int):
train_dataset = load_dataset_module(**TRAIN_ARGS)["train_dataset"] train_dataset = load_dataset_module(**TRAIN_ARGS)["train_dataset"]

View File

@@ -32,7 +32,7 @@ from llamafactory.model import load_tokenizer
TINY_LLAMA3 = os.getenv("TINY_LLAMA3", "llamafactory/tiny-random-Llama-3") TINY_LLAMA3 = os.getenv("TINY_LLAMA3", "llamafactory/tiny-random-Llama-3")
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_base_collator(): def test_base_collator():
model_args, data_args, *_ = get_infer_args({"model_name_or_path": TINY_LLAMA3, "template": "default"}) model_args, data_args, *_ = get_infer_args({"model_name_or_path": TINY_LLAMA3, "template": "default"})
tokenizer_module = load_tokenizer(model_args) tokenizer_module = load_tokenizer(model_args)
@@ -76,7 +76,7 @@ def test_base_collator():
assert batch_input[k].eq(torch.tensor(expected_input[k])).all() assert batch_input[k].eq(torch.tensor(expected_input[k])).all()
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_multimodal_collator(): def test_multimodal_collator():
model_args, data_args, *_ = get_infer_args( model_args, data_args, *_ = get_infer_args(
{"model_name_or_path": "Qwen/Qwen2-VL-2B-Instruct", "template": "qwen2_vl"} {"model_name_or_path": "Qwen/Qwen2-VL-2B-Instruct", "template": "qwen2_vl"}
@@ -270,7 +270,7 @@ def _get_expected_position_ids(
return torch.cat(all_position_ids, dim=-1) return torch.cat(all_position_ids, dim=-1)
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_multimodal_collator_with_packing(): def test_multimodal_collator_with_packing():
model_args, data_args, *_ = get_infer_args( model_args, data_args, *_ = get_infer_args(
{"model_name_or_path": "Qwen/Qwen2-VL-2B-Instruct", "template": "qwen2_vl"} {"model_name_or_path": "Qwen/Qwen2-VL-2B-Instruct", "template": "qwen2_vl"}
@@ -322,7 +322,7 @@ def test_multimodal_collator_with_packing():
assert batch_input["position_ids"][1:, :, :valid_len].eq(expected_position_ids).all() assert batch_input["position_ids"][1:, :, :valid_len].eq(expected_position_ids).all()
@pytest.mark.runs_on(["cpu"]) @pytest.mark.runs_on(["cpu", "xpu"])
def test_4d_attention_mask(): def test_4d_attention_mask():
o = 0.0 o = 0.0
x = torch.finfo(torch.float16).min x = torch.finfo(torch.float16).min

View File

@@ -20,7 +20,7 @@ from llamafactory.data.parser import DatasetAttr
from llamafactory.hparams import DataArguments from llamafactory.hparams import DataArguments
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_alpaca_converter(): def test_alpaca_converter():
dataset_attr = DatasetAttr("hf_hub", "llamafactory/tiny-supervised-dataset") dataset_attr = DatasetAttr("hf_hub", "llamafactory/tiny-supervised-dataset")
data_args = DataArguments() data_args = DataArguments()
@@ -41,7 +41,7 @@ def test_alpaca_converter():
} }
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_sharegpt_converter(): def test_sharegpt_converter():
dataset_attr = DatasetAttr("hf_hub", "llamafactory/tiny-supervised-dataset") dataset_attr = DatasetAttr("hf_hub", "llamafactory/tiny-supervised-dataset")
data_args = DataArguments() data_args = DataArguments()

View File

@@ -38,19 +38,19 @@ TOOLS = [
] ]
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_empty_formatter(): def test_empty_formatter():
formatter = EmptyFormatter(slots=["\n"]) formatter = EmptyFormatter(slots=["\n"])
assert formatter.apply() == ["\n"] assert formatter.apply() == ["\n"]
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_string_formatter(): def test_string_formatter():
formatter = StringFormatter(slots=["<s>", "Human: {{content}}\nAssistant:"]) formatter = StringFormatter(slots=["<s>", "Human: {{content}}\nAssistant:"])
assert formatter.apply(content="Hi") == ["<s>", "Human: Hi\nAssistant:"] assert formatter.apply(content="Hi") == ["<s>", "Human: Hi\nAssistant:"]
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_function_formatter(): def test_function_formatter():
formatter = FunctionFormatter(slots=["{{content}}", "</s>"], tool_format="default") formatter = FunctionFormatter(slots=["{{content}}", "</s>"], tool_format="default")
tool_calls = json.dumps(FUNCTION) tool_calls = json.dumps(FUNCTION)
@@ -60,7 +60,7 @@ def test_function_formatter():
] ]
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_multi_function_formatter(): def test_multi_function_formatter():
formatter = FunctionFormatter(slots=["{{content}}", "</s>"], tool_format="default") formatter = FunctionFormatter(slots=["{{content}}", "</s>"], tool_format="default")
tool_calls = json.dumps([FUNCTION] * 2) tool_calls = json.dumps([FUNCTION] * 2)
@@ -71,7 +71,7 @@ def test_multi_function_formatter():
] ]
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_default_tool_formatter(): def test_default_tool_formatter():
formatter = ToolFormatter(tool_format="default") formatter = ToolFormatter(tool_format="default")
assert formatter.apply(content=json.dumps(TOOLS)) == [ assert formatter.apply(content=json.dumps(TOOLS)) == [
@@ -90,14 +90,14 @@ def test_default_tool_formatter():
] ]
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_default_tool_extractor(): def test_default_tool_extractor():
formatter = ToolFormatter(tool_format="default") formatter = ToolFormatter(tool_format="default")
result = """Action: test_tool\nAction Input: {"foo": "bar", "size": 10}""" result = """Action: test_tool\nAction Input: {"foo": "bar", "size": 10}"""
assert formatter.extract(result) == [("test_tool", """{"foo": "bar", "size": 10}""")] assert formatter.extract(result) == [("test_tool", """{"foo": "bar", "size": 10}""")]
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_default_multi_tool_extractor(): def test_default_multi_tool_extractor():
formatter = ToolFormatter(tool_format="default") formatter = ToolFormatter(tool_format="default")
result = ( result = (
@@ -110,14 +110,14 @@ def test_default_multi_tool_extractor():
] ]
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_glm4_function_formatter(): def test_glm4_function_formatter():
formatter = FunctionFormatter(slots=["{{content}}"], tool_format="glm4") formatter = FunctionFormatter(slots=["{{content}}"], tool_format="glm4")
tool_calls = json.dumps(FUNCTION) tool_calls = json.dumps(FUNCTION)
assert formatter.apply(content=tool_calls) == ["""tool_name\n{"foo": "bar", "size": 10}"""] assert formatter.apply(content=tool_calls) == ["""tool_name\n{"foo": "bar", "size": 10}"""]
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_glm4_tool_formatter(): def test_glm4_tool_formatter():
formatter = ToolFormatter(tool_format="glm4") formatter = ToolFormatter(tool_format="glm4")
assert formatter.apply(content=json.dumps(TOOLS)) == [ assert formatter.apply(content=json.dumps(TOOLS)) == [
@@ -128,14 +128,14 @@ def test_glm4_tool_formatter():
] ]
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_glm4_tool_extractor(): def test_glm4_tool_extractor():
formatter = ToolFormatter(tool_format="glm4") formatter = ToolFormatter(tool_format="glm4")
result = """test_tool\n{"foo": "bar", "size": 10}\n""" result = """test_tool\n{"foo": "bar", "size": 10}\n"""
assert formatter.extract(result) == [("test_tool", """{"foo": "bar", "size": 10}""")] assert formatter.extract(result) == [("test_tool", """{"foo": "bar", "size": 10}""")]
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_llama3_function_formatter(): def test_llama3_function_formatter():
formatter = FunctionFormatter(slots=["{{content}}<|eot_id|>"], tool_format="llama3") formatter = FunctionFormatter(slots=["{{content}}<|eot_id|>"], tool_format="llama3")
tool_calls = json.dumps(FUNCTION) tool_calls = json.dumps(FUNCTION)
@@ -144,7 +144,7 @@ def test_llama3_function_formatter():
] ]
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_llama3_multi_function_formatter(): def test_llama3_multi_function_formatter():
formatter = FunctionFormatter(slots=["{{content}}<|eot_id|>"], tool_format="llama3") formatter = FunctionFormatter(slots=["{{content}}<|eot_id|>"], tool_format="llama3")
tool_calls = json.dumps([FUNCTION] * 2) tool_calls = json.dumps([FUNCTION] * 2)
@@ -155,7 +155,7 @@ def test_llama3_multi_function_formatter():
] ]
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_llama3_tool_formatter(): def test_llama3_tool_formatter():
formatter = ToolFormatter(tool_format="llama3") formatter = ToolFormatter(tool_format="llama3")
date = datetime.now().strftime("%d %b %Y") date = datetime.now().strftime("%d %b %Y")
@@ -169,14 +169,14 @@ def test_llama3_tool_formatter():
] ]
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_llama3_tool_extractor(): def test_llama3_tool_extractor():
formatter = ToolFormatter(tool_format="llama3") formatter = ToolFormatter(tool_format="llama3")
result = """{"name": "test_tool", "parameters": {"foo": "bar", "size": 10}}\n""" result = """{"name": "test_tool", "parameters": {"foo": "bar", "size": 10}}\n"""
assert formatter.extract(result) == [("test_tool", """{"foo": "bar", "size": 10}""")] assert formatter.extract(result) == [("test_tool", """{"foo": "bar", "size": 10}""")]
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_llama3_multi_tool_extractor(): def test_llama3_multi_tool_extractor():
formatter = ToolFormatter(tool_format="llama3") formatter = ToolFormatter(tool_format="llama3")
result = ( result = (
@@ -189,7 +189,7 @@ def test_llama3_multi_tool_extractor():
] ]
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_mistral_function_formatter(): def test_mistral_function_formatter():
formatter = FunctionFormatter(slots=["[TOOL_CALLS] {{content}}", "</s>"], tool_format="mistral") formatter = FunctionFormatter(slots=["[TOOL_CALLS] {{content}}", "</s>"], tool_format="mistral")
tool_calls = json.dumps(FUNCTION) tool_calls = json.dumps(FUNCTION)
@@ -199,7 +199,7 @@ def test_mistral_function_formatter():
] ]
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_mistral_multi_function_formatter(): def test_mistral_multi_function_formatter():
formatter = FunctionFormatter(slots=["[TOOL_CALLS] {{content}}", "</s>"], tool_format="mistral") formatter = FunctionFormatter(slots=["[TOOL_CALLS] {{content}}", "</s>"], tool_format="mistral")
tool_calls = json.dumps([FUNCTION] * 2) tool_calls = json.dumps([FUNCTION] * 2)
@@ -211,7 +211,7 @@ def test_mistral_multi_function_formatter():
] ]
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_mistral_tool_formatter(): def test_mistral_tool_formatter():
formatter = ToolFormatter(tool_format="mistral") formatter = ToolFormatter(tool_format="mistral")
wrapped_tool = {"type": "function", "function": TOOLS[0]} wrapped_tool = {"type": "function", "function": TOOLS[0]}
@@ -220,14 +220,14 @@ def test_mistral_tool_formatter():
] ]
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_mistral_tool_extractor(): def test_mistral_tool_extractor():
formatter = ToolFormatter(tool_format="mistral") formatter = ToolFormatter(tool_format="mistral")
result = """{"name": "test_tool", "arguments": {"foo": "bar", "size": 10}}""" result = """{"name": "test_tool", "arguments": {"foo": "bar", "size": 10}}"""
assert formatter.extract(result) == [("test_tool", """{"foo": "bar", "size": 10}""")] assert formatter.extract(result) == [("test_tool", """{"foo": "bar", "size": 10}""")]
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_mistral_multi_tool_extractor(): def test_mistral_multi_tool_extractor():
formatter = ToolFormatter(tool_format="mistral") formatter = ToolFormatter(tool_format="mistral")
result = ( result = (
@@ -240,7 +240,7 @@ def test_mistral_multi_tool_extractor():
] ]
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_qwen_function_formatter(): def test_qwen_function_formatter():
formatter = FunctionFormatter(slots=["{{content}}<|im_end|>\n"], tool_format="qwen") formatter = FunctionFormatter(slots=["{{content}}<|im_end|>\n"], tool_format="qwen")
tool_calls = json.dumps(FUNCTION) tool_calls = json.dumps(FUNCTION)
@@ -249,7 +249,7 @@ def test_qwen_function_formatter():
] ]
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_qwen_multi_function_formatter(): def test_qwen_multi_function_formatter():
formatter = FunctionFormatter(slots=["{{content}}<|im_end|>\n"], tool_format="qwen") formatter = FunctionFormatter(slots=["{{content}}<|im_end|>\n"], tool_format="qwen")
tool_calls = json.dumps([FUNCTION] * 2) tool_calls = json.dumps([FUNCTION] * 2)
@@ -260,7 +260,7 @@ def test_qwen_multi_function_formatter():
] ]
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_qwen_tool_formatter(): def test_qwen_tool_formatter():
formatter = ToolFormatter(tool_format="qwen") formatter = ToolFormatter(tool_format="qwen")
wrapped_tool = {"type": "function", "function": TOOLS[0]} wrapped_tool = {"type": "function", "function": TOOLS[0]}
@@ -274,14 +274,14 @@ def test_qwen_tool_formatter():
] ]
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_qwen_tool_extractor(): def test_qwen_tool_extractor():
formatter = ToolFormatter(tool_format="qwen") formatter = ToolFormatter(tool_format="qwen")
result = """<tool_call>\n{"name": "test_tool", "arguments": {"foo": "bar", "size": 10}}\n</tool_call>""" result = """<tool_call>\n{"name": "test_tool", "arguments": {"foo": "bar", "size": 10}}\n</tool_call>"""
assert formatter.extract(result) == [("test_tool", """{"foo": "bar", "size": 10}""")] assert formatter.extract(result) == [("test_tool", """{"foo": "bar", "size": 10}""")]
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_qwen38_tool_formatter(): def test_qwen38_tool_formatter():
formatter = ToolFormatter(tool_format="qwen3_8") formatter = ToolFormatter(tool_format="qwen3_8")
wrapped_tool = {"type": "function", "function": TOOLS[0]} wrapped_tool = {"type": "function", "function": TOOLS[0]}
@@ -289,7 +289,7 @@ def test_qwen38_tool_formatter():
assert json.dumps(wrapped_tool, ensure_ascii=False) in output assert json.dumps(wrapped_tool, ensure_ascii=False) in output
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_qwen_multi_tool_extractor(): def test_qwen_multi_tool_extractor():
formatter = ToolFormatter(tool_format="qwen") formatter = ToolFormatter(tool_format="qwen")
result = ( result = (
@@ -302,7 +302,7 @@ def test_qwen_multi_tool_extractor():
] ]
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_lfm2_function_formatter(): def test_lfm2_function_formatter():
formatter = FunctionFormatter(slots=["{{content}}<|im_end|>\n"], tool_format="lfm2") formatter = FunctionFormatter(slots=["{{content}}<|im_end|>\n"], tool_format="lfm2")
tool_calls = json.dumps(FUNCTION) tool_calls = json.dumps(FUNCTION)
@@ -311,7 +311,7 @@ def test_lfm2_function_formatter():
] ]
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_lfm2_multi_function_formatter(): def test_lfm2_multi_function_formatter():
formatter = FunctionFormatter(slots=["{{content}}<|im_end|>\n"], tool_format="lfm2") formatter = FunctionFormatter(slots=["{{content}}<|im_end|>\n"], tool_format="lfm2")
tool_calls = json.dumps([FUNCTION] * 2) tool_calls = json.dumps([FUNCTION] * 2)
@@ -321,7 +321,7 @@ def test_lfm2_multi_function_formatter():
] ]
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_lfm2_tool_formatter(): def test_lfm2_tool_formatter():
formatter = ToolFormatter(tool_format="lfm2") formatter = ToolFormatter(tool_format="lfm2")
assert formatter.apply(content=json.dumps(TOOLS)) == [ assert formatter.apply(content=json.dumps(TOOLS)) == [
@@ -329,14 +329,14 @@ def test_lfm2_tool_formatter():
] ]
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_lfm2_tool_extractor(): def test_lfm2_tool_extractor():
formatter = ToolFormatter(tool_format="lfm2") formatter = ToolFormatter(tool_format="lfm2")
result = """<|tool_call_start|>[test_tool(foo="bar", size=10)]<|tool_call_end|>""" result = """<|tool_call_start|>[test_tool(foo="bar", size=10)]<|tool_call_end|>"""
assert formatter.extract(result) == [("test_tool", """{"foo": "bar", "size": 10}""")] assert formatter.extract(result) == [("test_tool", """{"foo": "bar", "size": 10}""")]
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_lfm2_multi_tool_extractor(): def test_lfm2_multi_tool_extractor():
formatter = ToolFormatter(tool_format="lfm2") formatter = ToolFormatter(tool_format="lfm2")
result = """<|tool_call_start|>[test_tool(foo="bar", size=10), another_tool(foo="job", size=2)]<|tool_call_end|>""" result = """<|tool_call_start|>[test_tool(foo="bar", size=10), another_tool(foo="job", size=2)]<|tool_call_end|>"""
@@ -346,7 +346,7 @@ def test_lfm2_multi_tool_extractor():
] ]
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_lfm2_tool_extractor_with_nested_dict(): def test_lfm2_tool_extractor_with_nested_dict():
formatter = ToolFormatter(tool_format="lfm2") formatter = ToolFormatter(tool_format="lfm2")
result = """<|tool_call_start|>[search(query="test", options={"limit": 10, "offset": 0})]<|tool_call_end|>""" result = """<|tool_call_start|>[search(query="test", options={"limit": 10, "offset": 0})]<|tool_call_end|>"""
@@ -358,7 +358,7 @@ def test_lfm2_tool_extractor_with_nested_dict():
assert args["options"] == {"limit": 10, "offset": 0} assert args["options"] == {"limit": 10, "offset": 0}
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_lfm2_tool_extractor_with_list_arg(): def test_lfm2_tool_extractor_with_list_arg():
formatter = ToolFormatter(tool_format="lfm2") formatter = ToolFormatter(tool_format="lfm2")
result = """<|tool_call_start|>[batch_process(items=[1, 2, 3], enabled=True)]<|tool_call_end|>""" result = """<|tool_call_start|>[batch_process(items=[1, 2, 3], enabled=True)]<|tool_call_end|>"""
@@ -370,7 +370,7 @@ def test_lfm2_tool_extractor_with_list_arg():
assert args["enabled"] is True assert args["enabled"] is True
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_lfm2_tool_extractor_no_match(): def test_lfm2_tool_extractor_no_match():
formatter = ToolFormatter(tool_format="lfm2") formatter = ToolFormatter(tool_format="lfm2")
result = "This is a regular response without tool calls." result = "This is a regular response without tool calls."
@@ -378,7 +378,7 @@ def test_lfm2_tool_extractor_no_match():
assert extracted == result assert extracted == result
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_lfm2_tool_round_trip(): def test_lfm2_tool_round_trip():
formatter = FunctionFormatter(slots=["{{content}}"], tool_format="lfm2") formatter = FunctionFormatter(slots=["{{content}}"], tool_format="lfm2")
tool_formatter = ToolFormatter(tool_format="lfm2") tool_formatter = ToolFormatter(tool_format="lfm2")
@@ -390,7 +390,7 @@ def test_lfm2_tool_round_trip():
assert json.loads(extracted[0][1]) == original["arguments"] assert json.loads(extracted[0][1]) == original["arguments"]
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_minicpm5_function_formatter(): def test_minicpm5_function_formatter():
formatter = FunctionFormatter(slots=["{{content}}<|im_end|>\n"], tool_format="minicpm5") formatter = FunctionFormatter(slots=["{{content}}<|im_end|>\n"], tool_format="minicpm5")
tool_calls = json.dumps(FUNCTION) tool_calls = json.dumps(FUNCTION)
@@ -399,7 +399,7 @@ def test_minicpm5_function_formatter():
] ]
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_minicpm5_multi_function_formatter(): def test_minicpm5_multi_function_formatter():
formatter = FunctionFormatter(slots=["{{content}}<|im_end|>\n"], tool_format="minicpm5") formatter = FunctionFormatter(slots=["{{content}}<|im_end|>\n"], tool_format="minicpm5")
tool_calls = json.dumps([FUNCTION] * 2) tool_calls = json.dumps([FUNCTION] * 2)
@@ -411,7 +411,7 @@ def test_minicpm5_multi_function_formatter():
] ]
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_minicpm5_tool_formatter(): def test_minicpm5_tool_formatter():
formatter = ToolFormatter(tool_format="minicpm5") formatter = ToolFormatter(tool_format="minicpm5")
wrapped = json.dumps({"type": "function", "function": TOOLS[0]}, ensure_ascii=False) wrapped = json.dumps({"type": "function", "function": TOOLS[0]}, ensure_ascii=False)
@@ -427,28 +427,28 @@ def test_minicpm5_tool_formatter():
] ]
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_minicpm5_tool_extractor(): def test_minicpm5_tool_extractor():
formatter = ToolFormatter(tool_format="minicpm5") formatter = ToolFormatter(tool_format="minicpm5")
result = '<function name="test_tool"><param name="foo">bar</param><param name="size">10</param></function>' result = '<function name="test_tool"><param name="foo">bar</param><param name="size">10</param></function>'
assert formatter.extract(result) == [("test_tool", """{"foo": "bar", "size": 10}""")] assert formatter.extract(result) == [("test_tool", """{"foo": "bar", "size": 10}""")]
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_minicpm5_tool_extractor_cdata(): def test_minicpm5_tool_extractor_cdata():
formatter = ToolFormatter(tool_format="minicpm5") formatter = ToolFormatter(tool_format="minicpm5")
result = '<function name="test_tool"><param name="foo"><![CDATA[a < b\nsecond line]]></param></function>' result = '<function name="test_tool"><param name="foo"><![CDATA[a < b\nsecond line]]></param></function>'
assert formatter.extract(result) == [("test_tool", json.dumps({"foo": "a < b\nsecond line"}))] assert formatter.extract(result) == [("test_tool", json.dumps({"foo": "a < b\nsecond line"}))]
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_minicpm5_tool_extractor_malformed_value(): def test_minicpm5_tool_extractor_malformed_value():
formatter = ToolFormatter(tool_format="minicpm5") formatter = ToolFormatter(tool_format="minicpm5")
result = '<function name="test_tool"><param name="foo">{[1, 2], [3, 4]}</param></function>' result = '<function name="test_tool"><param name="foo">{[1, 2], [3, 4]}</param></function>'
assert formatter.extract(result) == [("test_tool", json.dumps({"foo": "{[1, 2], [3, 4]}"}))] assert formatter.extract(result) == [("test_tool", json.dumps({"foo": "{[1, 2], [3, 4]}"}))]
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
@pytest.mark.parametrize( @pytest.mark.parametrize(
"arguments", "arguments",
[ [

View File

@@ -40,21 +40,21 @@ TRAIN_ARGS = {
} }
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_load_train_only(): def test_load_train_only():
dataset_module = load_dataset_module(**TRAIN_ARGS) dataset_module = load_dataset_module(**TRAIN_ARGS)
assert dataset_module.get("train_dataset") is not None assert dataset_module.get("train_dataset") is not None
assert dataset_module.get("eval_dataset") is None assert dataset_module.get("eval_dataset") is None
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_load_val_size(): def test_load_val_size():
dataset_module = load_dataset_module(val_size=0.1, **TRAIN_ARGS) dataset_module = load_dataset_module(val_size=0.1, **TRAIN_ARGS)
assert dataset_module.get("train_dataset") is not None assert dataset_module.get("train_dataset") is not None
assert dataset_module.get("eval_dataset") is not None assert dataset_module.get("eval_dataset") is not None
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_load_eval_data(): def test_load_eval_data():
dataset_module = load_dataset_module(eval_dataset=TINY_DATA, **TRAIN_ARGS) dataset_module = load_dataset_module(eval_dataset=TINY_DATA, **TRAIN_ARGS)
assert dataset_module.get("train_dataset") is not None assert dataset_module.get("train_dataset") is not None

View File

@@ -180,7 +180,7 @@ def _check_plugin(
) )
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_base_plugin(): def test_base_plugin():
tokenizer_module = _load_tokenizer_module(model_name_or_path=TINY_LLAMA3) tokenizer_module = _load_tokenizer_module(model_name_or_path=TINY_LLAMA3)
base_plugin = get_mm_plugin(name="base") base_plugin = get_mm_plugin(name="base")
@@ -188,7 +188,7 @@ def test_base_plugin():
_check_plugin(**check_inputs) _check_plugin(**check_inputs)
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
@pytest.mark.skipif(not HF_TOKEN, reason="Gated model.") @pytest.mark.skipif(not HF_TOKEN, reason="Gated model.")
@pytest.mark.skipif(not is_transformers_version_greater_than("4.50.0"), reason="Requires transformers>=4.50.0") @pytest.mark.skipif(not is_transformers_version_greater_than("4.50.0"), reason="Requires transformers>=4.50.0")
def test_gemma3_plugin(): def test_gemma3_plugin():
@@ -211,7 +211,7 @@ def test_gemma3_plugin():
_check_plugin(**check_inputs) _check_plugin(**check_inputs)
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
@pytest.mark.skipif(not is_transformers_version_greater_than("5.6.0"), reason="Requires transformers>=5.6.0") @pytest.mark.skipif(not is_transformers_version_greater_than("5.6.0"), reason="Requires transformers>=5.6.0")
def test_gemma4_plugin(): def test_gemma4_plugin():
tokenizer_module = _load_tokenizer_module(model_name_or_path="google/gemma-4-31B-it") tokenizer_module = _load_tokenizer_module(model_name_or_path="google/gemma-4-31B-it")
@@ -244,7 +244,7 @@ def test_gemma4_plugin():
_check_plugin(**check_inputs) _check_plugin(**check_inputs)
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
@pytest.mark.skipif(not is_transformers_version_greater_than("4.52.0"), reason="Requires transformers>=4.52.0") @pytest.mark.skipif(not is_transformers_version_greater_than("4.52.0"), reason="Requires transformers>=4.52.0")
def test_internvl_plugin(): def test_internvl_plugin():
image_seqlen = 256 image_seqlen = 256
@@ -263,7 +263,7 @@ def test_internvl_plugin():
_check_plugin(**check_inputs) _check_plugin(**check_inputs)
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
@pytest.mark.skipif(not is_transformers_version_greater_than("4.51.0"), reason="Requires transformers>=4.51.0") @pytest.mark.skipif(not is_transformers_version_greater_than("4.51.0"), reason="Requires transformers>=4.51.0")
def test_llama4_plugin(): def test_llama4_plugin():
tokenizer_module = _load_tokenizer_module(model_name_or_path=TINY_LLAMA4) tokenizer_module = _load_tokenizer_module(model_name_or_path=TINY_LLAMA4)
@@ -285,7 +285,7 @@ def test_llama4_plugin():
_check_plugin(**check_inputs) _check_plugin(**check_inputs)
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_llava_plugin(): def test_llava_plugin():
image_seqlen = 576 image_seqlen = 576
tokenizer_module = _load_tokenizer_module(model_name_or_path="llava-hf/llava-1.5-7b-hf") tokenizer_module = _load_tokenizer_module(model_name_or_path="llava-hf/llava-1.5-7b-hf")
@@ -299,7 +299,7 @@ def test_llava_plugin():
_check_plugin(**check_inputs) _check_plugin(**check_inputs)
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_llava_next_plugin(): def test_llava_next_plugin():
image_seqlen = 1176 image_seqlen = 1176
tokenizer_module = _load_tokenizer_module(model_name_or_path="llava-hf/llava-v1.6-vicuna-7b-hf") tokenizer_module = _load_tokenizer_module(model_name_or_path="llava-hf/llava-v1.6-vicuna-7b-hf")
@@ -313,7 +313,7 @@ def test_llava_next_plugin():
_check_plugin(**check_inputs) _check_plugin(**check_inputs)
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_llava_next_video_plugin(): def test_llava_next_video_plugin():
image_seqlen = 1176 image_seqlen = 1176
tokenizer_module = _load_tokenizer_module(model_name_or_path="llava-hf/LLaVA-NeXT-Video-7B-hf") tokenizer_module = _load_tokenizer_module(model_name_or_path="llava-hf/LLaVA-NeXT-Video-7B-hf")
@@ -327,7 +327,7 @@ def test_llava_next_video_plugin():
_check_plugin(**check_inputs) _check_plugin(**check_inputs)
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
@pytest.mark.skipif(not HF_TOKEN, reason="Gated model.") @pytest.mark.skipif(not HF_TOKEN, reason="Gated model.")
def test_paligemma_plugin(): def test_paligemma_plugin():
image_seqlen = 256 image_seqlen = 256
@@ -347,7 +347,7 @@ def test_paligemma_plugin():
_check_plugin(**check_inputs) _check_plugin(**check_inputs)
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
@pytest.mark.skipif(not is_transformers_version_greater_than("4.50.0"), reason="Requires transformers>=4.50.0") @pytest.mark.skipif(not is_transformers_version_greater_than("4.50.0"), reason="Requires transformers>=4.50.0")
def test_pixtral_plugin(): def test_pixtral_plugin():
image_slice_height, image_slice_width = 2, 2 image_slice_height, image_slice_width = 2, 2
@@ -370,7 +370,7 @@ def test_pixtral_plugin():
_check_plugin(**check_inputs) _check_plugin(**check_inputs)
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
@pytest.mark.skipif(not is_transformers_version_greater_than("4.52.0"), reason="Requires transformers>=4.52.0") @pytest.mark.skipif(not is_transformers_version_greater_than("4.52.0"), reason="Requires transformers>=4.52.0")
def test_qwen2_omni_plugin(): def test_qwen2_omni_plugin():
image_seqlen, audio_seqlen = 4, 2 image_seqlen, audio_seqlen = 4, 2
@@ -401,7 +401,7 @@ def test_qwen2_omni_plugin():
_check_plugin(**check_inputs) _check_plugin(**check_inputs)
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_qwen2_vl_plugin(): def test_qwen2_vl_plugin():
image_seqlen = 4 image_seqlen = 4
tokenizer_module = _load_tokenizer_module(model_name_or_path="Qwen/Qwen2-VL-7B-Instruct") tokenizer_module = _load_tokenizer_module(model_name_or_path="Qwen/Qwen2-VL-7B-Instruct")
@@ -436,7 +436,7 @@ def test_moss_vl_plugin():
assert messages[0]["content"] == "First <image>, finally <image>." assert messages[0]["content"] == "First <image>, finally <image>."
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
@pytest.mark.skipif(not is_transformers_version_greater_than("4.57.0"), reason="Requires transformers>=4.57.0") @pytest.mark.skipif(not is_transformers_version_greater_than("4.57.0"), reason="Requires transformers>=4.57.0")
def test_qwen3_vl_plugin(): def test_qwen3_vl_plugin():
frame_seqlen = 1 frame_seqlen = 1
@@ -470,7 +470,7 @@ def test_qwen3_vl_plugin():
] ]
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
@pytest.mark.skipif(not is_transformers_version_greater_than("4.57.0"), reason="Requires transformers>=4.57.0") @pytest.mark.skipif(not is_transformers_version_greater_than("4.57.0"), reason="Requires transformers>=4.57.0")
@pytest.mark.skipif(not is_pyav_available(), reason="Requires pyav") @pytest.mark.skipif(not is_pyav_available(), reason="Requires pyav")
def test_qwen3_vl_plugin_video_path(): def test_qwen3_vl_plugin_video_path():
@@ -504,7 +504,7 @@ def test_qwen3_vl_plugin_video_path():
) )
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
@pytest.mark.skipif(not is_transformers_version_greater_than("4.47.0"), reason="Requires transformers>=4.47.0") @pytest.mark.skipif(not is_transformers_version_greater_than("4.47.0"), reason="Requires transformers>=4.47.0")
def test_video_llava_plugin(): def test_video_llava_plugin():
image_seqlen = 256 image_seqlen = 256
@@ -519,7 +519,7 @@ def test_video_llava_plugin():
_check_plugin(**check_inputs) _check_plugin(**check_inputs)
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_lfm2_vl_plugin(): def test_lfm2_vl_plugin():
"""Test LFM2.5-VL plugin instantiation.""" """Test LFM2.5-VL plugin instantiation."""
# Test plugin can be instantiated with correct tokens # Test plugin can be instantiated with correct tokens

View File

@@ -139,7 +139,7 @@ def test_qwen38_registration():
assert {"qwen3_5_moe_text", "qwen3_5_text"} <= MCA_SUPPORTED_MODELS assert {"qwen3_5_moe_text", "qwen3_5_text"} <= MCA_SUPPORTED_MODELS
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_encode_oneturn(): def test_encode_oneturn():
tokenizer = AutoTokenizer.from_pretrained(TINY_LLAMA3) tokenizer = AutoTokenizer.from_pretrained(TINY_LLAMA3)
template = get_template_and_fix_tokenizer(tokenizer, DataArguments(template="llama3")) template = get_template_and_fix_tokenizer(tokenizer, DataArguments(template="llama3"))
@@ -154,7 +154,7 @@ def test_encode_oneturn():
_check_tokenization(tokenizer, (prompt_ids, answer_ids), (prompt_str, answer_str)) _check_tokenization(tokenizer, (prompt_ids, answer_ids), (prompt_str, answer_str))
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_encode_multiturn(): def test_encode_multiturn():
tokenizer = AutoTokenizer.from_pretrained(TINY_LLAMA3) tokenizer = AutoTokenizer.from_pretrained(TINY_LLAMA3)
template = get_template_and_fix_tokenizer(tokenizer, DataArguments(template="llama3")) template = get_template_and_fix_tokenizer(tokenizer, DataArguments(template="llama3"))
@@ -175,7 +175,7 @@ def test_encode_multiturn():
) )
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
@pytest.mark.parametrize("cot_messages", [True, False]) @pytest.mark.parametrize("cot_messages", [True, False])
@pytest.mark.parametrize("enable_thinking", [True, False, None]) @pytest.mark.parametrize("enable_thinking", [True, False, None])
def test_reasoning_encode_oneturn(cot_messages: bool, enable_thinking: bool): def test_reasoning_encode_oneturn(cot_messages: bool, enable_thinking: bool):
@@ -201,7 +201,7 @@ def test_reasoning_encode_oneturn(cot_messages: bool, enable_thinking: bool):
_check_tokenization(tokenizer, (prompt_ids, answer_ids), (prompt_str, answer_str)) _check_tokenization(tokenizer, (prompt_ids, answer_ids), (prompt_str, answer_str))
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
@pytest.mark.parametrize("cot_messages", [True, False]) @pytest.mark.parametrize("cot_messages", [True, False])
@pytest.mark.parametrize("enable_thinking", [True, False, None]) @pytest.mark.parametrize("enable_thinking", [True, False, None])
def test_reasoning_encode_multiturn(cot_messages: bool, enable_thinking: bool): def test_reasoning_encode_multiturn(cot_messages: bool, enable_thinking: bool):
@@ -230,7 +230,7 @@ def test_reasoning_encode_multiturn(cot_messages: bool, enable_thinking: bool):
) )
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
@pytest.mark.parametrize("enable_thinking", [True, False, None]) @pytest.mark.parametrize("enable_thinking", [True, False, None])
@pytest.mark.parametrize("discarding_history_cot", [True, False]) @pytest.mark.parametrize("discarding_history_cot", [True, False])
def test_reasoning_encode_multiturn_discarding_history_cot(enable_thinking: bool, discarding_history_cot: bool): def test_reasoning_encode_multiturn_discarding_history_cot(enable_thinking: bool, discarding_history_cot: bool):
@@ -266,7 +266,7 @@ def test_reasoning_encode_multiturn_discarding_history_cot(enable_thinking: bool
) )
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_qwen38_reasoning_instruction(): def test_qwen38_reasoning_instruction():
tokenizer = CharTokenizer() tokenizer = CharTokenizer()
template = deepcopy(TEMPLATES["qwen3_8"]) template = deepcopy(TEMPLATES["qwen3_8"])
@@ -284,7 +284,7 @@ def test_qwen38_reasoning_instruction():
assert answer_text == "<think>\n\n</think>\n\nI am fine!<|im_end|>\n" assert answer_text == "<think>\n\n</think>\n\nI am fine!<|im_end|>\n"
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_qwen38_tool_system_order(): def test_qwen38_tool_system_order():
tokenizer = CharTokenizer() tokenizer = CharTokenizer()
template = deepcopy(TEMPLATES["qwen3_8"]) template = deepcopy(TEMPLATES["qwen3_8"])
@@ -308,7 +308,7 @@ def test_qwen38_tool_system_order():
) )
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_jinja_template(): def test_jinja_template():
tokenizer = AutoTokenizer.from_pretrained(TINY_LLAMA3) tokenizer = AutoTokenizer.from_pretrained(TINY_LLAMA3)
ref_tokenizer = AutoTokenizer.from_pretrained(TINY_LLAMA3) ref_tokenizer = AutoTokenizer.from_pretrained(TINY_LLAMA3)
@@ -318,7 +318,7 @@ def test_jinja_template():
assert tokenizer.apply_chat_template(MESSAGES) == ref_tokenizer.apply_chat_template(MESSAGES) assert tokenizer.apply_chat_template(MESSAGES) == ref_tokenizer.apply_chat_template(MESSAGES)
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_ollama_modelfile(): def test_ollama_modelfile():
tokenizer = AutoTokenizer.from_pretrained(TINY_LLAMA3) tokenizer = AutoTokenizer.from_pretrained(TINY_LLAMA3)
template = get_template_and_fix_tokenizer(tokenizer, DataArguments(template="llama3")) template = get_template_and_fix_tokenizer(tokenizer, DataArguments(template="llama3"))
@@ -336,14 +336,14 @@ def test_ollama_modelfile():
) )
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_get_stop_token_ids(): def test_get_stop_token_ids():
tokenizer = AutoTokenizer.from_pretrained(TINY_LLAMA3) tokenizer = AutoTokenizer.from_pretrained(TINY_LLAMA3)
template = get_template_and_fix_tokenizer(tokenizer, DataArguments(template="llama3")) template = get_template_and_fix_tokenizer(tokenizer, DataArguments(template="llama3"))
assert set(template.get_stop_token_ids(tokenizer)) == {128008, 128009} assert set(template.get_stop_token_ids(tokenizer)) == {128008, 128009}
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
@pytest.mark.skipif(not HF_TOKEN, reason="Gated model.") @pytest.mark.skipif(not HF_TOKEN, reason="Gated model.")
def test_gemma_template(): def test_gemma_template():
prompt_str = ( prompt_str = (
@@ -356,7 +356,7 @@ def test_gemma_template():
_check_template("google/gemma-3-4b-it", "gemma", prompt_str, answer_str) _check_template("google/gemma-3-4b-it", "gemma", prompt_str, answer_str)
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
@pytest.mark.skipif(not HF_TOKEN, reason="Gated model.") @pytest.mark.skipif(not HF_TOKEN, reason="Gated model.")
def test_gemma2_template(): def test_gemma2_template():
prompt_str = ( prompt_str = (
@@ -369,7 +369,7 @@ def test_gemma2_template():
_check_template("google/gemma-2-2b-it", "gemma2", prompt_str, answer_str) _check_template("google/gemma-2-2b-it", "gemma2", prompt_str, answer_str)
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
@pytest.mark.skipif(not HF_TOKEN, reason="Gated model.") @pytest.mark.skipif(not HF_TOKEN, reason="Gated model.")
def test_llama3_template(): def test_llama3_template():
prompt_str = ( prompt_str = (
@@ -382,7 +382,7 @@ def test_llama3_template():
_check_template("meta-llama/Meta-Llama-3-8B-Instruct", "llama3", prompt_str, answer_str) _check_template("meta-llama/Meta-Llama-3-8B-Instruct", "llama3", prompt_str, answer_str)
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_llama4_template(): def test_llama4_template():
prompt_str = ( prompt_str = (
f"<|begin_of_text|><|header_start|>user<|header_end|>\n\n{MESSAGES[0]['content']}<|eot|>" f"<|begin_of_text|><|header_start|>user<|header_end|>\n\n{MESSAGES[0]['content']}<|eot|>"
@@ -394,7 +394,7 @@ def test_llama4_template():
_check_template(TINY_LLAMA4, "llama4", prompt_str, answer_str) _check_template(TINY_LLAMA4, "llama4", prompt_str, answer_str)
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_phi4_template(): def test_phi4_template():
prompt_str = ( prompt_str = (
f"<|im_start|>user<|im_sep|>{MESSAGES[0]['content']}<|im_end|>" f"<|im_start|>user<|im_sep|>{MESSAGES[0]['content']}<|im_end|>"
@@ -406,7 +406,7 @@ def test_phi4_template():
_check_template("microsoft/phi-4", "phi4", prompt_str, answer_str) _check_template("microsoft/phi-4", "phi4", prompt_str, answer_str)
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
@pytest.mark.xfail(not HF_TOKEN, reason="Authorization.") @pytest.mark.xfail(not HF_TOKEN, reason="Authorization.")
def test_qwen2_5_template(): def test_qwen2_5_template():
prompt_str = ( prompt_str = (
@@ -420,7 +420,7 @@ def test_qwen2_5_template():
_check_template("Qwen/Qwen2.5-7B-Instruct", "qwen", prompt_str, answer_str) _check_template("Qwen/Qwen2.5-7B-Instruct", "qwen", prompt_str, answer_str)
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
@pytest.mark.parametrize("cot_messages", [True, False]) @pytest.mark.parametrize("cot_messages", [True, False])
def test_qwen3_template(cot_messages: bool): def test_qwen3_template(cot_messages: bool):
prompt_str = ( prompt_str = (
@@ -439,7 +439,7 @@ def test_qwen3_template(cot_messages: bool):
_check_template("Qwen/Qwen3-8B", "qwen3", prompt_str, answer_str, messages=messages) _check_template("Qwen/Qwen3-8B", "qwen3", prompt_str, answer_str, messages=messages)
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_parse_llama3_template(): def test_parse_llama3_template():
tokenizer = AutoTokenizer.from_pretrained(TINY_LLAMA3) tokenizer = AutoTokenizer.from_pretrained(TINY_LLAMA3)
template = parse_template(tokenizer) template = parse_template(tokenizer)
@@ -453,7 +453,7 @@ def test_parse_llama3_template():
assert template.default_system == "" assert template.default_system == ""
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
@pytest.mark.xfail(not HF_TOKEN, reason="Authorization.") @pytest.mark.xfail(not HF_TOKEN, reason="Authorization.")
def test_parse_qwen_template(): def test_parse_qwen_template():
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-7B-Instruct") tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-7B-Instruct")
@@ -466,7 +466,7 @@ def test_parse_qwen_template():
assert template.default_system == "You are Qwen, created by Alibaba Cloud. You are a helpful assistant." assert template.default_system == "You are Qwen, created by Alibaba Cloud. You are a helpful assistant."
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
@pytest.mark.xfail(not HF_TOKEN, reason="Authorization.") @pytest.mark.xfail(not HF_TOKEN, reason="Authorization.")
def test_parse_qwen3_template(): def test_parse_qwen3_template():
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-8B") tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-8B")

View File

@@ -37,13 +37,13 @@ MESSAGES = [
EXPECTED_RESPONSE = "_rho" EXPECTED_RESPONSE = "_rho"
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_chat(): def test_chat():
chat_model = ChatModel(INFER_ARGS) chat_model = ChatModel(INFER_ARGS)
assert chat_model.chat(MESSAGES)[0].response_text == EXPECTED_RESPONSE assert chat_model.chat(MESSAGES)[0].response_text == EXPECTED_RESPONSE
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_stream_chat(): def test_stream_chat():
chat_model = ChatModel(INFER_ARGS) chat_model = ChatModel(INFER_ARGS)
response = "" response = ""

View File

@@ -49,7 +49,7 @@ INFER_ARGS = {
OS_NAME = os.getenv("OS_NAME", "") OS_NAME = os.getenv("OS_NAME", "")
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
@pytest.mark.parametrize( @pytest.mark.parametrize(
"stage,dataset", "stage,dataset",
[ [
@@ -66,7 +66,7 @@ def test_run_exp(stage: str, dataset: str):
assert os.path.exists(output_dir) assert os.path.exists(output_dir)
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_export(): def test_export():
export_dir = os.path.join("output", "llama3_export") export_dir = os.path.join("output", "llama3_export")
export_model({"export_dir": export_dir, **INFER_ARGS}) export_model({"export_dir": export_dir, **INFER_ARGS})

View File

@@ -17,7 +17,7 @@ import pytest
from llamafactory.eval.template import get_eval_template from llamafactory.eval.template import get_eval_template
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_eval_template_en(): def test_eval_template_en():
support_set = [ support_set = [
{ {
@@ -56,7 +56,7 @@ def test_eval_template_en():
] ]
@pytest.mark.runs_on(["cpu", "mps"]) @pytest.mark.runs_on(["cpu", "mps", "xpu"])
def test_eval_template_zh(): def test_eval_template_zh():
support_set = [ support_set = [
{ {

View File

@@ -52,7 +52,7 @@ def test_all_device():
assert DistributedInterface().get_local_world_size() == int(os.getenv("LOCAL_WORLD_SIZE", "1")) assert DistributedInterface().get_local_world_size() == int(os.getenv("LOCAL_WORLD_SIZE", "1"))
@pytest.mark.runs_on(["cuda", "npu"]) @pytest.mark.runs_on(["cuda", "npu", "xpu"])
@pytest.mark.require_distributed(2) @pytest.mark.require_distributed(2)
def test_multi_device(): def test_multi_device():
master_port = find_available_port() master_port = find_available_port()

View File

@@ -79,6 +79,8 @@ def _get_visible_devices_env() -> str | None:
return "CUDA_VISIBLE_DEVICES" return "CUDA_VISIBLE_DEVICES"
elif CURRENT_DEVICE == "npu": elif CURRENT_DEVICE == "npu":
return "ASCEND_RT_VISIBLE_DEVICES" return "ASCEND_RT_VISIBLE_DEVICES"
elif CURRENT_DEVICE == "xpu":
return "ZE_AFFINITY_MASK"
else: else:
return None return None
@@ -172,6 +174,8 @@ def _manage_distributed_env(request: FixtureRequest, monkeypatch: MonkeyPatch) -
monkeypatch.setattr(torch.cuda, "device_count", lambda: 1) monkeypatch.setattr(torch.cuda, "device_count", lambda: 1)
elif CURRENT_DEVICE == "npu": elif CURRENT_DEVICE == "npu":
monkeypatch.setattr(torch.npu, "device_count", lambda: 1) monkeypatch.setattr(torch.npu, "device_count", lambda: 1)
elif CURRENT_DEVICE == "xpu":
monkeypatch.setattr(torch.xpu, "device_count", lambda: 1)
@pytest.fixture(scope="session", autouse=True) @pytest.fixture(scope="session", autouse=True)

View File

@@ -96,7 +96,7 @@ def _test_sequence_parallel_loss(
assert loss is not None assert loss is not None
@pytest.mark.runs_on(["cuda", "npu"]) @pytest.mark.runs_on(["cuda", "npu", "xpu"])
@pytest.mark.require_distributed(2) @pytest.mark.require_distributed(2)
@pytest.mark.parametrize(("cp_size", "dp_size", "batch_size"), [(2, 1, 1), (2, 1, 2)]) @pytest.mark.parametrize(("cp_size", "dp_size", "batch_size"), [(2, 1, 1), (2, 1, 2)])
def test_sequence_parallel_loss(cp_size, dp_size, batch_size): def test_sequence_parallel_loss(cp_size, dp_size, batch_size):

View File

@@ -19,7 +19,7 @@ from llamafactory.v1.core.model_engine import ModelEngine
from llamafactory.v1.samplers.cli_sampler import SyncSampler from llamafactory.v1.samplers.cli_sampler import SyncSampler
@pytest.mark.runs_on(["cuda", "npu"]) @pytest.mark.runs_on(["cuda", "npu", "xpu"])
def test_sync_sampler(): def test_sync_sampler():
model_args = ModelArguments(model="Qwen/Qwen3-4B-Instruct-2507") model_args = ModelArguments(model="Qwen/Qwen3-4B-Instruct-2507")
sample_args = SampleArguments() sample_args = SampleArguments()

View File

@@ -21,7 +21,7 @@ import pytest
@pytest.mark.xfail(reason="CI machines may OOM when heavily loaded.") @pytest.mark.xfail(reason="CI machines may OOM when heavily loaded.")
@pytest.mark.runs_on(["cuda", "npu"]) @pytest.mark.runs_on(["cuda", "npu", "xpu"])
def test_fsdp2_dpo_trainer(tmp_path: Path): def test_fsdp2_dpo_trainer(tmp_path: Path):
"""Test FSDP2 DPO trainer with sigmoid loss by simulating `llamafactory-cli dpo config.yaml`.""" """Test FSDP2 DPO trainer with sigmoid loss by simulating `llamafactory-cli dpo config.yaml`."""
config_yaml = """\ config_yaml = """\

View File

@@ -20,7 +20,7 @@ import pytest
@pytest.mark.xfail(reason="CI machines may OOM when heavily loaded.") @pytest.mark.xfail(reason="CI machines may OOM when heavily loaded.")
@pytest.mark.runs_on(["cuda", "npu"]) @pytest.mark.runs_on(["cuda", "npu", "xpu"])
def test_fsdp2_sft_trainer(tmp_path: Path): def test_fsdp2_sft_trainer(tmp_path: Path):
"""Test FSDP2 SFT trainer by simulating `llamafactory-cli sft config.yaml` behavior.""" """Test FSDP2 SFT trainer by simulating `llamafactory-cli sft config.yaml` behavior."""
config_yaml = """\ config_yaml = """\