tailot commited on
Commit
ff7acef
·
verified ·
1 Parent(s): 7caae58

Upload folder using huggingface_hub

Browse files
Files changed (6) hide show
  1. README.md +2 -2
  2. app.py +57 -50
  3. config.json +3 -3
  4. model.safetensors +1 -1
  5. model_distilled.pt +3 -0
  6. requirements.txt +0 -1
README.md CHANGED
@@ -31,7 +31,7 @@ the two weights this project publishes:
31
  | Model | Cosa è / What it is | Quando sceglierlo / Pick it when |
32
  |---|---|---|
33
  | 🔍 **Universal Upscale** (default) | LIIF v4 GAN, 4.60 M parametri, `model_scripted.pt` con tiled inference integrata | qualsiasi fattore 1x-9.2x o una dimensione target esatta, immagini grandi |
34
- | ⚡ **Universal Upscale Distilled** | student knowledge-distilled, 1.30 M parametri, ONNX single-file `model.onnx` (5.2 MB) | serve 9x in un solo passaggio: più leggero e veloce |
35
 
36
  Lo Student distillato ha una scala nativa **fissa**: sotto 9x l'output viene
37
  ricampionato in area-downsample, sopra viene ricampionato in bicubico. La riga di stato indica
@@ -42,7 +42,7 @@ quale modello ha girato e se l'output è stato ricampionato. Model card:
42
 
43
  1. **Carica un'immagine / Upload an image**: trascina o carica un'immagine dal tuo dispositivo.
44
  2. **Scegli il modello / Pick the model**: lascia **Universal Upscale** (default) oppure passa a
45
- **Universal Upscale Distilled** (ONNX, nativo 9x).
46
  3. **Imposta la scala / Set the scale**:
47
  - **Scale Factor**: scegli un fattore di scala da **1x fino a 9.2x** (passo 0.2x, es. 2x, 3.6x, 4x, 9.2x).
48
  - **Target Long Side (px)**: in alternativa, specifica la dimensione in pixel del lato più lungo desiderato (es. 2048, 3000, 4096).
 
31
  | Model | Cosa è / What it is | Quando sceglierlo / Pick it when |
32
  |---|---|---|
33
  | 🔍 **Universal Upscale** (default) | LIIF v4 GAN, 4.60 M parametri, `model_scripted.pt` con tiled inference integrata | qualsiasi fattore 1x-9.2x o una dimensione target esatta, immagini grandi |
34
+ | ⚡ **Universal Upscale Distilled** | student knowledge-distilled, 1.30 M parametri, TorchScript `model_distilled.pt` che gira sullo stesso device del modello di default | serve 9x in un solo passaggio: più leggero e veloce |
35
 
36
  Lo Student distillato ha una scala nativa **fissa**: sotto 9x l'output viene
37
  ricampionato in area-downsample, sopra viene ricampionato in bicubico. La riga di stato indica
 
42
 
43
  1. **Carica un'immagine / Upload an image**: trascina o carica un'immagine dal tuo dispositivo.
44
  2. **Scegli il modello / Pick the model**: lascia **Universal Upscale** (default) oppure passa a
45
+ **Universal Upscale Distilled** (nativo 9x, usa la stessa GPU del modello di default).
46
  3. **Imposta la scala / Set the scale**:
47
  - **Scale Factor**: scegli un fattore di scala da **1x fino a 9.2x** (passo 0.2x, es. 2x, 3.6x, 4x, 9.2x).
48
  - **Target Long Side (px)**: in alternativa, specifica la dimensione in pixel del lato più lungo desiderato (es. 2048, 3000, 4096).
app.py CHANGED
@@ -28,8 +28,11 @@ except ImportError:
28
  return fn
29
 
30
  MODEL_PATH = Path(__file__).resolve().parent / "model_scripted.pt"
31
- DISTILLED_ONNX_PATH = Path(__file__).resolve().parent / "model.onnx"
32
- # If the Space folder does not carry model.onnx, it is pulled from the distilled repo on first use.
 
 
 
33
  DISTILLED_REPO_ID = "tailot/universal-upscale-distilled"
34
 
35
  MAX_SIDE = 4096
@@ -44,7 +47,7 @@ DISTILLED_NATIVE = 9
44
  # Plain strings (not (label, value) tuples) so the dropdown works on any Gradio 5 minor version;
45
  # upscale() maps the label back to a model key.
46
  MODEL_LIIF = "🔍 Universal Upscale - arbitrary scale 1x-9.2x (default)"
47
- MODEL_DISTILLED = "⚡ Universal Upscale Distilled - ONNX, native 9x"
48
  MODEL_CHOICES = [MODEL_LIIF, MODEL_DISTILLED]
49
 
50
 
@@ -52,11 +55,11 @@ def normalize_model_choice(value) -> str:
52
  """Map the dropdown selection to a model key.
53
 
54
  Gradio pads short example rows with None, and a Dropdown can hand back either the label or
55
- the value, so the mapping is deliberately loose: anything mentioning "distilled" is the ONNX
56
- student, everything else stays on the published default.
57
  """
58
  text = str(value or "").strip().lower()
59
- return "distilled" if "distill" in text or text in ("onnx", "student") else "liif"
60
 
61
 
62
  def load_model(device: str):
@@ -76,61 +79,64 @@ def resolve_factor(input_size, scale, target_long_side):
76
  return factor, ""
77
 
78
 
79
- _ONNX_SESSION = None
80
- _ONNX_NATIVE = 0
81
 
82
 
83
- def onnx_session():
84
- """ONNX Runtime session for the distilled student, built once per container.
85
 
86
- The memory arena and the memory pattern are switched off on purpose: tiled inference hands
87
- ONNX Runtime a different shape at every tile, and its pattern allocator keeps the biggest
88
- tile's buffers alive until the Space runs out of RAM.
89
  """
90
- global _ONNX_SESSION, _ONNX_NATIVE
91
- if _ONNX_SESSION is not None:
92
- return _ONNX_SESSION
93
- import onnxruntime as ort
94
-
95
- path = DISTILLED_ONNX_PATH
96
  if not path.exists():
97
  from huggingface_hub import hf_hub_download
98
 
99
- path = Path(hf_hub_download(repo_id=DISTILLED_REPO_ID, filename="model.onnx"))
100
- options = ort.SessionOptions()
101
- options.enable_cpu_mem_arena = False
102
- options.enable_mem_pattern = False
103
- options.log_severity_level = 3
104
- available = ort.get_available_providers()
105
- providers = [p for p in ("CUDAExecutionProvider", "CPUExecutionProvider") if p in available]
106
- session = ort.InferenceSession(str(path), sess_options=options, providers=providers or None)
107
- # Read the fixed factor from the model itself instead of trusting a constant.
108
- probe = session.run(None, {session.get_inputs()[0].name: np.zeros((1, 3, 8, 9), dtype=np.float32)})[0]
109
- _ONNX_NATIVE, _ONNX_SESSION = int(round(max(probe.shape[-2] / 8.0, probe.shape[-1] / 9.0))), session
110
- return session
111
-
112
-
113
- def upscale_distilled(rgb, scale, native, out_w, out_h, tile: int = 64, pad: int = 6):
 
 
 
114
  """Tiled inference in LR space for the fixed-factor student.
115
 
116
  Same recipe as scripts/upscale_onnx.py: run the padded tile, resample the WHOLE padded tile
117
  to its own target size (so the crop offset stays inside it), then paste the crop on the canvas.
118
  """
119
- session = onnx_session()
120
- input_name = session.get_inputs()[0].name
121
  height, width = rgb.shape[:2]
122
  canvas = np.zeros((out_h, out_w, 3), dtype=np.uint8)
123
  resample = Image.Resampling.LANCZOS if native > scale else Image.Resampling.BICUBIC
124
  tile = max(8, min(int(tile), height))
 
 
 
 
125
  for top in range(0, height, tile):
126
  for left in range(0, width, tile):
127
  bottom, right = min(top + tile, height), min(left + tile, width)
128
- y0, y1 = max(top - pad, 0), min(bottom + pad, height)
129
- x0, x1 = max(left - pad, 0), min(right + pad, width)
130
  patch = rgb[y0:y1, x0:x1].astype(np.float32) / 255.0
131
- tensor = np.ascontiguousarray(patch.transpose(2, 0, 1)[None, :, :, :])
132
- sr = np.clip(session.run(None, {input_name: tensor})[0][0], 0.0, 1.0)
133
- sr = (sr.transpose(1, 2, 0) * 255.0).round().astype(np.uint8)
 
134
  full_w, full_h = int(round((y1 - y0) * scale)), int(round((x1 - x0) * scale))
135
  sr = np.asarray(Image.fromarray(sr).resize((full_w, full_h), resample))
136
  th, tw = int(round((bottom - top) * scale)), int(round((right - left) * scale))
@@ -162,15 +168,15 @@ def upscale(image, model_choice=None, scale: float = 4.0, target_long_side: floa
162
 
163
  if choice == "distilled":
164
  try:
165
- session = onnx_session()
166
  except Exception as exc:
167
- return None, f"Distilled ONNX unavailable: {exc}"
168
- native = _ONNX_NATIVE or DISTILLED_NATIVE
169
- arr = upscale_distilled(np.asarray(img, dtype=np.uint8), applied, native, out_w, out_h)
170
- provider = (session.get_providers()[0] or "CPU").replace("ExecutionProvider", "")
171
- model_note = f"Universal Upscale Distilled (ONNX, native {native}x"
172
  model_note += ", output resampled)" if abs(applied - native) > 1e-3 else ")"
173
- device = f"onnxruntime/{provider}"
174
  else:
175
  device = "cuda" if torch.cuda.is_available() else "cpu"
176
  model = load_model(device)
@@ -209,8 +215,9 @@ demo = gr.Interface(
209
  description=(
210
  "Pick the engine in the **Model** menu: **Universal Upscale** renders any factor from 1x up to the tested "
211
  "maximum 9.2x (or an exact target resolution), **Universal Upscale Distilled** is the 1.3 M "
212
- "parameter ONNX student that renders 9x natively in one pass and resamples it for "
213
- "other factors. Tiled inference keeps high-resolution upscaling inside memory (outputs are capped at "
 
214
  "4096 px on the long side); the status line reports which model ran and what it did."
215
  ),
216
  examples=example_files if example_files else None,
 
28
  return fn
29
 
30
  MODEL_PATH = Path(__file__).resolve().parent / "model_scripted.pt"
31
+ # The distilled student ships beside it as its own TorchScript file: the same torch runtime the
32
+ # default engine already uses, so the student lands on the Space GPU. The ONNX build was dropped
33
+ # because the Space installs the CPU-only `onnxruntime` wheel, which pinned it to CPU.
34
+ DISTILLED_PT_PATH = Path(__file__).resolve().parent / "model_distilled.pt"
35
+ # If the Space folder does not carry model_distilled.pt, it is pulled from the distilled repo on first use.
36
  DISTILLED_REPO_ID = "tailot/universal-upscale-distilled"
37
 
38
  MAX_SIDE = 4096
 
47
  # Plain strings (not (label, value) tuples) so the dropdown works on any Gradio 5 minor version;
48
  # upscale() maps the label back to a model key.
49
  MODEL_LIIF = "🔍 Universal Upscale - arbitrary scale 1x-9.2x (default)"
50
+ MODEL_DISTILLED = "⚡ Universal Upscale Distilled - fixed 9x, runs on GPU"
51
  MODEL_CHOICES = [MODEL_LIIF, MODEL_DISTILLED]
52
 
53
 
 
55
  """Map the dropdown selection to a model key.
56
 
57
  Gradio pads short example rows with None, and a Dropdown can hand back either the label or
58
+ the value, so the mapping is deliberately loose: anything mentioning "distilled" (or the old
59
+ ONNX labels) is the fixed-factor student, everything else stays on the published default.
60
  """
61
  text = str(value or "").strip().lower()
62
+ return "distilled" if "distill" in text or text in ("onnx", "student", "torchscript") else "liif"
63
 
64
 
65
  def load_model(device: str):
 
79
  return factor, ""
80
 
81
 
82
+ _DISTILLED = None # (model, native factor, device), built once per container
 
83
 
84
 
85
+ def distilled_model():
86
+ """(model, native factor, device) for the distilled student, built once per container.
87
 
88
+ The student is loaded with the same torch runtime as the default engine, so it runs on the
89
+ Space GPU whenever there is one. ONNX Runtime was dropped on purpose: the Space installs the
90
+ CPU-only `onnxruntime` wheel, so the ONNX build of these weights could never reach the GPU.
91
  """
92
+ global _DISTILLED
93
+ if _DISTILLED is not None:
94
+ return _DISTILLED
95
+ path = DISTILLED_PT_PATH
 
 
96
  if not path.exists():
97
  from huggingface_hub import hf_hub_download
98
 
99
+ path = Path(hf_hub_download(repo_id=DISTILLED_REPO_ID, filename="model_scripted.pt"))
100
+ device = "cuda" if torch.cuda.is_available() else "cpu"
101
+ model = torch.jit.load(str(path), map_location=device)
102
+ model.eval()
103
+ with torch.no_grad():
104
+ # Read the fixed factor from the model itself instead of trusting a constant.
105
+ probe = model(torch.zeros(1, 3, 8, 9, device=device))
106
+ native = int(round(max(probe.shape[-2] / 8.0, probe.shape[-1] / 9.0)))
107
+ # Warm the kernels with one full window (tile 64 + 2*6 padding, the defaults of
108
+ # upscale_distilled below) so the first real request does not pay for cuDNN kernel
109
+ # selection, which costs seconds on a cold GPU container.
110
+ model(torch.zeros(1, 3, 76, 76, device=device))
111
+ _DISTILLED = (model, native, device)
112
+ return _DISTILLED
113
+
114
+
115
+ def upscale_distilled(rgb, scale, native, out_w, out_h, model=None, device="cpu",
116
+ tile: int = 64, pad: int = 6):
117
  """Tiled inference in LR space for the fixed-factor student.
118
 
119
  Same recipe as scripts/upscale_onnx.py: run the padded tile, resample the WHOLE padded tile
120
  to its own target size (so the crop offset stays inside it), then paste the crop on the canvas.
121
  """
 
 
122
  height, width = rgb.shape[:2]
123
  canvas = np.zeros((out_h, out_w, 3), dtype=np.uint8)
124
  resample = Image.Resampling.LANCZOS if native > scale else Image.Resampling.BICUBIC
125
  tile = max(8, min(int(tile), height))
126
+ # Every window keeps the SAME size: at the borders it slides inward instead of shrinking.
127
+ # TorchScript re-specialises its fused convolutions for the first window of each new shape
128
+ # (~0.3 s each), so a variable window would make the first request pay for up to nine shapes.
129
+ win_h, win_w = min(tile + 2 * pad, height), min(tile + 2 * pad, width)
130
  for top in range(0, height, tile):
131
  for left in range(0, width, tile):
132
  bottom, right = min(top + tile, height), min(left + tile, width)
133
+ y0, x0 = max(0, min(top - pad, height - win_h)), max(0, min(left - pad, width - win_w))
134
+ y1, x1 = y0 + win_h, x0 + win_w
135
  patch = rgb[y0:y1, x0:x1].astype(np.float32) / 255.0
136
+ tensor = torch.from_numpy(patch).permute(2, 0, 1).unsqueeze(0).to(device)
137
+ with torch.no_grad():
138
+ sr = model(tensor)[0].clamp(0.0, 1.0).permute(1, 2, 0).cpu().numpy()
139
+ sr = (sr * 255.0).round().astype(np.uint8)
140
  full_w, full_h = int(round((y1 - y0) * scale)), int(round((x1 - x0) * scale))
141
  sr = np.asarray(Image.fromarray(sr).resize((full_w, full_h), resample))
142
  th, tw = int(round((bottom - top) * scale)), int(round((right - left) * scale))
 
168
 
169
  if choice == "distilled":
170
  try:
171
+ model, native, device = distilled_model()
172
  except Exception as exc:
173
+ return None, f"Distilled model unavailable: {exc}"
174
+ native = native or DISTILLED_NATIVE
175
+ arr = upscale_distilled(np.asarray(img, dtype=np.uint8), applied, native, out_w, out_h,
176
+ model=model, device=device)
177
+ model_note = f"Universal Upscale Distilled (TorchScript, native {native}x"
178
  model_note += ", output resampled)" if abs(applied - native) > 1e-3 else ")"
179
+ device = f"torch/{device}"
180
  else:
181
  device = "cuda" if torch.cuda.is_available() else "cpu"
182
  model = load_model(device)
 
215
  description=(
216
  "Pick the engine in the **Model** menu: **Universal Upscale** renders any factor from 1x up to the tested "
217
  "maximum 9.2x (or an exact target resolution), **Universal Upscale Distilled** is the 1.3 M "
218
+ "parameter student that renders 9x natively in one pass on the same device as the "
219
+ "default engine and resamples its output for other factors. Tiled inference keeps high-resolution "
220
+ "upscaling inside memory (outputs are capped at "
221
  "4096 px on the long side); the status line reports which model ran and what it did."
222
  ),
223
  examples=example_files if example_files else None,
config.json CHANGED
@@ -65,7 +65,7 @@
65
  },
66
  "export": {
67
  "torch_version": "2.14.1+cu130",
68
- "exported_at": "2026-10-25T21:52:14+00:00",
69
  "method": "torch.jit.script + torch.jit.freeze",
70
  "verification": {
71
  "checks": [
@@ -169,8 +169,8 @@
169
  "scales": "arbitrary"
170
  },
171
  "distilled": {
172
- "file": "model.onnx",
173
- "type": "onnx",
174
  "repo_id": "tailot/universal-upscale-distilled",
175
  "native_scale": 9,
176
  "note": "fixed-factor student; other factors are resampled from the native output"
 
65
  },
66
  "export": {
67
  "torch_version": "2.14.1+cu130",
68
+ "exported_at": "2026-10-25T23:09:39+00:00",
69
  "method": "torch.jit.script + torch.jit.freeze",
70
  "verification": {
71
  "checks": [
 
169
  "scales": "arbitrary"
170
  },
171
  "distilled": {
172
+ "file": "model_distilled.pt",
173
+ "type": "torchscript",
174
  "repo_id": "tailot/universal-upscale-distilled",
175
  "native_scale": 9,
176
  "note": "fixed-factor student; other factors are resampled from the native output"
model.safetensors CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:3479f51ce4243f71f5c6ffd2d8dc46df82eb39537936df37f30867f3a196e45e
3
  size 18439020
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3d12afef19d94cf6922b8ede382c85db09cd696fbc28ed8ef964b7278343c2d8
3
  size 18439020
model_distilled.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fbfc0d38849bf09d43cf5f56da50bba63de099ea7bfaff4169741cf90328785f
3
+ size 5292159
requirements.txt CHANGED
@@ -3,5 +3,4 @@ numpy>=1.26
3
  Pillow>=10.2
4
  huggingface_hub>=0.28.1
5
  gradio>=5.0.0
6
- onnxruntime>=1.16
7
  spaces
 
3
  Pillow>=10.2
4
  huggingface_hub>=0.28.1
5
  gradio>=5.0.0
 
6
  spaces