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 +3 -1
- models/CodeFormer/basicsr/models/codeformer_joint_model.py +3 -2
- models/CodeFormer/basicsr/train.py +0 -7
- pipeline.py +4 -4
- tools/benchmark_quant.py +103 -0
- tools/download_weights.py +1 -0
- tools/quantize_onnx_static.py +26 -4
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
|
| 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 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 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 =
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
quantize_static(
|
| 64 |
-
model_input=
|
| 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 |
|