3939
4040import argparse
4141import json
42+ import math
4243import os
4344import sys
4445import time
@@ -228,6 +229,55 @@ def gptq_quantize_linear(layer: nn.Linear, H: torch.Tensor,
228229 return export
229230
230231
232+ # ───────────────────────────── PPL eval ─────────────────────────────────────
233+
234+ @torch .no_grad ()
235+ def eval_ppl_wikitext (model , tokenizer , dev : str , seq_len : int = 2048 ,
236+ stride : int = 1024 , max_tokens : int | None = None ) -> dict :
237+ """Compute wikitext-2 test-split PPL via sliding-window evaluation.
238+
239+ Standard pattern (matches HF eval + llama.cpp --perplexity):
240+ 1. Concatenate all test text, tokenize once
241+ 2. Slide a window of seq_len with overlap (stride)
242+ 3. Compute next-token CE loss on the non-overlapping tail
243+ 4. PPL = exp(mean(loss))
244+ """
245+ from datasets import load_dataset
246+ ds = load_dataset ("Salesforce/wikitext" , "wikitext-2-raw-v1" , split = "test" )
247+ text = "\n \n " .join (r ["text" ] for r in ds if r ["text" ].strip ())
248+ enc = tokenizer (text , return_tensors = "pt" )
249+ ids = enc .input_ids .to (dev )
250+ n_tokens = ids .shape [1 ]
251+ if max_tokens is not None :
252+ n_tokens = min (n_tokens , max_tokens )
253+ ids = ids [:, :n_tokens ]
254+ print (f" [ppl] { n_tokens } tokens, window={ seq_len } stride={ stride } " )
255+
256+ nll_sum = 0.0
257+ n_pred = 0
258+ prev_end = 0
259+ for begin in range (0 , n_tokens , stride ):
260+ end = min (begin + seq_len , n_tokens )
261+ target_len = end - prev_end
262+ input_ids = ids [:, begin :end ]
263+ target_ids = input_ids .clone ()
264+ # Mask out the overlap tokens (already scored)
265+ target_ids [:, :- target_len ] = - 100
266+ outputs = model (input_ids , labels = target_ids )
267+ # outputs.loss is mean over unmasked tokens
268+ # Re-compute: nll_sum gets total CE, normalize at end
269+ n_unmasked = (target_ids != - 100 ).sum ().item ()
270+ nll_sum += outputs .loss .item () * n_unmasked
271+ n_pred += n_unmasked
272+ prev_end = end
273+ if end >= n_tokens :
274+ break
275+
276+ avg_nll = nll_sum / max (n_pred , 1 )
277+ ppl = math .exp (avg_nll )
278+ return {"ppl" : ppl , "avg_nll" : avg_nll , "n_tokens_scored" : n_pred }
279+
280+
231281# ───────────────────────────── Driver ───────────────────────────────────────
232282
233283def main ():
@@ -253,6 +303,11 @@ def main():
253303 p .add_argument ("--dtype" , choices = ("float16" , "bfloat16" ), default = "float16" )
254304 p .add_argument ("--max-layers" , type = int , default = None ,
255305 help = "Limit to first N transformer layers (fast iteration)" )
306+ p .add_argument ("--eval-ppl" , action = "store_true" ,
307+ help = "Compute wikitext-2 test-split PPL before and after "
308+ "quantization. Reports Δ_PPL vs f16 baseline." )
309+ p .add_argument ("--ppl-max-tokens" , type = int , default = None ,
310+ help = "Cap PPL eval tokens (default: full test split ~280k)." )
256311 args = p .parse_args ()
257312
258313 os .makedirs (args .output_dir , exist_ok = True )
@@ -276,6 +331,15 @@ def main():
276331
277332 manifest = {"model" : args .model , "args" : vars (args ), "results" : []}
278333
334+ # Baseline PPL on the f16 model BEFORE any quantization
335+ if args .eval_ppl :
336+ print (f"\n [ppl-baseline] computing f16 baseline PPL..." )
337+ baseline = eval_ppl_wikitext (model , tokenizer , args .device ,
338+ max_tokens = args .ppl_max_tokens )
339+ print (f" baseline PPL = { baseline ['ppl' ]:.4f} "
340+ f"(n_tokens={ baseline ['n_tokens_scored' ]} )" )
341+ manifest ["ppl_baseline" ] = baseline
342+
279343 for i , (name , layer ) in enumerate (targets ):
280344 t0 = time .time ()
281345 rows , in_feat = layer .weight .shape
@@ -339,6 +403,23 @@ def main():
339403 if args .device .startswith ("cuda" ):
340404 torch .cuda .empty_cache ()
341405
406+ # Post-quantization PPL on the in-place modified model
407+ if args .eval_ppl :
408+ print (f"\n [ppl-quant] computing quantized PPL..." )
409+ quantized = eval_ppl_wikitext (model , tokenizer , args .device ,
410+ max_tokens = args .ppl_max_tokens )
411+ print (f" quantized PPL = { quantized ['ppl' ]:.4f} " )
412+ manifest ["ppl_quantized" ] = quantized
413+ delta = quantized ["ppl" ] - manifest ["ppl_baseline" ]["ppl" ]
414+ manifest ["ppl_delta" ] = delta
415+ print (f"\n Δ_PPL = { delta :+.4f} "
416+ f"({ delta / manifest ['ppl_baseline' ]['ppl' ] * 100 :+.2f} %)" )
417+ # MAD-223 gate: <0.08 PPL → proceed without AWQ phase B.5
418+ if delta < 0.08 :
419+ print (f" ✓ Δ_PPL < 0.08 — MAD-223 gate PASSED (AWQ phase B.5 not needed)" )
420+ else :
421+ print (f" ⚠ Δ_PPL ≥ 0.08 — MAD-223 gate triggers AWQ phase B.5 consideration" )
422+
342423 manifest_path = Path (args .output_dir ) / "manifest.json"
343424 with open (manifest_path , "w" ) as f :
344425 json .dump (manifest , f , indent = 2 )
0 commit comments