Files

193 lines
5.4 KiB
Python

# Copyright © 2025 Apple Inc.
"""
Evaluate perplexity (PPL) of MLX models.
"""
import argparse
import math
import time
import types
import mlx.core as mx
import mlx.nn as nn
import numpy as np
from mlx_lm.tuner.datasets import load_dataset
from mlx_lm.utils import get_total_parameters, load
def load_data(
tokenizer,
data_path: str,
num_samples: int,
sequence_length: int,
):
args = types.SimpleNamespace(
hf_dataset={
"path": data_path,
"train_split": "train",
"valid_split": "train[:1]",
},
train=True,
test=False,
)
dataset = load_dataset(args, tokenizer)[0]
perm = np.random.permutation(len(dataset)).tolist()
num_tokens = sequence_length * num_samples if num_samples > 0 else float("inf")
data = []
i = 0
while len(data) < num_tokens:
tokens, _ = dataset.process(dataset[perm[i]])
i += 1
data.extend(tokens)
data = mx.array(data[: (len(data) // sequence_length) * sequence_length])
data = data.reshape(-1, sequence_length)
if num_samples > 0:
data = data[:num_samples]
return data
def eval_ppl(model, data, batch_size=8):
"""
Evaluate perplexity on a dataset with standard error calculation.
Args:
model: The model to evaluate
data: Tokenized data tensor
batch_size: Batch size for evaluation
Returns:
tuple: (perplexity, standard_error)
"""
all_losses = []
num_batches = (len(data) + batch_size - 1) // batch_size
for i, s in enumerate(range(0, len(data), batch_size)):
batch = data[s : s + batch_size]
# Forward pass: get logits for all tokens except last
logits = model(batch[:, :-1]).astype(mx.float32)
# Calculate cross-entropy loss with next tokens
losses = nn.losses.cross_entropy(logits, batch[:, 1:], reduction="none")
mx.eval(losses)
# Store individual token losses
all_losses.append(losses.flatten())
# Progress indicator
if (i + 1) % 1 == 0 or (i + 1) == num_batches:
print(f" Processed {i + 1}/{num_batches} batches...", end="\r")
print() # New line after progress
# Concatenate all losses into a single array
all_losses = mx.concatenate(all_losses)
# Calculate mean loss and perplexity
mean_loss = all_losses.mean().item()
ppl = math.exp(mean_loss)
# Calculate standard error
std_dev = mx.sqrt(mx.var(all_losses, ddof=1)).item()
num_tokens = all_losses.size
standard_error = std_dev / math.sqrt(num_tokens)
# Delta approximation for standard error of perplexity
standard_error_ppl = ppl * standard_error
return ppl, standard_error_ppl
def main():
parser = argparse.ArgumentParser(description="Evaluate perplexity of MLX models")
parser.add_argument(
"--model",
type=str,
required=True,
help="Path to model or Hugging Face model ID",
)
parser.add_argument(
"--trust-remote-code",
action="store_true",
help="Enable trusting remote code for tokenizer/model loading from Hugging Face.",
)
parser.add_argument(
"--batch-size", type=int, default=8, help="Batch size for evaluation"
)
parser.add_argument(
"--sequence-length",
type=int,
default=512,
help="Sequence length for evaluation",
)
parser.add_argument(
"--num-samples",
type=int,
default=256,
help="Number of samples to use (-1 for all available)",
)
parser.add_argument(
"--data-path",
type=str,
default="allenai/tulu-3-sft-mixture",
help="A Hugging Face dataset which is compatible with an mlx-lm dataset format.",
)
parser.add_argument(
"--seed", type=int, default=123, help="Random seed for data sampling"
)
args = parser.parse_args()
# Set random seed
np.random.seed(args.seed)
mx.random.seed(args.seed)
# Load model
print(f"Loading model from {args.model}...")
tokenizer_config = {"trust_remote_code": True if args.trust_remote_code else None}
model, tokenizer = load(args.model, tokenizer_config=tokenizer_config)
# Count parameters
total_params = get_total_parameters(model)
print(f"Model loaded: {total_params/1e6:.1f}M parameters")
# Load evaluation data
print(f"\nLoading dataset...")
print(f" Sequence length: {args.sequence_length}")
data = load_data(
tokenizer,
args.data_path,
num_samples=args.num_samples,
sequence_length=args.sequence_length,
)
print(f" Loaded {len(data)} samples")
# Evaluate perplexity
print(f"\nEvaluating perplexity with batch size {args.batch_size}...")
start_time = time.time()
ppl, se = eval_ppl(model, data, batch_size=args.batch_size)
eval_time = time.time() - start_time
tokens_evaluated = data.shape[0] * (data.shape[1] - 1) # B * (L - 1)
# Print results
print("\n" + "=" * 60)
print("EVALUATION RESULTS")
print("=" * 60)
print(f"Model: {args.model}")
print(f"Perplexity: {ppl:.3f} ± {se:.3f}")
print(f"Evaluation time: {eval_time:.2f} seconds")
print(f"Peak memory: {mx.get_peak_memory() / 1e9:.2f} GB")
print(f"Tokens per second: {tokens_evaluated / eval_time:.0f}")
# Additional statistics
print(f"\nDataset statistics:")
print(f" Total samples: {len(data)}")
print(f" Total tokens: {data.size}")
if __name__ == "__main__":
main()