Download export/detector-dfine-small.py from executorch-community/D-FINE-Small-COCO-ExecuTorch-WebGPU: direct link, hf CLI and curl.
- Browser
- Download file 16.8 kB
-
https://huggingface.co/executorch-community/D-FINE-Small-COCO-ExecuTorch-WebGPU/resolve/main/export/detector-dfine-small.py
- Command line
-
hf download hf://executorch-community/D-FINE-Small-COCO-ExecuTorch-WebGPU/export/detector-dfine-small.py
-
curl -L -o detector-dfine-small.py https://huggingface.co/executorch-community/D-FINE-Small-COCO-ExecuTorch-WebGPU/resolve/main/export/detector-dfine-small.py
16.8 kB
| #!/usr/bin/env python3 | |
| """D-FINE small (COCO, Apache-2.0) -> ExecuTorch WebGPU, for the playground's current forward runtime. | |
| Upstream: HF transformers port `ustc-community/dfine-small-coco` (Peterande/D-FINE, D-FINE-S). Every tensor in | |
| it is bit-identical to the authors' `dfine_s_coco.pth` on `Peterande/D-FINE` (see verify_official.py). | |
| Input: [1, 3, 640, 640] float32 RGB in [0, 1] (image / 255; the HF processor does no mean/std). | |
| Upstream resizes to 640x640 without keeping aspect; a letterboxed input also works. | |
| Output: [1, 84, 300] float32, the YOLO layout src/vision/detect.js reads: rows 0-3 are | |
| cx, cy, w, h in input pixels, rows 4-83 are sigmoid class probabilities (80 COCO classes, | |
| contiguous ids, same order as the YOLO head). 300 queries; no NMS needed (DETR). | |
| The eager graph is rebuilt from the HF modules with output-preserving rewrites so that the | |
| whole program is one delegate made of operators the current runtime registers | |
| (see _detector.py): topk -> TopKSet (same query set; queries come out in anchor order, and the | |
| decoder is permutation equivariant), grid_sample -> gathers, inverse_sigmoid/log -> identity, | |
| LQE top-4 -> rank-based selection, max over classes -> exact where-tree, SiLU -> x * sigmoid(x), | |
| frozen BN -> precomputed scale/shift, position embedding and anchors -> constants. | |
| """ | |
| import argparse | |
| import copy | |
| import json | |
| import sys | |
| from pathlib import Path | |
| import numpy as np | |
| import torch | |
| import torch.nn.functional as F | |
| sys.path.insert(0, str(Path(__file__).resolve().parent)) | |
| import _common # noqa: E402 | |
| import _detector as D # noqa: E402 | |
| UPSTREAM = { | |
| "repo": "ustc-community/dfine-small-coco", | |
| "revision": "f79e65b5fbb33ceb9d3ebba042955d7410c608f8", | |
| "files": { | |
| "model.safetensors": "a144baff3a27bfd538fe37b11fa8d4fca5744b8c054b5301e7c699a1379cd18f", | |
| }, | |
| "license": "Apache-2.0", | |
| } | |
| OFFICIAL = { | |
| "repo": "Peterande/D-FINE", | |
| "revision": "19214e91f5ce4c1e609022c38b1fc953c47404e7", | |
| "file": "dfine_s_coco.pth", | |
| "sha256": "48a6c8cc43eb57186843f752e2e8461ddd3326e0d3c575e71e6e960844683e89", | |
| "result": "all 764 tensors bit-identical (verify_official.py)", | |
| } | |
| SIZE = 640 | |
| def load_hf(): | |
| from transformers import DFineForObjectDetection | |
| path, digest = _common.hf_file( | |
| UPSTREAM["repo"], | |
| "model.safetensors", | |
| UPSTREAM["revision"], | |
| UPSTREAM["files"]["model.safetensors"], | |
| ) | |
| m = DFineForObjectDetection.from_pretrained( | |
| UPSTREAM["repo"], revision=UPSTREAM["revision"], attn_implementation="eager" | |
| ).eval() | |
| return m, digest | |
| class DFineStatic(torch.nn.Module): | |
| def __init__(self, hf, size=SIZE): | |
| super().__init__() | |
| cfg = hf.config | |
| self.cfg = cfg | |
| mdl = hf.model | |
| self.backbone = mdl.backbone.model | |
| from transformers.models.d_fine.modeling_d_fine import DFineFrozenBatchNorm2d | |
| D.replace_modules( | |
| self.backbone, | |
| lambda c: isinstance(c, DFineFrozenBatchNorm2d), | |
| lambda c: D.FoldedBatchNorm( | |
| c.weight, c.bias, c.running_mean, c.running_var, 1e-5 | |
| ), | |
| ) | |
| self.encoder_input_proj = mdl.encoder_input_proj | |
| self.encoder = mdl.encoder | |
| D.replace_modules( | |
| self, | |
| lambda c: type(c).__name__ in ("SiLUActivation", "SiLU"), | |
| lambda c: D.SiLU(), | |
| ) | |
| self.decoder_input_proj = mdl.decoder_input_proj | |
| self.enc_output = mdl.enc_output | |
| self.enc_score_head = mdl.enc_score_head | |
| self.enc_bbox_head = mdl.enc_bbox_head | |
| self.decoder = mdl.decoder | |
| self.class_embed = hf.class_embed | |
| self.bbox_embed = hf.bbox_embed | |
| strides = cfg.feat_strides | |
| self.shapes = [(size // s, size // s) for s in strides] | |
| for _ in range(cfg.num_feature_levels - len(strides)): | |
| h, w = self.shapes[-1] | |
| self.shapes.append(((h + 1) // 2, (w + 1) // 2)) | |
| n = sum(h * w for h, w in self.shapes) | |
| anchors, valid = mdl.generate_anchors( | |
| tuple(self.shapes), device="cpu", dtype=torch.float32 | |
| ) | |
| self.register_buffer("anchors", anchors.clone(), persistent=False) | |
| self.register_buffer("valid", valid.to(torch.float32).clone(), persistent=False) | |
| aifi_hw = self.shapes[cfg.encode_proj_layers[0]] | |
| from transformers.models.d_fine.modeling_d_fine import ( | |
| build_2d_sinusoidal_position_embedding, | |
| ) | |
| pos = build_2d_sinusoidal_position_embedding( | |
| aifi_hw[0], | |
| aifi_hw[1], | |
| cfg.encoder_hidden_dim, | |
| cfg.positional_encoding_temperature, | |
| ) | |
| self.register_buffer( | |
| "aifi_pos", pos.unsqueeze(0).to(torch.float32), persistent=False | |
| ) | |
| from transformers.models.d_fine.modeling_d_fine import weighting_function | |
| with torch.no_grad(): | |
| project = weighting_function( | |
| cfg.max_num_bins, self.decoder.up, self.decoder.reg_scale | |
| ) | |
| self.register_buffer( | |
| "project", project.reshape(1, -1).clone(), persistent=False | |
| ) | |
| self.reg_scale = float(self.decoder.reg_scale.item()) | |
| self.topk = D.TopKSet(n, cfg.num_queries) | |
| self.sampler = D.DeformSampler( | |
| self.shapes, | |
| list(self.decoder.layers[0].encoder_attn.num_points_list), | |
| cfg.decoder_attention_heads, | |
| cfg.d_model // cfg.decoder_attention_heads, | |
| ) | |
| self.register_buffer( | |
| "pix_scale", | |
| torch.tensor([size, size, size, size], dtype=torch.float32), | |
| persistent=False, | |
| ) | |
| self.register_buffer("one", torch.ones(1), persistent=False) | |
| self.register_buffer("two", torch.full((1,), 2.0), persistent=False) | |
| self.register_buffer("zero", torch.zeros(1), persistent=False) | |
| # ---- encoder ------------------------------------------------------------------------- | |
| def aifi(self, x): | |
| cfg = self.cfg | |
| b, c, h, w = x.shape | |
| hs = x.flatten(2).permute(0, 2, 1) | |
| for layer in self.encoder.aifi[0].layers: | |
| a = layer.self_attn | |
| att = D.attention( | |
| a.q_proj, | |
| a.k_proj, | |
| a.v_proj, | |
| a.o_proj, | |
| hs, | |
| self.aifi_pos, | |
| cfg.encoder_attention_heads, | |
| ) | |
| hs = layer.self_attn_layer_norm(hs + att) | |
| hs = layer.final_layer_norm(hs + layer.mlp(hs)) | |
| return hs.permute(0, 2, 1).reshape(b, c, h, w) | |
| def run_encoder(self, feats): | |
| enc = self.encoder | |
| feats = list(feats) | |
| idx = self.cfg.encode_proj_layers[0] | |
| feats[idx] = self.aifi(feats[idx]) | |
| fpn = [feats[-1]] | |
| for i, (lat, blk) in enumerate(zip(enc.lateral_convs, enc.fpn_blocks)): | |
| back = feats[enc.num_fpn_stages - i - 1] | |
| top = lat(fpn[-1]) | |
| fpn[-1] = top | |
| top = F.interpolate(top, scale_factor=2.0, mode="nearest") | |
| fpn.append(blk(torch.cat([top, back], dim=1))) | |
| fpn.reverse() | |
| pan = [fpn[0]] | |
| for i, (down, blk) in enumerate(zip(enc.downsample_convs, enc.pan_blocks)): | |
| pan.append(blk(torch.cat([down(pan[-1]), fpn[i + 1]], dim=1))) | |
| return pan | |
| # ---- decoder ------------------------------------------------------------------------- | |
| def deform(self, attn_mod, hidden, ref, value): | |
| """hidden [1, Q, C]; ref [1, Q, 4]; value [L, heads, hd].""" | |
| cfg = self.cfg | |
| q = hidden.shape[1] | |
| nh = cfg.decoder_attention_heads | |
| P = sum(attn_mod.num_points_list) | |
| off = attn_mod.sampling_offsets(hidden).view(q, nh, P, 2) | |
| aw = torch.softmax(attn_mod.attention_weights(hidden).view(q, nh, P), dim=-1) | |
| scale = attn_mod.num_points_scale.view(1, 1, P, 1) | |
| r = ref.view(q, 1, 1, 4) | |
| loc = r[..., :2] + off * scale * r[..., 2:] * attn_mod.offset_scale | |
| out = self.sampler(value, loc, aw).view(1, q, cfg.d_model) | |
| return out | |
| def decoder_layer(self, layer, hs, pos, ref, value): | |
| cfg = self.cfg | |
| a = layer.self_attn | |
| att = D.attention( | |
| a.q_proj, a.k_proj, a.v_proj, a.o_proj, hs, pos, cfg.decoder_attention_heads | |
| ) | |
| hs = layer.self_attn_layer_norm(hs + att) | |
| cross = self.deform(layer.encoder_attn, hs + pos, ref, value) | |
| hs = layer.gateway(hs, cross) | |
| hs = hs + layer.mlp(hs) | |
| return layer.final_layer_norm(D.clamp(hs, -65504.0, 65504.0)) | |
| def integral(self, corners): | |
| q = corners.shape[1] | |
| p = torch.softmax(corners.reshape(q * 4, self.cfg.max_num_bins + 1), dim=-1) | |
| return F.linear(p, self.project).reshape(1, q, 4) | |
| def distance2bbox(self, pts, dist): | |
| rs = self.reg_scale | |
| cx, cy, w, h = pts[..., 0:1], pts[..., 1:2], pts[..., 2:3], pts[..., 3:4] | |
| x0 = cx - (0.5 * rs + dist[..., 0:1]) * (w / rs) | |
| y0 = cy - (0.5 * rs + dist[..., 1:2]) * (h / rs) | |
| x1 = cx + (0.5 * rs + dist[..., 2:3]) * (w / rs) | |
| y1 = cy + (0.5 * rs + dist[..., 3:4]) * (h / rs) | |
| return torch.cat([(x0 + x1) / 2, (y0 + y1) / 2, x1 - x0, y1 - y0], dim=-1) | |
| def lqe(self, lqe_mod, scores, corners): | |
| q = corners.shape[1] | |
| nb = self.cfg.max_num_bins + 1 | |
| k = self.cfg.top_prob_values | |
| prob = torch.softmax(corners.reshape(q * 4, nb), dim=-1) # [R, nb] | |
| diff = prob.view(q * 4, 1, nb) - prob.view( | |
| q * 4, nb, 1 | |
| ) # [R, j, l] = p_l - p_j | |
| greater = torch.where(diff > 0, self.one, self.zero).sum(dim=-1) # [R, nb] | |
| tops = [] | |
| for i in range(k): | |
| masked = torch.where(greater <= i, prob, self.two) | |
| tops.append(D.tree_reduce(masked, D.where_min)) # i-th largest value | |
| top = torch.cat(tops, dim=-1) | |
| stat = torch.cat([top, top.mean(dim=-1, keepdim=True)], dim=-1).reshape( | |
| 1, q, 4 * (k + 1) | |
| ) | |
| return scores + lqe_mod.reg_conf(stat) | |
| def forward(self, pixel_values): | |
| cfg = self.cfg | |
| feats = self.backbone(pixel_values).feature_maps | |
| proj = [self.encoder_input_proj[i](f) for i, f in enumerate(feats)] | |
| enc = self.run_encoder(proj) | |
| sources = [self.decoder_input_proj[i](s) for i, s in enumerate(enc)] | |
| for i in range(len(sources), cfg.num_feature_levels): | |
| sources.append(self.decoder_input_proj[i](enc[-1])) | |
| flat = torch.cat( | |
| [s.flatten(2).permute(0, 2, 1) for s in sources], dim=1 | |
| ) # [1, L, C] | |
| L = flat.shape[1] | |
| memory = self.valid * flat | |
| out_mem = self.enc_output(memory) | |
| cls = self.enc_score_head(out_mem) # [1, L, 80] | |
| coord = self.enc_bbox_head(out_mem) + self.anchors # [1, L, 4] | |
| best = D.tree_reduce(cls, D.where_max).view(1, L) | |
| idx = self.topk(best) # [Q] float, ascending anchor order | |
| Q = cfg.num_queries | |
| idx_c = idx.view(1, Q, 1).expand(1, Q, cfg.d_model).to(torch.int64) | |
| target = torch.gather(out_mem, 1, idx_c) | |
| idx_4 = idx.view(1, Q, 1).expand(1, Q, 4).to(torch.int64) | |
| ref_unact = torch.gather(coord, 1, idx_4) | |
| nh = cfg.decoder_attention_heads | |
| value = flat.view(L, nh, cfg.d_model // nh) | |
| dec = self.decoder | |
| hs = target | |
| ref = torch.sigmoid(ref_unact) | |
| out_detach = None | |
| corners_prev = None | |
| ref_initial = None | |
| for i, layer in enumerate(dec.layers): | |
| pos = D.clamp(dec.query_pos_head(ref), -10.0, 10.0) | |
| hs = self.decoder_layer(layer, hs, pos, ref, value) | |
| if i == 0: | |
| ref_initial = D.sigmoid_add_logit(dec.pre_bbox_head(hs), ref) | |
| if out_detach is None: | |
| corners = self.bbox_embed[i](hs) | |
| else: | |
| corners = self.bbox_embed[i](hs + out_detach) + corners_prev | |
| box = self.distance2bbox(ref_initial, self.integral(corners)) | |
| corners_prev = corners | |
| ref = box | |
| out_detach = hs | |
| last = len(dec.layers) - 1 | |
| scores = self.class_embed[last](hs) | |
| scores = self.lqe(dec.lqe_layers[last], scores, corners) | |
| return D.yolo_layout_buf(box, torch.sigmoid(scores), self.pix_scale) | |
| # --------------------------------------------------------------------------------------------- | |
| def hf_reference(hf, x): | |
| """HF eager outputs in the same layout, with queries permuted to ascending anchor order.""" | |
| with torch.no_grad(): | |
| o = hf(pixel_values=x) | |
| order = torch.topk( | |
| o.enc_outputs_class.max(-1).values, hf.config.num_queries, dim=1 | |
| ).indices[0] | |
| perm = torch.argsort(order) | |
| boxes = o.pred_boxes[:, perm] | |
| probs = torch.sigmoid(o.logits[:, perm]) | |
| return torch.cat([boxes * SIZE, probs], dim=-1).permute(0, 2, 1).numpy(), order | |
| def load_image(path, size=SIZE): | |
| from PIL import Image | |
| img = Image.open(path).convert("RGB").resize((size, size), Image.BILINEAR) | |
| a = np.asarray(img, dtype=np.float32) / 255.0 | |
| return torch.from_numpy(a.transpose(2, 0, 1).copy()).unsqueeze(0) | |
| def top_detections(out, thr=0.4): | |
| out = np.asarray(out)[0] | |
| probs = out[4:] | |
| cls = probs.argmax(0) | |
| sc = probs.max(0) | |
| keep = np.argsort(-sc) | |
| res = [] | |
| for q in keep: | |
| if sc[q] < thr: | |
| break | |
| cx, cy, w, h = out[:4, q] | |
| res.append( | |
| ( | |
| int(cls[q]), | |
| round(float(sc[q]), 3), | |
| [ | |
| round(float(v), 1) | |
| for v in (cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2) | |
| ], | |
| ) | |
| ) | |
| return res | |
| def main(): | |
| ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) | |
| ap.add_argument("--out", required=True) | |
| ap.add_argument( | |
| "--image", | |
| required=True, | |
| help="any RGB photo; the rewritten graph is checked against upstream eager on it", | |
| ) | |
| args = ap.parse_args() | |
| out = Path(args.out) | |
| out.mkdir(parents=True, exist_ok=True) | |
| hf, digest = load_hf() | |
| model = DFineStatic(copy.deepcopy(hf)).eval() | |
| img = load_image(args.image) | |
| # Output-preservation check of the rewritten eager graph against HF eager, real image. | |
| ref, order = hf_reference(hf, img) | |
| with torch.no_grad(): | |
| mine = model(img).numpy() | |
| parity = { | |
| "image": Path(args.image).name, | |
| "rewritten_eager_vs_hf_eager": _common.compare(mine, ref), | |
| } | |
| parity["rewritten_eager_vs_hf_eager"]["max_abs_boxes_px"] = float( | |
| np.abs(mine[:, :4] - ref[:, :4]).max() | |
| ) | |
| parity["rewritten_eager_vs_hf_eager"]["max_abs_probs"] = float( | |
| np.abs(mine[:, 4:] - ref[:, 4:]).max() | |
| ) | |
| parity["detections_hf"] = top_detections(ref) | |
| parity["detections_rewritten"] = top_detections(mine) | |
| print(json.dumps(parity, indent=1)) | |
| ep, webgpu, portable = _common.export_vision( | |
| name="dfine_s_coco", | |
| model=model, | |
| example_input=img, | |
| out_dir=out, | |
| upstream={ | |
| "model": "D-FINE-S (COCO), HF transformers port", | |
| "weights": { | |
| "repo": UPSTREAM["repo"], | |
| "revision": UPSTREAM["revision"], | |
| "file": "model.safetensors", | |
| "sha256": digest, | |
| "verified_against": OFFICIAL, | |
| }, | |
| "license": UPSTREAM["license"], | |
| }, | |
| extra_recipe={ | |
| "preprocessing_outside_graph": "RGB resize to 640x640 (letterbox also works), / 255; no mean/std", | |
| "postprocessing_outside_graph": "output [1, 84, 300]: cx, cy, w, h (input px) + 80 sigmoid probs; threshold; NMS optional", | |
| "rewrites": "see recipes/detector-dfine-small.py docstring and recipes/_detector.py", | |
| }, | |
| golden=(img.numpy(), ref), | |
| ) | |
| # Real-image check of the exported paths as well. | |
| D.relabel_golden( | |
| out, | |
| "internal_golden_* here = upstream HF transformers eager output on the real image (same layout; DETR queries permuted to anchor order)", | |
| ) | |
| port = _common.run_pte(portable.buffer, (img,))[0] | |
| parity["portable_pte_vs_hf_eager"] = _common.compare(port, ref) | |
| parity["portable_pte_vs_rewritten_eager"] = _common.compare(port, mine) | |
| parity["detections_portable_pte"] = top_detections(port) | |
| (out / "hf_parity.json").write_text(json.dumps(parity, indent=1) + "\n") | |
| np.save(out / "cpu_eager_out.npy", mine) | |
| img.numpy().astype(np.float32).tofile(out / "input_640.f32") | |
| print( | |
| json.dumps( | |
| {k: v for k, v in parity.items() if not k.startswith("detections")}, | |
| indent=1, | |
| ) | |
| ) | |
| if __name__ == "__main__": | |
| main() | |