[Model] Add HiDream-O1-Image support (#5194)

Signed-off-by: yixiaoer <shin@yixiaoer.sg>
This commit is contained in:
Shin authored and GitHub committed 2026-08-21 14:15:37 +08:00
1 parent 73b623f2f7
commit 70bdd49402
16 files changed
+1621 -6

No files matched your search

+6
View File
@@ -255,6 +255,12 @@ steps:
- pytest -s -v tests/e2e/accuracy/test_hunyuan_image3_pixel_accuracy.py -m full_model --run-level full_model
mirror_hardwares: h100_4
- label: ":full_moon: HiDream-O1-Image · Accuracy Test"
timeout_in_minutes: 180
commands:
- pytest -s -v tests/e2e/accuracy/test_hidream_o1_image.py -m full_model --run-level full_model
mirror_hardwares: h100_2
- label: ":full_moon: Diffusion X2I(&A&T) · Perf Test · Qwen-Image"
key: nightly-diffusion-x2iat-performance-qwen-image
timeout_in_minutes: 180
+1
View File
@@ -109,6 +109,7 @@ th {
| `ErnieImagePipeline` | ERNIE-Image | `baidu/ERNIE-Image`, `baidu/ERNIE-Image-Turbo` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | — |
| `GepardTalkerForConditionalGeneration` | Gepard-1.0 | `nineninesix/gepard-1.0` | ✅︎ | | | | — |
|`HiDreamImagePipeline` | HiDream-I1-Full | `HiDream-ai/HiDream-I1-Full` | ✅︎ | ✅︎ | | | — |
| `HiDreamO1ImagePipeline` | HiDream-O1-Image | `HiDream-ai/HiDream-O1-Image` | ✅︎ | | | | — |
The `Recipe` column links the corresponding deployment recipe. `Published` is
preferred when a corresponding `recipes.vllm.ai` page exists; otherwise the
+1
View File
@@ -124,6 +124,7 @@ The following tables show which models support each feature:
| **FLUX.2-dev** | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
| **GLM-Image** | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
| **Hidream-I1-Full** | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| **HiDream-O1-Image** | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| **HunyuanImage3** | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅* |
| **Krea 2** | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ (decode) | ❌ | ❌ |
| **LongCat-Image** | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
@@ -89,7 +89,7 @@ def parse_args() -> argparse.Namespace:
"Qwen/Qwen-Image, Tongyi-MAI/Z-Image-Turbo, Qwen/Qwen-Image-2512, stepfun-ai/NextStep-1.1, "
"black-forest-labs/FLUX.1-dev, black-forest-labs/FLUX.2-klein-9B, "
"black-forest-labs/FLUX.2-dev, tencent/HunyuanImage-3.0-Instruct, "
"meituan-longcat/LongCat-Image, OvisAI/Ovis-Image, "
"HiDream-ai/HiDream-O1-Image, meituan-longcat/LongCat-Image, OvisAI/Ovis-Image, "
"stabilityai/stable-diffusion-3.5-medium, Tongyi-MAI/Z-Image-Turbo and etc.",
)
parser.add_argument(
+113
View File
@@ -0,0 +1,113 @@
# HiDream-O1-Image
> HiDream-O1 text-to-image generation through the shared offline example
## Summary
- Vendor: HiDream.ai
- Model: `HiDream-ai/HiDream-O1-Image`
- Task: Text-to-image
- Mode: Offline inference
- Maintainer: Community
## When to use this recipe
Use this recipe to generate native-resolution images with HiDream-O1 on one
or two CUDA GPUs. The two-GPU command shards the Qwen3-VL attention and MLP
weights, as well as the timestep and pixel projections.
## References
- Upstream model:
[`HiDream-ai/HiDream-O1-Image`](https://huggingface.co/HiDream-ai/HiDream-O1-Image)
- Related offline example:
[`examples/offline_inference/text_to_image/text_to_image.py`](../../examples/offline_inference/text_to_image/text_to_image.py)
- Related issue:
[`vllm-project/vllm-omni#3479`](https://github.com/vllm-project/vllm-omni/issues/3479)
## Hardware Support
## GPU
### 1x H100 80GB
#### Environment
- OS: Linux
- Python: Match the repository requirements for your checkout
- Driver / runtime: NVIDIA CUDA
- vLLM version: Match the repository requirements for your checkout
- vLLM-Omni version or commit: Use the commit you are deploying from
#### Command
```bash
python examples/offline_inference/text_to_image/text_to_image.py \
--model HiDream-ai/HiDream-O1-Image \
--prompt "A cat is sitting next to a sign that says 'HiDream-O1 vLLM-Omni'" \
--height 2048 \
--width 2048 \
--num-inference-steps 50 \
--guidance-scale 5.0 \
--seed 42 \
--output hidream_o1_output.png
```
#### Verification
```bash
python -c "from PIL import Image; im = Image.open('hidream_o1_output.png'); print(im.mode, im.size)"
```
Expected output:
```text
RGB (2048, 2048)
```
#### Notes
- The pipeline supports BF16 text-to-image generation.
- Requested sizes are matched to the closest resolution supported by the checkpoint.
### 2x H100 80GB
#### Environment
- OS: Linux
- Python: Match the repository requirements for your checkout
- Driver / runtime: NVIDIA CUDA
- vLLM version: Match the repository requirements for your checkout
- vLLM-Omni version or commit: Use the commit you are deploying from
#### Command
```bash
python examples/offline_inference/text_to_image/text_to_image.py \
--model HiDream-ai/HiDream-O1-Image \
--tensor-parallel-size 2 \
--prompt "A cat is sitting next to a sign that says 'HiDream-O1 vLLM-Omni'" \
--height 2048 \
--width 2048 \
--num-inference-steps 50 \
--guidance-scale 5.0 \
--seed 42 \
--output hidream_o1_tp2_output.png
```
#### Verification
```bash
python -c "from PIL import Image; im = Image.open('hidream_o1_tp2_output.png'); print(im.mode, im.size)"
```
Expected output:
```text
RGB (2048, 2048)
```
#### Notes
- Tensor parallelism shards the attention, MLP, timestep, and pixel projection weights across both GPUs.
- Image editing and image-conditioned generation are not supported.
+1
View File
@@ -47,6 +47,7 @@ recipes/
| [`ByteDance/Lance.md`](./ByteDance/Lance.md) | Unified AR+diffusion: text/img/video gen + understanding (Lance 3B) | 1x B300 / A100 80GB |
| [`fishaudio/Fish-Speech-S2-Pro.md`](./fishaudio/Fish-Speech-S2-Pro.md) | Online serving for TTS | 1x A800 80GB |
| [`Helios/Helios.md`](./Helios/Helios.md) | Text-to-video, image-to-video, and video-to-video generation | 1x NVIDIA H20 |
| [`HiDream-ai/HiDream-O1-Image.md`](./HiDream-ai/HiDream-O1-Image.md) | Text-to-image with shared offline example | 1x or 2x H100 80GB |
| [`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) |
| [`k2-fsa/OmniVoice.md`](./k2-fsa/OmniVoice.md) | Offline and online multilingual text-to-speech | 1x AMD MI300X (ROCm 7.2) |
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

@@ -0,0 +1,67 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from types import SimpleNamespace
from unittest.mock import patch
import pytest
from cache_dit import ForwardPattern
from torch import nn
from vllm_omni.diffusion.cache.cachedit import CacheDiTAdapterConfig, CacheDiTBackend
from vllm_omni.diffusion.models.hidream_o1_image.hidream_o1_image_transformer import (
HiDreamO1TextModel,
)
from vllm_omni.diffusion.models.hidream_o1_image.pipeline_hidream_o1_image import (
HiDreamO1ImagePipeline,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu, pytest.mark.diffusion]
def _empty_text_model() -> HiDreamO1TextModel:
model = HiDreamO1TextModel.__new__(HiDreamO1TextModel)
nn.Module.__init__(model)
model.layers = nn.ModuleList([nn.Identity()])
return model
def test_cache_dit_targets_the_decoder_module_that_owns_layers():
text_model = _empty_text_model()
pipeline = SimpleNamespace(
_dit_modules=HiDreamO1ImagePipeline._dit_modules,
model=SimpleNamespace(
model=SimpleNamespace(language_model=text_model),
),
)
with (
patch("vllm_omni.diffusion.cache.cachedit.backend.BlockAdapter") as block_adapter_cls,
patch("vllm_omni.diffusion.cache.cachedit.backend.cache_dit") as cache_dit,
):
backend = CacheDiTBackend()
backend.enable(pipeline)
backend.refresh(pipeline, num_inference_steps=50)
adapter_config = HiDreamO1TextModel._cache_dit_adapter_config
assert isinstance(adapter_config, CacheDiTAdapterConfig)
assert adapter_config.block_forward_patterns == {"layers": ForwardPattern.Pattern_3}
assert adapter_config.has_separate_cfg is True
block_adapter_cls.assert_called_once_with(
transformer=text_model,
blocks=[text_model.layers],
forward_pattern=[ForwardPattern.Pattern_3],
has_separate_cfg=True,
check_forward_pattern=True,
)
cache_dit.enable_cache.assert_called_once()
enable_call = cache_dit.enable_cache.call_args
assert enable_call.args == (block_adapter_cls.return_value,)
assert enable_call.kwargs["cache_config"].Fn_compute_blocks == 1
assert enable_call.kwargs["calibrator_config"] is None
cache_dit.refresh_context.assert_called_once_with(
text_model,
num_inference_steps=50,
verbose=True,
)
@@ -0,0 +1,65 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from __future__ import annotations
import os
from pathlib import Path
import pytest
from PIL import Image
from tests.e2e.accuracy.helpers import assert_similarity, model_output_dir
from tests.helpers.mark import hardware_test
from tests.helpers.runtime import OmniRunner
from vllm_omni.diffusion.data import DiffusionParallelConfig
from vllm_omni.diffusion.utils.image_output import extract_images_from_outputs
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
pytestmark = [pytest.mark.full_model, pytest.mark.diffusion]
MODEL_ID = "HiDream-ai/HiDream-O1-Image"
PROMPT = "A cat is sitting next to a sign that says 'HiDream-O1 vLLM-Omni'"
BASELINE_PATH = Path(__file__).resolve().parents[2] / "assets" / "hidream_o1" / "hidream_o1_baseline.png"
SSIM_THRESHOLD = 0.8
PSNR_THRESHOLD = 19.0
@hardware_test(res={"cuda": "H100"}, num_cards=2)
def test_hidream_o1_image_tp2_matches_baseline(
accuracy_artifact_root: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("DIFFUSION_ATTENTION_BACKEND", "TORCH_SDPA")
model = os.environ.get("HIDREAM_O1_IMAGE_MODEL", MODEL_ID)
with OmniRunner(
model,
parallel_config=DiffusionParallelConfig(tensor_parallel_size=2),
enforce_eager=True,
) as runner:
outputs = runner.omni.generate(
{"prompt": PROMPT},
OmniDiffusionSamplingParams(
height=2048,
width=2048,
num_inference_steps=50,
guidance_scale=5.0,
seed=42,
),
)
images = extract_images_from_outputs(outputs)
assert images
image = images[0].convert("RGB")
output_dir = model_output_dir(accuracy_artifact_root, MODEL_ID)
image.save(output_dir / "vllm_omni_tp2.png")
assert BASELINE_PATH.exists()
baseline = Image.open(BASELINE_PATH).convert("RGB")
resized = image.resize(baseline.size, Image.Resampling.LANCZOS)
assert_similarity(
model_name=MODEL_ID,
vllm_image=resized,
diffusers_image=baseline,
ssim_threshold=SSIM_THRESHOLD,
psnr_threshold=PSNR_THRESHOLD,
)
@@ -74,6 +74,7 @@ EXCLUDED_MODELS = [
"Cosmos3OmniPipeline",
"DiffusersAdapterPipeline",
"HiDreamImagePipeline",
"HiDreamO1ImagePipeline",
"DreamZeroPipeline",
"StableDiffusionXLPipeline",
"Gr00tN1d7Pipeline",
+15 -1
View File
@@ -613,6 +613,10 @@ def resolve_model_class_name(
model_type = cfg.get("model_type")
architectures = cfg.get("architectures") or []
from vllm_omni.diffusion.utils.hf_utils import _looks_like_hidream_o1
if _looks_like_hidream_o1(model, cfg):
return "HiDreamO1ImagePipeline"
if model_type == "bagel" or "BagelForConditionalGeneration" in architectures:
return "BagelPipeline"
if (
@@ -1318,7 +1322,17 @@ class OmniDiffusionConfig:
model_type = cfg.get("model_type")
architectures = cfg.get("architectures") or []
if model_type == "bagel" or "BagelForConditionalGeneration" in architectures:
from vllm_omni.diffusion.utils.hf_utils import _looks_like_hidream_o1
is_hidream_o1 = _looks_like_hidream_o1(self.model, cfg)
if self.model_class_name == "HiDreamO1ImagePipeline" and not is_hidream_o1:
raise ValueError(f"Checkpoint {self.model} does not have the HiDream-O1 signature")
if is_hidream_o1:
self.model_class_name = "HiDreamO1ImagePipeline"
self.set_tf_model_config(TransformerConfig())
self.update_multimodal_support()
elif model_type == "bagel" or "BagelForConditionalGeneration" in architectures:
self.model_class_name = "BagelPipeline"
self.set_tf_model_config(TransformerConfig())
self.update_multimodal_support()
@@ -0,0 +1,15 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from vllm_omni.diffusion.models.hidream_o1_image.hidream_o1_image_transformer import (
HiDreamO1ImageTransformer,
)
from vllm_omni.diffusion.models.hidream_o1_image.pipeline_hidream_o1_image import (
HiDreamO1ImagePipeline,
get_hidream_o1_image_post_process_func,
)
__all__ = [
"HiDreamO1ImageTransformer",
"HiDreamO1ImagePipeline",
"get_hidream_o1_image_post_process_func",
]
@@ -0,0 +1,572 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# Copyright 2025 The Qwen Team and The HuggingFace Inc. 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.
from __future__ import annotations
import math
from collections.abc import Iterable
from dataclasses import dataclass
from typing import TYPE_CHECKING
import torch
from cache_dit import ForwardPattern
from torch import nn
from transformers import Qwen3VLConfig
from vllm.distributed import get_tensor_model_parallel_world_size
from vllm.model_executor.layers.layernorm import RMSNorm
from vllm.model_executor.layers.linear import (
ColumnParallelLinear,
QKVParallelLinear,
RowParallelLinear,
)
from vllm.model_executor.layers.vocab_parallel_embedding import VocabParallelEmbedding
from vllm.model_executor.model_loader.weight_utils import default_weight_loader
from vllm.model_executor.models.qwen3 import Qwen3MLP
from vllm_omni.diffusion.attention.backends.abstract import AttentionMetadata
from vllm_omni.diffusion.attention.layer import Attention
from vllm_omni.diffusion.cache.cachedit import CacheDiTAdapterConfig
if TYPE_CHECKING:
from vllm.model_executor.layers.quantization.base_config import QuantizationConfig
def _prefix(parent: str, child: str) -> str:
return f"{parent}.{child}" if parent else child
def _rotate_half(x: torch.Tensor) -> torch.Tensor:
x1 = x[..., : x.shape[-1] // 2]
x2 = x[..., x.shape[-1] // 2 :]
return torch.cat((-x2, x1), dim=-1)
def _apply_rotary_pos_emb(
query: torch.Tensor,
key: torch.Tensor,
cos: torch.Tensor,
sin: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]:
cos = cos.unsqueeze(1)
sin = sin.unsqueeze(1)
return (
query * cos + _rotate_half(query) * sin,
key * cos + _rotate_half(key) * sin,
)
class HiDreamO1RotaryEmbedding(nn.Module):
def __init__(self, config) -> None:
super().__init__()
rope_parameters = getattr(config, "rope_parameters", None) or getattr(config, "rope_scaling", None) or {}
head_dim = getattr(config, "head_dim", config.hidden_size // config.num_attention_heads)
rope_theta = rope_parameters.get("rope_theta", getattr(config, "rope_theta", 10000.0))
inv_freq = 1.0 / (float(rope_theta) ** (torch.arange(0, head_dim, 2, dtype=torch.float32) / head_dim))
self.register_buffer("inv_freq", inv_freq, persistent=False)
self.mrope_section = rope_parameters.get("mrope_section", [24, 20, 20])
def forward(
self,
hidden_states: torch.Tensor,
position_ids: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]:
if position_ids.ndim == 2:
position_ids = position_ids.unsqueeze(0).expand(3, -1, -1)
inv_freq = self.inv_freq[None, None, :, None].float().to(hidden_states.device)
inv_freq = inv_freq.expand(3, position_ids.shape[1], -1, 1)
positions = position_ids[:, :, None, :].float()
device_type = hidden_states.device.type if hidden_states.device.type != "mps" else "cpu"
with torch.autocast(device_type=device_type, enabled=False):
freqs = (inv_freq @ positions).transpose(2, 3)
freqs_t = freqs[0]
for dim, offset in enumerate((1, 2), start=1):
length = self.mrope_section[dim] * 3
freqs_t[..., offset:length:3] = freqs[dim, ..., offset:length:3]
emb = torch.cat((freqs_t, freqs_t), dim=-1)
cos = emb.cos()
sin = emb.sin()
return cos.to(hidden_states.dtype), sin.to(hidden_states.dtype)
class HiDreamO1Attention(nn.Module):
def __init__(
self,
config,
*,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
) -> None:
super().__init__()
self.head_dim = getattr(config, "head_dim", config.hidden_size // config.num_attention_heads)
tp_size = get_tensor_model_parallel_world_size()
if config.num_attention_heads % tp_size != 0:
raise ValueError(
f"num_attention_heads={config.num_attention_heads} must be divisible by tensor_parallel_size={tp_size}"
)
if config.num_key_value_heads >= tp_size:
if config.num_key_value_heads % tp_size != 0:
raise ValueError(
f"num_key_value_heads={config.num_key_value_heads} must be divisible by "
f"tensor_parallel_size={tp_size}"
)
elif tp_size % config.num_key_value_heads != 0:
raise ValueError(
f"tensor_parallel_size={tp_size} must be divisible by num_key_value_heads={config.num_key_value_heads}"
)
self.qkv_proj = QKVParallelLinear(
config.hidden_size,
self.head_dim,
config.num_attention_heads,
config.num_key_value_heads,
bias=config.attention_bias,
quant_config=quant_config,
prefix=_prefix(prefix, "qkv_proj"),
)
self.o_proj = RowParallelLinear(
config.num_attention_heads * self.head_dim,
config.hidden_size,
bias=config.attention_bias,
quant_config=quant_config,
prefix=_prefix(prefix, "o_proj"),
)
self.q_norm = RMSNorm(self.head_dim, eps=config.rms_norm_eps)
self.k_norm = RMSNorm(self.head_dim, eps=config.rms_norm_eps)
self.scaling = self.head_dim**-0.5
self.attn = Attention(
num_heads=self.qkv_proj.num_heads,
head_size=self.head_dim,
causal=False,
softmax_scale=self.scaling,
num_kv_heads=self.qkv_proj.num_kv_heads,
prefix=_prefix(prefix, "attn"),
role="self",
)
def forward(
self,
hidden_states: torch.Tensor,
position_embeddings: tuple[torch.Tensor, torch.Tensor],
attention_mask: torch.Tensor | None,
full_attn_spans: list[list[tuple[int, int]]] | None,
) -> torch.Tensor:
batch_size, seq_len, _ = hidden_states.shape
qkv, _ = self.qkv_proj(hidden_states)
q_size = self.qkv_proj.num_heads * self.head_dim
kv_size = self.qkv_proj.num_kv_heads * self.head_dim
query, key, value = qkv.split([q_size, kv_size, kv_size], dim=-1)
query = query.view(batch_size, seq_len, self.qkv_proj.num_heads, self.head_dim).transpose(1, 2)
key = key.view(batch_size, seq_len, self.qkv_proj.num_kv_heads, self.head_dim).transpose(1, 2)
value = value.view(batch_size, seq_len, self.qkv_proj.num_kv_heads, self.head_dim).transpose(1, 2)
query = self.q_norm(query)
key = self.k_norm(key)
query, key = _apply_rotary_pos_emb(query, key, *position_embeddings)
query = query.transpose(1, 2)
key = key.transpose(1, 2)
value = value.transpose(1, 2)
attn_output = self.attn(
query,
key,
value,
AttentionMetadata(
attn_mask=attention_mask,
full_attn_spans=full_attn_spans,
),
)
attn_output = attn_output.reshape(batch_size, seq_len, q_size)
attn_output, _ = self.o_proj(attn_output)
return attn_output
class HiDreamO1DecoderLayer(nn.Module):
def __init__(
self,
config,
*,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
) -> None:
super().__init__()
self.self_attn = HiDreamO1Attention(
config,
quant_config=quant_config,
prefix=_prefix(prefix, "self_attn"),
)
self.mlp = Qwen3MLP(
hidden_size=config.hidden_size,
intermediate_size=config.intermediate_size,
hidden_act=config.hidden_act,
quant_config=quant_config,
prefix=_prefix(prefix, "mlp"),
)
self.input_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
self.post_attention_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
def forward(
self,
hidden_states: torch.Tensor,
position_embeddings: tuple[torch.Tensor, torch.Tensor],
attention_mask: torch.Tensor | None,
full_attn_spans: list[list[tuple[int, int]]] | None,
) -> torch.Tensor:
residual = hidden_states
hidden_states = self.input_layernorm(hidden_states)
hidden_states = self.self_attn(
hidden_states,
position_embeddings,
attention_mask,
full_attn_spans,
)
hidden_states = residual + hidden_states
residual = hidden_states
hidden_states = self.post_attention_layernorm(hidden_states)
hidden_states = self.mlp(hidden_states)
return residual + hidden_states
class HiDreamO1TextModel(nn.Module):
_cache_dit_adapter_config = CacheDiTAdapterConfig(
block_forward_patterns={"layers": ForwardPattern.Pattern_3},
has_separate_cfg=True,
)
def __init__(
self,
config,
*,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
) -> None:
super().__init__()
self.embed_tokens = VocabParallelEmbedding(
config.vocab_size,
config.hidden_size,
quant_config=quant_config,
prefix=_prefix(prefix, "embed_tokens"),
)
self.layers = nn.ModuleList(
[
HiDreamO1DecoderLayer(
config,
quant_config=quant_config,
prefix=_prefix(prefix, f"layers.{layer_idx}"),
)
for layer_idx in range(config.num_hidden_layers)
]
)
self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
self.rotary_emb = HiDreamO1RotaryEmbedding(config)
def forward(
self,
inputs_embeds: torch.Tensor,
position_ids: torch.Tensor,
attention_mask: torch.Tensor | None,
full_attn_spans: list[list[tuple[int, int]]] | None,
) -> torch.Tensor:
position_embeddings = self.rotary_emb(inputs_embeds, position_ids)
hidden_states = inputs_embeds
for layer in self.layers:
hidden_states = layer(
hidden_states,
position_embeddings=position_embeddings,
attention_mask=attention_mask,
full_attn_spans=full_attn_spans,
)
return self.norm(hidden_states)
class TimestepEmbedder(nn.Module):
def __init__(
self,
hidden_size: int,
*,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
frequency_embedding_size: int = 256,
) -> None:
super().__init__()
self.mlp = nn.Sequential(
ColumnParallelLinear(
frequency_embedding_size,
hidden_size,
bias=True,
gather_output=False,
quant_config=quant_config,
prefix=_prefix(prefix, "mlp.0"),
return_bias=False,
),
nn.SiLU(),
RowParallelLinear(
hidden_size,
hidden_size,
bias=True,
input_is_parallel=True,
quant_config=quant_config,
prefix=_prefix(prefix, "mlp.2"),
return_bias=False,
),
)
self.frequency_embedding_size = frequency_embedding_size
@staticmethod
def timestep_embedding(timestep: torch.Tensor, dim: int, max_period: int = 10000) -> torch.Tensor:
half = dim // 2
freqs = torch.exp(
-math.log(max_period) * torch.arange(0, half, dtype=torch.float32, device=timestep.device) / half
)
args = timestep[:, None].float() * freqs[None]
embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1)
if dim % 2:
embedding = torch.cat([embedding, torch.zeros_like(embedding[:, :1])], dim=-1)
return embedding
def forward(self, timestep: torch.Tensor) -> torch.Tensor:
embedding = self.timestep_embedding(timestep * 1000, self.frequency_embedding_size)
return self.mlp(embedding.to(self.mlp[0].weight.dtype))
class BottleneckPatchEmbed(nn.Module):
def __init__(
self,
hidden_size: int,
*,
patch_size: int,
in_channels: int,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
) -> None:
super().__init__()
bottleneck_dim = hidden_size // 4
self.proj1 = ColumnParallelLinear(
patch_size * patch_size * in_channels,
bottleneck_dim,
bias=False,
gather_output=False,
quant_config=quant_config,
prefix=_prefix(prefix, "proj1"),
return_bias=False,
)
self.proj2 = RowParallelLinear(
bottleneck_dim,
hidden_size,
bias=True,
input_is_parallel=True,
quant_config=quant_config,
prefix=_prefix(prefix, "proj2"),
return_bias=False,
)
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
return self.proj2(self.proj1(hidden_states))
class FinalLayer(nn.Module):
def __init__(
self,
hidden_size: int,
*,
patch_size: int,
out_channels: int,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
) -> None:
super().__init__()
self.linear = ColumnParallelLinear(
hidden_size,
patch_size * patch_size * out_channels,
bias=True,
gather_output=True,
quant_config=quant_config,
prefix=_prefix(prefix, "linear"),
return_bias=False,
)
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
return self.linear(hidden_states)
@dataclass
class HiDreamO1ImageOutput:
x_pred: torch.Tensor
class HiDreamO1ImageModel(nn.Module):
def __init__(
self,
config: Qwen3VLConfig,
*,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
) -> None:
super().__init__()
self.config = config
self.language_model = HiDreamO1TextModel(
config.text_config,
quant_config=quant_config,
prefix=_prefix(prefix, "language_model"),
)
self.patch_size = 32
self.in_channels = 3
hidden_size = config.text_config.hidden_size
self.t_embedder1 = TimestepEmbedder(
hidden_size,
quant_config=quant_config,
prefix=_prefix(prefix, "t_embedder1"),
)
self.x_embedder = BottleneckPatchEmbed(
hidden_size,
patch_size=self.patch_size,
in_channels=self.in_channels,
quant_config=quant_config,
prefix=_prefix(prefix, "x_embedder"),
)
self.final_layer2 = FinalLayer(
hidden_size,
patch_size=self.patch_size,
out_channels=self.in_channels,
quant_config=quant_config,
prefix=_prefix(prefix, "final_layer2"),
)
self.tms_token_id = 151673
def forward(
self,
input_ids: torch.Tensor,
position_ids: torch.Tensor,
vinputs: torch.Tensor,
timestep: torch.Tensor,
attention_mask: torch.Tensor | None,
full_attn_spans: list[list[tuple[int, int]]] | None,
) -> HiDreamO1ImageOutput:
inputs_embeds = self.language_model.embed_tokens(input_ids)
timestep_embeds = self.t_embedder1(timestep.to(inputs_embeds.device))
timestep_mask = input_ids == self.tms_token_id
inputs_embeds = torch.where(
timestep_mask.unsqueeze(-1),
timestep_embeds.unsqueeze(1).expand_as(inputs_embeds),
inputs_embeds,
)
image_embeds = self.x_embedder(vinputs.to(inputs_embeds.device)).to(inputs_embeds.dtype)
inputs_embeds = torch.cat([inputs_embeds, image_embeds], dim=1)
batch_size, total_seq_len, _ = inputs_embeds.shape
if attention_mask is not None:
if attention_mask.shape[0] == 1 and batch_size > 1:
attention_mask = attention_mask.expand(batch_size, -1, -1, -1)
expected_mask_shape = (batch_size, 1, total_seq_len, total_seq_len)
if attention_mask.shape != expected_mask_shape:
raise ValueError(
f"attention_mask must have shape {expected_mask_shape}, got {tuple(attention_mask.shape)}"
)
attention_mask = attention_mask.to(device=inputs_embeds.device, dtype=inputs_embeds.dtype)
if full_attn_spans is None or len(full_attn_spans) != batch_size:
raise ValueError(
f"full_attn_spans must contain one entry per batch item; got {full_attn_spans!r} "
f"for batch_size={batch_size}"
)
hidden_states = self.language_model(
inputs_embeds,
position_ids,
attention_mask,
full_attn_spans,
)
return HiDreamO1ImageOutput(x_pred=self.final_layer2(hidden_states))
class HiDreamO1ImageTransformer(nn.Module):
def __init__(
self,
config: Qwen3VLConfig,
*,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
) -> None:
super().__init__()
self.config = config
self.quant_config = quant_config
self.model = HiDreamO1ImageModel(
config,
quant_config=quant_config,
prefix=_prefix(prefix, "model"),
)
@property
def supports_piecewise_attention(self) -> bool:
return all(
layer.self_attn.attn.attn_backend.supports_piecewise_spans for layer in self.model.language_model.layers
)
def forward(
self,
input_ids: torch.Tensor,
position_ids: torch.Tensor,
vinputs: torch.Tensor,
timestep: torch.Tensor,
attention_mask: torch.Tensor | None,
full_attn_spans: list[list[tuple[int, int]]] | None = None,
**_: object,
) -> HiDreamO1ImageOutput:
return self.model(
input_ids=input_ids,
position_ids=position_ids,
vinputs=vinputs,
timestep=timestep,
attention_mask=attention_mask,
full_attn_spans=full_attn_spans,
)
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
stacked_params_mapping = [
(".qkv_proj", ".q_proj", "q"),
(".qkv_proj", ".k_proj", "k"),
(".qkv_proj", ".v_proj", "v"),
(".gate_up_proj", ".gate_proj", 0),
(".gate_up_proj", ".up_proj", 1),
]
params = dict(self.named_parameters())
loaded_params: set[str] = set()
for name, loaded_weight in weights:
if name.startswith("model.visual.") or name == "lm_head.weight":
continue
for param_name, weight_name, shard_id in stacked_params_mapping:
if weight_name not in name:
continue
name = name.replace(weight_name, param_name)
param = params[name]
param.weight_loader(param, loaded_weight, shard_id)
break
else:
if name not in params:
continue
param = params[name]
weight_loader = getattr(param, "weight_loader", default_weight_loader)
weight_loader(param, loaded_weight)
loaded_params.add(name)
return loaded_params
__all__ = ["HiDreamO1ImageTransformer"]
@@ -0,0 +1,732 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from __future__ import annotations
from collections.abc import Iterable
from typing import ClassVar, TypedDict
import numpy as np
import torch
from einops import rearrange
from PIL import Image
from torch import nn
from transformers import AutoProcessor, PreTrainedTokenizerBase, Qwen3VLConfig
from transformers.processing_utils import ProcessorMixin
from vllm.logger import init_logger
from vllm.model_executor.models.utils import AutoWeightsLoader
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
from vllm_omni.diffusion.distributed.utils import get_local_device
from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
from vllm_omni.diffusion.models.hidream_o1_image.hidream_o1_image_transformer import (
HiDreamO1ImageTransformer,
)
from vllm_omni.diffusion.models.progress_bar import ProgressBarMixin
from vllm_omni.diffusion.models.schedulers import FlowUniPCMultistepScheduler
from vllm_omni.diffusion.profiler.diffusion_pipeline_profiler import (
DiffusionPipelineProfilerMixin,
)
from vllm_omni.diffusion.worker.request_batch import DiffusionRequestBatch
logger = init_logger(__name__)
# upstream: HiDream-O1-Image models/pipeline.py L14-18 @21bcd30471ac
PATCH_SIZE = 32
TIMESTEP_TOKEN_NUM = 1
NOISE_SCALE = 8.0
T_EPS = 0.001
# upstream: HiDream-O1-Image models/utils.py L7-19 @21bcd30471ac
PREDEFINED_RESOLUTIONS: tuple[tuple[int, int], ...] = (
(2048, 2048),
(2304, 1728),
(1728, 2304),
(2560, 1440),
(1440, 2560),
(2496, 1664),
(1664, 2496),
(3104, 1312),
(1312, 3104),
(2304, 1792),
(1792, 2304),
)
class HiDreamO1TextSample(TypedDict):
input_ids: torch.Tensor
position_ids: torch.Tensor
attention_mask: torch.Tensor | None
full_attn_spans: list[list[tuple[int, int]]]
vinput_mask: torch.Tensor
def find_closest_resolution(width: int, height: int) -> tuple[int, int]:
img_ratio = width / height
best_res: tuple[int, int] | None = None
min_diff = float("inf")
for w, h in PREDEFINED_RESOLUTIONS:
diff = abs(w / h - img_ratio)
if diff < min_diff:
min_diff = diff
best_res = (w, h)
assert best_res is not None
return best_res
def get_rope_index_fix_point(
*,
spatial_merge_size: int,
image_token_id: int,
video_token_id: int,
vision_start_token_id: int,
input_ids: torch.Tensor | None = None,
image_grid_thw: torch.Tensor | None = None,
video_grid_thw: torch.Tensor | None = None,
attention_mask: torch.Tensor | None = None,
skip_vision_start_token: list[int],
fix_point: int = 4096,
) -> tuple[torch.Tensor, torch.Tensor]:
if video_grid_thw is not None:
video_grid_thw = torch.repeat_interleave(video_grid_thw, video_grid_thw[:, 0], dim=0)
video_grid_thw[:, 0] = 1
mrope_position_deltas = []
if input_ids is not None and (image_grid_thw is not None or video_grid_thw is not None):
total_input_ids = input_ids
if attention_mask is None:
attention_mask = torch.ones_like(total_input_ids)
position_ids = torch.ones(
3,
input_ids.shape[0],
input_ids.shape[1],
dtype=input_ids.dtype,
device=input_ids.device,
)
image_index, video_index = 0, 0
attention_mask = attention_mask.to(total_input_ids.device)
for i, input_ids in enumerate(total_input_ids):
input_ids = input_ids[attention_mask[i] == 1]
image_nums, video_nums = 0, 0
vision_start_indices = torch.argwhere(input_ids == vision_start_token_id).squeeze(1)
vision_tokens = input_ids[vision_start_indices + 1]
image_nums = (vision_tokens == image_token_id).sum()
video_nums = (vision_tokens == video_token_id).sum()
input_tokens = input_ids.tolist()
llm_pos_ids_list: list[torch.Tensor] = []
st = 0
remain_images, remain_videos = image_nums, video_nums
for _ in range(image_nums + video_nums):
if image_token_id in input_tokens and remain_images > 0:
ed_image = input_tokens.index(image_token_id, st)
else:
ed_image = len(input_tokens) + 1
if video_token_id in input_tokens and remain_videos > 0:
ed_video = input_tokens.index(video_token_id, st)
else:
ed_video = len(input_tokens) + 1
if ed_image < ed_video:
t, h, w = (
image_grid_thw[image_index][0],
image_grid_thw[image_index][1],
image_grid_thw[image_index][2],
)
image_index += 1
remain_images -= 1
ed = ed_image
else:
t, h, w = (
video_grid_thw[video_index][0],
video_grid_thw[video_index][1],
video_grid_thw[video_index][2],
)
video_index += 1
remain_videos -= 1
ed = ed_video
llm_grid_t, llm_grid_h, llm_grid_w = (
t.item(),
h.item() // spatial_merge_size,
w.item() // spatial_merge_size,
)
text_len = ed - st
text_len -= skip_vision_start_token[image_index - 1]
text_len = max(0, text_len)
st_idx = llm_pos_ids_list[-1].max() + 1 if len(llm_pos_ids_list) > 0 else 0
llm_pos_ids_list.append(torch.arange(text_len).view(1, -1).expand(3, -1) + st_idx)
t_index = torch.arange(llm_grid_t).view(-1, 1).expand(-1, llm_grid_h * llm_grid_w).flatten()
h_index = torch.arange(llm_grid_h).view(1, -1, 1).expand(llm_grid_t, -1, llm_grid_w).flatten()
w_index = torch.arange(llm_grid_w).view(1, 1, -1).expand(llm_grid_t, llm_grid_h, -1).flatten()
if skip_vision_start_token[image_index - 1]:
if fix_point > 0:
fix_point = fix_point - st_idx
llm_pos_ids_list.append(torch.stack([t_index, h_index, w_index]) + fix_point + st_idx)
fix_point = 0
else:
llm_pos_ids_list.append(torch.stack([t_index, h_index, w_index]) + text_len + st_idx)
st = ed + llm_grid_t * llm_grid_h * llm_grid_w
if st < len(input_tokens):
st_idx = llm_pos_ids_list[-1].max() + 1 if len(llm_pos_ids_list) > 0 else 0
text_len = len(input_tokens) - st
llm_pos_ids_list.append(torch.arange(text_len).view(1, -1).expand(3, -1) + st_idx)
llm_positions = torch.cat(llm_pos_ids_list, dim=1).reshape(3, -1)
position_ids[..., i, attention_mask[i] == 1] = llm_positions.to(position_ids.device)
mrope_position_deltas.append(llm_positions.max() + 1 - len(total_input_ids[i]))
mrope_position_deltas = torch.tensor(mrope_position_deltas, device=input_ids.device).unsqueeze(1)
return position_ids, mrope_position_deltas
else:
if attention_mask is not None:
position_ids = attention_mask.long().cumsum(-1) - 1
position_ids.masked_fill_(attention_mask == 0, 1)
position_ids = position_ids.unsqueeze(0).expand(3, -1, -1).to(attention_mask.device)
max_position_ids = position_ids.max(0, keepdim=False)[0].max(-1, keepdim=True)[0]
mrope_position_deltas = max_position_ids + 1 - attention_mask.shape[-1]
else:
position_ids = (
torch.arange(input_ids.shape[1], device=input_ids.device)
.view(1, 1, -1)
.expand(3, input_ids.shape[0], -1)
)
mrope_position_deltas = torch.zeros(
[input_ids.shape[0], 1],
device=input_ids.device,
dtype=input_ids.dtype,
)
return position_ids, mrope_position_deltas
def build_t2i_text_sample(
*,
prompt: str,
height: int,
width: int,
tokenizer: PreTrainedTokenizerBase,
processor: ProcessorMixin,
model_config: Qwen3VLConfig,
) -> dict[str, torch.Tensor]:
image_token_id = model_config.image_token_id
video_token_id = model_config.video_token_id
vision_start_token_id = model_config.vision_start_token_id
image_len = (height // PATCH_SIZE) * (width // PATCH_SIZE)
boi_token = getattr(tokenizer, "boi_token", "<|boi_token|>")
tms_token = getattr(tokenizer, "tms_token", "<|tms_token|>")
messages = [{"role": "user", "content": prompt}]
template_caption = (
processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
+ boi_token
+ tms_token * TIMESTEP_TOKEN_NUM
)
input_ids = tokenizer.encode(template_caption, return_tensors="pt", add_special_tokens=False)
image_grid_thw = torch.tensor([1, height // PATCH_SIZE, width // PATCH_SIZE], dtype=torch.int64).unsqueeze(0)
vision_tokens = torch.zeros((1, image_len), dtype=input_ids.dtype) + image_token_id
vision_tokens[0, 0] = vision_start_token_id
input_ids_pad = torch.cat([input_ids, vision_tokens], dim=-1)
position_ids, _ = get_rope_index_fix_point(
spatial_merge_size=1,
image_token_id=image_token_id,
video_token_id=video_token_id,
vision_start_token_id=vision_start_token_id,
input_ids=input_ids_pad,
image_grid_thw=image_grid_thw,
video_grid_thw=None,
attention_mask=None,
skip_vision_start_token=[1],
)
txt_seq_len = input_ids.shape[-1]
all_seq_len = position_ids.shape[-1]
token_types = torch.zeros((1, all_seq_len), dtype=input_ids.dtype)
bgn = txt_seq_len - TIMESTEP_TOKEN_NUM
token_types[0, bgn : bgn + image_len + TIMESTEP_TOKEN_NUM] = 1
token_types[0, txt_seq_len - TIMESTEP_TOKEN_NUM : txt_seq_len] = 3
vinput_mask = token_types == 1
token_types_bin = (token_types > 0).to(token_types.dtype)
return {
"input_ids": input_ids,
"position_ids": position_ids,
"token_types": token_types_bin,
"vinput_mask": vinput_mask,
}
def build_hidream_o1_attention_mask(
token_types: torch.Tensor,
*,
dtype: torch.dtype,
) -> torch.Tensor:
"""Build the mixed causal/full mask once while preparing a request.
Text rows stay causal; image and timestep rows get full attention.
"""
if token_types.ndim == 1:
token_types = token_types.unsqueeze(0)
elif token_types.ndim == 2 and token_types.shape[1] == 1 and token_types.shape[0] != 1:
token_types = token_types.transpose(0, 1)
if token_types.ndim != 2:
raise ValueError(f"token_types must have one or two dimensions, got shape {tuple(token_types.shape)}")
batch_size, seq_len = token_types.shape
min_value = torch.finfo(dtype).min
attention_mask = torch.triu(
torch.full(
(batch_size, seq_len, seq_len),
min_value,
dtype=dtype,
device=token_types.device,
),
diagonal=1,
)
attention_mask[token_types.bool()] = 0
return attention_mask.unsqueeze(1)
def build_hidream_o1_full_attn_spans(
token_types: torch.Tensor,
) -> list[list[tuple[int, int]]]:
"""Encode the mixed causal/full rows without allocating an ``S x S`` mask."""
if token_types.ndim == 1:
token_types = token_types.unsqueeze(0)
elif token_types.ndim == 2 and token_types.shape[1] == 1 and token_types.shape[0] != 1:
token_types = token_types.transpose(0, 1)
if token_types.ndim != 2:
raise ValueError(f"token_types must have one or two dimensions, got shape {tuple(token_types.shape)}")
spans: list[list[tuple[int, int]]] = []
for sample_types in token_types:
full_rows = sample_types.bool().nonzero(as_tuple=False).flatten()
if full_rows.numel() == 0:
spans.append([])
continue
start = int(full_rows[0].item())
end = int(full_rows[-1].item()) + 1
expected = torch.arange(start, end, device=full_rows.device)
if not torch.equal(full_rows, expected):
raise ValueError("HiDream-O1 full-attention rows must form one contiguous span")
spans.append([(start, end)])
return spans
def build_hidream_o1_scheduler(
*,
scheduler_name: str = "default",
num_inference_steps: int = 50,
shift: float = 3.0,
device: torch.device | str | None = None,
timesteps_list: list[int] | None = None,
) -> FlowUniPCMultistepScheduler:
if scheduler_name != "default":
raise NotImplementedError(f"unsupported scheduler_name={scheduler_name!r}")
scheduler = FlowUniPCMultistepScheduler(
num_train_timesteps=1000,
shift=shift,
prediction_type="flow_prediction",
use_dynamic_shifting=False,
)
scheduler.set_timesteps(num_inference_steps, device=device)
if timesteps_list is not None:
scheduler.timesteps = torch.tensor(timesteps_list, device=device, dtype=torch.long)
sigmas = [t.item() / 1000.0 for t in scheduler.timesteps]
sigmas.append(0.0)
scheduler.sigmas = torch.tensor(sigmas, device=device)
return scheduler
def get_hidream_o1_image_post_process_func(od_config: OmniDiffusionConfig):
del od_config
def post_process_func(
output_data: tuple[torch.Tensor, int, int],
) -> Image.Image:
"""Convert HiDream-O1 patch-space output to a PIL RGB image."""
if not isinstance(output_data, tuple) or len(output_data) != 3:
raise TypeError(
f"HiDreamO1 postprocess expects (patch_tensor, height, width), got {type(output_data).__name__}"
)
z, height, width = output_data
if not torch.is_tensor(z):
raise TypeError(
f"HiDreamO1 postprocess expects the first tuple element to be a torch.Tensor, got {type(z).__name__}"
)
height = int(height)
width = int(width)
if height <= 0 or width <= 0:
raise ValueError(
f"HiDreamO1 postprocess requires positive height/width, got height={height}, width={width}"
)
if height % PATCH_SIZE != 0 or width % PATCH_SIZE != 0:
raise ValueError(
f"HiDreamO1 output resolution must be divisible by PATCH_SIZE={PATCH_SIZE}, got {width}x{height}"
)
h_patches = height // PATCH_SIZE
w_patches = width // PATCH_SIZE
expected_image_len = h_patches * w_patches
expected_patch_dim = 3 * PATCH_SIZE * PATCH_SIZE
if z.ndim != 3:
raise ValueError(
f"HiDreamO1 patch tensor must have shape (B, image_len, patch_dim), got shape={tuple(z.shape)}"
)
if z.shape[0] != 1:
raise ValueError(f"HiDreamO1 postprocess currently expects batch size 1, got batch_size={z.shape[0]}")
if z.shape[1] != expected_image_len:
raise ValueError(
"HiDreamO1 patch count mismatch: "
f"got {z.shape[1]}, expected {expected_image_len} "
f"for resolution {width}x{height}"
)
if z.shape[2] != expected_patch_dim:
raise ValueError(f"HiDreamO1 patch dimension mismatch: got {z.shape[2]}, expected {expected_patch_dim}")
# Preserve upstream decode order exactly:
# 1. map patch values from [-1, 1] to [0, 1]
# 2. restore BCHW image layout
# 3. convert to float32 on CPU
img = (z + 1) / 2
img = rearrange(
img.cpu().float(),
"B (H W) (C p1 p2) -> B C (H p1) (W p2)",
H=h_patches,
W=w_patches,
p1=PATCH_SIZE,
p2=PATCH_SIZE,
)
arr = np.round(
np.clip(
img[0].numpy().transpose(1, 2, 0) * 255,
0,
255,
)
).astype(np.uint8)
return Image.fromarray(arr).convert("RGB")
return post_process_func
class HiDreamO1ImagePipeline(nn.Module, DiffusionPipelineProfilerMixin, ProgressBarMixin):
supports_request_batch = False
dummy_run_num_frames: ClassVar[int] = 0
_dit_modules: ClassVar[list[str]] = ["model.model.language_model"]
def __init__(self, *, od_config: OmniDiffusionConfig, prefix: str = ""):
super().__init__()
self.od_config = od_config
self.prefix = prefix
self.model_dir = od_config.model
self.dtype = od_config.dtype if od_config.dtype is not None else torch.bfloat16
self.device = get_local_device()
self.weights_sources = [
DiffusersPipelineLoader.ComponentSource(
model_or_path=od_config.model,
subfolder=None,
revision=od_config.revision,
prefix="model.",
fall_back_to_pt=True,
)
]
self._validate_static_config()
self.setup_diffusion_pipeline_profiler(
profiler_targets=["forward"],
enable_diffusion_pipeline_profiler=self.od_config.enable_diffusion_pipeline_profiler,
)
self._init_processor_and_model()
self._validate_tms_token_id()
def _validate_static_config(self) -> None:
if self.dtype != torch.bfloat16:
raise NotImplementedError(f"only bfloat16 is supported; got {self.dtype!r}")
cfg_parallel_size = int(self.od_config.parallel_config.cfg_parallel_size)
if cfg_parallel_size > 1:
raise NotImplementedError(f"cfg_parallel_size={cfg_parallel_size} not supported")
def _init_processor_and_model(self) -> None:
logger.info(
"HiDreamO1ImagePipeline: loading processor and model config from %s",
self.model_dir,
)
self.processor = AutoProcessor.from_pretrained(
self.model_dir,
revision=self.od_config.revision,
)
model_config = Qwen3VLConfig.from_pretrained(
self.model_dir,
revision=self.od_config.revision,
)
self.model = HiDreamO1ImageTransformer(
model_config,
quant_config=self.od_config.quantization_config,
prefix=self.prefix,
)
self._add_special_tokens(self.processor)
self.tokenizer = (
self.processor if isinstance(self.processor, PreTrainedTokenizerBase) else self.processor.tokenizer
)
def _validate_tms_token_id(self) -> None:
actual_tms_ids = self.tokenizer.encode(self.tokenizer.tms_token, add_special_tokens=False)
expected_tms_id = int(self.model.model.tms_token_id)
if actual_tms_ids != [expected_tms_id]:
raise RuntimeError(f"tms_token maps to {actual_tms_ids}, model expects [{expected_tms_id}]")
@staticmethod
def _add_special_tokens(processor: ProcessorMixin | PreTrainedTokenizerBase) -> None:
tok = processor if isinstance(processor, PreTrainedTokenizerBase) else processor.tokenizer
tok.boi_token = "<|boi_token|>"
tok.bor_token = "<|bor_token|>"
tok.eor_token = "<|eor_token|>"
tok.bot_token = "<|bot_token|>"
tok.tms_token = "<|tms_token|>"
def _resolve_generation_params(
self,
req: DiffusionRequestBatch,
) -> tuple[str, int, int, int, int, float]:
sp = req.sampling_params
if sp.timesteps is not None:
raise NotImplementedError("sp.timesteps not supported")
if sp.latents is not None:
raise NotImplementedError("sp.latents not supported")
if sp.num_outputs_per_prompt != 1:
raise NotImplementedError(f"num_outputs_per_prompt={sp.num_outputs_per_prompt} not supported")
if not req.prompts:
raise ValueError("req.prompts is empty")
if len(req.prompts) != 1:
raise NotImplementedError(f"only one prompt per request; got {len(req.prompts)}")
first_prompt = req.prompts[0]
if isinstance(first_prompt, str):
prompt = first_prompt
elif isinstance(first_prompt, dict):
raw_prompt = first_prompt.get("prompt")
if not isinstance(raw_prompt, str):
raise TypeError(f"dict prompt must contain a str 'prompt' field; got {type(raw_prompt).__name__}")
prompt = raw_prompt
else:
raise TypeError(f"prompt must be str or dict; got {type(first_prompt).__name__}")
raw_h = 2048 if sp.height is None else int(sp.height)
raw_w = 2048 if sp.width is None else int(sp.width)
if raw_h <= 0 or raw_w <= 0:
raise ValueError(f"h/w must be positive, got h={raw_h} w={raw_w}")
snapped_w, snapped_h = find_closest_resolution(raw_w, raw_h)
if (snapped_w, snapped_h) != (raw_w, raw_h):
logger.info(
"HiDreamO1: resolution snapped from %dx%d to %dx%d",
raw_w,
raw_h,
snapped_w,
snapped_h,
)
steps = 50 if sp.num_inference_steps is None else int(sp.num_inference_steps)
if steps <= 0:
raise ValueError(f"num_inference_steps must be positive, got {steps}")
generator = sp.generator
if isinstance(generator, list):
if len(generator) != 1:
raise NotImplementedError("generator list only supported for single-output requests with one generator")
generator = generator[0]
generator_seed: int | None = None
if generator is not None:
if not hasattr(generator, "initial_seed"):
raise TypeError(f"sp.generator must expose initial_seed(); got {type(generator).__name__}")
generator_seed = int(generator.initial_seed())
if sp.seed is not None and generator_seed is not None and int(sp.seed) != generator_seed:
raise ValueError(f"seed/generator mismatch: seed={int(sp.seed)} generator.initial_seed()={generator_seed}")
if sp.seed is None and generator_seed is None:
raise RuntimeError("expected request initialization to resolve a concrete seed")
seed = int(sp.seed) if sp.seed is not None else generator_seed
assert seed is not None
guidance_scale = float(sp.guidance_scale)
return prompt, snapped_h, snapped_w, steps, seed, guidance_scale
def _prepare_noise_and_patchify(
self,
height: int,
width: int,
seed: int,
dtype: torch.dtype,
device: torch.device,
) -> torch.Tensor:
gen = torch.Generator("cpu").manual_seed(seed + 1)
noise = NOISE_SCALE * torch.randn((1, 3, height, width), generator=gen)
noise = noise.to(device, dtype)
return rearrange(
noise,
"B C (H p1) (W p2) -> B (H W) (C p1 p2)",
p1=PATCH_SIZE,
p2=PATCH_SIZE,
)
def _forward_once(
self,
sample: HiDreamO1TextSample,
z_in: torch.Tensor,
t_pixeldit: torch.Tensor,
) -> torch.Tensor:
with torch.autocast(self.device.type, dtype=self.dtype, cache_enabled=False):
outputs = self.model(
input_ids=sample["input_ids"],
position_ids=sample["position_ids"],
vinputs=z_in,
timestep=t_pixeldit.reshape(-1).to(self.device),
attention_mask=sample["attention_mask"],
full_attn_spans=sample["full_attn_spans"],
)
if outputs.x_pred is None:
raise RuntimeError("model returned x_pred=None")
if outputs.x_pred.shape[-1] != z_in.shape[-1]:
raise RuntimeError(f"x_pred patch_dim {outputs.x_pred.shape[-1]} != z_in {z_in.shape[-1]}")
mask = sample["vinput_mask"][0]
return outputs.x_pred[0, mask].unsqueeze(0)
def _build_and_validate_sample(
self,
prompt: str,
height: int,
width: int,
expected_image_len: int,
) -> HiDreamO1TextSample:
sample = build_t2i_text_sample(
prompt=prompt,
height=height,
width=width,
tokenizer=self.tokenizer,
processor=self.processor,
model_config=self.model.config,
)
actual_image_len = int(sample["vinput_mask"].sum().item())
if actual_image_len != expected_image_len:
raise RuntimeError(
f"vinput_mask has {actual_image_len} slots, expected {expected_image_len} for {height}x{width}"
)
full_attn_spans = build_hidream_o1_full_attn_spans(sample["token_types"])
inputs = {key: value.to(self.device) for key, value in sample.items()}
attention_mask = None
if not self.model.supports_piecewise_attention:
attention_mask = build_hidream_o1_attention_mask(
inputs["token_types"],
dtype=self.dtype,
)
return {
"input_ids": inputs["input_ids"],
"position_ids": inputs["position_ids"],
"attention_mask": attention_mask,
"full_attn_spans": full_attn_spans,
"vinput_mask": inputs["vinput_mask"],
}
@torch.inference_mode()
def forward(self, req: DiffusionRequestBatch) -> DiffusionOutput:
prompt, height, width, num_inference_steps, seed, guidance_scale = self._resolve_generation_params(req)
expected_image_len = (height // PATCH_SIZE) * (width // PATCH_SIZE)
cond_sample = self._build_and_validate_sample(
prompt,
height,
width,
expected_image_len,
)
samples: list[HiDreamO1TextSample] = [cond_sample]
if guidance_scale > 1.0:
uncond_sample = self._build_and_validate_sample(
" ",
height,
width,
expected_image_len,
)
samples.append(uncond_sample)
z = self._prepare_noise_and_patchify(
height,
width,
seed,
self.dtype,
self.device,
)
sched = build_hidream_o1_scheduler(
scheduler_name="default",
num_inference_steps=num_inference_steps,
shift=3.0,
device=self.device,
)
with self.progress_bar(total=len(sched.timesteps)) as pbar:
for step_t in sched.timesteps:
step_t_f32 = step_t.float()
t_pixeldit = 1.0 - step_t_f32 / 1000.0
sigma = (step_t_f32 / 1000.0).to(dtype=torch.float32).clamp_min(T_EPS)
x_pred_cond = self._forward_once(samples[0], z, t_pixeldit)
v_cond = (x_pred_cond.to(torch.float32) - z.to(torch.float32)) / sigma
if len(samples) > 1:
x_pred_uncond = self._forward_once(
samples[1],
z,
t_pixeldit,
)
v_uncond = (x_pred_uncond.to(torch.float32) - z.to(torch.float32)) / sigma
v_guided = v_uncond + guidance_scale * (v_cond - v_uncond)
else:
v_guided = v_cond
model_output = -v_guided
z = sched.step(
model_output,
step_t_f32,
z.float(),
return_dict=False,
)[0].to(self.dtype)
pbar.update()
return DiffusionOutput(
output=(z, height, width),
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)
return loader.load_weights(weights)
+6
View File
@@ -336,6 +336,11 @@ _DIFFUSION_MODELS = {
"pipeline_hidream_image",
"HiDreamImagePipeline",
),
"HiDreamO1ImagePipeline": (
"hidream_o1_image",
"pipeline_hidream_o1_image",
"HiDreamO1ImagePipeline",
),
"DreamZeroPipeline": (
"dreamzero",
"pipeline_dreamzero",
@@ -600,6 +605,7 @@ _DIFFUSION_POST_PROCESS_FUNCS = {
"Cosmos3OmniDiffusersPipeline": "get_cosmos3_post_process_func",
"Cosmos3OmniPipeline": "get_cosmos3_post_process_func",
"HiDreamImagePipeline": "get_hidream_image_post_process_func",
"HiDreamO1ImagePipeline": "get_hidream_o1_image_post_process_func",
"StableDiffusionXLPipeline": "get_sdxl_image_post_process_func",
"Krea2Pipeline": "get_krea2_post_process_func",
"HunyuanImage3ForCausalMM": "get_hunyuan_image3_post_process_func",
+25 -4
View File
@@ -71,6 +71,30 @@ def _looks_like_dreamzero(model_name: str) -> bool:
return False
HIDREAM_O1_SIGNATURE_WEIGHTS = (
"model.final_layer2.linear.weight",
"final_layer2.linear.weight",
)
def _looks_like_hidream_o1(model_name: str, config: Mapping | None = None) -> bool:
"""Detect HiDream-O1 without matching regular Qwen3-VL checkpoints."""
try:
cfg = config if config is not None else get_hf_file_to_dict("config.json", model_name)
if not isinstance(cfg, Mapping) or cfg.get("model_type") != "qwen3_vl":
return False
index = get_hf_file_to_dict("model.safetensors.index.json", model_name)
if not isinstance(index, Mapping):
return False
weight_map = index.get("weight_map")
if not isinstance(weight_map, Mapping):
return False
return any(key in weight_map for key in HIDREAM_O1_SIGNATURE_WEIGHTS)
except (AttributeError, OSError, TypeError, ValueError):
return False
@lru_cache
def is_diffusion_model(model_name: str) -> bool:
"""Check if a model is a diffusion model.
@@ -115,7 +139,4 @@ def is_diffusion_model(model_name: str) -> bool:
except Exception as e:
logger.debug("Failed to load diffusers config via DiffusionPipeline: %s", e)
# Bagel and DreamZero are not diffusers pipelines (no model_index.json),
# but are still diffusion-style models in vllm-omni. Detect them via
# config.json.
return _looks_like_bagel(model_name) or _looks_like_dreamzero(model_name)
return _looks_like_bagel(model_name) or _looks_like_dreamzero(model_name) or _looks_like_hidream_o1(model_name)