Add DDColor paper tiny INT8 (default colouriser), DDColor build scripts, update model card
Browse files- README.md +12 -1
- ddcolor-paper-tiny-int8.onnx +3 -0
- scripts/compare_ddcolor.py +36 -0
- scripts/export_ddcolor_onnx.py +81 -0
- scripts/quantize_ddcolor_int8.py +66 -0
README.md
CHANGED
|
@@ -35,14 +35,25 @@ Pin a commit in your download URL (`/resolve/<commit>/<file>`) and verify the SH
|
|
| 35 |
| `u2netp.onnx` | 4,574,861 | `309c8469258dda742793dce0ebea8e6dd393174f89934733ecc8b14c76f4ddd8` | U²-Netp (small) salient-object matte (default background model) | Unmodified copy of `u2netp.onnx` from [danielgatis/rembg release v0.0.0](https://github.com/danielgatis/rembg/releases/tag/v0.0.0) (rembg is MIT), converted from the weights of [xuebinqin/U-2-Net](https://github.com/xuebinqin/U-2-Net) | Apache-2.0, © Xuebin Qin et al. |
|
| 36 |
| `u2net.onnx` | 175,997,641 | `8d10d2f3bb75ae3b6d527c77944fc5e7dcd94b29809d47a739a7a728a912b491` | U²-Net (full) salient-object matte (HD option) | Unmodified copy of `u2net.onnx` from [danielgatis/rembg release v0.0.0](https://github.com/danielgatis/rembg/releases/tag/v0.0.0) (rembg is MIT), converted from the weights of [xuebinqin/U-2-Net](https://github.com/xuebinqin/U-2-Net) | Apache-2.0, © Xuebin Qin et al. |
|
| 37 |
| `ddcolor-paper-tiny.onnx` | 220,552,834 | `5479b1b00d8ba2b3c312e5378e75dc93eac6f80c8497e90169801896e1eeb68d` | DDColor-T (`ddcolor_paper_tiny`), ConvNeXt-T encoder + multi-scale colour decoder, fp32, opset 17 (photo colourisation) | Built by MomoApps from the official weights [piddnad/ddcolor_paper_tiny](https://huggingface.co/piddnad/ddcolor_paper_tiny) (commit `cf9fd99c1d7472689ec7413441c1b799a51866a3`, `pytorch_model.bin` sha256 `8a1277bc90a1bfbb6d2d83933a9a6bc821931879ca93e26e4fcec12165d41fce`) and the architecture in [piddnad/DDColor](https://github.com/piddnad/DDColor) (commit `2adb63f2656ac41cbdf7b894cddd94121a3faf13`). The weights load with no missing or unexpected keys. The 8 spectral-norm layers are folded into plain weights, which is exact in eval mode. Exported with `torch.onnx.export` (torch 2.15.0). | Apache-2.0, © Xiaoyang Kang et al. (DAMO Academy, Alibaba) |
|
|
|
|
| 38 |
|
| 39 |
## Input and output contracts
|
| 40 |
|
| 41 |
- **GFPGAN (both files):** input `input`, float32 `[1,3,512,512]` RGB in −1..1 (one aligned face crop). Output `output`, the same shape and range.
|
| 42 |
- **Real-ESRGAN general x4v3:** input `input`, float32 `[1,3,H,W]` RGB in 0..1, with dynamic H and W. Output `output`, `[1,3,4H,4W]`. Clamp the output to 0..1.
|
| 43 |
-
- **DDColor paper tiny:** input `input`, float32 `[1,3,512,512]` RGB in 0..1. Build it as DDColor's `ColorizationPipeline` does: resize the photo to 512×512, convert to Lab (OpenCV float), keep L, set a = b = 0, and convert back to RGB. The result is the greyscale L channel replicated as RGB. Output `output`, float32 `[1,2,512,512]`: the predicted Lab a and b channels on OpenCV's float Lab scale (roughly −128..127; clamp them). Resize a and b to the original size, combine them with the original L, and convert Lab to RGB. Ops include `Einsum`, `Erf` and `LayerNormalization`, which the full ONNX Runtime Android package supports. A reduced-operator ORT Mobile build must include them.
|
| 44 |
- **U²-Net / U²-Netp:** input `input.1`, float32 `[1,3,320,320]` RGB, with ImageNet normalisation applied after dividing by the peak pixel. The first output is the matte.
|
| 45 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
## Training data note: FFHQ
|
| 47 |
|
| 48 |
GFPGAN was trained on the [FFHQ](https://github.com/NVlabs/ffhq-dataset) face dataset. FFHQ's images are published under a
|
|
|
|
| 35 |
| `u2netp.onnx` | 4,574,861 | `309c8469258dda742793dce0ebea8e6dd393174f89934733ecc8b14c76f4ddd8` | U²-Netp (small) salient-object matte (default background model) | Unmodified copy of `u2netp.onnx` from [danielgatis/rembg release v0.0.0](https://github.com/danielgatis/rembg/releases/tag/v0.0.0) (rembg is MIT), converted from the weights of [xuebinqin/U-2-Net](https://github.com/xuebinqin/U-2-Net) | Apache-2.0, © Xuebin Qin et al. |
|
| 36 |
| `u2net.onnx` | 175,997,641 | `8d10d2f3bb75ae3b6d527c77944fc5e7dcd94b29809d47a739a7a728a912b491` | U²-Net (full) salient-object matte (HD option) | Unmodified copy of `u2net.onnx` from [danielgatis/rembg release v0.0.0](https://github.com/danielgatis/rembg/releases/tag/v0.0.0) (rembg is MIT), converted from the weights of [xuebinqin/U-2-Net](https://github.com/xuebinqin/U-2-Net) | Apache-2.0, © Xuebin Qin et al. |
|
| 37 |
| `ddcolor-paper-tiny.onnx` | 220,552,834 | `5479b1b00d8ba2b3c312e5378e75dc93eac6f80c8497e90169801896e1eeb68d` | DDColor-T (`ddcolor_paper_tiny`), ConvNeXt-T encoder + multi-scale colour decoder, fp32, opset 17 (photo colourisation) | Built by MomoApps from the official weights [piddnad/ddcolor_paper_tiny](https://huggingface.co/piddnad/ddcolor_paper_tiny) (commit `cf9fd99c1d7472689ec7413441c1b799a51866a3`, `pytorch_model.bin` sha256 `8a1277bc90a1bfbb6d2d83933a9a6bc821931879ca93e26e4fcec12165d41fce`) and the architecture in [piddnad/DDColor](https://github.com/piddnad/DDColor) (commit `2adb63f2656ac41cbdf7b894cddd94121a3faf13`). The weights load with no missing or unexpected keys. The 8 spectral-norm layers are folded into plain weights, which is exact in eval mode. Exported with `torch.onnx.export` (torch 2.15.0). | Apache-2.0, © Xiaoyang Kang et al. (DAMO Academy, Alibaba) |
|
| 38 |
+
| `ddcolor-paper-tiny-int8.onnx` | 56,619,279 | `29f3a5f5f794959faebaf034a142c34712833623f9ce672f6ef8f0a8ae8e5f5a` | DDColor-T, weight-only per-channel INT8 (default colouriser) | Built by MomoApps from `ddcolor-paper-tiny.onnx` above with `scripts/quantize_ddcolor_int8.py`. The 161 Conv/MatMul/Gemm weight tensors are stored as symmetric INT8 with one scale per output channel and expanded by `DequantizeLinear`. All compute stays float32, and Einsum, Erf, LayerNormalization, norms and biases stay in float. Against the float32 ONNX on two DDColor test photos, the mean ab difference is 0.41 / 0.32. Visually indistinguishable apart from edge artifacts the float model already has. | Apache-2.0, © Xiaoyang Kang et al. (DAMO Academy, Alibaba) |
|
| 39 |
|
| 40 |
## Input and output contracts
|
| 41 |
|
| 42 |
- **GFPGAN (both files):** input `input`, float32 `[1,3,512,512]` RGB in −1..1 (one aligned face crop). Output `output`, the same shape and range.
|
| 43 |
- **Real-ESRGAN general x4v3:** input `input`, float32 `[1,3,H,W]` RGB in 0..1, with dynamic H and W. Output `output`, `[1,3,4H,4W]`. Clamp the output to 0..1.
|
| 44 |
+
- **DDColor paper tiny (both files):** input `input`, float32 `[1,3,512,512]` RGB in 0..1. Build it as DDColor's `ColorizationPipeline` does: resize the photo to 512×512, convert to Lab (OpenCV float), keep L, set a = b = 0, and convert back to RGB. The result is the greyscale L channel replicated as RGB. Output `output`, float32 `[1,2,512,512]`: the predicted Lab a and b channels on OpenCV's float Lab scale (roughly −128..127; clamp them). Resize a and b to the original size, combine them with the original L, and convert Lab to RGB. Ops include `Einsum`, `Erf` and `LayerNormalization`, which the full ONNX Runtime Android package supports. A reduced-operator ORT Mobile build must include them. The INT8 file also uses `DequantizeLinear` (opset 13+, per-axis). ONNX Runtime's mobile checker recommends the CPU execution provider for both files, and both convert to `.ort` format.
|
| 45 |
- **U²-Net / U²-Netp:** input `input.1`, float32 `[1,3,320,320]` RGB, with ImageNet normalisation applied after dividing by the peak pixel. The first output is the matte.
|
| 46 |
|
| 47 |
+
## Build scripts (DDColor)
|
| 48 |
+
|
| 49 |
+
`scripts/` holds the scripts that produced the DDColor files. They are deterministic: re-running them on the box gave byte-identical files.
|
| 50 |
+
|
| 51 |
+
- `export_ddcolor_onnx.py DDCOLOR_REPO WEIGHTS_DIR OUT.onnx TEST_IMAGE [PREVIEW.jpg]` exports the float32 ONNX (opset 17) from the official weights. It folds spectral norm into plain weights and checks the result against PyTorch: max |Δab| was 0.0022 on the Audrey Hepburn test photo.
|
| 52 |
+
- `quantize_ddcolor_int8.py SRC.onnx DEST.onnx` produces the weight-only per-channel INT8 file.
|
| 53 |
+
- `compare_ddcolor.py REF.onnx TEST.onnx OUTDIR IMG...` reports the ab differences and writes side-by-side colourised JPGs.
|
| 54 |
+
|
| 55 |
+
The tools used were torch 2.15.0 (CPU), onnx 1.23.2, onnxruntime 1.31.1 and opencv-python-headless.
|
| 56 |
+
|
| 57 |
## Training data note: FFHQ
|
| 58 |
|
| 59 |
GFPGAN was trained on the [FFHQ](https://github.com/NVlabs/ffhq-dataset) face dataset. FFHQ's images are published under a
|
ddcolor-paper-tiny-int8.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:29f3a5f5f794959faebaf034a142c34712833623f9ce672f6ef8f0a8ae8e5f5a
|
| 3 |
+
size 56619279
|
scripts/compare_ddcolor.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Compare two DDColor ONNX files on photos: ab max/mean difference plus side-by-side colourised JPGs.
|
| 3 |
+
Usage: compare_ddcolor.py REF.onnx TEST.onnx OUTDIR IMG [IMG ...]"""
|
| 4 |
+
import sys, time
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
import cv2, numpy as np, onnxruntime as ort
|
| 7 |
+
|
| 8 |
+
def preprocess(img_bgr): # DDColor ColorizationPipeline preprocessing
|
| 9 |
+
img = (img_bgr / 255.0).astype(np.float32)
|
| 10 |
+
l = cv2.cvtColor(cv2.resize(img, (512, 512)), cv2.COLOR_BGR2Lab)[:, :, :1]
|
| 11 |
+
rgb = cv2.cvtColor(np.concatenate((l, 0 * l, 0 * l), -1), cv2.COLOR_LAB2RGB)
|
| 12 |
+
return rgb.transpose(2, 0, 1)[None].astype(np.float32)
|
| 13 |
+
|
| 14 |
+
def colourise(img_bgr, ab):
|
| 15 |
+
h, w = img_bgr.shape[:2]
|
| 16 |
+
l = cv2.cvtColor((img_bgr / 255.0).astype(np.float32), cv2.COLOR_BGR2Lab)[:, :, :1]
|
| 17 |
+
ab = cv2.resize(ab[0].transpose(1, 2, 0), (w, h), interpolation=cv2.INTER_NEAREST)
|
| 18 |
+
return (cv2.cvtColor(np.concatenate((l, ab), -1), cv2.COLOR_LAB2BGR) * 255).round().clip(0, 255).astype(np.uint8)
|
| 19 |
+
|
| 20 |
+
ref_s = ort.InferenceSession(sys.argv[1], providers=["CPUExecutionProvider"])
|
| 21 |
+
tst_s = ort.InferenceSession(sys.argv[2], providers=["CPUExecutionProvider"])
|
| 22 |
+
out = Path(sys.argv[3]); out.mkdir(parents=True, exist_ok=True)
|
| 23 |
+
for p in sys.argv[4:]:
|
| 24 |
+
img = cv2.imread(p); x = preprocess(img)
|
| 25 |
+
a = ref_s.run(["output"], {"input": x})[0]
|
| 26 |
+
t = time.time(); b = tst_s.run(["output"], {"input": x})[0]; dt = time.time() - t
|
| 27 |
+
d = np.abs(a - b)
|
| 28 |
+
ca, cb = colourise(img, a), colourise(img, b)
|
| 29 |
+
pix = np.abs(ca.astype(int) - cb.astype(int))
|
| 30 |
+
print(f"{Path(p).name}: ab max {d.max():.3f} mean {d.mean():.4f} p99 {np.percentile(d, 99):.3f} | "
|
| 31 |
+
f"8-bit RGB max {pix.max()} mean {pix.mean():.3f} | test run {dt:.2f}s")
|
| 32 |
+
stem = Path(p).stem.split()[0]
|
| 33 |
+
gap = np.full((ca.shape[0], 16, 3), 255, np.uint8)
|
| 34 |
+
side = np.concatenate((ca, gap, cb), 1)
|
| 35 |
+
s = 1400 / side.shape[1]
|
| 36 |
+
cv2.imwrite(str(out / f"{stem}_fp32_vs_test.jpg"), cv2.resize(side, None, fx=s, fy=s, interpolation=cv2.INTER_AREA), [cv2.IMWRITE_JPEG_QUALITY, 92])
|
scripts/export_ddcolor_onnx.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Export DDColor paper_tiny (Apache-2.0, piddnad/DDColor) to the OptiMix ONNX contract.
|
| 3 |
+
|
| 4 |
+
Weights: huggingface.co/piddnad/ddcolor_paper_tiny @ cf9fd99c1d7472689ec7413441c1b799a51866a3, pytorch_model.bin
|
| 5 |
+
sha256 8a1277bc90a1bfbb6d2d83933a9a6bc821931879ca93e26e4fcec12165d41fce
|
| 6 |
+
Code: github.com/piddnad/DDColor @ 2adb63f2656ac41cbdf7b894cddd94121a3faf13 (basicsr/archs/ddcolor_arch.py)
|
| 7 |
+
Contract: input `input` float32 [1,3,512,512] RGB 0..1, the L channel rendered as grey RGB
|
| 8 |
+
output `output` float32 [1,2,512,512] Lab a,b (OpenCV float Lab scale)
|
| 9 |
+
"""
|
| 10 |
+
import hashlib, json, sys
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
import cv2, numpy as np, onnx, onnxruntime as ort, torch
|
| 13 |
+
|
| 14 |
+
# Usage: export_ddcolor_onnx.py DDCOLOR_REPO_DIR WEIGHTS_DIR OUT.onnx TEST_IMAGE [PREVIEW.jpg]
|
| 15 |
+
# DDCOLOR_REPO_DIR: a checkout of github.com/piddnad/DDColor (commit above)
|
| 16 |
+
# WEIGHTS_DIR: holds config.json and pytorch_model.bin from piddnad/ddcolor_paper_tiny (commit above)
|
| 17 |
+
REPO = Path(sys.argv[1]).resolve(); sys.path.insert(0, str(REPO))
|
| 18 |
+
from basicsr.archs.ddcolor_arch import DDColor
|
| 19 |
+
|
| 20 |
+
WEIGHTS = Path(sys.argv[2])
|
| 21 |
+
OUT = Path(sys.argv[3])
|
| 22 |
+
TEST_IMAGE = sys.argv[4]
|
| 23 |
+
PREVIEW = sys.argv[5] if len(sys.argv) > 5 else None
|
| 24 |
+
OPSET = 17
|
| 25 |
+
cfg = json.loads((WEIGHTS / "config.json").read_text())
|
| 26 |
+
model = DDColor(**cfg)
|
| 27 |
+
state = torch.load(WEIGHTS / "pytorch_model.bin", map_location="cpu", weights_only=True)
|
| 28 |
+
state = state.get("params", state)
|
| 29 |
+
missing, unexpected = model.load_state_dict(state, strict=False)
|
| 30 |
+
print("missing", missing, "unexpected", unexpected)
|
| 31 |
+
assert not unexpected, unexpected
|
| 32 |
+
assert all(k.endswith("num_batches_tracked") for k in missing), missing
|
| 33 |
+
model.eval()
|
| 34 |
+
|
| 35 |
+
# Reference = the official model exactly as loaded (spectral-norm hooks intact).
|
| 36 |
+
img = cv2.imread(TEST_IMAGE)
|
| 37 |
+
|
| 38 |
+
def preprocess(img_bgr):
|
| 39 |
+
img = (img_bgr / 255.0).astype(np.float32)
|
| 40 |
+
r = cv2.resize(img, (512, 512))
|
| 41 |
+
l = cv2.cvtColor(r, cv2.COLOR_BGR2Lab)[:, :, :1]
|
| 42 |
+
lab = np.concatenate((l, np.zeros_like(l), np.zeros_like(l)), axis=-1)
|
| 43 |
+
rgb = cv2.cvtColor(lab, cv2.COLOR_LAB2RGB)
|
| 44 |
+
return rgb.transpose(2, 0, 1)[None].astype(np.float32)
|
| 45 |
+
|
| 46 |
+
x = preprocess(img)
|
| 47 |
+
with torch.no_grad():
|
| 48 |
+
ref = model(torch.from_numpy(x)).numpy()
|
| 49 |
+
|
| 50 |
+
# Bake spectral norm into plain weights (W / sigma with the stored u, v; eval mode
|
| 51 |
+
# runs no power iteration), so the graph does not carry weight_orig plus a reshaped copy.
|
| 52 |
+
from torch.nn.utils import remove_spectral_norm
|
| 53 |
+
baked = 0
|
| 54 |
+
for mod in model.modules():
|
| 55 |
+
if hasattr(mod, "weight_orig"):
|
| 56 |
+
remove_spectral_norm(mod); baked += 1
|
| 57 |
+
print("spectral norms baked", baked)
|
| 58 |
+
with torch.no_grad():
|
| 59 |
+
torch.onnx.export(model, torch.rand(1, 3, 512, 512), str(OUT), opset_version=OPSET,
|
| 60 |
+
input_names=["input"], output_names=["output"], dynamo=False,
|
| 61 |
+
do_constant_folding=True)
|
| 62 |
+
m = onnx.load(str(OUT)); onnx.checker.check_model(m)
|
| 63 |
+
print("opset", [(o.domain, o.version) for o in m.opset_import])
|
| 64 |
+
print("inputs", [(i.name, [d.dim_value for d in i.type.tensor_type.shape.dim]) for i in m.graph.input])
|
| 65 |
+
print("outputs", [(o.name, [d.dim_value for d in o.type.tensor_type.shape.dim]) for o in m.graph.output])
|
| 66 |
+
|
| 67 |
+
with torch.no_grad():
|
| 68 |
+
baked_ref = model(torch.from_numpy(x)).numpy()
|
| 69 |
+
print("baked vs official torch max abs diff", float(np.abs(baked_ref - ref).max()))
|
| 70 |
+
sess = ort.InferenceSession(str(OUT), providers=["CPUExecutionProvider"])
|
| 71 |
+
got = sess.run(["output"], {"input": x})[0]
|
| 72 |
+
print("shape", got.shape, "ref range", float(ref.min()), float(ref.max()))
|
| 73 |
+
print("max abs diff", float(np.abs(got - ref).max()), "mean abs diff", float(np.abs(got - ref).mean()))
|
| 74 |
+
# Colourise the test image with the ONNX output, as DDColor's pipeline does.
|
| 75 |
+
h, w = img.shape[:2]
|
| 76 |
+
orig_l = cv2.cvtColor((img / 255.0).astype(np.float32), cv2.COLOR_BGR2Lab)[:, :, :1]
|
| 77 |
+
ab = cv2.resize(got[0].transpose(1, 2, 0), (w, h), interpolation=cv2.INTER_NEAREST)
|
| 78 |
+
out = cv2.cvtColor(np.concatenate((orig_l, ab), -1), cv2.COLOR_LAB2BGR)
|
| 79 |
+
if PREVIEW:
|
| 80 |
+
cv2.imwrite(PREVIEW, (out * 255).round().clip(0, 255).astype(np.uint8))
|
| 81 |
+
print("size", OUT.stat().st_size, "sha256", hashlib.sha256(OUT.read_bytes()).hexdigest())
|
scripts/quantize_ddcolor_int8.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Weight-only per-channel INT8 for ddcolor-paper-tiny.onnx (OptiMix).
|
| 3 |
+
|
| 4 |
+
Only weight initializers consumed by Conv, MatMul or Gemm are quantised (symmetric INT8, one scale per
|
| 5 |
+
output channel). Each becomes int8 data + float scale + DequantizeLinear(axis) feeding the original
|
| 6 |
+
tensor name, so compute stays float32. Einsum, Erf, LayerNormalization, norms, biases and small tensors
|
| 7 |
+
are left untouched in float32. DequantizeLinear with axis needs opset >= 13; the graph is opset 17.
|
| 8 |
+
|
| 9 |
+
Usage: quantize_ddcolor_int8.py SRC.onnx DEST.onnx
|
| 10 |
+
"""
|
| 11 |
+
import hashlib, sys
|
| 12 |
+
import numpy as np, onnx
|
| 13 |
+
from onnx import helper, numpy_helper
|
| 14 |
+
|
| 15 |
+
src, dest = sys.argv[1], sys.argv[2]
|
| 16 |
+
m = onnx.load(src)
|
| 17 |
+
g = m.graph
|
| 18 |
+
inits = {i.name: i for i in g.initializer}
|
| 19 |
+
consumers = {}
|
| 20 |
+
for n in g.node:
|
| 21 |
+
for k, name in enumerate(n.input):
|
| 22 |
+
consumers.setdefault(name, []).append((n, k))
|
| 23 |
+
|
| 24 |
+
def axis_for(name, arr):
|
| 25 |
+
uses = consumers.get(name, [])
|
| 26 |
+
if not uses:
|
| 27 |
+
return None
|
| 28 |
+
axes = set()
|
| 29 |
+
for n, k in uses:
|
| 30 |
+
if n.op_type == "Conv" and k == 1 and arr.ndim == 4:
|
| 31 |
+
axes.add(0) # [out, in, kh, kw]
|
| 32 |
+
elif n.op_type == "MatMul" and k == 1 and arr.ndim == 2:
|
| 33 |
+
axes.add(1) # B is [K, N]; channel = N
|
| 34 |
+
elif n.op_type == "Gemm" and k == 1 and arr.ndim == 2:
|
| 35 |
+
tb = next((a.i for a in n.attribute if a.name == "transB"), 0)
|
| 36 |
+
axes.add(0 if tb else 1)
|
| 37 |
+
else:
|
| 38 |
+
return None # any other consumer: keep float
|
| 39 |
+
return axes.pop() if len(axes) == 1 else None
|
| 40 |
+
|
| 41 |
+
new_inits, new_nodes, done, kept = [], [], 0, 0
|
| 42 |
+
for init in list(g.initializer):
|
| 43 |
+
arr = numpy_helper.to_array(init)
|
| 44 |
+
ax = axis_for(init.name, arr) if arr.dtype == np.float32 and arr.size >= 4096 else None
|
| 45 |
+
if ax is None:
|
| 46 |
+
new_inits.append(init); kept += arr.nbytes; continue
|
| 47 |
+
red = tuple(i for i in range(arr.ndim) if i != ax)
|
| 48 |
+
scale = np.maximum(np.abs(arr).max(axis=red) / 127.0, 1e-12).astype(np.float32)
|
| 49 |
+
shp = [1] * arr.ndim; shp[ax] = -1
|
| 50 |
+
q = np.clip(np.round(arr / scale.reshape(shp)), -127, 127).astype(np.int8)
|
| 51 |
+
new_inits += [numpy_helper.from_array(q, init.name + "_q"),
|
| 52 |
+
numpy_helper.from_array(scale, init.name + "_scale"),
|
| 53 |
+
numpy_helper.from_array(np.zeros_like(scale, dtype=np.int8), init.name + "_zp")]
|
| 54 |
+
new_nodes.append(helper.make_node("DequantizeLinear",
|
| 55 |
+
[init.name + "_q", init.name + "_scale", init.name + "_zp"], [init.name],
|
| 56 |
+
name=init.name + "_dq", axis=ax))
|
| 57 |
+
done += 1
|
| 58 |
+
|
| 59 |
+
del g.initializer[:]; g.initializer.extend(new_inits)
|
| 60 |
+
nodes = new_nodes + list(g.node)
|
| 61 |
+
del g.node[:]; g.node.extend(nodes)
|
| 62 |
+
onnx.checker.check_model(m)
|
| 63 |
+
onnx.save(m, dest)
|
| 64 |
+
data = open(dest, "rb").read()
|
| 65 |
+
print(f"quantised {done} weights; float32 kept {kept/1e6:.1f} MB")
|
| 66 |
+
print(f"{hashlib.sha256(data).hexdigest()} {dest} ({len(data)} bytes)")
|