diff --git a/.github/workflows/diffsynth_studio-quick-start.yml b/.github/workflows/diffsynth_studio-quick-start.yml new file mode 100644 index 000000000..7e141ce0c --- /dev/null +++ b/.github/workflows/diffsynth_studio-quick-start.yml @@ -0,0 +1,68 @@ +# diffsynth_studio quick start guard - project thin trigger. +# +# Calls the common engine .github/workflows/quick-start-template.yml; + +name: diffsynth_studio-quick-start + +concurrency: + # format() is load-bearing: a '||' between 'manual-' and + # github.run_id would short-circuit on the truthy literal and + # every dispatch would share one 'manual-' group. + group: ${{ github.event_name == 'schedule' && 'diffsynth_studio-quick-start-schedule' || format('manual-{0}', github.run_id) }} + # cancel-in-progress: false because (1) a schedule run cancelled + # mid-way loses its outcome writeback - the outcome is what makes + # the retry mechanism work, and the 'if: always()' guard isn't + # enough when the container is being torn down; (2) dispatch / PR + # runs already live in unique groups so there's nothing to cancel. + cancel-in-progress: false + +on: + schedule: + # Offset from peft's '30 */3 * * *' and llama_cpp's '45 */3 * * *'. + - cron: '20 */3 * * *' + workflow_dispatch: + # PR trigger: docs/tests changes get a guard run. paths filter avoids burning + # the self-hosted NPU runner on unrelated PRs. `pull_request` (not + # `pull_request_target`): contents: read is enough, no write-token risk. + pull_request: + branches: [main] + paths: + - 'sources/diffsynth_studio/**' + - 'tests/diffsynth_studio/**' + +permissions: + contents: read + +jobs: + diffsynth_studio-quick-start: + uses: ./.github/workflows/quick-start-template.yml + with: + # Namespaces cache keys (monitor-state-diffsynth_studio-*), artifacts + # (diffsynth_studio-quick-start-) and the test working dir + # (workflows/tests/diffsynth_studio). + project: diffsynth_studio + # Self-hosted NPU runner for the test job only; the engine pins + # the cache I/O jobs (restore-cache / publish-and-persist) to + # GitHub-hosted ubuntu-latest. + test_runner: '["linux-aarch64-a2-1"]' + image: swr.cn-south-1.myhuaweicloud.com/ascendhub/cann:9.1.0-910b-ubuntu22.04-py3.12 + # 2 h budget. Cold path is torch-npu wheels plus a ~4 GB Hugging Face + # snapshot of stable-diffusion-v1-5, then five SD inference steps. + # Do not add a container bind-mount for /root/.cache: the runner + # NFS already persists that tree. Hub files reuse the default + # /root/.cache/huggingface/hub layout. + timeout_minutes: 120 + upstream_repo: modelscope/DiffSynth-Studio + # Doc URL points to the upstream Ascend/docs repo. {0} is filled by the + # engine: PR head SHA on PR runs, 'main' otherwise. Same-repo PRs (head + # SHA exists on Ascend/docs) test the PR-version of the doc; fork PRs + # (head SHA on a fork) 404 on doc fetch - accepted, since the content + # lands on Ascend/docs post-merge. + doc_url: 'https://raw.githubusercontent.com/Ascend/docs/{0}/sources/diffsynth_studio/quick_start.md' + doc_path: https://github.com/Ascend/docs/blob/main/sources/diffsynth_studio/quick_start.md + # cwd is the repo root inside the `workflows` checkout (matches + # engine template's `working-directory: workflows`); env contract + # (MONITORED_DOC_URL / UPSTREAM_REF / NPU_READY) is injected by the + # engine. All project env prep lives in the test subclass's + # prepare_environment hook. + test_command: python -m unittest tests.diffsynth_studio.test_quick_start_ascend -v 2>&1 diff --git a/_static/images/diffsynth_studio.png b/_static/images/diffsynth_studio.png new file mode 100644 index 000000000..c0662d91e Binary files /dev/null and b/_static/images/diffsynth_studio.png differ diff --git a/index.rst b/index.rst index 838b7506e..49ec5e15a 100644 --- a/index.rst +++ b/index.rst @@ -477,6 +477,13 @@ + +
+

DiffSynth-Studio

+

ModelScope 的扩散模型引擎,支持在昇腾 NPU 上文生图。

+ +
+

lm-evaluation-harness

@@ -635,6 +642,7 @@ sources/Diffusers/index.rst sources/xdit/index.rst + sources/diffsynth_studio/index.rst sources/lm_evaluation/index.rst sources/open_clip/index.rst sources/opencompass/index.rst diff --git a/sources/diffsynth_studio/index.rst b/sources/diffsynth_studio/index.rst new file mode 100644 index 000000000..89892f3b6 --- /dev/null +++ b/sources/diffsynth_studio/index.rst @@ -0,0 +1,2 @@ +.. include:: quick_start.md + :parser: myst_parser.sphinx_ diff --git a/sources/diffsynth_studio/quick_start.md b/sources/diffsynth_studio/quick_start.md new file mode 100644 index 000000000..c0bff5089 --- /dev/null +++ b/sources/diffsynth_studio/quick_start.md @@ -0,0 +1,192 @@ +# DiffSynth-Studio + +[DiffSynth-Studio](https://github.com/modelscope/DiffSynth-Studio) 是 ModelScope 的扩散模型引擎,用于文生图等生成任务。本文在单卡昇腾上安装它,并用 Stable Diffusion 1.5 生成一张图。 + + +## 前置条件 + +### 硬件 + +Atlas 800T / 900 A2 训练系列,Ascend 910B。本文示例为单卡。 + +### 软件 + +| 类别 | 要求 | +| --- | --- | +| CANN | toolkit 与驱动已安装,并能 `source set_env.sh`。版本按 [昇腾软件配套清单](https://www.hiascend.com/developer/download/compatibility) 选择 | +| Python | 落在官方配套表范围内,并满足 DiffSynth-Studio 下限。当前正式版要求 3.10.1 及以上 | +| PyTorch | 安装官方当前推荐的 `torch` 与 `torch_npu`,CPU 轮子的版本号带 `+cpu`。见 [CANN 与 PyTorch 配套表](https://github.com/Ascend/pytorch/blob/master/COMPATIBILITY.md) 和 [PyTorch 安装包](https://www.hiascend.com/developer/software/ai-frameworks/pytorch/download) | +| DiffSynth-Studio | 当前正式版。将 `` 换成 PyPI 版本号,安装步骤见第 4 节 | +| 模型 | [stable-diffusion-v1-5/stable-diffusion-v1-5](https://huggingface.co/stable-diffusion-v1-5/stable-diffusion-v1-5) | + +阅读本文前,请先按 [快速安装昇腾环境](https://ascend.github.io/docs/sources/ascend/quick_install.html) 准备好 CANN 与驱动。 + +### 本文验证环境 + +看护镜像为 `swr.cn-south-1.myhuaweicloud.com/ascendhub/cann:9.1.0-910b-ubuntu22.04-py3.12`。该镜像自带 Python 3.12。torch 使用 CPU 轮子,版本号以 `+cpu` 结尾。这组版本不是唯一支持组合。 + +## 1. 加载 CANN 环境 + +加载 CANN,并把 `/usr/local/sbin` 加入 PATH。 + +```shell +source /usr/local/Ascend/ascend-toolkit/set_env.sh +export PATH=/usr/local/sbin:$PATH +``` + +## 2. 检查环境是否就绪 + +### 2.1 确认 NPU 在线 + +```shell +npu-smi info +``` + +输出类似: + +```text ++------------------------------------------------------------------------------------------------+ +| npu-smi 25.5.2 Version: 25.5.2 | ++---------------------------+---------------+----------------------------------------------------+ +| NPU Name | Health | Power(W) Temp(C) Hugepages-Usage(page)| +| Chip | Bus-Id | AICore(%) Memory-Usage(MB) HBM-Usage(MB) | ++===========================+===============+====================================================+ +| 5 910B4 | OK | 89.9 39 0 / 0 | +| 0 | 0000:41:00.0 | 0 0 / 0 2922 / 32768 | ++===========================+===============+====================================================+ ++---------------------------+---------------+----------------------------------------------------+ +| NPU Chip | Process id | Process name | Process memory(MB) | ++===========================+===============+====================================================+ +| No running processes found in NPU 5 | ++===========================+===============+====================================================+ +``` + +如果 `npu-smi` 找不到,回到 [快速安装昇腾环境](https://ascend.github.io/docs/sources/ascend/quick_install.html) 检查驱动与设备挂载。 + +### 2.2 确认 CANN 已加载 + +```shell +test -n "$ASCEND_HOME_PATH" +``` + +命令成功时没有输出。 + +## 3. 安装 PyTorch NPU 栈 + +安装 `torch_npu`、`torchvision`、`numpy` 与 `pyyaml`,并打印 `torch`、`torch_npu` 和 `npu_available`。 + +```shell #test id="install-torch" +python -m pip install --retries 3 \ + --index-url https://download.pytorch.org/whl/cpu \ + --extra-index-url https://pypi.org/simple \ + torch_npu torchvision numpy pyyaml +python -c "import numpy, yaml, torch, torch_npu; print('torch', torch.__version__); print('torch_npu', torch_npu.__version__); print('npu_available', torch.npu.is_available())" +``` + +完整输出较长,其中应包含: + +```shell #test-result id="install-torch" +... +torch ...+cpu +torch_npu ... +npu_available True +``` + +`npu_available` 为 True。 + +## 4. 安装 DiffSynth-Studio + +安装 `` 对应的 PyPI 正式版,然后打印设备类型和设备名。 + + + +```shell #test id="install-diffsynth" load="upstream_ref>>ref" +python -m pip install --retries 3 --no-build-isolation "diffsynth==" +python -c "import torch, torch_npu; from importlib.metadata import version; from diffsynth.core.device.npu_compatible_device import get_device_name, get_device_type; print('diffsynth', version('diffsynth')); print('device_type', get_device_type()); print('device_name', get_device_name())" +``` + +输出结果如下: + +```shell #test-result id="install-diffsynth" +... +diffsynth ... +device_type npu +device_name npu:0 +``` + +```{note} +将 `` 换成 最新的 release 版本号 +``` + +## 5. 在 NPU 上生成一张图 + +用 Python 执行以下代码。种子为 42,推理 5 步,高和宽都是 512。权重从 Hugging Face 下载。用 python 执行以下代码: + +```python #test id="generate" +import torch +import torch_npu +from diffsynth.core import ModelConfig +from diffsynth.core.device.npu_compatible_device import get_device_name +from diffsynth.pipelines.stable_diffusion import StableDiffusionPipeline + +print("device_name", get_device_name()) +pipe = StableDiffusionPipeline.from_pretrained( + torch_dtype=torch.float32, + device="npu", + model_configs=[ + ModelConfig( + model_id="stable-diffusion-v1-5/stable-diffusion-v1-5", + origin_file_pattern="text_encoder/model.safetensors", + download_source="huggingface", + ), + ModelConfig( + model_id="stable-diffusion-v1-5/stable-diffusion-v1-5", + origin_file_pattern="unet/diffusion_pytorch_model.safetensors", + download_source="huggingface", + ), + ModelConfig( + model_id="stable-diffusion-v1-5/stable-diffusion-v1-5", + origin_file_pattern="vae/diffusion_pytorch_model.safetensors", + download_source="huggingface", + ), + ], + tokenizer_config=ModelConfig( + model_id="stable-diffusion-v1-5/stable-diffusion-v1-5", + origin_file_pattern="tokenizer/", + download_source="huggingface", + ), +) +print("pipe.device", pipe.device) +print("unet.device", next(pipe.unet.parameters()).device) +image = pipe( + prompt="a photo of an astronaut riding a horse on mars, high quality, detailed", + negative_prompt="blurry, low quality, deformed", + cfg_scale=7.5, + height=512, + width=512, + seed=42, + rand_device="npu", + num_inference_steps=5, +) +image.save("image.jpg") +print("image_size", image.size) +``` + +完整输出较长,其中应包含: + +```text #test-result id="generate" +device_name npu:0 +... +pipe.device npu +unet.device npu:0 +image_size (512, 512) +... +``` + +## 6. 更多用法 + +本文演示的是单卡上用 Stable Diffusion 1.5 生成一张图。LoRA、视频生成和其他管线与社区文档相同,见 [DiffSynth-Studio 文档中心](https://diffsynth-studio-doc.readthedocs.io/zh-cn/latest/)。 diff --git a/tests/diffsynth_studio/__init__.py b/tests/diffsynth_studio/__init__.py new file mode 100644 index 000000000..630473671 --- /dev/null +++ b/tests/diffsynth_studio/__init__.py @@ -0,0 +1,29 @@ +"""Tests package marker. + +Single responsibility: inject the repo's ``tests/`` into ``sys.path`` +so that ``from doc_test.base import ...`` can resolve. + +Framework deps (mistune) are installed by the common quick-start workflow +template, not at import time here. + +Why it lives here: +* unittest treats ``tests/`` as a package; the parent ``__init__.py`` + executes before any submodule import. +* It runs before ``tests/test_*.py`` import, which is the earliest + opportunity to inject ``sys.path``. +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +# sys.path bootstrap: make ``doc_test.*`` resolvable. +# Layout: tests/diffsynth_studio/__init__.py -> parents[0]=tests/diffsynth_studio, +# parents[1]=tests, parents[2]=repo root. +_REPO_ROOT = Path(__file__).resolve().parents[2] +_TESTS_ROOT = _REPO_ROOT / 'tests' +for _p in (_TESTS_ROOT, _REPO_ROOT): + _ps = str(_p) + if _ps not in sys.path: + sys.path.insert(0, _ps) diff --git a/tests/diffsynth_studio/test_quick_start_ascend.py b/tests/diffsynth_studio/test_quick_start_ascend.py new file mode 100644 index 000000000..1ee2a5955 --- /dev/null +++ b/tests/diffsynth_studio/test_quick_start_ascend.py @@ -0,0 +1,111 @@ +"""Quick-start test: doc under test is ``sources/diffsynth_studio/quick_start.md``. + +Run: ``python -m unittest tests.diffsynth_studio.test_quick_start_ascend -v 2>&1`` + +Env (injected by the engine ``quick-start-template.yml``, triggered by +``diffsynth_studio-quick-start.yml``): ``MONITORED_DOC_URL``, ``UPSTREAM_REF``, +``NPU_READY=true`` (otherwise the class is skipped). +""" + +from __future__ import annotations + +import os +import subprocess +import unittest + +from doc_test.base import MarkdownDocTestBase +from doc_test.model_cache import ( + ensure_safetensors, + purge_huggingface_corrupt, + report_huggingface_state, + resolve_huggingface_cache, +) + + +def _is_truthy(value: str | None) -> bool: + """``'true'`` -> True (case-insensitive); anything else (including unset) -> False.""" + if not value: + return False + return value.strip().lower() == 'true' + + +def _e2e_enabled() -> bool: + """Return True when ``NPU_READY=true`` is set, releasing the skip.""" + return _is_truthy(os.environ.get('NPU_READY')) + + +class TestQuickStartAscend(MarkdownDocTestBase, unittest.TestCase): + """End-to-end test: fetch doc -> validate contract -> run ``#test-setup`` + / ``#test`` in order -> compare against ``#test-result``.""" + + # Cold Hugging Face snapshot of SD 1.5 plus five inference steps. + DEFAULT_COMMAND_TIMEOUT = 7200 + USER_AGENT = 'ascend-docs/quick-start' + ERROR_MARKERS = ( + *MarkdownDocTestBase.ERROR_MARKERS, + 'applicaiton exception', # typo in CANN's Python driver (sic) + 'ERR99999', # CANN sentinel for unrecoverable runtime failure + ) + + _MODEL_ID = 'stable-diffusion-v1-5/stable-diffusion-v1-5' + _CANN_SET_ENV = '/usr/local/Ascend/ascend-toolkit/set_env.sh' + + @classmethod + def prepare_environment(cls) -> None: + """Source CANN env once so later ``bash -c`` blocks inherit it. + + Class-level setup: run once per test class, triggered by + ``setUpClass``. Each labeled fence is a new subprocess, so a + ``source set_env.sh`` block in the document does not persist. + + Merge is overwrite, not ``setdefault``: the container image may + already ship ``LD_LIBRARY_PATH``, which would otherwise hide the + CANN increment from ``set_env.sh``. + """ + if os.path.isfile(cls._CANN_SET_ENV): + merged = subprocess.run( + ['bash', '-c', f'source {cls._CANN_SET_ENV} >/dev/null 2>&1; env'], + capture_output=True, text=True, check=True, + ) + for line in merged.stdout.splitlines(): + if '=' not in line: + continue + key, _, value = line.partition('=') + os.environ[key] = value + print('setup: sourced CANN env from set_env.sh') + else: + print( + f'setup: skipping CANN env source ({cls._CANN_SET_ENV} not present)' + ) + + path_dirs = '/usr/local/sbin:/usr/local/bin' + current_path = os.environ.get('PATH', '') + if path_dirs not in current_path: + os.environ['PATH'] = f'{path_dirs}:{current_path}' + + ensure_safetensors() + report_huggingface_state(cls._MODEL_ID) + purge_huggingface_corrupt(resolve_huggingface_cache()) + + @classmethod + def setUpClass(cls) -> None: + """Run env setup once per class. ``@unittest.skipIf`` only skips + the test *method* — ``setUpClass`` itself always runs, so the + ``if _e2e_enabled()`` guard keeps heavy setup from firing when + ``NPU_READY`` is unset. + """ + if _e2e_enabled(): + cls.prepare_environment() + + @unittest.skipIf( + not _e2e_enabled(), + 'end-to-end requires NPU runner; set NPU_READY=true', + ) + def test_runs_doc(self) -> None: + """Run the full pre_process -> parse -> execute -> post_process flow.""" + + self.run_template() + + +if __name__ == '__main__': + unittest.main()