Files
mlc-ai--mlc-llm/python/mlc_llm/bench/dataset.py
T
wehub-resource-sync 770d92cb1f
Lint / lint (push) Waiting to run
Windows CI / Windows (push) Waiting to run
Build Docs / Deploy Docs (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:23:58 +08:00

879 lines
38 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""MLC LLM benchmark dataset classes"""
import argparse
import json
import random
from datetime import datetime
from typing import ClassVar, Dict, List, Optional, Tuple # noqa: UP035
import numpy as np
import pandas as pd
from datasets import load_dataset
from transformers import AutoTokenizer
from mlc_llm.bench.request_record import GroupedRequestRecord, Metrics, RequestRecord
from mlc_llm.protocol.openai_api_protocol import (
ChatCompletionMessage,
ChatCompletionRequest,
DebugConfig,
)
class Dataset:
"""The dataset base class."""
# We set a truncation limit of 100k.
truncate_length = int(1e5)
# For some that datasets (e.g., dataset that has shared common prefix),
# we need fake warmup requests to avoid prefilling common prefixes to the engine.
require_fake_warmup: bool = False
# Whether the dataset contains timestamps already.
# If the dataset comes with timestamps, the benchmark can just replay
# the requests according to their timestamps.
timestamp_available: bool = False
def generate_request_records(
self,
input_len: Optional[int],
output_len: Optional[int],
input_len_std: float = 0.0,
output_len_std: float = 0.0,
) -> List[RequestRecord]: # noqa: UP006
"""Get the raw unprocessed request records of the dataset."""
raise NotImplementedError()
class ShareGPTDataset(Dataset):
"""The dataset class for ShareGPT dataset."""
_tokenized_dataset: List[Tuple[str, List[int], int]] # noqa: UP006
apply_chat_template: bool
def __init__(
self, dataset_path: str, tokenizer: AutoTokenizer, apply_chat_template: bool
) -> None:
self.apply_chat_template = apply_chat_template
with open(dataset_path, encoding="utf-8") as f:
raw_dataset = json.load(f)
# Filter out the conversations with less than 2 turns.
_dataset = [
(data["conversations"][0]["value"], data["conversations"][1]["value"])
for data in raw_dataset
if len(data["conversations"]) >= 2 and data["conversations"][0]["from"] == "human"
]
# Tokenize the prompts and completions.
self.tokenizer = tokenizer
prompts = [prompt for prompt, _ in _dataset]
if apply_chat_template:
assert getattr(tokenizer, "chat_template", None) is not None, (
'"--apply-chat-template" is set but the tokenizer does not have chat template.'
)
prompts = [
tokenizer.apply_chat_template(
[{"role": "user", "content": prompt}],
add_generation_prompt=True,
tokenize=False,
)
for prompt in prompts
]
prompt_token_ids = list(
tokenizer(
prompts,
truncation=True,
max_length=min(tokenizer.model_max_length, self.truncate_length),
add_special_tokens=False,
).input_ids
)
completions = [completion for _, completion in _dataset]
completion_token_ids = tokenizer(
completions,
truncation=True,
max_length=min(tokenizer.model_max_length, self.truncate_length),
add_special_tokens=False,
).input_ids
self._tokenized_dataset: List[Tuple[str, List[int], int]] = [] # noqa: UP006
for i in range(len(_dataset)):
if (
len(prompt_token_ids[i]) < 4
or len(completion_token_ids[i]) < 4
or len(prompt_token_ids[i]) + len(completion_token_ids[i])
>= min(tokenizer.model_max_length, 8192)
):
# Filter out sequences that are too short or too long
continue
self._tokenized_dataset.append(
(prompts[i], prompt_token_ids[i], len(completion_token_ids[i]))
)
def generate_request_records(
self,
input_len: Optional[int],
output_len: Optional[int],
input_len_std: float = 0.0,
output_len_std: float = 0.0,
) -> List[RequestRecord]: # noqa: UP006
if self.apply_chat_template:
assert input_len is None, (
'"--apply-chat-template" is not supported when "--input-len" is specified.'
)
request_records = []
for prompt, input_token_ids, output_length in self._tokenized_dataset:
input_length = len(input_token_ids)
# If the request does not have enough length, discard it.
if input_len is not None and input_length < input_len + 4 * input_len_std:
continue
if input_len is not None:
input_length = round(
float(np.random.normal(loc=input_len, scale=input_len_std, size=1)[0])
)
input_token_ids = input_token_ids[:input_length]
input_truncated = True
else:
input_truncated = False
if output_len is not None:
output_length = round(
float(np.random.normal(loc=output_len, scale=output_len_std, size=1)[0])
)
elif output_length <= 1:
continue
request_records.append(
RequestRecord(
chat_cmpl=ChatCompletionRequest(
messages=[
{
"role": "user",
"content": (
self.tokenizer.decode(input_token_ids)
if input_truncated
else prompt
),
}
],
model="",
max_tokens=output_length,
),
metrics=Metrics(
success=False,
start_time=0,
finish_time=0,
end_to_end_latency_s=0,
input_tokens=len(input_token_ids),
),
)
)
return request_records
class LoogleDataset(Dataset):
"""The dataset class for Loogle dataset."""
task2prompt: ClassVar[Dict[str, str]] = { # noqa: UP006
"shortdep_qa": "Please answer the question based on the long texts below. \n{input}\nQuestion: {Q}\nAnswer: ", # noqa: E501
"longdep_qa": "Please answer the question based on the long texts below. \n{input}\nQuestion: {Q}\nAnswer: ", # noqa: E501
"longdep_summarization": "Please generate a summary of the below paper. \n{input}\n Summarization: ", # noqa: E501
"shortdep_cloze": "Please fill in the clozes based on the given long texts below. Each of the placeholder '<mask-n>' in the question could be an entity of Person, Location or Organiocation. The same masks represent the same entity. Output a json format answer, for example: {{'<mask-0>': 'Bob', '<mask-1>': 'Gorrosion Magazine','<mask-2>': 'Bethel Horizon'}}\n{input}\n Question: {Q} What are the masked entities? \nAnswer:", # noqa: E501
}
require_fake_warmup: bool = True
def __init__(self, tokenizer: AutoTokenizer, testset_name: str) -> None:
raw_dataset = load_dataset("bigainlco/LooGLE", testset_name, split="test")
self.tokenizer = tokenizer
self.dataset = []
self.prompt_format = self.task2prompt[testset_name]
prompts = []
generate_lens = []
questions = []
for data in raw_dataset:
prompt = data["input"]
prompts.append(prompt)
qa_pairs = eval(data["qa_pairs"])
questions.append([j["Q"] for j in qa_pairs])
generate_lens.append(
[len(tokenizer.encode(j["A"], add_special_tokens=False)) for j in qa_pairs]
)
prompt_token_ids = tokenizer(
prompts,
truncation=True,
max_length=min(tokenizer.model_max_length, self.truncate_length),
add_special_tokens=False,
).input_ids
for prompt, prompt_token_id, question, generate_len in zip(
prompts, prompt_token_ids, questions, generate_lens
):
self.dataset.append((prompt, prompt_token_id, question, generate_len))
def generate_request_records(
self,
input_len: Optional[int],
output_len: Optional[int],
input_len_std: float = 0.0,
output_len_std: float = 0.0,
) -> List[RequestRecord]: # noqa: UP006
request_records = []
for prompt, input_token_ids, questions, generate_lens in self.dataset:
input_length = round(float(np.random.normal(loc=input_len, scale=input_len_std)))
if len(input_token_ids) > input_length:
input_token_ids = input_token_ids[:input_length]
prompt = self.tokenizer.decode(input_token_ids)
grouped_request_records = []
for question, generate_len in zip(questions, generate_lens):
json_obj = {"input": prompt, "Q": question}
full_prompt = self.prompt_format.format(**json_obj)
output_length = (
round(float(np.random.normal(loc=output_len, scale=output_len_std, size=1)[0]))
if output_len is not None
else generate_len
)
grouped_request_records.append(
RequestRecord(
chat_cmpl=ChatCompletionRequest(
messages=[
{
"role": "user",
"content": full_prompt,
}
],
model="",
max_tokens=output_length,
),
metrics=Metrics(
success=False,
start_time=0,
finish_time=0,
end_to_end_latency_s=0,
input_tokens=len(input_token_ids),
),
)
)
request_records.append(
GroupedRequestRecord(
# Create a dummy ChatCompletionRequest.
chat_cmpl=ChatCompletionRequest(messages=[]),
records=grouped_request_records,
)
)
return request_records
class LLMPerfDataset(Dataset):
"""The dataset class for LLMPerf dataset."""
def __init__(self, dataset_path: str, num_requests: int, tokenizer: AutoTokenizer) -> None:
self.tokenizer = tokenizer
self.num_requests = num_requests
with open(dataset_path, encoding="utf-8") as f:
untokenized_data = f.readlines()
# Tokenize the prompts and completions.
tokenized_data = tokenizer(
untokenized_data,
truncation=True,
max_length=min(tokenizer.model_max_length, self.truncate_length),
add_special_tokens=False,
).input_ids
tokenized_data_lengths = [len(tokens) for tokens in tokenized_data]
self.dataset: List[Tuple[str, List[int], int]] = list( # noqa: UP006
zip(untokenized_data, tokenized_data, tokenized_data_lengths)
)
def generate_request_records(
self,
input_len: Optional[int] = None,
output_len: Optional[int] = None,
input_len_std: float = 250,
output_len_std: float = 0.0,
) -> List[RequestRecord]: # noqa: UP006
if input_len is None or input_len < 40:
input_len = 550
if output_len is None:
output_len = 150
request_records = []
for _ in range(self.num_requests):
input_length = round(float(np.random.normal(loc=input_len, scale=input_len_std)))
output_length = round(float(np.random.normal(loc=output_len, scale=output_len_std)))
prompt = (
"Randomly stream lines from the following text "
f"with {output_length} output tokens. "
"Don't generate eos tokens:\n\n"
)
remaining_token_length = input_length - len(
self.tokenizer.encode(prompt, add_special_tokens=False)
)
random.shuffle(self.dataset)
while remaining_token_length > 0:
for text, tokens, token_length in self.dataset:
if remaining_token_length < token_length:
prompt += self.tokenizer.decode(tokens[:remaining_token_length])
else:
prompt += text
remaining_token_length -= token_length
if remaining_token_length < 0:
break
request_records.append(
RequestRecord(
chat_cmpl=ChatCompletionRequest(
messages=[{"role": "user", "content": prompt}],
model="",
max_tokens=output_length,
debug_config=DebugConfig(ignore_eos=True),
),
metrics=Metrics(
success=False,
start_time=0,
finish_time=0,
end_to_end_latency_s=0,
input_tokens=input_length,
),
)
)
return request_records
class JSONModeEvalDataset(Dataset):
"""The dataset class for JSON dataset."""
def __init__(self, tokenizer: AutoTokenizer) -> None:
raw_dataset = load_dataset("NousResearch/json-mode-eval")
self.tokenizer = tokenizer
self.dataset = []
for data in raw_dataset["train"]:
messages = data["prompt"]
schema = {
"type": "json_object",
"schema": data["schema"],
}
num_tokens = 0
for message in messages:
num_tokens += len(
self.tokenizer.encode(message["content"], add_special_tokens=False)
)
self.dataset.append((messages, schema, num_tokens))
def generate_request_records(
self,
input_len: Optional[int],
output_len: Optional[int],
input_len_std: float = 0.0,
output_len_std: float = 0.0,
) -> List[RequestRecord]: # noqa: UP006
request_records = []
for messages, schema, num_tokens in self.dataset:
# If the request does not have enough length, discard it.
if input_len is not None and num_tokens < input_len + 4 * input_len_std:
continue
if output_len is not None:
output_length = max(
round(np.random.normal(loc=output_len, scale=output_len_std)), 1
)
else:
output_length = None
request_records.append(
RequestRecord(
chat_cmpl=ChatCompletionRequest(
messages=[
ChatCompletionMessage(content=message["content"], role=message["role"])
for message in messages
],
model="",
max_tokens=output_length,
response_format=schema,
),
metrics=Metrics(
success=False,
start_time=0,
finish_time=0,
end_to_end_latency_s=0,
input_tokens=num_tokens,
),
)
)
return request_records
class ReActDataset(Dataset):
"""The dataset class for replaying a given ReAct trace for benchmark purpose.
It is not an actual ReAct agent implementation.
"""
_dataset: List[List[Tuple[str, int, int]]] # noqa: UP006
require_fake_warmup: bool = True
prefix: str = """Solve a question answering task with interleaving Thought, Action, Observation steps. Thought can reason about the current situation, and Action can be three types:
(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search.
(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage.
(3) Finish[answer], which returns the answer and finishes the task.
Here are some examples.
Question: What is the elevation range for the area that the eastern sector of the Colorado orogeny extends into?
Thought 1: I need to search Colorado orogeny, find the area that the eastern sector of the Colorado orogeny extends into, then find the elevation range of the area.
Action 1: Search[Colorado orogeny]
Observation 1: The Colorado orogeny was an episode of mountain building (an orogeny) in Colorado and surrounding areas.
Thought 2: It does not mention the eastern sector. So I need to look up eastern sector.
Action 2: Lookup[eastern sector]
Observation 2: (Result 1 / 1) The eastern sector extends into the High Plains and is called the Central Plains orogeny.
Thought 3: The eastern sector of Colorado orogeny extends into the High Plains. So I need to search High Plains and find its elevation range.
Action 3: Search[High Plains]
Observation 3: High Plains refers to one of two distinct land regions:
Thought 4: I need to instead search High Plains (United States).
Action 4: Search[High Plains (United States)]
Observation 4: The High Plains are a subregion of the Great Plains. From east to west, the High Plains rise in elevation from around 1,800 to 7,000 ft (550 to 2,130 m).[3]
Thought 5: High Plains rise in elevation from around 1,800 to 7,000 ft, so the answer is 1,800 to 7,000 ft.
Action 5: Finish[1,800 to 7,000 ft]
Question: Musician and satirist Allie Goertz wrote a song about the "The Simpsons" character Milhouse, who Matt Groening named after who?
Thought 1: The question simplifies to "The Simpsons" character Milhouse is named after who. I only need to search Milhouse and find who it is named after.
Action 1: Search[Milhouse]
Observation 1: Milhouse Mussolini Van Houten is a recurring character in the Fox animated television series The Simpsons voiced by Pamela Hayden and created by Matt Groening.
Thought 2: The paragraph does not tell who Milhouse is named after, maybe I can look up "named after".
Action 2: Lookup[named after]
Observation 2: (Result 1 / 1) Milhouse was named after U.S. president Richard Nixon, whose middle name was Milhous.
Thought 3: Milhouse was named after U.S. president Richard Nixon, so the answer is Richard Nixon.
Action 3: Finish[Richard Nixon]
Question: Which documentary is about Finnish rock groups, Adam Clayton Powell or The Saimaa Gesture?
Thought 1: I need to search Adam Clayton Powell and The Saimaa Gesture, and find which documentary is about Finnish rock groups.
Action 1: Search[Adam Clayton Powell]
Observation 1: Could not find [Adam Clayton Powell]. Similar: ['Adam Clayton Powell III', 'Seventh Avenue (Manhattan)', 'Adam Clayton Powell Jr. State Office Building', 'Isabel Washington Powell', 'Adam Powell', 'Adam Clayton Powell (film)', 'Giancarlo Esposito'].
Thought 2: To find the documentary, I can search Adam Clayton Powell (film).
Action 2: Search[Adam Clayton Powell (film)]
Observation 2: Adam Clayton Powell is a 1989 American documentary film directed by Richard Kilberg.
The film is about the rise and fall of influential African-American politician Adam Clayton Powell Jr.[3][4] It was later aired as part of the PBS series The American Experience.
Thought 3: Adam Clayton Powell (film) is a documentary about an African-American politician, not Finnish rock groups. So the documentary about Finnish rock groups must instead be The Saimaa Gesture.
Action 3: Finish[The Saimaa Gesture]
Question: What profession does Nicholas Ray and Elia Kazan have in common?
Thought 1: I need to search Nicholas Ray and Elia Kazan, find their professions, then find the profession they have in common.
Action 1: Search[Nicholas Ray]
Observation 1: Nicholas Ray (born Raymond Nicholas Kienzle Jr., August 7, 1911 June 16, 1979) was an American film director, screenwriter, and actor best known for the 1955 film Rebel Without a Cause.
Thought 2: Professions of Nicholas Ray are director, screenwriter, and actor. I need to search Elia Kazan next and find his professions.
Action 2: Search[Elia Kazan]
Observation 2: Elia Kazan was an American film and theatre director, producer, screenwriter and actor.
Thought 3: Professions of Elia Kazan are director, producer, screenwriter, and actor. So profession Nicholas Ray and Elia Kazan have in common is director, screenwriter, and actor.
Action 3: Finish[director, screenwriter, actor]
Question: Which magazine was started first Arthur's Magazine or First for Women?
Thought 1: I need to search Arthur's Magazine and First for Women, and find which was started first.
Action 1: Search[Arthur's Magazine]
Observation 1: Arthur's Magazine (1844-1846) was an American literary periodical published in Philadelphia in the 19th century.
Thought 2: Arthur's Magazine was started in 1844. I need to search First for Women next.
Action 2: Search[First for Women]
Observation 2: First for Women is a woman's magazine published by Bauer Media Group in the USA.[1] The magazine was started in 1989.
Thought 3: First for Women was started in 1989. 1844 (Arthur's Magazine) < 1989 (First for Women), so Arthur's Magazine was started first.
Action 3: Finish[Arthur's Magazine]
Question: Were Pavel Urysohn and Leonid Levin known for the same type of work?
Thought 1: I need to search Pavel Urysohn and Leonid Levin, find their types of work, then find if they are the same.
Action 1: Search[Pavel Urysohn]
Observation 1: Pavel Samuilovich Urysohn (February 3, 1898 â August 17, 1924) was a Soviet mathematician who is best known for his contributions in dimension theory.
Thought 2: Pavel Urysohn is a mathematician. I need to search Leonid Levin next and find its type of work.
Action 2: Search[Leonid Levin]
Observation 2: Leonid Anatolievich Levin is a Soviet-American mathematician and computer scientist.
Thought 3: Leonid Levin is a mathematician and computer scientist. So Pavel Urysohn and Leonid Levin have the same type of work.
Action 3: Finish[yes]
""" # noqa: E501, RUF001
def __init__(self, dataset_path: str, tokenizer: AutoTokenizer) -> None:
raw_entries: List[Dict] = [] # noqa: UP006
with open(dataset_path) as fin:
for line in fin:
line_content = json.loads(line)
raw_entries += list({"question": k, "triplets": v} for k, v in line_content.items())
self._dataset = []
max_rounds = 0
for raw_entry in raw_entries:
processed_entry = []
question = raw_entry["question"]
triplets = raw_entry["triplets"]
seq = self.prefix + question
max_rounds = max(max_rounds, len(triplets) + 1)
output_lengths: List[int] = [] # noqa: UP006
for i, triplet in enumerate(triplets):
output_lengths.append(
len(
tokenizer(
triplet["thought"]
+ "\nAction "
+ str(i + 1)
+ ": "
+ triplet["action"]
+ "\n",
truncation=True,
max_length=min(tokenizer.model_max_length, self.truncate_length),
add_special_tokens=False,
).input_ids
)
)
for i in range(1, len(triplets) + 2):
seq += "Thought " + str(i) + ":"
input_len = len(
tokenizer(
seq,
truncation=True,
max_length=min(tokenizer.model_max_length, self.truncate_length),
add_special_tokens=False,
).input_ids
)
output_length = (
output_lengths[i - 1]
if i <= len(triplets)
else int(sum(output_lengths) / len(triplets))
)
processed_entry.append((seq, input_len, output_length))
if i != len(triplets) + 1:
seq += (
triplets[i - 1]["thought"]
+ "\nAction "
+ str(i)
+ ": "
+ triplets[i - 1]["action"]
+ "\nObservation "
+ str(i)
+ ": "
+ triplets[i - 1]["observation"]
+ "\n"
)
self._dataset.append(processed_entry)
def generate_request_records(
self,
input_len: Optional[int],
output_len: Optional[int],
input_len_std: float = 0.0,
output_len_std: float = 0.0,
) -> List[RequestRecord]: # noqa: UP006
if input_len is not None or output_len is not None:
raise ValueError("ReAct dataset does not support specifying input/output length.")
request_records = []
for processed_entries in self._dataset:
grouped_request_records = []
for prompt, input_length, output_length in processed_entries:
grouped_request_records.append(
RequestRecord(
chat_cmpl=ChatCompletionRequest(
messages=[{"role": "user", "content": prompt}],
model="",
max_tokens=output_length,
),
metrics=Metrics(
success=False,
start_time=0,
finish_time=0,
end_to_end_latency_s=0,
input_tokens=input_length,
),
)
)
request_records.append(
GroupedRequestRecord(
# Create a dummy ChatCompletionRequest.
chat_cmpl=ChatCompletionRequest(messages=[]),
records=grouped_request_records,
)
)
return request_records
class WildChatDataset(Dataset):
"""The dataset class for WildChat dataset."""
apply_chat_template: bool
def __init__(self, tokenizer: AutoTokenizer, apply_chat_template: bool) -> None:
raw_dataset = load_dataset("allenai/WildChat", split="train")
self.tokenizer = tokenizer
self.apply_chat_template = apply_chat_template
# Filter out the conversations with less than 2 turns.
_dataset = [
(entry["conversation"][0]["content"], entry["conversation"][1]["content"])
for entry in raw_dataset
if len(entry["conversation"]) >= 2
and entry["conversation"][0]["role"] == "user"
and entry["conversation"][1]["role"] == "assistant"
]
prompts = []
completions = []
for prompt, completion in _dataset:
prompts.append(prompt)
completions.append(completion)
if apply_chat_template:
assert getattr(tokenizer, "chat_template", None) is not None, (
'"--apply-chat-template" is set but the tokenizer does not have chat template.'
)
prompts = [
tokenizer.apply_chat_template(
[{"role": "user", "content": prompt}],
add_generation_prompt=True,
tokenize=False,
)
for prompt in prompts
]
prompt_token_ids = list(
tokenizer(
prompts,
truncation=True,
max_length=min(tokenizer.model_max_length, self.truncate_length),
add_special_tokens=False,
).input_ids
)
completion_token_ids = tokenizer(
completions,
truncation=True,
max_length=min(tokenizer.model_max_length, self.truncate_length),
add_special_tokens=False,
).input_ids
self._tokenized_dataset: List[Tuple[str, List[int], int]] = [] # noqa: UP006
for i in range(len(_dataset)):
if len(prompt_token_ids[i]) < 4 or len(completion_token_ids[i]) < 4:
# Filter out sequences that are too short
continue
self._tokenized_dataset.append(
(prompts[i], prompt_token_ids[i], len(completion_token_ids[i]))
)
def generate_request_records(
self,
input_len: Optional[int],
output_len: Optional[int],
input_len_std: float = 0.0,
output_len_std: float = 0.0,
) -> List[RequestRecord]: # noqa: UP006
if self.apply_chat_template:
assert input_len is None, (
'"--apply-chat-template" is not supported when "--input-len" is specified.'
)
request_records = []
for prompt, input_token_ids, output_length in self._tokenized_dataset:
input_length = len(input_token_ids)
# If the request does not have enough length, discard it.
if input_len is not None and input_length < input_len + 4 * input_len_std:
continue
if input_len is not None:
input_length = round(
float(np.random.normal(loc=input_len, scale=input_len_std, size=1)[0])
)
input_token_ids = input_token_ids[:input_length]
input_truncated = True
else:
input_truncated = False
if output_len is not None:
output_length = round(
float(np.random.normal(loc=output_len, scale=output_len_std, size=1)[0])
)
elif output_length <= 1:
continue
request_records.append(
RequestRecord(
chat_cmpl=ChatCompletionRequest(
messages=[
{
"role": "user",
"content": (
self.tokenizer.decode(input_token_ids)
if input_truncated
else prompt
),
}
],
model="",
max_tokens=output_length,
),
metrics=Metrics(
success=False,
start_time=0,
finish_time=0,
end_to_end_latency_s=0,
input_tokens=len(input_token_ids),
),
)
)
return request_records
class AzureLLMInferenceDataset(Dataset):
"""The dataset class for AzureLLMInference dataset.
Reference: https://github.com/Azure/AzurePublicDataset
"""
timestamp_available: bool = True
def __init__(self, dataset_path: str, tokenizer: AutoTokenizer) -> None:
df = pd.read_csv(dataset_path)
self.tokenizer = tokenizer
# Filter out the conversations with less than 2 turns.
self.dataset = [
(
entry["TIMESTAMP"],
min(
entry["ContextTokens"],
tokenizer.model_max_length,
self.truncate_length,
),
min(
entry["GeneratedTokens"],
tokenizer.model_max_length,
self.truncate_length,
),
)
for _, entry in df.iterrows()
if entry["ContextTokens"] >= 4 and entry["GeneratedTokens"] >= 4
]
def generate_request_records(
self,
input_len: Optional[int],
output_len: Optional[int],
input_len_std: float = 0.0,
output_len_std: float = 0.0,
) -> List[RequestRecord]: # noqa: UP006
time_fmt = "%Y-%m-%d %H:%M:%S.%f"
start_time = datetime.strptime(self.dataset[0][0][:-1], time_fmt)
request_records = []
for timestamp, input_length, output_length in self.dataset:
# If the request does not have enough length, discard it.
if input_len is not None and input_length < input_len + 4 * input_len_std:
continue
if input_len is not None:
input_length = round(
float(np.random.normal(loc=input_len, scale=input_len_std, size=1)[0])
)
if output_len is not None:
output_length = round(
float(np.random.normal(loc=output_len, scale=output_len_std, size=1)[0])
)
elif output_length <= 1:
continue
prompt_token_ids = [
random.randint(0, self.tokenizer.vocab_size - 1) for _ in range(input_length)
]
while True:
# Adjust the token ids until the retokenization on the decoded string
# matches the required input length.
prompt = self.tokenizer.decode(prompt_token_ids)
retokenized_token_ids = self.tokenizer.encode(prompt, add_special_tokens=False)
if len(retokenized_token_ids) < input_length:
prompt_token_ids = retokenized_token_ids + [
random.randint(0, self.tokenizer.vocab_size - 1)
for _ in range(input_length - len(retokenized_token_ids))
]
elif len(retokenized_token_ids) > input_length:
prompt_token_ids = retokenized_token_ids[:input_length]
else:
break
time_diff = (datetime.strptime(timestamp[:-1], time_fmt) - start_time).total_seconds()
request_records.append(
RequestRecord(
chat_cmpl=ChatCompletionRequest(
messages=[{"role": "user", "content": prompt}],
model="",
max_tokens=output_length,
),
timestamp=time_diff,
metrics=Metrics(
success=False,
start_time=0,
finish_time=0,
end_to_end_latency_s=0,
input_tokens=input_length,
),
)
)
return request_records
SUPPORTED_DATASET = [
"sharegpt",
"llmperf",
"json-mode-eval",
"loogle",
"react",
"wildchat",
"azure-llm-inference",
]
def create_dataset(args: argparse.Namespace, tokenizer: AutoTokenizer) -> Dataset:
"""Create a dataset instance with regard to the specified dataset kind and file path."""
if args.dataset_path is not None and not isinstance(args.dataset_path, str):
raise TypeError(f"Invalid dataset path {args.dataset_path}. Please use a string.")
if args.dataset is None and args.dataset_path is not None:
# Auto-detect the dataset kind by looking into the dataset path.
if "sharegpt" in args.dataset_path.lower():
args.dataset = "sharegpt"
else:
raise ValueError(
f"Unable to detect the dataset kind from dataset path {args.dataset_path}. "
'Please specify the dataset kind via "--dataset".'
)
if args.dataset == "sharegpt":
if args.dataset_path is None:
raise ValueError(
'ShareGPT dataset requires dataset path. Please specify it with "--dataset-path".'
)
return ShareGPTDataset(args.dataset_path, tokenizer, args.apply_chat_template)
if args.dataset == "llmperf":
if args.dataset_path is None:
raise ValueError(
'LLMPerf dataset requires dataset path. Please specify it with "--dataset-path".'
)
assert args.apply_chat_template is False, (
"LLMPerf dataset does not support applying chat template"
)
return LLMPerfDataset(
args.dataset_path,
(args.num_requests + args.num_warmup_requests) * 4,
tokenizer,
)
if args.dataset == "json-mode-eval":
assert args.apply_chat_template is False, (
"JSON mode evaluation does not support applying chat template"
)
return JSONModeEvalDataset(tokenizer)
if args.dataset == "loogle":
if args.dataset_path is None:
raise ValueError(
'Loogle dataset requires a testset name. Please specify it with "--dataset-path".'
)
assert args.apply_chat_template is False, (
"Loogle dataset does not support applying chat template"
)
return LoogleDataset(tokenizer, testset_name=args.dataset_path)
if args.dataset == "react":
if args.dataset_path is None:
raise ValueError(
'ReAct dataset requires dataset path. Please specify it with "--dataset-path".'
)
assert args.apply_chat_template is False, (
"ReAct dataset does not support applying chat template"
)
return ReActDataset(args.dataset_path, tokenizer)
if args.dataset == "wildchat":
return WildChatDataset(tokenizer, args.apply_chat_template)
if args.dataset == "azure-llm-inference":
if args.dataset_path is None:
raise ValueError(
"AzureLLMInference dataset requires dataset path. "
'Please specify it with "--dataset-path".'
)
assert args.apply_chat_template is False, (
"AzureLLMInference dataset does not support applying chat template"
)
return AzureLLMInferenceDataset(args.dataset_path, tokenizer)
raise ValueError(f"Unrecognized dataset {args.dataset}")