Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/models/supported_models.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ th {
| `Qwen3OmniMoeForConditionalGeneration` | Qwen3-Omni | `Qwen/Qwen3-Omni-30B-A3B-Instruct` | ✅︎ | ✅︎ | ✅︎ | ✅︎ |
| `Qwen2_5OmniForConditionalGeneration` | Qwen2.5-Omni | `Qwen/Qwen2.5-Omni-7B`, `Qwen/Qwen2.5-Omni-3B` | ✅︎ | ✅︎ | ✅︎ | ✅︎ |
| `BagelForConditionalGeneration` | BAGEL (DiT-only) | `ByteDance-Seed/BAGEL-7B-MoT` | ✅︎ | ✅︎ | | ✅︎ |
| `TunaExternalPipeline` | Tuna/Tuna-2 (recognized only; runtime integration not yet available) | [`facebookresearch/tuna-2`](https://github.com/facebookresearch/tuna-2) | | | | |
| `InternVLAA1Pipeline` | InternVLA-A1 | `InternRobotics/InternVLA-A1-3B` | ✅︎ | ✅︎ | | |
| `HunyuanImage3ForCausalMM` | HunyuanImage3.0 (DiT-only) | `tencent/HunyuanImage-3.0`, `tencent/HunyuanImage-3.0-Instruct` | ✅︎ | ✅︎ | ✅︎ | ✅︎ |
| `QwenImagePipeline` | Qwen-Image | `Qwen/Qwen-Image` | ✅︎ | ✅︎ | ✅︎ | ✅︎ |
Expand Down Expand Up @@ -68,3 +69,5 @@ th {
|`DyninOmniForConditionalGeneration` | Dynin-Omni | `snu-aidas/Dynin-Omni` | ✅︎ | | | |

✅︎ indicates the model is supported on that backend. Empty cells mean not listed as supported on that backend.

Tuna/Tuna-2 metadata is recognized by vLLM-Omni so users get an explicit integration-status error instead of an unknown-model failure. Runtime inference is not available yet because the upstream [`facebookresearch/tuna-2`](https://github.com/facebookresearch/tuna-2) project currently uses its own Hydra-based inference flow and `.pt` checkpoint format without a stable HuggingFace/diffusers loading contract.
65 changes: 65 additions & 0 deletions tests/diffusion/models/test_tuna_detection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Tests for Tuna/Tuna-2 model recognition scaffolding."""

from types import SimpleNamespace

import pytest

from vllm_omni.diffusion.data import OmniDiffusionConfig
from vllm_omni.diffusion.registry import DiffusionModelRegistry
from vllm_omni.diffusion.utils import hf_utils

pytestmark = [pytest.mark.core_model, pytest.mark.cpu]


def test_tuna_external_pipeline_registered():
cls = DiffusionModelRegistry._try_load_model_cls("TunaExternalPipeline")
assert cls is not None


def test_tuna_config_enriches_to_external_pipeline(monkeypatch):
def fake_get_hf_file_to_dict(filename, model, revision=None):
if filename == "model_index.json":
return None
if filename == "config.json":
return {"model_type": "tuna_2_pixel"}
return None

monkeypatch.setattr("vllm.transformers_utils.config.get_hf_file_to_dict", fake_get_hf_file_to_dict)

od_config = OmniDiffusionConfig(model="dummy-tuna")
od_config.enrich_config()

assert od_config.model_class_name == "TunaExternalPipeline"


def test_is_diffusion_model_detects_tuna_config(monkeypatch):
monkeypatch.setattr("os.path.isdir", lambda _model: False)

def fake_get_hf_file_to_dict(filename, model_name):
if filename == "model_index.json":
return None
if filename == "config.json":
return {"architectures": ["Tuna2PixelModel"]}
return None

monkeypatch.setattr(hf_utils, "get_hf_file_to_dict", fake_get_hf_file_to_dict)
monkeypatch.setattr(hf_utils, "load_diffusers_config", lambda _model: (_ for _ in ()).throw(ValueError("nope")))
hf_utils.is_diffusion_model.cache_clear()

assert hf_utils.is_diffusion_model("dummy-tuna") is True


def test_tuna_external_pipeline_error_is_actionable():
cls = DiffusionModelRegistry._try_load_model_cls("TunaExternalPipeline")
od_config = SimpleNamespace()

with pytest.raises(RuntimeError) as exc_info:
cls(od_config=od_config)

message = str(exc_info.value)
assert "Tuna/Tuna-2 is recognized" in message
assert "runtime integration is not available yet" in message
assert "#3303" in message
assert "https://github.com/facebookresearch/tuna-2" in message
33 changes: 33 additions & 0 deletions tests/entrypoints/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,39 @@ def mock_exists(path):
assert result is not None
assert "voxcpm.yaml" in result

def test_tuna_transformers_format_resolution(self, mocker: MockerFixture):
"""Test Tuna-2 model_type aliases resolve to the tuna stage config."""
mocker.patch(
"vllm_omni.entrypoints.utils.get_config",
side_effect=ValueError("missing transformers config"),
)
mocker.patch(
"vllm_omni.entrypoints.utils.file_or_path_exists",
side_effect=lambda _model, filename, revision=None: filename == "config.json",
)
mocker.patch(
"vllm_omni.entrypoints.utils.get_hf_file_to_dict",
return_value={"model_type": "tuna_2_pixel"},
)
mocker.patch(
"vllm_omni.entrypoints.utils.current_omni_platform.get_default_stage_config_path",
return_value="vllm_omni/model_executor/stage_configs",
)

original_exists = os.path.exists

def mock_exists(path):
if "tuna.yaml" in str(path):
return True
return original_exists(path)

mocker.patch("os.path.exists", side_effect=mock_exists)

result = resolve_model_config_path("facebookresearch/tuna-2")

assert result is not None
assert "tuna.yaml" in result


class TestLoadAndResolveStageConfigs:
def test_load_and_resolve_with_kwargs(self):
Expand Down
1 change: 1 addition & 0 deletions tests/test_config_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,7 @@ def test_registry_has_known_models(self):
assert "qwen2_5_omni" in _PIPELINE_REGISTRY
assert "qwen3_omni_moe" in _PIPELINE_REGISTRY
assert "qwen3_tts" in _PIPELINE_REGISTRY
assert "tuna" in _PIPELINE_REGISTRY

def test_registry_loads_pipeline_on_getitem(self):
"""Looking up a registered model_type returns the matching PipelineConfig."""
Expand Down
4 changes: 4 additions & 0 deletions vllm_omni/config/pipeline_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@
"vllm_omni.model_executor.models.bagel.pipeline",
"BAGEL_SINGLE_STAGE_PIPELINE",
),
"tuna": (
"vllm_omni.model_executor.models.tuna.pipeline",
"TUNA_PIPELINE",
),
"glm_image": (
"vllm_omni.model_executor.models.glm_image.pipeline",
"GLM_IMAGE_PIPELINE",
Expand Down
14 changes: 14 additions & 0 deletions vllm_omni/config/stage_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ def get_pipeline_path(model_dir: str, filename: str) -> Path:

logger = init_logger(__name__)

_TUNA_MODEL_TYPES = {
"tuna",
"tuna2",
"tuna_2",
"tuna_2_pixel",
"tuna2_pixel",
"tuna_2r_pixel",
"tuna2r_pixel",
}


def _warn_deprecated_kwargs(kwargs: dict[str, Any]) -> None:
if "cli_explicit_keys" in kwargs:
Expand Down Expand Up @@ -1014,6 +1024,10 @@ def create_from_model(

# --- New path: check pipeline registry by model_type first ---
model_type, hf_config = cls._auto_detect_model_type(model, trust_remote_code=trust_remote_code)
if model_type:
normalized_model_type = model_type.replace("-", "_").lower()
if normalized_model_type in _TUNA_MODEL_TYPES:
model_type = "tuna"
if model_type and model_type in _PIPELINE_REGISTRY:
return cls._create_from_registry(model_type, cli_overrides, deploy_config_path)

Expand Down
17 changes: 17 additions & 0 deletions vllm_omni/deploy/tuna.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Tuna/Tuna-2 single-stage deploy placeholder.
#
# Upstream Tuna-2 currently publishes code and expects local `.pt` checkpoints
# driven through its Hydra CLI. This config lets vLLM-Omni recognize Tuna model
# metadata and route startup to a dedicated pipeline entrypoint with a clear
# integration message until a stable checkpoint/runtime contract is available.

async_chunk: false

stages:
- stage_id: 0
max_num_seqs: 1
devices: "0"
engine_extras:
model_class_name: TunaExternalPipeline
custom_pipeline_args:
variant: none_encoder
47 changes: 32 additions & 15 deletions vllm_omni/diffusion/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,17 +96,20 @@ def _validate_parallel_config(self) -> Self:
assert self.ulysses_degree > 0, "Ulysses degree must be > 0"
assert self.ring_degree > 0, "Ring degree must be > 0"
assert self.cfg_parallel_size > 0, "CFG parallel size must be > 0"
assert self.cfg_parallel_size in [1, 2, 3], (
f"CFG parallel size must be 1, 2, or 3, but got {self.cfg_parallel_size}"
)
assert self.cfg_parallel_size in [
1,
2,
3,
], f"CFG parallel size must be 1, 2, or 3, but got {self.cfg_parallel_size}"
assert self.vae_patch_parallel_size > 0, "VAE patch parallel size must be > 0"
assert self.sequence_parallel_size == self.ulysses_degree * self.ring_degree, (
"Sequence parallel size must be equal to the product of ulysses degree and ring degree,"
f" but got {self.sequence_parallel_size} != {self.ulysses_degree} * {self.ring_degree}"
)
assert self.ulysses_mode in {"strict", "advanced_uaa"}, (
f"ulysses_mode must be one of {{'strict','advanced_uaa'}}, but got {self.ulysses_mode!r}."
)
assert self.ulysses_mode in {
"strict",
"advanced_uaa",
}, f"ulysses_mode must be one of {{'strict','advanced_uaa'}}, but got {self.ulysses_mode!r}."

# Validate HSDP configuration
if self.use_hsdp:
Expand Down Expand Up @@ -525,12 +528,12 @@ class OmniDiffusionConfig:
@property
def is_moe(self) -> bool:
num_experts = self.tf_model_config.get("num_experts", None)
if not isinstance(num_experts, (list, tuple, int)):
if not isinstance(num_experts, list | tuple | int):
return False
if isinstance(num_experts, int):
return num_experts > 0

if isinstance(num_experts, (list, tuple)):
if isinstance(num_experts, list | tuple):
return any(isinstance(n, int) and n > 0 for n in num_experts)

return False
Expand Down Expand Up @@ -733,13 +736,27 @@ def enrich_config(self) -> None:
model_type = cfg.get("model_type")
architectures = cfg.get("architectures") or []

if model_type == "bagel" or "BagelForConditionalGeneration" in architectures:
self.model_class_name = "BagelPipeline"
self.tf_model_config = TransformerConfig()
self.update_multimodal_support()
elif model_type == "nextstep":
if self.model_class_name is None:
self.model_class_name = "NextStep11Pipeline"
normalized_model_type = str(model_type or "").replace("-", "_").lower()

if model_type == "bagel" or "BagelForConditionalGeneration" in architectures:
self.model_class_name = "BagelPipeline"
self.tf_model_config = TransformerConfig()
self.update_multimodal_support()
elif normalized_model_type in {
"tuna",
"tuna2",
"tuna_2",
"tuna_2_pixel",
"tuna2_pixel",
"tuna_2r_pixel",
"tuna2r_pixel",
} or any(str(arch).startswith("Tuna") for arch in architectures):
self.model_class_name = "TunaExternalPipeline"
self.tf_model_config = TransformerConfig()
self.update_multimodal_support()
elif model_type == "nextstep":
if self.model_class_name is None:
self.model_class_name = "NextStep11Pipeline"
self.tf_model_config = TransformerConfig()
self.update_multimodal_support()
elif architectures and len(architectures) == 1:
Expand Down
3 changes: 3 additions & 0 deletions vllm_omni/diffusion/models/tuna/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .pipeline_tuna import TunaExternalPipeline

__all__ = ["TunaExternalPipeline"]
59 changes: 59 additions & 0 deletions vllm_omni/diffusion/models/tuna/pipeline_tuna.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Tuna/Tuna-2 integration placeholder.

The upstream Tuna-2 project currently publishes research/inference code but no
released model weights or HuggingFace-style runtime package contract. vLLM-Omni
can still recognize Tuna configs and route them here so users get an actionable
message instead of a generic "unknown model" failure.
"""

from __future__ import annotations

from torch import nn

from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
from vllm_omni.diffusion.request import OmniDiffusionRequest

# TODO(#3303): Replace this placeholder once Tuna publishes a stable runtime
# loading contract that can be validated end to end in vLLM-Omni.
_TUNA_NOT_READY = (
"Tuna/Tuna-2 is recognized by vLLM-Omni, but the runtime integration is "
"not available yet. Track vLLM-Omni issue #3303 and the upstream "
"facebookresearch/tuna-2 repository at "
"https://github.com/facebookresearch/tuna-2. The upstream project "
"currently uses its own Hydra-based inference entrypoint and checkpoint "
"format, and does not publish full model weights or a stable "
"HuggingFace/diffusers loading contract. To finish this integration, "
"port Tuna2PixelPipeline/Tuna2RPixelPipeline/TunaPipeline into "
"vllm_omni.diffusion.models.tuna and add checkpoint loading for the "
"upstream .pt files."
)


def get_tuna_post_process_func(od_config: OmniDiffusionConfig):
def post_process_func(x):
return x

return post_process_func


class TunaExternalPipeline(nn.Module):
"""Recognized Tuna pipeline entrypoint.

This class intentionally fails during initialization with a clear message.
Keeping it registered lets model detection, stage-config resolution, and
documentation converge before upstream releases a stable checkpoint/runtime
contract that can be validated end to end.
"""

def __init__(self, *, od_config: OmniDiffusionConfig, prefix: str = ""):
super().__init__()
self.od_config = od_config
raise RuntimeError(_TUNA_NOT_READY)

def forward(self, req: OmniDiffusionRequest) -> DiffusionOutput:
raise RuntimeError(_TUNA_NOT_READY)

def load_weights(self, *args: object, **kwargs: object) -> set[str]:
raise RuntimeError(_TUNA_NOT_READY)
6 changes: 6 additions & 0 deletions vllm_omni/diffusion/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,11 @@
"pipeline_bagel",
"BagelPipeline",
),
"TunaExternalPipeline": (
"tuna",
"pipeline_tuna",
"TunaExternalPipeline",
),
"InternVLAA1Pipeline": (
"internvla_a1",
"pipeline_internvla_a1",
Expand Down Expand Up @@ -426,6 +431,7 @@ def _apply_sequence_parallel_if_enabled(model, od_config: OmniDiffusionConfig) -
"WanI2VDMD2Pipeline": "get_wan22_i2v_post_process_func",
"LongCatImagePipeline": "get_longcat_image_post_process_func",
"BagelPipeline": "get_bagel_post_process_func",
"TunaExternalPipeline": "get_tuna_post_process_func",
"InternVLAA1Pipeline": "get_internvla_a1_post_process_func",
"LongCatImageEditPipeline": "get_longcat_image_post_process_func",
"StableDiffusion3Pipeline": "get_sd3_image_post_process_func",
Expand Down
23 changes: 22 additions & 1 deletion vllm_omni/diffusion/utils/hf_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,27 @@ def _looks_like_bagel(model_name: str) -> bool:
return False


def _looks_like_tuna(model_name: str) -> bool:
"""Best-effort detection for Tuna/Tuna-2 unified image models."""
try:
cfg = get_hf_file_to_dict("config.json", model_name)
model_type = str(cfg.get("model_type", "")).replace("-", "_").lower()
if model_type in {
"tuna",
"tuna2",
"tuna_2",
"tuna_2_pixel",
"tuna2_pixel",
"tuna_2r_pixel",
"tuna2r_pixel",
}:
return True
architectures = cfg.get("architectures") or []
return any(str(arch).startswith("Tuna") for arch in architectures)
except Exception:
return False


@lru_cache
def is_diffusion_model(model_name: str) -> bool:
"""Check if a model is a diffusion model.
Expand Down Expand Up @@ -74,4 +95,4 @@ def is_diffusion_model(model_name: str) -> bool:

# Bagel is not a diffusers pipeline (no model_index.json), but is still a
# diffusion-style model in vllm-omni. Detect it via config.json.
return _looks_like_bagel(model_name)
return _looks_like_bagel(model_name) or _looks_like_tuna(model_name)
Loading
Loading