TalmajM commited on
Commit
24dbacf
·
verified ·
1 Parent(s): afd3292

Upload convert_da3.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. convert_da3.py +183 -0
convert_da3.py ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Repackage an original Depth Anything 3 checkpoint into ComfyUI's native layout.
3
+
4
+ Usage:
5
+ python scripts/convert_da3.py \
6
+ --input da3_original.safetensors \
7
+ --output models/diffusion_models/da3_comfy.safetensors
8
+
9
+ This applies, *offline*, the exact transform that ComfyUI used to do at load
10
+ time for DA3:
11
+
12
+ * remap the DINOv2 backbone keys ``backbone.pretrained.*`` (upstream DA3)
13
+ to the ``Dinov2Model`` runtime layout (``backbone.embeddings.*``,
14
+ ``backbone.encoder.layer.*``, ``backbone.layernorm.*``);
15
+ * split each fused ``attn.qkv`` projection into separate query/key/value
16
+ linears;
17
+ * drop the unused Gaussian-splat head weights (``gs_head.*``, ``gs_adapter.*``).
18
+
19
+ The head (``head.*``), camera encoder/decoder (``cam_enc.*``, ``cam_dec.*``)
20
+ and any other keys are passed through unchanged. After conversion the file
21
+ loads directly via ComfyUI auto-detection with no in-code remap.
22
+ """
23
+
24
+ import argparse
25
+ import glob
26
+ import os
27
+
28
+ import torch
29
+
30
+ from safetensors.torch import load_file, save_file
31
+
32
+ DROP_PREFIXES = ("gs_head.", "gs_adapter.")
33
+
34
+
35
+ def remap_backbone_keys(state_dict, prefix="backbone."):
36
+ """Map ``backbone.pretrained.*`` (upstream DA3) keys to ``Dinov2Model`` layout."""
37
+ pre = prefix + "pretrained."
38
+ src_keys = [k for k in state_dict.keys() if k.startswith(pre)]
39
+ if not src_keys:
40
+ return state_dict
41
+
42
+ static_renames = {
43
+ pre + "patch_embed.proj.weight": prefix + "embeddings.patch_embeddings.projection.weight",
44
+ pre + "patch_embed.proj.bias": prefix + "embeddings.patch_embeddings.projection.bias",
45
+ pre + "pos_embed": prefix + "embeddings.position_embeddings",
46
+ pre + "cls_token": prefix + "embeddings.cls_token",
47
+ pre + "camera_token": prefix + "embeddings.camera_token",
48
+ pre + "norm.weight": prefix + "layernorm.weight",
49
+ pre + "norm.bias": prefix + "layernorm.bias",
50
+ }
51
+ for src, dst in static_renames.items():
52
+ if src in state_dict:
53
+ state_dict[dst] = state_dict.pop(src)
54
+
55
+ block_pre = pre + "blocks."
56
+ block_keys = [k for k in state_dict.keys() if k.startswith(block_pre)]
57
+ for k in block_keys:
58
+ rest = k[len(block_pre):] # e.g. "5.attn.qkv.weight"
59
+ idx_str, _, sub = rest.partition(".")
60
+ target_block = "{}encoder.layer.{}.".format(prefix, idx_str)
61
+
62
+ # Fused QKV -> split query/key/value linears.
63
+ if sub == "attn.qkv.weight":
64
+ qkv = state_dict.pop(k)
65
+ c = qkv.shape[0] // 3
66
+ state_dict[target_block + "attention.attention.query.weight"] = qkv[:c].clone()
67
+ state_dict[target_block + "attention.attention.key.weight"] = qkv[c:2 * c].clone()
68
+ state_dict[target_block + "attention.attention.value.weight"] = qkv[2 * c:].clone()
69
+ continue
70
+ if sub == "attn.qkv.bias":
71
+ qkv = state_dict.pop(k)
72
+ c = qkv.shape[0] // 3
73
+ state_dict[target_block + "attention.attention.query.bias"] = qkv[:c].clone()
74
+ state_dict[target_block + "attention.attention.key.bias"] = qkv[c:2 * c].clone()
75
+ state_dict[target_block + "attention.attention.value.bias"] = qkv[2 * c:].clone()
76
+ continue
77
+
78
+ # Sub-key remap (suffix preserved).
79
+ if sub.startswith("attn.proj."):
80
+ tail = sub[len("attn.proj."):]
81
+ new = "attention.output.dense." + tail
82
+ elif sub.startswith("attn.q_norm."):
83
+ new = "attention.q_norm." + sub[len("attn.q_norm."):]
84
+ elif sub.startswith("attn.k_norm."):
85
+ new = "attention.k_norm." + sub[len("attn.k_norm."):]
86
+ elif sub == "ls1.gamma":
87
+ new = "layer_scale1.lambda1"
88
+ elif sub == "ls2.gamma":
89
+ new = "layer_scale2.lambda1"
90
+ elif sub.startswith("mlp.w12."):
91
+ new = "mlp.weights_in." + sub[len("mlp.w12."):]
92
+ elif sub.startswith("mlp.w3."):
93
+ new = "mlp.weights_out." + sub[len("mlp.w3."):]
94
+ elif sub.startswith(("norm1.", "norm2.", "mlp.fc1.", "mlp.fc2.")):
95
+ new = sub
96
+ else:
97
+ # Unrecognised key -- leave as-is so a later load can complain.
98
+ raise ValueError("Unrecognised DA3 backbone key: {}".format(k))
99
+
100
+ state_dict[target_block + new] = state_dict.pop(k)
101
+
102
+ return state_dict
103
+
104
+
105
+ def drop_unused(state_dict):
106
+ for k in list(state_dict.keys()):
107
+ if k.startswith(DROP_PREFIXES):
108
+ state_dict.pop(k)
109
+ return state_dict
110
+
111
+
112
+ def load_state_dict(path):
113
+ if os.path.isdir(path):
114
+ sd = {}
115
+ files = sorted(glob.glob(os.path.join(path, "*.safetensors")))
116
+ if not files:
117
+ raise FileNotFoundError("No .safetensors files in {}".format(path))
118
+ for f in files:
119
+ sd.update(load_file(f))
120
+ return sd
121
+
122
+ if path.endswith(".safetensors"):
123
+ return load_file(path)
124
+
125
+ sd = torch.load(path, map_location="cpu", weights_only=False)
126
+ # Unwrap common nesting (e.g. {"model": ...} / {"state_dict": ...}).
127
+ for wrap in ("state_dict", "model", "module"):
128
+ if isinstance(sd, dict) and wrap in sd and isinstance(sd[wrap], dict):
129
+ sd = sd[wrap]
130
+ break
131
+ return sd
132
+
133
+
134
+ def main():
135
+ parser = argparse.ArgumentParser(
136
+ description="Repackage an original Depth Anything 3 checkpoint into ComfyUI's native layout"
137
+ )
138
+ parser.add_argument("--input", type=str, required=True,
139
+ help="Path to original DA3 .safetensors / .pt / .pth file or directory")
140
+ parser.add_argument("--output", type=str, required=True,
141
+ help="Output .safetensors file path")
142
+ args = parser.parse_args()
143
+
144
+ print("Loading: {}".format(args.input))
145
+ sd = load_state_dict(args.input)
146
+ print(" Loaded {} keys".format(len(sd)))
147
+
148
+ # Original DA3 checkpoints store everything under a "model." string prefix
149
+ # (e.g. "model.backbone.pretrained.*"). Strip it so the remap works on bare
150
+ # "backbone.*" keys, then re-add it at the end: ComfyUI's loader resolves the
151
+ # diffusion-model prefix to "model." for DA3, so the saved file must keep it.
152
+ if any(k.startswith("model.") for k in sd):
153
+ print(' Stripping "model." prefix for processing')
154
+ sd = {(k[len("model."):] if k.startswith("model.") else k): v for k, v in sd.items()}
155
+
156
+ if any(k.startswith("backbone.pretrained.") for k in sd):
157
+ print(" Remapping backbone (backbone.pretrained.* -> Dinov2Model layout)...")
158
+ sd = remap_backbone_keys(sd, prefix="backbone.")
159
+ elif any(k.startswith("backbone.embeddings.") for k in sd):
160
+ print(" Backbone already in ComfyUI layout, skipping remap.")
161
+ else:
162
+ raise ValueError("Input does not look like a DA3 checkpoint (no backbone.* keys found)")
163
+
164
+ n_before = len(sd)
165
+ sd = drop_unused(sd)
166
+ dropped = n_before - len(sd)
167
+ if dropped:
168
+ print(" Dropped {} unused Gaussian-head keys".format(dropped))
169
+
170
+ # Re-add the "model." prefix expected by ComfyUI's diffusion-model loader.
171
+ sd = {"model." + k: v for k, v in sd.items()}
172
+
173
+ # safetensors requires contiguous tensors; the qkv split slices are cloned
174
+ # above but enforce contiguity defensively for all tensors.
175
+ sd = {k: v.contiguous() for k, v in sd.items()}
176
+
177
+ os.makedirs(os.path.dirname(os.path.abspath(args.output)), exist_ok=True)
178
+ save_file(sd, args.output)
179
+ print(" Saved {} keys to {}".format(len(sd), args.output))
180
+
181
+
182
+ if __name__ == "__main__":
183
+ main()