DSSD-dev / dssd /hub.py
FlorianVal's picture
DSSD demo from DSSD_demo 4a3ca5b (dssd 73b3f27)
74679d4 verified
Raw History Blame Contribute Delete
6.04 kB
"""Hub format of a DSSD head set: `heads.safetensors` + `dssd_config.json`.
heads.safetensors, for every head k (bf16 except the token ids):
h{k}.norm.weight [d], h{k}.proj.weight [B_k + 1, d] (last row = reject class ⊥),
h{k}.reject_bias [1], h{k}.vocab_ids [B_k] (int32).
dssd_config.json: format_version, base_model, base_revision, head_layers, budgets,
reject_class, dtype, default_domain, domains {name: {policy, stop}}, provenance.
A policy uses the bench format (scripts/compose_final.py): type "threshold", thresholds,
cum_gamma, acc_maps {k: [knots, values]}, max_draft, tree.
"""
from __future__ import annotations
import json
import os
from pathlib import Path
import torch
from .heads import ExitHead
FORMAT_VERSION = 1
HEADS_FILE = "heads.safetensors"
CONFIG_FILE = "dssd_config.json"
BASE_DIMS = ("hidden_size", "vocab_size", "num_hidden_layers")
def save_heads(out_dir: str | Path, heads: list[ExitHead], layers: list[int], config: dict,
model_config=None) -> None:
"""Write heads.safetensors and dssd_config.json (format, layers, budgets and reject class
are filled in here; every other key comes from `config`). With the base model's config, its
dimensions are recorded and checked at load time."""
from safetensors.torch import save_file
out = Path(out_dir)
out.mkdir(parents=True, exist_ok=True)
tensors = {}
for k, h in enumerate(heads):
if h.adapter is not None or h.vocab_ids is None or not h.reject:
raise ValueError("only pruned heads with a reject class and no adapter are supported")
tensors[f"h{k}.norm.weight"] = h.norm.weight.detach().to(torch.bfloat16).contiguous()
tensors[f"h{k}.proj.weight"] = h.proj.weight.detach().to(torch.bfloat16).contiguous()
tensors[f"h{k}.reject_bias"] = h.reject_bias.detach().to(torch.bfloat16).contiguous()
tensors[f"h{k}.vocab_ids"] = h.vocab_ids.to(torch.int32).contiguous()
save_file({n: t.cpu() for n, t in tensors.items()}, str(out / HEADS_FILE))
conf = {"format_version": FORMAT_VERSION, **config, "head_layers": [int(x) for x in layers],
"budgets": [h.n_vocab for h in heads], "reject_class": True, "dtype": "bfloat16"}
if model_config is not None:
conf["base_dims"] = {k: int(getattr(model_config, k)) for k in BASE_DIMS}
(out / CONFIG_FILE).write_text(json.dumps(conf, indent=1, ensure_ascii=False))
def _resolve(path_or_repo: str, revision: str | None, token: str | None) -> Path:
if os.path.isdir(path_or_repo):
return Path(path_or_repo)
from huggingface_hub import snapshot_download
return Path(snapshot_download(path_or_repo, revision=revision, token=token,
allow_patterns=[HEADS_FILE, CONFIG_FILE]))
def read_config(path_or_repo: str, revision: str | None = None, token: str | None = None) -> dict:
return json.loads((_resolve(path_or_repo, revision, token) / CONFIG_FILE).read_text())
def load_heads(path_or_repo: str, model_config, device="cpu", dtype=torch.bfloat16,
revision: str | None = None, token: str | None = None) -> tuple[list[ExitHead], list[int], dict]:
"""Heads, their layers and the config from a local directory or a Hub repo. Checks that
they fit the base model (hidden size, vocabulary, number of layers)."""
from safetensors.torch import load_file
root = _resolve(path_or_repo, revision, token)
conf = json.loads((root / CONFIG_FILE).read_text())
if conf.get("format_version") != FORMAT_VERSION:
raise ValueError(f"unsupported format_version {conf.get('format_version')!r} (expected {FORMAT_VERSION})")
for k, v in conf.get("base_dims", {}).items(): # heads trained on another base model
if int(getattr(model_config, k)) != v:
raise ValueError(f"base model {k} {getattr(model_config, k)} != {v} recorded with the heads")
layers = [int(x) for x in conf["head_layers"]]
n_layers = model_config.num_hidden_layers
if any(not 0 < x < n_layers for x in layers):
raise ValueError(f"head layer out of range for a {n_layers}-layer base model: {layers}")
t = load_file(str(root / HEADS_FILE))
heads = []
for k in range(len(layers)):
w, ids = t[f"h{k}.proj.weight"], t[f"h{k}.vocab_ids"].long()
if w.shape[1] != model_config.hidden_size:
raise ValueError(f"head {k}: hidden_size {w.shape[1]} != base model {model_config.hidden_size}")
if int(ids.max()) >= model_config.vocab_size:
raise ValueError(f"head {k}: token id {int(ids.max())} >= base model vocab_size {model_config.vocab_size}")
h = ExitHead(model_config.hidden_size, model_config.vocab_size, model_config.rms_norm_eps,
vocab_ids=ids, reject=True)
h.load_state_dict({"norm.weight": t[f"h{k}.norm.weight"], "proj.weight": w,
"reject_bias": t[f"h{k}.reject_bias"], "vocab_ids": ids})
heads.append(h.to(device=device, dtype=dtype).eval())
return heads, layers, conf
def heads_from_pt(path: str, model_config) -> tuple[list[ExitHead], list[int]]:
"""Pruned heads from a training checkpoint {'heads': [state_dict], 'meta': {'layers'}}
(scripts/train_pruned.py, merge_pruned_heads.py), dtype preserved."""
ck = torch.load(path, map_location="cpu", weights_only=False)
layers = [int(x) for x in ck["meta"]["layers"]]
heads = []
for k in range(len(layers)):
sd = ck["heads"][k]
if any(n.startswith("adapter.") for n in sd) or sd.get("vocab_ids") is None or "reject_bias" not in sd:
raise ValueError(f"head {k}: only pruned heads with a reject class and no adapter are supported")
n_out = sd["proj.weight"].shape[0] - 1
h = ExitHead(model_config.hidden_size, model_config.vocab_size, model_config.rms_norm_eps,
vocab_ids=sd["vocab_ids"][:n_out], reject=True)
h.load_state_dict(sd)
heads.append(h.to(sd["proj.weight"].dtype).eval())
return heads, layers