char-fork/llama.cpp

git clone https://git.t4t.associates/char-fork/llama.cpp

PikaPikachuconvert : add `--fuse-qkv` flag to fuse Q/K/V into QKV during HF-to-GGUF conversion (#22780)465e49b9c

char/aliceai-t5-moe
12.7 KiB312 linesraw
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3
4from __future__ import annotations
5
6import argparse
7import logging
8import os
9import sys
10from pathlib import Path
11
12import torch
13
14if 'NO_LOCAL_GGUF' not in os.environ:
15    sys.path.insert(1, str(Path(__file__).parent / 'gguf-py'))
16import gguf
17
18from conversion import (
19    ModelBase,
20    ModelType,
21    get_model_architecture,
22    get_model_class,
23    logger,
24    print_registered_models,
25    _mistral_common_installed,
26    _mistral_import_error_msg,
27)
28
29
30def split_str_to_n_bytes(split_str: str) -> int:
31    if split_str.endswith("K"):
32        n = int(split_str[:-1]) * 1000
33    elif split_str.endswith("M"):
34        n = int(split_str[:-1]) * 1000 * 1000
35    elif split_str.endswith("G"):
36        n = int(split_str[:-1]) * 1000 * 1000 * 1000
37    elif split_str.isnumeric():
38        n = int(split_str)
39    else:
40        raise ValueError(f"Invalid split size: {split_str}, must be a number, optionally followed by K, M, or G")
41
42    if n < 0:
43        raise ValueError(f"Invalid split size: {split_str}, must be positive")
44
45    return n
46
47
48def parse_args() -> argparse.Namespace:
49    parser = argparse.ArgumentParser(
50        description="Convert a huggingface model to a GGML compatible file")
51    parser.add_argument(
52        "--vocab-only", action="store_true",
53        help="extract only the vocab",
54    )
55    parser.add_argument(
56        "--outfile", type=Path,
57        help="path to write to; default: based on input. {ftype} will be replaced by the outtype.",
58    )
59    parser.add_argument(
60        "--outtype", type=str, choices=["f32", "f16", "bf16", "q8_0", "tq1_0", "tq2_0", "auto"], default="auto",
61        help="output format - use f32 for float32, f16 for float16, bf16 for bfloat16, q8_0 for Q8_0, tq1_0 or tq2_0 for ternary, and auto for the highest-fidelity 16-bit float type",
62    )
63    parser.add_argument(
64        "--bigendian", action="store_true",
65        help="model is executed on big endian machine",
66    )
67    parser.add_argument(
68        "model", type=str,
69        help="directory containing model file or huggingface repository ID (if --remote)",
70        nargs="?",
71    )
72    parser.add_argument(
73        "--use-temp-file", action="store_true",
74        help="use the tempfile library while processing (helpful when running out of memory, process killed)",
75    )
76    parser.add_argument(
77        "--no-lazy", action="store_true",
78        help="use more RAM by computing all outputs before writing (use in case lazy evaluation is broken)",
79    )
80    parser.add_argument(
81        "--model-name", type=str, default=None,
82        help="name of the model",
83    )
84    parser.add_argument(
85        "--verbose", action="store_true",
86        help="increase output verbosity",
87    )
88    parser.add_argument(
89        "--split-max-tensors", type=int, default=0,
90        help="max tensors in each split",
91    )
92    parser.add_argument(
93        "--split-max-size", type=str, default="0",
94        help="max size per split N(M|G)",
95    )
96    parser.add_argument(
97        "--dry-run", action="store_true",
98        help="only print out a split plan and exit, without writing any new files",
99    )
100    parser.add_argument(
101        "--no-tensor-first-split", action="store_true",
102        help="do not add tensors to the first split (disabled by default)"
103    )
104    parser.add_argument(
105        "--metadata", type=Path,
106        help="Specify the path for an authorship metadata override file"
107    )
108    parser.add_argument(
109        "--print-supported-models", action="store_true",
110        help="Print the supported models"
111    )
112    parser.add_argument(
113        "--remote", action="store_true",
114        help="(Experimental) Read safetensors file remotely without downloading to disk. Config and tokenizer files will still be downloaded. To use this feature, you need to specify Hugging Face model repo name instead of a local directory. For example: 'HuggingFaceTB/SmolLM2-1.7B-Instruct'. Note: To access gated repo, set HF_TOKEN environment variable to your Hugging Face token.",
115    )
116    parser.add_argument(
117        "--mmproj", action="store_true",
118        help="Export multimodal projector (mmproj) for vision models. This will only work on some vision models. An 'mmproj-' prefix will be added to the output file name.",
119    )
120    parser.add_argument(
121        "--mtp", action="store_true",
122        help="Export only the multi-token prediction (MTP) head as a separate GGUF, suitable for use as a speculative draft. An 'mtp-' prefix will be added to the output file name.",
123    )
124    parser.add_argument(
125        "--no-nextn", "--no-mtp", dest="no_mtp", action="store_true",
126        help="Exclude NextN speculative draft tensors from the converted GGUF. Pair with --mtp or --dspark on a second run to publish target and draft as two files.",
127    )
128    parser.add_argument(
129        "--dspark", action="store_true",
130        help="Export only the DeepSeek-V4 DSpark draft tensors as a separate GGUF.",
131    )
132    parser.add_argument(
133        "--mistral-format", action="store_true",
134        help="Whether the model is stored following the Mistral format.",
135    )
136    parser.add_argument(
137        "--disable-mistral-community-chat-template", action="store_true",
138        help=(
139            "Whether to disable usage of Mistral community chat templates. If set, use the Mistral official `mistral-common` library for tokenization and detokenization of Mistral models. "
140            "Using `mistral-common` ensure correctness and zero-day support of tokenization for models converted from the Mistral format but requires to manually setup the tokenization server."
141        )
142    )
143
144    parser.add_argument(
145        "--sentence-transformers-dense-modules", action="store_true",
146        help=("Whether to include sentence-transformers dense modules. "
147              "It can be used for sentence-transformers models, like google/embeddinggemma-300m. "
148              "Default these modules are not included.")
149    )
150
151    parser.add_argument(
152        "--fuse-gate-up-exps", action="store_true",
153        help="Fuse gate_exps and up_exps tensors into a single gate_up_exps tensor for MoE models.",
154    )
155    parser.add_argument(
156        "--fp8-as-q8", action="store_true",
157        help="Store tensors dequantized from FP8 as Q8_0 instead of BF16/F16.",
158    )
159
160    parser.add_argument(
161        "--fuse-qkv", action="store_true",
162        help="Fuse separate Q, K, V weight tensors into a single QKV tensor.",
163    )
164    parser.add_argument(
165        "--target-model-dir", type=str, default=None,
166        help=(
167            "path to the target model directory; required when converting a standalone draft model "
168            "(e.g. EAGLE3 / DFlash) that needs target-model metadata such as tokenizer, hidden size, and "
169            "layer count to populate its GGUF."
170        ),
171    )
172
173    args = parser.parse_args()
174    if not args.print_supported_models and args.model is None:
175        parser.error("the following arguments are required: model")
176    return args
177
178
179def main() -> None:
180    args = parse_args()
181
182    if args.print_supported_models:
183        logger.error("Supported models:")
184        print_registered_models()
185        sys.exit(0)
186
187    if args.verbose:
188        logging.basicConfig(level=logging.DEBUG)
189    else:
190        logging.basicConfig(level=logging.INFO)
191
192    if args.remote:
193        hf_repo_id = args.model
194        from huggingface_hub import snapshot_download
195        allowed_patterns = ["LICENSE", "*.json", "*.md", "*.txt", "tokenizer.model"]
196        if args.sentence_transformers_dense_modules:
197            # include sentence-transformers dense modules safetensors files
198            allowed_patterns.append("*.safetensors")
199        local_dir = snapshot_download(
200            repo_id=hf_repo_id,
201            allow_patterns=allowed_patterns)
202        dir_model = Path(local_dir)
203        logger.info(f"Downloaded config and tokenizer to {local_dir}")
204    else:
205        hf_repo_id = None
206        dir_model = Path(args.model)
207
208    if not dir_model.is_dir():
209        logger.error(f'Error: {dir_model} is not a directory')
210        sys.exit(1)
211
212    ftype_map: dict[str, gguf.LlamaFileType] = {
213        "f32": gguf.LlamaFileType.ALL_F32,
214        "f16": gguf.LlamaFileType.MOSTLY_F16,
215        "bf16": gguf.LlamaFileType.MOSTLY_BF16,
216        "q8_0": gguf.LlamaFileType.MOSTLY_Q8_0,
217        "tq1_0": gguf.LlamaFileType.MOSTLY_TQ1_0,
218        "tq2_0": gguf.LlamaFileType.MOSTLY_TQ2_0,
219        "auto": gguf.LlamaFileType.GUESSED,
220    }
221
222    is_split = args.split_max_tensors > 0 or args.split_max_size != "0"
223    if args.use_temp_file and is_split:
224        logger.error("Error: Cannot use temp file when splitting")
225        sys.exit(1)
226
227    if args.outfile is not None:
228        fname_out = args.outfile
229    elif hf_repo_id:
230        # if remote, use the model ID as the output file name
231        fname_out = Path("./" + hf_repo_id.replace("/", "-") + "-{ftype}.gguf")
232    else:
233        fname_out = dir_model
234
235    logger.info(f"Loading model: {dir_model.name}")
236
237    is_mistral_format = args.mistral_format
238    if is_mistral_format and not _mistral_common_installed:
239        raise ImportError(_mistral_import_error_msg)
240    disable_mistral_community_chat_template = args.disable_mistral_community_chat_template
241
242    with torch.inference_mode():
243        output_type = ftype_map[args.outtype]
244        model_type = ModelType.MMPROJ if args.mmproj else ModelType.TEXT
245        hparams = ModelBase.load_hparams(dir_model, is_mistral_format)
246        if not is_mistral_format:
247            model_architecture = get_model_architecture(hparams, model_type)
248            logger.info(f"Model architecture: {model_architecture}")
249            try:
250                model_class = get_model_class(model_architecture, mmproj=(model_type == ModelType.MMPROJ))
251            except NotImplementedError:
252                logger.error(f"Model {model_architecture} is not supported")
253                sys.exit(1)
254        elif args.mmproj:
255            assert hparams.get("vision_encoder") is not None, "This model does not support multimodal"
256            from conversion.pixtral import PixtralModel
257            model_class = PixtralModel
258        elif hparams.get("moe") is not None:
259            from conversion.mistral import MistralMoeModel
260            model_class = MistralMoeModel
261        else:
262            from conversion.mistral import MistralModel
263            model_class = MistralModel
264
265        if sum((args.mtp, args.no_mtp, args.dspark)) > 1:
266            logger.error("--mtp, --no-nextn, and --dspark are mutually exclusive")
267            sys.exit(1)
268
269        if args.dspark:
270            if is_mistral_format or model_architecture != "DeepseekV4ForCausalLM":
271                logger.error("--dspark is only supported for DeepseekV4ForCausalLM")
272                sys.exit(1)
273            from conversion.deepseek import DeepseekV4DSparkModel
274            model_class = DeepseekV4DSparkModel
275
276        if args.mtp or args.no_mtp:
277            if not model_class.supports_mtp_export:
278                logger.error("--mtp / --no-nextn are not supported for %s", model_architecture)
279                sys.exit(1)
280            if args.no_mtp:
281                model_class.no_mtp = True
282            if args.mtp:
283                model_class.mtp_only = True
284
285        model_instance = model_class(dir_model, output_type, fname_out,
286                                     is_big_endian=args.bigendian, use_temp_file=args.use_temp_file,
287                                     eager=args.no_lazy,
288                                     metadata_override=args.metadata, model_name=args.model_name,
289                                     split_max_tensors=args.split_max_tensors,
290                                     split_max_size=split_str_to_n_bytes(args.split_max_size), dry_run=args.dry_run,
291                                     small_first_shard=args.no_tensor_first_split,
292                                     remote_hf_model_id=hf_repo_id, disable_mistral_community_chat_template=disable_mistral_community_chat_template,
293                                     sentence_transformers_dense_modules=args.sentence_transformers_dense_modules,
294                                     target_model_dir=Path(args.target_model_dir) if args.target_model_dir else None,
295                                     fuse_gate_up_exps=args.fuse_gate_up_exps,
296                                     fp8_as_q8=args.fp8_as_q8,
297                                     fuse_qkv=args.fuse_qkv,
298                                     )
299
300        if args.vocab_only:
301            logger.info("Exporting model vocab...")
302            model_instance.write_vocab()
303            logger.info(f"Model vocab successfully exported to {model_instance.fname_out}")
304        else:
305            logger.info("Exporting model...")
306            model_instance.write()
307            out_path = f"{model_instance.fname_out.parent}{os.sep}" if is_split else model_instance.fname_out
308            logger.info(f"Model successfully exported to {out_path}")
309
310
311if __name__ == '__main__':
312    main()