Skip to content

Commit 4772018

Browse files
committed
Arm backend: Enable VGF KV-cache aliasing in LLM export
Expose mutable buffer aliasing through the LLM export configuration and forward it to the VGF partitioner for static quantized KV caches. Update the SmolLM2 VGF example with the supported export command, persistent runner handling, timestamp reporting, and parallel WikiText perplexity scoring. Wait for runner completion before reading logits, kill hung runners during cleanup, and let evaluation select the KV input dtype. Support tokenizers without a BOS token. AI-assisted-by: Codex Change-Id: Id001b3fde168262ed748b03498d278d594bbfa0b Signed-off-by: Xingguo Li <xingguo.li@arm.com>
1 parent e68a824 commit 4772018

6 files changed

Lines changed: 315 additions & 64 deletions

File tree

‎examples/arm/smollm2_example_vgf/README.md‎

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,13 +119,16 @@ Use the recommended command above with these changes:
119119
```bash
120120
export.output_dir=outputs/$(date +%F)/$(date +%H-%M-%S)_static_kvq_linear16a8w \
121121
export.output_name=smollm2_vgf_static_kvq_linear16a8w.pte \
122-
model.static_quantize_kv_cache=True
122+
model.static_quantize_kv_cache=True \
123+
backend.vgf.alias_buffer_mutations=True
123124
```
124125

125126
The static KV-cache path uses symmetric per-head-dimension scales with
126127
zero-point `0` and dequantizes the cache back to float for the existing
127-
attention path. Treat its accuracy and performance as experimental until they
128-
have been validated for the target hardware and context length.
128+
attention path. The aliasing option internalizes the cache input/output pairs
129+
as persistent VGF state; it has no effect on TOSA or Ethos-U exports. Treat its
130+
accuracy and performance as experimental until they have been validated for
131+
the target hardware and context length.
129132

130133
Alternative `linear8a8w` mode:
131134

@@ -268,7 +271,8 @@ below.
268271

269272
Build a reusable 1000-prompt file from `wikitext-2-raw-v1` and evaluate
270273
perplexity on the first 100 prompts for KV-cache FP32, `linear8a8w`, and
271-
`linear16a8w`:
274+
`linear16a8w`, including the experimental static INT8 KV cache. Four
275+
independent VKML runner processes score disjoint prompt shards:
272276

273277
```bash
274278
OUT_DIR=outputs/$(date +%F)/$(date +%H-%M-%S)_smollm2_vgf_eval
@@ -278,13 +282,17 @@ python examples/arm/smollm2_example_vgf/eval_wikitext_perplexity.py \
278282
--pte-fp32 "${OUT_DIR}/smollm2_vgf_kv_fp32.pte" \
279283
--pte-linear8a8w "${OUT_DIR}/smollm2_vgf_kv_linear8a8w.pte" \
280284
--pte-linear16a8w "${OUT_DIR}/smollm2_vgf_kv_linear16a8w.pte" \
285+
--pte-static-kvq-linear16a8w \
286+
"${OUT_DIR}/smollm2_vgf_static_kvq_linear16a8w.pte" \
281287
--tokenizer data/tokenizers/smollm2/tokenizer.json \
282288
--prompts-file "${OUT_DIR}/wikitext_prompts_1000.txt" \
283289
--num-prompts 1000 \
284290
--ppl-prompts 100 \
285291
--max-seq-length 128 \
286292
--max-prompt-tokens 128 \
287293
--max-tokens-per-prompt 64 \
294+
--input-dtype int32 \
295+
--workers 4 \
288296
--use-kv-cache \
289297
--refresh-prompts
290298
```
@@ -295,6 +303,13 @@ Notes:
295303
- Perplexity is computed on the first 100 prompts from that file.
296304
- KV-cache prompts are scored step-by-step with `token` and `input_pos`
297305
inputs, so this path is slower but matches the recommended decode contract.
306+
- `--input-dtype` defaults to `int64` for older KV exports. Use `int32` for
307+
exports with int32 token and position inputs, as in the example above.
308+
- `--workers` launches one independent VKML runner per worker. Each owns its
309+
mutable KV buffers and scores a disjoint prompt shard; only NLL and token
310+
totals are combined in the parent process.
311+
- Start with `--workers 2` on memory-constrained systems because model and
312+
delegate memory scale approximately with the number of workers.
298313
- `--max-tokens-per-prompt` controls how many tokens are scored from each
299314
prompt. Use `64` for comparison with the static-window baseline, or a larger
300315
value to stress a longer KV context.

‎examples/arm/smollm2_example_vgf/eval_wikitext_perplexity.py‎

Lines changed: 193 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@
88
from __future__ import annotations
99

1010
import argparse
11+
import concurrent.futures
1112
import math
13+
import multiprocessing
1214
import sys
1315
from pathlib import Path
14-
from typing import Iterable, List, Tuple
16+
from typing import Iterable, List, Optional, Tuple
1517

1618
import numpy as np
19+
from numpy.typing import DTypeLike
1720

1821
sys.path.insert(0, str(Path(__file__).resolve().parent))
1922
from 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

Comments
 (0)