Merge branch 'main' into codex/unskip-fp8-z-image-quality

This commit is contained in:
WeiQing Chen authored and GitHub committed 2026-05-28 19:18:52 +08:00
commit 4bbe81308b
56 files changed
+1821 -736

No files matched your search

+6 -5
View File
@@ -570,6 +570,7 @@ steps:
tests/e2e/online_serving/test_longcat_image_expansion.py
tests/e2e/online_serving/test_longcat_image_edit_expansion.py
tests/e2e/online_serving/test_flux_2_dev_expansion.py
tests/e2e/offline_inference/test_glm_image_autoround_w4a16_expansion.py
-m "full_model and diffusion and H100 and distributed_cuda"
--run-level "full_model"
agents:
@@ -842,8 +843,8 @@ steps:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: ":full_moon: Diffusion X2I(&A&T) · Perf Test · Qwen-Image-Edit-2509"
key: nightly-diffusion-x2iat-performance-qwen-image-edit-2509
- label: ":full_moon: Diffusion X2I(&A&T) · Perf Test · Qwen-Image-Edit-2511"
key: nightly-diffusion-x2iat-performance-qwen-image-edit-2511
timeout_in_minutes: 180
commands:
- export DIFFUSION_BENCHMARK_DIR=tests/dfx/perf/results
@@ -851,7 +852,7 @@ steps:
- export CACHE_DIT_VERSION=1.3.0
- |
set +e
pytest -s -v tests/dfx/perf/scripts/run_diffusion_benchmark.py --test-config-file tests/dfx/perf/tests/test_qwen_image_edit_2509_vllm_omni.json
pytest -s -v tests/dfx/perf/scripts/run_diffusion_benchmark.py --test-config-file tests/dfx/perf/tests/test_qwen_image_edit_2511_vllm_omni.json
EXIT=$$?
buildkite-agent artifact upload "tests/dfx/perf/results/diffusion_result_*.json"
buildkite-agent artifact upload "tests/dfx/perf/results/logs/*.log"
@@ -1287,7 +1288,7 @@ steps:
- nightly-tts-performance
- nightly-diffusion-x2iat-performance-qwen-image
- nightly-diffusion-x2iat-performance-qwen-image-edit
- nightly-diffusion-x2iat-performance-qwen-image-edit-2509
- nightly-diffusion-x2iat-performance-qwen-image-edit-2511
- nightly-diffusion-x2iat-performance-qwen-image-layered
- nightly-diffusion-x2iat-performance-bagel
- nightly-diffusion-x2v-performance
@@ -1304,7 +1305,7 @@ steps:
- buildkite-agent artifact download "tests/dfx/perf/results/*.json" . --step nightly-omni-performance-multi-replicas
- buildkite-agent artifact download "tests/dfx/perf/results/*.json" . --step nightly-diffusion-x2iat-performance-qwen-image
- buildkite-agent artifact download "tests/dfx/perf/results/*.json" . --step nightly-diffusion-x2iat-performance-qwen-image-edit
- buildkite-agent artifact download "tests/dfx/perf/results/*.json" . --step nightly-diffusion-x2iat-performance-qwen-image-edit-2509
- buildkite-agent artifact download "tests/dfx/perf/results/*.json" . --step nightly-diffusion-x2iat-performance-qwen-image-edit-2511
- buildkite-agent artifact download "tests/dfx/perf/results/*.json" . --step nightly-diffusion-x2iat-performance-qwen-image-layered
- buildkite-agent artifact download "tests/dfx/perf/results/*.json" . --step nightly-diffusion-x2iat-performance-bagel
- buildkite-agent artifact download "tests/dfx/perf/results/*.json" . --step nightly-diffusion-x2v-performance
+5 -7
View File
@@ -169,15 +169,13 @@ class DiffusionRequestStatus(enum.IntEnum):
@dataclass
class DiffusionRequestState:
sched_req_id: str
request_id: str
req: OmniDiffusionRequest
status: DiffusionRequestStatus = DiffusionRequestStatus.WAITING
```
**Design Features**:
- **Scheduler-owned ID**: Each `OmniDiffusionRequest` is tracked by an internal `sched_req_id`, separated from public `request_id` values.
- **Explicit lifecycle**: Requests move through waiting, running, optional preemption, and terminal states.
- **Centralized error handling**: Completion, abort, and error states are all normalized in the scheduler layer.
@@ -188,7 +186,6 @@ class DiffusionRequestState:
class _BaseScheduler(SchedulerInterface):
def __init__(self) -> None:
self._request_states = {}
self._request_id_to_sched_req_id = {}
self._waiting = deque()
self._running = []
self._finished_req_ids = set()
@@ -199,7 +196,8 @@ class _BaseScheduler(SchedulerInterface):
- **Common state storage**: Shared request maps and waiting/running sets live in the base class.
- **Shared cleanup logic**: Request-id registration, finish handling, and state removal are centralized instead of duplicated in each policy.
- **Shared cleanup logic**: Duplicate-request checks, finish handling, and state removal are centralized instead of
duplicated in each policy.
- **Current constraint boundary**: `_BaseScheduler` derives `max_num_running_reqs` from `max_num_seqs`, but request-mode diffusion is still clamped back to `1` by the engine. The step-wise path can keep this above `1` for compatible-request batching.
@@ -219,12 +217,12 @@ class RequestScheduler(_BaseScheduler):
- **Single-request admission**: `RequestScheduler` still admits one active request at a time because request-mode execution completes a whole request per dispatch.
- **Executor result feedback**: `update_from_output()` converts executor output into `FINISHED_COMPLETED` or `FINISHED_ERROR` and returns finished scheduler ids.
- **Executor result feedback**: `update_from_output()` converts executor output into `FINISHED_COMPLETED` or `FINISHED_ERROR` and returns finished request ids.
#### 2.5 Engine-Driven Execution Loop
```python
sched_req_id = scheduler.add_request(request)
request_id = scheduler.add_request(request)
while True:
sched_output = scheduler.schedule()
output = executor.add_req(req)
+16 -7
View File
@@ -16,6 +16,7 @@ ROOT_DIR_RELATIVE = "../../../../.."
EXAMPLE_DIR = ROOT_DIR / "examples"
EXAMPLE_DOC_DIR = ROOT_DIR / "docs/user_guide/examples"
NAV_FILE = ROOT_DIR / "docs/.nav.yml"
MAX_INLINE_MATERIAL_SIZE = 8 * 1024
def fix_case(text: str) -> str:
@@ -191,18 +192,21 @@ class Example:
return re.sub(link_pattern, replace_link, content)
def github_url(self, path: Path) -> str:
url = "https://github.com/vllm-project/vllm-omni/"
url += "tree/main" if path.is_dir() else "blob/main"
return f"{url}/{path.relative_to(ROOT_DIR).as_posix()}"
def generate(self) -> str:
content = f"# {self.title}\n\n"
url = "https://github.com/vllm-project/vllm-omni/"
url += "tree/main" if self.path.is_dir() else "blob/main"
content += f"Source <{url}/{self.path.relative_to(ROOT_DIR)}>.\n\n"
content += f"Source <{self.github_url(self.path)}>.\n\n"
# Use long code fence to avoid issues with
# included files containing code fences too
code_fence = "``````"
if self.is_code:
main_file_rel = self.main_file.relative_to(ROOT_DIR)
main_file_rel = self.main_file.relative_to(ROOT_DIR).as_posix()
content += f'{code_fence}{self.main_file.suffix[1:]}\n--8<-- "{main_file_rel}"\n{code_fence}\n'
else:
with open(self.main_file, encoding="utf-8") as f:
@@ -216,10 +220,15 @@ class Example:
content += "## Example materials\n\n"
for file in sorted(self.other_files):
content += f'??? abstract "{file.relative_to(self.path)}"\n'
content += f'??? abstract "{file.relative_to(self.path).as_posix()}"\n'
if file.stat().st_size > MAX_INLINE_MATERIAL_SIZE:
content += (
f" Large file omitted from the rendered docs. View it on GitHub: <{self.github_url(file)}>.\n\n"
)
continue
if file.suffix != ".md":
content += f" {code_fence}{file.suffix[1:]}\n"
content += f' --8<-- "{file.relative_to(ROOT_DIR)}"\n'
content += f' --8<-- "{file.relative_to(ROOT_DIR).as_posix()}"\n'
if file.suffix != ".md":
content += f" {code_fence}\n"
@@ -291,7 +300,7 @@ def update_nav_file(examples: list[Example]):
for example in category_examples:
doc_path = EXAMPLE_DOC_DIR / example.category / f"{example.path.stem}.md"
rel_path = doc_path.relative_to(ROOT_DIR / "docs")
category_items.append({example.title: str(rel_path)})
category_items.append({example.title: rel_path.as_posix()})
if category_items:
# Format category name (e.g., "offline_inference" -> "Offline Inference")
+29
View File
@@ -0,0 +1,29 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Reduce browser search-index memory for generated API reference pages."""
from material.plugins.search.plugin import SearchIndex
_add_entry_from_context = SearchIndex.add_entry_from_context
def add_entry_from_context(self: SearchIndex, page) -> None:
search = page.meta.get("search") or {}
if search.get("exclude"):
return
if str(page.url).startswith("api/"):
entry = {
"location": page.url,
"title": str(page.meta.get("title", page.title)),
"text": "",
}
if "boost" in search:
entry["boost"] = search["boost"]
self.entries.append(entry)
return
_add_entry_from_context(self, page)
SearchIndex.add_entry_from_context = add_entry_from_context
+42 -18
View File
@@ -1,20 +1,44 @@
// Enables MathJax rendering
// Enables MathJax rendering only on pages that contain math.
window.MathJax = {
tex: {
inlineMath: [["\\(", "\\)"]],
displayMath: [["\\[", "\\]"]],
processEscapes: true,
processEnvironments: true
},
options: {
ignoreHtmlClass: ".*|",
processHtmlClass: "arithmatex"
}
};
tex: {
inlineMath: [["\\(", "\\)"]],
displayMath: [["\\[", "\\]"]],
processEscapes: true,
processEnvironments: true,
},
options: {
ignoreHtmlClass: ".*|",
processHtmlClass: "arithmatex",
},
};
document$.subscribe(() => {
MathJax.startup.output.clearCache()
MathJax.typesetClear()
MathJax.texReset()
MathJax.typesetPromise()
})
let mathJaxLoading;
function loadMathJax() {
if (window.MathJax.typesetPromise) {
return Promise.resolve();
}
if (!mathJaxLoading) {
mathJaxLoading = new Promise((resolve, reject) => {
const script = document.createElement("script");
script.src = "https://unpkg.com/mathjax@3.2.2/es5/tex-mml-chtml.js";
script.async = true;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
return mathJaxLoading;
}
document$.subscribe(() => {
if (!document.querySelector(".arithmatex")) {
return;
}
loadMathJax().then(() => {
MathJax.startup.output.clearCache();
MathJax.typesetClear();
MathJax.texReset();
MathJax.typesetPromise();
});
});
+2
View File
@@ -0,0 +1,2 @@
// Opt out of bfcache so large generated API pages are released after navigation.
window.addEventListener("unload", () => {});
+38 -17
View File
@@ -1,21 +1,42 @@
// Initialize Mermaid for diagram rendering
mermaid.initialize({
startOnLoad: false,
theme: 'default',
securityLevel: 'loose',
flowchart: {
useMaxWidth: true,
htmlLabels: true
}
});
// Load Mermaid only on pages that contain diagrams.
let mermaidLoading;
// Render Mermaid diagrams when page content is ready
document$.subscribe(() => {
const mermaidElements = document.querySelectorAll('.mermaid');
if (mermaidElements.length > 0) {
mermaid.run({
querySelector: '.mermaid',
nodes: mermaidElements
function loadMermaid() {
if (window.mermaid) {
return Promise.resolve();
}
if (!mermaidLoading) {
mermaidLoading = new Promise((resolve, reject) => {
const script = document.createElement("script");
script.src = "https://unpkg.com/mermaid@10/dist/mermaid.min.js";
script.async = true;
script.onload = () => {
mermaid.initialize({
startOnLoad: false,
theme: "default",
securityLevel: "loose",
flowchart: {
useMaxWidth: true,
htmlLabels: true,
},
});
resolve();
};
script.onerror = reject;
document.head.appendChild(script);
});
}
return mermaidLoading;
}
document$.subscribe(() => {
if (!document.querySelector(".mermaid")) {
return;
}
loadMermaid().then(() => {
mermaid.run({
querySelector: ".mermaid",
nodes: document.querySelectorAll(".mermaid"),
});
});
});
+1 -1
View File
@@ -52,8 +52,8 @@ vLLM-Omni's runtime module names.
| Model | Scope | Status | Notes |
|-------|-------|--------|-------|
| GLM-Image | Diffusion transformer | ✅ | `Intel/GLM-Image-int4-AutoRound` |
| BAGEL | Checkpoint-defined diffusion or transformer stage | Not validated | Requires a compatible AutoRound checkpoint |
| GLM-Image | Checkpoint-defined diffusion or transformer stage | Not validated | Requires a compatible AutoRound checkpoint |
## Configuration
@@ -127,7 +127,12 @@ def run_one_path(
),
args.device,
)
pred = run_pipeline_forward(pipeline, batch_inputs, noise)
pred = run_pipeline_forward(
pipeline,
batch_inputs,
noise,
request_id=f"internvla-a1-sample-{index}",
)
pred = pred[:, :, : dataset.physical_action_dim].to(torch.float32).cpu()
results.append(
{
@@ -150,6 +155,7 @@ def run_pipeline_forward(
pipeline,
batch_inputs: dict[str, torch.Tensor],
noise: torch.Tensor,
request_id: str,
) -> torch.Tensor:
output = pipeline.forward(
OmniDiffusionRequest(
@@ -161,6 +167,7 @@ def run_pipeline_forward(
"decode_image": False,
}
),
request_id=request_id,
)
)
if output.error:
@@ -212,23 +219,38 @@ def benchmark_forward(
_synchronize(args.device)
cold_start_begin = time.perf_counter()
pred = run_pipeline_forward(pipeline, batch_inputs, noise)
pred = run_pipeline_forward(
pipeline,
batch_inputs,
noise,
request_id=f"internvla-a1-benchmark-{index}-cold",
)
_synchronize(args.device)
cold_start_ms = (time.perf_counter() - cold_start_begin) * 1000.0
warmup_ms: list[float] = []
for _ in range(args.warmup_iters):
for iter_idx in range(args.warmup_iters):
_synchronize(args.device)
begin = time.perf_counter()
_ = run_pipeline_forward(pipeline, batch_inputs, noise)
_ = run_pipeline_forward(
pipeline,
batch_inputs,
noise,
request_id=f"internvla-a1-benchmark-{index}-warmup-{iter_idx}",
)
_synchronize(args.device)
warmup_ms.append((time.perf_counter() - begin) * 1000.0)
benchmark_ms: list[float] = []
for _ in range(args.benchmark_iters):
for iter_idx in range(args.benchmark_iters):
_synchronize(args.device)
begin = time.perf_counter()
_ = run_pipeline_forward(pipeline, batch_inputs, noise)
_ = run_pipeline_forward(
pipeline,
batch_inputs,
noise,
request_id=f"internvla-a1-benchmark-{index}-iter-{iter_idx}",
)
_synchronize(args.device)
benchmark_ms.append((time.perf_counter() - begin) * 1000.0)
@@ -295,7 +317,12 @@ def main() -> None:
dataset=dataset,
train_meta=train_meta,
collate_samples=collate_open_loop_samples,
run_sample_actions=lambda policy, batch_inputs, noise: run_pipeline_forward(policy, batch_inputs, noise),
run_sample_actions=lambda policy, batch_inputs, noise: run_pipeline_forward(
policy,
batch_inputs,
noise,
request_id="internvla-a1-open-loop",
),
num_episodes=args.num_episodes,
seed=args.seed,
device=args.device,
+4 -4
View File
@@ -38,13 +38,12 @@ theme:
features:
- content.action.edit
- content.code.copy
- navigation.instant
- navigation.instant.progress
- navigation.tracking
- navigation.tabs
- navigation.tabs.sticky
- navigation.sections
- navigation.indexes
- navigation.prune
- navigation.top
- search.suggest
- search.highlight
@@ -56,6 +55,7 @@ theme:
custom_dir: docs/mkdocs/overrides
hooks:
- docs/mkdocs/hooks/search_index.py
- docs/mkdocs/hooks/generate_api_readme.py
- docs/mkdocs/hooks/url_schemes.py
- docs/mkdocs/hooks/generate_examples.py
@@ -117,6 +117,7 @@ plugins:
separate_signature: true
show_overloads: true
signature_crossrefs: true
show_source: false
inventories:
- https://docs.python.org/3/objects.inv
- https://typing-extensions.readthedocs.io/en/latest/objects.inv
@@ -167,9 +168,8 @@ extra_css:
- mkdocs/stylesheets/extra.css
extra_javascript:
- mkdocs/javascript/memory.js
- mkdocs/javascript/mathjax.js
- https://unpkg.com/mathjax@3.2.2/es5/tex-mml-chtml.js
- https://unpkg.com/mermaid@10/dist/mermaid.min.js
- mkdocs/javascript/mermaid.js
- mkdocs/javascript/edit_and_feedback.js
- mkdocs/javascript/slack_and_forum.js
@@ -63,24 +63,6 @@
}
},
"benchmark_params": [
{
"name": "512x512_steps20_i2i_2img",
"dataset": "random",
"task": "i2i",
"width": 512,
"height": 512,
"num-inference-steps": 20,
"num-prompts": 10,
"max-concurrency": 1,
"num-input-images": 2,
"enable-negative-prompt": true,
"baseline": {
"throughput_qps": 0.15,
"latency_mean": 10,
"peak_memory_mb_max": 55000,
"peak_memory_mb_mean": 55000
}
},
{
"name": "1536x1536_steps35_i2i_2img",
"dataset": "random",
@@ -61,23 +61,6 @@
}
},
"benchmark_params": [
{
"name": "512x512_steps20_i2i",
"dataset": "random",
"task": "i2i",
"width": 512,
"height": 512,
"num-inference-steps": 20,
"num-prompts": 10,
"max-concurrency": 1,
"enable-negative-prompt": true,
"baseline": {
"throughput_qps": 0.21,
"latency_mean": 10,
"peak_memory_mb_max": 56000,
"peak_memory_mb_mean": 56000
}
},
{
"name": "1536x1536_steps35_i2i",
"dataset": "random",
@@ -60,6 +60,7 @@ def _make_request(**overrides) -> OmniDiffusionRequest:
output_type="pil",
generator_device="cpu",
),
"request_id": "diffusers-backend",
}
defaults.update(overrides)
return OmniDiffusionRequest(**defaults)
@@ -48,6 +48,7 @@ def _make_request(prompts=None, **sp_kwargs) -> OmniDiffusionRequest:
return OmniDiffusionRequest(
prompts=prompts or [{"prompt": "a cat dancing"}],
sampling_params=sp,
request_id="dmd2-sanitize",
)
@@ -48,7 +48,7 @@ def _make_pipeline(cls):
def _make_request(**sp_kwargs) -> OmniDiffusionRequest:
sp = OmniDiffusionSamplingParams(**sp_kwargs)
return OmniDiffusionRequest(prompts=[{"prompt": "a cat"}], sampling_params=sp)
return OmniDiffusionRequest(prompts=[{"prompt": "a cat"}], sampling_params=sp, request_id="dmd2-scheduler")
@pytest.fixture(
@@ -73,7 +73,7 @@ def test_should_apply_pe_respects_sampling_extra_args():
def test_should_apply_pe_disables_dummy_warmup_request():
req = SimpleNamespace(request_ids=["dummy_req_id"], sampling_params=SimpleNamespace(extra_args={}))
req = SimpleNamespace(is_dummy_run=lambda: True, sampling_params=SimpleNamespace(extra_args={}))
assert ErnieImagePipeline._should_apply_pe(req) is False
@@ -0,0 +1,741 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Unit tests for GLM-Image quantization support (W4A16/AutoRound).
These tests verify that the GLM-Image DiT transformer correctly accepts and uses
quantization configs for W4A16/AutoRound quantization support.
"""
import os
import pytest
import torch
from pytest_mock import MockerFixture
from vllm_omni.diffusion.data import DiffusionParallelConfig
from vllm_omni.diffusion.models.glm_image.glm_image_transformer import (
ColumnParallelGELU,
ColumnParallelSiLU,
GlmImageAdaLayerNormContinuous,
GlmImageAdaLayerNormZero,
GlmImageAttention,
GlmImageFeedForward,
GlmImageImageProjector,
GlmImagePrepare,
GlmImageRotaryPosEmbed,
GlmImageTransformer2DModel,
GlmImageTransformerBlock,
_positive_divisors,
validate_glm_image_tp_constraints,
)
from vllm_omni.model_executor.models.glm_image.pipeline import GLM_IMAGE_PIPELINE
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
@pytest.fixture(scope="function", autouse=True)
def _init_distributed():
"""Initialize the minimal distributed environment required by
ReplicatedLinear (tensor-parallel group must exist)."""
from vllm.distributed.parallel_state import (
cleanup_dist_env_and_memory,
init_distributed_environment,
initialize_model_parallel,
)
os.environ.setdefault("MASTER_ADDR", "localhost")
os.environ.setdefault("MASTER_PORT", "29502")
init_distributed_environment(
world_size=1,
rank=0,
local_rank=0,
distributed_init_method="env://",
)
initialize_model_parallel()
yield
cleanup_dist_env_and_memory()
@pytest.fixture(scope="function", autouse=True)
def _force_default_gemm(monkeypatch):
"""Force CPU-compatible GEMM dispatch for tests using CPU tensors.
vLLM's dispatch_unquantized_gemm() selects the backend by platform
(e.g. rocm_unquantized_gemm on AMD machines), not by tensor device.
CPU test tensors crash with NotImplementedError on ROCm. Monkeypatch
the dispatcher to always return the default (torch.nn.functional.linear)
implementation which works on any device."""
from vllm.model_executor.layers.utils import default_unquantized_gemm
monkeypatch.setattr(
"vllm.model_executor.layers.linear.dispatch_unquantized_gemm",
lambda: default_unquantized_gemm,
)
@pytest.fixture(scope="function", autouse=True)
def setup_mocks(mocker: MockerFixture):
"""Set up common mocks for all tests."""
mocker.patch(
"vllm.model_executor.layers.linear.get_tensor_model_parallel_world_size",
return_value=1,
)
mock_get_tp_group = mocker.patch("vllm.distributed.parallel_state.get_tp_group")
mock_tp_group = mocker.MagicMock()
mock_tp_group.world_size = 1
mock_get_tp_group.return_value = mock_tp_group
yield
class TestPositiveDivisors:
"""Test _positive_divisors helper function."""
def test_divisors_of_1(self):
assert _positive_divisors(1) == {1}
def test_divisors_of_12(self):
assert _positive_divisors(12) == {1, 2, 3, 4, 6, 12}
def test_divisors_of_prime(self):
assert _positive_divisors(7) == {1, 7}
def test_divisors_of_0_returns_empty(self):
assert _positive_divisors(0) == set()
def test_divisors_of_negative_returns_empty(self):
assert _positive_divisors(-5) == set()
class TestValidateGlmImageTpConstraints:
"""Test TP constraint validation for GLM-Image."""
def test_valid_tp_size_1(self):
"""TP=1 should always be valid."""
result = validate_glm_image_tp_constraints(
dim=2560,
num_heads=64,
ffn_hidden_dim=10240,
tensor_parallel_size=1,
)
assert 1 in result
def test_valid_tp_size_2_for_divisible_dim(self):
"""TP=2 is valid when all dims are divisible by 2."""
result = validate_glm_image_tp_constraints(
dim=2560,
num_heads=64,
ffn_hidden_dim=10240,
tensor_parallel_size=2,
)
assert 1 in result
assert 2 in result
def test_valid_tp_size_4_for_divisible_dim(self):
"""TP=4 is valid when all dims are divisible by 4."""
result = validate_glm_image_tp_constraints(
dim=2560,
num_heads=64,
ffn_hidden_dim=10240,
tensor_parallel_size=4,
)
assert 1 in result
assert 2 in result
assert 4 in result
def test_invalid_tp_size_3_for_divisible_dim(self):
"""TP=3 is invalid when dim is not divisible by 3."""
with pytest.raises(ValueError, match="dim % tensor_parallel_size == 0"):
validate_glm_image_tp_constraints(
dim=2560, # 2560 % 3 != 0
num_heads=64,
ffn_hidden_dim=10240,
tensor_parallel_size=3,
)
def test_invalid_tp_size_zero(self):
"""TP=0 should raise error."""
with pytest.raises(ValueError, match="tensor_parallel_size must be > 0"):
validate_glm_image_tp_constraints(
dim=2560,
num_heads=64,
ffn_hidden_dim=10240,
tensor_parallel_size=0,
)
def test_invalid_tp_size_negative(self):
"""Negative TP size should raise error."""
with pytest.raises(ValueError, match="tensor_parallel_size must be > 0"):
validate_glm_image_tp_constraints(
dim=2560,
num_heads=64,
ffn_hidden_dim=10240,
tensor_parallel_size=-1,
)
class TestGlmImageAdaLayerNormZeroQuantization:
"""Test GlmImageAdaLayerNormZero with quantization config."""
def test_accepts_quant_config_parameter(self, mocker: MockerFixture):
"""Verify the class accepts quant_config parameter."""
mock_quant_config = mocker.MagicMock()
layer = GlmImageAdaLayerNormZero(
embedding_dim=512,
dim=2560,
quant_config=mock_quant_config,
prefix="test.norm1",
)
assert layer.linear.quant_config is mock_quant_config
def test_accepts_none_quant_config(self):
"""Verify quant_config=None is accepted."""
layer = GlmImageAdaLayerNormZero(
embedding_dim=512,
dim=2560,
quant_config=None,
)
assert layer.linear.quant_config is None
def test_forward_handles_tuple_return_from_linear(self):
"""Verify forward handles tuple returns from ReplicatedLinear."""
layer = GlmImageAdaLayerNormZero(
embedding_dim=512,
dim=2560,
quant_config=None,
)
batch_size = 2
seq_len = 10
hidden_states = torch.randn(batch_size, seq_len, 2560)
encoder_hidden_states = torch.randn(batch_size, seq_len, 2560)
temb = torch.randn(batch_size, 512)
# This should work regardless of whether linear returns tuple or tensor
result = layer(hidden_states, encoder_hidden_states, temb)
assert len(result) == 10 # Should return 10 chunks
class TestGlmImageAdaLayerNormContinuousQuantization:
"""Test GlmImageAdaLayerNormContinuous with quantization config."""
def test_accepts_quant_config_parameter(self, mocker: MockerFixture):
"""Verify the class accepts quant_config parameter."""
mock_quant_config = mocker.MagicMock()
layer = GlmImageAdaLayerNormContinuous(
embedding_dim=2560,
conditioning_embedding_dim=512,
quant_config=mock_quant_config,
prefix="test.norm_out",
)
assert layer.linear.quant_config is mock_quant_config
def test_accepts_none_quant_config(self):
"""Verify quant_config=None is accepted."""
layer = GlmImageAdaLayerNormContinuous(
embedding_dim=2560,
conditioning_embedding_dim=512,
quant_config=None,
)
assert layer.linear.quant_config is None
def test_forward_handles_tuple_return_from_linear(self):
"""Verify forward handles tuple returns from ReplicatedLinear."""
layer = GlmImageAdaLayerNormContinuous(
embedding_dim=2560,
conditioning_embedding_dim=512,
quant_config=None,
)
batch_size = 2
seq_len = 10
x = torch.randn(batch_size, seq_len, 2560)
conditioning_embedding = torch.randn(batch_size, 512)
result = layer(x, conditioning_embedding)
assert result.shape == (batch_size, seq_len, 2560)
class TestGlmImageAttentionQuantization:
"""Test GlmImageAttention with quantization config."""
def test_accepts_quant_config_parameter(self, mocker: MockerFixture):
"""Verify GlmImageAttention accepts quant_config parameter."""
mock_quant_config = mocker.MagicMock()
attn = GlmImageAttention(
dim=2560,
num_heads=64,
head_dim=40,
quant_config=mock_quant_config,
prefix="test.attn1",
)
assert attn.to_qkv.quant_config is mock_quant_config
def test_accepts_none_quant_config(self):
"""Verify quant_config=None is accepted."""
attn = GlmImageAttention(
dim=2560,
num_heads=64,
head_dim=40,
quant_config=None,
)
assert attn.to_qkv.quant_config is None
class TestColumnParallelModulesQuantization:
"""Test ColumnParallelGELU and ColumnParallelSiLU with quantization config."""
def test_column_parallel_gelu_accepts_quant_config(self, mocker: MockerFixture):
"""Verify ColumnParallelGELU accepts quant_config."""
mock_quant_config = mocker.MagicMock()
layer = ColumnParallelGELU(
dim_in=2560,
dim_out=10240,
quant_config=mock_quant_config,
prefix="test.gelu",
)
assert layer.proj.quant_config is mock_quant_config
def test_column_parallel_silu_accepts_quant_config(self, mocker: MockerFixture):
"""Verify ColumnParallelSiLU accepts quant_config."""
mock_quant_config = mocker.MagicMock()
layer = ColumnParallelSiLU(
dim_in=2560,
dim_out=10240,
quant_config=mock_quant_config,
prefix="test.silu",
)
assert layer.proj.quant_config is mock_quant_config
class TestGlmImageFeedForwardQuantization:
"""Test GlmImageFeedForward with quantization config."""
def test_accepts_quant_config_parameter(self, mocker: MockerFixture):
"""Verify GlmImageFeedForward accepts quant_config parameter."""
mock_quant_config = mocker.MagicMock()
ff = GlmImageFeedForward(
dim=2560,
dim_out=2560,
inner_dim=10240,
activation_fn="gelu-approximate",
quant_config=mock_quant_config,
prefix="test.ff",
)
# Check that the first layer (ColumnParallelGELU) has quant_config
gelu_layer = ff.net[0]
assert gelu_layer.proj.quant_config is mock_quant_config
def test_accepts_none_quant_config(self):
"""Verify quant_config=None is accepted."""
ff = GlmImageFeedForward(
dim=2560,
dim_out=2560,
inner_dim=10240,
activation_fn="gelu-approximate",
quant_config=None,
)
gelu_layer = ff.net[0]
assert gelu_layer.proj.quant_config is None
def test_linear_silu_activation(self):
"""Test linear-silu activation function initialization works."""
# This test verifies the module can be instantiated with linear-silu activation.
# Full forward testing requires proper TP group setup, so we just verify construction.
ff = GlmImageFeedForward(
dim=2560,
dim_out=2560,
inner_dim=10240,
activation_fn="linear-silu",
quant_config=None,
)
# Verify the FFN has the correct structure
assert len(ff.net) == 3 # ColumnParallelSiLU, Identity, RowParallelLinear
assert isinstance(ff.net[0], ColumnParallelSiLU)
class TestGlmImageTransformerBlockQuantization:
"""Test GlmImageTransformerBlock with quantization config."""
def test_accepts_quant_config_parameter(self, mocker: MockerFixture):
"""Verify GlmImageTransformerBlock accepts quant_config parameter."""
mock_quant_config = mocker.MagicMock()
parallel_config = DiffusionParallelConfig(
tensor_parallel_size=1,
sequence_parallel_size=1,
)
block = GlmImageTransformerBlock(
dim=2560,
num_attention_heads=64,
attention_head_dim=40,
time_embed_dim=512,
parallel_config=parallel_config,
quant_config=mock_quant_config,
prefix="test.block",
)
# Check that inner modules have quant_config
assert block.norm1.linear.quant_config is mock_quant_config
assert block.attn1.to_qkv.quant_config is mock_quant_config
assert block.ff.net[0].proj.quant_config is mock_quant_config
def test_accepts_none_quant_config(self):
"""Verify quant_config=None is accepted."""
block = GlmImageTransformerBlock(
dim=2560,
num_attention_heads=64,
attention_head_dim=40,
time_embed_dim=512,
quant_config=None,
)
assert block.norm1.linear.quant_config is None
class TestGlmImagePrepareModule:
"""Test GlmImagePrepare module."""
def test_prepare_module_exists(self):
"""Verify GlmImagePrepare module exists and works."""
projector = GlmImageImageProjector(in_channels=16, hidden_size=2560, patch_size=2)
rope = GlmImageRotaryPosEmbed(dim=40, patch_size=2)
prepare = GlmImagePrepare(
image_projector=projector,
rope=rope,
patch_size=2,
)
# Test forward pass
hidden_states = torch.randn(1, 16, 64, 64) # [B, C, H, W]
result = prepare(hidden_states)
assert len(result) == 5
hidden_out, rope_cos, rope_sin, height, width = result
assert hidden_out.shape[0] == 1 # batch size
assert hidden_out.shape[1] == 1024 # seq_len (32 * 32)
assert hidden_out.shape[2] == 2560 # hidden_dim
class TestGlmImagePipelineConfig:
"""Test GLM_IMAGE_PIPELINE configuration."""
def test_glm_image_pipeline_config_exists(self):
"""Verify GLM_IMAGE_PIPELINE config exists."""
assert GLM_IMAGE_PIPELINE is not None
def test_pipeline_has_correct_model_type(self):
"""Verify pipeline has correct model_type."""
assert GLM_IMAGE_PIPELINE.model_type == "glm_image"
def test_pipeline_has_correct_model_arch(self):
"""Verify pipeline has correct model_arch."""
assert GLM_IMAGE_PIPELINE.model_arch == "GlmImageForConditionalGeneration"
def test_pipeline_has_two_stages(self):
"""Verify pipeline has two stages (AR + DiT)."""
assert len(GLM_IMAGE_PIPELINE.stages) == 2
def test_stage_0_is_ar_llm(self):
"""Verify stage 0 is the AR LLM stage."""
stage_0 = GLM_IMAGE_PIPELINE.stages[0]
assert stage_0.stage_id == 0
assert stage_0.model_stage == "ar"
assert stage_0.execution_type.value == "llm_ar"
assert stage_0.owns_tokenizer is True
def test_stage_1_is_diffusion(self):
"""Verify stage 1 is the DiT diffusion stage."""
stage_1 = GLM_IMAGE_PIPELINE.stages[1]
assert stage_1.stage_id == 1
assert stage_1.model_stage == "dit"
assert stage_1.execution_type.value == "diffusion"
assert stage_1.final_output is True
assert stage_1.final_output_type == "image"
assert stage_1.input_sources == (0,) # Takes input from stage 0
def test_pipeline_has_diffusers_class_name(self):
"""Verify pipeline has diffusers_class_name."""
assert GLM_IMAGE_PIPELINE.diffusers_class_name == "GlmImagePipeline"
class TestGlmImageImageProjector:
"""Test GlmImageImageProjector module."""
def test_projector_output_shape(self):
"""Verify projector produces correct output shape."""
projector = GlmImageImageProjector(
in_channels=16,
hidden_size=2560,
patch_size=2,
)
# Input: [B, C, H, W] = [1, 16, 64, 64]
hidden_states = torch.randn(1, 16, 64, 64)
output = projector(hidden_states)
# After patchify: [B, H/2*W/2, D] = [1, 32*32, 2560] = [1, 1024, 2560]
assert output.shape == (1, 1024, 2560)
def test_projector_with_different_patch_sizes(self):
"""Verify projector works with different patch sizes."""
for patch_size in [2, 4]:
projector = GlmImageImageProjector(
in_channels=16,
hidden_size=2560,
patch_size=patch_size,
)
h, w = 64, 64
hidden_states = torch.randn(1, 16, h, w)
output = projector(hidden_states)
expected_seq_len = (h // patch_size) * (w // patch_size)
assert output.shape[1] == expected_seq_len
class TestGlmImageRotaryPosEmbed:
"""Test GlmImageRotaryPosEmbed module."""
def test_rope_output_shape(self):
"""Verify RoPE produces correct output shape."""
rope = GlmImageRotaryPosEmbed(dim=40, patch_size=2, theta=10000.0)
hidden_states = torch.randn(1, 16, 64, 64) # [B, C, H, W]
cos, sin = rope(hidden_states)
# After patchify: height=32, width=32
# Output: [32*32, dim] = [1024, 40]
assert cos.shape[0] == 1024
assert cos.shape[1] == 40
assert sin.shape == cos.shape
def test_rope_value_range(self):
"""Verify RoPE produces values in valid range."""
rope = GlmImageRotaryPosEmbed(dim=40, patch_size=2, theta=10000.0)
hidden_states = torch.randn(2, 16, 32, 32)
cos, sin = rope(hidden_states)
# cos and sin should be in [-1, 1]
assert cos.min() >= -1.0 and cos.max() <= 1.0
assert sin.min() >= -1.0 and sin.max() <= 1.0
def test_rope_consistency(self):
"""Verify RoPE is consistent for same input."""
rope = GlmImageRotaryPosEmbed(dim=40, patch_size=2, theta=10000.0)
hidden_states = torch.randn(1, 16, 32, 32)
cos1, sin1 = rope(hidden_states)
cos2, sin2 = rope(hidden_states)
# Same input should produce same output
assert torch.allclose(cos1, cos2)
assert torch.allclose(sin1, sin2)
class TestGlmImageTransformer2DModelQuantization:
"""Test GlmImageTransformer2DModel with quantization config."""
def test_accepts_quant_config_parameter(self, mocker: MockerFixture):
"""Verify the model accepts quant_config parameter."""
from vllm_omni.diffusion.data import OmniDiffusionConfig
mock_quant_config = mocker.MagicMock()
parallel_config = DiffusionParallelConfig(
tensor_parallel_size=1,
sequence_parallel_size=1,
)
# Create a minimal mock od_config
mock_tf_config = mocker.MagicMock()
mock_tf_config.patch_size = 2
mock_tf_config.in_channels = 16
mock_tf_config.out_channels = 16
mock_tf_config.num_attention_heads = 64
mock_tf_config.attention_head_dim = 40
mock_tf_config.time_embed_dim = 512
mock_tf_config.condition_dim = 256
mock_tf_config.prior_vq_quantizer_codebook_size = 16384
mock_tf_config.text_embed_dim = 1024
mock_tf_config.num_layers = 2 # Small number for testing
mock_od_config = mocker.MagicMock(spec=OmniDiffusionConfig)
mock_od_config.tf_model_config = mock_tf_config
mock_od_config.parallel_config = parallel_config
model = GlmImageTransformer2DModel(
od_config=mock_od_config,
quant_config=mock_quant_config,
)
# Check that quantization config was passed to transformer blocks
for block in model.transformer_blocks:
assert block.norm1.linear.quant_config is mock_quant_config
assert block.attn1.to_qkv.quant_config is mock_quant_config
assert block.ff.net[0].proj.quant_config is mock_quant_config
def test_accepts_none_quant_config(self, mocker: MockerFixture):
"""Verify quant_config=None is accepted."""
from vllm_omni.diffusion.data import OmniDiffusionConfig
parallel_config = DiffusionParallelConfig(
tensor_parallel_size=1,
sequence_parallel_size=1,
)
mock_tf_config = mocker.MagicMock()
mock_tf_config.patch_size = 2
mock_tf_config.in_channels = 16
mock_tf_config.out_channels = 16
mock_tf_config.num_attention_heads = 64
mock_tf_config.attention_head_dim = 40
mock_tf_config.time_embed_dim = 512
mock_tf_config.condition_dim = 256
mock_tf_config.prior_vq_quantizer_codebook_size = 16384
mock_tf_config.text_embed_dim = 1024
mock_tf_config.num_layers = 2
mock_od_config = mocker.MagicMock(spec=OmniDiffusionConfig)
mock_od_config.tf_model_config = mock_tf_config
mock_od_config.parallel_config = parallel_config
model = GlmImageTransformer2DModel(
od_config=mock_od_config,
quant_config=None,
)
# Check that quantization config is None
for block in model.transformer_blocks:
assert block.norm1.linear.quant_config is None
assert block.attn1.to_qkv.quant_config is None
def test_norm_out_has_no_quantization(self, mocker: MockerFixture):
"""Verify norm_out (output layer) does NOT use quantization to preserve precision."""
from vllm_omni.diffusion.data import OmniDiffusionConfig
mock_quant_config = mocker.MagicMock()
parallel_config = DiffusionParallelConfig(
tensor_parallel_size=1,
sequence_parallel_size=1,
)
mock_tf_config = mocker.MagicMock()
mock_tf_config.patch_size = 2
mock_tf_config.in_channels = 16
mock_tf_config.out_channels = 16
mock_tf_config.num_attention_heads = 64
mock_tf_config.attention_head_dim = 40
mock_tf_config.time_embed_dim = 512
mock_tf_config.condition_dim = 256
mock_tf_config.prior_vq_quantizer_codebook_size = 16384
mock_tf_config.text_embed_dim = 1024
mock_tf_config.num_layers = 2
mock_od_config = mocker.MagicMock(spec=OmniDiffusionConfig)
mock_od_config.tf_model_config = mock_tf_config
mock_od_config.parallel_config = parallel_config
model = GlmImageTransformer2DModel(
od_config=mock_od_config,
quant_config=mock_quant_config,
)
# norm_out.linear should NOT have quant_config to preserve output precision
assert model.norm_out.linear.quant_config is None
def test_model_has_sp_plan(self, mocker: MockerFixture):
"""Verify model has _sp_plan defined for sequence parallelism."""
from vllm_omni.diffusion.data import OmniDiffusionConfig
parallel_config = DiffusionParallelConfig(
tensor_parallel_size=1,
sequence_parallel_size=1,
)
mock_tf_config = mocker.MagicMock()
mock_tf_config.patch_size = 2
mock_tf_config.in_channels = 16
mock_tf_config.out_channels = 16
mock_tf_config.num_attention_heads = 64
mock_tf_config.attention_head_dim = 40
mock_tf_config.time_embed_dim = 512
mock_tf_config.condition_dim = 256
mock_tf_config.prior_vq_quantizer_codebook_size = 16384
mock_tf_config.text_embed_dim = 1024
mock_tf_config.num_layers = 2
mock_od_config = mocker.MagicMock(spec=OmniDiffusionConfig)
mock_od_config.tf_model_config = mock_tf_config
mock_od_config.parallel_config = parallel_config
model = GlmImageTransformer2DModel(
od_config=mock_od_config,
quant_config=None,
)
# Verify _sp_plan exists
assert hasattr(model, "_sp_plan")
assert "prepare" in model._sp_plan
assert "proj_out" in model._sp_plan
def test_model_has_hsdp_shard_conditions(self, mocker: MockerFixture):
"""Verify model has _hsdp_shard_conditions for HSDP parallelism."""
from vllm_omni.diffusion.data import OmniDiffusionConfig
parallel_config = DiffusionParallelConfig(
tensor_parallel_size=1,
sequence_parallel_size=1,
)
mock_tf_config = mocker.MagicMock()
mock_tf_config.patch_size = 2
mock_tf_config.in_channels = 16
mock_tf_config.out_channels = 16
mock_tf_config.num_attention_heads = 64
mock_tf_config.attention_head_dim = 40
mock_tf_config.time_embed_dim = 512
mock_tf_config.condition_dim = 256
mock_tf_config.prior_vq_quantizer_codebook_size = 16384
mock_tf_config.text_embed_dim = 1024
mock_tf_config.num_layers = 2
mock_od_config = mocker.MagicMock(spec=OmniDiffusionConfig)
mock_od_config.tf_model_config = mock_tf_config
mock_od_config.parallel_config = parallel_config
model = GlmImageTransformer2DModel(
od_config=mock_od_config,
quant_config=None,
)
assert hasattr(model, "_hsdp_shard_conditions")
assert len(model._hsdp_shard_conditions) > 0
def test_model_creates_kv_cache(self, mocker: MockerFixture):
"""Verify model can create KV cache for image editing."""
from vllm_omni.diffusion.data import OmniDiffusionConfig
parallel_config = DiffusionParallelConfig(
tensor_parallel_size=1,
sequence_parallel_size=1,
)
mock_tf_config = mocker.MagicMock()
mock_tf_config.patch_size = 2
mock_tf_config.in_channels = 16
mock_tf_config.out_channels = 16
mock_tf_config.num_attention_heads = 64
mock_tf_config.attention_head_dim = 40
mock_tf_config.time_embed_dim = 512
mock_tf_config.condition_dim = 256
mock_tf_config.prior_vq_quantizer_codebook_size = 16384
mock_tf_config.text_embed_dim = 1024
mock_tf_config.num_layers = 2
mock_od_config = mocker.MagicMock(spec=OmniDiffusionConfig)
mock_od_config.tf_model_config = mock_tf_config
mock_od_config.parallel_config = parallel_config
model = GlmImageTransformer2DModel(
od_config=mock_od_config,
quant_config=None,
)
kv_cache = model.create_kv_cache()
assert kv_cache is not None
assert len(kv_cache) == 2 # num_layers
@@ -339,6 +339,7 @@ class TestCFGParallelForwardPath:
audio_latents=audio_latents,
output_type="latent",
),
request_id="ltx23-cfg-parallel-forward-test",
)
output = pipe.forward(req)
@@ -167,6 +167,7 @@ def test_basic_generation(ovis_pipeline):
# Setup request
req = OmniDiffusionRequest(
prompts=["A photo of a cat"],
request_id="ovis-basic",
sampling_params=OmniDiffusionSamplingParams(
height=256,
width=256,
@@ -206,6 +207,7 @@ def test_guidance_scale(ovis_pipeline, monkeypatch: pytest.MonkeyPatch):
"negative_prompt": "bad quality",
}
],
request_id="ovis-guidance",
sampling_params=OmniDiffusionSamplingParams(
height=256,
width=256,
@@ -225,6 +227,7 @@ def test_resolution_check(ovis_pipeline):
# Pass odd resolution
req = OmniDiffusionRequest(
prompts=["test"],
request_id="ovis-resolution",
sampling_params=OmniDiffusionSamplingParams(
height=250, # Not divisible by 16 (8*2)
width=250,
+22 -14
View File
@@ -22,15 +22,15 @@ class DiffusionSchedulerOutput:
num_waiting_reqs: int = 0
@property
def scheduled_req_ids(self):
def scheduled_request_ids(self):
ids = [req.request_id for req in self.scheduled_new_reqs]
if self.scheduled_cached_reqs:
ids.extend(self.scheduled_cached_reqs.sched_req_ids)
ids.extend(self.scheduled_cached_reqs.request_ids)
return ids
@property
def is_empty(self):
return len(self.scheduled_req_ids) == 0
return len(self.scheduled_request_ids) == 0
class MockScheduler:
@@ -71,31 +71,36 @@ async def test_async_add_req_and_wait_for_response():
engine.scheduler = MockScheduler()
engine._out_queue = {}
engine.abort_queue: queue.Queue[str] = queue.Queue()
engine._rpc_lock = threading.Lock()
engine._rpc_queue = queue.Queue()
engine._rpc_lock = threading.RLock()
engine._cv = threading.Condition(engine._rpc_lock)
engine._init_lock = asyncio.Lock()
engine._closed = False
engine._loop_started = False
engine.main_loop = None
engine._finalize_finished_request = lambda rid, out, err: out.result
def mock_execute_batch(sched_output):
req_ids = sched_output.scheduled_req_ids
request_ids = sched_output.scheduled_request_ids
time.sleep(1)
class MockRunnerOutput:
def __init__(self, ids):
self.req_id = ids
self.request_id = ids
self.step_index = 0
self.finished = True
self._results = {rid: SimpleNamespace(result_data=f"data_{rid}") for rid in ids}
def get_req_output(self, rid):
def get_request_output(self, rid):
return SimpleNamespace(result=self._results[rid], step_index=0, finished=True)
return MockRunnerOutput(req_ids)
return MockRunnerOutput(request_ids)
engine.execute_fn = mock_execute_batch
engine.stop_event = threading.Event()
engine.start_background_loop()
await engine._check_and_start_background_loop()
async def run_task(rid):
req = SimpleNamespace(request_id=rid)
@@ -105,10 +110,13 @@ async def test_async_add_req_and_wait_for_response():
task_ids = [f"req_{i}" for i in range(5)]
tasks = [run_task(rid) for rid in task_ids]
results = await asyncio.gather(*tasks)
engine.stop_event.set()
engine.worker_thread.join(timeout=5)
try:
results = await asyncio.gather(*tasks)
finally:
with engine._cv:
engine.stop_event.set()
engine._cv.notify_all()
engine.worker_thread.join(timeout=5)
assert len(results) == 5
for rid, res, elapsed in results:
assert rid in res.result_data
@@ -17,15 +17,10 @@ from vllm_omni.inputs.data import OmniDiffusionSamplingParams
pytestmark = [pytest.mark.core_model, pytest.mark.cpu, pytest.mark.diffusion]
def _make_request(
request_id: str,
*,
request_ids: list[str] | None = None,
) -> OmniDiffusionRequest:
def _make_request(request_id: str) -> OmniDiffusionRequest:
return OmniDiffusionRequest(
prompts=[f"prompt_{request_id}"],
sampling_params=OmniDiffusionSamplingParams(num_inference_steps=1),
request_ids=request_ids or [request_id],
request_id=request_id,
)
@@ -79,15 +74,15 @@ def test_handle_finished_requests_ignores_already_drained_waiter() -> None:
engine._finalize_finished_request.assert_not_called()
def test_abort_parent_request_id_aborts_batched_scheduler_request() -> None:
def test_abort_request_id_aborts_scheduler_request() -> None:
engine = _make_engine()
request = _make_request("batch-parent", request_ids=["batch-parent-0", "batch-parent-1"])
sched_req_id = engine.scheduler.add_request(request)
request = _make_request("batch-parent")
request_id = engine.scheduler.add_request(request)
engine.abort("batch-parent")
engine._process_aborts_queue()
state = engine.scheduler.get_request_state(sched_req_id)
state = engine.scheduler.get_request_state(request_id)
assert state is not None
assert state.status == DiffusionRequestStatus.FINISHED_ABORTED
@@ -89,7 +89,7 @@ class _ConcurrencyTrackingExecutor:
if method in {"execute_model", "execute_stepwise", "generate"}:
# args[0] is the request-like object for execute_model.
req = args[0] if args else None
tag = req.request_ids[0] if req is not None and hasattr(req, "request_ids") else "unknown"
tag = req.request_id if req is not None and hasattr(req, "request_id") else "unknown"
return DiffusionOutput(error=f"result_for_{tag}")
tag = args[0] if args else method
return DiffusionOutput(error=f"rpc_result_for_{tag}")
@@ -108,7 +108,7 @@ class _ConcurrencyTrackingExecutor:
exec_all_ranks=True,
)
return RunnerOutput(
req_id=new_req.sched_req_id,
request_id=new_req.request_id,
step_index=None,
finished=True,
result=result,
@@ -120,7 +120,7 @@ class _ConcurrencyTrackingExecutor:
def _make_request(tag: str):
return SimpleNamespace(
request_ids=[tag],
request_id=tag,
prompts=[f"prompt_{tag}"],
sampling_params=SimpleNamespace(num_inference_steps=1, **_SAMPLING_KEY_DEFAULTS),
)
@@ -148,6 +148,7 @@ def _make_engine_with_loop(
engine._rpc_lock = threading.RLock()
engine._cv = threading.Condition(engine._rpc_lock)
engine._out_queue = {}
engine._closed = False
engine.abort_queue = queue.Queue()
engine._rpc_queue = queue.Queue()
+45 -1
View File
@@ -5,7 +5,7 @@ import random
import pytest
from vllm_omni.diffusion.request import OmniDiffusionRequest
from vllm_omni.diffusion.request import DUMMY_DIFFUSION_REQUEST_ID, OmniDiffusionRequest
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
@@ -15,9 +15,53 @@ def _make_request() -> OmniDiffusionRequest:
return OmniDiffusionRequest(
prompts=[{"prompt": "a cup of coffee on a table"}],
sampling_params=OmniDiffusionSamplingParams(num_inference_steps=1),
request_id="request-test",
)
def test_request_id_is_required():
with pytest.raises(TypeError, match="request_id"):
OmniDiffusionRequest(
prompts=[{"prompt": "a cup of coffee on a table"}],
sampling_params=OmniDiffusionSamplingParams(num_inference_steps=1),
)
def test_request_ids_identity_list_is_removed():
req = _make_request()
assert req.request_id == "request-test"
assert not hasattr(req, "request_ids")
def test_request_id_must_be_non_empty():
with pytest.raises(ValueError, match="request_id must be a non-empty string"):
OmniDiffusionRequest(
prompts=[{"prompt": "a cup of coffee on a table"}],
sampling_params=OmniDiffusionSamplingParams(num_inference_steps=1),
request_id="",
)
def test_dummy_run_request_is_identified_by_reserved_request_id():
req = OmniDiffusionRequest(
prompts=[{"prompt": "dummy run"}],
sampling_params=OmniDiffusionSamplingParams(num_inference_steps=1),
request_id=DUMMY_DIFFUSION_REQUEST_ID,
)
assert req.is_dummy_run()
assert OmniDiffusionRequest.is_dummy_run_request_id(DUMMY_DIFFUSION_REQUEST_ID)
def test_non_dummy_request_is_not_identified_as_dummy_run():
req = _make_request()
assert not req.is_dummy_run()
assert not OmniDiffusionRequest.is_dummy_run_request_id(req.request_id)
assert not OmniDiffusionRequest.is_dummy_run_request_id(None)
def test_tp_seed_same_across_ranks_and_varies_across_requests():
random.seed(0)
n_requests = 5
+37 -74
View File
@@ -31,13 +31,13 @@ def _make_request(req_id: str) -> OmniDiffusionRequest:
return OmniDiffusionRequest(
prompts=[f"prompt_{req_id}"],
sampling_params=OmniDiffusionSamplingParams(num_inference_steps=1),
request_ids=[req_id],
request_id=req_id,
)
def _make_request_output(req_id: str, *, error: str | None = None, finished: bool = True):
return RunnerOutput(
req_id=req_id,
request_id=req_id,
step_index=None,
finished=finished,
result=DiffusionOutput(output=None, error=error),
@@ -52,7 +52,7 @@ def _make_step_output(
error: str | None = None,
):
return RunnerOutput(
req_id=req_id,
request_id=req_id,
step_index=step_index,
finished=finished,
result=DiffusionOutput(output=None, error=error) if error is not None else None,
@@ -73,16 +73,16 @@ def _make_step_request(
num_inference_steps=num_inference_steps,
step_index=step_index,
),
request_ids=[req_id],
request_id=req_id,
)
def _new_ids(sched_output) -> list[str]:
return [req.sched_req_id for req in sched_output.scheduled_new_reqs]
return [req.request_id for req in sched_output.scheduled_new_reqs]
def _cached_ids(sched_output) -> list[str]:
return list(sched_output.scheduled_cached_reqs.sched_req_ids)
return list(sched_output.scheduled_cached_reqs.request_ids)
class _StubScheduler(SchedulerInterface):
@@ -90,7 +90,7 @@ class _StubScheduler(SchedulerInterface):
self._request = request
self._output = output
self.initialized_with = None
self._sched_req_id = request.request_ids[0]
self._request_id = request.request_id
self._state = None
self._scheduled = False
self.max_num_running_reqs = 1
@@ -100,22 +100,22 @@ class _StubScheduler(SchedulerInterface):
def add_request(self, request: OmniDiffusionRequest) -> str:
assert request is self._request
self._state = SimpleNamespace(sched_req_id=self._sched_req_id, req=request)
return self._sched_req_id
self._state = SimpleNamespace(request_id=self._request_id, req=request)
return self._request_id
def schedule(self):
if self._scheduled or self._state is None:
return SimpleNamespace(
scheduled_new_reqs=[],
scheduled_cached_reqs=CachedRequestData.make_empty(),
scheduled_req_ids=[],
scheduled_request_ids=[],
is_empty=True,
)
self._scheduled = True
return SimpleNamespace(
scheduled_new_reqs=[NewRequestData.from_state(self._state)],
scheduled_cached_reqs=CachedRequestData.make_empty(),
scheduled_req_ids=[self._state.sched_req_id],
scheduled_request_ids=[self._state.request_id],
is_empty=False,
)
@@ -123,30 +123,25 @@ class _StubScheduler(SchedulerInterface):
del sched_output
assert output is self._output
self._state.status = DiffusionRequestStatus.FINISHED_COMPLETED
return {self._sched_req_id}
return {self._request_id}
def has_requests(self) -> bool:
return not self._scheduled
def get_request_state(self, sched_req_id: str):
del sched_req_id
def get_request_state(self, request_id: str):
del request_id
return self._state
def get_sched_req_id(self, request_id: str) -> str | None:
if request_id in self._request.request_ids:
return self._sched_req_id
return None
def pop_request_state(self, sched_req_id: str):
del sched_req_id
def pop_request_state(self, request_id: str):
del request_id
return self._state
def preempt_request(self, sched_req_id: str) -> bool:
del sched_req_id
def preempt_request(self, request_id: str) -> bool:
del request_id
return False
def finish_requests(self, sched_req_ids, status) -> None:
del sched_req_ids, status
def finish_requests(self, request_ids, status) -> None:
del request_ids, status
return None
def close(self) -> None:
@@ -171,7 +166,7 @@ class TestGetSamplingParamsKey:
return OmniDiffusionRequest(
prompts=["prompt"],
sampling_params=sp,
request_ids=[f"req-{lora_int_id}-{lora_scale}"],
request_id=f"req-{lora_int_id}-{lora_scale}",
)
def test_distinguishes_lora_id(self) -> None:
@@ -291,7 +286,7 @@ class TestRequestScheduler:
OmniDiffusionRequest(
prompts=["prompt_b"],
sampling_params=OmniDiffusionSamplingParams(width=768),
request_ids=["b"],
request_id="b",
)
)
scheduler.add_request(_make_request("c"))
@@ -333,7 +328,7 @@ class TestRequestScheduler:
assert state_a.status == DiffusionRequestStatus.FINISHED_ABORTED
assert self.scheduler.has_requests() is False
assert self.scheduler.schedule().scheduled_req_ids == []
assert self.scheduler.schedule().scheduled_request_ids == []
def test_has_requests_state_transition(self) -> None:
assert self.scheduler.has_requests() is False
@@ -348,60 +343,28 @@ class TestRequestScheduler:
assert self.scheduler.get_request_state(req_id).status == DiffusionRequestStatus.FINISHED_COMPLETED
assert self.scheduler.has_requests() is False
def test_request_id_mapping_lifecycle(self) -> None:
def test_request_id_is_scheduler_key(self) -> None:
request = OmniDiffusionRequest(
prompts=["prompt_map_a", "prompt_map_b"],
sampling_params=OmniDiffusionSamplingParams(num_inference_steps=1),
request_ids=["map-a", "map-b"],
request_id="map-parent",
)
sched_req_id = self.scheduler.add_request(request)
request_id = self.scheduler.add_request(request)
assert self.scheduler.get_sched_req_id("map-a") == sched_req_id
assert self.scheduler.get_sched_req_id("map-b") == sched_req_id
assert self.scheduler.get_sched_req_id("map-parent") == sched_req_id
assert request_id == "map-parent"
state = self.scheduler.get_request_state("map-parent")
assert state.request_id == "map-parent"
self.scheduler.pop_request_state(sched_req_id)
self.scheduler.pop_request_state("map-parent")
assert self.scheduler.get_sched_req_id("map-a") is None
assert self.scheduler.get_sched_req_id("map-b") is None
assert self.scheduler.get_sched_req_id("map-parent") is None
assert self.scheduler.get_request_state("map-parent") is None
def test_parent_request_id_registration_failure_rolls_back_child_ids(self) -> None:
self.scheduler.add_request(
OmniDiffusionRequest(
prompts=["prompt_existing"],
sampling_params=OmniDiffusionSamplingParams(num_inference_steps=1),
request_ids=["existing-child"],
request_id="duplicate-parent",
)
)
def test_duplicate_request_id_is_rejected(self) -> None:
self.scheduler.add_request(_make_request("dup"))
colliding_request = OmniDiffusionRequest(
prompts=["prompt_unique"],
sampling_params=OmniDiffusionSamplingParams(num_inference_steps=1),
request_ids=["unique-child"],
request_id="duplicate-parent",
)
with pytest.raises(ValueError, match="duplicate-parent"):
self.scheduler.add_request(colliding_request)
assert self.scheduler.get_request_state("unique-child") is None
assert self.scheduler.get_sched_req_id("unique-child") is None
valid_request = OmniDiffusionRequest(
prompts=["prompt_unique"],
sampling_params=OmniDiffusionSamplingParams(num_inference_steps=1),
request_ids=["unique-child"],
request_id="unique-parent",
)
sched_req_id = self.scheduler.add_request(valid_request)
assert sched_req_id == "unique-child"
assert self.scheduler.get_sched_req_id("unique-child") == sched_req_id
assert self.scheduler.get_sched_req_id("unique-parent") == sched_req_id
with pytest.raises(ValueError, match="request_id 'dup' is already active"):
self.scheduler.add_request(_make_request("dup"))
class TestDiffusionEngine:
@@ -662,7 +625,7 @@ class TestStepScheduler:
finished = self.scheduler.update_from_output(
sched_output,
RunnerOutput(
req_id=req_id,
request_id=req_id,
step_index=None,
finished=True,
result=None,
@@ -934,7 +897,7 @@ class TestStepScheduler:
for _ in range(expected_steps - 1):
sched_output = self.scheduler.schedule()
assert sched_output.scheduled_req_ids == [req_id]
assert sched_output.scheduled_request_ids == [req_id]
next_step = request.sampling_params.step_index + 1
assert (
self.scheduler.update_from_output(
@@ -945,7 +908,7 @@ class TestStepScheduler:
)
final_output = self.scheduler.schedule()
assert final_output.scheduled_req_ids == [req_id]
assert final_output.scheduled_request_ids == [req_id]
assert self.scheduler.update_from_output(
final_output,
_make_step_output(req_id, step_index=expected_steps, finished=True),
+35 -35
View File
@@ -206,7 +206,7 @@ class _DistributedStepPipeline(CFGParallelMixin):
def _make_step_request(num_inference_steps: int = 2):
return SimpleNamespace(
prompts=["a prompt"],
request_ids=["req-1"],
request_id="req-1",
sampling_params=SimpleNamespace(
generator=None,
seed=None,
@@ -227,7 +227,7 @@ def _make_engine_request(req_id: str = "req-1", num_inference_steps: int = 2) ->
return OmniDiffusionRequest(
prompts=[f"prompt-{req_id}"],
sampling_params=OmniDiffusionSamplingParams(num_inference_steps=num_inference_steps),
request_ids=[req_id],
request_id=req_id,
)
@@ -274,10 +274,10 @@ def _make_distributed_runner(mode: str, device: torch.device):
return runner
def _make_scheduler_output(req, sched_req_id="req-1", step_id=0, finished_req_ids=None):
def _make_scheduler_output(req, request_id="req-1", step_id=0, finished_req_ids=None):
return DiffusionSchedulerOutput(
step_id=step_id,
scheduled_new_reqs=[NewRequestData(sched_req_id=sched_req_id, req=req)],
scheduled_new_reqs=[NewRequestData(request_id=request_id, req=req)],
scheduled_cached_reqs=CachedRequestData.make_empty(),
finished_req_ids=set() if finished_req_ids is None else set(finished_req_ids),
num_running_reqs=1,
@@ -287,7 +287,7 @@ def _make_scheduler_output(req, sched_req_id="req-1", step_id=0, finished_req_id
def _make_batch_scheduler_output(reqs, *, step_id=0, finished_req_ids=None):
"""Scheduler output for a homogeneous batch (one NewRequestData per req)."""
new_reqs = [NewRequestData(sched_req_id=r.request_ids[0], req=r) for r in reqs]
new_reqs = [NewRequestData(request_id=r.request_id, req=r) for r in reqs]
return DiffusionSchedulerOutput(
step_id=step_id,
scheduled_new_reqs=new_reqs,
@@ -298,11 +298,11 @@ def _make_batch_scheduler_output(reqs, *, step_id=0, finished_req_ids=None):
)
def _make_cached_scheduler_output(sched_req_id="req-1", step_id=1, finished_req_ids=None):
def _make_cached_scheduler_output(request_id="req-1", step_id=1, finished_req_ids=None):
return DiffusionSchedulerOutput(
step_id=step_id,
scheduled_new_reqs=[],
scheduled_cached_reqs=CachedRequestData(sched_req_ids=[sched_req_id]),
scheduled_cached_reqs=CachedRequestData(request_ids=[request_id]),
finished_req_ids=set() if finished_req_ids is None else set(finished_req_ids),
num_running_reqs=1,
num_waiting_reqs=0,
@@ -359,7 +359,7 @@ def _distributed_step_worker(local_rank: int, world_size: int, mode: str, master
runner,
_make_scheduler_output(_make_step_request(num_inference_steps=1), step_id=0),
)
output = result.get_req_output("req-1")
output = result.get_request_output("req-1")
assert output.finished is True
assert output.result is not None
@@ -384,16 +384,16 @@ class TestRunner:
monkeypatch.setattr(model_runner_module, "set_forward_context", _noop_forward_context)
result = DiffusionModelRunner.execute_stepwise(runner, _make_scheduler_output(req, step_id=0))
first = result.get_req_output("req-1")
assert first.req_id == "req-1"
first = result.get_request_output("req-1")
assert first.request_id == "req-1"
assert first.step_index == 1
assert first.finished is False
assert first.result is None
assert "req-1" in runner.state_cache
result = DiffusionModelRunner.execute_stepwise(runner, _make_cached_scheduler_output(step_id=1))
second = result.get_req_output("req-1")
assert second.req_id == "req-1"
second = result.get_request_output("req-1")
assert second.request_id == "req-1"
assert second.step_index == 2
assert second.finished is True
assert second.result is not None
@@ -410,13 +410,13 @@ class TestRunner:
runner = _make_runner()
req_1 = _make_step_request()
req_2 = _make_step_request()
req_2.request_ids = ["req-2"]
req_2.request_id = "req-2"
scheduler_output = DiffusionSchedulerOutput(
step_id=0,
scheduled_new_reqs=[
NewRequestData(sched_req_id="req-1", req=req_1),
NewRequestData(sched_req_id="req-2", req=req_2),
NewRequestData(request_id="req-1", req=req_1),
NewRequestData(request_id="req-2", req=req_2),
],
scheduled_cached_reqs=CachedRequestData.make_empty(),
finished_req_ids=set(),
@@ -431,7 +431,7 @@ class TestRunner:
runner = _make_runner()
with pytest.raises(ValueError, match="Missing cached state"):
DiffusionModelRunner.execute_stepwise(runner, _make_cached_scheduler_output(sched_req_id="req-missing"))
DiffusionModelRunner.execute_stepwise(runner, _make_cached_scheduler_output(request_id="req-missing"))
def test_interrupt_marks_request_finished_and_clears_state(self, monkeypatch):
runner = _make_runner()
@@ -440,8 +440,8 @@ class TestRunner:
monkeypatch.setattr(model_runner_module, "set_forward_context", _noop_forward_context)
result = DiffusionModelRunner.execute_stepwise(runner, _make_scheduler_output(req, step_id=0))
output = result.get_req_output("req-1")
assert output.req_id == "req-1"
output = result.get_request_output("req-1")
assert output.request_id == "req-1"
assert output.step_index == 0
assert output.finished is True
assert output.result is not None
@@ -515,7 +515,7 @@ def _make_step_worker(lora_manager=None, *, expected_output=None):
worker = object.__new__(DiffusionWorker)
worker.lora_manager = lora_manager
worker._step_lora_state = {}
output = expected_output if expected_output is not None else RunnerOutput(req_id="req-1")
output = expected_output if expected_output is not None else RunnerOutput(request_id="req-1")
worker.model_runner = SimpleNamespace(execute_stepwise=lambda arg: output)
return worker
@@ -525,9 +525,9 @@ class TestWorker:
"""DiffusionWorker.execute_stepwise"""
def test_delegates_to_model_runner(self):
expected = RunnerOutput(req_id="req-1", step_index=1, finished=False, result=None)
expected = RunnerOutput(request_id="req-1", step_index=1, finished=False, result=None)
worker = _make_step_worker(expected_output=expected)
scheduler_output = _make_scheduler_output(_make_engine_request("req-1"), sched_req_id="req-1")
scheduler_output = _make_scheduler_output(_make_engine_request("req-1"), request_id="req-1")
output = DiffusionWorker.execute_stepwise(worker, scheduler_output)
@@ -536,7 +536,7 @@ class TestWorker:
def test_deactivates_lora_when_request_has_no_adapter(self):
manager = _RecordingLoRAManager()
worker = _make_step_worker(lora_manager=manager)
scheduler_output = _make_scheduler_output(_make_engine_request("req-1"), sched_req_id="req-1")
scheduler_output = _make_scheduler_output(_make_engine_request("req-1"), request_id="req-1")
DiffusionWorker.execute_stepwise(worker, scheduler_output)
@@ -552,7 +552,7 @@ class TestWorker:
manager = _RecordingLoRAManager()
worker = _make_step_worker(lora_manager=manager)
scheduler_output = _make_scheduler_output(request, sched_req_id="req-1")
scheduler_output = _make_scheduler_output(request, request_id="req-1")
DiffusionWorker.execute_stepwise(worker, scheduler_output)
@@ -568,8 +568,8 @@ class TestWorker:
manager = _RecordingLoRAManager()
worker = _make_step_worker(lora_manager=manager)
first = _make_scheduler_output(request, sched_req_id="req-1")
second = _make_cached_scheduler_output(sched_req_id="req-1", step_id=1)
first = _make_scheduler_output(request, request_id="req-1")
second = _make_cached_scheduler_output(request_id="req-1", step_id=1)
DiffusionWorker.execute_stepwise(worker, first)
DiffusionWorker.execute_stepwise(worker, second)
@@ -610,10 +610,10 @@ class TestWorker:
next_request.sampling_params.lora_request = lora_request
worker = _make_step_worker(lora_manager=_RecordingLoRAManager())
first = _make_scheduler_output(finishing, sched_req_id="req-1")
first = _make_scheduler_output(finishing, request_id="req-1")
next_batch = _make_scheduler_output(
next_request,
sched_req_id="req-2",
request_id="req-2",
step_id=1,
finished_req_ids={"req-1"},
)
@@ -633,11 +633,11 @@ class TestExecutor:
def test_execute_step_passes_through_runner_output(self, mocker: MockerFixture):
executor = object.__new__(MultiprocDiffusionExecutor)
executor._ensure_open = lambda: None
expected = RunnerOutput(req_id="req-step", step_index=1, finished=False, result=None)
expected = RunnerOutput(request_id="req-step", step_index=1, finished=False, result=None)
executor.collective_rpc = mocker.Mock(return_value=expected)
request = _make_engine_request("req-step", num_inference_steps=2)
scheduler_output = _make_scheduler_output(request, sched_req_id="req-step")
scheduler_output = _make_scheduler_output(request, request_id="req-step")
output = MultiprocDiffusionExecutor.execute_step(executor, scheduler_output)
@@ -653,7 +653,7 @@ class TestEngine:
[
(
lambda _: RunnerOutput(
req_id="req-error",
request_id="req-error",
step_index=1,
finished=True,
result=DiffusionOutput(error="boom"),
@@ -688,7 +688,7 @@ class TestEngine:
call_count["n"] += 1
finished = call_count["n"] == 2
return RunnerOutput(
req_id="req-step",
request_id="req-step",
step_index=call_count["n"],
finished=finished,
result=(DiffusionOutput(output=torch.tensor([2.0])) if finished else None),
@@ -714,7 +714,7 @@ class TestEngine:
step["n"] += 1
engine.abort("req-stop")
return RunnerOutput(
req_id="req-stop",
request_id="req-stop",
step_index=1,
finished=False,
result=None,
@@ -741,7 +741,7 @@ class TestEngine:
assert sched_output == _make_cached_scheduler_output("req-mid", step_id=1)
engine.abort("req-mid")
return RunnerOutput(
req_id="req-mid",
request_id="req-mid",
step_index=step["n"],
finished=False,
result=None,
@@ -760,7 +760,7 @@ class TestEngine:
engine = _make_engine(
scheduler,
execute_fn=lambda _: RunnerOutput(
req_id="req-missing",
request_id="req-missing",
step_index=1,
finished=True,
result=None,
@@ -777,7 +777,7 @@ class TestEngine:
class TestIPC:
def test_pack_unpack_runner_output_shm(self):
tensor = torch.zeros(300_000, dtype=torch.float32)
output = RunnerOutput(req_id="req-1", finished=True, result=DiffusionOutput(output=tensor))
output = RunnerOutput(request_id="req-1", finished=True, result=DiffusionOutput(output=tensor))
packed = pack_diffusion_output_shm(output)
assert isinstance(packed.result.output, dict)
@@ -36,7 +36,7 @@ def _tagged_output(tag: str) -> DiffusionOutput:
def _mock_request(tag: str):
"""Return a lightweight request object identifiable by *tag*."""
return SimpleNamespace(
request_ids=[tag],
request_id=tag,
prompts=[f"prompt_{tag}"],
sampling_params=OmniDiffusionSamplingParams(num_inference_steps=1),
)
@@ -97,8 +97,8 @@ def _start_worker(req_q, res_q, count=2):
req = req_q.get(timeout=10)
method = req.get("method", "")
args = req.get("args", ())
if method in {"generate", "execute_model"} and args and hasattr(args[0], "request_ids"):
tag = f"result_for_{args[0].request_ids[0]}"
if method in {"generate", "execute_model"} and args and hasattr(args[0], "request_id"):
tag = f"result_for_{args[0].request_id}"
elif args:
tag = f"result_for_{args[0]}"
else:
+1 -1
View File
@@ -68,7 +68,7 @@ def test_process_batch_request_preserves_parent_request_id_and_kv_sender_info():
request = captured["request"]
assert request.request_id == "req-parent"
assert request.request_ids == ["req-parent-0", "req-parent-1"]
assert not hasattr(request, "request_ids")
assert request.kv_sender_info == {0: {"host": "10.0.0.2", "zmq_port": 50151}}
assert result.request_id == "req-parent"
assert result.images == ["img-1", "img-2"]
@@ -384,7 +384,7 @@ def test_manager_reception(kv_config, mock_connector, common_constants):
req = OmniDiffusionRequest(
prompts=["test_recv"],
sampling_params=OmniDiffusionSamplingParams(),
request_ids=[req_id],
request_id=req_id,
)
# req.need_kv_receive = True # Implicitly handled by receive_kv_cache check? No, manager doesn't check it, runner does.
# But receive_kv_cache in manager checks request_id. Which we need to fix in manager next.
@@ -428,7 +428,6 @@ def test_manager_reception_prefers_parent_request_id_for_batched_request(kv_conf
req = OmniDiffusionRequest(
prompts=["prompt-a", "prompt-b"],
sampling_params=OmniDiffusionSamplingParams(),
request_ids=[f"{parent_req_id}-0", f"{parent_req_id}-1"],
request_id=parent_req_id,
)
@@ -454,7 +453,6 @@ def test_receive_multi_kv_cache_uses_parent_request_id_for_cfg_collection(kv_con
req = OmniDiffusionRequest(
prompts=["prompt-a", "prompt-b"],
sampling_params=OmniDiffusionSamplingParams(),
request_ids=["req-parent-0", "req-parent-1"],
request_id="req-parent",
)
req.sampling_params.cfg_kv_request_ids = {"cfg_text": "req-parent__cfg_text"}
@@ -516,7 +514,7 @@ def test_integration_flow(common_constants):
req = OmniDiffusionRequest(
prompts=["test_integ"],
sampling_params=OmniDiffusionSamplingParams(),
request_ids=[req_id],
request_id=req_id,
)
# Receive
@@ -253,37 +253,6 @@ def _diffusers_output_single_image(accuracy_artifact_root: Path, qwen_bear_image
)
def _vllm_omni_output_multiple_image(
accuracy_artifact_root: Path,
qwen_bear_image: Image.Image,
rabbit_image: Image.Image,
) -> Image.Image:
output_dir = model_output_dir(accuracy_artifact_root, MULTIPLE_MODEL)
output_path = output_dir / "vllm_omni_multiple.png"
with OmniServer(model=MULTIPLE_MODEL, serve_args=SERVER_ARGS) as server:
output = _run_vllm_omni_image_edit(
omni_server=server,
prompt=PROMPT_MULTIPLE_IMAGE,
input_images=[qwen_bear_image, rabbit_image],
output_path=output_path,
)
return output
def _diffusers_output_multiple_image(
accuracy_artifact_root: Path, qwen_bear_image: Image.Image, rabbit_image: Image.Image
) -> Image.Image:
output_dir = model_output_dir(accuracy_artifact_root, MULTIPLE_MODEL)
output_path = output_dir / "diffusers_multiple.png"
return _run_diffusers_image_edit(
model=MULTIPLE_MODEL,
pipeline_class=QwenImageEditPlusPipeline,
prompt=PROMPT_MULTIPLE_IMAGE,
input_images=[qwen_bear_image, rabbit_image],
output_path=output_path,
)
@pytest.mark.benchmark
@hardware_test(res={"cuda": "H100"}, num_cards=1)
def test_qwen_image_edit_single_matches_diffusers(
@@ -309,37 +278,6 @@ def test_qwen_image_edit_single_matches_diffusers(
)
@pytest.mark.benchmark
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@pytest.mark.skip(
reason="Skipping as the second image seems to be ignored by the API. Will come back to this later after #2772 is merged."
)
def test_qwen_image_edit_multiple_matches_diffusers(
accuracy_artifact_root: Path,
qwen_bear_image: Image.Image,
rabbit_image: Image.Image,
) -> None:
vllm_image = _vllm_omni_output_multiple_image(
accuracy_artifact_root=accuracy_artifact_root,
qwen_bear_image=qwen_bear_image,
rabbit_image=rabbit_image,
)
diffusers_image = _diffusers_output_multiple_image(
accuracy_artifact_root=accuracy_artifact_root,
qwen_bear_image=qwen_bear_image,
rabbit_image=rabbit_image,
)
assert_similarity(
model_name=MULTIPLE_MODEL,
vllm_image=vllm_image,
diffusers_image=diffusers_image,
width=WIDTH,
height=HEIGHT,
ssim_threshold=SSIM_THRESHOLD,
psnr_threshold=PSNR_THRESHOLD,
)
@pytest.mark.benchmark
@hardware_test(res={"cuda": "H100"}, num_cards=1)
def test_qwen_image_edit_2511_matches_diffusers_pixelwise(accuracy_artifact_root: Path) -> None:
@@ -0,0 +1,238 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""E2E tests for GLM-Image AutoRound W4A16 quantized inference.
These tests cover text-to-image and image-to-image generation with
the W4A16 quantized GLM-Image model.
Requirements:
- 2 CUDA GPUs (H100 or equivalent)
- The quantized model checkpoint (Intel/GLM-Image-int4-AutoRound)
"""
import gc
import math
import os
import numpy as np
import pytest
from PIL import Image
from vllm import SamplingParams
from tests.helpers.env import DeviceMemoryMonitor
from tests.helpers.mark import hardware_test
from tests.helpers.media import generate_synthetic_image
from tests.helpers.runtime import OmniRunnerHandler
from tests.helpers.stage_config import get_deploy_config_path, modify_stage_config
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
from vllm_omni.outputs import OmniRequestOutput
from vllm_omni.platforms import current_omni_platform
QUANTIZED_MODEL = os.environ.get("GLM_IMAGE_AUTOROUND_MODEL", "Intel/GLM-Image-int4-AutoRound")
# Small resolution to keep GPU memory & time manageable
HEIGHT = 256
WIDTH = 256
NUM_STEPS = 2 # minimal for smoke-test
# GLM-Image AR generation config (from generation_config.json)
GLM_IMAGE_EOS_TOKEN_ID = 16385
GLM_IMAGE_VISION_VOCAB_SIZE = 16512
_CI_DEPLOY = get_deploy_config_path("glm_image.yaml")
def _get_stage_config():
"""Build a CI-friendly stage config with eager mode for testing."""
return modify_stage_config(
_CI_DEPLOY,
updates={
"stages": {
0: {"enforce_eager": True},
1: {"enforce_eager": True},
},
},
)
stage_config = _get_stage_config()
# (model, stage_config_path) for ``omni_runner`` indirect parametrize
_OMNI_RUNNER_PARAM = (QUANTIZED_MODEL, stage_config)
def compute_max_tokens(height: int, width: int, factor: int = 32) -> int:
"""Compute max_new_tokens for GLM-Image AR text-to-image generation."""
token_h = height // factor
token_w = width // factor
large_tokens = token_h * token_w
ratio = token_h / token_w if token_w > 0 else 1.0
small_token_h = max(1, int(math.sqrt(ratio) * (factor // 2)))
small_token_w = max(1, int(math.sqrt(1 / ratio) * (factor // 2)))
small_tokens = small_token_h * small_token_w
return small_tokens + large_tokens + 1
def _ar_sampling_params(max_tokens: int, height: int, width: int, seed: int = 42) -> SamplingParams:
"""Build AR stage SamplingParams for GLM-Image."""
return SamplingParams(
temperature=0.9,
top_p=0.75,
top_k=GLM_IMAGE_VISION_VOCAB_SIZE,
max_tokens=max_tokens,
stop_token_ids=[GLM_IMAGE_EOS_TOKEN_ID],
seed=seed,
detokenize=False,
extra_args={
"target_h": height,
"target_w": width,
},
)
def _diffusion_sampling_params(
height: int = HEIGHT,
width: int = WIDTH,
num_steps: int = NUM_STEPS,
seed: int = 42,
) -> OmniDiffusionSamplingParams:
"""Build Diffusion stage OmniDiffusionSamplingParams."""
return OmniDiffusionSamplingParams(
height=height,
width=width,
num_inference_steps=num_steps,
guidance_scale=0.0,
seed=seed,
)
pytestmark = [
pytest.mark.full_model,
pytest.mark.diffusion,
]
# ------------------------------------------------------------------
# Test: text-to-image generation produces a valid image (quantized)
# ------------------------------------------------------------------
@pytest.mark.parametrize("omni_runner", [_OMNI_RUNNER_PARAM], indirect=True)
@hardware_test(res={"cuda": "H100"}, num_cards=2)
def test_glm_image_autoround_w4a16_generates_image(omni_runner_handler: OmniRunnerHandler):
"""Load the W4A16 quantized GLM-Image model and verify it produces a valid image."""
gc.collect()
current_omni_platform.empty_cache()
device_index = current_omni_platform.current_device()
current_omni_platform.reset_peak_memory_stats()
monitor = DeviceMemoryMonitor(device_index=device_index, interval=0.02)
monitor.start()
prompt_dict = {
"prompt": "A photo of a cat sitting on a laptop keyboard",
"height": HEIGHT,
"width": WIDTH,
"mm_processor_kwargs": {
"target_h": HEIGHT,
"target_w": WIDTH,
},
}
ar_params = _ar_sampling_params(
max_tokens=compute_max_tokens(HEIGHT, WIDTH),
height=HEIGHT,
width=WIDTH,
seed=42,
)
diffusion_params = _diffusion_sampling_params(
height=HEIGHT,
width=WIDTH,
num_steps=NUM_STEPS,
seed=42,
)
outputs = omni_runner_handler.runner.generate(
[prompt_dict],
[ar_params, diffusion_params],
)
monitor.stop()
first_output = outputs[0]
assert first_output.final_output_type == "image"
req_out = first_output.request_output
assert isinstance(req_out, OmniRequestOutput) and hasattr(req_out, "images")
images = req_out.images
assert len(images) >= 1, "Expected at least one generated image"
img = images[0]
assert isinstance(img, Image.Image)
assert img.width == WIDTH, f"Expected width {WIDTH}, got {img.width}"
assert img.height == HEIGHT, f"Expected height {HEIGHT}, got {img.height}"
# Sanity: image should not be blank (all-zero)
arr = np.array(img)
assert arr.std() > 1.0, "Generated image appears blank (std ≈ 0)"
gc.collect()
current_omni_platform.empty_cache()
# ------------------------------------------------------------------
# Test: image-to-image generation (quantized)
# ------------------------------------------------------------------
@pytest.mark.parametrize("omni_runner", [_OMNI_RUNNER_PARAM], indirect=True)
@hardware_test(res={"cuda": "H100"}, num_cards=2)
def test_glm_image_autoround_w4a16_image_to_image(omni_runner_handler: OmniRunnerHandler):
"""Load the W4A16 quantized GLM-Image and verify image-to-image generation works."""
ref_image_arr = generate_synthetic_image(WIDTH, HEIGHT)["np_array"]
gc.collect()
current_omni_platform.empty_cache()
current_omni_platform.reset_peak_memory_stats()
prompt_dict = {
"prompt": "Make it look like winter",
"multi_modal_data": {"image": ref_image_arr},
"height": HEIGHT,
"width": WIDTH,
"mm_processor_kwargs": {
"target_h": HEIGHT,
"target_w": WIDTH,
},
}
ar_params = _ar_sampling_params(
max_tokens=compute_max_tokens(HEIGHT, WIDTH),
height=HEIGHT,
width=WIDTH,
seed=42,
)
diffusion_params = _diffusion_sampling_params(
height=HEIGHT,
width=WIDTH,
num_steps=NUM_STEPS,
seed=42,
)
outputs = omni_runner_handler.runner.generate(
[prompt_dict],
[ar_params, diffusion_params],
)
first_output = outputs[0]
assert first_output.final_output_type == "image"
req_out = first_output.request_output
assert isinstance(req_out, OmniRequestOutput) and hasattr(req_out, "images")
images = req_out.images
assert len(images) >= 1, "Expected at least one generated image"
img = images[0]
assert isinstance(img, Image.Image)
assert img.width == WIDTH
assert img.height == HEIGHT
gc.collect()
current_omni_platform.empty_cache()
@@ -5,8 +5,8 @@
Online serving tests for the Qwen-Image-Edit family (image-to-image via chat completions).
- ``test_single_image_to_image_001``: ``Qwen/Qwen-Image-Edit`` — one reference image, fixed 512×512.
- ``test_multi_images_to_image_001``: ``Qwen/Qwen-Image-Edit-2509`` — two reference images, fixed 512×512.
- ``test_different_sizes_001``: ``Qwen/Qwen-Image-Edit-2509`` only, ``advanced_model`` — mixed input
- ``test_multi_images_to_image_001``: ``Qwen/Qwen-Image-Edit-2511`` — two reference images, fixed 512×512.
- ``test_different_sizes_001``: ``Qwen/Qwen-Image-Edit-2511`` only, ``advanced_model`` — mixed input
resolutions; ``extra_body`` uses per-output ``width``/``height`` lists; the test client sends one
scalar-size request per list index in parallel and merges images (see ``OpenAIClientHandler.send_diffusion_request``).
@@ -53,7 +53,6 @@ def _get_diffusion_feature_cases(model: str):
@pytest.mark.advanced_model
@pytest.mark.core_model
@pytest.mark.diffusion
@pytest.mark.parametrize(
"omni_server",
@@ -83,15 +82,16 @@ def test_single_image_to_image_001(omni_server: OmniServer, openai_client: OpenA
openai_client.send_diffusion_request(request_config)
@pytest.mark.core_model
@pytest.mark.advanced_model
@pytest.mark.diffusion
@pytest.mark.parametrize(
"omni_server",
_get_diffusion_feature_cases("Qwen/Qwen-Image-Edit-2509"),
_get_diffusion_feature_cases("Qwen/Qwen-Image-Edit-2511"),
indirect=True,
)
def test_multi_images_to_image_001(omni_server: OmniServer, openai_client: OpenAIClientHandler):
"""Two-reference edit smoke for ``Qwen/Qwen-Image-Edit-2509``."""
"""Two-reference edit smoke for ``Qwen/Qwen-Image-Edit-2511``."""
image_data_url_list = [f"data:image/jpeg;base64,{generate_synthetic_image(512, 512)['base64']}" for _ in range(2)]
@@ -118,7 +118,7 @@ def test_multi_images_to_image_001(omni_server: OmniServer, openai_client: OpenA
@pytest.mark.diffusion
@pytest.mark.parametrize(
"omni_server",
_get_diffusion_feature_cases("Qwen/Qwen-Image-Edit-2509"),
_get_diffusion_feature_cases("Qwen/Qwen-Image-Edit-2511"),
indirect=True,
)
def test_different_sizes_001(omni_server: OmniServer, openai_client: OpenAIClientHandler):
@@ -2,7 +2,7 @@
Comprehensive tests of diffusion features that are available in online serving mode
and are supported by the following models:
- Qwen-Image-Edit: single image input
- Qwen-Image-Edit-2509: single image input and two image inputs
- Qwen-Image-Edit-2511: single image input and two image inputs
"""
import pytest
@@ -14,7 +14,7 @@ from tests.helpers.runtime import OmniServer, OmniServerParams, OpenAIClientHand
pytestmark = [pytest.mark.diffusion, pytest.mark.full_model]
EDIT_PROMPT = "Transform this modern, geometrist image into a Vincent van Gogh style impressionist painting."
SINGLE_EDIT_PROMPT_2509 = "Restyle this image into a Vincent van Gogh style impressionist painting."
SINGLE_EDIT_PROMPT_2511 = "Restyle this image into a Vincent van Gogh style impressionist painting."
MULTI_EDIT_PROMPT = (
"Transform the first image into a Dadaism collage art. "
"Transform the second image into a Vincent van Gogh style painting. "
@@ -34,7 +34,7 @@ def _get_diffusion_feature_cases(model: str):
model=model,
server_args=[
"--cache-backend",
"tea_cache", # [TODO] may consider changing to cache_dit after #1779 is resolved. Currently cache_dit and layerwise offload cannot work together.
"cache_dit",
"--enable-layerwise-offload",
],
),
@@ -59,7 +59,7 @@ def _get_diffusion_feature_cases(model: str):
model=model,
server_args=[
"--cache-backend",
"cache_dit",
"tea_cache",
"--ring",
"2",
],
@@ -145,7 +145,7 @@ def test_qwen_image_edit(omni_server: OmniServer, openai_client: OpenAIClientHan
indirect=True,
)
def test_qwen_image_edit_2511_single_image(omni_server: OmniServer, openai_client: OpenAIClientHandler):
"""Test Qwen-Image-Edit-2509 with a single image input.
"""Test Qwen-Image-Edit-2511 with a single image input.
Regression: with tea_cache enabled and zero_cond_t=True, the TeaCache
postprocess closure used the doubled temb (shape 2*batch) without halving
@@ -156,7 +156,7 @@ def test_qwen_image_edit_2511_single_image(omni_server: OmniServer, openai_clien
"""
image_data_url = f"data:image/jpeg;base64,{generate_synthetic_image(512, 512)['base64']}"
messages = dummy_messages_from_mix_data(image_data_url=image_data_url, content_text=SINGLE_EDIT_PROMPT_2509)
messages = dummy_messages_from_mix_data(image_data_url=image_data_url, content_text=SINGLE_EDIT_PROMPT_2511)
request_config = {
"model": omni_server.model,
@@ -180,7 +180,7 @@ def test_qwen_image_edit_2511_single_image(omni_server: OmniServer, openai_clien
indirect=True,
)
def test_qwen_image_edit_2511_two_images(omni_server: OmniServer, openai_client: OpenAIClientHandler):
"""Test Qwen-Image-Edit-2509 with two image inputs."""
"""Test Qwen-Image-Edit-2511 with two image inputs."""
image_data_url_1 = f"data:image/jpeg;base64,{generate_synthetic_image(512, 512)['base64']}"
image_data_url_2 = f"data:image/jpeg;base64,{generate_synthetic_image(512, 512)['base64']}"
@@ -0,0 +1,76 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Unit tests for GLM-Image model __init__.py lazy import pattern.
The ``__init__.py`` uses ``__getattr__`` for lazy loading to avoid importing
``transformers.models.glm_image`` at module init, which may not be available
in all environments.
"""
import pytest
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
class TestLazyImport:
"""Test the __getattr__ lazy import pattern in glm_image __init__.py."""
def test_getattr_exists_and_is_callable(self):
"""Verify __getattr__ exists and is callable."""
import vllm_omni.model_executor.models.glm_image as glm_image_pkg
assert hasattr(glm_image_pkg, "__getattr__")
assert callable(glm_image_pkg.__getattr__)
def test_getattr_returns_class_for_known_attribute(self):
"""Verify __getattr__ returns GlmImageForConditionalGeneration for known attribute."""
import vllm_omni.model_executor.models.glm_image as glm_image_pkg
# Call __getattr__ directly to test the lazy import logic
result = glm_image_pkg.__getattr__("GlmImageForConditionalGeneration")
# Verify we get a class (the actual GlmImageForConditionalGeneration)
assert result is not None
assert isinstance(result, type)
assert result.__name__ == "GlmImageForConditionalGeneration"
def test_getattr_raises_for_unknown_attribute(self):
"""Verify __getattr__ raises AttributeError for unknown attributes."""
import vllm_omni.model_executor.models.glm_image as glm_image_pkg
# Test unknown attribute via __getattr__ directly
with pytest.raises(AttributeError, match="has no attribute"):
glm_image_pkg.__getattr__("UnknownClass")
def test___all___exports_correct_symbols(self):
"""Verify __all__ contains the expected exported symbols."""
import vllm_omni.model_executor.models.glm_image as glm_image_pkg
assert hasattr(glm_image_pkg, "__all__")
assert "GlmImageForConditionalGeneration" in glm_image_pkg.__all__
class TestLazyImportDoesNotImportTransformersAtInit:
"""Verify that importing the package does not eagerly load transformers."""
def test_glm_image_module_has_getattr(self):
"""Test that the module has __getattr__ for lazy loading."""
import vllm_omni.model_executor.models.glm_image as glm_image_pkg
# The module should have __getattr__
assert hasattr(glm_image_pkg, "__getattr__")
assert callable(glm_image_pkg.__getattr__)
# And __all__
assert hasattr(glm_image_pkg, "__all__")
def test_module_has_proper_structure(self):
"""Test that the module has proper Python module structure."""
import vllm_omni.model_executor.models.glm_image as glm_image_pkg
# Should be a module
assert hasattr(glm_image_pkg, "__name__")
assert glm_image_pkg.__name__ == "vllm_omni.model_executor.models.glm_image"
# Should have __file__ (even if it's a package)
assert hasattr(glm_image_pkg, "__file__")
@@ -267,8 +267,8 @@ def test_base_voice_clone_normalizes_ref_audio_once_for_ref_code_and_speaker():
model._normalize_ref_audio = lambda raw: normalize_calls.append(raw) or (ref_audio, 16000)
ref_audio_ids = []
model._encode_ref_audio_to_code = lambda wav, _sr: (
ref_audio_ids.append(id(wav)) or torch.ones((2, 2), dtype=torch.long)
model._encode_ref_audio_batch = lambda wavs, sr, *, device: (
[ref_audio_ids.append(id(w)) or torch.ones((2, 2), dtype=torch.long) for w in wavs]
)
model._extract_speaker_embedding = lambda wav, _sr: (
ref_audio_ids.append(id(wav)) or torch.ones(4, dtype=torch.bfloat16)
@@ -303,21 +303,16 @@ def test_base_voice_clone_batch_preprocess_encodes_ref_code_by_sample_rate():
)
)
class FakeSpeechTokenizer:
def __init__(self):
self.calls = []
encode_calls = []
def encode(self, audios, *, sr, return_dict):
self.calls.append((audios, sr, return_dict))
return SimpleNamespace(
audio_codes=[
torch.full((2, 2), 11, dtype=torch.long),
torch.full((3, 2), 22, dtype=torch.long),
]
)
def _fake_encode_batch(wavs, sr, *, device):
encode_calls.append((wavs, sr))
return [
torch.full((2, 2), 11, dtype=torch.long),
torch.full((3, 2), 22, dtype=torch.long),
]
tok = FakeSpeechTokenizer()
model._ensure_speech_tokenizer_loaded = lambda: tok
model._encode_ref_audio_batch = _fake_encode_batch
class FakeTextTokenizer:
def __init__(self):
@@ -353,12 +348,11 @@ def test_base_voice_clone_batch_preprocess_encodes_ref_code_by_sample_rate():
)
assert normalize_calls == ["a.wav", "b.wav"]
assert len(tok.calls) == 1
audios, sr, return_dict = tok.calls[0]
assert audios[0] is wav1
assert audios[1] is wav2
assert len(encode_calls) == 1
wavs, sr = encode_calls[0]
assert wavs[0] is wav1
assert wavs[1] is wav2
assert sr == 16000
assert return_dict is True
assert torch.equal(buf["r1"]["codes"][_PRECOMPUTED_REF_CODE_KEY], torch.full((2, 2), 11))
assert torch.equal(buf["r2"]["codes"][_PRECOMPUTED_REF_CODE_KEY], torch.full((3, 2), 22))
assert buf["r1"][_NORMALIZED_REF_AUDIO_KEY][0] is wav1
@@ -123,10 +123,6 @@ class OmniRandomMultiModalDataset(RandomMultiModalDataset):
"23",
"-movflags",
"+faststart",
"-pix_fmt",
"yuv420p",
"-vf",
f"scale={width}:{height}",
],
}
+5
View File
@@ -28,6 +28,11 @@ connectors:
codec_left_context_frames: 72
# Emit only the first audio chunk early, then return to codec_chunk_frames.
initial_codec_chunk_frames: 1
# Common Stage1 decode buckets:
# no-ref first/steady chunks: 25 / 97 frames
# Base ref-context first/steady chunks: 73 / 169 frames
# decoder internal non-streaming chunks: 325 frames
decode_cudagraph_capture_sizes: [25, 73, 97, 169, 325]
stages:
- stage_id: 0
@@ -77,7 +77,7 @@ class DefaultAdapter:
os.path.join("transformer", "config.json"),
od_config.model,
)
od_config.tf_model_config = TransformerConfig.from_dict(tf_config_dict)
od_config.set_tf_model_config(TransformerConfig.from_dict(tf_config_dict))
loader = DiffusersPipelineLoader(LoadConfig(), od_config=od_config)
# load_model will handle dtypes / device placement, put in .eval() mode
@@ -202,6 +202,7 @@ class TeaCacheCoefficientEstimator:
self.hook.start_collection()
req = OmniDiffusionRequest(
prompts=[prompt],
request_id="teacache-coefficient-estimator",
sampling_params=OmniDiffusionSamplingParams(
num_inference_steps=generate_kwargs.get("num_inference_steps", 20),
seed=generate_kwargs.get("seed", 42),
+32 -40
View File
@@ -30,10 +30,10 @@ from vllm_omni.diffusion.registry import (
get_diffusion_post_process_func,
get_diffusion_pre_process_func,
)
from vllm_omni.diffusion.request import OmniDiffusionRequest
from vllm_omni.diffusion.request import DUMMY_DIFFUSION_REQUEST_ID, OmniDiffusionRequest
from vllm_omni.diffusion.sched import RequestScheduler, SchedulerInterface, StepScheduler
from vllm_omni.diffusion.sched.interface import DiffusionRequestStatus
from vllm_omni.diffusion.worker.utils import BatchRunnerOutput, RunnerOutput
from vllm_omni.diffusion.worker.utils import BaseRunnerOutput, BatchRunnerOutput, RunnerOutput
from vllm_omni.inputs.data import OmniDiffusionSamplingParams, OmniTextPrompt
from vllm_omni.outputs import OmniRequestOutput
@@ -217,7 +217,7 @@ class DiffusionEngine:
logger.warning("Output is None, returning empty OmniRequestOutput")
return [
OmniRequestOutput.from_diffusion(
request_id=request.request_ids[i] if i < len(request.request_ids) else "",
request_id=request.request_id,
images=[],
prompt=prompt,
metrics={},
@@ -304,7 +304,7 @@ class DiffusionEngine:
if len(request.prompts) == 1:
# Single request: return single OmniRequestOutput
prompt = request.prompts[0]
request_id = request.request_ids[0] if request.request_ids else ""
request_id = request.request_id
if is_text_output:
return [
@@ -368,10 +368,9 @@ class DiffusionEngine:
# Split images based on num_outputs_per_prompt for each request
results = []
output_idx = 0
request_id = request.request_id
for i, prompt in enumerate(request.prompts):
request_id = request.request_ids[i] if i < len(request.request_ids) else ""
# Get images for this request
num_outputs = request.sampling_params.num_outputs_per_prompt
start_idx = output_idx
@@ -467,17 +466,17 @@ class DiffusionEngine:
runner_output = self.execute_fn(sched_output)
except Exception as exc:
logger.error(
"Execution failed for diffusion requests %s", sched_output.scheduled_req_ids, exc_info=True
"Execution failed for diffusion requests %s", sched_output.scheduled_request_ids, exc_info=True
)
runner_output = BatchRunnerOutput.from_list(
[
RunnerOutput(
req_id=req_id,
request_id=request_id,
step_index=None,
finished=True,
result=DiffusionOutput(error=str(exc)),
)
for req_id in sched_output.scheduled_req_ids
for request_id in sched_output.scheduled_request_ids
]
)
@@ -544,7 +543,7 @@ class DiffusionEngine:
def _handle_finished_requests(
self,
finished_ids: set[str],
runner_output: RunnerOutput | None = None,
runner_output: BaseRunnerOutput | None = None,
missing_result_error: str = "Diffusion execution finished without a final output",
):
for rid in finished_ids:
@@ -553,7 +552,7 @@ class DiffusionEngine:
if fut is None:
continue
if runner_output is not None:
_output = runner_output.get_req_output(rid)
_output = runner_output.get_request_output(rid)
else:
_output = None
out = self._finalize_finished_request(rid, _output, missing_result_error)
@@ -579,11 +578,11 @@ class DiffusionEngine:
if self._closed:
raise RuntimeError("DiffusionEngine is closed.")
fut = self.main_loop.create_future()
sched_req_id = self.scheduler.add_request(request)
self._out_queue[sched_req_id] = fut
request_id = self.scheduler.add_request(request)
self._out_queue[request_id] = fut
self._cv.notify_all()
return sched_req_id
return request_id
async def get_result(self, request_id: str) -> DiffusionOutput:
fut = self._out_queue.get(request_id)
@@ -599,37 +598,37 @@ class DiffusionEngine:
async def async_add_req_and_wait_for_response(self, request: OmniDiffusionRequest) -> DiffusionOutput:
# No lock needed: add_request is already protected by self._cv, and
# all executor calls are serialized inside the busy loop.
sched_req_id = self.add_request(request)
return await self.get_result(sched_req_id)
request_id = self.add_request(request)
return await self.get_result(request_id)
def add_req_and_wait_for_response(self, request: OmniDiffusionRequest) -> DiffusionOutput:
with self._rpc_lock:
if self._closed:
raise RuntimeError("DiffusionEngine is closed.")
target_sched_req_id = self.scheduler.add_request(request)
target_request_id = self.scheduler.add_request(request)
# keep scheduling and executing until the target request is finished
while True:
self._process_aborts_queue()
sched_output = self.scheduler.schedule()
if sched_output.is_empty:
if target_sched_req_id in sched_output.finished_req_ids:
return self._finalize_finished_request(target_sched_req_id)
if target_request_id in sched_output.finished_req_ids:
return self._finalize_finished_request(target_request_id)
if not self.scheduler.has_requests():
raise RuntimeError("Diffusion scheduler has no runnable requests.")
continue
# NOTE: add_req_and_wait_for_response() is synchronous, will be only called
# within _dummy_run, only one request will be scheduled
sched_req_id = sched_output.scheduled_req_ids[0]
request_id = sched_output.scheduled_request_ids[0]
try:
runner_output = self.execute_fn(sched_output)
except EngineDeadError:
raise
except Exception as exc:
logger.error("Execution failed for diffusion request %s", sched_req_id, exc_info=True)
logger.error("Execution failed for diffusion request %s", request_id, exc_info=True)
runner_output = RunnerOutput(
req_id=sched_req_id,
request_id=request_id,
step_index=None,
finished=True,
result=DiffusionOutput(error=str(exc)),
@@ -642,10 +641,10 @@ class DiffusionEngine:
# sync func should receive one result
if not isinstance(runner_output, RunnerOutput) and not len(runner_output) == 1:
raise ValueError("Sync func should receive one result at one time")
if target_sched_req_id in finished_req_ids:
runner_output = runner_output.get_req_output(target_sched_req_id)
if target_request_id in finished_req_ids:
runner_output = runner_output.get_request_output(target_request_id)
return self._finalize_finished_request(
target_sched_req_id,
target_request_id,
runner_output=runner_output,
missing_result_error="Diffusion execution finished without a final output.",
)
@@ -697,7 +696,7 @@ class DiffusionEngine:
num_frames = 2 if supports_audio_input or supports_audio_output(self.od_config.model_class_name) else 1
req = OmniDiffusionRequest(
prompts=[prompt],
request_ids=["dummy_req_id"],
request_id=DUMMY_DIFFUSION_REQUEST_ID,
sampling_params=OmniDiffusionSamplingParams(
height=height,
width=width,
@@ -904,34 +903,27 @@ class DiffusionEngine:
def _abort_requests(self, request_ids: str | Iterable[str]) -> None:
request_ids = [request_ids] if isinstance(request_ids, str) else list(request_ids)
sched_req_ids: list[str] = []
for request_id in dict.fromkeys(request_ids):
sched_req_id = self.scheduler.get_sched_req_id(request_id)
if sched_req_id is not None:
sched_req_ids.append(sched_req_id)
for sched_req_id in dict.fromkeys(sched_req_ids):
if self.scheduler.get_request_state(sched_req_id) is not None:
self.scheduler.finish_requests(sched_req_id, DiffusionRequestStatus.FINISHED_ABORTED)
if self.scheduler.get_request_state(request_id) is not None:
self.scheduler.finish_requests(request_id, DiffusionRequestStatus.FINISHED_ABORTED)
def _finalize_finished_request(
self,
sched_req_id: str,
request_id: str,
runner_output: RunnerOutput | None = None,
missing_result_error: str = "Diffusion scheduler finished target request without execution output.",
) -> DiffusionOutput:
state = self.scheduler.get_request_state(sched_req_id)
popped_state = self.scheduler.pop_request_state(sched_req_id)
state = self.scheduler.get_request_state(request_id)
popped_state = self.scheduler.pop_request_state(request_id)
state = state or popped_state
if state is None:
raise RuntimeError(f"Diffusion scheduler lost state for request {sched_req_id}.")
raise RuntimeError(f"Diffusion scheduler lost state for request {request_id}.")
if state.status == DiffusionRequestStatus.FINISHED_ABORTED:
request_id = state.req.request_ids[0] if state.req.request_ids else sched_req_id
return DiffusionOutput(
aborted=True,
abort_message=f"Request {request_id} aborted.",
abort_message=f"Request {state.req.request_id} aborted.",
)
if runner_output is not None and runner_output.result is not None:
@@ -311,7 +311,7 @@ class MultiprocDiffusionExecutor(DiffusionExecutor):
raise RuntimeError(f"Unexpected response type for execute_request: {type(result)!r}")
return RunnerOutput(
req_id=new_req.sched_req_id,
request_id=new_req.request_id,
step_index=None,
finished=True,
result=result,
@@ -334,9 +334,9 @@ class MultiprocDiffusionExecutor(DiffusionExecutor):
# TODO: Remove this fallback; DiffusionOutput cannot faithfully represent
# failed multi-request step batches.
if isinstance(result, DiffusionOutput):
req_id = scheduler_output.scheduled_req_ids[0] if scheduler_output.scheduled_req_ids else ""
request_id = scheduler_output.scheduled_request_ids[0] if scheduler_output.scheduled_request_ids else ""
return RunnerOutput(
req_id=req_id,
request_id=request_id,
step_index=None,
finished=True,
result=result,
@@ -128,7 +128,6 @@ class InlineStageDiffusionClient(StageClientBase):
request = OmniDiffusionRequest(
prompts=[prompt],
sampling_params=sampling_params,
request_ids=[request_id],
request_id=request_id,
kv_sender_info=kv_sender_info,
)
@@ -187,7 +186,6 @@ class InlineStageDiffusionClient(StageClientBase):
request = OmniDiffusionRequest(
prompts=prompts,
sampling_params=sampling_params,
request_ids=[f"{request_id}-{i}" for i in range(len(prompts))],
request_id=request_id,
kv_sender_info=kv_sender_info,
)
@@ -247,8 +247,10 @@ class ErnieImagePipeline(
@staticmethod
def _is_warmup_request(req: OmniDiffusionRequest) -> bool:
request_ids = getattr(req, "request_ids", None) or ()
return len(request_ids) == 1 and request_ids[0] == "dummy_req_id"
is_dummy_run = getattr(req, "is_dummy_run", None)
if callable(is_dummy_run):
return bool(is_dummy_run())
return OmniDiffusionRequest.is_dummy_run_request_id(getattr(req, "request_id", None))
@staticmethod
def _should_apply_pe(req: OmniDiffusionRequest) -> bool:
@@ -15,6 +15,7 @@ from vllm.logger import init_logger
from vllm.model_executor.layers.linear import (
ColumnParallelLinear,
QKVParallelLinear,
ReplicatedLinear,
RowParallelLinear,
)
from vllm.model_executor.model_loader.weight_utils import default_weight_loader
@@ -33,6 +34,9 @@ from vllm_omni.diffusion.distributed.sp_plan import (
)
from vllm_omni.diffusion.forward_context import get_forward_context
if TYPE_CHECKING:
from vllm.model_executor.layers.quantization.base_config import QuantizationConfig
logger = init_logger(__name__)
@@ -230,11 +234,24 @@ class GlmImagePrepare(nn.Module):
class GlmImageAdaLayerNormZero(nn.Module):
"""Adaptive LayerNorm with zero initialization for both image and text streams."""
def __init__(self, embedding_dim: int, dim: int) -> None:
def __init__(
self,
embedding_dim: int,
dim: int,
quant_config: "QuantizationConfig | None" = None,
prefix: str = "",
) -> None:
super().__init__()
self.norm = nn.LayerNorm(dim, elementwise_affine=False, eps=1e-5)
self.norm_context = nn.LayerNorm(dim, elementwise_affine=False, eps=1e-5)
self.linear = nn.Linear(embedding_dim, 12 * dim, bias=True)
self.linear = ReplicatedLinear(
embedding_dim,
12 * dim,
bias=True,
return_bias=False,
quant_config=quant_config,
prefix=f"{prefix}.linear",
)
def forward(
self, hidden_states: torch.Tensor, encoder_hidden_states: torch.Tensor, temb: torch.Tensor
@@ -244,6 +261,8 @@ class GlmImageAdaLayerNormZero(nn.Module):
norm_encoder_hidden_states = self.norm_context(encoder_hidden_states).to(dtype=dtype)
emb = self.linear(temb)
if isinstance(emb, tuple):
emb = emb[0]
(
shift_msa,
c_shift_msa,
@@ -286,14 +305,25 @@ class GlmImageAdaLayerNormContinuous(nn.Module):
elementwise_affine: bool = True,
eps: float = 1e-5,
bias: bool = True,
quant_config: "QuantizationConfig | None" = None,
prefix: str = "",
):
super().__init__()
self.linear = nn.Linear(conditioning_embedding_dim, embedding_dim * 2, bias=bias)
self.linear = ReplicatedLinear(
conditioning_embedding_dim,
embedding_dim * 2,
bias=bias,
return_bias=False,
quant_config=quant_config,
prefix=f"{prefix}.linear",
)
self.norm = nn.LayerNorm(embedding_dim, eps=eps, elementwise_affine=elementwise_affine)
def forward(self, x: torch.Tensor, conditioning_embedding: torch.Tensor) -> torch.Tensor:
# NO SiLU here
emb = self.linear(conditioning_embedding.to(x.dtype))
if isinstance(emb, tuple):
emb = emb[0]
scale, shift = torch.chunk(emb, 2, dim=1)
x = self.norm(x) * (1 + scale)[:, None, :] + shift[:, None, :]
return x
@@ -469,6 +499,7 @@ class GlmImageAttention(nn.Module):
out_bias: bool = True,
eps: float = 1e-5,
quant_config: "QuantizationConfig | None" = None,
prefix: str = "",
):
super().__init__()
self.dim = dim
@@ -485,6 +516,7 @@ class GlmImageAttention(nn.Module):
bias=True,
return_bias=False,
quant_config=quant_config,
prefix=f"{prefix}.to_qkv",
)
# QK normalization (LayerNorm, not RMSNorm for GLM-Image)
@@ -501,6 +533,7 @@ class GlmImageAttention(nn.Module):
input_is_parallel=True,
return_bias=False,
quant_config=quant_config,
prefix=f"{prefix}.to_out.0",
),
nn.Dropout(0.0),
]
@@ -554,7 +587,7 @@ class GlmImageAttention(nn.Module):
hidden_states_combined = torch.cat([encoder_hidden_states, hidden_states], dim=1)
# QKV projection
qkv = self.to_qkv(hidden_states_combined)
qkv = self.to_qkv(hidden_states_combined.contiguous())
q_size = self.to_qkv.num_heads * self.head_dim
kv_size = self.to_qkv.num_kv_heads * self.head_dim
query, key, value = qkv.split([q_size, kv_size, kv_size], dim=-1)
@@ -604,8 +637,12 @@ class GlmImageAttention(nn.Module):
# Project combined [text, image] outputs, then split.
# This keeps SP numerically aligned with the non-SP path.
joint_hidden_states_out = joint_hidden_states_out.flatten(2, 3).to(dtype)
# Contiguous for FP8/W4A16 quantized RowParallelLinear
for module in self.to_out:
joint_hidden_states_out = module(joint_hidden_states_out)
if isinstance(module, RowParallelLinear):
joint_hidden_states_out = module(joint_hidden_states_out.contiguous())
else:
joint_hidden_states_out = module(joint_hidden_states_out)
encoder_hidden_states_out = joint_hidden_states_out[:, :text_seq_length, :]
hidden_states_out = joint_hidden_states_out[:, text_seq_length:, :]
@@ -645,7 +682,10 @@ class GlmImageAttention(nn.Module):
# Output projection
for module in self.to_out:
hidden_states_out = module(hidden_states_out)
if isinstance(module, RowParallelLinear):
hidden_states_out = module(hidden_states_out.contiguous())
else:
hidden_states_out = module(hidden_states_out)
# Split back to text and image
encoder_hidden_states_out = hidden_states_out[:, :text_seq_length, :]
@@ -663,6 +703,7 @@ class ColumnParallelGELU(nn.Module):
approximate: str = "none",
bias: bool = True,
quant_config: "QuantizationConfig | None" = None,
prefix: str = "",
):
super().__init__()
self.proj = ColumnParallelLinear(
@@ -672,6 +713,7 @@ class ColumnParallelGELU(nn.Module):
gather_output=False,
return_bias=False,
quant_config=quant_config,
prefix=f"{prefix}.proj",
)
self.approximate = approximate
@@ -688,6 +730,7 @@ class ColumnParallelSiLU(nn.Module):
*,
bias: bool = True,
quant_config: "QuantizationConfig | None" = None,
prefix: str = "",
):
super().__init__()
self.proj = ColumnParallelLinear(
@@ -697,6 +740,7 @@ class ColumnParallelSiLU(nn.Module):
gather_output=False,
return_bias=False,
quant_config=quant_config,
prefix=f"{prefix}.proj",
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
@@ -714,6 +758,7 @@ class GlmImageFeedForward(nn.Module):
bias: bool = True,
activation_fn: str = "gelu",
quant_config: "QuantizationConfig | None" = None,
prefix: str = "",
):
super().__init__()
inner_dim = inner_dim or int(dim * mult)
@@ -721,7 +766,7 @@ class GlmImageFeedForward(nn.Module):
if activation_fn == "linear-silu":
layers: list[nn.Module] = [
ColumnParallelSiLU(dim, inner_dim, bias=bias, quant_config=quant_config),
ColumnParallelSiLU(dim, inner_dim, bias=bias, quant_config=quant_config, prefix=f"{prefix}.net.0"),
nn.Identity(),
RowParallelLinear(
inner_dim,
@@ -730,12 +775,20 @@ class GlmImageFeedForward(nn.Module):
input_is_parallel=True,
return_bias=False,
quant_config=quant_config,
prefix=f"{prefix}.net.2",
),
]
else:
approximate = "tanh" if activation_fn == "gelu-approximate" else "none"
layers = [
ColumnParallelGELU(dim, inner_dim, approximate=approximate, bias=bias, quant_config=quant_config),
ColumnParallelGELU(
dim,
inner_dim,
approximate=approximate,
bias=bias,
quant_config=quant_config,
prefix=f"{prefix}.net.0",
),
nn.Identity(),
RowParallelLinear(
inner_dim,
@@ -744,6 +797,7 @@ class GlmImageFeedForward(nn.Module):
input_is_parallel=True,
return_bias=False,
quant_config=quant_config,
prefix=f"{prefix}.net.2",
),
]
@@ -751,7 +805,12 @@ class GlmImageFeedForward(nn.Module):
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
for module in self.net:
hidden_states = module(hidden_states)
if isinstance(module, ColumnParallelLinear):
hidden_states, _ = module(hidden_states)
elif isinstance(module, RowParallelLinear):
hidden_states = module(hidden_states.contiguous())
else:
hidden_states = module(hidden_states)
return hidden_states
@@ -765,19 +824,21 @@ class GlmImageTransformerBlock(nn.Module):
attention_head_dim: int = 40,
time_embed_dim: int = 512,
ffn_hidden_dim: int | None = None,
quant_config: "QuantizationConfig | None" = None,
parallel_config: DiffusionParallelConfig | None = None,
quant_config: "QuantizationConfig | None" = None,
prefix: str = "",
) -> None:
super().__init__()
# 1. Attention with AdaLN
self.norm1 = GlmImageAdaLayerNormZero(time_embed_dim, dim)
self.norm1 = GlmImageAdaLayerNormZero(time_embed_dim, dim, quant_config=quant_config, prefix=f"{prefix}.norm1")
self.attn1 = GlmImageAttention(
dim=dim,
num_heads=num_attention_heads,
head_dim=attention_head_dim,
quant_config=quant_config,
parallel_config=parallel_config,
quant_config=quant_config,
prefix=f"{prefix}.attn1",
)
# 2. Feedforward
@@ -789,6 +850,7 @@ class GlmImageTransformerBlock(nn.Module):
inner_dim=ffn_hidden_dim,
activation_fn="gelu-approximate",
quant_config=quant_config,
prefix=f"{prefix}.ff",
)
def forward(
@@ -959,6 +1021,7 @@ class GlmImageTransformer2DModel(CachedTransformer):
inner_dim=inner_dim,
activation_fn="gelu",
quant_config=quant_config,
prefix="glyph_projector",
)
self.prior_token_embedding = nn.Embedding(prior_vq_quantizer_codebook_size, inner_dim)
self.prior_projector = GlmImageFeedForward(
@@ -967,6 +1030,7 @@ class GlmImageTransformer2DModel(CachedTransformer):
inner_dim=inner_dim,
activation_fn="linear-silu",
quant_config=quant_config,
prefix="prior_projector",
)
# Prepare module for SP (encapsulates patch embedding and RoPE for _sp_plan)
@@ -988,15 +1052,20 @@ class GlmImageTransformer2DModel(CachedTransformer):
attention_head_dim,
time_embed_dim,
ffn_hidden_dim=ffn_hidden_dim,
quant_config=quant_config,
parallel_config=self.parallel_config,
quant_config=quant_config,
prefix=f"transformer_blocks.{i}",
)
for _ in range(num_layers)
for i in range(num_layers)
]
)
# 4. Output projection
self.norm_out = GlmImageAdaLayerNormContinuous(inner_dim, time_embed_dim, elementwise_affine=False)
# Final modulation feeds proj_out; quant_config is NOT applied here
# to avoid precision degradation in the final projection layer.
self.norm_out = GlmImageAdaLayerNormContinuous(
inner_dim, time_embed_dim, elementwise_affine=False, quant_config=None, prefix="norm_out"
)
self.proj_out = nn.Linear(inner_dim, patch_size * patch_size * out_channels, bias=True)
def forward(
@@ -301,10 +301,7 @@ class GlmImagePipeline(nn.Module, DiffusionPipelineProfilerMixin):
# Load transformer (DiT)
logger.info("Loading GlmImageTransformer2DModel (DiT)...")
self.transformer = GlmImageTransformer2DModel(
od_config=od_config,
quant_config=od_config.quantization_config,
)
self.transformer = GlmImageTransformer2DModel(od_config=od_config, quant_config=od_config.quantization_config)
# Weight sources for DiT loading
self.weights_sources = [
@@ -689,15 +686,11 @@ class GlmImagePipeline(nn.Module, DiffusionPipelineProfilerMixin):
prompt = first_prompt if isinstance(first_prompt, str) else (first_prompt.get("prompt") or "")
# NOTE: DiffusionEngine does an internal warmup "dummy run" during
# initialization. That request has no request_id and does not carry
# initialization. That request carries the fixed dummy request_id and does not carry
# Stage-0 (AR) outputs via req.extra. For GLM-Image, we allow that
# specific warmup request to proceed by synthesizing minimal prior
# tokens, while still raising a clear error for real requests.
is_dummy_warmup = (
not getattr(req, "request_id", None)
and prompt == "dummy run"
and (req.sampling_params.num_inference_steps == 1)
)
is_dummy_warmup = req.is_dummy_run()
# Get pre-computed prompt embeddings if provided
if isinstance(first_prompt, str):
+14 -4
View File
@@ -3,10 +3,12 @@
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import random
from dataclasses import dataclass, field
from dataclasses import dataclass
from vllm_omni.inputs.data import OmniDiffusionSamplingParams, OmniPromptType
DUMMY_DIFFUSION_REQUEST_ID = "dummy_req_id"
@dataclass
class OmniDiffusionRequest:
@@ -24,13 +26,14 @@ class OmniDiffusionRequest:
prompts: list[OmniPromptType] # Actually supporting str-based prompts
sampling_params: OmniDiffusionSamplingParams
request_ids: list[str] = field(default_factory=list)
request_id: str | None = None
request_id: str
kv_sender_info: dict | None = None
def __post_init__(self):
"""Initialize dependent fields after dataclass initialization."""
if not isinstance(self.request_id, str) or not self.request_id:
raise ValueError("OmniDiffusionRequest.request_id must be a non-empty string.")
# When neither a generator nor a seed is provided, assign a random seed
# so that all ranks derive the same generator state.
if self.sampling_params.generator is None and self.sampling_params.seed is None:
@@ -56,3 +59,10 @@ class OmniDiffusionRequest:
# so downstream code always has a valid value.
if self.sampling_params.guidance_scale_2 is None:
self.sampling_params.guidance_scale_2 = self.sampling_params.guidance_scale
def is_dummy_run(self) -> bool:
return self.is_dummy_run_request_id(self.request_id)
@classmethod
def is_dummy_run_request_id(cls, request_id: str | None) -> bool:
return request_id == DUMMY_DIFFUSION_REQUEST_ID
+50 -83
View File
@@ -46,7 +46,6 @@ class _BaseScheduler(SchedulerInterface):
def __init__(self) -> None:
self.od_config: OmniDiffusionConfig | None = None
self._request_states: dict[str, DiffusionRequestState] = {}
self._request_id_to_sched_req_id: dict[str, str] = {}
self._step_id: int = 0
self._waiting: deque[str] = deque()
self._running: list[str] = []
@@ -57,7 +56,6 @@ class _BaseScheduler(SchedulerInterface):
def initialize(self, od_config: OmniDiffusionConfig) -> None:
self.od_config = od_config
self._request_states.clear()
self._request_id_to_sched_req_id.clear()
self._step_id = 0
self._waiting.clear()
self._running.clear()
@@ -71,32 +69,31 @@ class _BaseScheduler(SchedulerInterface):
self._reset_scheduler_state()
def add_request(self, request: OmniDiffusionRequest) -> str:
sched_req_id = self._make_sched_req_id(request)
return self._add_request_with_sched_req_id(sched_req_id, request)
return self._add_request_with_request_id(request.request_id, request)
def _add_request_with_sched_req_id(self, sched_req_id: str, request: OmniDiffusionRequest) -> str:
state = self._make_request_state(sched_req_id, request)
request_ids = self._request_ids_for_mapping(request)
self._register_request_ids(request_ids, sched_req_id)
self._request_states[sched_req_id] = state
self._waiting.append(sched_req_id)
logger.debug("%s add_request: %s (waiting=%d)", self.__class__.__name__, sched_req_id, len(self._waiting))
return sched_req_id
def _add_request_with_request_id(self, request_id: str, request: OmniDiffusionRequest) -> str:
if request_id in self._request_states:
raise ValueError(f"request_id {request_id!r} is already active.")
state = self._make_request_state(request_id, request)
self._request_states[request_id] = state
self._waiting.append(request_id)
logger.debug("%s add_request: %s (waiting=%d)", self.__class__.__name__, request_id, len(self._waiting))
return request_id
def schedule(self) -> DiffusionSchedulerOutput:
scheduled_new_reqs: list[NewRequestData] = []
scheduled_cached_req_ids: list[str] = []
scheduled_cached_request_ids: list[str] = []
# First, schedule the RUNNING request(s)
for sched_req_id in self._running:
state = self._request_states.get(sched_req_id)
for request_id in self._running:
state = self._request_states.get(request_id)
if state is not None:
scheduled_cached_req_ids.append(sched_req_id)
scheduled_cached_request_ids.append(request_id)
# Second, schedule WAITING requests while capacity remains.
while self._waiting and len(self._running) < self.max_num_running_reqs:
sched_req_id = self._waiting[0]
state = self._request_states.get(sched_req_id)
request_id = self._waiting[0]
state = self._request_states.get(request_id)
if state is None:
self._waiting.popleft()
continue
@@ -108,16 +105,16 @@ class _BaseScheduler(SchedulerInterface):
if not self._running:
self._running_sampling_params_key = state.sampling_params_key
state.status = DiffusionRequestStatus.RUNNING
self._running.append(sched_req_id)
self._running.append(request_id)
if was_new_request:
scheduled_new_reqs.append(NewRequestData.from_state(state))
else:
scheduled_cached_req_ids.append(sched_req_id)
scheduled_cached_request_ids.append(request_id)
scheduler_output = DiffusionSchedulerOutput(
step_id=self._step_id,
scheduled_new_reqs=scheduled_new_reqs,
scheduled_cached_reqs=CachedRequestData(sched_req_ids=scheduled_cached_req_ids),
scheduled_cached_reqs=CachedRequestData(request_ids=scheduled_cached_request_ids),
finished_req_ids=set(self._finished_req_ids),
num_running_reqs=len(self._running),
num_waiting_reqs=len(self._waiting),
@@ -131,40 +128,33 @@ class _BaseScheduler(SchedulerInterface):
def has_requests(self) -> bool:
return bool(self._waiting or self._running)
def get_request_state(self, sched_req_id: str) -> DiffusionRequestState | None:
return self._request_states.get(sched_req_id)
def get_request_state(self, request_id: str) -> DiffusionRequestState | None:
return self._request_states.get(request_id)
def get_sched_req_id(self, request_id: str) -> str | None:
return self._request_id_to_sched_req_id.get(request_id)
def pop_request_state(self, request_id: str) -> DiffusionRequestState | None:
self._pop_extra_request_state(request_id)
return self._request_states.pop(request_id, None)
def pop_request_state(self, sched_req_id: str) -> DiffusionRequestState | None:
self._pop_extra_request_state(sched_req_id)
state = self._request_states.pop(sched_req_id, None)
if state is not None:
self._unregister_request_ids(self._request_ids_for_mapping(state.req), sched_req_id)
return state
def preempt_request(self, sched_req_id: str) -> bool:
if sched_req_id not in self._request_states:
def preempt_request(self, request_id: str) -> bool:
if request_id not in self._request_states:
return False
if sched_req_id in self._running:
self._running.remove(sched_req_id)
if request_id in self._running:
self._running.remove(request_id)
if not self._running:
self._running_sampling_params_key = None
self._waiting.appendleft(sched_req_id)
self._request_states[sched_req_id].status = DiffusionRequestStatus.PREEMPTED
self._waiting.appendleft(request_id)
self._request_states[request_id].status = DiffusionRequestStatus.PREEMPTED
return True
return False
def finish_requests(self, sched_req_ids: str | list[str], status: DiffusionRequestStatus) -> None:
def finish_requests(self, request_ids: str | list[str], status: DiffusionRequestStatus) -> None:
assert DiffusionRequestStatus.is_finished(status)
if isinstance(sched_req_ids, str):
sched_req_ids = [sched_req_ids]
self._finish_requests({sched_req_id: status for sched_req_id in sched_req_ids})
if isinstance(request_ids, str):
request_ids = [request_ids]
self._finish_requests({request_id: status for request_id in request_ids})
def close(self) -> None:
self._request_states.clear()
self._request_id_to_sched_req_id.clear()
self._waiting.clear()
self._running.clear()
self._running_sampling_params_key = None
@@ -183,33 +173,31 @@ class _BaseScheduler(SchedulerInterface):
running_to_remove: set[str] = set()
waiting_to_remove: set[str] = set()
for sched_req_id, status in statuses.items():
for request_id, status in statuses.items():
assert DiffusionRequestStatus.is_finished(status)
state = self._request_states.get(sched_req_id)
state = self._request_states.get(request_id)
if state is None or state.is_finished():
continue
finished_req_ids.add(sched_req_id)
if sched_req_id in self._running:
running_to_remove.add(sched_req_id)
if sched_req_id in self._waiting:
waiting_to_remove.add(sched_req_id)
finished_req_ids.add(request_id)
if request_id in self._running:
running_to_remove.add(request_id)
if request_id in self._waiting:
waiting_to_remove.add(request_id)
if running_to_remove:
self._running = [sched_req_id for sched_req_id in self._running if sched_req_id not in running_to_remove]
self._running = [request_id for request_id in self._running if request_id not in running_to_remove]
if not self._running:
self._running_sampling_params_key = None
if waiting_to_remove:
self._waiting = deque(
sched_req_id for sched_req_id in self._waiting if sched_req_id not in waiting_to_remove
)
self._waiting = deque(request_id for request_id in self._waiting if request_id not in waiting_to_remove)
for sched_req_id in finished_req_ids:
state = self._request_states[sched_req_id]
status = statuses[sched_req_id]
for request_id in finished_req_ids:
state = self._request_states[request_id]
status = statuses[request_id]
state.status = status
if status == DiffusionRequestStatus.FINISHED_ERROR:
state.error = None if errors is None else errors.get(sched_req_id)
state.error = None if errors is None else errors.get(request_id)
else:
state.error = None
@@ -227,7 +215,7 @@ class _BaseScheduler(SchedulerInterface):
# marked finished at that point, but we still need to surface its id
# in this update so the engine can observe the terminal state.
finished_req_ids = {
sched_req_id for sched_req_id in sched_output.scheduled_req_ids if sched_req_id in self._finished_req_ids
request_id for request_id in sched_output.scheduled_request_ids if request_id in self._finished_req_ids
}
finished_req_ids |= self._finish_requests(statuses, errors)
return finished_req_ids
@@ -235,12 +223,12 @@ class _BaseScheduler(SchedulerInterface):
def _reset_scheduler_state(self) -> None:
"""Reset subclass-owned state during initialize()/close()."""
def _pop_extra_request_state(self, sched_req_id: str) -> None:
def _pop_extra_request_state(self, request_id: str) -> None:
"""Remove subclass-owned per-request state before popping request state."""
def _make_request_state(self, sched_req_id: str, request: OmniDiffusionRequest) -> DiffusionRequestState:
def _make_request_state(self, request_id: str, request: OmniDiffusionRequest) -> DiffusionRequestState:
return DiffusionRequestState(
sched_req_id=sched_req_id,
request_id=request_id,
req=request,
sampling_params_key=get_sampling_params_key(request),
)
@@ -258,24 +246,3 @@ class _BaseScheduler(SchedulerInterface):
state = self._request_states.get(self._running[0])
self._running_sampling_params_key = None if state is None else state.sampling_params_key
return self._running_sampling_params_key
def _register_request_ids(self, request_ids: list[str], sched_req_id: str) -> None:
unique_request_ids = list(dict.fromkeys(request_ids))
for request_id in unique_request_ids:
existing = self._request_id_to_sched_req_id.get(request_id)
if existing is not None and existing != sched_req_id:
raise ValueError(f"request_id {request_id!r} is already mapped to active sched_req_id {existing!r}.")
for request_id in unique_request_ids:
self._request_id_to_sched_req_id[request_id] = sched_req_id
def _unregister_request_ids(self, request_ids: list[str], sched_req_id: str) -> None:
for request_id in request_ids:
if self._request_id_to_sched_req_id.get(request_id) == sched_req_id:
self._request_id_to_sched_req_id.pop(request_id, None)
@staticmethod
def _request_ids_for_mapping(request: OmniDiffusionRequest) -> list[str]:
request_ids = list(request.request_ids)
if request_id := getattr(request, "request_id", None):
request_ids.append(request_id)
return list(dict.fromkeys(request_ids))
+14 -48
View File
@@ -4,22 +4,17 @@
from __future__ import annotations
import enum
import uuid
from abc import ABC, abstractmethod
from dataclasses import dataclass
from functools import cached_property
from typing import TYPE_CHECKING
from vllm.logger import init_logger
from vllm_omni.diffusion.data import OmniDiffusionConfig
from vllm_omni.diffusion.request import OmniDiffusionRequest
if TYPE_CHECKING:
from vllm_omni.diffusion.worker.utils import RunnerOutput
logger = init_logger(__name__)
class DiffusionRequestStatus(enum.IntEnum):
"""Request status tracked by diffusion scheduler."""
@@ -75,10 +70,7 @@ class SamplingParamsKey:
class DiffusionRequestState:
"""Scheduler-owned state for one queued OmniDiffusionRequest."""
# Unique scheduler-owned request ID.
# NOTE: This identifies one OmniDiffusionRequest, which may contain multiple request_ids.
# TODO: Align this with OmniDiffusionRequest.request_ids once scheduler batching is supported.
sched_req_id: str
request_id: str
req: OmniDiffusionRequest
sampling_params_key: SamplingParamsKey | None = None
status: DiffusionRequestStatus = DiffusionRequestStatus.WAITING
@@ -92,23 +84,23 @@ class DiffusionRequestState:
class NewRequestData:
"""Full request payload for a newly scheduled diffusion request."""
sched_req_id: str
request_id: str
req: OmniDiffusionRequest
@classmethod
def from_state(cls, state: DiffusionRequestState) -> NewRequestData:
return cls(sched_req_id=state.sched_req_id, req=state.req)
return cls(request_id=state.request_id, req=state.req)
@dataclass
class CachedRequestData:
"""Cached diffusion requests that only need their scheduler ids resent."""
"""Cached diffusion requests that only need their request ids resent."""
sched_req_ids: list[str]
request_ids: list[str]
@classmethod
def make_empty(cls) -> CachedRequestData:
return cls(sched_req_ids=[])
return cls(request_ids=[])
@dataclass
@@ -123,21 +115,18 @@ class DiffusionSchedulerOutput:
num_waiting_reqs: int
@cached_property
def scheduled_req_ids(self) -> list[str]:
def scheduled_request_ids(self) -> list[str]:
"""
All scheduled request ids in this cycle, including both new and cached ones.
NOTE:
This id is generated and owned by the scheduler,
and may be different from the OmniDiffusionRequest.request_ids.
"""
return [
*(req.sched_req_id for req in self.scheduled_new_reqs),
*self.scheduled_cached_reqs.sched_req_ids,
*(req.request_id for req in self.scheduled_new_reqs),
*self.scheduled_cached_reqs.request_ids,
]
@property
def num_scheduled_reqs(self) -> int:
return len(self.scheduled_req_ids)
return len(self.scheduled_request_ids)
@property
def is_empty(self) -> bool:
@@ -147,25 +136,6 @@ class DiffusionSchedulerOutput:
class SchedulerInterface(ABC):
"""Abstract lifecycle contract for diffusion schedulers."""
def _make_sched_req_id(self, request: OmniDiffusionRequest) -> str:
"""
Generate a unique scheduler request ID for the given request.
The default implementation uses the first request_id from the request if available,
otherwise generates a random one.
"""
if request.request_ids:
base = request.request_ids[0]
else:
logger.warning("Request has no request_ids, generating a random one. Request: %s", request)
base = f"req_{uuid.uuid4().hex[:8]}"
sched_req_id = base
suffix = 1
while self.get_request_state(sched_req_id) is not None:
sched_req_id = f"{base}#{suffix}"
suffix += 1
return sched_req_id
@abstractmethod
def initialize(self, od_config: OmniDiffusionConfig) -> None:
"""Initialize or reset scheduler state."""
@@ -183,7 +153,7 @@ class SchedulerInterface(ABC):
"""Update scheduler state from executor output."""
@abstractmethod
def get_request_state(self, sched_req_id: str) -> DiffusionRequestState | None:
def get_request_state(self, request_id: str) -> DiffusionRequestState | None:
"""Return request state if present."""
@abstractmethod
@@ -191,19 +161,15 @@ class SchedulerInterface(ABC):
"""Return whether the scheduler still owns runnable requests."""
@abstractmethod
def get_sched_req_id(self, request_id: str) -> str | None:
"""Resolve a public request_id to the active scheduler request id."""
@abstractmethod
def pop_request_state(self, sched_req_id: str) -> DiffusionRequestState | None:
def pop_request_state(self, request_id: str) -> DiffusionRequestState | None:
"""Remove and return request state if present."""
@abstractmethod
def preempt_request(self, sched_req_id: str) -> bool:
def preempt_request(self, request_id: str) -> bool:
"""Preempt a running request back to waiting."""
@abstractmethod
def finish_requests(self, sched_req_ids: str | list[str], status: DiffusionRequestStatus) -> None:
def finish_requests(self, request_ids: str | list[str], status: DiffusionRequestStatus) -> None:
"""Mark one or more requests finished."""
@abstractmethod
+11 -11
View File
@@ -26,26 +26,26 @@ class RequestScheduler(_BaseScheduler):
return super().schedule()
def update_from_output(self, sched_output: DiffusionSchedulerOutput, output: RunnerOutput) -> set[str]:
scheduled_req_ids = sched_output.scheduled_req_ids
if not scheduled_req_ids:
scheduled_request_ids = sched_output.scheduled_request_ids
if not scheduled_request_ids:
return set()
terminal_statuses: dict[str, DiffusionRequestStatus] = {}
terminal_errors: dict[str, str | None] = {}
for sched_req_id in scheduled_req_ids:
state = self._request_states.get(sched_req_id)
for request_id in scheduled_request_ids:
state = self._request_states.get(request_id)
if state is None or state.is_finished():
continue
req_output = output.get_req_output(sched_req_id)
req_output = output.get_request_output(request_id)
result = req_output.result if req_output is not None else None
if result is None:
terminal_statuses[sched_req_id] = DiffusionRequestStatus.FINISHED_ERROR
terminal_errors[sched_req_id] = "No output result"
terminal_statuses[request_id] = DiffusionRequestStatus.FINISHED_ERROR
terminal_errors[request_id] = "No output result"
elif result.error:
terminal_statuses[sched_req_id] = DiffusionRequestStatus.FINISHED_ERROR
terminal_errors[sched_req_id] = result.error
terminal_statuses[request_id] = DiffusionRequestStatus.FINISHED_ERROR
terminal_errors[request_id] = result.error
else:
terminal_statuses[sched_req_id] = DiffusionRequestStatus.FINISHED_COMPLETED
terminal_errors[sched_req_id] = None
terminal_statuses[request_id] = DiffusionRequestStatus.FINISHED_COMPLETED
terminal_errors[request_id] = None
return self._finalize_update_from_output(sched_output, terminal_statuses, terminal_errors)
+25 -25
View File
@@ -38,84 +38,84 @@ class StepScheduler(_BaseScheduler):
self._request_progress.clear()
def add_request(self, request: OmniDiffusionRequest) -> str:
sched_req_id = self._make_sched_req_id(request)
request_id = request.request_id
total_steps = self._get_total_steps(request)
if total_steps <= 0:
raise ValueError(f"Diffusion request {sched_req_id} must have positive total_steps, got {total_steps}")
raise ValueError(f"Diffusion request {request_id} must have positive total_steps, got {total_steps}")
current_step = request.sampling_params.step_index or 0
if current_step < 0 or current_step >= total_steps:
raise ValueError(
f"Diffusion request {sched_req_id} has invalid initial step_index {current_step} "
f"Diffusion request {request_id} has invalid initial step_index {current_step} "
f"for total_steps={total_steps}"
)
request.sampling_params.step_index = current_step
sched_req_id = self._add_request_with_sched_req_id(sched_req_id, request)
self._request_progress[sched_req_id] = _StepProgress(current_step=current_step, total_steps=total_steps)
request_id = self._add_request_with_request_id(request_id, request)
self._request_progress[request_id] = _StepProgress(current_step=current_step, total_steps=total_steps)
logger.debug(
"StepScheduler add_request: %s (step=%d/%d, waiting=%d)",
sched_req_id,
request_id,
current_step,
total_steps,
len(self._waiting),
)
return sched_req_id
return request_id
def schedule(self) -> DiffusionSchedulerOutput:
return super().schedule()
def update_from_output(self, sched_output: DiffusionSchedulerOutput, output: RunnerOutput) -> set[str]:
scheduled_req_ids = sched_output.scheduled_req_ids
if not scheduled_req_ids:
scheduled_request_ids = sched_output.scheduled_request_ids
if not scheduled_request_ids:
return set()
terminal_statuses: dict[str, DiffusionRequestStatus] = {}
terminal_errors: dict[str, str | None] = {}
for sched_req_id in scheduled_req_ids:
state = self._request_states.get(sched_req_id)
progress = self._request_progress.get(sched_req_id)
for request_id in scheduled_request_ids:
state = self._request_states.get(request_id)
progress = self._request_progress.get(request_id)
if state is None or progress is None or state.is_finished():
continue
req_output = output.get_req_output(sched_req_id)
req_output = output.get_request_output(request_id)
if req_output is None:
logger.warning(
"No RunnerOutput for request %s, treating as error",
sched_req_id,
request_id,
)
terminal_statuses[sched_req_id] = DiffusionRequestStatus.FINISHED_ERROR
terminal_errors[sched_req_id] = "No output for request"
terminal_statuses[request_id] = DiffusionRequestStatus.FINISHED_ERROR
terminal_errors[request_id] = "No output for request"
continue
req_result = req_output.result
output_error = req_result.error if req_result is not None else None
if output_error is not None:
terminal_statuses[sched_req_id] = DiffusionRequestStatus.FINISHED_ERROR
terminal_errors[sched_req_id] = output_error
terminal_statuses[request_id] = DiffusionRequestStatus.FINISHED_ERROR
terminal_errors[request_id] = output_error
continue
if req_output.step_index is None:
logger.warning(
"Received RunnerOutput with no step_index for request %s, treating as error",
sched_req_id,
request_id,
)
terminal_statuses[sched_req_id] = DiffusionRequestStatus.FINISHED_ERROR
terminal_errors[sched_req_id] = "Missing step_index in RunnerOutput"
terminal_statuses[request_id] = DiffusionRequestStatus.FINISHED_ERROR
terminal_errors[request_id] = "Missing step_index in RunnerOutput"
continue
# We assume that the decoding stage is executed immediately after the denoising stage completes.
progress.current_step = req_output.step_index
state.req.sampling_params.step_index = req_output.step_index
if req_output.finished:
terminal_statuses[sched_req_id] = DiffusionRequestStatus.FINISHED_COMPLETED
terminal_errors[sched_req_id] = None
terminal_statuses[request_id] = DiffusionRequestStatus.FINISHED_COMPLETED
terminal_errors[request_id] = None
else:
state.error = None
return self._finalize_update_from_output(sched_output, terminal_statuses, terminal_errors)
def _pop_extra_request_state(self, sched_req_id: str) -> None:
self._request_progress.pop(sched_req_id, None)
def _pop_extra_request_state(self, request_id: str) -> None:
self._request_progress.pop(request_id, None)
def _get_total_steps(self, request: OmniDiffusionRequest) -> int:
sampling = request.sampling_params
@@ -159,7 +159,6 @@ class StageDiffusionProc:
request = OmniDiffusionRequest(
prompts=[prompt],
sampling_params=sampling_params,
request_ids=[request_id],
request_id=request_id,
kv_sender_info=kv_sender_info,
)
@@ -189,7 +188,6 @@ class StageDiffusionProc:
request = OmniDiffusionRequest(
prompts=prompts,
sampling_params=sampling_params,
request_ids=[f"{request_id}-{i}" for i in range(len(prompts))],
request_id=request_id,
kv_sender_info=kv_sender_info,
)
@@ -322,50 +322,44 @@ class DiffusionModelRunner(OmniConnectorModelRunnerMixin):
self, scheduler_output: DiffusionSchedulerOutput
) -> tuple[list[DiffusionRequestState], list[str]]:
"""Step-before update: cleanup finished requests and get/create one running state."""
for req_id in scheduler_output.finished_req_ids:
self.state_cache.pop(req_id, None)
for request_id in scheduler_output.finished_req_ids:
self.state_cache.pop(request_id, None)
resolved: list[DiffusionRequestState] = []
new_req_id: list[str] = []
new_request_ids: list[str] = []
try:
# process new requests
for sched_new_req in scheduler_output.scheduled_new_reqs:
# new_req_data = scheduler_output.scheduled_new_reqs[0]
req_id = sched_new_req.sched_req_id
request_id = sched_new_req.request_id
req = sched_new_req.req
new_req_id.append(req_id)
if req_id in self.state_cache:
raise ValueError(f"Received duplicate new-request payload for cached request {req_id}.")
request_ids = req.request_ids or [req_id]
if len(request_ids) != len(req.prompts):
raise ValueError(
f"request_ids length ({len(request_ids)}) does not match prompts length ({len(req.prompts)})"
)
new_request_ids.append(request_id)
if request_id in self.state_cache:
raise ValueError(f"Received duplicate new-request payload for cached request {request_id}.")
new_state = DiffusionRequestState(
req_id=req_id,
request_id=request_id,
sampling=copy.deepcopy(req.sampling_params),
prompts=req.prompts,
)
self.state_cache[req_id] = new_state
self.state_cache[request_id] = new_state
resolved.append(new_state)
# process cached requests
for req_id in scheduler_output.scheduled_cached_reqs.sched_req_ids:
state = self.state_cache.get(req_id)
for request_id in scheduler_output.scheduled_cached_reqs.request_ids:
state = self.state_cache.get(request_id)
if state is None:
raise ValueError(f"Missing cached state for request {req_id}.")
raise ValueError(f"Missing cached state for request {request_id}.")
resolved.append(state)
except Exception:
for req_id in new_req_id:
self.state_cache.pop(req_id, None)
for request_id in new_request_ids:
self.state_cache.pop(request_id, None)
raise
return resolved, new_req_id
return resolved, new_request_ids
def _prepare_batch_inputs(self, states: list[DiffusionRequestState], new_request_ids: list[str]) -> InputBatch:
# process new reqs
for state in states:
if state.req_id in new_request_ids:
if state.request_id in new_request_ids:
# set generator
if state.sampling.generator is None and state.sampling.seed is not None:
if state.sampling.generator_device is not None:
@@ -407,7 +401,7 @@ class DiffusionModelRunner(OmniConnectorModelRunnerMixin):
for state in states:
if interrupted or state.denoise_completed:
self.state_cache.pop(state.req_id, None)
self.state_cache.pop(state.request_id, None)
def _prepare_attn_metadata(self, input_batch: InputBatch) -> Any:
model_state = getattr(self, "model_state", None)
@@ -449,7 +443,7 @@ class DiffusionModelRunner(OmniConnectorModelRunnerMixin):
for state in states:
runner_output_list.append(
RunnerOutput(
req_id=state.req_id,
request_id=state.request_id,
step_index=state.step_index,
finished=True,
result=DiffusionOutput(error="stepwise denoise interrupted"),
@@ -470,7 +464,7 @@ class DiffusionModelRunner(OmniConnectorModelRunnerMixin):
result = None
runner_output_list.append(
RunnerOutput(
req_id=req.req_id,
request_id=req.request_id,
step_index=req.step_index,
finished=req.denoise_completed,
result=result,
@@ -189,7 +189,7 @@ class DiffusionWorker:
self.lora_manager: DiffusionLoRAManager | None = None
# Worker-side cache of (lora_request, lora_scale) per scheduled
# request id. Used by step mode to recover LoRA identity for cached
# requests, which only carry their sched_req_id in subsequent ticks.
# requests, which only carry their request_id in subsequent ticks.
self._step_lora_state: dict[str, tuple[LoRARequest | None, float]] = {}
self.stage_id = getattr(od_config, "stage_id", 0)
self.init_device()
@@ -391,12 +391,12 @@ class DiffusionWorker:
homogeneity is enforced by the scheduler via ``SamplingParamsKey``,
so any scheduled request id resolves to the active LoRA identity.
"""
for sched_req_id in scheduler_output.finished_req_ids:
self._step_lora_state.pop(sched_req_id, None)
for request_id in scheduler_output.finished_req_ids:
self._step_lora_state.pop(request_id, None)
for new_req in scheduler_output.scheduled_new_reqs:
sampling = new_req.req.sampling_params
self._step_lora_state[new_req.sched_req_id] = (
self._step_lora_state[new_req.request_id] = (
sampling.lora_request,
sampling.lora_scale,
)
@@ -406,8 +406,8 @@ class DiffusionWorker:
lora_request: LoRARequest | None = None
lora_scale = 1.0
for sched_req_id in scheduler_output.scheduled_req_ids:
entry = self._step_lora_state.get(sched_req_id)
for request_id in scheduler_output.scheduled_request_ids:
entry = self._step_lora_state.get(request_id)
if entry is not None:
lora_request, lora_scale = entry
break
+23 -21
View File
@@ -81,8 +81,8 @@ def _select_states(
return selected_states, idx_mapping, idx_mapping.detach().cpu().numpy()
def _prepare_req_ids(states: Sequence[DiffusionRequestState]) -> list[str]:
return [state.req_id for state in states]
def _prepare_request_ids(states: Sequence[DiffusionRequestState]) -> list[str]:
return [state.request_id for state in states]
def _prepare_prompt_field_on_state(
@@ -192,11 +192,13 @@ def _get_request_prompt_seq_lens(
mask_attr=mask_attr,
)
if embeds is None:
raise ValueError(f"{embeds_attr} is not initialized on request {state.req_id}.")
raise ValueError(f"{embeds_attr} is not initialized on request {state.request_id}.")
if mask is not None:
if mask.shape[0] != embeds.shape[0]:
raise ValueError(f"{mask_attr} batch dimension does not match {embeds_attr} for request {state.req_id}.")
raise ValueError(
f"{mask_attr} batch dimension does not match {embeds_attr} for request {state.request_id}."
)
return _get_seq_lens_from_mask(mask)
seq_lens = getattr(state, seq_lens_attr)
@@ -220,7 +222,7 @@ def _prepare_request_prompt_field(
mask_attr=mask_attr,
)
if embeds is None:
raise ValueError(f"{embeds_attr} is not initialized on request {state.req_id}.")
raise ValueError(f"{embeds_attr} is not initialized on request {state.request_id}.")
actual_seq_lens = _get_request_prompt_seq_lens(
state,
@@ -248,7 +250,7 @@ def _prepare_request_prompt_field(
if current_seq_len > target_seq_len:
if max_actual_seq_len > target_seq_len:
raise ValueError(
f"{embeds_attr} for request {state.req_id} requires seq_len "
f"{embeds_attr} for request {state.request_id} requires seq_len "
f"{max_actual_seq_len}, got target {target_seq_len}."
)
return embeds[:, :target_seq_len], None if mask is None else mask[:, :target_seq_len]
@@ -349,7 +351,7 @@ def _require_state_latents(
) -> torch.Tensor:
latents = state.latents
if latents is None:
raise ValueError(f"Request {state.req_id} has no latents while preparing {for_field}.")
raise ValueError(f"Request {state.request_id} has no latents while preparing {for_field}.")
return latents
@@ -526,12 +528,12 @@ def _prepare_negative_prompt_embeds(
def _same_composition(
cached_batch: InputBatch | None,
req_ids: list[str],
request_ids: list[str],
idx_mapping_np: np.ndarray,
) -> bool:
if cached_batch is None:
return False
if cached_batch.req_ids != req_ids:
if cached_batch.request_ids != request_ids:
return False
return np.array_equal(cached_batch.idx_mapping_np, idx_mapping_np)
@@ -581,7 +583,7 @@ class InputBatch:
unchanged.
"""
req_ids: list[str]
request_ids: list[str]
num_reqs: int
num_reqs_after_padding: int
idx_mapping: torch.Tensor
@@ -604,9 +606,9 @@ class InputBatch:
negative_txt_seq_lens: list[int] | None = None
def __post_init__(self) -> None:
if len(self.req_ids) != int(self.idx_mapping.numel()):
raise ValueError("`req_ids` and `idx_mapping` must have the same length.")
if self.num_reqs != len(self.req_ids):
if len(self.request_ids) != int(self.idx_mapping.numel()):
raise ValueError("`request_ids` and `idx_mapping` must have the same length.")
if self.num_reqs != len(self.request_ids):
raise ValueError("`num_reqs` must match the number of request ids.")
if self.num_reqs_after_padding < self.num_reqs:
raise ValueError("`num_reqs_after_padding` must be >= `num_reqs`.")
@@ -653,11 +655,11 @@ class InputBatch:
selected_states: Sequence[DiffusionRequestState],
idx_mapping: torch.Tensor,
idx_mapping_np: np.ndarray,
req_ids: list[str],
request_ids: list[str],
) -> InputBatch:
self.req_ids = req_ids
self.num_reqs = len(req_ids)
self.num_reqs_after_padding = len(req_ids)
self.request_ids = request_ids
self.num_reqs = len(request_ids)
self.num_reqs_after_padding = len(request_ids)
self.idx_mapping = idx_mapping
self.idx_mapping_np = idx_mapping_np
self.latents = _prepare_latents(selected_states, out=self.latents)
@@ -675,9 +677,9 @@ class InputBatch:
) -> InputBatch:
"""Build a temporary step-local batch view from request states."""
selected_states, idx_mapping, idx_mapping_np = _select_states(states, idx_mapping)
req_ids = _prepare_req_ids(selected_states)
request_ids = _prepare_request_ids(selected_states)
if _same_composition(cached_batch, req_ids, idx_mapping_np):
if _same_composition(cached_batch, request_ids, idx_mapping_np):
assert cached_batch is not None
cached_batch._repack_dynamic_fields(selected_states)
return cached_batch
@@ -687,14 +689,14 @@ class InputBatch:
selected_states,
idx_mapping,
idx_mapping_np,
req_ids,
request_ids,
)
prompt_embeds, prompt_embeds_mask = _prepare_prompt_embeds(selected_states)
negative_prompt_embeds, negative_prompt_embeds_mask = _prepare_negative_prompt_embeds(selected_states)
do_true_cfg, true_cfg_scale, cfg_normalize = _prepare_cfg_scalars(selected_states)
return cls(
req_ids=req_ids,
request_ids=request_ids,
num_reqs=len(selected_states),
num_reqs_after_padding=len(selected_states),
idx_mapping=idx_mapping,
+12 -12
View File
@@ -38,7 +38,7 @@ class DiffusionRequestState:
"""
# ── Identity / request-level inputs ──
req_id: str
request_id: str
sampling: OmniDiffusionSamplingParams
prompts: list[OmniPromptType] | None = None
@@ -112,7 +112,7 @@ class DiffusionRequestState:
class BaseRunnerOutput(ABC):
@abstractmethod
def get_req_output(self, sched_req_id: str) -> RunnerOutput | None:
def get_request_output(self, request_id: str) -> RunnerOutput | None:
pass
@@ -125,13 +125,13 @@ class RunnerOutput(BaseRunnerOutput):
_request_state_cache.
"""
req_id: str
request_id: str
step_index: int | None = None
finished: bool = False
result: DiffusionOutput | None = None
def get_req_output(self, sched_req_id: str) -> RunnerOutput | None:
return self if self.req_id == sched_req_id else None
def get_request_output(self, request_id: str) -> RunnerOutput | None:
return self if self.request_id == request_id else None
@dataclass
@@ -140,18 +140,18 @@ class BatchRunnerOutput(BaseRunnerOutput):
_id_to_idx: dict[str, int] = field(init=False, repr=False)
def __post_init__(self) -> None:
self._id_to_idx = {out.req_id: i for i, out in enumerate(self.runner_outputs)}
self._id_to_idx = {out.request_id: i for i, out in enumerate(self.runner_outputs)}
def __getitem__(self, req_id: str) -> RunnerOutput | None:
"""access single RunnerOutput by req_id"""
idx = self._id_to_idx.get(req_id)
def __getitem__(self, request_id: str) -> RunnerOutput | None:
"""access single RunnerOutput by request_id"""
idx = self._id_to_idx.get(request_id)
return self.runner_outputs[idx] if idx is not None else None
def get_req_output(self, sched_req_id: str) -> RunnerOutput | None:
return self[sched_req_id]
def get_request_output(self, request_id: str) -> RunnerOutput | None:
return self[request_id]
@property
def req_ids(self) -> list[str]:
def request_ids(self) -> list[str]:
return list(self._id_to_idx.keys())
def __len__(self) -> int:
@@ -12,6 +12,8 @@ from typing import Any
import torch
from vllm.logger import init_logger
from vllm_omni.diffusion.request import OmniDiffusionRequest
from .factory import OmniConnectorFactory
from .utils.config import TRANSFER_ENGINE_CONNECTOR_NAMES, ConnectorSpec
from .utils.env import expand_env_int
@@ -1014,7 +1016,7 @@ class OmniKVTransferManager:
return None, 0
# Skip during warmup dummy run — no sender is available.
if request_id == "dummy_req_id":
if OmniDiffusionRequest.is_dummy_run_request_id(request_id):
logger.info("Skip receiving KV cache for dummy warmup request")
return None, 0
@@ -1171,12 +1173,7 @@ class OmniKVTransferManager:
@staticmethod
def _resolve_request_id(req: Any) -> str | None:
"""Resolve the logical request ID used for KV transfer lookups."""
request_id = getattr(req, "request_id", None)
if request_id:
return request_id
if hasattr(req, "request_ids") and req.request_ids:
return req.request_ids[0]
return None
return getattr(req, "request_id", None)
# Legacy compatibility method
def receive_kv_cache(self, req: Any, target_device: torch.device | None = None) -> bool:
@@ -4,7 +4,6 @@ import base64
import copy
import hashlib
import io
import os
from collections import OrderedDict
from collections.abc import Callable, Iterable, Mapping
from functools import lru_cache
@@ -16,14 +15,14 @@ import soundfile as sf
import torch
import torch.nn as nn
import torch.nn.functional as F
from transformers import AutoTokenizer
from transformers import AutoFeatureExtractor, AutoTokenizer
from transformers.activations import ACT2FN
from transformers.utils.hub import cached_file
from vllm.config import VllmConfig
from vllm.distributed import get_pp_group
from vllm.logger import init_logger
from vllm.model_executor.layers.logits_processor import LogitsProcessor
from vllm.model_executor.layers.vocab_parallel_embedding import ParallelLMHead
from vllm.model_executor.model_loader.default_loader import DefaultModelLoader
from vllm.model_executor.models.qwen3 import Qwen3Model
from vllm.model_executor.models.utils import AutoWeightsLoader, PPMissingLayer, WeightsMapper, maybe_prefix
from vllm.multimodal.audio import AudioResampler
@@ -36,7 +35,8 @@ from vllm_omni.utils.speaker_cache import get_speaker_cache
from .configuration_qwen3_tts import Qwen3TTSConfig, Qwen3TTSSpeakerEncoderConfig, Qwen3TTSTalkerConfig
from .qwen3_tts_code_predictor_vllm import Qwen3TTSTalkerCodePredictorForConditionalGenerationVLLM
from .qwen3_tts_tokenizer import Qwen3TTSTokenizer
from .tokenizer_12hz.configuration_qwen3_tts_tokenizer_v2 import Qwen3TTSTokenizerV2Config
from .tokenizer_12hz.modeling_qwen3_tts_tokenizer_v2 import Qwen3TTSTokenizerV2Encoder
logger = init_logger(__name__)
@@ -459,7 +459,23 @@ class Qwen3TTSTalkerForConditionalGeneration(nn.Module):
# Tokenizer for prompt building.
self._tokenizer = None
self._speech_tokenizer: Qwen3TTSTokenizer | None = None
tokenizer_config = Qwen3TTSTokenizerV2Config.from_pretrained(
self.model_path,
subfolder="speech_tokenizer",
)
self.encoder = Qwen3TTSTokenizerV2Encoder._from_config(
tokenizer_config.encoder_config,
)
self.encoder.eval()
self.encoder.to(dtype=torch.bfloat16)
self._encoder_valid_num_quantizers = int(tokenizer_config.encoder_valid_num_quantizers)
self._encoder_downsample_rate = int(tokenizer_config.encode_downsample_rate)
self._encoder_feature_extractor = AutoFeatureExtractor.from_pretrained(
self.model_path,
subfolder="speech_tokenizer",
)
self._speaker_cache = get_speaker_cache()
self._ref_audio_artifact_cache_max_entries = 256
@@ -1393,53 +1409,35 @@ class Qwen3TTSTalkerForConditionalGeneration(nn.Module):
cache[key] = cached
return cached
def _ensure_speech_tokenizer_loaded(self) -> Qwen3TTSTokenizer:
if self._speech_tokenizer is not None:
return self._speech_tokenizer
speech_tokenizer_path = cached_file(self.model_path, "speech_tokenizer/config.json")
if speech_tokenizer_path is None:
raise ValueError(f"{self.model_path}/speech_tokenizer/config.json not found")
# Ensure the HF feature extractor config is present. Transformers'
# AutoFeatureExtractor does not proactively fetch this file.
preprocessor_config_path = cached_file(self.model_path, "speech_tokenizer/preprocessor_config.json")
if preprocessor_config_path is None:
raise ValueError(f"{self.model_path}/speech_tokenizer/preprocessor_config.json not found")
speech_tokenizer_dir = os.path.dirname(speech_tokenizer_path)
tok = Qwen3TTSTokenizer.from_pretrained(
speech_tokenizer_dir,
torch_dtype=torch.bfloat16,
)
# Only move encoder to GPU; the decoder is unused by Talker (which
# only calls tok.encode()) and would otherwise waste bf16 VRAM.
# NOTE: after this point the tokenizer instance is encode-only;
# calling tok.decode() will fail because tok.model.decoder is None.
dev = next(self.parameters()).device
if dev.type != "cpu":
try:
del tok.model.decoder
tok.model.decoder = None
tok.model.encoder.to(dev)
tok.device = dev
except Exception as e:
raise RuntimeError(f"Failed to move speech tokenizer encoder to {dev}: {e}") from e
else:
tok.device = dev
self._speech_tokenizer = tok
return tok
def _encode_ref_audio_batch(self, wavs: list[np.ndarray], sr: int, *, device: torch.device) -> list[torch.Tensor]:
fe = self._encoder_feature_extractor
target_sr = int(fe.sampling_rate)
if int(sr) != target_sr:
resampler = AudioResampler(target_sr=target_sr)
wavs = [resampler.resample(w.astype(np.float32), orig_sr=int(sr)) for w in wavs]
def _encode_ref_audio_to_code(self, wav: np.ndarray, sr: int) -> torch.Tensor:
tok = self._ensure_speech_tokenizer_loaded()
enc = tok.encode(wav, sr=int(sr), return_dict=True)
ref_code = getattr(enc, "audio_codes", None)
if isinstance(ref_code, list):
ref_code = ref_code[0] if ref_code else None
if isinstance(ref_code, torch.Tensor):
# 12Hz: likely [T, Q] or [B, T, Q]
if ref_code.ndim == 3:
ref_code = ref_code[0]
out = ref_code.to(device=next(self.parameters()).device, dtype=torch.long)
return out
raise ValueError("SpeechTokenizer.encode did not return audio_codes tensor")
inputs = fe(
raw_audio=wavs,
sampling_rate=target_sr,
return_tensors="pt",
)
inputs = inputs.to(device).to(torch.bfloat16)
input_values = inputs["input_values"].squeeze(1)
padding_mask = inputs["padding_mask"].squeeze(1)
with torch.inference_mode():
encoded = self.encoder.encode(
input_values=input_values.unsqueeze(1),
return_dict=True,
)
audio_codes = encoded.audio_codes[:, : self._encoder_valid_num_quantizers]
downsample = self._encoder_downsample_rate
return [
code[..., : -(-mask.sum() // downsample)].transpose(0, 1).to(device=device, dtype=torch.long)
for code, mask in zip(audio_codes, padding_mask)
]
@staticmethod
def _make_ref_audio_cache_key(wav: np.ndarray, sr: int) -> str:
@@ -1525,18 +1523,6 @@ class Qwen3TTSTalkerForConditionalGeneration(nn.Module):
return None
return ids.to(device=device, dtype=torch.long).contiguous()
@staticmethod
def _split_ref_code_batch(ref_codes: object, expected: int) -> list[object] | None:
if isinstance(ref_codes, torch.Tensor):
if ref_codes.ndim == 3 and int(ref_codes.shape[0]) == expected:
return [ref_codes[i] for i in range(expected)]
if expected == 1:
return [ref_codes]
return None
if isinstance(ref_codes, (list, tuple)) and len(ref_codes) == expected:
return list(ref_codes)
return None
@staticmethod
def _voice_clone_prompt_dict(raw: object) -> dict[str, object] | None:
raw = Qwen3TTSTalkerForConditionalGeneration._first_value(raw)
@@ -1663,17 +1649,13 @@ class Qwen3TTSTalkerForConditionalGeneration(nn.Module):
if not groups:
return
tok = self._ensure_speech_tokenizer_loaded()
for sr, items in groups.items():
wavs = [wav for _, wav, _ in items]
try:
enc = tok.encode(wavs, sr=int(sr), return_dict=True)
ref_codes = self._split_ref_code_batch(getattr(enc, "audio_codes", None), len(items))
ref_codes = self._encode_ref_audio_batch(wavs, int(sr), device=device)
except Exception as exc:
logger.debug("Qwen3-TTS batched ref_code encode failed; falling back to serial path: %s", exc)
continue
if ref_codes is None:
continue
for (info_dict, wav, item_sr), ref_code in zip(items, ref_codes, strict=True):
ref_code_t = self._coerce_ref_code_tensor(ref_code, device=device)
if ref_code_t is None:
@@ -1982,7 +1964,7 @@ class Qwen3TTSTalkerForConditionalGeneration(nn.Module):
ref_code_len = int(ref_code_t.shape[0])
else:
wav_np, sr = _get_ref_audio()
ref_code_t = self._encode_ref_audio_to_code(wav_np, sr).to(device=input_ids.device)
ref_code_t = self._encode_ref_audio_batch([wav_np], int(sr), device=input_ids.device)[0]
ref_code_len = int(ref_code_t.shape[0])
self._put_ref_audio_artifacts(cache_key, ref_code=ref_code_t)
if isinstance(ref_code_t, torch.Tensor):
@@ -2240,6 +2222,43 @@ class Qwen3TTSTalkerForConditionalGeneration(nn.Module):
# Some checkpoints do not include speaker_encoder weights; keep the
# eagerly initialized module and satisfy the strict loader check.
loaded |= {name for name, _ in self.named_parameters() if name.startswith("speaker_encoder.")}
# Load speech tokenizer encoder weights from speech_tokenizer/
# subfolder. Skip decoder weights — the Talker only uses the
# encoder for ref_audio encoding.
model_loader = DefaultModelLoader(self.vllm_config.load_config)
source = DefaultModelLoader.Source(
model_or_path=self.model_path,
revision=self.vllm_config.model_config.revision,
subfolder="speech_tokenizer",
)
subfolder_weights = model_loader._get_weights_iterator(source)
enc_loaded = AutoWeightsLoader(
self,
skip_prefixes=["decoder."],
).load_weights(subfolder_weights)
loaded |= enc_loaded
# AutoWeightsLoader only loads parameters; the encoder's VQ
# codebook state (embed, embed_sum, cluster_usage, initialized)
# are registered as buffers. Load them from the checkpoint
# directly so the quantizer produces correct codes.
encoder_buffers = dict(self.encoder.named_buffers())
source2 = DefaultModelLoader.Source(
model_or_path=self.model_path,
revision=self.vllm_config.model_config.revision,
subfolder="speech_tokenizer",
)
for name, tensor in model_loader._get_weights_iterator(source2):
if not name.startswith("encoder."):
continue
buf_name = name[len("encoder.") :]
if buf_name in encoder_buffers:
encoder_buffers[buf_name].copy_(tensor)
loaded.add(name)
device = self.vllm_config.device_config.device
self.encoder.to(device=device, dtype=torch.bfloat16)
logger.info("Loaded %d weights for Qwen3TTSTalkerForConditionalGeneration", len(loaded))
self._build_stacked_codec_embed()
return loaded
@@ -1245,9 +1245,7 @@ class OmniConnectorModelRunnerMixin:
if self._kv_transfer_manager is None:
return False
request_id = getattr(req, "request_id", None) or (
req.request_ids[0] if hasattr(req, "request_ids") and req.request_ids else None
)
request_id = getattr(req, "request_id", None)
if not request_id:
logger.warning("Request has no ID, cannot receive KV cache")
return False