voice_audio_multi/tools/models/convert_ming3_audio_gguf.py
Fabio c36510c21e
Some checks failed
Linux Build / Linux cpu (push) Has been cancelled
Linux Build / Linux cpu-clang (push) Has been cancelled
Linux Build / Linux vulkan (push) Has been cancelled
mac-build / macOS CPU (push) Has been cancelled
Nix Build / Nix (cpu) (push) Has been cancelled
Nix Build / Nix (vulkan) (push) Has been cancelled
windows-build / Windows CPU (push) Has been cancelled
Initial import: audio.cpp with Orange Pi documentation
2026-11-04 19:05:22 +01:00

136 lines
7.1 KiB
Python

#!/usr/bin/env python3
"""Package Ming3 Audio Tiny, stacking routed experts for indexed GGML matmuls."""
import argparse
import json
import math
from pathlib import Path
import re
import struct
import subprocess
import tempfile
import gguf
import numpy as np
from safetensors import safe_open
from safetensors.numpy import save_file
import torch
from convert_ming3_tts_gguf import campplus_weights
def stack_thinker(source, destination, config):
weight_map = json.loads((source / "model.safetensors.index.json").read_text())["weight_map"]
handles = {}
headers = {}
for name in dict.fromkeys(weight_map.values()):
handles[name] = safe_open(source / name, framework="pt", device="cpu")
with (source / name).open("rb") as stream:
size = struct.unpack("<Q", stream.read(8))[0]
header = json.loads(stream.read(size))
header.pop("__metadata__", None)
headers.update(header)
text = config["text_config"]
layers = text["num_hidden_layers"]
plan = []
for name in weight_map:
layer = re.match(r"model\.layers\.(\d+)\.", name)
if layer and int(layer[1]) >= layers:
continue
if ".mlp.experts." not in name:
plan.append((name, headers[name]["shape"], headers[name]["dtype"], name))
for layer in range(text["first_k_dense_replace"], layers):
for projection in ("gate_proj", "up_proj", "down_proj"):
names = [f"model.layers.{layer}.mlp.experts.{i}.{projection}.weight"
for i in range(text["num_experts"])]
first = headers[names[0]]
for name in names:
if headers[name]["shape"] != first["shape"] or headers[name]["dtype"] != first["dtype"]:
raise ValueError(f"Inconsistent expert bank at {name}")
plan.append((f"model.layers.{layer}.mlp.experts.{projection}.weight",
[len(names), *first["shape"]], first["dtype"], names))
writer = gguf.GGUFWriter(destination, "ming3_audio")
writer.add_string("audiocpp.family", "ming3_audio")
writer.add_array("audiocpp.tensor_names", [name for name, *_ in plan])
writer.add_key_value("audiocpp.tensor_ranks", [len(shape) for _, shape, _, _ in plan],
gguf.GGUFValueType.ARRAY, gguf.GGUFValueType.INT32)
writer.add_key_value("audiocpp.tensor_shapes", [dim for _, shape, _, _ in plan for dim in shape],
gguf.GGUFValueType.ARRAY, gguf.GGUFValueType.INT64)
dtypes = {"BF16": (np.uint16, gguf.GGMLQuantizationType.BF16),
"F16": (np.float16, gguf.GGMLQuantizationType.F16),
"F32": (np.float32, gguf.GGMLQuantizationType.F32)}
for i, (_, shape, dtype, _) in enumerate(plan):
numpy_type, raw_type = dtypes[dtype]
writer.add_tensor_info(f"_audiocpp.{i}", shape, np.dtype(numpy_type),
math.prod(shape) * np.dtype(numpy_type).itemsize, raw_dtype=raw_type)
writer.write_header_to_file()
writer.write_kv_data_to_file()
writer.write_ti_data_to_file()
try:
for i, (name, shape, dtype, source_names) in enumerate(plan):
numpy_type = dtypes[dtype][0]
if isinstance(source_names, list):
values = np.empty(shape, dtype=numpy_type)
for expert, source_name in enumerate(source_names):
tensor = handles[weight_map[source_name]].get_tensor(source_name)
values[expert] = tensor.view(torch.uint16).numpy() if dtype == "BF16" else tensor.numpy()
else:
tensor = handles[weight_map[source_names]].get_tensor(source_names)
values = tensor.view(torch.uint16).numpy() if dtype == "BF16" else tensor.numpy()
writer.write_tensor_data(np.ascontiguousarray(values))
if i % 100 == 0 or isinstance(source_names, list):
print(f"{i + 1}/{len(plan)} {name}", flush=True)
finally:
writer.close()
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--source", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--type", choices=("orig", "q8_0", "q4_k"), default="orig",
help="Quantize Thinker experts; Q4_K also uses a Q8_0 text output head")
parser.add_argument("--converter", type=Path, default=Path("build/debug/bin/audiocpp_gguf"))
parser.add_argument("--temporary-dir", type=Path, required=True,
help="Scratch filesystem with space for one unquantized Thinker GGUF")
parser.add_argument("--log", action="store_true", required=True)
args = parser.parse_args()
if args.output.exists():
raise FileExistsError(args.output)
source = args.source.resolve()
config = json.loads((source / "config.json").read_text())
if config["model_type"] != "bailing_moe_v3_audio":
raise ValueError("Expected the official Ming3 Audio checkpoint")
args.output.parent.mkdir(parents=True, exist_ok=True)
args.temporary_dir.mkdir(parents=True, exist_ok=True)
spec = Path(__file__).resolve().parents[2] / "model_specs/ming3_audio.json"
with tempfile.TemporaryDirectory(prefix="ming3-audio-", dir=args.temporary_dir) as temporary:
root = Path(temporary)
thinker = root / "thinker.gguf"
stack_thinker(source, thinker, config)
speaker = root / "speaker.safetensors"
save_file(campplus_weights(source / "talker/campplus.onnx"), speaker)
command = [str(args.converter.resolve()), "--input", f"thinker={thinker}",
"--input", f"talker={source / 'talker/model.safetensors'}",
"--input", f"vae={source / 'talker/vae/model.safetensors'}",
"--input", f"speaker={speaker}", "--root", str(source),
"--model-spec", str(spec), "--family", "ming3_audio",
"--output", str(args.output.resolve()), "--type", "orig", "--log",
"--keep-type", "speaker*=f32", "--keep-type", "talker*=orig",
"--keep-type", "vae*=orig", "--keep-type", "thinker/model.audio*=orig",
"--keep-type", "thinker/linear_proj_audio*=orig"]
for layer in range(config["text_config"]["first_k_dense_replace"], config["text_config"]["num_hidden_layers"]):
command += ["--keep-type", f"thinker/model.layers.{layer}.mlp.experts.*={args.type}"]
command += ["--keep-type", f"thinker/model.layers.{layer}.mlp.gate*=f32"]
if args.type == "q4_k":
command += ["--keep-type", "thinker/lm_head.weight=q8_0"]
for relative in ("config.json", "tokenizer.json", "tokenizer_config.json", "generation_config.json",
"chat_template.jinja", "special_tokens_map.json", "talker/config.json",
"talker/llm/config.json", "talker/llm/tokenizer.json", "talker/llm/tokenizer_config.json",
"talker/vae/config.json"):
command += ["--sidecar", f"{source / relative}={relative}"]
subprocess.run(command, check=True)
if __name__ == "__main__":
main()