[Model] Add Dynin-omni model in vllm-omni (#1759)

Signed-off-by: kdg6245@snu.ac.kr <kdg6245@snu.ac.kr>
Signed-off-by: Yejoon Lee (IPAI) <leeyejoon@snu.ac.kr>
Signed-off-by: aidas (arpa-kt) <aidaslab@gmail.com>
Signed-off-by: Dogeun Kim <82812668+DOGEUNNKIM@users.noreply.github.com>
Co-authored-by: Yejoon Lee (IPAI) <leeyejoon@snu.ac.kr>
Co-authored-by: aidas (arpa-kt) <aidaslab@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-04-09 16:37:34 +08:00
1 parent a7bf4050de
commit e2b0ee4b87
18 files changed
+7072

No files matched your search

+1
View File
@@ -60,5 +60,6 @@ th {
| `HunyuanVideo15Pipeline` | HunyuanVideo-1.5-T2V | `hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_t2v`, `hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-720p_t2v` | ✅︎ | ✅︎ | | |
| `HunyuanVideo15ImageToVideoPipeline` | HunyuanVideo-1.5-I2V | `hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_i2v`, `hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-720p_i2v` | ✅︎ | ✅︎ | | |
| `VoxtralTTSForConditionalGeneration` | Voxtral TTS | `mistralai/Voxtral-4B-TTS-2603` | ✅︎ | ✅︎ | | |
|`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.
@@ -0,0 +1,110 @@
# Dynin-Omni Offline End2End Example
This folder contains a unified offline inference entrypoint:
- `end2end.py`
## 1. Environment Setup
Run from repository root:
```bash
cd <REPO_ROOT>
```
If needed, install this repo in editable mode:
```bash
pip install -e .
```
## 2. Extra Dependencies (EMOVA)
Install the following packages for EMOVA-related components:
```bash
pip install \
"phonemizer==3.3.0" \
"Unidecode==1.4.0" \
"hydra-core==1.3.2" \
"pytorch-lightning==1.1.0" \
"wget==3.2" \
"wrapt==2.1.1" \
"onnx==1.20.1" \
"frozendict==2.4.7" \
"inflect==7.5.0" \
"braceexpand==0.1.7" \
"webdataset==1.0.2" \
"torch-stft==0.1.4" \
"editdistance==0.8.1"
```
## 3. Hardware and VRAM Requirements
This example uses a 3-stage pipeline on one GPU by default
([`dynin_omni.yaml`](../../../vllm_omni/model_executor/stage_configs/dynin_omni.yaml)):
- Stage-0 (`token2text`): `gpu_memory_utilization: 0.5`
- Stage-1 (`token2image`): `gpu_memory_utilization: 0.1`
- Stage-2 (`token2audio`): `gpu_memory_utilization: 0.1`
### Requested GPU Memory Budget from `gpu_memory_utilization`
| Stage | Utilization | A100 80GB | H200 141GB |
| :-- | :-- | :-- | :-- |
| Stage-0 (token2text) | 0.5 | ~40.0 GB | ~70.5 GB |
| Stage-1 (token2image) | 0.1 | ~8.0 GB | ~14.1 GB |
| Stage-2 (token2audio) | 0.1 | ~8.0 GB | ~14.1 GB |
| Total requested budget | 0.7 | ~56.0 GB | ~98.7 GB |
### Observed Runtime Signal (from your log)
- Stage-0 reported: `Model loading took 15.12 GiB memory` (weights footprint signal).
- Stages 1/2 can still add runtime memory depending on task path and backend allocations.
- Keep extra headroom for CUDA/PyTorch overhead and temporary allocations.
### GPU Compatibility
- Confirmed target GPUs for this setup: **NVIDIA H200**, **NVIDIA A100**.
- CI/e2e coverage in this repo also includes CUDA **L4** markers for Dynin tests.
## 4. End2End Run Examples
```bash
# t2t
python <REPO_ROOT>/examples/offline_inference/dynin_omni/end2end.py \
--task t2t --model snu-aidas/Dynin-Omni --text <INSTRUCTION_TEXT>
# i2t
python <REPO_ROOT>/examples/offline_inference/dynin_omni/end2end.py \
--task i2t --model snu-aidas/Dynin-Omni --image <IMAGE_PATH> --text "Please describe this image in detail."
# s2t
python <REPO_ROOT>/examples/offline_inference/dynin_omni/end2end.py \
--task s2t --model snu-aidas/Dynin-Omni --audio <AUDIO_PATH> --text "Transcribe the given audio."
# t2i
python <REPO_ROOT>/examples/offline_inference/dynin_omni/end2end.py \
--task t2i --model snu-aidas/Dynin-Omni --text <INSTRUCTION_TEXT>
# v2t
python <REPO_ROOT>/examples/offline_inference/dynin_omni/end2end.py \
--task v2t --model snu-aidas/Dynin-Omni --video <VIDEO_PATH> --text "Describe this video in detail."
# i2i
python <REPO_ROOT>/examples/offline_inference/dynin_omni/end2end.py \
--task i2i --model snu-aidas/Dynin-Omni --image <IMAGE_PATH> --text <INSTRUCTION_TEXT>
# t2s
python <REPO_ROOT>/examples/offline_inference/dynin_omni/end2end.py \
--task t2s --model snu-aidas/Dynin-Omni --text <INSTRUCTION_TEXT>
```
## 5. Notes
- Outputs are saved under task-specific directories in `/tmp` by default.
- You can override output path with `--output-dir`.
- If you want to force local config resolution, pass `--dynin-config-path <PATH_TO_DYNIN_OMNI_YAML>`.
- If you see the warning
`max_num_batched_tokens (32768) exceeds max_num_seqs * max_model_len (4096)`,
reduce `max_num_batched_tokens` in stage config (for example, `4096` in CI config).
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,97 @@
# Dynin-Omni Online Serving Example
## Installation
Please refer to [README.md](../../../README.md).
## Launch the Server
First, find the `transformers_modules` path:
```bash
python - <<'PY'
from transformers.utils.hub import HF_MODULES_CACHE
print(HF_MODULES_CACHE)
PY
```
Then export it for both `PYTHONPATH` and `HF_MODULES_CACHE`:
```bash
export PYTHONPATH=<transformers_modules_path>:$PYTHONPATH
export HF_MODULES_CACHE=<transformers_modules_path>
```
Run from repository root:
```bash
vllm-omni serve snu-aidas/Dynin-Omni \
--omni \
--port 8091 \
--stage-configs-path "$(pwd)/vllm_omni/model_executor/stage_configs/dynin_omni.yaml"
```
If `vllm-omni` is not in PATH, run:
```bash
PYTHONPATH="$(pwd)" python -m vllm_omni.entrypoints.cli.main serve snu-aidas/Dynin-Omni \
--omni \
--port 8091 \
--stage-configs-path "$(pwd)/vllm_omni/model_executor/stage_configs/dynin_omni.yaml"
```
Wait until the server logs show both `All stages initialized successfully` and
`Application startup complete.` before sending requests.
## Send Requests via Python Client
Move to the example directory:
```bash
cd examples/online_serving/dynin_omni
```
### Text -> Image
```bash
python openai_chat_completion_client_for_multimodal_generation.py \
--query-type t2i \
--prompt "A realistic indoor living room with natural daylight."
```
### Image -> Image
```bash
python openai_chat_completion_client_for_multimodal_generation.py \
--query-type i2i \
--image-path ../../offline_inference/dynin_omni/data/image/sofa_under_water.jpg \
--prompt "Transform this surreal underwater setting into a realistic indoor living room while preserving the sofa layout."
```
### Text -> Speech
```bash
python openai_chat_completion_client_for_multimodal_generation.py \
--query-type t2s \
--prompt "Hello. This is Dynin-omni."
```
## CLI Arguments
- `--query-type` (`t2i|t2s|i2i`)
- `--model` (default: `snu-aidas/Dynin-Omni`)
- `--host` / `--port` (OpenAI-compatible vLLM endpoint)
- `--prompt` (custom text)
- `--image-path` (required for `i2i`)
- `--modalities` (optional output modalities override)
- `--output-dir` (default: `/tmp/dynin_online_outputs`)
## Notes
- This client currently supports only `t2i`, `t2s`, and `i2i`.
- `t2t` is intentionally not exposed in this online example.
- This example intentionally uses the OpenAI-compatible chat completion endpoint.
- Task routing for non-text outputs relies on Dynin task trigger tokens (`<|t2i|>`, `<|i2i|>`, `<|t2s|>`) injected by the client.
- Outputs are saved under `/tmp/dynin_online_outputs` by default.
- Dynin stage-0 warmup can take a while on first startup; do not send requests before startup completes.
- Dynin itself can execute text-returning tasks such as `t2t`, `s2t`, `i2t`, and `v2t`, but this online serving example currently runs stage-0 in `generation` mode. In that path, the generation worker does not surface the final text as `output.text`, so OpenAI chat responses for those text-output tasks may complete internally but still return empty text.
@@ -0,0 +1,342 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
import argparse
import base64
import json
import mimetypes
import os
import time
from pathlib import Path
from typing import Any
DEFAULT_MODEL = "snu-aidas/Dynin-Omni"
DEFAULT_OUTPUT_DIR = "/tmp/dynin_online_outputs"
QUERY_CHOICES = ("t2i", "t2s", "i2i")
DEFAULT_PROMPT_BY_QUERY = {
"t2i": "A high quality detailed living room interior photo.",
"t2s": "Please read this sentence naturally: Hello from Dynin-Omni online serving.",
"i2i": "Transform this image into a realistic indoor living room while preserving layout.",
}
DEFAULT_MODALITIES_BY_QUERY = {
"t2i": ["image"],
"t2s": ["audio"],
"i2i": ["image"],
}
OFFLINE_PARITY_STAGE_COUNT = 3
OFFLINE_PARITY_STAGE_SAMPLING = {
"max_tokens": 1,
"temperature": 0.0,
"top_p": 1.0,
"detokenize": False,
}
def _infer_mime_type(path: Path) -> str:
mime_type, _ = mimetypes.guess_type(str(path))
return mime_type or "application/octet-stream"
def _encode_file_as_data_url(path: Path) -> str:
mime_type = _infer_mime_type(path)
raw = path.read_bytes()
encoded = base64.b64encode(raw).decode("utf-8")
return f"data:{mime_type};base64,{encoded}"
def _to_image_url(path_or_url: str) -> str:
value = str(path_or_url)
if value.startswith(("http://", "https://", "data:image/")):
return value
path = Path(value).expanduser().resolve()
if not path.exists():
raise FileNotFoundError(f"Image file not found: {path}")
return _encode_file_as_data_url(path)
def _build_user_content(query_type: str, prompt: str, image_path: str | None) -> list[dict[str, Any]]:
if query_type == "t2i":
return [{"type": "text", "text": f"<|t2i|> {prompt}"}]
if query_type == "t2s":
return [{"type": "text", "text": f"<|t2s|> {prompt}"}]
if query_type == "i2i":
if not image_path:
raise ValueError("--image-path is required for query type i2i")
return [
{"type": "text", "text": f"<|i2i|> {prompt}"},
{"type": "image_url", "image_url": {"url": _to_image_url(image_path)}},
]
raise ValueError(f"Unsupported query_type: {query_type}")
def _collect_text_from_content(content: Any) -> list[str]:
texts: list[str] = []
if isinstance(content, str):
stripped = content.strip()
if stripped:
texts.append(stripped)
return texts
if isinstance(content, dict):
for key in ("text", "content", "value", "output_text"):
text_value = content.get(key)
if isinstance(text_value, str) and text_value.strip():
texts.append(text_value.strip())
return texts
if isinstance(content, list):
for item in content:
texts.extend(_collect_text_from_content(item))
return texts
content_text = getattr(content, "text", None)
if isinstance(content_text, str) and content_text.strip():
texts.append(content_text.strip())
content_value = getattr(content, "content", None)
if isinstance(content_value, str) and content_value.strip():
texts.append(content_value.strip())
output_text = getattr(content, "output_text", None)
if isinstance(output_text, str) and output_text.strip():
texts.append(output_text.strip())
return texts
def _extract_text_outputs(chat_completion: Any) -> list[str]:
texts: list[str] = []
for choice in getattr(chat_completion, "choices", []) or []:
message = getattr(choice, "message", None)
if message is None:
continue
content = getattr(message, "content", None)
texts.extend(_collect_text_from_content(content))
reasoning_content = getattr(message, "reasoning_content", None)
if isinstance(reasoning_content, str) and reasoning_content.strip():
texts.append(reasoning_content.strip())
choice_text = getattr(choice, "text", None)
if isinstance(choice_text, str) and choice_text.strip():
texts.append(choice_text.strip())
top_level_output_text = getattr(chat_completion, "output_text", None)
if isinstance(top_level_output_text, str) and top_level_output_text.strip():
texts.append(top_level_output_text.strip())
return texts
def _extract_image_data_urls(chat_completion: Any) -> list[str]:
urls: list[str] = []
for choice in getattr(chat_completion, "choices", []) or []:
message = getattr(choice, "message", None)
if message is None:
continue
content = getattr(message, "content", None)
if not isinstance(content, list):
continue
for item in content:
if not isinstance(item, dict):
continue
if item.get("type") != "image_url":
continue
image_url = (item.get("image_url") or {}).get("url")
if isinstance(image_url, str) and image_url.startswith("data:image"):
urls.append(image_url)
return urls
def _extract_audio_payloads(chat_completion: Any) -> list[bytes]:
payloads: list[bytes] = []
for choice in getattr(chat_completion, "choices", []) or []:
message = getattr(choice, "message", None)
if message is None:
continue
message_audio = getattr(message, "audio", None)
if message_audio is None:
continue
data_b64 = getattr(message_audio, "data", None)
if isinstance(data_b64, str) and data_b64:
try:
payloads.append(base64.b64decode(data_b64))
except Exception:
continue
return payloads
def _decode_data_url(data_url: str) -> tuple[bytes, str]:
header, data = data_url.split(",", 1)
mime_type = "image/png"
if ";" in header and ":" in header:
mime_type = header.split(":", 1)[1].split(";", 1)[0]
return base64.b64decode(data), mime_type
def _image_extension_from_mime(mime_type: str) -> str:
if mime_type == "image/jpeg":
return ".jpg"
if mime_type == "image/webp":
return ".webp"
if mime_type == "image/gif":
return ".gif"
return ".png"
def _save_outputs(
*,
query_type: str,
chat_completion: Any,
output_dir: Path,
) -> None:
output_dir.mkdir(parents=True, exist_ok=True)
stamp = time.strftime("%Y%m%d_%H%M%S")
text_outputs = _extract_text_outputs(chat_completion)
image_data_urls = _extract_image_data_urls(chat_completion)
audio_payloads = _extract_audio_payloads(chat_completion)
if text_outputs:
text_path = output_dir / f"{query_type}_{stamp}.txt"
text_path.write_text("\n\n".join(text_outputs) + "\n", encoding="utf-8")
print(f"[dynin-online] text saved: {text_path}")
print(text_outputs[0])
for idx, image_url in enumerate(image_data_urls):
image_bytes, mime_type = _decode_data_url(image_url)
ext = _image_extension_from_mime(mime_type)
image_path = output_dir / f"{query_type}_{stamp}_{idx}{ext}"
image_path.write_bytes(image_bytes)
print(f"[dynin-online] image saved: {image_path}")
for idx, audio_bytes in enumerate(audio_payloads):
audio_path = output_dir / f"{query_type}_{stamp}_{idx}.wav"
audio_path.write_bytes(audio_bytes)
print(f"[dynin-online] audio saved: {audio_path}")
if not text_outputs and not image_data_urls and not audio_payloads:
print("[dynin-online] no output extracted from response")
raw_path = output_dir / f"{query_type}_{stamp}_raw_response.json"
try:
if hasattr(chat_completion, "model_dump_json"):
serialized = chat_completion.model_dump_json(indent=2)
else:
if hasattr(chat_completion, "model_dump"):
raw_payload: Any = chat_completion.model_dump(mode="json")
else:
raw_payload = chat_completion
try:
serialized = json.dumps(raw_payload, ensure_ascii=False, indent=2)
except Exception:
serialized = json.dumps({"repr": repr(raw_payload)}, ensure_ascii=False, indent=2)
raw_path.write_text(serialized + "\n", encoding="utf-8")
print(f"[dynin-online] raw response saved: {raw_path}")
except Exception:
pass
def _build_offline_parity_sampling_params_list() -> list[dict[str, Any]]:
return [dict(OFFLINE_PARITY_STAGE_SAMPLING) for _ in range(OFFLINE_PARITY_STAGE_COUNT)]
def run_request(args: argparse.Namespace) -> None:
from openai import OpenAI
client = OpenAI(
api_key="EMPTY",
base_url=f"http://{args.host}:{args.port}/v1",
)
prompt = args.prompt.strip() if args.prompt else DEFAULT_PROMPT_BY_QUERY[args.query_type]
user_content = _build_user_content(
query_type=args.query_type,
prompt=prompt,
image_path=args.image_path,
)
if args.modalities:
modalities = [item.strip() for item in args.modalities.split(",") if item.strip()]
else:
modalities = DEFAULT_MODALITIES_BY_QUERY[args.query_type]
extra_body = {
"sampling_params_list": _build_offline_parity_sampling_params_list(),
}
chat_completion = client.chat.completions.create(
model=args.model,
messages=[{"role": "user", "content": user_content}],
modalities=modalities,
extra_body=extra_body,
)
_save_outputs(
query_type=args.query_type,
chat_completion=chat_completion,
output_dir=Path(args.output_dir).expanduser(),
)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Dynin-Omni online chat completion client")
parser.add_argument(
"--query-type",
"-q",
type=str,
default="t2i",
choices=QUERY_CHOICES,
help="Dynin query type",
)
parser.add_argument(
"--model",
"-m",
type=str,
default=DEFAULT_MODEL,
help="Model name/path",
)
parser.add_argument(
"--host",
type=str,
default="localhost",
help="Host/IP of the vLLM Omni API server",
)
parser.add_argument(
"--port",
type=int,
default=8091,
help="Port of the vLLM Omni API server",
)
parser.add_argument(
"--prompt",
"-p",
type=str,
default="",
help="Custom prompt text",
)
parser.add_argument(
"--image-path",
"-i",
type=str,
default=None,
help="Image path/URL for i2i",
)
parser.add_argument(
"--modalities",
type=str,
default="",
help="Comma-separated output modalities override (e.g., text,image,audio)",
)
parser.add_argument(
"--output-dir",
"-o",
type=str,
default=DEFAULT_OUTPUT_DIR,
help="Directory to save outputs",
)
return parser.parse_args()
def main() -> None:
args = parse_args()
os.environ.setdefault("HF_HUB_DISABLE_XET", "1")
run_request(args)
if __name__ == "__main__":
main()
@@ -0,0 +1,419 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""
E2E offline smoke tests for Dynin-Omni.
- model: "snu-aidas/Dynin-Omni"
- stage config: tests/e2e/stage_configs/dynin_omni_ci.yaml
"""
from __future__ import annotations
import os
from pathlib import Path
from typing import Any
import numpy as np
import pytest
import torch
from transformers import AutoTokenizer
from tests.conftest import OmniRunner
from tests.utils import hardware_test
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
os.environ["VLLM_TEST_CLEAN_GPU_MEMORY"] = "0"
_REPO_ROOT = Path(__file__).resolve().parents[3]
_DEFAULT_DYNIN_CONFIG_PATH: Path | None = None
_DEFAULT_STAGE_CONFIG_PATH = _REPO_ROOT / "tests" / "e2e" / "stage_configs" / "dynin_omni_ci.yaml"
models = ["snu-aidas/Dynin-Omni"]
stage_configs = [str(_DEFAULT_STAGE_CONFIG_PATH)]
test_params = [(model, stage_config) for model in models for stage_config in stage_configs]
DYNIN_CONFIG_PATH = str(_DEFAULT_DYNIN_CONFIG_PATH) if _DEFAULT_DYNIN_CONFIG_PATH is not None else None
pytestmark = [
pytest.mark.core_model,
pytest.mark.omni,
]
# prompting util
def _build_mmu_prompt(tokenizer: Any, question: str, dynin_config_path: str | None) -> dict[str, Any]:
encoded = tokenizer(question, return_tensors="pt", add_special_tokens=True)
token_ids = [int(v) for v in encoded["input_ids"][0].tolist()]
attention_mask = [int(v) for v in encoded["attention_mask"][0].tolist()]
additional_information: dict[str, Any] = {
"task": ["mmu"],
"detok_id": [0],
"prompt_length": [len(token_ids)],
"attention_mask": [attention_mask],
"max_new_tokens": [64],
"steps": [64],
"block_length": [16],
"temperature": [0.0],
}
if dynin_config_path:
additional_information["dynin_config_path"] = [str(dynin_config_path)]
return {
"prompt_token_ids": token_ids,
"additional_information": additional_information,
"modalities": ["text"],
}
def _build_mmu_multimodal_prompt(
tokenizer: Any,
question: str,
dynin_config_path: str | None,
*,
image: Any | None = None,
audio: tuple[np.ndarray, int] | None = None,
) -> dict[str, Any]:
if image is None and audio is None:
raise ValueError("At least one multimodal input (image or audio) must be provided.")
prefix_chunks: list[str] = []
mm_data: dict[str, Any] = {}
if image is not None:
prefix_chunks.append("<|soi|><|image|><|eoi|>")
mm_data["image"] = image
if audio is not None:
prefix_chunks.append("<|soa|><|audio|><|eoa|>")
mm_data["audio"] = audio
prefixed_question = " ".join(prefix_chunks + [question]).strip()
prompt = _build_mmu_prompt(
tokenizer=tokenizer,
question=prefixed_question,
dynin_config_path=dynin_config_path,
)
prompt["multi_modal_data"] = mm_data
prompt["modalities"] = ["text"]
return prompt
def _generate_synthetic_image(width: int = 224, height: int = 224) -> np.ndarray:
x = np.linspace(0, 255, width, dtype=np.uint8)
y = np.linspace(0, 255, height, dtype=np.uint8)[:, None]
red = np.tile(x, (height, 1))
green = np.tile(y, (1, width))
blue = ((red.astype(np.uint16) + green.astype(np.uint16)) // 2).astype(np.uint8)
return np.stack([red, green, blue], axis=-1)
def _generate_synthetic_audio(duration_s: int = 5, sample_rate: int = 48_000) -> tuple[np.ndarray, int]:
t = np.linspace(0, duration_s, int(sample_rate * duration_s), endpoint=False, dtype=np.float32)
waveform = 0.1 * np.sin(2.0 * np.pi * 440.0 * t)
return waveform.astype(np.float32), sample_rate
# prompting util
def _build_t2s_decode_prompt(dynin_config_path: str | None) -> dict[str, Any]:
# Bypass stage-0 generation and directly validate token->audio decode path.
generated_audio_token_ids = [int(v) for v in ([10, 11, 12, 13, 14] * 32)]
additional_information: dict[str, Any] = {
"task": ["t2s"],
"detok_id": [1],
"generated_token_ids": [generated_audio_token_ids],
"audio_codebook_size": [4096],
}
if dynin_config_path:
additional_information["dynin_config_path"] = [str(dynin_config_path)]
return {
"prompt_token_ids": [0],
"additional_information": additional_information,
"modalities": ["audio"],
}
# prompting util
def _build_t2i_decode_prompt(dynin_config_path: str | None) -> dict[str, Any]:
# Bypass stage-0 generation and directly validate token->image decode path.
# MAGVIT decode path expects a square token grid; 1024 tokens -> 32x32.
generated_image_token_ids = [int(v) for v in ([10, 11, 12, 13, 14, 15, 16, 17] * 128)]
additional_information: dict[str, Any] = {
"task": ["t2i"],
"detok_id": [2],
"generated_token_ids": [generated_image_token_ids],
"codebook_size": [8192],
}
if dynin_config_path:
additional_information["dynin_config_path"] = [str(dynin_config_path)]
return {
"prompt_token_ids": [0],
"additional_information": additional_information,
"modalities": ["image"],
}
def _configure_dynin_config_env() -> None:
if DYNIN_CONFIG_PATH:
os.environ["DYNIN_CONFIG_PATH"] = str(DYNIN_CONFIG_PATH)
else:
os.environ.pop("DYNIN_CONFIG_PATH", None)
def _is_finished_request_output(request_output: Any) -> bool:
if request_output is None:
return False
req_list = request_output if isinstance(request_output, list) else [request_output]
for req in req_list:
if req is not None and bool(getattr(req, "finished", False)):
return True
return False
def _find_stage_output(outputs: list[Any], output_type: str) -> Any | None:
matched = [
stage_output for stage_output in outputs if getattr(stage_output, "final_output_type", None) == output_type
]
if not matched:
return None
# Prefer the latest finished chunk to avoid picking an intermediate stream output.
for stage_output in reversed(matched):
if _is_finished_request_output(getattr(stage_output, "request_output", None)):
return stage_output
return matched[-1]
def _to_token_list(value: Any) -> list[int]:
if value is None:
return []
if hasattr(value, "detach"):
value = value.detach()
if hasattr(value, "cpu"):
value = value.cpu()
if hasattr(value, "flatten"):
value = value.flatten().tolist()
if isinstance(value, tuple):
value = list(value)
if not isinstance(value, list):
return []
out: list[int] = []
for token in value:
if isinstance(token, bool):
continue
try:
out.append(int(token))
except Exception:
continue
return out
def _extract_text(stage_output: Any, tokenizer: Any | None = None) -> str:
request_output = getattr(stage_output, "request_output", None)
if request_output is None:
return ""
req_list = request_output if isinstance(request_output, list) else [request_output]
for req in req_list:
completions = getattr(req, "outputs", None) or []
if not completions:
continue
completion = completions[0]
mm_out = (
getattr(completion, "multimodal_output", None)
or getattr(req, "multimodal_output", None)
or getattr(stage_output, "multimodal_output", None)
or {}
)
text = mm_out.get("text")
if isinstance(text, list) and text:
text = text[-1]
if isinstance(text, str) and text.strip():
return text.strip()
if tokenizer is not None:
for key in ("text_tokens", "token_ids"):
token_ids = _to_token_list(mm_out.get(key))
if not token_ids:
continue
decoded = tokenizer.decode(token_ids, skip_special_tokens=True)
if isinstance(decoded, str) and decoded.strip():
return decoded.strip()
fallback = getattr(completion, "text", None)
if isinstance(fallback, str) and fallback.strip():
return fallback.strip()
return ""
def _extract_audio(stage_output: Any) -> Any | None:
request_output = getattr(stage_output, "request_output", None)
if request_output is None:
return None
req_list = request_output if isinstance(request_output, list) else [request_output]
for req in req_list:
completions = getattr(req, "outputs", None) or []
if not completions:
continue
completion = completions[0]
mm_out = getattr(completion, "multimodal_output", None) or {}
if "audio" in mm_out:
return mm_out["audio"]
return None
def _extract_image(stage_output: Any) -> Any | None:
request_output = getattr(stage_output, "request_output", None)
if request_output is None:
return None
req_list = request_output if isinstance(request_output, list) else [request_output]
for req in req_list:
completions = getattr(req, "outputs", None) or []
if not completions:
continue
completion = completions[0]
mm_out = getattr(completion, "multimodal_output", None) or {}
if "image" in mm_out:
return mm_out["image"]
return None
def _numel(value: Any) -> int:
if value is None:
return 0
if isinstance(value, torch.Tensor):
return int(value.numel())
shape = getattr(value, "shape", None)
if shape is not None:
try:
total = 1
for dim in shape:
total *= int(dim)
return int(total)
except Exception:
pass
if isinstance(value, (list, tuple)):
return len(value)
return 0
@hardware_test(res={"cuda": "L4", "rocm": "MI325"})
@pytest.mark.parametrize("test_config", test_params)
def test_dynin_t2i_decode_to_image(test_config: tuple[str, str]) -> None:
model, stage_config_path = test_config
_configure_dynin_config_env()
prompt = _build_t2i_decode_prompt(dynin_config_path=DYNIN_CONFIG_PATH)
with OmniRunner(
model,
seed=42,
stage_configs_path=stage_config_path,
stage_init_timeout=600,
init_timeout=600,
) as runner:
outputs = runner.generate([prompt])
image_output = _find_stage_output(outputs, "image")
assert image_output is not None
image_value = _extract_image(image_output)
assert image_value is not None
assert _numel(image_value) > 0
@hardware_test(res={"cuda": "L4", "rocm": "MI325"})
@pytest.mark.parametrize("test_config", test_params)
def test_dynin_mmu_to_text(test_config: tuple[str, str]) -> None:
model, stage_config_path = test_config
_configure_dynin_config_env()
tokenizer = AutoTokenizer.from_pretrained(model, trust_remote_code=True)
prompt = _build_mmu_prompt(
tokenizer=tokenizer,
question="What is 2 + 2? Answer in one short sentence.",
dynin_config_path=DYNIN_CONFIG_PATH,
)
with OmniRunner(
model,
seed=42,
stage_configs_path=stage_config_path,
stage_init_timeout=600,
init_timeout=600,
) as runner:
outputs = runner.generate([prompt])
text_output = _find_stage_output(outputs, "text")
assert text_output is not None
text_content = _extract_text(text_output, tokenizer=tokenizer)
assert text_content
@hardware_test(res={"cuda": "L4", "rocm": "MI325"})
@pytest.mark.parametrize("test_config", test_params)
def test_dynin_image_to_text(test_config: tuple[str, str]) -> None:
model, stage_config_path = test_config
_configure_dynin_config_env()
tokenizer = AutoTokenizer.from_pretrained(model, trust_remote_code=True)
prompt = _build_mmu_multimodal_prompt(
tokenizer=tokenizer,
question="Describe the image briefly in one sentence.",
dynin_config_path=DYNIN_CONFIG_PATH,
image=_generate_synthetic_image(),
)
with OmniRunner(
model,
seed=42,
stage_configs_path=stage_config_path,
stage_init_timeout=600,
init_timeout=600,
) as runner:
outputs = runner.generate([prompt])
text_output = _find_stage_output(outputs, "text")
assert text_output is not None
text_content = _extract_text(text_output, tokenizer=tokenizer)
assert text_content
@hardware_test(res={"cuda": "L4", "rocm": "MI325"})
@pytest.mark.parametrize("test_config", test_params)
def test_dynin_speech_to_text(test_config: tuple[str, str]) -> None:
model, stage_config_path = test_config
_configure_dynin_config_env()
tokenizer = AutoTokenizer.from_pretrained(model, trust_remote_code=True)
prompt = _build_mmu_multimodal_prompt(
tokenizer=tokenizer,
question="Transcribe the audio briefly in one sentence.",
dynin_config_path=DYNIN_CONFIG_PATH,
audio=_generate_synthetic_audio(),
)
with OmniRunner(
model,
seed=42,
stage_configs_path=stage_config_path,
stage_init_timeout=600,
init_timeout=600,
) as runner:
outputs = runner.generate([prompt])
text_output = _find_stage_output(outputs, "text")
assert text_output is not None
text_content = _extract_text(text_output, tokenizer=tokenizer)
assert text_content
@hardware_test(res={"cuda": "L4", "rocm": "MI325"})
@pytest.mark.parametrize("test_config", test_params)
def test_dynin_t2s_decode_to_audio(test_config: tuple[str, str]) -> None:
model, stage_config_path = test_config
_configure_dynin_config_env()
prompt = _build_t2s_decode_prompt(dynin_config_path=DYNIN_CONFIG_PATH)
with OmniRunner(
model,
seed=42,
stage_configs_path=stage_config_path,
stage_init_timeout=600,
init_timeout=600,
) as runner:
outputs = runner.generate([prompt])
audio_output = _find_stage_output(outputs, "audio")
assert audio_output is not None
audio_value = _extract_audio(audio_output)
assert audio_value is not None
assert _numel(audio_value) > 0
@@ -0,0 +1,160 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""
Example online tests for Dynin-Omni model.
"""
import base64
import gc
import os
from io import BytesIO
from pathlib import Path
import numpy as np
import pytest
import soundfile as sf
from vllm.assets.image import ImageAsset
from tests import conftest as tests_conftest
from tests.conftest import OmniServerParams
from tests.utils import hardware_test
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
os.environ["VLLM_TEST_CLEAN_GPU_MEMORY"] = "0"
MODEL = "snu-aidas/Dynin-Omni"
STAGE_CONFIG = str(Path(__file__).parent.parent / "stage_configs" / "dynin_omni_ci.yaml")
_WHISPER_SAMPLE_RATE_HZ = 16_000
T2I_PROMPT = "A high quality detailed living room interior photo."
T2S_PROMPT = "Please read this sentence naturally: Hello from Dynin-Omni online serving."
I2I_PROMPT = "Transform this outdoor nature boardwalk scene into a painting style with vivid colors."
TEST_PARAMS = [OmniServerParams(model=MODEL, stage_config_path=STAGE_CONFIG)]
_STAGE_COUNT = 3
_I2I_STAGE_SAMPLING = {"max_tokens": 1, "temperature": 0.0, "top_p": 1.0, "detokenize": False}
def _prepare_audio_waveform_for_whisper(audio_data: np.ndarray, samplerate: int) -> np.ndarray:
"""Normalize decoded audio into a mono 16 kHz float32 waveform for Whisper."""
if samplerate <= 0:
raise ValueError(f"Invalid audio sample rate: {samplerate}")
waveform = np.asarray(audio_data, dtype=np.float32)
if waveform.ndim == 0:
raise ValueError("Audio waveform must have at least one dimension")
if waveform.ndim > 1:
waveform = np.mean(waveform, axis=1)
if waveform.size == 0:
raise ValueError("Empty audio waveform")
if samplerate != _WHISPER_SAMPLE_RATE_HZ:
target_num_samples = max(int(round(waveform.shape[0] * _WHISPER_SAMPLE_RATE_HZ / samplerate)), 1)
source_positions = np.arange(waveform.shape[0], dtype=np.float64)
target_positions = np.linspace(
0.0,
max(waveform.shape[0] - 1, 0),
num=target_num_samples,
dtype=np.float64,
)
waveform = np.interp(target_positions, source_positions, waveform).astype(np.float32)
return np.ascontiguousarray(np.clip(waveform, -1.0, 1.0), dtype=np.float32)
def _convert_audio_bytes_to_text_without_ffmpeg(raw_bytes: bytes) -> str:
"""Dynin t2s keeps Whisper transcription local to this test module and avoids ffmpeg."""
import whisper
data, samplerate = sf.read(BytesIO(raw_bytes), dtype="float32", always_2d=True)
audio_waveform = _prepare_audio_waveform_for_whisper(data, samplerate)
model = whisper.load_model("small", device="cpu")
try:
transcript = model.transcribe(
audio_waveform,
temperature=0.0,
word_timestamps=True,
condition_on_previous_text=False,
)["text"]
finally:
del model
gc.collect()
return transcript or ""
@pytest.fixture
def dynin_t2s_openai_client(openai_client, monkeypatch):
monkeypatch.setattr(
tests_conftest,
"convert_audio_bytes_to_text",
_convert_audio_bytes_to_text_without_ffmpeg,
)
return openai_client
def _build_t2i_messages(prompt: str) -> list[dict]:
return [{"role": "user", "content": [{"type": "text", "text": f"<|t2i|> {prompt}"}]}]
def _build_t2s_messages(prompt: str) -> list[dict]:
return [{"role": "user", "content": [{"type": "text", "text": f"<|t2s|> {prompt}"}]}]
def _build_i2i_messages(prompt: str) -> list[dict]:
input_image = ImageAsset("2560px-Gfp-wisconsin-madison-the-nature-boardwalk").pil_image.convert("RGB")
buffer = BytesIO()
input_image.save(buffer, format="JPEG")
image_b64 = base64.b64encode(buffer.getvalue()).decode("utf-8")
return [
{
"role": "user",
"content": [
{"type": "text", "text": f"<|i2i|> {prompt}"},
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{image_b64}"}},
],
}
]
@pytest.mark.advanced_model
@pytest.mark.omni
@hardware_test(res={"cuda": "L4", "rocm": "MI325"})
@pytest.mark.parametrize("omni_server", TEST_PARAMS, indirect=True)
def test_send_i2i_request_001(omni_server, openai_client) -> None:
request_config = {
"model": omni_server.model,
"messages": _build_i2i_messages(I2I_PROMPT),
"modalities": ["image"],
"extra_body": {
"sampling_params_list": [dict(_I2I_STAGE_SAMPLING) for _ in range(_STAGE_COUNT)],
},
}
openai_client.send_diffusion_request(request_config)
@pytest.mark.advanced_model
@pytest.mark.omni
@hardware_test(res={"cuda": "L4", "rocm": "MI325"})
@pytest.mark.parametrize("omni_server", TEST_PARAMS, indirect=True)
def test_send_t2i_request_001(omni_server, openai_client) -> None:
request_config = {
"model": omni_server.model,
"messages": _build_t2i_messages(T2I_PROMPT),
"modalities": ["image"],
}
openai_client.send_diffusion_request(request_config)
@pytest.mark.core_model
@pytest.mark.omni
@hardware_test(res={"cuda": "L4", "rocm": "MI325"})
@pytest.mark.parametrize("omni_server", TEST_PARAMS, indirect=True)
def test_send_t2s_request_001(omni_server, dynin_t2s_openai_client) -> None:
request_config = {
"model": omni_server.model,
"messages": _build_t2s_messages(T2S_PROMPT),
"modalities": ["audio"],
}
dynin_t2s_openai_client.send_omni_request(request_config)
@@ -0,0 +1,84 @@
# stage config for running dynin_omni with a 3-stage architecture.
# this config is intended for e2e smoke tests.
stage_args:
- stage_id: 0
stage_type: llm
runtime:
process: true
devices: "0"
max_batch_size: 1
engine_args:
model_stage: token2text
model_arch: DyninOmniForConditionalGeneration
worker_type: generation
scheduler_cls: vllm_omni.core.sched.omni_generation_scheduler.OmniGenerationScheduler
engine_output_type: latent
trust_remote_code: true
gpu_memory_utilization: 0.5
enforce_eager: true
enable_prefix_caching: false
async_scheduling: false
max_num_batched_tokens: 4096
is_comprehension: true
final_output: true
final_output_type: text
- stage_id: 1
stage_type: llm
runtime:
process: true
devices: "0"
max_batch_size: 1
engine_args:
model_stage: token2image
model_arch: DyninOmniForConditionalGeneration
worker_type: generation
scheduler_cls: vllm_omni.core.sched.omni_generation_scheduler.OmniGenerationScheduler
engine_output_type: latent
trust_remote_code: true
gpu_memory_utilization: 0.2
enforce_eager: true
enable_prefix_caching: false
async_scheduling: false
max_num_batched_tokens: 4096
engine_input_source: [0]
custom_process_input_func: vllm_omni.model_executor.stage_input_processors.dynin_omni.token2text_to_token2image
final_output: true
final_output_type: image
- stage_id: 2
stage_type: llm
runtime:
process: true
devices: "0"
max_batch_size: 1
engine_args:
model_stage: token2audio
model_arch: DyninOmniForConditionalGeneration
worker_type: generation
scheduler_cls: vllm_omni.core.sched.omni_generation_scheduler.OmniGenerationScheduler
engine_output_type: latent
trust_remote_code: true
gpu_memory_utilization: 0.2
enforce_eager: true
enable_prefix_caching: false
async_scheduling: false
max_num_batched_tokens: 4096
engine_input_source: [1]
custom_process_input_func: vllm_omni.model_executor.stage_input_processors.dynin_omni.token2image_to_token2audio
final_output: true
final_output_type: audio
runtime:
enabled: true
defaults:
window_size: -1
max_inflight: 1
edges:
- from: 0
to: 1
window_size: -1
- from: 1
to: 2
window_size: -1
@@ -0,0 +1,59 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from .dynin_omni import DyninOmniForConditionalGeneration
from .dynin_omni_common import (
get_dynin_magvit_attr,
get_dynin_modeling_attr,
get_dynin_sampling_attr,
)
if TYPE_CHECKING:
from .dynin_omni_token2audio import DyninOmniToken2Audio
from .dynin_omni_token2image import DyninOmniToken2Image
from .dynin_omni_token2text import DyninOmniToken2Text
_STAGE_EXPORTS = {
"DyninOmniToken2Audio": (".dynin_omni_token2audio", "DyninOmniToken2Audio"),
"DyninOmniToken2Image": (".dynin_omni_token2image", "DyninOmniToken2Image"),
"DyninOmniToken2Text": (".dynin_omni_token2text", "DyninOmniToken2Text"),
}
_MODELING_EXPORTS = {"DyninOmniConfig", "DyninOmniModelLM", "VideoTokenMerger"}
_MAGVIT_EXPORTS = {"VQGANEncoder", "VQGANDecoder", "LFQuantizer", "MAGVITv2"}
def __getattr__(name: str) -> Any:
if name in _STAGE_EXPORTS:
module_name, attr_name = _STAGE_EXPORTS[name]
module = __import__(module_name, globals(), locals(), [attr_name], 1)
return getattr(module, attr_name)
if name in _MODELING_EXPORTS:
return get_dynin_modeling_attr(name)
if name in _MAGVIT_EXPORTS:
return get_dynin_magvit_attr(name)
if name == "get_mask_schedule":
return get_dynin_sampling_attr("get_mask_schedule")
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
__all__ = [
"DyninOmniForConditionalGeneration",
"DyninOmniToken2Audio",
"DyninOmniToken2Image",
"DyninOmniToken2Text",
"DyninOmniConfig",
"DyninOmniModelLM",
"VideoTokenMerger",
"VQGANEncoder",
"VQGANDecoder",
"LFQuantizer",
"MAGVITv2",
"get_mask_schedule",
]
@@ -0,0 +1,744 @@
from __future__ import annotations
from collections.abc import Iterable, Mapping, Sequence
from functools import cached_property
from importlib import import_module
from typing import Any
import numpy as np
import torch
import torch.nn as nn
from vllm.config import VllmConfig
from vllm.config.multimodal import BaseDummyOptions
from vllm.inputs import MultiModalDataDict
from vllm.inputs import MultiModalInput as MultiModalInputs
from vllm.model_executor.models.interfaces import SupportsMultiModal
from vllm.multimodal import MULTIMODAL_REGISTRY
from vllm.multimodal.inputs import (
MultiModalFieldConfig,
MultiModalKwargsItems,
PlaceholderRange,
)
from vllm.multimodal.parse import MultiModalDataItems, MultiModalDataParser
from vllm.multimodal.processing import (
BaseDummyInputsBuilder,
BaseMultiModalProcessor,
BaseProcessingInfo,
ProcessorInputs,
PromptUpdate,
TimingContext,
)
from vllm.sequence import IntermediateTensors
from vllm.v1.outputs import SamplerOutput
from vllm.v1.sample.metadata import SamplingMetadata
from vllm.v1.sample.sampler import Sampler
from vllm_omni.model_executor.models.output_templates import OmniOutput
from .dynin_omni_common import build_zero_input_embeddings
try:
from PIL import Image as PILImage
except Exception: # pragma: no cover
PILImage = None
_MODALITY_ORDER = ("image", "video", "audio")
_MODALITY_ALIASES = {
"img2img": "image",
}
_MODALITY_INPUT_KEY_BY_NAME = {
"image": "pixel_values",
"video": "pixel_values_videos",
"audio": "input_audio_features",
}
_MODALITY_PLACEHOLDER_BY_NAME = {
"image": "<|soi|><|image|><|eoi|>",
"video": "<|sov|><|video|><|eov|>",
"audio": "<|soa|><|audio|><|eoa|>",
}
_MODALITY_INPUT_ALIASES = {
"image": ("pixel_values", "image_embeds", "img2img"),
"video": ("pixel_values_videos", "video_embeds"),
"audio": ("input_audio_features", "audio_embeds"),
}
def _normalize_modality_name(modality: str) -> str:
return _MODALITY_ALIASES.get(modality, modality)
def _get_modality_count(mm_counts: Mapping[str, int], modality: str) -> int:
canonical = _normalize_modality_name(modality)
count = mm_counts.get(canonical, 0)
for alias, target in _MODALITY_ALIASES.items():
if target == canonical:
count += mm_counts.get(alias, 0)
return count
def _normalize_mm_data_aliases(mm_data: MultiModalDataDict) -> MultiModalDataDict:
normalized: dict[str, Any] = {}
for modality, value in mm_data.items():
canonical = _normalize_modality_name(modality)
if canonical in normalized and normalized[canonical] is not None and value is not None:
raise ValueError(
"Dynin received duplicate multimodal inputs for "
f"{canonical!r} via {modality!r}. "
"Provide either the canonical modality or its alias, not both."
)
if canonical not in normalized or normalized[canonical] is None:
normalized[canonical] = value
return normalized
def _get_placeholder_text(modality: str) -> str | None:
modality = _normalize_modality_name(modality)
for base_modality, placeholder in _MODALITY_PLACEHOLDER_BY_NAME.items():
if modality.startswith(base_modality):
return placeholder
return None
class DyninOmniProcessingInfo(BaseProcessingInfo):
def get_data_parser(self) -> MultiModalDataParser:
return DyninOmniMultiModalDataParser(
expected_hidden_size=self._get_expected_hidden_size(),
)
def get_supported_mm_limits(self) -> Mapping[str, int | None]:
limits = {modality: 1 for modality in _MODALITY_ORDER}
for alias, target in _MODALITY_ALIASES.items():
if target in limits:
limits[alias] = limits[target]
return limits
def get_mm_max_tokens_per_item(
self,
seq_len: int,
mm_counts: Mapping[str, int],
) -> Mapping[str, int] | None:
del seq_len, mm_counts
limits = {modality: 1 for modality in _MODALITY_ORDER}
for alias, target in _MODALITY_ALIASES.items():
if target in limits:
limits[alias] = limits[target]
return limits
class DyninOmniDummyInputsBuilder(BaseDummyInputsBuilder[DyninOmniProcessingInfo]):
def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
chunks: list[str] = []
for modality in _MODALITY_ORDER:
placeholder = _get_placeholder_text(modality)
if placeholder is None:
continue
chunks.extend([placeholder] * _get_modality_count(mm_counts, modality))
return " ".join(chunks)
def get_dummy_mm_data(
self,
seq_len: int,
mm_counts: Mapping[str, int],
mm_options: Mapping[str, BaseDummyOptions] | None = None,
) -> MultiModalDataDict:
del seq_len
mm_data: dict[str, Any] = {}
num_images = _get_modality_count(mm_counts, "image")
if num_images > 0:
mm_data["image"] = self._get_dummy_images(
width=224,
height=224,
num_images=num_images,
overrides=mm_options.get("image") if mm_options else None,
)
num_videos = _get_modality_count(mm_counts, "video")
if num_videos > 0:
mm_data["video"] = self._get_dummy_videos(
width=224,
height=224,
num_frames=8,
num_videos=num_videos,
overrides=mm_options.get("video") if mm_options else None,
)
num_audios = _get_modality_count(mm_counts, "audio")
if num_audios > 0:
mm_data["audio"] = self._get_dummy_audios(
length=16000,
num_audios=num_audios,
overrides=mm_options.get("audio") if mm_options else None,
)
return mm_data
class DyninOmniMultiModalDataParser(MultiModalDataParser):
def parse_mm_data(self, mm_data: MultiModalDataDict) -> MultiModalDataItems:
normalized = _normalize_mm_data_aliases(mm_data)
mm_items = super().parse_mm_data(normalized)
for alias, canonical in _MODALITY_ALIASES.items():
if alias in mm_data and canonical in mm_items and alias not in mm_items:
mm_items[alias] = mm_items[canonical]
return mm_items
def _get_audio_with_sr(self, audio: Any) -> tuple[np.ndarray, float | None]:
audio_array, orig_sr = super()._get_audio_with_sr(audio)
if self.audio_resampler.target_sr is None:
return audio_array, None
return audio_array, orig_sr
class DyninOmniMultiModalProcessor(BaseMultiModalProcessor[DyninOmniProcessingInfo]):
@staticmethod
def _find_subsequence(
haystack: list[int],
needle: list[int],
start: int,
) -> int | None:
if not needle:
return None
max_start = len(haystack) - len(needle)
if max_start < start:
return None
for idx in range(start, max_start + 1):
if haystack[idx : idx + len(needle)] == needle:
return idx
return None
@staticmethod
def _make_disabled_embed_mask(length: int) -> torch.Tensor:
return torch.zeros(length, dtype=torch.bool)
@staticmethod
def _encode_prompt_to_token_ids(
prompt: str | list[int],
tokenizer: Any | None,
) -> list[int]:
if isinstance(prompt, str):
if tokenizer is None:
raise ValueError("Tokenizer is required to process string prompts for Dynin multimodal inputs.")
return tokenizer.encode(prompt, add_special_tokens=False)
return list(prompt)
@staticmethod
def _ensure_non_empty_prompt_ids(
prompt_token_ids: list[int],
tokenizer: Any | None,
) -> list[int]:
if prompt_token_ids:
return prompt_token_ids
fallback_id = None
if tokenizer is not None:
fallback_id = getattr(tokenizer, "bos_token_id", None)
if fallback_id is None:
fallback_id = getattr(tokenizer, "eos_token_id", None)
if fallback_id is None:
fallback_id = getattr(tokenizer, "pad_token_id", None)
return [0 if fallback_id is None else int(fallback_id)]
@classmethod
def _image_to_chw_float_tensor(cls, image: Any) -> torch.Tensor:
if isinstance(image, torch.Tensor):
tensor = image.detach()
elif isinstance(image, np.ndarray):
tensor = torch.from_numpy(image)
elif PILImage is not None and isinstance(image, PILImage.Image):
tensor = torch.from_numpy(np.asarray(image).copy())
else:
raise TypeError(f"Unsupported image item type: {type(image)!r}")
if tensor.ndim == 2:
tensor = tensor.unsqueeze(-1)
if tensor.ndim != 3:
raise ValueError(f"Expected 3D image tensor, got shape={tuple(tensor.shape)}")
if tensor.shape[-1] in (1, 3, 4) and tensor.shape[0] not in (1, 3, 4):
tensor = tensor.permute(2, 0, 1)
if tensor.shape[0] == 1:
tensor = tensor.repeat(3, 1, 1)
if tensor.shape[0] == 4:
tensor = tensor[:3]
tensor = tensor.to(dtype=torch.float32)
if tensor.numel() > 0 and torch.max(tensor) > 1.0:
tensor = tensor / 255.0
return tensor.contiguous()
@classmethod
def _video_to_tchw_float_tensor(cls, video: Any) -> torch.Tensor:
if isinstance(video, (list, tuple)) and not isinstance(video, torch.Tensor):
frames = [cls._image_to_chw_float_tensor(frame) for frame in video]
if not frames:
return torch.zeros((1, 3, 1, 1), dtype=torch.float32)
return torch.stack(frames, dim=0).contiguous()
if isinstance(video, torch.Tensor):
tensor = video.detach()
elif isinstance(video, np.ndarray):
tensor = torch.from_numpy(video)
else:
raise TypeError(f"Unsupported video item type: {type(video)!r}")
if tensor.ndim == 3:
return cls._image_to_chw_float_tensor(tensor).unsqueeze(0).contiguous()
if tensor.ndim != 4:
raise ValueError(f"Expected 4D video tensor, got shape={tuple(tensor.shape)}")
if tensor.shape[-1] in (1, 3, 4) and tensor.shape[1] not in (1, 3, 4):
tensor = tensor.permute(0, 3, 1, 2)
if tensor.shape[1] == 1:
tensor = tensor.repeat(1, 3, 1, 1)
if tensor.shape[1] == 4:
tensor = tensor[:, :3]
tensor = tensor.to(dtype=torch.float32)
if tensor.numel() > 0 and torch.max(tensor) > 1.0:
tensor = tensor / 255.0
return tensor.contiguous()
@staticmethod
def _audio_to_float_tensor(audio: Any) -> torch.Tensor:
if isinstance(audio, tuple) and len(audio) == 2:
audio = audio[0]
if isinstance(audio, torch.Tensor):
tensor = audio.detach()
elif isinstance(audio, np.ndarray):
tensor = torch.from_numpy(audio)
else:
tensor = torch.as_tensor(audio)
tensor = tensor.to(dtype=torch.float32).contiguous().view(-1)
if tensor.numel() == 0:
return torch.zeros((16000,), dtype=torch.float32)
max_abs = torch.max(torch.abs(tensor))
if max_abs > 1.0:
tensor = tensor / max_abs
return tensor.contiguous()
@classmethod
def _convert_modality_item(cls, modality: str, item: Any) -> torch.Tensor:
if modality == "image":
return cls._image_to_chw_float_tensor(item)
if modality == "video":
return cls._video_to_tchw_float_tensor(item)
if modality == "audio":
return cls._audio_to_float_tensor(item)
raise ValueError(f"Unsupported modality for Dynin processor: {modality}")
def _build_modality_kwargs(
self,
modality: str,
modality_items: Sequence[Any],
) -> Sequence[Any]:
modality = _normalize_modality_name(modality)
input_key = _MODALITY_INPUT_KEY_BY_NAME[modality]
tensor_items = [self._convert_modality_item(modality, item) for item in modality_items]
mm_kwargs = MultiModalKwargsItems.from_hf_inputs(
{input_key: tensor_items},
{input_key: MultiModalFieldConfig.batched(modality)},
)
return mm_kwargs[modality]
def _build_placeholder_ranges(
self,
*,
modality: str,
item_count: int,
prompt_token_ids: list[int],
tokenizer: Any | None,
search_start: int,
) -> tuple[list[PlaceholderRange], int]:
ranges: list[PlaceholderRange] = []
for _ in range(item_count):
placeholder_text = _get_placeholder_text(modality)
placeholder_token_ids: list[int] = []
if placeholder_text and tokenizer is not None:
placeholder_token_ids = tokenizer.encode(
placeholder_text,
add_special_tokens=False,
)
found_offset = None
if placeholder_token_ids:
found_offset = self._find_subsequence(
prompt_token_ids,
placeholder_token_ids,
search_start,
)
if found_offset is None:
found_offset = min(search_start, len(prompt_token_ids) - 1)
placeholder_len = 1
else:
placeholder_len = len(placeholder_token_ids)
ranges.append(
PlaceholderRange(
offset=found_offset,
length=placeholder_len,
is_embed=self._make_disabled_embed_mask(placeholder_len),
)
)
search_start = found_offset + placeholder_len
return ranges, search_start
def _get_mm_fields_config(
self,
hf_inputs: Any,
hf_processor_mm_kwargs: Mapping[str, object],
) -> Mapping[str, MultiModalFieldConfig]:
del hf_inputs, hf_processor_mm_kwargs
return {}
def _get_prompt_updates(
self,
mm_items: MultiModalDataItems,
hf_processor_mm_kwargs: Mapping[str, object],
out_mm_kwargs: MultiModalKwargsItems,
) -> Sequence[PromptUpdate]:
del mm_items, hf_processor_mm_kwargs, out_mm_kwargs
return []
def apply(
self,
inputs: ProcessorInputs,
timing_ctx: TimingContext,
) -> MultiModalInputs:
prompt = inputs.prompt
mm_items = inputs.mm_data_items
with timing_ctx.record("get_mm_hashes"):
mm_hashes = inputs.get_mm_hashes(self.info.model_id)
tokenizer = self.info.ctx.tokenizer
prompt_token_ids = self._encode_prompt_to_token_ids(prompt, tokenizer)
prompt_token_ids = self._ensure_non_empty_prompt_ids(prompt_token_ids, tokenizer)
mm_kwargs_by_modality: dict[str, Sequence[Any]] = {}
mm_placeholders: dict[str, list[PlaceholderRange]] = {}
search_start = 0
mm_counts = mm_items.get_all_counts()
for modality in _MODALITY_ORDER:
item_count = mm_counts.get(modality, 0)
if item_count <= 0:
continue
modality_items = mm_items[modality].get_all()
if len(modality_items) != item_count:
raise RuntimeError(
f"Parsed {len(modality_items)} items but expected {item_count} for modality={modality!r}"
)
mm_kwargs_by_modality[modality] = self._build_modality_kwargs(
modality,
modality_items,
)
placeholder_ranges, search_start = self._build_placeholder_ranges(
modality=modality,
item_count=item_count,
prompt_token_ids=prompt_token_ids,
tokenizer=tokenizer,
search_start=search_start,
)
mm_placeholders[modality] = placeholder_ranges
return MultiModalInputs(
type="multimodal",
prompt_token_ids=prompt_token_ids,
mm_kwargs=MultiModalKwargsItems(mm_kwargs_by_modality),
mm_hashes=mm_hashes,
mm_placeholders=mm_placeholders,
)
class DyninOmniStageBase(nn.Module):
stage_name = "Dynin stage"
def make_empty_intermediate_tensors(
self,
batch_size: int,
dtype: torch.dtype,
device: torch.device,
) -> IntermediateTensors:
del batch_size, dtype, device
return IntermediateTensors({})
def embed_input_ids(
self,
input_ids: torch.Tensor,
multimodal_embeddings: Any = None,
is_multimodal: torch.Tensor | None = None,
**kwargs: Any,
) -> torch.Tensor:
del multimodal_embeddings, is_multimodal, kwargs
return build_zero_input_embeddings(
input_ids=input_ids,
hidden_size=self.hidden_size,
stage_name=self.stage_name,
)
def load_weights(
self,
weights: Iterable[tuple[str, torch.Tensor]],
) -> set[str]:
return {name for name, _ in weights}
def compute_logits(
self,
hidden_states: torch.Tensor | OmniOutput,
sampling_metadata: Any = None,
) -> torch.Tensor | None:
del hidden_states, sampling_metadata
return None
@MULTIMODAL_REGISTRY.register_processor(
DyninOmniMultiModalProcessor,
info=DyninOmniProcessingInfo,
dummy_inputs=DyninOmniDummyInputsBuilder,
)
class DyninOmniForConditionalGeneration(nn.Module, SupportsMultiModal):
supports_multimodal_raw_input_only = True
STAGE_ALIAS = {
"tokenizer": "token2text",
"token2token": "token2text",
"detok_text": "token2text",
"token2img": "token2image",
"token2wav": "token2audio",
"token2speech": "token2audio",
}
STAGE_IMPL = {
"token2text": (".dynin_omni_token2text", "DyninOmniToken2Text"),
"token2image": (".dynin_omni_token2image", "DyninOmniToken2Image"),
"token2audio": (".dynin_omni_token2audio", "DyninOmniToken2Audio"),
}
_STAGE_IMPL_CACHE: dict[str, type[nn.Module]] = {}
@classmethod
def get_placeholder_str(cls, modality: str, i: int) -> str | None:
del i
return _get_placeholder_text(modality)
@classmethod
def _resolve_stage_impl_class(cls, model_stage: str) -> type[nn.Module]:
impl = cls._STAGE_IMPL_CACHE.get(model_stage)
if impl is not None:
return impl
module_name, class_name = cls.STAGE_IMPL[model_stage]
module = import_module(module_name, package=__package__)
impl = getattr(module, class_name)
cls._STAGE_IMPL_CACHE[model_stage] = impl
return impl
@classmethod
def _normalize_stage_name(cls, raw_stage: str) -> str:
normalized = cls.STAGE_ALIAS.get(raw_stage, raw_stage)
if normalized not in cls.STAGE_IMPL:
raise ValueError(
"Unsupported DYNIN omni model_stage: "
f"{raw_stage} (normalized={normalized}). "
f"Supported: {sorted(cls.STAGE_IMPL.keys())}"
)
return normalized
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
super().__init__()
raw_stage = str(getattr(vllm_config.model_config, "model_stage", "token2text")).lower()
self.model_stage = self._normalize_stage_name(raw_stage)
impl_cls = self._resolve_stage_impl_class(self.model_stage)
self.impl = impl_cls(vllm_config=vllm_config, prefix=prefix)
self.model = self.impl
self.has_preprocess = False
self.has_postprocess = False
self.have_multimodal_outputs = getattr(self.impl, "have_multimodal_outputs", True)
self.requires_raw_input_tokens = getattr(self.impl, "requires_raw_input_tokens", True)
self.language_model = self._resolve_language_model()
def _resolve_language_model(self) -> Any | None:
if hasattr(self.impl, "get_language_model"):
language_model = self.impl.get_language_model()
if language_model is not None:
return language_model
if hasattr(self.impl, "language_model"):
language_model = getattr(self.impl, "language_model")
if language_model is not None:
return language_model
if self.model_stage == "token2text":
return getattr(self.impl, "model", None)
return None
def get_language_model(self) -> Any | None:
return self.language_model
@cached_property
def sampler(self):
if hasattr(self.model, "sampler"):
return self.model.sampler
if self.language_model is not None and hasattr(self.language_model, "sampler"):
return self.language_model.sampler
return Sampler()
def init_multi_modal(self, thinker_config: Any = None) -> None:
if hasattr(self.model, "init_multi_modal"):
self.model.init_multi_modal(thinker_config)
def _collect_multimodal_inputs(self, **kwargs: Any) -> dict[str, Any]:
mm_inputs: dict[str, Any] = {}
for modality, aliases in _MODALITY_INPUT_ALIASES.items():
for alias in aliases:
if alias in kwargs and kwargs[alias] is not None:
mm_inputs[modality] = kwargs[alias]
break
return mm_inputs
def _normalize_loaded_weight_names(
self,
loaded: set[str],
expected_param_names: set[str],
) -> set[str]:
if self.model_stage != "token2text":
return loaded
normalized_loaded: set[str] = set()
prefixes = ("", "impl.", "impl.model.")
for name in loaded:
for prefix in prefixes:
candidate = f"{prefix}{name}" if prefix else name
if candidate in expected_param_names:
normalized_loaded.add(candidate)
break
if len(normalized_loaded) < len(expected_param_names):
normalized_loaded.update(expected_param_names)
return normalized_loaded
def forward(
self,
input_ids: torch.Tensor | None = None,
positions: torch.Tensor | None = None,
intermediate_tensors: IntermediateTensors | None = None,
inputs_embeds: torch.Tensor | None = None,
**kwargs: Any,
) -> OmniOutput:
return self.model(
input_ids=input_ids,
positions=positions,
intermediate_tensors=intermediate_tensors,
inputs_embeds=inputs_embeds,
**kwargs,
)
def make_empty_intermediate_tensors(
self,
batch_size: int,
dtype: torch.dtype,
device: torch.device,
) -> IntermediateTensors:
return self.model.make_empty_intermediate_tensors(batch_size, dtype, device)
def embed_input_ids(
self,
input_ids: torch.Tensor,
multimodal_embeddings: Any = None,
is_multimodal: torch.Tensor | None = None,
**kwargs: Any,
) -> torch.Tensor:
squeezed_batch = False
staged_input_ids = input_ids
if input_ids.ndim == 0:
staged_input_ids = input_ids.view(1, 1)
squeezed_batch = True
elif input_ids.ndim == 1:
staged_input_ids = input_ids.unsqueeze(0)
squeezed_batch = True
embeddings = self.model.embed_input_ids(
staged_input_ids,
multimodal_embeddings=multimodal_embeddings,
is_multimodal=is_multimodal,
**kwargs,
)
if squeezed_batch and isinstance(embeddings, torch.Tensor):
if embeddings.ndim == 3 and embeddings.shape[0] == 1:
return embeddings.squeeze(0)
if embeddings.ndim == 2 and input_ids.ndim == 0 and embeddings.shape[0] == 1:
return embeddings
return embeddings
def embed_multimodal(self, **kwargs: Any) -> Any:
if hasattr(self.model, "embed_multimodal"):
return self.model.embed_multimodal(**kwargs)
self._collect_multimodal_inputs(**kwargs)
return None
def load_weights(
self,
weights: Iterable[tuple[str, torch.Tensor]],
) -> set[str]:
loaded = self.model.load_weights(weights)
if loaded is None:
loaded = set()
expected_param_names = {name for name, _ in self.named_parameters()}
if not expected_param_names:
return loaded
return self._normalize_loaded_weight_names(loaded, expected_param_names)
def compute_logits(
self,
hidden_states: torch.Tensor | OmniOutput,
sampling_metadata: Any = None,
) -> torch.Tensor | None:
return self.model.compute_logits(hidden_states, sampling_metadata=sampling_metadata)
def sample(
self,
logits: torch.Tensor,
sampling_metadata: SamplingMetadata,
) -> SamplerOutput | None:
if hasattr(self.model, "sample"):
return self.model.sample(logits, sampling_metadata)
if self.language_model is not None and hasattr(self.language_model, "sample"):
return self.language_model.sample(logits, sampling_metadata)
return None
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,274 @@
from __future__ import annotations
import os
import tempfile
from pathlib import Path
from typing import Any
import torch
from vllm.config import VllmConfig
from vllm.logger import init_logger
from vllm.sequence import IntermediateTensors
from vllm_omni.model_executor.models.output_templates import OmniOutput
from .dynin_omni import DyninOmniStageBase
from .dynin_omni_common import (
DetokTarget,
_looks_like_hf_repo_id,
coerce_token_ids_1d,
normalize_runtime_info,
resolve_dynin_infer_sources,
resolve_hidden_size,
unwrap_first_value,
)
logger = init_logger(__name__)
def _get_hf_token() -> str | None:
return os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACE_HUB_TOKEN")
def _ensure_remote_s2u_vendor_root(
*,
repo_id: str,
local_files_only: bool,
) -> str | None:
if local_files_only or not _looks_like_hf_repo_id(repo_id):
return None
existing = os.environ.get("DYNIN_S2U_VENDOR_ROOT")
if existing:
existing_path = Path(existing).expanduser().resolve()
if existing_path.is_dir():
return str(existing_path)
try:
from huggingface_hub import snapshot_download
except Exception as e:
logger.warning("huggingface_hub unavailable; cannot fetch s2u_vendor from %s: %s", repo_id, e)
return None
token = _get_hf_token()
last_error: Exception | None = None
revisions: list[str | None] = [None]
for revision in revisions:
try:
snapshot_dir = snapshot_download(
repo_id=repo_id,
revision=revision,
allow_patterns=["s2u_vendor/**"],
token=token,
)
except TypeError:
try:
snapshot_dir = snapshot_download(
repo_id=repo_id,
revision=revision,
allow_patterns=["s2u_vendor/**"],
)
except Exception as e:
last_error = e
continue
except Exception as e:
last_error = e
continue
vendor_root = (Path(snapshot_dir) / "s2u_vendor").resolve()
if vendor_root.is_dir():
os.environ["DYNIN_S2U_VENDOR_ROOT"] = str(vendor_root)
logger.info("Using remote S2U vendor root: %s", vendor_root)
return str(vendor_root)
if last_error is not None:
logger.warning("Failed to download remote s2u_vendor from %s: %s", repo_id, last_error)
return None
class DyninOmniToken2Audio(DyninOmniStageBase):
"""Stage-3: token detokenization to speech (or pass-through)."""
stage_name = "Dynin token2audio"
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
del prefix
super().__init__()
self.vllm_config = vllm_config
self.have_multimodal_outputs = True
self.requires_raw_input_tokens = True
self.hidden_size = resolve_hidden_size(vllm_config=vllm_config)
self._vq_audio = None
self._vq_audio_path: str | None = None
self._vq_audio_local_files_only: bool | None = None
def forward(
self,
input_ids: torch.Tensor | None = None,
positions: torch.Tensor | None = None,
intermediate_tensors: IntermediateTensors | None = None,
inputs_embeds: torch.Tensor | None = None,
**kwargs: Any,
) -> OmniOutput:
del positions, intermediate_tensors, inputs_embeds
if input_ids is None:
raise ValueError("token2audio stage requires input_ids")
runtime_info = normalize_runtime_info(kwargs.get("runtime_additional_information"))
detok_id = int(unwrap_first_value(runtime_info.get("detok_id"), 0))
tokens = coerce_token_ids_1d(input_ids)
if detok_id != DetokTarget.AUDIO:
return OmniOutput(
text_hidden_states=None,
multimodal_outputs={
"token_ids": tokens,
"detok_id": torch.tensor([detok_id], dtype=torch.long, device=tokens.device),
},
)
audio, sample_rate = self._decode_audio_tokens(tokens, runtime_info=runtime_info)
return OmniOutput(
text_hidden_states=None,
multimodal_outputs={
"speech": audio,
"audio": audio,
"sr": torch.tensor([sample_rate], dtype=torch.int, device=audio.device),
"detok_id": torch.tensor([detok_id], dtype=torch.long, device=audio.device),
},
)
def _decode_audio_tokens(self, tokens: torch.Tensor, runtime_info: dict[str, Any]) -> tuple[torch.Tensor, int]:
# Follow DYNIN validation path:
# token list -> "<|speech_x|>" string -> vq_model_audio.decode(...).
vq_audio = self._ensure_vq_audio(runtime_info=runtime_info, ref_device=tokens.device)
audio_codebook_size = int(unwrap_first_value(runtime_info.get("audio_codebook_size"), 4096))
audio_vocab_offset = unwrap_first_value(
runtime_info.get("audio_vocab_offset"),
unwrap_first_value(runtime_info.get("t2s_vocab_start"), None),
)
token_ids = tokens.to(torch.long)
if audio_vocab_offset is not None:
off = int(audio_vocab_offset)
token_ids = torch.where(token_ids >= off, token_ids - off, token_ids)
token_ids = token_ids[(token_ids >= 0) & (token_ids < audio_codebook_size)]
if token_ids.numel() == 0:
raise RuntimeError("Audio detokenizer got no valid audio token ids.")
speech_unit_str = " ".join(map(str, token_ids.detach().cpu().tolist()))
speech_unit_for_decode = "".join(f"<|speech_{unit}|>" for unit in speech_unit_str.split(" ") if unit != "")
condition = unwrap_first_value(
runtime_info.get("condition"),
unwrap_first_value(runtime_info.get("t2s_condition"), None),
)
output_wav_file = unwrap_first_value(runtime_info.get("output_wav_file"), None)
created_tmp = False
if output_wav_file is None:
fd, tmp_wav = tempfile.mkstemp(prefix="dynin_t2s_", suffix=".wav")
os.close(fd)
output_wav_file = tmp_wav
created_tmp = True
audio_array = vq_audio.decode(speech_unit_for_decode, condition=condition, output_wav_file=output_wav_file)
if created_tmp:
try:
os.remove(output_wav_file)
except Exception:
pass
if not isinstance(audio_array, torch.Tensor):
audio_array = torch.as_tensor(audio_array, dtype=torch.float32, device=tokens.device)
else:
audio_array = audio_array.to(device=tokens.device, dtype=torch.float32)
if audio_array.ndim > 1:
audio_array = audio_array.reshape(-1)
audio_array = audio_array.contiguous()
sample_rate = int(
unwrap_first_value(
runtime_info.get("sr"),
unwrap_first_value(runtime_info.get("sample_rate"), 24000),
)
)
try:
cfg = getattr(vq_audio, "u2s_config", None)
cfg_sr = getattr(cfg, "sampling_rate", None)
if cfg_sr is None:
cfg_sr = getattr(getattr(cfg, "data", None), "sampling_rate", None)
if cfg_sr is not None:
sample_rate = int(cfg_sr)
except Exception:
pass
return audio_array, sample_rate
def _ensure_vq_audio(self, runtime_info: dict[str, Any], ref_device: torch.device) -> Any:
sources = resolve_dynin_infer_sources(vllm_config=self.vllm_config, runtime_info=runtime_info)
model_path = str(sources.vq_audio_source)
local_files_only = bool(sources.vq_audio_local_files_only)
_ensure_remote_s2u_vendor_root(
repo_id=model_path,
local_files_only=local_files_only,
)
if (
self._vq_audio is None
or self._vq_audio_path != model_path
or self._vq_audio_local_files_only != local_files_only
):
logger.info(
"Loading DYNIN audio detokenizer from %s (local_files_only=%s)",
model_path,
local_files_only,
)
try:
from transformers import AutoModel
except Exception as e:
raise RuntimeError(
"transformers is required to load EMOVASpeechTokenizer remote code from Hugging Face."
) from e
try:
self._vq_audio = AutoModel.from_pretrained(
model_path,
trust_remote_code=True,
local_files_only=local_files_only,
low_cpu_mem_usage=False,
)
except TypeError:
try:
self._vq_audio = AutoModel.from_pretrained(
model_path,
trust_remote_code=True,
local_files_only=local_files_only,
)
except TypeError:
self._vq_audio = AutoModel.from_pretrained(
model_path,
trust_remote_code=True,
)
except Exception as e:
raise RuntimeError(
f"Failed to load EMOVASpeechTokenizer from Hugging Face remote code for model path '{model_path}'."
) from e
if not hasattr(self._vq_audio, "decode"):
raise RuntimeError(
"Loaded audio tokenizer does not expose decode(). "
"Check HF config.json auto_map/model_type and ensure trust_remote_code=True."
)
self._vq_audio.eval()
self._vq_audio.requires_grad_(False)
self._vq_audio_path = model_path
self._vq_audio_local_files_only = local_files_only
if hasattr(self._vq_audio, "to"):
self._vq_audio = self._vq_audio.to(ref_device)
return self._vq_audio
def embed_multimodal(self, **kwargs: Any) -> Any:
del kwargs
return None
@@ -0,0 +1,150 @@
from __future__ import annotations
import os
from typing import Any
import torch
from vllm.config import VllmConfig
from vllm.logger import init_logger
from vllm.sequence import IntermediateTensors
from vllm_omni.model_executor.models.output_templates import OmniOutput
from .dynin_omni import DyninOmniStageBase
from .dynin_omni_common import (
DetokTarget,
_to_bool,
coerce_token_ids_1d,
get_dynin_magvit_attr,
normalize_runtime_info,
resolve_dynin_infer_sources,
resolve_hidden_size,
unwrap_first_value,
)
logger = init_logger(__name__)
class DyninOmniToken2Image(DyninOmniStageBase):
"""Stage-2: token detokenization to image (or pass-through)."""
stage_name = "Dynin token2image"
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
del prefix
super().__init__()
self.vllm_config = vllm_config
self.have_multimodal_outputs = True
self.requires_raw_input_tokens = True
self.hidden_size = resolve_hidden_size(vllm_config=vllm_config)
self._vq_model = None
self._vq_model_path: str | None = None
self._vq_local_files_only: bool | None = None
def forward(
self,
input_ids: torch.Tensor | None = None,
positions: torch.Tensor | None = None,
intermediate_tensors: IntermediateTensors | None = None,
inputs_embeds: torch.Tensor | None = None,
**kwargs: Any,
) -> OmniOutput:
del positions, intermediate_tensors, inputs_embeds
if input_ids is None:
raise ValueError("token2image stage requires input_ids")
runtime_info = normalize_runtime_info(kwargs.get("runtime_additional_information"))
detok_id = int(unwrap_first_value(runtime_info.get("detok_id"), 0))
tokens = coerce_token_ids_1d(input_ids)
if detok_id != DetokTarget.IMAGE:
return OmniOutput(
text_hidden_states=None,
multimodal_outputs={
"token_ids": tokens,
"detok_id": torch.tensor([detok_id], dtype=torch.long, device=tokens.device),
},
)
image = self._decode_image_tokens(tokens, runtime_info=runtime_info)
return OmniOutput(
text_hidden_states=None,
multimodal_outputs={
"image": image,
"detok_id": torch.tensor([detok_id], dtype=torch.long, device=image.device),
},
)
def _decode_image_tokens(self, tokens: torch.Tensor, runtime_info: dict[str, Any]) -> torch.Tensor:
# Follow DYNIN validation path:
# tokens -> clamp -> vq_model.decode_code -> (x+1)/2 -> [0,1].
vq_model = self._ensure_vq_model(runtime_info=runtime_info, ref_device=tokens.device)
codebook_size = int(unwrap_first_value(runtime_info.get("codebook_size"), 8192))
image_vocab_offset = unwrap_first_value(runtime_info.get("image_vocab_offset"), None)
if image_vocab_offset is None:
text_vocab_size = unwrap_first_value(runtime_info.get("text_vocab_size"), None)
num_new_special_tokens = int(unwrap_first_value(runtime_info.get("num_new_special_tokens"), 0))
if text_vocab_size is not None:
image_vocab_offset = int(text_vocab_size) + num_new_special_tokens
token_ids = tokens.to(torch.long)
if image_vocab_offset is not None:
off = int(image_vocab_offset)
token_ids = torch.where(token_ids >= off, token_ids - off, token_ids)
token_ids = torch.clamp(token_ids, min=0, max=max(0, codebook_size - 1))
token_ids = token_ids.unsqueeze(0)
decoded = vq_model.decode_code(token_ids)
decoded = torch.clamp((decoded + 1.0) / 2.0, min=0.0, max=1.0)
if decoded.ndim != 4 or decoded.shape[0] == 0:
raise RuntimeError(f"Unexpected MAGVIT decode output shape: {tuple(decoded.shape)}")
return decoded[0].contiguous()
def _ensure_vq_model(self, runtime_info: dict[str, Any], ref_device: torch.device) -> Any:
sources = resolve_dynin_infer_sources(vllm_config=self.vllm_config, runtime_info=runtime_info)
model_path = str(sources.vq_image_source)
local_files_only = bool(sources.vq_image_local_files_only)
if self._vq_model is None or self._vq_model_path != model_path or self._vq_local_files_only != local_files_only:
disable_xet = unwrap_first_value(
runtime_info.get("hf_hub_disable_xet"),
unwrap_first_value(runtime_info.get("disable_hf_xet"), True),
)
if _to_bool(disable_xet, default=True):
os.environ.setdefault("HF_HUB_DISABLE_XET", "1")
logger.info(
"Loading DYNIN image detokenizer from %s (local_files_only=%s)",
model_path,
local_files_only,
)
try:
MAGVITv2 = get_dynin_magvit_attr(
"MAGVITv2",
source=model_path,
local_files_only=local_files_only,
)
try:
self._vq_model = MAGVITv2.from_pretrained(
model_path,
local_files_only=local_files_only,
)
except TypeError:
self._vq_model = MAGVITv2.from_pretrained(model_path)
except Exception as e:
raise RuntimeError(
"Failed to load MAGVITv2 from local DYNIN submodel implementation "
f"for model path '{model_path}'. "
"If your environment cannot access huggingface.co, set "
"additional_information.vq_model_image_path to a local MAGVITv2 directory "
"and set additional_information.vq_model_image_local_files_only=true."
) from e
self._vq_model.eval()
self._vq_model.requires_grad_(False)
self._vq_model_path = model_path
self._vq_local_files_only = local_files_only
if hasattr(self._vq_model, "to"):
self._vq_model = self._vq_model.to(ref_device)
return self._vq_model
def embed_multimodal(self, **kwargs: Any) -> Any:
del kwargs
return None
File diff suppressed because it is too large. Load diff
@@ -157,6 +157,11 @@ _OMNI_MODELS = {
"VoxtralTTSAudioGenerationForConditionalGeneration",
),
"VoxtralTTSAudioTokenizer": ("voxtral_tts", "voxtral_tts_audio_tokenizer", "VoxtralTTSAudioTokenizer"),
"DyninOmniForConditionalGeneration": (
"dynin_omni",
"dynin_omni",
"DyninOmniForConditionalGeneration",
),
}
@@ -0,0 +1,80 @@
stage_args:
- stage_id: 0
stage_type: llm
runtime:
devices: "0"
max_batch_size: 1
engine_args:
model_stage: token2text
model_arch: DyninOmniForConditionalGeneration
worker_type: generation
scheduler_cls: vllm_omni.core.sched.omni_generation_scheduler.OmniGenerationScheduler
engine_output_type: latent
trust_remote_code: true
gpu_memory_utilization: 0.5
enforce_eager: true
enable_prefix_caching: false
async_scheduling: false
max_num_batched_tokens: 32768
is_comprehension: true
final_output: true
final_output_type: text
- stage_id: 1
stage_type: llm
runtime:
devices: "0"
max_batch_size: 1
engine_args:
model_stage: token2image
model_arch: DyninOmniForConditionalGeneration
worker_type: generation
scheduler_cls: vllm_omni.core.sched.omni_generation_scheduler.OmniGenerationScheduler
engine_output_type: latent
trust_remote_code: true
gpu_memory_utilization: 0.1
enforce_eager: true
enable_prefix_caching: false
async_scheduling: false
max_num_batched_tokens: 32768
engine_input_source: [0]
custom_process_input_func: vllm_omni.model_executor.stage_input_processors.dynin_omni.token2text_to_token2image
final_output: true
final_output_type: image
- stage_id: 2
stage_type: llm
runtime:
devices: "0"
max_batch_size: 1
engine_args:
model_stage: token2audio
model_arch: DyninOmniForConditionalGeneration
worker_type: generation
scheduler_cls: vllm_omni.core.sched.omni_generation_scheduler.OmniGenerationScheduler
engine_output_type: latent
trust_remote_code: true
gpu_memory_utilization: 0.1
enforce_eager: true
enable_prefix_caching: false
async_scheduling: false
max_num_batched_tokens: 32768
engine_input_source: [1]
custom_process_input_func: vllm_omni.model_executor.stage_input_processors.dynin_omni.token2image_to_token2audio
final_output: true
final_output_type: audio
# Top-level runtime config (concise): default windows and stage edges
runtime:
enabled: true
defaults:
window_size: -1 # Simplified: trigger downstream only after full upstream completion
max_inflight: 1 # Simplified: process serially within each stage
edges:
- from: 0
to: 1
window_size: -1
- from: 1
to: 2
window_size: -1
@@ -0,0 +1,114 @@
stage_args:
- stage_id: 0
stage_type: llm
runtime:
devices: "0"
max_batch_size: 1
engine_args:
model_stage: token2text
model_arch: DyninOmniForConditionalGeneration
worker_type: generation
scheduler_cls: vllm_omni.core.sched.omni_generation_scheduler.OmniGenerationScheduler
engine_output_type: latent
trust_remote_code: false
enforce_eager: true
enable_prefix_caching: false
async_scheduling: false
max_num_batched_tokens: 32768
output_connectors:
to_stage_1: mooncake_connector
final_output: true
final_output_type: text
- stage_id: 1
stage_type: llm
runtime:
devices: "0"
max_batch_size: 1
engine_args:
model_stage: token2image
model_arch: DyninOmniForConditionalGeneration
worker_type: generation
scheduler_cls: vllm_omni.core.sched.omni_generation_scheduler.OmniGenerationScheduler
engine_output_type: latent
trust_remote_code: false
enforce_eager: true
enable_prefix_caching: false
async_scheduling: false
max_num_batched_tokens: 32768
engine_input_source: [0]
custom_process_input_func: vllm_omni.model_executor.stage_input_processors.dynin_omni.token2text_to_token2image
final_output: true
final_output_type: image
input_connectors:
from_stage_0: mooncake_connector
output_connectors:
to_stage_2: mooncake_connector
- stage_id: 2
stage_type: llm
runtime:
devices: "0"
max_batch_size: 1
engine_args:
model_stage: token2audio
model_arch: DyninOmniForConditionalGeneration
worker_type: generation
scheduler_cls: vllm_omni.core.sched.omni_generation_scheduler.OmniGenerationScheduler
engine_output_type: latent
trust_remote_code: false
enforce_eager: true
enable_prefix_caching: false
async_scheduling: false
max_num_batched_tokens: 32768
engine_input_source: [1]
custom_process_input_func: vllm_omni.model_executor.stage_input_processors.dynin_omni.token2image_to_token2audio
final_output: true
final_output_type: audio
input_connectors:
from_stage_1: mooncake_connector
# Top-level runtime config (concise): default windows and stage edges
runtime:
enabled: true
defaults:
window_size: -1 # Simplified: trigger downstream only after full upstream completion
max_inflight: 1 # Simplified: process serially within each stage
####
# same as Qwen2.5_omni version
# Distributed connectors configuration (optional)
# More connectors will be supported in the future.
connectors:
# Mooncake connector for cross-node/intra-node communication
mooncake_connector:
name: MooncakeConnector
extra:
host: "127.0.0.1"
metadata_server: "http://10.90.67.86:8080/metadata"
master: "10.90.67.86:50051"
segment: 512000000 # 512MB
localbuf: 64000000 # 64MB
proto: "tcp"
# Yuanrong connector for cross-node/intra-node communication
yuanrong_connector:
name: YuanrongConnector
extra:
host: "127.0.0.1"
port: "35000"
# SharedMemory connector for intra-node communication
# Alternative SHM connector with different threshold
shared_memory_connector:
name: SharedMemoryConnector
extra:
shm_threshold_bytes: 65536 # 64KB threshold
####
edges:
- from: 0
to: 1
window_size: -1
- from: 1
to: 2
window_size: -1
@@ -0,0 +1,164 @@
from __future__ import annotations
import json
from typing import Any
import torch
from vllm.inputs import TextPrompt
from vllm_omni.inputs.data import OmniTokensPrompt
def _to_prompt_dict(prompt_item: OmniTokensPrompt | TextPrompt | str | None) -> dict[str, Any]:
if isinstance(prompt_item, dict):
return prompt_item
return {}
def _to_token_id_list(value: Any) -> list[int]:
if isinstance(value, torch.Tensor):
value = value.detach().to("cpu")
if value.ndim == 0:
return [int(value.item())]
if value.ndim > 1:
value = value[0]
return [int(x) for x in value.tolist()]
if isinstance(value, list):
if not value:
return []
if isinstance(value[0], list):
return [int(x) for x in value[0]]
return [int(x) for x in value]
if value is None:
return []
return [int(value)]
def _to_int(value: Any, default: int = 0) -> int:
if isinstance(value, torch.Tensor):
if value.numel() == 0:
return default
return int(value.view(-1)[0].item())
if isinstance(value, list):
if not value:
return default
return int(value[0])
if value is None:
return default
return int(value)
def _normalize_additional_info(value: Any) -> dict[str, Any]:
if not isinstance(value, dict):
return {}
normalized: dict[str, Any] = {}
for key, val in value.items():
if isinstance(val, list):
normalized[key] = val
else:
normalized[key] = [val]
return normalized
def _decode_runtime_bridge_info(value: Any) -> dict[str, Any]:
if isinstance(value, torch.Tensor):
tensor = value.detach().to("cpu").reshape(-1).to(torch.uint8)
raw = bytes(tensor.tolist())
elif isinstance(value, (bytes, bytearray)):
raw = bytes(value)
elif isinstance(value, list):
try:
raw = bytes(int(item) for item in value)
except Exception:
return {}
elif value is None:
return {}
else:
return value if isinstance(value, dict) else {}
if not raw:
return {}
try:
decoded = json.loads(raw.decode("utf-8"))
except Exception:
return {}
return decoded if isinstance(decoded, dict) else {}
def _bridge_tokens(
stage_list,
engine_input_source,
prompt: OmniTokensPrompt | TextPrompt = None,
requires_multimodal_data: bool = False,
):
if not engine_input_source:
raise ValueError("engine_input_source cannot be empty")
source_stage_id = engine_input_source[0]
if source_stage_id >= len(stage_list):
raise IndexError(f"Invalid stage_id: {source_stage_id}")
if stage_list[source_stage_id].engine_outputs is None:
raise RuntimeError(f"Stage {source_stage_id} has no outputs yet")
source_outputs = stage_list[source_stage_id].engine_outputs
next_inputs = []
if not isinstance(prompt, list):
prompt = [prompt]
prompt_meta_by_reqid = {src_out.request_id: _to_prompt_dict(p) for src_out, p in zip(source_outputs, prompt)}
for source_output in source_outputs:
output = source_output.outputs[0]
mm_out = getattr(output, "multimodal_output", None) or {}
token_ids = _to_token_id_list(mm_out.get("token_ids"))
if not token_ids:
token_ids = _to_token_id_list(mm_out.get("text_tokens"))
if not token_ids:
token_ids = list(getattr(output, "token_ids", []) or [])
if not token_ids:
raise RuntimeError(
f"Stage {source_stage_id} output for request {source_output.request_id} has no token_ids"
)
detok_id = _to_int(mm_out.get("detok_id"), default=0)
src_prompt = prompt_meta_by_reqid.get(source_output.request_id, {})
src_additional_info = src_prompt.get("additional_information", {}) or {}
runtime_bridge_info = _decode_runtime_bridge_info(mm_out.get("runtime_info_json"))
if not runtime_bridge_info:
runtime_bridge_info = mm_out.get("runtime_info", {}) or {}
additional_information: dict[str, Any] = _normalize_additional_info(src_additional_info)
additional_information.update(_normalize_additional_info(runtime_bridge_info))
additional_information["detok_id"] = [detok_id]
next_inputs.append(
OmniTokensPrompt(
prompt_token_ids=token_ids,
additional_information=additional_information,
multi_modal_data=(src_prompt.get("multi_modal_data") if requires_multimodal_data else None),
mm_processor_kwargs=None,
)
)
return next_inputs
def token2text_to_token2image(
stage_list,
engine_input_source,
prompt: OmniTokensPrompt | TextPrompt = None,
requires_multimodal_data: bool = False,
):
return _bridge_tokens(stage_list, engine_input_source, prompt, requires_multimodal_data)
def token2image_to_token2audio(
stage_list,
engine_input_source,
prompt: OmniTokensPrompt | TextPrompt = None,
requires_multimodal_data: bool = False,
):
return _bridge_tokens(stage_list, engine_input_source, prompt, requires_multimodal_data)