-
Notifications
You must be signed in to change notification settings - Fork 1.4k
[Model] feat: FastGen DMD2-distilled Wan 2.1 pipelines (T2V, I2V) #2749
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
hsliuustc0106
merged 8 commits into
vllm-project:main
from
ayushag-nv:ayushag/fastgen-wan-i2v
Apr 20, 2026
Merged
Changes from 5 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
76b8571
chore: t2v pipeline for wan2.1 dmd2p
ayushag-nv 3914181
chore: i2v pipeline for wan 2.1 dmd2p
ayushag-nv 73c88df
chore: added unit tests
ayushag-nv 3176727
chore: mixin based architecture + fixes
ayushag-nv 11bd9a4
chore: merge upstream/main and resolve conflicts
ayushag-nv a7fa2b3
chore: unified extensible structure
ayushag-nv 45aea7c
chore: util cleanup
ayushag-nv 1a970c8
chore: ltx2 add
ayushag-nv File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Empty file.
173 changes: 173 additions & 0 deletions
173
tests/diffusion/models/wan2_2/test_wan_dmd2_request_sanitization.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,173 @@ | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # SPDX-FileCopyrightText: Copyright contributors to the vLLM project | ||
| from unittest.mock import MagicMock, patch | ||
|
|
||
| import pytest | ||
| import torch | ||
|
|
||
| from vllm_omni.diffusion.models.wan2_2.pipeline_wan2_2 import Wan22Pipeline, WanT2VDMD2Pipeline | ||
| from vllm_omni.diffusion.models.wan2_2.pipeline_wan2_2_i2v import Wan22I2VPipeline, WanI2VDMD2Pipeline | ||
| from vllm_omni.diffusion.request import OmniDiffusionRequest, OmniDiffusionSamplingParams | ||
|
|
||
| pytestmark = [pytest.mark.core_model, pytest.mark.cpu] | ||
|
|
||
| # Wan base pipeline whose __init__ loads model weights — mocked in tests. | ||
| _WAN_BASE = { | ||
| WanT2VDMD2Pipeline: Wan22Pipeline, | ||
| WanI2VDMD2Pipeline: Wan22I2VPipeline, | ||
| } | ||
|
|
||
|
|
||
| def _make_pipeline(cls): | ||
| """Run the DMD2 __init__ with the Wan base mocked out (no model weights loaded).""" | ||
|
|
||
| base = _WAN_BASE[cls] | ||
| od_config = MagicMock() | ||
| od_config.model = "/nonexistent" | ||
|
|
||
| def _mock_base_init(self, *a, **kw): | ||
| self.od_config = od_config | ||
|
|
||
| with patch.object(base, "__init__", _mock_base_init): | ||
| pipeline = object.__new__(cls) | ||
| torch.nn.Module.__init__(pipeline) | ||
| cls.__init__(pipeline, od_config=od_config) | ||
| return pipeline | ||
|
|
||
|
|
||
| def _make_request(prompts=None, **sp_kwargs) -> OmniDiffusionRequest: | ||
| sp = OmniDiffusionSamplingParams(**sp_kwargs) | ||
| return OmniDiffusionRequest( | ||
| prompts=prompts or [{"prompt": "a cat dancing"}], | ||
| sampling_params=sp, | ||
| ) | ||
|
|
||
|
|
||
| @pytest.fixture(params=[WanT2VDMD2Pipeline, WanI2VDMD2Pipeline], ids=["t2v", "i2v"]) | ||
| def pipeline(request): | ||
| return _make_pipeline(request.param) | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # num_inference_steps | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
|
|
||
| def test_num_inference_steps_forced_to_dmd2_value(pipeline): | ||
| req = _make_request(num_inference_steps=40) | ||
| pipeline._sanitize_dmd2_request(req) | ||
| assert req.sampling_params.num_inference_steps == pipeline.num_inference_steps | ||
|
|
||
|
|
||
| def test_num_inference_steps_already_correct(pipeline): | ||
| req = _make_request(num_inference_steps=pipeline.num_inference_steps) | ||
| pipeline._sanitize_dmd2_request(req) | ||
| assert req.sampling_params.num_inference_steps == pipeline.num_inference_steps | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # guidance_scale | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
|
|
||
| def test_guidance_scale_forced_to_one(pipeline): | ||
| req = _make_request(guidance_scale=5.0, guidance_scale_provided=True) | ||
| pipeline._sanitize_dmd2_request(req) | ||
| assert req.sampling_params.guidance_scale == pipeline.dmd2_guidance_scale | ||
| assert req.sampling_params.guidance_scale_provided is False | ||
|
|
||
|
|
||
| def test_guidance_scale_already_correct(pipeline): | ||
| req = _make_request(guidance_scale=pipeline.dmd2_guidance_scale, guidance_scale_provided=False) | ||
| pipeline._sanitize_dmd2_request(req) | ||
| assert req.sampling_params.guidance_scale == pipeline.dmd2_guidance_scale | ||
|
|
||
|
|
||
| def test_guidance_scale_provided_flag_cleared(pipeline): | ||
| """guidance_scale_provided=True must be cleared even if scale is already dmd2_guidance_scale.""" | ||
| req = _make_request(guidance_scale=pipeline.dmd2_guidance_scale, guidance_scale_provided=True) | ||
| pipeline._sanitize_dmd2_request(req) | ||
| assert req.sampling_params.guidance_scale_provided is False | ||
|
|
||
|
|
||
| def test_guidance_scale_2_cleared(pipeline): | ||
| req = _make_request(guidance_scale_2=3.0) | ||
| pipeline._sanitize_dmd2_request(req) | ||
| assert req.sampling_params.guidance_scale_2 is None | ||
|
|
||
|
|
||
| def test_guidance_scale_2_unset_unchanged(pipeline): | ||
| req = _make_request() | ||
| pipeline._sanitize_dmd2_request(req) | ||
| assert req.sampling_params.guidance_scale_2 is None | ||
|
|
||
|
|
||
| def test_true_cfg_scale_cleared(pipeline): | ||
| req = _make_request(true_cfg_scale=2.0) | ||
| pipeline._sanitize_dmd2_request(req) | ||
| assert req.sampling_params.true_cfg_scale is None | ||
|
|
||
|
|
||
| def test_do_classifier_free_guidance_forced_false(pipeline): | ||
| req = _make_request(do_classifier_free_guidance=True) | ||
| pipeline._sanitize_dmd2_request(req) | ||
| assert req.sampling_params.do_classifier_free_guidance is False | ||
|
|
||
|
|
||
| def test_is_cfg_negative_forced_false(pipeline): | ||
| req = _make_request(is_cfg_negative=True) | ||
| pipeline._sanitize_dmd2_request(req) | ||
| assert req.sampling_params.is_cfg_negative is False | ||
|
|
||
|
|
||
| def test_negative_prompt_stripped_from_prompt_dict(pipeline): | ||
| req = _make_request(prompts=[{"prompt": "a cat", "negative_prompt": "blurry"}]) | ||
| pipeline._sanitize_dmd2_request(req) | ||
| assert "negative_prompt" not in req.prompts[0] | ||
| assert req.prompts[0]["prompt"] == "a cat" | ||
|
|
||
|
|
||
| def test_no_negative_prompt_unchanged(pipeline): | ||
| req = _make_request(prompts=[{"prompt": "a cat"}]) | ||
| pipeline._sanitize_dmd2_request(req) | ||
| assert req.prompts[0] == {"prompt": "a cat"} | ||
|
|
||
|
|
||
| def test_string_prompt_not_mutated(pipeline): | ||
| """String prompts (not dicts) must pass through unchanged.""" | ||
| req = _make_request(prompts=["a cat dancing"]) | ||
| pipeline._sanitize_dmd2_request(req) | ||
| assert req.prompts == ["a cat dancing"] | ||
|
|
||
|
|
||
| def test_multiple_prompts_all_sanitized(pipeline): | ||
| req = _make_request( | ||
| prompts=[ | ||
| {"prompt": "a cat", "negative_prompt": "blurry"}, | ||
| {"prompt": "a dog", "negative_prompt": "ugly"}, | ||
| ] | ||
| ) | ||
| pipeline._sanitize_dmd2_request(req) | ||
| for p in req.prompts: | ||
| assert "negative_prompt" not in p | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Clean request — nothing changes | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
|
|
||
| def test_clean_request_no_changes(pipeline): | ||
| req = _make_request( | ||
| guidance_scale=pipeline.dmd2_guidance_scale, | ||
| guidance_scale_provided=False, | ||
| do_classifier_free_guidance=False, | ||
| is_cfg_negative=False, | ||
| ) | ||
| pipeline._sanitize_dmd2_request(req) | ||
| assert req.sampling_params.guidance_scale == pipeline.dmd2_guidance_scale | ||
| assert req.sampling_params.guidance_scale_provided is False | ||
| assert req.sampling_params.guidance_scale_2 is None | ||
| assert req.sampling_params.true_cfg_scale is None | ||
| assert req.sampling_params.do_classifier_free_guidance is False | ||
| assert req.sampling_params.is_cfg_negative is False |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # SPDX-FileCopyrightText: Copyright contributors to the vLLM project | ||
| from unittest.mock import MagicMock, patch | ||
|
|
||
| import pytest | ||
| import torch | ||
|
|
||
| from vllm_omni.diffusion.models.wan2_2.pipeline_wan2_2 import Wan22Pipeline, WanT2VDMD2Pipeline | ||
| from vllm_omni.diffusion.models.wan2_2.pipeline_wan2_2_i2v import Wan22I2VPipeline, WanI2VDMD2Pipeline | ||
| from vllm_omni.diffusion.request import OmniDiffusionRequest, OmniDiffusionSamplingParams | ||
|
|
||
| pytestmark = [pytest.mark.core_model, pytest.mark.cpu] | ||
|
|
||
| _DMD2_TIMESTEPS = [999, 937, 833, 624] | ||
|
|
||
| # Wan base pipeline whose __init__ loads model weights — mocked in tests. | ||
| _WAN_BASE = { | ||
| WanT2VDMD2Pipeline: Wan22Pipeline, | ||
| WanI2VDMD2Pipeline: Wan22I2VPipeline, | ||
| } | ||
|
|
||
|
|
||
| def _make_pipeline(cls): | ||
| """Run the DMD2 __init__ (including __init_dmd2__) with the Wan base mocked.""" | ||
|
|
||
| base = _WAN_BASE[cls] | ||
| od_config = MagicMock() | ||
| od_config.model = "/nonexistent" # _load_model_index returns {} → uses inline defaults | ||
|
|
||
| def _mock_base_init(self, *a, **kw): | ||
| self.od_config = od_config # __init_dmd2__ needs this | ||
|
|
||
| with patch.object(base, "__init__", _mock_base_init): | ||
| pipeline = object.__new__(cls) | ||
| torch.nn.Module.__init__(pipeline) | ||
| cls.__init__(pipeline, od_config=od_config) | ||
| return pipeline | ||
|
|
||
|
|
||
| def _make_request(**sp_kwargs) -> OmniDiffusionRequest: | ||
| sp = OmniDiffusionSamplingParams(**sp_kwargs) | ||
| return OmniDiffusionRequest(prompts=[{"prompt": "a cat"}], sampling_params=sp) | ||
|
|
||
|
|
||
| @pytest.fixture(params=[WanT2VDMD2Pipeline, WanI2VDMD2Pipeline], ids=["t2v", "i2v"]) | ||
| def pipeline(request): | ||
| return _make_pipeline(request.param) | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # forward() timestep injection | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
|
|
||
| def _fake_parent_forward(self, req, *args, num_inference_steps=40, **kwargs): | ||
| """Stub that calls set_timesteps as the real parent does.""" | ||
| self.scheduler.set_timesteps(num_inference_steps, device="cpu") | ||
| return MagicMock() | ||
|
|
||
|
|
||
| def test_forward_timesteps_match_dmd2_schedule(pipeline): | ||
| """After forward() runs, scheduler.timesteps must equal the DMD2 training schedule.""" | ||
| parent = _WAN_BASE[type(pipeline)] | ||
|
|
||
| # Baseline: calling set_timesteps(40) without the DMD2 override gives a different schedule | ||
| pipeline.scheduler.set_timesteps(40, device="cpu") | ||
| default_timesteps = pipeline.scheduler.timesteps.long().tolist() | ||
| assert default_timesteps == _DMD2_TIMESTEPS, ( | ||
| "DMD2EulerScheduler should always return DMD2 timesteps regardless of num_steps" | ||
| ) | ||
|
|
||
| with patch.object(parent, "forward", _fake_parent_forward): | ||
| pipeline.forward(_make_request()) | ||
|
|
||
| assert pipeline.scheduler.timesteps.long().tolist() == _DMD2_TIMESTEPS | ||
|
|
||
|
|
||
| def test_forward_timesteps_fixed_across_num_steps(pipeline): | ||
| """scheduler.timesteps is always the DMD2 schedule regardless of num_steps passed.""" | ||
| parent = _WAN_BASE[type(pipeline)] | ||
|
|
||
| for num_steps in [1, 4, 10, 40, 100]: | ||
| with patch.object(parent, "forward", _fake_parent_forward): | ||
| pipeline.forward(_make_request()) | ||
|
|
||
| assert pipeline.scheduler.timesteps.long().tolist() == _DMD2_TIMESTEPS, ( | ||
| f"num_steps={num_steps}: got {pipeline.scheduler.timesteps.tolist()}" | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.