Image-to-Video
Cosmos
Diffusers
Safetensors
cosmos3_omni
nvidia
cosmos3
video-generation
fp8
quantized
modelopt
Instructions to use prometheusAIR/Cosmos3-Super-Image2Video-4Step-FP8 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Cosmos
How to use prometheusAIR/Cosmos3-Super-Image2Video-4Step-FP8 with Cosmos:
# No code snippets available yet for this library. # To use this model, check the repository files and the library's documentation. # Want to help? PRs adding snippets are welcome at: # https://github.com/huggingface/huggingface.js
- Notebooks
- Google Colab
- Kaggle
File size: 19,868 Bytes
5a866f5 31e237d 5a866f5 31e237d 5a866f5 66412be 5a866f5 66412be 5a866f5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 | #!/usr/bin/env python
"""
Local generation server for your quantized Cosmos3-Super-Image2Video-4Step, built on
the *validated* diffusers path (NOT vLLM-Omni). Sibling of serve_cosmos3_diffusers.py
(the nvidia/Cosmos3-Super base-model server) -- same build recipe, adapted for three
real differences in this checkpoint:
1. NO /generate (text -> still) ENDPOINT. This checkpoint is Image2Video-specific
(unlike the T2I/T2V/I2V-omni base model); NVIDIA's docs never demonstrate a
text-only call for it, and there's no local way to verify one works without
the checkpoint in hand. Rather than ship a silently-untested endpoint, this
server exposes /animate only. If a text-only path turns out to work when you
test it, port /generate over from serve_cosmos3_diffusers.py at that point.
2. NO SCHEDULER OBJECT SWAP, BUT A PATCHED set_timesteps. serve_cosmos3_diffusers.py
replaces the shipped scheduler with UniPCMultistepScheduler(flow_shift=...); this
checkpoint instead keeps FlowMatchEulerDiscreteScheduler as shipped (do not swap
it) but needs one targeted patch: Cosmos3OmniPipeline.__call__ always calls
scheduler.set_timesteps(num_inference_steps, device=device) with no passthrough
for this checkpoint's scheduler_config.json fixed_step_sampler_config (verified by
running it -- an unpatched call silently executed the pipeline's 35-step default
instead of the checkpoint's trained 4-step sde schedule). make_pipeline() below
applies _force_fixed_step_schedule() to fix this.
3. NO num_inference_steps IN THE REQUEST; guidance_scale IS FIXED AT 1.0. Per
NVIDIA's model card these are fixed by the distilled checkpoint -- CFG is
distilled out (do_classifier_free_guidance is guidance_scale != 1.0 in the
pipeline source, so the 6.0 pipeline default would silently re-enable it) and
the step count comes from the scheduler patch above, not from the caller.
ENDPOINTS
---------
GET /health -> readiness + which format is loaded
POST /animate -> image -> video (multipart upload; returns MP4, or GIF if no
mp4 encoder is installed)
USAGE
-----
CUDA_VISIBLE_DEVICES=0 python serve_cosmos3_i2v4step_diffusers.py --repo prometheusAIR/Cosmos3-I2V4Step-fp8
CUDA_VISIBLE_DEVICES=0 python serve_cosmos3_i2v4step_diffusers.py --repo ./cosmos3-i2v4step-fp8-hf
# or rebuild from the bf16 source (the original streaming path):
CUDA_VISIBLE_DEVICES=0 python serve_cosmos3_i2v4step_diffusers.py --format fp8
CUDA_VISIBLE_DEVICES=0 python serve_cosmos3_i2v4step_diffusers.py --format fp8 --cache ./cosmos3-i2v4step-cache
Image -> video:
curl -s -X POST http://localhost:8000/animate \
-F image=@out.png \
-F 'prompt=The robotic arm slowly lowers its gripper toward the objects and holds. Static camera.' \
-F num_frames=49 -F fps=24 \
--output clip.mp4
Health:
curl -s http://localhost:8000/health
"""
import argparse
import asyncio
import contextlib
import gc
import io
import os
import tempfile
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
import torch
from accelerate import init_empty_weights, load_checkpoint_in_model
from accelerate.utils import get_max_memory, infer_auto_device_map
from accelerate.utils.dataclasses import CustomDtype
from fastapi import FastAPI, File, Form, UploadFile
from fastapi.responses import Response
from huggingface_hub import snapshot_download
from PIL import Image
from pydantic import BaseModel
import modelopt.torch.quantization as mtq
from diffusers import Cosmos3OmniTransformer
from diffusers.utils import export_to_gif, export_to_video
SRC_REPO = "nvidia/Cosmos3-Super-Image2Video-4Step"
SPARE_SUBSTRINGS = [
"time_embedder", "proj_in", "proj_out", "lm_head", "embed", "norm", "audio_proj",
]
def _is_spare(name: str) -> bool:
return any(s in name for s in SPARE_SUBSTRINGS)
def build_quant_cfg(fmt: str) -> dict:
if fmt == "fp8":
return {
"quant_cfg": {
"*weight_quantizer": {"num_bits": (4, 3), "axis": None, "enable": True},
"*input_quantizer": {"enable": False},
"*output_quantizer": {"enable": False},
"*softmax_quantizer": {"enable": False},
},
"algorithm": "max",
}
if fmt == "nvfp4":
import copy
# Return the base preset UNMODIFIED -- on this installed modelopt version,
# W4A16_NVFP4_CFG's quant_cfg is a LIST (newer format), not the dict this
# function used to assume (confirmed empirically: "TypeError: list indices
# must be integers or slices, not str" trying dict-style key assignment
# here). enforce_weight_only_and_spare() below disables activations/spares
# on the model's actual inserted quantizer modules AFTER mtq.quantize(),
# which is config-form agnostic -- no need to pre-bake into the config dict.
# load_cosmos3_modelopt.py's loader re-applies the same disabling on every
# restore anyway (modelopt_state replays the config, not our imperative
# .disable() calls), so skipping the pre-bake here loses nothing.
base = getattr(mtq, "W4A16_NVFP4_CFG", None) or mtq.NVFP4_DEFAULT_CFG
return copy.deepcopy(base)
raise ValueError(f"Unknown format: {fmt!r}")
def enforce_weight_only_and_spare(model) -> tuple[int, int]:
n_spare = n_act = 0
for name, module in model.named_modules():
if not (name.endswith("_quantizer") and hasattr(module, "disable")):
continue
if name.endswith("weight_quantizer"):
if _is_spare(name.rsplit(".", 1)[0]):
module.disable()
n_spare += 1
else:
module.disable()
n_act += 1
return n_spare, n_act
def compressed_device_map(model, gpu_mem_fraction: float = 0.85) -> dict:
max_memory = {k: v * gpu_mem_fraction for k, v in get_max_memory().items()}
no_split = set()
for name, module in model.named_modules():
if name.endswith((".layers.0", ".blocks.0", ".transformer_blocks.0")):
no_split.add(module.__class__.__name__)
special_dtypes = {}
for name, module in model.named_modules():
if (
hasattr(module, "weight")
and hasattr(module, "weight_quantizer")
and getattr(module.weight_quantizer, "is_enabled", True)
and not getattr(module.weight_quantizer, "fake_quant", True)
):
nb = module.weight_quantizer.num_bits
if isinstance(nb, tuple):
nb = nb[0] + nb[1] + 1
special_dtypes[name + ".weight"] = CustomDtype.FP8 if nb == 8 else CustomDtype.INT4
return infer_auto_device_map(
model, max_memory=max_memory,
no_split_module_classes=list(no_split), special_dtypes=special_dtypes,
)
def _materialize_residual_meta(model) -> int:
n = 0
for _, module in model.named_modules():
for bn, buf in list(module._buffers.items()):
if buf is not None and getattr(buf, "is_meta", False):
module._buffers[bn] = torch.zeros(buf.shape, dtype=buf.dtype, device="cuda")
n += 1
for pn, par in list(module._parameters.items()):
if par is not None and getattr(par, "is_meta", False):
module._parameters[pn] = torch.nn.Parameter(
torch.zeros(par.shape, dtype=par.dtype, device="cuda"), requires_grad=False
)
n += 1
return n
def _transformer_dir() -> str:
local_root = snapshot_download(SRC_REPO, allow_patterns=["transformer/*"])
return os.path.join(local_root, "transformer")
def build_quantized_transformer(fmt: str, gpu_mem_fraction: float = 0.85):
"""The proven path: empty-on-meta -> quantize -> compress -> stream shards in."""
transformer_dir = _transformer_dir()
print(f"[build] empty transformer on meta from {transformer_dir}")
config = Cosmos3OmniTransformer.load_config(transformer_dir)
with init_empty_weights(include_buffers=False):
model = Cosmos3OmniTransformer.from_config(config)
print(f"[build] inserting weight-only {fmt} quantizers")
mtq.quantize(model, build_quant_cfg(fmt))
n_spare, n_act = enforce_weight_only_and_spare(model)
print(f"[build] weight-only: disabled {n_act} activation quantizers; {n_spare} spare weight layers")
print("[build] setting up compressed parameter shapes")
try:
mtq.compress(model, config=mtq.CompressConfig(quant_gemm=False))
except (AttributeError, TypeError):
mtq.compress(model)
print("[build] streaming BF16 shards into compressed form (slow step)")
load_checkpoint_in_model(
model, checkpoint=transformer_dir,
device_map=compressed_device_map(model, gpu_mem_fraction), dtype=torch.bfloat16,
)
fixed = _materialize_residual_meta(model)
if fixed:
print(f"[build] materialized {fixed} residual meta tensors")
return model
# --- optional fast-restart cache (modelopt_state + weights, per ModelOpt docs) ----------
def _cache_paths(cache_dir: str, fmt: str):
return (os.path.join(cache_dir, f"modelopt_state_{fmt}.pt"),
os.path.join(cache_dir, f"weights_{fmt}.pt"))
def save_quantized(model, fmt: str, cache_dir: str) -> None:
try:
from modelopt.torch.opt import modelopt_state
except ImportError:
from modelopt.torch.opt.conversion import modelopt_state
os.makedirs(cache_dir, exist_ok=True)
state_path, weights_path = _cache_paths(cache_dir, fmt)
print(f"[cache] writing {state_path} + {weights_path} (large; one time)")
torch.save(modelopt_state(model), state_path)
torch.save(model.state_dict(), weights_path)
def try_restore_quantized(fmt: str, cache_dir: str):
state_path, weights_path = _cache_paths(cache_dir, fmt)
if not (os.path.isfile(state_path) and os.path.isfile(weights_path)):
return None
try:
try:
from modelopt.torch.opt import restore_from_modelopt_state
except ImportError:
from modelopt.torch.opt.conversion import restore_from_modelopt_state
print(f"[cache] restoring from {state_path}")
config = Cosmos3OmniTransformer.load_config(_transformer_dir())
with init_empty_weights(include_buffers=False):
model = Cosmos3OmniTransformer.from_config(config)
state = torch.load(state_path, map_location="cpu", weights_only=False)
restore_from_modelopt_state(model, state)
weights = torch.load(weights_path, map_location="cpu", weights_only=False)
model.load_state_dict(weights, strict=False, assign=True)
_materialize_residual_meta(model)
print("[cache] restore OK")
return model
except Exception as e:
import traceback
print(f"[cache] restore failed ({type(e).__name__}: {e}); falling back to full rebuild")
traceback.print_exc()
return None
def _force_fixed_step_schedule(scheduler) -> bool:
"""See module docstring point 2. Patches THIS scheduler instance's set_timesteps
to always use the checkpoint's own fixed_step_sampler_config.t_list, regardless of
whatever num_inference_steps Cosmos3OmniPipeline.__call__ passes in internally.
ALSO disables stochastic_sampling (forces the deterministic ODE branch) -- CONFIRMED
by direct A/B render (same seed/image/prompt), not speculative. This checkpoint's
scheduler ships stochastic_sampling=True (SDE). Cosmos3's image-conditioning anchors
frame 0 by zeroing the model's predicted velocity there; that only means "leave this
position unchanged" under the deterministic step (prev_sample = sample + dt*0). The
SDE branch instead computes x0 = sample - current_sigma*model_output (= sample when
velocity is 0), then prev_sample = (1-next_sigma)*x0 + next_sigma*randn_tensor(...) --
it re-noises by next_sigma regardless of velocity, with no zero-velocity no-op. Over
this checkpoint's 4 steps that compounds to ~99.6% fresh noise in the conditioned
frame by the end: the input image comes out as colorful static while the genuinely
denoised motion frames still look like a plausible video. Disabling stochastic_
sampling restores the correct zero-velocity-is-a-no-op behavior."""
cfg = getattr(scheduler.config, "fixed_step_sampler_config", None)
t_list = cfg.get("t_list") if isinstance(cfg, dict) else None
if not t_list:
print("[warn] no fixed_step_sampler_config.t_list on this scheduler; leaving set_timesteps unpatched")
return False
_orig = scheduler.set_timesteps
def _patched(num_inference_steps=None, device=None, sigmas=None, mu=None, timesteps=None):
return _orig(sigmas=list(t_list), device=device)
scheduler.set_timesteps = _patched
if getattr(scheduler.config, "stochastic_sampling", False):
scheduler.register_to_config(stochastic_sampling=False)
print("[build] disabled stochastic_sampling (SDE re-noising corrupts the "
"image-conditioned frame -- see docstring)")
print(f"[build] forced fixed {len(t_list)}-step sde schedule: {t_list}")
return True
# --- pipeline assembly --------------------------------------------------------------
def make_pipeline(model):
from diffusers import Cosmos3OmniPipeline
model.to("cuda")
for m in model.modules():
for bn, buf in list(m._buffers.items()):
if buf is not None and buf.dtype == torch.float32:
m._buffers[bn] = buf.to(torch.bfloat16)
def _cast_bf16(_m, args):
return tuple(
a.to(torch.bfloat16)
if torch.is_tensor(a) and a.is_floating_point() and a.dtype != torch.bfloat16 else a
for a in args
)
for name, m in model.named_modules():
if "time_embedder" in name and hasattr(m, "linear_1"):
m.register_forward_pre_hook(_cast_bf16)
pipe = Cosmos3OmniPipeline.from_pretrained(
SRC_REPO, transformer=model, torch_dtype=torch.bfloat16,
enable_safety_checker=False, # local single-user server; revisit if exposing it
)
# Keep the shipped FlowMatchEulerDiscreteScheduler object, but patch its
# set_timesteps to actually honor the checkpoint's fixed 4-step sde schedule.
_force_fixed_step_schedule(pipe.scheduler)
for name, comp in pipe.components.items():
if name != "transformer" and isinstance(comp, torch.nn.Module):
comp.to("cuda")
return pipe
# --- HTTP server ------------------------------------------------------------------------
STATE: dict = {}
_gen_lock = asyncio.Lock() # one generation at a time on a single GPU
def _run_i2v(pil_image, prompt, negative_prompt, num_frames, fps, height, width, seed) -> tuple[bytes, str]:
pipe = STATE["pipe"]
image = pil_image.convert("RGB") # the pipeline resizes this to (height, width)
gen = torch.Generator(device="cuda").manual_seed(int(seed)) if seed >= 0 else None
with torch.inference_mode():
# num_inference_steps is a no-op once _force_fixed_step_schedule has patched
# the scheduler (it always substitutes the checkpoint's own t_list).
# guidance_scale=1.0 disables CFG -- see module docstring point 3.
result = pipe(
prompt=prompt, negative_prompt=negative_prompt,
image=image, num_frames=num_frames, fps=fps,
height=height, width=width,
num_inference_steps=4, guidance_scale=1.0,
generator=gen, output_type="pil",
)
frames = result.video
try:
with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tf:
path = tf.name
export_to_video(frames, path, fps=int(round(fps)))
media = "video/mp4"
except Exception: # no mp4 backend installed -> GIF (PIL-only, always works)
with tempfile.NamedTemporaryFile(suffix=".gif", delete=False) as tf:
path = tf.name
export_to_gif(frames, path)
media = "image/gif"
data = open(path, "rb").read()
os.remove(path)
del result
gc.collect()
torch.cuda.empty_cache()
return data, media
@contextlib.asynccontextmanager
async def lifespan(app: FastAPI):
fmt = STATE["fmt"]
repo = STATE.get("repo")
if repo:
from load_cosmos3_modelopt import load_pipe
path = repo
if not os.path.isdir(path):
from huggingface_hub import snapshot_download as _snap
print(f"[serve] fetching full snapshot of {repo} from the Hub ...")
path = _snap(repo)
print(f"[serve] loading drop-in checkpoint: {path}")
pipe = load_pipe(path)
# load_cosmos3_modelopt.load_pipe() is shared with the base model and doesn't
# know about this checkpoint's fixed-step schedule -- patch it here instead.
_force_fixed_step_schedule(pipe.scheduler)
STATE["pipe"] = pipe
print(f"[serve] ready (drop-in: {repo}, {fmt.upper()} assumed from repo contents)")
yield
STATE.clear()
return
cache_dir = STATE.get("cache_dir")
model = None
if cache_dir:
model = try_restore_quantized(fmt, cache_dir)
if model is None:
model = build_quantized_transformer(fmt, STATE["gpu_mem_fraction"])
if cache_dir:
try:
save_quantized(model, fmt, cache_dir)
except Exception as e:
print(f"[cache] save failed ({type(e).__name__}: {e}); continuing without cache")
STATE["pipe"] = make_pipeline(model)
print(f"[ready] serving {fmt.upper()} Cosmos3-Super-Image2Video-4Step on diffusers")
yield
STATE.clear()
app = FastAPI(lifespan=lifespan)
@app.get("/health")
async def health():
return {"status": "ok" if "pipe" in STATE else "loading", "format": STATE.get("fmt")}
@app.post("/animate")
async def animate(
image: UploadFile = File(...),
prompt: str = Form(...),
negative_prompt: str = Form(""),
num_frames: int = Form(49), # ~2.04s @ 24fps; 4n+1 maps cleanly to the VAE's 4x temporal compression
fps: float = Form(24.0),
height: int = Form(1024),
width: int = Form(1024),
seed: int = Form(1234), # pass -1 for a random clip each call
):
pil = Image.open(io.BytesIO(await image.read()))
async with _gen_lock:
loop = asyncio.get_running_loop()
data, media = await loop.run_in_executor(
None, _run_i2v, pil, prompt, negative_prompt, num_frames, fps, height, width, seed,
)
return Response(content=data, media_type=media)
if __name__ == "__main__":
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--format", choices=["fp8", "nvfp4"], default="fp8")
ap.add_argument("--repo", default=None,
help="Serve a published drop-in checkpoint: an HF repo id or a local "
"repackaged dir. Skips the bf16 rebuild entirely; requires "
"load_cosmos3_modelopt.py next to this file.")
ap.add_argument("--host", default="0.0.0.0")
ap.add_argument("--port", type=int, default=8000)
ap.add_argument("--gpu-mem-fraction", type=float, default=0.85)
ap.add_argument("--cache", default=None,
help="Dir for a fast-restart cache. First boot rebuilds + writes it; "
"later boots restore from it. Any restore error -> full rebuild.")
args = ap.parse_args()
STATE.update(
fmt=args.format, gpu_mem_fraction=args.gpu_mem_fraction, cache_dir=args.cache, repo=args.repo,
)
import uvicorn
uvicorn.run(app, host=args.host, port=args.port)
|