144 lines
5.4 KiB
Python
144 lines
5.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Unified PPL evaluation: FP16, W8A16, Cider per-channel, per-group gs=64, per-group gs=128."""
|
|
import argparse, math, sys, time, gc, os
|
|
import mlx.core as mx
|
|
import mlx.nn as nn
|
|
import numpy as np
|
|
import tqdm
|
|
from mlx_lm.utils import get_total_parameters, load
|
|
from cider.nn import CiderLinear
|
|
|
|
home_dir = os.path.expanduser("~")
|
|
WIKITEXT_LOCAL = os.path.join(home_dir, "Downloads/wikitext/wikitext-2-raw-v1")
|
|
|
|
FP16_MODEL = os.path.join(home_dir, "Downloads/Meta-Llama-3-8B")
|
|
W8A16_MODEL = os.path.join(home_dir, "Downloads/Llama3-8B-GPTQ-W8A16-mlx")
|
|
|
|
def load_wikitext_local(tokenizer, split="test"):
|
|
from datasets import load_dataset
|
|
split_map = {
|
|
"test": f"{WIKITEXT_LOCAL}/test-00000-of-00001.parquet",
|
|
}
|
|
data = load_dataset("parquet", data_files={split: split_map[split]}, split=split)
|
|
encodings = tokenizer.encode("\n\n".join(data["text"]), return_tensors="pt")
|
|
return encodings.numpy()
|
|
|
|
def convert_model_with_gs(model, target_group_size):
|
|
counter = [0]
|
|
_TARGET = (nn.Linear, nn.QuantizedLinear)
|
|
def _walk(module):
|
|
for name, child in module.children().items():
|
|
if isinstance(child, _TARGET):
|
|
setattr(module, name, CiderLinear.from_float(child, target_group_size=target_group_size))
|
|
counter[0] += 1
|
|
if counter[0] % 28 == 0: gc.collect()
|
|
elif isinstance(child, list):
|
|
for i, item in enumerate(child):
|
|
if isinstance(item, _TARGET):
|
|
child[i] = CiderLinear.from_float(item, target_group_size=target_group_size)
|
|
counter[0] += 1
|
|
if counter[0] % 28 == 0: gc.collect()
|
|
elif isinstance(item, nn.Module):
|
|
_walk(item)
|
|
elif isinstance(child, nn.Module):
|
|
_walk(child)
|
|
_walk(model)
|
|
return counter[0]
|
|
|
|
def eval_ppl(model, data, n_samples, seq_length):
|
|
all_losses = []
|
|
total_chunks = data.shape[1] // seq_length
|
|
n = n_samples if n_samples > 0 else total_chunks
|
|
for i in tqdm.tqdm(range(n), desc="Evaluating PPL"):
|
|
batch = mx.array(data[:, i * seq_length: (i + 1) * seq_length])
|
|
logits = model(batch[:, :-1]).astype(mx.float32)
|
|
losses = nn.losses.cross_entropy(logits, batch[:, 1:], reduction="none")
|
|
mx.eval(losses)
|
|
all_losses.append(losses.flatten())
|
|
all_losses = mx.concatenate(all_losses)
|
|
mean_loss = all_losses.mean().item()
|
|
ppl = math.exp(mean_loss)
|
|
std_dev = mx.sqrt(mx.var(all_losses, ddof=1)).item()
|
|
se_ppl = ppl * (std_dev / math.sqrt(all_losses.size))
|
|
return ppl, se_ppl
|
|
|
|
def run_config(config_name, model_path, cider_gs=None, data=None, n_samples=50, seq_length=2048):
|
|
"""Run a single config and return results dict."""
|
|
print(f"\n{'='*60}")
|
|
print(f" {config_name}")
|
|
print(f"{'='*60}")
|
|
|
|
print(f" Loading {model_path}...")
|
|
model, tokenizer = load(model_path, tokenizer_config={"trust_remote_code": True})
|
|
|
|
if cider_gs is not None:
|
|
gs_str = "per-channel" if cider_gs == 0 else f"per-group(gs={cider_gs})"
|
|
print(f" Converting to CiderLinear ({gs_str})...")
|
|
t0 = time.perf_counter()
|
|
n = convert_model_with_gs(model, cider_gs)
|
|
print(f" {n} layers converted in {time.perf_counter()-t0:.1f}s")
|
|
|
|
# Warmup
|
|
dummy = mx.array([[1, 2, 3, 4, 5, 6, 7, 8]])
|
|
_ = model(dummy); mx.eval(_)
|
|
|
|
if data is None:
|
|
data = load_wikitext_local(tokenizer, "test")
|
|
|
|
mx.reset_peak_memory()
|
|
start = time.time()
|
|
ppl, se = eval_ppl(model, data, n_samples, seq_length)
|
|
elapsed = time.time() - start
|
|
peak_mem = mx.get_peak_memory() / 1e9
|
|
|
|
result = {"name": config_name, "ppl": ppl, "se": se, "time": elapsed, "mem": peak_mem}
|
|
print(f" PPL = {ppl:.3f} ± {se:.3f} | {elapsed:.1f}s | {peak_mem:.2f} GB")
|
|
|
|
# Free memory
|
|
del model
|
|
gc.collect()
|
|
mx.metal.clear_cache()
|
|
|
|
return result, data
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--num-samples", type=int, default=-1)
|
|
parser.add_argument("--sequence-length", type=int, default=2048)
|
|
args = parser.parse_args()
|
|
|
|
np.random.seed(123)
|
|
mx.random.seed(123)
|
|
|
|
configs = [
|
|
# ("Baseline FP16", FP16_MODEL, None),
|
|
# ("Baseline W8A16 (MLX native)", W8A16_MODEL, None),
|
|
# ("Cider W8A8 per-channel", W8A16_MODEL, 0),
|
|
("Cider W8A8 per-group(gs=64)", W8A16_MODEL, 64),
|
|
("Cider W8A8 per-group(gs=128)", W8A16_MODEL, 128),
|
|
]
|
|
|
|
results = []
|
|
data = None
|
|
|
|
for name, model_path, cider_gs in configs:
|
|
np.random.seed(123)
|
|
mx.random.seed(123)
|
|
r, data = run_config(name, model_path, cider_gs, data, args.num_samples, args.sequence_length)
|
|
results.append(r)
|
|
|
|
# Summary table
|
|
print(f"\n\n{'='*70}")
|
|
print(f" PPL COMPARISON SUMMARY (Qwen3-8B, wikitext-2, seq={args.sequence_length}, n={args.num_samples})")
|
|
print(f"{'='*70}")
|
|
print(f" {'Config':<30} {'PPL':>10} {'±SE':>8} {'Time':>8} {'Mem':>8}")
|
|
print(f" {'-'*30} {'-'*10} {'-'*8} {'-'*8} {'-'*8}")
|
|
|
|
baseline_ppl = results[0]["ppl"] if results else None
|
|
for r in results:
|
|
delta = f"(+{(r['ppl']-baseline_ppl)/baseline_ppl*100:.2f}%)" if baseline_ppl and r['ppl'] != baseline_ppl else ""
|
|
print(f" {r['name']:<30} {r['ppl']:>8.3f} ±{r['se']:.3f} {r['time']:>6.1f}s {r['mem']:>6.2f}GB {delta}")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|