Describe the bug
CosmosRotaryPosEmbed scales its temporal frequencies by FPS whenever fps is not None:
https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/transformers/transformer_cosmos.py#L505-L512
if fps is None:
emb_t = torch.outer(seq[: pe_size[0]], temporal_freqs)
else:
emb_t = torch.outer(seq[: pe_size[0]] / fps * self.base_fps, temporal_freqs) # base_fps = 24
and Cosmos2VideoToWorldPipeline always feeds it — fps: int = 16 by default:
https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/cosmos/pipeline_cosmos2_video2world.py#L498
https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/cosmos/pipeline_cosmos2_video2world.py#L713
However, in the reference implementation this modulation sits behind an enable_fps_modulation
flag, and the released Cosmos-Predict2 checkpoints ship with it turned off:
So natively these models use raw integer temporal RoPE positions and ignore FPS entirely,
while the diffusers port has no such flag and, with the pipeline default fps=16 against
base_fps=24, runs every denoising step with temporal RoPE positions stretched by
24/16 = 1.5× relative to how the checkpoints were trained. We traced this while
investigating consistently odd motion in default Video2World outputs.
Consequences with the current defaults:
- Every default
Cosmos2VideoToWorldPipeline run diverges from the reference implementation.
- The
fps argument changes the generated content in diffusers, whereas natively it only
affects the exported container framerate.
Not affected:
Cosmos2TextToImagePipeline — single frame, so the temporal position is 0 either way.
Cosmos2_5_PredictBasePipeline / Cosmos2_5_TransferPipeline — they don't take an fps
argument, so the transformer sees fps=None and uses raw positions, matching the native
configs (which also disable the modulation).
- The Cosmos-Predict1 pipelines are out of scope here; this report is only about the
Cosmos-Predict2 checkpoints.
Suggested fix
Either of:
- Port
enable_fps_modulation into CosmosTransformer3DModel's config (default False for
the converted Cosmos-Predict2 checkpoints), mirroring the reference nets — the faithful fix; or
- Stop passing
fps to the transformer in Cosmos2VideoToWorldPipeline (or change the
default to fps=24, which makes the scale factor 1), keeping fps for video export only.
Reproduction
import torch
from diffusers import Cosmos2VideoToWorldPipeline
from diffusers.utils import load_image
pipe = Cosmos2VideoToWorldPipeline.from_pretrained(
"nvidia/Cosmos-Predict2-2B-Video2World", torch_dtype=torch.bfloat16
).to("cuda")
image = load_image(
"https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/diffusers/yellow-scrubber.png"
)
# identical seeds, only fps differs — natively these would be identical, here they are not
common = dict(image=image, prompt="a yellow scrubber cleaning a plate")
video_16 = pipe(**common, fps=16, generator=torch.Generator().manual_seed(1)).frames[0]
video_24 = pipe(**common, fps=24, generator=torch.Generator().manual_seed(1)).frames[0]
# video_24 corresponds to the un-modulated (native) temporal RoPE; video_16 is the default
Logs
System Info
- diffusers: main (also reproduces on 0.38.0)
- the referenced code is identical on current
main (line links above)
Who can help?
@a-r-r-o-w @yiyixuxu
Describe the bug
CosmosRotaryPosEmbedscales its temporal frequencies by FPS wheneverfps is not None:https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/transformers/transformer_cosmos.py#L505-L512
and
Cosmos2VideoToWorldPipelinealways feeds it —fps: int = 16by default:https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/cosmos/pipeline_cosmos2_video2world.py#L498
https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/cosmos/pipeline_cosmos2_video2world.py#L713
However, in the reference implementation this modulation sits behind an
enable_fps_modulationflag, and the released Cosmos-Predict2 checkpoints ship with it turned off:
rope_enable_fps_modulation=False)So natively these models use raw integer temporal RoPE positions and ignore FPS entirely,
while the diffusers port has no such flag and, with the pipeline default
fps=16againstbase_fps=24, runs every denoising step with temporal RoPE positions stretched by24/16 = 1.5× relative to how the checkpoints were trained. We traced this while
investigating consistently odd motion in default Video2World outputs.
Consequences with the current defaults:
Cosmos2VideoToWorldPipelinerun diverges from the reference implementation.fpsargument changes the generated content in diffusers, whereas natively it onlyaffects the exported container framerate.
Not affected:
Cosmos2TextToImagePipeline— single frame, so the temporal position is 0 either way.Cosmos2_5_PredictBasePipeline/Cosmos2_5_TransferPipeline— they don't take anfpsargument, so the transformer sees
fps=Noneand uses raw positions, matching the nativeconfigs (which also disable the modulation).
Cosmos-Predict2 checkpoints.
Suggested fix
Either of:
enable_fps_modulationintoCosmosTransformer3DModel's config (defaultFalseforthe converted Cosmos-Predict2 checkpoints), mirroring the reference nets — the faithful fix; or
fpsto the transformer inCosmos2VideoToWorldPipeline(or change thedefault to
fps=24, which makes the scale factor 1), keepingfpsfor video export only.Reproduction
Logs
System Info
main(line links above)Who can help?
@a-r-r-o-w @yiyixuxu