Opera-NVFP4 / quantize_opera_nvfp4.py
beyoru's picture
NVFP4 (W4A4) quant of beyoru/Opera — recipe from unsloth/Qwen3.6-27B-NVFP4
8899636 verified
Raw
History Blame Contribute Delete
6.95 kB
#!/usr/bin/env python3
"""Lượng tử hoá beyoru/Opera sang NVFP4 theo ĐÚNG recipe của unsloth/Qwen3.6-27B-NVFP4.
Recipe đọc ngược từ `quantization_config` của unsloth (compressed-tensors,
format=mixed-precision, version 0.17.2.a20260707):
group_0 — FP8 W8A8
W: 8b float, symmetric, strategy=channel, dynamic=False
A: 8b float, symmetric, strategy=token, dynamic=True
targets: self_attn.(q|k|v|o)_proj
linear_attn.(in_proj_qkv|in_proj_z|out_proj)
lm_head
layers.(56..63).mlp.(gate|up|down)_proj <-- 8 LỚP CUỐI giữ FP8
group_1 — NVFP4 W4A4
W: 4b float, gs=16, symmetric, strategy=tensor_group, dynamic=False
A: 4b float, gs=16, symmetric, strategy=tensor_group, dynamic=local
targets: mlp.(gate|up|down)_proj
ignore: toàn bộ model.visual.* (vision tower giữ bf16)
Ba mẹo giữ chất lượng, đều suy ra từ config chứ không phải đoán:
1. 8 lớp cuối MLP giữ FP8 — lớp cuối nhạy nhất với lượng tử hoá.
2. Attention + GDN projection giữ FP8; chỉ MLP thân giữa xuống W4A4.
3. Vision tower nguyên bf16.
⚠️ QUAN TRỌNG — recipe này DATA-FREE: cả hai group đều có activation dynamic
(`token` / `local`), và scale của weight suy ra từ chính weight. Nên KHÔNG cần
dataset hiệu chuẩn. Đừng thêm calibration vào, sẽ lệch khỏi bản của unsloth.
Opera cùng kiến trúc với Qwen3.6-27B (64 lớp, hidden 5120, 24 head, interval 4)
nên các regex chuyển sang dùng được nguyên, KHÔNG cần sửa chỉ số lớp.
Chạy:
pip install llmcompressor compressed-tensors
python3 quantize_opera_nvfp4.py \
--model beyoru/Opera \
--output ./Opera-NVFP4
"""
import argparse
import re
# 8 lớp cuối giữ FP8. Opera có 64 lớp (0..63) => 56..63, y hệt unsloth.
LAST_LAYERS_FP8 = list(range(56, 64))
FP8_TARGETS = [
r"re:.*self_attn\.(q|k|v|o)_proj$",
r"re:.*linear_attn\.(in_proj_qkv|in_proj_z|out_proj)$",
r"re:.*lm_head",
r"re:.*layers\.(%s)\.mlp\.(gate|up|down)_proj$"
% "|".join(str(i) for i in LAST_LAYERS_FP8),
]
NVFP4_TARGETS = [r"re:.*mlp\.(gate|up|down)_proj$"]
def build_recipe():
"""Recipe llmcompressor khớp từng trường với config của unsloth."""
from llmcompressor.modifiers.quantization import QuantizationModifier
return QuantizationModifier(
config_groups={
# ---- group_0: FP8 W8A8 -------------------------------------------
"group_0": {
"targets": FP8_TARGETS,
"weights": {
"num_bits": 8,
"type": "float",
"symmetric": True,
"strategy": "channel",
"dynamic": False,
},
"input_activations": {
"num_bits": 8,
"type": "float",
"symmetric": True,
"strategy": "token",
"dynamic": True,
},
},
# ---- group_1: NVFP4 W4A4 -----------------------------------------
"group_1": {
"targets": NVFP4_TARGETS,
"weights": {
"num_bits": 4,
"type": "float",
"group_size": 16,
"symmetric": True,
"strategy": "tensor_group",
"dynamic": False,
},
"input_activations": {
"num_bits": 4,
"type": "float",
"group_size": 16,
"symmetric": True,
"strategy": "tensor_group",
"dynamic": "local",
},
},
},
# Vision tower giữ bf16. Dùng regex thay vì liệt kê 303 tên như unsloth --
# tương đương về hiệu lực nhưng không phụ thuộc số block của vision.
# Lưu ý: KHÔNG đưa lm_head vào đây -- nó thuộc group_0 (FP8) trong recipe gốc.
ignore=["re:.*visual\\..*"],
)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--model", default="beyoru/Opera")
ap.add_argument("--output", default="./Opera-NVFP4")
ap.add_argument(
"--dry-run",
action="store_true",
help="chi in recipe + doi chieu voi unsloth, khong nap model",
)
args = ap.parse_args()
print("=== recipe se ap dung ===")
print(" group_0 (FP8 W8A8) targets:")
for t in FP8_TARGETS:
print(" ", t)
print(" group_1 (NVFP4 W4A4) targets:")
for t in NVFP4_TARGETS:
print(" ", t)
print(" ignore: re:.*visual\\..* (vision tower giu bf16)")
print(" 8 lop cuoi giu FP8:", LAST_LAYERS_FP8)
print(" KHONG dung calibration dataset (recipe data-free)")
if args.dry_run:
print("\n[dry-run] dung o day.")
return
from llmcompressor import oneshot
from transformers import AutoConfig, AutoModelForCausalLM, AutoTokenizer
# Opera là `Qwen3_5ForConditionalGeneration` (có vision tower 333 tensor), KHÔNG phải
# CausalLM thuần. Dùng AutoModelForCausalLM sẽ nạp thiếu/sai phần visual. Chọn class
# theo `architectures` trong config thay vì đoán.
cfg = AutoConfig.from_pretrained(args.model)
arch = (getattr(cfg, "architectures", None) or [""])[0]
print(f"\n=== nap {args.model} (bf16, ~55 GB) ===")
print(f" architectures: {arch}")
if "ConditionalGeneration" in arch or "ImageTextToText" in arch:
from transformers import AutoModelForImageTextToText
loader = AutoModelForImageTextToText
print(" -> dung AutoModelForImageTextToText (model da mode)")
else:
loader = AutoModelForCausalLM
print(" -> dung AutoModelForCausalLM")
model = loader.from_pretrained(args.model, dtype="auto", device_map="auto")
tokenizer = AutoTokenizer.from_pretrained(args.model)
# ⚠️ PHẢI truyền pipeline="datafree". Mặc định là "independent", nó dispatch sang
# "sequential" — pipeline đó cần dataloader để trace model qua từng lớp, không có
# dataset thì vỡ ngay: `TypeError: 'NoneType' object is not iterable` tại
# pipelines/sequential/pipeline.py:103 (`next(iter(dataloader))`).
# Recipe này data-free thật (activation dynamic, scale weight suy từ chính weight)
# nhưng llmcompressor không tự suy ra được — phải nói thẳng.
print("=== oneshot quantize (pipeline=datafree) ===")
oneshot(model=model, recipe=build_recipe(), pipeline="datafree")
print(f"=== luu {args.output} ===")
model.save_pretrained(args.output, save_compressed=True)
tokenizer.save_pretrained(args.output)
print("XONG")
if __name__ == "__main__":
main()