Skip to content

Commit 177ee1e

Browse files
committed
Cosmos3 distilled refactor for separate modualr pipeline
1 parent 586c8f3 commit 177ee1e

13 files changed

Lines changed: 609 additions & 98 deletions

File tree

docs/source/en/api/pipelines/cosmos3.md

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -938,13 +938,14 @@ if outputs["action"] is not None:
938938

939939
Few-step distilled checkpoints ship a fixed sigma schedule in
940940
`scheduler.config.fixed_step_sampler_config.t_list` and bake classifier-free guidance into
941-
the weights. **Only the modular pipeline supports them today** — the task-based
942-
[`Cosmos3OmniPipeline`] does not implement the distilled contract.
941+
the weights. They are served by the dedicated [`Cosmos3DistilledModularPipeline`] (blocks:
942+
`Cosmos3DistilledBlocks`) — the task-based [`Cosmos3OmniPipeline`] and the base
943+
[`Cosmos3OmniModularPipeline`] do not implement the distilled contract.
943944

944-
Load a distilled repo and call the modular pipeline **without** `num_inference_steps` or
945-
`guidance_scale`; `Cosmos3SetTimestepsStep` resolves both from the scheduler config
946-
(fixed step count, `guidance_scale=1.0`). Because classifier-free guidance is baked into the
947-
weights, negative prompts are unused.
945+
Load a distilled repo and call the pipeline **without** `num_inference_steps` or
946+
`guidance_scale`; `Cosmos3DistilledSetTimestepsStep` reads the fixed step count from the
947+
scheduler config and forces `guidance_scale=1.0`. Because classifier-free guidance is baked
948+
into the weights, `negative_prompt` is not supported (passing one raises an error).
948949

949950
Prompts follow the same descriptive JSON structure as the non-distilled models, so short text
950951
must be upsampled first — use `--mode text2image` (T2I) or `--mode image2video` (I2V) as
@@ -953,14 +954,14 @@ described in [Prompt upsampling](#prompt-upsampling), then pass the JSON via `js
953954
```python
954955
import json
955956
import torch
956-
from diffusers import Cosmos3OmniModularPipeline
957+
from diffusers import Cosmos3DistilledModularPipeline
957958
from diffusers.utils import export_to_video, load_image
958959

959960
# JSON-upsampled prompt (see "Prompt upsampling" above).
960961
json_prompt = json.load(open("assets/example_t2i_prompt.json"))
961962

962963
repo = "nvidia/Cosmos3-Super-Text2Image-4Step"
963-
pipe = Cosmos3OmniModularPipeline.from_pretrained(repo, torch_dtype=torch.bfloat16)
964+
pipe = Cosmos3DistilledModularPipeline.from_pretrained(repo, torch_dtype=torch.bfloat16)
964965
pipe.load_components(torch_dtype=torch.bfloat16)
965966
pipe.to("cuda")
966967

@@ -980,7 +981,7 @@ videos[0].save("cosmos3_distilled_t2i.jpg", format="JPEG", quality=85)
980981
json_prompt_i2v = json.load(open("assets/example_i2v_prompt.json"))
981982

982983
repo_i2v = "nvidia/Cosmos3-Super-Image2Video-4Step"
983-
pipe = Cosmos3OmniModularPipeline.from_pretrained(repo_i2v, torch_dtype=torch.bfloat16)
984+
pipe = Cosmos3DistilledModularPipeline.from_pretrained(repo_i2v, torch_dtype=torch.bfloat16)
984985
pipe.load_components(torch_dtype=torch.bfloat16)
985986
pipe.to("cuda")
986987

@@ -1006,6 +1007,13 @@ For a CLI wrapper with warmup / iteration timing, see
10061007
- all
10071008
- __call__
10081009

1010+
## Cosmos3DistilledModularPipeline
1011+
1012+
[[autodoc]] Cosmos3DistilledModularPipeline
1013+
1014+
- all
1015+
- __call__
1016+
10091017
## CosmosActionCondition
10101018

10111019
[[autodoc]] CosmosActionCondition

src/diffusers/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,8 @@
477477
[
478478
"AnimaAutoBlocks",
479479
"AnimaModularPipeline",
480+
"Cosmos3DistilledBlocks",
481+
"Cosmos3DistilledModularPipeline",
480482
"Cosmos3OmniBlocks",
481483
"Cosmos3OmniModularPipeline",
482484
"ErnieImageAutoBlocks",
@@ -1329,6 +1331,8 @@
13291331
from .modular_pipelines import (
13301332
AnimaAutoBlocks,
13311333
AnimaModularPipeline,
1334+
Cosmos3DistilledBlocks,
1335+
Cosmos3DistilledModularPipeline,
13321336
Cosmos3OmniBlocks,
13331337
Cosmos3OmniModularPipeline,
13341338
ErnieImageAutoBlocks,

src/diffusers/modular_pipelines/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@
9898
"AnimaModularPipeline",
9999
]
100100
_import_structure["cosmos"] = [
101+
"Cosmos3DistilledBlocks",
102+
"Cosmos3DistilledModularPipeline",
101103
"Cosmos3OmniBlocks",
102104
"Cosmos3OmniModularPipeline",
103105
]
@@ -128,7 +130,12 @@
128130
else:
129131
from .anima import AnimaAutoBlocks, AnimaModularPipeline
130132
from .components_manager import ComponentsManager
131-
from .cosmos import Cosmos3OmniBlocks, Cosmos3OmniModularPipeline
133+
from .cosmos import (
134+
Cosmos3DistilledBlocks,
135+
Cosmos3DistilledModularPipeline,
136+
Cosmos3OmniBlocks,
137+
Cosmos3OmniModularPipeline,
138+
)
132139
from .ernie_image import ErnieImageAutoBlocks, ErnieImageModularPipeline
133140
from .flux import FluxAutoBlocks, FluxKontextAutoBlocks, FluxKontextModularPipeline, FluxModularPipeline
134141
from .flux2 import (

src/diffusers/modular_pipelines/cosmos/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
_dummy_objects.update(get_objects_from_module(dummy_torch_and_transformers_objects))
2323
else:
2424
_import_structure["modular_blocks_cosmos3"] = ["Cosmos3OmniBlocks"]
25-
_import_structure["modular_pipeline"] = ["Cosmos3OmniModularPipeline"]
25+
_import_structure["modular_blocks_cosmos3_distilled"] = ["Cosmos3DistilledBlocks"]
26+
_import_structure["modular_pipeline"] = ["Cosmos3DistilledModularPipeline", "Cosmos3OmniModularPipeline"]
2627

2728
if TYPE_CHECKING or DIFFUSERS_SLOW_IMPORT:
2829
try:
@@ -32,7 +33,8 @@
3233
from ...utils.dummy_torch_and_transformers_objects import * # noqa F403
3334
else:
3435
from .modular_blocks_cosmos3 import Cosmos3OmniBlocks
35-
from .modular_pipeline import Cosmos3OmniModularPipeline
36+
from .modular_blocks_cosmos3_distilled import Cosmos3DistilledBlocks
37+
from .modular_pipeline import Cosmos3DistilledModularPipeline, Cosmos3OmniModularPipeline
3638
else:
3739
import sys
3840

src/diffusers/modular_pipelines/cosmos/before_denoise.py

Lines changed: 77 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44

55
from ...models.transformers.transformer_cosmos3 import Cosmos3OmniTransformer
66
from ...pipelines.cosmos.pipeline_cosmos3_omni import _EMBODIMENT_TO_DOMAIN_ID, CosmosActionCondition
7-
from ...schedulers import SchedulerMixin
7+
from ...schedulers import FlowMatchEulerDiscreteScheduler, UniPCMultistepScheduler
88
from ...utils.torch_utils import randn_tensor
99
from ..modular_pipeline import ModularPipelineBlocks, PipelineState
10-
from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
10+
from ..modular_pipeline_utils import ComponentSpec, ConfigSpec, InputParam, OutputParam
1111
from .modular_pipeline import Cosmos3OmniModularPipeline
1212

1313

@@ -187,7 +187,7 @@ def description(self) -> str:
187187
def expected_components(self) -> list[ComponentSpec]:
188188
return [
189189
ComponentSpec("transformer", Cosmos3OmniTransformer),
190-
ComponentSpec("scheduler", SchedulerMixin),
190+
ComponentSpec("scheduler", UniPCMultistepScheduler),
191191
]
192192

193193
@property
@@ -263,7 +263,7 @@ def description(self) -> str:
263263
def expected_components(self) -> list[ComponentSpec]:
264264
return [
265265
ComponentSpec("transformer", Cosmos3OmniTransformer),
266-
ComponentSpec("scheduler", SchedulerMixin),
266+
ComponentSpec("scheduler", UniPCMultistepScheduler),
267267
]
268268

269269
@property
@@ -940,7 +940,48 @@ def description(self) -> str:
940940

941941
@property
942942
def expected_components(self) -> list[ComponentSpec]:
943-
return [ComponentSpec("scheduler", SchedulerMixin)]
943+
return [ComponentSpec("scheduler", UniPCMultistepScheduler)]
944+
945+
@property
946+
def inputs(self) -> list[InputParam]:
947+
return [
948+
InputParam.template("num_inference_steps", required=True),
949+
]
950+
951+
@property
952+
def intermediate_outputs(self) -> list[OutputParam]:
953+
return [
954+
OutputParam("timesteps", type_hint=torch.Tensor, description="Scheduler timesteps for denoising."),
955+
OutputParam("num_warmup_steps", type_hint=int, description="Number of scheduler warmup steps."),
956+
]
957+
958+
@torch.no_grad()
959+
def __call__(self, components: Cosmos3OmniModularPipeline, state: PipelineState) -> PipelineState:
960+
block_state = self.get_block_state(state)
961+
device = components._execution_device
962+
components.scheduler.set_timesteps(block_state.num_inference_steps, device=device)
963+
block_state.timesteps = components.scheduler.timesteps
964+
block_state.num_warmup_steps = (
965+
len(block_state.timesteps) - block_state.num_inference_steps * components.scheduler.order
966+
)
967+
self.set_block_state(state, block_state)
968+
return components, state
969+
970+
971+
class Cosmos3DistilledSetTimestepsStep(ModularPipelineBlocks):
972+
model_name = "cosmos3-omni"
973+
974+
@property
975+
def description(self) -> str:
976+
return "Initializes the fixed distilled sampling schedule from the scheduler's `fixed_step_sampler_config`."
977+
978+
@property
979+
def expected_components(self) -> list[ComponentSpec]:
980+
return [ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler)]
981+
982+
@property
983+
def expected_configs(self) -> list[ConfigSpec]:
984+
return [ConfigSpec(name="is_distilled", default=True)]
944985

945986
@property
946987
def inputs(self) -> list[InputParam]:
@@ -950,7 +991,10 @@ def inputs(self) -> list[InputParam]:
950991
name="guidance_scale",
951992
type_hint=float,
952993
default=None,
953-
description="Classifier-free guidance scale. Forced to 1.0 for distilled checkpoints.",
994+
description=(
995+
"Unused for distilled checkpoints; classifier-free guidance is baked into the weights and the "
996+
"scale is forced to 1.0. Passing a value other than 1.0 raises an error."
997+
),
954998
),
955999
]
9561000

@@ -962,32 +1006,46 @@ def intermediate_outputs(self) -> list[OutputParam]:
9621006
OutputParam(
9631007
"num_inference_steps",
9641008
type_hint=int,
965-
description="Resolved number of denoising steps.",
1009+
description="Resolved number of denoising steps (fixed by the distilled schedule).",
9661010
),
9671011
OutputParam(
9681012
name="guidance_scale",
9691013
type_hint=float,
970-
description="Resolved classifier-free guidance scale for the denoising loop.",
1014+
description="Resolved classifier-free guidance scale (always 1.0 for distilled checkpoints).",
9711015
),
9721016
]
9731017

9741018
@torch.no_grad()
9751019
def __call__(self, components: Cosmos3OmniModularPipeline, state: PipelineState) -> PipelineState:
9761020
block_state = self.get_block_state(state)
9771021
device = components._execution_device
978-
num_inference_steps, guidance_scale = components.resolve_inference_schedule(
979-
block_state.num_inference_steps,
980-
block_state.guidance_scale,
981-
)
982-
block_state.num_inference_steps = num_inference_steps
983-
block_state.guidance_scale = guidance_scale
9841022

985-
if components.is_distilled_checkpoint:
986-
components.scheduler.set_timesteps(sigmas=components.distilled_sigmas, device=device)
987-
else:
988-
components.scheduler.set_timesteps(num_inference_steps, device=device)
1023+
fixed_step_cfg = components.scheduler.config.get("fixed_step_sampler_config", None)
1024+
if not fixed_step_cfg or not fixed_step_cfg.get("t_list"):
1025+
raise ValueError(
1026+
"Cosmos3DistilledSetTimestepsStep requires a scheduler that ships a distilled "
1027+
"`fixed_step_sampler_config.t_list`. Load a distilled Cosmos3 checkpoint or use "
1028+
"`Cosmos3OmniModularPipeline` for base checkpoints."
1029+
)
1030+
sigmas = [float(s) for s in fixed_step_cfg["t_list"]]
1031+
distilled_steps = len(sigmas)
1032+
1033+
if block_state.num_inference_steps is not None and block_state.num_inference_steps != distilled_steps:
1034+
raise ValueError(
1035+
"This is a distilled checkpoint; the step count is fixed by the scheduler's "
1036+
f"`fixed_step_sampler_config.t_list` ({distilled_steps} steps). "
1037+
f"`num_inference_steps` must be {distilled_steps} or left unset (got {block_state.num_inference_steps})."
1038+
)
1039+
if block_state.guidance_scale is not None and block_state.guidance_scale != 1.0:
1040+
raise ValueError(
1041+
"This is a distilled checkpoint; classifier-free guidance is baked into the weights. "
1042+
f"`guidance_scale` must be 1.0 or left unset (got {block_state.guidance_scale})."
1043+
)
9891044

1045+
components.scheduler.set_timesteps(sigmas=sigmas, device=device)
1046+
block_state.num_inference_steps = distilled_steps
1047+
block_state.guidance_scale = 1.0
9901048
block_state.timesteps = components.scheduler.timesteps
991-
block_state.num_warmup_steps = len(block_state.timesteps) - num_inference_steps * components.scheduler.order
1049+
block_state.num_warmup_steps = len(block_state.timesteps) - distilled_steps * components.scheduler.order
9921050
self.set_block_state(state, block_state)
9931051
return components, state

src/diffusers/modular_pipelines/cosmos/denoise.py

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import torch
44

55
from ...models.transformers.transformer_cosmos3 import Cosmos3OmniTransformer
6-
from ...schedulers import SchedulerMixin
6+
from ...schedulers import FlowMatchEulerDiscreteScheduler, UniPCMultistepScheduler
77
from ..modular_pipeline import BlockState, LoopSequentialPipelineBlocks, ModularPipelineBlocks, PipelineState
88
from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
99
from .modular_pipeline import Cosmos3OmniModularPipeline
@@ -254,7 +254,39 @@ def description(self) -> str:
254254

255255
@property
256256
def expected_components(self) -> list[ComponentSpec]:
257-
return [ComponentSpec("scheduler", SchedulerMixin)]
257+
return [ComponentSpec("scheduler", UniPCMultistepScheduler)]
258+
259+
@property
260+
def inputs(self) -> list[InputParam]:
261+
return [
262+
InputParam.template("latents", required=True, description="Noisy vision latents to update."),
263+
InputParam(
264+
name="velocity_vision", type_hint=torch.Tensor, required=True, description="Predicted vision velocity."
265+
),
266+
]
267+
268+
@property
269+
def intermediate_outputs(self) -> list[OutputParam]:
270+
return [OutputParam.template("latents")]
271+
272+
@torch.no_grad()
273+
def __call__(self, components: Cosmos3OmniModularPipeline, block_state: BlockState, i: int, t: torch.Tensor):
274+
block_state.latents = components.scheduler.step(
275+
block_state.velocity_vision.unsqueeze(0), t, block_state.latents.unsqueeze(0), return_dict=False
276+
)[0].squeeze(0)
277+
return components, block_state
278+
279+
280+
class Cosmos3DistilledVisionLoopSchedulerStep(ModularPipelineBlocks):
281+
model_name = "cosmos3-omni"
282+
283+
@property
284+
def description(self) -> str:
285+
return "Updates vision latents after one distilled denoising iteration, re-anchoring conditioned frames."
286+
287+
@property
288+
def expected_components(self) -> list[ComponentSpec]:
289+
return [ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler)]
258290

259291
@property
260292
def inputs(self) -> list[InputParam]:
@@ -296,11 +328,7 @@ def __call__(self, components: Cosmos3OmniModularPipeline, block_state: BlockSta
296328
# Distilled checkpoints use stochastic (SDE) scheduler steps that re-noise every position.
297329
# Re-anchor conditioned frames to the clean encoded reference after each step.
298330
has_image_condition = bool(block_state.vision_condition_indexes_for_pack)
299-
if (
300-
components.is_distilled_checkpoint
301-
and has_image_condition
302-
and block_state.vision_conditioning_latents is not None
303-
):
331+
if has_image_condition and block_state.vision_conditioning_latents is not None:
304332
mask = block_state.vision_condition_mask
305333
reference = block_state.vision_conditioning_latents.to(block_state.latents.dtype)
306334
block_state.latents = mask * reference + (1.0 - mask) * block_state.latents
@@ -326,7 +354,7 @@ def inputs(self) -> list[InputParam]:
326354
),
327355
InputParam(
328356
name="sound_scheduler",
329-
type_hint=SchedulerMixin,
357+
type_hint=UniPCMultistepScheduler,
330358
required=True,
331359
description="Scheduler used to update sound latents.",
332360
),
@@ -365,7 +393,7 @@ def inputs(self) -> list[InputParam]:
365393
),
366394
InputParam(
367395
name="action_scheduler",
368-
type_hint=SchedulerMixin,
396+
type_hint=UniPCMultistepScheduler,
369397
required=True,
370398
description="Scheduler used to update action latents.",
371399
),
@@ -412,7 +440,7 @@ def description(self) -> str:
412440
@property
413441
def loop_expected_components(self) -> list[ComponentSpec]:
414442
return [
415-
ComponentSpec("scheduler", SchedulerMixin),
443+
ComponentSpec("scheduler", UniPCMultistepScheduler),
416444
ComponentSpec("transformer", Cosmos3OmniTransformer),
417445
]
418446

@@ -453,6 +481,27 @@ def description(self) -> str:
453481
return "Runs the vision-only Cosmos3 denoising loop."
454482

455483

484+
class Cosmos3DistilledVisionDenoiseStep(Cosmos3DenoiseLoopWrapper):
485+
model_name = "cosmos3-omni"
486+
block_classes = [
487+
Cosmos3VisionLoopPrepareStep,
488+
Cosmos3LoopDenoiser,
489+
Cosmos3DistilledVisionLoopSchedulerStep,
490+
]
491+
block_names = ["prepare_vision", "denoiser", "update_vision"]
492+
493+
@property
494+
def description(self) -> str:
495+
return "Runs the vision-only distilled Cosmos3 denoising loop."
496+
497+
@property
498+
def loop_expected_components(self) -> list[ComponentSpec]:
499+
return [
500+
ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler),
501+
ComponentSpec("transformer", Cosmos3OmniTransformer),
502+
]
503+
504+
456505
class Cosmos3VisionSoundDenoiseStep(Cosmos3DenoiseLoopWrapper):
457506
block_classes = [
458507
Cosmos3VisionLoopPrepareStep,

0 commit comments

Comments
 (0)