Skip to content

Commit abb37e7

Browse files
committed
Cosmos3 distilled support (dedicated modular pipeline + blocks)
1 parent a437794 commit abb37e7

12 files changed

Lines changed: 680 additions & 7 deletions

File tree

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

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -989,11 +989,86 @@ 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 ship a fixed sigma schedule in
995+
`scheduler.config.fixed_step_sampler_config.t_list` and bake classifier-free guidance into
996+
the weights. They are served by the dedicated [`Cosmos3DistilledModularPipeline`] (blocks:
997+
`Cosmos3DistilledBlocks`) — the task-based [`Cosmos3OmniPipeline`] and the base
998+
[`Cosmos3OmniModularPipeline`] do not implement the distilled contract.
999+
1000+
Load a distilled repo and call the pipeline **without** `num_inference_steps` or
1001+
`guidance_scale`; `Cosmos3DistilledSetTimestepsStep` reads the fixed step count from the
1002+
scheduler config and forces `guidance_scale=1.0`. Because classifier-free guidance is baked
1003+
into the weights, `negative_prompt` is not supported (passing one raises an error).
1004+
1005+
Prompts follow the same descriptive JSON structure as the non-distilled models, so short text
1006+
must be upsampled first — use `--mode text2image` (T2I) or `--mode image2video` (I2V) as
1007+
described in [Prompt upsampling](#prompt-upsampling), then pass the JSON via `json.dumps(...)`.
1008+
1009+
```python
1010+
import json
1011+
import torch
1012+
from diffusers import Cosmos3DistilledModularPipeline
1013+
from diffusers.utils import export_to_video, load_image
1014+
1015+
# JSON-upsampled prompt (see "Prompt upsampling" above).
1016+
json_prompt = json.load(open("assets/example_t2i_prompt.json"))
1017+
1018+
repo = "nvidia/Cosmos3-Super-Text2Image-4Step"
1019+
pipe = Cosmos3DistilledModularPipeline.from_pretrained(repo, torch_dtype=torch.bfloat16)
1020+
pipe.load_components(torch_dtype=torch.bfloat16)
1021+
pipe.to("cuda")
1022+
1023+
# text-to-image (distilled)
1024+
videos = pipe(
1025+
prompt=json.dumps(json_prompt),
1026+
num_frames=1,
1027+
height=720,
1028+
width=1280,
1029+
output="videos",
1030+
)
1031+
videos[0].save("cosmos3_distilled_t2i.jpg", format="JPEG", quality=85)
1032+
1033+
# image-to-video (distilled) — load the I2V repo instead
1034+
# JSON-upsampled prompt (see "Prompt upsampling" above); upsampled from the source prompt
1035+
# "The right robotic hand picks up the red sphere on the shelf."
1036+
json_prompt_i2v = json.load(open("assets/example_i2v_prompt.json"))
1037+
1038+
repo_i2v = "nvidia/Cosmos3-Super-Image2Video-4Step"
1039+
pipe = Cosmos3DistilledModularPipeline.from_pretrained(repo_i2v, torch_dtype=torch.bfloat16)
1040+
pipe.load_components(torch_dtype=torch.bfloat16)
1041+
pipe.to("cuda")
1042+
1043+
image = load_image(
1044+
"https://github.com/nvidia-cosmos/cosmos-dependencies/raw/refs/heads/assets/cosmos3/inputs/vision/robot_153.jpg"
1045+
)
1046+
videos = pipe(
1047+
prompt=json.dumps(json_prompt_i2v),
1048+
image=image,
1049+
num_frames=189,
1050+
height=720,
1051+
width=1280,
1052+
output="videos",
1053+
)
1054+
export_to_video(videos, "cosmos3_distilled_i2v.mp4", fps=24, macro_block_size=1)
1055+
```
1056+
1057+
For a CLI wrapper with warmup / iteration timing, see
1058+
[`examples/cosmos3/inference_cosmos3_modular_distilled.py`](../../../examples/cosmos3/inference_cosmos3_modular_distilled.py).
1059+
9921060
[[autodoc]] Cosmos3OmniModularPipeline
9931061

9941062
- all
9951063
- __call__
9961064

1065+
## Cosmos3DistilledModularPipeline
1066+
1067+
[[autodoc]] Cosmos3DistilledModularPipeline
1068+
1069+
- all
1070+
- __call__
1071+
9971072
## CosmosActionCondition
9981073

9991074
[[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: 91 additions & 2 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 UniPCMultistepScheduler
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

@@ -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
@@ -1224,3 +1230,86 @@ def __call__(self, components: Cosmos3OmniModularPipeline, state: PipelineState)
12241230
)
12251231
self.set_block_state(state, block_state)
12261232
return components, state
1233+
1234+
1235+
class Cosmos3DistilledSetTimestepsStep(ModularPipelineBlocks):
1236+
model_name = "cosmos3-omni"
1237+
1238+
@property
1239+
def description(self) -> str:
1240+
return "Initializes the fixed distilled sampling schedule from the scheduler's `fixed_step_sampler_config`."
1241+
1242+
@property
1243+
def expected_components(self) -> list[ComponentSpec]:
1244+
return [ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler)]
1245+
1246+
@property
1247+
def expected_configs(self) -> list[ConfigSpec]:
1248+
return [ConfigSpec(name="is_distilled", default=True)]
1249+
1250+
@property
1251+
def inputs(self) -> list[InputParam]:
1252+
return [
1253+
InputParam.template("num_inference_steps", required=False, default=None),
1254+
InputParam(
1255+
name="guidance_scale",
1256+
type_hint=float,
1257+
default=None,
1258+
description=(
1259+
"Unused for distilled checkpoints; classifier-free guidance is baked into the weights and the "
1260+
"scale is forced to 1.0. Passing a value other than 1.0 raises an error."
1261+
),
1262+
),
1263+
]
1264+
1265+
@property
1266+
def intermediate_outputs(self) -> list[OutputParam]:
1267+
return [
1268+
OutputParam("timesteps", type_hint=torch.Tensor, description="Scheduler timesteps for denoising."),
1269+
OutputParam("num_warmup_steps", type_hint=int, description="Number of scheduler warmup steps."),
1270+
OutputParam(
1271+
"num_inference_steps",
1272+
type_hint=int,
1273+
description="Resolved number of denoising steps (fixed by the distilled schedule).",
1274+
),
1275+
OutputParam(
1276+
name="guidance_scale",
1277+
type_hint=float,
1278+
description="Resolved classifier-free guidance scale (always 1.0 for distilled checkpoints).",
1279+
),
1280+
]
1281+
1282+
@torch.no_grad()
1283+
def __call__(self, components: Cosmos3OmniModularPipeline, state: PipelineState) -> PipelineState:
1284+
block_state = self.get_block_state(state)
1285+
device = components._execution_device
1286+
1287+
fixed_step_cfg = components.scheduler.config.get("fixed_step_sampler_config", None)
1288+
if not fixed_step_cfg or not fixed_step_cfg.get("t_list"):
1289+
raise ValueError(
1290+
"Cosmos3DistilledSetTimestepsStep requires a scheduler that ships a distilled "
1291+
"`fixed_step_sampler_config.t_list`. Load a distilled Cosmos3 checkpoint or use "
1292+
"`Cosmos3OmniModularPipeline` for base checkpoints."
1293+
)
1294+
sigmas = [float(s) for s in fixed_step_cfg["t_list"]]
1295+
distilled_steps = len(sigmas)
1296+
1297+
if block_state.num_inference_steps is not None and block_state.num_inference_steps != distilled_steps:
1298+
raise ValueError(
1299+
"This is a distilled checkpoint; the step count is fixed by the scheduler's "
1300+
f"`fixed_step_sampler_config.t_list` ({distilled_steps} steps). "
1301+
f"`num_inference_steps` must be {distilled_steps} or left unset (got {block_state.num_inference_steps})."
1302+
)
1303+
if block_state.guidance_scale is not None and block_state.guidance_scale != 1.0:
1304+
raise ValueError(
1305+
"This is a distilled checkpoint; classifier-free guidance is baked into the weights. "
1306+
f"`guidance_scale` must be 1.0 or left unset (got {block_state.guidance_scale})."
1307+
)
1308+
1309+
components.scheduler.set_timesteps(sigmas=sigmas, device=device)
1310+
block_state.num_inference_steps = distilled_steps
1311+
block_state.guidance_scale = 1.0
1312+
block_state.timesteps = components.scheduler.timesteps
1313+
block_state.num_warmup_steps = len(block_state.timesteps) - distilled_steps * components.scheduler.order
1314+
self.set_block_state(state, block_state)
1315+
return components, state

src/diffusers/modular_pipelines/cosmos/denoise.py

Lines changed: 81 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,65 @@ 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+
]
322+
323+
@property
324+
def intermediate_outputs(self) -> list[OutputParam]:
325+
return [OutputParam.template("latents")]
326+
327+
@torch.no_grad()
328+
def __call__(self, components: Cosmos3OmniModularPipeline, block_state: BlockState, i: int, t: torch.Tensor):
329+
block_state.latents = components.scheduler.step(
330+
block_state.velocity_vision.unsqueeze(0), t, block_state.latents.unsqueeze(0), return_dict=False
331+
)[0].squeeze(0)
332+
333+
# Distilled checkpoints use stochastic (SDE) scheduler steps that re-noise every position.
334+
# Re-anchor conditioned frames to the clean encoded reference after each step.
335+
has_image_condition = bool(block_state.vision_condition_indexes_for_pack)
336+
if has_image_condition and block_state.vision_conditioning_latents is not None:
337+
mask = block_state.vision_condition_mask
338+
reference = block_state.vision_conditioning_latents.to(block_state.latents.dtype)
339+
block_state.latents = mask * reference + (1.0 - mask) * block_state.latents
340+
341+
return components, block_state
342+
343+
285344
class Cosmos3SoundLoopSchedulerStep(ModularPipelineBlocks):
286345
model_name = "cosmos3-omni"
287346

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

429488

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

src/diffusers/modular_pipelines/cosmos/encoders.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,23 @@ def _tokenize(text: str) -> list[int]:
271271
return components, state
272272

273273

274+
class Cosmos3DistilledTextEncoderStep(Cosmos3TextEncoderStep):
275+
model_name = "cosmos3-omni"
276+
277+
@property
278+
def description(self) -> str:
279+
return "Prepares distilled prompt token IDs; rejects `negative_prompt` since guidance is baked in."
280+
281+
@staticmethod
282+
def _check_inputs(block_state) -> None:
283+
if block_state.negative_prompt is not None:
284+
raise ValueError(
285+
"This is a distilled Cosmos3 checkpoint; classifier-free guidance is baked into the weights, so "
286+
"`negative_prompt` is not supported. Leave it unset."
287+
)
288+
Cosmos3TextEncoderStep._check_inputs(block_state)
289+
290+
274291
class Cosmos3ActionTextStep(ModularPipelineBlocks):
275292
model_name = "cosmos3-omni"
276293

0 commit comments

Comments
 (0)