Nekochu commited on
Commit
6ee4bac
·
verified ·
1 Parent(s): d65d5b5

Switch to ONNX INT8 DiT

Browse files
Files changed (1) hide show
  1. app.py +162 -76
app.py CHANGED
@@ -1,91 +1,87 @@
1
- """FE2E: Depth + Normal estimation from a single image (CPU, pre-quantized INT8)"""
2
  from __future__ import annotations
3
 
4
  import gc
 
5
  import os
 
6
  import sys
7
  import time
8
 
 
9
  import torch
 
 
 
 
10
 
11
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
12
 
13
  MODELS_DIR = "/tmp/fe2e_models"
14
- os.makedirs(MODELS_DIR, exist_ok=True)
 
15
 
 
16
 
17
- class Args:
18
- prompt_type = "empty"
19
- single_denoise = True
20
- empty_prompt_cache = os.path.join(os.path.dirname(os.path.abspath(__file__)), "latent", "no_info.npz")
21
- norm_type = "ln"
22
 
23
-
24
- def _download(repo, filename, token=None):
25
- import shutil
26
  from huggingface_hub import hf_hub_download
27
  basename = os.path.basename(filename)
28
- dest = os.path.join(MODELS_DIR, basename)
29
  if not os.path.exists(dest):
30
  print(f"[init] Downloading {repo}/{filename}...")
31
  src = hf_hub_download(repo, filename, token=token)
32
  shutil.copy2(src, dest)
33
- print(f"[init] {basename}: {os.path.getsize(dest)/1024/1024:.0f} MB")
 
34
  return dest
35
 
36
 
37
- def _load_generator():
38
- """Load pre-quantized INT8 DiT + FP32 VAE. No FP8 casting, no LoRA merge at runtime."""
39
- from infer.inference import ImageGenerator
40
 
41
  token = os.environ.get("HF_TOKEN")
42
- dit_path = _download("WeReCooking2/FE2E-INT8", "dit_int8_full.pt", token)
43
- vae_path = _download("WeReCooking2/FE2E-INT8", "vae_full.pt", token)
44
 
45
- args = Args()
46
- print("[init] Loading pre-quantized INT8 DiT (full model, mmap)...")
47
- t0 = time.time()
48
- dit = torch.load(dit_path, map_location="cpu", weights_only=False, mmap=True)
49
- gc.collect()
50
- print(f"[init] DiT loaded in {time.time()-t0:.0f}s")
51
 
52
- print("[init] Loading VAE (full model)...")
53
- ae = torch.load(vae_path, map_location="cpu", weights_only=False, mmap=True)
54
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  gc.collect()
56
 
57
- # Build generator wrapper (no LoRA needed, already merged)
58
- generator = ImageGenerator.__new__(ImageGenerator)
59
- generator.device = torch.device("cpu")
60
- generator.args = args
61
- generator.ae = ae
62
- generator.dit = dit
63
- generator.llm_encoder = None
64
- generator.quantized = False
65
- generator.offload = False
66
- generator.lora_module = None
67
 
68
- print(f"[init] Ready. Total load: {time.time()-t0:.0f}s")
69
- return generator
70
 
71
 
72
- print("[init] Loading model at startup (not lazy)...")
73
- GENERATOR = _load_generator()
74
- print("[init] Model ready, starting Gradio...")
75
 
76
 
77
  def generate(image):
78
- """Estimate depth and surface normals from a single image.
79
-
80
- Args:
81
- image: Input image (PIL Image).
82
-
83
- Returns:
84
- tuple: (depth_map, normal_map, status_message)
85
- """
86
  import gradio as gr
87
- from PIL import Image
88
- from torchvision.transforms import functional as F
89
 
90
  if image is None:
91
  raise gr.Error("Please upload an image.")
@@ -95,45 +91,135 @@ def generate(image):
95
  elif not isinstance(image, Image.Image):
96
  image = Image.fromarray(image).convert("RGB")
97
 
98
- args = Args()
99
- print(f"[gen] Input: {image.size}")
100
  t0 = time.time()
101
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  with torch.inference_mode():
103
- images, Lpred, Rpred = GENERATOR.generate_image(
104
- prompt="",
105
- negative_prompt="",
106
- ref_images=image,
107
- num_samples=1,
108
- num_steps=1,
109
- cfg_guidance=6.0,
110
- seed=42,
111
- show_progress=True,
112
- args=args,
113
- )
114
 
115
- elapsed = time.time() - t0
116
 
117
- import numpy as np
118
- import matplotlib.cm as cm
119
 
120
- # Normal map: normalize vectors, map [-1,1] to [0,255] RGB
121
- normal_np = Rpred[0].cpu().float().numpy().transpose(1, 2, 0) # (H, W, 3)
122
  normal_norm = np.linalg.norm(normal_np, axis=-1, keepdims=True)
123
  normal_norm[normal_norm < 1e-12] = 1e-12
124
  normal_np = normal_np / normal_norm
125
  normal_rgb = (((normal_np + 1) * 0.5) * 255).clip(0, 255).astype(np.uint8)
126
- normal_map = Image.fromarray(normal_rgb)
127
- normal_map = normal_map.resize(image.size)
128
 
129
- # Depth map: apply turbo colormap for visualization
130
- depth_np = Lpred[0].cpu().float().mean(dim=0).numpy() # (H, W) average channels
131
  depth_np = (depth_np - depth_np.min()) / (depth_np.max() - depth_np.min() + 1e-8)
132
  depth_colored = (cm.turbo(depth_np)[:, :, :3] * 255).astype(np.uint8)
133
- depth_map = Image.fromarray(depth_colored)
134
- depth_map = depth_map.resize(image.size)
135
 
136
- status = f"Generated in {elapsed:.1f}s ({image.size[0]}x{image.size[1]}, single denoise, INT8)"
137
  print(f"[gen] {status}")
138
  return depth_map, normal_map, status
139
 
@@ -143,7 +229,7 @@ import gradio as gr
143
  with gr.Blocks(title="FE2E: Depth + Normal (CPU)") as demo:
144
  gr.Markdown(
145
  "**[FE2E](https://github.com/AMAP-ML/FE2E)** Depth + Normal from a single image (CVPR 2026). "
146
- "Takes ~29 min for 768x1024, 1 step, Step1X-Edit DiT + LDRN LoRA (pre-merged), INT8 quantized on CPU."
147
  )
148
  with gr.Row(equal_height=True):
149
  input_img = gr.Image(label="Input", type="pil", height=256)
 
1
+ """FE2E: Depth + Normal estimation from a single image (CPU, ONNX INT8 DiT + PyTorch VAE)"""
2
  from __future__ import annotations
3
 
4
  import gc
5
+ import math
6
  import os
7
+ import shutil
8
  import sys
9
  import time
10
 
11
+ import numpy as np
12
  import torch
13
+ from einops import rearrange, repeat
14
+ from PIL import Image
15
+ from torchvision.transforms import functional as TF
16
+ import torch.nn.functional as Func
17
 
18
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
19
 
20
  MODELS_DIR = "/tmp/fe2e_models"
21
+ ONNX_DIR = os.path.join(MODELS_DIR, "onnx")
22
+ os.makedirs(ONNX_DIR, exist_ok=True)
23
 
24
+ EMPTY_PROMPT_CACHE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "latent", "no_info.npz")
25
 
 
 
 
 
 
26
 
27
+ def _download(repo, filename, dest_dir, token=None):
 
 
28
  from huggingface_hub import hf_hub_download
29
  basename = os.path.basename(filename)
30
+ dest = os.path.join(dest_dir, basename)
31
  if not os.path.exists(dest):
32
  print(f"[init] Downloading {repo}/{filename}...")
33
  src = hf_hub_download(repo, filename, token=token)
34
  shutil.copy2(src, dest)
35
+ size_mb = os.path.getsize(dest) / 1024 / 1024
36
+ print(f"[init] {basename}: {size_mb:.0f} MB")
37
  return dest
38
 
39
 
40
+ def _load_all():
41
+ import onnxruntime as ort
 
42
 
43
  token = os.environ.get("HF_TOKEN")
44
+ repo = "WeReCooking2/FE2E-INT8"
 
45
 
46
+ print("[init] Downloading ONNX INT8 DiT...")
47
+ onnx_path = _download(repo, "onnx/dit_int8.onnx", ONNX_DIR, token)
48
+ _download(repo, "onnx/dit_int8.onnx.data", ONNX_DIR, token)
 
 
 
49
 
50
+ print("[init] Downloading VAE...")
51
+ vae_path = _download(repo, "vae_full.pt", MODELS_DIR, token)
52
 
53
+ print("[init] Creating ONNX Runtime session...")
54
+ t0 = time.time()
55
+ opts = ort.SessionOptions()
56
+ opts.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
57
+ opts.inter_op_num_threads = 2
58
+ opts.intra_op_num_threads = 2
59
+ dit_session = ort.InferenceSession(onnx_path, opts, providers=["CPUExecutionProvider"])
60
+ print(f"[init] DiT session ready in {time.time() - t0:.0f}s")
61
+
62
+ print("[init] Loading VAE...")
63
+ vae = torch.load(vae_path, map_location="cpu", weights_only=False, mmap=True)
64
+ vae.eval()
65
  gc.collect()
66
 
67
+ print("[init] Loading empty prompt cache...")
68
+ data = np.load(EMPTY_PROMPT_CACHE, allow_pickle=True)
69
+ embeds = torch.from_numpy(data["embeds"])
70
+ masks = torch.from_numpy(data["masks"])
 
 
 
 
 
 
71
 
72
+ print("[init] Ready.")
73
+ return dit_session, vae, embeds, masks
74
 
75
 
76
+ print("[init] Loading models at startup...")
77
+ DIT_SESSION, VAE, PROMPT_EMBEDS, PROMPT_MASKS = _load_all()
78
+ print("[init] All models loaded, starting Gradio...")
79
 
80
 
81
  def generate(image):
82
+ """Run depth + normal estimation on a single image."""
 
 
 
 
 
 
 
83
  import gradio as gr
84
+ import matplotlib.cm as cm
 
85
 
86
  if image is None:
87
  raise gr.Error("Please upload an image.")
 
91
  elif not isinstance(image, Image.Image):
92
  image = Image.fromarray(image).convert("RGB")
93
 
94
+ orig_size = image.size
95
+ print(f"[gen] Input: {orig_size}")
96
  t0 = time.time()
97
 
98
+ # --- Resize to 1024x768 (matches ONNX model's fixed shape) ---
99
+ image_resized = image.resize((1024, 768))
100
+ img_tensor = TF.to_tensor(image_resized).unsqueeze(0) # [1, 3, 768, 1024]
101
+ height, width = 768, 1024
102
+
103
+ # --- VAE encode ---
104
+ with torch.inference_mode():
105
+ ref_latent = VAE.encode(img_tensor * 2 - 1) # [1, 16, 96, 128]
106
+
107
+ # --- Prepare DiT inputs ---
108
+ h_lat, w_lat = ref_latent.shape[2], ref_latent.shape[3] # 96, 128
109
+ h_half, w_half = h_lat // 2, w_lat // 2 # 48, 64
110
+ n_patches = h_half * w_half # 3072
111
+
112
+ # Noise (zeros for single denoise)
113
+ noise = torch.zeros(1, 16, h_lat, w_lat, dtype=torch.float32)
114
+
115
+ # Rearrange to patches
116
+ noise_patches = rearrange(noise, "b c (h ph) (w pw) -> b (h w) (c ph pw)", ph=2, pw=2)
117
+ ref_patches = rearrange(ref_latent, "b c (h ph) (w pw) -> b (h w) (c ph pw)", ph=2, pw=2)
118
+
119
+ # Concatenate noise + reference along sequence dim
120
+ img = torch.cat([noise_patches, ref_patches], dim=1) # [1, 6144, 64]
121
+ # Duplicate for CFG (conditional + unconditional)
122
+ img = torch.cat([img, img], dim=0) # [2, 6144, 64]
123
+
124
+ # Position IDs for noise patches
125
+ img_ids_noise = torch.zeros(h_half, w_half, 3)
126
+ img_ids_noise[..., 1] = torch.arange(h_half)[:, None].float()
127
+ img_ids_noise[..., 2] = torch.arange(w_half)[None, :].float()
128
+ img_ids_noise = rearrange(img_ids_noise, "h w c -> 1 (h w) c")
129
+
130
+ # Position IDs for reference patches (same layout)
131
+ img_ids_ref = torch.zeros(h_half, w_half, 3)
132
+ img_ids_ref[..., 1] = torch.arange(h_half)[:, None].float()
133
+ img_ids_ref[..., 2] = torch.arange(w_half)[None, :].float()
134
+ img_ids_ref = rearrange(img_ids_ref, "h w c -> 1 (h w) c")
135
+
136
+ img_ids = torch.cat([img_ids_noise, img_ids_ref], dim=1) # [1, 6144, 3]
137
+ img_ids = img_ids.repeat(2, 1, 1) # [2, 6144, 3]
138
+
139
+ # Text embeddings from cache
140
+ txt = torch.cat([PROMPT_EMBEDS, PROMPT_EMBEDS], dim=0) # [2, 640, 3584]
141
+ mask = torch.cat([PROMPT_MASKS, PROMPT_MASKS], dim=0) # [2, 640]
142
+ txt_ids = torch.zeros(2, txt.shape[1], 3) # [2, 640, 3]
143
+
144
+ # Timestep = 1.0 (single denoise step)
145
+ t_vec = torch.full((2,), 1.0, dtype=torch.float32)
146
+
147
+ # --- Run DiT via ONNX Runtime ---
148
+ print("[gen] Running ONNX INT8 DiT inference...")
149
+ t_dit = time.time()
150
+
151
+ feeds = {
152
+ "img": img.numpy().astype(np.float16),
153
+ "img_ids": img_ids.numpy().astype(np.float16),
154
+ "txt_ids": txt_ids.numpy().astype(np.float16),
155
+ "timesteps": t_vec.numpy().astype(np.float16),
156
+ "llm_embedding": txt.numpy().astype(np.float16),
157
+ "t_vec": t_vec.numpy().astype(np.float16),
158
+ "mask": mask.numpy().astype(np.float16),
159
+ }
160
+
161
+ pred_np = DIT_SESSION.run(None, feeds)[0] # [2, 6144, 64] float16
162
+ pred = torch.from_numpy(pred_np).float()
163
+
164
+ dit_elapsed = time.time() - t_dit
165
+ print(f"[gen] DiT inference: {dit_elapsed:.1f}s")
166
+
167
+ # --- CFG ---
168
+ cfg_guidance = 6.0
169
+ cond = pred[:1]
170
+ uncond = pred[1:]
171
+ pred_cfg = uncond + cfg_guidance * (cond - uncond) # [1, 6144, 64]
172
+
173
+ # --- Apply single denoise step: img + (0 - 1) * pred = img - pred ---
174
+ img_out = img[:1].float() + (0.0 - 1.0) * pred_cfg # [1, 6144, 64]
175
+
176
+ # Split: first half is output (depth/normal latent), second half was reference
177
+ output_patches = img_out[:, :n_patches] # [1, 3072, 64]
178
+ pred1_ref = cond[:, n_patches:] # [1, 3072, 64] (reference prediction)
179
+
180
+ # --- Unpack patches back to spatial ---
181
+ depth_latent = rearrange(
182
+ output_patches, "b (h w) (c ph pw) -> b c (h ph) (w pw)",
183
+ h=h_half, w=w_half, ph=2, pw=2
184
+ ) # [1, 16, 96, 128]
185
+
186
+ normal_latent = rearrange(
187
+ pred1_ref, "b (h w) (c ph pw) -> b c (h ph) (w pw)",
188
+ h=h_half, w=w_half, ph=2, pw=2
189
+ ) # [1, 16, 96, 128]
190
+
191
+ # --- Unpack as the original code does (depth, normal from 2-panel layout) ---
192
+ # Actually, looking at the original code more carefully:
193
+ # Lpred, Rpred = unpack_latents(pred, h//16, w//16)
194
+ # which splits the patches into two panels
195
+ # But in single_denoise mode, the code uses denoise() not double_denoise()
196
+ # denoise() returns: img[:, :seq_len//2], pred1[:, seq_len//2:]
197
+ # So depth = first half of updated image, normal = second half of cond prediction
198
+
199
+ # --- VAE decode ---
200
  with torch.inference_mode():
201
+ depth_decoded = VAE.decode(depth_latent)
202
+ normal_decoded = VAE.decode(normal_latent)
 
 
 
 
 
 
 
 
 
203
 
204
+ depth_decoded = depth_decoded.clamp(-1, 1).mul(0.5).add(0.5)
205
 
206
+ elapsed = time.time() - t0
 
207
 
208
+ # --- Normal map visualization ---
209
+ normal_np = normal_decoded[0].cpu().float().numpy().transpose(1, 2, 0) # (H, W, 3)
210
  normal_norm = np.linalg.norm(normal_np, axis=-1, keepdims=True)
211
  normal_norm[normal_norm < 1e-12] = 1e-12
212
  normal_np = normal_np / normal_norm
213
  normal_rgb = (((normal_np + 1) * 0.5) * 255).clip(0, 255).astype(np.uint8)
214
+ normal_map = Image.fromarray(normal_rgb).resize(orig_size)
 
215
 
216
+ # --- Depth map visualization (turbo colormap) ---
217
+ depth_np = depth_decoded[0].cpu().float().mean(dim=0).numpy()
218
  depth_np = (depth_np - depth_np.min()) / (depth_np.max() - depth_np.min() + 1e-8)
219
  depth_colored = (cm.turbo(depth_np)[:, :, :3] * 255).astype(np.uint8)
220
+ depth_map = Image.fromarray(depth_colored).resize(orig_size)
 
221
 
222
+ status = f"Generated in {elapsed:.1f}s (DiT: {dit_elapsed:.1f}s, 1024x768, ONNX INT8)"
223
  print(f"[gen] {status}")
224
  return depth_map, normal_map, status
225
 
 
229
  with gr.Blocks(title="FE2E: Depth + Normal (CPU)") as demo:
230
  gr.Markdown(
231
  "**[FE2E](https://github.com/AMAP-ML/FE2E)** Depth + Normal from a single image (CVPR 2026). "
232
+ "Step1X-Edit DiT + LDRN LoRA (pre-merged), ONNX INT8 quantized on CPU."
233
  )
234
  with gr.Row(equal_height=True):
235
  input_img = gr.Image(label="Input", type="pil", height=256)