File size: 6,344 Bytes
fcc874d
 
 
 
 
 
 
54b7620
fcc874d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54b7620
 
 
 
fcc874d
 
 
 
 
 
 
54b7620
 
 
 
fcc874d
 
 
 
 
 
 
 
 
 
 
54b7620
 
 
 
 
 
 
 
 
 
fcc874d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""OracleZoom recursive zoom, one image at a time, all in memory.

Flattened from src/opd_zoom/teacher/oracle_infer.py for the Space. Three differences, none
of them to the model: no disk round-trip between recursions, the zoom window can sit
anywhere instead of only the centre, and the loop is a generator so the UI can show each
level the moment it lands. Everything on one device, which is what ZeroGPU gives us.
"""
import gc
import os
import sys

import torch
from PIL import Image
from torchvision import transforms

from geometry import PROCESS_SIZE, resize_and_center_crop, zoom_window

VENDOR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "vendor")
if VENDOR not in sys.path:
    sys.path.insert(0, VENDOR)

REPO = "dipta007/OracleZoom"
SD3 = "stabilityai/stable-diffusion-3-medium-diffusers"
VLM = "Qwen/Qwen2.5-VL-3B-Instruct"
COZ_PROMPT = (
    "The second image is a zoom-in of the first image. Based on this knowledge, "
    "what is in the second image? Give me a set of words."
)
_to_tensor = transforms.Compose([transforms.ToTensor()])


class _SRArgs:
    def __init__(self, coz_ckpt):
        self.lora_path = f"{coz_ckpt}/SR_LoRA/model_20001.pkl"
        self.vae_path = f"{coz_ckpt}/SR_VAE/vae_encoder_20001.pt"
        self.pretrained_model_name_or_path = SD3
        self.process_size = PROCESS_SIZE
        self.lora_rank = 4
        self.merge_and_unload_lora = False
        self.mixed_precision = "fp16"


class Models:
    """Built once at import, read-only afterwards. Concurrent requests only read."""

    def __init__(self, weights=None):
        from huggingface_hub import snapshot_download

        w = weights or snapshot_download(repo_id=REPO)
        self.vlm, self.proc, self.process_vision_info = _build_vlm(f"{w}/ckpt/VLM_LoRA/checkpoint-10000")
        self.sr = _build_sr(f"{w}/ckpt", f"{w}/merged_transformer.safetensors")


def _build_vlm(lora_path):
    from transformers import Qwen2_5_VLForConditionalGeneration, AutoProcessor
    from qwen_vl_utils import process_vision_info
    from peft import PeftModel

    # device_map="auto" asks accelerate to read real GPU memory, which does not exist at
    # module scope on ZeroGPU. Place it ourselves. sdpa because flash-attn has no wheel here.
    model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
        VLM, torch_dtype=torch.bfloat16, attn_implementation="sdpa"
    )
    # torch_device="cpu" is load-bearing. Left to itself peft calls infer_device(), which reads
    # the cuda flag ZeroGPU patches to True, then hands "cuda" to safetensors. That builds
    # tensors straight on a device that is not attached yet: RuntimeError, no CUDA GPUs.
    model = PeftModel.from_pretrained(model, lora_path, torch_device="cpu").merge_and_unload()
    return model.eval().to("cuda"), AutoProcessor.from_pretrained(VLM), process_vision_info


def _build_sr(coz_ckpt, merged_transformer):
    from safetensors.torch import load_file
    from osediff_sd3 import OSEDiff_SD3_TEST, SD3Euler

    # Assemble entirely on CPU, then move once. Two places here materialise weights straight
    # onto the model's own device (inject_lora's torch.load map_location, and safetensors), and
    # on ZeroGPU that device reads as cuda while no GPU is attached yet.
    sr = SD3Euler(device="cpu")
    # Construct first, load second. OSEDiff_SD3_TEST swaps every targeted Linear for a
    # LoraInjectedLinear, which renames the keys. Our merged checkpoint was saved after that
    # swap, so loading it earlier matches nothing and silently leaves the base transformer.
    test = OSEDiff_SD3_TEST(_SRArgs(coz_ckpt), sr)
    sd = load_file(merged_transformer)
    missing, unexpected = test.model.transformer.load_state_dict(
        {k: v.to(torch.float32) for k, v in sd.items()}, strict=False)
    if len(unexpected) > len(sd) // 2:
        raise RuntimeError(f"merged transformer did not match: {len(unexpected)} of {len(sd)} "
                           f"keys unexpected. LoRA injection order or checkpoint is wrong.")
    print(f"#### merged transformer loaded (missing {len(missing)} unexpected {len(unexpected)})")
    del sd
    gc.collect()

    for m in (sr.text_enc_1, sr.text_enc_2, sr.text_enc_3):
        m.to("cuda")
    sr.transformer.to("cuda", dtype=torch.float32)
    sr.vae.to("cuda", dtype=torch.float32)
    for m in (sr.text_enc_1, sr.text_enc_2, sr.text_enc_3, sr.transformer, sr.vae):
        m.requires_grad_(False)
    sr.device = "cuda"   # encode_prompt and set_timesteps read this, not the module devices
    return test


def write_prompt(models, first, second, max_new_tokens=32):
    messages = [
        {"role": "system", "content": COZ_PROMPT},
        {"role": "user", "content": [{"type": "image", "image": first},
                                     {"type": "image", "image": second}]},
    ]
    text = models.proc.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
    images, videos = models.process_vision_info(messages)
    inputs = models.proc(text=[text], images=images, videos=videos,
                         padding=True, return_tensors="pt").to("cuda")
    gen = models.vlm.generate(**inputs, max_new_tokens=max_new_tokens)
    trimmed = [o[len(i):] for i, o in zip(inputs.input_ids, gen)]
    return models.proc.batch_decode(trimmed, skip_special_tokens=True,
                                    clean_up_tokenization_spaces=False)[0].strip()


def super_resolve(models, img, prompt):
    lq = _to_tensor(img).unsqueeze(0).to("cuda") * 2 - 1
    with torch.no_grad():
        out = torch.clamp(models.sr(lq, prompt=prompt)[0].cpu(), -1.0, 1.0)
    return transforms.ToPILImage()(out * 0.5 + 0.5)


def zoom(models, image, levels=4, upscale=4, center=(0.5, 0.5)):
    """Yield one (level, factor, prompt, blurry_input, result) per recursion.

    `blurry_input` is the plain bicubic enlargement the model starts from. It is what the
    super-resolution has to improve on, so it doubles as the honest before-picture.
    """
    cur = resize_and_center_crop(image)
    yield 0, 1, "", cur, cur
    for i in range(levels):
        blurry = zoom_window(cur, upscale, center).resize(cur.size, Image.BICUBIC)
        prompt = write_prompt(models, cur, blurry)
        cur = super_resolve(models, blurry, prompt)
        yield i + 1, upscale ** (i + 1), prompt, blurry, cur