Download tools/calibrate_w4.py from Sariel00/Ling-3.0-tiny-RKNN: direct link, hf CLI and curl.
- Browser
- Download file 9.1 kB
-
https://huggingface.co/Sariel00/Ling-3.0-tiny-RKNN/resolve/main/tools/calibrate_w4.py
- Command line
-
hf download hf://Sariel00/Ling-3.0-tiny-RKNN/tools/calibrate_w4.py
-
curl -L -o calibrate_w4.py https://huggingface.co/Sariel00/Ling-3.0-tiny-RKNN/resolve/main/tools/calibrate_w4.py
9.1 kB
| #!/usr/bin/env python3 | |
| """Activation-diagonal weighted W4 scale search; unchanged per-row INT4 layout. | |
| Calibration and held-out fixtures must be different. This is not GPTQ/AWQ: | |
| no full Hessian compensation, channel rescaling, or optimizer training is used. | |
| """ | |
| import argparse | |
| import hashlib | |
| import json | |
| import time | |
| from pathlib import Path | |
| import torch | |
| from safetensors.torch import save_file | |
| from transformers import AutoTokenizer | |
| from quant_loss_reference import load_model, evaluate | |
| def safe_rows(codes, splits): | |
| """Exact worst-case signed INT4 products, matching SplitAligned(K, splits, 32).""" | |
| blocks = codes.shape[1] // 32 | |
| if codes.shape[1] % 32 or not 1 <= splits <= blocks: | |
| raise ValueError('invalid aligned K partition') | |
| size, extra = divmod(blocks, splits) | |
| begin = 0 | |
| safe = torch.ones(codes.shape[0], dtype=torch.bool, device=codes.device) | |
| for part in range(splits): | |
| end = begin + (size + (part < extra)) * 32 | |
| q = codes[:,begin:end].float() | |
| positive = q.clamp_min(0).sum(1) | |
| negative = (-q).clamp_min(0).sum(1) | |
| safe &= (7*positive+8*negative <= 32767) & (8*positive+7*negative <= 32768) | |
| begin = end | |
| return safe | |
| def choose_scales(weight, diagonal, rows=256, k_splits=None): | |
| """Search clipping and least-squares scales; retain baseline if no improvement.""" | |
| selected = [] | |
| before, after = 0.0, 0.0 | |
| for offset in range(0, weight.shape[0], rows): | |
| w = weight[offset:offset+rows].float() | |
| base = torch.maximum(w.amax(1).clamp_min(0)/7, (-w.amin(1)).clamp_min(0)/8) | |
| base = torch.where(base > 0, base, torch.ones_like(base)) | |
| def error(scale): | |
| q = torch.round(w/scale[:, None]).clamp(-8, 7) | |
| return ((w-q*scale[:, None]).square()*diagonal).sum(1), q | |
| best_error, baseline_codes = error(base) | |
| if k_splits is not None and not safe_rows(baseline_codes,k_splits).all(): | |
| raise ValueError('baseline scale is unsafe for required K partitions') | |
| before += best_error.double().sum().item() | |
| best = base.clone() | |
| # Include the old solution exactly. Search never worsens this local objective. | |
| for fraction in (1.0, .95, .9, .85, .8, .75, .7, .65, .6, .55, .5): | |
| scale = base * fraction | |
| for iteration in range(3): | |
| loss, q = error(scale) | |
| improve = loss < best_error | |
| if k_splits is not None: | |
| improve &= safe_rows(q,k_splits) | |
| best = torch.where(improve, scale, best) | |
| best_error = torch.where(improve, loss, best_error) | |
| if iteration < 2: | |
| scale = ((w*q*diagonal).sum(1) / | |
| (q.square()*diagonal).sum(1).clamp_min(1e-30)).clamp_min(1e-20) | |
| after += best_error.double().sum().item() | |
| selected.append(best) | |
| return torch.cat(selected), before, after | |
| def main(): | |
| p = argparse.ArgumentParser(description=__doc__) | |
| p.add_argument('--source', type=Path, required=True) | |
| p.add_argument('--corpus', type=Path, required=True) | |
| p.add_argument('--suite', type=Path, required=True) | |
| p.add_argument('--output', type=Path, required=True) | |
| p.add_argument('--max-tokens', type=int, default=512) | |
| p.add_argument('--reuse-activations', action='store_true') | |
| p.add_argument('--baseline-manifest', type=Path, help='Preserve baseline INT16-safe K partitions') | |
| a = p.parse_args() | |
| torch.set_num_threads(4) | |
| torch.backends.cuda.matmul.allow_tf32 = False | |
| a.output.mkdir(parents=True, exist_ok=True) | |
| corpus = json.loads(a.corpus.read_text()) | |
| suite = json.loads(a.suite.read_text()) | |
| # Disallow accidental reuse of held-out questions or answers in calibration. | |
| for c in corpus: | |
| for e in suite['cases']: | |
| assert c['prompt'] not in e['prompt_text'] and c['answer'] not in e['target_text'] | |
| model = load_model(a.source) | |
| modules = {name: m for name, m in model.named_modules() if isinstance(m, torch.nn.Linear)} | |
| partitions = {} | |
| if a.baseline_manifest: | |
| from quantize_model import source_linear_names | |
| for item in json.loads(a.baseline_manifest.read_text())['tensors']: | |
| if item['role'] == 'linear_weight': | |
| for n in source_linear_names(item['name'].removesuffix('.weight')): | |
| partitions[n.removesuffix('.weight')] = item['flags'] | |
| assert set(partitions) == set(modules) | |
| stats_path = a.output/'activations.pt' | |
| if a.reuse_activations: | |
| stats = torch.load(stats_path, map_location='cuda', weights_only=True) | |
| assert stats['corpus_sha256'] == hashlib.sha256(a.corpus.read_bytes()).hexdigest() | |
| else: | |
| sums, counts = {}, {} | |
| def hook(name): | |
| def accumulate(module, inputs): | |
| x = inputs[0].detach().reshape(-1, inputs[0].shape[-1]).float() | |
| if not x.shape[0]: | |
| return | |
| value = x.square().sum(0) | |
| if name in sums: | |
| sums[name].add_(value) | |
| else: | |
| sums[name] = value | |
| counts[name] = counts.get(name, 0) + x.shape[0] | |
| return accumulate | |
| hooks = [m.register_forward_pre_hook(hook(n)) for n,m in modules.items()] | |
| tokenizer = AutoTokenizer.from_pretrained(a.source, trust_remote_code=True, local_files_only=True) | |
| lengths = [] | |
| with torch.inference_mode(): | |
| for i, c in enumerate(corpus): | |
| for thinking in (False, True): | |
| prompt = tokenizer.apply_chat_template([{'role':'user','content':c['prompt']}], | |
| tokenize=False, add_generation_prompt=True, enable_thinking=thinking) | |
| target = (c['reasoning']+'</think>' if thinking else '') + c['answer'] + tokenizer.eos_token | |
| ids = tokenizer.encode(prompt+target, add_special_tokens=False)[:a.max_tokens] | |
| output = model(input_ids=torch.tensor([ids],device='cuda'), use_cache=False) | |
| del output | |
| lengths.append(len(ids)) | |
| print(json.dumps({'calibration_cases':i+1, 'total_tokens':sum(lengths), | |
| 'matrices_observed':len(sums)}), flush=True) | |
| for h in hooks: | |
| h.remove() | |
| stats = {'sum_squares':{k:v.cpu() for k,v in sums.items()},'counts':counts, | |
| 'lengths':lengths, 'corpus_sha256':hashlib.sha256(a.corpus.read_bytes()).hexdigest()} | |
| torch.save(stats, stats_path) | |
| del sums | |
| scales, records = {}, [] | |
| start = time.monotonic() | |
| with torch.inference_mode(): | |
| for i,(name,m) in enumerate(modules.items()): | |
| count = stats['counts'].get(name,0) | |
| diag = stats['sum_squares'].get(name) | |
| if diag is None or count == 0: | |
| # Unobserved experts retain baseline weights, not a guessed calibration. | |
| w = m.weight.float() | |
| scale = torch.maximum(w.amax(1).clamp_min(0)/7,(-w.amin(1)).clamp_min(0)/8) | |
| scale = torch.where(scale>0,scale,torch.ones_like(scale)) | |
| old = new = None | |
| else: | |
| diag = diag.to('cuda') / count | |
| mean = diag.mean().clamp_min(1e-20) | |
| # Regularize rarely visited experts more strongly towards uniform weights. | |
| strength = .05 + .95 * max(0., 1.-count/64.) | |
| diag = (1-strength)*diag/mean + strength | |
| scale, old, new = choose_scales(m.weight,diag,k_splits=partitions.get(name)) | |
| scales[name+'.weight'] = scale.cpu().contiguous() | |
| records.append({'name':name,'observed_tokens':count,'weighted_error_before':old, | |
| 'weighted_error_after':new}) | |
| for row in range(0,m.weight.shape[0],1024): | |
| w = m.weight[row:row+1024].float() | |
| s = scale[row:row+1024,None] | |
| m.weight[row:row+1024].copy_((torch.round(w/s).clamp(-8,7)*s).to(m.weight.dtype)) | |
| if (i+1)%200 == 0: | |
| print(json.dumps({'calibrated_matrices':i+1,'seconds':time.monotonic()-start}),flush=True) | |
| save_file(scales,str(a.output/'scales.safetensors')) | |
| report = {'method':'activation_diagonal_weighted_scale_search_v1','corpus_sha256':stats['corpus_sha256'], | |
| 'calibration_sequences':len(stats['lengths']),'calibration_tokens':sum(stats['lengths']), | |
| 'matrices':records,'seconds':time.monotonic()-start,'max_tokens':a.max_tokens, | |
| 'baseline_manifest_sha256':hashlib.sha256(a.baseline_manifest.read_bytes()).hexdigest() if a.baseline_manifest else None, | |
| 'preserve_baseline_k_partitions':bool(a.baseline_manifest)} | |
| (a.output/'calibration.json').write_text(json.dumps(report,indent=2)+'\n') | |
| evaluate(model,suite['cases'],a.output,'calibrated_bf16') | |
| print('PASS: calibrated scales and held-out logits exported',flush=True) | |
| if __name__ == '__main__': | |
| main() | |