merge master

This commit is contained in:
hjh0119
2026-08-03 20:49:52 +08:00
24 changed files with 1804 additions and 225 deletions

View File

@@ -1,8 +1,7 @@
if [ "$MODELSCOPE_SDK_DEBUG" == "True" ]; then
pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/
pip config set global.extra-index-url https://pypi.org/simple/
pip config set install.trusted-host mirrors.aliyun.com
pip install -r requirements/tests.txt
PIP_EXTRA_INDEX_URL=https://pypi.org/simple pip install -r requirements/tests.txt
git config --global --add safe.directory /Maas-lib
git config --global user.email tmp
git config --global user.name tmp.com

View File

@@ -11,7 +11,7 @@ on:
description: 'ModelScope branch to build from(release/x.xx)'
required: true
image_type:
description: 'The image type to build(base/old/stable/latest)'
description: 'The image type to build(base/old/stable/latest/amd/ascend)'
required: true
modelscope_version:
description: 'ModelScope version to use(x.xx.x)'

26
docker/Dockerfile.amd Normal file
View File

@@ -0,0 +1,26 @@
FROM {base_image}
ARG BASE_IMAGE_TAG={base_image_tag}
LABEL modelscope.base_image="vllm/vllm-openai-rocm:${BASE_IMAGE_TAG}"
# Build-time only (ARG does not persist into the image). Aliyun mirror can lag PyPI (~1h).
ARG PIP_EXTRA_INDEX_URL=https://pypi.org/simple
COPY docker/scripts/modelscope_env_init.sh /usr/local/bin/ms_env_init.sh
ARG CUR_TIME={cur_time}
RUN echo "CUR_TIME=${CUR_TIME}" && echo "BASE_IMAGE_TAG=${BASE_IMAGE_TAG}"
ARG PIP_EXTRA_INDEX_URL=https://pypi.org/simple
RUN export PIP_EXTRA_INDEX_URL="${PIP_EXTRA_INDEX_URL}" && \
pip config set global.index-url https://mirrors.aliyun.com/pypi/simple && \
pip config set install.trusted-host mirrors.aliyun.com && \
cd /tmp && GIT_LFS_SKIP_SMUDGE=1 git clone -b {modelscope_branch} --single-branch https://github.com/modelscope/modelscope.git && \
cd modelscope && pip install --no-cache-dir . -f https://modelscope.oss-cn-beijing.aliyuncs.com/releases/repo.html && \
cd / && rm -fr /tmp/modelscope && pip cache purge
ENV VLLM_USE_MODELSCOPE=True
ENV LMDEPLOY_USE_MODELSCOPE=True
ENV MODELSCOPE_CACHE=/mnt/workspace/.cache/modelscope/hub
SHELL ["/bin/bash", "-c"]

View File

@@ -5,39 +5,54 @@ ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \
PIP_RETRIES=10 \
SOC_VERSION={soc_version} \
CANN_VERSION={cann_version}
# Build-time only (ARG does not persist into the image).
ARG PIP_EXTRA_INDEX_URL=https://pypi.org/simple
SHELL ["/bin/bash", "-c"]
# ---------- System dependencies ----------
RUN rm -f /etc/apt/apt.conf.d/docker-clean && \
find /etc/apt/apt.conf.d -maxdepth 1 -type f | xargs -r grep -l "APT::Update::Post-Invoke\|docker-clean" | xargs -r rm -f && \
apt-get update -y && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
gcc g++ cmake ninja-build libnuma-dev libgl1 libglib2.0-0 libsm6 libxext6 libxrender1 \
wget git curl jq vim build-essential ca-certificates && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
RUN set -eux; \
. /etc/os-release; \
case "${ID,,}" in \
ubuntu) \
rm -f /etc/apt/apt.conf.d/docker-clean; \
find /etc/apt/apt.conf.d -maxdepth 1 -type f | xargs -r grep -l "APT::Update::Post-Invoke\|docker-clean" | xargs -r rm -f; \
apt-get update -y; \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
gcc g++ cmake ninja-build libnuma-dev libgl1 libglib2.0-0 libsm6 libxext6 libxrender1 \
wget git curl jq vim build-essential ca-certificates; \
apt-get clean; \
rm -rf /var/lib/apt/lists/* \
;; \
openeuler) \
yum install -y \
gcc gcc-c++ cmake ninja-build numactl-devel mesa-libGL glib2 libSM libXext libXrender \
wget git curl jq vim make ca-certificates; \
yum clean all; \
rm -rf /var/cache/yum \
;; \
*) \
echo "Unsupported base image OS: ${ID}" >&2; \
exit 1 \
;; \
esac
RUN pip config set global.index-url https://mirrors.aliyun.com/pypi/simple && \
pip config set global.extra-index-url "https://pypi.org/simple" && \
pip config set install.trusted-host mirrors.aliyun.com && \
ARCH=$(uname -m) && \
if [ "$ARCH" = "x86_64" ]; then \
pip config set global.extra-index-url "https://pypi.org/simple https://download.pytorch.org/whl/cpu/"; \
fi
pip config set install.trusted-host mirrors.aliyun.com
{extra_content}
# ---------- Install vllm + vllm-ascend ----------
RUN source /usr/local/Ascend/ascend-toolkit/set_env.sh && \
if [ -f /usr/local/Ascend/nnal/atb/set_env.sh ]; then source /usr/local/Ascend/nnal/atb/set_env.sh; fi && \
git clone --depth 1 --branch v0.18.0 https://github.com/vllm-project/vllm && \
git clone --depth 1 --branch v0.18.0 https://github.com/vllm-project/vllm-ascend.git
git clone --depth 1 --branch {vllm_git_ref} https://github.com/vllm-project/vllm && \
git clone --depth 1 --branch {vllm_ascend_git_ref} https://github.com/vllm-project/vllm-ascend.git
RUN ARCH=$(uname -m) && \
export PIP_EXTRA_INDEX_URL="${PIP_EXTRA_INDEX_URL}" && \
source /usr/local/Ascend/ascend-toolkit/set_env.sh && \
source /usr/local/Ascend/nnal/atb/set_env.sh && \
# Install torch & torch_npu & torchvision
pip install torch==2.9.0 torch_npu==2.9.0.post2 torchvision==0.24.0 && \
pip install torch=={torch_version} torch_npu=={torch_npu_version} torchvision=={torchvision_version} && \
# Install vllm
cd vllm && VLLM_TARGET_DEVICE=empty pip install -v -e . && cd .. && \
# Install vllm-ascend
@@ -46,32 +61,33 @@ RUN ARCH=$(uname -m) && \
# ---------- Clone training-side repositories ----------
RUN git clone --depth 1 --branch {megatron_branch} https://github.com/NVIDIA/Megatron-LM.git /Megatron-LM && \
git clone --depth 1 --branch {mindspeed_branch} https://gitcode.com/Ascend/MindSpeed.git /MindSpeed && \
GIT_LFS_SKIP_SMUDGE=1 git clone --depth 1 -b {swift_branch} --single-branch https://github.com/modelscope/ms-swift.git /ms-swift && \
git clone --depth 1 https://github.com/modelscope/mcore-bridge.git /mcore-bridge
GIT_LFS_SKIP_SMUDGE=1 git clone --depth 1 -b {swift_branch} --single-branch https://github.com/modelscope/ms-swift.git /ms-swift
# ---------- Install training-side repositories ----------
RUN source /usr/local/Ascend/ascend-toolkit/set_env.sh && \
RUN export PIP_EXTRA_INDEX_URL="${PIP_EXTRA_INDEX_URL}" && \
source /usr/local/Ascend/ascend-toolkit/set_env.sh && \
if [ -f /usr/local/Ascend/nnal/atb/set_env.sh ]; then source /usr/local/Ascend/nnal/atb/set_env.sh; fi && \
cd /MindSpeed && pip install --no-cache-dir -e . && \
cd /mcore-bridge && pip install --no-cache-dir -e . && \
pip install --no-cache-dir mcore-bridge -i https://pypi.org/simple/ -U && \
cd /ms-swift && pip install --no-cache-dir -e .
# ---------- Pin torch to the correct version + torch_npu ----------
# x86: must force-install the CPU build from pytorch.org/whl/cpu
# aarch64: PyPI only provides the CPU build, so install it directly from the Aliyun mirror
RUN source /usr/local/Ascend/ascend-toolkit/set_env.sh && \
RUN export PIP_EXTRA_INDEX_URL="${PIP_EXTRA_INDEX_URL}" && \
source /usr/local/Ascend/ascend-toolkit/set_env.sh && \
if [ -f /usr/local/Ascend/nnal/atb/set_env.sh ]; then source /usr/local/Ascend/nnal/atb/set_env.sh; fi && \
ARCH=$(uname -m) && \
if [ "$ARCH" = "x86_64" ]; then \
pip install --no-cache-dir --force-reinstall --no-deps \
--index-url https://download.pytorch.org/whl/cpu \
torch==2.9.0 torchvision==0.24.0 torchaudio==2.9.0; \
torch=={torch_version} torchvision=={torchvision_version} torchaudio=={torchaudio_version}; \
else \
pip install --no-cache-dir --force-reinstall --no-deps \
torch==2.9.0 torchvision==0.24.0 torchaudio==2.9.0; \
torch=={torch_version} torchvision=={torchvision_version} torchaudio=={torchaudio_version}; \
fi && \
pip install --no-cache-dir --force-reinstall --no-deps \
torch_npu==2.9.0.post2 && \
torch_npu=={torch_npu_version} && \
rm -rf /root/.cache/pip
# ---------- Remove CUDA-only dependencies pulled in by vllm (they cause missing libtorch_cuda.so errors on NPU) ----------
@@ -83,7 +99,8 @@ ENV PYTHONPATH=/Megatron-LM:${PYTHONPATH}
# install dependencies
COPY requirements /var/modelscope
RUN pip uninstall ms-swift modelscope -y && pip install --no-cache-dir pip==23.* -U && \
RUN export PIP_EXTRA_INDEX_URL="${PIP_EXTRA_INDEX_URL}" && \
pip uninstall ms-swift modelscope -y && pip install --no-cache-dir pip==23.* -U && \
if [ "$INSTALL_MS_DEPS" = "True" ]; then \
pip install --no-cache-dir omegaconf==2.0.6 && \
pip install 'editdistance==0.8.1' && \
@@ -109,9 +126,11 @@ fi
ARG CUR_TIME={cur_time}
RUN echo $CUR_TIME
RUN pip install --no-cache-dir --no-build-isolation OpenCC
RUN export PIP_EXTRA_INDEX_URL="${PIP_EXTRA_INDEX_URL}" && \
pip install --no-cache-dir --no-build-isolation OpenCC
RUN source /usr/local/Ascend/ascend-toolkit/set_env.sh && \
RUN export PIP_EXTRA_INDEX_URL="${PIP_EXTRA_INDEX_URL}" && \
source /usr/local/Ascend/ascend-toolkit/set_env.sh && \
if [ -f /usr/local/Ascend/nnal/atb/set_env.sh ]; then source /usr/local/Ascend/nnal/atb/set_env.sh; fi && \
pip install --no-cache-dir -U funasr scikit-learn && \
pip install --no-cache-dir -U qwen_vl_utils qwen_omni_utils librosa 'timm>=0.9.0' transformers accelerate peft trl safetensors && \
@@ -126,36 +145,20 @@ RUN source /usr/local/Ascend/ascend-toolkit/set_env.sh && \
pip install --no-cache-dir omegaconf==2.3.0 && \
pip cache purge
# ---------- Reinstall triton-ascend for the selected CANN version ----------
# ---------- Install training and evaluation dependencies ----------
RUN source /usr/local/Ascend/ascend-toolkit/set_env.sh && \
if [ -f /usr/local/Ascend/nnal/atb/set_env.sh ]; then source /usr/local/Ascend/nnal/atb/set_env.sh; fi && \
TORCH_DEVICE_BACKEND_AUTOLOAD=0 pip install --no-cache-dir "deepspeed<0.19" ray liger_kernel pre-commit -U && \
pip cache purge
# ---------- Install triton-ascend ----------
RUN set -eux; \
export PIP_EXTRA_INDEX_URL="${PIP_EXTRA_INDEX_URL}"; \
pip uninstall -y triton || true; \
pip uninstall -y triton-ascend || true; \
case "${CANN_VERSION}" in \
8.5.*) \
pip install --no-cache-dir --force-reinstall triton-ascend==3.2.0; \
;; \
9.0.0) \
PY_ABI="cp$(python -c 'import sys; print(f"{sys.version_info.major}{sys.version_info.minor}")')"; \
case "${PY_ABI}" in \
cp310|cp311|cp312|cp313) ;; \
*) echo "Unsupported Python ABI for triton-ascend 3.2.1: ${PY_ABI}" >&2; exit 1 ;; \
esac; \
ARCH="$(uname -m)"; \
case "${ARCH}" in \
aarch64|x86_64) ;; \
*) echo "Unsupported architecture for triton-ascend 3.2.1: ${ARCH}" >&2; exit 1 ;; \
esac; \
WHEEL_NAME="triton_ascend-3.2.1-${PY_ABI}-${PY_ABI}-manylinux_2_27_${ARCH}.manylinux_2_28_${ARCH}.whl"; \
WHEEL_PATH="/tmp/${WHEEL_NAME}"; \
curl -fL "https://gitcode.com/Ascend/triton-ascend/releases/download/v3.2.1/${WHEEL_NAME}" -o "${WHEEL_PATH}"; \
pip install --no-cache-dir --force-reinstall "${WHEEL_PATH}"; \
rm -f "${WHEEL_PATH}"; \
;; \
*) \
echo "Unsupported CANN_VERSION for triton-ascend install: ${CANN_VERSION}" >&2; \
exit 1; \
;; \
esac
pip install --no-cache-dir --force-reinstall \
triton-ascend=={triton_ascend_version} \
--extra-index-url=https://triton-ascend.osinfra.cn/pypi/simple
RUN echo 'source /usr/local/Ascend/ascend-toolkit/set_env.sh' >> /root/.bashrc && \
echo '[ -f /usr/local/Ascend/nnal/atb/set_env.sh ] && source /usr/local/Ascend/nnal/atb/set_env.sh' >> /root/.bashrc && \

View File

@@ -3,6 +3,8 @@ FROM {base_image}
ARG DEBIAN_FRONTEND=noninteractive
ENV TZ=Asia/Shanghai
ENV arch=x86_64
# Build-time only (ARG does not persist into the image). Aliyun mirror can lag PyPI (~1h).
ARG PIP_EXTRA_INDEX_URL=https://pypi.org/simple
COPY docker/scripts/modelscope_env_init.sh /usr/local/bin/ms_env_init.sh
RUN apt-get update && \
@@ -21,7 +23,9 @@ ARG IMAGE_TYPE={image_type}
# install dependencies
COPY requirements /var/modelscope
RUN pip uninstall ms-swift modelscope -y && pip --no-cache-dir install pip==23.* -U && \
ARG PIP_EXTRA_INDEX_URL=https://pypi.org/simple
RUN export PIP_EXTRA_INDEX_URL="${PIP_EXTRA_INDEX_URL}" && \
pip uninstall ms-swift modelscope -y && pip --no-cache-dir install pip==23.* -U && \
if [ "$INSTALL_MS_DEPS" = "True" ]; then \
pip --no-cache-dir install omegaconf==2.0.6 && \
pip install 'editdistance==0.8.1' && \
@@ -34,7 +38,7 @@ if [ "$INSTALL_MS_DEPS" = "True" ]; then \
pip install --no-cache-dir 'scipy' && \
pip install --no-cache-dir funtextprocessing typeguard==2.13.3 scikit-learn -f https://modelscope.oss-cn-beijing.aliyuncs.com/releases/repo.html && \
pip install --no-cache-dir 'decord>=0.6.0' mpi4py paint_ldm ipykernel fasttext -f https://modelscope.oss-cn-beijing.aliyuncs.com/releases/repo.html && \
pip install --no-cache-dir ipywidgets && \
pip install --no-cache-dir ipywidgets jupyter_core nbconvert nbclient && \
pip install --no-cache-dir 'blobfile>=1.0.5' && \
pip uninstall MinDAEC -y && \
pip install https://modelscope.oss-cn-beijing.aliyuncs.com/releases/dependencies/MinDAEC-0.0.2-py3-none-any.whl && \
@@ -47,7 +51,9 @@ fi
ARG CUR_TIME={cur_time}
RUN echo $CUR_TIME
RUN bash /tmp/install.sh {version_args} && \
ARG PIP_EXTRA_INDEX_URL=https://pypi.org/simple
RUN export PIP_EXTRA_INDEX_URL="${PIP_EXTRA_INDEX_URL}" && \
bash /tmp/install.sh {version_args} && \
pip install --no-cache-dir -U funasr scikit-learn && \
pip install --no-cache-dir -U qwen_vl_utils qwen_omni_utils librosa timm transformers accelerate peft trl safetensors && \
cd /tmp && GIT_LFS_SKIP_SMUDGE=1 git clone -b {swift_branch} --single-branch https://github.com/modelscope/ms-swift.git && \
@@ -63,23 +69,17 @@ RUN bash /tmp/install.sh {version_args} && \
pip install --no-cache-dir transformers diffusers 'timm>=0.9.0' && pip cache purge; \
pip install --no-cache-dir omegaconf==2.3.0 && pip cache purge; \
pip config set global.index-url https://mirrors.aliyun.com/pypi/simple && \
pip config set global.extra-index-url https://pypi.org/simple && \
pip config set install.trusted-host mirrors.aliyun.com && \
cp /tmp/resources/ubuntu2204.aliyun /etc/apt/sources.list
RUN if [ "$IMAGE_TYPE" = "gpu" ]; then \
pip install --no-cache-dir math_verify "datasets<4.8.5" "gradio<5.33" "deepspeed<0.19" ray -U && \
ARG PIP_EXTRA_INDEX_URL=https://pypi.org/simple
RUN export PIP_EXTRA_INDEX_URL="${PIP_EXTRA_INDEX_URL}" && \
if [ "$IMAGE_TYPE" = "gpu" ]; then \
pip install --no-cache-dir math_verify "gradio<5.33" "deepspeed<0.19" ray -U && \
pip install --no-cache-dir mcore-bridge -i https://pypi.org/simple/ -U && \
pip install --no-cache-dir pybind11 liger_kernel wandb swanlab nvitop pre-commit "transformers<5.15" "trl<1.0" "peft<0.21" huggingface-hub -U && \
pip install git+https://github.com/NVIDIA/TransformerEngine.git@stable --no-build-isolation; \
pip install git+https://github.com/deepseek-ai/DeepGEMM.git@v2.1.1.post3 --no-build-isolation; \
pip install -U flash-linear-attention --no-build-isolation; \
pip install -U git+https://github.com/Dao-AILab/causal-conv1d --no-build-isolation; \
pip install git+https://github.com/Dao-AILab/fast-hadamard-transform --no-build-isolation; \
pip install git+https://github.com/NVIDIA-NeMo/Emerging-Optimizers.git@v0.3.0; \
mv /usr/local/lib/python3.12/site-packages/tilelang/lib/libcudart_stub.so /usr/local/lib/python3.12/site-packages/tilelang/lib/libcudart_stub.so.bak; \
ln -s /usr/local/cuda-13.0/targets/x86_64-linux/lib/libcudart.so /usr/local/lib/python3.12/site-packages/tilelang/lib/libcudart_stub.so; \
pip install --no-cache-dir liger_kernel wandb swanlab nvitop pre-commit "transformers" "trl<1.0" "peft<0.21" huggingface-hub -U && \
pip install --no-cache-dir --no-build-isolation "transformer_engine[pytorch]==2.16.0"; \
cd /tmp && GIT_LFS_SKIP_SMUDGE=1 git clone https://github.com/NVIDIA/apex && \
cd apex && pip install -v --disable-pip-version-check --no-cache-dir --no-build-isolation --config-settings "--build-option=--cpp_ext" --config-settings "--build-option=--cuda_ext" ./ && \
cd / && rm -fr /tmp/apex && pip cache purge; \

View File

@@ -10,7 +10,8 @@ ms-swift Ascend images provide a ready-to-use ms-swift environment for Huawei As
- Build template: `docker/Dockerfile.ascend`
- Build entrypoint: `docker/build_image.py --image_type ascend`
- Default base image: `quay.io/ascend/cann:8.5.1-a3-ubuntu22.04-py3.11`
- Default output tag: `${DOCKER_REGISTRY}:main-A3-py311-CANN8.5.1-ubuntu22.04-<arch>`
- Supported base OSes: Ubuntu and openEuler, selected from the CANN base-image tag
- Default output tag: `${DOCKER_REGISTRY}:main-cann8.5.1-torch_npu2.9.0.post2-a3-ubuntu22.04-py3.11-<arch>`
- Ascend runtime environment is sourced from `/usr/local/Ascend/ascend-toolkit/set_env.sh`
- If available, NNAL/ATB runtime is sourced from `/usr/local/Ascend/nnal/atb/set_env.sh`
@@ -22,45 +23,46 @@ The Ascend Dockerfile installs and configures:
| --- | --- |
| CANN | inherited from the selected `quay.io/ascend/cann` base image |
| Python | inherited from the base image tag, for example `py3.11` |
| PyTorch | `torch==2.9.0` |
| torch-npu | `torch_npu==2.9.0.post2` |
| torchvision / torchaudio | `torchvision==0.24.0`, `torchaudio==2.9.0` |
| vLLM | source install from `vllm-project/vllm`, default branch `v0.18.0` |
| vLLM Ascend | source install from `vllm-project/vllm-ascend`, default branch `v0.18.0` |
| PyTorch | `torch==2.9.0` by default; configurable with `--torch_version` |
| torch-npu | `torch_npu==2.9.0.post2` by default; configurable with `--torch_npu_version` |
| torchvision / torchaudio | `torchvision==0.24.0`, `torchaudio==2.9.0` by default; pass both explicitly when overriding `--torch_version` |
| vLLM | source install from `vllm-project/vllm`, default `0.18.0`; configurable with `--vllm_version` |
| vLLM Ascend | source install from `vllm-project/vllm-ascend`, default `0.18.0`; configurable with `--vllm_ascend_version` |
| Megatron-LM | source checkout, default branch `v0.15.3` |
| MindSpeed | source checkout, default branch `core_r0.15.3` |
| mcore-bridge | source checkout from `modelscope/mcore-bridge` |
| mcore-bridge | latest release from PyPI |
| ms-swift | source checkout from `modelscope/ms-swift`, default branch `main` |
| ModelScope | source checkout from `modelscope/modelscope`, default branch `master` |
| triton-ascend | `3.2.0` for CANN `8.5.*`; local wheel install of `3.2.1` for CANN `9.0.0` |
| triton-ascend | CANN `8.5.*` defaults to `3.2.0`; CANN `9.0.*` defaults to `3.2.1`; configurable with `--triton_ascend_version` and installed from the Triton Ascend PyPI index |
## Supported Tag Format
Images built by `docker/build_image.py --image_type ascend` use this tag format:
```text
${DOCKER_REGISTRY}:<swift-branch>-<atlas-hardware>-<python-tag>-<cann-version-tag>-<os-tag>-<arch>
${DOCKER_REGISTRY}:<swift-branch>-<cann-version-tag>-torch_npu<torch-npu-version>-<atlas-hardware>-<os-tag>-<python-tag>-<arch>
```
| Field | Example | Description |
| --- | --- | --- |
| `swift-branch` | `main` | ms-swift branch used during image build |
| `atlas-hardware` | `A2`, `A3`, `300I`, `A5` | Derived from `--soc_version` |
| `python-tag` | `py311` | Derived from `--python_version` |
| `cann-version-tag` | `CANN8.5.1`, `CANN9.0.0` | Parsed from the CANN base image tag |
| `os-tag` | `ubuntu22.04` | Parsed from the CANN base image tag |
| `arch` | `arm`, `x86` | Derived from host architecture or `--arch` |
| `cann-version-tag` | `cann8.5.1`, `cann9.0.0` | Parsed from the CANN base image tag |
| `torch-npu-version` | `2.9.0.post2` | From `--torch_npu_version`; defaults to `2.9.0.post2` |
| `atlas-hardware` | `a2`, `a3`, `300i`, `a5` | Derived from `--soc_version` |
| `os-tag` | `ubuntu22.04`, `openeuler24.03` | Parsed from the CANN base-image tag; prevents tags for different OSes from colliding |
| `python-tag` | `py3.11` | Parsed from the CANN base image tag |
| `arch` | `aarch64`, `x86_64` | Derived from host architecture or `--arch` |
Default example on an ARM64 host:
```text
${DOCKER_REGISTRY}:main-A3-py311-CANN8.5.1-ubuntu22.04-arm
${DOCKER_REGISTRY}:main-cann8.5.1-torch_npu2.9.0.post2-a3-ubuntu22.04-py3.11-aarch64
```
A2 / CANN 9.0.0 example:
```text
${DOCKER_REGISTRY}:main-A2-py311-CANN9.0.0-ubuntu22.04-arm
${DOCKER_REGISTRY}:main-cann9.0.0-torch_npu2.9.0.post2-a2-ubuntu22.04-py3.11-aarch64
```
## Build Locally
@@ -85,6 +87,39 @@ python docker/build_image.py \
--soc_version ascend910b1
```
Build an openEuler image. The system-dependency layer automatically uses `yum`; Ubuntu images continue to use `apt-get`.
```bash
python docker/build_image.py \
--image_type ascend \
--base_image quay.io/ascend/cann:8.5.1-a3-openeuler24.03-py3.11 \
--soc_version ascend910_9391
```
Override the PyTorch stack. `--torch_version` must match the base version of
`--torch_npu_version`; when overriding PyTorch, pass its matching torchvision
and torchaudio versions explicitly.
```bash
python docker/build_image.py \
--image_type ascend \
--torch_version 2.9.0 \
--torch_npu_version 2.9.0.post2 \
--torchvision_version 0.24.0 \
--torchaudio_version 2.9.0
```
Override the vLLM stack or triton-ascend. The vLLM version arguments select
the matching Git tag, for example `0.18.0` selects `v0.18.0`.
```bash
python docker/build_image.py \
--image_type ascend \
--vllm_version 0.18.0 \
--vllm_ascend_version 0.18.0 \
--triton_ascend_version 3.2.1
```
Override Megatron or MindSpeed source branches when needed:
```bash
@@ -94,11 +129,11 @@ python docker/build_image.py \
--mindspeed_branch core_r0.15.3
```
For slow networks, Linux hosts can use Docker host networking after the root `Dockerfile` is generated:
To run the rendered Dockerfile manually, use:
```bash
docker build --network host \
-t ${DOCKER_REGISTRY}:main-A2-py311-CANN9.0.0-ubuntu22.04-arm \
docker build \
-t ${DOCKER_REGISTRY}:main-cann9.0.0-torch_npu2.9.0.post2-a2-ubuntu22.04-py3.11-aarch64 \
-f Dockerfile .
```
@@ -119,7 +154,7 @@ docker run --rm -it \
-v /usr/local/Ascend/driver/version.info:/usr/local/Ascend/driver/version.info \
-v /etc/ascend_install.info:/etc/ascend_install.info \
-v /mnt/workspace:/mnt/workspace \
${DOCKER_REGISTRY}:main-A2-py311-CANN9.0.0-ubuntu22.04-arm \
${DOCKER_REGISTRY}:main-cann9.0.0-torch_npu2.9.0.post2-a2-ubuntu22.04-py3.11-aarch64 \
bash
```
@@ -147,7 +182,8 @@ pip show ms-swift modelscope torch-npu triton-ascend
## Notes
- CANN, firmware, and driver versions must be compatible with each other.
- CANN `8.5.*` and CANN `9.0.0` use different `triton-ascend` install paths in this Dockerfile.
- Ubuntu base images install system dependencies through `apt-get`; openEuler base images install the corresponding RPM packages through `yum`.
- `triton-ascend` is installed from `https://triton-ascend.osinfra.cn/pypi/simple`; select a version compatible with the chosen CANN, Python, and architecture.
- The image is intended for Ascend NPU ms-swift workflows. CUDA-only packages pulled in by dependencies are removed when they conflict with NPU runtime libraries.
- Use a fixed image tag for production jobs instead of relying on a moving branch name.

View File

@@ -10,7 +10,8 @@ ms-swift Ascend 镜像面向华为昇腾 Atlas NPU提供可直接使用的 ms
- 构建模板:`docker/Dockerfile.ascend`
- 构建入口:`docker/build_image.py --image_type ascend`
- 默认基础镜像:`quay.io/ascend/cann:8.5.1-a3-ubuntu22.04-py3.11`
- 默认输出 tag`${DOCKER_REGISTRY}:main-A3-py311-CANN8.5.1-ubuntu22.04-<arch>`
- 支持的基础 OSUbuntu 和 openEuler由 CANN 基础镜像 tag 选择
- 默认输出 tag`${DOCKER_REGISTRY}:main-cann8.5.1-torch_npu2.9.0.post2-a3-ubuntu22.04-py3.11-<arch>`
- Ascend runtime 环境来自 `/usr/local/Ascend/ascend-toolkit/set_env.sh`
- 如果镜像内存在 NNAL/ATB则会加载 `/usr/local/Ascend/nnal/atb/set_env.sh`
@@ -22,45 +23,46 @@ Ascend Dockerfile 会安装和配置:
| --- | --- |
| CANN | 继承自选定的 `quay.io/ascend/cann` 基础镜像 |
| Python | 继承自基础镜像 tag例如 `py3.11` |
| PyTorch | `torch==2.9.0` |
| torch-npu | `torch_npu==2.9.0.post2` |
| torchvision / torchaudio | `torchvision==0.24.0``torchaudio==2.9.0` |
| vLLM | 从 `vllm-project/vllm` 源码安装,默认分支 `v0.18.0` |
| vLLM Ascend | 从 `vllm-project/vllm-ascend` 源码安装,默认分支 `v0.18.0` |
| PyTorch | 默认 `torch==2.9.0`;可通过 `--torch_version` 配置 |
| torch-npu | 默认 `torch_npu==2.9.0.post2`;可通过 `--torch_npu_version` 配置 |
| torchvision / torchaudio | 默认 `torchvision==0.24.0``torchaudio==2.9.0`;覆盖 `--torch_version` 时必须同时显式传入两者 |
| vLLM | 从 `vllm-project/vllm` 源码安装,默认 `0.18.0`;可通过 `--vllm_version` 配置 |
| vLLM Ascend | 从 `vllm-project/vllm-ascend` 源码安装,默认 `0.18.0`;可通过 `--vllm_ascend_version` 配置 |
| Megatron-LM | 源码 checkout默认分支 `v0.15.3` |
| MindSpeed | 源码 checkout默认分支 `core_r0.15.3` |
| mcore-bridge | 来自 `modelscope/mcore-bridge` 的源码 checkout |
| mcore-bridge | PyPI 上的最新发布版 |
| ms-swift | 来自 `modelscope/ms-swift` 的源码 checkout默认分支 `main` |
| ModelScope | 来自 `modelscope/modelscope` 的源码 checkout默认分支 `master` |
| triton-ascend | CANN `8.5.*` 安装 `3.2.0`CANN `9.0.0` 下载并本地安装 `3.2.1` wheel |
| triton-ascend | CANN `8.5.*` 默认 `3.2.0`CANN `9.0.*` 默认 `3.2.1`;可通过 `--triton_ascend_version` 配置,并从 Triton Ascend PyPI 源安装 |
## 支持的 Tag 格式
通过 `docker/build_image.py --image_type ascend` 构建的镜像使用以下 tag 格式:
```text
${DOCKER_REGISTRY}:<swift-branch>-<atlas-hardware>-<python-tag>-<cann-version-tag>-<os-tag>-<arch>
${DOCKER_REGISTRY}:<swift-branch>-<cann-version-tag>-torch_npu<torch-npu-version>-<atlas-hardware>-<os-tag>-<python-tag>-<arch>
```
| 字段 | 示例 | 说明 |
| --- | --- | --- |
| `swift-branch` | `main` | 构建镜像时使用的 ms-swift 分支 |
| `atlas-hardware` | `A2``A3``300I``A5` | 从 `--soc_version` 推导 |
| `python-tag` | `py311` | `--python_version` 推导 |
| `cann-version-tag` | `CANN8.5.1``CANN9.0.0` | 从 CANN 基础镜像 tag 解析 |
| `os-tag` | `ubuntu22.04` | 从 CANN 基础镜像 tag 解析 |
| `arch` | `arm``x86` | 从宿主机架构或 `--arch` 推导 |
| `cann-version-tag` | `cann8.5.1``cann9.0.0` | 从 CANN 基础镜像 tag 解析 |
| `torch-npu-version` | `2.9.0.post2` | 来自 `--torch_npu_version`,默认 `2.9.0.post2` |
| `atlas-hardware` | `a2``a3``300i``a5` | 从 `--soc_version` 推导 |
| `os-tag` | `ubuntu22.04``openeuler24.03` | 从 CANN 基础镜像 tag 解析;避免不同 OS 的镜像 tag 冲突 |
| `python-tag` | `py3.11` | 从 CANN 基础镜像 tag 解析 |
| `arch` | `aarch64``x86_64` | 从宿主机架构或 `--arch` 推导 |
ARM64 宿主机上的默认示例:
```text
${DOCKER_REGISTRY}:main-A3-py311-CANN8.5.1-ubuntu22.04-arm
${DOCKER_REGISTRY}:main-cann8.5.1-torch_npu2.9.0.post2-a3-ubuntu22.04-py3.11-aarch64
```
A2 / CANN 9.0.0 示例:
```text
${DOCKER_REGISTRY}:main-A2-py311-CANN9.0.0-ubuntu22.04-arm
${DOCKER_REGISTRY}:main-cann9.0.0-torch_npu2.9.0.post2-a2-ubuntu22.04-py3.11-aarch64
```
## 本地构建
@@ -85,6 +87,36 @@ python docker/build_image.py \
--soc_version ascend910b1
```
构建 openEuler 镜像。系统依赖层会自动使用 `yum`Ubuntu 镜像继续使用 `apt-get`
```bash
python docker/build_image.py \
--image_type ascend \
--base_image quay.io/ascend/cann:8.5.1-a3-openeuler24.03-py3.11 \
--soc_version ascend910_9391
```
覆盖 PyTorch 版本组。`--torch_version` 必须与 `--torch_npu_version` 的基础版本一致;覆盖 PyTorch 时,必须显式传入匹配的 torchvision 和 torchaudio 版本:
```bash
python docker/build_image.py \
--image_type ascend \
--torch_version 2.9.0 \
--torch_npu_version 2.9.0.post2 \
--torchvision_version 0.24.0 \
--torchaudio_version 2.9.0
```
覆盖 vLLM 版本组或 triton-ascend。vLLM 版本参数会选择对应 Git tag例如 `0.18.0` 会选择 `v0.18.0`
```bash
python docker/build_image.py \
--image_type ascend \
--vllm_version 0.18.0 \
--vllm_ascend_version 0.18.0 \
--triton_ascend_version 3.2.1
```
需要时可以覆盖 Megatron 或 MindSpeed 源码分支:
```bash
@@ -94,11 +126,11 @@ python docker/build_image.py \
--mindspeed_branch core_r0.15.3
```
果构建时网络较慢Linux 宿主机可以在根目录 `Dockerfile` 生成后使用 host network 构建
需手工构建生成后的根目录 `Dockerfile`,可使用
```bash
docker build --network host \
-t ${DOCKER_REGISTRY}:main-A2-py311-CANN9.0.0-ubuntu22.04-arm \
docker build \
-t ${DOCKER_REGISTRY}:main-cann9.0.0-torch_npu2.9.0.post2-a2-ubuntu22.04-py3.11-aarch64 \
-f Dockerfile .
```
@@ -119,7 +151,7 @@ docker run --rm -it \
-v /usr/local/Ascend/driver/version.info:/usr/local/Ascend/driver/version.info \
-v /etc/ascend_install.info:/etc/ascend_install.info \
-v /mnt/workspace:/mnt/workspace \
${DOCKER_REGISTRY}:main-A2-py311-CANN9.0.0-ubuntu22.04-arm \
${DOCKER_REGISTRY}:main-cann9.0.0-torch_npu2.9.0.post2-a2-ubuntu22.04-py3.11-aarch64 \
bash
```
@@ -147,7 +179,8 @@ pip show ms-swift modelscope torch-npu triton-ascend
## 注意事项
- CANN、firmware 和 driver 版本必须互相兼容。
- 这个 Dockerfile 对 CANN `8.5.*` 和 CANN `9.0.0` 使用不同的 `triton-ascend` 安装路径
- Ubuntu 基础镜像通过 `apt-get` 安装系统依赖openEuler 基础镜像通过 `yum` 安装对应 RPM 包
- `triton-ascend``https://triton-ascend.osinfra.cn/pypi/simple` 安装;请选择与 CANN、Python 和架构兼容的版本。
- 该镜像面向 Ascend NPU 上的 ms-swift 工作流。依赖安装过程中引入且与 NPU runtime 冲突的 CUDA-only 包会被移除。
- 生产任务建议使用固定镜像 tag不要依赖浮动分支名。

View File

@@ -3,14 +3,27 @@ import os
import platform
import re
import subprocess
import urllib.error
import urllib.request
from copy import copy
from datetime import datetime
from typing import Any
from typing import Any, List, Optional
import json
docker_registry = os.environ['DOCKER_REGISTRY']
assert docker_registry, 'You must pass a valid DOCKER_REGISTRY'
timestamp = datetime.now()
formatted_time = timestamp.strftime('%Y%m%d%H%M%S')
VLLM_ROCM_REPO = 'vllm/vllm-openai-rocm'
_FLOATING_ROCM_TAGS = frozenset({
'latest',
'latest-base',
'nightly',
'base-nightly',
})
_VERSION_TAG_PATTERN = re.compile(r'^v\d+(?:\.\d+)*$')
_NIGHTLY_HASH_PATTERN = re.compile(r'^(?:base-)?nightly-[0-9a-f]{7,40}$')
class Builder:
@@ -359,7 +372,8 @@ class StableGPUImageBuilder(Builder):
extra_content = extra_content.replace('{python_version}',
self.args.python_version)
extra_content += """
RUN pip install --no-cache-dir -U icecream soundfile pybind11 py-spy
RUN export PIP_EXTRA_INDEX_URL=https://pypi.org/simple && \
pip install --no-cache-dir -U icecream soundfile pybind11 py-spy
"""
version_args = (
f'{self.args.torch_version} {self.args.torchvision_version} {self.args.torchaudio_version} '
@@ -434,7 +448,8 @@ class LatestGPUImageBuilder(StableGPUImageBuilder):
extra_content = extra_content.replace('{python_version}',
self.args.python_version)
extra_content += """
RUN pip install --no-cache-dir -U icecream soundfile pybind11 py-spy
RUN export PIP_EXTRA_INDEX_URL=https://pypi.org/simple && \
pip install --no-cache-dir -U icecream soundfile pybind11 py-spy
"""
version_args = (
f'{self.args.torch_version} {self.args.torchvision_version} {self.args.torchaudio_version} '
@@ -481,22 +496,399 @@ RUN pip install --no-cache-dir -U icecream soundfile pybind11 py-spy
return self.run_cmd('docker', 'push', image_tag2)
class AmdImageBuilder(Builder):
"""Build ModelScope image on top of vllm/vllm-openai-rocm."""
@staticmethod
def _is_specific_release_tag(tag: str) -> bool:
tag = tag.strip()
if not tag or tag.lower() in _FLOATING_ROCM_TAGS:
return False
if tag.endswith('-base'):
return False
if _NIGHTLY_HASH_PATTERN.fullmatch(tag):
return False
return bool(_VERSION_TAG_PATTERN.fullmatch(tag))
@staticmethod
def _image_digest(tag_info: dict) -> Optional[str]:
digest = tag_info.get('digest')
if digest:
return digest
for image in tag_info.get('images') or []:
digest = image.get('digest')
if digest:
return digest
return None
@classmethod
def _fetch_rocm_tags(cls, page_size: int = 100) -> List[dict]:
tags: List[dict] = []
url = (f'https://hub.docker.com/v2/repositories/{VLLM_ROCM_REPO}/tags'
f'?page_size={page_size}&ordering=-last_updated')
while url:
req = urllib.request.Request(
url, headers={'User-Agent': 'modelscope-docker-builder'})
try:
with urllib.request.urlopen(req, timeout=60) as resp:
payload = json.load(resp)
except (urllib.error.URLError, json.JSONDecodeError) as exc:
raise RuntimeError(
f'Failed to query Docker Hub tags for {VLLM_ROCM_REPO}: '
f'{exc}') from exc
tags.extend(payload.get('results') or [])
url = payload.get('next')
# Only scan the first few pages; release tags are near the top.
if len(tags) >= 300:
break
if not tags:
raise RuntimeError(
f'No tags returned from Docker Hub for {VLLM_ROCM_REPO}')
return tags
@classmethod
def resolve_latest_rocm_tag(cls) -> str:
"""Resolve the newest concrete release tag for vllm-openai-rocm.
Preference order:
1. Semver tag (vX.Y.Z) that shares digest with floating ``latest``
2. Newest semver tag by Docker Hub ``last_updated``
"""
tags = cls._fetch_rocm_tags()
by_name = {item['name']: item for item in tags if item.get('name')}
release_tags = [
item for item in tags
if cls._is_specific_release_tag(item.get('name', ''))
]
latest_info = by_name.get('latest')
latest_digest = cls._image_digest(latest_info) if latest_info else None
if latest_digest:
matched = [
item for item in release_tags
if cls._image_digest(item) == latest_digest
]
if matched:
# Prefer the first match in last_updated order from API.
chosen = matched[0]['name']
print(
f'Resolved {VLLM_ROCM_REPO} latest digest to release tag: '
f'{chosen}')
return chosen
if not release_tags:
raise RuntimeError(
f'No concrete release tags found for {VLLM_ROCM_REPO}')
chosen = release_tags[0]['name']
print(f'Resolved newest {VLLM_ROCM_REPO} release tag: {chosen}')
return chosen
def init_args(self, args: Any) -> Any:
# Auto-discover from Docker Hub unless an explicit override is given.
override = getattr(args, 'base_image_tag', None)
if override and str(override).strip() and str(
override).strip().lower() not in {'auto', 'latest'}:
args.base_image_tag = str(override).strip()
if not self._is_specific_release_tag(args.base_image_tag):
raise ValueError(
'base_image_tag override must be a concrete release tag '
f'(e.g. v0.25.1), got: {args.base_image_tag}')
print(f'Using override AMD ROCm base image tag: '
f'{args.base_image_tag}')
else:
args.base_image_tag = self.resolve_latest_rocm_tag()
if not args.base_image:
args.base_image = f'{VLLM_ROCM_REPO}:{args.base_image_tag}'
if not args.cuda_version:
args.cuda_version = '0.0.0'
return args
@staticmethod
def _sanitize_tag(tag: str) -> str:
return re.sub(r'[^A-Za-z0-9._-]+', '-', tag)
@staticmethod
def _normalize_version(version: str) -> str:
version = version.strip().lstrip('vV')
version = version.split('+')[0].split(' ')[0]
return re.sub(r'[^0-9A-Za-z._-]+', '', version)
@staticmethod
def _python_tag_from_version(version: str) -> str:
parts = version.strip().split('.')
if len(parts) >= 2 and parts[0].isdigit() and parts[1].isdigit():
return f'py{parts[0]}{parts[1]}'
return f'py{re.sub(r"[^0-9]", "", version)}'
@classmethod
def _run_capture(cls, *cmd: str) -> subprocess.CompletedProcess:
return subprocess.run(
list(cmd), capture_output=True, text=True, check=False)
@classmethod
def _probe_via_entrypoint(cls, base_image: str) -> dict:
"""Read versions with docker run --entrypoint (no GPU required)."""
# Keep this script compact: it runs inside the base image via python -c.
script = (
'import json,os,pathlib,subprocess,sys\n'
'info={"python":"%d.%d.%d"%sys.version_info[:3]}\n'
'try:\n'
' import torch\n'
' info["torch"]=torch.__version__\n'
' hip=getattr(torch.version,"hip",None)\n'
' if hip: info["torch_hip"]=hip\n'
'except Exception as e:\n'
' info["torch_error"]=str(e)\n'
'for p in ("/opt/rocm/.info/version","/opt/rocm/.info/version-dev"):\n'
' f=pathlib.Path(p)\n'
' if f.is_file():\n'
' info["rocm_file"]=f.read_text().strip().splitlines()[0]\n'
' break\n'
'for k in ("ROCM_VERSION","HIP_VERSION","TORCH_VERSION"):\n'
' if os.environ.get(k): info[k.lower()]=os.environ[k]\n'
'def _dpkg_ver(*names):\n'
' for n in names:\n'
' try:\n'
' r=subprocess.run(["dpkg-query","-W","-f=${Version}",n],'
'capture_output=True,text=True)\n'
' if r.returncode==0 and r.stdout.strip():\n'
' return r.stdout.strip()\n'
' except Exception:\n'
' pass\n'
' return None\n'
'def _dpkg_scan(prefixes):\n'
' try:\n'
' r=subprocess.run(["dpkg-query","-W","-f=${Package}\\t${Version}\\n"],'
'capture_output=True,text=True)\n'
' except Exception:\n'
' return {}\n'
' found={}\n'
' for line in (r.stdout or "").splitlines():\n'
' if "\\t" not in line: continue\n'
' pkg,ver=line.split("\\t",1)\n'
' for pref in prefixes:\n'
' if pkg==pref or pkg.startswith(pref+"-"):\n'
' found.setdefault(pref,ver)\n'
' return found\n'
'pkgs=_dpkg_scan(("rccl","miopen"))\n'
'info["system.library.rccl"]=_dpkg_ver("rccl") or pkgs.get("rccl")\n'
'info["system.library.miopen"]=('
'_dpkg_ver("miopen-hip","miopen") or pkgs.get("miopen"))\n'
'print(json.dumps(info))\n')
for py in ('python3', 'python'):
result = cls._run_capture('docker', 'run', '--rm', '--network',
'none', '--entrypoint', py, base_image,
'-c', script)
if result.returncode == 0 and result.stdout.strip():
try:
return json.loads(result.stdout.strip().splitlines()[-1])
except json.JSONDecodeError:
continue
return {}
@classmethod
def _probe_via_history(cls, base_image: str) -> dict:
"""Parse build ARGs from docker history (no container start)."""
result = cls._run_capture('docker', 'history', '--no-trunc',
'--format', '{{.CreatedBy}}', base_image)
if result.returncode != 0:
return {}
text = result.stdout
info = {}
for key, pattern in (
('rocm', r'ROCM_VERSION=([0-9]+(?:\.[0-9]+)*)'),
('python', r'PYTHON_VERSION=([0-9]+(?:\.[0-9]+)*)'),
('ubuntu',
r'org\.opencontainers\.image\.version=([0-9]+(?:\.[0-9]+)*)'),
):
matches = re.findall(pattern, text)
if matches:
# docker history lists newest layers first.
info[key] = matches[0]
return info
@classmethod
def _probe_via_create_cp(cls, base_image: str) -> dict:
"""Copy version files out of a created (not started) container."""
import tempfile
create = cls._run_capture('docker', 'create', base_image)
if create.returncode != 0:
return {}
cid = create.stdout.strip()
info = {}
try:
with tempfile.TemporaryDirectory() as tmp:
dest = os.path.join(tmp, 'version')
for src in ('/opt/rocm/.info/version',
'/opt/rocm/.info/version-dev'):
result = cls._run_capture('docker', 'cp', f'{cid}:{src}',
dest)
if result.returncode == 0 and os.path.isfile(dest):
with open(dest, 'r', encoding='utf-8') as f:
line = f.read().strip().splitlines()
if line:
info['rocm_file'] = line[0].strip()
break
finally:
cls._run_capture('docker', 'rm', '-f', cid)
return info
@classmethod
def probe_base_image_versions(cls, base_image: str) -> dict:
"""Discover rocm/python/torch without needing AMD GPU.
Methods (in order):
1. docker run --entrypoint python -c ... (CPU-only, no --device)
2. docker history --no-trunc parse ROCM_VERSION/PYTHON_VERSION
3. docker create + docker cp /opt/rocm/.info/version
"""
probed = {}
entry = cls._probe_via_entrypoint(base_image)
history = cls._probe_via_history(base_image)
copied = cls._probe_via_create_cp(base_image)
probed.update(history)
probed.update(copied)
probed.update(entry)
rocm = (
probed.get('rocm_file') or probed.get('rocm_version')
or probed.get('rocm') or probed.get('torch_hip')
or probed.get('hip_version'))
python_ver = probed.get('python')
torch_ver = probed.get('torch') or probed.get('torch_version')
ubuntu_ver = probed.get('ubuntu')
# Keep dpkg package versions as-is (may contain '~', e.g. 2.27.7.70201-81~22.04).
rccl_ver = probed.get('system.library.rccl')
miopen_ver = probed.get('system.library.miopen')
versions = {
'rocm': cls._normalize_version(rocm) if rocm else None,
'python':
cls._normalize_version(python_ver) if python_ver else None,
'torch': cls._normalize_version(torch_ver) if torch_ver else None,
'ubuntu':
cls._normalize_version(ubuntu_ver) if ubuntu_ver else None,
'system.library.rccl': rccl_ver or None,
'system.library.miopen': miopen_ver or None,
}
print('Probed AMD base image versions:')
for key, value in versions.items():
print(f' {key}: {value or "unknown"}')
return versions
def generate_dockerfile(self) -> str:
with open('docker/Dockerfile.amd', 'r') as f:
content = f.read()
content = content.replace('{base_image}', self.args.base_image)
content = content.replace('{base_image_tag}', self.args.base_image_tag)
content = content.replace('{modelscope_branch}',
self.args.modelscope_branch)
content = content.replace('{cur_time}', formatted_time)
return content
def image(self) -> str:
ubuntu = getattr(self.args, 'amd_ubuntu_version',
None) or self.args.ubuntu_version
rocm = getattr(self.args, 'amd_rocm_version', None)
py_tag = getattr(self.args, 'amd_python_tag', None) or getattr(
self.args, 'python_tag', None)
torch = getattr(self.args, 'amd_torch_version', None)
if not (rocm and py_tag and torch):
raise RuntimeError(
'AMD image tag requires probed rocm/python/torch versions. '
f'Got rocm={rocm}, python={py_tag}, torch={torch}')
return (f'{docker_registry}:ubuntu{ubuntu}-rocm{rocm}-{py_tag}-'
f'torch{torch}-{self.args.modelscope_version}-test')
def _log_base_image_info(self) -> int:
base_image = self.args.base_image
print('=' * 60)
print(f'AMD ROCm base image: {base_image}')
print(f'AMD ROCm base image tag: {self.args.base_image_tag}')
print('=' * 60)
ret = self.run_cmd('docker', 'pull', base_image)
if ret != 0:
return ret
result = self._run_capture(
'docker', 'image', 'inspect', base_image,
'--format={{.Id}} {{if index .RepoDigests 0}}'
'{{index .RepoDigests 0}}{{else}}local-only{{end}}')
if result.returncode == 0:
print(f'AMD base image resolved: {result.stdout.strip()}')
else:
print(f'AMD base image inspect warning: {result.stderr.strip()}')
versions = self.probe_base_image_versions(base_image)
if not versions.get('rocm') or not versions.get(
'python') or not versions.get('torch'):
print('ERROR: failed to probe rocm/python/torch from base image')
return 1
self.args.amd_rocm_version = versions['rocm']
self.args.amd_torch_version = versions['torch']
self.args.amd_python_tag = self._python_tag_from_version(
versions['python'])
if versions.get('ubuntu'):
self.args.amd_ubuntu_version = versions['ubuntu']
else:
self.args.amd_ubuntu_version = self.args.ubuntu_version
print(f'AMD output image tag will be: {self.image()}')
print('=' * 60)
return 0
def build(self) -> int:
ret = self._log_base_image_info()
if ret != 0:
return ret
return self.run_cmd('docker', 'build', '-t', self.image(), '-f',
'Dockerfile', '.')
def push(self):
image_name = self.image()
ret = self.run_cmd('docker', 'push', image_name)
if ret != 0:
return ret
ubuntu = self.args.amd_ubuntu_version
rocm = self.args.amd_rocm_version
py_tag = self.args.amd_python_tag
torch = self.args.amd_torch_version
image_tag2 = (f'{docker_registry}:ubuntu{ubuntu}-rocm{rocm}-{py_tag}-'
f'torch{torch}-{self.args.modelscope_version}-'
f'{formatted_time}-test')
ret = self.run_cmd('docker', 'tag', image_name, image_tag2)
if ret != 0:
return ret
print(f'AMD image timestamp tag: {image_tag2}')
return self.run_cmd('docker', 'push', image_tag2)
class AscendImageBuilder(StableGPUImageBuilder):
_DEFAULT_TORCH_VERSION = '2.9.0'
_DEFAULT_TORCHVISION_VERSION = '0.24.0'
_DEFAULT_TORCHAUDIO_VERSION = '2.9.0'
_DEFAULT_TORCH_NPU_VERSION = '2.9.0.post2'
_DEFAULT_VLLM_VERSION = '0.18.0'
_DEFAULT_VLLM_ASCEND_VERSION = '0.18.0'
_DEFAULT_TRITON_ASCEND_VERSIONS = {
'8.5': '3.2.0',
'9.0': '3.2.1',
}
_CANN_VERSION_PATTERN = re.compile(r'^\d+(?:\.[0-9A-Za-z]+)+$')
_OS_TAG_PATTERN = re.compile(r'^[A-Za-z]+[0-9][0-9A-Za-z.]*$')
_PYTHON_TAG_PATTERN = re.compile(r'^py\d+\.\d+$', re.IGNORECASE)
_TORCH_NPU_VERSION_PATTERN = re.compile(
r'^(?P<torch_version>\d+\.\d+\.\d+)(?:\.post\d+)?$')
@staticmethod
def _normalize_arch(arch: str = None) -> str:
arch = arch or platform.machine()
arch = arch.lower()
arch_mapping = {
'x86': 'x86',
'x86_64': 'x86',
'amd64': 'x86',
'arm': 'arm',
'aarch64': 'arm',
'arm64': 'arm',
'x86': 'x86_64',
'x86_64': 'x86_64',
'amd64': 'x86_64',
'arm': 'aarch64',
'aarch64': 'aarch64',
'arm64': 'aarch64',
}
if arch not in arch_mapping:
raise ValueError(f'Unsupported architecture: {arch}. '
@@ -536,34 +928,130 @@ class AscendImageBuilder(StableGPUImageBuilder):
cann_version = parts[0]
os_tag = parts[2]
python_tag = parts[3]
if not cls._CANN_VERSION_PATTERN.fullmatch(cann_version):
raise ValueError(f'Invalid CANN version in Ascend base image tag: '
f'{cann_version}')
if not cls._OS_TAG_PATTERN.fullmatch(os_tag):
raise ValueError(
f'Invalid OS tag in Ascend base image tag: {os_tag}')
if not cls._PYTHON_TAG_PATTERN.fullmatch(python_tag):
raise ValueError(
f'Invalid Python tag in Ascend base image tag: {python_tag}')
return cann_version, f'CANN{cann_version}', os_tag
return cann_version, f'CANN{cann_version}', os_tag, python_tag
@staticmethod
def _get_os_family(os_tag: str) -> str:
os_tag = os_tag.lower()
if os_tag.startswith('ubuntu'):
return 'ubuntu'
if os_tag.startswith('openeuler'):
return 'openeuler'
raise ValueError(f'Unsupported Ascend base image OS tag: {os_tag}. '
'Supported OS families are Ubuntu and openEuler.')
@classmethod
def _init_torch_versions(cls, args) -> None:
torch_version_specified = args.torch_version is not None
torchvision_version_specified = args.torchvision_version is not None
torchaudio_version_specified = args.torchaudio_version is not None
if torch_version_specified:
if (not torchvision_version_specified
or not torchaudio_version_specified):
raise ValueError(
'When overriding --torch_version for an Ascend image, also '
'pass matching --torchvision_version and '
'--torchaudio_version.')
elif torchvision_version_specified or torchaudio_version_specified:
raise ValueError(
'--torchvision_version and --torchaudio_version require an '
'explicit --torch_version for an Ascend image.')
args.torch_version = args.torch_version or cls._DEFAULT_TORCH_VERSION
args.torchvision_version = (
args.torchvision_version or cls._DEFAULT_TORCHVISION_VERSION)
args.torchaudio_version = (
args.torchaudio_version or cls._DEFAULT_TORCHAUDIO_VERSION)
args.torch_npu_version = (
args.torch_npu_version or cls._DEFAULT_TORCH_NPU_VERSION)
match = cls._TORCH_NPU_VERSION_PATTERN.fullmatch(
args.torch_npu_version)
if not match:
raise ValueError('Invalid --torch_npu_version. Expected '
'<major>.<minor>.<patch> or '
'<major>.<minor>.<patch>.post<revision>.')
if args.torch_version != match.group('torch_version'):
raise ValueError(
'--torch_version must exactly match the base version of '
f'--torch_npu_version, got torch={args.torch_version} and '
f'torch_npu={args.torch_npu_version}.')
@classmethod
def _init_component_versions(cls, args) -> None:
args.vllm_version = args.vllm_version or cls._DEFAULT_VLLM_VERSION
args.vllm_ascend_version = (
args.vllm_ascend_version or cls._DEFAULT_VLLM_ASCEND_VERSION)
args.vllm_git_ref = cls._get_vllm_git_ref(args.vllm_version)
args.vllm_ascend_git_ref = cls._get_vllm_git_ref(
args.vllm_ascend_version)
if not args.triton_ascend_version:
cann_series = '.'.join(args.cann_version.split('.')[:2])
try:
args.triton_ascend_version = (
cls._DEFAULT_TRITON_ASCEND_VERSIONS[cann_series])
except KeyError as e:
raise ValueError('No default triton-ascend version for CANN '
f'{args.cann_version}. Please pass '
'--triton_ascend_version explicitly.') from e
@staticmethod
def _get_vllm_git_ref(version: str) -> str:
return version if version.startswith('v') else f'v{version}'
def init_args(self, args) -> Any:
if not args.base_image:
# Reuse the prebuilt vllm-ascend image to avoid rebuilding its stack.
args.base_image = 'quay.io/ascend/cann:8.5.1-a3-ubuntu22.04-py3.11'
self._init_torch_versions(args)
args.arch = self._normalize_arch(args.arch)
args.atlas_hardware = self._get_atlas_hardware(args.soc_version)
args.cann_version, args.cann_version_tag, args.os_tag = (
self._get_cann_os_tags(args.base_image))
(args.cann_version, args.cann_version_tag, args.os_tag,
args.ascend_python_tag) = (
self._get_cann_os_tags(args.base_image))
self._get_os_family(args.os_tag)
self._init_component_versions(args)
return super().init_args(args)
def _generate_python_tag(self, _python_version: str) -> str:
return self.args.ascend_python_tag
def generate_dockerfile(self) -> str:
extra_content = """
RUN pip install --no-cache-dir -U icecream soundfile pybind11 py-spy
RUN export PIP_EXTRA_INDEX_URL=https://pypi.org/simple && \
pip install --no-cache-dir -U icecream soundfile pybind11 py-spy
"""
with open('docker/Dockerfile.ascend', 'r') as f:
content = f.read()
content = content.replace('{base_image}', self.args.base_image)
content = content.replace('{soc_version}', self.args.soc_version)
content = content.replace('{cann_version}', self.args.cann_version)
content = content.replace('{torch_version}',
self.args.torch_version)
content = content.replace('{torchvision_version}',
self.args.torchvision_version)
content = content.replace('{torchaudio_version}',
self.args.torchaudio_version)
content = content.replace('{torch_npu_version}',
self.args.torch_npu_version)
content = content.replace('{vllm_git_ref}', self.args.vllm_git_ref)
content = content.replace('{vllm_ascend_git_ref}',
self.args.vllm_ascend_git_ref)
content = content.replace('{triton_ascend_version}',
self.args.triton_ascend_version)
content = content.replace('{extra_content}', extra_content)
content = content.replace('{cur_time}', formatted_time)
content = content.replace('{install_ms_deps}', 'False')
@@ -577,11 +1065,12 @@ RUN pip install --no-cache-dir -U icecream soundfile pybind11 py-spy
return content
def image(self) -> str:
return (
f'{docker_registry}:{self.args.swift_branch}-'
f'{self.args.atlas_hardware}-{self.args.python_tag}-'
f'{self.args.cann_version_tag}-{self.args.os_tag}-{self.args.arch}'
)
tag = (f'{self.args.swift_branch}-{self.args.cann_version_tag}-'
f'torch_npu{self.args.torch_npu_version}-'
f'{self.args.atlas_hardware}-{self.args.os_tag}-'
f'{self.args.python_tag}-'
f'{self.args.arch}')
return f'{docker_registry}:{tag.lower()}'
def push(self):
return 0
@@ -593,6 +1082,7 @@ parser.add_argument('--image_type', type=str)
parser.add_argument('--python_version', type=str, default='3.12.13')
parser.add_argument('--ubuntu_version', type=str, default='22.04')
parser.add_argument('--torch_version', type=str, default=None)
parser.add_argument('--torch_npu_version', type=str, default=None)
parser.add_argument('--torchvision_version', type=str, default=None)
parser.add_argument('--cuda_version', type=str, default=None)
parser.add_argument('--ci_image', type=int, default=0)
@@ -600,6 +1090,8 @@ parser.add_argument('--torchaudio_version', type=str, default=None)
parser.add_argument('--optimum_version', type=str, default=None)
parser.add_argument('--tf_version', type=str, default=None)
parser.add_argument('--vllm_version', type=str, default=None)
parser.add_argument('--vllm_ascend_version', type=str, default=None)
parser.add_argument('--triton_ascend_version', type=str, default=None)
parser.add_argument('--lmdeploy_version', type=str, default=None)
parser.add_argument('--flashattn_version', type=str, default=None)
parser.add_argument('--autogptq_version', type=str, default=None)
@@ -610,6 +1102,12 @@ parser.add_argument('--megatron_branch', type=str, default='v0.15.3')
parser.add_argument('--mindspeed_branch', type=str, default='core_r0.15.3')
parser.add_argument('--soc_version', type=str, default='ascend910_9391')
parser.add_argument('--arch', type=str, choices=['x86', 'arm'], default=None)
parser.add_argument(
'--base_image_tag',
type=str,
default=None,
help='Optional AMD ROCm override tag. Default: auto-resolve newest '
'concrete vllm/vllm-openai-rocm release tag from Docker Hub.')
parser.add_argument('--dry_run', type=int, default=0)
args = parser.parse_args()
@@ -621,6 +1119,8 @@ elif args.image_type.lower() == 'stable':
builder_cls = [StableCPUImageBuilder, StableGPUImageBuilder]
elif args.image_type.lower() == 'ascend':
builder_cls = [AscendImageBuilder]
elif args.image_type.lower() == 'amd':
builder_cls = [AmdImageBuilder]
elif args.image_type.lower() == 'latest':
builder_cls = [LatestGPUImageBuilder]
else:

View File

@@ -47,5 +47,4 @@ else
fi
pip config set global.index-url https://mirrors.cloud.aliyuncs.com/pypi/simple
pip config set global.extra-index-url https://pypi.org/simple
pip config set install.trusted-host mirrors.cloud.aliyuncs.com

View File

@@ -18,7 +18,8 @@ from typing import Dict, List, Optional, Type
import requests
# --- Hub file downloads (delegated) ---
from modelscope_hub.compat import dataset_file_download # noqa: E402,F401
from modelscope_hub.compat.file_download import \
dataset_file_download as _compat_dataset_file_download
from modelscope_hub.compat.file_download import \
model_file_download as _compat_model_file_download
from requests.adapters import Retry
@@ -31,7 +32,7 @@ from modelscope.hub.constants import (API_FILE_DOWNLOAD_CHUNK_SIZE,
from modelscope.utils.logger import get_logger
from .callback import ProgressCallback, TqdmCallback
from .errors import FileDownloadError
from .utils.utils import get_endpoint
from .utils.utils import find_reusable_legacy_repo_dir, get_endpoint
logger = get_logger()
@@ -77,6 +78,9 @@ def model_file_download(
revision = detail.get('Revision')
except Exception:
pass
if local_dir is None:
local_dir = find_reusable_legacy_repo_dir(
model_id, repo_type='model', cache_dir=cache_dir)
return _compat_model_file_download(
model_id,
file_path,
@@ -91,6 +95,37 @@ def model_file_download(
)
def dataset_file_download(
dataset_id: str,
file_path: str,
*,
cache_dir: str = None,
local_dir: str = None,
revision: str = None,
cookies: dict = None,
token: str = None,
endpoint: str = None,
local_files_only: bool = False,
user_agent=None,
) -> str:
"""Download a single dataset file, reusing flat/hub legacy caches when present."""
if local_dir is None:
local_dir = find_reusable_legacy_repo_dir(
dataset_id, repo_type='dataset', cache_dir=cache_dir)
return _compat_dataset_file_download(
dataset_id,
file_path,
cache_dir=cache_dir,
local_dir=local_dir,
revision=revision,
cookies=cookies,
token=token,
endpoint=endpoint,
local_files_only=local_files_only,
user_agent=user_agent,
)
# --- Direct HTTP downloads (retained - non-Hub API) ---

View File

@@ -4,16 +4,58 @@ Delegates to ``modelscope_hub.compat`` while keeping ``revision``, ``cache_dir``
and friends accessible as positional arguments for backward compatibility.
"""
from __future__ import annotations
import threading
from pathlib import Path
from typing import Dict, List, Optional, Union
from typing import TYPE_CHECKING, Dict, List, Optional, Type, Union
from modelscope_hub.compat.snapshot_download import \
dataset_snapshot_download as _compat_dataset_snapshot_download
from modelscope_hub.compat.snapshot_download import \
snapshot_download as _compat_snapshot_download
from modelscope.hub.utils.utils import find_reusable_legacy_repo_dir
from modelscope.utils.logger import get_logger
if TYPE_CHECKING:
from .callback import ProgressCallback
logger = get_logger()
__all__ = ['snapshot_download', 'dataset_snapshot_download']
# Capability probe: pre-1.38 cache auto-detection lives in modelscope-hub
# (DownloadManager._find_legacy_repo_dir, added in modelscope-hub>=0.1.7).
# Warn once if the installed hub predates it, so an existing legacy cache is
# not silently ignored and re-downloaded into the new layout.
_legacy_cache_capability: Optional[bool] = None
_legacy_cache_lock = threading.Lock()
def _warn_if_legacy_cache_detection_unavailable() -> None:
"""Warn once when the installed modelscope-hub cannot auto-detect a
pre-1.38 (legacy) cache layout, so downloads don't silently skip an
existing local cache and re-fetch into the new layout.
"""
global _legacy_cache_capability
if _legacy_cache_capability is not None:
return
with _legacy_cache_lock:
if _legacy_cache_capability is not None:
return
try:
from modelscope_hub._download import DownloadManager
_legacy_cache_capability = hasattr(DownloadManager,
'_find_legacy_repo_dir')
except Exception:
_legacy_cache_capability = False
if not _legacy_cache_capability:
logger.warning(
'The installed modelscope-hub lacks legacy cache '
'auto-detection (added in modelscope-hub>=0.1.7). An existing '
'pre-1.38 cache will not be reused; files will be downloaded '
'into the new cache layout. Upgrade with: '
"pip install -U 'modelscope-hub>=0.1.8'.")
def snapshot_download(
model_id: Optional[str] = None,
@@ -30,6 +72,7 @@ def snapshot_download(
max_workers: Optional[int] = None,
repo_id: Optional[str] = None,
repo_type: Optional[str] = None,
progress_callbacks: Optional[List[Type[ProgressCallback]]] = None,
token: Optional[str] = None,
endpoint: Optional[str] = None,
) -> str:
@@ -37,11 +80,20 @@ def snapshot_download(
Preserves the legacy positional-argument signature for backward
compatibility while delegating to ``modelscope_hub.compat``.
``progress_callbacks`` is a list of :class:`ProgressCallback` subclasses
(not instances), each instantiated per file to report download progress.
"""
_warn_if_legacy_cache_detection_unavailable()
effective_id = repo_id or model_id
effective_type = repo_type or 'model'
cache_dir_str = str(cache_dir) if cache_dir is not None else None
if local_dir is None and effective_id is not None:
local_dir = find_reusable_legacy_repo_dir(
effective_id, repo_type=effective_type, cache_dir=cache_dir_str)
return _compat_snapshot_download(
model_id=model_id,
revision=revision,
cache_dir=str(cache_dir) if cache_dir is not None else None,
cache_dir=cache_dir_str,
local_dir=local_dir,
allow_file_pattern=allow_file_pattern,
ignore_file_pattern=ignore_file_pattern,
@@ -56,6 +108,7 @@ def snapshot_download(
local_files_only=bool(local_files_only)
if local_files_only is not None else False,
user_agent=user_agent,
progress_callbacks=progress_callbacks,
)
@@ -75,11 +128,16 @@ def dataset_snapshot_download(
endpoint: Optional[str] = None,
) -> str:
"""Download a dataset repo snapshot (legacy positional-arg signature)."""
_warn_if_legacy_cache_detection_unavailable()
effective_id = dataset_id or repo_id
cache_dir_str = str(cache_dir) if cache_dir is not None else None
if local_dir is None and effective_id is not None:
local_dir = find_reusable_legacy_repo_dir(
effective_id, repo_type='dataset', cache_dir=cache_dir_str)
return _compat_dataset_snapshot_download(
dataset_id=effective_id,
revision=revision,
cache_dir=str(cache_dir) if cache_dir is not None else None,
cache_dir=cache_dir_str,
local_dir=local_dir,
allow_file_pattern=allow_file_pattern,
ignore_file_pattern=ignore_file_pattern,

View File

@@ -194,6 +194,78 @@ def get_cache_dir(model_id: Optional[str] = None):
base_path, model_id + '/')
def _modelscope_hub_cache_root() -> Path:
"""Cache root used by ``modelscope_hub`` downloads (not SDK ``.../hub``)."""
env = os.environ.get('MODELSCOPE_CACHE')
if env:
return Path(env).expanduser()
return Path.home() / '.cache' / 'modelscope'
def find_reusable_legacy_repo_dir(
repo_id: str,
repo_type: str = 'model',
cache_dir: Optional[Union[str, Path]] = None,
) -> Optional[str]:
"""Find old on-disk cache layouts that ``modelscope_hub`` download misses.
``modelscope_hub`` reuses ``{cache}/{type}s/{owner}/{safe_name}/`` (dots in
``name`` replaced by ``___``) and writes to
``{cache}/{type}s/{owner}--{name}/snapshots/{rev}/``. Older SDKs also
stored repos at:
- ``{cache}/{owner}/{name}/`` (flat, when ``MODELSCOPE_CACHE`` was set)
- ``{cache}/hub/{owner}/{name}/`` (pre-``models/`` restructuring)
- ``{cache}/{type}s/{owner}/{name}/`` (unsafed name; hub only checks
``safe_name``)
Returns a non-empty legacy path only when the layouts hub already handles
are absent, so callers can pass it as ``local_dir`` and avoid re-download.
"""
if not repo_id or '/' not in repo_id:
return None
base = Path(cache_dir).expanduser() if cache_dir is not None else \
_modelscope_hub_cache_root()
segment = f'{repo_type}s' if not repo_type.endswith('s') else repo_type
owner, name = repo_id.split('/', 1)
safe_name = name.replace('.', '___')
safe_id = repo_id.replace('/', '--')
# Layouts already handled by modelscope_hub — do not override.
hub_known = [
base / segment / safe_id,
base / segment / owner / safe_name,
]
for path in hub_known:
if _non_empty_dir(path):
return None
# Layouts hub download does not probe today.
legacy_candidates = [
base / owner / name,
base / owner / safe_name,
base / 'hub' / owner / name,
base / 'hub' / owner / safe_name,
base / segment / owner / name,
]
for path in legacy_candidates:
if _non_empty_dir(path):
logger.info('Found legacy cache at %s for %s, reusing.', path,
repo_id)
return str(path)
return None
def _non_empty_dir(path: Path) -> bool:
if not path.is_dir():
return False
try:
return any(path.iterdir())
except OSError:
return False
def get_release_datetime():
if MODELSCOPE_SDK_DEBUG in os.environ:
rt = int(round(datetime.now().timestamp()))

View File

@@ -1185,6 +1185,8 @@ def load_dataset_with_ctx(*args, **kwargs):
generate_from_dict_origin = features.generate_from_dict
hf_fs_open_origin = HfFileSystem._open
hf_fs_init_origin = HfFileSystem.__init__
hf_fs_open_was_patched = hf_fs_open_origin is _hf_fs_open
hf_fs_init_was_patched = hf_fs_init_origin is _hf_fs_init_with_cookie
# Apply patches
config.HF_ENDPOINT = get_endpoint()
@@ -1201,25 +1203,33 @@ def load_dataset_with_ctx(*args, **kwargs):
if _HAS_SCRIPT_LOADING:
HubDatasetModuleFactoryWithScript.get_module = get_module_with_script
features.generate_from_dict = generate_from_dict_ms
_hf_fs_open_original = hf_fs_open_origin
HfFileSystem._open = _hf_fs_open
_hf_fs_init_original = hf_fs_init_origin
HfFileSystem.__init__ = _hf_fs_init_with_cookie
if not hf_fs_open_was_patched:
_hf_fs_open_original = hf_fs_open_origin
HfFileSystem._open = _hf_fs_open
if not hf_fs_init_was_patched:
_hf_fs_init_original = hf_fs_init_origin
HfFileSystem.__init__ = _hf_fs_init_with_cookie
streaming = kwargs.get('streaming', False)
_streaming_dataset_returned = False
try:
dataset_res = DatasetsWrapperHF.load_dataset(*args, **kwargs)
_streaming_dataset_returned = streaming
yield dataset_res
finally:
_repo_tree_cache.clear()
HubApi._dataset_id_type_cache.clear()
if not streaming:
HfFileSystem._open = hf_fs_open_origin
_hf_fs_open_original = None
HfFileSystem.__init__ = hf_fs_init_origin
_hf_fs_init_original = None
should_restore = not _streaming_dataset_returned
if should_restore:
if not hf_fs_open_was_patched:
HfFileSystem._open = hf_fs_open_origin
_hf_fs_open_original = None
if not hf_fs_init_was_patched:
HfFileSystem.__init__ = hf_fs_init_origin
_hf_fs_init_original = None
config.HF_ENDPOINT = hf_endpoint_origin
file_utils.get_from_cache = get_from_cache_origin

View File

@@ -141,18 +141,19 @@ def check_model_from_owner_group(model_dir: str,
return False
if owner_group is None:
owner_group = ['iic', 'damo']
model_dir = model_dir.rstrip('/').rstrip('\\')
model_dir = os.path.normpath(model_dir.rstrip('/').rstrip('\\'))
parent_dir = os.path.dirname(model_dir)
group = os.path.basename(parent_dir)
if group in owner_group:
return True
# Also check cache path pattern: {cache_root}/{owner}--{model_name}/snapshots/{revision}
# Require exactly "{owner}--{name}" format (2 segments split by --)
# to prevent spoofing via accounts like "iic--hacked" which would
# produce paths like "iic--hacked--evil" and bypass the check.
# Require exactly "{owner}--{name}" with both segments non-empty
# to prevent spoofing via accounts like "iic--hacked" (paths like
# "iic--hacked--evil") or empty names like "iic--".
grandparent = os.path.basename(os.path.dirname(parent_dir))
if '--' in grandparent:
parts = grandparent.split('--')
if len(parts) == 2 and parts[0] in owner_group:
# Both owner and name must be non-empty; reject "iic--" / "--name".
if len(parts) == 2 and all(parts) and parts[0] in owner_group:
return True
return False

View File

@@ -155,6 +155,148 @@ def _decide_allow_file_pattern(module_name, cls=None):
return extra_allow_file_pattern
def _ms_revision(revision):
"""Translate an HF revision string into one ModelScope accepts."""
return 'master' if revision in (None, 'main') else revision
def _ms_download_kwargs_from_hf(kwargs, revision=None):
"""Map transformers download kwargs onto ``snapshot_download`` arguments.
Forwards ``local_files_only``, ``cache_dir``, string ``token``, and an
optional revision (normalized via ``_ms_revision``).
"""
download_kwargs = {
'local_files_only': kwargs.get('local_files_only', False),
}
cache_dir = kwargs.get('cache_dir')
if cache_dir is not None:
download_kwargs['cache_dir'] = cache_dir
token = kwargs.get('token')
if isinstance(token, str):
download_kwargs['token'] = token
if revision is not None:
download_kwargs['revision'] = _ms_revision(revision)
return download_kwargs
def _get_class_from_dynamic_module(class_reference, *args, **kwargs):
"""Wrapper that redirects dynamic-module downloads to ModelScope.
When a config's ``auto_map`` references another repo, transformers calls
``get_class_from_dynamic_module`` to fetch it. This wrapper ensures that
fetch goes through ModelScope instead of HuggingFace.
Cross-repo ``auto_map`` entries use ``repo_id--module.Class``. After
``snapshot_download``, the local cache path may itself contain ``--``
(modelscope_hub 0.1.x layout: ``models/{owner}--{name}/snapshots/...``).
Re-joining that path with ``--`` would make transformers'
``class_reference.split("--")`` raise ``ValueError``. Instead, pass the
local directory as ``pretrained_model_name_or_path`` and the bare
``module.Class`` as ``class_reference`` so transformers takes the
``os.path.isdir`` branch.
"""
from transformers.dynamic_module_utils import origin_get_class_from_dynamic_module
has_pretrained_arg = (
'pretrained_model_name_or_path'
in inspect.signature(origin_get_class_from_dynamic_module).parameters)
# Resolve pretrained_model_name_or_path from kwargs or positional args.
# ``args`` is a tuple; never mutate it in place.
pretrained_in_kwargs = False
pretrained_model_name_or_path = None
if has_pretrained_arg:
if 'pretrained_model_name_or_path' in kwargs:
pretrained_model_name_or_path = kwargs[
'pretrained_model_name_or_path']
pretrained_in_kwargs = True
elif args:
pretrained_model_name_or_path = args[0]
if (pretrained_model_name_or_path is not None
and not os.path.exists(pretrained_model_name_or_path)):
from modelscope import snapshot_download
# Model weights/config: use ``revision`` (not ``code_revision``).
downloaded_path = snapshot_download(
pretrained_model_name_or_path,
**_ms_download_kwargs_from_hf(
kwargs, revision=kwargs.get('revision')))
if pretrained_in_kwargs:
kwargs['pretrained_model_name_or_path'] = downloaded_path
else:
args = (downloaded_path, ) + args[1:]
pretrained_model_name_or_path = downloaded_path
if '--' in class_reference:
# Only the first ``--`` is the auto_map delimiter (repo vs module).
repo_id, class_reference = class_reference.split('--', 1)
if not os.path.exists(repo_id):
# Cross-repo code: transformers uses ``code_revision`` for this repo.
download_kwargs = _ms_download_kwargs_from_hf(
kwargs, revision=kwargs.get('code_revision'))
extra_allow_file_pattern = _decide_allow_file_pattern(
class_reference)
if extra_allow_file_pattern is not None:
download_kwargs[
'allow_file_pattern'] = extra_allow_file_pattern
if 'Config' in class_reference or 'Processor' in class_reference or 'Tokenizer' in class_reference:
download_kwargs['ignore_file_pattern'] = ignore_file_pattern
from modelscope import snapshot_download
repo_id = snapshot_download(repo_id, **download_kwargs)
if has_pretrained_arg:
# Local path + bare class name; do not rejoin with ``--``.
# Keep kwargs/positional form consistent with the original call.
if pretrained_in_kwargs:
kwargs['pretrained_model_name_or_path'] = repo_id
else:
args = (repo_id, ) + args[1:]
else:
# Legacy transformers without pretrained_model_name_or_path.
# Unsafe if repo_id (local cache) contains '--'; modern
# transformers always take the branch above.
class_reference = repo_id + '--' + class_reference
return origin_get_class_from_dynamic_module(class_reference, *args,
**kwargs)
def _patch_dynamic_module():
"""Globally patch ``get_class_from_dynamic_module`` to redirect to ModelScope."""
from transformers import dynamic_module_utils
if not hasattr(dynamic_module_utils,
'origin_get_class_from_dynamic_module'):
dynamic_module_utils.origin_get_class_from_dynamic_module = dynamic_module_utils.get_class_from_dynamic_module
dynamic_module_utils.get_class_from_dynamic_module = _get_class_from_dynamic_module
from transformers.models.auto import configuration_auto
configuration_auto.get_class_from_dynamic_module = _get_class_from_dynamic_module
def _unpatch_dynamic_module():
from transformers import dynamic_module_utils
if hasattr(dynamic_module_utils, 'origin_get_class_from_dynamic_module'):
dynamic_module_utils.get_class_from_dynamic_module = dynamic_module_utils.origin_get_class_from_dynamic_module
from transformers.models.auto import configuration_auto
configuration_auto.get_class_from_dynamic_module = dynamic_module_utils.origin_get_class_from_dynamic_module
delattr(dynamic_module_utils, 'origin_get_class_from_dynamic_module')
@contextlib.contextmanager
def _dynamic_module_patch_scope():
"""Temporarily patch ``get_class_from_dynamic_module`` for the duration of
the ``with`` block, unless an outer patch (e.g. ``patch_hub()``) is already
in effect.
This ensures that ``auto_map`` references inside a config are resolved via
ModelScope when loading through a modelscope-wrapped class, without
polluting the global transformers state for unrelated callers.
"""
from transformers import dynamic_module_utils
if hasattr(dynamic_module_utils, 'origin_get_class_from_dynamic_module'):
yield
return
_patch_dynamic_module()
try:
yield
finally:
_unpatch_dynamic_module()
def _patch_pretrained_class(all_imported_modules, wrap=False):
"""Patch all class to download from modelscope
@@ -175,18 +317,16 @@ def _patch_pretrained_class(all_imported_modules, wrap=False):
if subfolder:
file_filter = f'{subfolder}/*'
if not os.path.exists(pretrained_model_name_or_path):
revision = kwargs.pop('revision', None)
if revision is None or revision == 'main':
revision = 'master'
revision = _ms_revision(kwargs.pop('revision', None))
if file_filter is not None:
allow_file_pattern = file_filter
local_files_only = kwargs.pop('local_files_only', False)
download_kwargs = _ms_download_kwargs_from_hf(
kwargs, revision=revision)
model_dir = snapshot_download(
pretrained_model_name_or_path,
revision=revision,
local_files_only=local_files_only,
ignore_file_pattern=ignore_file_pattern,
allow_file_pattern=allow_file_pattern)
allow_file_pattern=allow_file_pattern,
**download_kwargs)
if subfolder:
model_dir = os.path.join(model_dir, subfolder)
else:
@@ -272,8 +412,9 @@ def _patch_pretrained_class(all_imported_modules, wrap=False):
cls=module_class,
**kwargs)
module_obj = module_class.from_pretrained(model, model_dir,
*model_args, **kwargs)
with _dynamic_module_patch_scope():
module_obj = module_class.from_pretrained(
model, model_dir, *model_args, **kwargs)
return module_obj
@@ -286,8 +427,9 @@ def _patch_pretrained_class(all_imported_modules, wrap=False):
model_dir = get_model_dir(pretrained_model_name_or_path,
**kwargs)
module_obj = module_class.from_pretrained(
model_dir, *model_args, **kwargs)
with _dynamic_module_patch_scope():
module_obj = module_class.from_pretrained(
model_dir, *model_args, **kwargs)
if module_class.__name__.startswith('AutoModel'):
module_obj.model_dir = model_dir
@@ -315,8 +457,9 @@ def _patch_pretrained_class(all_imported_modules, wrap=False):
allow_file_pattern=allow_file_pattern,
**kwargs)
module_obj = module_class.get_config_dict(
model_dir, *model_args, **kwargs)
with _dynamic_module_patch_scope():
module_obj = module_class.get_config_dict(
model_dir, *model_args, **kwargs)
return module_obj
def save_pretrained(
@@ -441,39 +584,13 @@ def _patch_pretrained_class(all_imported_modules, wrap=False):
all_available_modules.append(var)
def get_class_from_dynamic_module(class_reference, *args, **kwargs):
from transformers.dynamic_module_utils import origin_get_class_from_dynamic_module
if 'pretrained_model_name_or_path' in inspect.signature(
origin_get_class_from_dynamic_module).parameters:
pretrained_model_name_or_path = args[0]
if not os.path.exists(pretrained_model_name_or_path):
from modelscope import snapshot_download
args[0] = snapshot_download(pretrained_model_name_or_path)
if '--' in class_reference:
repo_id, class_reference = class_reference.split('--')
if not os.path.exists(repo_id):
download_kwargs = {}
extra_allow_file_pattern = _decide_allow_file_pattern(
class_reference)
if extra_allow_file_pattern is not None:
download_kwargs[
'allow_file_pattern'] = extra_allow_file_pattern
if 'Config' in class_reference or 'Processor' in class_reference or 'Tokenizer' in class_reference:
download_kwargs[
'ignore_file_pattern'] = ignore_file_pattern
from modelscope import snapshot_download
repo_id = snapshot_download(repo_id, **download_kwargs)
class_reference = repo_id + '--' + class_reference
return origin_get_class_from_dynamic_module(class_reference, *args,
**kwargs)
from transformers import dynamic_module_utils
if not hasattr(dynamic_module_utils,
'origin_get_class_from_dynamic_module'):
dynamic_module_utils.origin_get_class_from_dynamic_module = dynamic_module_utils.get_class_from_dynamic_module
dynamic_module_utils.get_class_from_dynamic_module = get_class_from_dynamic_module
from transformers.models.auto import configuration_auto
configuration_auto.get_class_from_dynamic_module = get_class_from_dynamic_module
# Only apply the global get_class_from_dynamic_module monkey-patch in
# direct-patch mode (wrap=False). In wrap mode the ClassWrapper scopes
# the patch to each from_pretrained / get_config_dict call via
# _dynamic_module_patch_scope(), so the global state stays clean for
# unrelated ``from transformers import AutoConfig`` callers (issue #1751).
if not wrap:
_patch_dynamic_module()
return all_available_modules
@@ -509,12 +626,7 @@ def _unpatch_pretrained_class(all_imported_modules):
if has_get_config_dict and hasattr(var, '_get_config_dict_origin'):
_restore(var, 'get_config_dict', '_get_config_dict_origin')
from transformers import dynamic_module_utils
if hasattr(dynamic_module_utils, 'origin_get_class_from_dynamic_module'):
dynamic_module_utils.get_class_from_dynamic_module = dynamic_module_utils.origin_get_class_from_dynamic_module
from transformers.models.auto import configuration_auto
configuration_auto.get_class_from_dynamic_module = dynamic_module_utils.origin_get_class_from_dynamic_module
delattr(dynamic_module_utils, 'origin_get_class_from_dynamic_module')
_unpatch_dynamic_module()
def _patch_kernels():
@@ -547,11 +659,6 @@ def _unpatch_kernels():
del kernels_utils._get_hf_api_origin
def _ms_revision(revision):
"""Translate an HF revision string into one ModelScope accepts."""
return 'master' if revision in (None, 'main') else revision
class _MsKernelApi:
"""Minimal `HfApi` look-alike that forwards to ModelScope. Only the
handful of methods that `kernels` actually calls are implemented.

View File

@@ -374,6 +374,30 @@ def create_module_from_files(file_list, file_prefix, module_name):
importlib.invalidate_caches()
def _module_name_from_model_dir(model_dir: str) -> str:
"""Derive a valid Python package name from a local model directory.
modelscope_hub 0.1.x returns paths like
``{cache}/models/{owner}--{name}/snapshots/{revision}``. Using
``Path(model_dir).stem`` on a revision such as ``v1.0.4`` yields
``v1.0``, and ``importlib.import_module('v1.0.xxx')`` fails with
``ModuleNotFoundError: No module named 'v1'`` because dots are package
separators. Prefer the ``{owner}--{name}`` segment (plus revision for
isolation) and sanitize to a valid identifier.
"""
path = Path(model_dir).resolve()
if path.parent.name == 'snapshots':
base = f'{path.parent.parent.name}__{path.name}'
else:
# Use the full directory name, not .stem, so dotted names are kept
# intact before sanitization (stem would turn ``v1.0.4`` into ``v1.0``).
base = path.name
module_name = re.sub(r'[^0-9A-Za-z_]', '_', base)
if not module_name or module_name[0].isdigit():
module_name = f'm_{module_name}'
return module_name
def import_module_from_model_dir(model_dir):
""" import all the necessary module from a model dir
@@ -383,7 +407,6 @@ def import_module_from_model_dir(model_dir):
No returns, raise error if failed
"""
from pathlib import Path
file_scanner = FilesAstScanning()
file_scanner.traversal_files(model_dir, include_init=True)
file_dirs = file_scanner.file_dirs
@@ -395,7 +418,7 @@ def import_module_from_model_dir(model_dir):
if BASE_MODULE_DIR not in sys.path:
sys.path.append(BASE_MODULE_DIR)
module_name = Path(model_dir).stem
module_name = _module_name_from_model_dir(model_dir)
# in order to keep forward compatibility, we add module path to
# sys.path so that submodule can be imported directly as before

View File

@@ -1,5 +1,5 @@
filelock
modelscope-hub>=0.0.7
modelscope-hub>=0.2.0
packaging
requests>=2.25
setuptools

View File

@@ -1,5 +1,6 @@
import tempfile
import unittest
from unittest import mock
from tqdm import tqdm
@@ -48,5 +49,36 @@ class ProgressCallbackTest(unittest.TestCase):
print(f'model_dir: {model_dir}')
class SnapshotDownloadForwardTest(unittest.TestCase):
"""Network-free tests: the shim forwards progress_callbacks to compat."""
# ``modelscope.hub.snapshot_download`` the attribute is shadowed by the
# re-exported function, so patch via the fully qualified module string.
_COMPAT_TARGET = \
'modelscope.hub.snapshot_download._compat_snapshot_download'
def test_progress_callbacks_forwarded_to_compat(self):
from modelscope.hub.snapshot_download import snapshot_download
with mock.patch(
self._COMPAT_TARGET, return_value='/tmp/snapshot') as m:
result = snapshot_download(
'owner/repo', progress_callbacks=[NewProgressCallback])
self.assertEqual(result, '/tmp/snapshot')
_, kwargs = m.call_args
self.assertEqual(kwargs['progress_callbacks'], [NewProgressCallback])
def test_progress_callbacks_default_none(self):
from modelscope.hub.snapshot_download import snapshot_download
with mock.patch(
self._COMPAT_TARGET, return_value='/tmp/snapshot') as m:
snapshot_download('owner/repo')
_, kwargs = m.call_args
self.assertIsNone(kwargs['progress_callbacks'])
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,54 @@
import importlib
import sys
import unittest
from unittest import mock
class LegacyCacheGuardTest(unittest.TestCase):
"""Tests for the modelscope-hub capability guard in the shim.
Network-free: only probes whether the loaded modelscope-hub exposes the
legacy-cache auto-detection capability and warns once when it does not.
"""
def setUp(self):
# NOTE: ``modelscope.hub.snapshot_download`` the *attribute* is shadowed
# by the re-exported function in ``modelscope.hub.__init__``; use
# importlib to obtain the actual submodule object.
sd = importlib.import_module('modelscope.hub.snapshot_download')
self.sd = sd
# Reset the fire-once probe cache before each test.
sd._legacy_cache_capability = None
def tearDown(self):
self.sd._legacy_cache_capability = None
def test_capability_present_no_warning(self):
sd = self.sd
# The real modelscope-hub DownloadManager has _find_legacy_repo_dir.
with mock.patch.object(sd.logger, 'warning') as warn:
sd._warn_if_legacy_cache_detection_unavailable()
self.assertTrue(sd._legacy_cache_capability)
warn.assert_not_called()
def test_capability_absent_warns_once(self):
sd = self.sd
class _OldDownloadManager: # lacks _find_legacy_repo_dir
pass
fake_module = mock.MagicMock()
fake_module.DownloadManager = _OldDownloadManager
with mock.patch.dict(sys.modules,
{'modelscope_hub._download': fake_module}):
with mock.patch.object(sd.logger, 'warning') as warn:
sd._warn_if_legacy_cache_detection_unavailable()
sd._warn_if_legacy_cache_detection_unavailable()
self.assertFalse(sd._legacy_cache_capability)
self.assertEqual(warn.call_count, 1) # fire-once
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,103 @@
# Copyright (c) Alibaba, Inc. and its affiliates.
import os
import tempfile
import unittest
from pathlib import Path
from unittest import mock
from modelscope.hub.utils.utils import find_reusable_legacy_repo_dir
class LegacyCacheReuseTest(unittest.TestCase):
"""Old flat/hub cache layouts should be reusable without re-download."""
def setUp(self):
self._tmpdir = tempfile.TemporaryDirectory()
self.cache = Path(self._tmpdir.name)
self.model_id = 'iic/nlp_xlmr_named-entity-recognition_eng-ecommerce-query'
self.owner, self.name = self.model_id.split('/', 1)
def tearDown(self):
self._tmpdir.cleanup()
def _touch_model_dir(self, path: Path):
path.mkdir(parents=True, exist_ok=True)
(path / 'configuration.json').write_text('{}', encoding='utf-8')
def test_finds_flat_legacy_cache(self):
legacy = self.cache / self.owner / self.name
self._touch_model_dir(legacy)
found = find_reusable_legacy_repo_dir(
self.model_id, cache_dir=self.cache)
self.assertEqual(found, str(legacy))
def test_finds_hub_legacy_cache(self):
legacy = self.cache / 'hub' / self.owner / self.name
self._touch_model_dir(legacy)
found = find_reusable_legacy_repo_dir(
self.model_id, cache_dir=self.cache)
self.assertEqual(found, str(legacy))
def test_reuses_unsafed_models_slash_layout(self):
# Hub only probes safe_name (dots -> ___); unsafed path is reusable.
dotted_id = 'org/model.with.dots'
owner, name = dotted_id.split('/', 1)
slash = self.cache / 'models' / owner / name
self._touch_model_dir(slash)
found = find_reusable_legacy_repo_dir(dotted_id, cache_dir=self.cache)
self.assertEqual(found, str(slash))
def test_prefers_hub_known_safe_slash_layout(self):
dotted_id = 'org/model.with.dots'
owner, name = dotted_id.split('/', 1)
safe = self.cache / 'models' / owner / name.replace('.', '___')
flat = self.cache / owner / name
self._touch_model_dir(safe)
self._touch_model_dir(flat)
found = find_reusable_legacy_repo_dir(dotted_id, cache_dir=self.cache)
self.assertIsNone(found)
def test_prefers_hub_known_owner_dash_layout(self):
modern = self.cache / 'models' / self.model_id.replace('/', '--')
flat = self.cache / self.owner / self.name
self._touch_model_dir(modern)
self._touch_model_dir(flat)
found = find_reusable_legacy_repo_dir(
self.model_id, cache_dir=self.cache)
self.assertIsNone(found)
def test_empty_legacy_dir_ignored(self):
(self.cache / self.owner / self.name).mkdir(parents=True)
found = find_reusable_legacy_repo_dir(
self.model_id, cache_dir=self.cache)
self.assertIsNone(found)
def test_uses_modelscope_cache_env(self):
legacy = self.cache / self.owner / self.name
self._touch_model_dir(legacy)
with mock.patch.dict(os.environ,
{'MODELSCOPE_CACHE': str(self.cache)}):
found = find_reusable_legacy_repo_dir(self.model_id)
self.assertEqual(found, str(legacy))
def test_default_root_matches_hub_not_sdk_hub_suffix(self):
# Without MODELSCOPE_CACHE, hub uses ~/.cache/modelscope (no /hub).
modern = (
Path.home() / '.cache' / 'modelscope' / 'models'
/ self.model_id.replace('/', '--'))
# Do not create real home dirs; patch the hub root helper instead.
with mock.patch(
'modelscope.hub.utils.utils._modelscope_hub_cache_root',
return_value=self.cache):
modern_under_test = (
self.cache / 'models' / self.model_id.replace('/', '--'))
flat = self.cache / self.owner / self.name
self._touch_model_dir(modern_under_test)
self._touch_model_dir(flat)
found = find_reusable_legacy_repo_dir(self.model_id)
self.assertIsNone(found)
self.assertFalse(modern.exists()) # we never touched real home cache
if __name__ == '__main__':
unittest.main()

View File

@@ -1,6 +1,9 @@
# Copyright (c) Alibaba, Inc. and its affiliates.
import unittest
from unittest import mock
from huggingface_hub.hf_file_system import HfFileSystem
from modelscope import MsDataset
from modelscope.utils.logger import get_logger
@@ -11,6 +14,78 @@ logger = get_logger()
class TestStreamLoad(unittest.TestCase):
@staticmethod
def _reset_hf_filesystem_patch(hf_datasets_util):
if (HfFileSystem._open is hf_datasets_util._hf_fs_open
and hf_datasets_util._hf_fs_open_original is not None):
HfFileSystem._open = hf_datasets_util._hf_fs_open_original
hf_datasets_util._hf_fs_open_original = None
if (HfFileSystem.__init__ is hf_datasets_util._hf_fs_init_with_cookie
and hf_datasets_util._hf_fs_init_original is not None):
HfFileSystem.__init__ = hf_datasets_util._hf_fs_init_original
hf_datasets_util._hf_fs_init_original = None
def test_hf_filesystem_patch_idempotent_for_repeated_streaming_loads(self):
from modelscope.msdatasets.utils import hf_datasets_util
hf_fs_open_before = HfFileSystem._open
hf_fs_init_before = HfFileSystem.__init__
open_original_before = hf_datasets_util._hf_fs_open_original
init_original_before = hf_datasets_util._hf_fs_init_original
try:
self._reset_hf_filesystem_patch(hf_datasets_util)
with mock.patch.object(
hf_datasets_util.DatasetsWrapperHF,
'load_dataset',
return_value=object()):
with hf_datasets_util.load_dataset_with_ctx(streaming=True):
pass
with hf_datasets_util.load_dataset_with_ctx(streaming=True):
pass
self.assertIs(HfFileSystem._open, hf_datasets_util._hf_fs_open)
self.assertIsNot(hf_datasets_util._hf_fs_open_original,
hf_datasets_util._hf_fs_open)
self.assertIs(HfFileSystem.__init__,
hf_datasets_util._hf_fs_init_with_cookie)
self.assertIsNot(hf_datasets_util._hf_fs_init_original,
hf_datasets_util._hf_fs_init_with_cookie)
finally:
HfFileSystem._open = hf_fs_open_before
HfFileSystem.__init__ = hf_fs_init_before
hf_datasets_util._hf_fs_open_original = open_original_before
hf_datasets_util._hf_fs_init_original = init_original_before
def test_hf_filesystem_patch_restored_when_streaming_load_fails(self):
from modelscope.msdatasets.utils import hf_datasets_util
hf_fs_open_before = HfFileSystem._open
hf_fs_init_before = HfFileSystem.__init__
open_original_before = hf_datasets_util._hf_fs_open_original
init_original_before = hf_datasets_util._hf_fs_init_original
try:
self._reset_hf_filesystem_patch(hf_datasets_util)
hf_fs_open_clean = HfFileSystem._open
hf_fs_init_clean = HfFileSystem.__init__
with mock.patch.object(
hf_datasets_util.DatasetsWrapperHF,
'load_dataset',
side_effect=RuntimeError('load failed')):
with self.assertRaises(RuntimeError):
with hf_datasets_util.load_dataset_with_ctx(
streaming=True):
pass
self.assertIs(HfFileSystem._open, hf_fs_open_clean)
self.assertIs(HfFileSystem.__init__, hf_fs_init_clean)
self.assertIsNone(hf_datasets_util._hf_fs_open_original)
self.assertIsNone(hf_datasets_util._hf_fs_init_original)
finally:
HfFileSystem._open = hf_fs_open_before
HfFileSystem.__init__ = hf_fs_init_before
hf_datasets_util._hf_fs_open_original = open_original_before
hf_datasets_util._hf_fs_init_original = init_original_before
def setUp(self):
...

View File

@@ -249,19 +249,367 @@ class HFUtilTest(unittest.TestCase):
f'Expected no weight files in {model_dir}, but found: '
f"{[f for f in files if f.endswith('.safetensors') or f.endswith('.bin')]}"
)
cache_dir = os.path.dirname(model_dir)
cache_dir = os.path.dirname(cache_dir)
model_dir_2 = os.path.join(cache_dir, 'nomic-ai', 'nomic-bert-2048')
if os.path.exists(model_dir_2):
files = os.listdir(model_dir_2)
has_weight_files = any(
f.endswith('.safetensors') or f.endswith('.bin')
for f in files)
# modelscope_hub 0.1.x layout uses models/{owner}--{name}/...;
# older layout used {owner}/{name}/. Accept either.
cache_root = model_dir
for _ in range(4):
parent = os.path.dirname(cache_root)
if parent == cache_root:
break
cache_root = parent
candidates = [
os.path.join(cache_root, 'nomic-ai', 'nomic-bert-2048'),
os.path.join(cache_root, 'models',
'nomic-ai--nomic-bert-2048'),
]
for model_dir_2 in candidates:
if not os.path.exists(model_dir_2):
continue
# Walk into snapshots/{rev} if present.
check_dirs = [model_dir_2]
snapshots = os.path.join(model_dir_2, 'snapshots')
if os.path.isdir(snapshots):
check_dirs.extend(
os.path.join(snapshots, d)
for d in os.listdir(snapshots)
if os.path.isdir(os.path.join(snapshots, d)))
for check_dir in check_dirs:
files = os.listdir(check_dir)
has_weight_files = any(
f.endswith('.safetensors') or f.endswith('.bin')
for f in files)
self.assertFalse(
has_weight_files,
f'Expected no weight files in {check_dir}, but found: '
f"{[f for f in files if f.endswith('.safetensors') or f.endswith('.bin')]}"
)
def test_dynamic_module_double_dash_cache_path(self):
"""Cross-repo auto_map must survive cache paths that contain '--'.
modelscope_hub 0.1.x stores repos under ``models/{owner}--{name}/``.
Rejoining that path into ``class_reference`` with ``--`` makes
transformers' ``split("--")`` raise ValueError.
"""
from unittest import mock
from modelscope.utils.hf_util.patcher import \
_get_class_from_dynamic_module
tmp = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, tmp, ignore_errors=True)
local_path = os.path.join(tmp, 'models', 'nomic-ai--nomic-bert-2048',
'snapshots', 'rev')
os.makedirs(local_path)
pretrained = os.path.join(tmp, 'models',
'nomic-ai--nomic-embed-text-v1.5',
'snapshots', 'rev')
os.makedirs(pretrained)
captured = {}
def fake_origin(class_reference, pretrained_model_name_or_path, *args,
**kwargs):
# Signature must match transformers so has_pretrained_arg is True.
captured['class_reference'] = class_reference
captured['pretrained'] = pretrained_model_name_or_path
return type('DummyConfig', (), {})
class_ref = ('nomic-ai/nomic-bert-2048--'
'configuration_hf_nomic_bert.NomicBertConfig')
# create=True: do not permanently leave origin_* on the module
# (would break test_import_not_pollute_dynamic_module).
with mock.patch(
'transformers.dynamic_module_utils.origin_get_class_from_dynamic_module',
new=fake_origin,
create=True):
with mock.patch(
'modelscope.snapshot_download', return_value=local_path):
_get_class_from_dynamic_module(class_ref, pretrained)
# Must pass bare module.Class (no '--') so transformers does not split.
self.assertEqual(captured['class_reference'],
'configuration_hf_nomic_bert.NomicBertConfig')
# Local cache path (which contains '--') is pretrained_model_name_or_path.
self.assertEqual(captured['pretrained'], local_path)
def test_dynamic_module_remote_pretrained_tuple_args(self):
"""Remote pretrained_model_name_or_path must not mutate args in place.
``*args`` is a tuple; ``args[0] = snapshot_download(...)`` raises
TypeError. Rebuild the tuple instead (regression from 9379504f).
"""
from unittest import mock
from modelscope.utils.hf_util.patcher import \
_get_class_from_dynamic_module
tmp = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, tmp, ignore_errors=True)
downloaded = os.path.join(tmp, 'models', 'org--model', 'snapshots',
'rev')
os.makedirs(downloaded)
captured = {}
def fake_origin(class_reference, pretrained_model_name_or_path, *args,
**kwargs):
captured['class_reference'] = class_reference
captured['pretrained'] = pretrained_model_name_or_path
return type('DummyConfig', (), {})
# No '--' in class_reference: only the pretrained download branch runs.
remote_id = 'org/model-not-on-disk'
with mock.patch(
'transformers.dynamic_module_utils.origin_get_class_from_dynamic_module',
new=fake_origin,
create=True):
with mock.patch(
'modelscope.snapshot_download',
return_value=downloaded) as sd:
# Must not raise TypeError: 'tuple' object does not support
# item assignment.
_get_class_from_dynamic_module('modeling.Foo', remote_id)
sd.assert_called_once_with(remote_id, local_files_only=False)
self.assertEqual(captured['class_reference'], 'modeling.Foo')
self.assertEqual(captured['pretrained'], downloaded)
def test_dynamic_module_local_files_only_forwarded(self):
"""Download kwargs must be forwarded to both snapshot_download calls.
Cross-repo auto_map references previously omitted local_files_only /
cache_dir / token / code_revision, so offline and custom-cache loads
still hit the wrong download path for the referenced repo.
"""
from unittest import mock
from modelscope.utils.hf_util.patcher import \
_get_class_from_dynamic_module
tmp = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, tmp, ignore_errors=True)
downloaded = os.path.join(tmp, 'models', 'org--model', 'snapshots',
'rev')
cross_repo = os.path.join(tmp, 'models', 'org--other', 'snapshots',
'rev')
os.makedirs(downloaded)
os.makedirs(cross_repo)
def fake_origin(class_reference, pretrained_model_name_or_path, *args,
**kwargs):
return type('DummyConfig', (), {})
remote_id = 'org/model-not-on-disk'
class_ref = 'org/other--configuration_foo.FooConfig'
call_kwargs = []
cache_dir = os.path.join(tmp, 'custom_cache')
token = 'ms-test-token'
def fake_download(repo_id, **kwargs):
call_kwargs.append((repo_id, dict(kwargs)))
if repo_id == remote_id:
return downloaded
if repo_id == 'org/other':
return cross_repo
raise AssertionError(f'unexpected download: {repo_id}')
with mock.patch(
'transformers.dynamic_module_utils.origin_get_class_from_dynamic_module',
new=fake_origin,
create=True):
with mock.patch(
'modelscope.snapshot_download', side_effect=fake_download):
_get_class_from_dynamic_module(
class_ref,
pretrained_model_name_or_path=remote_id,
local_files_only=True,
cache_dir=cache_dir,
token=token,
revision='model-rev',
code_revision='code-rev')
self.assertEqual(len(call_kwargs), 2)
self.assertEqual(call_kwargs[0][0], remote_id)
self.assertEqual(
call_kwargs[0][1], {
'local_files_only': True,
'cache_dir': cache_dir,
'token': token,
'revision': 'model-rev',
})
self.assertEqual(call_kwargs[1][0], 'org/other')
self.assertEqual(call_kwargs[1][1]['local_files_only'], True)
self.assertEqual(call_kwargs[1][1]['cache_dir'], cache_dir)
self.assertEqual(call_kwargs[1][1]['token'], token)
self.assertEqual(call_kwargs[1][1]['revision'], 'code-rev')
self.assertIn('ignore_file_pattern', call_kwargs[1][1])
def test_ms_download_kwargs_from_hf(self):
"""Shared HF→MS download kwargs mapping used by patcher download paths."""
from modelscope.utils.hf_util.patcher import _ms_download_kwargs_from_hf
self.assertEqual(
_ms_download_kwargs_from_hf({}), {'local_files_only': False})
got = _ms_download_kwargs_from_hf(
{
'local_files_only': True,
'cache_dir': '/tmp/c',
'token': 'sekrit',
'token_ignored': True,
},
revision='main')
self.assertEqual(
got, {
'local_files_only': True,
'cache_dir': '/tmp/c',
'token': 'sekrit',
'revision': 'master',
})
# HF token=True means "default creds"; only string tokens are forwarded.
self.assertNotIn('token', _ms_download_kwargs_from_hf({'token': True}))
def test_get_model_dir_forwards_cache_dir_and_token(self):
"""from_pretrained download path must forward cache_dir and token."""
from unittest import mock
from modelscope import AutoConfig
tmp = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, tmp, ignore_errors=True)
with open(os.path.join(tmp, 'config.json'), 'w') as f:
f.write('{"model_type": "bert", "hidden_size": 8}')
cache_dir = os.path.join(tmp, 'custom_cache')
token = 'ms-test-token'
with mock.patch(
'modelscope.snapshot_download', return_value=tmp) as sd:
AutoConfig.from_pretrained(
'org/model-not-on-disk',
cache_dir=cache_dir,
token=token,
local_files_only=True)
sd.assert_called_once()
_, kwargs = sd.call_args
self.assertEqual(kwargs.get('cache_dir'), cache_dir)
self.assertEqual(kwargs.get('token'), token)
self.assertTrue(kwargs.get('local_files_only'))
def test_dynamic_module_pretrained_via_kwargs(self):
"""pretrained_model_name_or_path may be passed as a keyword argument."""
from unittest import mock
from modelscope.utils.hf_util.patcher import \
_get_class_from_dynamic_module
tmp = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, tmp, ignore_errors=True)
downloaded = os.path.join(tmp, 'models', 'org--model', 'snapshots',
'rev')
cross_repo = os.path.join(tmp, 'models', 'org--other', 'snapshots',
'rev')
os.makedirs(downloaded)
os.makedirs(cross_repo)
captured = {}
def fake_origin(class_reference, pretrained_model_name_or_path, *args,
**kwargs):
captured['class_reference'] = class_reference
captured['pretrained'] = pretrained_model_name_or_path
captured['kwargs'] = kwargs
return type('DummyConfig', (), {})
remote_id = 'org/model-not-on-disk'
class_ref = 'org/other--configuration_foo.FooConfig'
def fake_download(repo_id, **kwargs):
if repo_id == remote_id:
return downloaded
if repo_id == 'org/other':
return cross_repo
raise AssertionError(f'unexpected download: {repo_id}')
with mock.patch(
'transformers.dynamic_module_utils.origin_get_class_from_dynamic_module',
new=fake_origin,
create=True):
with mock.patch(
'modelscope.snapshot_download', side_effect=fake_download):
# Keyword form: must download and not pass duplicate positional.
_get_class_from_dynamic_module(
class_ref, pretrained_model_name_or_path=remote_id)
self.assertEqual(captured['class_reference'],
'configuration_foo.FooConfig')
self.assertEqual(captured['pretrained'], cross_repo)
self.assertNotIn('pretrained_model_name_or_path', captured['kwargs'])
def test_import_not_pollute_dynamic_module(self):
"""Importing from modelscope must not globally patch
transformers' get_class_from_dynamic_module (issue #1751).
The patch should be scoped to each from_pretrained / get_config_dict
call via _dynamic_module_patch_scope(), leaving the global state clean
for unrelated ``from transformers import AutoConfig`` callers.
"""
from modelscope import AutoConfig
from modelscope.utils.file_utils import get_modelscope_cache_dir
from transformers import dynamic_module_utils
# 1. Importing AutoConfig from modelscope (wrap=True) must NOT
# globally patch get_class_from_dynamic_module.
self.assertFalse(
hasattr(dynamic_module_utils,
'origin_get_class_from_dynamic_module'),
'Importing AutoConfig from modelscope must not globally patch '
'get_class_from_dynamic_module (issue #1751)')
# 2. The modelscope AutoConfig should still work with
# trust_remote_code, downloading through ModelScope (not HF).
model = 'nomic-ai/nomic-embed-text-v1.5'
config = AutoConfig.from_pretrained(model, trust_remote_code=True)
model_dir = config.name_or_path
# Verify files are in the ModelScope cache, not the HF cache.
ms_cache = get_modelscope_cache_dir()
self.assertTrue(
os.path.realpath(model_dir).startswith(os.path.realpath(ms_cache)),
f'Model files should be in ModelScope cache ({ms_cache}), '
f'but found at {model_dir}')
# 3. After the call the global patch should still NOT be in effect
# (the _dynamic_module_patch_scope was temporary).
self.assertFalse(
hasattr(dynamic_module_utils,
'origin_get_class_from_dynamic_module'),
'Global get_class_from_dynamic_module should not be patched '
'after a modelscope AutoConfig call (issue #1751)')
# 4. Pure transformers AutoConfig (without modelscope patching) must
# NOT be redirected to ModelScope. With the old code the global
# patch would intercept this call and download from ModelScope;
# with the fix the call goes to HuggingFace directly.
from transformers import AutoConfig as HFAutoConfig
try:
hf_config = HFAutoConfig.from_pretrained(
model, trust_remote_code=True)
hf_model_dir = hf_config.name_or_path
# If the download succeeded, files must be outside the
# ModelScope cache (i.e. in the HuggingFace cache).
self.assertFalse(
has_weight_files,
f'Expected no weight files in {model_dir}, but found: '
f"{[f for f in files if f.endswith('.safetensors') or f.endswith('.bin')]}"
)
os.path.realpath(hf_model_dir).startswith(
os.path.realpath(ms_cache)),
f'Transformers AutoConfig should download from HuggingFace, '
f'not ModelScope (issue #1751). Found files at: '
f'{hf_model_dir}')
except Exception:
# If HuggingFace is unreachable the call fails — which is also
# correct: it means the request did NOT go through ModelScope
# (which would have succeeded).
pass
@unittest.skipUnless(test_level() >= 1, 'skip test in current test level')
def test_push_to_hub(self):

View File

@@ -0,0 +1,30 @@
# Copyright (c) Alibaba, Inc. and its affiliates.
import unittest
from modelscope.utils.automodel_utils import check_model_from_owner_group
class OwnerGroupPathSafetyTest(unittest.TestCase):
"""Safety checks for trusted-owner cache path recognition."""
def test_empty_name_cache_path_rejected(self):
# modelscope_hub layout: {cache}/{owner}--{name}/snapshots/{rev}
# Empty name ("iic--") must not be treated as a trusted owner path.
self.assertFalse(
check_model_from_owner_group('/cache/iic--/snapshots/v1'))
self.assertFalse(
check_model_from_owner_group('/cache/damo--/snapshots/v1'))
def test_valid_and_spoof_cache_paths(self):
self.assertTrue(
check_model_from_owner_group('/cache/iic--x/snapshots/v1'))
self.assertFalse(
check_model_from_owner_group('/cache/--iic/snapshots/v1'))
self.assertFalse(
check_model_from_owner_group(
'/cache/iic--hacked--evil/snapshots/v1'))
self.assertTrue(check_model_from_owner_group('/cache/iic/some_model'))
if __name__ == '__main__':
unittest.main()

View File

@@ -127,5 +127,40 @@ class PluginTest(unittest.TestCase):
self.assertEqual(len(result.items()), len(OFFICIAL_PLUGINS))
class ModuleNameFromModelDirTest(unittest.TestCase):
"""Regression: snapshot revision paths must not become dotted module names."""
def test_snapshot_revision_with_dots(self):
from modelscope.utils.plugins import _module_name_from_model_dir
tmp = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, tmp, ignore_errors=True)
model_dir = os.path.join(tmp, 'models',
'iic--cv_unet_skin_retouching_torch',
'snapshots', 'v1.0.4')
os.makedirs(model_dir)
name = _module_name_from_model_dir(model_dir)
self.assertNotIn('.', name)
self.assertTrue(
name.startswith('iic__cv_unet_skin_retouching_torch__v1_0_4'))
# Old Path(model_dir).stem produced 'v1.0', which importlib treats as
# package 'v1' → ModuleNotFoundError: No module named 'v1'.
self.assertNotEqual(name, 'v1.0')
self.assertFalse(name.startswith('v1'))
def test_flat_cache_layout_unchanged(self):
from modelscope.utils.plugins import _module_name_from_model_dir
tmp = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, tmp, ignore_errors=True)
model_dir = os.path.join(tmp, 'iic', 'cv_unet_skin_retouching_torch')
os.makedirs(model_dir)
self.assertEqual(
_module_name_from_model_dir(model_dir),
'cv_unet_skin_retouching_torch')
if __name__ == '__main__':
unittest.main()