[New Model] Ming-Image (inclusionAI/Ming-Image-0.1-Design) Support (#8021)
Signed-off-by: Yuanheng <jonathan.zhaoyh@gmail.com>
This commit is contained in:
1 parent
cb5f508212
commit
71125f80ad
31 files changed
+1952
-51
No files matched your search
@@ -0,0 +1,141 @@
|
||||
# Ming-Image 0.1 Design
|
||||
|
||||
> Text-to-image, image editing, and layer decomposition.
|
||||
|
||||
## Summary
|
||||
|
||||
- Vendor: inclusionAI
|
||||
- Models: `inclusionAI/Ming-Image-0.1-Design` and `inclusionAI/Ming-Image-0.1-Design-Layer`
|
||||
- Runtime: vLLM-Omni two-stage online serving
|
||||
- API: OpenAI-compatible chat completions
|
||||
|
||||
## Architecture
|
||||
|
||||
Stage 0 uses a Qwen2.5-VL vision tower plus a 20-layer BailingMoeV2 language model.
|
||||
It appends 256 learned image-query tokens and exports the final query states plus direct VLM states from layers 5, 12, and 20.
|
||||
The query-token checkpoint is in the root sibling `mlp/` directory, so remote stage-0 materialization downloads both `mllm/` and `mlp/`.
|
||||
|
||||
Stage 1 projects those conditions through the checkpoint connector and runs a 30-layer Z-Image DiT with a Qwen-Image RGBA VAE.
|
||||
|
||||
For Design-Layer, the first returned image is the reconstructed composite and the remaining images are the requested layers.
|
||||
|
||||
## CUDA
|
||||
|
||||
### Environment
|
||||
|
||||
- OS: Linux
|
||||
- Python: 3.10+
|
||||
- CUDA: 13.0
|
||||
- vLLM version: 0.29.0
|
||||
- vLLM-Omni version or commit: 59db6a428
|
||||
- 2x H100 80GB (1xH100 to be validated)
|
||||
|
||||
### Commands
|
||||
|
||||
```bash
|
||||
MODEL=inclusionAI/Ming-Image-0.1-Design
|
||||
vllm serve "$MODEL" --omni --deploy-config vllm_omni/deploy/ming_image.yaml --port 8091
|
||||
```
|
||||
|
||||
For layer decomposition, set `MODEL` to `inclusionAI/Ming-Image-0.1-Design-Layer`.
|
||||
|
||||
## Text-to-image
|
||||
|
||||
Note that a prompt refiner is expected to describe the prompts with details; we will refine with more example inputs soon.
|
||||
|
||||
```bash
|
||||
curl -s http://127.0.0.1:8091/v1/chat/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"model": "inclusionAI/Ming-Image-0.1-Design",
|
||||
"messages": [{"role": "user", "content": "A clean editorial botanical poster"}],
|
||||
"modalities": ["image"],
|
||||
"extra_body": {
|
||||
"height": 1024, "width": 1024,
|
||||
"steps": 12, "cfg": 1.0, "seed": 42
|
||||
}
|
||||
}' \
|
||||
| jq -r '.choices[0].message.content[0].image_url.url | split(",")[1]' \
|
||||
| base64 -d > ming_design_smoke.png
|
||||
```
|
||||
|
||||
## Image editing
|
||||
|
||||
Pass one input image and an editing instruction:
|
||||
|
||||
```bash
|
||||
MODEL=inclusionAI/Ming-Image-0.1-Design
|
||||
INPUT_IMAGE=/path/to/input.png
|
||||
|
||||
jq -n \
|
||||
--arg model "$MODEL" \
|
||||
--rawfile image <(base64 -w0 "$INPUT_IMAGE") \
|
||||
'{
|
||||
model: $model,
|
||||
messages: [{role: "user", content: [
|
||||
{type: "image_url", image_url: {url: ("data:image/png;base64," + $image)}},
|
||||
{type: "text", text: "Change the background to blue"}
|
||||
]}],
|
||||
modalities: ["image"],
|
||||
extra_body: {
|
||||
height: 1024, width: 1024,
|
||||
steps: 12, cfg: 1.0, seed: 42
|
||||
}
|
||||
}' |
|
||||
curl -sS http://127.0.0.1:8091/v1/chat/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
--data-binary @- \
|
||||
| jq -r '.choices[0].message.content[0].image_url.url | split(",")[1]' \
|
||||
| base64 -d > ming_edit.png
|
||||
```
|
||||
|
||||
## Layer decomposition
|
||||
|
||||
Use the Design-Layer checkpoint and set `INPUT_IMAGE` to a local flattened design image.
|
||||
Note that the prompt should better depict each layer to be decomposed, we will refine with more example inputs soon.
|
||||
|
||||
```bash
|
||||
MODEL=inclusionAI/Ming-Image-0.1-Design-Layer
|
||||
INPUT_IMAGE=/path/to/input.png
|
||||
PROMPT="Decompose this design into editable visual layers"
|
||||
|
||||
jq -n \
|
||||
--arg model "$MODEL" \
|
||||
--rawfile image <(base64 -w0 "$INPUT_IMAGE") \
|
||||
--arg prompt "$PROMPT" \
|
||||
'{
|
||||
model: $model,
|
||||
messages: [{role: "user", content: [
|
||||
{type: "image_url", image_url: {url: ("data:image/png;base64," + $image)}},
|
||||
{type: "text", text: $prompt}
|
||||
]}],
|
||||
modalities: ["image"],
|
||||
extra_body: {
|
||||
num_layers: 6,
|
||||
height: 1024, width: 1024,
|
||||
steps: 12, cfg: 2.0, seed: 42
|
||||
}
|
||||
}' |
|
||||
curl -sS http://127.0.0.1:8091/v1/chat/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
--data-binary @- > response.json
|
||||
|
||||
jq -r '.choices[0].message.content[].image_url.url | split(",")[1]' response.json |
|
||||
nl -v 0 |
|
||||
while read -r index data; do
|
||||
printf "%s" "$data" | base64 -d > "ming_layer_${index}.png"
|
||||
done
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- The default deployment keeps Stage 0 eager and enables dynamic regional
|
||||
compilation with CUDA Graph Trees for the repeated Stage 1 DiT blocks.
|
||||
The first request for each new image shape or layer count pays compilation
|
||||
and graph-capture cost; warm up every production shape before measuring or
|
||||
serving latency-sensitive traffic.
|
||||
- Only one reference image and one request at a time are currently supported.
|
||||
- Design-Layer requires a reference image except during warmup.
|
||||
- A non-empty `negative_prompt` is rejected; Ming-Image uses zero negative conditioning.
|
||||
- Height and width must be divisible by 16.
|
||||
- Returned images retain the checkpoint's four RGBA channels.
|
||||
@@ -33,6 +33,7 @@ from vllm_omni.diffusion.cache.teacache.extractors import (
|
||||
extract_flux2_klein_context,
|
||||
extract_flux_context,
|
||||
extract_minimax_h3_context,
|
||||
extract_zimage_context,
|
||||
)
|
||||
from vllm_omni.diffusion.models.flux.flux_transformer import FluxTransformer2DModel
|
||||
from vllm_omni.diffusion.models.flux2_klein.flux2_klein_transformer import (
|
||||
@@ -53,6 +54,62 @@ def setup_tp_group():
|
||||
yield
|
||||
|
||||
|
||||
@pytest.mark.cpu
|
||||
def test_zimage_extractor_accepts_extended_patchify_output():
|
||||
class _Block(nn.Module):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.attention_norm1 = nn.Identity()
|
||||
|
||||
def adaLN_modulation(self, value):
|
||||
return value.new_zeros((value.shape[0], 16))
|
||||
|
||||
class _Model(nn.Module):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.layers = nn.ModuleList([_Block()])
|
||||
self.noise_refiner = nn.ModuleList()
|
||||
self.context_refiner = nn.ModuleList()
|
||||
self.all_x_embedder = nn.ModuleDict({"2-1": nn.Linear(2, 4)})
|
||||
self.cap_embedder = nn.Linear(3, 4)
|
||||
self.x_pad_token = nn.Parameter(torch.zeros(1, 4))
|
||||
self.cap_pad_token = nn.Parameter(torch.zeros(1, 4))
|
||||
self.t_scale = 1.0
|
||||
|
||||
@staticmethod
|
||||
def t_embedder(timestep):
|
||||
return timestep.new_ones((timestep.shape[0], 4))
|
||||
|
||||
@staticmethod
|
||||
def rope_embedder(position_ids):
|
||||
rope = torch.zeros((position_ids.shape[0], 2))
|
||||
return rope, rope
|
||||
|
||||
@staticmethod
|
||||
def patchify_and_embed(*_args):
|
||||
positions = [torch.zeros((32, 3), dtype=torch.int32)]
|
||||
masks = [torch.zeros(32, dtype=torch.bool)]
|
||||
return (
|
||||
[torch.zeros((32, 2))],
|
||||
[torch.zeros((32, 3))],
|
||||
[(1, 1, 1)],
|
||||
positions,
|
||||
positions,
|
||||
masks,
|
||||
masks,
|
||||
[],
|
||||
)
|
||||
|
||||
context = extract_zimage_context(
|
||||
_Model(),
|
||||
x=[torch.zeros((1, 1, 1, 1))],
|
||||
t=torch.zeros(1),
|
||||
cap_feats=[torch.zeros((1, 3))],
|
||||
)
|
||||
|
||||
assert context.modulated_input.shape == (1, 64, 4)
|
||||
|
||||
|
||||
class BaseExtractorTest(ABC):
|
||||
"""Base class for testing TeaCache extractors.
|
||||
|
||||
|
||||
@@ -1,10 +1,47 @@
|
||||
import pytest
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
from vllm_omni.diffusion.models.z_image.z_image_transformer import validate_zimage_tp_constraints
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm_omni.diffusion.models.z_image.z_image_transformer import (
|
||||
UnifiedPrepare,
|
||||
validate_zimage_tp_constraints,
|
||||
)
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
|
||||
def test_unified_prepare_applies_padding_masks_before_sharding():
|
||||
x = torch.zeros((2, 4, 2))
|
||||
cap = torch.zeros((2, 3, 2))
|
||||
x_rope = torch.zeros((2, 4, 1))
|
||||
cap_rope = torch.zeros((2, 3, 1))
|
||||
x_mask = torch.tensor([[1, 0, 1, 0], [1, 1, 0, 0]], dtype=torch.bool)
|
||||
cap_mask = torch.tensor([[1, 1, 0], [1, 0, 1]], dtype=torch.bool)
|
||||
|
||||
*_, unified_mask = UnifiedPrepare()(
|
||||
x,
|
||||
x_rope,
|
||||
x_rope,
|
||||
cap,
|
||||
cap_rope,
|
||||
cap_rope,
|
||||
[4, 2],
|
||||
[3, 3],
|
||||
x_mask,
|
||||
cap_mask,
|
||||
)
|
||||
|
||||
assert torch.equal(
|
||||
unified_mask,
|
||||
torch.tensor(
|
||||
[[1, 0, 1, 0, 1, 1, 0], [1, 1, 1, 0, 1, 0, 0]],
|
||||
dtype=torch.bool,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_validate_zimage_tp_constraints_tp2_ok():
|
||||
ffn_hidden_dim, final_out_dims, supported_tp = validate_zimage_tp_constraints(
|
||||
dim=3840,
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm_omni.diffusion.data import OmniDiffusionConfig
|
||||
from vllm_omni.diffusion.models.ming_image.pipeline import (
|
||||
MingImageDiffusionPipeline,
|
||||
_validate_variant_config,
|
||||
)
|
||||
from vllm_omni.diffusion.request import OmniDiffusionRequest
|
||||
from vllm_omni.diffusion.worker.request_batch import DiffusionRequestBatch
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
from vllm_omni.model_executor.models.ming_flash_omni.ming_flash_omni_thinker import (
|
||||
MingFlashOmniThinkerForConditionalGeneration,
|
||||
)
|
||||
from vllm_omni.model_executor.models.ming_image import checkpoint
|
||||
from vllm_omni.model_executor.models.ming_image.model import MingImageForConditionalGeneration
|
||||
from vllm_omni.model_executor.models.ming_image.pipeline import MING_IMAGE_PIPELINE
|
||||
from vllm_omni.model_executor.stage_input_processors.ming_image import thinker2image
|
||||
from vllm_omni.model_extras import (
|
||||
get_extra_body_params,
|
||||
should_init_extra_args_for_non_diffusion_stages,
|
||||
)
|
||||
from vllm_omni.transformers_utils.configs.ming_flash_omni import BailingMM2Config
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
|
||||
def test_config_selects_qwen25_vision_tower():
|
||||
config = BailingMM2Config(vision_config={"model_type": "qwen2_5_vit"}, llm_config={})
|
||||
|
||||
assert type(config.vision_config).__name__ == "Qwen2_5_VLVisionConfig"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("transformer_config", "layered"),
|
||||
(
|
||||
(
|
||||
SimpleNamespace(
|
||||
_class_name="DiffusionTransformer",
|
||||
alignment_padding_mode="zero_masked",
|
||||
multi_frame_output=False,
|
||||
),
|
||||
False,
|
||||
),
|
||||
(
|
||||
SimpleNamespace(
|
||||
_class_name="DiffusionTransformer",
|
||||
alignment_padding_mode="learned",
|
||||
multi_frame_output=True,
|
||||
),
|
||||
True,
|
||||
),
|
||||
),
|
||||
)
|
||||
def test_variant_config_without_model_index(transformer_config, layered):
|
||||
assert _validate_variant_config(None, transformer_config) is layered
|
||||
|
||||
|
||||
def test_variant_config_rejects_mixed_semantics():
|
||||
with pytest.raises(ValueError, match="must use either"):
|
||||
_validate_variant_config(
|
||||
{"_class_name": "MingImageDiffusionPipeline"},
|
||||
SimpleNamespace(
|
||||
_class_name="DiffusionTransformer",
|
||||
alignment_padding_mode="learned",
|
||||
multi_frame_output=False,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_layer_latent_frames_flatten_once_in_frame_major_order():
|
||||
latents = torch.arange(2 * 4 * 3 * 2 * 2).reshape(2, 4, 3, 2, 2)
|
||||
|
||||
pipeline = MingImageDiffusionPipeline.__new__(MingImageDiffusionPipeline)
|
||||
flat = pipeline._flatten_latent_frames(latents)
|
||||
|
||||
assert flat.shape == (6, 4, 2, 2)
|
||||
assert torch.equal(flat[0], latents[0, :, 0])
|
||||
assert torch.equal(flat[1], latents[1, :, 0])
|
||||
assert torch.equal(flat[2], latents[0, :, 1])
|
||||
|
||||
|
||||
def test_layer_pipeline_allows_missing_reference_only_for_dummy_run():
|
||||
pipeline = MingImageDiffusionPipeline.__new__(MingImageDiffusionPipeline)
|
||||
pipeline.is_layer_decomposition = True
|
||||
pipeline._configure_output_frames(
|
||||
reference=None,
|
||||
num_layers=1,
|
||||
is_dummy_run=True,
|
||||
)
|
||||
assert pipeline._num_frames_per_prompt == 2
|
||||
|
||||
with pytest.raises(ValueError, match="requires a reference image"):
|
||||
pipeline._configure_output_frames(
|
||||
reference=None,
|
||||
num_layers=1,
|
||||
is_dummy_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_pipeline_rejects_multiple_outputs_per_prompt():
|
||||
pipeline = MingImageDiffusionPipeline.__new__(MingImageDiffusionPipeline)
|
||||
request = DiffusionRequestBatch(
|
||||
requests=[
|
||||
OmniDiffusionRequest(
|
||||
prompt="test",
|
||||
sampling_params=OmniDiffusionSamplingParams(num_outputs_per_prompt=2),
|
||||
request_id="test-request",
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="num_outputs_per_prompt=1 only, got 2"):
|
||||
pipeline.forward(request)
|
||||
|
||||
|
||||
def _source_output(prefix_len: int = 4):
|
||||
prompt_ids = [11] * prefix_len + [157158] + [157157] * 256 + [157159]
|
||||
length = len(prompt_ids)
|
||||
multimodal = {
|
||||
"final_hidden_states": torch.arange(length * 2048).view(length, 2048),
|
||||
}
|
||||
for layer in (5, 12, 20):
|
||||
multimodal[f"hidden_states_{layer}"] = torch.full((length, 2048), layer)
|
||||
return SimpleNamespace(
|
||||
prompt_token_ids=prompt_ids,
|
||||
outputs=[SimpleNamespace(multimodal_output=multimodal)],
|
||||
)
|
||||
|
||||
|
||||
def test_bridge_extracts_query_and_direct_conditions():
|
||||
result = thinker2image(
|
||||
[_source_output()],
|
||||
prompt={"multi_modal_data": {"img2img": "reference"}},
|
||||
sampling_params=SimpleNamespace(extra_args={"num_layers": 3}),
|
||||
)
|
||||
|
||||
extra = result[0]["extra"]
|
||||
assert extra["query_hidden_states"].shape == (256, 2048)
|
||||
assert extra["direct_hidden_states"].shape == (4, 6144)
|
||||
assert extra["reference_image"] == "reference"
|
||||
assert extra["num_layers"] == 3
|
||||
assert set(extra["direct_hidden_states"][:, :2048].unique().tolist()) == {5}
|
||||
|
||||
|
||||
def test_bridge_rejects_text_negative_condition():
|
||||
with pytest.raises(ValueError, match="does not accept negative_prompt"):
|
||||
thinker2image(
|
||||
[_source_output()],
|
||||
prompt={"negative_prompt": "bad"},
|
||||
sampling_params=SimpleNamespace(extra_args={}),
|
||||
)
|
||||
|
||||
|
||||
def test_two_stage_topology_and_request_metadata():
|
||||
assert MING_IMAGE_PIPELINE.model_type == "ming_image"
|
||||
assert [stage.model_stage for stage in MING_IMAGE_PIPELINE.stages] == ["mllm", "dit"]
|
||||
assert MING_IMAGE_PIPELINE.diffusers_class_name == "MingImageDiffusionPipeline"
|
||||
assert MING_IMAGE_PIPELINE.diffusers_class_aliases == ("MingImageLayeredDiffusionPipeline",)
|
||||
assert MING_IMAGE_PIPELINE.stages[0].model_subdir == "mllm"
|
||||
assert MING_IMAGE_PIPELINE.stages[0].model_path_resolver.endswith(".resolve_ming_image_model_root")
|
||||
assert MING_IMAGE_PIPELINE.stages[1].model_arch == "MingImageDiffusionPipeline"
|
||||
assert MING_IMAGE_PIPELINE.hf_architectures == ("MingImageForConditionalGeneration",)
|
||||
for class_name in MING_IMAGE_PIPELINE.diffusers_class_aliases + (MING_IMAGE_PIPELINE.diffusers_class_name,):
|
||||
assert "num_layers" in get_extra_body_params(class_name)
|
||||
assert should_init_extra_args_for_non_diffusion_stages(class_name)
|
||||
|
||||
|
||||
def test_stage1_compile_uses_configured_dynamic_regional_cuda_graph(monkeypatch):
|
||||
pipeline = MingImageDiffusionPipeline.__new__(MingImageDiffusionPipeline)
|
||||
torch.nn.Module.__init__(pipeline)
|
||||
pipeline.od_config = SimpleNamespace(
|
||||
diffusion_compile_granularity="regional",
|
||||
diffusion_compile_dynamic=True,
|
||||
)
|
||||
pipeline.transformer = torch.nn.Identity()
|
||||
captured = {}
|
||||
|
||||
def _regionally_compile(model, **kwargs):
|
||||
captured.update(kwargs)
|
||||
return model
|
||||
|
||||
monkeypatch.setattr(
|
||||
"vllm_omni.diffusion.models.ming_image.pipeline.regionally_compile",
|
||||
_regionally_compile,
|
||||
)
|
||||
|
||||
pipeline.setup_compile()
|
||||
|
||||
assert captured == {
|
||||
"mode": "reduce-overhead",
|
||||
"fullgraph": True,
|
||||
"dynamic": True,
|
||||
}
|
||||
assert pipeline._uses_cudagraph_trees
|
||||
|
||||
|
||||
def test_explicit_pipeline_loads_component_config_without_model_index(monkeypatch):
|
||||
transformer_config = {
|
||||
"_class_name": "DiffusionTransformer",
|
||||
"alignment_padding_mode": "zero_masked",
|
||||
"multi_frame_output": False,
|
||||
}
|
||||
|
||||
def _get_hf_file_to_dict(filename, model, revision=None):
|
||||
del model, revision
|
||||
return transformer_config if filename == "transformer/config.json" else None
|
||||
|
||||
monkeypatch.setattr(
|
||||
"vllm_omni.diffusion.utils.hf_utils.get_diffusion_model_index",
|
||||
lambda *_args, **_kwargs: None,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"vllm.transformers_utils.config.get_hf_file_to_dict",
|
||||
_get_hf_file_to_dict,
|
||||
)
|
||||
|
||||
config = OmniDiffusionConfig(
|
||||
model="org/model",
|
||||
model_class_name="MingImageDiffusionPipeline",
|
||||
)
|
||||
config.enrich_config()
|
||||
|
||||
assert config.model_class_name == "MingImageDiffusionPipeline"
|
||||
assert config.tf_model_config.alignment_padding_mode == "zero_masked"
|
||||
assert config.tf_model_config.multi_frame_output is False
|
||||
|
||||
|
||||
def test_checkpoint_resolver_downloads_mllm_and_sibling_mlp(monkeypatch):
|
||||
captured = {}
|
||||
|
||||
def _download(**kwargs):
|
||||
captured.update(kwargs)
|
||||
return "/cache/model"
|
||||
|
||||
monkeypatch.setattr(checkpoint, "download_weights_from_hf_specific", _download)
|
||||
|
||||
assert checkpoint.resolve_ming_image_model_root("org/model", "rev", None) == "/cache/model"
|
||||
assert captured["allow_patterns"] == ["mllm/**", "mlp/**"]
|
||||
assert captured["revision"] == "rev"
|
||||
assert captured["require_all"] is True
|
||||
|
||||
|
||||
def test_compute_logits_accepts_vllm_v1_signature():
|
||||
class _LanguageModel(torch.nn.Module):
|
||||
def compute_logits(self, hidden_states, sampling_metadata):
|
||||
assert sampling_metadata is None
|
||||
return hidden_states + 1
|
||||
|
||||
model = MingImageForConditionalGeneration.__new__(MingImageForConditionalGeneration)
|
||||
torch.nn.Module.__init__(model)
|
||||
model.language_model = _LanguageModel()
|
||||
|
||||
hidden_states = torch.zeros(2, 3)
|
||||
assert torch.equal(model.compute_logits(hidden_states), hidden_states + 1)
|
||||
|
||||
|
||||
def test_embedding_only_run_uses_empty_modality_masks():
|
||||
model = MingImageForConditionalGeneration.__new__(MingImageForConditionalGeneration)
|
||||
torch.nn.Module.__init__(model)
|
||||
image_mask, audio_mask = model._compute_modality_masks(None, torch.empty(7, 2048))
|
||||
|
||||
assert image_mask.shape == (7,)
|
||||
assert not image_mask.any()
|
||||
assert torch.equal(audio_mask, image_mask)
|
||||
|
||||
|
||||
def test_mllm_reports_root_mlp_query_tokens_as_loaded(monkeypatch):
|
||||
model = MingImageForConditionalGeneration.__new__(MingImageForConditionalGeneration)
|
||||
torch.nn.Module.__init__(model)
|
||||
model.query_tokens_dict = torch.nn.ParameterDict({"16x16": torch.nn.Parameter(torch.empty(1), requires_grad=False)})
|
||||
monkeypatch.setattr(
|
||||
MingFlashOmniThinkerForConditionalGeneration,
|
||||
"load_weights",
|
||||
lambda self, weights: {"language_model.weight"},
|
||||
)
|
||||
|
||||
assert model.load_weights([]) == {
|
||||
"language_model.weight",
|
||||
"query_tokens_dict.16x16",
|
||||
}
|
||||
@@ -53,6 +53,8 @@ EXCLUDED_MODELS = [
|
||||
"BooguImageTurboPipeline",
|
||||
"LancePipeline",
|
||||
"MingImagePipeline",
|
||||
"MingImageDiffusionPipeline",
|
||||
"MingImageLayeredDiffusionPipeline",
|
||||
"InternVLAA1Pipeline",
|
||||
"StableDiffusion3Pipeline",
|
||||
"HunyuanImage3ForCausalMM",
|
||||
|
||||
@@ -86,6 +86,7 @@ from vllm_omni.model_executor.models.ming_flash_omni.pipeline import (
|
||||
MING_FLASH_OMNI_THINKER_ONLY_PIPELINE,
|
||||
MING_FLASH_OMNI_TTS_PIPELINE,
|
||||
)
|
||||
from vllm_omni.model_executor.models.ming_image.pipeline import MING_IMAGE_PIPELINE
|
||||
from vllm_omni.model_executor.models.ming_tts.pipeline import (
|
||||
MING_TTS_MOE_PIPELINE,
|
||||
MING_TTS_PIPELINE,
|
||||
@@ -187,6 +188,7 @@ OMNI_PIPELINES: dict[str, PipelineConfig | PipelineResolverFunc] = {
|
||||
"ming_flash_omni_tts": MING_FLASH_OMNI_TTS_PIPELINE,
|
||||
"ming_flash_omni_thinker_only": MING_FLASH_OMNI_THINKER_ONLY_PIPELINE,
|
||||
"ming_flash_omni_image": MING_FLASH_OMNI_IMAGE_PIPELINE,
|
||||
"ming_image": MING_IMAGE_PIPELINE,
|
||||
"moss_tts_nano": MOSS_TTS_NANO_PIPELINE,
|
||||
"minimax_h3_disaggregated": MINIMAX_H3_PIPELINE,
|
||||
"omnivoice": OMNIVOICE_PIPELINE,
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# Ming-Image Design and Design-Layer two-stage deployment.
|
||||
pipeline: ming_image
|
||||
async_chunk: false
|
||||
|
||||
trust_remote_code: false
|
||||
distributed_executor_backend: mp
|
||||
dtype: bfloat16
|
||||
enable_prefix_caching: false
|
||||
|
||||
stages:
|
||||
- stage_id: 0
|
||||
enforce_eager: true
|
||||
tensor_parallel_size: 1
|
||||
devices: "0"
|
||||
gpu_memory_utilization: 0.55
|
||||
max_num_seqs: 1
|
||||
output_connectors:
|
||||
to_stage_1: shared_memory_connector
|
||||
default_sampling_params:
|
||||
max_tokens: 1
|
||||
temperature: 0
|
||||
seed: 42
|
||||
|
||||
- stage_id: 1
|
||||
enforce_eager: false
|
||||
diffusion_compile_granularity: regional
|
||||
diffusion_compile_dynamic: true
|
||||
tensor_parallel_size: 1
|
||||
devices: "1"
|
||||
max_num_seqs: 1
|
||||
input_connectors:
|
||||
from_stage_0: shared_memory_connector
|
||||
default_sampling_params:
|
||||
seed: 42
|
||||
|
||||
connectors:
|
||||
shared_memory_connector:
|
||||
name: SharedMemoryConnector
|
||||
|
||||
edges:
|
||||
- from: 0
|
||||
to: 1
|
||||
window_size: -1
|
||||
max_inflight: 1
|
||||
|
||||
platforms: {}
|
||||
@@ -444,6 +444,7 @@ def extract_zimage_context(
|
||||
cap_pos_ids,
|
||||
x_inner_pad_mask,
|
||||
cap_inner_pad_mask,
|
||||
_cap_feats_2,
|
||||
) = module.patchify_and_embed(x, cap_feats, patch_size, f_patch_size)
|
||||
|
||||
# Process image patches through embedder and noise refiner
|
||||
|
||||
+41
-17
@@ -1487,6 +1487,29 @@ class OmniDiffusionConfig:
|
||||
self.max_multimodal_image_inputs = metadata.max_multimodal_image_inputs
|
||||
self.supports_mixed_reference_inputs = metadata.supports_mixed_reference_inputs
|
||||
|
||||
def _load_component_transformer_config(self) -> bool:
|
||||
"""Load a registered pipeline's component-level DiT configuration."""
|
||||
from vllm.transformers_utils.config import get_hf_file_to_dict
|
||||
|
||||
from vllm_omni.model_extras import get_transformer_config_subfolder
|
||||
|
||||
transformer_subfolder = get_transformer_config_subfolder(
|
||||
self.model_class_name,
|
||||
model=self.model,
|
||||
revision=self.revision,
|
||||
)
|
||||
tf_config_dict = get_hf_file_to_dict(
|
||||
f"{transformer_subfolder}/config.json",
|
||||
self.model,
|
||||
revision=self.revision,
|
||||
)
|
||||
if tf_config_dict is None:
|
||||
tf_config_dict = get_hf_file_to_dict("unet/config.json", self.model, revision=self.revision)
|
||||
if tf_config_dict is None:
|
||||
return False
|
||||
self.set_tf_model_config(TransformerConfig.from_dict(tf_config_dict))
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _looks_like_lance_subfolder(model: str | None) -> bool:
|
||||
"""Return True when ``--model`` points at a Lance per-component subfolder.
|
||||
@@ -1553,23 +1576,7 @@ class OmniDiffusionConfig:
|
||||
exc,
|
||||
)
|
||||
else:
|
||||
from vllm_omni.model_extras import get_transformer_config_subfolder
|
||||
|
||||
transformer_subfolder = get_transformer_config_subfolder(
|
||||
self.model_class_name,
|
||||
model=self.model,
|
||||
revision=self.revision,
|
||||
)
|
||||
tf_config_dict = get_hf_file_to_dict(
|
||||
f"{transformer_subfolder}/config.json",
|
||||
self.model,
|
||||
revision=self.revision,
|
||||
)
|
||||
if tf_config_dict is None:
|
||||
tf_config_dict = get_hf_file_to_dict("unet/config.json", self.model, revision=self.revision)
|
||||
if tf_config_dict is not None:
|
||||
self.set_tf_model_config(TransformerConfig.from_dict(tf_config_dict))
|
||||
else:
|
||||
if not self._load_component_transformer_config():
|
||||
self.set_tf_model_config(TransformerConfig())
|
||||
else:
|
||||
raise FileNotFoundError("Diffusers pipeline index not found")
|
||||
@@ -1605,6 +1612,23 @@ class OmniDiffusionConfig:
|
||||
self.set_tf_model_config(TransformerConfig())
|
||||
self.update_multimodal_support()
|
||||
return
|
||||
# An explicit topology or CLI override can select a native,
|
||||
# registered pipeline whose repository contains component
|
||||
# configs but no root HF config or Diffusers index. Trust
|
||||
# that explicit class selection only when its component
|
||||
# transformer config is present.
|
||||
from vllm_omni.diffusion.registry import DiffusionModelRegistry
|
||||
|
||||
if (
|
||||
self.model_class_name in DiffusionModelRegistry.get_supported_archs()
|
||||
and self._load_component_transformer_config()
|
||||
):
|
||||
logger.info(
|
||||
"Using explicitly selected diffusion pipeline %r with component-level configuration.",
|
||||
self.model_class_name,
|
||||
)
|
||||
self.update_multimodal_support()
|
||||
return
|
||||
raise ValueError(f"Could not find config.json or a Diffusers pipeline index for {self.model}")
|
||||
|
||||
self.set_tf_model_config(TransformerConfig.from_dict(cfg))
|
||||
|
||||
@@ -48,6 +48,8 @@ class ForwardContext:
|
||||
total_denoise_steps: int | None = None
|
||||
# Per-request reference latent for img2img DiT models (e.g. Ming)
|
||||
ref_latent: torch.Tensor | None = None
|
||||
# Per-request projected direct-VLM condition (e.g., Ming-Image). For now for bsz 1.
|
||||
direct_condition: torch.Tensor | None = None
|
||||
# whether to split the text embed in sequence parallel, if True, the text embed will be split in sequence parallel
|
||||
|
||||
# Sequence Parallel padding support
|
||||
@@ -377,3 +379,9 @@ def set_forward_context_ref_latent(ref_latent: torch.Tensor | None) -> None:
|
||||
"""
|
||||
if _forward_context is not None:
|
||||
_forward_context.ref_latent = ref_latent
|
||||
|
||||
|
||||
def set_forward_context_direct_condition(direct_condition: torch.Tensor | None) -> None:
|
||||
"""Set the projected direct-VLM condition on the active context."""
|
||||
if _forward_context is not None:
|
||||
_forward_context.direct_condition = direct_condition
|
||||
@@ -1,4 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
# Copyright 2025 The vLLM-Omni team.
|
||||
#
|
||||
# Adapted from Ming repository (inclusionAI/Ming) — the ``get_condition_embeds
|
||||
@@ -65,12 +66,16 @@ class MingConditionEncoder(nn.Module):
|
||||
thinker_hidden_size: int = 4096,
|
||||
device: torch.device | str | None = None,
|
||||
dtype: torch.dtype | None = None,
|
||||
normalize_output: bool = True,
|
||||
strict_loading: bool = False,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.config = image_gen_config
|
||||
self.thinker_hidden_size = thinker_hidden_size
|
||||
self._target_device = torch.device(device) if device is not None else None
|
||||
self._target_dtype = dtype
|
||||
self.normalize_output = normalize_output
|
||||
self.strict_loading = strict_loading
|
||||
|
||||
# Populated lazily by ``load_from_checkpoint`` to keep this module
|
||||
# cheap to construct (useful for dummy-init paths and unit tests).
|
||||
@@ -195,6 +200,8 @@ class MingConditionEncoder(nn.Module):
|
||||
condition path to be meaningful.
|
||||
"""
|
||||
if not mlp_path.exists():
|
||||
if self.strict_loading:
|
||||
raise FileNotFoundError(f"Missing Ming MLP directory: {mlp_path}")
|
||||
logger.warning(
|
||||
"[MingConditionEncoder] mlp/ subfolder missing at %s — proj/norm "
|
||||
"will stay randomly initialized. EXPECT BAD IMAGES until this "
|
||||
@@ -206,6 +213,8 @@ class MingConditionEncoder(nn.Module):
|
||||
try:
|
||||
from safetensors.torch import load_file # type: ignore
|
||||
except ImportError:
|
||||
if self.strict_loading:
|
||||
raise
|
||||
logger.exception("[MingConditionEncoder] safetensors not installed")
|
||||
return
|
||||
|
||||
@@ -213,6 +222,8 @@ class MingConditionEncoder(nn.Module):
|
||||
if not candidates:
|
||||
candidates = sorted(mlp_path.glob("*.bin"))
|
||||
if not candidates:
|
||||
if self.strict_loading:
|
||||
raise FileNotFoundError(f"No Ming MLP weights under {mlp_path}")
|
||||
logger.warning("[MingConditionEncoder] no weight files under %s", mlp_path)
|
||||
return
|
||||
|
||||
@@ -258,6 +269,8 @@ class MingConditionEncoder(nn.Module):
|
||||
ok_ow = _copy(self.proj_out.weight, "proj_out.weight")
|
||||
ok_ob = _copy(self.proj_out.bias, "proj_out.bias")
|
||||
if not (ok_w and ok_b and ok_ow and ok_ob):
|
||||
if self.strict_loading:
|
||||
raise ValueError(f"Ming condition projection weights are incomplete or incompatible under {mlp_path}.")
|
||||
logger.error(
|
||||
"[MingConditionEncoder] proj_in/proj_out NOT fully loaded; diffusion conditioning will be garbage."
|
||||
)
|
||||
@@ -342,7 +355,8 @@ class MingConditionEncoder(nn.Module):
|
||||
# L2 normalize + ×1000 rescale. Ming's ``get_condition_embeds_for_image_gen``
|
||||
# applies F.normalize; ``ZImageModel_withMLP`` then rescales by 1000
|
||||
# when ``text_encoder_norm=True``. We fold both into one place.
|
||||
cap_feats = torch.nn.functional.normalize(cap_feats, dim=-1)
|
||||
if self.normalize_output:
|
||||
cap_feats = torch.nn.functional.normalize(cap_feats, dim=-1)
|
||||
if self.config.text_encoder_norm:
|
||||
cap_feats = cap_feats * 1000.0
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
"""Ming-Image specific diffusion components."""
|
||||
@@ -0,0 +1,37 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Validation for Ming-Image component configuration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
def load_mlp_config(model_path: str | Path) -> dict[str, Any]:
|
||||
path = Path(model_path) / "mlp" / "config.json"
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError(f"Ming-Image checkpoint is missing MLP config: {path}")
|
||||
data = json.loads(path.read_text())
|
||||
required = {
|
||||
"diffusion_c_input_dim",
|
||||
"diffusion_inner_dim",
|
||||
"img_gen_scales",
|
||||
"selected_hidden_states_layers",
|
||||
"use_identity_mlp",
|
||||
"use_learnable_token_condition",
|
||||
"use_vlm_directvlm_condition",
|
||||
}
|
||||
missing = sorted(required - data.keys())
|
||||
if missing:
|
||||
raise ValueError(f"Ming-Image MLP config is missing keys: {missing}")
|
||||
if data["img_gen_scales"] != [16]:
|
||||
raise ValueError(f"Only img_gen_scales=[16] is supported, got {data['img_gen_scales']!r}.")
|
||||
if data["selected_hidden_states_layers"] != [5, 12, 20]:
|
||||
raise ValueError("Ming-Image requires selected hidden layers [5, 12, 20].")
|
||||
if not data["use_identity_mlp"]:
|
||||
raise NotImplementedError("Ming-Image non-identity diffusion MLP is not supported.")
|
||||
if not data["use_learnable_token_condition"] or not data["use_vlm_directvlm_condition"]:
|
||||
raise ValueError("Ming-Image requires both query-token and direct-VLM conditioning.")
|
||||
return data
|
||||
@@ -0,0 +1,88 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Ming-Image query and direct-VLM condition projectors."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from safetensors.torch import load_file
|
||||
|
||||
from vllm_omni.diffusion.models.ming_flash_omni.condition_encoder import MingConditionEncoder
|
||||
from vllm_omni.transformers_utils.configs.ming_flash_omni import MingImageGenConfig
|
||||
|
||||
from .component_config import load_mlp_config
|
||||
|
||||
|
||||
class MingImageConditioning(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
model_path: str | Path,
|
||||
*,
|
||||
device: torch.device | str,
|
||||
dtype: torch.dtype,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
model_path = Path(model_path)
|
||||
mlp_config = load_mlp_config(model_path)
|
||||
config = MingImageGenConfig(
|
||||
diffusion_c_input_dim=int(mlp_config["diffusion_c_input_dim"]),
|
||||
thinker_hidden_size=2048,
|
||||
img_gen_scales=list(mlp_config["img_gen_scales"]),
|
||||
text_encoder_norm=bool(mlp_config.get("text_encoder_norm", False)),
|
||||
)
|
||||
self.query_encoder = MingConditionEncoder(
|
||||
config,
|
||||
thinker_hidden_size=2048,
|
||||
device=device,
|
||||
dtype=dtype,
|
||||
normalize_output=bool(mlp_config.get("connector_norm", False)),
|
||||
strict_loading=True,
|
||||
)
|
||||
self.query_encoder.load_from_checkpoint(model_path)
|
||||
|
||||
selected_layers = mlp_config["selected_hidden_states_layers"]
|
||||
direct_input_dim = 2048 * len(selected_layers)
|
||||
direct_output_dim = int(mlp_config["diffusion_inner_dim"])
|
||||
self.direct_projector = nn.Sequential(
|
||||
nn.RMSNorm(direct_input_dim, eps=1e-5),
|
||||
nn.Linear(direct_input_dim, direct_output_dim, bias=True),
|
||||
).to(device=device, dtype=dtype)
|
||||
self._load_direct_weights(model_path / "mlp" / "model.safetensors")
|
||||
|
||||
def _load_direct_weights(self, path: Path) -> None:
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError(f"Missing Ming-Image direct projector weights: {path}")
|
||||
state = load_file(str(path))
|
||||
mapping = {
|
||||
"0.weight": "proj_directvlm.0.weight",
|
||||
"1.weight": "proj_directvlm.1.weight",
|
||||
"1.bias": "proj_directvlm.1.bias",
|
||||
}
|
||||
own_state = self.direct_projector.state_dict()
|
||||
projected: dict[str, torch.Tensor] = {}
|
||||
for target, source in mapping.items():
|
||||
if source not in state:
|
||||
raise ValueError(f"Missing Ming-Image MLP weight {source!r}.")
|
||||
if own_state[target].shape != state[source].shape:
|
||||
raise ValueError(
|
||||
f"Ming-Image MLP shape mismatch for {source}: "
|
||||
f"expected {tuple(own_state[target].shape)}, got {tuple(state[source].shape)}."
|
||||
)
|
||||
projected[target] = state[source]
|
||||
self.direct_projector.load_state_dict(projected, strict=True)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
query_hidden_states: torch.Tensor,
|
||||
direct_hidden_states: torch.Tensor,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
return (
|
||||
self.query_encoder(query_hidden_states),
|
||||
self.direct_projector(direct_hidden_states),
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["MingImageConditioning"]
|
||||
@@ -0,0 +1,425 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Diffusion pipeline for Ming-Image Design and Design-Layer checkpoints."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from collections.abc import Iterable
|
||||
from typing import Any, ClassVar
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from diffusers.image_processor import VaeImageProcessor
|
||||
from diffusers.schedulers import FlowMatchEulerDiscreteScheduler
|
||||
from PIL import Image
|
||||
from vllm.model_executor.models.utils import AutoWeightsLoader
|
||||
|
||||
from vllm_omni.diffusion.compile import regionally_compile
|
||||
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.forward_context import (
|
||||
set_forward_context_direct_condition,
|
||||
set_forward_context_ref_latent,
|
||||
)
|
||||
from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
|
||||
from vllm_omni.diffusion.model_loader.hub_prefetch import from_pretrained_with_prefetch
|
||||
from vllm_omni.diffusion.models.ming_image.condition import MingImageConditioning
|
||||
from vllm_omni.diffusion.models.ming_image.transformer import MingImageTransformer2DModel
|
||||
from vllm_omni.diffusion.models.z_image.pipeline_z_image import ZImagePipeline
|
||||
from vllm_omni.diffusion.request import OmniDiffusionRequest
|
||||
from vllm_omni.diffusion.utils.hf_utils import get_diffusion_model_index
|
||||
from vllm_omni.diffusion.utils.tf_utils import get_transformer_config_kwargs
|
||||
from vllm_omni.diffusion.worker.request_batch import DiffusionRequestBatch
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
from vllm_omni.model_executor.model_loader.weight_utils import download_weights_from_hf_specific
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_DESIGN_PIPELINE = "MingImageDiffusionPipeline"
|
||||
_LAYERED_PIPELINE = "MingImageLayeredDiffusionPipeline"
|
||||
_VENDOR_TRANSFORMER_CLASS = "DiffusionTransformer"
|
||||
_DIFFUSION_REQUIRED_PATTERNS = [
|
||||
"model_index.json",
|
||||
"scheduler/**",
|
||||
"transformer/**",
|
||||
"vae/**",
|
||||
"mlp/**",
|
||||
"connector/**",
|
||||
]
|
||||
|
||||
|
||||
def _validate_variant_config(
|
||||
model_index: dict[str, Any] | None,
|
||||
transformer_config: Any,
|
||||
) -> bool:
|
||||
"""Validate checkpoint-owned variant metadata and return layered mode."""
|
||||
declared_transformer = getattr(transformer_config, "_class_name", None)
|
||||
if declared_transformer != _VENDOR_TRANSFORMER_CLASS:
|
||||
raise ValueError(
|
||||
"Ming-Image transformer/config.json must preserve the vendor "
|
||||
f"_class_name={_VENDOR_TRANSFORMER_CLASS!r}, got {declared_transformer!r}. "
|
||||
"vLLM-Omni loads those weights through MingImageTransformer2DModel."
|
||||
)
|
||||
|
||||
alignment_padding_mode = getattr(transformer_config, "alignment_padding_mode", None)
|
||||
multi_frame_output = getattr(transformer_config, "multi_frame_output", None)
|
||||
variant = (alignment_padding_mode, multi_frame_output)
|
||||
if variant == ("zero_masked", False):
|
||||
is_layer_decomposition = False
|
||||
elif variant == ("learned", True):
|
||||
is_layer_decomposition = True
|
||||
else:
|
||||
raise ValueError(
|
||||
"Ming-Image transformer/config.json must use either "
|
||||
"alignment_padding_mode='zero_masked' with multi_frame_output=False "
|
||||
"or alignment_padding_mode='learned' with multi_frame_output=True; "
|
||||
f"got alignment_padding_mode={alignment_padding_mode!r}, "
|
||||
f"multi_frame_output={multi_frame_output!r}."
|
||||
)
|
||||
|
||||
declared_pipeline = (model_index or {}).get("_class_name")
|
||||
if declared_pipeline is not None:
|
||||
if declared_pipeline not in {_DESIGN_PIPELINE, _LAYERED_PIPELINE}:
|
||||
raise ValueError(
|
||||
"Ming-Image model_index.json must declare "
|
||||
f"{_DESIGN_PIPELINE!r} or {_LAYERED_PIPELINE!r}, got {declared_pipeline!r}."
|
||||
)
|
||||
expected_pipeline = _LAYERED_PIPELINE if is_layer_decomposition else _DESIGN_PIPELINE
|
||||
if declared_pipeline != expected_pipeline:
|
||||
raise ValueError(
|
||||
f"Ming-Image model_index.json declares {declared_pipeline!r}, but "
|
||||
f"transformer/config.json describes {expected_pipeline!r}."
|
||||
)
|
||||
return is_layer_decomposition
|
||||
|
||||
|
||||
class MingImageDiffusionPipeline(ZImagePipeline):
|
||||
"""Ming-Image component adapter around the canonical Z-Image loop."""
|
||||
|
||||
supports_request_batch = False
|
||||
|
||||
_dit_modules: ClassVar[list[str]] = ["transformer"]
|
||||
_encoder_modules: ClassVar[list[str]] = ["conditioning"]
|
||||
_vae_modules: ClassVar[list[str]] = ["vae"]
|
||||
|
||||
def __init__(self, *, od_config: OmniDiffusionConfig, prefix: str = "") -> None:
|
||||
del prefix
|
||||
nn.Module.__init__(self)
|
||||
|
||||
model_path = od_config.model
|
||||
if not os.path.exists(model_path):
|
||||
model_path = download_weights_from_hf_specific(
|
||||
model_name_or_path=model_path,
|
||||
cache_dir=None,
|
||||
allow_patterns=_DIFFUSION_REQUIRED_PATTERNS,
|
||||
revision=od_config.revision,
|
||||
require_all=True,
|
||||
)
|
||||
local_files_only = os.path.isdir(model_path)
|
||||
dtype = od_config.dtype
|
||||
|
||||
self.od_config = od_config
|
||||
self._execution_device = get_local_device()
|
||||
self.device = self._execution_device
|
||||
model_index = get_diffusion_model_index(model_path, revision=od_config.revision) or {}
|
||||
transformer_config = od_config.tf_model_config
|
||||
self.is_layer_decomposition = _validate_variant_config(
|
||||
model_index,
|
||||
transformer_config,
|
||||
)
|
||||
|
||||
self.default_num_inference_steps = 12
|
||||
self.default_guidance_scale = 2.0 if self.is_layer_decomposition else 1.0
|
||||
self._num_frames_per_prompt = 1
|
||||
self._pending_prompt_embeds: list[torch.Tensor] | None = None
|
||||
self._pending_negative_prompt_embeds: list[torch.Tensor] | None = None
|
||||
self._uses_cudagraph_trees = False
|
||||
|
||||
self.weights_sources = [
|
||||
DiffusersPipelineLoader.ComponentSource(
|
||||
model_or_path=model_path,
|
||||
subfolder="transformer",
|
||||
revision=od_config.revision,
|
||||
prefix="transformer.",
|
||||
fall_back_to_pt=True,
|
||||
)
|
||||
]
|
||||
subfolders = ["scheduler", "transformer", "vae"]
|
||||
|
||||
self.scheduler = FlowMatchEulerDiscreteScheduler.from_pretrained(
|
||||
model_path,
|
||||
subfolder="scheduler",
|
||||
local_files_only=local_files_only,
|
||||
)
|
||||
|
||||
self.vae = from_pretrained_with_prefetch(
|
||||
DistributedAutoencoderKLQwenImage.from_pretrained,
|
||||
model_path,
|
||||
subfolder="vae",
|
||||
prefetch_list=subfolders,
|
||||
local_files_only=local_files_only,
|
||||
torch_dtype=dtype,
|
||||
).to(self.device)
|
||||
self.vae.eval()
|
||||
vae_channels = int(getattr(self.vae.config, "input_channels", 3))
|
||||
if vae_channels != 4:
|
||||
raise ValueError(f"Ming-Image requires an RGBA VAE, got input_channels={vae_channels}.")
|
||||
|
||||
transformer_kwargs = get_transformer_config_kwargs(
|
||||
od_config.tf_model_config,
|
||||
MingImageTransformer2DModel,
|
||||
)
|
||||
self.transformer = MingImageTransformer2DModel(
|
||||
quant_config=od_config.quantization_config,
|
||||
**transformer_kwargs,
|
||||
)
|
||||
self.conditioning = MingImageConditioning(
|
||||
model_path,
|
||||
device=self.device,
|
||||
dtype=dtype,
|
||||
)
|
||||
self.text_encoder = None
|
||||
self.tokenizer = None
|
||||
|
||||
self.vae_scale_factor = 2 ** len(self.vae.config.temperal_downsample)
|
||||
self.image_processor = VaeImageProcessor(
|
||||
vae_scale_factor=self.vae_scale_factor * 2,
|
||||
do_convert_rgb=False,
|
||||
)
|
||||
self.setup_diffusion_pipeline_profiler(
|
||||
enable_diffusion_pipeline_profiler=od_config.enable_diffusion_pipeline_profiler
|
||||
)
|
||||
|
||||
def setup_compile(self) -> None:
|
||||
# Keep request preparation, scheduling, and VAE work eager,
|
||||
# while capturing repeated DiT blocks for CUDAGraph Trees replay.
|
||||
if self.od_config.diffusion_compile_granularity != "regional":
|
||||
logger.warning(
|
||||
"Ming-Image CUDA Graph uses regional DiT compilation; diffusion_compile_granularity=%r is ignored.",
|
||||
self.od_config.diffusion_compile_granularity,
|
||||
)
|
||||
self.transformer = regionally_compile(
|
||||
self.transformer,
|
||||
mode="reduce-overhead",
|
||||
fullgraph=True,
|
||||
dynamic=self.od_config.diffusion_compile_dynamic,
|
||||
)
|
||||
self._uses_cudagraph_trees = True
|
||||
|
||||
def encode_prompt(self, *args, **kwargs):
|
||||
del args, kwargs
|
||||
if self._pending_prompt_embeds is None or self._pending_negative_prompt_embeds is None:
|
||||
raise RuntimeError("Ming-Image conditioning is only available during forward.")
|
||||
return self._pending_prompt_embeds, self._pending_negative_prompt_embeds
|
||||
|
||||
def prepare_latents(self, batch_size, *args, **kwargs):
|
||||
frames = self._num_frames_per_prompt
|
||||
flat = super().prepare_latents(batch_size * frames, *args, **kwargs)
|
||||
return torch.stack(flat.chunk(frames, dim=0), dim=2)
|
||||
|
||||
def _encode_reference(
|
||||
self,
|
||||
reference: Any | None,
|
||||
height: int,
|
||||
width: int,
|
||||
) -> torch.Tensor | None:
|
||||
if reference is None:
|
||||
return None
|
||||
if isinstance(reference, list):
|
||||
if len(reference) != 1:
|
||||
raise ValueError("Ming-Image currently accepts exactly one reference image.")
|
||||
reference = reference[0]
|
||||
if isinstance(reference, Image.Image):
|
||||
reference = reference.convert("RGBA")
|
||||
if not isinstance(reference, torch.Tensor):
|
||||
reference = self.image_processor.preprocess(reference, height=height, width=width)
|
||||
if reference.ndim != 4 or reference.shape[1] != 4:
|
||||
raise ValueError("Ming-Image reference must be a batched RGBA tensor or an RGBA-compatible image.")
|
||||
reference = reference.to(device=self.device, dtype=self.vae.dtype).unsqueeze(2)
|
||||
latent = self.vae.encode(reference).latent_dist.mode()
|
||||
return (latent - self.vae.config.shift_factor) * self.vae.config.scaling_factor
|
||||
|
||||
@staticmethod
|
||||
def _get_prompt_extra(req: DiffusionRequestBatch) -> dict[str, Any]:
|
||||
if not req.prompts:
|
||||
return {}
|
||||
prompt = req.prompts[0]
|
||||
if isinstance(prompt, dict):
|
||||
return dict(prompt.get("extra") or {})
|
||||
if hasattr(prompt, "_asdict"):
|
||||
return dict(prompt._asdict().get("extra") or {})
|
||||
return {}
|
||||
|
||||
def _configure_output_frames(
|
||||
self,
|
||||
*,
|
||||
reference: Any | None,
|
||||
num_layers: int,
|
||||
is_dummy_run: bool,
|
||||
) -> None:
|
||||
if num_layers < 1:
|
||||
raise ValueError("num_layers must be at least 1.")
|
||||
if self.is_layer_decomposition:
|
||||
if reference is None and not is_dummy_run:
|
||||
raise ValueError("Ming-Image Design-Layer requires a reference image.")
|
||||
self._num_frames_per_prompt = num_layers + 1
|
||||
return
|
||||
if num_layers != 1:
|
||||
raise ValueError("Ming-Image Design supports exactly one output frame.")
|
||||
self._num_frames_per_prompt = 1
|
||||
|
||||
@torch.inference_mode()
|
||||
def forward(self, req: DiffusionRequestBatch) -> DiffusionOutput:
|
||||
sampling = req.sampling_params
|
||||
if sampling.num_outputs_per_prompt != 1:
|
||||
# TODO(yuanheng-zhao): enable after supporting batching
|
||||
raise ValueError(
|
||||
f"Ming-Image currently supports num_outputs_per_prompt=1 only, got {sampling.num_outputs_per_prompt}."
|
||||
)
|
||||
|
||||
extra = self._get_prompt_extra(req)
|
||||
extra_args = sampling.extra_args or {}
|
||||
|
||||
is_dummy_run = req.is_dummy_run()
|
||||
query_hidden = extra.get("query_hidden_states")
|
||||
direct_hidden = extra.get("direct_hidden_states")
|
||||
if query_hidden is None or direct_hidden is None:
|
||||
if not is_dummy_run:
|
||||
raise ValueError("Ming-Image requests require query and direct conditions.")
|
||||
logger.warning("Ming-Image conditions are absent during warmup; using zero tensors.")
|
||||
query_hidden = torch.zeros((256, 2048), device=self.device, dtype=self.od_config.dtype)
|
||||
direct_hidden = torch.zeros((1, 6144), device=self.device, dtype=self.od_config.dtype)
|
||||
if not isinstance(query_hidden, torch.Tensor) or not isinstance(direct_hidden, torch.Tensor):
|
||||
raise TypeError("Ming-Image query and direct conditions must be tensors.")
|
||||
if query_hidden.ndim == 2:
|
||||
query_hidden = query_hidden.unsqueeze(0)
|
||||
if direct_hidden.ndim == 2:
|
||||
direct_hidden = direct_hidden.unsqueeze(0)
|
||||
query_hidden = query_hidden.to(device=self.device, dtype=self.od_config.dtype)
|
||||
direct_hidden = direct_hidden.to(device=self.device, dtype=self.od_config.dtype)
|
||||
cap_feats, direct_condition = self.conditioning(query_hidden, direct_hidden)
|
||||
|
||||
reference = extra.get("reference_image")
|
||||
num_layers = int(extra_args.get("num_layers", extra.get("num_layers", 1)))
|
||||
self._configure_output_frames(
|
||||
reference=reference,
|
||||
num_layers=num_layers,
|
||||
is_dummy_run=is_dummy_run,
|
||||
)
|
||||
|
||||
height = int(extra_args.get("height") or sampling.height or 1024)
|
||||
width = int(extra_args.get("width") or sampling.width or 1024)
|
||||
steps = int(extra_args.get("steps") or sampling.num_inference_steps or self.default_num_inference_steps)
|
||||
cfg = float(
|
||||
extra_args["cfg"]
|
||||
if extra_args.get("cfg") is not None
|
||||
else sampling.guidance_scale
|
||||
if sampling.guidance_scale is not None
|
||||
else self.default_guidance_scale
|
||||
)
|
||||
seed = extra_args.get("seed", sampling.seed)
|
||||
generator = (
|
||||
torch.Generator(device=self.device).manual_seed(int(seed)) if seed is not None else sampling.generator
|
||||
)
|
||||
|
||||
ref_latent = self._encode_reference(reference, height, width)
|
||||
positive = [item for item in cap_feats]
|
||||
negative = [torch.zeros_like(item) for item in positive]
|
||||
self._pending_prompt_embeds = positive
|
||||
self._pending_negative_prompt_embeds = negative
|
||||
|
||||
apply_cfg = cfg > 0
|
||||
context_direct = (
|
||||
torch.cat([direct_condition, torch.zeros_like(direct_condition)], dim=0) if apply_cfg else direct_condition
|
||||
)
|
||||
context_ref = ref_latent
|
||||
if apply_cfg and context_ref is not None:
|
||||
context_ref = context_ref.repeat(2, 1, 1, 1, 1)
|
||||
|
||||
inner_sampling = OmniDiffusionSamplingParams(
|
||||
height=height,
|
||||
width=width,
|
||||
num_inference_steps=steps,
|
||||
guidance_scale=cfg,
|
||||
generator=generator,
|
||||
output_type="latent",
|
||||
)
|
||||
inner_req = DiffusionRequestBatch(
|
||||
requests=[
|
||||
OmniDiffusionRequest(
|
||||
prompt={"prompt": ""},
|
||||
sampling_params=inner_sampling,
|
||||
request_id=req.request_id or "ming-image",
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
set_forward_context_ref_latent(context_ref)
|
||||
set_forward_context_direct_condition(context_direct)
|
||||
try:
|
||||
latent_output = super().forward(inner_req)
|
||||
if not isinstance(latent_output.output, torch.Tensor):
|
||||
raise TypeError("Ming-Image denoising must return latent tensors.")
|
||||
image = self._decode_latent_frames(latent_output.output)
|
||||
return DiffusionOutput(
|
||||
output=image,
|
||||
stage_durations=latent_output.stage_durations,
|
||||
)
|
||||
finally:
|
||||
set_forward_context_ref_latent(None)
|
||||
set_forward_context_direct_condition(None)
|
||||
self._pending_prompt_embeds = None
|
||||
self._pending_negative_prompt_embeds = None
|
||||
self._num_frames_per_prompt = 1
|
||||
|
||||
@staticmethod
|
||||
def _flatten_latent_frames(latents: torch.Tensor) -> torch.Tensor:
|
||||
if latents.ndim == 4:
|
||||
return latents
|
||||
if latents.ndim != 5:
|
||||
raise ValueError(f"Expected 4D or 5D Ming-Image latents, got {tuple(latents.shape)}")
|
||||
batch, channels, frames, height, width = latents.shape
|
||||
return latents.permute(2, 0, 1, 3, 4).reshape(
|
||||
frames * batch,
|
||||
channels,
|
||||
height,
|
||||
width,
|
||||
)
|
||||
|
||||
def _decode_latent_frames(self, latents: torch.Tensor) -> torch.Tensor:
|
||||
latents = self._flatten_latent_frames(latents).to(self.vae.dtype).unsqueeze(2)
|
||||
latents = (latents / self.vae.config.scaling_factor) + self.vae.config.shift_factor
|
||||
image = self.vae.decode(latents, return_dict=False)[0]
|
||||
if image.ndim == 5:
|
||||
if image.shape[2] != 1:
|
||||
raise ValueError(
|
||||
f"Ming-Image VAE returned multiple decoded frames for a single latent frame: {tuple(image.shape)}"
|
||||
)
|
||||
image = image.squeeze(2)
|
||||
return image
|
||||
|
||||
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
||||
loaded = AutoWeightsLoader(self).load_weights(weights)
|
||||
loaded |= {f"vae.{name}" for name, _ in self.vae.named_parameters()}
|
||||
loaded |= {f"conditioning.{name}" for name, _ in self.conditioning.named_parameters()}
|
||||
return loaded
|
||||
|
||||
|
||||
def get_ming_image_post_process_func(od_config: OmniDiffusionConfig):
|
||||
del od_config
|
||||
image_processor = VaeImageProcessor(vae_scale_factor=16, do_convert_rgb=False)
|
||||
|
||||
def post_process(images: torch.Tensor):
|
||||
if images.ndim == 5:
|
||||
images = images.permute(0, 2, 1, 3, 4).flatten(0, 1)
|
||||
return image_processor.postprocess(images.float())
|
||||
|
||||
return post_process
|
||||
|
||||
|
||||
__all__ = ["MingImageDiffusionPipeline", "get_ming_image_post_process_func"]
|
||||
@@ -0,0 +1,52 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Ming-Image adapter for the shared Z-Image transformer."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import torch
|
||||
|
||||
from vllm_omni.diffusion.forward_context import get_forward_context, is_forward_context_available
|
||||
from vllm_omni.diffusion.models.z_image.z_image_transformer import ZImageTransformer2DModel
|
||||
|
||||
|
||||
class MingImageTransformer2DModel(ZImageTransformer2DModel):
|
||||
"""Runtime adapter for vendor DiffusionTransformer checkpoints.
|
||||
|
||||
This adapter reuses the shared Z-Image DiT and injects Ming-specific request conditions.
|
||||
"""
|
||||
|
||||
def forward(
|
||||
self,
|
||||
x: list[torch.Tensor],
|
||||
t,
|
||||
cap_feats: list[torch.Tensor],
|
||||
patch_size=2,
|
||||
f_patch_size=1,
|
||||
):
|
||||
generated_frames = [item.shape[1] for item in x]
|
||||
ref_x = None
|
||||
cap_feats_2 = None
|
||||
if is_forward_context_available():
|
||||
context = get_forward_context()
|
||||
if context.ref_latent is not None:
|
||||
ref_x = [item.to(device=x[0].device, dtype=x[0].dtype) for item in context.ref_latent.unbind(dim=0)]
|
||||
if context.direct_condition is not None:
|
||||
cap_feats_2 = [
|
||||
item.to(device=x[0].device, dtype=x[0].dtype) for item in context.direct_condition.unbind(dim=0)
|
||||
]
|
||||
|
||||
output, metadata = super().forward(
|
||||
x,
|
||||
t,
|
||||
cap_feats,
|
||||
patch_size=patch_size,
|
||||
f_patch_size=f_patch_size,
|
||||
ref_x=ref_x,
|
||||
cap_feats_2=cap_feats_2,
|
||||
)
|
||||
output = [item[:, :frames] for item, frames in zip(output, generated_frames)]
|
||||
return output, metadata
|
||||
|
||||
|
||||
__all__ = ["MingImageTransformer2DModel"]
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
# Copyright 2025 Alibaba Z-Image Team and The HuggingFace Team. All rights reserved.
|
||||
#
|
||||
@@ -584,7 +584,8 @@ class ZImagePipeline(nn.Module, DiffusionPipelineProfilerMixin, SupportsComponen
|
||||
|
||||
# 5. Prepare timesteps
|
||||
if image is None:
|
||||
image_seq_len = (latents.shape[2] // 2) * (latents.shape[3] // 2)
|
||||
# for both [B, C, H, W] and multi-layer/frame [B, C, F, H, W]
|
||||
image_seq_len = (latents.shape[-2] // 2) * (latents.shape[-1] // 2)
|
||||
mu = calculate_shift(
|
||||
image_seq_len,
|
||||
self.scheduler.config.get("base_image_seq_len", 256),
|
||||
@@ -643,7 +644,8 @@ class ZImagePipeline(nn.Module, DiffusionPipelineProfilerMixin, SupportsComponen
|
||||
latents_typed = latents.to(self.od_config.dtype)
|
||||
|
||||
if apply_cfg:
|
||||
latent_model_input = latents_typed.repeat(2, 1, 1, 1)
|
||||
repeat_dims = (2,) + (1,) * (latents_typed.ndim - 1)
|
||||
latent_model_input = latents_typed.repeat(*repeat_dims)
|
||||
prompt_embeds_model_input = prompt_embeds + negative_prompt_embeds
|
||||
timestep_model_input = timestep.repeat(2)
|
||||
else:
|
||||
@@ -651,9 +653,13 @@ class ZImagePipeline(nn.Module, DiffusionPipelineProfilerMixin, SupportsComponen
|
||||
prompt_embeds_model_input = prompt_embeds
|
||||
timestep_model_input = timestep
|
||||
|
||||
latent_model_input = latent_model_input.unsqueeze(2)
|
||||
if latent_model_input.ndim == 4:
|
||||
latent_model_input = latent_model_input.unsqueeze(2)
|
||||
latent_model_input_list = list(latent_model_input.unbind(dim=0))
|
||||
|
||||
if getattr(self, "_uses_cudagraph_trees", False):
|
||||
# check Ming-Image
|
||||
torch.compiler.cudagraph_mark_step_begin()
|
||||
model_out_list = self.transformer(
|
||||
latent_model_input_list,
|
||||
timestep_model_input,
|
||||
@@ -690,7 +696,8 @@ class ZImagePipeline(nn.Module, DiffusionPipelineProfilerMixin, SupportsComponen
|
||||
else:
|
||||
noise_pred = torch.stack([t.float() for t in model_out_list], dim=0)
|
||||
|
||||
noise_pred = noise_pred.squeeze(2)
|
||||
if latents.ndim == 4:
|
||||
noise_pred = noise_pred.squeeze(2)
|
||||
noise_pred = -noise_pred
|
||||
|
||||
# compute the previous noisy sample x_t -> x_t-1
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
# _sp_plan definition adapted from HuggingFace diffusers library (_cp_plan)
|
||||
|
||||
# Copyright 2025 Alibaba Z-Image Team and The HuggingFace Team. All rights reserved.
|
||||
@@ -56,6 +56,8 @@ from vllm_omni.model_executor.layers.timestep_embedding import timestep_embeddin
|
||||
ADALN_EMBED_DIM = 256
|
||||
SEQ_MULTI_OF = 32
|
||||
|
||||
LEARNED_PADDING = "learned"
|
||||
ZERO_MASKED_PADDING = "zero_masked"
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
@@ -81,6 +83,8 @@ class UnifiedPrepare(nn.Module):
|
||||
cap_sin: torch.Tensor,
|
||||
x_item_seqlens: list[int],
|
||||
cap_item_seqlens: list[int],
|
||||
x_attn_mask: torch.Tensor,
|
||||
cap_attn_mask: torch.Tensor,
|
||||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
"""Combine x and cap tensors into unified sequences.
|
||||
|
||||
@@ -110,8 +114,9 @@ class UnifiedPrepare(nn.Module):
|
||||
unified_cos = pad_sequence(unified_cos, batch_first=True, padding_value=0.0)
|
||||
unified_sin = pad_sequence(unified_sin, batch_first=True, padding_value=0.0)
|
||||
unified_attn_mask = torch.zeros((bsz, unified_max_item_seqlen), dtype=torch.bool, device=device)
|
||||
for i, seq_len in enumerate(unified_item_seqlens):
|
||||
unified_attn_mask[i, :seq_len] = 1
|
||||
for i, (x_len, cap_len) in enumerate(zip(x_item_seqlens, cap_item_seqlens)):
|
||||
unified_attn_mask[i, :x_len] = x_attn_mask[i, :x_len]
|
||||
unified_attn_mask[i, x_len : x_len + cap_len] = cap_attn_mask[i, :cap_len]
|
||||
|
||||
return unified, unified_cos, unified_sin, unified_attn_mask
|
||||
|
||||
@@ -671,6 +676,8 @@ class ZImageTransformer2DModel(CachedTransformer):
|
||||
axes_dims=[32, 48, 48],
|
||||
axes_lens=[1024, 512, 512],
|
||||
quant_config: "QuantizationConfig | None" = None,
|
||||
alignment_padding_mode: str = LEARNED_PADDING,
|
||||
multi_frame_output: bool = True,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
# NOTE: `DiffusersPipelineLoader.load_model()` initializes this module
|
||||
@@ -685,6 +692,12 @@ class ZImageTransformer2DModel(CachedTransformer):
|
||||
self.dim = dim
|
||||
self.n_heads = n_heads
|
||||
|
||||
if alignment_padding_mode not in {LEARNED_PADDING, ZERO_MASKED_PADDING}:
|
||||
raise ValueError(f"Unsupported Z-Image alignment padding mode: {alignment_padding_mode!r}")
|
||||
if type(multi_frame_output) is not bool:
|
||||
raise ValueError("multi_frame_output must be boolean.")
|
||||
self.alignment_padding_mode = alignment_padding_mode
|
||||
self.multi_frame_output = multi_frame_output
|
||||
self.rope_theta = rope_theta
|
||||
self.t_scale = t_scale
|
||||
self.gradient_checkpointing = False
|
||||
@@ -780,8 +793,12 @@ class ZImageTransformer2DModel(CachedTransformer):
|
||||
),
|
||||
)
|
||||
|
||||
self.x_pad_token = nn.Parameter(torch.empty((1, dim)))
|
||||
self.cap_pad_token = nn.Parameter(torch.empty((1, dim)))
|
||||
if alignment_padding_mode == LEARNED_PADDING:
|
||||
self.x_pad_token = nn.Parameter(torch.empty((1, dim)))
|
||||
self.cap_pad_token = nn.Parameter(torch.empty((1, dim)))
|
||||
else:
|
||||
self.register_parameter("x_pad_token", None)
|
||||
self.register_parameter("cap_pad_token", None)
|
||||
|
||||
self.layers = nn.ModuleList(
|
||||
[
|
||||
@@ -823,6 +840,8 @@ class ZImageTransformer2DModel(CachedTransformer):
|
||||
.permute(6, 0, 3, 1, 4, 2, 5)
|
||||
.reshape(self.out_channels, F, H, W)
|
||||
)
|
||||
if not self.multi_frame_output:
|
||||
x[i] = x[i][:, :1]
|
||||
return x
|
||||
|
||||
@staticmethod
|
||||
@@ -840,6 +859,8 @@ class ZImageTransformer2DModel(CachedTransformer):
|
||||
all_cap_feats: list[torch.Tensor],
|
||||
patch_size: int,
|
||||
f_patch_size: int,
|
||||
all_image_ref: list[torch.Tensor | None] | None = None,
|
||||
all_cap_feats_2: list[torch.Tensor | None] | None = None,
|
||||
):
|
||||
pH = pW = patch_size
|
||||
pF = f_patch_size
|
||||
@@ -852,10 +873,16 @@ class ZImageTransformer2DModel(CachedTransformer):
|
||||
all_cap_pos_ids = []
|
||||
all_cap_pad_mask = []
|
||||
all_cap_feats_out = []
|
||||
all_cap_feats_2_out = []
|
||||
|
||||
for i, (image, cap_feat) in enumerate(zip(all_image, all_cap_feats)):
|
||||
if all_image_ref is None:
|
||||
all_image_ref = [None] * len(all_image)
|
||||
if all_cap_feats_2 is None:
|
||||
all_cap_feats_2 = [None] * len(all_image)
|
||||
|
||||
for image, cap_feat, cap_feat_2, image_ref in zip(all_image, all_cap_feats, all_cap_feats_2, all_image_ref):
|
||||
### Process Caption
|
||||
cap_ori_len = len(cap_feat)
|
||||
cap_ori_len = len(cap_feat) + (len(cap_feat_2) if cap_feat_2 is not None else 0)
|
||||
cap_padding_len = (-cap_ori_len) % SEQ_MULTI_OF
|
||||
# padded position ids
|
||||
cap_padded_pos_ids = self.create_coordinate_grid(
|
||||
@@ -875,13 +902,17 @@ class ZImageTransformer2DModel(CachedTransformer):
|
||||
)
|
||||
)
|
||||
# padded feature
|
||||
cap_padded_feat = torch.cat(
|
||||
[cap_feat, cap_feat[-1:].repeat(cap_padding_len, 1)],
|
||||
dim=0,
|
||||
)
|
||||
all_cap_feats_out.append(cap_padded_feat)
|
||||
if cap_feat_2 is None:
|
||||
all_cap_feats_out.append(torch.cat([cap_feat, cap_feat[-1:].repeat(cap_padding_len, 1)], dim=0))
|
||||
else:
|
||||
all_cap_feats_out.append(cap_feat)
|
||||
all_cap_feats_2_out.append(
|
||||
torch.cat([cap_feat_2, torch.zeros_like(cap_feat_2[-1:]).repeat(cap_padding_len, 1)])
|
||||
)
|
||||
|
||||
### Process Image
|
||||
if image_ref is not None:
|
||||
image = torch.cat([image, image_ref], dim=1)
|
||||
C, F, H, W = image.size()
|
||||
all_image_size.append((F, H, W))
|
||||
F_tokens, H_tokens, W_tokens = F // pF, H // pH, W // pW
|
||||
@@ -931,6 +962,7 @@ class ZImageTransformer2DModel(CachedTransformer):
|
||||
all_cap_pos_ids,
|
||||
all_image_pad_mask,
|
||||
all_cap_pad_mask,
|
||||
all_cap_feats_2_out,
|
||||
)
|
||||
|
||||
def forward(
|
||||
@@ -940,6 +972,8 @@ class ZImageTransformer2DModel(CachedTransformer):
|
||||
cap_feats: list[torch.Tensor],
|
||||
patch_size=2,
|
||||
f_patch_size=1,
|
||||
ref_x: list[torch.Tensor | None] | None = None,
|
||||
cap_feats_2: list[torch.Tensor] | None = None,
|
||||
):
|
||||
assert patch_size in self.all_patch_size
|
||||
assert f_patch_size in self.all_f_patch_size
|
||||
@@ -957,7 +991,8 @@ class ZImageTransformer2DModel(CachedTransformer):
|
||||
cap_pos_ids,
|
||||
x_inner_pad_mask,
|
||||
cap_inner_pad_mask,
|
||||
) = self.patchify_and_embed(x, cap_feats, patch_size, f_patch_size)
|
||||
cap_feats_2,
|
||||
) = self.patchify_and_embed(x, cap_feats, patch_size, f_patch_size, ref_x, cap_feats_2)
|
||||
|
||||
# x embed & refine
|
||||
x_item_seqlens = [len(_) for _ in x]
|
||||
@@ -971,9 +1006,14 @@ class ZImageTransformer2DModel(CachedTransformer):
|
||||
adaln_input = t.type_as(x)
|
||||
# Use torch.where instead of x[mask]= to avoid aten::index_put_/nonzero and cudaStreamSynchronize
|
||||
x_pad_mask = torch.cat(x_inner_pad_mask)
|
||||
x_padding = (
|
||||
self.x_pad_token.expand(x.shape[0], -1)
|
||||
if self.alignment_padding_mode == LEARNED_PADDING
|
||||
else torch.zeros_like(x)
|
||||
)
|
||||
x = torch.where(
|
||||
x_pad_mask.unsqueeze(1).expand_as(x),
|
||||
self.x_pad_token.expand(x.shape[0], -1),
|
||||
x_padding,
|
||||
x,
|
||||
)
|
||||
x = list(x.split(x_item_seqlens, dim=0))
|
||||
@@ -987,22 +1027,36 @@ class ZImageTransformer2DModel(CachedTransformer):
|
||||
x_attn_mask = torch.zeros((bsz, x_max_item_seqlen), dtype=torch.bool, device=device)
|
||||
for i, seq_len in enumerate(x_item_seqlens):
|
||||
x_attn_mask[i, :seq_len] = 1
|
||||
if self.alignment_padding_mode == ZERO_MASKED_PADDING:
|
||||
x_attn_mask[i, :seq_len].masked_fill_(x_inner_pad_mask[i], False)
|
||||
|
||||
for layer in self.noise_refiner:
|
||||
x = layer(x, x_attn_mask, x_cos, x_sin, adaln_input)
|
||||
|
||||
# cap embed & refine
|
||||
cap_item_seqlens = [len(_) for _ in cap_feats]
|
||||
cap_feats = torch.cat(cap_feats, dim=0)
|
||||
cap_feats = self.cap_embedder(cap_feats)
|
||||
if cap_feats_2:
|
||||
cap_feats = list(cap_feats.split(cap_item_seqlens, dim=0))
|
||||
if len(cap_feats) != len(cap_feats_2):
|
||||
raise ValueError("Primary and direct caption conditions must have equal batch size.")
|
||||
cap_feats = [torch.cat([primary, direct], dim=0) for primary, direct in zip(cap_feats, cap_feats_2)]
|
||||
cap_item_seqlens = [len(item) for item in cap_feats]
|
||||
cap_feats = torch.cat(cap_feats, dim=0)
|
||||
assert all(_ % SEQ_MULTI_OF == 0 for _ in cap_item_seqlens)
|
||||
cap_max_item_seqlen = max(cap_item_seqlens)
|
||||
|
||||
cap_feats = torch.cat(cap_feats, dim=0)
|
||||
cap_feats = self.cap_embedder(cap_feats)
|
||||
# Use torch.where instead of cap_feats[mask]= to avoid aten::index_put_/nonzero and cudaStreamSynchronize
|
||||
cap_pad_mask = torch.cat(cap_inner_pad_mask)
|
||||
cap_padding = (
|
||||
self.cap_pad_token.expand(cap_feats.shape[0], -1)
|
||||
if self.alignment_padding_mode == LEARNED_PADDING
|
||||
else torch.zeros_like(cap_feats)
|
||||
)
|
||||
cap_feats = torch.where(
|
||||
cap_pad_mask.unsqueeze(1).expand_as(cap_feats),
|
||||
self.cap_pad_token.expand(cap_feats.shape[0], -1),
|
||||
cap_padding,
|
||||
cap_feats,
|
||||
)
|
||||
cap_feats = list(cap_feats.split(cap_item_seqlens, dim=0))
|
||||
@@ -1016,6 +1070,8 @@ class ZImageTransformer2DModel(CachedTransformer):
|
||||
cap_attn_mask = torch.zeros((bsz, cap_max_item_seqlen), dtype=torch.bool, device=device)
|
||||
for i, seq_len in enumerate(cap_item_seqlens):
|
||||
cap_attn_mask[i, :seq_len] = 1
|
||||
if self.alignment_padding_mode == ZERO_MASKED_PADDING:
|
||||
cap_attn_mask[i, :seq_len].masked_fill_(cap_inner_pad_mask[i], False)
|
||||
|
||||
for layer in self.context_refiner:
|
||||
cap_feats = layer(cap_feats, cap_attn_mask, cap_cos, cap_sin)
|
||||
@@ -1023,9 +1079,17 @@ class ZImageTransformer2DModel(CachedTransformer):
|
||||
# Prepare unified tensors via UnifiedPrepare module
|
||||
# This enables _cp_plan to shard outputs via split_output=True
|
||||
unified, unified_cos, unified_sin, unified_attn_mask = self.unified_prepare(
|
||||
x, x_cos, x_sin, cap_feats, cap_cos, cap_sin, x_item_seqlens, cap_item_seqlens
|
||||
x,
|
||||
x_cos,
|
||||
x_sin,
|
||||
cap_feats,
|
||||
cap_cos,
|
||||
cap_sin,
|
||||
x_item_seqlens,
|
||||
cap_item_seqlens,
|
||||
x_attn_mask,
|
||||
cap_attn_mask,
|
||||
)
|
||||
|
||||
# Main transformer blocks
|
||||
for layer in self.layers:
|
||||
unified = layer(unified, unified_attn_mask, unified_cos, unified_sin, adaln_input)
|
||||
|
||||
@@ -204,6 +204,11 @@ _DIFFUSION_MODELS = {
|
||||
"pipeline_ming_imagegen",
|
||||
"MingImagePipeline",
|
||||
),
|
||||
"MingImageDiffusionPipeline": (
|
||||
"ming_image",
|
||||
"pipeline",
|
||||
"MingImageDiffusionPipeline",
|
||||
),
|
||||
"SanaWmPipeline": (
|
||||
"sana_wm",
|
||||
"pipeline_sana_wm",
|
||||
@@ -390,6 +395,7 @@ _DIFFUSION_MODELS = {
|
||||
"Krea2Pipeline",
|
||||
),
|
||||
}
|
||||
_DIFFUSION_MODELS["MingImageLayeredDiffusionPipeline"] = _DIFFUSION_MODELS["MingImageDiffusionPipeline"]
|
||||
|
||||
|
||||
DiffusionModelRegistry = _ModelRegistry(
|
||||
@@ -663,6 +669,7 @@ _DIFFUSION_POST_PROCESS_FUNCS = {
|
||||
"BagelPipeline": "get_bagel_post_process_func",
|
||||
"LancePipeline": "get_lance_post_process_func",
|
||||
"MingImagePipeline": "get_ming_image_post_process_func",
|
||||
"MingImageDiffusionPipeline": "get_ming_image_post_process_func",
|
||||
"InternVLAA1Pipeline": "get_internvla_a1_post_process_func",
|
||||
"Pi0Pipeline": "get_pi0_post_process_func",
|
||||
"Pi05Pipeline": "get_pi05_post_process_func",
|
||||
@@ -696,6 +703,9 @@ _DIFFUSION_POST_PROCESS_FUNCS = {
|
||||
"Krea2Pipeline": "get_krea2_post_process_func",
|
||||
"HunyuanImage3ForCausalMM": "get_hunyuan_image3_post_process_func",
|
||||
}
|
||||
_DIFFUSION_POST_PROCESS_FUNCS["MingImageLayeredDiffusionPipeline"] = _DIFFUSION_POST_PROCESS_FUNCS[
|
||||
"MingImageDiffusionPipeline"
|
||||
]
|
||||
|
||||
_DIFFUSION_IR_OP_PRIORITY_FUNCS = {
|
||||
# arch: ir_op_priority_func
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
# Copyright 2025 The vLLM-Omni team.
|
||||
# Copyright 2023 Antgroup and The HuggingFace Inc. team. All rights reserved.
|
||||
# Adapted from Ming
|
||||
@@ -728,7 +729,8 @@ class BailingMoeV2Model(nn.Module):
|
||||
inputs_embeds: torch.Tensor | None = None,
|
||||
image_mask: torch.Tensor | None = None,
|
||||
audio_mask: torch.Tensor | None = None,
|
||||
) -> torch.Tensor | IntermediateTensors:
|
||||
capture_layers: tuple[int, ...] | None = None,
|
||||
) -> torch.Tensor | IntermediateTensors | tuple[torch.Tensor, dict[int, torch.Tensor]]:
|
||||
if get_pp_group().is_first_rank:
|
||||
if inputs_embeds is not None:
|
||||
hidden_states = inputs_embeds
|
||||
@@ -740,7 +742,14 @@ class BailingMoeV2Model(nn.Module):
|
||||
hidden_states = intermediate_tensors["hidden_states"]
|
||||
residual = intermediate_tensors["residual"]
|
||||
|
||||
for layer in self.layers[self.start_layer : self.end_layer]:
|
||||
capture_set = set(capture_layers or ())
|
||||
captured: dict[int, torch.Tensor] = {}
|
||||
for layer_idx, layer in enumerate(
|
||||
self.layers[self.start_layer : self.end_layer],
|
||||
start=self.start_layer,
|
||||
):
|
||||
if layer_idx in capture_set:
|
||||
captured[layer_idx] = hidden_states if residual is None else hidden_states + residual
|
||||
hidden_states, residual = layer(
|
||||
positions,
|
||||
hidden_states,
|
||||
@@ -753,6 +762,10 @@ class BailingMoeV2Model(nn.Module):
|
||||
return IntermediateTensors({"hidden_states": hidden_states, "residual": residual})
|
||||
|
||||
hidden_states, _ = self.norm(hidden_states, residual)
|
||||
if self.end_layer in capture_set:
|
||||
captured[self.end_layer] = hidden_states
|
||||
if capture_layers:
|
||||
return hidden_states, captured
|
||||
return hidden_states
|
||||
|
||||
|
||||
@@ -811,6 +824,7 @@ class BailingMoeV2ForCausalLM(nn.Module, CustomProcessMixin):
|
||||
inputs_embeds: torch.Tensor | None = None,
|
||||
image_mask: torch.Tensor | None = None,
|
||||
audio_mask: torch.Tensor | None = None,
|
||||
capture_layers: tuple[int, ...] | None = None,
|
||||
):
|
||||
hidden_states = self.model(
|
||||
input_ids=input_ids,
|
||||
@@ -819,6 +833,7 @@ class BailingMoeV2ForCausalLM(nn.Module, CustomProcessMixin):
|
||||
inputs_embeds=inputs_embeds,
|
||||
image_mask=image_mask,
|
||||
audio_mask=audio_mask,
|
||||
capture_layers=capture_layers,
|
||||
)
|
||||
return hidden_states
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
@@ -0,0 +1,45 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Checkpoint resolution for the Ming-Image MLLM stage."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from vllm_omni.model_executor.model_loader.weight_utils import (
|
||||
download_weights_from_hf_specific,
|
||||
)
|
||||
|
||||
_MLLM_REQUIRED_PATTERNS = [
|
||||
"mllm/**",
|
||||
"mlp/**",
|
||||
]
|
||||
|
||||
|
||||
def resolve_ming_image_model_root(
|
||||
model: str,
|
||||
revision: str | None,
|
||||
task_type: str | None,
|
||||
) -> str:
|
||||
"""Materialize the MLLM and its sibling query-token checkpoint.
|
||||
|
||||
The autoregressive weights and tokenizer live under mllm/,
|
||||
while the learned image-query tokens consumed by that stage live under mlp/.
|
||||
Stage subdirectory resolution runs after this hook, so return the repository
|
||||
root and let model_subdir/tokenizer_subdir select mllm/.
|
||||
"""
|
||||
del task_type
|
||||
path = Path(model)
|
||||
if path.is_dir():
|
||||
if path.name == "mllm" and (path / "config.json").is_file():
|
||||
return str(path.parent)
|
||||
return str(path)
|
||||
|
||||
return download_weights_from_hf_specific(
|
||||
model_name_or_path=model,
|
||||
cache_dir=None,
|
||||
allow_patterns=_MLLM_REQUIRED_PATTERNS,
|
||||
revision=revision,
|
||||
require_all=True,
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["resolve_ming_image_model_root"]
|
||||
@@ -0,0 +1,223 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""vLLM-native MLLM stage for inclusionAI Ming-Image."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable, Mapping
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from vllm.config import VllmConfig
|
||||
from vllm.inputs import MultiModalDataDict
|
||||
from vllm.model_executor.models.qwen2_5_vl import Qwen2_5_VisionTransformer
|
||||
from vllm.model_executor.models.utils import WeightsMapper, maybe_prefix
|
||||
from vllm.multimodal import MULTIMODAL_REGISTRY
|
||||
from vllm.multimodal.parse import MultiModalDataParser
|
||||
|
||||
from vllm_omni.model_executor.models.ming_flash_omni.ming_flash_omni_thinker import (
|
||||
MingFlashOmniThinkerDummyInputsBuilder,
|
||||
MingFlashOmniThinkerForConditionalGeneration,
|
||||
MingFlashOmniThinkerMultiModalProcessor,
|
||||
MingFlashOmniThinkerProcessingInfo,
|
||||
)
|
||||
from vllm_omni.model_executor.models.ming_flash_omni.modeling_bailing_moe_v2 import (
|
||||
BailingMoeV2ForCausalLM,
|
||||
)
|
||||
from vllm_omni.model_executor.models.ming_flash_omni.projectors import VisionProjector
|
||||
from vllm_omni.model_executor.models.output_templates import OmniOutput
|
||||
from vllm_omni.transformers_utils.configs.ming_flash_omni import BailingMM2Config
|
||||
from vllm_omni.transformers_utils.processors.ming import MingImageProcessor
|
||||
|
||||
|
||||
class _MingImageDataParser(MultiModalDataParser):
|
||||
def parse_mm_data(self, mm_data):
|
||||
normalized = dict(mm_data)
|
||||
if "img2img" in normalized:
|
||||
references = normalized.pop("img2img")
|
||||
references = references if isinstance(references, list) else [references]
|
||||
existing = normalized.get("image", [])
|
||||
existing = existing if isinstance(existing, list) else [existing]
|
||||
normalized["image"] = existing + references
|
||||
return super().parse_mm_data(normalized)
|
||||
|
||||
|
||||
class MingImageProcessingInfo(MingFlashOmniThinkerProcessingInfo):
|
||||
def get_hf_config(self) -> BailingMM2Config:
|
||||
return self.ctx.get_hf_config(BailingMM2Config)
|
||||
|
||||
def get_hf_processor(self, **kwargs: object):
|
||||
return self.ctx.get_hf_processor(MingImageProcessor, **kwargs)
|
||||
|
||||
def get_supported_mm_limits(self) -> Mapping[str, int | None]:
|
||||
return {"image": None, "img2img": 1}
|
||||
|
||||
def get_mm_max_tokens_per_item(self, seq_len, mm_counts):
|
||||
counts = dict(mm_counts or {})
|
||||
counts["image"] = counts.get("image", 0) + counts.pop("img2img", 0)
|
||||
return super(MingFlashOmniThinkerProcessingInfo, self).get_mm_max_tokens_per_item(
|
||||
seq_len=seq_len,
|
||||
mm_counts=counts,
|
||||
)
|
||||
|
||||
def get_data_parser(self):
|
||||
return _MingImageDataParser()
|
||||
|
||||
|
||||
class MingImageDummyInputsBuilder(MingFlashOmniThinkerDummyInputsBuilder):
|
||||
def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
|
||||
count = mm_counts.get("image", 0) + mm_counts.get("img2img", 0)
|
||||
return self.info.get_hf_processor().image_token * count
|
||||
|
||||
def get_dummy_mm_data(
|
||||
self,
|
||||
seq_len: int,
|
||||
mm_counts: Mapping[str, int],
|
||||
mm_options=None,
|
||||
) -> MultiModalDataDict:
|
||||
count = mm_counts.get("image", 0) + mm_counts.get("img2img", 0)
|
||||
return {
|
||||
"image": self._get_dummy_images(
|
||||
width=448,
|
||||
height=448,
|
||||
num_images=count,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
class MingImageMultiModalProcessor(MingFlashOmniThinkerMultiModalProcessor):
|
||||
pass
|
||||
|
||||
|
||||
@MULTIMODAL_REGISTRY.register_processor(
|
||||
MingImageMultiModalProcessor,
|
||||
info=MingImageProcessingInfo,
|
||||
dummy_inputs=MingImageDummyInputsBuilder,
|
||||
)
|
||||
class MingImageForConditionalGeneration(MingFlashOmniThinkerForConditionalGeneration):
|
||||
"""Ming-Image MLLM stage with Qwen2.5-VL and direct-state capture."""
|
||||
|
||||
hf_to_vllm_mapper = WeightsMapper(
|
||||
orig_to_new_prefix={
|
||||
"model.": "language_model.",
|
||||
"linear_proj.": "linear_proj.proj.",
|
||||
},
|
||||
)
|
||||
|
||||
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
|
||||
torch.nn.Module.__init__(self)
|
||||
thinker_config = vllm_config.model_config.hf_config
|
||||
if not isinstance(thinker_config, BailingMM2Config):
|
||||
raise TypeError(f"Expected BailingMM2Config, got {type(thinker_config).__name__}")
|
||||
if thinker_config.llm_config is None or thinker_config.vision_config is None:
|
||||
raise ValueError("Ming-Image requires llm_config and vision_config.")
|
||||
|
||||
llm_config = thinker_config.llm_config
|
||||
self.config = llm_config
|
||||
self.thinker_config = thinker_config
|
||||
self.have_multimodal_outputs = True
|
||||
|
||||
with self._mark_language_model(vllm_config):
|
||||
self.language_model = BailingMoeV2ForCausalLM(
|
||||
vllm_config=vllm_config.with_hf_config(llm_config),
|
||||
prefix=maybe_prefix(prefix, "llm"),
|
||||
)
|
||||
|
||||
with self._mark_tower_model(vllm_config, "image"):
|
||||
self.vision = Qwen2_5_VisionTransformer(
|
||||
thinker_config.vision_config,
|
||||
norm_eps=llm_config.rms_norm_eps,
|
||||
quant_config=vllm_config.quant_config,
|
||||
prefix=maybe_prefix(prefix, "vision"),
|
||||
)
|
||||
self.linear_proj = VisionProjector(
|
||||
vision_dim=thinker_config.vision_config.out_hidden_size,
|
||||
llm_dim=llm_config.hidden_size,
|
||||
mlp_depth=getattr(thinker_config, "mlp_depth", 2),
|
||||
)
|
||||
|
||||
self.audio = None
|
||||
self.linear_proj_audio = None
|
||||
self.query_tokens_dict = torch.nn.ParameterDict()
|
||||
component_path = Path(vllm_config.model_config.model)
|
||||
model_root = component_path.parent if component_path.name == "mllm" else component_path
|
||||
self._load_image_gen_query_tokens(str(model_root))
|
||||
if not self.query_tokens_dict:
|
||||
raise ValueError(f"Ming-Image query tokens were not found under {model_root / 'mlp'}")
|
||||
|
||||
self.capture_layers = (5, 12, llm_config.num_hidden_layers)
|
||||
self.make_empty_intermediate_tensors = self.language_model.make_empty_intermediate_tensors
|
||||
|
||||
def extract_image_feature(self, pixel_values: torch.Tensor, grid_thw: torch.Tensor) -> torch.Tensor:
|
||||
with torch.amp.autocast(pixel_values.device.type, dtype=torch.bfloat16):
|
||||
image_embeds = self.vision(pixel_values, grid_thw=grid_thw.tolist())
|
||||
return F.normalize(self.linear_proj(image_embeds), dim=-1)
|
||||
|
||||
def compute_logits(
|
||||
self,
|
||||
hidden_states: torch.Tensor,
|
||||
sampling_metadata=None,
|
||||
) -> torch.Tensor | None:
|
||||
# vLLM v1 calls this with hidden states only; the reused Bailing model
|
||||
# still accepts the legacy metadata argument (which it does not use).
|
||||
return self.language_model.compute_logits(hidden_states, sampling_metadata)
|
||||
|
||||
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
||||
loaded_weights = super().load_weights(weights)
|
||||
# Query tokens live in the root-level mlp checkpoint and are loaded by
|
||||
# ``_load_image_gen_query_tokens`` during construction, outside vLLM's
|
||||
# main checkpoint iterator. Include them in the returned set so strict
|
||||
# post-load validation accounts for that checkpoint-owned component.
|
||||
loaded_weights.update(f"query_tokens_dict.{scale}" for scale in self.query_tokens_dict)
|
||||
return loaded_weights
|
||||
|
||||
def _compute_modality_masks(
|
||||
self,
|
||||
input_ids: torch.Tensor | None,
|
||||
inputs_embeds: torch.Tensor | None = None,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
if input_ids is None:
|
||||
if inputs_embeds is None:
|
||||
raise ValueError("input_ids and inputs_embeds cannot both be None")
|
||||
empty_mask = torch.zeros(
|
||||
inputs_embeds.shape[:-1],
|
||||
device=inputs_embeds.device,
|
||||
dtype=torch.bool,
|
||||
)
|
||||
return empty_mask, empty_mask
|
||||
return input_ids == self.config.image_patch_token, torch.zeros_like(input_ids, dtype=torch.bool)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
input_ids: torch.Tensor,
|
||||
positions: torch.Tensor,
|
||||
intermediate_tensors=None,
|
||||
inputs_embeds: torch.Tensor | None = None,
|
||||
**kwargs,
|
||||
) -> OmniOutput:
|
||||
image_mask, audio_mask = self._compute_modality_masks(input_ids, inputs_embeds)
|
||||
result = self.language_model.forward(
|
||||
input_ids=input_ids,
|
||||
positions=positions,
|
||||
intermediate_tensors=intermediate_tensors,
|
||||
inputs_embeds=inputs_embeds,
|
||||
image_mask=image_mask,
|
||||
audio_mask=audio_mask,
|
||||
capture_layers=self.capture_layers,
|
||||
)
|
||||
if not isinstance(result, tuple):
|
||||
return OmniOutput(text_hidden_states=result, multimodal_outputs={"final_hidden_states": result})
|
||||
|
||||
hidden_states, captured = result
|
||||
multimodal_outputs: dict[str, Any] = {"final_hidden_states": hidden_states}
|
||||
for layer_idx, layer_hidden in captured.items():
|
||||
multimodal_outputs[f"hidden_states_{layer_idx}"] = layer_hidden
|
||||
return OmniOutput(
|
||||
text_hidden_states=hidden_states,
|
||||
multimodal_outputs=multimodal_outputs,
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["MingImageForConditionalGeneration"]
|
||||
@@ -0,0 +1,46 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Two-stage Ming-Image topology."""
|
||||
|
||||
from vllm_omni.config.stage_config import PipelineConfig, StageExecutionType, StagePipelineConfig
|
||||
|
||||
_PROC = "vllm_omni.model_executor.stage_input_processors.ming_image"
|
||||
_CHECKPOINT = "vllm_omni.model_executor.models.ming_image.checkpoint"
|
||||
|
||||
MING_IMAGE_PIPELINE = PipelineConfig(
|
||||
model_type="ming_image",
|
||||
default_deploy_config_name="ming_image.yaml",
|
||||
model_arch="MingImageForConditionalGeneration",
|
||||
hf_architectures=("MingImageForConditionalGeneration",),
|
||||
diffusers_class_name="MingImageDiffusionPipeline",
|
||||
diffusers_class_aliases=("MingImageLayeredDiffusionPipeline",),
|
||||
stages=(
|
||||
StagePipelineConfig(
|
||||
stage_id=0,
|
||||
model_stage="mllm",
|
||||
model_arch="MingImageForConditionalGeneration",
|
||||
execution_type=StageExecutionType.LLM_AR,
|
||||
input_sources=(),
|
||||
final_output=False,
|
||||
owns_tokenizer=True,
|
||||
requires_multimodal_data=True,
|
||||
engine_output_type="latent",
|
||||
model_subdir="mllm",
|
||||
tokenizer_subdir="mllm",
|
||||
sampling_constraints={"detokenize": False},
|
||||
model_path_resolver=f"{_CHECKPOINT}.resolve_ming_image_model_root",
|
||||
),
|
||||
StagePipelineConfig(
|
||||
stage_id=1,
|
||||
model_stage="dit",
|
||||
model_arch="MingImageDiffusionPipeline",
|
||||
execution_type=StageExecutionType.DIFFUSION,
|
||||
input_sources=(0,),
|
||||
final_output=True,
|
||||
final_output_type="image",
|
||||
custom_process_input_func=f"{_PROC}.thinker2image",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
__all__ = ["MING_IMAGE_PIPELINE"]
|
||||
@@ -425,6 +425,11 @@ _OMNI_MODELS = {
|
||||
"ming_flash_omni_thinker",
|
||||
"MingFlashOmniThinkerForConditionalGeneration",
|
||||
),
|
||||
"MingImageForConditionalGeneration": (
|
||||
"ming_image",
|
||||
"model",
|
||||
"MingImageForConditionalGeneration",
|
||||
),
|
||||
"MingFlashOmniTalkerForConditionalGeneration": (
|
||||
"ming_flash_omni",
|
||||
"ming_flash_omni_talker",
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Strict MLLM-to-diffusion bridge for Ming-Image."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
|
||||
from vllm_omni.model_executor.stage_input_processors.ming_flash_omni import _ensure_list
|
||||
|
||||
_IMAGE_PATCH_TOKEN_ID = 157157
|
||||
_IMAGE_END_TOKEN_ID = 157159
|
||||
_NUM_QUERY_TOKENS = 256
|
||||
_CAPTURE_LAYERS = (5, 12, 20)
|
||||
|
||||
|
||||
def _output_payload(source_output: Any) -> tuple[list[int], dict[str, torch.Tensor]]:
|
||||
if not getattr(source_output, "outputs", None):
|
||||
raise ValueError("Ming-Image thinker produced no outputs.")
|
||||
output = source_output.outputs[0]
|
||||
multimodal = getattr(output, "multimodal_output", None) or {}
|
||||
return _ensure_list(source_output.prompt_token_ids), multimodal
|
||||
|
||||
|
||||
def _validate_query_suffix(prompt_ids: list[int]) -> tuple[int, int]:
|
||||
expected = _NUM_QUERY_TOKENS + 2
|
||||
if len(prompt_ids) < expected:
|
||||
raise ValueError("Ming-Image prompt is shorter than its image-query suffix.")
|
||||
start = len(prompt_ids) - _NUM_QUERY_TOKENS - 1
|
||||
end = len(prompt_ids) - 1
|
||||
if prompt_ids[end] != _IMAGE_END_TOKEN_ID or any(token != _IMAGE_PATCH_TOKEN_ID for token in prompt_ids[start:end]):
|
||||
raise ValueError("Ming-Image query suffix must end with 256 image-patch tokens and an image-end token.")
|
||||
return start, end
|
||||
|
||||
|
||||
def _as_sequence(hidden: torch.Tensor, prompt_length: int, name: str) -> torch.Tensor:
|
||||
if hidden.ndim == 3:
|
||||
if hidden.shape[0] != 1:
|
||||
raise ValueError(f"{name} must have batch size 1, got {tuple(hidden.shape)}")
|
||||
hidden = hidden[0]
|
||||
if hidden.ndim != 2 or hidden.shape[0] != prompt_length:
|
||||
raise ValueError(f"{name} shape {tuple(hidden.shape)} does not match prompt length {prompt_length}.")
|
||||
return hidden
|
||||
|
||||
|
||||
def _extract_conditions(source_output: Any) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
prompt_ids, multimodal = _output_payload(source_output)
|
||||
query_start, query_end = _validate_query_suffix(prompt_ids)
|
||||
final_hidden = multimodal.get("final_hidden_states")
|
||||
if not isinstance(final_hidden, torch.Tensor):
|
||||
raise ValueError("Ming-Image thinker output is missing final_hidden_states.")
|
||||
final_hidden = _as_sequence(final_hidden, len(prompt_ids), "final_hidden_states")
|
||||
query_hidden = final_hidden[query_start:query_end].detach().contiguous()
|
||||
|
||||
selected: list[torch.Tensor] = []
|
||||
for layer_idx in _CAPTURE_LAYERS:
|
||||
value = multimodal.get(f"hidden_states_{layer_idx}")
|
||||
if not isinstance(value, torch.Tensor):
|
||||
raise ValueError(f"Ming-Image thinker output is missing hidden_states_{layer_idx}.")
|
||||
selected.append(_as_sequence(value, len(prompt_ids), f"hidden_states_{layer_idx}"))
|
||||
|
||||
# Vendor labels mark the full generated query block, including delimiters.
|
||||
# Direct VLM conditioning is restricted to the original prompt/reference
|
||||
# prefix and therefore excludes all 258 appended tokens.
|
||||
direct_end = len(prompt_ids) - (_NUM_QUERY_TOKENS + 2)
|
||||
direct_hidden = torch.cat([value[:direct_end] for value in selected], dim=-1)
|
||||
return query_hidden, direct_hidden.detach().contiguous()
|
||||
|
||||
|
||||
def thinker2image(
|
||||
source_outputs: list[Any],
|
||||
prompt: Any | None = None,
|
||||
requires_multimodal_data: bool = False,
|
||||
sampling_params: Any | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
del requires_multimodal_data
|
||||
if len(source_outputs) != 1:
|
||||
raise ValueError(f"Ming-Image expects one thinker output, got {len(source_outputs)}.")
|
||||
|
||||
extra_args = getattr(sampling_params, "extra_args", None) or {}
|
||||
negative_prompt = extra_args.get("negative_prompt")
|
||||
if isinstance(prompt, dict):
|
||||
negative_prompt = prompt.get("negative_prompt", negative_prompt)
|
||||
if isinstance(negative_prompt, str) and negative_prompt.strip():
|
||||
raise ValueError("Ming-Image uses zero negative conditioning and does not accept negative_prompt.")
|
||||
|
||||
query_hidden, direct_hidden = _extract_conditions(source_outputs[0])
|
||||
extra: dict[str, Any] = {
|
||||
"query_hidden_states": query_hidden,
|
||||
"direct_hidden_states": direct_hidden,
|
||||
}
|
||||
|
||||
if isinstance(prompt, dict):
|
||||
mm_data = prompt.get("multi_modal_data") or {}
|
||||
reference = mm_data.get("img2img", mm_data.get("image"))
|
||||
if isinstance(reference, list):
|
||||
reference = reference[0] if reference else None
|
||||
if reference is not None:
|
||||
extra["reference_image"] = reference
|
||||
|
||||
num_layers = int(extra_args.get("num_layers", 1))
|
||||
if num_layers < 1:
|
||||
raise ValueError("num_layers must be at least 1.")
|
||||
extra["num_layers"] = num_layers
|
||||
return [{"prompt": "", "extra": extra}]
|
||||
|
||||
|
||||
__all__ = ["thinker2image"]
|
||||
@@ -0,0 +1,29 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
from vllm_omni.model_extras.ming_flash_omni import (
|
||||
build_image_to_image_prompt,
|
||||
build_text_to_image_prompt,
|
||||
)
|
||||
|
||||
MING_IMAGE_EXTRA_BODY_PARAMS = frozenset(
|
||||
{
|
||||
"height",
|
||||
"width",
|
||||
"steps",
|
||||
"cfg",
|
||||
"seed",
|
||||
"num_layers",
|
||||
"negative_prompt",
|
||||
}
|
||||
)
|
||||
MING_IMAGE_EXTRA_OUTPUT_PARAMS: frozenset[str] = frozenset()
|
||||
MING_IMAGE_INIT_EXTRA_ARGS_FOR_NON_DIFFUSION_STAGES = True
|
||||
|
||||
__all__ = [
|
||||
"MING_IMAGE_EXTRA_BODY_PARAMS",
|
||||
"MING_IMAGE_EXTRA_OUTPUT_PARAMS",
|
||||
"MING_IMAGE_INIT_EXTRA_ARGS_FOR_NON_DIFFUSION_STAGES",
|
||||
"build_image_to_image_prompt",
|
||||
"build_text_to_image_prompt",
|
||||
]
|
||||
@@ -75,6 +75,17 @@ from vllm_omni.model_extras.ming_flash_omni import (
|
||||
from vllm_omni.model_extras.ming_flash_omni import (
|
||||
build_text_to_image_prompt as build_ming_flash_omni_text_to_image_prompt,
|
||||
)
|
||||
from vllm_omni.model_extras.ming_image import (
|
||||
MING_IMAGE_EXTRA_BODY_PARAMS,
|
||||
MING_IMAGE_EXTRA_OUTPUT_PARAMS,
|
||||
MING_IMAGE_INIT_EXTRA_ARGS_FOR_NON_DIFFUSION_STAGES,
|
||||
)
|
||||
from vllm_omni.model_extras.ming_image import (
|
||||
build_image_to_image_prompt as build_ming_image_image_to_image_prompt,
|
||||
)
|
||||
from vllm_omni.model_extras.ming_image import (
|
||||
build_text_to_image_prompt as build_ming_image_text_to_image_prompt,
|
||||
)
|
||||
from vllm_omni.model_extras.sana_video import SANA_VIDEO_EXTRA_BODY_PARAMS
|
||||
from vllm_omni.model_extras.sensenova_u1 import (
|
||||
SENSENOVA_U1_EXTRA_BODY_PARAMS,
|
||||
@@ -299,7 +310,15 @@ _EXTRA_SPECS: dict[str, dict[str, Any]] = {
|
||||
"text_to_image_prompt_builder": build_ming_flash_omni_text_to_image_prompt,
|
||||
"image_to_image_prompt_builder": build_ming_flash_omni_image_to_image_prompt,
|
||||
},
|
||||
"MingImageDiffusionPipeline": {
|
||||
"extra_body_params": MING_IMAGE_EXTRA_BODY_PARAMS,
|
||||
"extra_output_params": MING_IMAGE_EXTRA_OUTPUT_PARAMS,
|
||||
"init_extra_args_for_non_diffusion_stages": MING_IMAGE_INIT_EXTRA_ARGS_FOR_NON_DIFFUSION_STAGES,
|
||||
"text_to_image_prompt_builder": build_ming_image_text_to_image_prompt,
|
||||
"image_to_image_prompt_builder": build_ming_image_image_to_image_prompt,
|
||||
},
|
||||
}
|
||||
_EXTRA_SPECS["MingImageLayeredDiffusionPipeline"] = _EXTRA_SPECS["MingImageDiffusionPipeline"]
|
||||
|
||||
for model_class_name in ("LTX2Pipeline", "LTX2TwoStagePipeline"):
|
||||
_EXTRA_SPECS[model_class_name]["transformer_config_subfolder_resolver"] = ltx_transformer_config_subfolder
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
# Copyright 2025 The vLLM-Omni team.
|
||||
# Copyright 2024 ANT Group and the HuggingFace Inc. team. All rights reserved.
|
||||
#
|
||||
@@ -19,7 +20,13 @@
|
||||
import os
|
||||
from typing import Any, ClassVar
|
||||
|
||||
from transformers import AutoConfig, AutoTokenizer, PretrainedConfig, PreTrainedTokenizerFast
|
||||
from transformers import (
|
||||
AutoConfig,
|
||||
AutoTokenizer,
|
||||
PretrainedConfig,
|
||||
PreTrainedTokenizerFast,
|
||||
Qwen2_5_VLVisionConfig,
|
||||
)
|
||||
from transformers.utils import logging
|
||||
|
||||
logger = logging.get_logger(__name__)
|
||||
@@ -251,9 +258,19 @@ class BailingMM2Config(PretrainedConfig):
|
||||
**kwargs,
|
||||
):
|
||||
self.audio_config = WhisperEncoderConfig(**audio_config) if isinstance(audio_config, dict) else audio_config
|
||||
self.vision_config = (
|
||||
Qwen3VLMoeVisionConfig(**vision_config) if isinstance(vision_config, dict) else vision_config
|
||||
)
|
||||
if isinstance(vision_config, dict):
|
||||
vision_config = dict(vision_config)
|
||||
vision_model_type = vision_config.get("model_type")
|
||||
if vision_model_type in {"qwen2_5_vit", "qwen2_5_vl"}:
|
||||
# 1) Ming-Image uses the Qwen2.5-VL vision tower while
|
||||
# 2) Ming-flash-omni-2.0 uses Qwen3-VL. Keep the composition config shared
|
||||
# and select the concrete tower from ckpt metadata.
|
||||
vision_config.pop("model_type", None)
|
||||
self.vision_config = Qwen2_5_VLVisionConfig(**vision_config)
|
||||
else:
|
||||
self.vision_config = Qwen3VLMoeVisionConfig(**vision_config)
|
||||
else:
|
||||
self.vision_config = vision_config
|
||||
self.llm_config = BailingMoeV2Config(**llm_config) if isinstance(llm_config, dict) else llm_config
|
||||
self.mlp_depth = mlp_depth
|
||||
super().__init__(**kwargs)
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
# Copyright 2025 The vLLM-Omni team.
|
||||
|
||||
from vllm_omni.transformers_utils.processors.ming import (
|
||||
MingFlashOmniProcessor,
|
||||
MingImageProcessor,
|
||||
MingWhisperFeatureExtractor,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"MingFlashOmniProcessor",
|
||||
"MingImageProcessor",
|
||||
"MingWhisperFeatureExtractor",
|
||||
]
|
||||
@@ -1,4 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
# Copyright 2025 The vLLM-Omni team.
|
||||
# Copyright 2024 ANT Group and the HuggingFace Inc. team.
|
||||
#
|
||||
@@ -18,7 +19,7 @@ from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from transformers import AutoFeatureExtractor, AutoProcessor
|
||||
from transformers import AutoFeatureExtractor, AutoProcessor, AutoTokenizer, Qwen2VLImageProcessor
|
||||
from transformers.feature_extraction_utils import BatchFeature, FeatureExtractionMixin
|
||||
from transformers.processing_utils import ProcessorMixin
|
||||
from transformers.tokenization_utils_base import PreTokenizedInput, TextInput
|
||||
@@ -481,6 +482,87 @@ class MingFlashOmniProcessor(ProcessorMixin):
|
||||
return list(dict.fromkeys(names))
|
||||
|
||||
|
||||
class MingImageProcessor(ProcessorMixin):
|
||||
"""Image-only Bailing processor used by Ming-Image ckpts.
|
||||
|
||||
Equivalent to Qwen2-VL preprocessing. Construct the upstream component explicitly
|
||||
in case model repo auto_map points at files that are not bundled in the ckpt.
|
||||
"""
|
||||
|
||||
attributes = ["image_processor", "tokenizer"]
|
||||
image_processor_class = "Qwen2VLImageProcessor"
|
||||
tokenizer_class = "AutoTokenizer"
|
||||
|
||||
def __init__(self, image_processor=None, tokenizer=None, merge_size: int = 2, **kwargs):
|
||||
if image_processor is None or tokenizer is None:
|
||||
raise ValueError("MingImageProcessor requires image_processor and tokenizer.")
|
||||
self.spatial_merge_size = int(getattr(image_processor, "merge_size", merge_size))
|
||||
self.image_token = PLACEHOLDER_IMAGE_TOKEN_IN_TEXT
|
||||
super().__init__(image_processor=image_processor, tokenizer=tokenizer, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def from_pretrained(cls, pretrained_model_name_or_path, *args, **kwargs):
|
||||
image_processor = Qwen2VLImageProcessor.from_pretrained(
|
||||
pretrained_model_name_or_path,
|
||||
*args,
|
||||
**kwargs,
|
||||
)
|
||||
tokenizer = AutoTokenizer.from_pretrained(
|
||||
pretrained_model_name_or_path,
|
||||
*args,
|
||||
**kwargs,
|
||||
)
|
||||
return cls(image_processor=image_processor, tokenizer=tokenizer)
|
||||
|
||||
def __call__(self, text, images=None, **kwargs) -> BatchFeature:
|
||||
if isinstance(text, str):
|
||||
text = [text]
|
||||
data: dict[str, Any] = {}
|
||||
if images is not None:
|
||||
image_outputs = self.image_processor(
|
||||
images=images,
|
||||
return_tensors="pt",
|
||||
**kwargs.get("images_kwargs", {}),
|
||||
)
|
||||
data.update(image_outputs)
|
||||
text = self._expand_image_tokens(text, image_outputs["image_grid_thw"])
|
||||
data.update(
|
||||
self.tokenizer(
|
||||
text,
|
||||
return_tensors="pt",
|
||||
**kwargs.get("text_kwargs", {}),
|
||||
)
|
||||
)
|
||||
return BatchFeature(data=data)
|
||||
|
||||
def _expand_image_tokens(self, text: list[str], image_grid_thw: torch.Tensor) -> list[str]:
|
||||
num_patches = image_grid_thw.prod(dim=1) // (self.spatial_merge_size**2)
|
||||
image_index = 0
|
||||
expanded: list[str] = []
|
||||
for sample in text:
|
||||
for _ in range(sample.count(self.image_token)):
|
||||
replacement = (
|
||||
DEFAULT_IM_START_TOKEN
|
||||
+ DEFAULT_IMAGE_PATCH_TOKEN * int(num_patches[image_index].item())
|
||||
+ DEFAULT_IM_END_TOKEN
|
||||
+ "\n"
|
||||
)
|
||||
sample = sample.replace(self.image_token, replacement, 1)
|
||||
image_index += 1
|
||||
expanded.append(sample)
|
||||
return expanded
|
||||
|
||||
def batch_decode(self, *args, **kwargs):
|
||||
return self.tokenizer.batch_decode(*args, **kwargs)
|
||||
|
||||
def decode(self, *args, **kwargs):
|
||||
return self.tokenizer.decode(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def model_input_names(self):
|
||||
return list(dict.fromkeys(self.tokenizer.model_input_names + self.image_processor.model_input_names))
|
||||
|
||||
|
||||
# transformers >= 5.x requires the config *class* (not a string) as the first
|
||||
# arg to AutoFeatureExtractor/AutoProcessor.register — it does `key.__module__`
|
||||
# internally. Passing "MingWhisperFeatureExtractor" raises:
|
||||
|
||||
Reference in new issue
Block a user