88from __future__ import annotations
99
1010import argparse
11+ import concurrent .futures
1112import math
13+ import multiprocessing
1214import sys
1315from pathlib import Path
14- from typing import Iterable , List , Tuple
16+ from typing import Iterable , List , Optional , Tuple
1517
1618import numpy as np
19+ from numpy .typing import DTypeLike
1720
1821sys .path .insert (0 , str (Path (__file__ ).resolve ().parent ))
1922from generate_sampled import ( # type: ignore[import-not-found]
@@ -200,45 +203,178 @@ def eval_prompt_nll_kv(
200203 return total_nll , len (token_ids ) - 1
201204
202205
203- def eval_model_ppl (
206+ def _split_prompt_shards (prompts : List [str ], workers : int ) -> List [List [str ]]:
207+ worker_count = min (workers , len (prompts ))
208+ shards : List [List [str ]] = [[] for _ in range (worker_count )]
209+ for index , prompt in enumerate (prompts ):
210+ shards [index % worker_count ].append (prompt )
211+ return shards
212+
213+
214+ def _eval_prompt_shard (
204215 * ,
205- runner : Path ,
206- pte : Path ,
207- tokenizer ,
216+ runner : str ,
217+ pte : str ,
218+ tokenizer_path : str ,
208219 prompts : List [str ],
209220 window : int ,
210221 pad_id : int ,
211222 max_tokens_per_prompt : int ,
212223 use_kv_cache : bool ,
213- ) -> float :
224+ input_dtype : np .dtype ,
225+ ) -> Tuple [float , int ]:
226+ tokenizer = get_tokenizer (tokenizer_path )
214227 total_nll = 0.0
215228 total_tokens = 0
216229
217230 if use_kv_cache :
218- with KvRunnerSession (
219- runner = str (runner ),
220- pte = str (pte ),
231+ for prompt in prompts :
232+ with KvRunnerSession (
233+ runner = runner ,
234+ pte = pte ,
235+ extra_args = [],
236+ persistent = True ,
237+ input_dtype = input_dtype ,
238+ ) as session :
239+ prompt_nll , prompt_tokens = eval_prompt_nll_kv (
240+ runner = session ,
241+ tokenizer = tokenizer ,
242+ prompt = prompt ,
243+ max_tokens_per_prompt = max_tokens_per_prompt ,
244+ )
245+ total_nll += prompt_nll
246+ total_tokens += prompt_tokens
247+ else :
248+ with RunnerSession (
249+ runner = runner ,
250+ pte = pte ,
221251 extra_args = [],
222252 persistent = True ,
223253 ) as session :
224- for idx , prompt in enumerate (prompts , start = 1 ):
225- print (f"[eval-kv] { pte .name } prompt { idx } /{ len (prompts )} " )
226- prompt_nll , prompt_tokens = eval_prompt_nll_kv (
254+ for prompt in prompts :
255+ prompt_nll , prompt_tokens = eval_prompt_nll (
227256 runner = session ,
228257 tokenizer = tokenizer ,
229258 prompt = prompt ,
259+ window = window ,
260+ pad_id = pad_id ,
230261 max_tokens_per_prompt = max_tokens_per_prompt ,
231262 )
232263 total_nll += prompt_nll
233264 total_tokens += prompt_tokens
234- if total_tokens > 0 :
235- cumulative_ppl = f"{ math .exp (total_nll / total_tokens ):.4f} "
236- else :
237- cumulative_ppl = "n/a"
265+
266+ return total_nll , total_tokens
267+
268+
269+ def eval_model_ppl (
270+ * ,
271+ runner : Path ,
272+ pte : Path ,
273+ tokenizer ,
274+ prompts : List [str ],
275+ window : int ,
276+ pad_id : int ,
277+ max_tokens_per_prompt : int ,
278+ use_kv_cache : bool ,
279+ workers : int = 1 ,
280+ tokenizer_path : Optional [Path ] = None ,
281+ input_dtype : DTypeLike = None ,
282+ ) -> float :
283+ """Evaluate corpus perplexity for one exported model.
284+
285+ Args:
286+ runner (Path): Path to the executor runner.
287+ pte (Path): Path to the exported model.
288+ tokenizer: Loaded tokenizer used for single-process evaluation.
289+ prompts (List[str]): Prompt strings to score.
290+ window (int): Fixed input length for a non-KV export.
291+ pad_id (int): Token used to pad a non-KV input window.
292+ max_tokens_per_prompt (int): Maximum tokens scored per prompt, or zero
293+ for no limit.
294+ use_kv_cache (bool): Whether the export uses token-by-token KV inputs.
295+ workers (int): Number of independent runner processes. Defaults to 1.
296+ tokenizer_path (Optional[Path]): Tokenizer loaded by spawned workers.
297+ input_dtype (DTypeLike): Token and position dtype for KV
298+ inputs. Defaults to int64.
299+
300+ Returns:
301+ float: Token-weighted perplexity across all prompts.
302+
303+ """
304+ if workers <= 0 :
305+ raise ValueError ("workers must be > 0" )
306+ if not prompts :
307+ raise RuntimeError ("No prompts were provided." )
308+
309+ input_dtype = np .dtype (input_dtype or np .int64 )
310+
311+ total_nll = 0.0
312+ total_tokens = 0
313+
314+ if workers > 1 :
315+ if tokenizer_path is None :
316+ raise ValueError ("tokenizer_path is required when workers > 1" )
317+
318+ shards = _split_prompt_shards (prompts , workers )
319+ process_context = multiprocessing .get_context ("spawn" )
320+ with concurrent .futures .ProcessPoolExecutor (
321+ max_workers = len (shards ), mp_context = process_context
322+ ) as executor :
323+ futures = [
324+ executor .submit (
325+ _eval_prompt_shard ,
326+ runner = str (runner ),
327+ pte = str (pte ),
328+ tokenizer_path = str (tokenizer_path ),
329+ prompts = shard ,
330+ window = window ,
331+ pad_id = pad_id ,
332+ max_tokens_per_prompt = max_tokens_per_prompt ,
333+ use_kv_cache = use_kv_cache ,
334+ input_dtype = input_dtype ,
335+ )
336+ for shard in shards
337+ ]
338+ for completed , future in enumerate (
339+ concurrent .futures .as_completed (futures ), start = 1
340+ ):
341+ shard_nll , shard_tokens = future .result ()
342+ total_nll += shard_nll
343+ total_tokens += shard_tokens
238344 print (
239- f"[eval-kv] { pte .name } prompt { idx } : "
240- f"tokens={ prompt_tokens } cumulative_ppl={ cumulative_ppl } "
345+ f"[eval] { pte .name } worker { completed } /{ len (shards )} "
346+ f"complete: tokens={ shard_tokens } "
347+ )
348+ if total_tokens == 0 :
349+ raise RuntimeError ("No prompt tokens were scored." )
350+ return math .exp (total_nll / total_tokens )
351+
352+ if use_kv_cache :
353+ for idx , prompt in enumerate (prompts , start = 1 ):
354+ print (f"[eval-kv] { pte .name } prompt { idx } /{ len (prompts )} " )
355+ with KvRunnerSession (
356+ runner = str (runner ),
357+ pte = str (pte ),
358+ extra_args = [],
359+ persistent = True ,
360+ input_dtype = input_dtype ,
361+ ) as session :
362+ prompt_nll , prompt_tokens = eval_prompt_nll_kv (
363+ runner = session ,
364+ tokenizer = tokenizer ,
365+ prompt = prompt ,
366+ max_tokens_per_prompt = max_tokens_per_prompt ,
241367 )
368+ total_nll += prompt_nll
369+ total_tokens += prompt_tokens
370+ if total_tokens > 0 :
371+ cumulative_ppl = f"{ math .exp (total_nll / total_tokens ):.4f} "
372+ else :
373+ cumulative_ppl = "n/a"
374+ print (
375+ f"[eval-kv] { pte .name } prompt { idx } : "
376+ f"tokens={ prompt_tokens } cumulative_ppl={ cumulative_ppl } "
377+ )
242378 else :
243379 with RunnerSession (
244380 runner = str (runner ),
@@ -278,8 +414,8 @@ def main() -> None:
278414 parser .add_argument (
279415 "--pte-fp32" ,
280416 type = Path ,
281- required = True ,
282- help = "Path to the FP32 full-logits SmolLM2 VGF PTE." ,
417+ default = None ,
418+ help = "Path to the FP32 SmolLM2 VGF PTE." ,
283419 )
284420 parser .add_argument (
285421 "--pte-linear8a8w" ,
@@ -293,6 +429,12 @@ def main() -> None:
293429 default = None ,
294430 help = "Path to the linear-only 16a8w full-logits SmolLM2 VGF PTE." ,
295431 )
432+ parser .add_argument (
433+ "--pte-static-kvq-linear16a8w" ,
434+ type = Path ,
435+ default = None ,
436+ help = "Path to the linear16a8w SmolLM2 VGF PTE with an INT8 KV cache." ,
437+ )
296438 parser .add_argument (
297439 "--tokenizer" ,
298440 type = Path ,
@@ -329,12 +471,39 @@ def main() -> None:
329471 action = "store_true" ,
330472 help = "Evaluate KV-cache exports that take token and input_pos inputs." ,
331473 )
474+ parser .add_argument (
475+ "--input-dtype" ,
476+ choices = ("int32" , "int64" ),
477+ default = "int64" ,
478+ help = (
479+ "KV token and position dtype; must match the exported PTE inputs "
480+ "(default: int64)."
481+ ),
482+ )
483+ parser .add_argument (
484+ "--workers" ,
485+ type = int ,
486+ default = 1 ,
487+ help = "Number of independent executor_runner processes (default: 1)." ,
488+ )
332489 parser .add_argument (
333490 "--refresh-prompts" ,
334491 action = "store_true" ,
335492 help = "Rebuild prompts even if --prompts-file already exists." ,
336493 )
337494 args = parser .parse_args ()
495+ if not any (
496+ (
497+ args .pte_fp32 ,
498+ args .pte_linear8a8w ,
499+ args .pte_linear16a8w ,
500+ args .pte_static_kvq_linear16a8w ,
501+ )
502+ ):
503+ parser .error (
504+ "at least one of --pte-fp32, --pte-linear8a8w, "
505+ "--pte-linear16a8w, or --pte-static-kvq-linear16a8w is required"
506+ )
338507
339508 tokenizer = get_tokenizer (str (args .tokenizer ))
340509 pad_id = getattr (tokenizer , "pad_id" , tokenizer .eos_id )
@@ -357,6 +526,7 @@ def main() -> None:
357526 "fp32" : args .pte_fp32 ,
358527 "linear8a8w" : args .pte_linear8a8w ,
359528 "linear16a8w" : args .pte_linear16a8w ,
529+ "static-kvq-linear16a8w" : args .pte_static_kvq_linear16a8w ,
360530 }
361531 results = {}
362532 for name , pte in ptes .items ():
@@ -371,6 +541,9 @@ def main() -> None:
371541 pad_id = pad_id ,
372542 max_tokens_per_prompt = args .max_tokens_per_prompt ,
373543 use_kv_cache = args .use_kv_cache ,
544+ workers = args .workers ,
545+ tokenizer_path = args .tokenizer ,
546+ input_dtype = np .dtype (args .input_dtype ),
374547 )
375548
376549 print ("\n === Perplexity summary ===" )
0 commit comments