jburtoft's picture
Initial upload: 4-layer distilled Voxtral draft + training pipeline
f9e3832 verified
Raw
History Blame Contribute Delete
5.19 kB
"""Build a naive layer-pruned Voxtral draft from Voxtral-Mini-3B-2507.
Loads the full model, keeps a subset of the 30 Llama decoder layers, saves
the pruned model to disk with a corrected config so HF `.from_pretrained()`
reloads it as a smaller `VoxtralForConditionalGeneration`.
Design:
- Full model: 30 decoder layers in `model.language_model.model.layers`.
- Keep layers by 0-indexed positions (default: [0, 6, 12, 18, 24, 29]).
- Preserve everything else unchanged: audio encoder, multi_modal_projector,
embed_tokens, norm, lm_head. This ensures the draft shares the audio path
and vocab with the target (both required for HF `assistant_model=`).
- Save to `--out-dir` so HF can reload with `.from_pretrained()`.
Usage:
python build_draft.py --keep 0,6,12,18,24,29 --out-dir /mnt/drafts/voxtral-mini-3B-draft-6of30
python build_draft.py --keep 0,10,20,29 --out-dir /mnt/drafts/voxtral-mini-3B-draft-4of30
Then load in a benchmark script:
target = VoxtralForConditionalGeneration.from_pretrained("mistralai/Voxtral-Mini-3B-2507", ...)
draft = VoxtralForConditionalGeneration.from_pretrained("/mnt/drafts/voxtral-mini-3B-draft-6of30", ...)
outputs = target.generate(..., assistant_model=draft)
"""
from __future__ import annotations
import argparse
import json
import os
import shutil
from pathlib import Path
import torch
from transformers import (
AutoProcessor,
VoxtralForConditionalGeneration,
)
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser()
p.add_argument("--source", default="mistralai/Voxtral-Mini-3B-2507")
p.add_argument("--out-dir", required=True, type=Path)
p.add_argument(
"--keep",
default="0,6,12,18,24,29",
help="Comma-separated 0-indexed layer indices to keep from the 30 "
"Llama decoder layers. Must be sorted ascending, must include "
"at least the first and last for stability.",
)
p.add_argument("--dtype", default="bfloat16", choices=["bfloat16", "float16", "float32"])
p.add_argument("--device", default="cuda")
return p.parse_args()
def main() -> int:
args = parse_args()
keep_layers = [int(x) for x in args.keep.split(",")]
n_keep = len(keep_layers)
assert keep_layers == sorted(keep_layers), "--keep must be ascending"
assert n_keep >= 2, "Keep at least 2 layers"
dtype = {"bfloat16": torch.bfloat16, "float16": torch.float16, "float32": torch.float32}[args.dtype]
print(f"[build_draft] Loading {args.source} on {args.device} dtype={args.dtype}")
model = VoxtralForConditionalGeneration.from_pretrained(
args.source,
torch_dtype=dtype,
low_cpu_mem_usage=True,
)
processor = AutoProcessor.from_pretrained(args.source)
# Introspect layer container. transformers 5.x path:
# VoxtralForConditionalGeneration
# .model (VoxtralModel)
# .language_model (LlamaModel)
# .layers (ModuleList of 30 LlamaDecoderLayer)
# .lm_head
# transformers 4.5x path had .language_model directly under the top-level
# object (no intermediate .model). Support both.
if hasattr(model, "model") and hasattr(model.model, "language_model"):
decoder = model.model.language_model
elif hasattr(model, "language_model"):
lm = model.language_model
decoder = lm.model if hasattr(lm, "model") else lm
else:
raise AttributeError(
"Cannot find decoder. Model has: " + ", ".join(sorted(dict(model.named_children()).keys()))
)
assert hasattr(decoder, "layers"), (
f"Decoder container {type(decoder).__name__} has no .layers. "
f"Children: {list(dict(decoder.named_children()).keys())}"
)
layers = decoder.layers
n_full = len(layers)
print(f"[build_draft] Full model has {n_full} decoder layers")
assert max(keep_layers) < n_full, f"--keep {keep_layers} exceeds n_full={n_full}"
# Rewrite layer_idx on each kept layer so RoPE / KV cache accounting still works
kept = torch.nn.ModuleList([layers[i] for i in keep_layers])
for new_idx, layer in enumerate(kept):
if hasattr(layer, "self_attn") and hasattr(layer.self_attn, "layer_idx"):
layer.self_attn.layer_idx = new_idx
decoder.layers = kept
# Update text_config so config.json reflects the new depth
tcfg = model.config.text_config
tcfg.num_hidden_layers = n_keep
# Some HF paths look for the num_hidden_layers on the top config too.
if hasattr(model.config, "num_hidden_layers"):
model.config.num_hidden_layers = n_keep
# Save
args.out_dir.mkdir(parents=True, exist_ok=True)
print(f"[build_draft] Saving pruned model to {args.out_dir}")
model.save_pretrained(args.out_dir, safe_serialization=True)
processor.save_pretrained(args.out_dir)
# Sanity print
with open(args.out_dir / "config.json") as f:
saved = json.load(f)
print(f"[build_draft] saved text_config.num_hidden_layers = "
f"{saved.get('text_config', {}).get('num_hidden_layers')}")
print(f"[build_draft] Done.")
return 0
if __name__ == "__main__":
raise SystemExit(main())