Akahsizrr commited on
Commit
90eef57
Β·
verified Β·
1 Parent(s): 54ebe57

Upload export_gguf.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. export_gguf.py +306 -0
export_gguf.py ADDED
@@ -0,0 +1,306 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Standalone GGUF exporter for Fuse3.
2
+
3
+ Exports a Fuse3 HuggingFace model to GGUF format without requiring
4
+ modifications to llama.cpp's converter infrastructure. Writes the
5
+ GGUF file directly using the gguf Python package.
6
+
7
+ The resulting GGUF file uses the `fuse3` architecture and contains:
8
+ - Standard LFM2 host tensors (attention, conv, dense FFN)
9
+ - Custom Fuse3 expert tensors (router, scale, expert weights)
10
+
11
+ Usage:
12
+ python export_gguf.py --model-dir /path/to/fuse3 --output fuse3.gguf --outtype f16
13
+
14
+ Note: To actually RUN inference with this GGUF, you need the custom
15
+ llama.cpp fork with Fuse3 architecture support (see INTEGRATION.md).
16
+ The GGUF file can be created without the fork, but cannot be loaded
17
+ by stock llama.cpp.
18
+ """
19
+ import argparse
20
+ import os
21
+ import sys
22
+ import json
23
+ from pathlib import Path
24
+
25
+ import torch
26
+ import numpy as np
27
+
28
+ # gguf package from llama.cpp
29
+ try:
30
+ import gguf
31
+ except ImportError:
32
+ print("Error: gguf package not found. Install with: pip install gguf")
33
+ sys.exit(1)
34
+
35
+
36
+ def load_config(model_dir: str) -> dict:
37
+ with open(os.path.join(model_dir, "config.json")) as f:
38
+ return json.load(f)
39
+
40
+
41
+ def load_weights(model_dir: str) -> dict:
42
+ """Load all safetensors weights."""
43
+ from safetensors.torch import load_file
44
+ import glob
45
+
46
+ weights = {}
47
+ for shard in sorted(glob.glob(os.path.join(model_dir, "*.safetensors"))):
48
+ weights.update(load_file(shard))
49
+ return weights
50
+
51
+
52
+ def compute_ff_dim(config: dict) -> int:
53
+ """Compute the actual FF dim (same as LFM2)."""
54
+ ff_dim = config.get("block_ff_dim", config.get("intermediate_size", 0))
55
+ auto_adjust = config.get("block_auto_adjust_ff_dim", False)
56
+ multiplier = config.get("block_ffn_dim_multiplier")
57
+ multiple_of = config.get("block_multiple_of", 1)
58
+
59
+ if auto_adjust:
60
+ ff_dim = int(2 * ff_dim / 3)
61
+ if multiplier is not None:
62
+ ff_dim = int(multiplier * ff_dim)
63
+ ff_dim = multiple_of * ((ff_dim + multiple_of - 1) // multiple_of)
64
+
65
+ return ff_dim
66
+
67
+
68
+ def export_gguf(model_dir: str, output_path: str, outtype: str = "f16"):
69
+ """Export Fuse3 model to GGUF format."""
70
+ config = load_config(model_dir)
71
+ weights = load_weights(model_dir)
72
+
73
+ print(f"[export] Config: {json.dumps(config, indent=2)[:500]}...")
74
+ print(f"[export] Loaded {len(weights)} tensors")
75
+
76
+ # Create GGUF writer
77
+ writer = gguf.GGUFWriter(output_path, "fuse3")
78
+
79
+ # ── Set model parameters ──
80
+ n_vocab = config["vocab_size"]
81
+ n_embd = config["hidden_size"]
82
+ n_layer = config["num_hidden_layers"]
83
+ n_head = config["num_attention_heads"]
84
+ n_kv_heads = config["num_key_value_heads"]
85
+ n_ff = compute_ff_dim(config)
86
+ norm_eps = config.get("norm_eps", 1e-5)
87
+ conv_L_cache = config.get("conv_L_cache", 3)
88
+
89
+ # Per-layer KV heads (0 for conv layers)
90
+ layer_types = config.get("layer_types", [])
91
+ n_kv_heads_list = [
92
+ n_kv_heads if lt != "conv" else 0
93
+ for lt in layer_types
94
+ ]
95
+
96
+ writer.add_vocab_size(n_vocab)
97
+ writer.add_embedding_length(n_embd)
98
+ writer.add_block_count(n_layer)
99
+ writer.add_head_count(n_head)
100
+ writer.add_head_count_kv(n_kv_heads)
101
+ writer.add_feed_forward_length(n_ff)
102
+ writer.add_shortconv_l_cache(conv_L_cache)
103
+ writer.add_layer_norm_rms_eps(norm_eps)
104
+
105
+ # Per-layer KV heads β€” use raw key name since gguf package version varies
106
+ try:
107
+ writer.add_key_value_heads(n_kv_heads_list)
108
+ except AttributeError:
109
+ # Use add_array with raw key string
110
+ writer.add_array("attention.key_value_head_count", n_kv_heads_list)
111
+
112
+ # Fuse3-specific parameters
113
+ experts_per_layer = config.get("experts_per_layer", {})
114
+ expert_intermediate_size = config.get("expert_intermediate_size", 512)
115
+ top_k_experts = config.get("top_k_experts", 8)
116
+ swiglu_limit = config.get("swiglu_limit", 10.0)
117
+ expert_scale_init = config.get("expert_scale_init", -5.0)
118
+
119
+ # Convert to int keys
120
+ experts_per_layer_int = {
121
+ int(k): len(v) if isinstance(v, list) else v
122
+ for k, v in experts_per_layer.items()
123
+ }
124
+
125
+ augmented_layers = sorted(experts_per_layer_int.keys())
126
+ expert_counts = [experts_per_layer_int.get(i, 0) for i in range(n_layer)]
127
+
128
+ writer.add_expert_feed_forward_length(expert_intermediate_size)
129
+ writer.add_expert_count(max(expert_counts) if expert_counts else 0)
130
+ # expert_top_k β€” use raw key since gguf package may not have this method
131
+ try:
132
+ writer.add_expert_top_k(top_k_experts)
133
+ except AttributeError:
134
+ writer.add_uint32("expert_top_k", top_k_experts)
135
+
136
+ # Custom Fuse3 keys
137
+ writer.add_array("fuse3.augmented_layers", augmented_layers)
138
+ writer.add_array("fuse3.expert_counts", expert_counts)
139
+ # swiglu_limit as a float
140
+ try:
141
+ writer.add_float32("fuse3.swiglu_limit", swiglu_limit)
142
+ except AttributeError:
143
+ pass # Non-critical, default is 10.0 in C++
144
+
145
+ print(f"[export] Augmented layers: {augmented_layers}")
146
+ print(f"[export] Expert counts: {expert_counts}")
147
+ print(f"[export] Expert intermediate: {expert_intermediate_size}")
148
+ print(f"[export] Top-k: {top_k_experts}")
149
+
150
+ # ── Determine output dtype ──
151
+ dtype_map = {
152
+ "f32": gguf.GGMLQuantizationType.F32,
153
+ "f16": gguf.GGMLQuantizationType.F16,
154
+ "bf16": gguf.GGMLQuantizationType.BF16,
155
+ "q8_0": gguf.GGMLQuantizationType.Q8_0,
156
+ }
157
+ out_dtype = dtype_map.get(outtype, gguf.GGMLQuantizationType.F16)
158
+
159
+ # ── Write tensors ──
160
+ tensor_count = 0
161
+
162
+ def add_tensor(name, data, raw_dtype=None):
163
+ nonlocal tensor_count
164
+ if data.dim() == 0:
165
+ data = data.unsqueeze(0)
166
+ # Convert bfloat16 to float16 (numpy doesn't support bfloat16)
167
+ if data.dtype == torch.bfloat16:
168
+ data = data.to(torch.float16)
169
+ data_np = data.numpy()
170
+ # Use the specified dtype or infer from data
171
+ if raw_dtype is not None:
172
+ writer.add_tensor(name, data_np, raw_dtype=raw_dtype)
173
+ else:
174
+ writer.add_tensor(name, data_np, raw_dtype=out_dtype)
175
+ tensor_count += 1
176
+
177
+ # Token embedding
178
+ embd = weights["model.embed_tokens.weight"]
179
+ add_tensor("token_embd.weight", embd)
180
+
181
+ # Output norm (LFM2 uses token_embd_norm)
182
+ output_norm = weights.get("model.embedding_norm.weight", weights.get("model.output_norm.weight"))
183
+ if output_norm is not None:
184
+ add_tensor("token_embd_norm.weight", output_norm)
185
+
186
+ # Output (tied weights)
187
+ lm_head = weights.get("lm_head.weight")
188
+ if lm_head is not None:
189
+ add_tensor("output.weight", lm_head)
190
+
191
+ # Per-layer tensors
192
+ for i in range(n_layer):
193
+ prefix = f"model.layers.{i}"
194
+
195
+ # Check if this is an augmented layer (has host_layer prefix)
196
+ is_augmented = i in experts_per_layer_int and experts_per_layer_int[i] > 0
197
+ host_prefix = f"{prefix}.host_layer" if is_augmented else prefix
198
+
199
+ # FFN norm
200
+ ffn_norm = weights.get(f"{host_prefix}.ffn_norm.weight", weights.get(f"{prefix}.ffn_norm.weight"))
201
+ if ffn_norm is not None:
202
+ add_tensor(f"blk.{i}.ffn_norm.weight", ffn_norm)
203
+
204
+ # Dense FFN (host)
205
+ for tensor_name, gguf_name in [
206
+ ("feed_forward.w1.weight", "ffn_gate.weight"),
207
+ ("feed_forward.w3.weight", "ffn_up.weight"),
208
+ ("feed_forward.w2.weight", "ffn_down.weight"),
209
+ ]:
210
+ w = weights.get(f"{host_prefix}.{tensor_name}", weights.get(f"{prefix}.{tensor_name}"))
211
+ if w is not None:
212
+ add_tensor(f"blk.{i}.{gguf_name}", w)
213
+
214
+ # Attention norm (operator_norm)
215
+ attn_norm = weights.get(f"{host_prefix}.operator_norm.weight", weights.get(f"{prefix}.operator_norm.weight"))
216
+ if attn_norm is not None:
217
+ add_tensor(f"blk.{i}.attn_norm.weight", attn_norm)
218
+
219
+ # Check if attention or conv layer
220
+ is_attn = layer_types[i] == "full_attention" if i < len(layer_types) else True
221
+
222
+ if is_attn:
223
+ # Attention tensors
224
+ for tensor_name, gguf_name in [
225
+ ("self_attn.q_proj.weight", "attn_q.weight"),
226
+ ("self_attn.k_proj.weight", "attn_k.weight"),
227
+ ("self_attn.v_proj.weight", "attn_v.weight"),
228
+ ("self_attn.o_proj.weight", "attn_output.weight"),
229
+ ("self_attn.q_layernorm.weight", "attn_q_norm.weight"),
230
+ ("self_attn.k_layernorm.weight", "attn_k_norm.weight"),
231
+ ]:
232
+ w = weights.get(f"{host_prefix}.{tensor_name}", weights.get(f"{prefix}.{tensor_name}"))
233
+ if w is not None:
234
+ add_tensor(f"blk.{i}.{gguf_name}", w)
235
+ else:
236
+ # ShortConv tensors
237
+ for tensor_name, gguf_name in [
238
+ ("conv.conv.weight", "shortconv_conv.weight"),
239
+ ("conv.in_proj.weight", "shortconv_inproj.weight"),
240
+ ("conv.out_proj.weight", "shortconv_outproj.weight"),
241
+ ]:
242
+ w = weights.get(f"{host_prefix}.{tensor_name}", weights.get(f"{prefix}.{tensor_name}"))
243
+ if w is not None:
244
+ # Conv weight needs squeezing (2D for ggml)
245
+ if "conv.conv" in tensor_name and w.dim() == 3:
246
+ w = w.squeeze(1)
247
+ add_tensor(f"blk.{i}.{gguf_name}", w)
248
+
249
+ # Fuse3 expert tensors (only for augmented layers)
250
+ if is_augmented:
251
+ n_exp = experts_per_layer_int[i]
252
+
253
+ # Router
254
+ router_w = weights.get(f"{prefix}.router.gate.weight")
255
+ if router_w is not None:
256
+ add_tensor(f"blk.{i}.fuse3_router.weight", router_w)
257
+
258
+ # Expert scale
259
+ scale = weights.get(f"{prefix}.expert_scale")
260
+ if scale is not None:
261
+ add_tensor(f"blk.{i}.fuse3_expert_scale.weight", scale.unsqueeze(0) if scale.dim() == 0 else scale)
262
+
263
+ # Expert weights β€” stack per-expert weights
264
+ for proj_name, gguf_proj in [
265
+ ("gate_proj", "gate"),
266
+ ("up_proj", "up"),
267
+ ("down_proj", "down"),
268
+ ]:
269
+ expert_weights = []
270
+ for eid in range(n_exp):
271
+ key = f"{prefix}.experts.{eid}.{proj_name}.weight"
272
+ if key in weights:
273
+ expert_weights.append(weights[key])
274
+ else:
275
+ print(f"[export] WARNING: Missing {key}")
276
+
277
+ if expert_weights:
278
+ # Stack: {n_exp, ...dims} -> transpose to {...dims, n_exp}
279
+ stacked = torch.stack(expert_weights, dim=-1)
280
+ add_tensor(f"blk.{i}.fuse3_experts.{gguf_proj}.weight", stacked)
281
+
282
+ print(f"[export] Written {tensor_count} tensors")
283
+
284
+ # Write the GGUF file
285
+ writer.write_header_to_file()
286
+ writer.write_kv_data_to_file()
287
+ writer.write_tensors_to_file()
288
+ writer.close()
289
+
290
+ file_size = os.path.getsize(output_path) / 1e9
291
+ print(f"[export] GGUF written: {output_path} ({file_size:.2f} GB)")
292
+
293
+
294
+ def main():
295
+ parser = argparse.ArgumentParser(description="Export Fuse3 to GGUF")
296
+ parser.add_argument("--model-dir", required=True, help="Path to Fuse3 model directory")
297
+ parser.add_argument("--output", required=True, help="Output GGUF file path")
298
+ parser.add_argument("--outtype", default="f16", choices=["f32", "f16", "bf16", "q8_0"],
299
+ help="Output tensor dtype")
300
+ args = parser.parse_args()
301
+
302
+ export_gguf(args.model_dir, args.output, args.outtype)
303
+
304
+
305
+ if __name__ == "__main__":
306
+ main()