Compare commits

...
Author SHA1 Message Date
wzliu 26cf9ef4ed update 2025-12-01 10:49:39 +08:00
wzliu 33e03b44c5 added omni mooncake connectors to distributed directory, integrate omni connector to omni-llm, rebased with main (twice).
ray + mooncake connector work on 2 nodes, but needes several updates\n1.start ray inside vllm-omni \n2.placement group optimize \n3.fix close
2025-12-01 10:45:26 +08:00
26 changed files with 2737 additions and 78 deletions

No files matched your search

+1
View File
@@ -232,3 +232,4 @@ configs/development.yaml
# Docker
.dockerignore
Dockerfile.dev
discussion
@@ -283,6 +283,19 @@ def parse_args():
default=None,
help="Path to a .txt file with one prompt per line (preferred).",
)
parser.add_argument(
"--worker-backend",
type=str,
default="process",
choices=["process","ray"],
help="backend"
)
parser.add_argument(
"--ray-address",
type=str,
default=None,
help="Path to a .txt file with one prompt per line (preferred).",
return parser.parse_args()
@@ -0,0 +1,34 @@
# Offline Example of vLLM-omni for Qwen2.5-omni
## 🛠️ Installation
Please refer to [README.md](../../../README.md)
## Run examples (Qwen2.5-omni)
### Multiple Prompts
Download dataset from [seed_tts](https://drive.google.com/file/d/1GlSjVfSHkW3-leKKBlfrjuuTGqQ_xaLP/edit). To get the prompt, you can:
```bash
tar -xf <Your Download Path>/seedtts_testset.tar
cp seedtts_testset/en/meta.lst examples/offline_inference/qwen2_5_omni/meta.lst
python3 examples/offline_inference/qwen2_5_omni/extract_prompts.py \
--input examples/offline_inference/qwen2_5_omni/meta.lst \
--output examples/offline_inference/qwen2_5_omni/top100.txt \
--topk 100
```
Get into the example folder
```bash
cd examples/offline_inference/qwen2_5_omni
```
Then run the command below.
```bash
bash run_multiple_prompts.sh
```
### Single Prompts
Get into the example folder
```bash
cd examples/offline_inference/qwen2_5_omni
```
Then run the command below.
```bash
bash run_single_prompt.sh
```
@@ -0,0 +1,264 @@
import argparse
import os
import os as _os_env_toggle
import random
import numpy as np
import soundfile as sf
import torch
from utils import make_omni_prompt
from vllm.sampling_params import SamplingParams
from vllm_omni.entrypoints.omni_llm import OmniLLM
_os_env_toggle.environ["VLLM_USE_V1"] = "1"
SEED = 42
# Set all random seeds
random.seed(SEED)
np.random.seed(SEED)
torch.manual_seed(SEED)
torch.cuda.manual_seed(SEED)
torch.cuda.manual_seed_all(SEED)
# Make PyTorch deterministic
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
# Set environment variables for deterministic behavior
os.environ["PYTHONHASHSEED"] = str(SEED)
os.environ["CUBLAS_WORKSPACE_CONFIG"] = ":4096:8"
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument(
"--model",
required=True,
help="Path to merged model directory (will be created if downloading).",
)
parser.add_argument("--thinker-model", type=str, default=None)
parser.add_argument("--talker-model", type=str, default=None)
parser.add_argument("--code2wav-model", type=str, default=None)
parser.add_argument(
"--hf-hub-id",
default="Qwen/Qwen2.5-Omni-7B",
help="Hugging Face repo id to download if needed.",
)
parser.add_argument("--hf-revision", default=None, help="Optional HF revision (branch/tag/commit).")
parser.add_argument("--prompts", nargs="+", default=None, help="Input text prompts.")
parser.add_argument("--voice-type", default="default", help="Voice type, e.g., m02, f030, default.")
parser.add_argument(
"--code2wav-dir",
default=None,
help="Path to code2wav folder (contains spk_dict.pt).",
)
parser.add_argument("--dit-ckpt", default=None, help="Path to DiT checkpoint file (e.g., dit.pt).")
parser.add_argument("--bigvgan-ckpt", default=None, help="Path to BigVGAN checkpoint file.")
parser.add_argument("--dtype", default="bfloat16", choices=["float16", "bfloat16", "float32"])
parser.add_argument("--max-model-len", type=int, default=32768)
parser.add_argument(
"--init-sleep-seconds",
type=int,
default=20,
help="Sleep seconds after starting each stage process to allow initialization (default: 20)",
)
parser.add_argument("--thinker-only", action="store_true")
parser.add_argument("--text-only", action="store_true")
parser.add_argument("--do-wave", action="store_true")
parser.add_argument(
"--prompt_type",
choices=[
"text",
"audio",
"audio-long",
"audio-long-chunks",
"audio-long-expand-chunks",
"image",
"video",
"video-frames",
"audio-in-video",
"audio-in-video-v2",
"audio-multi-round",
"badcase-vl",
"badcase-text",
"badcase-image-early-stop",
"badcase-two-audios",
"badcase-two-videos",
"badcase-multi-round",
"badcase-voice-type",
"badcase-voice-type-v2",
"badcase-audio-tower-1",
"badcase-audio-only",
],
default="text",
)
parser.add_argument("--use-torchvision", action="store_true")
parser.add_argument("--tokenize", action="store_true")
parser.add_argument(
"--output-wav",
default="output.wav",
help="[Deprecated] Output wav directory (use --output-dir).",
)
parser.add_argument(
"--output-dir",
default="outputs",
help="Output directory to save text and wav files together.",
)
parser.add_argument(
"--thinker-hidden-states-dir",
default="thinker_hidden_states",
help="Path to thinker hidden states directory.",
)
parser.add_argument(
"--batch-timeout",
type=int,
default=5,
help="Timeout for batching in seconds (default: 5)",
)
parser.add_argument(
"--init-timeout",
type=int,
default=300,
help="Timeout for initializing stages in seconds (default: 300)",
)
parser.add_argument(
"--shm-threshold-bytes",
type=int,
default=65536,
help="Threshold for using shared memory in bytes (default: 65536)",
)
parser.add_argument(
"--enable-stats",
action="store_true",
default=False,
help="Enable writing detailed statistics (default: disabled)",
)
parser.add_argument(
"--txt-prompts",
type=str,
default=None,
help="Path to a .txt file with one prompt per line (preferred).",
)
parser.add_argument(
"--worker-backend",
type=str,
default="process",
choices=["process","ray"],
help="backend"
)
parser.add_argument(
"--ray-address",
type=str,
default=None,
help="Path to a .txt file with one prompt per line (preferred)."
)
args = parser.parse_args()
return args
def main():
args = parse_args()
model_name = args.model
try:
# Preferred: load from txt file (one prompt per line)
if getattr(args, "txt_prompts", None) and args.prompt_type == "text":
with open(args.txt_prompts, encoding="utf-8") as f:
lines = [ln.strip() for ln in f.readlines()]
args.prompts = [ln for ln in lines if ln != ""]
print(f"[Info] Loaded {len(args.prompts)} prompts from {args.txt_prompts}")
except Exception as e:
print(f"[Error] Failed to load prompts: {e}")
raise
if args.prompts is None:
raise ValueError("No prompts provided. Use --prompts ... or --txt-prompts <file.txt> (with --prompt_type text)")
omni_llm = OmniLLM(
model=model_name,
log_stats=args.enable_stats,
log_file=("omni_llm_pipeline.log" if args.enable_stats else None),
init_sleep_seconds=args.init_sleep_seconds,
batch_timeout=args.batch_timeout,
init_timeout=args.init_timeout,
shm_threshold_bytes=args.shm_threshold_bytes,
worker_backend=args.worker_backend,
ray_address=args.ray_address
)
thinker_sampling_params = SamplingParams(
temperature=0.0, # Deterministic - no randomness
top_p=1.0, # Disable nucleus sampling
top_k=-1, # Disable top-k sampling
max_tokens=2048,
seed=SEED, # Fixed seed for sampling
detokenize=True,
repetition_penalty=1.1,
)
talker_sampling_params = SamplingParams(
temperature=0.9,
top_p=0.8,
top_k=40,
max_tokens=2048,
seed=SEED, # Fixed seed for sampling
detokenize=True,
repetition_penalty=1.05,
stop_token_ids=[8294],
)
code2wav_sampling_params = SamplingParams(
temperature=0.0, # Deterministic - no randomness
top_p=1.0, # Disable nucleus sampling
top_k=-1, # Disable top-k sampling
max_tokens=2048,
seed=SEED, # Fixed seed for sampling
detokenize=True,
repetition_penalty=1.1,
)
sampling_params_list = [
thinker_sampling_params,
talker_sampling_params,
code2wav_sampling_params,
]
import time
for i in range(1):
t1 = time.time()
prompt = [make_omni_prompt(args, prompt) for prompt in args.prompts]
print(f"prompt:{prompt}")
omni_outputs = omni_llm.generate(prompt, sampling_params_list)
t2 = time.time()
print(f"==========> time:{t2-t1}")
# Determine output directory: prefer --output-dir; fallback to --output-wav
output_dir = args.output_dir if getattr(args, "output_dir", None) else args.output_wav
os.makedirs(output_dir, exist_ok=True)
for stage_outputs in omni_outputs:
if stage_outputs.final_output_type == "text":
for output in stage_outputs.request_output:
request_id = int(output.request_id)
text_output = output.outputs[0].text
# Save aligned text file per request
prompt_text = args.prompts[request_id]
out_txt = os.path.join(output_dir, f"{request_id:05d}.txt")
lines = []
lines.append("Prompt:\n")
lines.append(str(prompt_text) + "\n")
lines.append("vllm_text_output:\n")
lines.append(str(text_output).strip() + "\n")
try:
with open(out_txt, "w", encoding="utf-8") as f:
f.writelines(lines)
except Exception as e:
print(f"[Warn] Failed writing text file {out_txt}: {e}")
print(f"Request ID: {request_id}, Text saved to {out_txt}")
elif stage_outputs.final_output_type == "audio":
for output in stage_outputs.request_output:
request_id = int(output.request_id)
audio_tensor = output.multimodal_output["audio"]
output_wav = os.path.join(output_dir, f"output_{output.request_id}.wav")
sf.write(output_wav, audio_tensor.detach().cpu().numpy(), samplerate=24000)
print(f"Request ID: {request_id}, Saved audio to {output_wav}")
if __name__ == "__main__":
main()
@@ -0,0 +1,45 @@
#!/usr/bin/env python3
import argparse
from typing import Optional
def extract_prompt(line: str) -> Optional[str]:
# 提取第一个 '|' 与第二个 '|' 之间的内容
i = line.find("|")
if i == -1:
return None
j = line.find("|", i + 1)
if j == -1:
return None
return line[i + 1 : j].strip()
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--input", "-i", required=True, help="Input .lst file path")
parser.add_argument("--output", "-o", required=True, help="Output file path")
parser.add_argument(
"--topk",
"-k",
type=int,
default=100,
help="Extract the top K prompts (default: 100)",
)
args = parser.parse_args()
prompts = []
with open(args.input, encoding="utf-8", errors="ignore") as f:
for line in f:
if len(prompts) >= args.topk:
break
p = extract_prompt(line.rstrip("\n"))
if p:
prompts.append(p)
with open(args.output, "w", encoding="utf-8") as f:
for p in prompts:
f.write(p + "\n")
if __name__ == "__main__":
main()
@@ -0,0 +1,367 @@
from __future__ import annotations
import base64
import logging
import math
import os
import time
import warnings
from functools import lru_cache
from io import BytesIO
import requests
import torch
import torchvision
from packaging import version
from PIL import Image
from torchvision import io, transforms
from torchvision.transforms import InterpolationMode
logger = logging.getLogger(__name__)
IMAGE_FACTOR = 28
MIN_PIXELS = 4 * 28 * 28
MAX_PIXELS = 16384 * 28 * 28
MAX_RATIO = 200
VIDEO_MIN_PIXELS = 128 * 28 * 28
VIDEO_MAX_PIXELS = 768 * 28 * 28
VIDEO_TOTAL_PIXELS = 24576 * 28 * 28
FRAME_FACTOR = 2
FPS = 2.0
FPS_MIN_FRAMES = 4
FPS_MAX_FRAMES = 768
temporal_patch_size = 2
spatial_patch_size = 14
spatial_merge_size = 2
def round_by_factor(number: int, factor: int) -> int:
"""Returns the closest integer to 'number' that is divisible by 'factor'."""
return round(number / factor) * factor
def ceil_by_factor(number: int, factor: int) -> int:
"""Returns the smallest integer greater than or equal to 'number' that is
divisible by 'factor'."""
return math.ceil(number / factor) * factor
def floor_by_factor(number: int, factor: int) -> int:
"""Returns the largest integer less than or equal to 'number' that is
divisible by 'factor'."""
return math.floor(number / factor) * factor
def smart_resize(
height: int,
width: int,
factor: int = IMAGE_FACTOR,
min_pixels: int = MIN_PIXELS,
max_pixels: int = MAX_PIXELS,
) -> tuple[int, int]:
"""
Rescales the image so that the following conditions are met:
1. Both dimensions (height and width) are divisible by 'factor'.
2. The total number of pixels is within the range ['min_pixels', 'max_pixels'].
3. The aspect ratio of the image is maintained as closely as possible.
"""
if max(height, width) / min(height, width) > MAX_RATIO:
raise ValueError(
f"absolute aspect ratio must be smaller than {MAX_RATIO}, got {max(height, width) / min(height, width)}"
)
h_bar = max(factor, round_by_factor(height, factor))
w_bar = max(factor, round_by_factor(width, factor))
if h_bar * w_bar > max_pixels:
beta = math.sqrt((height * width) / max_pixels)
h_bar = floor_by_factor(height / beta, factor)
w_bar = floor_by_factor(width / beta, factor)
elif h_bar * w_bar < min_pixels:
beta = math.sqrt(min_pixels / (height * width))
h_bar = ceil_by_factor(height * beta, factor)
w_bar = ceil_by_factor(width * beta, factor)
return h_bar, w_bar
def fetch_image(ele: dict[str, str | Image.Image], size_factor: int = IMAGE_FACTOR) -> Image.Image:
if "image" in ele:
image = ele["image"]
else:
image = ele["image_url"]
image_obj = None
if isinstance(image, Image.Image):
image_obj = image
elif image.startswith("http://") or image.startswith("https://"):
image_obj = Image.open(requests.get(image, stream=True).raw)
elif image.startswith("file://"):
image_obj = Image.open(image[7:])
elif image.startswith("data:image"):
if "base64," in image:
_, base64_data = image.split("base64,", 1)
data = base64.b64decode(base64_data)
image_obj = Image.open(BytesIO(data))
else:
image_obj = Image.open(image)
if image_obj is None:
raise ValueError(f"Unrecognized image input, support local path, http url, base64 and PIL.Image, got {image}")
image = image_obj.convert("RGB")
# resize
if "resized_height" in ele and "resized_width" in ele:
resized_height, resized_width = smart_resize(
ele["resized_height"],
ele["resized_width"],
factor=size_factor,
)
else:
width, height = image.size
min_pixels = ele.get("min_pixels", MIN_PIXELS)
max_pixels = ele.get("max_pixels", MAX_PIXELS)
resized_height, resized_width = smart_resize(
height,
width,
factor=size_factor,
min_pixels=min_pixels,
max_pixels=max_pixels,
)
image = image.resize((resized_width, resized_height))
return image
def smart_nframes(
ele: dict,
total_frames: int,
video_fps: int | float,
) -> int:
"""calculate the number of frames for video used for model inputs.
Args:
ele (dict): a dict contains the configuration of video.
support either `fps` or `nframes`:
- nframes: the number of frames to extract for model inputs.
- fps: the fps to extract frames for model inputs.
- min_frames: the minimum number of frames of the video,
only used when fps is provided.
- max_frames: the maximum number of frames of the video,
only used when fps is provided.
total_frames (int): the original total number of frames of the video.
video_fps (int | float): the original fps of the video.
Raises:
ValueError: nframes should in interval [FRAME_FACTOR, total_frames].
Returns:
int: the number of frames for video used for model inputs.
"""
assert not ("fps" in ele and "nframes" in ele), "Only accept either `fps` or `nframes`"
if "nframes" in ele:
nframes = round_by_factor(ele["nframes"], FRAME_FACTOR)
else:
fps = ele.get("fps", FPS)
min_frames = ceil_by_factor(ele.get("min_frames", FPS_MIN_FRAMES), FRAME_FACTOR)
max_frames = floor_by_factor(
ele.get("max_frames", min(FPS_MAX_FRAMES, total_frames)),
FRAME_FACTOR,
)
nframes = total_frames / video_fps * fps
nframes = min(max(nframes, min_frames), max_frames)
nframes = round_by_factor(nframes, FRAME_FACTOR)
if not (FRAME_FACTOR <= nframes and nframes <= total_frames):
raise ValueError(f"nframes should in interval [{FRAME_FACTOR}, {total_frames}], but got {nframes}.")
return nframes
def _read_video_torchvision(
ele: dict,
) -> torch.Tensor:
"""read video using torchvision.io.read_video
Args:
ele (dict): a dict contains the configuration of video.
support keys:
- video: the path of video. support "file://", "http://",
"https://" and local path.
- video_start: the start time of video.
- video_end: the end time of video.
Returns:
torch.Tensor: the video tensor with shape (T, C, H, W).
"""
video_path = ele["video"]
if version.parse(torchvision.__version__) < version.parse("0.19.0"):
if "http://" in video_path or "https://" in video_path:
warnings.warn("torchvision < 0.19.0 does not support http/https video path, please upgrade to 0.19.0.")
if "file://" in video_path:
video_path = video_path[7:]
st = time.time()
video, audio, info = io.read_video(
video_path,
start_pts=ele.get("video_start", 0.0),
end_pts=ele.get("video_end", None),
pts_unit="sec",
output_format="TCHW",
)
total_frames, video_fps = video.size(0), info["video_fps"]
total_duration = round(total_frames / video_fps, 3)
logger.info(
f"torchvision: {video_path=}, {total_frames=}, {video_fps=}, duration={total_duration}s, time={time.time() - st:.3f}s"
)
nframes = smart_nframes(ele, total_frames=total_frames, video_fps=video_fps)
idx = torch.linspace(0, total_frames - 1, nframes).round().long()
video = video[idx]
return video, total_duration, nframes
def is_decord_available() -> bool:
import importlib.util
return importlib.util.find_spec("decord") is not None
def _read_video_decord(
ele: dict,
) -> torch.Tensor:
"""read video using decord.VideoReader
Args:
ele (dict): a dict contains the configuration of video.
support keys:
- video: the path of video. support "file://", "http://",
"https://" and local path.
- video_start: the start time of video.
- video_end: the end time of video.
Returns:
torch.Tensor: the video tensor with shape (T, C, H, W).
"""
import decord
video_path = ele["video"]
st = time.time()
vr = decord.VideoReader(video_path)
# TODO: support start_pts and end_pts
if "video_start" in ele or "video_end" in ele:
raise NotImplementedError("not support start_pts and end_pts in decord for now.")
total_frames, video_fps = len(vr), vr.get_avg_fps()
total_duration = round(total_frames / video_fps, 3)
logger.info(f"decord: {video_path=}, {total_frames=}, {video_fps=}, time={time.time() - st:.3f}s")
nframes = smart_nframes(ele, total_frames=total_frames, video_fps=video_fps)
idx = torch.linspace(0, total_frames - 1, nframes).round().long().tolist()
video = vr.get_batch(idx).asnumpy()
video = torch.tensor(video).permute(0, 3, 1, 2) # Convert to TCHW format
return video, total_duration, nframes
VIDEO_READER_BACKENDS = {
"decord": _read_video_decord,
"torchvision": _read_video_torchvision,
}
FORCE_QWENVL_VIDEO_READER = os.getenv("FORCE_QWENVL_VIDEO_READER", None)
@lru_cache(maxsize=1)
def get_video_reader_backend() -> str:
if FORCE_QWENVL_VIDEO_READER is not None:
video_reader_backend = FORCE_QWENVL_VIDEO_READER
elif is_decord_available():
video_reader_backend = "decord"
else:
video_reader_backend = "torchvision"
# print(f"qwen-vl-utils using {video_reader_backend} to read video.",
# file=sys.stderr)
return video_reader_backend
def fetch_video(ele: dict, image_factor: int = IMAGE_FACTOR) -> torch.Tensor | list[Image.Image]:
if isinstance(ele["video"], str):
video_reader_backend = get_video_reader_backend()
video, total_dur, nframes = VIDEO_READER_BACKENDS[video_reader_backend](ele)
frame_timestamps = total_dur * torch.arange(1, nframes + 1) / nframes
grid_timestamps = frame_timestamps[::FRAME_FACTOR]
second_per_grid = grid_timestamps[1] - grid_timestamps[0]
nframes, _, height, width = video.shape
min_pixels = ele.get("min_pixels", VIDEO_MIN_PIXELS)
total_pixels = ele.get("total_pixels", VIDEO_TOTAL_PIXELS)
max_pixels = max(
min(VIDEO_MAX_PIXELS, total_pixels / nframes * FRAME_FACTOR),
int(min_pixels * 1.05),
)
max_pixels = ele.get("max_pixels", max_pixels)
# min_pixels = (factor ** 2) * 52
# max_pixels = (factor ** 2) * min(768, (16384 / nframes * temporal_patch_size))
if "resized_height" in ele and "resized_width" in ele:
resized_height, resized_width = smart_resize(
ele["resized_height"],
ele["resized_width"],
factor=image_factor,
)
else:
resized_height, resized_width = smart_resize(
height,
width,
factor=image_factor,
min_pixels=min_pixels,
max_pixels=max_pixels,
)
video = transforms.functional.resize(
video,
[resized_height, resized_width],
interpolation=InterpolationMode.BICUBIC,
antialias=True,
).float()
return video, total_dur, nframes, second_per_grid
else:
assert isinstance(ele["video"], (list, tuple))
process_info = ele.copy()
process_info.pop("type", None)
process_info.pop("video", None)
images = [
fetch_image({"image": video_element, **process_info}, size_factor=image_factor)
for video_element in ele["video"]
]
nframes = ceil_by_factor(len(images), FRAME_FACTOR)
if len(images) < nframes:
images.extend([images[-1]] * (nframes - len(images)))
return images, None, None, None
def extract_vision_info(conversations: list[dict] | list[list[dict]]) -> list[dict]:
vision_infos = []
if isinstance(conversations[0], dict):
conversations = [conversations]
for conversation in conversations:
for message in conversation:
if isinstance(message["content"], list):
for ele in message["content"]:
if (
"image" in ele
or "image_url" in ele
or "video" in ele
or ele["type"] in ("image", "image_url", "video")
):
vision_infos.append(ele)
return vision_infos
def process_vision_info(
conversations: list[dict] | list[list[dict]],
) -> tuple[list[Image.Image] | None, list[torch.Tensor | list[Image.Image]] | None]:
vision_infos = extract_vision_info(conversations)
# Read images or videos
image_inputs = []
video_inputs = []
for vision_info in vision_infos:
if "image" in vision_info or "image_url" in vision_info:
image_inputs.append(fetch_image(vision_info))
elif "video" in vision_info:
video_inputs.append(fetch_video(vision_info))
else:
raise ValueError("image, image_url or video should in content.")
if len(image_inputs) == 0:
image_inputs = None
if len(video_inputs) == 0:
video_inputs = None
return image_inputs, video_inputs
@@ -0,0 +1 @@
ray start --head --port=6399 --num-gpus=1
@@ -0,0 +1 @@
ray start --address=10.90.67.86:6399
@@ -0,0 +1,8 @@
python end2end.py --model Qwen/Qwen2.5-Omni-7B \
--voice-type "m02" \
--dit-ckpt none \
--bigvgan-ckpt none \
--output-wav output_audio \
--prompt_type text \
--init-sleep-seconds 0 \
--txt-prompts top100.txt
@@ -0,0 +1,11 @@
export PYTHONPATH=/workspace/omni/vllm-omni/:$PYTHONPATH
CUDA_VISIBLE_DEVICES=4 python end2end.py --model /workspace/Qwen2.5-Omni-7B/ \
--voice-type "m02" \
--dit-ckpt none \
--bigvgan-ckpt none \
--output-wav output_audio \
--prompt_type text \
--init-sleep-seconds 0 \
--worker-backend ray \
--ray-address auto \
--prompts "Explain the system architecture for a scalable audio generation pipeline. Answer in 15 words."
@@ -0,0 +1,312 @@
import tempfile
from typing import Optional, Union
from urllib.request import urlopen
import librosa
import requests
import resampy
import soundfile as sf
import torch
import torchvision.io
from processing_omni import fetch_image, fetch_video
from transformers import AutoConfig, AutoProcessor
from vllm.inputs import TextPrompt
from vllm_omni.inputs.data import OmniTokensPrompt
# Simple caches to avoid repeated heavy HF loads per prompt
_PROCESSOR_CACHE: dict[str, "AutoProcessor"] = {}
_CONFIG_CACHE: dict[str, "AutoConfig"] = {}
def get_system_prompt():
return {
"role": "system",
"content": [
{
"type": "text",
"text": (
"You are Qwen, a virtual human developed by the Qwen Team, "
"Alibaba Group, capable of perceiving auditory and visual inputs, "
"as well as generating text and speech."
),
}
],
}
def resample_wav_to_16khz(input_filepath):
data, original_sample_rate = sf.read(input_filepath)
# Only use the first channel
if len(data.shape) > 1:
data = data[:, 0]
# resample to 16kHz
data_resampled = resampy.resample(data, sr_orig=original_sample_rate, sr_new=16000)
return data_resampled
def fetch_and_read_video(args, video_url: str, fps=2):
def read_video_with_torchvision(video_file_name: str):
video, audio, info = torchvision.io.read_video(
video_file_name,
start_pts=0.0,
end_pts=None,
pts_unit="sec",
output_format="TCHW",
)
total_frames, video_fps = video.size(0), info["video_fps"]
total_duration = round(total_frames / video_fps, 3)
nframes = int(total_frames / video_fps * fps)
frame_timestamps = total_duration * torch.arange(1, nframes + 1) / nframes
grid_timestamps = frame_timestamps[::2]
second_per_grid = grid_timestamps[1] - grid_timestamps[0]
idx = torch.linspace(0, video.size(0) - 1, nframes).round().long()
video = video[idx]
if args.legacy_omni_video:
return [video, total_duration, nframes, second_per_grid.item()]
else:
return video
def read_video_with_transformers(video_file_name: Union[str, list[str]]):
video, total_duration, nframes, second_per_grid = fetch_video({"video": video_file_name})
if total_duration is None and nframes is None:
nframes = len(video)
total_duration = 0.5 * nframes
second_per_grid = 1.0
if args.legacy_omni_video:
return [video, total_duration, nframes, second_per_grid]
else:
return video
def read_video(video_file_name: str):
if args.use_torchvision:
return read_video_with_torchvision(video_file_name)
else:
return read_video_with_transformers(video_file_name)
if isinstance(video_url, str) and video_url.startswith("http"):
with tempfile.NamedTemporaryFile(delete=True) as temp_video_file:
resp = requests.get(video_url)
assert resp.status_code == requests.codes.ok, (
f"Failed to fetch video from {video_url}, status_code:{resp.status_code}, resp:{resp}"
)
temp_video_file.write(urlopen(video_url).read())
temp_video_file_path = temp_video_file.name
video_file_name = temp_video_file_path
return read_video(video_file_name)
else:
video_file_name = video_url
return read_video(video_file_name)
def make_inputs_qwen2_omni(
args,
messages: list[dict[str, Union[str, list[dict[str, str]]]]],
use_audio_in_video: Optional[bool] = False,
tokenize: bool = False,
) -> Union[OmniTokensPrompt, TextPrompt]:
from transformers import AutoConfig, AutoProcessor
# Cached processor/config to prevent per-prompt reloading and repeated warnings
if args.model not in _PROCESSOR_CACHE:
_PROCESSOR_CACHE[args.model] = AutoProcessor.from_pretrained(args.model)
processor = _PROCESSOR_CACHE[args.model]
config = _CONFIG_CACHE.get(args.model)
if config is None:
try:
config = AutoConfig.from_pretrained(args.model)
except Exception:
config = None
_CONFIG_CACHE[args.model] = config # cache even if None to avoid retry storms
# Decide legacy flag only once based on config (default True if unknown)
if getattr(args, "legacy_omni_video", None) is None:
if config is not None and hasattr(config, "architectures"):
args.legacy_omni_video = "Qwen2_5OmniModel" not in config.architectures
else:
args.legacy_omni_video = True
audios, images, videos = [], [], []
for message in messages:
if not isinstance(message["content"], list):
message["content"] = [
{
"type": "text",
"text": message["content"],
}
]
index, num_contents = 0, len(message["content"])
while index < num_contents:
ele = message["content"][index]
if "type" not in ele:
if "text" in ele:
ele["type"] = "text"
elif "audio" in ele:
ele["type"] = "audio"
elif "audio_url" in ele:
ele["type"] = "audio_url"
elif "image" in ele:
ele["type"] = "image"
elif "image_url" in ele:
ele["type"] = "image_url"
elif "video" in ele:
ele["type"] = "video"
elif "video_url" in ele:
ele["type"] = "video_url"
else:
raise ValueError(f"Unknown ele: {ele}")
if ele["type"] == "audio" or ele["type"] == "audio_url":
if "audio_url" in ele:
audio_key = "audio_url"
with tempfile.NamedTemporaryFile(delete=True) as temp_audio_file:
temp_audio_file.write(urlopen(ele[audio_key]).read())
temp_audio_file_path = temp_audio_file.name
audios.append(resample_wav_to_16khz(temp_audio_file_path))
ele["audio"] = temp_audio_file_path
elif "audio" in ele:
audio_key = "audio"
audios.append(resample_wav_to_16khz(ele[audio_key]))
else:
raise ValueError(f"Unknown ele {ele}")
elif use_audio_in_video and (ele["type"] == "video" or ele["type"] == "video_url"):
# use video as audio as well
if "video_url" in ele:
audio_key = "video_url"
with tempfile.NamedTemporaryFile(delete=True) as temp_video_file:
temp_video_file.write(urlopen(ele[audio_key]).read())
temp_video_file_path = temp_video_file.name
ele[audio_key] = temp_video_file_path
audios.append(librosa.load(temp_video_file_path, sr=16000)[0])
videos.append(fetch_and_read_video(args, temp_video_file_path))
ele["video"] = temp_video_file_path
elif "video" in ele:
audio_key = "video"
audios.append(librosa.load(ele[audio_key], sr=16000)[0])
videos.append(fetch_and_read_video(args, audio_key))
else:
raise ValueError(f"Unknown ele {ele}")
# insert a audio after the video
message["content"].insert(
index + 1,
{
"type": "audio",
"audio": ele[audio_key],
},
)
# no need to load the added audio again
index += 1
elif ele["type"] == "video" or ele["type"] == "video_url":
if "video_url" in ele:
video_key = "video_url"
with tempfile.NamedTemporaryFile(delete=True) as temp_video_file:
temp_video_file.write(urlopen(ele["video_url"]).read())
temp_video_file_path = temp_video_file.name
videos.append(fetch_and_read_video(args, temp_video_file))
ele["video"] = temp_video_file_path
else:
video_key = "video"
videos.append(fetch_and_read_video(args, ele[video_key]))
elif ele["type"] == "image" or ele["type"] == "image_url":
images.append(fetch_image(ele))
# move to the next content
index += 1
prompt = processor.apply_chat_template(
messages,
tokenize=tokenize,
add_generation_prompt=True,
add_vision_id=True,
)
audios = audios if len(audios) > 0 else None
images = images if len(images) > 0 else None
videos = videos if len(videos) > 0 else None
multi_modal_data = {}
if audios:
multi_modal_data["audio"] = audios
if images:
multi_modal_data["image"] = images
if videos:
multi_modal_data["video"] = videos
if isinstance(prompt, list) and isinstance(prompt[0], (list, str)):
prompt = prompt[0]
if tokenize:
return OmniTokensPrompt(
prompt_token_ids=prompt,
multi_modal_data=multi_modal_data,
)
else:
return TextPrompt(
prompt=prompt,
multi_modal_data=multi_modal_data,
)
def make_text_prompt(args, prompt):
messages = [
get_system_prompt(),
{
"role": "user",
"content": [
{"type": "text", "text": prompt},
],
},
]
prompt = make_inputs_qwen2_omni(args, messages, tokenize=args.tokenize)
return prompt
def make_audio_in_video_v2_prompt(args):
messages = [
{
"role": "system",
"content": [
{
"type": "text",
"text": (
"You are Qwen, a virtual human developed by the Qwen Team, "
"Alibaba Group, capable of perceiving auditory and visual "
"inputs, as well as generating text and speech."
),
}
],
},
{
"role": "user",
"content": [
{
"type": "video_url",
"video_url": ("https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen2.5-Omni/draw_small.mp4"),
},
],
},
]
prompt = make_inputs_qwen2_omni(
args,
messages,
use_audio_in_video=True,
tokenize=args.tokenize,
)
return prompt
def make_omni_prompt(args, prompt=None) -> Union[OmniTokensPrompt, list[OmniTokensPrompt]]:
if args.prompt_type == "text":
prompt = make_text_prompt(args, prompt)
elif args.prompt_type == "audio-in-video-v2":
prompt = make_audio_in_video_v2_prompt(args)
else:
raise ValueError(f"Unsupported prompt type: {args.prompt_type}")
return prompt
+25
View File
@@ -0,0 +1,25 @@
# SPDX-License-Identifier: Apache-2.0
from .connectors import (
ConnectorSpec, OmniTransferConfig,
OmniConnectorBase, InMemoryOmniConnector,
OmniConnectorFactory, MooncakeConnector,
load_omni_transfer_config, create_simple_config, create_mooncake_config
)
__all__ = [
# Config
"ConnectorSpec",
"OmniTransferConfig",
# Connectors
"OmniConnectorBase",
"InMemoryOmniConnector",
"OmniConnectorFactory",
"MooncakeConnector",
# Utilities
"load_omni_transfer_config",
"create_simple_config",
"create_mooncake_config",
]
@@ -0,0 +1,44 @@
# SPDX-License-Identifier: Apache-2.0
from .config import ConnectorSpec, OmniTransferConfig
from .base import OmniConnectorBase, InMemoryOmniConnector
from .factory import OmniConnectorFactory
from .mooncake_connector import MooncakeConnector
from .utils import (
load_omni_transfer_config,
initialize_connectors_from_config,
create_simple_config,
create_mooncake_config,
get_connectors_config_for_stage,
initialize_orchestrator_connectors,
get_stage_connector_config,
build_stage_connectors,
)
__all__ = [
# Config
"ConnectorSpec",
"OmniTransferConfig",
# Base classes and implementations
"OmniConnectorBase",
"InMemoryOmniConnector",
# Factory
"OmniConnectorFactory",
# Specific implementations
"MooncakeConnector",
# Utilities
"load_omni_transfer_config",
"initialize_connectors_from_config",
"create_simple_config",
"create_mooncake_config",
"get_connectors_config_for_stage",
# Manager helpers
"initialize_orchestrator_connectors",
"get_stage_connector_config",
"build_stage_connectors",
]
+145
View File
@@ -0,0 +1,145 @@
# SPDX-License-Identifier: Apache-2.0
# temporary for compatibility with vllm_omni.entrypoints.omni_stage.py
# and vllm_omni.entrypoints.omni_llm.py
import time
import logging
from typing import Any, Optional, Dict, Callable
logger = logging.getLogger(__name__)
def try_send_via_connector(
connector: Any,
stage_id: int,
next_stage_id: int,
req_id: str,
next_inputs: Any,
sampling_params: Any,
original_prompt: Any,
next_stage_queue_submit_fn: Callable[[Dict[str, Any]], None],
metrics: Any
) -> bool:
"""
Attempts to send data via OmniConnector.
Returns True if successful, False otherwise.
Encapsulates the logic of preparing payload, sending via connector,
sending notification, and recording metrics.
"""
try:
t0 = time.time()
# Prepare data for connector
payload_data = {
"engine_inputs": next_inputs,
"sampling_params": sampling_params,
"metadata": {
"original_prompt": original_prompt,
"stage_transition": f"{stage_id}->{next_stage_id}",
"timestamp": time.time()
}
}
# Send data via connector
success, serialized_size = connector.put(str(stage_id), str(next_stage_id), str(req_id), payload_data)
if success:
# Send lightweight notification via queue
notify_payload = {
"request_id": req_id,
"sampling_params": sampling_params,
"from_connector": True,
"from_stage": str(stage_id),
"to_stage": str(next_stage_id),
"sent_ts": time.time(),
}
next_stage_queue_submit_fn(notify_payload)
t1 = time.time()
tx_ms = (t1 - t0) * 1000.0
metrics.on_forward(
stage_id,
next_stage_id,
req_id,
serialized_size, # Use size from connector
float(tx_ms),
True, # Mark as using connector
)
return True
else:
# If put returned False, we let the caller handle fallback
return False
except Exception as e:
logger.warning(
"[Orchestrator] OmniConnector failed for req %s: %s; falling back to queue",
req_id,
e,
)
return False
def try_recv_via_connector(
task: Dict[str, Any],
connectors: Dict[Any, Any],
stage_id: int,
) -> tuple[Any, Optional[Dict[str, Any]]]:
"""
Attempts to resolve input data from either connector or IPC.
Returns (engine_inputs, rx_metrics) or (None, None) if failed/skipped.
"""
rid = task["request_id"]
if task.get("from_connector"):
from_stage = task.get("from_stage")
to_stage = str(stage_id)
if not from_stage:
logger.error(
"[Stage-%s] 'from_connector' is true but 'from_stage' is missing for request %s", stage_id, rid
)
return None, None
# Get connector for this edge
connector_key = (from_stage, to_stage)
connector = connectors.get(connector_key)
if connector:
try:
# Get data from connector with timeout
_t_start = time.time()
payload_data = connector.get(from_stage, to_stage, str(rid))
_t_end = time.time()
if payload_data and isinstance(payload_data, dict):
ein = payload_data.get("engine_inputs")
# Use connector's serialization size for consistency
serialized_size = len(connector.serialize_obj(payload_data))
decode_ms = (_t_end - _t_start) * 1000.0
rx_metrics = {
"rx_decode_time_ms": decode_ms,
"rx_transfer_bytes": serialized_size
}
return ein, rx_metrics
else:
logger.error(
"[Stage-%s] Failed to get data from connector for request %s or payload is empty", stage_id, rid
)
return None, None
except Exception as e:
logger.error(
"[Stage-%s] Error retrieving data from connector for request %s: %s", stage_id, rid, e
)
return None, None
else:
logger.error(
"[Stage-%s] No connector found for edge %s -> %s for request %s", stage_id, from_stage, to_stage, rid
)
return None, None
else:
# Data comes from queue as usual
#return maybe_load_from_ipc_with_metrics_fn(task, obj_key="engine_inputs", shm_key="engine_inputs_shm")
return None, None
+119
View File
@@ -0,0 +1,119 @@
# SPDX-License-Identifier: Apache-2.0
import logging
from abc import ABC, abstractmethod
from typing import Any, Optional
logger = logging.getLogger(__name__)
class OmniConnectorBase(ABC):
"""Base class for all OmniConnectors."""
@abstractmethod
def put(self, from_stage: str, to_stage: str, request_id: str, data: Any) -> tuple[bool, int]:
"""Store Python object, internal serialization handled by connector.
Args:
from_stage: Source stage identifier
to_stage: Destination stage identifier
request_id: Unique request identifier
data: Python object to store
Returns:
tuple: (success: bool, serialized_size: int)
"""
pass
@abstractmethod
def get(self, from_stage: str, to_stage: str, request_id: str) -> Optional[Any]:
"""Retrieve Python object, internal deserialization handled by connector.
Args:
from_stage: Source stage identifier
to_stage: Destination stage identifier
request_id: Unique request identifier
Returns:
Python object if found, None otherwise
"""
pass
@abstractmethod
def cleanup(self, request_id: str) -> None:
"""Clean up resources for a request."""
pass
@abstractmethod
def health(self) -> dict[str, Any]:
"""Return health status and metrics."""
pass
@staticmethod
def serialize_obj(obj: Any) -> bytes:
"""Serialize a Python object to bytes using torch.save (consistent with stage_utils)."""
import io
import torch
buf = io.BytesIO()
torch.save(obj, buf)
return buf.getvalue()
@staticmethod
def deserialize_obj(data: bytes) -> Any:
"""Deserialize bytes to Python object using torch.load (consistent with stage_utils)."""
import io
import torch
return torch.load(io.BytesIO(data), map_location="cpu", weights_only=False)
class InMemoryOmniConnector(OmniConnectorBase):
"""Simple in-memory connector for testing and co-located stages."""
def __init__(self):
self._storage: dict[str, Any] = {}
self._metrics = {
"puts": 0,
"gets": 0,
"errors": 0,
}
def _make_key(self, from_stage: str, to_stage: str, request_id: str) -> str:
return f"{from_stage}:{to_stage}:{request_id}"
def put(self, from_stage: str, to_stage: str, request_id: str, data: Any) -> tuple[bool, int]:
try:
key = self._make_key(from_stage, to_stage, request_id)
self._storage[key] = data
serialized_size = len(self.serialize_obj(data))
self._metrics["puts"] += 1
logger.debug(f"InMemoryConnector: stored {key}")
return True, serialized_size
except Exception as e:
self._metrics["errors"] += 1
logger.error(f"InMemoryConnector put failed: {e}")
return False, 0
def get(self, from_stage: str, to_stage: str, request_id: str) -> Optional[Any]:
try:
key = self._make_key(from_stage, to_stage, request_id)
data = self._storage.get(key)
if data is not None:
self._metrics["gets"] += 1
return data
return None
except Exception as e:
logger.error(f"InMemoryConnector get failed: {e}")
return None
def cleanup(self, request_id: str) -> None:
keys_to_remove = [k for k in self._storage.keys() if k.endswith(f":{request_id}")]
for key in keys_to_remove:
del self._storage[key]
logger.debug(f"InMemoryConnector: cleaned up {len(keys_to_remove)} keys for {request_id}")
def health(self) -> dict[str, Any]:
return {
"status": "healthy",
"storage_size": len(self._storage),
**self._metrics,
}
@@ -0,0 +1,31 @@
# SPDX-License-Identifier: Apache-2.0
import logging
from dataclasses import dataclass, field
from typing import Any, Optional
logger = logging.getLogger(__name__)
@dataclass
class ConnectorSpec:
"""Specification for a connector instance."""
name: str # e.g., "MooncakeConnector", "InMemoryConnector"
extra: dict[str, Any] = field(default_factory=dict) # backend-specific config
@dataclass
class OmniTransferConfig:
"""Top-level configuration for OmniConnector system."""
# Direct mapping: (from_stage, to_stage) -> connector
connectors: dict[tuple[str, str], ConnectorSpec] = field(default_factory=dict)
default_connector: Optional[ConnectorSpec] = None
def get_connector_for_edge(self, from_stage: str, to_stage: str) -> Optional[ConnectorSpec]:
"""Get connector spec for a specific edge."""
edge_key = (from_stage, to_stage)
return self.connectors.get(edge_key, self.default_connector)
def has_connector_for_edge(self, from_stage: str, to_stage: str) -> bool:
"""Check if there's a connector configured for the edge."""
return self.get_connector_for_edge(from_stage, to_stage) is not None
@@ -0,0 +1,9 @@
export PYTHONPATH=/workspace/omni/vllm-omni/:$PYTHONPATH
CUDA_VISIBLE_DEVICES=0,1,2,3 python end2end.py --model /workspace/Qwen2.5-Omni-7B/ \
--voice-type "m02" \
--dit-ckpt none \
--bigvgan-ckpt none \
--output-wav output_audio \
--prompt_type text \
--init-sleep-seconds 0 \
--prompts "Explain the system architecture for a scalable audio generation pipeline. Answer in 15 words."
@@ -0,0 +1,78 @@
#!/usr/bin/env python3
"""
Test OmniConnector integration with OmniLLM.
"""
import sys
import os
sys.path.insert(0, os.path.dirname(__file__))
from vllm_omni.distributed.connectors import create_mooncake_config
def test_connector_config():
"""Test connector configuration creation."""
print("Testing connector configuration...")
# Create Mooncake config
config = create_mooncake_config(
host="127.0.0.1",
metadata_server="http://127.0.0.1:8080/metadata",
master="127.0.0.1:50051"
)
print(f"Created config with {len(config.connectors)} connectors")
for edge, connector_spec in config.connectors.items():
print(f" {edge}: {connector_spec.name}")
return config
def test_omni_llm_initialization():
"""Test OmniLLM with connector config."""
print("\nTesting OmniLLM initialization with connectors...")
try:
from vllm_omni.entrypoints.omni_llm import OmniLLM
# Create connector config
connector_config = {
"connectors": {
"0_to_1": {
"name": "InMemoryConnector", # Use InMemory for testing
"extra": {}
}
}
}
# Test connector initialization logic
omni = OmniLLM.__new__(OmniLLM) # Create without calling __init__
omni.omni_transfer_config = connector_config
omni.connectors = {}
omni._initialize_connectors()
# Check if connector was created
if ("0", "1") in omni.connectors:
print(f"✅ Connector created: {type(omni.connectors[('0', '1')]).__name__}")
return True
else:
print("❌ Connector not created")
return False
except Exception as e:
print(f"Error: {e}")
return False
if __name__ == "__main__":
print("OmniConnector Integration Test")
print("=" * 40)
# Test config creation
config = test_connector_config()
# Test OmniLLM integration
success = test_omni_llm_initialization()
if success:
print("\n✅ Integration test passed!")
print("Connector system is ready for use with OmniLLM")
else:
print("\n❌ Integration test failed!")
@@ -0,0 +1,332 @@
#!/usr/bin/env python3
"""
Test EPD (Encode-Prefill-Decode) 3-stage pipeline with connectors.
Tests data flow between stages using configured connectors.
"""
import os
import sys
from typing import Any, Dict, Optional
from dataclasses import dataclass
import uuid
try:
import yaml
except ImportError:
print("PyYAML not available, using basic YAML parsing")
yaml = None
# Add the connectors package to path
current_dir = os.path.dirname(os.path.abspath(__file__))
parent_dir = os.path.dirname(current_dir)
sys.path.insert(0, parent_dir)
# Import from parent package
try:
from connectors.config import ConnectorSpec, OmniTransferConfig
from connectors.base import OmniConnectorBase, InMemoryOmniConnector
from connectors.factory import OmniConnectorFactory
from connectors.utils import load_omni_transfer_config
except ImportError:
# Fallback for direct execution
sys.path.insert(0, current_dir)
from config import ConnectorSpec, OmniTransferConfig
from base import OmniConnectorBase, InMemoryOmniConnector
from factory import OmniConnectorFactory
from utils import load_omni_transfer_config
@dataclass
class StageResult:
"""Result from a stage processing."""
stage_name: str
request_id: str
output_data: Any
metadata: Dict[str, Any]
class EncodeStage:
"""Simple Encode stage for testing."""
def __init__(self, stage_id: int, output_connector: Optional[OmniConnectorBase] = None):
self.stage_id = stage_id
self.stage_name = "encode"
self.output_connector = output_connector
def process(self, request_id: str, input_data: Any) -> StageResult:
"""Process input data and optionally send to next stage."""
print(f"[{self.stage_name}] Processing request {request_id}")
# Simple encoding simulation
encoded_data = {
"original_text": input_data,
"encoded_tokens": [ord(c) for c in str(input_data)][:10], # Simple tokenization
"embeddings": [0.1 * i for i in range(10)], # Mock embeddings
"stage": self.stage_name
}
result = StageResult(
stage_name=self.stage_name,
request_id=request_id,
output_data=encoded_data,
metadata={"tokens_count": len(encoded_data["encoded_tokens"])}
)
# Send to next stage if connector is available
if self.output_connector:
success = self.output_connector.put(
str(self.stage_id), "1", request_id, encoded_data
)
print(f"[{self.stage_name}] Sent data to stage 1 via connector: {success}")
return result
class PrefillStage:
"""Simple Prefill stage for testing."""
def __init__(self, stage_id: int,
input_connector: Optional[OmniConnectorBase] = None,
output_connector: Optional[OmniConnectorBase] = None):
self.stage_id = stage_id
self.stage_name = "prefill"
self.input_connector = input_connector
self.output_connector = output_connector
def process(self, request_id: str) -> Optional[StageResult]:
"""Process data from previous stage."""
print(f"[{self.stage_name}] Processing request {request_id}")
# Try to get data from input connector
input_data = None
if self.input_connector:
input_data = self.input_connector.get("0", str(self.stage_id), request_id)
if input_data is None:
print(f"[{self.stage_name}] No input data received for {request_id}")
return None
print(f"[{self.stage_name}] Received data from encode")
# Simple prefill simulation
prefills_data = {
**input_data,
"kv_cache": {"layer_0": [0.2 * i for i in range(20)]}, # Mock KV cache
"stage": self.stage_name
}
result = StageResult(
stage_name=self.stage_name,
request_id=request_id,
output_data=prefills_data,
metadata={"kv_size": len(prefills_data["kv_cache"])}
)
# Send to next stage if connector is available
if self.output_connector:
success = self.output_connector.put(
str(self.stage_id), "2", request_id, prefills_data
)
print(f"[{self.stage_name}] Sent data to stage 2 via connector: {success}")
return result
class DecodeStage:
"""Simple Decode stage for testing (final stage in 3-stage pipeline)."""
def __init__(self, stage_id: int,
input_connector: Optional[OmniConnectorBase] = None):
self.stage_id = stage_id
self.stage_name = "decode"
self.input_connector = input_connector
def process(self, request_id: str) -> Optional[StageResult]:
"""Process data from previous stage (final output)."""
print(f"[{self.stage_name}] Processing request {request_id}")
# Try to get data from input connector
input_data = None
if self.input_connector:
input_data = self.input_connector.get("1", str(self.stage_id), request_id)
if input_data is None:
print(f"[{self.stage_name}] No input data received for {request_id}")
return None
print(f"[{self.stage_name}] Received data from prefill")
# Simple decode simulation (final output)
decoded_data = {
**input_data,
"hidden_states": [0.3 * i for i in range(15)], # Mock hidden states
"final_output": f"Decoded text from: {input_data.get('original_text', 'unknown')}",
"stage": self.stage_name
}
result = StageResult(
stage_name=self.stage_name,
request_id=request_id,
output_data=decoded_data,
metadata={"hidden_size": len(decoded_data["hidden_states"]), "final": True}
)
return result
class GeneratorStage:
"""Simple Generator stage for testing (removed for 3-stage pipeline)."""
pass # Not used in current 3-stage setup
def load_config_from_yaml(yaml_path: str) -> OmniTransferConfig:
"""Load connector configuration from YAML file."""
if yaml is None:
# Fallback: create a simple test config
print("Using fallback InMemoryConnector config for testing")
return create_test_config()
with open(yaml_path, 'r', encoding='utf-8') as f:
config_dict = yaml.safe_load(f)
try:
config = load_omni_transfer_config(config_dict=config_dict)
print(f"Successfully loaded config from YAML with {len(config.connectors)} connectors")
return config
except Exception as e:
print(f"Failed to load YAML config: {e}, using fallback config")
return create_test_config()
def create_test_config() -> OmniTransferConfig:
"""Create a test configuration with InMemoryConnectors."""
from .config import ConnectorSpec
connectors = {}
# Create connectors for EPD (3-stage) pipeline: 0->1->2
pipeline_stages = [("0", "1"), ("1", "2")]
for from_stage, to_stage in pipeline_stages:
connectors[(from_stage, to_stage)] = ConnectorSpec(
name="InMemoryConnector",
extra={}
)
return OmniTransferConfig(connectors=connectors)
def create_stages_with_connectors(config: OmniTransferConfig) -> Dict[str, Any]:
"""Create EPD (3-stage) pipeline with appropriate connectors."""
# Create connector instances based on config
connectors = {}
for edge_key, connector_spec in config.connectors.items():
from_stage, to_stage = edge_key
connectors[edge_key] = OmniConnectorFactory.create_connector(connector_spec)
# Create stages with connectors (3-stage pipeline: 0->1->2)
stages = {}
# Encode stage (stage 0)
encode_output_connector = connectors.get(("0", "1")) # to prefill
stages["encode"] = EncodeStage(0, encode_output_connector)
# Prefill stage (stage 1)
prefill_input_connector = connectors.get(("0", "1")) # from encode
prefill_output_connector = connectors.get(("1", "2")) # to decode
stages["prefill"] = PrefillStage(1, prefill_input_connector, prefill_output_connector)
# Decode stage (stage 2) - final stage
decode_input_connector = connectors.get(("1", "2")) # from prefill
stages["decode"] = DecodeStage(2, decode_input_connector)
return stages
def test_pipeline_with_config(yaml_path: str):
"""Test the EPDG pipeline with connector configuration."""
print("🔧 Loading configuration from YAML...")
try:
config = load_config_from_yaml(yaml_path)
print(f"✓ Loaded config with {len(config.connectors)} connector configurations")
except Exception as e:
print(f"✗ Failed to load config: {e}")
return False
print("\n🏗️ Creating EPDG stages with connectors...")
try:
stages = create_stages_with_connectors(config)
print("✓ Created all stages with connectors")
except Exception as e:
print(f"✗ Failed to create stages: {e}")
return False
print("\n🚀 Testing data flow through pipeline...")
# Test request
request_id = f"test_req_{uuid.uuid4()}"
input_text = "Hello, EPDG pipeline!"
try:
# Stage 1: Encode
print(f"\n--- Stage 1: Encode ---")
encode_result = stages["encode"].process(request_id, input_text)
print(f"✓ Encode completed: {encode_result.metadata}")
# Stage 2: Prefill
print(f"\n--- Stage 2: Prefill ---")
prefill_result = stages["prefill"].process(request_id)
if prefill_result:
print(f"✓ Prefill completed: {prefill_result.metadata}")
else:
print("✗ Prefill failed - no input data")
return False
# Stage 3: Decode
print(f"\n--- Stage 3: Decode ---")
decode_result = stages["decode"].process(request_id)
if decode_result:
print(f"✓ Decode completed: {decode_result.metadata}")
else:
print("✗ Decode failed - no input data")
return False
# Decode stage produces final output in 3-stage pipeline
except Exception as e:
print(f"✗ Pipeline test failed: {e}")
import traceback
traceback.print_exc()
return False
print("\n🎉 Pipeline test completed successfully!")
return True
def main():
"""Main test function."""
# Path to the YAML config file
yaml_path = "/workspace/omni/vllm-omni/vllm_omni/model_executor/stage_configs/qwen2_5_omni.yaml"
if not os.path.exists(yaml_path):
print(f"✗ YAML config file not found: {yaml_path}")
return False
print("Testing EPDG Pipeline with Connectors")
print("=" * 50)
print(f"Config file: {yaml_path}")
success = test_pipeline_with_config(yaml_path)
if success:
print("\n✅ All tests passed!")
else:
print("\n❌ Tests failed!")
return success
if __name__ == "__main__":
success = main()
sys.exit(0 if success else 1)
@@ -0,0 +1,86 @@
# SPDX-License-Identifier: Apache-2.0
import logging
from collections.abc import Callable
from typing import Any
try:
from .base import OmniConnectorBase
from .config import ConnectorSpec
except ImportError:
# Fallback for direct execution
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from connectors.base import OmniConnectorBase
from connectors.config import ConnectorSpec
logger = logging.getLogger(__name__)
class OmniConnectorFactory:
"""Factory for creating OmniConnectors."""
_registry: dict[str, Callable[[dict[str, Any]], OmniConnectorBase]] = {}
@classmethod
def register_connector(
cls,
name: str,
constructor: Callable[[dict[str, Any]], OmniConnectorBase]
) -> None:
"""Register a connector constructor."""
if name in cls._registry:
raise ValueError(f"Connector '{name}' is already registered.")
cls._registry[name] = constructor
logger.debug(f"Registered connector: {name}")
@classmethod
def create_connector(cls, spec: ConnectorSpec) -> OmniConnectorBase:
"""Create a connector from specification."""
if spec.name not in cls._registry:
raise ValueError(f"Unknown connector: {spec.name}. Available: {list(cls._registry.keys())}")
constructor = cls._registry[spec.name]
try:
connector = constructor(spec.extra)
logger.info(f"Created connector: {spec.name}")
return connector
except Exception as e:
logger.error(f"Failed to create connector {spec.name}: {e}")
raise
@classmethod
def list_registered_connectors(cls) -> list[str]:
"""List all registered connector names."""
return list(cls._registry.keys())
# Register built-in connectors with lazy imports
def _create_mooncake_connector(config: dict[str, Any]) -> OmniConnectorBase:
try:
from .mooncake_connector import MooncakeConnector
except ImportError:
# Fallback import
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from connectors.mooncake_connector import MooncakeConnector
return MooncakeConnector(config)
def _create_in_memory_connector(config: dict[str, Any]) -> OmniConnectorBase:
try:
from .base import InMemoryOmniConnector
except ImportError:
# Fallback import
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from connectors.base import InMemoryOmniConnector
return InMemoryOmniConnector()
# Register connectors
OmniConnectorFactory.register_connector("MooncakeConnector", _create_mooncake_connector)
OmniConnectorFactory.register_connector("InMemoryConnector", _create_in_memory_connector)
@@ -0,0 +1,157 @@
# SPDX-License-Identifier: Apache-2.0
import io
import logging
import time
from typing import Any, Optional
import torch
from .base import OmniConnectorBase
logger = logging.getLogger(__name__)
try:
from mooncake.store import MooncakeDistributedStore, ReplicateConfig
except ImportError:
try:
from mooncake import MooncakeDistributedStore, ReplicateConfig
except ImportError:
logger.warning("Mooncake not available, MooncakeOmniConnector will not work")
MooncakeDistributedStore = None
ReplicateConfig = None
class MooncakeConnector(OmniConnectorBase):
"""Mooncake-based distributed connector for OmniConnector."""
def __init__(self, config: dict[str, Any]):
if MooncakeDistributedStore is None:
raise ImportError("Mooncake not available")
self.config = config
self.host = config.get("host", "127.0.0.1")
self.metadata = config.get("metadata_server", "http://127.0.0.1:8080/metadata")
self.master = config.get("master", "127.0.0.1:50051")
self.segment = config.get("segment", 512 * 1024 * 1024) # 512MB
self.localbuf = config.get("localbuf", 64 * 1024 * 1024) # 64MB
self.proto = config.get("proto", "tcp")
self.rdma = config.get("rdma", "")
self.store: Optional[MooncakeDistributedStore] = None
self.pin: Optional[ReplicateConfig] = None
self._metrics = {
"puts": 0,
"gets": 0,
"bytes_transferred": 0,
"errors": 0,
"timeouts": 0,
}
self._init_store()
def _make_key(self, rid: str, from_stage: str, to_stage: str) -> str:
"""Generate store key for request between stages."""
return f"{rid}/{from_stage}_to_{to_stage}"
def _init_store(self):
"""Initialize Mooncake store."""
try:
self.store = MooncakeDistributedStore()
rc = self.store.setup(
self.host, self.metadata, self.segment,
self.localbuf, self.proto, self.rdma, self.master
)
if rc != 0:
raise RuntimeError(f"Mooncake setup failed: {rc}")
self.pin = ReplicateConfig()
self.pin.with_soft_pin = True
logger.info("MooncakeConnector initialized successfully")
except Exception as e:
logger.error(f"Failed to initialize Mooncake store: {e}")
raise
# Use base class serialization methods for consistency
def put(self, from_stage: str, to_stage: str, request_id: str, data: Any) -> tuple[bool, int]:
if not self.store:
logger.error("Store not initialized")
return False, 0
try:
serialized_data = self.serialize_obj(data)
key = self._make_key(request_id, from_stage, to_stage)
self.store.put(key, serialized_data, self.pin)
self._metrics["puts"] += 1
self._metrics["bytes_transferred"] += len(serialized_data)
logger.debug(f"MooncakeConnector: stored {key}, {len(serialized_data)} bytes")
print(f"========= mooncake put {from_stage}-{to_stage}-{request_id}, with size {len(serialized_data)}")
return True, len(serialized_data)
except Exception as e:
self._metrics["errors"] += 1
logger.error(f"MooncakeConnector put failed: {e}")
return False, 0
def get(self, from_stage: str, to_stage: str, request_id: str) -> Optional[Any]:
if not self.store:
logger.error("Store not initialized")
return None
retries = 20
sleep_s = 0.05
key = self._make_key(request_id, from_stage, to_stage)
for attempt in range(retries):
try:
raw_data = self.store.get(key)
if raw_data:
data = self.deserialize_obj(raw_data)
self._metrics["gets"] += 1
print(f"========= mooncake get {from_stage}-{to_stage}-{request_id}, with size {len(raw_data)}")
return data
except Exception as e:
logger.debug(f"MooncakeConnector get attempt {attempt} failed: {e}")
if attempt < retries - 1:
time.sleep(sleep_s)
self._metrics["timeouts"] += 1
logger.warning(f"MooncakeConnector: timeout waiting for {key}")
return None
def cleanup(self, request_id: str) -> None:
if not self.store:
return
# Note: Mooncake doesn't have explicit delete, data will be garbage collected
# We could implement a cleanup mechanism by storing deletion markers
logger.debug(f"MooncakeConnector: cleanup requested for {request_id} (no-op)")
def health(self) -> dict[str, Any]:
if not self.store:
return {"status": "unhealthy", "error": "Store not initialized"}
return {
"status": "healthy",
"host": self.host,
"metadata_server": self.metadata,
"master": self.master,
**self._metrics,
}
def close(self):
"""Clean shutdown."""
if self.store:
try:
self.store.close()
self.store = None
logger.info("MooncakeConnector closed")
except Exception as e:
logger.error(f"Error closing Mooncake store: {e}")
+314
View File
@@ -0,0 +1,314 @@
# SPDX-License-Identifier: Apache-2.0
"""Utilities for OmniConnector configuration and validation."""
import json
import logging
from pathlib import Path
from typing import Any, Dict, Optional, Tuple, Union
from vllm_omni.distributed.connectors.config import OmniTransferConfig, ConnectorSpec
from vllm_omni.distributed.connectors.factory import OmniConnectorFactory
from vllm_omni.distributed.connectors.base import OmniConnectorBase
logger = logging.getLogger(__name__)
def initialize_connectors_from_config(
config_path: Optional[Union[str, Path]] = None
) -> tuple[Optional[OmniTransferConfig], dict[tuple[str, str], OmniConnectorBase]]:
"""
Initialize connectors from configuration file.
Returns:
tuple: (OmniTransferConfig, dict of {(from, to): connector_instance})
"""
try:
transfer_config = load_omni_transfer_config(config_path)
except Exception as e:
logger.warning(f"Failed to load OmniTransferConfig from {config_path}: {e}")
return None, {}
if not transfer_config:
logger.info("No OmniTransferConfig provided, using default IPC")
return None, {}
# 使用统一的连接器创建逻辑
connectors = create_connectors_from_config(transfer_config.connectors)
return transfer_config, connectors
def create_connectors_from_config(
connectors_config: dict[tuple[str, str], ConnectorSpec]
) -> dict[tuple[str, str], OmniConnectorBase]:
"""通用连接器创建逻辑,从配置字典创建连接器实例。"""
connectors = {}
try:
for edge_key, connector_spec in connectors_config.items():
connector = OmniConnectorFactory.create_connector(connector_spec)
connectors[edge_key] = connector
logger.info(f"Created connector for {edge_key[0]} -> {edge_key[1]}: {type(connector).__name__}")
except Exception as e:
logger.error(f"Failed to initialize connectors: {e}")
return connectors
def get_connectors_config_for_stage(
transfer_config: Optional[OmniTransferConfig],
stage_id: Union[str, int]
) -> dict[str, Any]:
"""
Extract connector configurations relevant for a specific stage worker.
Returns a dict compatible with worker initialization:
{
"from_stage_X": {
"spec": {
"name": "ConnectorName",
"extra": {...}
}
},
...
}
"""
if not transfer_config:
return {}
stage_connectors_config = {}
target_stage = str(stage_id)
# Iterate through all configured edges
for (from_stage, to_stage), spec in transfer_config.connectors.items():
# We only care about incoming edges for the worker process
# (Worker needs to create connectors to receive data)
if to_stage == target_stage:
stage_connectors_config[f"from_stage_{from_stage}"] = {
"spec": {
"name": spec.name,
"extra": spec.extra
}
}
return stage_connectors_config
def load_omni_transfer_config(
config_path: Optional[Union[str, Path]] = None,
config_dict: Optional[dict[str, Any]] = None
) -> Optional[OmniTransferConfig]:
"""Load OmniTransferConfig from file or dict."""
if config_path is None and config_dict is None:
return None
if config_path is not None:
config_path = Path(config_path)
if not config_path.exists():
raise FileNotFoundError(f"Config file not found: {config_path}")
with open(config_path, 'r', encoding='utf-8') as f:
if config_path.suffix.lower() == '.json':
config_dict = json.load(f)
elif config_path.suffix.lower() in ['.yaml', '.yml']:
try:
import yaml
config_dict = yaml.safe_load(f)
except ImportError:
raise ImportError("PyYAML required for YAML config files")
else:
raise ValueError(f"Unsupported config file format: {config_path.suffix}")
if config_dict is None:
return None
# Parse connectors
connectors = {}
runtime_config = config_dict.get('runtime', {})
# Parse global connectors (from runtime.connectors)
global_connectors = runtime_config.get('connectors', {})
for conn_name, conn_config in global_connectors.items():
connector = ConnectorSpec(
name=conn_config['name'],
extra=conn_config.get('extra', {})
)
# Store globally defined connectors for reference
# These will be resolved to specific edges later
# Parse stage-level connectors
for stage_config in config_dict.get('stage_args', []):
stage_id = str(stage_config['stage_id'])
# Input connectors
for input_key, conn_ref in stage_config.get('input_connectors', {}).items():
if isinstance(conn_ref, str):
# Reference to global connector
if conn_ref in global_connectors:
conn_config = global_connectors[conn_ref]
connector = ConnectorSpec(
name=conn_config['name'],
extra=conn_config.get('extra', {})
)
else:
raise ValueError(f"Undefined connector reference: {conn_ref}")
else:
# Inline connector definition
connector = ConnectorSpec(
name=conn_ref['name'],
extra=conn_ref.get('extra', {})
)
# Parse from_stage from key (e.g., "from_stage_0" -> "0")
from_stage = input_key.replace("from_stage_", "")
edge_key = (from_stage, stage_id)
connectors[edge_key] = connector
# Output connectors
for output_key, conn_ref in stage_config.get('output_connectors', {}).items():
if isinstance(conn_ref, str):
# Reference to global connector
if conn_ref in global_connectors:
conn_config = global_connectors[conn_ref]
connector = ConnectorSpec(
name=conn_config['name'],
extra=conn_config.get('extra', {})
)
else:
raise ValueError(f"Undefined connector reference: {conn_ref}")
else:
# Inline connector definition
connector = ConnectorSpec(
name=conn_ref['name'],
extra=conn_ref.get('extra', {})
)
# Parse to_stage from key (e.g., "to_stage_1" -> "1")
to_stage = output_key.replace("to_stage_", "")
edge_key = (stage_id, to_stage)
connectors[edge_key] = connector
config = OmniTransferConfig(connectors=connectors)
logger.info(f"Loaded OmniTransferConfig with {len(connectors)} connector configurations")
return config
def create_simple_config(
connector_name: str = "InMemoryConnector",
connector_config: Optional[dict[str, Any]] = None
) -> OmniTransferConfig:
"""Create a simple OmniTransferConfig for testing."""
if connector_config is None:
connector_config = {}
# Create connectors for typical EPDG pipeline
connectors = {}
pipeline_edges = [
("0", "1"), # thinker -> talker
("1", "2"), # talker -> code2wav
]
for from_stage, to_stage in pipeline_edges:
connectors[(from_stage, to_stage)] = ConnectorSpec(
name=connector_name,
extra=connector_config
)
return OmniTransferConfig(connectors=connectors)
def create_mooncake_config(
host: str = "127.0.0.1",
metadata_server: str = "http://127.0.0.1:8080/metadata",
master: str = "127.0.0.1:50051",
segment: int = 512 * 1024 * 1024,
localbuf: int = 64 * 1024 * 1024,
) -> OmniTransferConfig:
"""Create OmniTransferConfig for Mooncake-based setup."""
connector_config = {
"host": host,
"metadata_server": metadata_server,
"master": master,
"segment": segment,
"localbuf": localbuf,
}
return create_simple_config("MooncakeConnector", connector_config)
# High-level management functions (moved from manager.py)
def initialize_orchestrator_connectors(
config_path: Optional[str],
) -> Tuple[Optional[OmniTransferConfig], Dict[tuple[str, str], OmniConnectorBase]]:
"""Initialize connectors shared at orchestrator level."""
try:
transfer_config, connectors = initialize_connectors_from_config(config_path)
return transfer_config, connectors
except Exception as exc: # pragma: no cover - defensive logging
logger.error("Unexpected error initializing connectors: %s", exc)
return None, {}
def get_stage_connector_config(
transfer_config: Optional[OmniTransferConfig],
stage_id: int,
) -> dict[str, Any]:
"""Return the serialized connector config payload for a specific stage."""
if transfer_config is None:
return {}
try:
return get_connectors_config_for_stage(transfer_config, stage_id)
except Exception as exc: # pragma: no cover - defensive logging
logger.warning(
"Failed to build connector config for stage %s: %s. Using IPC fallback.",
stage_id,
exc,
)
return {}
def build_stage_connectors(
stage_id: int,
connectors_config: dict[str, Any],
) -> Optional[dict[tuple[str, str], Any]]:
"""Instantiate OmniConnectors for a stage based on config."""
if not connectors_config:
return {}
logger.info(
"[Stage-%s] Initializing OmniConnectors with config keys: %s",
stage_id,
list(connectors_config.keys()),
)
from .factory import OmniConnectorFactory
from .config import ConnectorSpec
connectors: dict[tuple[str, str], Any] = {}
try:
# 将字典格式的配置转换为ConnectorSpec对象
stage_connector_specs = {}
for input_key, config in connectors_config.items():
if not input_key.startswith("from_stage_"):
continue
from_stage = input_key.replace("from_stage_", "")
spec_dict = config.get("spec", {})
if not spec_dict:
continue
connector_spec = ConnectorSpec(
name=spec_dict.get("name", "InMemoryConnector"),
extra=spec_dict.get("extra", {}),
)
stage_connector_specs[(str(from_stage), str(stage_id))] = connector_spec
# 使用统一的连接器创建逻辑
connectors = create_connectors_from_config(stage_connector_specs)
except Exception as exc: # pragma: no cover - defensive logging
logger.exception("[Stage-%s] Failed to initialize connectors: %s", stage_id, exc)
return None
return connectors
+233 -55
View File
@@ -1,6 +1,8 @@
import multiprocessing as mp
import os
import sys
import time
import uuid
from collections.abc import Sequence
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import Any, Optional, Union
@@ -20,6 +22,11 @@ from vllm.utils import Counter
from vllm.v1.engine.llm_engine import LLMEngine
# Internal imports (our code)
from vllm_omni.distributed.connectors.adapter import try_send_via_connector
from vllm_omni.distributed.connectors import (
initialize_orchestrator_connectors,
get_stage_connector_config,
)
from vllm_omni.engine.arg_utils import OmniEngineArgs
from vllm_omni.engine.output_processor import MultimodalOutputProcessor
from vllm_omni.engine.processor import OmniProcessor
@@ -31,9 +38,14 @@ from vllm_omni.entrypoints.log_utils import (
)
from vllm_omni.entrypoints.omni_stage import OmniStage
from vllm_omni.entrypoints.stage_utils import encode_for_ipc as _encode
from vllm_omni.entrypoints.stage_utils import _to_dict
from vllm_omni.entrypoints.stage_utils import maybe_load_from_ipc as _load
from vllm_omni.entrypoints.stage_utils import serialize_obj as _set
from vllm_omni.entrypoints.utils import load_stage_configs_from_model, load_stage_configs_from_yaml
from vllm_omni.entrypoints.utils import (
load_stage_configs_from_model,
load_stage_configs_from_yaml,
resolve_model_config_path,
)
from vllm_omni.outputs import OmniRequestOutput
logger = init_logger(__name__)
@@ -84,14 +96,23 @@ class OmniLLM:
init_timeout: int = 300,
**kwargs: Any,
):
self.worker_backend = kwargs.get("worker_backend", "process")
self.ray_address = kwargs.get("ray_address", None)
self.batch_timeout = batch_timeout
self._enable_stats: bool = bool(log_stats)
self._ray_pg = None
# Do NOT call super().__init__ to avoid creating OmniStageLLM instances in parent.
if stage_configs_path is None:
self.stage_configs = load_stage_configs_from_model(model)
self.config_path = resolve_model_config_path(model)
self.stage_configs = load_stage_configs_from_yaml(self.config_path)
else:
self.config_path = stage_configs_path
self.stage_configs = load_stage_configs_from_yaml(stage_configs_path)
# Initialize connectors
self.omni_transfer_config,self.connectors = initialize_orchestrator_connectors(self.config_path)
# Optional file handler for orchestrator
self._log_file = log_file
@@ -124,36 +145,140 @@ class OmniLLM:
self.stage_list = [st for _, st in results]
logger.debug("[Orchestrator] Loaded %d stages", len(self.stage_list))
self._ctx = mp.get_context("spawn")
if self.worker_backend == "ray":
try:
import ray
from ray.util.queue import Queue as RayQueue
except ImportError:
raise ImportError("ray is required for worker_backend='ray'")
self._queue_cls = lambda: RayQueue(maxsize=0)
else:
self._ctx = mp.get_context("spawn")
self._queue_cls = lambda: self._ctx.Queue(maxsize=0)
self._stage_in_queues: list[mp.Queue] = []
self._stage_out_queues: list[mp.Queue] = []
self._init_sleep_seconds = max(0, int(init_sleep_seconds))
self._shm_threshold_bytes = max(0, int(shm_threshold_bytes))
self._start_stage_processes(model)
self._start_stages(model)
# Wait for all stages to report readiness before seeding
self._stages_ready: set[int] = set()
self._wait_for_stages_ready(timeout=init_timeout)
def _start_stage_processes(self, model: str) -> None:
def _start_stages(self, model: str) -> None:
if self.worker_backend == "ray":
self._start_stage_actors_ray(model)
else:
self._start_stage_processes_mp(model)
def _start_stage_processes_mp(self, model: str) -> None:
for stage_id, stage in enumerate(self.stage_list):
# Use unbounded queues to avoid deadlock when seeding many requests
in_q: mp.Queue = self._ctx.Queue(maxsize=0)
out_q: mp.Queue = self._ctx.Queue(maxsize=0)
in_q: mp.Queue = self._queue_cls()
out_q: mp.Queue = self._queue_cls()
self._stage_in_queues.append(in_q)
self._stage_out_queues.append(out_q)
# Attach queues and start Stage-owned worker process
stage.attach_queues(in_q, out_q)
# Build connectors config for this stage
stage_connectors_config = get_stage_connector_config(
self.omni_transfer_config,
stage_id,
)
stage.init_stage_worker(
model,
log_file=self._log_file,
shm_threshold_bytes=self._shm_threshold_bytes,
ctx=self._ctx,
batch_timeout=self.batch_timeout,
connectors_config=stage_connectors_config,
)
logger.debug("[Orchestrator] Stage-%s process started", stage_id)
time.sleep(self._init_sleep_seconds)
def _start_stage_actors_ray(self, model: str) -> None:
import ray
import os
from ray.util.scheduling_strategies import PlacementGroupSchedulingStrategy
# Initialize Ray Cluster
if not ray.is_initialized():
# Create a dummy parallel config just to reuse vLLM's init logic if needed
# or just call ray.init()
# Pass current PYTHONPATH to workers to ensure they can find vllm_omni
runtime_env = {
"env_vars": {"PYTHONPATH": os.environ.get("PYTHONPATH", "")}
}
ray.init(address=self.ray_address, ignore_reinit_error=True, runtime_env=runtime_env)
# Create Placement Group
# We allocate 1 bundle per stage. Assume 1 GPU per stage for now.
# Must include CPU=1 because default Actor request includes 1 CPU.
bundles = [{"GPU": 1.0, "CPU": 1.0} for _ in self.stage_list]
self._ray_pg = ray.util.placement_group(bundles, strategy="SPREAD")
ray.get(self._ray_pg.ready())
logger.info("[Orchestrator] Ray Placement Group created")
# Define Actor Wrapper
@ray.remote(num_gpus=1)
class OmniStageRayWorker:
def run(self, func, *args, **kwargs):
return func(*args, **kwargs)
from vllm_omni.entrypoints.omni_stage import _stage_worker
for stage_id, stage in enumerate(self.stage_list):
in_q = self._queue_cls()
out_q = self._queue_cls()
self._stage_in_queues.append(in_q)
self._stage_out_queues.append(out_q)
stage.attach_queues(in_q, out_q)
stage_connectors_config = get_stage_connector_config(
self.omni_transfer_config,
stage_id,
)
# Prepare payload
# Reuse logic from OmniStage.init_stage_worker but do not start process
engine_args = _to_dict(stage.engine_args)
runtime_cfg = _to_dict(getattr(stage.stage_config, "runtime", {}))
stage_payload = {
"stage_id": stage.stage_id,
"engine_args": engine_args,
"runtime": runtime_cfg,
"shm_threshold_bytes": sys.maxsize, # Disable SHM for Ray mode by setting huge threshold
"connectors_config": stage_connectors_config or {},
}
# Start Actor
# Inject PYTHONPATH again for the specific actor to be safe
worker_actor = OmniStageRayWorker.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(
placement_group=self._ray_pg,
placement_group_bundle_index=stage_id
),
runtime_env={"env_vars": {"PYTHONPATH": os.environ.get("PYTHONPATH", "")}}
).remote()
# Start worker loop asynchronously
worker_actor.run.remote(
_stage_worker,
model=model,
stage_payload=stage_payload,
in_q=in_q,
out_q=out_q,
log_file=self._log_file,
batch_timeout=self.batch_timeout,
)
stage._ray_actor = worker_actor
logger.debug("[Orchestrator] Stage-%s Ray Actor started", stage_id)
time.sleep(self._init_sleep_seconds)
def close(self) -> None:
"""Close all stage processes and clean up resources.
@@ -171,10 +296,21 @@ class OmniLLM:
)
for stage in self.stage_list:
try:
stage.stop_stage_worker()
if self.worker_backend == "ray" and hasattr(stage, "_ray_actor"):
import ray
# Ray kill
ray.kill(stage._ray_actor)
else:
stage.stop_stage_worker()
except Exception as e:
logger.warning("[Orchestrator] Failed to stop stage worker: %s", e)
if self.worker_backend == "ray" and self._ray_pg:
try:
ray.util.remove_placement_group(self._ray_pg)
except Exception as e:
logger.warning("[Orchestrator] Failed to remove placement group: %s", e)
def __del__(self) -> None: # best-effort
try:
self.close()
@@ -237,11 +373,14 @@ class OmniLLM:
# Orchestrator keeps stage objects for input derivation
num_stages = len(self.stage_list)
# Map from request_id to original prompt
request_id_to_prompt: dict[int, PromptType] = {i: p for i, p in enumerate(request_prompts)}
# Generate globally unique request IDs and map them to original prompts
request_ids: list[str] = [f"{i}_{uuid.uuid4()}" for i in range(len(request_prompts))]
request_id_to_prompt: dict[str, PromptType] = {
rid: p for rid, p in zip(request_ids, request_prompts)
}
# Track per-request start time for end-to-end timing
_req_start_ts: dict[int, float] = {}
_req_start_ts: dict[str, float] = {}
_wall_start_ts: float = time.time()
# Determine the final stage for E2E stats (highest stage_id with final_output=True; fallback to last stage)
@@ -387,51 +526,90 @@ class OmniLLM:
)
continue
sp_next: SamplingParams = sampling_params_list[next_stage_id] # type: ignore[index]
try:
# Measure transfer size and time (encode + enqueue)
size_bytes = 0
# Check if we have a connector for this edge
connector_key = (str(stage_id), str(next_stage_id))
connector = self.connectors.get(connector_key)
print(f"connectors: {self.connectors}")
sent_via_connector = False
if connector:
sent_via_connector = try_send_via_connector(
connector=connector,
stage_id=stage_id,
next_stage_id=next_stage_id,
req_id=req_id,
next_inputs=next_inputs,
sampling_params=sp_next,
original_prompt=request_id_to_prompt[req_id],
next_stage_queue_submit_fn=self.stage_list[next_stage_id].submit,
metrics=metrics
)
if not sent_via_connector:
# Use original queue mechanism
try:
size_bytes = len(_set(next_inputs))
except Exception:
size_bytes = 0
t0 = time.time()
ipc_payload = _encode(
next_inputs,
getattr(self, "_shm_threshold_bytes", 65536),
obj_key="engine_inputs",
shm_key="engine_inputs_shm",
)
ipc_payload.update(
{
"request_id": req_id,
"sampling_params": sp_next,
"sent_ts": time.time(),
}
)
self.stage_list[next_stage_id].submit(ipc_payload)
t1 = time.time()
tx_ms = (t1 - t0) * 1000.0
metrics.on_forward(
stage_id,
next_stage_id,
req_id,
int(size_bytes),
float(tx_ms),
bool("engine_inputs_shm" in ipc_payload),
)
except Exception as e:
logger.warning(
"[Orchestrator] IPC encode failed for req %s: %s; falling back to inline payload",
req_id,
e,
)
self.stage_list[next_stage_id].submit(
{
"request_id": req_id,
"engine_inputs": next_inputs,
"sampling_params": sp_next,
}
)
if self.worker_backend == "ray":
# Ray mode: Skip SHM fallback, just send object via Queue (Ray handles serialization)
# This prevents SHM access errors across nodes
ipc_payload = {
"request_id": req_id,
"engine_inputs": next_inputs,
"sampling_params": sp_next,
"sent_ts": time.time(),
}
size_bytes = 0 # Approximation or calculate size if needed
try:
# Approximate size for stats
size_bytes = len(_set(next_inputs))
except:
pass
t0 = time.time()
# Direct submit to Ray Queue
else:
size_bytes = 0
try:
size_bytes = len(_set(next_inputs))
except Exception:
size_bytes = 0
t0 = time.time()
ipc_payload = _encode(
next_inputs,
getattr(self, "_shm_threshold_bytes", 65536),
obj_key="engine_inputs",
shm_key="engine_inputs_shm",
)
ipc_payload.update(
{
"request_id": req_id,
"sampling_params": sp_next,
"sent_ts": time.time(),
}
)
self.stage_list[next_stage_id].submit(ipc_payload)
t1 = time.time()
tx_ms = (t1 - t0) * 1000.0
metrics.on_forward(
stage_id,
next_stage_id,
req_id,
int(size_bytes),
float(tx_ms),
bool("engine_inputs_shm" in ipc_payload),
)
except Exception as e:
logger.warning(
"[Orchestrator] IPC encode failed for req %s: %s; falling back to inline payload",
req_id,
e,
)
self.stage_list[next_stage_id].submit(
{
"request_id": req_id,
"engine_inputs": next_inputs,
"sampling_params": sp_next,
}
)
logger.debug(
"[Orchestrator] Forwarded request %s to stage-%s",
req_id,
+32 -3
View File
@@ -163,6 +163,7 @@ class OmniStage:
shm_threshold_bytes: int = 65536,
ctx: Optional[mp.context.BaseContext] = None,
batch_timeout: int = 10,
connectors_config: Optional[dict] = None, # New parameter
) -> None:
"""Initialize and start the stage worker process.
@@ -192,6 +193,7 @@ class OmniStage:
"engine_args": engine_args,
"runtime": runtime_cfg,
"shm_threshold_bytes": self._shm_threshold_bytes,
"connectors_config": connectors_config or {},
}
if is_async:
self._proc = ctx.Process(
@@ -340,6 +342,8 @@ def _stage_worker(
log_stage_batch_stats,
log_stage_running_avg,
)
from vllm_omni.distributed.connectors.adapter import try_recv_via_connector
from vllm_omni.distributed.connectors.utils import build_stage_connectors
from vllm_omni.entrypoints.omni_llm import OmniStageLLM
# no inline JSONL/serialization imports; logging handled by utilities
@@ -347,6 +351,7 @@ def _stage_worker(
engine_args = stage_payload.get("engine_args", {})
runtime_cfg = stage_payload.get("runtime", {})
shm_threshold_bytes = int(stage_payload.get("shm_threshold_bytes", 65536))
connectors_config = stage_payload.get("connectors_config", {})
# Per-stage logger: clear inherited handlers to avoid broken parent streams
try:
@@ -440,6 +445,18 @@ def _stage_worker(
)
stage_engine = OmniStageLLM(model=model, **engine_args)
_logging.getLogger(__name__).debug("[Stage-%s] Engine initialized", stage_id)
# Initialize OmniConnectors if configured
connectors = {}
if connectors_config:
built_connectors = build_stage_connectors(
stage_id=stage_id,
connectors_config=connectors_config,
)
if built_connectors is None:
return
connectors = built_connectors
# Signal readiness to orchestrator
try:
out_q.put({"type": "stage_ready", "stage_id": stage_id})
@@ -496,9 +513,21 @@ def _stage_worker(
_in_flight_ms_by_rid[rid] = 0.0
except Exception:
_in_flight_ms_by_rid[rid] = 0.0
ein, _rx_metrics = maybe_load_from_ipc_with_metrics(t, obj_key="engine_inputs", shm_key="engine_inputs_shm")
_rx_decode_ms_by_rid[rid] = float(_rx_metrics.get("rx_decode_time_ms", 0.0))
_rx_bytes_by_rid[rid] = int(_rx_metrics.get("rx_transfer_bytes", 0))
# Resolve input data (via Connector or IPC)
ein, _rx_metrics = try_recv_via_connector(
task=t,
connectors=connectors,
stage_id=stage_id
)
if ein is None or _rx_metrics is None:
ein, _rx_metrics = maybe_load_from_ipc_with_metrics(task, obj_key="engine_inputs", shm_key="engine_inputs_shm")
if _rx_metrics:
_rx_decode_ms_by_rid[rid] = float(_rx_metrics.get("rx_decode_time_ms", 0.0))
_rx_bytes_by_rid[rid] = int(_rx_metrics.get("rx_transfer_bytes", 0))
batch_request_ids.append(rid)
if isinstance(ein, list):
batch_engine_inputs.extend(ein)
+37 -18
View File
@@ -10,6 +10,41 @@ from vllm_omni.utils import detect_device_type
PROJECT_ROOT = Path(__file__).parent.parent.parent
def resolve_model_config_path(model: str) -> str:
"""Resolve the stage config file path from the model name.
Resolves stage configuration path based on the model type and device type.
First tries to find a device-specific YAML file from stage_configs/{device_type}/
directory. If not found, falls back to the default config file.
Args:
model: Model name or path (used to determine model_type)
Returns:
String path to the stage configuration file
Raises:
FileNotFoundError: If no stage config file exists for the model type
"""
hf_config = get_config(model, trust_remote_code=True)
model_type = hf_config.model_type
device_type = detect_device_type()
# Try device-specific config first
if device_type != "cuda":
device_config_file = f"vllm_omni/model_executor/stage_configs/{device_type}/{model_type}.yaml"
device_config_path = PROJECT_ROOT / device_config_file
if os.path.exists(device_config_path):
return str(device_config_path)
# Fall back to default config
stage_config_file = f"vllm_omni/model_executor/stage_configs/{model_type}.yaml"
stage_config_path = PROJECT_ROOT / stage_config_file
if not os.path.exists(stage_config_path):
raise FileNotFoundError(f"Stage config file {stage_config_path} not found")
return str(stage_config_path)
def load_stage_configs_from_model(model: str) -> list:
"""Load stage configurations from model's default config file.
@@ -26,24 +61,8 @@ def load_stage_configs_from_model(model: str) -> list:
Raises:
FileNotFoundError: If no stage config file exists for the model type
"""
hf_config = get_config(model, trust_remote_code=True)
model_type = hf_config.model_type
device_type = detect_device_type()
# Try device-specific config first
if device_type != "cuda":
device_config_file = f"vllm_omni/model_executor/stage_configs/{device_type}/{model_type}.yaml"
device_config_path = PROJECT_ROOT / device_config_file
if os.path.exists(device_config_path):
stage_configs = load_stage_configs_from_yaml(config_path=str(device_config_path))
return stage_configs
# Fall back to default config
stage_config_file = f"vllm_omni/model_executor/stage_configs/{model_type}.yaml"
stage_config_path = PROJECT_ROOT / stage_config_file
if not os.path.exists(stage_config_path):
raise FileNotFoundError(f"Stage config file {stage_config_path} not found")
stage_configs = load_stage_configs_from_yaml(config_path=str(stage_config_path))
stage_config_path = resolve_model_config_path(model)
stage_configs = load_stage_configs_from_yaml(config_path=stage_config_path)
return stage_configs
@@ -26,6 +26,9 @@ stage_args:
seed: 42
detokenize: True
repetition_penalty: 1.1
# Distributed connector configuration (optional)
output_connectors:
to_stage_1: connector_thinker2talker # thinker -> talker: use local mooncake
- stage_id: 1
runtime:
process: true
@@ -52,17 +55,22 @@ stage_args:
detokenize: True
repetition_penalty: 1.05
stop_token_ids: [8294]
# Distributed connector configuration (optional)
input_connectors:
from_stage_0: connector_thinker2talker # talker <- thinker: use local mooncake
output_connectors:
to_stage_2: connector_talker2generator # talker -> code2wav: use remote mooncake (example)
- stage_id: 2
runtime:
process: true
devices: "0" # Example: use a different GPU than the previous stage; use "0" if single GPU
devices: "2" # Example: use a different GPU than the previous stage; use "0" if single GPU
max_batch_size: 1
engine_args:
model_stage: code2wav
model_arch: Qwen2_5OmniForConditionalGeneration
worker_cls: vllm_omni.worker.gpu_diffusion_worker.GPUDiffusionWorker
scheduler_cls: vllm_omni.core.sched.diffusion_scheduler.DiffusionScheduler
gpu_memory_utilization: 0.15
gpu_memory_utilization: 0.3
enforce_eager: true
trust_remote_code: true
enable_prefix_caching: false
@@ -78,6 +86,9 @@ stage_args:
seed: 42
detokenize: True
repetition_penalty: 1.1
# Distributed connector configuration (optional)
input_connectors:
from_stage_1: connector_talker2generator # code2wav <- talker: use remote mooncake
# Top-level runtime config (concise): default windows and stage edges
runtime:
@@ -85,6 +96,31 @@ runtime:
defaults:
window_size: -1 # Simplified: trigger downstream only after full upstream completion
max_inflight: 1 # Simplified: process serially within each stage
# Distributed connectors configuration (optional)
connectors:
# Local mooncake for same-node communication
connector_thinker2talker:
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"
# Remote mooncake for cross-node communication (example)
connector_talker2generator:
name: MooncakeConnector
extra:
host: "127.0.0.1"
metadata_server: "http://10.90.67.86:8080/metadata"
master: "10.90.67.86:50051"
segment: 1073741824 # 1GB
localbuf: 134217728 # 128MB
proto: "tcp"
edges:
- from: 0 # thinker → talker: trigger only after receiving full input (-1)
to: 1