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
164 lines
6.6 KiB
Python
164 lines
6.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Convert a released SAMSONE checkpoint to a standalone GGUF."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
import torch
|
|
from safetensors.torch import save_file
|
|
from transformers import AutoTokenizer
|
|
|
|
|
|
REPO = Path(__file__).resolve().parents[2]
|
|
CONVERTER = REPO / "build/debug/bin/audiocpp_gguf"
|
|
|
|
|
|
def build_token_map(tokenizer) -> dict[int, int]:
|
|
special_ids = set(tokenizer.all_special_ids)
|
|
output: dict[int, int] = {}
|
|
for token, original_id in sorted(tokenizer.get_vocab().items(), key=lambda item: item[1]):
|
|
cleaned = re.sub(r"[^A-Za-z0-9]", "", token)
|
|
keep = original_id in special_ids or (
|
|
not any(character.isupper() for character in cleaned)
|
|
and not re.search(r"[\sĠĊĉ]{4,}", token)
|
|
and not re.search(r"[^\x00-\x7FĠĊĉ\s]", token)
|
|
and not re.search(r"[-#]{3,}", token)
|
|
)
|
|
if keep:
|
|
output[len(output)] = original_id
|
|
return output
|
|
|
|
|
|
def converted_name(name: str) -> str | None:
|
|
name = name.removeprefix("audio_lm.")
|
|
if name == "text_model.embedding_layer.weight":
|
|
return None
|
|
if name.startswith("audio_encoder.encoder.encoder."):
|
|
suffix = name.removeprefix("audio_encoder.encoder.encoder.")
|
|
if suffix == "embed_positions.weight":
|
|
return "encoder.positional_embedding"
|
|
parts = suffix.split(".")
|
|
if parts[0] == "layers":
|
|
parts[0] = "blocks"
|
|
replacements = {
|
|
"self_attn": "attn",
|
|
"q_proj": "query",
|
|
"k_proj": "key",
|
|
"v_proj": "value",
|
|
"out_proj": "out",
|
|
"self_attn_layer_norm": "attn_ln",
|
|
"final_layer_norm": "mlp_ln",
|
|
"fc1": "mlp.0",
|
|
"fc2": "mlp.2",
|
|
}
|
|
parts = [replacements.get(part, part) for part in parts]
|
|
elif parts[0] == "layer_norm":
|
|
parts[0] = "ln_post"
|
|
return "encoder." + ".".join(parts)
|
|
if name.startswith("text_model.model.model."):
|
|
return "model." + name.removeprefix("text_model.model.model.")
|
|
if name.startswith("text_model.model.lm_head."):
|
|
return None
|
|
return name
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--checkpoint", type=Path, required=True)
|
|
parser.add_argument("--text-model-dir", type=Path, required=True)
|
|
parser.add_argument("--output", type=Path, required=True)
|
|
parser.add_argument("--type", choices=("bf16", "q8_0"), default="bf16")
|
|
args = parser.parse_args()
|
|
|
|
checkpoint = args.checkpoint.resolve()
|
|
text_model_dir = args.text_model_dir.resolve()
|
|
output = args.output.resolve()
|
|
state = torch.load(checkpoint, map_location="cpu", weights_only=True)["state_dict"]
|
|
if not state or not all(name.startswith("audio_lm.") for name in state):
|
|
raise RuntimeError("expected a released SAMSONE AudioLM checkpoint")
|
|
tensors = {}
|
|
for name, tensor in state.items():
|
|
target = converted_name(name)
|
|
if target is not None:
|
|
tensors[target] = tensor.contiguous()
|
|
|
|
tokenizer = AutoTokenizer.from_pretrained(text_model_dir)
|
|
token_map = build_token_map(tokenizer)
|
|
hidden = tensors["sep_token"].shape[0]
|
|
layers = sum(name.endswith(".self_attn.q_proj.weight") and name.startswith("model.layers.") for name in tensors)
|
|
q_shape = tensors["model.layers.0.self_attn.q_proj.weight"].shape
|
|
k_shape = tensors["model.layers.0.self_attn.k_proj.weight"].shape
|
|
intermediate = tensors["model.layers.0.mlp.gate_proj.weight"].shape[0]
|
|
text_config = json.loads((text_model_dir / "config.json").read_text())
|
|
original_eos = int(text_config["eos_token_id"])
|
|
original_to_pruned = {original: pruned for pruned, original in token_map.items()}
|
|
config = {
|
|
"model_type": "samsone",
|
|
"hidden_size": hidden,
|
|
"intermediate_size": intermediate,
|
|
"num_attention_heads": int(text_config["num_attention_heads"]),
|
|
"num_key_value_heads": int(k_shape[0] // (q_shape[0] // int(text_config["num_attention_heads"]))),
|
|
"num_hidden_layers": layers,
|
|
"vocab_size": len(token_map),
|
|
"max_position_embeddings": int(text_config["max_position_embeddings"]),
|
|
"rms_norm_eps": float(text_config["rms_norm_eps"]),
|
|
"rope_theta": float(text_config["rope_theta"]),
|
|
"eos_token_id": original_to_pruned[original_eos],
|
|
}
|
|
if tensors["model.embed_tokens.weight"].shape != (len(token_map), hidden):
|
|
raise RuntimeError("checkpoint vocabulary does not match the reconstructed token map")
|
|
|
|
subprocess.run(
|
|
["cmake", "--build", str(REPO / "build/debug"), "--target", "audiocpp_gguf", "-j", "8"],
|
|
check=True,
|
|
)
|
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
with tempfile.TemporaryDirectory(prefix="samsone-convert-") as temporary:
|
|
root = Path(temporary)
|
|
source = root / "model.safetensors"
|
|
save_file(tensors, source)
|
|
(root / "config.json").write_text(json.dumps(config, indent=2) + "\n")
|
|
(root / "token_map.json").write_text(json.dumps(token_map, sort_keys=True) + "\n")
|
|
for filename in ("tokenizer.json", "tokenizer_config.json"):
|
|
shutil.copyfile(text_model_dir / filename, root / filename)
|
|
fd, temporary_output = tempfile.mkstemp(prefix=output.stem + "-", suffix=".gguf", dir=output.parent)
|
|
os.close(fd)
|
|
temporary_output = Path(temporary_output)
|
|
try:
|
|
subprocess.run(
|
|
[
|
|
str(CONVERTER),
|
|
"--input", f"weights={source}",
|
|
"--root", str(root),
|
|
"--output", str(temporary_output),
|
|
"--type", args.type,
|
|
"--family", "samsone",
|
|
"--model-spec", str(REPO / "model_specs/samsone.json"),
|
|
"--overwrite",
|
|
],
|
|
check=True,
|
|
)
|
|
inspection = subprocess.run(
|
|
[str(CONVERTER), "--inspect", str(temporary_output)],
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
).stdout
|
|
if f"tensors={len(tensors)}" not in inspection.splitlines():
|
|
raise RuntimeError(f"SAMSONE GGUF tensor count mismatch:\n{inspection}")
|
|
os.replace(temporary_output, output)
|
|
print(inspection, end="")
|
|
finally:
|
|
temporary_output.unlink(missing_ok=True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|