#!/usr/bin/env python3 """ REFERENCE ONLY - build a small test "slice" of a HF safetensors model without downloading the full weights. The heuristics below (layer/expert detection, config patching) cover common layouts only. New models often use tensor names, shapes or config keys that this script does not know about. Always run --inspect first, check the plan, and adapt the script to the model. How it works: each safetensors shard starts with an 8-byte header length + JSON header, which gives the exact byte span of every tensor. We fetch only the headers, then use HTTP range requests to fetch the tensors we keep: 1. keep only some layers (per numeric "family" in tensor names whose count matches a layer count in config.json), renumbered to 0..n-1 2. keep only the first E experts (per-expert tensors are dropped, stacked expert tensors and router weights are row-sliced along dim 0) 3. optionally keep only the first V rows of vocab-sized tensors (off by default, see --vocab-size) config.json is patched to match. Other small repo files are copied as-is. Usage: python slice_model.py Qwen/Qwen3-0.6B --inspect python slice_model.py Qwen/Qwen3-30B-A3B --num-layers 2 --num-experts 8 --dry-run python slice_model.py Qwen/Qwen3-30B-A3B --keep-layers 0,3 --output ../models/qwen3moe-slice """ from __future__ import annotations import argparse import concurrent.futures as cf import fnmatch import json import logging import os import re import struct import sys import threading import time from dataclasses import dataclass, field from pathlib import Path from typing import Any import requests from huggingface_hub import HfApi, get_token, hf_hub_url logging.basicConfig(level=logging.INFO, format="%(message)s") logger = logging.getLogger("slice_model") INDEX_FILE = "model.safetensors.index.json" CONFIG_FILE = "config.json" # files not worth copying into a slice AUX_BLACKLIST = [ # weights in other formats (the slice replaces them) "*.bin", "*.pth", "*.pt", "*.ckpt", "*.h5", "*.msgpack", "*.onnx", "*.tflite", "*.mlmodel", "*.mlpackage/*", "*.gguf", "*.nemo", "onnx/*", "openvino/*", "coreml/*", "original/*", "metal/*", # media / demo assets "*.png", "*.jpg", "*.jpeg", "*.gif", "*.webp", "*.avif", "*.svg", "*.mp4", "*.webm", "*.mov", "*.wav", "*.mp3", "*.flac", "*.ogg", "*.pdf", ".gitattributes", ] AUX_MAX_SIZE = 50 * 1024 * 1024 LAYER_CONTAINERS = {"layers", "layer", "blocks", "block", "h"} EXPERT_ROW_KEYWORDS = ("expert", "router", "gate", "score") LAYER_KEYS = { "num_hidden_layers", "num_layers", "n_layer", "n_layers", "num_layer", "num_decoder_layers", "encoder_layers", "decoder_layers", "depth", "num_encoder_layers", "num_attention_layers", "layer_count", } VOCAB_KEYS = {"vocab_size", "padded_vocab_size"} NEXTN_KEYS = {"num_nextn_predict_layers", "num_mtp_layers"} TOPK_SUBSTR = ("per_tok", "topk", "top_k", "moe_k") EXPERT_KEY_EXCLUDE = ("per_tok", "topk", "top_k", "shared", "group", "intermediate", "size", "dim", "dtype") def parse_size(text: str) -> int: m = re.fullmatch(r"(\d+(?:\.\d+)?)\s*([kKmMgGtT]?)[bB]?", text.strip()) if not m: raise argparse.ArgumentTypeError(f"invalid size: {text!r}") mult = {"": 1, "k": 1024, "m": 1024**2, "g": 1024**3, "t": 1024**4}[m.group(2).lower()] return int(float(m.group(1)) * mult) def human(n: float) -> str: for unit in ("B", "KB", "MB", "GB"): if n < 1024 or unit == "GB": return f"{n:.1f}{unit}" if unit != "B" else f"{int(n)}B" n /= 1024 return f"{n}GB" # -------------------------------------------------------------------------------------- # HTTP # -------------------------------------------------------------------------------------- _tls = threading.local() def _session() -> requests.Session: s = getattr(_tls, "s", None) if s is None: s = requests.Session() _tls.s = s return s class HubClient: def __init__(self, repo_id: str, revision: str, token: str | None): self.repo_id = repo_id self.revision = revision self.headers = {"Authorization": f"Bearer {token}"} if token else {} def url(self, filename: str) -> str: return hf_hub_url(self.repo_id, filename, revision=self.revision) def _request(self, filename: str, headers: dict, stream: bool = False, retries: int = 5): last: Exception | None = None for attempt in range(1, retries + 1): try: r = _session().get(self.url(filename), headers=headers, stream=stream, timeout=90, allow_redirects=True) if r.status_code in (429, 500, 502, 503, 504): raise requests.HTTPError(f"HTTP {r.status_code}", response=r) r.raise_for_status() return r except Exception as e: # noqa: BLE001 last = e if attempt == retries or "404" in str(e) or "401" in str(e) or "403" in str(e): break time.sleep(2.0 * attempt) raise RuntimeError(f"{self.repo_id}/{filename}: {last}") from last def get_range(self, filename: str, start: int, end_inclusive: int) -> bytes: h = dict(self.headers) h["Range"] = f"bytes={start}-{end_inclusive}" return self._request(filename, h).content def get_json(self, filename: str) -> Any: return json.loads(self._request(filename, dict(self.headers)).content) def download_file(self, filename: str, dest: Path): r = self._request(filename, dict(self.headers), stream=True) dest.parent.mkdir(parents=True, exist_ok=True) with open(dest, "wb") as f: for chunk in r.iter_content(chunk_size=4 * 1024 * 1024): if chunk: f.write(chunk) def stream_range_into_fd(self, filename: str, start: int, end_inclusive: int, fd: int, write_offset: int): h = dict(self.headers) h["Range"] = f"bytes={start}-{end_inclusive}" r = self._request(filename, h, stream=True) pos = write_offset for chunk in r.iter_content(chunk_size=8 * 1024 * 1024): if chunk: os.pwrite(fd, chunk, pos) pos += len(chunk) if pos - write_offset != end_inclusive - start + 1: raise RuntimeError(f"short read: {filename} bytes={start}-{end_inclusive}") def fetch_shard_header(client: HubClient, filename: str) -> tuple[dict, int]: (header_len,) = struct.unpack(" ConfigInfo: info = ConfigInfo(cfg=cfg) if cfg is None: return info for _d, key, val in walk_config(cfg): if not isinstance(val, int) or isinstance(val, bool) or val <= 0: continue if key in LAYER_KEYS: info.layer_counts.add(val) elif key in NEXTN_KEYS: info.nextn_counts.add(val) elif key in VOCAB_KEYS: info.vocab_sizes.add(val) elif key == "first_k_dense_replace": info.first_k_dense = val elif "expert" in key and val > 1 and not any(x in key for x in EXPERT_KEY_EXCLUDE): info.expert_counts.add(val) return info def patch_config(cfg: dict, families: dict[str, Family], new_vocab: int | None, orig_vocabs: set[int]) -> list[str]: """Patch counts in-place to match the slice. Returns log lines.""" log = [] by_card: dict[int, Family] = {} for fam in families.values(): if fam.kept is not None: by_card.setdefault(fam.card, fam) by_card.setdefault(fam.regular_total, fam) max_new_experts = max((f.new_count for f in families.values() if f.kind == "expert" and f.kept is not None), default=None) for d, key, val in list(walk_config(cfg)): if isinstance(val, int) and not isinstance(val, bool): fam = by_card.get(val) if key in LAYER_KEYS and fam is not None and fam.kind == "layer": d[key] = fam.new_regular elif key == "first_k_dense_replace": lfam = next((f for f in families.values() if f.kind == "layer"), None) if lfam is None: continue assert lfam.kept is not None d[key] = sum(1 for i in lfam.kept if i < val) elif "expert" in key and fam is not None and fam.kind == "expert" and not any(x in key for x in EXPERT_KEY_EXCLUDE): d[key] = fam.new_count elif "expert" in key and any(x in key for x in TOPK_SUBSTR) and max_new_experts is not None and val > max_new_experts: d[key] = max_new_experts elif key in VOCAB_KEYS and new_vocab is not None and val in orig_vocabs: d[key] = new_vocab if d[key] != val: log.append(f"{key}: {val} -> {d[key]}") elif isinstance(val, list) and val and all(not isinstance(x, (dict, list)) for x in val): # per-layer lists, e.g. layer_types fam = by_card.get(len(val)) if fam is not None and fam.kind == "layer" and len(val) in (fam.card, fam.regular_total): assert fam.kept is not None d[key] = [val[i] for i in fam.kept if i < len(val)] log.append(f"{key}: list[{len(val)}] -> list[{len(d[key])}]") return log # -------------------------------------------------------------------------------------- # tensor name analysis # -------------------------------------------------------------------------------------- @dataclass class Family: """One numeric slot in tensor names, e.g. 'model.layers.#'.""" prefix: str kind: str # "layer" | "expert" | "other" indices: set[int] = field(default_factory=set) kept: list[int] | None = None # None = keep all, no renumbering renumber: dict[int, int] = field(default_factory=dict) regular_total: int = 0 # layer count excluding MTP tail new_regular: int = 0 @property def card(self) -> int: return len(self.indices) @property def new_count(self) -> int: return len(self.kept) if self.kept is not None else self.card def name_slots(name: str): """Yield (slot_pos, family_prefix, index) for each numeric path component.""" tokens = name.split(".") norm: list[str] = [] for i, tok in enumerate(tokens): if tok.isdigit(): yield i, ".".join(norm), int(tok) norm.append("#") else: norm.append(tok) def build_families(names: list[str], cfg_info: ConfigInfo) -> dict[str, Family]: families: dict[str, Family] = {} for name in names: tokens = name.split(".") for i, prefix, idx in name_slots(name): container = tokens[i - 1] if i > 0 else "" fam = families.get(prefix) if fam is None: fam = Family(prefix=prefix, kind="expert" if "expert" in container else "other") families[prefix] = fam fam.indices.add(idx) acceptable_layer_cards: dict[int, int] = {} # card -> regular layer count for c in cfg_info.layer_counts: acceptable_layer_cards[c] = c for m in cfg_info.nextn_counts: acceptable_layer_cards[c + m] = c for fam in families.values(): if fam.kind == "expert": continue container = fam.prefix.rsplit(".", 1)[-1] if cfg_info.cfg is not None: if fam.card in acceptable_layer_cards: fam.kind = "layer" fam.regular_total = acceptable_layer_cards[fam.card] elif fam.card >= 4 and container in LAYER_CONTAINERS: fam.kind = "layer" fam.regular_total = fam.card return families def plan_families(families: dict[str, Family], num_layers: int, keep_layers: list[int] | None, num_experts: int, first_k_dense: int): for fam in families.values(): if fam.kind == "layer": if keep_layers is not None: kept = [i for i in keep_layers if i < fam.regular_total] else: base = min(num_layers, fam.regular_total) kept = list(range(base)) # keep one MoE layer for models whose first k layers are dense if 0 < first_k_dense < fam.regular_total and first_k_dense >= base >= 1: kept = list(range(base - 1)) + [first_k_dense] fam.new_regular = len(kept) # keep the MTP tail (layer indices beyond the regular count) kept += [i for i in sorted(fam.indices) if i >= fam.regular_total] fam.kept = sorted(set(kept)) elif fam.kind == "expert": fam.kept = sorted(fam.indices)[:num_experts] fam.new_regular = len(fam.kept) else: fam.kept = None continue fam.renumber = {old: new for new, old in enumerate(fam.kept)} def slice_tensor_name(name: str, families: dict[str, Family]) -> str | None: """Return the renumbered output name, or None if the tensor is dropped.""" tokens = name.split(".") for i, prefix, idx in name_slots(name): fam = families[prefix] if fam.kept is None: continue if idx not in fam.renumber: return None tokens[i] = str(fam.renumber[idx]) return ".".join(tokens) # -------------------------------------------------------------------------------------- # slicing # -------------------------------------------------------------------------------------- @dataclass class TensorPlan: src_name: str out_name: str dtype: str shape: list[int] orig_shape: list[int] size: int src_file: str seg_start: int # absolute byte range in src_file seg_end: int # inclusive def pick_weight_files(filenames: list[str]) -> tuple[list[str], bool]: """Return (weight files, has_index). Preference: index -> model.safetensors -> any root safetensors.""" if INDEX_FILE in filenames: return [], True if "model.safetensors" in filenames: return ["model.safetensors"], False root_st = [f for f in filenames if f.endswith(".safetensors") and "/" not in f] if "consolidated.safetensors" in root_st: return ["consolidated.safetensors"], False return root_st[:1], False def row_slice(shape: list[int], size: int, keep_rows: int) -> tuple[list[int], int] | None: # only a prefix of dim 0 is a single contiguous byte range if len(shape) < 1 or shape[0] <= keep_rows or size % shape[0] != 0: return None row_bytes = size // shape[0] return [keep_rows] + shape[1:], keep_rows * row_bytes def build_plan(args, families, headers, weight_files, src_file_of, cfg_info) -> list[TensorPlan]: plan_families(families, args.num_layers, args.keep_layers, args.num_experts, cfg_info.first_k_dense) plans = [] for fname in weight_files: header, data_start = headers[fname] for name, meta in header.items(): out_name = slice_tensor_name(name, families) if out_name is None: continue start, end = meta["data_offsets"] shape, size = list(meta["shape"]), end - start new_rows = None if args.vocab_size and shape and shape[0] > args.vocab_size \ and any(shape[0] >= vs and shape[0] - vs <= 1024 for vs in cfg_info.vocab_sizes): new_rows = args.vocab_size elif shape and shape[0] in cfg_info.expert_counts and shape[0] > args.num_experts \ and any(k in name for k in EXPERT_ROW_KEYWORDS): new_rows = args.num_experts if new_rows is not None: sliced = row_slice(shape, size, new_rows) if sliced is not None: shape, size = sliced plans.append(TensorPlan(name, out_name, meta["dtype"], shape, list(meta["shape"]), size, src_file_of[name], data_start + start, data_start + start + size - 1)) return plans def print_inspect(cfg_info: ConfigInfo, families: dict[str, Family], plans: list[TensorPlan], all_names: list[str]): logger.info("=== config analysis ===") logger.info(f"layer counts: {sorted(cfg_info.layer_counts)} nextn: {sorted(cfg_info.nextn_counts)} " f"experts: {sorted(cfg_info.expert_counts)} vocab: {sorted(cfg_info.vocab_sizes)} first_k_dense: {cfg_info.first_k_dense}") logger.info("\n=== numeric families in tensor names ===") for fam in sorted(families.values(), key=lambda f: f.prefix): kept = "all" if fam.kept is None else f"{fam.kept[:16]}{'...' if len(fam.kept) > 16 else ''}" logger.info(f"{fam.prefix:60s} kind={fam.kind:6s} count={fam.card:4d} kept={kept}") logger.info("\n=== kept tensors (first index of each family only) ===") seen = set() for t in plans: key = re.sub(r"\.\d+\.", ".#.", t.out_name) if key in seen and t.shape == t.orig_shape: continue seen.add(key) mark = f" (sliced from {t.orig_shape})" if t.shape != t.orig_shape else "" logger.info(f"{t.out_name:70s} {t.dtype:5s} {t.shape}{mark}") kept_src = {t.src_name for t in plans} dropped = [n for n in all_names if n not in kept_src] logger.info(f"\n{len(plans)}/{len(all_names)} tensors kept, {len(dropped)} dropped") def write_slice(out_dir: Path, client: HubClient, plans: list[TensorPlan], jobs: int): header_out: dict[str, Any] = {} offset = 0 for t in plans: header_out[t.out_name] = {"dtype": t.dtype, "shape": t.shape, "data_offsets": [offset, offset + t.size]} offset += t.size header_out["__metadata__"] = {"format": "pt"} hbytes = json.dumps(header_out, separators=(",", ":")).encode() hbytes += b" " * ((-(len(hbytes) + 8)) % 8) data_start = 8 + len(hbytes) shard_path = out_dir / "model.safetensors" with open(shard_path, "wb") as f: f.write(struct.pack(" args.max_size: logger.error(f"slice is {human(est)} > --max-size {human(args.max_size)}") return 1 if args.inspect or args.dry_run: return 0 out_dir = args.output or Path("tmp") / "hf_slices" / args.repo.replace("/", "--") out_dir.mkdir(parents=True, exist_ok=True) write_slice(out_dir, client, plans, args.jobs) skipped_aux = [] for fname in filenames: if fname in weight_files or fname in (INDEX_FILE, CONFIG_FILE) or fname.endswith(".safetensors"): continue if any(fnmatch.fnmatch(fname, p) or fnmatch.fnmatch(Path(fname).name, p) for p in AUX_BLACKLIST): continue if siblings[fname] > AUX_MAX_SIZE: skipped_aux.append(f"{fname} ({human(siblings[fname])})") continue client.download_file(fname, out_dir / fname) for f in skipped_aux: logger.warning(f"skipped large aux file {f}") patch_log = [] if cfg is not None: new_vocab = args.vocab_size if args.vocab_size and any(vs > args.vocab_size for vs in cfg_info.vocab_sizes) else None patch_log = patch_config(cfg, families, new_vocab, cfg_info.vocab_sizes) with open(out_dir / CONFIG_FILE, "w") as f: json.dump(cfg, f, indent=2) f.write("\n") for line in patch_log: logger.info(f"config: {line}") with open(out_dir / ".slice_meta.json", "w") as f: json.dump({ "repo": args.repo, "revision": info.sha, "tensors": len(plans), "total_tensors": len(all_names), "size": est, "config_patches": patch_log, "skipped_aux": skipped_aux, }, f, indent=2) logger.info(f"wrote {out_dir}") return 0 if __name__ == "__main__": sys.exit(main())