Skip to content

Commit 8eac6ad

Browse files
Cosmos3 Distilled support (#14177)
* Cosmos3 distilled support (dedicated modular pipeline + blocks) * Address review comments * Refactor unit test to use ModularPipelineTesterMixin * Docs fix * Tests refactor * Pass generator to SDE scheduler for reproducibility * Apply suggestion from @yiyixuxu * Distilled text encoder step bugfix --------- Co-authored-by: YiYi Xu <yixu310@gmail.com>
1 parent 2f2a74f commit 8eac6ad

11 files changed

Lines changed: 831 additions & 5 deletions

File tree

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

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -989,11 +989,79 @@ videos = pipe(
989989
export_to_video(videos, "cosmos3_modular_transfer_edge.mp4", fps=30, macro_block_size=1)
990990
```
991991

992+
### Distilled (few-step) text-to-image and image-to-video
993+
994+
Few-step distilled checkpoints are served by [`Cosmos3DistilledModularPipeline`] (blocks:
995+
`Cosmos3DistilledBlocks`); the base [`Cosmos3OmniModularPipeline`] and [`Cosmos3OmniPipeline`] do
996+
not support them. `num_inference_steps` is fixed to the length of the `distilled_sigmas` pipeline
997+
config (from the checkpoint's `modular_model_index.json`) and `guidance_scale` is forced to
998+
1.0 since guidance is baked into the weights — passing any other value for either raises an error,
999+
and `negative_prompt` is warned about and ignored.
1000+
1001+
Prompts follow the same descriptive JSON structure as the non-distilled models, so short text
1002+
must be upsampled first — use `--mode text2image` (T2I) or `--mode image2video` (I2V) as
1003+
described in [Prompt upsampling](#prompt-upsampling), then pass the JSON via `json.dumps(...)`.
1004+
1005+
```python
1006+
import json
1007+
import torch
1008+
from diffusers import Cosmos3DistilledModularPipeline
1009+
from diffusers.utils import export_to_video, load_image
1010+
1011+
# JSON-upsampled prompt (see "Prompt upsampling" above).
1012+
json_prompt = json.load(open("assets/example_t2i_prompt.json"))
1013+
1014+
repo = "nvidia/Cosmos3-Super-Text2Image-4Step"
1015+
pipe = Cosmos3DistilledModularPipeline.from_pretrained(repo, torch_dtype=torch.bfloat16)
1016+
pipe.load_components(torch_dtype=torch.bfloat16)
1017+
pipe.to("cuda")
1018+
1019+
# text-to-image (distilled)
1020+
videos = pipe(
1021+
prompt=json.dumps(json_prompt),
1022+
num_frames=1,
1023+
height=720,
1024+
width=1280,
1025+
output="videos",
1026+
)
1027+
videos[0].save("cosmos3_distilled_t2i.jpg", format="JPEG", quality=85)
1028+
1029+
# image-to-video (distilled) — load the I2V repo instead
1030+
# JSON-upsampled prompt (see "Prompt upsampling" above); upsampled from the source prompt
1031+
# "The right robotic hand picks up the red sphere on the shelf."
1032+
json_prompt_i2v = json.load(open("assets/example_i2v_prompt.json"))
1033+
1034+
repo_i2v = "nvidia/Cosmos3-Super-Image2Video-4Step"
1035+
pipe = Cosmos3DistilledModularPipeline.from_pretrained(repo_i2v, torch_dtype=torch.bfloat16)
1036+
pipe.load_components(torch_dtype=torch.bfloat16)
1037+
pipe.to("cuda")
1038+
1039+
image = load_image(
1040+
"https://github.com/nvidia-cosmos/cosmos-dependencies/raw/refs/heads/assets/cosmos3/inputs/vision/robot_153.jpg"
1041+
)
1042+
videos = pipe(
1043+
prompt=json.dumps(json_prompt_i2v),
1044+
image=image,
1045+
num_frames=189,
1046+
height=720,
1047+
width=1280,
1048+
output="videos",
1049+
)
1050+
export_to_video(videos, "cosmos3_distilled_i2v.mp4", fps=24, macro_block_size=1)
1051+
```
1052+
9921053
[[autodoc]] Cosmos3OmniModularPipeline
9931054

9941055
- all
9951056
- __call__
9961057

1058+
## Cosmos3DistilledModularPipeline
1059+
1060+
[[autodoc]] Cosmos3DistilledModularPipeline
1061+
1062+
- all
1063+
- __call__
1064+
9971065
## CosmosActionCondition
9981066

9991067
[[autodoc]] CosmosActionCondition

src/diffusers/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,8 @@
489489
[
490490
"AnimaAutoBlocks",
491491
"AnimaModularPipeline",
492+
"Cosmos3DistilledBlocks",
493+
"Cosmos3DistilledModularPipeline",
492494
"Cosmos3OmniBlocks",
493495
"Cosmos3OmniModularPipeline",
494496
"ErnieImageAutoBlocks",
@@ -1349,6 +1351,8 @@
13491351
from .modular_pipelines import (
13501352
AnimaAutoBlocks,
13511353
AnimaModularPipeline,
1354+
Cosmos3DistilledBlocks,
1355+
Cosmos3DistilledModularPipeline,
13521356
Cosmos3OmniBlocks,
13531357
Cosmos3OmniModularPipeline,
13541358
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: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from ...models.transformers.transformer_cosmos3 import Cosmos3OmniTransformer
77
from ...pipelines.cosmos.pipeline_cosmos3_omni import _EMBODIMENT_TO_DOMAIN_ID, CosmosActionCondition
8-
from ...schedulers import UniPCMultistepScheduler
8+
from ...schedulers import FlowMatchEulerDiscreteScheduler, UniPCMultistepScheduler
99
from ...utils.torch_utils import randn_tensor
1010
from ..modular_pipeline import ModularPipelineBlocks, PipelineState
1111
from ..modular_pipeline_utils import ComponentSpec, ConfigSpec, InputParam, OutputParam
@@ -116,6 +116,11 @@ def intermediate_outputs(self) -> list[OutputParam]:
116116
type_hint=list[int],
117117
description="Indexes of conditioned vision latent frames.",
118118
),
119+
OutputParam(
120+
"vision_conditioning_latents",
121+
type_hint=torch.Tensor,
122+
description="Clean encoded vision latents used to re-anchor image conditioning each step.",
123+
),
119124
]
120125

121126
@torch.no_grad()
@@ -166,6 +171,7 @@ def __call__(self, components: Cosmos3OmniModularPipeline, state: PipelineState)
166171
block_state.vision_condition_mask[:, 0, 0] > 0, as_tuple=False
167172
).flatten()
168173
block_state.vision_condition_indexes_for_pack = [int(idx.item()) for idx in vision_condition_indexes]
174+
block_state.vision_conditioning_latents = x0_tokens_vision
169175

170176
self.set_block_state(state, block_state)
171177
return components, state
@@ -1237,3 +1243,89 @@ def __call__(self, components: Cosmos3OmniModularPipeline, state: PipelineState)
12371243
)
12381244
self.set_block_state(state, block_state)
12391245
return components, state
1246+
1247+
1248+
class Cosmos3DistilledSetTimestepsStep(ModularPipelineBlocks):
1249+
model_name = "cosmos3-omni"
1250+
1251+
@property
1252+
def description(self) -> str:
1253+
return "Initializes the fixed distilled sampling schedule from the pipeline's `distilled_sigmas` config."
1254+
1255+
@property
1256+
def expected_components(self) -> list[ComponentSpec]:
1257+
return [ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler)]
1258+
1259+
@property
1260+
def expected_configs(self) -> list[ConfigSpec]:
1261+
return [
1262+
ConfigSpec(name="is_distilled", default=True),
1263+
ConfigSpec(name="distilled_sigmas", default=None),
1264+
]
1265+
1266+
@property
1267+
def inputs(self) -> list[InputParam]:
1268+
return [
1269+
InputParam.template("num_inference_steps", required=False, default=None),
1270+
InputParam(
1271+
name="guidance_scale",
1272+
type_hint=float,
1273+
default=None,
1274+
description=(
1275+
"Unused for distilled checkpoints; classifier-free guidance is baked into the weights and the "
1276+
"scale is forced to 1.0. Passing a value other than 1.0 raises an error."
1277+
),
1278+
),
1279+
]
1280+
1281+
@property
1282+
def intermediate_outputs(self) -> list[OutputParam]:
1283+
return [
1284+
OutputParam("timesteps", type_hint=torch.Tensor, description="Scheduler timesteps for denoising."),
1285+
OutputParam("num_warmup_steps", type_hint=int, description="Number of scheduler warmup steps."),
1286+
OutputParam(
1287+
"num_inference_steps",
1288+
type_hint=int,
1289+
description="Resolved number of denoising steps (fixed by the distilled schedule).",
1290+
),
1291+
OutputParam(
1292+
name="guidance_scale",
1293+
type_hint=float,
1294+
description="Resolved classifier-free guidance scale (always 1.0 for distilled checkpoints).",
1295+
),
1296+
]
1297+
1298+
@torch.no_grad()
1299+
def __call__(self, components: Cosmos3OmniModularPipeline, state: PipelineState) -> PipelineState:
1300+
block_state = self.get_block_state(state)
1301+
device = components._execution_device
1302+
1303+
sigmas = components.config.distilled_sigmas
1304+
if not sigmas:
1305+
raise ValueError(
1306+
"Cosmos3DistilledSetTimestepsStep requires the pipeline config `distilled_sigmas` to be set "
1307+
"(populated from the distilled checkpoint's `modular_model_index.json`). Load a distilled Cosmos3 "
1308+
"checkpoint or use `Cosmos3OmniModularPipeline` for base checkpoints."
1309+
)
1310+
sigmas = [float(s) for s in sigmas]
1311+
distilled_steps = len(sigmas)
1312+
1313+
if block_state.num_inference_steps is not None and block_state.num_inference_steps != distilled_steps:
1314+
raise ValueError(
1315+
"This is a distilled checkpoint; the step count is fixed by the pipeline's "
1316+
f"`distilled_sigmas` config ({distilled_steps} steps). "
1317+
f"`num_inference_steps` must be {distilled_steps} or left unset (got {block_state.num_inference_steps})."
1318+
)
1319+
if block_state.guidance_scale is not None and block_state.guidance_scale != 1.0:
1320+
raise ValueError(
1321+
"This is a distilled checkpoint; classifier-free guidance is baked into the weights. "
1322+
f"`guidance_scale` must be 1.0 or left unset (got {block_state.guidance_scale})."
1323+
)
1324+
1325+
components.scheduler.set_timesteps(sigmas=sigmas, device=device)
1326+
block_state.num_inference_steps = distilled_steps
1327+
block_state.guidance_scale = 1.0
1328+
block_state.timesteps = components.scheduler.timesteps
1329+
block_state.num_warmup_steps = len(block_state.timesteps) - distilled_steps * components.scheduler.order
1330+
self.set_block_state(state, block_state)
1331+
return components, state

src/diffusers/modular_pipelines/cosmos/denoise.py

Lines changed: 87 additions & 1 deletion
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 FlowMatchEulerDiscreteScheduler, UniPCMultistepScheduler
77
from ..modular_pipeline import (
88
BlockState,
99
LoopSequentialPipelineBlocks,
@@ -282,6 +282,71 @@ def __call__(self, components: Cosmos3OmniModularPipeline, block_state: BlockSta
282282
return components, block_state
283283

284284

285+
class Cosmos3DistilledVisionLoopSchedulerStep(ModularPipelineBlocks):
286+
model_name = "cosmos3-omni"
287+
288+
@property
289+
def description(self) -> str:
290+
return "Updates vision latents after one distilled denoising iteration, re-anchoring conditioned frames."
291+
292+
@property
293+
def expected_components(self) -> list[ComponentSpec]:
294+
return [ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler)]
295+
296+
@property
297+
def inputs(self) -> list[InputParam]:
298+
return [
299+
InputParam.template("latents", required=True, description="Noisy vision latents to update."),
300+
InputParam(
301+
name="velocity_vision", type_hint=torch.Tensor, required=True, description="Predicted vision velocity."
302+
),
303+
InputParam(
304+
name="vision_condition_mask",
305+
type_hint=torch.Tensor,
306+
required=True,
307+
description="Mask marking conditioned vision latent frames.",
308+
),
309+
InputParam(
310+
name="vision_conditioning_latents",
311+
type_hint=torch.Tensor,
312+
default=None,
313+
description="Clean encoded vision latents for re-anchoring conditioned frames.",
314+
),
315+
InputParam(
316+
name="vision_condition_indexes_for_pack",
317+
type_hint=list,
318+
default=None,
319+
description="Indexes of conditioned vision latent frames; non-empty for image-to-video.",
320+
),
321+
InputParam.template("generator"),
322+
]
323+
324+
@property
325+
def intermediate_outputs(self) -> list[OutputParam]:
326+
return [OutputParam.template("latents")]
327+
328+
@torch.no_grad()
329+
def __call__(self, components: Cosmos3OmniModularPipeline, block_state: BlockState, i: int, t: torch.Tensor):
330+
# Pass the generator so the scheduler's stochastic (SDE) re-noising is seedable/reproducible.
331+
block_state.latents = components.scheduler.step(
332+
block_state.velocity_vision.unsqueeze(0),
333+
t,
334+
block_state.latents.unsqueeze(0),
335+
generator=block_state.generator,
336+
return_dict=False,
337+
)[0].squeeze(0)
338+
339+
# Distilled checkpoints use stochastic (SDE) scheduler steps that re-noise every position.
340+
# Re-anchor conditioned frames to the clean encoded reference after each step.
341+
has_image_condition = bool(block_state.vision_condition_indexes_for_pack)
342+
if has_image_condition and block_state.vision_conditioning_latents is not None:
343+
mask = block_state.vision_condition_mask
344+
reference = block_state.vision_conditioning_latents.to(block_state.latents.dtype)
345+
block_state.latents = mask * reference + (1.0 - mask) * block_state.latents
346+
347+
return components, block_state
348+
349+
285350
class Cosmos3SoundLoopSchedulerStep(ModularPipelineBlocks):
286351
model_name = "cosmos3-omni"
287352

@@ -427,6 +492,27 @@ def description(self) -> str:
427492
return "Runs the vision-only Cosmos3 denoising loop."
428493

429494

495+
class Cosmos3DistilledVisionDenoiseStep(Cosmos3DenoiseLoopWrapper):
496+
model_name = "cosmos3-omni"
497+
block_classes = [
498+
Cosmos3VisionLoopPrepareStep,
499+
Cosmos3LoopDenoiser,
500+
Cosmos3DistilledVisionLoopSchedulerStep,
501+
]
502+
block_names = ["prepare_vision", "denoiser", "update_vision"]
503+
504+
@property
505+
def description(self) -> str:
506+
return "Runs the vision-only distilled Cosmos3 denoising loop."
507+
508+
@property
509+
def loop_expected_components(self) -> list[ComponentSpec]:
510+
return [
511+
ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler),
512+
ComponentSpec("transformer", Cosmos3OmniTransformer),
513+
]
514+
515+
430516
class Cosmos3VisionSoundDenoiseStep(Cosmos3DenoiseLoopWrapper):
431517
block_classes = [
432518
Cosmos3VisionLoopPrepareStep,

0 commit comments

Comments
 (0)