| |
| """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 |
|
|
| |
| 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": { |
| "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": { |
| "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", |
| }, |
| }, |
| }, |
| |
| |
| |
| 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 |
|
|
| |
| |
| |
| 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) |
|
|
| |
| |
| |
| |
| |
| |
| 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() |
|
|