diff --git a/.github/workflows/xdit-quick-start.yml b/.github/workflows/xdit-quick-start.yml new file mode 100644 index 000000000..c11d2dd0c --- /dev/null +++ b/.github/workflows/xdit-quick-start.yml @@ -0,0 +1,34 @@ +# xdit quick start guard - thin trigger over quick-start-template.yml; the engine owns the monitor loop, cache I/O and result publishing. +name: xdit-quick-start + +concurrency: + group: ${{ github.event_name == 'schedule' && 'xdit-quick-start-schedule' || format('manual-{0}', github.run_id) }} + cancel-in-progress: false + +on: + schedule: + - cron: '45 */6 * * *' + workflow_dispatch: + pull_request: + branches: [main] + paths: + - 'sources/xdit/**' + - 'tests/xdit/**' + +permissions: + contents: read + +jobs: + xdit-quick-start: + uses: ./.github/workflows/quick-start-template.yml + with: + project: xdit + test_runner: '["linux-aarch64-a2-2"]' + image: swr.cn-south-1.myhuaweicloud.com/ascendhub/cann:9.1.0-910b-ubuntu22.04-py3.12 + container_options: >- + --volume=/data/ci-cache/modelscope/xdit:/root/.cache/modelscope:z + timeout_minutes: 90 + upstream_repo: xdit-project/xDiT + doc_url: 'https://raw.githubusercontent.com/Ascend/docs/{0}/sources/xdit/quick_start.md' + doc_path: https://github.com/Ascend/docs/blob/main/sources/xdit/quick_start.md + test_command: python -m unittest tests.xdit.test_quick_start_ascend -v 2>&1 diff --git a/_static/images/xdit.png b/_static/images/xdit.png new file mode 100644 index 000000000..856ef9ebc Binary files /dev/null and b/_static/images/xdit.png differ diff --git a/conf.py b/conf.py index 5bc93024e..902c063be 100644 --- a/conf.py +++ b/conf.py @@ -77,7 +77,8 @@ 'sources/llama_cpp/quick_start.md', 'sources/whisper_cpp/quick_start.md', 'sources/llm_compressor/quick_start.md', - 'sources/axolotl/quick_start.md'] + 'sources/axolotl/quick_start.md', + 'sources/xdit/quick_start.md'] # -- Options for HTML output ------------------------------------------------- diff --git a/index.rst b/index.rst index 9c4356f98..e2e19f3d4 100644 --- a/index.rst +++ b/index.rst @@ -336,6 +336,13 @@

扩散模型工具链,支持昇腾 NPU 加速图像生成。

+ +
+

xDiT

+

扩散模型推理加速框架,支持昇腾 NPU 单卡/多卡加速图像生成。

+ +
+
@@ -463,6 +470,7 @@ :caption: 🎨 多模态、应用与评测 sources/Diffusers/index.rst + sources/xdit/index.rst sources/lm_evaluation/index.rst sources/open_clip/index.rst sources/opencompass/index.rst diff --git a/sources/xdit/index.rst b/sources/xdit/index.rst new file mode 100644 index 000000000..89892f3b6 --- /dev/null +++ b/sources/xdit/index.rst @@ -0,0 +1,2 @@ +.. include:: quick_start.md + :parser: myst_parser.sphinx_ diff --git a/sources/xdit/quick_start.md b/sources/xdit/quick_start.md new file mode 100644 index 000000000..63dc22d9a --- /dev/null +++ b/sources/xdit/quick_start.md @@ -0,0 +1,162 @@ +# xDiT + +xDiT(PyPI 包名 `xfuser`)是一套统一的并行推理框架。本示例在单卡昇腾 NPU 上生成第一张图。 + +## 前置条件 + +### 硬件 + +Atlas 900 A2 训练服务器(Ascend 910B),并按需完成物理机或容器内的设备挂载。单卡生成示例需 1 张卡,序列并行示例需 2 张卡。 + +### 基础软件 + +在运行本文档示例之前,你的机器上需要已经装好并可用: + +- 可用的 Python 环境 +- 可用的 CANN(参考[快速安装昇腾环境](https://ascend.github.io/docs/sources/ascend/quick_install.html)) + +本文档示例在 Python 3.12、CANN 9.1.0 环境下验证通过。 + +## 加载 CANN 环境 + +```shell +source /usr/local/Ascend/ascend-toolkit/set_env.sh +``` + +## 安装 PyTorch NPU 栈 + +参考的版本配套如下(更多组合见 [CANN 与 PyTorch 配套表](https://github.com/Ascend/pytorch/blob/master/COMPATIBILITY.md)): + +| CANN | PyTorch | `torch_npu` 安装包 | +| --- | --- | --- | +| 9.1.0 | 2.9.0 | 2.9.0.post6 | +| 9.1.0 | 2.10.0 | 2.10.0.post4 | +| 9.1.0 | 2.11.0 | 2.11.0 | + +本示例使用第一行的组合: + +```shell #test-setup id="xdit-install-torch" +pip install torch==2.9.0 torch_npu==2.9.0.post6 +``` + +## 安装 xDiT + +安装 `xfuser`(PyPI 包名),并打印安装版本: + +```shell #test id="xdit-install" +pip install xfuser +python -c "from importlib.metadata import version; print('xDiT version:', version('xfuser'))" +``` + +输出结果如下: + +```shell #test-result id="xdit-install" fuzzy='...' fuzzy='xxx' +... +xDiT version: xxx +``` + +其中 `xxx` 是安装的 xDiT(`xfuser`)版本号。 + +## 运行示例:文生图 + +安装示例使用的 Triton 和模型下载所需的 ModelScope: + +```shell #test-setup +pip install triton==3.5.0 "modelscope==1.37.0" +``` + +用 [SD3 medium](https://modelscope.cn/models/stabilityai/stable-diffusion-3-medium-diffusers) 在单卡上生成一张 256×256 的图。模型约 28 GB。 + +将下面的 Python 代码保存为 `sd3_npu.py`: + +```python +import os +import sys +import time + +import torch +import torch_npu +from modelscope import snapshot_download +from transformers import T5EncoderModel +from xfuser import xFuserArgs, xFuserStableDiffusion3Pipeline +from xfuser.config import FlexibleArgumentParser +from xfuser.core.distributed import get_runtime_state, get_world_group + +model_path = snapshot_download('stabilityai/stable-diffusion-3-medium-diffusers') + +parser = FlexibleArgumentParser(description="xFuser SD3 Arguments") +args = xFuserArgs.add_cli_args(parser).parse_args(['--model', model_path] + sys.argv[1:]) +engine_args = xFuserArgs.from_cli_args(args) +engine_config, input_config = engine_args.create_config() +local_rank = get_world_group().rank + +text_encoder_3 = T5EncoderModel.from_pretrained( + model_path, subfolder="text_encoder_3", dtype=torch.float16 +) +pipe = xFuserStableDiffusion3Pipeline.from_pretrained( + pretrained_model_name_or_path=model_path, + engine_config=engine_config, + dtype=torch.float16, + text_encoder_3=text_encoder_3, +).to(f"npu:{local_rank}") +pipe.prepare_run(input_config) + +torch.npu.synchronize(device=local_rank) +start = time.perf_counter() +output = pipe( + height=input_config.height, + width=input_config.width, + prompt=input_config.prompt, + num_inference_steps=input_config.num_inference_steps, + output_type=input_config.output_type, + guidance_scale=input_config.guidance_scale, + generator=torch.Generator(device="npu").manual_seed(input_config.seed), +) +torch.npu.synchronize(device=local_rank) +elapsed = time.perf_counter() - start + +os.makedirs("results", exist_ok=True) +if pipe.is_dp_last_group(): + world_size = get_world_group().world_size + path = f"results/sd3_npu{world_size}_ulysses{engine_args.ulysses_degree}.png" + output.images[0].save(path) + print(f"inference time: {elapsed:.2f} sec") + print(f"image saved to {path}") +get_runtime_state().destroy_distributed_env() +``` + +用 `torchrun` 在单卡上运行: + +```shell #test id="xdit-sd3-smoke" +torchrun --nproc_per_node=1 sd3_npu.py --prompt "a tiny test sketch" --height 256 --width 256 --num_inference_steps 1 --seed 42 +``` + +输出结果如下: + +```shell #test-result id="xdit-sd3-smoke" fuzzy='...' fuzzy='xxx' +... +inference time: xxx sec +image saved to results/sd3_npu1_ulysses1.png +``` + +### 多卡运行示例 + +同一个脚本、同一个模型,加 `--ulysses_degree 2` 在 2 卡上做序列并行,attention 用 SDPA 后端: + +```shell #test id="xdit-sd3-2card" +torchrun --nproc_per_node=2 sd3_npu.py --prompt "a tiny test sketch" --height 256 --width 256 --num_inference_steps 1 --seed 42 --ulysses_degree 2 --attention_backend SDPA +``` + +输出结果如下: + +```shell #test-result id="xdit-sd3-2card" fuzzy='...' fuzzy='xxx' +... +inference time: xxx sec +image saved to results/sd3_npu2_ulysses2.png +``` + +其中 `xxx` 为实际推理耗时,单位为秒。 + +## 更多用法 + +更多模型与多卡并行(PipeFusion / CFG 并行 / Ring 等)见 [xDiT examples](https://github.com/xdit-project/xDiT/tree/main/examples)。 diff --git a/tests/xdit/__init__.py b/tests/xdit/__init__.py new file mode 100644 index 000000000..3767dfb23 --- /dev/null +++ b/tests/xdit/__init__.py @@ -0,0 +1,13 @@ +"""Tests package marker (injects repo tests/ into sys.path).""" + +from __future__ import annotations + +import sys +from pathlib import Path + +_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/xdit/test_quick_start_ascend.py b/tests/xdit/test_quick_start_ascend.py new file mode 100644 index 000000000..8abe2d3e9 --- /dev/null +++ b/tests/xdit/test_quick_start_ascend.py @@ -0,0 +1,162 @@ +"""Quick-start-Ascend test: doc under test is sources/xdit/quick_start.md.""" + +from __future__ import annotations + +import os +import re +import subprocess +import unittest +from pathlib import Path + +from doc_test.base import MarkdownDocTestBase, TestCommand +from doc_test.model_cache import ( + ensure_safetensors, + purge_modelscope_corrupt, + resolve_modelscope_cache, +) + + +def _is_truthy(value: str | None) -> bool: + if not value: + return False + return value.strip().lower() == "true" + + +def _e2e_enabled() -> bool: + return _is_truthy(os.environ.get("NPU_READY")) + + +def _write_example_script(document: str) -> None: + """Write the single reader-facing Python example into the test cwd.""" + blocks = re.findall(r"(?ms)^```python[ \t]*\r?\n(.*?)^```[ \t]*$", document) + if len(blocks) != 1: + raise AssertionError(f"expected one unlabeled Python example, found {len(blocks)}") + script = blocks[0].rstrip() + "\n" + compile(script, "sd3_npu.py", "exec") + Path("sd3_npu.py").write_text(script, encoding="utf-8") + + +class TestQuickStartAscend(MarkdownDocTestBase, unittest.TestCase): + """SD3 medium smoke on 1 card + 2-card Ulysses parallel (NPU/hccl).""" + + DEFAULT_COMMAND_TIMEOUT = 1800 + USER_AGENT = "cosdt-ci-test/quick-start" + ERROR_MARKERS = ( + *MarkdownDocTestBase.ERROR_MARKERS, + ) + + _CUDA_CONSTRAINTS = ( + "cuda-toolkit<0", "cuda-python<0", "cuda-bindings<0", "cuda-core<0", "cuda-pathfinder<0", + "flashinfer-python<0", "nvidia-cublas<0", "nvidia-cuda-runtime<0", "nvidia-cuda-nvrtc<0", + "nvidia-cuda-cupti<0", "nvidia-cudnn<0", "nvidia-cudnn-frontend<0", "nvidia-cufft<0", + "nvidia-curand<0", "nvidia-cusolver<0", "nvidia-cusparse<0", "nvidia-cutlass-dsl<0", + "nvidia-cutlass-dsl-libs-base<0", "nvidia-cutlass-dsl-libs-core<0", "nvidia-cutlass-dsl-libs-cu12<0", + "nvidia-ml-py<0", "nvidia-nccl<0", "nvidia-nvjitlink<0", "nvidia-nvtx<0", + "nvidia-cublas-cu12<0", "nvidia-cuda-nvdisasm<0", "nvidia-cuda-runtime-cu12<0", "nvidia-cuda-nvrtc-cu12<0", + "nvidia-cuda-cupti-cu12<0", "nvidia-cudnn-cu12<0", "nvidia-cufft-cu12<0", "nvidia-curand-cu12<0", + "nvidia-cusolver-cu12<0", "nvidia-cusparse-cu12<0", "nvidia-cusparselt-cu12<0", "nvidia-nccl-cu12<0", + "nvidia-nvjitlink-cu12<0", "nvidia-nvtx-cu12<0", + ) + _CONSTRAINTS_FILE = "/tmp/xdit_npu_constraints.txt" + _CANN_SET_ENV = "/usr/local/Ascend/ascend-toolkit/set_env.sh" + _PROJECT_ROOT = Path("/root/xdit-test") + _GENERATED_PNGS = { + "xdit-sd3-smoke": Path("results/sd3_npu1_ulysses1.png"), + "xdit-sd3-2card": Path("results/sd3_npu2_ulysses2.png"), + } + + def _verify_generated_png(self, path: Path) -> None: + """Keep the PNG integrity check in CI, not in the quick start.""" + if not path.is_file(): + raise AssertionError(f"generated image not found: {path}") + image = path.read_bytes() + if len(image) <= 50_000: + raise AssertionError( + "generated image is suspiciously small " + f"({len(image)} bytes): {path}" + ) + if image[:8] != b"\x89PNG\r\n\x1a\n": + raise AssertionError( + "generated image is not a PNG " + f"(magic={image[:8]!r}): {path}" + ) + self.log( + f"[Step] verified generated PNG ({len(image)}B): " + f"{path}" + ) + + def _run_one(self, cmd, results, env, cwd, timeout, idx): + if isinstance(cmd, TestCommand) and cmd.id in self._GENERATED_PNGS: + super()._run_one(cmd, results, env, cwd, timeout, idx) + self._verify_generated_png(self._GENERATED_PNGS[cmd.id]) + return + return super()._run_one(cmd, results, env, cwd, timeout, idx) + + def pre_process(self) -> str: + doc = Path(__file__).resolve().parent.parent.parent / "sources" / "xdit" / "quick_start.md" + document = doc.read_text(encoding="utf-8") + _write_example_script(document) + return document + + @classmethod + def prepare_environment(cls) -> None: + 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.setdefault(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)") + + with open(cls._CONSTRAINTS_FILE, "w", encoding="utf-8") as f: + f.write(chr(10).join(cls._CUDA_CONSTRAINTS) + chr(10)) + os.environ["PIP_CONSTRAINT"] = cls._CONSTRAINTS_FILE + + # purge stale xfuser from the image so the doc install block really + # installs the PyPI release instead of keeping a baked-in copy + subprocess.run(["python", "-m", "pip", "uninstall", "-y", "xfuser"], + capture_output=True, text=True, check=False) + + # Run the documented script and write results outside the repository. + os.makedirs(cls._PROJECT_ROOT, exist_ok=True) + os.chdir(cls._PROJECT_ROOT) + print(f"setup: cwd -> {cls._PROJECT_ROOT}") + + ps = "import torch, torch_npu\nraise SystemExit(0 if torch.npu.is_available() else 1)\n" + probe = subprocess.run(["python", "-c", ps], capture_output=True, check=False) + if probe.returncode == 0: + vs = subprocess.run(["python", "-c", "import torch, torch_npu; print(torch.__version__, torch_npu.__version__)"], + capture_output=True, text=True, check=True) + print(f"setup: reusing image torch stack ({vs.stdout.strip()})") + else: + print("setup: torch probe failed, doc install-torch will install the pinned stack") + + os.environ["ASCEND_RT_VISIBLE_DEVICES"] = "0,1" + + ensure_safetensors() + try: + purge_modelscope_corrupt(resolve_modelscope_cache()) + except Exception as e: + print(f"setup: cache purge skipped ({e})") + + @classmethod + def setUpClass(cls) -> None: + 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: + self.run_template() + + +if __name__ == "__main__": + unittest.main()