* WIP OCP FP8 E4M3 support * Fix python type-checker * Materialize input_scales also for FP8 when converting * Fix conversion of https://huggingface.co/RedHatAI/Qwen3.8-27B-NVFP4 1. Per row FP8 wrongly fell to NVFP4 conversion, as NVFP4 checked on dims alone. Also check on dtype for FP8 2. Need to reshape FP8 QKV projections scales in the same way that weights are reshaped * Allow remaining FP8 tensors to be dequantized during conversion * Allow tensor-strategy for FP8-only compressed-tensor ckpts * Fix and test gguf-py behavior for FP8_E4M3 * Disallow QKV-fusion for FP8 temporarily * Fix conversion of fused qkvz & FP8 for Qwen3-Next * Avoid fusing FP8 for --fuse-gate-up-exps as we may have different ws * Emit warnings when not fusing FP8 during conversion * More FP8 conversion fixes * Add supported for strided copies to/from FP8 in CPU * remove unneeded scratch buffer from FP8 cpu cpy/dup path * add explanatory comments and TODOs * Add dedicated encoder and decoder tests for FP8 * Whitelist ggml_op_get_rows in metal instead of blacklisting required to fix failing CI, and white-listing should be more future-proof than adding FP8 to the black-list https://github.com/ggml-org/llama.cpp/actions/runs/38433916498/job/115422880633?pr=27512 * Fix more conversion bugs * Defer weight materialization during conversion * Blacklist fp8 in metal backend instead of white-list * Adjust indents * Fix lint + execute tests * Add fast CPU kernel for F8_E4M3 * Conversion fixes * Fix remaining gaps * Enable tied weights with shared scales * Make mandatory-F32 rules apply to FP8 during conversion * Fix python type checks https://github.com/ggml-org/llama.cpp/actions/runs/39765670052/job/119780379595?pr=27512 * Fix kaleidai unit-test failures https://github.com/ggml-org/llama.cpp/actions/runs/39765670125/job/119780383150 * Relax numeric precision constraints for pre-check * Leave fast-FP8 CPU on RISC-V for follow-up We already exclude arm from the fast-path * Allow output scale tensors to be located in any gguf shard * Move test_conversion_fp8.py test execution to behavior-check.yaml * Apply batched suggestions from code review Co-authored-by: Sigbjørn Skjæret <sigbjorn.skjaeret@huggingface.co> * Move mixed compressed-tensor logic into dequant_fp8 + add unit-tests for it * Install all requirements also on Windows for behavior-check Fixes https://github.com/ggml-org/llama.cpp/actions/runs/40535852590/job/122300758223?pr=27512 * Unmap lazy tensors before clean-up in Windows Should fix https://github.com/ggml-org/llama.cpp/actions/runs/40539797824/job/122314782459?pr=27512 * LF line endings https://github.com/ggml-org/llama.cpp/actions/runs/40539798168/job/122314791118?pr=27512 * Extract FP8 tests from llama-arch to its own test-fixture See https://github.com/ggml-org/llama.cpp/pull/27512#discussion_r4400628221 * Update gguf-py/gguf/scripts/gguf_convert_endian.py Co-authored-by: Sigbjørn Skjæret <sigbjorn.skjaeret@huggingface.co> --------- Co-authored-by: Sigbjørn Skjæret <sigbjorn.skjaeret@huggingface.co>
326 lines
14 KiB
Python
Executable File
326 lines
14 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import logging
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import torch
|
|
|
|
if 'NO_LOCAL_GGUF' not in os.environ:
|
|
sys.path.insert(1, str(Path(__file__).parent / 'gguf-py'))
|
|
import gguf
|
|
|
|
from conversion import (
|
|
ModelBase,
|
|
ModelType,
|
|
get_model_architecture,
|
|
get_model_class,
|
|
logger,
|
|
print_registered_models,
|
|
_mistral_common_installed,
|
|
_mistral_import_error_msg,
|
|
)
|
|
|
|
|
|
def split_str_to_n_bytes(split_str: str) -> int:
|
|
if split_str.endswith("K"):
|
|
n = int(split_str[:-1]) * 1000
|
|
elif split_str.endswith("M"):
|
|
n = int(split_str[:-1]) * 1000 * 1000
|
|
elif split_str.endswith("G"):
|
|
n = int(split_str[:-1]) * 1000 * 1000 * 1000
|
|
elif split_str.isnumeric():
|
|
n = int(split_str)
|
|
else:
|
|
raise ValueError(f"Invalid split size: {split_str}, must be a number, optionally followed by K, M, or G")
|
|
|
|
if n < 0:
|
|
raise ValueError(f"Invalid split size: {split_str}, must be positive")
|
|
|
|
return n
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(
|
|
description="Convert a huggingface model to a GGML compatible file")
|
|
parser.add_argument(
|
|
"--vocab-only", action="store_true",
|
|
help="extract only the vocab",
|
|
)
|
|
parser.add_argument(
|
|
"--outfile", type=Path,
|
|
help="path to write to; default: based on input. {ftype} will be replaced by the outtype.",
|
|
)
|
|
parser.add_argument(
|
|
"--outtype", type=str, choices=["f32", "f16", "bf16", "q8_0", "tq1_0", "tq2_0", "auto"], default="auto",
|
|
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 to preserve supported FP8 weights and otherwise select the highest-fidelity 16-bit float type",
|
|
)
|
|
parser.add_argument(
|
|
"--bigendian", action="store_true",
|
|
help="model is executed on big endian machine",
|
|
)
|
|
parser.add_argument(
|
|
"model", type=str,
|
|
help="directory containing model file or huggingface repository ID (if --remote)",
|
|
nargs="?",
|
|
)
|
|
parser.add_argument(
|
|
"--use-temp-file", action="store_true",
|
|
help="use the tempfile library while processing (helpful when running out of memory, process killed)",
|
|
)
|
|
parser.add_argument(
|
|
"--no-lazy", action="store_true",
|
|
help="use more RAM by computing all outputs before writing (use in case lazy evaluation is broken)",
|
|
)
|
|
parser.add_argument(
|
|
"--model-name", type=str, default=None,
|
|
help="name of the model",
|
|
)
|
|
parser.add_argument(
|
|
"--verbose", action="store_true",
|
|
help="increase output verbosity",
|
|
)
|
|
parser.add_argument(
|
|
"--split-max-tensors", type=int, default=0,
|
|
help="max tensors in each split",
|
|
)
|
|
parser.add_argument(
|
|
"--split-max-size", type=str, default="0",
|
|
help="max size per split N(M|G)",
|
|
)
|
|
parser.add_argument(
|
|
"--dry-run", action="store_true",
|
|
help="only print out a split plan and exit, without writing any new files",
|
|
)
|
|
parser.add_argument(
|
|
"--no-tensor-first-split", action="store_true",
|
|
help="do not add tensors to the first split (disabled by default)"
|
|
)
|
|
parser.add_argument(
|
|
"--metadata", type=Path,
|
|
help="Specify the path for an authorship metadata override file"
|
|
)
|
|
parser.add_argument(
|
|
"--print-supported-models", action="store_true",
|
|
help="Print the supported models"
|
|
)
|
|
parser.add_argument(
|
|
"--remote", action="store_true",
|
|
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.",
|
|
)
|
|
parser.add_argument(
|
|
"--mmproj", action="store_true",
|
|
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.",
|
|
)
|
|
parser.add_argument(
|
|
"--mtp", action="store_true",
|
|
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.",
|
|
)
|
|
parser.add_argument(
|
|
"--no-nextn", "--no-mtp", dest="no_mtp", action="store_true",
|
|
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.",
|
|
)
|
|
parser.add_argument(
|
|
"--dspark", action="store_true",
|
|
help="Export only the DeepSeek-V4 or Mistral Large 4 DSpark draft tensors as a separate GGUF.",
|
|
)
|
|
parser.add_argument(
|
|
"--mistral-format", action="store_true",
|
|
help="Whether the model is stored following the Mistral format.",
|
|
)
|
|
parser.add_argument(
|
|
"--disable-mistral-community-chat-template", action="store_true",
|
|
help=(
|
|
"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. "
|
|
"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."
|
|
)
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--sentence-transformers-dense-modules", action="store_true",
|
|
help=("Whether to include sentence-transformers dense modules. "
|
|
"It can be used for sentence-transformers models, like google/embeddinggemma-300m. "
|
|
"Default these modules are not included.")
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--fuse-gate-up-exps", action="store_true",
|
|
help="Fuse gate_exps and up_exps tensors into a single gate_up_exps tensor for MoE models.",
|
|
)
|
|
parser.add_argument(
|
|
"--fp8-as-q8", action="store_true",
|
|
help="Store tensors dequantized from FP8 as Q8_0 instead of BF16/F16.",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--fuse-qkv", action="store_true",
|
|
help="Fuse separate Q, K, V weight tensors into a single QKV tensor.",
|
|
)
|
|
parser.add_argument(
|
|
"--target-model-dir", type=str, default=None,
|
|
help=(
|
|
"path to the target model directory; required when converting a standalone draft model "
|
|
"(e.g. EAGLE3 / DFlash) that needs target-model metadata such as tokenizer, hidden size, and "
|
|
"layer count to populate its GGUF."
|
|
),
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
if not args.print_supported_models and args.model is None:
|
|
parser.error("the following arguments are required: model")
|
|
return args
|
|
|
|
|
|
def main() -> None:
|
|
args = parse_args()
|
|
|
|
if args.print_supported_models:
|
|
logger.error("Supported models:")
|
|
print_registered_models()
|
|
sys.exit(0)
|
|
|
|
if args.verbose:
|
|
logging.basicConfig(level=logging.DEBUG)
|
|
else:
|
|
logging.basicConfig(level=logging.INFO)
|
|
|
|
if args.remote:
|
|
hf_repo_id = args.model
|
|
from huggingface_hub import snapshot_download
|
|
allowed_patterns = ["LICENSE", "*.json", "*.md", "*.txt", "tokenizer.model"]
|
|
if args.sentence_transformers_dense_modules:
|
|
# include sentence-transformers dense modules safetensors files
|
|
allowed_patterns.append("*.safetensors")
|
|
local_dir = snapshot_download(
|
|
repo_id=hf_repo_id,
|
|
allow_patterns=allowed_patterns)
|
|
dir_model = Path(local_dir)
|
|
logger.info(f"Downloaded config and tokenizer to {local_dir}")
|
|
else:
|
|
hf_repo_id = None
|
|
dir_model = Path(args.model)
|
|
|
|
if not dir_model.is_dir():
|
|
logger.error(f'Error: {dir_model} is not a directory')
|
|
sys.exit(1)
|
|
|
|
ftype_map: dict[str, gguf.LlamaFileType] = {
|
|
"f32": gguf.LlamaFileType.ALL_F32,
|
|
"f16": gguf.LlamaFileType.MOSTLY_F16,
|
|
"bf16": gguf.LlamaFileType.MOSTLY_BF16,
|
|
"q8_0": gguf.LlamaFileType.MOSTLY_Q8_0,
|
|
"tq1_0": gguf.LlamaFileType.MOSTLY_TQ1_0,
|
|
"tq2_0": gguf.LlamaFileType.MOSTLY_TQ2_0,
|
|
"auto": gguf.LlamaFileType.GUESSED,
|
|
}
|
|
|
|
is_split = args.split_max_tensors > 0 or args.split_max_size != "0"
|
|
if args.use_temp_file and is_split:
|
|
logger.error("Error: Cannot use temp file when splitting")
|
|
sys.exit(1)
|
|
|
|
if args.outfile is not None:
|
|
fname_out = args.outfile
|
|
elif hf_repo_id:
|
|
# if remote, use the model ID as the output file name
|
|
fname_out = Path("./" + hf_repo_id.replace("/", "-") + "-{ftype}.gguf")
|
|
else:
|
|
fname_out = dir_model
|
|
|
|
logger.info(f"Loading model: {dir_model.name}")
|
|
|
|
is_mistral_format = args.mistral_format
|
|
if is_mistral_format and not _mistral_common_installed:
|
|
raise ImportError(_mistral_import_error_msg)
|
|
disable_mistral_community_chat_template = args.disable_mistral_community_chat_template
|
|
|
|
with torch.inference_mode():
|
|
output_type = ftype_map[args.outtype]
|
|
model_type = ModelType.MMPROJ if args.mmproj else ModelType.TEXT
|
|
hparams = ModelBase.load_hparams(dir_model, is_mistral_format)
|
|
if not is_mistral_format:
|
|
model_architecture = get_model_architecture(hparams, model_type)
|
|
logger.info(f"Model architecture: {model_architecture}")
|
|
try:
|
|
model_class = get_model_class(model_architecture, mmproj=(model_type == ModelType.MMPROJ))
|
|
except NotImplementedError:
|
|
logger.error(f"Model {model_architecture} is not supported")
|
|
sys.exit(1)
|
|
elif args.mmproj:
|
|
assert hparams.get("vision_encoder") is not None, "This model does not support multimodal"
|
|
from conversion.pixtral import PixtralModel
|
|
model_class = PixtralModel
|
|
elif hparams.get("moe") is not None and hparams.get("dsa") is not None:
|
|
from conversion.mistral4 import MistralLarge4Model
|
|
model_class = MistralLarge4Model
|
|
elif hparams.get("moe") is not None:
|
|
from conversion.mistral import MistralMoeModel
|
|
model_class = MistralMoeModel
|
|
else:
|
|
from conversion.mistral import MistralModel
|
|
model_class = MistralModel
|
|
|
|
if sum((args.mtp, args.no_mtp, args.dspark)) > 1:
|
|
logger.error("--mtp, --no-nextn, and --dspark are mutually exclusive")
|
|
sys.exit(1)
|
|
|
|
if args.dspark:
|
|
if is_mistral_format or model_architecture == "MistralLarge4ForConditionalGeneration":
|
|
if model_class.__name__ != "MistralLarge4Model":
|
|
logger.error("--dspark is only supported for Mistral Large 4 among Mistral-format models")
|
|
sys.exit(1)
|
|
from conversion.mistral4 import MistralLarge4DSparkModel
|
|
model_class = MistralLarge4DSparkModel
|
|
elif model_architecture not in ("DeepseekV4ForCausalLM", "DeepseekV41ForCausalLM"):
|
|
logger.error("--dspark is only supported for DeepseekV4ForCausalLM, DeepseekV41ForCausalLM and Mistral Large 4")
|
|
sys.exit(1)
|
|
elif model_architecture == "DeepseekV41ForCausalLM":
|
|
from conversion.deepseek41 import DeepseekV41DSparkModel
|
|
model_class = DeepseekV41DSparkModel
|
|
else:
|
|
from conversion.deepseek import DeepseekV4DSparkModel
|
|
model_class = DeepseekV4DSparkModel
|
|
|
|
if args.mtp or args.no_mtp:
|
|
if not model_class.supports_mtp_export:
|
|
logger.error("--mtp / --no-nextn are not supported for %s", model_architecture)
|
|
sys.exit(1)
|
|
if args.no_mtp:
|
|
model_class.no_mtp = True
|
|
if args.mtp:
|
|
model_class.mtp_only = True
|
|
|
|
model_instance = model_class(dir_model, output_type, fname_out,
|
|
is_big_endian=args.bigendian, use_temp_file=args.use_temp_file,
|
|
eager=args.no_lazy,
|
|
metadata_override=args.metadata, model_name=args.model_name,
|
|
split_max_tensors=args.split_max_tensors,
|
|
split_max_size=split_str_to_n_bytes(args.split_max_size), dry_run=args.dry_run,
|
|
small_first_shard=args.no_tensor_first_split,
|
|
remote_hf_model_id=hf_repo_id, disable_mistral_community_chat_template=disable_mistral_community_chat_template,
|
|
sentence_transformers_dense_modules=args.sentence_transformers_dense_modules,
|
|
target_model_dir=Path(args.target_model_dir) if args.target_model_dir else None,
|
|
fuse_gate_up_exps=args.fuse_gate_up_exps,
|
|
fp8_as_q8=args.fp8_as_q8,
|
|
fuse_qkv=args.fuse_qkv,
|
|
)
|
|
|
|
if args.vocab_only:
|
|
logger.info("Exporting model vocab...")
|
|
model_instance.write_vocab()
|
|
logger.info(f"Model vocab successfully exported to {model_instance.fname_out}")
|
|
else:
|
|
logger.info("Exporting model...")
|
|
model_instance.write()
|
|
out_path = f"{model_instance.fname_out.parent}{os.sep}" if is_split else model_instance.fname_out
|
|
logger.info(f"Model successfully exported to {out_path}")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|