| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | """Convert a plain-text file (local path or URL) into a JSONL dataset |
| 4 | compatible with ``CustomDataset`` (``--dataset-name custom``), by |
| 5 | randomly slicing the tokenized text into prompts. |
| 6 | |
| 7 | Each line of the output JSONL contains a ``prompt`` (decoded from a random |
| 8 | slice of the tokenized source text) and an ``output_tokens`` count. |
| 9 | |
| 10 | Usage |
| 11 | ----- |
| 12 | :: |
| 13 | |
| 14 | python -m vllm.benchmarks.datasets.create_txt_slices_dataset \\ |
| 15 | --input sonnet.txt \\ |
| 16 | --output sonnet_dataset.jsonl \\ |
| 17 | --tokenizer gpt2 \\ |
| 18 | --num-prompts 1000 \\ |
| 19 | --input-len 1024 \\ |
| 20 | --output-len 128 |
| 21 | |
| 22 | The resulting JSONL file can then be used with the serving benchmark:: |
| 23 | |
| 24 | python -m vllm.benchmarks.serve \\ |
| 25 | --dataset-name custom \\ |
| 26 | --dataset-path sonnet_dataset.jsonl \\ |
| 27 | ... |
| 28 | """ |
| 29 | |
| 30 | from __future__ import annotations |
| 31 | |
| 32 | import json |
| 33 | import logging |
| 34 | import random |
| 35 | import urllib.request |
| 36 | |
| 37 | import numpy as np |
| 38 | from transformers import AutoTokenizer |
| 39 | |
| 40 | from vllm.benchmarks.datasets.utils import RangeRatio, get_sampling_params |
| 41 | from vllm.utils.argparse_utils import FlexibleArgumentParser |
| 42 | |
| 43 | logger = logging.getLogger(__name__) |
| 44 | |
| 45 | |
| 46 | def load_text(path: str) -> str: |
| 47 | """Load text from a local file or URL.""" |
| 48 | if path.startswith(("http://", "https://")): |
| 49 | with urllib.request.urlopen(path) as response: |
| 50 | return response.read().decode("utf-8") |
| 51 | with open(path, encoding="utf-8") as f: |
| 52 | return f.read() |
| 53 | |
| 54 | |
| 55 | def create_txt_slices_jsonl( |
| 56 | *, |
| 57 | input_path: str, |
| 58 | output_path: str, |
| 59 | tokenizer_name: str, |
| 60 | num_prompts: int, |
| 61 | input_len: int, |
| 62 | output_len: int, |
| 63 | range_ratio: RangeRatio = 0.0, |
| 64 | seed: int = 0, |
| 65 | trust_remote_code: bool = False, |
| 66 | ) -> None: |
| 67 | """Read *input_path*, slice it into prompts, and write JSONL to |
| 68 | *output_path*.""" |
| 69 | tokenizer = AutoTokenizer.from_pretrained( |
| 70 | tokenizer_name, trust_remote_code=trust_remote_code |
| 71 | ) |
| 72 | |
| 73 | text = load_text(input_path) |
| 74 | if not text: |
| 75 | raise ValueError("The text file is empty and cannot be sampled from.") |
| 76 | |
| 77 | token_ids = tokenizer(text, add_special_tokens=False).input_ids |
| 78 | if not token_ids: |
| 79 | raise ValueError("Tokenizing the text produced zero tokens; cannot sample.") |
| 80 | |
| 81 | rng_np = np.random.default_rng(seed) |
| 82 | rng_py = random.Random(seed) |
| 83 | |
| 84 | input_lens, output_lens, _ = get_sampling_params( |
| 85 | rng_np, |
| 86 | num_prompts, |
| 87 | range_ratio, |
| 88 | input_len, |
| 89 | output_len, |
| 90 | tokenizer, |
| 91 | ) |
| 92 | |
| 93 | num_available_tokens = len(token_ids) |
| 94 | |
| 95 | records: list[dict[str, object]] = [] |
| 96 | for i in range(num_prompts): |
| 97 | req_input_len = int(input_lens[i]) |
| 98 | req_output_len = int(output_lens[i]) |
| 99 | |
| 100 | # Randomly select a start position and slice with cycling |
| 101 | start_pos = rng_py.randint(0, num_available_tokens - 1) |
| 102 | prompt_token_ids = [ |
| 103 | token_ids[(start_pos + j) % num_available_tokens] |
| 104 | for j in range(req_input_len) |
| 105 | ] |
| 106 | prompt = tokenizer.decode(prompt_token_ids, skip_special_tokens=False) |
| 107 | |
| 108 | records.append({"prompt": prompt, "output_tokens": req_output_len}) |
| 109 | |
| 110 | with open(output_path, "w", encoding="utf-8") as f: |
| 111 | for record in records: |
| 112 | f.write(json.dumps(record, ensure_ascii=False) + "\n") |
| 113 | |
| 114 | logger.info( |
| 115 | "Wrote %d prompts to %s", |
| 116 | len(records), |
| 117 | output_path, |
| 118 | ) |
| 119 | |
| 120 | |
| 121 | def main(argv: list[str] | None = None) -> None: |
| 122 | parser = FlexibleArgumentParser( |
| 123 | description="Convert a plain-text file into a JSONL dataset " |
| 124 | "for CustomDataset (--dataset-name custom).", |
| 125 | ) |
| 126 | parser.add_argument( |
| 127 | "--input", |
| 128 | required=True, |
| 129 | help="Path or URL to the source text file.", |
| 130 | ) |
| 131 | parser.add_argument( |
| 132 | "--output", |
| 133 | required=True, |
| 134 | help="Path for the output JSONL file.", |
| 135 | ) |
| 136 | parser.add_argument( |
| 137 | "--tokenizer", |
| 138 | required=True, |
| 139 | help="HuggingFace tokenizer name or path.", |
| 140 | ) |
| 141 | parser.add_argument( |
| 142 | "--num-prompts", |
| 143 | type=int, |
| 144 | default=1000, |
| 145 | help="Number of prompt samples to generate (default: 1000).", |
| 146 | ) |
| 147 | parser.add_argument( |
| 148 | "--input-len", |
| 149 | type=int, |
| 150 | default=1024, |
| 151 | help="Target number of input tokens per prompt (default: 1024).", |
| 152 | ) |
| 153 | parser.add_argument( |
| 154 | "--output-len", |
| 155 | type=int, |
| 156 | default=128, |
| 157 | help="Target number of output tokens per prompt (default: 128).", |
| 158 | ) |
| 159 | parser.add_argument( |
| 160 | "--range-ratio", |
| 161 | type=str, |
| 162 | default="0.0", |
| 163 | help="Range ratio for input/output length sampling (default: 0.0). " |
| 164 | "A single float applies to both ISL and OSL. " |
| 165 | 'A JSON dict like \'{"input": 0.3, "output": 0.5}\' sets them ' |
| 166 | "independently. Values must be in [0, 1).", |
| 167 | ) |
| 168 | parser.add_argument( |
| 169 | "--seed", |
| 170 | type=int, |
| 171 | default=0, |
| 172 | help="Random seed for reproducibility (default: 0).", |
| 173 | ) |
| 174 | parser.add_argument( |
| 175 | "--trust-remote-code", |
| 176 | action="store_true", |
| 177 | help="Trust remote code from HuggingFace.", |
| 178 | ) |
| 179 | |
| 180 | args = parser.parse_args(argv) |
| 181 | |
| 182 | logging.basicConfig(level=logging.INFO) |
| 183 | |
| 184 | # Parse --range-ratio: try float first, then JSON dict. |
| 185 | range_ratio: RangeRatio |
| 186 | try: |
| 187 | range_ratio = float(args.range_ratio) |
| 188 | except ValueError: |
| 189 | import json as _json |
| 190 | |
| 191 | range_ratio = _json.loads(args.range_ratio) |
| 192 | |
| 193 | create_txt_slices_jsonl( |
| 194 | input_path=args.input, |
| 195 | output_path=args.output, |
| 196 | tokenizer_name=args.tokenizer, |
| 197 | num_prompts=args.num_prompts, |
| 198 | input_len=args.input_len, |
| 199 | output_len=args.output_len, |
| 200 | range_ratio=range_ratio, |
| 201 | seed=args.seed, |
| 202 | trust_remote_code=args.trust_remote_code, |
| 203 | ) |
| 204 | |
| 205 | |
| 206 | if __name__ == "__main__": |
| 207 | main() |