mirror of
https://github.com/modelscope/modelscope.git
synced 2026-08-29 10:08:40 +02:00
merge master
This commit is contained in:
@@ -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
|
||||
|
||||
2
.github/workflows/docker-image.yml
vendored
2
.github/workflows/docker-image.yml
vendored
@@ -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
26
docker/Dockerfile.amd
Normal 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"]
|
||||
@@ -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 && \
|
||||
|
||||
@@ -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; \
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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>`
|
||||
- 支持的基础 OS:Ubuntu 和 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,不要依赖浮动分支名。
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) ---
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
filelock
|
||||
modelscope-hub>=0.0.7
|
||||
modelscope-hub>=0.2.0
|
||||
packaging
|
||||
requests>=2.25
|
||||
setuptools
|
||||
|
||||
@@ -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()
|
||||
|
||||
54
tests/hub/test_legacy_cache_guard.py
Normal file
54
tests/hub/test_legacy_cache_guard.py
Normal 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()
|
||||
103
tests/hub/test_legacy_cache_reuse.py
Normal file
103
tests/hub/test_legacy_cache_reuse.py
Normal 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()
|
||||
@@ -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):
|
||||
...
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
30
tests/utils/test_owner_group_path_safety.py
Normal file
30
tests/utils/test_owner_group_path_safety.py
Normal 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()
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user