Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions docs/source/en/api/pipelines/cosmos3.md
Original file line number Diff line number Diff line change
Expand Up @@ -989,11 +989,86 @@ videos = pipe(
export_to_video(videos, "cosmos3_modular_transfer_edge.mp4", fps=30, macro_block_size=1)
```

### Distilled (few-step) text-to-image and image-to-video

Few-step distilled checkpoints ship a fixed sigma schedule in
`scheduler.config.fixed_step_sampler_config.t_list` and bake classifier-free guidance into
the weights. They are served by the dedicated [`Cosmos3DistilledModularPipeline`] (blocks:
`Cosmos3DistilledBlocks`) — the task-based [`Cosmos3OmniPipeline`] and the base
[`Cosmos3OmniModularPipeline`] do not implement the distilled contract.

Load a distilled repo and call the pipeline **without** `num_inference_steps` or
`guidance_scale`; `Cosmos3DistilledSetTimestepsStep` reads the fixed step count from the
scheduler config and forces `guidance_scale=1.0`. Because classifier-free guidance is baked
into the weights, `negative_prompt` is not supported (passing one raises an error).

Prompts follow the same descriptive JSON structure as the non-distilled models, so short text
must be upsampled first — use `--mode text2image` (T2I) or `--mode image2video` (I2V) as
described in [Prompt upsampling](#prompt-upsampling), then pass the JSON via `json.dumps(...)`.

```python
import json
import torch
from diffusers import Cosmos3DistilledModularPipeline
from diffusers.utils import export_to_video, load_image

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

repo = "nvidia/Cosmos3-Super-Text2Image-4Step"
pipe = Cosmos3DistilledModularPipeline.from_pretrained(repo, torch_dtype=torch.bfloat16)
pipe.load_components(torch_dtype=torch.bfloat16)
pipe.to("cuda")

# text-to-image (distilled)
videos = pipe(
prompt=json.dumps(json_prompt),
num_frames=1,
height=720,
width=1280,
output="videos",
)
videos[0].save("cosmos3_distilled_t2i.jpg", format="JPEG", quality=85)

# image-to-video (distilled) — load the I2V repo instead
# JSON-upsampled prompt (see "Prompt upsampling" above); upsampled from the source prompt
# "The right robotic hand picks up the red sphere on the shelf."
json_prompt_i2v = json.load(open("assets/example_i2v_prompt.json"))

repo_i2v = "nvidia/Cosmos3-Super-Image2Video-4Step"
pipe = Cosmos3DistilledModularPipeline.from_pretrained(repo_i2v, torch_dtype=torch.bfloat16)
pipe.load_components(torch_dtype=torch.bfloat16)
pipe.to("cuda")

image = load_image(
"https://github.com/nvidia-cosmos/cosmos-dependencies/raw/refs/heads/assets/cosmos3/inputs/vision/robot_153.jpg"
)
videos = pipe(
prompt=json.dumps(json_prompt_i2v),
image=image,
num_frames=189,
height=720,
width=1280,
output="videos",
)
export_to_video(videos, "cosmos3_distilled_i2v.mp4", fps=24, macro_block_size=1)
```

For a CLI wrapper with warmup / iteration timing, see
[`examples/cosmos3/inference_cosmos3_modular_distilled.py`](../../../examples/cosmos3/inference_cosmos3_modular_distilled.py).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This link does not exist


[[autodoc]] Cosmos3OmniModularPipeline

- all
- __call__

## Cosmos3DistilledModularPipeline

[[autodoc]] Cosmos3DistilledModularPipeline

- all
- __call__

## CosmosActionCondition

[[autodoc]] CosmosActionCondition
Expand Down
4 changes: 4 additions & 0 deletions src/diffusers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,8 @@
[
"AnimaAutoBlocks",
"AnimaModularPipeline",
"Cosmos3DistilledBlocks",
"Cosmos3DistilledModularPipeline",
"Cosmos3OmniBlocks",
"Cosmos3OmniModularPipeline",
"ErnieImageAutoBlocks",
Expand Down Expand Up @@ -1349,6 +1351,8 @@
from .modular_pipelines import (
AnimaAutoBlocks,
AnimaModularPipeline,
Cosmos3DistilledBlocks,
Cosmos3DistilledModularPipeline,
Cosmos3OmniBlocks,
Cosmos3OmniModularPipeline,
ErnieImageAutoBlocks,
Expand Down
9 changes: 8 additions & 1 deletion src/diffusers/modular_pipelines/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@
"AnimaModularPipeline",
]
_import_structure["cosmos"] = [
"Cosmos3DistilledBlocks",
"Cosmos3DistilledModularPipeline",
"Cosmos3OmniBlocks",
"Cosmos3OmniModularPipeline",
]
Expand Down Expand Up @@ -128,7 +130,12 @@
else:
from .anima import AnimaAutoBlocks, AnimaModularPipeline
from .components_manager import ComponentsManager
from .cosmos import Cosmos3OmniBlocks, Cosmos3OmniModularPipeline
from .cosmos import (
Cosmos3DistilledBlocks,
Cosmos3DistilledModularPipeline,
Cosmos3OmniBlocks,
Cosmos3OmniModularPipeline,
)
from .ernie_image import ErnieImageAutoBlocks, ErnieImageModularPipeline
from .flux import FluxAutoBlocks, FluxKontextAutoBlocks, FluxKontextModularPipeline, FluxModularPipeline
from .flux2 import (
Expand Down
6 changes: 4 additions & 2 deletions src/diffusers/modular_pipelines/cosmos/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
_dummy_objects.update(get_objects_from_module(dummy_torch_and_transformers_objects))
else:
_import_structure["modular_blocks_cosmos3"] = ["Cosmos3OmniBlocks"]
_import_structure["modular_pipeline"] = ["Cosmos3OmniModularPipeline"]
_import_structure["modular_blocks_cosmos3_distilled"] = ["Cosmos3DistilledBlocks"]
_import_structure["modular_pipeline"] = ["Cosmos3DistilledModularPipeline", "Cosmos3OmniModularPipeline"]

if TYPE_CHECKING or DIFFUSERS_SLOW_IMPORT:
try:
Expand All @@ -32,7 +33,8 @@
from ...utils.dummy_torch_and_transformers_objects import * # noqa F403
else:
from .modular_blocks_cosmos3 import Cosmos3OmniBlocks
from .modular_pipeline import Cosmos3OmniModularPipeline
from .modular_blocks_cosmos3_distilled import Cosmos3DistilledBlocks
from .modular_pipeline import Cosmos3DistilledModularPipeline, Cosmos3OmniModularPipeline
else:
import sys

Expand Down
93 changes: 91 additions & 2 deletions src/diffusers/modular_pipelines/cosmos/before_denoise.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

from ...models.transformers.transformer_cosmos3 import Cosmos3OmniTransformer
from ...pipelines.cosmos.pipeline_cosmos3_omni import _EMBODIMENT_TO_DOMAIN_ID, CosmosActionCondition
from ...schedulers import UniPCMultistepScheduler
from ...schedulers import FlowMatchEulerDiscreteScheduler, UniPCMultistepScheduler
from ...utils.torch_utils import randn_tensor
from ..modular_pipeline import ModularPipelineBlocks, PipelineState
from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
from ..modular_pipeline_utils import ComponentSpec, ConfigSpec, InputParam, OutputParam
from .modular_pipeline import Cosmos3OmniModularPipeline


Expand Down Expand Up @@ -115,6 +115,11 @@ def intermediate_outputs(self) -> list[OutputParam]:
type_hint=list[int],
description="Indexes of conditioned vision latent frames.",
),
OutputParam(
"vision_conditioning_latents",
type_hint=torch.Tensor,
description="Clean encoded vision latents used to re-anchor image conditioning each step.",
),
]

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

self.set_block_state(state, block_state)
return components, state
Expand Down Expand Up @@ -1224,3 +1230,86 @@ def __call__(self, components: Cosmos3OmniModularPipeline, state: PipelineState)
)
self.set_block_state(state, block_state)
return components, state


class Cosmos3DistilledSetTimestepsStep(ModularPipelineBlocks):
model_name = "cosmos3-omni"

@property
def description(self) -> str:
return "Initializes the fixed distilled sampling schedule from the scheduler's `fixed_step_sampler_config`."

@property
def expected_components(self) -> list[ComponentSpec]:
return [ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler)]

@property
def expected_configs(self) -> list[ConfigSpec]:
return [ConfigSpec(name="is_distilled", default=True)]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add a distilled_sigmas too?


@property
def inputs(self) -> list[InputParam]:
return [
InputParam.template("num_inference_steps", required=False, default=None),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you write a new block (Cosmos3DistilledSetTimestepsStep) for distilled checkpoint?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should add a new modular_blocks_cosmos3_distilled.py file

and assemble a Cosmo3DistilledBlocks that will map directly to the modular_model_index.json in your repo for the distilled checkpoint

see flux2 as a reference on how to structure this https://github.com/huggingface/diffusers/tree/main/src/diffusers/modular_pipelines/flux2

InputParam(
name="guidance_scale",
type_hint=float,
default=None,
description=(
"Unused for distilled checkpoints; classifier-free guidance is baked into the weights and the "
"scale is forced to 1.0. Passing a value other than 1.0 raises an error."
),
),
]

@property
def intermediate_outputs(self) -> list[OutputParam]:
return [
OutputParam("timesteps", type_hint=torch.Tensor, description="Scheduler timesteps for denoising."),
OutputParam("num_warmup_steps", type_hint=int, description="Number of scheduler warmup steps."),
OutputParam(
"num_inference_steps",
type_hint=int,
description="Resolved number of denoising steps (fixed by the distilled schedule).",
),
OutputParam(
name="guidance_scale",
type_hint=float,
description="Resolved classifier-free guidance scale (always 1.0 for distilled checkpoints).",
),
]

@torch.no_grad()
def __call__(self, components: Cosmos3OmniModularPipeline, state: PipelineState) -> PipelineState:
block_state = self.get_block_state(state)
device = components._execution_device

fixed_step_cfg = components.scheduler.config.get("fixed_step_sampler_config", None)
if not fixed_step_cfg or not fixed_step_cfg.get("t_list"):
raise ValueError(
"Cosmos3DistilledSetTimestepsStep requires a scheduler that ships a distilled "
"`fixed_step_sampler_config.t_list`. Load a distilled Cosmos3 checkpoint or use "
"`Cosmos3OmniModularPipeline` for base checkpoints."
)
sigmas = [float(s) for s in fixed_step_cfg["t_list"]]
distilled_steps = len(sigmas)
Comment on lines +1287 to +1295

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fixed_step_cfg = components.scheduler.config.get("fixed_step_sampler_config", None)
if not fixed_step_cfg or not fixed_step_cfg.get("t_list"):
raise ValueError(
"Cosmos3DistilledSetTimestepsStep requires a scheduler that ships a distilled "
"`fixed_step_sampler_config.t_list`. Load a distilled Cosmos3 checkpoint or use "
"`Cosmos3OmniModularPipeline` for base checkpoints."
)
sigmas = [float(s) for s in fixed_step_cfg["t_list"]]
distilled_steps = len(sigmas)
sigmas = components.distilled_sigmas
distilled_steps = len(sigmas)

our scheduler does not actually have this config -> it is better to add the fixed steps as a pipeline config, you can set the value from modular_model_index.json


if block_state.num_inference_steps is not None and block_state.num_inference_steps != distilled_steps:
raise ValueError(
"This is a distilled checkpoint; the step count is fixed by the scheduler's "
f"`fixed_step_sampler_config.t_list` ({distilled_steps} steps). "
f"`num_inference_steps` must be {distilled_steps} or left unset (got {block_state.num_inference_steps})."
)
if block_state.guidance_scale is not None and block_state.guidance_scale != 1.0:
raise ValueError(
"This is a distilled checkpoint; classifier-free guidance is baked into the weights. "
f"`guidance_scale` must be 1.0 or left unset (got {block_state.guidance_scale})."
)

components.scheduler.set_timesteps(sigmas=sigmas, device=device)
block_state.num_inference_steps = distilled_steps
block_state.guidance_scale = 1.0
block_state.timesteps = components.scheduler.timesteps
block_state.num_warmup_steps = len(block_state.timesteps) - distilled_steps * components.scheduler.order
self.set_block_state(state, block_state)
return components, state
82 changes: 81 additions & 1 deletion src/diffusers/modular_pipelines/cosmos/denoise.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import torch

from ...models.transformers.transformer_cosmos3 import Cosmos3OmniTransformer
from ...schedulers import UniPCMultistepScheduler
from ...schedulers import FlowMatchEulerDiscreteScheduler, UniPCMultistepScheduler
from ..modular_pipeline import (
BlockState,
LoopSequentialPipelineBlocks,
Expand Down Expand Up @@ -282,6 +282,65 @@ def __call__(self, components: Cosmos3OmniModularPipeline, block_state: BlockSta
return components, block_state


class Cosmos3DistilledVisionLoopSchedulerStep(ModularPipelineBlocks):
model_name = "cosmos3-omni"

@property
def description(self) -> str:
return "Updates vision latents after one distilled denoising iteration, re-anchoring conditioned frames."

@property
def expected_components(self) -> list[ComponentSpec]:
return [ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler)]

@property
def inputs(self) -> list[InputParam]:
return [
InputParam.template("latents", required=True, description="Noisy vision latents to update."),
InputParam(
name="velocity_vision", type_hint=torch.Tensor, required=True, description="Predicted vision velocity."
),
InputParam(
name="vision_condition_mask",
type_hint=torch.Tensor,
required=True,
description="Mask marking conditioned vision latent frames.",
),
InputParam(
name="vision_conditioning_latents",
type_hint=torch.Tensor,
default=None,
description="Clean encoded vision latents for re-anchoring conditioned frames.",
),
InputParam(
name="vision_condition_indexes_for_pack",
type_hint=list,
default=None,
description="Indexes of conditioned vision latent frames; non-empty for image-to-video.",
),
]

@property
def intermediate_outputs(self) -> list[OutputParam]:
return [OutputParam.template("latents")]

@torch.no_grad()
def __call__(self, components: Cosmos3OmniModularPipeline, block_state: BlockState, i: int, t: torch.Tensor):
block_state.latents = components.scheduler.step(
block_state.velocity_vision.unsqueeze(0), t, block_state.latents.unsqueeze(0), return_dict=False
)[0].squeeze(0)

# Distilled checkpoints use stochastic (SDE) scheduler steps that re-noise every position.
# Re-anchor conditioned frames to the clean encoded reference after each step.
has_image_condition = bool(block_state.vision_condition_indexes_for_pack)
if has_image_condition and block_state.vision_conditioning_latents is not None:
mask = block_state.vision_condition_mask
reference = block_state.vision_conditioning_latents.to(block_state.latents.dtype)
block_state.latents = mask * reference + (1.0 - mask) * block_state.latents

return components, block_state


class Cosmos3SoundLoopSchedulerStep(ModularPipelineBlocks):
model_name = "cosmos3-omni"

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


class Cosmos3DistilledVisionDenoiseStep(Cosmos3DenoiseLoopWrapper):
model_name = "cosmos3-omni"
block_classes = [
Cosmos3VisionLoopPrepareStep,
Cosmos3LoopDenoiser,
Cosmos3DistilledVisionLoopSchedulerStep,
]
block_names = ["prepare_vision", "denoiser", "update_vision"]

@property
def description(self) -> str:
return "Runs the vision-only distilled Cosmos3 denoising loop."

@property
def loop_expected_components(self) -> list[ComponentSpec]:
return [
ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler),
ComponentSpec("transformer", Cosmos3OmniTransformer),
]


class Cosmos3VisionSoundDenoiseStep(Cosmos3DenoiseLoopWrapper):
block_classes = [
Cosmos3VisionLoopPrepareStep,
Expand Down
17 changes: 17 additions & 0 deletions src/diffusers/modular_pipelines/cosmos/encoders.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,23 @@ def _tokenize(text: str) -> list[int]:
return components, state


class Cosmos3DistilledTextEncoderStep(Cosmos3TextEncoderStep):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might be cleaner to write its own block (not subclass from Cosmos3TextEncoderStep) and not list the negative_prompt as input at all, see flux2 klein https://github.com/huggingface/diffusers/blob/main/src/diffusers/modular_pipelines/flux2/encoders.py#L249

model_name = "cosmos3-omni"

@property
def description(self) -> str:
return "Prepares distilled prompt token IDs; rejects `negative_prompt` since guidance is baked in."

@staticmethod
def _check_inputs(block_state) -> None:
if block_state.negative_prompt is not None:
raise ValueError(
"This is a distilled Cosmos3 checkpoint; classifier-free guidance is baked into the weights, so "
"`negative_prompt` is not supported. Leave it unset."
)
Cosmos3TextEncoderStep._check_inputs(block_state)


class Cosmos3ActionTextStep(ModularPipelineBlocks):
model_name = "cosmos3-omni"

Expand Down
Loading
Loading