# /// script # requires-python = ">=3.10" # dependencies = ["gguf>=0.19", "numpy>=1.26", "safetensors>=0.5", "torch>=2.6"] # [tool.uv.sources] # torch = { index = "pytorch-cpu" } # [[tool.uv.index]] # name = "pytorch-cpu" # url = "https://download.pytorch.org/whl/cpu" # explicit = true # /// """Convert Drama 3's scaled FP8 experts into a native audio.cpp GGUF package. Uses CPU memory one expert bank at a time. The optional native converter stage folds codec weight norms, embeds tokenizer/config files, and applies mixed quantization. It never loads weights onto a GPU. """ import argparse import json import math import struct import subprocess from pathlib import Path import gguf import numpy as np import torch from safetensors import safe_open parser = argparse.ArgumentParser() parser.add_argument("--checkpoint", type=Path, required=True) parser.add_argument("--output", type=Path, help="Intermediate FP16 GGUF (about 68 GiB)") parser.add_argument("--input-gguf", type=Path, help="Reuse a prepared FP16 GGUF instead of exporting again") parser.add_argument("--quantized-output", type=Path, help="Standalone mixed-quantization GGUF") parser.add_argument("--gguf-tool", type=Path, help="Path to the native audiocpp_gguf executable") parser.add_argument("--expert-type", choices=("q2_k", "q3_k", "q4_k", "q5_k", "q6_k", "q8_0"), default="q4_k") parser.add_argument("--backbone-type", choices=("q2_k", "q3_k", "q4_k", "q5_k", "q6_k", "q8_0"), default="q8_0", help="Dense backbone matrix quantization; audio tower and codec retain their preparation types") parser.add_argument("--embedding-type", choices=("f16", "q4_0", "q4_1", "q5_0", "q5_1", "q8_0"), default="q8_0", help="Main text embedding storage; defaults to Q8_0 and exposes CUDA-supported row lookup types") parser.add_argument("--threads", type=int, default=8) arguments = parser.parse_args() if arguments.threads <= 0: parser.error("--threads must be positive") if arguments.input_gguf and arguments.output: parser.error("choose --output or --input-gguf") if not arguments.input_gguf and not arguments.output: parser.error("provide --output or --input-gguf") if arguments.input_gguf and not arguments.quantized_output: parser.error("--input-gguf requires --quantized-output") if arguments.quantized_output and (not arguments.gguf_tool or not arguments.gguf_tool.is_file()): parser.error("--quantized-output requires an existing --gguf-tool executable") if arguments.output and arguments.quantized_output and arguments.output.resolve() == arguments.quantized_output.resolve(): parser.error("intermediate and quantized outputs must be different files") for destination in (arguments.output, arguments.quantized_output): if destination and destination.exists(): parser.error(f"output already exists: {destination}") torch.set_num_threads(arguments.threads) configuration = json.loads((arguments.checkpoint / "config.json").read_text()) if configuration.get("model_type") != "fish_omni": parser.error("checkpoint config must describe Fish Drama 3 (fish_omni)") def quantize_archive(archive): if not arguments.quantized_output: return repository = Path(__file__).resolve().parents[2] command = [str(arguments.gguf_tool.resolve()), "--input", str(archive.resolve()), "--output", str(arguments.quantized_output.resolve()), "--type", arguments.backbone_type, "--family", "fish_drama3", "--model-spec", str(repository / "model_specs/fish_drama3.json"), "--root", str(arguments.checkpoint.resolve()), "--fold-weight-norm", "codec_weights*", "--keep-type", "codec_weights*=f16", "--keep-type", "model_weights/audio_decoder*=orig", "--keep-type", "model_weights/model.language_model.embed_tokens.weight=" + arguments.embedding_type] layers = configuration["text_config"]["num_hidden_layers"] for layer in range(layers): command += ["--keep-type", f"model_weights/model.language_model.layers.{layer}.mlp.experts.*={arguments.expert_type}"] for name in ("config.json", "tokenizer.json", "tokenizer_config.json", "special_tokens_map.json", "LICENSE", "chat_template.jinja"): path = arguments.checkpoint / name if path.is_file(): command += ["--sidecar", f"{path.resolve()}={name}"] subprocess.run(command, check=True) if arguments.input_gguf: if not arguments.input_gguf.is_file(): parser.error(f"input GGUF does not exist: {arguments.input_gguf}") quantize_archive(arguments.input_gguf) raise SystemExit(0) weight_map = json.loads((arguments.checkpoint / "model.safetensors.index.json").read_text())["weight_map"] layer_count = configuration["text_config"]["num_hidden_layers"] expert_count = configuration["text_config"]["num_experts"] expert_width = configuration["text_config"]["moe_intermediate_size"] hidden_width = configuration["text_config"]["hidden_size"] shards = {} headers = {} for shard_name in dict.fromkeys(weight_map.values()): shards[shard_name] = safe_open(arguments.checkpoint / shard_name, framework="pt", device="cpu") with (arguments.checkpoint / shard_name).open("rb") as shard_file: header_length = struct.unpack("