Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
151eb37a07 | ||
|
|
a28ded243f | ||
|
|
d3c47efc4f | ||
|
|
dda88a6f86 | ||
|
|
9014bb9917 | ||
|
|
6581e2afa9 | ||
|
|
b95905deb1 |
No files matched your search
@@ -108,10 +108,7 @@ steps:
|
||||
depends_on: upload-merge-pipeline
|
||||
steps:
|
||||
- label: "Entrypoint Test with H100"
|
||||
# The suite relaunches an OmniServer per test class (~30 model loads,
|
||||
# incl. ten ~27.5 GiB Bagel loads); 60 min passed only ~1 in 3 runs
|
||||
# even on main, and vLLM 0.25 weight loading needs ~10 min more.
|
||||
timeout_in_minutes: 90
|
||||
timeout_in_minutes: 60
|
||||
commands:
|
||||
- pytest -s -v tests/entrypoints/ -m "advanced_model and cuda and H100" --run-level "advanced_model"
|
||||
agents:
|
||||
@@ -149,9 +146,7 @@ steps:
|
||||
type: DirectoryOrCreate
|
||||
|
||||
- label: "Entrypoint Test with L4"
|
||||
# 30 min left no headroom for a cold image pull (~11 min for new base
|
||||
# layers) plus per-class engine relaunches; main passed at 26/30.
|
||||
timeout_in_minutes: 45
|
||||
timeout_in_minutes: 30
|
||||
commands:
|
||||
- pytest -s -v tests/entrypoints/ -m "advanced_model and cuda and L4" --run-level "advanced_model"
|
||||
agents:
|
||||
|
||||
@@ -650,6 +650,8 @@ steps:
|
||||
tests/e2e/online_serving/test_qwen_image_edit_expansion.py
|
||||
tests/e2e/online_serving/test_qwen_image_layered_expansion.py
|
||||
tests/e2e/online_serving/test_bagel_expansion.py
|
||||
tests/e2e/online_serving/test_krea2_expansion.py
|
||||
tests/e2e/offline_inference/test_krea2_expansion.py
|
||||
-m "full_model and diffusion and H100 and not distributed_cuda"
|
||||
--run-level "full_model"
|
||||
agents:
|
||||
@@ -693,6 +695,7 @@ steps:
|
||||
pytest -sv
|
||||
tests/e2e/online_serving/test_qwen_image_expansion.py
|
||||
tests/e2e/online_serving/test_qwen_image_edit_expansion.py
|
||||
tests/e2e/offline_inference/test_krea2_expansion.py
|
||||
-m "full_model and diffusion and H100 and distributed_cuda"
|
||||
--run-level "full_model"
|
||||
agents:
|
||||
|
||||
@@ -684,3 +684,24 @@ steps:
|
||||
hostPath:
|
||||
path: /mnt/hf-cache
|
||||
type: DirectoryOrCreate
|
||||
|
||||
- label: "Diffusion · GGUF Plugin Test"
|
||||
soft_fail: true
|
||||
source_file_dependencies:
|
||||
- tests/plugin/test_gguf_diffusion.py
|
||||
- vllm_omni/diffusion/model_loader
|
||||
commands:
|
||||
- pip install "vllm-gguf-plugin>=0.0.3"
|
||||
- timeout 30m pytest -sv tests/plugin/test_gguf_diffusion.py
|
||||
agents:
|
||||
queue: "gpu_1_queue" # g6.4xlarge instance on AWS, has 1 L4 GPU
|
||||
plugins:
|
||||
- docker#v5.2.0:
|
||||
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
always-pull: true
|
||||
propagate-environment: true
|
||||
environment:
|
||||
- "HF_HOME=/fsx/hf_cache"
|
||||
- "HF_TOKEN"
|
||||
volumes:
|
||||
- "/fsx/hf_cache:/fsx/hf_cache"
|
||||
@@ -275,8 +275,3 @@ tests/.ci_generated/
|
||||
|
||||
# Local Lance precision-matching dump scripts (not part of the repo)
|
||||
lance_compare/
|
||||
|
||||
# FP8 quantization test sample images: tests/diffusion/quantization/
|
||||
# test_quantization_fp8.py saves these into the repo root for manual
|
||||
# inspection; they are byproducts and must never be committed.
|
||||
test_*_fp8.png
|
||||
@@ -16,6 +16,7 @@ Easy, fast, and cheap omni-modality model serving for everyone
|
||||
---
|
||||
|
||||
*Latest News* 🔥
|
||||
- [2026/07] We released [0.24.0](https://github.com/vllm-project/vllm-omni/releases/tag/v0.24.0) - aligned with the vLLM 0.24 release line, expanding production-ready coverage across TTS, speech, diffusion, image/video generation, and robot-policy serving, with major Omni stage runtime refactoring, diffusion request-level batching, async output materialization, quantization/cache/memory improvements, and broad CUDA/ROCm/XPU/NPU support.
|
||||
- [2026/06] We released [0.22.0](https://github.com/vllm-project/vllm-omni/releases/tag/v0.22.0) - an **omnimodal world-model** release aligned with vLLM 0.22, featuring [Nvidia Cosmos3](recipes/cosmos3/Cosmos3-Nano.md)/DreamZero world model support, expanded quantization coverage across Blackwell/NPU/XPU, TTS production improvements, new models including MiniCPM-o 4.5, MOSS-TTS, and Lance, plus RL integration with [VeRL-Omni](https://github.com/verl-project/verl-omni).
|
||||
- [2026/05] We released [0.20.0](https://github.com/vllm-project/vllm-omni/releases/tag/v0.20.0) - refreshes the serving/runtime stack for large-scale omni workloads, and improves diffusion model performance, quantization, and hardware readiness across CUDA, ROCm, MUSA, NPU, and XPU backends.
|
||||
- [2026/03] We released [0.18.0](https://github.com/vllm-project/vllm-omni/releases/tag/v0.18.0) - strengthens the core runtime through a large entrypoint refactor and scheduler/runtime cleanups, expands unified quantization and diffusion execution, broadens multimodal model coverage, and improves production readiness across audio, omni, image, video, RL, and multi-platform deployments.
|
||||
@@ -31,9 +32,9 @@ Easy, fast, and cheap omni-modality model serving for everyone
|
||||
|
||||
[vLLM](https://github.com/vllm-project/vllm) was originally designed to support large language models for text-based autoregressive generation tasks. vLLM-Omni is a framework that extends its support for omni-modality model inference and serving:
|
||||
|
||||
- **Omni-modality**: Text, image, video, and audio data processing
|
||||
- **Omni-modality**: Text, image, audio, video, and action data processing
|
||||
- **Non-autoregressive Architectures**: extend the AR support of vLLM to Diffusion Transformers (DiT) and other parallel generation models
|
||||
- **Heterogeneous outputs**: from traditional text generation to multimodal outputs
|
||||
- **Heterogeneous outputs**: from traditional text generation to multimodal and action outputs
|
||||
|
||||
<p align="center">
|
||||
<picture>
|
||||
@@ -57,9 +58,10 @@ vLLM-Omni is flexible and easy to use with:
|
||||
|
||||
vLLM-Omni seamlessly supports most popular open-source models on HuggingFace, including:
|
||||
|
||||
- **Omni-modality models** (e.g. Qwen3-Omni, Cosmos, HunyuanImage, BAGEL)
|
||||
- **Omni-modality models** (e.g. Qwen3-Omni, Cosmos3, HunyuanImage, BAGEL)
|
||||
- **TTS models** (e.g. Qwen3-TTS, VoxCPM2, Ming-Omni-TTS, CosyVoice3)
|
||||
- **Diffusion models** — image, video, and audio generation (e.g. Qwen-Image, Wan2.2, FLUX)
|
||||
- **Robot-policy and action models** (e.g. GR00T-N1.7, DreamZero-DROID, InternVLA-A1, Cosmos3 action policy)
|
||||
|
||||
## Getting Started
|
||||
|
||||
|
||||
@@ -19,4 +19,4 @@ RUN uv pip install --system ".[dev]"
|
||||
|
||||
RUN ln -sf /usr/bin/python3 /usr/bin/python
|
||||
|
||||
ENTRYPOINT []
|
||||
ENTRYPOINT []
|
||||
+6
-4
@@ -28,9 +28,9 @@ Easy, fast, and cheap omni-modality model serving for everyone
|
||||
|
||||
[vLLM](https://github.com/vllm-project/vllm) was originally designed to support large language models for text-based autoregressive generation tasks. vLLM-Omni is a framework that extends its support for omni-modality model inference and serving:
|
||||
|
||||
- **Omni-modality**: Text, image, video, and audio data processing
|
||||
- **Omni-modality**: Text, image, audio, video, and action data processing
|
||||
- **Non-autoregressive Architectures**: extend the AR support of vLLM to Diffusion Transformers (DiT) and other parallel generation models
|
||||
- **Heterogeneous outputs**: from traditional text generation to multimodal outputs
|
||||
- **Heterogeneous outputs**: from traditional text generation to multimodal and action outputs
|
||||
|
||||
<p align="center">
|
||||
<picture>
|
||||
@@ -55,8 +55,10 @@ vLLM-Omni is flexible and easy to use with:
|
||||
|
||||
vLLM-Omni seamlessly supports most popular open-source models on HuggingFace, including:
|
||||
|
||||
- Omni-modality models (e.g. Qwen2.5-Omni, Qwen3-Omni)
|
||||
- Multi-modality generation models (e.g. Qwen-Image)
|
||||
- Omni-modality models (e.g. Qwen3-Omni, Cosmos3, HunyuanImage, BAGEL)
|
||||
- TTS models (e.g. Qwen3-TTS, VoxCPM2, Ming-Omni-TTS, CosyVoice3)
|
||||
- Diffusion models — image, video, and audio generation (e.g. Qwen-Image, Wan2.2, FLUX)
|
||||
- Robot-policy and action models (e.g. GR00T-N1.7, DreamZero-DROID, InternVLA-A1, Cosmos3 action policy)
|
||||
|
||||
For more information, checkout the following:
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ This document outlines the architectural design for vLLM-Omni.
|
||||
|
||||
The primary goal of the vLLM-Omni project is to build the fastest and easiest-to-use open-source Omni-Modality model inference & serving engine. vLLM-Omni extends the original vLLM, which was created to support large language models for text-based autoregressive (AR) generation tasks. vLLM-Omni is designed to support:
|
||||
|
||||
* **Non-textual Output:** Enables the integration, efficient processing and output of various data types, including but not limited to, images, audio, and video, alongside text.
|
||||
* **Non-textual Output:** Enables the integration, efficient processing and output of various data types, including but not limited to, images, audio, video, and action trajectories, alongside text.
|
||||
* **Non-Autoregressive Structure:** Support model structure beyond autoregressive, especially Diffusion Transformer (DiT), which is widely used in visual and audio generation.
|
||||
* **Integration with vLLM Core:** Maintain compatibility and leverage existing vLLM key modules and optimizations where applicable.
|
||||
* **Extensibility:** Design a modular and flexible architecture that can easily accommodate new modalities, model architectures, and output formats.
|
||||
|
||||
@@ -34,6 +34,7 @@ th {
|
||||
| `QwenImageEditPlusPipeline` | Qwen-Image-Edit-2511 | `Qwen/Qwen-Image-Edit-2511` | ✅︎ | ✅︎ | ✅︎ | ✅︎ |
|
||||
| `GlmImagePipeline` | GLM-Image | `zai-org/GLM-Image` | ✅︎ | ✅︎ | | |
|
||||
| `ZImagePipeline` | Z-Image | `Tongyi-MAI/Z-Image-Turbo` | ✅︎ | ✅︎ | ✅︎ | ✅︎ |
|
||||
| `Krea2Pipeline` | Krea 2 (Raw + Turbo) | `krea/Krea-2-Raw`, `krea/Krea-2-Turbo` | ✅︎ | | | |
|
||||
| `WanPipeline` | Wan2.1-T2V, Wan2.2-T2V, Wan2.2-TI2V | `Wan-AI/Wan2.1-T2V-1.3B-Diffusers`, `Wan-AI/Wan2.1-T2V-14B-Diffusers`, `Wan-AI/Wan2.2-T2V-A14B-Diffusers`, `Wan-AI/Wan2.2-TI2V-5B-Diffusers` | ✅︎ | ✅︎ | ✅︎ | ✅︎ |
|
||||
| `WanImageToVideoPipeline` | Wan2.2-I2V | `Wan-AI/Wan2.2-I2V-A14B-Diffusers` | ✅︎ | ✅︎ | ✅︎ | ✅︎ |
|
||||
| `Cosmos3OmniDiffusersPipeline` | Cosmos3 T2I, T2V, I2V, V2V, T2V with sound, action policy | `nvidia/Cosmos3-Nano`, `nvidia/Cosmos3-Super` | ✅︎ | ✅︎ | ✅︎ | ✅︎ |
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 1.1 MiB |
@@ -121,6 +121,7 @@ The following tables show which models support each feature:
|
||||
| **GLM-Image** | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
| **Hidream-I1-Full** | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
| **HunyuanImage3** | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ |
|
||||
| **Krea 2** | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ (decode) | ❌ | ❌ |
|
||||
| **LongCat-Image** | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
||||
| **LongCat-Image-Edit** | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
||||
| **MagiHuman** | ❌ | ❌ | ❌ | ❓ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
||||
@@ -144,6 +145,7 @@ The following tables show which models support each feature:
|
||||
> 1. Nextstep_1(T2I) does not support cache acceleration methods such as TeaCache or Cache-DiT.
|
||||
> 2. `Tongyi-MAI/Z-Image-Turbo` and `SII-GAIR/daVinci-MagiHuman-Base-1080p` are distilled models with minimal NFEs; CFG-Parallel is not necessary.
|
||||
> 3. Cosmos3 T2I uses `Cosmos3OmniDiffusersPipeline` with `modalities=["image"]`. Model-level CPU offload is not supported; use layerwise offload.
|
||||
> 4. Krea 2 currently supports single-GPU inference plus LoRA, Cache-DiT, HSDP, CPU/layerwise offload, and VAE-patch-parallel (decode). TP/SP/CFG-Parallel are not yet wired. The few-step distilled (Turbo) checkpoint uses `is_distilled=true` (fixed timestep shift `mu=1.15`); generate at 2048x2048 by default with `num_inference_steps≈8` and `guidance_scale=0`. The Raw checkpoint uses 1024x1024, `num_inference_steps=28`, and `guidance_scale=4.5`.
|
||||
|
||||
### VideoGen
|
||||
|
||||
|
||||
@@ -9,6 +9,15 @@ for the transformer.
|
||||
|
||||
GGUF is static quantization: the quantized weights are produced before serving.
|
||||
|
||||
!!! note
|
||||
GGUF support has migrated to OOT [vllm-gguf-plugin](https://github.com/vllm-project/vllm-gguf-plugin). Make sure you have GGUF plugin installed before serving a GGUF model.
|
||||
|
||||
Before serving a GGUF model, make sure to install the [vllm-gguf-plugin](https://github.com/vllm-project/vllm-gguf-plugin):
|
||||
|
||||
```bash
|
||||
uv pip install vllm-gguf-plugin>=0.0.3
|
||||
```
|
||||
|
||||
## Hardware Support
|
||||
|
||||
| Device | Support |
|
||||
|
||||
@@ -220,7 +220,7 @@ def parse_args() -> argparse.Namespace:
|
||||
"--quantization",
|
||||
type=str,
|
||||
default=None,
|
||||
choices=["fp8", "mxfp8", "mxfp4", "mxfp4_dualscale", "int8", "gguf"],
|
||||
choices=["fp8", "mxfp8", "mxfp4", "mxfp4_dualscale", "int8"],
|
||||
help="Quantization method for the transformer. mxfp8: W8A8 MXFP8 (NPU). mxfp4: W4A4 MXFP4 (NPU). mxfp4_dualscale: W4A4 MXFP4 dual-scale + BF16 fallback mixed (NPU). fp8: online FP8 (GPU).",
|
||||
)
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ This folder provides several entrypoints for experimenting with text-to-image di
|
||||
| `black-forest-labs/FLUX.2-dev` | 1024 x 1024 | 65.7 | >80 (CPU offload required) |
|
||||
| `HunyuanImage-3.0` | 1024 x 1024 | 80.0 (TP≥3) | 160 |
|
||||
| `HiDream-I1-Full` | 1024 x 1024 | 63.7 | 57.7 |
|
||||
| `krea/Krea-2-Raw`, `krea/Krea-2-Turbo` | 1024 (Raw) / 2048 (Turbo) | — | ~30 |
|
||||
|
||||
!!! info
|
||||
*Peak VRAM: based on basic single-card usage, batch size =1, without any acceleration/optimization features. FLUX.2-dev requires `--enable-cpu-offload` on a single 80 GiB GPU.
|
||||
@@ -196,6 +197,37 @@ python examples/offline_inference/text_to_image/text_to_image.py \
|
||||
--output /output.png
|
||||
```
|
||||
|
||||
### Krea 2
|
||||
|
||||
Krea 2 is published as diffusers-format checkpoints: `krea/Krea-2-Turbo` (few-step distilled) and `krea/Krea-2-Raw`.
|
||||
The pipeline class (`Krea2Pipeline`) is auto-detected from `model_index.json`. The distilled Turbo checkpoint sets
|
||||
`is_distilled=true` and is best run at 2048x2048 with few steps and guidance disabled:
|
||||
|
||||
```bash
|
||||
# Few-step distilled (Turbo) checkpoint
|
||||
python examples/offline_inference/text_to_image/text_to_image.py \
|
||||
--model krea/Krea-2-Turbo \
|
||||
--prompt "a fox in the snow" \
|
||||
--num-inference-steps 8 \
|
||||
--guidance-scale 0.0 \
|
||||
--height 2048 --width 2048 \
|
||||
--output krea2.png
|
||||
```
|
||||
|
||||
For the Raw checkpoint, use more steps with CFG enabled:
|
||||
|
||||
```bash
|
||||
python examples/offline_inference/text_to_image/text_to_image.py \
|
||||
--model krea/Krea-2-Raw \
|
||||
--prompt "a fox in the snow" \
|
||||
--num-inference-steps 28 \
|
||||
--guidance-scale 4.5 \
|
||||
--output krea2.png
|
||||
```
|
||||
|
||||
`guidance-scale` follows the Krea 2 convention (`velocity = cond + guidance_scale * (cond - uncond)`); CFG is enabled
|
||||
whenever `guidance-scale > 0`.
|
||||
|
||||
### Batch Requests (Multiple Prompts)
|
||||
|
||||
You can pass multiple prompts in a single `Omni.generate` call. `Omni`
|
||||
|
||||
@@ -202,17 +202,11 @@ def parse_args() -> argparse.Namespace:
|
||||
"--quantization",
|
||||
type=str,
|
||||
default=None,
|
||||
choices=["fp8", "int8", "gguf"],
|
||||
choices=["fp8", "int8"],
|
||||
help="Quantization method for the transformer. "
|
||||
"Options: 'fp8' (FP8 W8A8 on Ada/Hopper, weight-only on older GPUs), 'int8' (Int8 W8A8), 'gguf' (GGUF quantized weights). "
|
||||
"Options: 'fp8' (FP8 W8A8 on Ada/Hopper, weight-only on older GPUs), 'int8' (Int8 W8A8). "
|
||||
"Default: None (no quantization, uses BF16).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--gguf-model",
|
||||
type=str,
|
||||
default=None,
|
||||
help=("GGUF file path or HF reference for transformer weights. Required when --quantization gguf is set."),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ignored-layers",
|
||||
type=str,
|
||||
@@ -402,14 +396,7 @@ def main():
|
||||
# ignored_layers is specified so the list flows through OmniDiffusionConfig
|
||||
quant_kwargs: dict[str, Any] = {}
|
||||
ignored_layers = [s.strip() for s in args.ignored_layers.split(",") if s.strip()] if args.ignored_layers else None
|
||||
if args.quantization == "gguf":
|
||||
if not args.gguf_model:
|
||||
raise ValueError("--gguf-model is required when --quantization gguf is set.")
|
||||
quant_kwargs["quantization_config"] = {
|
||||
"method": "gguf",
|
||||
"gguf_model": args.gguf_model,
|
||||
}
|
||||
elif args.quantization and ignored_layers:
|
||||
if args.quantization and ignored_layers:
|
||||
quant_kwargs["quantization_config"] = {
|
||||
"method": args.quantization,
|
||||
"ignored_layers": ignored_layers,
|
||||
|
||||
@@ -231,7 +231,7 @@ python text_to_video.py \
|
||||
- `--frame-rate`: generation FPS for pipelines that require it (e.g., LTX2).
|
||||
- `--audio-sample-rate`: audio sample rate for embedded audio (when the
|
||||
pipeline returns audio; LTX-2.3 outputs 48kHz audio).
|
||||
- `--quantization`: quantization method (`fp8` for FP8, `gguf` for GGUF).
|
||||
- `--quantization`: quantization method (such as `fp8` for FP8).
|
||||
- `--flow-shift`: scheduler flow_shift parameter.
|
||||
- `--extra-body`: JSON object of model-specific generation params, filtered against the model's declared `extra_body_params` (see [`vllm_omni/model_extras`](../../../vllm_omni/model_extras)). Used by Cosmos3 (see above).
|
||||
|
||||
|
||||
@@ -227,7 +227,7 @@ def parse_args() -> argparse.Namespace:
|
||||
"--quantization",
|
||||
type=str,
|
||||
default=None,
|
||||
choices=["fp8", "mxfp8", "mxfp4", "mxfp4_dualscale", "int8", "gguf"],
|
||||
choices=["fp8", "mxfp8", "mxfp4", "mxfp4_dualscale", "int8"],
|
||||
help="Quantization method for the transformer. mxfp8: W8A8 MXFP8 (NPU). mxfp4: W4A4 MXFP4 (NPU). mxfp4_dualscale: W4A4 MXFP4 dual-scale + BF16 fallback mixed (NPU). fp8: online FP8 (GPU).",
|
||||
)
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ recipes/
|
||||
| [`inclusionAI/Ming-flash-omni-2.0.md`](./inclusionAI/Ming-flash-omni-2.0.md) | Online serving for multimodal chat + standalone TTS | 4x H100 / 1x H100 80GB |
|
||||
| [`inclusionAI/Ming-omni-tts.md`](./inclusionAI/Ming-omni-tts.md) | Offline + online dense Ming TTS/audio generation | 1x H100 80GB / 1x AMD MI300X (ROCm 7.2) |
|
||||
| [`IndexTeam/IndexTTS-2.md`](./IndexTeam/IndexTTS-2.md) | Online serving for voice-cloned TTS with optional emotion control | 1x L4 24GB or larger CUDA GPU |
|
||||
| [`krea/Krea-2.md`](./krea/Krea-2.md) | Text-to-image (Turbo + Raw), offline + online, with LoRA | 1x H100 80GB |
|
||||
| [`LTX/LTX-2.md`](./LTX/LTX-2.md) | Text-to-video and image-to-video serving | 1x H200 141GB |
|
||||
| [`LTX/LTX-2.3.md`](./LTX/LTX-2.3.md) | Text-to-video with audio generation (22B) | 1x GPU (96GB VRAM) |
|
||||
| [`MammothModa2/MammothModa2-Preview.md`](./MammothModa2/MammothModa2-Preview.md) | Text-to-image with the shared offline image example (AR → DiT) | 1x L40S 48GB / 1x ≥40GB GPU |
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
# Krea 2 for text-to-image (base + LoRA)
|
||||
|
||||
## Summary
|
||||
|
||||
- Vendor: Krea
|
||||
- Model: `krea/Krea-2-Turbo` (few-step distilled) and `krea/Krea-2-Raw`
|
||||
- Task: Text-to-image (T2I)
|
||||
- Mode: Offline engine and online OpenAI-compatible serving
|
||||
- Maintainer: Community
|
||||
|
||||
## When to use this recipe
|
||||
|
||||
Use this recipe for a known-good starting point to run the Krea 2 single-stream
|
||||
MMDiT with vLLM-Omni on a **single 80 GB NVIDIA H100** — both the offline
|
||||
`text_to_image.py` entrypoint and the online `/v1/images/generations` route,
|
||||
including PEFT LoRA adapters. `Krea2Pipeline` is auto-detected from
|
||||
`model_index.json`; guidance is checkpoint-aware (distilled Turbo runs no-CFG,
|
||||
Raw runs CFG).
|
||||
|
||||
## References
|
||||
|
||||
- Related example under `examples/`:
|
||||
[`examples/offline_inference/text_to_image/README.md`](../../examples/offline_inference/text_to_image/README.md)
|
||||
(see the "Krea 2" section)
|
||||
- Feature support:
|
||||
[`docs/user_guide/diffusion_features.md`](../../docs/user_guide/diffusion_features.md)
|
||||
- Related PR: [#4730](https://github.com/vllm-project/vllm-omni/pull/4730)
|
||||
|
||||
## Hardware Support
|
||||
|
||||
This recipe documents a **single-GPU** CUDA layout on H100 80 GB. Add more
|
||||
platforms (ROCm / NPU) or multi-GPU layouts as community validation lands.
|
||||
|
||||
## GPU
|
||||
|
||||
### 1× H100 80GB
|
||||
|
||||
#### Environment
|
||||
|
||||
Versions from a working editable install (activate `vllm-omni/.venv` or your equivalent):
|
||||
|
||||
- OS: Linux
|
||||
- Python: 3.12
|
||||
- Driver / runtime: NVIDIA driver **595.71.05** (CUDA 13.x)
|
||||
- torch: **2.11.0+cu130**
|
||||
- vLLM: **0.24.0**
|
||||
- vLLM-Omni: editable install from this repo (Git **`960ce8f6`**)
|
||||
- Transformers: **5.12.1**
|
||||
|
||||
Krea 2 weights are ~30 GB and fit comfortably on a single 80 GB H100.
|
||||
|
||||
#### Command
|
||||
|
||||
Offline — few-step distilled (Turbo) checkpoint (2048×2048, no CFG):
|
||||
|
||||
```bash
|
||||
python examples/offline_inference/text_to_image/text_to_image.py \
|
||||
--model krea/Krea-2-Turbo \
|
||||
--prompt "a fox in the snow" \
|
||||
--num-inference-steps 8 \
|
||||
--guidance-scale 0.0 \
|
||||
--height 2048 --width 2048 \
|
||||
--output krea2_turbo.png
|
||||
```
|
||||
|
||||
Offline — Raw checkpoint (1024×1024, CFG enabled):
|
||||
|
||||
```bash
|
||||
python examples/offline_inference/text_to_image/text_to_image.py \
|
||||
--model krea/Krea-2-Raw \
|
||||
--prompt "a fox in the snow" \
|
||||
--num-inference-steps 28 \
|
||||
--guidance-scale 4.5 \
|
||||
--height 1024 --width 1024 \
|
||||
--output krea2_raw.png
|
||||
```
|
||||
|
||||
LoRA (offline) — pass a PEFT adapter with `--lora-path` / `--lora-scale`.
|
||||
`NagaSaiAbhinay/Krea-2-vllm-darkbrush-LoRA` is a vLLM-Omni-compatible repackaging
|
||||
of `krea/Krea-2-LoRA-darkbrush`; its trigger word is `monochrome ink wash style`:
|
||||
|
||||
```bash
|
||||
python examples/offline_inference/text_to_image/text_to_image.py \
|
||||
--model krea/Krea-2-Turbo \
|
||||
--prompt "a fox in the snow, monochrome ink wash style" \
|
||||
--num-inference-steps 8 --guidance-scale 0.0 \
|
||||
--lora-path NagaSaiAbhinay/Krea-2-vllm-darkbrush-LoRA --lora-scale 1.0 \
|
||||
--output krea2_darkbrush.png
|
||||
```
|
||||
|
||||
Online — serve and query the OpenAI-compatible image route:
|
||||
|
||||
```bash
|
||||
vllm serve krea/Krea-2-Turbo --omni --enforce-eager --port 8091
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:8091/v1/images/generations \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"prompt": "a fox in the snow, monochrome ink wash style",
|
||||
"size": "1024x1024",
|
||||
"num_inference_steps": 8,
|
||||
"guidance_scale": 0.0,
|
||||
"seed": 42,
|
||||
"lora": {"name": "darkbrush", "path": "NagaSaiAbhinay/Krea-2-vllm-darkbrush-LoRA", "scale": 1.0}
|
||||
}' | jq -r '.data[0].b64_json' | base64 -d > krea2_online.png
|
||||
```
|
||||
|
||||
Cache-DiT acceleration (offline) — add `--cache-backend cache_dit`:
|
||||
|
||||
```bash
|
||||
python examples/offline_inference/text_to_image/text_to_image.py \
|
||||
--model krea/Krea-2-Turbo --prompt "a fox in the snow" \
|
||||
--num-inference-steps 8 --guidance-scale 0.0 \
|
||||
--cache-backend cache_dit --output krea2_cachedit.png
|
||||
```
|
||||
|
||||
#### Verification
|
||||
|
||||
```bash
|
||||
# Offline: the script prints "Saved generated image to <path>"; confirm the file exists.
|
||||
ls -lh krea2_turbo.png
|
||||
```
|
||||
|
||||
For the online route, decode the returned `b64_json` (see the `curl` above) and
|
||||
confirm a valid 1024×1024 PNG is written.
|
||||
|
||||
#### Notes
|
||||
|
||||
- Key flags: distilled Turbo → few steps (~8) with `guidance_scale=0.0`; Raw →
|
||||
more steps (~28) with `guidance_scale>0` (CFG). `guidance-scale` follows the
|
||||
Krea 2 convention `velocity = cond + guidance_scale * (cond - uncond)`.
|
||||
- LoRA: adapters must be PEFT format (`adapter_config.json` +
|
||||
`adapter_model.safetensors`); the projections are `ReplicatedLinear`
|
||||
(264 modules). Offline uses `--lora-path` (which sets
|
||||
`OmniDiffusionSamplingParams.lora_request` / `lora_scale`); online uses the
|
||||
top-level `lora` object. Style LoRAs like darkbrush need their trigger word in
|
||||
the prompt.
|
||||
- Cache-DiT is supported; `has_separate_cfg` is checkpoint-aware (False for Turbo
|
||||
no-CFG, True for Raw CFG).
|
||||
- Known limitations: TP / SP / CFG-Parallel are not yet wired for Krea 2. HSDP,
|
||||
layerwise CPU offload, and VAE-patch-parallel (decode) are supported.
|
||||
@@ -751,3 +751,14 @@ def test_diffusion_config_field_classification_covers_current_fields():
|
||||
"distributed_executor_backend",
|
||||
} <= omni_config_module._DIFFUSION_SHARED_CONFIG_FIELDS
|
||||
assert "prompt_file_path" in omni_config_module._DIFFUSION_RUNTIME_CONFIG_FIELDS
|
||||
|
||||
|
||||
def test_diffusion_config_projection_keeps_mapping_quantization_config_serializable():
|
||||
quantization_config = {
|
||||
"method": "example_quant",
|
||||
"weights": "weights.bin",
|
||||
}
|
||||
|
||||
cfg = omni_config_module._DiffusionConfigProjection.from_kwargs(quantization_config=quantization_config)
|
||||
|
||||
assert cfg.quantization_config == quantization_config
|
||||
@@ -62,8 +62,8 @@ def _finalize_edits_form_data(template: dict[str, Any], server_model: str) -> di
|
||||
),
|
||||
pytest.param(
|
||||
True,
|
||||
{"prompt": "edit", "model": _IMAGE_EDITS_SERVER_MODEL, "layers": "2"},
|
||||
("Invalid layers", "3", "1"),
|
||||
{"prompt": "edit", "model": _IMAGE_EDITS_SERVER_MODEL, "layers": "1"},
|
||||
("Invalid layers", "2", "10"),
|
||||
400,
|
||||
id="invalid_layers",
|
||||
),
|
||||
|
||||
@@ -87,8 +87,8 @@ def _minimal_images_gen_json(omni_server: OmniServer) -> dict[str, object]:
|
||||
pytest.param(
|
||||
{"true_cfg_scale": 21.0}, ("true_cfg_scale", "less_than_equal", "20"), id="true_cfg_scale_above_max"
|
||||
),
|
||||
pytest.param({"layers": 2}, ("layers", "value_error", "3", "10"), id="layers_below_min"),
|
||||
pytest.param({"layers": 11}, ("layers", "value_error", "3", "10"), id="layers_above_max"),
|
||||
pytest.param({"layers": 1}, ("layers", "value_error", "2", "10"), id="layers_below_min"),
|
||||
pytest.param({"layers": 11}, ("layers", "value_error", "2", "10"), id="layers_above_max"),
|
||||
pytest.param({"seed": -1}, ("seed", "greater_than_equal", "0"), id="seed_negative", marks=_SKIP_ISSUE_3649),
|
||||
pytest.param(
|
||||
{"seed": 2**32},
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Unit tests for the Flux2-Klein GGUF adapter."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm_omni.diffusion.model_loader.gguf_adapters import get_gguf_adapter
|
||||
from vllm_omni.diffusion.model_loader.gguf_adapters.flux2_klein import (
|
||||
Flux2KleinGGUFAdapter,
|
||||
)
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
|
||||
|
||||
|
||||
def _make_od_config(
|
||||
model_class_name: str = "Flux2Pipeline",
|
||||
model_type: str = "flux",
|
||||
):
|
||||
return SimpleNamespace(
|
||||
model_class_name=model_class_name,
|
||||
tf_model_config={"model_type": model_type},
|
||||
)
|
||||
|
||||
|
||||
def _make_source(prefix: str = "", subfolder: str = "transformer"):
|
||||
return SimpleNamespace(prefix=prefix, subfolder=subfolder)
|
||||
|
||||
|
||||
def test_flux2_klein_adapter_selected_for_flux_family():
|
||||
adapter = get_gguf_adapter(
|
||||
"dummy.gguf",
|
||||
object(),
|
||||
_make_source(),
|
||||
_make_od_config(),
|
||||
)
|
||||
|
||||
assert isinstance(adapter, Flux2KleinGGUFAdapter)
|
||||
|
||||
|
||||
def test_flux2_klein_adapter_matches_flux_model_type():
|
||||
assert Flux2KleinGGUFAdapter.is_compatible(
|
||||
_make_od_config(model_class_name="OtherPipeline", model_type="flux-dev"),
|
||||
object(),
|
||||
_make_source(),
|
||||
)
|
||||
|
||||
|
||||
def test_flux2_klein_adapter_renames_core_projection_paths(monkeypatch: pytest.MonkeyPatch):
|
||||
import vllm_omni.diffusion.model_loader.gguf_adapters.flux2_klein as flux_module
|
||||
|
||||
monkeypatch.setattr(
|
||||
flux_module,
|
||||
"gguf_quant_weights_iterator",
|
||||
lambda _path: iter(
|
||||
[
|
||||
("img_in.qweight", torch.ones((1, 1))),
|
||||
("time_in.in_layer.qweight_type", torch.tensor(1)),
|
||||
("double_blocks.0.img_attn.norm.query_norm.weight", torch.full((1, 1), 2.0)),
|
||||
("final_layer.linear.qweight", torch.full((1, 1), 3.0)),
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
adapter = Flux2KleinGGUFAdapter(
|
||||
"dummy.gguf",
|
||||
object(),
|
||||
_make_source(),
|
||||
_make_od_config(),
|
||||
)
|
||||
|
||||
weights = list(adapter.weights_iterator())
|
||||
names = [name for name, _ in weights]
|
||||
|
||||
assert "x_embedder.qweight" in names
|
||||
assert "time_guidance_embed.timestep_embedder.linear_1.qweight_type" in names
|
||||
assert "transformer_blocks.0.attn.norm_q.weight" in names
|
||||
assert "proj_out.qweight" in names
|
||||
|
||||
|
||||
def test_flux2_klein_adapter_swaps_final_adaln_shift_and_scale(monkeypatch: pytest.MonkeyPatch):
|
||||
import vllm_omni.diffusion.model_loader.gguf_adapters.flux2_klein as flux_module
|
||||
|
||||
monkeypatch.setattr(
|
||||
flux_module,
|
||||
"gguf_quant_weights_iterator",
|
||||
lambda _path: iter(
|
||||
[
|
||||
(
|
||||
"final_layer.adaLN_modulation.1.scale",
|
||||
torch.tensor([1.0, 2.0, 3.0, 4.0]),
|
||||
)
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
adapter = Flux2KleinGGUFAdapter(
|
||||
"dummy.gguf",
|
||||
object(),
|
||||
_make_source(),
|
||||
_make_od_config(),
|
||||
)
|
||||
|
||||
weights = list(adapter.weights_iterator())
|
||||
|
||||
assert len(weights) == 1
|
||||
assert weights[0][0] == "norm_out.linear.weight"
|
||||
assert torch.equal(weights[0][1], torch.tensor([3.0, 4.0, 1.0, 2.0]))
|
||||
@@ -1,184 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from vllm_omni.diffusion.model_loader.gguf_adapters import get_gguf_adapter
|
||||
from vllm_omni.diffusion.model_loader.gguf_adapters.qwen_image import (
|
||||
QwenImageGGUFAdapter,
|
||||
)
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
|
||||
|
||||
|
||||
def _make_od_config(
|
||||
model_class_name: str = "QwenImagePipeline",
|
||||
model_type: str = "qwen_image",
|
||||
):
|
||||
return SimpleNamespace(
|
||||
model_class_name=model_class_name,
|
||||
tf_model_config={"model_type": model_type},
|
||||
)
|
||||
|
||||
|
||||
def _make_source(prefix: str = "", subfolder: str = "transformer"):
|
||||
return SimpleNamespace(prefix=prefix, subfolder=subfolder)
|
||||
|
||||
|
||||
class _FakeTransformer:
|
||||
def __init__(self) -> None:
|
||||
self._params = {
|
||||
"img_in.qweight": 1,
|
||||
"img_in.qweight_type": 1,
|
||||
"txt_in.qweight": 1,
|
||||
"txt_in.qweight_type": 1,
|
||||
"proj_out.qweight": 1,
|
||||
"proj_out.qweight_type": 1,
|
||||
"time_text_embed.timestep_embedder.linear_1.qweight": 1,
|
||||
"time_text_embed.timestep_embedder.linear_1.qweight_type": 1,
|
||||
"time_text_embed.timestep_embedder.linear_2.qweight": 1,
|
||||
"time_text_embed.timestep_embedder.linear_2.qweight_type": 1,
|
||||
"transformer_blocks.0.img_mod.1.qweight": 1,
|
||||
"transformer_blocks.0.img_mod.1.qweight_type": 1,
|
||||
"transformer_blocks.0.txt_mod.1.qweight": 1,
|
||||
"transformer_blocks.0.txt_mod.1.qweight_type": 1,
|
||||
"transformer_blocks.0.attn.to_qkv.qweight": 1,
|
||||
"transformer_blocks.0.attn.to_qkv.qweight_type": 1,
|
||||
"transformer_blocks.0.attn.add_kv_proj.qweight": 1,
|
||||
"transformer_blocks.0.attn.add_kv_proj.qweight_type": 1,
|
||||
"transformer_blocks.0.attn.to_out.weight": 1,
|
||||
"transformer_blocks.0.img_mlp.net.0.proj.qweight": 1,
|
||||
"transformer_blocks.0.img_mlp.net.0.proj.qweight_type": 1,
|
||||
}
|
||||
|
||||
def named_parameters(self):
|
||||
return list(self._params.items())
|
||||
|
||||
def named_buffers(self):
|
||||
return []
|
||||
|
||||
def get_submodule(self, _prefix: str):
|
||||
return self
|
||||
|
||||
|
||||
def test_qwen_adapter_selected_for_qwen_image_family():
|
||||
adapter = get_gguf_adapter(
|
||||
"dummy.gguf",
|
||||
_FakeTransformer(),
|
||||
_make_source(),
|
||||
_make_od_config(),
|
||||
)
|
||||
assert isinstance(adapter, QwenImageGGUFAdapter)
|
||||
|
||||
|
||||
def test_qwen_adapter_matches_multiple_pipeline_variants():
|
||||
for model_class_name in (
|
||||
"QwenImagePipeline",
|
||||
"QwenImageEditPipeline",
|
||||
"QwenImageEditPlusPipeline",
|
||||
"QwenImageLayeredPipeline",
|
||||
):
|
||||
assert QwenImageGGUFAdapter.is_compatible(
|
||||
_make_od_config(model_class_name=model_class_name),
|
||||
_FakeTransformer(),
|
||||
_make_source(),
|
||||
)
|
||||
|
||||
|
||||
def test_qwen_adapter_preserves_split_projection_names(monkeypatch: pytest.MonkeyPatch):
|
||||
import vllm_omni.diffusion.model_loader.gguf_adapters.qwen_image as qwen_image_module
|
||||
|
||||
monkeypatch.setattr(
|
||||
qwen_image_module,
|
||||
"gguf_quant_weights_iterator",
|
||||
lambda _path: iter(
|
||||
[
|
||||
("transformer_blocks.0.attn.to_q.qweight_type", 1),
|
||||
("transformer_blocks.0.attn.to_q.qweight", 2),
|
||||
("transformer_blocks.0.attn.to_k.qweight_type", 3),
|
||||
("transformer_blocks.0.attn.to_k.qweight", 4),
|
||||
("transformer_blocks.0.attn.to_out.0.qweight_type", 5),
|
||||
("transformer_blocks.0.attn.to_out.0.qweight", 6),
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
adapter = QwenImageGGUFAdapter(
|
||||
"dummy.gguf",
|
||||
_FakeTransformer(),
|
||||
_make_source(),
|
||||
_make_od_config(),
|
||||
)
|
||||
|
||||
weights = list(adapter.weights_iterator())
|
||||
|
||||
assert ("transformer_blocks.0.attn.to_q.qweight_type", 1) in weights
|
||||
assert ("transformer_blocks.0.attn.to_q.qweight", 2) in weights
|
||||
assert ("transformer_blocks.0.attn.to_k.qweight_type", 3) in weights
|
||||
assert ("transformer_blocks.0.attn.to_k.qweight", 4) in weights
|
||||
assert ("transformer_blocks.0.attn.to_out.0.qweight_type", 5) in weights
|
||||
assert ("transformer_blocks.0.attn.to_out.0.qweight", 6) in weights
|
||||
|
||||
|
||||
def test_qwen_adapter_keeps_top_level_quantized_weights(monkeypatch: pytest.MonkeyPatch):
|
||||
import vllm_omni.diffusion.model_loader.gguf_adapters.qwen_image as qwen_image_module
|
||||
|
||||
monkeypatch.setattr(
|
||||
qwen_image_module,
|
||||
"gguf_quant_weights_iterator",
|
||||
lambda _path: iter(
|
||||
[
|
||||
("img_in.qweight_type", 1),
|
||||
("img_in.qweight", 2),
|
||||
("time_text_embed.timestep_embedder.linear_1.qweight_type", 3),
|
||||
("time_text_embed.timestep_embedder.linear_1.qweight", 4),
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
adapter = QwenImageGGUFAdapter(
|
||||
"dummy.gguf",
|
||||
_FakeTransformer(),
|
||||
_make_source(),
|
||||
_make_od_config(),
|
||||
)
|
||||
|
||||
weights = list(adapter.weights_iterator())
|
||||
|
||||
assert ("img_in.qweight_type", 1) in weights
|
||||
assert ("img_in.qweight", 2) in weights
|
||||
assert ("time_text_embed.timestep_embedder.linear_1.qweight_type", 3) in weights
|
||||
assert ("time_text_embed.timestep_embedder.linear_1.qweight", 4) in weights
|
||||
|
||||
|
||||
def test_qwen_adapter_keeps_modulation_quantized_weights(monkeypatch: pytest.MonkeyPatch):
|
||||
import vllm_omni.diffusion.model_loader.gguf_adapters.qwen_image as qwen_image_module
|
||||
|
||||
monkeypatch.setattr(
|
||||
qwen_image_module,
|
||||
"gguf_quant_weights_iterator",
|
||||
lambda _path: iter(
|
||||
[
|
||||
("transformer_blocks.0.img_mod.1.qweight_type", 1),
|
||||
("transformer_blocks.0.img_mod.1.qweight", 2),
|
||||
("transformer_blocks.0.txt_mod.1.qweight_type", 3),
|
||||
("transformer_blocks.0.txt_mod.1.qweight", 4),
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
adapter = QwenImageGGUFAdapter(
|
||||
"dummy.gguf",
|
||||
_FakeTransformer(),
|
||||
_make_source(),
|
||||
_make_od_config(),
|
||||
)
|
||||
|
||||
weights = list(adapter.weights_iterator())
|
||||
|
||||
assert ("transformer_blocks.0.img_mod.1.qweight_type", 1) in weights
|
||||
assert ("transformer_blocks.0.img_mod.1.qweight", 2) in weights
|
||||
assert ("transformer_blocks.0.txt_mod.1.qweight_type", 3) in weights
|
||||
assert ("transformer_blocks.0.txt_mod.1.qweight", 4) in weights
|
||||
@@ -1,83 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Unit tests for the Z-Image GGUF adapter."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm_omni.diffusion.model_loader.gguf_adapters import get_gguf_adapter
|
||||
from vllm_omni.diffusion.model_loader.gguf_adapters.z_image import (
|
||||
ZImageGGUFAdapter,
|
||||
)
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
|
||||
|
||||
|
||||
def _make_od_config(
|
||||
model_class_name: str = "ZImagePipeline",
|
||||
model_type: str = "z_image",
|
||||
):
|
||||
return SimpleNamespace(
|
||||
model_class_name=model_class_name,
|
||||
tf_model_config={"model_type": model_type},
|
||||
)
|
||||
|
||||
|
||||
def _make_source(prefix: str = "", subfolder: str = "transformer"):
|
||||
return SimpleNamespace(prefix=prefix, subfolder=subfolder)
|
||||
|
||||
|
||||
def test_z_image_adapter_selected_for_z_image_family():
|
||||
adapter = get_gguf_adapter(
|
||||
"dummy.gguf",
|
||||
object(),
|
||||
_make_source(),
|
||||
_make_od_config(),
|
||||
)
|
||||
|
||||
assert isinstance(adapter, ZImageGGUFAdapter)
|
||||
|
||||
|
||||
def test_z_image_adapter_matches_model_type_variants():
|
||||
for model_type in ("z_image", "zimage", "z-image"):
|
||||
assert ZImageGGUFAdapter.is_compatible(
|
||||
_make_od_config(model_class_name="OtherPipeline", model_type=model_type),
|
||||
object(),
|
||||
_make_source(),
|
||||
)
|
||||
|
||||
|
||||
def test_z_image_adapter_renames_known_gguf_tensor_paths(monkeypatch: pytest.MonkeyPatch):
|
||||
import vllm_omni.diffusion.model_loader.gguf_adapters.z_image as z_image_module
|
||||
|
||||
monkeypatch.setattr(
|
||||
z_image_module,
|
||||
"gguf_quant_weights_iterator",
|
||||
lambda _path: iter(
|
||||
[
|
||||
("model.diffusion_model.final_layer.qweight", torch.ones((1, 1))),
|
||||
("model.diffusion_model.x_embedder.qweight_type", torch.tensor(1)),
|
||||
("transformer_blocks.0.attention.out.weight", torch.full((1, 1), 2.0)),
|
||||
("transformer_blocks.0.attention.qkv.qweight", torch.full((1, 1), 3.0)),
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
adapter = ZImageGGUFAdapter(
|
||||
"dummy.gguf",
|
||||
object(),
|
||||
_make_source(),
|
||||
_make_od_config(),
|
||||
)
|
||||
|
||||
weights = list(adapter.weights_iterator())
|
||||
names = [name for name, _ in weights]
|
||||
|
||||
assert "all_final_layer.2-1.qweight" in names
|
||||
assert "all_x_embedder.2-1.qweight_type" in names
|
||||
assert "transformer_blocks.0.attention.to_out.0.weight" in names
|
||||
assert "transformer_blocks.0.attention.to_qkv.qweight" in names
|
||||
@@ -16,7 +16,6 @@ from vllm.config.load import LoadConfig
|
||||
from vllm_omni.diffusion.config import get_current_diffusion_config, get_current_diffusion_config_or_none
|
||||
from vllm_omni.diffusion.data import OmniDiffusionConfig
|
||||
from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
|
||||
from vllm_omni.diffusion.model_loader.gguf_adapters import get_gguf_adapter
|
||||
from vllm_omni.diffusion.models.helios import HeliosPipeline
|
||||
from vllm_omni.diffusion.registry import initialize_model
|
||||
|
||||
@@ -111,27 +110,6 @@ def test_empty_source_prefix_keeps_full_model_strict_check():
|
||||
loader.load_weights(model)
|
||||
|
||||
|
||||
def test_qwen_model_class_selects_qwen_gguf_adapter():
|
||||
od_config = type(
|
||||
"Config",
|
||||
(),
|
||||
{
|
||||
"model_class_name": "QwenImagePipeline",
|
||||
"tf_model_config": {"model_type": "qwen_image"},
|
||||
},
|
||||
)()
|
||||
source = DiffusersPipelineLoader.ComponentSource(
|
||||
model_or_path="dummy",
|
||||
subfolder="transformer",
|
||||
revision=None,
|
||||
prefix="transformer.",
|
||||
)
|
||||
|
||||
adapter = get_gguf_adapter("dummy.gguf", object(), source, od_config)
|
||||
|
||||
assert adapter.__class__.__name__ == "QwenImageGGUFAdapter"
|
||||
|
||||
|
||||
class _ConfigAwareModel(nn.Module):
|
||||
def __init__(self, *, od_config):
|
||||
super().__init__()
|
||||
@@ -186,7 +164,6 @@ def test_load_model_custom_pipeline_sets_current_diffusion_config(monkeypatch):
|
||||
loader = DiffusersPipelineLoader(LoadConfig(), od_config)
|
||||
loader.load_weights = lambda model: None # type: ignore[assignment]
|
||||
loader._process_weights_after_loading = lambda model, target_device: None # type: ignore[assignment]
|
||||
loader._is_gguf_quantization = lambda: False # type: ignore[assignment]
|
||||
|
||||
monkeypatch.setattr(loader_mod, "resolve_obj_by_qualname", lambda _name: _ConfigAwareModel)
|
||||
monkeypatch.setattr(loader_mod.torch, "device", lambda _name: _DeviceContext("cpu"))
|
||||
|
||||
@@ -1,259 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Unit tests for GGUF-specific DiffusersPipelineLoader behavior."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
import vllm_omni.diffusion.model_loader.diffusers_loader as loader_module
|
||||
from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
|
||||
|
||||
|
||||
class _DummyModel(nn.Module):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.transformer = nn.Linear(2, 2, bias=True)
|
||||
self.vae = nn.Linear(2, 2, bias=False)
|
||||
self.register_buffer("transformer_buffer", torch.ones(1))
|
||||
self.calls: list[list[str]] = []
|
||||
self.weights_sources = [
|
||||
DiffusersPipelineLoader.ComponentSource(
|
||||
model_or_path="dummy",
|
||||
subfolder="transformer",
|
||||
revision=None,
|
||||
prefix="transformer.",
|
||||
fall_back_to_pt=True,
|
||||
),
|
||||
DiffusersPipelineLoader.ComponentSource(
|
||||
model_or_path="dummy",
|
||||
subfolder="vae",
|
||||
revision=None,
|
||||
prefix="vae.",
|
||||
fall_back_to_pt=True,
|
||||
),
|
||||
]
|
||||
|
||||
def load_weights(self, weights):
|
||||
loadable = dict(self.named_parameters())
|
||||
loadable.update(dict(self.named_buffers()))
|
||||
seen: list[str] = []
|
||||
loaded: set[str] = set()
|
||||
for name, tensor in weights:
|
||||
seen.append(name)
|
||||
if name in loadable:
|
||||
target = loadable[name]
|
||||
target.data.copy_(tensor.to(dtype=target.dtype))
|
||||
loaded.add(name)
|
||||
self.calls.append(seen)
|
||||
return loaded
|
||||
|
||||
|
||||
def _make_loader() -> DiffusersPipelineLoader:
|
||||
loader = object.__new__(DiffusersPipelineLoader)
|
||||
loader.load_config = SimpleNamespace(
|
||||
download_dir="cache-dir",
|
||||
ignore_patterns=["*.tmp"],
|
||||
)
|
||||
loader.od_config = None
|
||||
loader.quant_config = None
|
||||
loader.counter_before_loading_weights = 0.0
|
||||
loader.counter_after_loading_weights = 0.0
|
||||
return loader
|
||||
|
||||
|
||||
def test_is_gguf_quantization_accepts_dict_config():
|
||||
loader = _make_loader()
|
||||
loader.quant_config = {
|
||||
"method": "gguf",
|
||||
"gguf_model": "weights.gguf",
|
||||
}
|
||||
|
||||
assert loader._is_gguf_quantization() is True
|
||||
|
||||
|
||||
def test_is_gguf_quantization_rejects_non_gguf_dict_config():
|
||||
loader = _make_loader()
|
||||
loader.quant_config = {
|
||||
"method": "fp8",
|
||||
"gguf_model": "weights.gguf",
|
||||
}
|
||||
|
||||
assert loader._is_gguf_quantization() is False
|
||||
|
||||
|
||||
def test_is_gguf_quantization_requires_gguf_model_for_dict_config():
|
||||
loader = _make_loader()
|
||||
loader.quant_config = {"method": "gguf"}
|
||||
|
||||
with pytest.raises(ValueError, match="gguf_model"):
|
||||
loader._is_gguf_quantization()
|
||||
|
||||
|
||||
def test_is_gguf_quantization_accepts_object_config():
|
||||
loader = _make_loader()
|
||||
loader.quant_config = SimpleNamespace(
|
||||
get_name=lambda: "gguf",
|
||||
gguf_model="weights.gguf",
|
||||
)
|
||||
|
||||
assert loader._is_gguf_quantization() is True
|
||||
|
||||
|
||||
def test_is_gguf_quantization_uses_fallback_object_without_get_name():
|
||||
loader = _make_loader()
|
||||
loader.quant_config = SimpleNamespace(gguf_model="weights.gguf")
|
||||
|
||||
assert loader._is_gguf_quantization() is True
|
||||
|
||||
|
||||
def test_get_model_loadable_names_collects_parameters_and_buffers():
|
||||
loader = _make_loader()
|
||||
model = _DummyModel()
|
||||
|
||||
names = loader._get_model_loadable_names(model)
|
||||
|
||||
assert "transformer.weight" in names
|
||||
assert "transformer.bias" in names
|
||||
assert "transformer_buffer" in names
|
||||
|
||||
|
||||
def test_resolve_gguf_model_path_returns_local_file(tmp_path):
|
||||
loader = _make_loader()
|
||||
gguf_file = tmp_path / "model.gguf"
|
||||
gguf_file.write_bytes(b"gguf")
|
||||
|
||||
resolved = loader._resolve_gguf_model_path(str(gguf_file), revision=None)
|
||||
|
||||
assert resolved == str(gguf_file)
|
||||
|
||||
|
||||
def test_resolve_gguf_model_path_downloads_explicit_gguf_filename(monkeypatch: pytest.MonkeyPatch):
|
||||
loader = _make_loader()
|
||||
|
||||
monkeypatch.setattr(loader_module.os.path, "isfile", lambda _path: False)
|
||||
monkeypatch.setattr(
|
||||
loader_module,
|
||||
"hf_hub_download",
|
||||
lambda repo_id, filename, revision, cache_dir: f"{cache_dir}/{repo_id}/{filename}",
|
||||
)
|
||||
|
||||
resolved = loader._resolve_gguf_model_path("owner/repo/model-Q4_0.gguf", revision="main")
|
||||
|
||||
assert resolved == "cache-dir/owner/repo/model-Q4_0.gguf"
|
||||
|
||||
|
||||
def test_resolve_gguf_model_path_downloads_by_quant_type(monkeypatch: pytest.MonkeyPatch):
|
||||
loader = _make_loader()
|
||||
|
||||
monkeypatch.setattr(loader_module.os.path, "isfile", lambda _path: False)
|
||||
monkeypatch.setattr(
|
||||
loader_module,
|
||||
"download_gguf",
|
||||
lambda repo_id, quant_type, cache_dir, revision, ignore_patterns: f"{cache_dir}/{repo_id}/{quant_type}.gguf",
|
||||
)
|
||||
|
||||
resolved = loader._resolve_gguf_model_path("owner/repo:Q4_0", revision="main")
|
||||
|
||||
assert resolved == "cache-dir/owner/repo/Q4_0.gguf"
|
||||
|
||||
|
||||
def test_get_gguf_weights_iterator_prefixes_source_names(monkeypatch: pytest.MonkeyPatch):
|
||||
loader = _make_loader()
|
||||
loader.quant_config = SimpleNamespace(gguf_model="weights.gguf")
|
||||
loader.od_config = SimpleNamespace(revision="main")
|
||||
source = DiffusersPipelineLoader.ComponentSource(
|
||||
model_or_path="dummy",
|
||||
subfolder="transformer",
|
||||
revision=None,
|
||||
prefix="transformer.",
|
||||
)
|
||||
|
||||
class _Adapter:
|
||||
def weights_iterator(self):
|
||||
yield "weight", torch.ones((2, 2))
|
||||
yield "bias", torch.zeros(2)
|
||||
|
||||
monkeypatch.setattr(loader, "_resolve_gguf_model_path", lambda gguf_model, revision: "resolved.gguf")
|
||||
monkeypatch.setattr(loader_module, "get_gguf_adapter", lambda gguf_file, model, source, od_config: _Adapter())
|
||||
|
||||
weights = list(loader._get_gguf_weights_iterator(source, object()))
|
||||
|
||||
assert weights[0][0] == "transformer.weight"
|
||||
assert torch.equal(weights[0][1], torch.ones((2, 2)))
|
||||
assert weights[1][0] == "transformer.bias"
|
||||
assert torch.equal(weights[1][1], torch.zeros(2))
|
||||
|
||||
|
||||
def test_load_weights_with_gguf_falls_back_only_for_missing_transformer_weights(monkeypatch: pytest.MonkeyPatch):
|
||||
loader = _make_loader()
|
||||
model = _DummyModel()
|
||||
|
||||
monkeypatch.setattr(loader, "_get_weight_sources", lambda _model: tuple(model.weights_sources))
|
||||
monkeypatch.setattr(
|
||||
loader,
|
||||
"_get_gguf_weights_iterator",
|
||||
lambda source, model: iter([("transformer.weight", torch.ones((2, 2), dtype=torch.float32))]),
|
||||
)
|
||||
|
||||
def _hf_iter(source):
|
||||
if source.subfolder == "transformer":
|
||||
return iter([("transformer.bias", torch.zeros(2, dtype=torch.float32))])
|
||||
return iter([("vae.weight", torch.full((2, 2), 2.0, dtype=torch.float32))])
|
||||
|
||||
monkeypatch.setattr(loader, "_get_weights_iterator", _hf_iter)
|
||||
monkeypatch.setattr(
|
||||
loader,
|
||||
"_get_expected_parameter_names",
|
||||
lambda _model: {"transformer.weight", "transformer.bias", "vae.weight"},
|
||||
)
|
||||
|
||||
loaded = loader._load_weights_with_gguf(model)
|
||||
|
||||
assert loaded == {"transformer.weight", "transformer.bias", "vae.weight"}
|
||||
assert model.calls[0] == ["transformer.weight"]
|
||||
assert model.calls[1] == ["transformer.bias"]
|
||||
assert model.calls[2] == ["vae.weight"]
|
||||
|
||||
|
||||
def test_load_weights_with_gguf_skips_transformer_fallback_when_gguf_is_complete(monkeypatch: pytest.MonkeyPatch):
|
||||
loader = _make_loader()
|
||||
model = _DummyModel()
|
||||
|
||||
monkeypatch.setattr(loader, "_get_weight_sources", lambda _model: tuple(model.weights_sources))
|
||||
monkeypatch.setattr(
|
||||
loader,
|
||||
"_get_gguf_weights_iterator",
|
||||
lambda source, model: iter(
|
||||
[
|
||||
("transformer.weight", torch.ones((2, 2), dtype=torch.float32)),
|
||||
("transformer.bias", torch.zeros(2, dtype=torch.float32)),
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
hf_calls: list[str] = []
|
||||
|
||||
def _hf_iter(source):
|
||||
hf_calls.append(source.subfolder or "")
|
||||
if source.subfolder == "transformer":
|
||||
return iter([("transformer.bias", torch.zeros(2, dtype=torch.float32))])
|
||||
return iter([("vae.weight", torch.full((2, 2), 2.0, dtype=torch.float32))])
|
||||
|
||||
monkeypatch.setattr(loader, "_get_weights_iterator", _hf_iter)
|
||||
monkeypatch.setattr(
|
||||
loader,
|
||||
"_get_expected_parameter_names",
|
||||
lambda _model: {"transformer.weight", "transformer.bias", "vae.weight"},
|
||||
)
|
||||
|
||||
loaded = loader._load_weights_with_gguf(model)
|
||||
|
||||
assert loaded == {"transformer.weight", "transformer.bias", "vae.weight"}
|
||||
assert hf_calls == ["vae"]
|
||||
@@ -154,21 +154,6 @@ def test_component_config_with_default():
|
||||
assert config.resolve("vae.encoder") is None
|
||||
|
||||
|
||||
def test_gguf_config():
|
||||
from vllm_omni.quantization import build_quant_config
|
||||
from vllm_omni.quantization.gguf_config import DiffusionGGUFConfig
|
||||
|
||||
config = build_quant_config(
|
||||
{
|
||||
"method": "gguf",
|
||||
"gguf_model": "path/to/model.gguf",
|
||||
}
|
||||
)
|
||||
assert isinstance(config, DiffusionGGUFConfig)
|
||||
assert config.gguf_model == "path/to/model.gguf"
|
||||
assert config.get_name() == "gguf"
|
||||
|
||||
|
||||
def test_integration_string():
|
||||
from vllm_omni.diffusion.data import OmniDiffusionConfig
|
||||
|
||||
@@ -236,7 +221,7 @@ def test_transformer_config_auto_detects_modelopt_fp8():
|
||||
def test_supported_methods_includes_vllm():
|
||||
from vllm_omni.quantization import SUPPORTED_QUANTIZATION_METHODS
|
||||
|
||||
for method in ["fp8", "gguf", "awq", "gptq", "bitsandbytes", "modelopt"]:
|
||||
for method in ["fp8", "awq", "gptq", "bitsandbytes", "modelopt"]:
|
||||
assert method in SUPPORTED_QUANTIZATION_METHODS, f"{method} missing"
|
||||
|
||||
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Unit tests for diffusion GGUF quantization config and linear method."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
from vllm.model_executor.layers.linear import LinearBase, UnquantizedLinearMethod
|
||||
|
||||
from vllm_omni.quantization import build_quant_config
|
||||
from vllm_omni.quantization.gguf_config import (
|
||||
UNQUANTIZED_TYPES,
|
||||
DiffusionGGUFConfig,
|
||||
DiffusionGGUFLinearMethod,
|
||||
dequant_gemm_gguf,
|
||||
)
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
|
||||
|
||||
|
||||
def test_gguf_config_creation_and_delegation():
|
||||
config = DiffusionGGUFConfig(
|
||||
gguf_model="weights.gguf",
|
||||
unquantized_modules=["proj_out"],
|
||||
)
|
||||
|
||||
assert config.gguf_model == "weights.gguf"
|
||||
assert config.unquantized_modules == ["proj_out"]
|
||||
assert config.get_name() == "gguf"
|
||||
|
||||
|
||||
def test_build_quant_config_builds_gguf_config():
|
||||
config = build_quant_config(
|
||||
"gguf",
|
||||
gguf_model="repo/model-Q4_0.gguf",
|
||||
unquantized_modules=["norm_out"],
|
||||
)
|
||||
|
||||
assert isinstance(config, DiffusionGGUFConfig)
|
||||
assert config.gguf_model == "repo/model-Q4_0.gguf"
|
||||
assert config.unquantized_modules == ["norm_out"]
|
||||
|
||||
|
||||
def test_gguf_vllm_config_returns_diffusion_linear_method_for_linear_layers():
|
||||
linear = object.__new__(LinearBase)
|
||||
method = DiffusionGGUFConfig(unquantized_modules=[]).get_quant_method(linear, "transformer.img_in")
|
||||
|
||||
assert isinstance(method, DiffusionGGUFLinearMethod)
|
||||
|
||||
|
||||
def test_gguf_vllm_config_respects_unquantized_modules():
|
||||
linear = object.__new__(LinearBase)
|
||||
method = DiffusionGGUFConfig(unquantized_modules=["proj_out"]).get_quant_method(linear, "transformer.proj_out")
|
||||
|
||||
assert isinstance(method, UnquantizedLinearMethod)
|
||||
|
||||
|
||||
def test_gguf_vllm_config_returns_none_for_non_linear_layers():
|
||||
method = DiffusionGGUFConfig(unquantized_modules=[]).get_quant_method(torch.nn.LayerNorm(4), "norm")
|
||||
|
||||
assert method is None
|
||||
|
||||
|
||||
def test_dequant_gemm_gguf_uses_plain_matmul_for_unquantized_types():
|
||||
qweight_type = next(iter(UNQUANTIZED_TYPES))
|
||||
x = torch.tensor([[1.0, 2.0]], dtype=torch.float32)
|
||||
qweight = torch.tensor([[3.0, 4.0], [5.0, 6.0]], dtype=torch.float32)
|
||||
|
||||
out = dequant_gemm_gguf(x, qweight, qweight_type)
|
||||
|
||||
assert torch.allclose(out, x @ qweight.T)
|
||||
|
||||
|
||||
def test_diffusion_gguf_linear_method_applies_bias_on_unquantized_weight():
|
||||
qweight_type = next(iter(UNQUANTIZED_TYPES))
|
||||
method = DiffusionGGUFLinearMethod(DiffusionGGUFConfig(unquantized_modules=[]))
|
||||
layer = SimpleNamespace(
|
||||
qweight=torch.tensor([[1.0, 2.0], [3.0, 4.0]], dtype=torch.float32),
|
||||
qweight_type=SimpleNamespace(weight_type=qweight_type),
|
||||
)
|
||||
x = torch.tensor([[[1.0, 0.5]]], dtype=torch.float32)
|
||||
bias = torch.tensor([0.25, -0.5], dtype=torch.float32)
|
||||
|
||||
out = method.apply(layer, x, bias)
|
||||
|
||||
expected = x @ layer.qweight.T + bias
|
||||
assert torch.allclose(out, expected)
|
||||
|
||||
|
||||
def test_diffusion_gguf_linear_method_concatenates_sharded_outputs():
|
||||
qweight_type = next(iter(UNQUANTIZED_TYPES))
|
||||
method = DiffusionGGUFLinearMethod(DiffusionGGUFConfig(unquantized_modules=[]))
|
||||
qweight = torch.nn.Parameter(
|
||||
torch.tensor(
|
||||
[
|
||||
[1.0, 0.0],
|
||||
[0.0, 1.0],
|
||||
[2.0, 0.0],
|
||||
[0.0, 2.0],
|
||||
],
|
||||
dtype=torch.float32,
|
||||
),
|
||||
requires_grad=False,
|
||||
)
|
||||
qweight.shard_id = ["left", "right"]
|
||||
qweight.shard_offset_map = {
|
||||
"left": (0, 2, 2),
|
||||
"right": (2, 4, 2),
|
||||
}
|
||||
layer = SimpleNamespace(
|
||||
qweight=qweight,
|
||||
qweight_type=SimpleNamespace(
|
||||
shard_weight_type={
|
||||
"left": qweight_type,
|
||||
"right": qweight_type,
|
||||
}
|
||||
),
|
||||
)
|
||||
x = torch.tensor([[1.5, 2.0]], dtype=torch.float32)
|
||||
|
||||
out = method.apply(layer, x)
|
||||
|
||||
expected_q = x @ qweight[:2].T
|
||||
expected_k = x @ qweight[2:4].T
|
||||
assert torch.allclose(out, torch.cat([expected_q, expected_k], dim=-1))
|
||||
@@ -99,7 +99,6 @@ def test_candidate_model_can_point_to_offline_checkpoint_without_online_quantiza
|
||||
candidate_quantization_config_json=None,
|
||||
candidate_ignored_layers=None,
|
||||
ignored_layers=None,
|
||||
candidate_gguf_model=None,
|
||||
candidate_load_format="default",
|
||||
)
|
||||
|
||||
|
||||
@@ -27,15 +27,8 @@ WIDTH = 1024
|
||||
PROMPT = "A brown and white dog is running on the grass."
|
||||
MEAN_THRESHOLD = 3e-2
|
||||
P99_THRESHOLD = 3e-1
|
||||
# NOTE: re-baselined for the vLLM rebase (dev/vllm-align). HunyuanImage-3 is an
|
||||
# expert-parallel MoE model; the newer upstream vLLM (65a7b4628409, 585 commits
|
||||
# past the v0.24.0 tag that main tracks) shifted MoE kernel numerics, so the
|
||||
# structural-similarity metrics against the committed baseline drift to a
|
||||
# deterministic SSIM 0.9490 / PSNR 26.21 dB (the mean/p99 pixel-diff checks above
|
||||
# still pass — the image renders correctly). Thresholds relaxed to accept this
|
||||
# drift as the new bar. On main (older vLLM) the stricter 0.97 / 30.0 hold.
|
||||
SSIM_THRESHOLD = 0.94
|
||||
PSNR_THRESHOLD = 25.0
|
||||
SSIM_THRESHOLD = 0.97
|
||||
PSNR_THRESHOLD = 30.0
|
||||
|
||||
_REPO_ROOT = Path(__file__).resolve().parent.parent.parent.parent
|
||||
BASELINE_PATH = _REPO_ROOT / "tests" / "assets" / "hunyuan" / "hunyuan_baseline.png"
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
"""L4 functionality tests for the Krea 2 text-to-image diffusion pipeline (``Krea2Pipeline``).
|
||||
|
||||
Runs against the public few-step distilled checkpoint ``krea/Krea-2-Turbo`` by default (override with the
|
||||
``KREA2_MODEL`` environment variable, e.g. ``krea/Krea-2-Raw`` for the Raw checkpoint or a local diffusers
|
||||
directory). They cover a basic functional smoke plus the layerwise-CPU-offload path that the pipeline declares
|
||||
via ``SupportsComponentDiscovery`` / ``_layerwise_offload_blocks_attrs``.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from tests.helpers.mark import hardware_test
|
||||
from tests.helpers.runtime import OmniRunnerHandler
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
from vllm_omni.lora.request import LoRARequest
|
||||
from vllm_omni.lora.utils import stable_lora_int_id
|
||||
|
||||
MODEL = os.environ.get("KREA2_MODEL", "krea/Krea-2-Turbo")
|
||||
# vLLM-Omni-compatible PEFT repackaging of krea/Krea-2-LoRA-darkbrush (264 modules, r=alpha=32).
|
||||
LORA = os.environ.get("KREA2_LORA", "NagaSaiAbhinay/Krea-2-vllm-darkbrush-LoRA")
|
||||
PROMPT = "a fox in the snow, photorealistic"
|
||||
# darkbrush is a trigger-word LoRA: the trained monochrome ink-wash style only manifests when
|
||||
# its trigger phrase is present, so the LoRA test prompt appends it for a strong, stable signal.
|
||||
LORA_TRIGGER = "monochrome ink wash style"
|
||||
LORA_PROMPT = f"a fox in the snow, {LORA_TRIGGER}"
|
||||
|
||||
pytestmark = [
|
||||
pytest.mark.diffusion,
|
||||
pytest.mark.full_model,
|
||||
]
|
||||
|
||||
|
||||
def _sampling() -> OmniDiffusionSamplingParams:
|
||||
# Small resolution + few steps to keep the L4 case light. guidance_scale resolution is checkpoint-aware inside
|
||||
# the pipeline (distilled -> no-CFG, Raw -> CFG), so this stays agnostic to which checkpoint KREA2_MODEL points at.
|
||||
return OmniDiffusionSamplingParams(
|
||||
height=512,
|
||||
width=512,
|
||||
num_inference_steps=8,
|
||||
guidance_scale=0.0,
|
||||
seed=42,
|
||||
)
|
||||
|
||||
|
||||
@hardware_test(res={"cuda": "H100"})
|
||||
@pytest.mark.parametrize("omni_runner", [(MODEL, None)], indirect=True)
|
||||
def test_krea2_text_to_image_001(omni_runner_handler: OmniRunnerHandler) -> None:
|
||||
"""Basic functional smoke: a single prompt produces a decoded image."""
|
||||
omni_runner_handler.send_diffusion_request({"model": MODEL, "prompt": PROMPT, "sampling_params": _sampling()})
|
||||
|
||||
|
||||
@hardware_test(res={"cuda": "H100"})
|
||||
@pytest.mark.parametrize(
|
||||
"omni_runner",
|
||||
[(MODEL, None, {"enable_layerwise_offload": True})],
|
||||
indirect=True,
|
||||
)
|
||||
def test_krea2_layerwise_offload(omni_runner_handler: OmniRunnerHandler) -> None:
|
||||
"""Exercise layerwise CPU offload on the DiT (SupportsComponentDiscovery + _layerwise_offload_blocks_attrs)."""
|
||||
omni_runner_handler.send_diffusion_request({"model": MODEL, "prompt": PROMPT, "sampling_params": _sampling()})
|
||||
|
||||
|
||||
@hardware_test(res={"cuda": "H100"})
|
||||
@pytest.mark.cache
|
||||
@pytest.mark.parametrize("omni_runner", [(MODEL, None, {"cache_backend": "cache_dit"})], indirect=True)
|
||||
def test_krea2_cache_dit(omni_runner_handler: OmniRunnerHandler) -> None:
|
||||
"""Exercise Cache-DiT on Krea 2 via the custom Krea2Pipeline enabler (``enable_cache_for_krea2``).
|
||||
|
||||
Validates the docs' Cache-DiT support claim for Krea 2. ``has_separate_cfg`` is checkpoint-aware
|
||||
(False for the distilled Turbo no-CFG path, True for the Raw CFG path); the default Turbo checkpoint
|
||||
exercises the no-CFG branch.
|
||||
"""
|
||||
omni_runner_handler.send_diffusion_request({"model": MODEL, "prompt": PROMPT, "sampling_params": _sampling()})
|
||||
|
||||
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=2)
|
||||
@pytest.mark.parametrize(
|
||||
"omni_runner",
|
||||
# Standalone HSDP: use_hsdp=True with an explicit hsdp_shard_size shards the DiT weights
|
||||
# across 2 ranks (replicate_size defaults to 1 -> world_size = 1 * 2 = 2). HSDP must keep
|
||||
# tensor_parallel_size/data_parallel_size at 1 (see vllm_omni/diffusion/data.py __post_init__).
|
||||
# Also validated locally at 8 GPUs (hsdp_shard_size=8); CI shard provisions 2.
|
||||
[(MODEL, None, {"use_hsdp": True, "hsdp_shard_size": 2})],
|
||||
indirect=True,
|
||||
)
|
||||
def test_krea2_hsdp(omni_runner_handler: OmniRunnerHandler) -> None:
|
||||
"""Exercise Hybrid Sharded Data Parallel (HSDP) weight sharding on the DiT across 2 GPUs.
|
||||
|
||||
Validates the docs' HSDP-support claim for Krea 2. Scheduled on the multi-GPU (distributed_cuda)
|
||||
shard by ``num_cards=2``; skipped automatically on boxes with fewer than 2 CUDA devices.
|
||||
"""
|
||||
omni_runner_handler.send_diffusion_request({"model": MODEL, "prompt": PROMPT, "sampling_params": _sampling()})
|
||||
|
||||
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=2)
|
||||
@pytest.mark.parametrize(
|
||||
"omni_runner",
|
||||
# VAE patch-parallel decode: partition the VAE decode across the TP world. Mirrors the online
|
||||
# "tp_vae_patch" case (tensor_parallel_size=2 + vae_patch_parallel_size=2) from the wan2.2/z-image
|
||||
# expansion suites, so vae_patch_parallel_size must equal the world size established by TP.
|
||||
# Also validated locally at 8 GPUs (tp=8, vae_patch=8); CI shard provisions 2.
|
||||
[(MODEL, None, {"tensor_parallel_size": 2, "vae_patch_parallel_size": 2})],
|
||||
indirect=True,
|
||||
)
|
||||
def test_krea2_vae_patch_parallel(omni_runner_handler: OmniRunnerHandler) -> None:
|
||||
"""Exercise VAE patch-parallel (decode) across 2 GPUs.
|
||||
|
||||
Validates the docs' VAE-patch-parallel (decode) support claim for Krea 2. Scheduled on the
|
||||
multi-GPU (distributed_cuda) shard by ``num_cards=2``; skipped automatically on boxes with
|
||||
fewer than 2 CUDA devices.
|
||||
"""
|
||||
omni_runner_handler.send_diffusion_request({"model": MODEL, "prompt": PROMPT, "sampling_params": _sampling()})
|
||||
|
||||
|
||||
def _generate(handler: OmniRunnerHandler, lora_request: LoRARequest | None, lora_scale: float = 1.0) -> np.ndarray:
|
||||
sp = _sampling()
|
||||
sp.lora_request = lora_request
|
||||
sp.lora_scale = lora_scale
|
||||
resp = handler.send_diffusion_request({"model": MODEL, "prompt": LORA_PROMPT, "sampling_params": sp})
|
||||
return np.asarray(resp.images[0].convert("RGB"), dtype=np.int16)
|
||||
|
||||
|
||||
@hardware_test(res={"cuda": "H100"})
|
||||
@pytest.mark.parametrize("omni_runner", [(MODEL, None)], indirect=True)
|
||||
def test_krea2_lora(omni_runner_handler: OmniRunnerHandler) -> None:
|
||||
"""Validate diffusion LoRA on Krea 2: visible effect, scale sensitivity, clean deactivation.
|
||||
|
||||
Uses ``LORA`` (a vLLM-Omni-compatible PEFT repackaging of ``krea/Krea-2-LoRA-darkbrush``,
|
||||
264 modules matched via the ReplicatedLinear projections) passed per-request through
|
||||
``OmniDiffusionSamplingParams.lora_request`` / ``lora_scale`` — the same fields the
|
||||
diffusion LoRA manager reads to activate/deactivate the adapter.
|
||||
"""
|
||||
lora_request = LoRARequest(lora_name="darkbrush", lora_int_id=stable_lora_int_id(LORA), lora_path=LORA)
|
||||
|
||||
baseline = _generate(omni_runner_handler, None)
|
||||
img_1x = _generate(omni_runner_handler, lora_request, lora_scale=1.0)
|
||||
img_2x = _generate(omni_runner_handler, lora_request, lora_scale=2.0)
|
||||
restored = _generate(omni_runner_handler, None)
|
||||
|
||||
diff_1x = np.abs(baseline - img_1x).mean()
|
||||
diff_2x = np.abs(baseline - img_2x).mean()
|
||||
diff_restored = np.abs(baseline - restored).mean()
|
||||
|
||||
# (a) Adapter has a visible effect at both scales.
|
||||
assert diff_1x > 0.5, f"LoRA scale=1.0 had no visible effect: diff={diff_1x}"
|
||||
assert diff_2x > 0.5, f"LoRA scale=2.0 had no visible effect: diff={diff_2x}"
|
||||
# (b) Scale changes the output (stronger adapter -> larger perturbation).
|
||||
assert not np.isclose(diff_1x, diff_2x, atol=1.0), f"LoRA scale had no effect: 1x={diff_1x:.2f}, 2x={diff_2x:.2f}"
|
||||
# (c) Passing lora_request=None cleanly deactivates: same seed -> back to baseline (modulo fp drift).
|
||||
assert diff_restored < 5.0, f"LoRA did not deactivate cleanly: diff_restored={diff_restored:.2f}"
|
||||
@@ -0,0 +1,152 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
"""L4 online-serving tests for the Krea 2 text-to-image diffusion pipeline.
|
||||
|
||||
Exercises the OpenAI-compatible ``/v1/images/generations`` route (served via
|
||||
``vllm serve <model> --omni --enforce-eager``) rather than the offline engine path
|
||||
covered by ``tests/e2e/offline_inference/test_krea2_expansion.py``.
|
||||
|
||||
Runs against the public few-step distilled checkpoint ``krea/Krea-2-Turbo`` by default
|
||||
(override with the ``KREA2_MODEL`` environment variable, e.g. ``krea/Krea-2-Raw`` for the
|
||||
Raw checkpoint or a local diffusers directory). Krea 2 shares the Qwen-Image VAE and a
|
||||
similar single-stream MMDiT, so this test mirrors the structure/fixtures/markers of
|
||||
``test_qwen_image_expansion.py``.
|
||||
|
||||
Coverage mirrors the offline suite: a basic functional smoke plus the layerwise-CPU-offload
|
||||
path that the pipeline declares via ``SupportsComponentDiscovery`` /
|
||||
``_layerwise_offload_blocks_attrs``. Sizing is kept light (512x512, few steps) so the case
|
||||
fits single-GPU H100 CI rather than the full 2048x2048 distilled resolution.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import os
|
||||
from io import BytesIO
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import requests
|
||||
from PIL import Image
|
||||
|
||||
from tests.helpers.mark import hardware_marks
|
||||
from tests.helpers.runtime import OmniServer, OmniServerParams, OpenAIClientHandler, dummy_messages_from_mix_data
|
||||
|
||||
pytestmark = [pytest.mark.diffusion, pytest.mark.full_model]
|
||||
|
||||
MODEL = os.environ.get("KREA2_MODEL", "krea/Krea-2-Turbo")
|
||||
# vLLM-Omni-compatible PEFT repackaging of krea/Krea-2-LoRA-darkbrush (264 modules, r=alpha=32).
|
||||
LORA = os.environ.get("KREA2_LORA", "NagaSaiAbhinay/Krea-2-vllm-darkbrush-LoRA")
|
||||
PROMPT = "a fox in the snow, photorealistic"
|
||||
# darkbrush is a trigger-word LoRA; include its trigger phrase so the LoRA case has a strong signal.
|
||||
LORA_PROMPT = "a fox in the snow, monochrome ink wash style"
|
||||
|
||||
SINGLE_CARD_FEATURE_MARKS = hardware_marks(res={"cuda": "H100"})
|
||||
|
||||
|
||||
def _get_diffusion_feature_cases(model: str):
|
||||
"""Return single-card L4 online-serving cases for Krea 2.
|
||||
|
||||
Mirrors the offline suite: a basic functional smoke plus layerwise CPU offload.
|
||||
"""
|
||||
return [
|
||||
# Basic functional smoke (single-card).
|
||||
pytest.param(
|
||||
OmniServerParams(
|
||||
model=model,
|
||||
server_args=["--enforce-eager"],
|
||||
),
|
||||
id="single_card_basic",
|
||||
marks=SINGLE_CARD_FEATURE_MARKS,
|
||||
),
|
||||
# Layerwise CPU offload (single-card): SupportsComponentDiscovery +
|
||||
# _layerwise_offload_blocks_attrs.
|
||||
pytest.param(
|
||||
OmniServerParams(
|
||||
model=model,
|
||||
server_args=["--enforce-eager", "--enable-layerwise-offload"],
|
||||
),
|
||||
id="single_card_layerwise_offload",
|
||||
marks=SINGLE_CARD_FEATURE_MARKS,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"omni_server",
|
||||
_get_diffusion_feature_cases(MODEL),
|
||||
indirect=True,
|
||||
)
|
||||
def test_krea2(omni_server: OmniServer, openai_client: OpenAIClientHandler):
|
||||
"""Serve Krea 2 and validate the OpenAI-compatible image-generation route.
|
||||
|
||||
guidance_scale resolution is checkpoint-aware inside the pipeline (distilled -> no-CFG,
|
||||
Raw -> CFG), so guidance_scale=0.0 stays agnostic to which checkpoint KREA2_MODEL points at.
|
||||
Validation is delegated to assert_diffusion_response, which checks output dimensions and
|
||||
basic correctness.
|
||||
"""
|
||||
messages = dummy_messages_from_mix_data(content_text=PROMPT)
|
||||
request_config = {
|
||||
"model": omni_server.model,
|
||||
"messages": messages,
|
||||
"extra_body": {
|
||||
"height": 512,
|
||||
"width": 512,
|
||||
"num_inference_steps": 8,
|
||||
"guidance_scale": 0.0,
|
||||
"seed": 42,
|
||||
},
|
||||
}
|
||||
openai_client.send_diffusion_request(request_config)
|
||||
|
||||
|
||||
def _basic_lora_payload(lora: dict | None = None) -> dict:
|
||||
payload = {
|
||||
"prompt": LORA_PROMPT,
|
||||
"n": 1,
|
||||
"size": "512x512",
|
||||
"num_inference_steps": 8,
|
||||
"guidance_scale": 0.0,
|
||||
"seed": 42,
|
||||
}
|
||||
if lora is not None:
|
||||
payload["lora"] = lora
|
||||
return payload
|
||||
|
||||
|
||||
def _post_image(server: OmniServer, payload: dict) -> np.ndarray:
|
||||
url = f"http://{server.host}:{server.port}/v1/images/generations"
|
||||
resp = requests.post(url, json=payload, headers={"Authorization": "Bearer EMPTY"}, timeout=900)
|
||||
resp.raise_for_status()
|
||||
b64 = resp.json()["data"][0]["b64_json"]
|
||||
img = Image.open(BytesIO(base64.b64decode(b64)))
|
||||
img.load()
|
||||
return np.asarray(img.convert("RGB"), dtype=np.int16)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"omni_server",
|
||||
[
|
||||
pytest.param(
|
||||
OmniServerParams(model=MODEL, server_args=["--enforce-eager"]),
|
||||
id="single_card_lora",
|
||||
marks=SINGLE_CARD_FEATURE_MARKS,
|
||||
)
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
def test_krea2_lora(omni_server: OmniServer):
|
||||
"""Serve Krea 2 and validate per-request LoRA over ``/v1/images/generations``.
|
||||
|
||||
Passes ``LORA`` via the top-level ``lora`` object (``{name, path, scale}``) the diffusion
|
||||
image route parses into a ``LoRARequest`` — no server-side LoRA flag needed. Confirms the
|
||||
adapter has a visible effect and that omitting ``lora`` cleanly restores the base output.
|
||||
"""
|
||||
baseline = _post_image(omni_server, _basic_lora_payload())
|
||||
with_lora = _post_image(omni_server, _basic_lora_payload({"name": "darkbrush", "path": LORA, "scale": 1.0}))
|
||||
restored = _post_image(omni_server, _basic_lora_payload())
|
||||
|
||||
diff_lora = np.abs(baseline - with_lora).mean()
|
||||
diff_restored = np.abs(baseline - restored).mean()
|
||||
|
||||
assert diff_lora > 0.5, f"LoRA had no visible effect over the serving path: diff={diff_lora}"
|
||||
assert diff_restored < 5.0, f"LoRA did not deactivate cleanly: diff_restored={diff_restored:.2f}"
|
||||
@@ -200,22 +200,17 @@ def _assert_realtime_smoke(result: dict) -> None:
|
||||
|
||||
def _assert_realtime_accuracy(
|
||||
result: dict,
|
||||
threshold: float = 0.8,
|
||||
whisper_model_size: str = "large-v3",
|
||||
) -> None:
|
||||
"""Assert that whisper transcription of audio output matches model text.
|
||||
|
||||
Args:
|
||||
result: Roundtrip result dict from ``_run_realtime_audio_roundtrip``.
|
||||
threshold: Minimum cosine similarity between whisper text and model text.
|
||||
Default 0.8 is appropriate for sync mode (exact match expected).
|
||||
Use 0.65 for async_chunk mode where chunked upload with pacing
|
||||
delays introduces inherent variability in audio codec quality.
|
||||
whisper_model_size: Whisper model used to transcribe the generated audio
|
||||
for the accuracy check. Defaults to ``large-v3``: the default
|
||||
``small`` model mishears short Chinese TTS clips (observed:
|
||||
北京→韦京 and a dropped leading sentence, sim=0.443), which
|
||||
caused spurious sim<threshold failures under async_chunk codec
|
||||
caused spurious sim<0.8 failures under async_chunk codec
|
||||
variability even though audio generation was correct. large-v3
|
||||
transcribes these clips reliably, so a failure here now points
|
||||
at the model, not the ASR grader.
|
||||
@@ -228,7 +223,7 @@ def _assert_realtime_accuracy(
|
||||
assert whisper_text, "Whisper returned empty string for synthesized output audio"
|
||||
|
||||
sim = cosine_similarity_text(whisper_text.lower(), final_text.lower())
|
||||
assert sim > threshold, (
|
||||
assert sim > 0.8, (
|
||||
f"Output audio transcript should match model text (sim={sim:.3f}): "
|
||||
f"whisper={whisper_text!r}, model_text={final_text!r}"
|
||||
)
|
||||
@@ -255,12 +250,7 @@ class TestQwen3OmniRealtimeWebSocket:
|
||||
)
|
||||
|
||||
_assert_realtime_smoke(result)
|
||||
# Use threshold=0.65 for async_chunk: chunked upload with send_delay_ms
|
||||
# introduces inherent variability in audio codec quality. Whisper
|
||||
# transcription is nondeterministic and may miss 1-2 words in async
|
||||
# mode even when model text generation is correct.
|
||||
# Sync mode uses the default 0.8 (exact match expected).
|
||||
_assert_realtime_accuracy(result, threshold=0.65)
|
||||
_assert_realtime_accuracy(result)
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.omni
|
||||
|
||||
@@ -359,3 +359,15 @@ def test_resolve_stage_configs_injects_additional_config_into_diffusion_stage(mo
|
||||
|
||||
assert not hasattr(stage_configs[0].engine_args, "additional_config")
|
||||
assert stage_configs[1].engine_args.additional_config == {"torchair_graph_config": {"enabled": True}}
|
||||
|
||||
|
||||
def test_default_stage_config_includes_quantization_config():
|
||||
"""Ensure structured quantization_config survives default diffusion-stage creation."""
|
||||
quantization_config = {
|
||||
"method": "example_quant",
|
||||
"weights": "weights.bin",
|
||||
}
|
||||
|
||||
stage_cfg = AsyncOmniEngine._create_default_diffusion_stage_cfg({"quantization_config": quantization_config})[0]
|
||||
|
||||
assert stage_cfg["engine_args"]["quantization_config"] == quantization_config
|
||||
@@ -2826,7 +2826,7 @@ def iter_omni_server(
|
||||
# be ideal to cleanup consistently everywhere.
|
||||
original_model = model_prefix + params.model
|
||||
model = original_model
|
||||
if run_level == "core_model":
|
||||
if run_level == "core_model" and request.node.get_closest_marker("diffusion"):
|
||||
model = resolve_tiny_model_path(model)
|
||||
port = params.port
|
||||
stage_config_path = stage_config_path_for_run_level(params.stage_config_path, run_level)
|
||||
@@ -2917,7 +2917,7 @@ def iter_omni_runner(
|
||||
extra_omni_kwargs = dict(extra) if extra is not None else {}
|
||||
stage_config_path = stage_config_path_for_run_level(stage_config_path, run_level)
|
||||
model = model_prefix + model
|
||||
if run_level == "core_model":
|
||||
if run_level == "core_model" and request.node.get_closest_marker("diffusion"):
|
||||
model = resolve_tiny_model_path(model)
|
||||
with OmniRunner(model, seed=42, stage_configs_path=stage_config_path, **extra_omni_kwargs) as runner:
|
||||
print("OmniRunner started successfully")
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
from collections import defaultdict
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm_omni.model_executor.models.ming_tts.constants import LATENT_DIM, PATCH_SIZE
|
||||
from vllm_omni.model_executor.stage_input_processors.ming_tts import (
|
||||
MING_EMIT_PATCH_COUNT_KEY,
|
||||
llm2audio_vae_async_chunk,
|
||||
)
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
|
||||
def _make_transfer_manager(*, chunk_size: int = 5, initial_chunk_size: int = 2):
|
||||
return SimpleNamespace(
|
||||
code_prompt_token_ids=defaultdict(list),
|
||||
put_req_chunk=defaultdict(int),
|
||||
request_payload={},
|
||||
connector=SimpleNamespace(
|
||||
config={
|
||||
"extra": {
|
||||
"latent_chunk_size": chunk_size,
|
||||
"initial_latent_chunk_size": initial_chunk_size,
|
||||
"latent_left_context": 0,
|
||||
}
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _make_request(req_id: str = "req", *, finished: bool = False):
|
||||
return SimpleNamespace(
|
||||
external_req_id=req_id,
|
||||
is_finished=lambda: finished,
|
||||
)
|
||||
|
||||
|
||||
def _make_output(value: float):
|
||||
return {
|
||||
"ming_has_patch": torch.tensor([1], dtype=torch.bool),
|
||||
"ming_latent_patch": torch.full((1, PATCH_SIZE, LATENT_DIM), value),
|
||||
}
|
||||
|
||||
|
||||
def _append_patch(tm, req_id: str, idx: int, *, finished: bool = False):
|
||||
return llm2audio_vae_async_chunk(
|
||||
tm,
|
||||
_make_output(float(idx)),
|
||||
_make_request(req_id, finished=finished),
|
||||
is_finished=finished,
|
||||
)
|
||||
|
||||
|
||||
def test_ming_async_chunk_emits_small_initial_chunk_then_steady_chunks():
|
||||
tm = _make_transfer_manager(chunk_size=5, initial_chunk_size=2)
|
||||
|
||||
assert _append_patch(tm, "req", 0) is None
|
||||
|
||||
first = _append_patch(tm, "req", 1)
|
||||
assert first is not None
|
||||
assert first.latent.shape == (2, PATCH_SIZE, LATENT_DIM)
|
||||
assert first.kv_metadata[MING_EMIT_PATCH_COUNT_KEY] == 2
|
||||
assert first.meta.finished.item() is False
|
||||
|
||||
for idx in range(2, 6):
|
||||
assert _append_patch(tm, "req", idx) is None
|
||||
|
||||
second = _append_patch(tm, "req", 6)
|
||||
assert second is not None
|
||||
assert second.latent.shape == (5, PATCH_SIZE, LATENT_DIM)
|
||||
assert second.kv_metadata[MING_EMIT_PATCH_COUNT_KEY] == 5
|
||||
|
||||
|
||||
def test_ming_async_chunk_flushes_short_final_chunk():
|
||||
tm = _make_transfer_manager(chunk_size=5, initial_chunk_size=2)
|
||||
|
||||
payload = _append_patch(tm, "req", 0, finished=True)
|
||||
|
||||
assert payload is not None
|
||||
assert payload.latent.shape == (1, PATCH_SIZE, LATENT_DIM)
|
||||
assert payload.kv_metadata[MING_EMIT_PATCH_COUNT_KEY] == 1
|
||||
assert payload.meta.finished.item() is True
|
||||
|
||||
|
||||
def test_ming_async_chunk_flushes_leftover_after_initial_chunk():
|
||||
tm = _make_transfer_manager(chunk_size=5, initial_chunk_size=2)
|
||||
|
||||
assert _append_patch(tm, "req", 0) is None
|
||||
assert _append_patch(tm, "req", 1) is not None
|
||||
assert _append_patch(tm, "req", 2) is None
|
||||
|
||||
final = _append_patch(tm, "req", 3, finished=True)
|
||||
|
||||
assert final is not None
|
||||
assert final.latent.shape == (2, PATCH_SIZE, LATENT_DIM)
|
||||
assert final.kv_metadata[MING_EMIT_PATCH_COUNT_KEY] == 2
|
||||
assert final.meta.finished.item() is True
|
||||
@@ -56,6 +56,7 @@ EXCLUDED_MODELS = [
|
||||
"NextStep11Pipeline",
|
||||
"FluxPipeline",
|
||||
"FluxDMD2Pipeline",
|
||||
"Krea2Pipeline",
|
||||
"QwenImageDMD2Pipeline",
|
||||
"OmniGen2Pipeline",
|
||||
"HeliosPipeline",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
@@ -0,0 +1,154 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""End-to-end tests for GGUF quantization on diffusion models.
|
||||
|
||||
Validates that GGUF-quantized diffusion models generate valid images and
|
||||
use less peak GPU memory than BF16 baseline.
|
||||
|
||||
Requires vllm-omni to be installed alongside the plugin.
|
||||
|
||||
Usage:
|
||||
pytest tests/diffusion/test_gguf_memory.py -v
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import NamedTuple
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from tests.helpers.env import DeviceMemoryMonitor
|
||||
from tests.helpers.mark import hardware_test
|
||||
from tests.helpers.runtime import OmniRunner
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
from vllm_omni.outputs import OmniRequestOutput
|
||||
from vllm_omni.platforms import current_omni_platform
|
||||
|
||||
|
||||
class DiffusionGGUFTestConfig(NamedTuple):
|
||||
artifact_prefix: str
|
||||
gguf_model: str
|
||||
hf_model: str
|
||||
min_cosine_similarity: float = 0.95
|
||||
|
||||
|
||||
Z_IMAGE_CONFIG = DiffusionGGUFTestConfig(
|
||||
artifact_prefix="zimage",
|
||||
hf_model="Tongyi-MAI/Z-Image-Turbo",
|
||||
gguf_model="unsloth/Z-Image-Turbo-GGUF:Q4_0",
|
||||
)
|
||||
|
||||
|
||||
FLUX_CONFIG = DiffusionGGUFTestConfig(
|
||||
artifact_prefix="flux2_klein",
|
||||
hf_model="black-forest-labs/FLUX.2-klein-4B",
|
||||
gguf_model="unsloth/FLUX.2-klein-4B-GGUF:Q8_0",
|
||||
)
|
||||
|
||||
|
||||
def _image_cosine_similarity(hf_image, gguf_image) -> float:
|
||||
hf_tensor = torch.as_tensor(np.array(hf_image.convert("RGB"), copy=True), dtype=torch.float32).flatten()
|
||||
gguf_tensor = torch.as_tensor(np.array(gguf_image.convert("RGB"), copy=True), dtype=torch.float32).flatten()
|
||||
return torch.nn.functional.cosine_similarity(hf_tensor, gguf_tensor, dim=0).item()
|
||||
|
||||
|
||||
def _generate_single_stage_image(
|
||||
model: str,
|
||||
height: int = 256,
|
||||
width: int = 256,
|
||||
num_inference_steps: int = 20,
|
||||
seed: int = 42,
|
||||
**extra_kwargs,
|
||||
) -> tuple[list, float]:
|
||||
"""Generate an image with a single-stage diffusion model.
|
||||
|
||||
Returns (images, peak_memory_gib).
|
||||
"""
|
||||
omni_kwargs = dict(extra_kwargs)
|
||||
|
||||
memory_monitor = DeviceMemoryMonitor(device_index=0)
|
||||
memory_monitor.start()
|
||||
try:
|
||||
with OmniRunner(model, **omni_kwargs) as runner:
|
||||
generator = torch.Generator(
|
||||
device=current_omni_platform.device_type,
|
||||
).manual_seed(seed)
|
||||
outputs = runner.omni.generate(
|
||||
"a photo of a cat sitting on a laptop keyboard",
|
||||
OmniDiffusionSamplingParams(
|
||||
height=height,
|
||||
width=width,
|
||||
num_inference_steps=num_inference_steps,
|
||||
guidance_scale=0.0,
|
||||
generator=generator,
|
||||
),
|
||||
)
|
||||
finally:
|
||||
memory_monitor.stop()
|
||||
peak_mem = memory_monitor.peak_used_mb / 1024
|
||||
|
||||
first_output = outputs[0]
|
||||
assert first_output.final_output_type == "image"
|
||||
if hasattr(first_output, "images") and first_output.images:
|
||||
images = first_output.images
|
||||
else:
|
||||
assert hasattr(first_output, "request_output") and first_output.request_output
|
||||
request_output = first_output.request_output
|
||||
if isinstance(request_output, list):
|
||||
req_out = request_output[0]
|
||||
else:
|
||||
req_out = request_output
|
||||
assert isinstance(req_out, OmniRequestOutput) and hasattr(req_out, "images")
|
||||
images = req_out.images
|
||||
assert len(images) >= 1
|
||||
assert images[0].width == width
|
||||
assert images[0].height == height
|
||||
|
||||
return images, peak_mem
|
||||
|
||||
|
||||
@hardware_test(res={"cuda": "L4"})
|
||||
@pytest.mark.full_model
|
||||
@pytest.mark.diffusion
|
||||
@pytest.mark.slow
|
||||
@pytest.mark.parametrize("model", [Z_IMAGE_CONFIG, FLUX_CONFIG], ids=["Z-Image-Turbo", "FLUX.2-klein"])
|
||||
def test_single_stage_diffusion_gguf(model: DiffusionGGUFTestConfig, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Z-Image-Turbo GGUF generates valid images and uses less memory than BF16."""
|
||||
monkeypatch.setenv("VLLM_WORKER_MULTIPROC_METHOD", "spawn")
|
||||
|
||||
# BF16 baseline
|
||||
hf_images, mem_bf16 = _generate_single_stage_image(
|
||||
model=model.hf_model,
|
||||
)
|
||||
|
||||
# GGUF
|
||||
images, mem_gguf = _generate_single_stage_image(
|
||||
model=model.hf_model,
|
||||
diffusion_quantization_config={
|
||||
"method": "gguf",
|
||||
"gguf_model": model.gguf_model,
|
||||
},
|
||||
)
|
||||
|
||||
hf_image_path = f"test_{model.artifact_prefix}_hf.png"
|
||||
gguf_image_path = f"test_{model.artifact_prefix}_gguf.png"
|
||||
assert len(hf_images) >= 1
|
||||
hf_images[0].save(hf_image_path)
|
||||
assert len(images) >= 1
|
||||
images[0].save(gguf_image_path)
|
||||
print(f"Saved HF image: {hf_image_path}")
|
||||
print(f"Saved GGUF image: {gguf_image_path}")
|
||||
|
||||
image_cosine_similarity = _image_cosine_similarity(hf_images[0], images[0])
|
||||
print(f"{model.artifact_prefix} image cosine similarity: {image_cosine_similarity:.4f}")
|
||||
assert image_cosine_similarity >= model.min_cosine_similarity, (
|
||||
f"GGUF image cosine similarity ({image_cosine_similarity:.4f}) should be >= {model.min_cosine_similarity:.4f}"
|
||||
)
|
||||
|
||||
print(f"{model.artifact_prefix} BF16 peak VRAM delta: {mem_bf16:.2f} GiB")
|
||||
print(f"{model.artifact_prefix} GGUF peak VRAM delta: {mem_gguf:.2f} GiB")
|
||||
reduction = (mem_bf16 - mem_gguf) / mem_bf16 * 100
|
||||
print(f"VRAM reduction: {reduction:.1f}%")
|
||||
assert mem_gguf < mem_bf16, f"GGUF ({mem_gguf:.2f} GiB) should use less VRAM than BF16 ({mem_bf16:.2f} GiB)"
|
||||
@@ -56,3 +56,26 @@ def test_warning_contains_version_strings():
|
||||
msg = str(record[0].message)
|
||||
assert "0.19.0rc2.dev21" in msg
|
||||
assert "0.18.0" in msg
|
||||
|
||||
|
||||
def test_omni_plugin_loader_invokes_vllm_general_plugins_first(monkeypatch):
|
||||
import vllm.plugins as vllm_plugins
|
||||
|
||||
import vllm_omni.plugins as omni_plugins
|
||||
|
||||
calls = []
|
||||
monkeypatch.setattr(omni_plugins, "omni_plugins_loaded", False)
|
||||
monkeypatch.setattr(vllm_plugins, "load_general_plugins", lambda: calls.append("vllm"))
|
||||
monkeypatch.setattr(
|
||||
omni_plugins,
|
||||
"load_omni_plugins_by_group",
|
||||
lambda group: {"omni": lambda: calls.append(group)},
|
||||
)
|
||||
|
||||
omni_plugins.load_omni_general_plugins()
|
||||
|
||||
assert calls == ["vllm", omni_plugins.OMNI_DEFAULT_PLUGINS_GROUP]
|
||||
|
||||
omni_plugins.load_omni_general_plugins()
|
||||
|
||||
assert calls == ["vllm", omni_plugins.OMNI_DEFAULT_PLUGINS_GROUP]
|
||||
@@ -551,7 +551,7 @@ class _DiffusionConfigProjection:
|
||||
elif isinstance(self.quantization_config, str):
|
||||
self.quantization_config = build_quant_config(self.quantization_config)
|
||||
elif isinstance(self.quantization_config, Mapping):
|
||||
self.quantization_config = build_quant_config(dict(self.quantization_config))
|
||||
self.quantization_config = dict(self.quantization_config)
|
||||
else:
|
||||
raise TypeError(
|
||||
"quantization_config must be str, dict, QuantizationConfig, or None, "
|
||||
|
||||
@@ -15,6 +15,7 @@ connectors:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
latent_chunk_size: 25
|
||||
initial_latent_chunk_size: 4
|
||||
latent_left_context: 0
|
||||
|
||||
stages:
|
||||
|
||||
@@ -23,6 +23,7 @@ connectors:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
latent_chunk_size: 25
|
||||
initial_latent_chunk_size: 4
|
||||
latent_left_context: 0
|
||||
|
||||
stages:
|
||||
|
||||
+33
@@ -975,6 +975,38 @@ def enable_cache_for_cosmos3(pipeline: Any, cache_config: Any) -> RefreshCacheCo
|
||||
return enable_cache_for_dit(pipeline, cache_config, block_adapter)
|
||||
|
||||
|
||||
def enable_cache_for_krea2(pipeline: Any, cache_config: Any) -> RefreshCacheContextFunc:
|
||||
"""Enable cache-dit for Krea 2.
|
||||
|
||||
Krea 2 is a single-stream MMDiT: each ``Krea2TransformerBlock`` takes and returns only
|
||||
``hidden_states`` (text is fused into the token stream), so the blocks follow
|
||||
``ForwardPattern.Pattern_3``.
|
||||
|
||||
``has_separate_cfg`` is checkpoint-dependent, which is why this needs a custom enabler
|
||||
rather than a static ``_cache_dit_adapter_config``: the distilled Turbo checkpoint runs
|
||||
no-CFG (a single transformer forward per denoise step), while the Raw checkpoint runs CFG
|
||||
as two separate forwards. cache-dit tells cond/uncond apart purely by transformer-forward
|
||||
parity, so the flag must match the actual per-step forward count — ``True`` only for the
|
||||
CFG (Raw) path. The pipeline exposes this via ``is_distilled`` (read from ``model_index.json``).
|
||||
|
||||
Args:
|
||||
pipeline: The Krea2Pipeline instance.
|
||||
cache_config: DiffusionCacheConfig instance with cache configuration.
|
||||
|
||||
Returns:
|
||||
A refresh function that can be called to update cache context with new num_inference_steps.
|
||||
"""
|
||||
transformer = default_get_pipeline_transformer(pipeline)
|
||||
block_adapter = BlockAdapter(
|
||||
transformer=transformer,
|
||||
blocks=[transformer.transformer_blocks],
|
||||
forward_pattern=[ForwardPattern.Pattern_3],
|
||||
has_separate_cfg=not pipeline.is_distilled,
|
||||
check_forward_pattern=False,
|
||||
)
|
||||
return enable_cache_for_dit(pipeline, cache_config, block_adapter)
|
||||
|
||||
|
||||
# Register custom cache-dit enablers after function definitions
|
||||
CUSTOM_DIT_ENABLERS.update(
|
||||
{
|
||||
@@ -984,6 +1016,7 @@ CUSTOM_DIT_ENABLERS.update(
|
||||
"Wan22VACEPipeline": enable_cache_for_wan22,
|
||||
"Wan22S2VPipeline": enable_cache_for_wan22_s2v,
|
||||
"Cosmos3OmniDiffusersPipeline": enable_cache_for_cosmos3,
|
||||
"Krea2Pipeline": enable_cache_for_krea2,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
import torch
|
||||
from huggingface_hub import hf_hub_download
|
||||
from torch import nn
|
||||
from vllm.config import ModelConfig
|
||||
from vllm.config.load import LoadConfig
|
||||
@@ -36,7 +35,6 @@ from vllm_omni.diffusion.distributed.hsdp import HSDPInferenceConfig, apply_hsdp
|
||||
from vllm_omni.diffusion.model_loader.checkpoint_adapters import (
|
||||
get_checkpoint_adapter,
|
||||
)
|
||||
from vllm_omni.diffusion.model_loader.gguf_adapters import get_gguf_adapter
|
||||
from vllm_omni.diffusion.models.diffusers_adapter.pipeline_diffusers_adapter import DiffusersAdapterPipeline
|
||||
from vllm_omni.diffusion.offloader.module_collector import ModuleDiscovery
|
||||
from vllm_omni.diffusion.registry import initialize_model
|
||||
@@ -381,10 +379,7 @@ class DiffusersPipelineLoader:
|
||||
# DiffusionPipeline.from_pretrained() internally; it does
|
||||
# not use our native customized pipeline classes.
|
||||
cast(DiffusersAdapterPipeline, model).load_weights()
|
||||
elif self._is_gguf_quantization():
|
||||
self._load_weights_with_gguf(model)
|
||||
else:
|
||||
# Quantization does not happen in `load_weights` but after it
|
||||
self.load_weights(model)
|
||||
|
||||
# Process weights after loading for quantization (e.g., FP8 online quantization)
|
||||
@@ -515,7 +510,7 @@ class DiffusersPipelineLoader:
|
||||
def _is_expected_quantized_weight(name: str) -> bool:
|
||||
"""Return True if *name* is a quantization-specific parameter.
|
||||
|
||||
Quantization methods (GPTQ, AWQ, FP8, GGUF, Autoround, etc.) create extra
|
||||
Quantization methods (GPTQ, AWQ, FP8, Autoround, etc.) create extra
|
||||
parameters that have no counterpart in an unquantized checkpoint.
|
||||
These are expected to be absent and should not trigger a load error.
|
||||
"""
|
||||
@@ -528,8 +523,6 @@ class DiffusersPipelineLoader:
|
||||
".weight_scale",
|
||||
".weight_scale_inv",
|
||||
".input_scale",
|
||||
# GGUF
|
||||
".qweight_type",
|
||||
# INT8 (weight_scale already covered above)
|
||||
)
|
||||
return name.endswith(_QUANTIZED_WEIGHT_SUFFIXES)
|
||||
@@ -561,116 +554,6 @@ class DiffusersPipelineLoader:
|
||||
if unexpected_missing:
|
||||
raise ValueError(f"Following weights were not initialized from checkpoint: {unexpected_missing}")
|
||||
|
||||
def _get_gguf_model_from_config(self) -> str | None:
|
||||
"""Extract gguf_model from quant_config (handles both dict and object)."""
|
||||
if self.quant_config is None:
|
||||
return None
|
||||
if isinstance(self.quant_config, dict):
|
||||
return self.quant_config.get("gguf_model")
|
||||
return getattr(self.quant_config, "gguf_model", None)
|
||||
|
||||
def _is_gguf_quantization(self) -> bool:
|
||||
"""Check whether or not this pipeline loader is pointing at a GGUF config."""
|
||||
from vllm_omni.quantization.gguf_config import DiffusionGGUFConfig
|
||||
|
||||
maybe_gguf_model = self._get_gguf_model_from_config()
|
||||
is_gguf = (
|
||||
isinstance(self.quant_config, DiffusionGGUFConfig)
|
||||
or (isinstance(self.quant_config, dict) and self.quant_config.get("method") == "gguf")
|
||||
or (hasattr(self.quant_config, "get_name") and self.quant_config.get_name() == "gguf")
|
||||
)
|
||||
if is_gguf:
|
||||
if not maybe_gguf_model:
|
||||
raise ValueError("GGUF quantization requires gguf_model")
|
||||
return True
|
||||
|
||||
if isinstance(self.quant_config, dict):
|
||||
return False
|
||||
|
||||
return bool(maybe_gguf_model)
|
||||
|
||||
def _is_transformer_source(self, source: "ComponentSource") -> bool:
|
||||
if source.subfolder == "transformer":
|
||||
return True
|
||||
return source.prefix.startswith("transformer.")
|
||||
|
||||
def _get_model_loadable_names(self, model: nn.Module) -> set[str]:
|
||||
# Avoid model.state_dict() here because GGUF uses UninitializedParameter
|
||||
# which raises during detach(). Collect names directly.
|
||||
return {name for name, _ in model.named_parameters()} | {name for name, _ in model.named_buffers()}
|
||||
|
||||
def _resolve_gguf_model_path(self, gguf_model: str, revision: str | None) -> str:
|
||||
if os.path.isfile(gguf_model):
|
||||
return gguf_model
|
||||
# repo_id/filename.gguf
|
||||
if "/" in gguf_model and gguf_model.endswith(".gguf"):
|
||||
repo_id, filename = gguf_model.rsplit("/", 1)
|
||||
return hf_hub_download(
|
||||
repo_id=repo_id,
|
||||
filename=filename,
|
||||
revision=revision,
|
||||
cache_dir=self.load_config.download_dir,
|
||||
)
|
||||
# repo_id:quant_type
|
||||
if "/" in gguf_model and ":" in gguf_model:
|
||||
repo_id, quant_type = gguf_model.rsplit(":", 1)
|
||||
return download_gguf(
|
||||
repo_id,
|
||||
quant_type,
|
||||
cache_dir=self.load_config.download_dir,
|
||||
revision=revision,
|
||||
ignore_patterns=self.load_config.ignore_patterns,
|
||||
)
|
||||
raise ValueError(
|
||||
f"Unrecognized GGUF reference: {gguf_model!r} (expected local file, "
|
||||
"<repo_id>/<filename>.gguf, or <repo_id>:<quant_type>)"
|
||||
)
|
||||
|
||||
def _get_gguf_weights_iterator(
|
||||
self,
|
||||
source: "ComponentSource",
|
||||
model: nn.Module,
|
||||
) -> Generator[tuple[str, torch.Tensor], None, None]:
|
||||
gguf_model = self._get_gguf_model_from_config()
|
||||
if gguf_model is None:
|
||||
raise ValueError("GGUF quantization requires quantization_config.gguf_model")
|
||||
gguf_file = self._resolve_gguf_model_path(gguf_model, self.od_config.revision)
|
||||
adapter = get_gguf_adapter(gguf_file, model, source, self.od_config)
|
||||
weights_iter = adapter.weights_iterator()
|
||||
return ((source.prefix + name, tensor) for (name, tensor) in weights_iter)
|
||||
|
||||
def _load_weights_with_gguf(self, model: nn.Module) -> set[str]:
|
||||
sources = self._get_weight_sources(model)
|
||||
loaded: set[str] = set()
|
||||
loadable_names: set[str] | None = None
|
||||
|
||||
for source in sources:
|
||||
if self._is_transformer_source(source):
|
||||
loaded |= model.load_weights(self._get_gguf_weights_iterator(source, model))
|
||||
|
||||
# GGUF checkpoints can be transformer-only or partially quantized.
|
||||
# Only fall back to HF if this source still has missing loadable weights.
|
||||
loadable_names = loadable_names or self._get_model_loadable_names(model)
|
||||
has_missing_for_source = any(
|
||||
name.startswith(source.prefix) and name not in loaded for name in loadable_names
|
||||
)
|
||||
if not has_missing_for_source:
|
||||
continue
|
||||
|
||||
hf_iter = self._get_weights_iterator(source)
|
||||
hf_iter = (
|
||||
(name, tensor) for (name, tensor) in hf_iter if name in loadable_names and name not in loaded
|
||||
)
|
||||
loaded |= model.load_weights(hf_iter)
|
||||
else:
|
||||
loaded |= model.load_weights(self._get_weights_iterator(source))
|
||||
|
||||
weights_to_load = self._get_expected_parameter_names(model)
|
||||
weights_not_loaded = weights_to_load - loaded
|
||||
if weights_not_loaded:
|
||||
raise ValueError(f"Following weights were not initialized from checkpoint: {weights_not_loaded}")
|
||||
return loaded
|
||||
|
||||
def _init_from_load_format(
|
||||
self,
|
||||
load_format: str,
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
|
||||
from .base import GGUFAdapter
|
||||
from .flux2_klein import Flux2KleinGGUFAdapter
|
||||
from .qwen_image import QwenImageGGUFAdapter
|
||||
from .z_image import ZImageGGUFAdapter
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vllm_omni.diffusion.data import OmniDiffusionConfig
|
||||
from vllm_omni.diffusion.model_loader.diffusers_loader import (
|
||||
DiffusersPipelineLoader,
|
||||
)
|
||||
|
||||
|
||||
def get_gguf_adapter(
|
||||
gguf_file: str,
|
||||
model: torch.nn.Module,
|
||||
source: DiffusersPipelineLoader.ComponentSource,
|
||||
od_config: OmniDiffusionConfig,
|
||||
) -> GGUFAdapter:
|
||||
adapter_classes = (QwenImageGGUFAdapter, ZImageGGUFAdapter, Flux2KleinGGUFAdapter)
|
||||
for adapter_cls in adapter_classes:
|
||||
if adapter_cls.is_compatible(od_config, model, source):
|
||||
return adapter_cls(gguf_file, model, source, od_config)
|
||||
model_type = None
|
||||
if od_config.tf_model_config is not None:
|
||||
model_type = od_config.tf_model_config.get("model_type")
|
||||
supported = ", ".join(cls.__name__ for cls in adapter_classes)
|
||||
raise ValueError(
|
||||
"No GGUF adapter matched diffusion model "
|
||||
f"(model_class_name={od_config.model_class_name!r}, model_type={model_type!r}). "
|
||||
f"Supported adapters: {supported}."
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"GGUFAdapter",
|
||||
"Flux2KleinGGUFAdapter",
|
||||
"QwenImageGGUFAdapter",
|
||||
"ZImageGGUFAdapter",
|
||||
"get_gguf_adapter",
|
||||
]
|
||||
@@ -1,109 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Generator
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
try:
|
||||
import gguf
|
||||
except ImportError:
|
||||
# Optional dependency: only required for GGUF-quantized diffusion models.
|
||||
# Keep this import lazy so a missing ``gguf`` does not break importing the
|
||||
# diffusion model loader (and thus inspection/loading of every other
|
||||
# diffusion model that imports through this package).
|
||||
gguf = None
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vllm_omni.diffusion.data import OmniDiffusionConfig
|
||||
from vllm_omni.diffusion.model_loader.diffusers_loader import (
|
||||
DiffusersPipelineLoader,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class MappedTensor:
|
||||
name: str
|
||||
tensor: Any
|
||||
tensor_type: Any
|
||||
row_slice: slice | None = None
|
||||
swap_scale_shift: bool = False
|
||||
|
||||
|
||||
class GGUFAdapter(ABC):
|
||||
"""Base class for model-specific GGUF adapters."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
gguf_file: str,
|
||||
model: torch.nn.Module,
|
||||
source: DiffusersPipelineLoader.ComponentSource,
|
||||
od_config: OmniDiffusionConfig,
|
||||
) -> None:
|
||||
self.gguf_file = gguf_file
|
||||
self.model = model
|
||||
self.source = source
|
||||
self.od_config = od_config
|
||||
|
||||
@staticmethod
|
||||
def is_compatible(
|
||||
od_config: OmniDiffusionConfig,
|
||||
model: torch.nn.Module,
|
||||
source: DiffusersPipelineLoader.ComponentSource,
|
||||
) -> bool:
|
||||
return False
|
||||
|
||||
@abstractmethod
|
||||
def weights_iterator(self) -> Generator[tuple[str, torch.Tensor], None, None]:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
# FIXME(Isotr0py): Sync implemnentation with upstream vLLM?
|
||||
def gguf_quant_weights_iterator(gguf_file: str) -> Generator[tuple[str, torch.Tensor]]:
|
||||
"""
|
||||
Iterate over the quant weights in the model gguf files and convert
|
||||
them to torch tensors.
|
||||
Be careful of the order of yielding weight types and weights data,
|
||||
we have to yield all weight types first before yielding any weights.
|
||||
Otherwise it would cause issue when loading weights with for packed
|
||||
layer with different quant types.
|
||||
"""
|
||||
|
||||
if gguf is None:
|
||||
raise ImportError(
|
||||
"The 'gguf' package is required to load GGUF-quantized diffusion models. "
|
||||
"Install it with `pip install gguf`."
|
||||
)
|
||||
reader = gguf.GGUFReader(gguf_file)
|
||||
|
||||
for tensor in reader.tensors:
|
||||
weight_type = tensor.tensor_type
|
||||
name = tensor.name
|
||||
|
||||
if weight_type.name not in ("F32", "F16"):
|
||||
weight_type_name = name.replace("weight", "qweight_type")
|
||||
weight_type = torch.tensor(weight_type)
|
||||
yield weight_type_name, weight_type
|
||||
|
||||
for tensor in reader.tensors:
|
||||
weight = tensor.data
|
||||
weight_type = tensor.tensor_type
|
||||
name = tensor.name
|
||||
if weight_type.name not in ("F32", "F16"):
|
||||
name = name.replace("weight", "qweight")
|
||||
if weight_type.name == "BF16" and tensor.data.dtype == np.uint8:
|
||||
# BF16 is currently the only "quantization" type that isn't
|
||||
# actually quantized but is read as a raw byte tensor.
|
||||
# Reinterpret as `torch.bfloat16` tensor.
|
||||
weight = weight.view(np.uint16)
|
||||
if reader.byte_order == "S":
|
||||
# GGUF endianness != system endianness
|
||||
weight = weight.byteswap()
|
||||
param = torch.tensor(weight).view(torch.bfloat16)
|
||||
else:
|
||||
param = torch.tensor(weight)
|
||||
yield name, param
|
||||
@@ -1,98 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
|
||||
import torch
|
||||
from vllm.model_executor.models.utils import WeightsMapper
|
||||
|
||||
from .base import GGUFAdapter, gguf_quant_weights_iterator
|
||||
|
||||
FLUX2_TRANSFORMER_KEYS_RENAME_DICT = {
|
||||
"single_blocks.": "single_transformer_blocks.",
|
||||
# Image and text input projections
|
||||
"img_in": "x_embedder",
|
||||
"txt_in": "context_embedder",
|
||||
# Timestep and guidance embeddings
|
||||
"time_in.in_layer": "time_guidance_embed.timestep_embedder.linear_1",
|
||||
"time_in.out_layer": "time_guidance_embed.timestep_embedder.linear_2",
|
||||
"guidance_in.in_layer": "time_guidance_embed.guidance_embedder.linear_1",
|
||||
"guidance_in.out_layer": "time_guidance_embed.guidance_embedder.linear_2",
|
||||
# Modulation parameters
|
||||
"double_stream_modulation_img.lin": "double_stream_modulation_img.linear",
|
||||
"double_stream_modulation_txt.lin": "double_stream_modulation_txt.linear",
|
||||
"single_stream_modulation.lin": "single_stream_modulation.linear",
|
||||
# Final output layer
|
||||
# "final_layer.adaLN_modulation.1": "norm_out.linear", # Handle separately since we need to swap mod params
|
||||
"final_layer.linear": "proj_out",
|
||||
}
|
||||
|
||||
FLUX2_TRANSFORMER_ADA_LAYER_NORM_KEY_MAP = {
|
||||
"final_layer.adaLN_modulation.1": "norm_out.linear",
|
||||
}
|
||||
|
||||
FLUX2_TRANSFORMER_DOUBLE_BLOCK_KEY_MAP = {
|
||||
"double_blocks.": "transformer_blocks.",
|
||||
# Handle fused QKV projections separately as we need to break into Q, K, V projections
|
||||
"img_attn.norm.query_norm": "attn.norm_q",
|
||||
"img_attn.norm.key_norm": "attn.norm_k",
|
||||
"img_attn.proj": "attn.to_out.0",
|
||||
"img_mlp.0": "ff.linear_in",
|
||||
"img_mlp.2": "ff.linear_out",
|
||||
"txt_attn.norm.query_norm": "attn.norm_added_q",
|
||||
"txt_attn.norm.key_norm": "attn.norm_added_k",
|
||||
"txt_attn.proj": "attn.to_add_out",
|
||||
"txt_mlp.0": "ff_context.linear_in",
|
||||
"txt_mlp.2": "ff_context.linear_out",
|
||||
# Additional for fuse qkv
|
||||
"img_attn.qkv": "attn.to_qkv",
|
||||
"txt_attn.qkv": "attn.add_kv_proj",
|
||||
}
|
||||
|
||||
FLUX2_TRANSFORMER_SINGLE_BLOCK_KEY_MAP = {
|
||||
"linear1": "attn.to_qkv_mlp_proj",
|
||||
"linear2": "attn.to_out",
|
||||
"norm.query_norm": "attn.norm_q",
|
||||
"norm.key_norm": "attn.norm_k",
|
||||
}
|
||||
|
||||
|
||||
class Flux2KleinGGUFAdapter(GGUFAdapter):
|
||||
"""GGUF adapter for Flux2-Klein models with qkv splitting and adaLN swap."""
|
||||
|
||||
@staticmethod
|
||||
def is_compatible(od_config, model: torch.nn.Module, source) -> bool:
|
||||
model_class = od_config.model_class_name or ""
|
||||
if model_class.startswith("Flux2"):
|
||||
return True
|
||||
cfg = od_config.tf_model_config
|
||||
if cfg is not None:
|
||||
model_type = str(cfg.get("model_type", "")).lower()
|
||||
if model_type.startswith("flux"):
|
||||
return True
|
||||
return False
|
||||
|
||||
gguf_to_hf_mapper = WeightsMapper(
|
||||
# double_stream_modulation
|
||||
orig_to_new_prefix=FLUX2_TRANSFORMER_KEYS_RENAME_DICT | FLUX2_TRANSFORMER_ADA_LAYER_NORM_KEY_MAP,
|
||||
orig_to_new_substr=FLUX2_TRANSFORMER_DOUBLE_BLOCK_KEY_MAP | FLUX2_TRANSFORMER_SINGLE_BLOCK_KEY_MAP,
|
||||
)
|
||||
|
||||
def weights_iterator(self) -> Iterable[tuple[str, torch.Tensor]]:
|
||||
def custom_weights_adapter(
|
||||
weights: Iterable[tuple[str, torch.Tensor]],
|
||||
) -> Iterable[tuple[str, torch.Tensor]]:
|
||||
for name, weight in weights:
|
||||
# Handle the special case for adaLN modulation parameters that require swapping shift and scale
|
||||
if name.endswith(".scale"):
|
||||
name = name.replace(".scale", ".weight")
|
||||
if name == "norm_out.linear.weight":
|
||||
shift, scale = weight.chunk(2, dim=0)
|
||||
weight = torch.cat([scale, shift], dim=0)
|
||||
yield name, weight
|
||||
else:
|
||||
yield name, weight
|
||||
|
||||
weights = gguf_quant_weights_iterator(self.gguf_file)
|
||||
weights = self.gguf_to_hf_mapper.apply(weights)
|
||||
yield from custom_weights_adapter(weights)
|
||||
@@ -1,27 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
|
||||
import torch
|
||||
|
||||
from .base import GGUFAdapter, gguf_quant_weights_iterator
|
||||
|
||||
|
||||
class QwenImageGGUFAdapter(GGUFAdapter):
|
||||
"""GGUF adapter for the Qwen-Image transformer family."""
|
||||
|
||||
@staticmethod
|
||||
def is_compatible(od_config, model: torch.nn.Module, source) -> bool:
|
||||
model_class = od_config.model_class_name or ""
|
||||
if model_class.startswith("QwenImage"):
|
||||
return True
|
||||
cfg = od_config.tf_model_config
|
||||
if cfg is not None:
|
||||
model_type = str(cfg.get("model_type", "")).lower()
|
||||
if model_type.startswith("qwen_image"):
|
||||
return True
|
||||
return False
|
||||
|
||||
def weights_iterator(self) -> Iterable[tuple[str, torch.Tensor]]:
|
||||
yield from gguf_quant_weights_iterator(self.gguf_file)
|
||||
@@ -1,43 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
|
||||
import torch
|
||||
from vllm.model_executor.models.utils import WeightsMapper
|
||||
|
||||
from .base import GGUFAdapter, gguf_quant_weights_iterator
|
||||
|
||||
Z_IMAGE_KEYS_RENAME_DICT = {
|
||||
"final_layer.": "all_final_layer.2-1.",
|
||||
"x_embedder.": "all_x_embedder.2-1.",
|
||||
".attention.qkv": ".attention.to_qkv",
|
||||
".attention.k_norm": ".attention.norm_k",
|
||||
".attention.q_norm": ".attention.norm_q",
|
||||
".attention.out": ".attention.to_out.0",
|
||||
"model.diffusion_model.": "",
|
||||
}
|
||||
|
||||
|
||||
class ZImageGGUFAdapter(GGUFAdapter):
|
||||
"""GGUF adapter for Z-Image models with QKV/FFN shard support."""
|
||||
|
||||
@staticmethod
|
||||
def is_compatible(od_config, model: torch.nn.Module, source) -> bool:
|
||||
model_class = od_config.model_class_name or ""
|
||||
if model_class.startswith("ZImage"):
|
||||
return True
|
||||
cfg = od_config.tf_model_config
|
||||
if cfg is not None:
|
||||
model_type = str(cfg.get("model_type", "")).lower()
|
||||
if model_type in {"z_image", "zimage", "z-image"}:
|
||||
return True
|
||||
return False
|
||||
|
||||
gguf_to_hf_mapper = WeightsMapper(
|
||||
orig_to_new_substr=Z_IMAGE_KEYS_RENAME_DICT,
|
||||
)
|
||||
|
||||
def weights_iterator(self) -> Iterable[tuple[str, torch.Tensor]]:
|
||||
weights = gguf_quant_weights_iterator(self.gguf_file)
|
||||
yield from self.gguf_to_hf_mapper.apply(weights)
|
||||
@@ -1034,7 +1034,7 @@ class Flux2Transformer2DModel(nn.Module):
|
||||
name = original_name
|
||||
if name not in params_dict and ".to_out.0." in name:
|
||||
name = name.replace(".to_out.0.", ".to_out.")
|
||||
# Some GGUF checkpoints include quantized tensors for modules that
|
||||
# Some quantized checkpoints include tensors for modules that
|
||||
# are intentionally left unquantized in this model.
|
||||
param = params_dict.get(name)
|
||||
if param is None:
|
||||
|
||||
@@ -1044,7 +1044,7 @@ class Flux2Transformer2DModel(nn.Module):
|
||||
name = original_name
|
||||
if name not in params_dict and ".to_out.0." in name:
|
||||
name = name.replace(".to_out.0.", ".to_out.")
|
||||
# Some GGUF checkpoints include quantized tensors for modules that
|
||||
# Some quantized checkpoints include tensors for modules that
|
||||
# are intentionally left unquantized in this model.
|
||||
param = params_dict.get(name)
|
||||
if param is None:
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Krea 2 diffusion model components."""
|
||||
|
||||
from vllm_omni.diffusion.models.krea2.krea2_transformer import Krea2Transformer2DModel
|
||||
from vllm_omni.diffusion.models.krea2.pipeline_krea2 import (
|
||||
Krea2Pipeline,
|
||||
get_krea2_post_process_func,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"Krea2Pipeline",
|
||||
"Krea2Transformer2DModel",
|
||||
"get_krea2_post_process_func",
|
||||
]
|
||||
@@ -0,0 +1,571 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
# Copyright 2026 Krea AI and The HuggingFace Team. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import math
|
||||
from collections.abc import Iterable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from diffusers.models.embeddings import apply_rotary_emb, get_1d_rotary_pos_embed
|
||||
from vllm.logger import init_logger
|
||||
from vllm.model_executor.layers.linear import ReplicatedLinear
|
||||
from vllm.model_executor.model_loader.weight_utils import default_weight_loader
|
||||
|
||||
from vllm_omni.diffusion.attention.backends.abstract import AttentionMetadata
|
||||
from vllm_omni.diffusion.attention.layer import Attention
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vllm.model_executor.layers.quantization.base_config import QuantizationConfig
|
||||
|
||||
from vllm_omni.diffusion.data import OmniDiffusionConfig
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
# Devices that do not support float64; RoPE frequencies fall back to float32 there.
|
||||
_FP64_UNSUPPORTED_DEVICES = frozenset({"mps", "npu", "neuron"})
|
||||
|
||||
|
||||
def _rope_freqs_dtype(device: torch.device) -> torch.dtype:
|
||||
return torch.float32 if device.type in _FP64_UNSUPPORTED_DEVICES else torch.float64
|
||||
|
||||
|
||||
def _join_prefix(prefix: str, name: str) -> str:
|
||||
return f"{prefix}.{name}" if prefix else name
|
||||
|
||||
|
||||
def _linear(
|
||||
in_features: int,
|
||||
out_features: int,
|
||||
bias: bool,
|
||||
quant_config: "QuantizationConfig | None",
|
||||
prefix: str,
|
||||
) -> ReplicatedLinear:
|
||||
# ReplicatedLinear (not nn.Linear) so the diffusion LoRA manager can wrap these projections.
|
||||
return ReplicatedLinear(
|
||||
in_features,
|
||||
out_features,
|
||||
bias=bias,
|
||||
quant_config=quant_config,
|
||||
prefix=prefix,
|
||||
return_bias=False,
|
||||
)
|
||||
|
||||
|
||||
class Krea2RMSNorm(nn.Module):
|
||||
"""RMSNorm with a zero-centered scale: the effective multiplier is ``1 + weight``, matching the Krea 2 checkpoint
|
||||
format. Activations are upcast so the normalization runs in float32."""
|
||||
|
||||
def __init__(self, dim: int, eps: float = 1e-5) -> None:
|
||||
super().__init__()
|
||||
self.dim = dim
|
||||
self.eps = eps
|
||||
self.weight = nn.Parameter(torch.zeros(dim))
|
||||
|
||||
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
||||
dtype = hidden_states.dtype
|
||||
hidden_states = F.rms_norm(hidden_states.float(), (self.dim,), weight=self.weight + 1.0, eps=self.eps)
|
||||
return hidden_states.to(dtype)
|
||||
|
||||
|
||||
class Krea2Attention(nn.Module):
|
||||
"""Self-attention with grouped-query projections, q/k RMSNorm, rotary embeddings and a sigmoid output gate.
|
||||
|
||||
Q/K/V layout is ``[B, seq, heads, head_dim]``; ``attention_mask`` is a 2D boolean key-padding mask
|
||||
``(batch, key_seq_len)`` that the backend broadcasts.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hidden_size: int,
|
||||
num_heads: int,
|
||||
num_kv_heads: int | None = None,
|
||||
eps: float = 1e-5,
|
||||
role: str = "self",
|
||||
quant_config: "QuantizationConfig | None" = None,
|
||||
prefix: str = "",
|
||||
) -> None:
|
||||
super().__init__()
|
||||
if hidden_size % num_heads != 0:
|
||||
raise ValueError(f"hidden_size={hidden_size} must be divisible by num_heads={num_heads}")
|
||||
self.hidden_size = hidden_size
|
||||
self.num_heads = num_heads
|
||||
self.num_kv_heads = num_kv_heads if num_kv_heads is not None else num_heads
|
||||
self.head_dim = hidden_size // num_heads
|
||||
|
||||
q_dim = self.head_dim * self.num_heads
|
||||
kv_dim = self.head_dim * self.num_kv_heads
|
||||
self.to_q = _linear(hidden_size, q_dim, False, quant_config, _join_prefix(prefix, "to_q"))
|
||||
self.to_k = _linear(hidden_size, kv_dim, False, quant_config, _join_prefix(prefix, "to_k"))
|
||||
self.to_v = _linear(hidden_size, kv_dim, False, quant_config, _join_prefix(prefix, "to_v"))
|
||||
self.to_gate = _linear(hidden_size, hidden_size, False, quant_config, _join_prefix(prefix, "to_gate"))
|
||||
self.norm_q = Krea2RMSNorm(self.head_dim, eps=eps)
|
||||
self.norm_k = Krea2RMSNorm(self.head_dim, eps=eps)
|
||||
self.to_out = nn.ModuleList(
|
||||
[_linear(hidden_size, hidden_size, False, quant_config, _join_prefix(prefix, "to_out.0")), nn.Dropout(0.0)]
|
||||
)
|
||||
|
||||
self.attn = Attention(
|
||||
num_heads=self.num_heads,
|
||||
head_size=self.head_dim,
|
||||
softmax_scale=1.0 / (self.head_dim**0.5),
|
||||
causal=False,
|
||||
num_kv_heads=self.num_kv_heads,
|
||||
role=role,
|
||||
)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
hidden_states: torch.Tensor,
|
||||
attention_mask: torch.Tensor | None = None,
|
||||
image_rotary_emb: tuple[torch.Tensor, torch.Tensor] | None = None,
|
||||
) -> torch.Tensor:
|
||||
query = self.to_q(hidden_states).unflatten(-1, (self.num_heads, self.head_dim))
|
||||
key = self.to_k(hidden_states).unflatten(-1, (self.num_kv_heads, self.head_dim))
|
||||
value = self.to_v(hidden_states).unflatten(-1, (self.num_kv_heads, self.head_dim))
|
||||
gate = self.to_gate(hidden_states)
|
||||
|
||||
query = self.norm_q(query)
|
||||
key = self.norm_k(key)
|
||||
|
||||
if image_rotary_emb is not None:
|
||||
query = apply_rotary_emb(query, image_rotary_emb, sequence_dim=1)
|
||||
key = apply_rotary_emb(key, image_rotary_emb, sequence_dim=1)
|
||||
query = query.to(value.dtype)
|
||||
key = key.to(value.dtype)
|
||||
|
||||
attn_metadata = AttentionMetadata(attn_mask=attention_mask) if attention_mask is not None else None
|
||||
hidden_states = self.attn(query, key, value, attn_metadata)
|
||||
hidden_states = hidden_states.flatten(2, 3)
|
||||
hidden_states = hidden_states * torch.sigmoid(gate)
|
||||
return self.to_out[0](hidden_states)
|
||||
|
||||
|
||||
class Krea2SwiGLU(nn.Module):
|
||||
"""SwiGLU feed-forward network."""
|
||||
|
||||
def __init__(
|
||||
self, dim: int, hidden_dim: int, quant_config: "QuantizationConfig | None" = None, prefix: str = ""
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.gate = _linear(dim, hidden_dim, False, quant_config, _join_prefix(prefix, "gate"))
|
||||
self.up = _linear(dim, hidden_dim, False, quant_config, _join_prefix(prefix, "up"))
|
||||
self.down = _linear(hidden_dim, dim, False, quant_config, _join_prefix(prefix, "down"))
|
||||
|
||||
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
||||
return self.down(F.silu(self.gate(hidden_states)) * self.up(hidden_states))
|
||||
|
||||
|
||||
class Krea2TextFusionBlock(nn.Module):
|
||||
"""Pre-norm transformer block (no rotary embeddings, no time modulation) used by the text fusion stage."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dim: int,
|
||||
num_heads: int,
|
||||
num_kv_heads: int,
|
||||
intermediate_size: int,
|
||||
eps: float,
|
||||
quant_config: "QuantizationConfig | None" = None,
|
||||
prefix: str = "",
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.norm1 = Krea2RMSNorm(dim, eps=eps)
|
||||
self.norm2 = Krea2RMSNorm(dim, eps=eps)
|
||||
self.attn = Krea2Attention(
|
||||
dim,
|
||||
num_heads,
|
||||
num_kv_heads,
|
||||
eps=eps,
|
||||
role="self",
|
||||
quant_config=quant_config,
|
||||
prefix=_join_prefix(prefix, "attn"),
|
||||
)
|
||||
self.ff = Krea2SwiGLU(dim, intermediate_size, quant_config=quant_config, prefix=_join_prefix(prefix, "ff"))
|
||||
|
||||
def forward(self, hidden_states: torch.Tensor, attention_mask: torch.Tensor | None = None) -> torch.Tensor:
|
||||
hidden_states = hidden_states + self.attn(self.norm1(hidden_states), attention_mask=attention_mask)
|
||||
hidden_states = hidden_states + self.ff(self.norm2(hidden_states))
|
||||
return hidden_states
|
||||
|
||||
|
||||
class Krea2TextFusion(nn.Module):
|
||||
"""Fuses the stack of tapped text-encoder hidden states into a single sequence of text features.
|
||||
|
||||
Two ``layerwise_blocks`` attend across the ``num_text_layers`` axis independently for every token, a linear
|
||||
``projector`` collapses that axis, and two ``refiner_blocks`` attend across the token sequence.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
num_text_layers: int,
|
||||
dim: int,
|
||||
num_heads: int,
|
||||
num_kv_heads: int,
|
||||
intermediate_size: int,
|
||||
num_layerwise_blocks: int,
|
||||
num_refiner_blocks: int,
|
||||
eps: float,
|
||||
quant_config: "QuantizationConfig | None" = None,
|
||||
prefix: str = "",
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.layerwise_blocks = nn.ModuleList(
|
||||
[
|
||||
Krea2TextFusionBlock(
|
||||
dim,
|
||||
num_heads,
|
||||
num_kv_heads,
|
||||
intermediate_size,
|
||||
eps,
|
||||
quant_config=quant_config,
|
||||
prefix=_join_prefix(prefix, f"layerwise_blocks.{i}"),
|
||||
)
|
||||
for i in range(num_layerwise_blocks)
|
||||
]
|
||||
)
|
||||
self.projector = _linear(num_text_layers, 1, False, quant_config, _join_prefix(prefix, "projector"))
|
||||
self.refiner_blocks = nn.ModuleList(
|
||||
[
|
||||
Krea2TextFusionBlock(
|
||||
dim,
|
||||
num_heads,
|
||||
num_kv_heads,
|
||||
intermediate_size,
|
||||
eps,
|
||||
quant_config=quant_config,
|
||||
prefix=_join_prefix(prefix, f"refiner_blocks.{i}"),
|
||||
)
|
||||
for i in range(num_refiner_blocks)
|
||||
]
|
||||
)
|
||||
|
||||
def forward(self, encoder_hidden_states: torch.Tensor, attention_mask: torch.Tensor | None = None) -> torch.Tensor:
|
||||
batch_size, seq_len, num_text_layers, dim = encoder_hidden_states.shape
|
||||
|
||||
hidden_states = encoder_hidden_states.reshape(batch_size * seq_len, num_text_layers, dim)
|
||||
for block in self.layerwise_blocks:
|
||||
hidden_states = block(hidden_states.contiguous())
|
||||
|
||||
hidden_states = hidden_states.reshape(batch_size, seq_len, num_text_layers, dim).permute(0, 1, 3, 2)
|
||||
hidden_states = self.projector(hidden_states).squeeze(-1)
|
||||
|
||||
for block in self.refiner_blocks:
|
||||
hidden_states = block(hidden_states, attention_mask=attention_mask)
|
||||
|
||||
return hidden_states
|
||||
|
||||
|
||||
class Krea2TransformerBlock(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
hidden_size: int,
|
||||
intermediate_size: int,
|
||||
num_heads: int,
|
||||
num_kv_heads: int,
|
||||
norm_eps: float,
|
||||
quant_config: "QuantizationConfig | None" = None,
|
||||
prefix: str = "",
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.scale_shift_table = nn.Parameter(torch.zeros(6, hidden_size))
|
||||
self.norm1 = Krea2RMSNorm(hidden_size, eps=norm_eps)
|
||||
self.norm2 = Krea2RMSNorm(hidden_size, eps=norm_eps)
|
||||
self.attn = Krea2Attention(
|
||||
hidden_size,
|
||||
num_heads,
|
||||
num_kv_heads,
|
||||
eps=norm_eps,
|
||||
role="self",
|
||||
quant_config=quant_config,
|
||||
prefix=_join_prefix(prefix, "attn"),
|
||||
)
|
||||
self.ff = Krea2SwiGLU(
|
||||
hidden_size, intermediate_size, quant_config=quant_config, prefix=_join_prefix(prefix, "ff")
|
||||
)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
hidden_states: torch.Tensor,
|
||||
temb: torch.Tensor,
|
||||
image_rotary_emb: tuple[torch.Tensor, torch.Tensor],
|
||||
attention_mask: torch.Tensor | None = None,
|
||||
) -> torch.Tensor:
|
||||
# temb: (B, 1, 6 * hidden_size), shared across all blocks; each block only learns an additive table.
|
||||
modulation = temb.unflatten(-1, (6, -1)) + self.scale_shift_table
|
||||
prescale, preshift, pregate, postscale, postshift, postgate = modulation.unbind(-2)
|
||||
|
||||
attn_out = self.attn(
|
||||
(1.0 + prescale) * self.norm1(hidden_states) + preshift,
|
||||
attention_mask=attention_mask,
|
||||
image_rotary_emb=image_rotary_emb,
|
||||
)
|
||||
hidden_states = hidden_states + pregate * attn_out
|
||||
ff_out = self.ff((1.0 + postscale) * self.norm2(hidden_states) + postshift)
|
||||
hidden_states = hidden_states + postgate * ff_out
|
||||
return hidden_states
|
||||
|
||||
|
||||
class Krea2TimestepEmbedding(nn.Module):
|
||||
"""Sinusoidal flow-time embedding (cos-first, input scaled by 1000) followed by a two-layer MLP.
|
||||
|
||||
Keeps the sequence dimension at size 1 so the per-block modulations broadcast over tokens.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, embed_dim: int, hidden_size: int, quant_config: "QuantizationConfig | None" = None, prefix: str = ""
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.embed_dim = embed_dim
|
||||
self.linear_1 = _linear(embed_dim, hidden_size, True, quant_config, _join_prefix(prefix, "linear_1"))
|
||||
self.linear_2 = _linear(hidden_size, hidden_size, True, quant_config, _join_prefix(prefix, "linear_2"))
|
||||
|
||||
def forward(self, timestep: torch.Tensor, dtype: torch.dtype) -> torch.Tensor:
|
||||
half = self.embed_dim // 2
|
||||
freqs = torch.exp(-math.log(1e4) * torch.arange(half, dtype=torch.float32, device=timestep.device) / half)
|
||||
args = (timestep.float() * 1e3)[:, None, None] * freqs
|
||||
emb = torch.cat([torch.cos(args), torch.sin(args)], dim=-1).to(dtype)
|
||||
return self.linear_2(F.gelu(self.linear_1(emb), approximate="tanh"))
|
||||
|
||||
|
||||
class Krea2TextProjection(nn.Module):
|
||||
"""Projects the fused text features into the transformer width."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
text_dim: int,
|
||||
hidden_size: int,
|
||||
eps: float,
|
||||
quant_config: "QuantizationConfig | None" = None,
|
||||
prefix: str = "",
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.norm = Krea2RMSNorm(text_dim, eps=eps)
|
||||
self.linear_1 = _linear(text_dim, hidden_size, True, quant_config, _join_prefix(prefix, "linear_1"))
|
||||
self.linear_2 = _linear(hidden_size, hidden_size, True, quant_config, _join_prefix(prefix, "linear_2"))
|
||||
|
||||
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
||||
hidden_states = self.linear_1(self.norm(hidden_states))
|
||||
return self.linear_2(F.gelu(hidden_states, approximate="tanh"))
|
||||
|
||||
|
||||
class Krea2FinalLayer(nn.Module):
|
||||
"""Final adaptive RMSNorm and output projection."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hidden_size: int,
|
||||
out_channels: int,
|
||||
eps: float,
|
||||
quant_config: "QuantizationConfig | None" = None,
|
||||
prefix: str = "",
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.scale_shift_table = nn.Parameter(torch.zeros(2, hidden_size))
|
||||
self.norm = Krea2RMSNorm(hidden_size, eps=eps)
|
||||
self.linear = _linear(hidden_size, out_channels, True, quant_config, _join_prefix(prefix, "linear"))
|
||||
|
||||
def forward(self, hidden_states: torch.Tensor, temb: torch.Tensor) -> torch.Tensor:
|
||||
modulation = temb + self.scale_shift_table
|
||||
scale, shift = modulation.chunk(2, dim=1)
|
||||
hidden_states = (1.0 + scale) * self.norm(hidden_states) + shift
|
||||
return self.linear(hidden_states)
|
||||
|
||||
|
||||
class Krea2RotaryPosEmbed(nn.Module):
|
||||
"""Multi-axis (t, h, w) rotary position embedding, following the Flux/Krea 2 convention."""
|
||||
|
||||
def __init__(self, theta: int, axes_dim: list[int]):
|
||||
super().__init__()
|
||||
self.theta = theta
|
||||
self.axes_dim = axes_dim
|
||||
|
||||
def forward(self, ids: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
n_axes = ids.shape[-1]
|
||||
cos_out = []
|
||||
sin_out = []
|
||||
pos = ids.float()
|
||||
freqs_dtype = _rope_freqs_dtype(ids.device)
|
||||
for i in range(n_axes):
|
||||
cos, sin = get_1d_rotary_pos_embed(
|
||||
self.axes_dim[i],
|
||||
pos[:, i],
|
||||
theta=self.theta,
|
||||
repeat_interleave_real=True,
|
||||
use_real=True,
|
||||
freqs_dtype=freqs_dtype,
|
||||
)
|
||||
cos_out.append(cos)
|
||||
sin_out.append(sin)
|
||||
freqs_cos = torch.cat(cos_out, dim=-1).to(ids.device)
|
||||
freqs_sin = torch.cat(sin_out, dim=-1).to(ids.device)
|
||||
return freqs_cos, freqs_sin
|
||||
|
||||
|
||||
class Krea2Transformer2DModel(nn.Module):
|
||||
r"""The single-stream MMDiT flow-matching backbone used by the Krea 2 pipeline (vLLM-Omni port).
|
||||
|
||||
Text conditioning enters as a stack of hidden states tapped from several layers of a multimodal text encoder. A
|
||||
small text-fusion transformer collapses the layer axis and refines the token sequence; the result is concatenated
|
||||
with the patchified image latents into a single ``[text, image]`` sequence processed by the transformer blocks.
|
||||
The timestep conditions every block through one shared modulation vector plus per-block learned tables.
|
||||
"""
|
||||
|
||||
_repeated_blocks = ["Krea2TransformerBlock"]
|
||||
_layerwise_offload_blocks_attrs = ["transformer_blocks"]
|
||||
|
||||
@staticmethod
|
||||
def _is_transformer_block(name: str, module) -> bool:
|
||||
return "transformer_blocks" in name and name.split(".")[-1].isdigit()
|
||||
|
||||
_hsdp_shard_conditions = [_is_transformer_block]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
in_channels: int = 64,
|
||||
num_layers: int = 28,
|
||||
attention_head_dim: int = 128,
|
||||
num_attention_heads: int = 48,
|
||||
num_key_value_heads: int = 12,
|
||||
intermediate_size: int = 16384,
|
||||
timestep_embed_dim: int = 256,
|
||||
text_hidden_dim: int = 2560,
|
||||
num_text_layers: int = 12,
|
||||
text_num_attention_heads: int = 20,
|
||||
text_num_key_value_heads: int = 20,
|
||||
text_intermediate_size: int = 6912,
|
||||
num_layerwise_text_blocks: int = 2,
|
||||
num_refiner_text_blocks: int = 2,
|
||||
axes_dims_rope: tuple[int, int, int] = (32, 48, 48),
|
||||
rope_theta: float = 1000.0,
|
||||
norm_eps: float = 1e-5,
|
||||
od_config: "OmniDiffusionConfig | None" = None,
|
||||
quant_config: "QuantizationConfig | None" = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
# The pipeline reads ``self.dtype`` to cast inputs before the transformer forward.
|
||||
self.dtype = od_config.dtype if od_config is not None else torch.get_default_dtype()
|
||||
self.od_config = od_config
|
||||
|
||||
hidden_size = attention_head_dim * num_attention_heads
|
||||
if sum(axes_dims_rope) != attention_head_dim:
|
||||
raise ValueError(
|
||||
f"sum(axes_dims_rope)={sum(axes_dims_rope)} must equal attention_head_dim={attention_head_dim}"
|
||||
)
|
||||
|
||||
self.in_channels = in_channels
|
||||
self.out_channels = in_channels
|
||||
self.hidden_size = hidden_size
|
||||
self.num_layers = num_layers
|
||||
|
||||
self.img_in = _linear(in_channels, hidden_size, True, quant_config, "img_in")
|
||||
self.time_embed = Krea2TimestepEmbedding(timestep_embed_dim, hidden_size, quant_config, "time_embed")
|
||||
self.time_mod_proj = _linear(hidden_size, 6 * hidden_size, True, quant_config, "time_mod_proj")
|
||||
self.text_fusion = Krea2TextFusion(
|
||||
num_text_layers=num_text_layers,
|
||||
dim=text_hidden_dim,
|
||||
num_heads=text_num_attention_heads,
|
||||
num_kv_heads=text_num_key_value_heads,
|
||||
intermediate_size=text_intermediate_size,
|
||||
num_layerwise_blocks=num_layerwise_text_blocks,
|
||||
num_refiner_blocks=num_refiner_text_blocks,
|
||||
eps=norm_eps,
|
||||
quant_config=quant_config,
|
||||
prefix="text_fusion",
|
||||
)
|
||||
self.txt_in = Krea2TextProjection(text_hidden_dim, hidden_size, norm_eps, quant_config, "txt_in")
|
||||
self.rotary_emb = Krea2RotaryPosEmbed(theta=rope_theta, axes_dim=list(axes_dims_rope))
|
||||
|
||||
self.transformer_blocks = nn.ModuleList(
|
||||
[
|
||||
Krea2TransformerBlock(
|
||||
hidden_size=hidden_size,
|
||||
intermediate_size=intermediate_size,
|
||||
num_heads=num_attention_heads,
|
||||
num_kv_heads=num_key_value_heads,
|
||||
norm_eps=norm_eps,
|
||||
quant_config=quant_config,
|
||||
prefix=f"transformer_blocks.{i}",
|
||||
)
|
||||
for i in range(num_layers)
|
||||
]
|
||||
)
|
||||
|
||||
self.final_layer = Krea2FinalLayer(hidden_size, in_channels, norm_eps, quant_config, "final_layer")
|
||||
|
||||
def forward(
|
||||
self,
|
||||
hidden_states: torch.Tensor,
|
||||
encoder_hidden_states: torch.Tensor,
|
||||
timestep: torch.Tensor,
|
||||
position_ids: torch.Tensor,
|
||||
encoder_attention_mask: torch.Tensor | None = None,
|
||||
) -> torch.Tensor:
|
||||
r"""Predict the flow-matching velocity for the image tokens.
|
||||
|
||||
Args:
|
||||
hidden_states: Packed (patchified) noisy image latents ``(batch, image_seq_len, in_channels)``.
|
||||
encoder_hidden_states: Tapped text-encoder hidden states
|
||||
``(batch, text_seq_len, num_text_layers, text_hidden_dim)``.
|
||||
timestep: Flow-matching time in ``[0, 1]`` of shape ``(batch,)``.
|
||||
position_ids: ``(t, h, w)`` rotary coordinates ``(text_seq_len + image_seq_len, 3)``.
|
||||
encoder_attention_mask: Boolean mask marking valid text tokens ``(batch, text_seq_len)`` or ``None``.
|
||||
"""
|
||||
if position_ids.ndim != 2 or position_ids.shape[-1] != 3:
|
||||
raise ValueError(f"`position_ids` must have shape (sequence_length, 3), got {tuple(position_ids.shape)}.")
|
||||
|
||||
batch_size, image_seq_len, _ = hidden_states.shape
|
||||
|
||||
temb = self.time_embed(timestep, dtype=hidden_states.dtype)
|
||||
temb_mod = self.time_mod_proj(F.gelu(temb, approximate="tanh"))
|
||||
|
||||
# 2D key-padding masks: padded text tokens are excluded as keys; their lanes are dropped at the output slice.
|
||||
text_attention_mask = None
|
||||
attention_mask = None
|
||||
if encoder_attention_mask is not None:
|
||||
text_attention_mask = encoder_attention_mask.bool()
|
||||
image_mask = text_attention_mask.new_ones((batch_size, image_seq_len))
|
||||
attention_mask = torch.cat([text_attention_mask, image_mask], dim=1)
|
||||
|
||||
encoder_hidden_states = self.text_fusion(encoder_hidden_states, attention_mask=text_attention_mask)
|
||||
encoder_hidden_states = self.txt_in(encoder_hidden_states)
|
||||
|
||||
text_seq_len = encoder_hidden_states.shape[1]
|
||||
hidden_states = self.img_in(hidden_states)
|
||||
hidden_states = torch.cat([encoder_hidden_states, hidden_states], dim=1)
|
||||
|
||||
image_rotary_emb = self.rotary_emb(position_ids)
|
||||
|
||||
for block in self.transformer_blocks:
|
||||
hidden_states = block(hidden_states, temb_mod, image_rotary_emb, attention_mask)
|
||||
|
||||
hidden_states = hidden_states[:, text_seq_len:]
|
||||
return self.final_layer(hidden_states, temb)
|
||||
|
||||
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
||||
# Module/parameter names mirror the diffusers ``Krea2Transformer2DModel`` exactly, so weights map 1:1.
|
||||
params_dict = dict(self.named_parameters())
|
||||
loaded_params: set[str] = set()
|
||||
for name, loaded_weight in weights:
|
||||
if name not in params_dict:
|
||||
continue
|
||||
param = params_dict[name]
|
||||
weight_loader = getattr(param, "weight_loader", default_weight_loader)
|
||||
weight_loader(param, loaded_weight)
|
||||
loaded_params.add(name)
|
||||
return loaded_params
|
||||
@@ -0,0 +1,551 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
# Copyright 2026 Krea AI and The HuggingFace Team. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import inspect
|
||||
import logging
|
||||
import os
|
||||
from collections.abc import Iterable
|
||||
from typing import ClassVar
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from diffusers.image_processor import VaeImageProcessor
|
||||
from diffusers.schedulers.scheduling_flow_match_euler_discrete import (
|
||||
FlowMatchEulerDiscreteScheduler,
|
||||
)
|
||||
from diffusers.utils.torch_utils import randn_tensor
|
||||
from torch import nn
|
||||
from transformers import AutoTokenizer, Qwen3VLModel
|
||||
from vllm.model_executor.models.utils import AutoWeightsLoader
|
||||
from vllm.transformers_utils.config import get_hf_file_to_dict
|
||||
|
||||
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
|
||||
from vllm_omni.diffusion.distributed.autoencoders.autoencoder_kl_qwenimage import DistributedAutoencoderKLQwenImage
|
||||
from vllm_omni.diffusion.distributed.utils import get_local_device
|
||||
from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
|
||||
from vllm_omni.diffusion.model_loader.hub_prefetch import from_pretrained_with_prefetch, prefetch_subfolders
|
||||
from vllm_omni.diffusion.models.interface import SupportsComponentDiscovery
|
||||
from vllm_omni.diffusion.models.krea2.krea2_transformer import Krea2Transformer2DModel
|
||||
from vllm_omni.diffusion.models.progress_bar import ProgressBarMixin
|
||||
from vllm_omni.diffusion.profiler.diffusion_pipeline_profiler import DiffusionPipelineProfilerMixin
|
||||
from vllm_omni.diffusion.request import OmniDiffusionRequest
|
||||
from vllm_omni.diffusion.utils.tf_utils import get_transformer_config_kwargs
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Text-encoder layer taps used when model_index.json does not specify ``text_encoder_select_layers``.
|
||||
DEFAULT_TEXT_ENCODER_SELECT_LAYERS = (2, 5, 8, 11, 14, 17, 20, 23, 26, 29, 32, 35)
|
||||
|
||||
|
||||
def get_krea2_post_process_func(od_config: OmniDiffusionConfig):
|
||||
model_name = od_config.model
|
||||
# Read only the VAE config (``get_hf_file_to_dict`` resolves a local dir via
|
||||
# ``Path(model)/file`` and a hub repo via a single-file download) instead of
|
||||
# pulling all weights just to compute ``vae_scale_factor``.
|
||||
vae_config = get_hf_file_to_dict("vae/config.json", model_name) or {}
|
||||
vae_scale_factor = 2 ** len(vae_config["temperal_downsample"]) if "temperal_downsample" in vae_config else 8
|
||||
|
||||
patch_size = 2
|
||||
try:
|
||||
model_index = get_hf_file_to_dict("model_index.json", model_name) or {}
|
||||
patch_size = int(model_index.get("patch_size", patch_size))
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
|
||||
image_processor = VaeImageProcessor(vae_scale_factor=vae_scale_factor * patch_size)
|
||||
|
||||
def post_process_func(images: torch.Tensor):
|
||||
return image_processor.postprocess(images)
|
||||
|
||||
return post_process_func
|
||||
|
||||
|
||||
# Copied from diffusers.pipelines.flux.pipeline_flux.calculate_shift
|
||||
def calculate_shift(
|
||||
image_seq_len,
|
||||
base_seq_len: int = 256,
|
||||
max_seq_len: int = 4096,
|
||||
base_shift: float = 0.5,
|
||||
max_shift: float = 1.15,
|
||||
):
|
||||
m = (max_shift - base_shift) / (max_seq_len - base_seq_len)
|
||||
b = base_shift - m * base_seq_len
|
||||
mu = image_seq_len * m + b
|
||||
return mu
|
||||
|
||||
|
||||
# Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.retrieve_timesteps
|
||||
def retrieve_timesteps(
|
||||
scheduler,
|
||||
num_inference_steps: int | None = None,
|
||||
device: str | torch.device | None = None,
|
||||
timesteps: list[int] | None = None,
|
||||
sigmas: list[float] | None = None,
|
||||
**kwargs,
|
||||
) -> tuple[torch.Tensor, int]:
|
||||
if timesteps is not None and sigmas is not None:
|
||||
raise ValueError("Only one of `timesteps` or `sigmas` can be passed. Please choose one to set custom values")
|
||||
if timesteps is not None:
|
||||
accepts_timesteps = "timesteps" in set(inspect.signature(scheduler.set_timesteps).parameters.keys())
|
||||
if not accepts_timesteps:
|
||||
raise ValueError(
|
||||
f"The current scheduler class {scheduler.__class__}'s `set_timesteps` does not support custom"
|
||||
f" timestep schedules. Please check whether you are using the correct scheduler."
|
||||
)
|
||||
scheduler.set_timesteps(timesteps=timesteps, device=device, **kwargs)
|
||||
timesteps = scheduler.timesteps
|
||||
num_inference_steps = len(timesteps)
|
||||
elif sigmas is not None:
|
||||
accept_sigmas = "sigmas" in set(inspect.signature(scheduler.set_timesteps).parameters.keys())
|
||||
if not accept_sigmas:
|
||||
raise ValueError(
|
||||
f"The current scheduler class {scheduler.__class__}'s `set_timesteps` does not support custom"
|
||||
f" sigmas schedules. Please check whether you are using the correct scheduler."
|
||||
)
|
||||
scheduler.set_timesteps(sigmas=sigmas, device=device, **kwargs)
|
||||
timesteps = scheduler.timesteps
|
||||
num_inference_steps = len(timesteps)
|
||||
else:
|
||||
scheduler.set_timesteps(num_inference_steps, device=device, **kwargs)
|
||||
timesteps = scheduler.timesteps
|
||||
return timesteps, num_inference_steps
|
||||
|
||||
|
||||
class Krea2Pipeline(nn.Module, DiffusionPipelineProfilerMixin, ProgressBarMixin, SupportsComponentDiscovery):
|
||||
"""The Krea 2 text-to-image pipeline (vLLM-Omni port of ``diffusers.Krea2Pipeline``).
|
||||
|
||||
Components:
|
||||
- ``scheduler``: ``FlowMatchEulerDiscreteScheduler`` (resolution-aware exponential time shift).
|
||||
- ``vae``: the Qwen-Image VAE (f8, 16 latent channels).
|
||||
- ``text_encoder``: a Qwen3-VL model; the pipeline consumes a stack of hidden states tapped from several
|
||||
decoder layers rather than the last hidden state.
|
||||
- ``tokenizer``: the tokenizer paired with the text encoder.
|
||||
- ``transformer``: the Krea 2 single-stream MMDiT that predicts the flow-matching velocity.
|
||||
"""
|
||||
|
||||
_dit_modules: ClassVar[list[str]] = ["transformer"]
|
||||
_encoder_modules: ClassVar[list[str]] = ["text_encoder"]
|
||||
_vae_modules: ClassVar[list[str]] = ["vae"]
|
||||
|
||||
_PROFILER_TARGETS: ClassVar[list[str]] = ["text_encoder.forward", "diffuse", "vae.decode"]
|
||||
|
||||
def __init__(self, *, od_config: OmniDiffusionConfig, prefix: str = ""):
|
||||
super().__init__()
|
||||
self.od_config = od_config
|
||||
self.parallel_config = od_config.parallel_config
|
||||
# Only the transformer goes through the standard weights loader; text encoder and VAE load eagerly below.
|
||||
self.weights_sources = [
|
||||
DiffusersPipelineLoader.ComponentSource(
|
||||
model_or_path=od_config.model,
|
||||
subfolder="transformer",
|
||||
revision=od_config.revision,
|
||||
prefix="transformer.",
|
||||
fall_back_to_pt=True,
|
||||
)
|
||||
]
|
||||
|
||||
self.device = get_local_device()
|
||||
model = od_config.model
|
||||
local_files_only = os.path.isdir(model)
|
||||
|
||||
# Pipeline-level config lives in model_index.json, not the transformer config.
|
||||
model_index = get_hf_file_to_dict("model_index.json", model) or {}
|
||||
select_layers = model_index.get("text_encoder_select_layers") or DEFAULT_TEXT_ENCODER_SELECT_LAYERS
|
||||
self.text_encoder_select_layers = tuple(int(i) for i in select_layers)
|
||||
self.is_distilled = bool(model_index.get("is_distilled", False))
|
||||
self.patch_size = int(model_index.get("patch_size", 2))
|
||||
|
||||
subfolders = ["scheduler", "text_encoder", "vae", "tokenizer"]
|
||||
# See ``hub_prefetch.py`` for the transformers v5 subfolder race.
|
||||
prefetch_subfolders(model, subfolders, local_files_only=local_files_only)
|
||||
|
||||
self.scheduler = FlowMatchEulerDiscreteScheduler.from_pretrained(
|
||||
model, subfolder="scheduler", local_files_only=local_files_only
|
||||
)
|
||||
|
||||
self.text_encoder = from_pretrained_with_prefetch(
|
||||
Qwen3VLModel.from_pretrained,
|
||||
model,
|
||||
subfolder="text_encoder",
|
||||
prefetch_list=subfolders,
|
||||
local_files_only=local_files_only,
|
||||
torch_dtype=od_config.dtype,
|
||||
)
|
||||
# Drop the unused Qwen3-VL vision tower before moving to GPU so it never consumes GPU memory.
|
||||
if hasattr(self.text_encoder, "visual"):
|
||||
del self.text_encoder.visual
|
||||
else:
|
||||
logger.warning("Krea2: vision tower not found on text encoder; skipping drop")
|
||||
self.text_encoder = self.text_encoder.to(self.device)
|
||||
|
||||
self.vae = from_pretrained_with_prefetch(
|
||||
DistributedAutoencoderKLQwenImage.from_pretrained,
|
||||
model,
|
||||
subfolder="vae",
|
||||
prefetch_list=subfolders,
|
||||
local_files_only=local_files_only,
|
||||
torch_dtype=od_config.dtype,
|
||||
).to(self.device)
|
||||
|
||||
transformer_kwargs = get_transformer_config_kwargs(od_config.tf_model_config, Krea2Transformer2DModel)
|
||||
self.transformer = Krea2Transformer2DModel(
|
||||
od_config=od_config, quant_config=od_config.quantization_config, **transformer_kwargs
|
||||
)
|
||||
|
||||
self.tokenizer = AutoTokenizer.from_pretrained(model, subfolder="tokenizer", local_files_only=local_files_only)
|
||||
|
||||
self.vae_scale_factor = 2 ** len(self.vae.temperal_downsample) if getattr(self, "vae", None) else 8
|
||||
self.image_processor = VaeImageProcessor(vae_scale_factor=self.vae_scale_factor * self.patch_size)
|
||||
self.default_sample_size = 1024
|
||||
|
||||
# Chat template tokenized as a fixed-length block: prompt padded to a fixed length first, assistant suffix
|
||||
# appended after the padding; the first ``prompt_template_encode_start_idx`` system tokens are later dropped.
|
||||
self.prompt_template_encode_prefix = (
|
||||
"<|im_start|>system\nDescribe the image by detailing the color, shape, size, texture, quantity, text, "
|
||||
"spatial relationships of the objects and background:<|im_end|>\n<|im_start|>user\n"
|
||||
)
|
||||
self.prompt_template_encode_suffix = "<|im_end|>\n<|im_start|>assistant\n"
|
||||
self.prompt_template_encode_start_idx = 34
|
||||
self.prompt_template_encode_num_suffix_tokens = 5
|
||||
|
||||
self.setup_diffusion_pipeline_profiler(
|
||||
enable_diffusion_pipeline_profiler=self.od_config.enable_diffusion_pipeline_profiler
|
||||
)
|
||||
|
||||
def get_text_hidden_states(
|
||||
self,
|
||||
prompt: str | list[str],
|
||||
max_sequence_length: int = 512,
|
||||
device: torch.device | None = None,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Tokenize ``prompt`` into the fixed-length Krea 2 layout and tap the selected encoder hidden states.
|
||||
|
||||
Returns ``(hidden_states, attention_mask)`` of shapes ``(batch, text_seq_len, num_text_layers,
|
||||
text_hidden_dim)`` and ``(batch, text_seq_len)`` (bool).
|
||||
"""
|
||||
device = device or self.device
|
||||
prompt = [prompt] if isinstance(prompt, str) else prompt
|
||||
prefix_idx = self.prompt_template_encode_start_idx
|
||||
text = [self.prompt_template_encode_prefix + e for e in prompt]
|
||||
text_tokens = self.tokenizer(
|
||||
text,
|
||||
truncation=True,
|
||||
padding="max_length",
|
||||
max_length=max_sequence_length + prefix_idx - self.prompt_template_encode_num_suffix_tokens,
|
||||
return_tensors="pt",
|
||||
).to(device)
|
||||
suffix_tokens = self.tokenizer([self.prompt_template_encode_suffix] * len(text), return_tensors="pt").to(device)
|
||||
|
||||
input_ids = torch.cat([text_tokens.input_ids, suffix_tokens.input_ids], dim=1)
|
||||
attention_mask = torch.cat([text_tokens.attention_mask, suffix_tokens.attention_mask], dim=1).bool()
|
||||
|
||||
# Padding sits in the middle (``[prefix | prompt | PAD | suffix]``), so positions must count only real tokens
|
||||
# (padding consumes no position). Broadcast across the 3 mRoPE axes (T/H/W equal for text).
|
||||
position_ids = (attention_mask.long().cumsum(dim=-1) - 1).clamp(min=0)
|
||||
position_ids = position_ids.unsqueeze(0).expand(3, -1, -1)
|
||||
|
||||
outputs = self.text_encoder(
|
||||
input_ids=input_ids,
|
||||
attention_mask=attention_mask,
|
||||
position_ids=position_ids,
|
||||
output_hidden_states=True,
|
||||
)
|
||||
hidden_states = torch.stack([outputs.hidden_states[i] for i in self.text_encoder_select_layers], dim=2)
|
||||
|
||||
hidden_states = hidden_states[:, prefix_idx:]
|
||||
attention_mask = attention_mask[:, prefix_idx:]
|
||||
return hidden_states, attention_mask
|
||||
|
||||
def encode_prompt(
|
||||
self,
|
||||
prompt: str | list[str],
|
||||
device: torch.device | None = None,
|
||||
num_images_per_prompt: int = 1,
|
||||
prompt_embeds: torch.Tensor | None = None,
|
||||
prompt_embeds_mask: torch.Tensor | None = None,
|
||||
max_sequence_length: int = 512,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
device = device or self.device
|
||||
|
||||
if prompt_embeds is None:
|
||||
prompt_embeds, prompt_embeds_mask = self.get_text_hidden_states(prompt, max_sequence_length, device)
|
||||
|
||||
batch_size, seq_len, num_text_layers, dim = prompt_embeds.shape
|
||||
prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1, 1)
|
||||
prompt_embeds = prompt_embeds.view(batch_size * num_images_per_prompt, seq_len, num_text_layers, dim)
|
||||
prompt_embeds_mask = prompt_embeds_mask.repeat(1, num_images_per_prompt)
|
||||
prompt_embeds_mask = prompt_embeds_mask.view(batch_size * num_images_per_prompt, seq_len)
|
||||
|
||||
return prompt_embeds, prompt_embeds_mask
|
||||
|
||||
def _pack_latents(self, latents, batch_size, num_channels_latents, height, width):
|
||||
p = self.patch_size
|
||||
latents = latents.view(batch_size, num_channels_latents, height // p, p, width // p, p)
|
||||
latents = latents.permute(0, 2, 4, 1, 3, 5)
|
||||
latents = latents.reshape(batch_size, (height // p) * (width // p), num_channels_latents * p * p)
|
||||
return latents
|
||||
|
||||
def _unpack_latents(self, latents, height, width):
|
||||
batch_size, _, channels = latents.shape
|
||||
p = self.patch_size
|
||||
|
||||
height = p * (int(height) // (self.vae_scale_factor * p))
|
||||
width = p * (int(width) // (self.vae_scale_factor * p))
|
||||
|
||||
latents = latents.view(batch_size, height // p, width // p, channels // (p * p), p, p)
|
||||
latents = latents.permute(0, 3, 1, 4, 2, 5)
|
||||
latents = latents.reshape(batch_size, channels // (p * p), 1, height, width)
|
||||
return latents
|
||||
|
||||
@staticmethod
|
||||
def prepare_position_ids(text_seq_len: int, grid_height: int, grid_width: int, device: torch.device):
|
||||
"""Build the ``(text_seq_len + grid_height * grid_width, 3)`` rotary coordinates: text tokens sit at the
|
||||
origin, image tokens carry their ``(0, h, w)`` latent-grid coordinates."""
|
||||
text_ids = torch.zeros(text_seq_len, 3, device=device)
|
||||
image_ids = torch.zeros(grid_height, grid_width, 3, device=device)
|
||||
image_ids[..., 1] = torch.arange(grid_height, device=device)[:, None]
|
||||
image_ids[..., 2] = torch.arange(grid_width, device=device)[None, :]
|
||||
image_ids = image_ids.reshape(grid_height * grid_width, 3)
|
||||
return torch.cat([text_ids, image_ids], dim=0)
|
||||
|
||||
def prepare_latents(self, batch_size, num_channels_latents, height, width, dtype, device, generator, latents=None):
|
||||
if latents is not None:
|
||||
return latents.to(device=device, dtype=dtype)
|
||||
|
||||
latent_height = height // self.vae_scale_factor
|
||||
latent_width = width // self.vae_scale_factor
|
||||
shape = (batch_size, num_channels_latents, latent_height, latent_width)
|
||||
|
||||
if isinstance(generator, list) and len(generator) != batch_size:
|
||||
raise ValueError(
|
||||
f"You have passed a list of generators of length {len(generator)}, but requested an effective batch"
|
||||
f" size of {batch_size}. Make sure the batch size matches the length of the generators."
|
||||
)
|
||||
|
||||
latents = randn_tensor(shape, generator=generator, device=device, dtype=dtype)
|
||||
latents = self._pack_latents(latents, batch_size, num_channels_latents, latent_height, latent_width)
|
||||
return latents
|
||||
|
||||
def _extract_prompts(self, prompts):
|
||||
"""Extract prompt and negative_prompt from the request's OmniPromptType list."""
|
||||
prompt = [p if isinstance(p, str) else (p.get("prompt") or "") for p in prompts] or None
|
||||
if all(isinstance(p, str) or p.get("negative_prompt") is None for p in prompts):
|
||||
negative_prompt = None
|
||||
elif prompts:
|
||||
negative_prompt = ["" if isinstance(p, str) else (p.get("negative_prompt") or "") for p in prompts]
|
||||
else:
|
||||
negative_prompt = None
|
||||
return prompt, negative_prompt
|
||||
|
||||
@property
|
||||
def do_classifier_free_guidance(self):
|
||||
return self._guidance_scale > 0
|
||||
|
||||
def diffuse(
|
||||
self,
|
||||
latents: torch.Tensor,
|
||||
timesteps: torch.Tensor,
|
||||
position_ids: torch.Tensor,
|
||||
prompt_embeds: torch.Tensor,
|
||||
prompt_embeds_mask: torch.Tensor,
|
||||
negative_prompt_embeds: torch.Tensor | None,
|
||||
negative_prompt_embeds_mask: torch.Tensor | None,
|
||||
guidance_scale: float,
|
||||
) -> torch.Tensor:
|
||||
"""Run the flow-matching denoising loop and return the final latents."""
|
||||
self.scheduler.set_begin_index(0)
|
||||
transformer_dtype = self.transformer.dtype
|
||||
with self.progress_bar(total=len(timesteps)) as progress_bar:
|
||||
for _, t in enumerate(timesteps):
|
||||
timestep = (t / self.scheduler.config.num_train_timesteps).expand(latents.shape[0])
|
||||
timestep = timestep.to(transformer_dtype)
|
||||
latent_input = latents.to(transformer_dtype)
|
||||
|
||||
noise_pred = self.transformer(
|
||||
hidden_states=latent_input,
|
||||
encoder_hidden_states=prompt_embeds.to(transformer_dtype),
|
||||
timestep=timestep,
|
||||
position_ids=position_ids,
|
||||
encoder_attention_mask=prompt_embeds_mask,
|
||||
)
|
||||
|
||||
if self.do_classifier_free_guidance:
|
||||
neg_noise_pred = self.transformer(
|
||||
hidden_states=latent_input,
|
||||
encoder_hidden_states=negative_prompt_embeds.to(transformer_dtype),
|
||||
timestep=timestep,
|
||||
position_ids=position_ids,
|
||||
encoder_attention_mask=negative_prompt_embeds_mask,
|
||||
)
|
||||
noise_pred = noise_pred + guidance_scale * (noise_pred - neg_noise_pred)
|
||||
|
||||
latents = self.scheduler.step(noise_pred, t, latents, return_dict=False)[0]
|
||||
progress_bar.update()
|
||||
|
||||
return latents
|
||||
|
||||
def forward(
|
||||
self,
|
||||
req: OmniDiffusionRequest,
|
||||
prompt: str | list[str] | None = None,
|
||||
negative_prompt: str | list[str] | None = None,
|
||||
height: int = 1024,
|
||||
width: int = 1024,
|
||||
num_inference_steps: int = 28,
|
||||
sigmas: list[float] | None = None,
|
||||
guidance_scale: float = 4.5,
|
||||
num_images_per_prompt: int = 1,
|
||||
generator: torch.Generator | list[torch.Generator] | None = None,
|
||||
latents: torch.Tensor | None = None,
|
||||
output_type: str | None = "pil",
|
||||
max_sequence_length: int = 512,
|
||||
) -> DiffusionOutput:
|
||||
extracted_prompt, extracted_negative = self._extract_prompts(req.prompts)
|
||||
prompt = extracted_prompt or prompt
|
||||
if extracted_negative is not None:
|
||||
negative_prompt = extracted_negative
|
||||
|
||||
sp = req.sampling_params
|
||||
height = sp.height or height
|
||||
width = sp.width or width
|
||||
num_inference_steps = sp.num_inference_steps or num_inference_steps
|
||||
sigmas = sp.sigmas or sigmas
|
||||
max_sequence_length = sp.max_sequence_length or max_sequence_length
|
||||
generator = sp.generator or generator
|
||||
if sp.guidance_scale_provided:
|
||||
guidance_scale = sp.guidance_scale
|
||||
else:
|
||||
# Distilled checkpoint is trained guidance-free (and the request layer coerces an explicit
|
||||
# guidance_scale=0 to "not provided"), so default it to no CFG.
|
||||
guidance_scale = 0.0 if self.is_distilled else guidance_scale
|
||||
num_images_per_prompt = sp.num_outputs_per_prompt if sp.num_outputs_per_prompt > 0 else num_images_per_prompt
|
||||
|
||||
multiple = self.vae_scale_factor * self.patch_size
|
||||
if height % multiple != 0 or width % multiple != 0:
|
||||
rounded_height = ((height + multiple - 1) // multiple) * multiple
|
||||
rounded_width = ((width + multiple - 1) // multiple) * multiple
|
||||
logger.warning(
|
||||
"`height` and `width` must be multiples of %d; rounding up from %dx%d to %dx%d.",
|
||||
multiple,
|
||||
height,
|
||||
width,
|
||||
rounded_height,
|
||||
rounded_width,
|
||||
)
|
||||
height, width = rounded_height, rounded_width
|
||||
|
||||
device = self.device
|
||||
self._guidance_scale = guidance_scale
|
||||
|
||||
if prompt is not None and isinstance(prompt, str):
|
||||
batch_size = 1
|
||||
elif prompt is not None and isinstance(prompt, list):
|
||||
batch_size = len(prompt)
|
||||
else:
|
||||
batch_size = 1
|
||||
|
||||
prompt_embeds, prompt_embeds_mask = self.encode_prompt(
|
||||
prompt=prompt,
|
||||
device=device,
|
||||
num_images_per_prompt=num_images_per_prompt,
|
||||
max_sequence_length=max_sequence_length,
|
||||
)
|
||||
negative_prompt_embeds = None
|
||||
negative_prompt_embeds_mask = None
|
||||
if self.do_classifier_free_guidance:
|
||||
if negative_prompt is None:
|
||||
negative_prompt = ""
|
||||
if isinstance(negative_prompt, str):
|
||||
negative_prompt = [negative_prompt] * batch_size
|
||||
negative_prompt_embeds, negative_prompt_embeds_mask = self.encode_prompt(
|
||||
prompt=negative_prompt,
|
||||
device=device,
|
||||
num_images_per_prompt=num_images_per_prompt,
|
||||
max_sequence_length=max_sequence_length,
|
||||
)
|
||||
|
||||
num_channels_latents = self.transformer.in_channels // (self.patch_size**2)
|
||||
latents = self.prepare_latents(
|
||||
batch_size * num_images_per_prompt,
|
||||
num_channels_latents,
|
||||
height,
|
||||
width,
|
||||
prompt_embeds.dtype,
|
||||
device,
|
||||
generator,
|
||||
latents,
|
||||
)
|
||||
grid_height = height // (self.vae_scale_factor * self.patch_size)
|
||||
grid_width = width // (self.vae_scale_factor * self.patch_size)
|
||||
position_ids = self.prepare_position_ids(prompt_embeds.shape[1], grid_height, grid_width, device)
|
||||
|
||||
sigmas = np.linspace(1.0, 1 / num_inference_steps, num_inference_steps) if sigmas is None else sigmas
|
||||
image_seq_len = latents.shape[1]
|
||||
if self.is_distilled:
|
||||
mu = 1.15
|
||||
else:
|
||||
mu = calculate_shift(
|
||||
image_seq_len,
|
||||
self.scheduler.config.get("base_image_seq_len", 256),
|
||||
self.scheduler.config.get("max_image_seq_len", 6400),
|
||||
self.scheduler.config.get("base_shift", 0.5),
|
||||
self.scheduler.config.get("max_shift", 1.15),
|
||||
)
|
||||
timesteps, num_inference_steps = retrieve_timesteps(
|
||||
self.scheduler, num_inference_steps, device, sigmas=sigmas, mu=mu
|
||||
)
|
||||
|
||||
latents = self.diffuse(
|
||||
latents=latents,
|
||||
timesteps=timesteps,
|
||||
position_ids=position_ids,
|
||||
prompt_embeds=prompt_embeds,
|
||||
prompt_embeds_mask=prompt_embeds_mask,
|
||||
negative_prompt_embeds=negative_prompt_embeds,
|
||||
negative_prompt_embeds_mask=negative_prompt_embeds_mask,
|
||||
guidance_scale=guidance_scale,
|
||||
)
|
||||
|
||||
if output_type == "latent":
|
||||
image = latents
|
||||
else:
|
||||
latents = self._unpack_latents(latents, height, width)
|
||||
latents = latents.to(self.vae.dtype)
|
||||
latents_mean = (
|
||||
torch.tensor(self.vae.config.latents_mean)
|
||||
.view(1, self.vae.config.z_dim, 1, 1, 1)
|
||||
.to(latents.device, latents.dtype)
|
||||
)
|
||||
latents_std = 1.0 / torch.tensor(self.vae.config.latents_std).view(1, self.vae.config.z_dim, 1, 1, 1).to(
|
||||
latents.device, latents.dtype
|
||||
)
|
||||
latents = latents / latents_std + latents_mean
|
||||
image = self.vae.decode(latents, return_dict=False)[0][:, :, 0]
|
||||
|
||||
return DiffusionOutput(
|
||||
output=image,
|
||||
stage_durations=self.stage_durations if hasattr(self, "stage_durations") else None,
|
||||
)
|
||||
|
||||
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
||||
loader = AutoWeightsLoader(self)
|
||||
loaded_weights = loader.load_weights(weights)
|
||||
# VAE and text encoder load eagerly in __init__; record their params so strict load checks pass.
|
||||
loaded_weights |= {f"vae.{name}" for name, _ in self.vae.named_parameters()}
|
||||
if self.text_encoder is not None:
|
||||
loaded_weights |= {f"text_encoder.{name}" for name, _ in self.text_encoder.named_parameters()}
|
||||
return loaded_weights
|
||||
@@ -306,6 +306,11 @@ _DIFFUSION_MODELS = {
|
||||
"pipeline_sdxl",
|
||||
"StableDiffusionXLPipeline",
|
||||
),
|
||||
"Krea2Pipeline": (
|
||||
"krea2",
|
||||
"pipeline_krea2",
|
||||
"Krea2Pipeline",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -533,6 +538,7 @@ _DIFFUSION_POST_PROCESS_FUNCS = {
|
||||
"Cosmos3OmniDiffusersPipeline": "get_cosmos3_post_process_func",
|
||||
"HiDreamImagePipeline": "get_hidream_image_post_process_func",
|
||||
"StableDiffusionXLPipeline": "get_sdxl_image_post_process_func",
|
||||
"Krea2Pipeline": "get_krea2_post_process_func",
|
||||
}
|
||||
|
||||
_DIFFUSION_ACTION_POST_PROCESS_FUNCS = {
|
||||
|
||||
@@ -1043,6 +1043,7 @@ class AsyncOmniEngine:
|
||||
"enable_multithread_weight_load": kwargs.get("enable_multithread_weight_load", True),
|
||||
"num_weight_load_threads": kwargs.get("num_weight_load_threads", 4),
|
||||
"quantization": kwargs.get("quantization", None),
|
||||
"quantization_config": kwargs.get("quantization_config", None),
|
||||
"diffusion_kv_cache_dtype": kwargs.get("diffusion_kv_cache_dtype", None),
|
||||
"diffusion_kv_cache_skip_steps": kwargs.get("diffusion_kv_cache_skip_steps", None),
|
||||
"diffusion_kv_cache_skip_layers": kwargs.get("diffusion_kv_cache_skip_layers", None),
|
||||
@@ -1233,7 +1234,7 @@ class AsyncOmniEngine:
|
||||
kwargs.get("diffusion_attention_config"),
|
||||
attention_backend=kwargs.get("diffusion_attention_backend"),
|
||||
)
|
||||
quantization_config = kwargs.get("diffusion_quantization_config")
|
||||
quantization_config = kwargs.get("diffusion_quantization_config") or kwargs.get("quantization_config")
|
||||
if quantization_config is not None:
|
||||
if (
|
||||
not hasattr(cfg.engine_args, "quantization_config")
|
||||
|
||||
@@ -478,7 +478,7 @@ class OmniServeCommand(CLISubcommand):
|
||||
default=None,
|
||||
help=(
|
||||
"JSON string for diffusion quantization_config. "
|
||||
'Example: \'{"method":"gguf","gguf_model":"/path/to/model.gguf"}\'.'
|
||||
'Example: \'{"method":"fp8","activation_scheme":"dynamic"}\'.'
|
||||
),
|
||||
)
|
||||
omni_config_group.add_argument(
|
||||
|
||||
@@ -22,6 +22,7 @@ from .constants import (
|
||||
DEFAULT_SIGMA,
|
||||
DEFAULT_TEMPERATURE,
|
||||
HISTORY_PATCH_SIZE,
|
||||
INITIAL_LATENT_CHUNK_SIZE,
|
||||
KEY_CFG,
|
||||
KEY_CHUNK_ID,
|
||||
KEY_DECODE_STEP,
|
||||
@@ -239,6 +240,7 @@ class MingTTSConfig:
|
||||
max_decode_steps: int = MAX_DECODE_STEPS
|
||||
|
||||
latent_chunk_size: int = LATENT_CHUNK_SIZE
|
||||
initial_latent_chunk_size: int = INITIAL_LATENT_CHUNK_SIZE
|
||||
latent_left_context: int = LATENT_LEFT_CONTEXT
|
||||
|
||||
text_eos_token_id: int = TEXT_EOS_TOKEN_ID
|
||||
|
||||
@@ -58,6 +58,7 @@ DEFAULT_TEMPERATURE = 0.0
|
||||
|
||||
# Connector / Stage-2 streaming defaults (runtime tuning)
|
||||
LATENT_CHUNK_SIZE = 25
|
||||
INITIAL_LATENT_CHUNK_SIZE = 4
|
||||
LATENT_LEFT_CONTEXT = 0
|
||||
MAX_DECODE_STEPS = 200
|
||||
|
||||
|
||||
@@ -172,6 +172,9 @@ def validate_ming_tts_config(cfg: Any) -> None:
|
||||
|
||||
if cfg.latent_chunk_size <= 0:
|
||||
raise ValueError(f"latent_chunk_size must be > 0, got {cfg.latent_chunk_size}.")
|
||||
initial_latent_chunk_size = getattr(cfg, "initial_latent_chunk_size", 0)
|
||||
if initial_latent_chunk_size < 0:
|
||||
raise ValueError(f"initial_latent_chunk_size must be >= 0, got {initial_latent_chunk_size}.")
|
||||
if cfg.latent_left_context < 0:
|
||||
raise ValueError(f"latent_left_context must be >= 0, got {cfg.latent_left_context}.")
|
||||
if cfg.max_decode_steps <= 0:
|
||||
|
||||
@@ -12,6 +12,7 @@ from vllm.logger import init_logger
|
||||
from vllm_omni.data_entry_keys import CodesStruct, MetaStruct, OmniPayloadStruct
|
||||
from vllm_omni.inputs.data import OmniTokensPrompt
|
||||
from vllm_omni.model_executor.models.ming_tts.config_ming_tts import (
|
||||
INITIAL_LATENT_CHUNK_SIZE,
|
||||
KEY_CHUNK_ID,
|
||||
KEY_REQUEST_ID,
|
||||
LATENT_CHUNK_SIZE,
|
||||
@@ -116,7 +117,7 @@ def _decode_stop_reason(value: Any) -> str | None:
|
||||
return MING_STOP_REASON_BY_CODE.get(int(value))
|
||||
|
||||
|
||||
def _get_async_chunk_config(transfer_manager: Any) -> tuple[int, int]:
|
||||
def _get_async_chunk_config(transfer_manager: Any) -> tuple[int, int, int]:
|
||||
connector = getattr(transfer_manager, "connector", None)
|
||||
raw_cfg = getattr(connector, "config", {}) or {}
|
||||
cfg = raw_cfg.get("extra", raw_cfg) if isinstance(raw_cfg, Mapping) else {}
|
||||
@@ -131,9 +132,19 @@ def _get_async_chunk_config(transfer_manager: Any) -> tuple[int, int]:
|
||||
LATENT_LEFT_CONTEXT,
|
||||
)
|
||||
chunk_size = int(cfg.get("latent_chunk_size", LATENT_CHUNK_SIZE))
|
||||
initial_chunk_size = int(cfg.get("initial_latent_chunk_size", INITIAL_LATENT_CHUNK_SIZE))
|
||||
left_context = int(cfg.get("latent_left_context", LATENT_LEFT_CONTEXT))
|
||||
if chunk_size <= 0:
|
||||
raise ValueError(f"Invalid Ming latent_chunk_size={chunk_size}")
|
||||
if initial_chunk_size < 0:
|
||||
raise ValueError(f"Invalid Ming initial_latent_chunk_size={initial_chunk_size}")
|
||||
if initial_chunk_size > chunk_size:
|
||||
logger.warning(
|
||||
"Ming initial_latent_chunk_size=%d > latent_chunk_size=%d, clamping to latent_chunk_size.",
|
||||
initial_chunk_size,
|
||||
chunk_size,
|
||||
)
|
||||
initial_chunk_size = chunk_size
|
||||
# Stage-2 VAE caches past_key_values and stream_state by request_id.
|
||||
# Replaying left-context latents would double-feed cached decoder state.
|
||||
if left_context != 0:
|
||||
@@ -142,7 +153,7 @@ def _get_async_chunk_config(transfer_manager: Any) -> tuple[int, int]:
|
||||
"Ming boundary continuity is handled by per-request decoder state cache, not "
|
||||
f"latent replay. Got latent_left_context={left_context}."
|
||||
)
|
||||
return chunk_size, left_context
|
||||
return chunk_size, initial_chunk_size, left_context
|
||||
|
||||
|
||||
def _build_chunk_observability(
|
||||
@@ -196,7 +207,7 @@ def llm2audio_vae_async_chunk(
|
||||
if patch is not None:
|
||||
transfer_manager.code_prompt_token_ids[request_id].append(patch)
|
||||
|
||||
chunk_size, _ = _get_async_chunk_config(transfer_manager)
|
||||
chunk_size, initial_chunk_size, _ = _get_async_chunk_config(transfer_manager)
|
||||
|
||||
patches = transfer_manager.code_prompt_token_ids[request_id]
|
||||
seen_patch_len = int(state.get("seen_patch_len", 0))
|
||||
@@ -225,11 +236,17 @@ def llm2audio_vae_async_chunk(
|
||||
)
|
||||
return None
|
||||
|
||||
chunk_length = length % chunk_size
|
||||
if chunk_length != 0 and not finished:
|
||||
return None
|
||||
|
||||
emit_count = chunk_length if chunk_length != 0 else chunk_size
|
||||
use_first_chunk = 0 < initial_chunk_size < chunk_size and seen_patch_len == 0
|
||||
if finished:
|
||||
emit_count = length
|
||||
elif use_first_chunk:
|
||||
if length < initial_chunk_size:
|
||||
return None
|
||||
emit_count = initial_chunk_size
|
||||
else:
|
||||
if length < chunk_size:
|
||||
return None
|
||||
emit_count = chunk_size
|
||||
emit_patches = list(new_patches[:emit_count])
|
||||
state["seen_patch_len"] = seen_patch_len + len(emit_patches)
|
||||
latent_patches = torch.stack(emit_patches, dim=0)
|
||||
|
||||
@@ -68,6 +68,10 @@ def load_omni_general_plugins() -> None:
|
||||
return
|
||||
omni_plugins_loaded = True
|
||||
|
||||
from vllm.plugins import load_general_plugins
|
||||
|
||||
load_general_plugins()
|
||||
|
||||
plugins = load_omni_plugins_by_group(group=OMNI_DEFAULT_PLUGINS_GROUP)
|
||||
# General plugins: we only need to execute the loaded functions.
|
||||
for func in plugins.values():
|
||||
|
||||
@@ -12,10 +12,10 @@ Adds per-component quantization for multi-stage models.
|
||||
"""
|
||||
|
||||
from .component_config import ComponentQuantizationConfig
|
||||
from .factory import SUPPORTED_QUANTIZATION_METHODS, build_quant_config
|
||||
from .factory import SUPPORTED_QUANTIZATION_METHODS, build_quant_config, register_quantization_override
|
||||
from .inc_config import OmniINCConfig
|
||||
|
||||
# Heavy configs (GGUF, MXFP8) are NOT imported here to avoid pulling in
|
||||
# Heavy configs are NOT imported here to avoid pulling in
|
||||
# optional dependencies (pynvml, torch_npu) at module load time.
|
||||
# Import them directly when needed:
|
||||
# from vllm_omni.quantization.mxfp8_config import DiffusionMXFP8Config
|
||||
@@ -25,4 +25,5 @@ __all__ = [
|
||||
"ComponentQuantizationConfig",
|
||||
"OmniINCConfig",
|
||||
"SUPPORTED_QUANTIZATION_METHODS",
|
||||
"register_quantization_override",
|
||||
]
|
||||
@@ -3,8 +3,8 @@
|
||||
"""Factory for building quantization configs.
|
||||
|
||||
build_quant_config() delegates to vLLM's quantization registry.
|
||||
The only extension point is _OVERRIDES for methods that need a
|
||||
different QuantizationConfig subclass in the OMNI context (e.g. GGUF).
|
||||
Out-of-tree integrations can register Omni-specific builders with
|
||||
register_quantization_override().
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -81,13 +81,6 @@ from .component_config import ComponentQuantizationConfig # noqa: E402
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
def _build_gguf(**kw: Any) -> QuantizationConfig:
|
||||
"""Lazy import to avoid pulling in CUDA/pynvml at module load time."""
|
||||
from .gguf_config import DiffusionGGUFConfig
|
||||
|
||||
return DiffusionGGUFConfig(**kw)
|
||||
|
||||
|
||||
def _build_int8(**kw: Any) -> QuantizationConfig:
|
||||
"""Lazy import for Int8 diffusion config (supports CUDA + NPU)."""
|
||||
from .int8_config import DiffusionInt8Config
|
||||
@@ -140,7 +133,6 @@ def _build_inc(**kw: Any) -> QuantizationConfig:
|
||||
|
||||
|
||||
_OVERRIDES: dict[str, Callable[..., QuantizationConfig]] = {
|
||||
"gguf": _build_gguf,
|
||||
"int8": _build_int8,
|
||||
"mxfp8": _build_mxfp8,
|
||||
"mxfp4": _build_mxfp4,
|
||||
@@ -150,7 +142,24 @@ _OVERRIDES: dict[str, Callable[..., QuantizationConfig]] = {
|
||||
"auto_round": _build_inc,
|
||||
}
|
||||
|
||||
SUPPORTED_QUANTIZATION_METHODS: list[str] = list(dict.fromkeys(QUANTIZATION_METHODS + list(_OVERRIDES.keys())))
|
||||
|
||||
def _compute_supported_quantization_methods() -> list[str]:
|
||||
return list(dict.fromkeys(QUANTIZATION_METHODS + list(_OVERRIDES.keys())))
|
||||
|
||||
|
||||
SUPPORTED_QUANTIZATION_METHODS: list[str] = _compute_supported_quantization_methods()
|
||||
|
||||
|
||||
def _refresh_registered_methods() -> None:
|
||||
SUPPORTED_QUANTIZATION_METHODS[:] = _compute_supported_quantization_methods()
|
||||
global _CACHED_ALIAS_MAP
|
||||
_CACHED_ALIAS_MAP = None
|
||||
|
||||
|
||||
def register_quantization_override(method: str, builder: Callable[..., QuantizationConfig]) -> None:
|
||||
"""Register an Omni-specific quantization config builder."""
|
||||
_OVERRIDES[_normalize_method_name(method)] = builder
|
||||
_refresh_registered_methods()
|
||||
|
||||
|
||||
def _build_reverse_alias_map() -> dict[str, str]:
|
||||
|
||||
@@ -1,323 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""GGUF quantization config for diffusion transformers.
|
||||
|
||||
Uses dequant+GEMM instead of the fused kernel path (which expects 2D inputs).
|
||||
|
||||
The GGUF quantization module was migrated to an external plugin in upstream
|
||||
vLLM (commit 6635279d8). This file inlines the base classes that
|
||||
DiffusionGGUFConfig / DiffusionGGUFLinearMethod depend on, reproduced from
|
||||
the last upstream version before the migration.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from types import MappingProxyType
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import gguf
|
||||
import torch
|
||||
from gguf import GGMLQuantizationType as WeightType
|
||||
from torch.nn.parameter import Parameter, UninitializedParameter
|
||||
from vllm import _custom_ops as ops
|
||||
from vllm.logger import init_logger
|
||||
from vllm.model_executor.layers.linear import (
|
||||
LinearBase,
|
||||
LinearMethodBase,
|
||||
UnquantizedLinearMethod,
|
||||
)
|
||||
from vllm.model_executor.layers.quantization.base_config import (
|
||||
QuantizationConfig,
|
||||
QuantizeMethodBase,
|
||||
)
|
||||
from vllm.model_executor.utils import set_weight_attrs
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vllm.model_executor.layers.quantization import QuantizationMethods
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GGUF base types (inlined from upstream vllm before migration to plugin)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
UNQUANTIZED_TYPES: set[WeightType] = {WeightType.F32, WeightType.F16, WeightType.BF16}
|
||||
|
||||
STANDARD_QUANT_TYPES: set[WeightType] = {
|
||||
WeightType.Q4_0,
|
||||
WeightType.Q4_1,
|
||||
WeightType.Q5_0,
|
||||
WeightType.Q5_1,
|
||||
WeightType.Q8_0,
|
||||
WeightType.Q8_1,
|
||||
}
|
||||
KQUANT_TYPES: set[WeightType] = {
|
||||
WeightType.Q2_K,
|
||||
WeightType.Q3_K,
|
||||
WeightType.Q4_K,
|
||||
WeightType.Q5_K,
|
||||
WeightType.Q6_K,
|
||||
}
|
||||
IMATRIX_QUANT_TYPES: set[WeightType] = {
|
||||
WeightType.IQ1_M,
|
||||
WeightType.IQ1_S,
|
||||
WeightType.IQ2_XXS,
|
||||
WeightType.IQ2_XS,
|
||||
WeightType.IQ2_S,
|
||||
WeightType.IQ3_XXS,
|
||||
WeightType.IQ3_S,
|
||||
WeightType.IQ4_XS,
|
||||
WeightType.IQ4_NL,
|
||||
}
|
||||
DEQUANT_TYPES: set[WeightType] = STANDARD_QUANT_TYPES | KQUANT_TYPES | IMATRIX_QUANT_TYPES
|
||||
|
||||
|
||||
def is_layer_skipped_gguf(
|
||||
prefix: str,
|
||||
unquantized_modules: list[str],
|
||||
fused_mapping: Mapping[str, list[str]] = MappingProxyType({}),
|
||||
) -> bool:
|
||||
# Fused layers like gate_up_proj or qkv_proj will not be fused
|
||||
# in the safetensors checkpoint. So, we convert the name
|
||||
# from the fused version to unfused + check to make sure that
|
||||
# each shard of the fused layer has the same scheme.
|
||||
proj_name = prefix.split(".")[-1]
|
||||
if proj_name in fused_mapping:
|
||||
shard_prefixes = [prefix.replace(proj_name, shard_proj_name) for shard_proj_name in fused_mapping[proj_name]]
|
||||
|
||||
is_skipped = None
|
||||
for shard_prefix in shard_prefixes:
|
||||
is_shard_skipped = any(shard_prefix in module_name for module_name in unquantized_modules)
|
||||
|
||||
if is_skipped is None:
|
||||
is_skipped = is_shard_skipped
|
||||
elif is_shard_skipped != is_skipped:
|
||||
raise ValueError(
|
||||
f"Detected some but not all shards of {prefix} "
|
||||
"are quantized. All shards of fused layers "
|
||||
"to have the same precision."
|
||||
)
|
||||
else:
|
||||
is_skipped = any(module_name in prefix for module_name in unquantized_modules)
|
||||
|
||||
assert is_skipped is not None
|
||||
return is_skipped
|
||||
|
||||
|
||||
class _GGUFUninitializedParameter(UninitializedParameter):
|
||||
"""UninitializedParameter subclass that stores GGUF weight data in a list.
|
||||
|
||||
The actual tensor is materialized later via _create_padded_weight_param.
|
||||
"""
|
||||
|
||||
cls_to_become = Parameter
|
||||
data_container: list[torch.Tensor]
|
||||
|
||||
|
||||
class GGUFConfig(QuantizationConfig):
|
||||
"""Config class for GGUF (inlined from upstream vllm)."""
|
||||
|
||||
def __init__(self, unquantized_modules: list[str] | None = None) -> None:
|
||||
super().__init__()
|
||||
self.unquantized_modules = unquantized_modules or []
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "GGUFConfig()"
|
||||
|
||||
def get_name(self) -> QuantizationMethods:
|
||||
return "gguf"
|
||||
|
||||
def get_supported_act_dtypes(self) -> list[torch.dtype]:
|
||||
return [torch.half, torch.bfloat16, torch.float32]
|
||||
|
||||
@classmethod
|
||||
def get_min_capability(cls) -> int:
|
||||
return 60
|
||||
|
||||
@classmethod
|
||||
def get_config_filenames(cls) -> list[str]:
|
||||
return [] # no extra configs.
|
||||
|
||||
@classmethod
|
||||
def from_config(cls, config: dict[str, Any]) -> GGUFConfig:
|
||||
return cls()
|
||||
|
||||
@classmethod
|
||||
def override_quantization_method(
|
||||
cls, hf_quant_cfg: dict[str, Any], user_quant: str | None, hf_config=None
|
||||
) -> QuantizationMethods | None:
|
||||
if user_quant == "gguf":
|
||||
return "gguf"
|
||||
return None
|
||||
|
||||
def get_quant_method(self, layer: torch.nn.Module, prefix: str) -> QuantizeMethodBase | None:
|
||||
if isinstance(layer, LinearBase):
|
||||
if is_layer_skipped_gguf(prefix, self.unquantized_modules, self.packed_modules_mapping):
|
||||
return UnquantizedLinearMethod()
|
||||
return GGUFLinearMethod(self)
|
||||
return None
|
||||
|
||||
|
||||
class GGUFLinearMethod(LinearMethodBase):
|
||||
"""Linear method for GGUF (inlined from upstream vllm)."""
|
||||
|
||||
def __init__(self, quant_config: GGUFConfig):
|
||||
self.quant_config = quant_config
|
||||
|
||||
def create_weights(
|
||||
self,
|
||||
layer: torch.nn.Module,
|
||||
input_size_per_partition: int,
|
||||
output_partition_sizes: list[int],
|
||||
input_size: int,
|
||||
output_size: int,
|
||||
params_dtype: torch.dtype,
|
||||
**extra_weight_attrs,
|
||||
):
|
||||
self.params_dtype = params_dtype
|
||||
output_size_per_partition = sum(output_partition_sizes)
|
||||
|
||||
tensor_shape = (output_size_per_partition, input_size_per_partition)
|
||||
qweight = _GGUFUninitializedParameter(requires_grad=False)
|
||||
set_weight_attrs(
|
||||
qweight,
|
||||
{
|
||||
"input_dim": 1,
|
||||
"output_dim": 0,
|
||||
"tensor_shape": tensor_shape,
|
||||
"is_gguf_weight": True,
|
||||
"data_container": [],
|
||||
"shard_id": [],
|
||||
"shard_id_map": {},
|
||||
},
|
||||
)
|
||||
set_weight_attrs(qweight, extra_weight_attrs)
|
||||
layer.register_parameter("qweight", qweight)
|
||||
|
||||
qweight_type = Parameter(
|
||||
torch.empty(len(output_partition_sizes), dtype=torch.uint8),
|
||||
requires_grad=False,
|
||||
)
|
||||
set_weight_attrs(
|
||||
qweight_type,
|
||||
{
|
||||
"is_gguf_weight_type": True,
|
||||
"weight_type": 0,
|
||||
"shard_weight_type": {},
|
||||
"ignore_warning": True,
|
||||
},
|
||||
)
|
||||
set_weight_attrs(qweight_type, extra_weight_attrs)
|
||||
layer.register_parameter("qweight_type", qweight_type)
|
||||
|
||||
def process_weights_after_loading(self, layer: torch.nn.Module):
|
||||
qweight_type = layer.qweight_type.weight_type
|
||||
if not (qweight_type in UNQUANTIZED_TYPES or qweight_type in DEQUANT_TYPES):
|
||||
qweight_type = WeightType(qweight_type)
|
||||
raise ValueError(f"Unsupported GGUF quantization type {qweight_type} in layer {layer}.")
|
||||
self._create_padded_weight_param(layer)
|
||||
|
||||
def _create_padded_weight_param(self, layer: torch.nn.Module):
|
||||
"""Create padded weight parameter for GGUF MergedLinear layer."""
|
||||
qweight = layer.qweight
|
||||
shard_id_map = qweight.shard_id_map
|
||||
shard_id = qweight.shard_id
|
||||
if len(data_container := qweight.data_container) > 1:
|
||||
dtype = {data.dtype for data in data_container}
|
||||
assert len(dtype) == 1, ValueError(f"Data container has mixed dtypes: {dtype}")
|
||||
dtype = next(iter(dtype))
|
||||
padded_side = max(x.size(1) for x in data_container)
|
||||
concat_side = sum(x.size(0) for x in data_container)
|
||||
padded_data = torch.zeros((concat_side, padded_side), dtype=dtype, device=qweight.device)
|
||||
shard_offset_map = dict[str, tuple[int, int, int]]()
|
||||
for idx in shard_id:
|
||||
id_in_container = shard_id_map[idx]
|
||||
start = sum(x.size(0) for x in data_container[:id_in_container])
|
||||
end = start + data_container[id_in_container].size(0)
|
||||
size = data_container[id_in_container].size(1)
|
||||
padded_data[start:end, :size] = data_container[id_in_container]
|
||||
shard_offset_map[idx] = (start, end, size)
|
||||
qweight.data_container.clear()
|
||||
padded_param = Parameter(padded_data, requires_grad=False)
|
||||
set_weight_attrs(padded_param, vars(qweight))
|
||||
set_weight_attrs(padded_param, {"shard_offset_map": shard_offset_map})
|
||||
layer.register_parameter("qweight", padded_param)
|
||||
|
||||
def apply(
|
||||
self,
|
||||
layer: torch.nn.Module,
|
||||
x: torch.Tensor,
|
||||
bias: torch.Tensor | None = None,
|
||||
) -> torch.Tensor:
|
||||
# Base apply: overridden by DiffusionGGUFLinearMethod.
|
||||
raise NotImplementedError("Use DiffusionGGUFLinearMethod for diffusion GGUF")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Diffusion-specific GGUF (dequant+GEMM, replaces fused kernel path)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def dequant_gemm_gguf(x: torch.Tensor, qweight: torch.Tensor, qweight_type: int) -> torch.Tensor:
|
||||
if qweight_type in UNQUANTIZED_TYPES:
|
||||
return x @ qweight.T
|
||||
block_size, type_size = gguf.GGML_QUANT_SIZES[qweight_type]
|
||||
shape = (qweight.shape[0], qweight.shape[1] // type_size * block_size)
|
||||
weight = ops.ggml_dequantize(qweight, qweight_type, *shape, x.dtype)
|
||||
return x @ weight.T
|
||||
|
||||
|
||||
class DiffusionGGUFLinearMethod(GGUFLinearMethod):
|
||||
"""GGUF linear method using dequant+GEMM for N-D diffusion tensors."""
|
||||
|
||||
def apply(
|
||||
self,
|
||||
layer: torch.nn.Module,
|
||||
x: torch.Tensor,
|
||||
bias: torch.Tensor | None = None,
|
||||
) -> torch.Tensor:
|
||||
shard_id = getattr(layer.qweight, "shard_id", None)
|
||||
|
||||
if shard_id:
|
||||
shard_id = ["q", "k", "v"] if "q" in shard_id else shard_id
|
||||
qweight = layer.qweight
|
||||
result = []
|
||||
for idx in shard_id:
|
||||
start, end, offset = layer.qweight.shard_offset_map[idx]
|
||||
qweight_type = layer.qweight_type.shard_weight_type[idx]
|
||||
result.append(
|
||||
dequant_gemm_gguf(
|
||||
x,
|
||||
qweight[start:end, :offset].contiguous(),
|
||||
qweight_type,
|
||||
)
|
||||
)
|
||||
out = torch.cat(result, axis=-1)
|
||||
else:
|
||||
qweight = layer.qweight
|
||||
qweight_type = layer.qweight_type.weight_type
|
||||
out = dequant_gemm_gguf(x, qweight, qweight_type)
|
||||
|
||||
if bias is not None:
|
||||
out.add_(bias)
|
||||
return out
|
||||
|
||||
|
||||
class DiffusionGGUFConfig(GGUFConfig):
|
||||
"""GGUF config that carries gguf_model path and uses dequant+GEMM."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
gguf_model: str | None = None,
|
||||
unquantized_modules: list[str] | None = None,
|
||||
) -> None:
|
||||
super().__init__(unquantized_modules=unquantized_modules or [])
|
||||
self.gguf_model = gguf_model
|
||||
|
||||
def get_quant_method(self, layer: torch.nn.Module, prefix: str) -> QuantizeMethodBase | None:
|
||||
if isinstance(layer, LinearBase):
|
||||
if is_layer_skipped_gguf(prefix, self.unquantized_modules, self.packed_modules_mapping):
|
||||
return UnquantizedLinearMethod()
|
||||
return DiffusionGGUFLinearMethod(self)
|
||||
return None
|
||||
@@ -280,7 +280,6 @@ def _build_quantization_config(
|
||||
quantization: str | None,
|
||||
quantization_config_json: str | None,
|
||||
ignored_layers: str | None,
|
||||
gguf_model: str | None,
|
||||
) -> dict[str, Any] | None:
|
||||
explicit = _load_json_arg(quantization_config_json)
|
||||
if explicit is not None:
|
||||
@@ -292,8 +291,6 @@ def _build_quantization_config(
|
||||
ignored = _split_csv(ignored_layers)
|
||||
if ignored:
|
||||
config["ignored_layers"] = ignored
|
||||
if gguf_model is not None:
|
||||
config["gguf_model"] = gguf_model
|
||||
return config
|
||||
|
||||
|
||||
@@ -305,7 +302,6 @@ def _build_variant_config(args: argparse.Namespace, variant: str) -> VariantConf
|
||||
quantization=quantization,
|
||||
quantization_config_json=getattr(args, f"{variant}_quantization_config_json"),
|
||||
ignored_layers=ignored_layers,
|
||||
gguf_model=getattr(args, f"{variant}_gguf_model"),
|
||||
)
|
||||
return VariantConfig(
|
||||
label=variant,
|
||||
@@ -554,8 +550,6 @@ def parse_args() -> argparse.Namespace:
|
||||
parser.add_argument("--reference-ignored-layers", help="Comma-separated reference ignored layer patterns.")
|
||||
parser.add_argument("--candidate-ignored-layers", help="Comma-separated candidate ignored layer patterns.")
|
||||
parser.add_argument("--ignored-layers", help="Alias for --candidate-ignored-layers.")
|
||||
parser.add_argument("--reference-gguf-model", help="Reference GGUF file or HF reference when using gguf.")
|
||||
parser.add_argument("--candidate-gguf-model", help="Candidate GGUF file or HF reference when using gguf.")
|
||||
parser.add_argument("--reference-load-format", default="default", help="Reference diffusion load format.")
|
||||
parser.add_argument("--candidate-load-format", default="default", help="Candidate diffusion load format.")
|
||||
parser.add_argument("--prompt", default="a cup of coffee on the table")
|
||||
|
||||
Reference in new issue
Block a user