File size: 8,199 Bytes
64f6172
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import argparse
import copy
import hashlib
import importlib.metadata
import json
import random
import shutil
import time
from pathlib import Path

import modelopt.torch.quantization as mtq
import pyarrow.parquet as parquet
import torch
from huggingface_hub import HfApi, hf_hub_download, save_torch_state_dict
from transformers import AutoConfig, AutoModelForCausalLM, AutoTokenizer
from packed_int4 import PackedLinear, pack_weight

parser = argparse.ArgumentParser()
parser.add_argument('--method', choices=['awq', 'gptq'], required=True)
parser.add_argument('--source', required=True)
parser.add_argument('--output', required=True)
parser.add_argument('--samples', type=int, default=128)
args = parser.parse_args()
source, output = Path(args.source), Path(args.output)
output.mkdir(parents=True, exist_ok=False)
torch.set_num_threads(4)
torch.manual_seed(20261020)
tokenizer = AutoTokenizer.from_pretrained(source, trust_remote_code=True, local_files_only=True)
tokenizer.chat_template = (Path(__file__).parent / 'chat_template.jinja').read_text()
dataset = 'Salesforce/wikitext'
revision = HfApi(token=False).dataset_info(dataset).sha
data_path = hf_hub_download(dataset, 'wikitext-2-raw-v1/train-00000-of-00001.parquet',
                           repo_type='dataset', revision=revision)
text = '\n\n'.join(parquet.read_table(data_path, columns=['text'])['text'].to_pylist())
ids = tokenizer(text, return_tensors='pt', add_special_tokens=False).input_ids
rng = random.Random(20261020)
starts = [rng.randrange(ids.shape[1] - 512) for _ in range(args.samples)]
batches = [ids[:, start:start + 512].clone() for start in starts]
calibration = {'dataset': dataset, 'revision': revision, 'split': 'train', 'samples': args.samples,
               'sequence_length': 512, 'seed': 20261020, 'token_offsets': starts,
               'token_ids_sha256': hashlib.sha256(torch.cat(batches).numpy().tobytes()).hexdigest()}
(output / 'calibration.json').write_text(json.dumps(calibration, indent=2) + '\n')
config = AutoConfig.from_pretrained(source, trust_remote_code=True, local_files_only=True)
config.backend = 'torch'
config.prefill_chunk_size = 512
model = AutoModelForCausalLM.from_pretrained(
    source, config=config, trust_remote_code=True, local_files_only=True,
    dtype=torch.bfloat16, device_map={'': 0}).eval().requires_grad_(False)
model.generation_config.eos_token_id = [248046, 248044]
model.generation_config.pad_token_id = 248044
modules = {name: module for name, module in model.named_modules()
           if isinstance(module, torch.nn.Linear) and name != 'lm_head'
           and min(module.weight.shape) >= 128 and '.admission_' not in name}
assert len(modules) == 256
recipe = copy.deepcopy(mtq.INT4_AWQ_CFG)
for name, module in model.named_modules():
    if isinstance(module, torch.nn.Linear) and name not in modules:
        recipe['quant_cfg'].append({'quantizer_name': name + '.*', 'enable': False})
if args.method == 'gptq':
    recipe['algorithm'] = {'method': 'gptq', 'block_size': 128, 'perc_damp': 0.01}

def forward_loop(current):
    for index, batch in enumerate(batches):
        current(input_ids=batch.to('cuda'), use_cache=False, logits_to_keep=1)
        if index % 32 == 0:
            print(json.dumps({'stage': 'calibration', 'batch': index, 'samples': args.samples}), flush=True)

started = time.perf_counter()
print(json.dumps({'stage': 'quantizing', 'method': args.method, 'modules': len(modules)}), flush=True)
with torch.inference_mode():
    mtq.quantize(model, recipe, forward_loop=forward_loop)
    manifest = {'method': args.method, 'bits': 4, 'group_size': 128,
                'activation_dtype': 'bfloat16', 'scale_dtype': 'float32',
                'zero_point': 8, 'awq_input_scaling': args.method == 'awq', 'modules': {}}
    state = {}
    maximum_weight_error = 0.0
    maximum_kernel_relative_error = 0.0
    for index, (name, module) in enumerate(modules.items()):
        weight = module.weight_quantizer(module.weight).to(torch.bfloat16)
        rows, columns = weight.shape
        scale = (module.weight_quantizer.amax.float().reshape(rows, columns // 128) / 7).clamp_min(1e-12)
        codes = (weight.float().reshape(rows, columns // 128, 128) / scale[..., None]).round().clamp(-8, 7).reshape(rows, columns).to(torch.int32) + 8
        input_scale = getattr(module.input_quantizer, '_pre_quant_scale', None)
        layer = PackedLinear(columns, rows, args.method == 'awq', input_scale is not None, module.bias is not None).to('cuda')
        layer.qweight = pack_weight(codes, args.method == 'awq')
        layer.scales = scale.t().contiguous()
        layer.qzeros.fill_(-2004318072 if args.method == 'awq' else 2004318071)
        if layer.g_idx is not None:
            layer.g_idx = torch.arange(columns, device='cuda', dtype=torch.int32) // 128
        if input_scale is not None:
            layer.input_scale = input_scale.to(torch.bfloat16).reshape(-1).contiguous()
        if module.bias is not None:
            layer.bias = module.bias.detach()
        dense = layer.dequantize()
        error = (dense.float() - weight.float()).abs().max().item()
        maximum_weight_error = max(maximum_weight_error, error)
        inputs = torch.randn((3, columns), device='cuda', dtype=torch.bfloat16)
        expected_inputs = inputs if layer.input_scale is None else inputs * layer.input_scale
        expected = torch.nn.functional.linear(expected_inputs, dense, layer.bias)
        actual = layer(inputs)
        relative = (expected.float() - actual.float()).abs().max().item() / max(expected.float().abs().max().item(), 1e-12)
        assert relative < 0.025, (name, relative)
        maximum_kernel_relative_error = max(maximum_kernel_relative_error, relative)
        manifest['modules'][name] = {'shape': [rows, columns], 'input_scale': input_scale is not None, 'bias': module.bias is not None}
        state.update({name + '.' + field: value.detach().cpu().contiguous() for field, value in layer.state_dict().items()})
        if index % 32 == 0:
            print(json.dumps({'stage': 'packing', 'module': index, 'weight_error': error, 'kernel_relative_error': relative}), flush=True)
    for name, tensor in model.state_dict().items():
        if name == 'lm_head.weight' or 'quantizer' in name or name.removesuffix('.weight') in modules:
            continue
        state[name] = tensor.detach().cpu().contiguous()
save_torch_state_dict(state, output, max_shard_size='3GB', safe_serialization=True)
model.config.quantization_config = {
    'quant_method': args.method, 'bits': 4, 'group_size': 128,
    **({'version': 'gemm', 'zero_point': True} if args.method == 'awq'
       else {'checkpoint_format': 'gptq', 'desc_act': False, 'sym': True, 'damp_percent': 0.01}),
    'edgerock_runtime': 'packed_int4.load_int4',
}
model.config.save_pretrained(output)
model.generation_config.save_pretrained(output)
tokenizer.save_pretrained(output)
for name in ('configuration_edgerock.py', 'modeling_edgerock.py', 'LICENSE', 'LICENSE-CODE', 'NOTICE'):
    shutil.copy2(source / name, output / name)
for name in ('packed_int4.py', 'quantize.py', 'validate.py', 'chat_template.jinja'):
    shutil.copy2(Path(__file__).parent / name, output / name)
(output / 'edgerock_quantization.json').write_text(json.dumps(manifest, indent=2) + '\n')
versions = {name: importlib.metadata.version(name) for name in
            ('torch', 'transformers', 'nvidia-modelopt', 'triton', 'accelerate', 'safetensors', 'huggingface-hub')}
conversion = {'method': args.method, 'recipe': recipe, 'quantized_modules': len(modules),
              'maximum_weight_repacking_error': maximum_weight_error,
              'maximum_kernel_relative_error': maximum_kernel_relative_error,
              'weight_bytes': sum(path.stat().st_size for path in output.glob('model*.safetensors')),
              'seconds': time.perf_counter() - started, 'versions': versions,
              'hardware': torch.cuda.get_device_name(0)}
(output / 'conversion.json').write_text(json.dumps(conversion, indent=2) + '\n')
print(json.dumps({'stage': 'conversion_complete', 'weight_bytes': conversion['weight_bytes'],
                  'seconds': conversion['seconds']}), flush=True)