Qwen3-Embedding-4B-NVFP4 / extras /quantize_qwen_embedding.py
catplusplus's picture
Upload folder using huggingface_hub
ad895de verified
Raw
History Blame Contribute Delete
6.7 kB
import os
# Configure workspace-local cache defaults to prevent permission errors on hosts
if "HF_HOME" not in os.environ:
os.environ["HF_HOME"] = os.path.expanduser("~/Nikola/.cache/huggingface")
if "HF_DATASETS_CACHE" not in os.environ:
os.environ["HF_DATASETS_CACHE"] = os.path.expanduser("~/Nikola/.cache/huggingface/datasets")
import sys
import random
import yaml
from pathlib import Path
import torch
from datasets import load_dataset, Dataset
from transformers import AutoModel, AutoTokenizer
from llmcompressor import oneshot
from llmcompressor.modifiers.quantization import QuantizationModifier
# Model ID configuration
MODEL_ID = "/home/olegk/Nikola/models/embedding/Qwen3-Embedding-4B"
SAVE_DIR = "/home/olegk/Nikola/models/embedding/Qwen3-Embedding-4B-NVFP4"
print(f"✨ Loading model: {MODEL_ID}")
model = AutoModel.from_pretrained(MODEL_ID, torch_dtype="auto")
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
# Select number of samples.
NUM_CALIBRATION_SAMPLES = 512
MAX_SEQUENCE_LENGTH = 512
samples_per_dataset = NUM_CALIBRATION_SAMPLES // 2
# Extract local notes for domain calibration
notes_texts = []
notes_dir = Path.home() / ".sakura" / "notes"
if notes_dir.exists():
print("✨ Found local Sakura notes! Extracting domain-specific calibration data...")
for category in ["persons", "places", "topics"]:
cat_dir = notes_dir / category
if cat_dir.exists():
for f in cat_dir.glob("*.yaml"):
if category == "persons" and f.stem in ["oleg", "sakura"]:
continue
try:
with open(f, "r", encoding="utf-8") as file:
data = yaml.safe_load(file) or {}
except Exception:
continue
# Format text parts based on category
if category == "persons":
name = data.get("name", f.stem.title())
desc = data.get("short_description", "")
details = " ".join(data.get("details", []))
notes_texts.append(f"Person Profile: {name} - {desc} {details}")
if desc: notes_texts.append(desc)
if details: notes_texts.append(details)
elif category == "places":
name = data.get("name", f.stem.title())
desc = data.get("short_description", "")
details = " ".join(data.get("details", []))
notes_texts.append(f"Place Profile: {name} - {desc} {details}")
if desc: notes_texts.append(desc)
if details: notes_texts.append(details)
elif category == "topics":
topic = data.get("topic", f.stem.replace("_", " ").title())
words = data.get("words", [])
suggestions = " ".join(data.get("suggestions", []))
notes_texts.append(f"Vocabulary Topic: {topic}")
if suggestions: notes_texts.append(suggestions)
for w in words:
word = w.get("word")
trans = w.get("translation")
notes = w.get("notes", "")
if word and trans:
notes_texts.append(f"{word} : {trans}")
notes_texts.append(word)
notes_texts.append(trans)
if notes:
notes_texts.append(notes)
print(f"✨ Extracted {len(notes_texts)} local note calibration segments.")
print("✨ Building balanced bilingual calibration dataset...")
# Load English calibration set (from ultrachat_200k)
ds_en = load_dataset(
"HuggingFaceH4/ultrachat_200k",
split=f"train_sft[:{samples_per_dataset}]",
)
en_texts = [example["messages"][0]["content"] for example in ds_en]
# Load Japanese calibration set (from llm-jp-instructions)
ds_ja = load_dataset(
"llm-jp/llm-jp-instructions",
split=f"train[:{samples_per_dataset}]",
)
ja_texts = [example["text"] for example in ds_ja]
# Combine and shuffle
calibration_texts = notes_texts + en_texts + ja_texts
random.seed(42)
random.shuffle(calibration_texts)
# Slice to target sample count
calibration_texts = calibration_texts[:NUM_CALIBRATION_SAMPLES]
# Convert to HF Dataset object
ds = Dataset.from_dict({"text": calibration_texts})
# Tokenize inputs
def tokenize(sample):
return tokenizer(
sample["text"],
padding=False,
max_length=MAX_SEQUENCE_LENGTH,
truncation=True,
add_special_tokens=False,
)
print("✨ Tokenizing dataset...")
ds = ds.map(tokenize, remove_columns=ds.column_names)
# Custom data collator to ensure everything goes to cuda
def data_collator(batch):
assert len(batch) == 1
return {key: torch.tensor(value).unsqueeze(0).to(model.device) for key, value in batch[0].items()}
# Configure the quantization algorithm and scheme.
# We target only the MLP layers, leaving self-attention projections in bfloat16.
# Using NVFP4 (W4A4) ensures that calibration runs and generates the activation scales vLLM needs. 🌸✨
recipe = QuantizationModifier(
targets=[
"re:.*mlp.gate_proj$",
"re:.*mlp.up_proj$",
"re:.*mlp.down_proj$",
],
scheme="NVFP4",
)
print("✨ Running oneshot calibration & quantization to NVFP4 format...")
oneshot(
model=model,
dataset=ds,
recipe=recipe,
max_seq_length=MAX_SEQUENCE_LENGTH,
num_calibration_samples=NUM_CALIBRATION_SAMPLES,
data_collator=data_collator,
)
print(f"✨ Saving compressed model to {SAVE_DIR}...")
model.save_pretrained(SAVE_DIR, save_compressed=True)
tokenizer.save_pretrained(SAVE_DIR)
# Post-save patching to add regex ignores for vLLM compatibility.
# vLLM fuses attention layers into qkv_proj, which doesn't match the leaf-level
# ignores generated by llmcompressor. Add a regex to ignore all attention subtrees.
import json
config_path = os.path.join(SAVE_DIR, "config.json")
if os.path.exists(config_path):
with open(config_path, "r") as f:
config = json.load(f)
if "quantization_config" in config:
qc = config["quantization_config"]
ignore = qc.setdefault("ignore", [])
for pattern in ["re:.*self_attn.*"]:
if pattern not in ignore:
ignore.insert(0, pattern)
with open(config_path, "w") as f:
json.dump(config, f, indent=2)
print("✨ Successfully patched quantization_config.ignore for vLLM compatibility!")
print("🎉 Done! Model successfully quantized to NVFP4.")