184 lines
6.3 KiB
Python
184 lines
6.3 KiB
Python
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
import json
|
|
from collections import Counter
|
|
|
|
from prefix_data_generator.logging_utils import calculate_and_print_statistics
|
|
|
|
|
|
class PrefixAnalyzer:
|
|
"""
|
|
A class for analyzing dataset characteristics related to prefixes, hash IDs, and cache hit rates.
|
|
"""
|
|
|
|
def __init__(self, dataset_path, block_size=1):
|
|
"""
|
|
Initialize the analyzer with dataset path and block size.
|
|
|
|
Args:
|
|
dataset_path: Path to the JSONL dataset file
|
|
block_size: Size of each block for prefix calculation
|
|
"""
|
|
self.dataset_path = dataset_path
|
|
self.block_size = block_size
|
|
self.dataset = self._load_dataset()
|
|
self.hash_counter = self._build_hash_counter()
|
|
self.repeated_hash_ids = {
|
|
(pos, hash_id)
|
|
for (pos, hash_id), count in self.hash_counter.items()
|
|
if count > 1
|
|
}
|
|
|
|
def _load_dataset(self) -> list:
|
|
print(f"Loading dataset from {self.dataset_path}...")
|
|
dataset = []
|
|
with open(self.dataset_path, "r") as f:
|
|
for line in f:
|
|
dataset.append(json.loads(line))
|
|
print(f"Dataset loaded: {len(dataset)} examples")
|
|
return dataset
|
|
|
|
def _build_hash_counter(self) -> Counter:
|
|
all_hash_positions = []
|
|
for item in self.dataset:
|
|
for pos, hash_id in enumerate(item["hash_ids"]):
|
|
all_hash_positions.append((pos, hash_id))
|
|
counter = Counter(all_hash_positions)
|
|
print(f"Hash counter built: {len(counter)} unique (position, hash_id) pairs")
|
|
return counter
|
|
|
|
def analyze(self) -> dict[str, list]:
|
|
"""
|
|
Analyze dataset to extract various length metrics and print statistics.
|
|
|
|
Returns:
|
|
Tuple of lists: (input_lengths, prefix_lengths, user_prompt_lengths, output_lengths)
|
|
"""
|
|
# Extract input and output lengths directly from fields
|
|
input_lengths = [item["input_length"] for item in self.dataset]
|
|
output_lengths = [item["output_length"] for item in self.dataset]
|
|
|
|
# Calculate prefix length and user prompt length for each row
|
|
prefix_lengths = []
|
|
user_prompt_lengths = []
|
|
|
|
for i, item in enumerate(self.dataset):
|
|
input_len = item["input_length"]
|
|
hash_ids = item["hash_ids"]
|
|
assert len(hash_ids) * self.block_size >= input_len
|
|
|
|
# Special case: if all (position, hash_id) pairs in the row are repeated elsewhere
|
|
if all(
|
|
(pos, hash_id) in self.repeated_hash_ids
|
|
for pos, hash_id in enumerate(hash_ids)
|
|
):
|
|
prefix_len = input_len # Set prefix length to input length
|
|
user_prompt_len = 0 # Set user prompt length to 0
|
|
else:
|
|
# Count how many (position, hash_id) pairs in this row are repeated elsewhere in the dataset
|
|
repeated_count = sum(
|
|
1
|
|
for pos, hash_id in enumerate(hash_ids)
|
|
if (pos, hash_id) in self.repeated_hash_ids
|
|
)
|
|
prefix_len = repeated_count * self.block_size
|
|
user_prompt_len = input_len - prefix_len
|
|
|
|
prefix_lengths.append(prefix_len)
|
|
user_prompt_lengths.append(user_prompt_len)
|
|
|
|
# Check if prefix length is greater than input length
|
|
if prefix_len > input_len:
|
|
print(f"WARNING: Line {i}: {json.dumps(item)}")
|
|
|
|
cache_hit_rates = self._analyze_cache_hit_rates()
|
|
|
|
# Print statistics table
|
|
metrics = {
|
|
"Input Length": input_lengths,
|
|
"Context Length": prefix_lengths,
|
|
"Unique Prompt Length": user_prompt_lengths,
|
|
"Output Length": output_lengths,
|
|
"Theoretical Hit Rates": cache_hit_rates,
|
|
}
|
|
|
|
calculate_and_print_statistics(metrics)
|
|
|
|
return metrics
|
|
|
|
def _analyze_cache_hit_rates(self) -> list[float]:
|
|
"""
|
|
Analyze theoretical cache hit rates based on hash ID repetition.
|
|
|
|
Assumes that hash IDs are cached as the dataset is iterated through,
|
|
i.e., each hash ID is considered "cached" after its first appearance,
|
|
similar to how KV caching would work in real life.
|
|
Assumes the cache is infinite in size (hence "theoretical"), so no hash IDs are ever evicted.
|
|
|
|
Returns:
|
|
List of cache hit rates for each row in the dataset
|
|
"""
|
|
# Set to track all hash IDs we've seen
|
|
seen_hash_ids = set()
|
|
|
|
# Store cache hit rates for each row
|
|
cache_hit_rates = []
|
|
|
|
for item in self.dataset:
|
|
hash_ids = item["hash_ids"]
|
|
|
|
# Skip if there are no hash IDs
|
|
if len(hash_ids) == 0:
|
|
continue
|
|
|
|
# Find the first index where the hash ID hasn't been seen before
|
|
first_unseen_idx = len(hash_ids) # Default if all are seen
|
|
for idx, hash_id in enumerate(hash_ids):
|
|
if hash_id not in seen_hash_ids:
|
|
first_unseen_idx = idx
|
|
break
|
|
|
|
# Calculate cache hit rate
|
|
cache_hit_rate = first_unseen_idx / len(hash_ids)
|
|
cache_hit_rates.append(cache_hit_rate)
|
|
|
|
# Add all hash IDs from this row to the seen set
|
|
seen_hash_ids.update(hash_ids)
|
|
|
|
return cache_hit_rates
|
|
|
|
|
|
def main():
|
|
import argparse
|
|
|
|
parser = argparse.ArgumentParser(description="Analyze prefix dataset statistics")
|
|
parser.add_argument(
|
|
"--input-file",
|
|
type=str,
|
|
default="mooncake_trace.jsonl",
|
|
help="Path to the input dataset file (default: mooncake_trace.jsonl)",
|
|
)
|
|
parser.add_argument(
|
|
"--block-size",
|
|
type=int,
|
|
default=512,
|
|
help="Block size for prefix calculation (default: 512)",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
block_size = args.block_size
|
|
dataset_path = args.input_file
|
|
|
|
print(f"Analyzing dataset: {dataset_path}")
|
|
print(f"Using block size: {block_size}")
|
|
print()
|
|
|
|
# Create analyzer instance
|
|
analyzer = PrefixAnalyzer(dataset_path, block_size=block_size)
|
|
analyzer.analyze()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|