File size: 6,951 Bytes
8899636
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
#!/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()