CoolFace
Apppublic

Upsampler/seedvr2-3b

sourceHugging Faceapache-2.0updated 1mo agoView on Hugging Face
0likes
app.py293 linesDownload Raw Back to root
1# SeedVR2-3B image restoration, Upsampler v3 recipe.2#3# Ported from ByteDance-Seed/SeedVR2-3B. What changed and why:4#5# 1. Image only. The upstream Space serves video and images from one entry6#    point, carrying the sequence-parallel plumbing, frame cutting and video7#    writing along with it. This Space backs an image tool, so that is all gone.8# 2. ONE @spaces.GPU entry. Upstream decorates configure_runner,9#    generation_step AND generation_loop, so a single request booked three GPU10#    allocations of 100s each. ZeroGPU checks the requested duration against the11#    visitor's remaining quota, and an unauthenticated visitor has 120 seconds a12#    day in total, so the upstream shape cannot serve an anonymous user at all.13#    Everything now runs inside one call with a measured dynamic duration.14# 3. No apex. Upstream installs a prebuilt `apex-0.1-cp310-...whl` and selects15#    `fusedrms` / `fusedln` norms in configs_3b/main.yaml. ZeroGPU runs Python16#    3.12, where that wheel does not install, so every norm layer then failed.17#    The config now selects the `rms` / `layer` paths that the same source file18#    already implements in pure PyTorch, with identical parameter names and19#    shapes so the checkpoint loads unchanged.20# 4. No hard flash-attn dependency. See models/dit_v2/attention.py.21#22# The model restores at a fixed working resolution regardless of input size (it23# was trained at high res and NaResize scales the input to meet it), so GPU cost24# per request is essentially constant and no tiling is involved. See WORK_AREA.25 26# Must precede the torch import: the allocator reads this at initialization.27#28# Fragmentation insurance, NOT the fix for the NVML assert this Space used to29# die on. That was measured: the ZeroGPU worker printed30# 'expandable_segments:True' on the runs that still crashed, so the setting was31# applied the whole time and made no difference. What actually decides it is32# WORK_AREA below. Kept because it costs nothing and reduces fragmentation.33import os34 35os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")36 37import gc38 39import mimetypes40from pathlib import Path41 42import gradio as gr43import spaces44import torch45import torch.nn.functional as F46from einops import rearrange47from omegaconf import OmegaConf48from PIL import Image49from huggingface_hub import hf_hub_download50from torchvision.transforms import Compose, Lambda, Normalize51import torchvision.transforms as T52 53from upsampler_theme import UPSAMPLER_CSS, UPSAMPLER_THEME, footer_html, header_html54 55from data.image.transforms.divisible_crop import DivisibleCrop56from data.image.transforms.na_resize import NaResize57from data.video.transforms.rearrange import Rearrange58from common.config import load_config59from common.distributed import init_torch60from common.seed import set_seed61from projects.video_diffusion_sr.infer import VideoDiffusionInfer62 63try:64    from projects.video_diffusion_sr.color_fix import wavelet_reconstruction65 66    USE_COLOR_FIX = True67except ImportError:68    USE_COLOR_FIX = False69    print("color fix unavailable; output will not be wavelet-reconstructed")70 71# Weights come from the official ByteDance repo rather than a re-upload. It is72# the canonical source for these files and is Apache-2.0, so there is no mirror73# in the chain that could change under us.74WEIGHTS_REPO = "ByteDance-Seed/SeedVR2-3B"75CKPT_DIR = Path("./ckpts")76CKPT_DIR.mkdir(exist_ok=True)77 78 79def _fetch(filename: str, target: Path) -> str:80    if target.exists():81        return str(target)82    path = hf_hub_download(repo_id=WEIGHTS_REPO, filename=filename)83    target.symlink_to(path)84    return str(target)85 86 87DIT_CKPT = _fetch("seedvr2_ema_3b.pth", CKPT_DIR / "seedvr2_ema_3b.pth")88VAE_CKPT = _fetch("ema_vae.pth", CKPT_DIR / "ema_vae.pth")89# The text branch is conditioned by two fixed embeddings shipped with the90# weights, so this Space runs no text encoder at all.91POS_EMB = _fetch("pos_emb.pt", Path("./pos_emb.pt"))92NEG_EMB = _fetch("neg_emb.pt", Path("./neg_emb.pt"))93 94# The resolution the model restores at, as an area. Upstream hardcodes95# 2560*1440 for images with `downsample_only=False`, meaning small inputs are96# scaled UP to it and large inputs down, because the model was only trained at97# high resolution.98#99# THIS is what decides whether the VAE decode survives on ZeroGPU. The100# decoder's peak contiguous allocation scales with it, and that allocation is101# what trips "NVML_SUCCESS == r INTERNAL ASSERT FAILED" in the caching102# allocator. Measured: 2560x1440 (the upstream value) fails, 1920x1080 passes103# in 14.7s. Left overridable so the ceiling can be probed without a code104# change, but do not raise the default without re-testing a real run.105#106# The cost is honest to state: the model was trained at high resolution, so107# restoring at 2MP rather than 3.7MP gives up some of its headroom on very108# large outputs. It still upscales ~4.9x from a 360x240 input.109WORK_AREA = int(os.environ.get("SEEDVR2_WORK_AREA", 1920 * 1080))110 111# Single-process "distributed" context. The model code routes every device112# placement through common.distributed, which reads these.113os.environ.setdefault("MASTER_ADDR", "127.0.0.1")114os.environ.setdefault("MASTER_PORT", "12355")115os.environ.setdefault("RANK", "0")116os.environ.setdefault("WORLD_SIZE", "1")117os.environ.setdefault("LOCAL_RANK", "0")118 119_runner = None120 121 122def _ensure_runner():123    """Build the runner once, inside a GPU context.124 125    Deliberately NOT done at module scope, even though ZeroGPU prefers that for126    placement: `init_torch` ends in `dist.init_process_group(backend="nccl")`127    and `torch.cuda.set_device`, which need a real device rather than the CUDA128    emulation that applies outside `@spaces.GPU`. Memoized because129    `init_process_group` raises if called twice, and because a warm worker130    should not reload 3B parameters per request.131    """132    global _runner133    if _runner is not None:134        return _runner135 136    if not torch.distributed.is_initialized():137        init_torch(cudnn_benchmark=False)138 139    runner = VideoDiffusionInfer(load_config(os.path.join("./configs_3b", "main.yaml")))140    OmegaConf.set_readonly(runner.config, False)141    runner.configure_dit_model(device="cuda", checkpoint=DIT_CKPT)142    runner.configure_vae_model()143    if hasattr(runner.vae, "set_memory_limit"):144        runner.vae.set_memory_limit(**runner.config.vae.memory_limit)145 146    _runner = runner147    return _runner148 149 150def _transform():151    return Compose(152        [153            NaResize(resolution=WORK_AREA**0.5, mode="area", downsample_only=False),154            Lambda(lambda x: torch.clamp(x, 0.0, 1.0)),155            DivisibleCrop((16, 16)),156            Normalize(0.5, 0.5),157            Rearrange("t c h w -> c t h w"),158        ]159    )160 161 162def _duration(image, steps=1, progress=None) -> int:163    """Every request restores at the same working resolution (WORK_AREA), so cost164    tracks the step count and little else. Measured on a cold worker, then165    given headroom; kept as small as honesty allows, because the request is166    checked against the visitor's remaining quota and a smaller one also ranks167    higher in the ZeroGPU queue."""168    return int(min(90, 14 + 12 * int(steps)))169 170 171@spaces.GPU(duration=_duration)172@torch.no_grad()173def upscale_image(image, steps=1, progress=gr.Progress(track_tqdm=True)):174    if image is None:175        raise gr.Error("Upload an image first.")176 177    # The allocator config has to be read by the process that actually owns the178    # CUDA context. ZeroGPU runs this function in its own worker, so an env var179    # exported at import time in the parent is not proof it applied here —180    # print what the worker sees, and set it directly as well.181    print(f"[alloc] PYTORCH_CUDA_ALLOC_CONF={os.environ.get('PYTORCH_CUDA_ALLOC_CONF')!r}", flush=True)182    try:183        torch.cuda.memory._set_allocator_settings("expandable_segments:True")184        print("[alloc] expandable_segments set at runtime", flush=True)185    except Exception as exc:  # older/newer torch may not expose this186        print(f"[alloc] runtime allocator setting unavailable: {exc}", flush=True)187    print(f"[alloc] work area {WORK_AREA} px", flush=True)188 189    runner = _ensure_runner()190 191    runner.config.diffusion.cfg.scale = 1.0192    runner.config.diffusion.cfg.rescale = 0.0193    runner.config.diffusion.timesteps.sampling.steps = int(steps)194    runner.configure_diffusion()195    # Fixed seed: a restorer should be deterministic for a given input, and the196    # knob was noise in a tool whose job is 'make this photo better'.197    set_seed(666, same_across_ranks=True)198 199    img = Image.open(image).convert("RGB") if isinstance(image, str) else image.convert("RGB")200    tensor = T.ToTensor()(img).unsqueeze(0)  # (t=1, c, h, w)201 202    cond = _transform()(tensor.to("cuda"))203    original = cond204    latents = runner.vae_encode([cond])205 206    text_embeds = {207        "texts_pos": [torch.load(POS_EMB).to("cuda")],208        "texts_neg": [torch.load(NEG_EMB).to("cuda")],209    }210 211    noise = [torch.randn_like(latent) for latent in latents]212    aug_noise = [torch.randn_like(latent) for latent in latents]213 214    def _add_noise(x, aug):215        t = torch.tensor([1000.0], device="cuda") * 0.1216        shape = torch.tensor(x.shape[1:], device="cuda")[None]217        return runner.schedule.forward(x, aug, runner.timestep_transform(t, shape))218 219    conditions = [220        runner.get_condition(n, task="sr", latent_blur=_add_noise(latent, a))221        for n, a, latent in zip(noise, aug_noise, latents)222    ]223 224    with torch.autocast("cuda", torch.bfloat16, enabled=True):225        videos = runner.inference(226            noises=noise, conditions=conditions, dit_offload=False, **text_embeds227        )228 229    sample = videos[0]230    sample = (231        rearrange(sample[:, None], "c t h w -> t c h w")232        if sample.ndim == 3233        else rearrange(sample, "c t h w -> t c h w")234    )235    reference = (236        rearrange(original[:, None], "c t h w -> t c h w")237        if original.ndim == 3238        else rearrange(original, "c t h w -> t c h w")239    )240    if USE_COLOR_FIX:241        sample = wavelet_reconstruction(sample.to("cpu"), reference[: sample.size(0)].to("cpu"))242    else:243        sample = sample.to("cpu")244 245    sample = rearrange(sample, "t c h w -> t h w c")246    sample = sample.clip(-1, 1).mul_(0.5).add_(0.5).mul_(255).round().to(torch.uint8).numpy()247 248    del latents, conditions, videos249    gc.collect()250    torch.cuda.empty_cache()251 252    return Image.fromarray(sample[0])253 254 255with gr.Blocks(css=UPSAMPLER_CSS, theme=UPSAMPLER_THEME) as demo:256    gr.HTML(257        header_html(258            "SeedVR2 3B Image Upscaler",259            "One-step diffusion restoration that rebuilds real detail in blurry, "260            "compressed, and low-resolution photos.",261        )262    )263 264    with gr.Row():265        with gr.Column():266            image_in = gr.Image(label="Image", type="filepath")267            steps = gr.Slider(1, 4, value=1, step=1, label="Steps")268            run = gr.Button("Upscale Image", variant="primary")269        with gr.Column():270            image_out = gr.Image(label="Result", type="pil")271 272    # No leading slash: gradio prefixes it, and "/upscale_image" here would273    # publish the endpoint as "//upscale_image".274    run.click(275        upscale_image,276        inputs=[image_in, steps],277        outputs=[image_out],278        api_name="upscale_image",279    )280 281    gr.HTML(282        footer_html(283            "SeedVR2-3B is ByteDance's one-step diffusion model for image and video "284            "restoration. It rebuilds genuine texture in photos that are blurry, "285            "heavily compressed, or simply too small, restoring at high resolution "286            "rather than smoothing detail away the way a conventional upscaler does.",287            "https://upsampler.com/free-image-upscaler-no-signup",288            "free image upscaler",289        )290    )291 292demo.launch(ssr_mode=False, show_error=True)293