supli6669 commited on
Commit
b42379b
·
1 Parent(s): d74ed13

fix(hf): integrate Static INT8 ONNX build into Dockerfile and fix thread state guard in app.py

Browse files
Dockerfile CHANGED
@@ -33,8 +33,10 @@ COPY . /app/
33
  # Create weights directory and make sure it has permissions
34
  RUN mkdir -p /app/weights && chmod -R 777 /app/weights
35
 
36
- # Download model weights during build phase to prevent runtime startup hang
37
  RUN python tools/download_weights.py
 
 
38
 
39
  # Expose port 7860 for Hugging Face Spaces
40
  EXPOSE 7860
 
33
  # Create weights directory and make sure it has permissions
34
  RUN mkdir -p /app/weights && chmod -R 777 /app/weights
35
 
36
+ # Download model weights, export ONNX models, and generate static INT8 quantization during build phase
37
  RUN python tools/download_weights.py
38
+ RUN python tools/export_onnx.py
39
+ RUN python tools/quantize_onnx_static.py
40
 
41
  # Expose port 7860 for Hugging Face Spaces
42
  EXPOSE 7860
models/CodeFormer/basicsr/models/codeformer_joint_model.py CHANGED
@@ -105,11 +105,12 @@ class CodeFormerJointModel(SRModel):
105
  self.setup_schedulers()
106
 
107
  def calculate_adaptive_weight(self, recon_loss, g_loss, last_layer, disc_weight_max):
108
- recon_grads = torch.autograd.grad(recon_loss, last_layer, retain_graph=True)[0]
109
- g_grads = torch.autograd.grad(g_loss, last_layer, retain_graph=True)[0]
110
 
111
  d_weight = torch.norm(recon_grads) / (torch.norm(g_grads) + 1e-4)
112
  d_weight = torch.clamp(d_weight, 0.0, disc_weight_max).detach()
 
113
  return d_weight
114
 
115
  def setup_optimizers(self):
 
105
  self.setup_schedulers()
106
 
107
  def calculate_adaptive_weight(self, recon_loss, g_loss, last_layer, disc_weight_max):
108
+ recon_grads = torch.autograd.grad(recon_loss, last_layer, retain_graph=True)[0].detach()
109
+ g_grads = torch.autograd.grad(g_loss, last_layer, retain_graph=True)[0].detach()
110
 
111
  d_weight = torch.norm(recon_grads) / (torch.norm(g_grads) + 1e-4)
112
  d_weight = torch.clamp(d_weight, 0.0, disc_weight_max).detach()
113
+ del recon_grads, g_grads
114
  return d_weight
115
 
116
  def setup_optimizers(self):
models/CodeFormer/basicsr/train.py CHANGED
@@ -203,13 +203,6 @@ def train_pipeline(root_path):
203
  if current_iter % opt['logger']['save_checkpoint_freq'] == 0:
204
  logger.info('Saving models and training states.')
205
  model.save(epoch, current_iter)
206
- import gc
207
- gc.collect()
208
-
209
- # Garbage collect periodically on CPU to prevent RAM memory leaks
210
- if current_iter % 50 == 0:
211
- import gc
212
- gc.collect()
213
 
214
  # validation
215
  if opt.get('val') is not None and opt['datasets'].get('val') is not None \
 
203
  if current_iter % opt['logger']['save_checkpoint_freq'] == 0:
204
  logger.info('Saving models and training states.')
205
  model.save(epoch, current_iter)
 
 
 
 
 
 
 
206
 
207
  # validation
208
  if opt.get('val') is not None and opt['datasets'].get('val') is not None \
pipeline.py CHANGED
@@ -404,10 +404,10 @@ class LocalAIEnhancerPipeline:
404
 
405
  self._report_progress("restoration", 0.8, "Face restoration complete")
406
  else:
407
- # B2 FIX: Only use ThreadPoolExecutor when there are multiple faces.
408
- # For single-face images (the common case), spawning a thread pool
409
- # adds ~20 ms of overhead with zero parallelism benefit.
410
- if parallel and len(face_helper.cropped_faces) > 1:
411
  def _process_face(idx, cropped_face):
412
  if self.use_onnx:
413
  try:
 
404
 
405
  self._report_progress("restoration", 0.8, "Face restoration complete")
406
  else:
407
+ # B2 FIX: Only use ThreadPoolExecutor when there are multiple faces.
408
+ # For single-face images (the common case), spawning a thread pool
409
+ # adds ~20 ms of overhead with zero parallelism benefit.
410
+ if parallel and len(face_helper.cropped_faces) > 1:
411
  def _process_face(idx, cropped_face):
412
  if self.use_onnx:
413
  try:
tools/benchmark_quant.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import time
4
+ import cv2
5
+ import numpy as np
6
+ import onnxruntime as ort
7
+
8
+ project_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
9
+ weights_dir = os.path.join(project_dir, "weights", "CodeFormer")
10
+ calib_dir = os.path.join(project_dir, "models", "CodeFormer", "datasets", "ffhq", "ffhq_512")
11
+
12
+ def calculate_psnr(img1, img2):
13
+ mse = np.mean((img1.astype(np.float64) - img2.astype(np.float64)) ** 2)
14
+ if mse == 0:
15
+ return float('inf')
16
+ return 20 * np.log10(255.0 / np.sqrt(mse))
17
+
18
+ import gc
19
+
20
+ def benchmark_model(model_name, model_path, test_images, ort_opts):
21
+ if not os.path.exists(model_path):
22
+ print(f"[{model_name}] Model file not found: {model_path}")
23
+ return None
24
+
25
+ session = ort.InferenceSession(model_path, sess_options=ort_opts, providers=['CPUExecutionProvider'])
26
+ input_names = [inp.name for inp in session.get_inputs()]
27
+
28
+ times = []
29
+ outputs = []
30
+
31
+ # Warmup
32
+ dummy_input = np.zeros((1, 3, 512, 512), dtype=np.float32)
33
+ w = np.array([0.5], dtype=np.float32)
34
+ session.run(None, {input_names[0]: dummy_input, input_names[1]: w})
35
+
36
+ for img_path in test_images:
37
+ img = cv2.imread(img_path)
38
+ if img is None:
39
+ continue
40
+ img = cv2.resize(img, (512, 512), interpolation=cv2.INTER_LANCZOS4)
41
+ img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0
42
+ img_norm = (img_rgb - 0.5) / 0.5
43
+ tensor = np.transpose(img_norm, (2, 0, 1))[np.newaxis, ...].astype(np.float32)
44
+
45
+ t0 = time.time()
46
+ out = session.run(None, {input_names[0]: tensor, input_names[1]: w})[0]
47
+ t1 = time.time()
48
+ times.append(t1 - t0)
49
+ outputs.append(out)
50
+
51
+ del session
52
+ gc.collect()
53
+
54
+ avg_time = np.mean(times) if times else 0
55
+ return avg_time, outputs
56
+
57
+ def main():
58
+ print("=== ONNX Quantization Benchmark (P5.4) ===")
59
+ opts = ort.SessionOptions()
60
+ opts.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
61
+
62
+ test_files = [os.path.join(calib_dir, f) for f in os.listdir(calib_dir)[:10] if f.endswith('.png')]
63
+ print(f"Benchmarking on {len(test_files)} sample face crops...")
64
+
65
+ models = {
66
+ "FP32 Baseline": os.path.join(weights_dir, "codeformer.onnx"),
67
+ "Dynamic INT8": os.path.join(weights_dir, "codeformer_int8.onnx"),
68
+ "Static INT8 (v2)": os.path.join(weights_dir, "codeformer_int8_v2.onnx")
69
+ }
70
+
71
+ results = {}
72
+ baseline_outputs = None
73
+
74
+ for name, path in models.items():
75
+ res = benchmark_model(name, path, test_files, opts)
76
+ if res is not None:
77
+ avg_time, outputs = res
78
+ results[name] = {"time": avg_time, "outputs": outputs}
79
+ if name == "FP32 Baseline":
80
+ baseline_outputs = outputs
81
+
82
+ print("\n-----------------------------------------------------------")
83
+ print(f"{'Model Variant':<20} | {'Avg Latency (ms)':<18} | {'PSNR vs FP32 (dB)':<18}")
84
+ print("-----------------------------------------------------------")
85
+
86
+ for name, data in results.items():
87
+ avg_ms = data["time"] * 1000
88
+ if name == "FP32 Baseline":
89
+ psnr_str = "N/A (Baseline)"
90
+ else:
91
+ psnrs = []
92
+ for b_out, q_out in zip(baseline_outputs, data["outputs"]):
93
+ b_img = ((np.squeeze(b_out).transpose(1, 2, 0) + 1.0) * 127.5).clip(0, 255).astype(np.uint8)
94
+ q_img = ((np.squeeze(q_out).transpose(1, 2, 0) + 1.0) * 127.5).clip(0, 255).astype(np.uint8)
95
+ psnrs.append(calculate_psnr(b_img, q_img))
96
+ mean_psnr = np.mean(psnrs)
97
+ psnr_str = f"{mean_psnr:.2f} dB"
98
+
99
+ print(f"{name:<20} | {avg_ms:<18.2f} | {psnr_str:<18}")
100
+ print("-----------------------------------------------------------\n")
101
+
102
+ if __name__ == "__main__":
103
+ main()
tools/download_weights.py CHANGED
@@ -39,6 +39,7 @@ def main():
39
  "weights/facelib/parsing_parsenet.pth": "https://github.com/sczhou/CodeFormer/releases/download/v0.1.0/parsing_parsenet.pth",
40
  "weights/facelib/yolov5l-face.pth": "https://github.com/sczhou/CodeFormer/releases/download/v0.1.0/yolov5l-face.pth",
41
  "weights/facelib/yolov5n-face.pth": "https://github.com/sczhou/CodeFormer/releases/download/v0.1.0/yolov5n-face.pth",
 
42
  "weights/realesrgan/RealESRGAN_x2plus.pth": "https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.1/RealESRGAN_x2plus.pth"
43
  }
44
 
 
39
  "weights/facelib/parsing_parsenet.pth": "https://github.com/sczhou/CodeFormer/releases/download/v0.1.0/parsing_parsenet.pth",
40
  "weights/facelib/yolov5l-face.pth": "https://github.com/sczhou/CodeFormer/releases/download/v0.1.0/yolov5l-face.pth",
41
  "weights/facelib/yolov5n-face.pth": "https://github.com/sczhou/CodeFormer/releases/download/v0.1.0/yolov5n-face.pth",
42
+ "weights/facelib/arcface_resnet18.pth": "https://github.com/sczhou/CodeFormer/releases/download/v0.1.0/arcface_resnet18.pth",
43
  "weights/realesrgan/RealESRGAN_x2plus.pth": "https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.1/RealESRGAN_x2plus.pth"
44
  }
45
 
tools/quantize_onnx_static.py CHANGED
@@ -9,7 +9,7 @@ from onnxruntime.quantization import quantize_static, CalibrationDataReader, Qua
9
 
10
  class CodeFormerCalibrationDataReader(CalibrationDataReader):
11
  """Calibration Data Reader for ONNX Runtime static quantization of CodeFormer."""
12
- def __init__(self, calibration_folder: str, w_val: float = 0.5, max_samples: int = 100):
13
  super().__init__()
14
  valid_exts = ('.png', '.jpg', '.jpeg', '.webp')
15
  self.image_paths = [
@@ -41,6 +41,16 @@ class CodeFormerCalibrationDataReader(CalibrationDataReader):
41
  })
42
  count += 1
43
 
 
 
 
 
 
 
 
 
 
 
44
  print(f"[Calib] Prepared {len(self.enum_data_dicts)} calibration samples.")
45
  self.enum_data = iter(self.enum_data_dicts)
46
 
@@ -59,14 +69,26 @@ def quantize_static_model(model_path: str, output_path: str, calibration_folder:
59
  print(f"[ERROR] Calibration folder is empty or invalid: {calibration_folder}")
60
  sys.exit(1)
61
 
62
- print(f"[Static Quant] Quantizing {model_path} -> {output_path}")
 
 
 
 
 
 
 
 
 
 
63
  quantize_static(
64
- model_input=model_path,
65
  model_output=output_path,
66
  calibration_data_reader=dr,
67
  quant_format=QuantType.QInt8,
68
  activation_type=QuantType.QUInt8,
69
- weight_type=QuantType.QInt8
 
 
70
  )
71
  print(f"[Static Quant] Completed successfully: {output_path}")
72
 
 
9
 
10
  class CodeFormerCalibrationDataReader(CalibrationDataReader):
11
  """Calibration Data Reader for ONNX Runtime static quantization of CodeFormer."""
12
+ def __init__(self, calibration_folder: str, w_val: float = 0.5, max_samples: int = 30):
13
  super().__init__()
14
  valid_exts = ('.png', '.jpg', '.jpeg', '.webp')
15
  self.image_paths = [
 
41
  })
42
  count += 1
43
 
44
+ if len(self.enum_data_dicts) == 0:
45
+ print("[Calib] No calibration images found. Generating 10 synthetic calibration samples...")
46
+ np.random.seed(42)
47
+ for _ in range(10):
48
+ dummy_tensor = np.random.uniform(-1.0, 1.0, (1, 3, 512, 512)).astype(np.float32)
49
+ self.enum_data_dicts.append({
50
+ "input": dummy_tensor,
51
+ "w": self.w_val
52
+ })
53
+
54
  print(f"[Calib] Prepared {len(self.enum_data_dicts)} calibration samples.")
55
  self.enum_data = iter(self.enum_data_dicts)
56
 
 
69
  print(f"[ERROR] Calibration folder is empty or invalid: {calibration_folder}")
70
  sys.exit(1)
71
 
72
+ # Pre-compute shape inference to file to avoid loading entire 370MB external data into RAM
73
+ inferred_model_path = model_path.replace(".onnx", "_inferred.onnx")
74
+ print(f"[Static Quant] Inferring shapes to {inferred_model_path}...")
75
+ try:
76
+ onnx.shape_inference.infer_shapes_path(model_path, inferred_model_path)
77
+ input_for_quant = inferred_model_path
78
+ except Exception as e:
79
+ print(f"[Static Quant] Warning during shape inference: {e}. Falling back to input model.")
80
+ input_for_quant = model_path
81
+
82
+ print(f"[Static Quant] Quantizing {input_for_quant} -> {output_path}")
83
  quantize_static(
84
+ model_input=input_for_quant,
85
  model_output=output_path,
86
  calibration_data_reader=dr,
87
  quant_format=QuantType.QInt8,
88
  activation_type=QuantType.QUInt8,
89
+ weight_type=QuantType.QInt8,
90
+ use_external_data_format=True,
91
+ extra_options={"DisableShapeInference": True}
92
  )
93
  print(f"[Static Quant] Completed successfully: {output_path}")
94