Download scripts/rebuild_fused.py from imbcmdth/faceage-onnx: direct link, hf CLI and curl.
- Browser
- Download file 14.3 kB
-
https://huggingface.co/imbcmdth/faceage-onnx/resolve/main/scripts/rebuild_fused.py
- Command line
-
hf download hf://imbcmdth/faceage-onnx/scripts/rebuild_fused.py
-
curl -L -o rebuild_fused.py https://huggingface.co/imbcmdth/faceage-onnx/resolve/main/scripts/rebuild_fused.py
14.3 kB
| """Rebuild faceage_dino_fp32.onnx as a graph of few, large kernels for ONNX Runtime's CUDA provider. | |
| python rebuild_fused.py [OUT.onnx] [--fp32] | |
| The network and its weights are the source's; only the way the graph is written changes. | |
| The weights are read out of source/faceage_dino_fp32.onnx (the source's PyTorch | |
| checkpoint is gated on the hub), and a new graph is written by hand: | |
| - The input is Bx3x224x224 fp32 with the batch dynamic, as before. Every Reshape takes a | |
| constant shape with -1 for the batch, so no Shape, Gather or Concat runs on the host. | |
| - The rotary tables are computed once here in float64 from the source's own angle table | |
| and stored as fp16 constants. The five prefix tokens (CLS and four registers) get rows | |
| of cos 1, sin 0, so one com.microsoft RotaryEmbedding covers all 201 tokens. | |
| - Attention is com.microsoft MultiHeadAttention in fp16, fed q, k and v from three | |
| separate projections, so nothing is split (a Split on the last axis was the slowest | |
| kernel in a packed-qkv version of this graph). | |
| - The MatMuls are fp16. The residual stream and every LayerNorm stay fp32, because of | |
| DINOv3's massive activation (about 1.56e5 on channel 757, written by block 0's MLP). | |
| The residual add and the LayerNorm after it are one com.microsoft SkipLayerNormalization, | |
| which also adds the output projection's bias, in fp32. Each block has four Casts: down to | |
| fp16 after each of its two LayerNorms, and back up to fp32 after each of its two output | |
| projections. | |
| - The residual stream is carried scaled by S = 1/8, with every LayerNorm's epsilon scaled | |
| by S^2, which leaves each LayerNorm's output exactly what it was. The layer scales, | |
| times S, are folded into the output projections' weights and biases. Block 0's MLP then | |
| writes about 1.95e4 into channel 757, inside fp16's 65,504; every other branch output | |
| on the WIDER FACE crops stays under 300. | |
| - v's bias is folded into o's bias: a row of attention weights sums to one, so a constant | |
| added to every value comes through attention unchanged. | |
| - The last block computes the CLS query only, and its MLP for the CLS token only, since | |
| only the CLS token is read after it. | |
| With --fp32 the same graph is written without the casts, all in fp32, as a check on the | |
| structure against the source. | |
| """ | |
| import hashlib | |
| import os | |
| import sys | |
| import numpy as np | |
| import onnx | |
| from onnx import TensorProto, helper, numpy_helper | |
| SRC = r"E:/projects/faceage-onnx/source/faceage_dino_fp32.onnx" | |
| DST = r"E:/projects/faceage-onnx/faceage-dino-vitl16-fused-fp16.onnx" | |
| S = 0.125 # scale of the residual stream | |
| EPS = 1e-5 # the source's LayerNorm epsilon | |
| HEADS = 16 | |
| DIM = 1024 | |
| TOKENS = 201 # CLS + 4 registers + 14x14 patches | |
| PREFIX = 5 | |
| P = "model.backbone." | |
| def sha256(path): | |
| d = hashlib.sha256() | |
| with open(path, "rb") as f: | |
| for chunk in iter(lambda: f.read(1 << 20), b""): | |
| d.update(chunk) | |
| return d.hexdigest() | |
| def read_source(): | |
| m = onnx.load(SRC) | |
| w = {i.name: numpy_helper.to_array(i).astype(np.float32) for i in m.graph.initializer} | |
| consumers = {} | |
| for n in m.graph.node: | |
| for i in n.input: | |
| consumers.setdefault(i, []).append(n) | |
| # The MatMuls with a weight, in graph order: q, k, v, o, up, down for each block. | |
| mm = [n for n in m.graph.node if n.op_type == "MatMul" and n.input[1] in w] | |
| assert len(mm) == 24 * 6, len(mm) | |
| roles = ["q_proj", "k_proj", "v_proj", "o_proj", "up_proj", "down_proj"] | |
| layers = [] | |
| for L in range(24): | |
| layer = {} | |
| for k, role in enumerate(roles): | |
| node = mm[L * 6 + k] | |
| layer[role] = w[node.input[1]] | |
| # Check the role against the bias the source adds to it. | |
| adds = [c for c in consumers.get(node.output[0], []) if c.op_type == "Add"] | |
| sub = "mlp" if role in ("up_proj", "down_proj") else "attention" | |
| bias = f"{P}model.layer.{L}.{sub}.{role}.bias" | |
| if role == "k_proj": | |
| assert not adds and bias not in w, (L, role) | |
| else: | |
| assert adds and adds[0].input[1] == bias, (L, role, [a.input for a in adds]) | |
| layer[role + ".bias"] = w[bias] | |
| for nm in ("norm1.weight", "norm1.bias", "norm2.weight", "norm2.bias", | |
| "layer_scale1.lambda1", "layer_scale2.lambda1"): | |
| layer[nm] = w[f"{P}model.layer.{L}.{nm}"] | |
| layers.append(layer) | |
| return w, layers | |
| class G: | |
| def __init__(self): | |
| self.nodes, self.inits, self.n = [], [], 0 | |
| def const(self, name, arr): | |
| self.inits.append(numpy_helper.from_array(arr, name)) | |
| return name | |
| def op(self, op_type, inputs, n_out=1, domain="", name=None, **attrs): | |
| self.n += 1 | |
| base = name or f"{op_type.lower()}_{self.n}" | |
| outs = [base] if n_out == 1 else [f"{base}_{k}" for k in range(n_out)] | |
| self.nodes.append(helper.make_node(op_type, inputs, outs, name=base, domain=domain or None, **attrs)) | |
| return outs[0] if n_out == 1 else outs | |
| def build(fp32_only=False): | |
| w, layers = read_source() | |
| lo = np.float32 if fp32_only else np.float16 | |
| g = G() | |
| def down(x, name): # fp32 -> matmul precision | |
| return x if fp32_only else g.op("Cast", [x], name=name, to=TensorProto.FLOAT16) | |
| def up(x, name): # matmul precision -> fp32 | |
| return x if fp32_only else g.op("Cast", [x], name=name, to=TensorProto.FLOAT) | |
| shape = lambda name, dims: g.const(name, np.array(dims, dtype=np.int64)) | |
| sh_tok3 = shape("shape_tokens3d", [-1, TOKENS, DIM]) | |
| sh_cls3 = shape("shape_cls3d", [-1, 1, DIM]) | |
| sh_2d = shape("shape_rows2d", [-1, DIM]) | |
| sh_patch = shape("shape_patches", [-1, DIM, 196]) | |
| first = g.const("slice_first", np.array([0], np.int64)) | |
| one = g.const("slice_one", np.array([1], np.int64)) | |
| axis1 = g.const("slice_axis1", np.array([1], np.int64)) | |
| # Rotary tables: cos/sin of the source's angle table, one half (the two halves are equal). | |
| tile = w["tile"].astype(np.float64) | |
| assert tile.shape == (196, 64) and np.array_equal(tile[:, :32], tile[:, 32:]) | |
| ang = tile[:, :32] | |
| cos = np.concatenate([np.ones((PREFIX, 32)), np.cos(ang)]).astype(lo) | |
| sin = np.concatenate([np.zeros((PREFIX, 32)), np.sin(ang)]).astype(lo) | |
| cos_c, sin_c = g.const("rope_cos", cos), g.const("rope_sin", sin) | |
| pos0 = g.const("rope_position_offset", np.array([0], dtype=np.int64)) | |
| def rope(t, name): | |
| return g.op("RotaryEmbedding", [t, pos0, cos_c, sin_c], domain="com.microsoft", | |
| name=name, num_heads=HEADS, rotary_embedding_dim=64) | |
| # Patch embedding (fp32), scaled by S; CLS, registers and the conv bias go in one table | |
| # that the first SkipLayerNormalization adds. | |
| conv_w = g.const("patch_embed.weight", (w[P + "embeddings.patch_embeddings.weight"] * S).astype(np.float32)) | |
| table = np.zeros((1, TOKENS, DIM), dtype=np.float32) | |
| table[0, 0] = w[P + "embeddings.cls_token"][0, 0] | |
| table[0, 1:PREFIX] = w[P + "embeddings.register_tokens"][0] | |
| table[0, PREFIX:] = w[P + "embeddings.patch_embeddings.bias"][None, :] | |
| table_c = g.const("token_table", (table * S).astype(np.float32)) | |
| x = g.op("Conv", ["face", conv_w], name="patch_embed", strides=[16, 16], kernel_shape=[16, 16]) | |
| x = g.op("Reshape", [x, sh_patch], name="patches_flat") | |
| x = g.op("Transpose", [x], name="patches_tokens", perm=[0, 2, 1]) # B,196,1024 | |
| pads = g.const("prefix_pads", np.array([0, PREFIX, 0, 0, 0, 0], dtype=np.int64)) | |
| x = g.op("Pad", [x, pads], name="prefix_slots") # B,201,1024, zeros in the prefix rows | |
| # Everything below is B x tokens x 1024, and B x 1 x 1024 after the last block's attention. | |
| # A projection's bias is added in fp32 by the SkipLayerNormalization after it, except | |
| # q's, which comes before the rotary embedding. v's bias is folded into o's: the | |
| # attention weights of a row sum to one, so it passes through attention unchanged. | |
| eps = EPS * S * S | |
| resid = None | |
| pending_bias = None # the last down projection's bias, for the next SkipLayerNormalization | |
| for L, ly in enumerate(layers): | |
| last = L == len(layers) - 1 | |
| c = lambda nm, arr, dt=np.float32: g.const(f"l{L}.{nm}", np.ascontiguousarray(arr).astype(dt)) | |
| n1w, n1b = c("norm1.weight", ly["norm1.weight"]), c("norm1.bias", ly["norm1.bias"]) | |
| if L == 0: | |
| # Residual = prefix-padded patches + token table, and its LayerNorm, in one op. | |
| ln, _, _, resid = g.op("SkipLayerNormalization", [x, table_c, n1w, n1b], n_out=4, | |
| domain="com.microsoft", name="l0.norm1", epsilon=eps) | |
| else: | |
| ln, _, _, resid = g.op("SkipLayerNormalization", [branch, resid, n1w, n1b, pending_bias], n_out=4, | |
| domain="com.microsoft", name=f"l{L}.norm1", epsilon=eps) | |
| h = down(ln, f"l{L}.norm1_to_fp16") | |
| lam1 = ly["layer_scale1.lambda1"] * S | |
| lam2 = ly["layer_scale2.lambda1"] * S | |
| wq, bq = c("q.weight", ly["q_proj"], lo), c("q.bias", ly["q_proj.bias"], lo) | |
| wk, wv = c("k.weight", ly["k_proj"], lo), c("v.weight", ly["v_proj"], lo) | |
| wo = c("o.weight", ly["o_proj"] * lam1[None, :], lo) | |
| bo = c("o.bias", (ly["v_proj.bias"].astype(np.float64) @ ly["o_proj"] + ly["o_proj.bias"]) * lam1) | |
| wup, bup = c("up.weight", ly["up_proj"], lo), c("up.bias", ly["up_proj.bias"], lo) | |
| wdn = c("down.weight", ly["down_proj"] * lam2[None, :], lo) | |
| pending_bias = c("down.bias", ly["down_proj.bias"] * lam2) | |
| n2w, n2b = c("norm2.weight", ly["norm2.weight"]), c("norm2.bias", ly["norm2.bias"]) | |
| k = rope(g.op("MatMul", [h, wk], name=f"l{L}.k_proj"), f"l{L}.rope_k") | |
| v = g.op("MatMul", [h, wv], name=f"l{L}.v_proj") | |
| if not last: | |
| q = g.op("Gemm", [g.op("Reshape", [h, sh_2d], name=f"l{L}.q_in_2d"), wq, bq], name=f"l{L}.q_proj") | |
| q = rope(g.op("Reshape", [q, sh_tok3], name=f"l{L}.q_3d"), f"l{L}.rope_q") | |
| skip = resid | |
| else: | |
| # Only the CLS token is read after this block: its query alone, and its row of | |
| # the residual. The CLS token is not rotated. | |
| hc = g.op("Slice", [h, first, one, axis1], name=f"l{L}.cls_rows") | |
| q = g.op("Gemm", [g.op("Reshape", [hc, sh_2d], name=f"l{L}.q_in_2d"), wq, bq], name=f"l{L}.q_proj") | |
| q = g.op("Reshape", [q, sh_cls3], name=f"l{L}.q_3d") | |
| skip = g.op("Slice", [resid, first, one, axis1], name=f"l{L}.resid_cls") | |
| a = g.op("MultiHeadAttention", [q, k, v], domain="com.microsoft", name=f"l{L}.attention", num_heads=HEADS) | |
| o = up(g.op("MatMul", [a, wo], name=f"l{L}.o_proj"), f"l{L}.o_proj_to_fp32") | |
| ln2, _, _, resid = g.op("SkipLayerNormalization", [o, skip, n2w, n2b, bo], n_out=4, | |
| domain="com.microsoft", name=f"l{L}.norm2", epsilon=eps) | |
| m = down(ln2, f"l{L}.norm2_to_fp16") | |
| m = g.op("BiasGelu", [g.op("MatMul", [m, wup], name=f"l{L}.up_proj"), bup], domain="com.microsoft", name=f"l{L}.gelu") | |
| branch = up(g.op("MatMul", [m, wdn], name=f"l{L}.down_proj"), f"l{L}.down_proj_to_fp32") | |
| # Final backbone norm (residual add fused), then the neck and the heads, all fp32. | |
| feat, _, _, _ = g.op("SkipLayerNormalization", [branch, resid, g.const("norm.weight", w[P + "norm.weight"]), | |
| g.const("norm.bias", w[P + "norm.bias"]), pending_bias], | |
| n_out=4, domain="com.microsoft", name="backbone_norm", epsilon=eps) | |
| feat = g.op("Reshape", [feat, sh_2d], name="cls_2d") | |
| feat = g.op("LayerNormalization", [feat, g.const("neck.0.weight", w["model.neck.0.weight"]), | |
| g.const("neck.0.bias", w["model.neck.0.bias"])], name="neck_norm", epsilon=EPS, axis=-1) | |
| feat = g.op("MatMul", [feat, g.const("neck.1.weight", np.ascontiguousarray(w["model.neck.1.weight"].T))], name="neck_linear") | |
| feat = g.op("BiasGelu", [feat, g.const("neck.1.bias", w["model.neck.1.bias"])], domain="com.microsoft", name="neck_gelu") | |
| g.nodes.append(helper.make_node("Gemm", [feat, g.const("age_head.weight", w["model.age_head.weight"]), | |
| g.const("age_head.bias", w["model.age_head.bias"])], | |
| ["age_logits"], name="age_head", transB=1)) | |
| g.nodes.append(helper.make_node("Gemm", [feat, g.const("gender_head.weight", w["model.gender_head.weight"]), | |
| g.const("gender_head.bias", w["model.gender_head.bias"])], | |
| ["gender_logits"], name="gender_head", transB=1)) | |
| graph = helper.make_graph( | |
| g.nodes, "faceage_dino_vitl16_fused", | |
| [helper.make_tensor_value_info("face", TensorProto.FLOAT, ["batch", 3, 224, 224])], | |
| [helper.make_tensor_value_info("age_logits", TensorProto.FLOAT, ["batch", 100]), | |
| helper.make_tensor_value_info("gender_logits", TensorProto.FLOAT, ["batch", 2])], | |
| g.inits) | |
| model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 18), helper.make_opsetid("com.microsoft", 1)], | |
| producer_name="faceage-onnx/scripts/rebuild_fused.py") | |
| model.ir_version = 10 | |
| return model | |
| def main(): | |
| args = [a for a in sys.argv[1:] if not a.startswith("--")] | |
| fp32_only = "--fp32" in sys.argv | |
| out = args[0] if args else DST | |
| model = build(fp32_only) | |
| onnx.checker.check_model(model) | |
| onnx.save(model, out, save_as_external_data=False) | |
| ops = {} | |
| for n in model.graph.node: | |
| ops[n.op_type] = ops.get(n.op_type, 0) + 1 | |
| print("nodes", len(model.graph.node), "casts", ops.get("Cast", 0), sorted(ops.items(), key=lambda kv: -kv[1])) | |
| print("saved", out, os.path.getsize(out), sha256(out)) | |
| if __name__ == "__main__": | |
| main() | |