mlboydaisuke commited on
Commit
33377c8
·
verified ·
1 Parent(s): 980b008

Upload convert_da3.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. convert_da3.py +227 -0
convert_da3.py ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Produce the full GPU-clean DA3-SMALL mono tflite:
3
+ weight-prefix fix + C14 (RoPE const) + C12 (qkv-decompose) + pos_embed bilinear + ConvTranspose->bilinear+1x1.
4
+ Measures depth corr vs the ORIGINAL model, op-checks, then FP16-quantizes."""
5
+ import sys, types, math, numpy as np, json, collections
6
+ class _Dummy:
7
+ def __getattr__(self, n): return lambda *a, **k: None
8
+ _pp = types.ModuleType('scipy.sparse.linalg._propack')
9
+ for nm in ('_spropack', '_dpropack', '_cpropack', '_zpropack'): setattr(_pp, nm, _Dummy())
10
+ sys.modules['scipy.sparse.linalg._propack'] = _pp
11
+ for _n, _t in (("bool", bool), ("float", float), ("int", int), ("object", object), ("str", str)):
12
+ if not hasattr(np, _n): setattr(np, _n, _t)
13
+ sys.path.insert(0, "src")
14
+ import torch, torch.nn as nn, torch.nn.functional as F, types as _ty
15
+ from PIL import Image
16
+ from huggingface_hub import hf_hub_download
17
+ from safetensors.torch import load_file
18
+ from depth_anything_3.cfg import create_object
19
+ from depth_anything_3.model.dinov2.layers.attention import Attention
20
+ import depth_anything_3.model.dinov2.layers.rope as _rope
21
+
22
+ # C14: RoPE max_position = int(positions.max())+1 is data-dependent (torch.export aborts). Fix = constant
23
+ # 128 (>= the 33 a 518 grid needs); extra table rows are unused -> numerically identical to stock.
24
+ def _rope_fwd(self, tokens, positions):
25
+ fd = tokens.size(-1) // 2
26
+ c, s = self._compute_frequency_components(fd, 128, tokens.device, tokens.dtype)
27
+ v, h = tokens.chunk(2, dim=-1)
28
+ v = self._apply_1d_rope(v, positions[..., 0], c, s)
29
+ h = self._apply_1d_rope(h, positions[..., 1], c, s)
30
+ return torch.cat((v, h), dim=-1)
31
+ _rope.RotaryPositionEmbedding2D.forward = _rope_fwd
32
+
33
+ cfg = json.load(open(hf_hub_download("depth-anything/DA3-SMALL", "config.json")))
34
+ net = create_object(cfg["config"]).eval()
35
+ sd = load_file(hf_hub_download("depth-anything/DA3-SMALL", "model.safetensors"))
36
+ net.load_state_dict({(k[6:] if k.startswith("model.") else k): v for k, v in sd.items()}, strict=False)
37
+
38
+ class Mono(nn.Module):
39
+ def __init__(s, n): super().__init__(); s.b, s.h = n.backbone, n.head
40
+ def forward(s, x):
41
+ H, W = x.shape[-2], x.shape[-1]
42
+ f, _ = s.b(x.unsqueeze(1), cam_token=None)
43
+ return s.h(f, H, W, patch_start_idx=0)["depth"]
44
+ m = Mono(net).eval()
45
+
46
+ # input H,W (multiples of 14). argv[2]=H, argv[3]=W (default square). Native aspect (no padding) matches official.
47
+ H_IN = int(sys.argv[2]) if len(sys.argv) > 2 else 448
48
+ W_IN = int(sys.argv[3]) if len(sys.argv) > 3 else H_IN
49
+ S = H_IN # back-compat alias used below for the square-ish paths
50
+ img = Image.open(sys.argv[1]).convert("RGB").resize((W_IN, H_IN), Image.BILINEAR)
51
+ a = (np.asarray(img, np.float32)/255.0 - [0.485,0.456,0.406]) / [0.229,0.224,0.225]
52
+ x_img = torch.from_numpy(np.transpose(a, (2,0,1))[None].astype(np.float32))
53
+ with torch.no_grad():
54
+ d_orig = m(x_img)[0,0].numpy() # ORIGINAL (all stock)
55
+
56
+ # ---- C12: qkv-decompose -> 4D manual attention ----
57
+ def _attn(self, x, pos=None, attn_mask=None):
58
+ B,N,C = x.shape; H=self.num_heads; Hd=C//H
59
+ q=self.q_lin(x).reshape(B,N,H,Hd).permute(0,2,1,3); k=self.k_lin(x).reshape(B,N,H,Hd).permute(0,2,1,3)
60
+ v=self.v_lin(x).reshape(B,N,H,Hd).permute(0,2,1,3); q,k=self.q_norm(q),self.k_norm(k)
61
+ if self.rope is not None and pos is not None: q=self.rope(q,pos); k=self.rope(k,pos)
62
+ q=q*self.scale; attn=(q@k.transpose(-2,-1)).softmax(-1)
63
+ return self.proj_drop(self.proj((attn@v).transpose(1,2).reshape(B,N,C)))
64
+ Attention.forward = _attn
65
+ for mod in net.modules():
66
+ if isinstance(mod, Attention):
67
+ C=mod.qkv.in_features; w=mod.qkv.weight; b=mod.qkv.bias
68
+ for nm,sl in (("q_lin",slice(0,C)),("k_lin",slice(C,2*C)),("v_lin",slice(2*C,3*C))):
69
+ lin=nn.Linear(C,C,bias=b is not None)
70
+ with torch.no_grad():
71
+ lin.weight.copy_(w[sl]); b is not None and lin.bias.copy_(b[sl])
72
+ setattr(mod,nm,lin)
73
+
74
+ # ---- LayerScale bake (GPU FC-layout fix): fold ls1/ls2 gamma into attn.proj / mlp.fc2, ls->Identity.
75
+ # The LayerScale MUL (FC output [N,C] * gamma [C]) makes ML Drift mis-lay-out the token dim
76
+ # ({1,1,1025,384} vs {1025,1,1,384}) -> GPU compile fails. Baking eliminates the MUL. (MoGe's fix.)
77
+ def bake_layerscale(model):
78
+ cnt = 0
79
+ for block in model.modules():
80
+ if hasattr(block, "ls1") and hasattr(getattr(block, "ls1"), "gamma") and hasattr(block, "attn"):
81
+ g = block.ls1.gamma.data.squeeze()
82
+ with torch.no_grad():
83
+ block.attn.proj.weight.data.mul_(g.unsqueeze(1))
84
+ if block.attn.proj.bias is not None: block.attn.proj.bias.data.mul_(g)
85
+ block.ls1 = nn.Identity(); cnt += 1
86
+ if hasattr(block, "ls2") and hasattr(getattr(block, "ls2"), "gamma") and hasattr(block, "mlp"):
87
+ g = block.ls2.gamma.data.squeeze()
88
+ last = None
89
+ for ch in reversed(list(block.mlp.children())):
90
+ if isinstance(ch, nn.Linear): last = ch; break
91
+ if last is None and hasattr(block.mlp, "fc2"): last = block.mlp.fc2
92
+ if last is not None:
93
+ with torch.no_grad():
94
+ last.weight.data.mul_(g.unsqueeze(1))
95
+ if last.bias is not None: last.bias.data.mul_(g)
96
+ block.ls2 = nn.Identity(); cnt += 1
97
+ print(f"baked {cnt} LayerScale into Linear")
98
+ return cnt
99
+ bake_layerscale(net)
100
+
101
+ # ---- C15: pos_embed BAKE (interpolating the constant pos_embed emits RESIZE_BILINEAR with 0 runtime
102
+ # inputs -> GPU rejects). Precompute the bilinear-resized pos_embed as a constant buffer -> no resize op. ----
103
+ GH, GW = H_IN // 14, W_IN // 14
104
+ for mod in net.modules():
105
+ if hasattr(mod, "interpolate_pos_encoding") and hasattr(mod, "pos_embed"):
106
+ with torch.no_grad():
107
+ N = mod.pos_embed.shape[1] - 1; Mb = int(math.sqrt(N)); dim = mod.pos_embed.shape[-1]
108
+ pe = mod.pos_embed.float(); cls = pe[:, 0]; patch = pe[:, 1:]
109
+ patch = F.interpolate(patch.reshape(1, Mb, Mb, dim).permute(0, 3, 1, 2),
110
+ size=(GH, GW), mode="bicubic", antialias=False) # bicubic = match official (baked const)
111
+ patch = patch.permute(0, 2, 3, 1).view(1, -1, dim)
112
+ baked = torch.cat((cls.unsqueeze(0), patch), dim=1).to(mod.pos_embed.dtype) # [1,1025,dim]
113
+ mod.register_buffer("_baked_pos", baked)
114
+ mod.interpolate_pos_encoding = _ty.MethodType(lambda self, x, w, h: self._baked_pos, mod)
115
+
116
+ # ---- SELECT_V2 fix: `x[:, :, 0] = cam_token` (in-place index assign) lowers to SELECT_V2 with a
117
+ # broadcast 'else' shape the GPU delegate rejects. Replace with an equivalent torch.cat (exact, GPU-clean).
118
+ # (alt_start must stay on — the camera-token / global-attn path DOES affect mono depth.) ----
119
+ import depth_anything_3.model.dinov2.vision_transformer as _vt
120
+ def _patched_gil(self, x, n=1, export_feat_layers=[], **kwargs):
121
+ B, S, _, H, W = x.shape
122
+ x = self.prepare_tokens_with_masks(x)
123
+ output, total_block_len, aux_output = [], len(self.blocks), []
124
+ blocks_to_take = range(total_block_len - n, total_block_len) if isinstance(n, int) else n
125
+ pos, pos_nodiff = self._prepare_rope(B, S, H, W, x.device)
126
+ local_x = x
127
+ for i, blk in enumerate(self.blocks):
128
+ if i < self.rope_start or self.rope is None:
129
+ g_pos, l_pos = None, None
130
+ else:
131
+ g_pos, l_pos = pos_nodiff, pos
132
+ if self.alt_start != -1 and (i == self.alt_start - 1) and x.shape[1] >= _vt.THRESH_FOR_REF_SELECTION and kwargs.get("cam_token", None) is None:
133
+ b_idx = _vt.select_reference_view(x, strategy=kwargs.get("ref_view_strategy", "saddle_balanced"))
134
+ x = _vt.reorder_by_reference(x, b_idx); local_x = _vt.reorder_by_reference(local_x, b_idx)
135
+ if self.alt_start != -1 and i == self.alt_start:
136
+ if kwargs.get("cam_token", None) is not None:
137
+ cam_token = kwargs.get("cam_token")
138
+ else:
139
+ ref_token = self.camera_token[:, :1].expand(B, -1, -1)
140
+ src_token = self.camera_token[:, 1:].expand(B, S - 1, -1)
141
+ cam_token = torch.cat([ref_token, src_token], dim=1)
142
+ x = torch.cat([cam_token.unsqueeze(2), x[:, :, 1:]], dim=2) # was: x[:, :, 0] = cam_token
143
+ if self.alt_start != -1 and i >= self.alt_start and i % 2 == 1:
144
+ x = self.process_attention(x, blk, "global", pos=g_pos, attn_mask=kwargs.get("attn_mask", None))
145
+ else:
146
+ x = self.process_attention(x, blk, "local", pos=l_pos); local_x = x
147
+ if i in blocks_to_take:
148
+ out_x = torch.cat([local_x, x], dim=-1) if self.cat_token else x
149
+ if x.shape[1] >= _vt.THRESH_FOR_REF_SELECTION and self.alt_start != -1 and "b_idx" in locals():
150
+ out_x = _vt.restore_original_order(out_x, b_idx)
151
+ output.append((out_x[:, :, 0], out_x))
152
+ if i in export_feat_layers:
153
+ aux_output.append(x)
154
+ return output, aux_output
155
+ _vt.DinoVisionTransformer._get_intermediate_layers_not_chunked = _patched_gil
156
+
157
+ # ---- TRANSPOSE_CONV (Pixel 8a reject): ConvTranspose2d(k=s,stride=s) -> bilinear-resize + 1x1 conv ----
158
+ # 1x1 conv weight = mean of the transposed kernel over its s*s positions (preserves channel mixing;
159
+ # spatial upsampling handled by bilinear). 1x1 conv commutes with bilinear so order is exact for the mix.
160
+ # EXACT GPU-clean equivalent: ConvTranspose2d(k=s,stride=s) == zero-stuff (nearest-upsample × top-left
161
+ # mask) + Conv2d(flipped weight). Matches the learned upsampler to ~1e-7 → depth stays as sharp as the
162
+ # original (a bilinear approx blurred it). Mask is a precomputed constant buffer (no index-assign/broadcast).
163
+ class ZeroStuffConvT(nn.Module):
164
+ def __init__(self, ct, H, W):
165
+ super().__init__(); self.s = ct.stride[0]; self.k = ct.kernel_size[0]
166
+ self.register_buffer("w", ct.weight.flip(2, 3).transpose(0, 1).contiguous())
167
+ self.register_buffer("b", ct.bias.detach().clone() if ct.bias is not None else torch.zeros(ct.out_channels))
168
+ s = self.s; mk = np.zeros((H*s, W*s), np.float32); mk[::s, ::s] = 1.0
169
+ self.register_buffer("mask", torch.from_numpy(mk)[None, None])
170
+ def forward(self, x):
171
+ H, W = x.shape[-2], x.shape[-1]; s, k = self.s, self.k
172
+ xn = F.interpolate(x, size=(H*s, W*s), mode="nearest")
173
+ y = F.conv2d(xn * self.mask, self.w, bias=self.b, padding=k-1)
174
+ return y[:, :, :H*s, :W*s]
175
+ # discover each ConvTranspose input size via a dry run, then swap with the exact equivalent
176
+ _ct_hw, _hooks = {}, []
177
+ for _nm, _mod in net.named_modules():
178
+ if isinstance(_mod, nn.ConvTranspose2d):
179
+ def _mk(nm):
180
+ def _h(m, inp, out): _ct_hw[nm] = (inp[0].shape[-2], inp[0].shape[-1])
181
+ return _h
182
+ _hooks.append(_mod.register_forward_hook(_mk(_nm)))
183
+ with torch.no_grad(): m(x_img)
184
+ for _hk in _hooks: _hk.remove()
185
+ def swap_ct(module, prefix=""):
186
+ for name, ch in module.named_children():
187
+ full = f"{prefix}.{name}" if prefix else name
188
+ if isinstance(ch, nn.ConvTranspose2d):
189
+ H, W = _ct_hw[full]; setattr(module, name, ZeroStuffConvT(ch, H, W))
190
+ else: swap_ct(ch, full)
191
+ swap_ct(net)
192
+
193
+ # ---- DPT head: align_corners=True -> False (banned RESIZE_BILINEAR) + drop _add_pos_embed expand (BROADCAST_TO) ----
194
+ import depth_anything_3.model.utils.head_utils as _hu
195
+ import depth_anything_3.model.dualdpt as _dd
196
+ import depth_anything_3.model.dpt as _dpt
197
+ _orig_ci = _hu.custom_interpolate
198
+ def _ci_no_ac(x, size=None, scale_factor=None, mode="bilinear", align_corners=True):
199
+ return _orig_ci(x, size=size, scale_factor=scale_factor, mode=mode, align_corners=False)
200
+ _hu.custom_interpolate = _ci_no_ac; _dd.custom_interpolate = _ci_no_ac; _dpt.custom_interpolate = _ci_no_ac
201
+ # head pos-embed-again (UV sincos, ratio 0.1): make_sincos broadcast emits BROADCAST_TO. Baking it as
202
+ # constants matches official ~0.0002 better but adds ~64 MB (full [1,C,H,W] per shape) — not worth it.
203
+ # Disable it (the ratio-0.1 UV refinement is negligible vs the size cost).
204
+ _n_pe = 0
205
+ for mod in net.modules():
206
+ if isinstance(mod, _dd.DualDPT) and getattr(mod, "pos_embed", False):
207
+ mod.pos_embed = False; _n_pe += 1
208
+ print(f"disabled head pos_embed on {_n_pe} DualDPT")
209
+
210
+ with torch.no_grad():
211
+ d_clean = m(x_img)[0,0].numpy() # FULLY GPU-CLEAN
212
+ corr = np.corrcoef(d_orig.flatten(), d_clean.flatten())[0,1]
213
+ print(f"depth corr (original vs full GPU-clean) = {corr:.6f} mean-rel-diff = {np.abs(d_orig-d_clean).mean()/(np.abs(d_orig).mean()+1e-9)*100:.3f}%")
214
+
215
+ # ---- convert + op-check (canonical banned list; SELECT_V2 is NOT banned) ----
216
+ dummy = torch.rand(1, 3, H_IN, W_IN)
217
+ import litert_torch
218
+ litert_torch.convert(m.eval(), (dummy,)).export("da3_small_gpu.tflite")
219
+ from ai_edge_litert.interpreter import Interpreter
220
+ BANNED={'GATHER_ND','GATHER','TOPK_V2','PACK','SPLIT','FLEX_ERF','ERF','TRANSPOSE_CONV','BROADCAST_TO'}
221
+ it=Interpreter(model_path="da3_small_gpu.tflite"); it.allocate_tensors()
222
+ ops=collections.Counter(d.get('op_name','?') for d in it._get_ops_details())
223
+ bad={k:v for k,v in ops.items() if k in BANNED}
224
+ over=sum(1 for d in it.get_tensor_details() if len(d.get('shape',[]))>4)
225
+ print(f"op-check FP32: banned {bad or 'NONE'} | >4D {over} | GELU {ops.get('GELU',0)} | SELECT_V2 {ops.get('SELECT_V2',0)}")
226
+ print("VERDICT:", "GPU-CLEAN" if not bad and not over else "BLOCKERS REMAIN")
227
+ import os; print("FP32 size %.1f MB" % (os.path.getsize("da3_small_gpu.tflite")/1e6))