Skip to content

Commit 61230d7

Browse files
committed
Cosmos3 Distilled support
1 parent 0196914 commit 61230d7

5 files changed

Lines changed: 188 additions & 20 deletions

File tree

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

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -934,6 +934,73 @@ if outputs["action"] is not None:
934934
json.dump(outputs["action"][0].tolist(), f)
935935
```
936936

937+
### Distilled (few-step) text-to-image and image-to-video
938+
939+
Few-step distilled checkpoints ship a fixed sigma schedule in
940+
`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.
943+
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.
948+
949+
Prompts follow the same descriptive JSON structure as the non-distilled models, so short text
950+
must be upsampled first — use `--mode text2image` (T2I) or `--mode image2video` (I2V) as
951+
described in [Prompt upsampling](#prompt-upsampling), then pass the JSON via `json.dumps(...)`.
952+
953+
```python
954+
import json
955+
import torch
956+
from diffusers import Cosmos3OmniModularPipeline
957+
from diffusers.utils import export_to_video, load_image
958+
959+
# JSON-upsampled prompt (see "Prompt upsampling" above).
960+
json_prompt = json.load(open("assets/example_t2i_prompt.json"))
961+
962+
repo = "nvidia/Cosmos3-Super-Text2Image-4Step"
963+
pipe = Cosmos3OmniModularPipeline.from_pretrained(repo, torch_dtype=torch.bfloat16)
964+
pipe.load_components(torch_dtype=torch.bfloat16)
965+
pipe.to("cuda")
966+
967+
# text-to-image (distilled)
968+
videos = pipe(
969+
prompt=json.dumps(json_prompt),
970+
num_frames=1,
971+
height=720,
972+
width=1280,
973+
output="videos",
974+
)
975+
videos[0].save("cosmos3_distilled_t2i.jpg", format="JPEG", quality=85)
976+
977+
# image-to-video (distilled) — load the I2V repo instead
978+
# JSON-upsampled prompt (see "Prompt upsampling" above); upsampled from the source prompt
979+
# "The right robotic hand picks up the red sphere on the shelf."
980+
json_prompt_i2v = json.load(open("assets/example_i2v_prompt.json"))
981+
982+
repo_i2v = "nvidia/Cosmos3-Super-Image2Video-4Step"
983+
pipe = Cosmos3OmniModularPipeline.from_pretrained(repo_i2v, torch_dtype=torch.bfloat16)
984+
pipe.load_components(torch_dtype=torch.bfloat16)
985+
pipe.to("cuda")
986+
987+
image = load_image(
988+
"https://github.com/nvidia-cosmos/cosmos-dependencies/raw/refs/heads/assets/cosmos3/inputs/vision/robot_153.jpg"
989+
)
990+
videos = pipe(
991+
prompt=json.dumps(json_prompt_i2v),
992+
image=image,
993+
num_frames=189,
994+
height=720,
995+
width=1280,
996+
output="videos",
997+
)
998+
export_to_video(videos, "cosmos3_distilled_i2v.mp4", fps=24, macro_block_size=1)
999+
```
1000+
1001+
For a CLI wrapper with warmup / iteration timing, see
1002+
[`examples/cosmos3/inference_cosmos3_modular_distilled.py`](../../../examples/cosmos3/inference_cosmos3_modular_distilled.py).
1003+
9371004
[[autodoc]] Cosmos3OmniModularPipeline
9381005

9391006
- all

src/diffusers/modular_pipelines/cosmos/before_denoise.py

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
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 UniPCMultistepScheduler
7+
from ...schedulers import SchedulerMixin
88
from ...utils.torch_utils import randn_tensor
99
from ..modular_pipeline import ModularPipelineBlocks, PipelineState
1010
from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
@@ -115,6 +115,11 @@ def intermediate_outputs(self) -> list[OutputParam]:
115115
type_hint=list[int],
116116
description="Indexes of conditioned vision latent frames.",
117117
),
118+
OutputParam(
119+
"vision_conditioning_latents",
120+
type_hint=torch.Tensor,
121+
description="Clean encoded vision latents used to re-anchor image conditioning each step.",
122+
),
118123
]
119124

120125
@torch.no_grad()
@@ -165,6 +170,7 @@ def __call__(self, components: Cosmos3OmniModularPipeline, state: PipelineState)
165170
block_state.vision_condition_mask[:, 0, 0] > 0, as_tuple=False
166171
).flatten()
167172
block_state.vision_condition_indexes_for_pack = [int(idx.item()) for idx in vision_condition_indexes]
173+
block_state.vision_conditioning_latents = x0_tokens_vision
168174

169175
self.set_block_state(state, block_state)
170176
return components, state
@@ -181,7 +187,7 @@ def description(self) -> str:
181187
def expected_components(self) -> list[ComponentSpec]:
182188
return [
183189
ComponentSpec("transformer", Cosmos3OmniTransformer),
184-
ComponentSpec("scheduler", UniPCMultistepScheduler),
190+
ComponentSpec("scheduler", SchedulerMixin),
185191
]
186192

187193
@property
@@ -257,7 +263,7 @@ def description(self) -> str:
257263
def expected_components(self) -> list[ComponentSpec]:
258264
return [
259265
ComponentSpec("transformer", Cosmos3OmniTransformer),
260-
ComponentSpec("scheduler", UniPCMultistepScheduler),
266+
ComponentSpec("scheduler", SchedulerMixin),
261267
]
262268

263269
@property
@@ -934,29 +940,54 @@ def description(self) -> str:
934940

935941
@property
936942
def expected_components(self) -> list[ComponentSpec]:
937-
return [ComponentSpec("scheduler", UniPCMultistepScheduler)]
943+
return [ComponentSpec("scheduler", SchedulerMixin)]
938944

939945
@property
940946
def inputs(self) -> list[InputParam]:
941947
return [
942-
InputParam.template("num_inference_steps", required=True),
948+
InputParam.template("num_inference_steps", required=False),
949+
InputParam(
950+
name="guidance_scale",
951+
type_hint=float,
952+
default=None,
953+
description="Classifier-free guidance scale. Forced to 1.0 for distilled checkpoints.",
954+
),
943955
]
944956

945957
@property
946958
def intermediate_outputs(self) -> list[OutputParam]:
947959
return [
948960
OutputParam("timesteps", type_hint=torch.Tensor, description="Scheduler timesteps for denoising."),
949961
OutputParam("num_warmup_steps", type_hint=int, description="Number of scheduler warmup steps."),
962+
OutputParam(
963+
"num_inference_steps",
964+
type_hint=int,
965+
description="Resolved number of denoising steps.",
966+
),
967+
OutputParam(
968+
name="guidance_scale",
969+
type_hint=float,
970+
description="Resolved classifier-free guidance scale for the denoising loop.",
971+
),
950972
]
951973

952974
@torch.no_grad()
953975
def __call__(self, components: Cosmos3OmniModularPipeline, state: PipelineState) -> PipelineState:
954976
block_state = self.get_block_state(state)
955977
device = components._execution_device
956-
components.scheduler.set_timesteps(block_state.num_inference_steps, device=device)
957-
block_state.timesteps = components.scheduler.timesteps
958-
block_state.num_warmup_steps = (
959-
len(block_state.timesteps) - block_state.num_inference_steps * components.scheduler.order
978+
num_inference_steps, guidance_scale = components.resolve_inference_schedule(
979+
block_state.num_inference_steps,
980+
block_state.guidance_scale,
960981
)
982+
block_state.num_inference_steps = num_inference_steps
983+
block_state.guidance_scale = guidance_scale
984+
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)
989+
990+
block_state.timesteps = components.scheduler.timesteps
991+
block_state.num_warmup_steps = len(block_state.timesteps) - num_inference_steps * components.scheduler.order
961992
self.set_block_state(state, block_state)
962993
return components, state

src/diffusers/modular_pipelines/cosmos/denoise.py

Lines changed: 36 additions & 5 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 UniPCMultistepScheduler
6+
from ...schedulers import SchedulerMixin
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,7 @@ def description(self) -> str:
254254

255255
@property
256256
def expected_components(self) -> list[ComponentSpec]:
257-
return [ComponentSpec("scheduler", UniPCMultistepScheduler)]
257+
return [ComponentSpec("scheduler", SchedulerMixin)]
258258

259259
@property
260260
def inputs(self) -> list[InputParam]:
@@ -263,6 +263,24 @@ def inputs(self) -> list[InputParam]:
263263
InputParam(
264264
name="velocity_vision", type_hint=torch.Tensor, required=True, description="Predicted vision velocity."
265265
),
266+
InputParam(
267+
name="vision_condition_mask",
268+
type_hint=torch.Tensor,
269+
required=True,
270+
description="Mask marking conditioned vision latent frames.",
271+
),
272+
InputParam(
273+
name="vision_conditioning_latents",
274+
type_hint=torch.Tensor,
275+
default=None,
276+
description="Clean encoded vision latents for re-anchoring conditioned frames.",
277+
),
278+
InputParam(
279+
name="vision_condition_indexes_for_pack",
280+
type_hint=list,
281+
default=None,
282+
description="Indexes of conditioned vision latent frames; non-empty for image-to-video.",
283+
),
266284
]
267285

268286
@property
@@ -274,6 +292,19 @@ def __call__(self, components: Cosmos3OmniModularPipeline, block_state: BlockSta
274292
block_state.latents = components.scheduler.step(
275293
block_state.velocity_vision.unsqueeze(0), t, block_state.latents.unsqueeze(0), return_dict=False
276294
)[0].squeeze(0)
295+
296+
# Distilled checkpoints use stochastic (SDE) scheduler steps that re-noise every position.
297+
# Re-anchor conditioned frames to the clean encoded reference after each step.
298+
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+
):
304+
mask = block_state.vision_condition_mask
305+
reference = block_state.vision_conditioning_latents.to(block_state.latents.dtype)
306+
block_state.latents = mask * reference + (1.0 - mask) * block_state.latents
307+
277308
return components, block_state
278309

279310

@@ -295,7 +326,7 @@ def inputs(self) -> list[InputParam]:
295326
),
296327
InputParam(
297328
name="sound_scheduler",
298-
type_hint=UniPCMultistepScheduler,
329+
type_hint=SchedulerMixin,
299330
required=True,
300331
description="Scheduler used to update sound latents.",
301332
),
@@ -334,7 +365,7 @@ def inputs(self) -> list[InputParam]:
334365
),
335366
InputParam(
336367
name="action_scheduler",
337-
type_hint=UniPCMultistepScheduler,
368+
type_hint=SchedulerMixin,
338369
required=True,
339370
description="Scheduler used to update action latents.",
340371
),
@@ -381,7 +412,7 @@ def description(self) -> str:
381412
@property
382413
def loop_expected_components(self) -> list[ComponentSpec]:
383414
return [
384-
ComponentSpec("scheduler", UniPCMultistepScheduler),
415+
ComponentSpec("scheduler", SchedulerMixin),
385416
ComponentSpec("transformer", Cosmos3OmniTransformer),
386417
]
387418

src/diffusers/modular_pipelines/cosmos/modular_blocks_cosmos3.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ class Cosmos3VisionCoreDenoiseStep(SequentialPipelineBlocks):
242242
Runs the text-and-vision Cosmos3 denoising workflow.
243243
244244
Components:
245-
transformer (`Cosmos3OmniTransformer`) scheduler (`UniPCMultistepScheduler`)
245+
transformer (`Cosmos3OmniTransformer`) scheduler (`SchedulerMixin`)
246246
247247
Inputs:
248248
cond_input_ids (`None`):
@@ -310,7 +310,7 @@ class Cosmos3VisionSoundCoreDenoiseStep(SequentialPipelineBlocks):
310310
Runs the text, vision, and sound Cosmos3 denoising workflow.
311311
312312
Components:
313-
transformer (`Cosmos3OmniTransformer`) scheduler (`UniPCMultistepScheduler`)
313+
transformer (`Cosmos3OmniTransformer`) scheduler (`SchedulerMixin`)
314314
315315
Inputs:
316316
cond_input_ids (`None`):
@@ -391,7 +391,7 @@ class Cosmos3VisionActionCoreDenoiseStep(SequentialPipelineBlocks):
391391
Runs the text, vision, and action Cosmos3 denoising workflow.
392392
393393
Components:
394-
transformer (`Cosmos3OmniTransformer`) scheduler (`UniPCMultistepScheduler`)
394+
transformer (`Cosmos3OmniTransformer`) scheduler (`SchedulerMixin`)
395395
396396
Inputs:
397397
cond_input_ids (`None`):
@@ -476,7 +476,7 @@ class Cosmos3VisionSoundActionCoreDenoiseStep(SequentialPipelineBlocks):
476476
Runs the text, vision, sound, and action Cosmos3 denoising workflow.
477477
478478
Components:
479-
transformer (`Cosmos3OmniTransformer`) scheduler (`UniPCMultistepScheduler`)
479+
transformer (`Cosmos3OmniTransformer`) scheduler (`SchedulerMixin`)
480480
481481
Inputs:
482482
cond_input_ids (`None`):
@@ -576,7 +576,7 @@ class Cosmos3AutoCoreDenoiseStep(ConditionalPipelineBlocks):
576576
- vision runs otherwise.
577577
578578
Components:
579-
transformer (`Cosmos3OmniTransformer`) scheduler (`UniPCMultistepScheduler`)
579+
transformer (`Cosmos3OmniTransformer`) scheduler (`SchedulerMixin`)
580580
581581
Inputs:
582582
cond_input_ids (`None`):
@@ -690,7 +690,7 @@ class Cosmos3OmniBlocks(SequentialPipelineBlocks):
690690
691691
Components:
692692
text_tokenizer (`AutoTokenizer`) video_processor (`VideoProcessor`) vae (`AutoencoderKLWan`) transformer
693-
(`Cosmos3OmniTransformer`) scheduler (`UniPCMultistepScheduler`) sound_tokenizer
693+
(`Cosmos3OmniTransformer`) scheduler (`SchedulerMixin`) sound_tokenizer
694694
(`Cosmos3AVAEAudioTokenizer`)
695695
696696
Inputs:

src/diffusers/modular_pipelines/cosmos/modular_pipeline.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,45 @@ def disable_safety_checker(self):
7979
def requires_safety_checker(self):
8080
return getattr(self, "_is_safety_checker_enabled", True)
8181

82+
@property
83+
def is_distilled_checkpoint(self) -> bool:
84+
"""True when the loaded scheduler ships a fixed-step distilled sampler config."""
85+
scheduler = getattr(self, "scheduler", None)
86+
if scheduler is None:
87+
return False
88+
fixed_step_cfg = scheduler.config.get("fixed_step_sampler_config", None)
89+
requires_explicit_sigmas = bool(scheduler.config.get("fixed_step_requires_explicit_sigmas", False))
90+
return bool(fixed_step_cfg is not None and requires_explicit_sigmas and fixed_step_cfg.get("t_list"))
91+
92+
@property
93+
def distilled_sigmas(self) -> list[float]:
94+
"""Fixed sigma schedule for distilled checkpoints (`fixed_step_sampler_config.t_list`)."""
95+
if not self.is_distilled_checkpoint:
96+
raise ValueError("distilled_sigmas is only defined for distilled checkpoints.")
97+
fixed_step_cfg = self.scheduler.config["fixed_step_sampler_config"]
98+
return [float(s) for s in fixed_step_cfg["t_list"]]
99+
100+
def resolve_inference_schedule(
101+
self,
102+
num_inference_steps: int | None,
103+
guidance_scale: float | None,
104+
) -> tuple[int, float]:
105+
"""Resolve step count and guidance for base vs distilled checkpoints."""
106+
if self.is_distilled_checkpoint:
107+
distilled_steps = len(self.distilled_sigmas)
108+
if guidance_scale is not None and guidance_scale != 1.0:
109+
raise ValueError(
110+
"This is a distilled checkpoint; classifier-free guidance is baked into the weights. "
111+
f"`guidance_scale` must be 1.0 or left unset (got {guidance_scale})."
112+
)
113+
return distilled_steps, 1.0
114+
115+
if guidance_scale is None:
116+
guidance_scale = 6.0
117+
if num_inference_steps is None:
118+
num_inference_steps = 35
119+
return num_inference_steps, guidance_scale
120+
82121
def _encode_video(self, x):
83122
return Cosmos3OmniPipeline._encode_video(self, x)
84123

0 commit comments

Comments
 (0)