chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:17:40 +08:00
commit f1825c8ceb
10096 changed files with 2364182 additions and 0 deletions
@@ -0,0 +1,635 @@
#!/usr/bin/env python
"""
Benchmark Ray Data LLM offline batch inference throughput.
Sample usage:
python ray.llm._internal.batch.benchmark.benchmark_processor --mode vllm_engine --batch-size 64 --concurrency 1 --num-prompts 10000 --model facebook/opt-1.3b
--tensor-parallel-size 2 --pipeline-parallel-size 2 --distributed-executor-backend ray
"""
import argparse
import sys
from dataclasses import dataclass
from enum import Enum
from time import perf_counter, sleep
import ray
from .dataset import ShareGPTDataset
from ray import data, serve
from ray.data.llm import (
ChatTemplateStageConfig,
DetokenizeStageConfig,
ServeDeploymentProcessorConfig,
TokenizerStageConfig,
build_processor,
vLLMEngineProcessorConfig,
)
from ray.serve.llm import (
LLMConfig,
ModelLoadingConfig,
build_llm_deployment,
)
from ray.serve.llm.openai_api_models import CompletionRequest
class Mode(Enum):
"""Processor to benchmark."""
VLLM_ENGINE = "vllm_engine"
SHARED_VLLM_ENGINE = "shared_vllm_engine"
SERVE_DEPLOYMENT = "serve_deployment"
SHARED_SERVE_DEPLOYMENT = "shared_serve_deployment"
CLASSIFY = "classify"
# Default sampling parameters -- ensure a fair comparison by omitting sampling-induced variance
VLLM_SAMPLING_PARAMS = {
"top_p": 1.0,
"temperature": 1.0,
"max_tokens": 100,
"ignore_eos": True,
}
# Default vLLM engine kwargs
VLLM_ENGINE_KWARGS = {
"max_num_batched_tokens": 4096,
}
# Default tokenization kwargs for classification -- truncate to max_model_len.
CLASSIFY_TOKENIZATION_KWARGS_DEFAULT = {"truncation": True, "max_length": 512}
def build_vllm_engine_kwargs(**kwargs) -> dict:
"""Build vLLM engine kwargs from command line arguments."""
engine_kwargs = VLLM_ENGINE_KWARGS.copy()
engine_kwargs.update({k: v for k, v in kwargs.items() if v is not None})
return engine_kwargs
def _build_vllm_engine_config(
model: str,
batch_size: int,
concurrency: int,
pipeline_parallel_size: int = None,
tensor_parallel_size: int = None,
distributed_executor_backend: str = None,
task_type: str = None,
max_model_len: int = None,
) -> vLLMEngineProcessorConfig:
"""Helper to create vLLMEngineProcessorConfig."""
engine_kwargs = build_vllm_engine_kwargs(
pipeline_parallel_size=pipeline_parallel_size,
tensor_parallel_size=tensor_parallel_size,
distributed_executor_backend=distributed_executor_backend,
)
if max_model_len is not None:
engine_kwargs["max_model_len"] = max_model_len
config = vLLMEngineProcessorConfig(
model_source=model,
batch_size=batch_size,
concurrency=concurrency,
chat_template_stage=False,
tokenize_stage=False,
detokenize_stage=False,
engine_kwargs=engine_kwargs,
)
if task_type is not None:
config.task_type = task_type
return config
def _build_serve_deployment_config(
batch_size: int,
concurrency: int,
deployment_name: str = None,
app_name: str = None,
) -> ServeDeploymentProcessorConfig:
"""Helper to create ServeDeploymentProcessorConfig."""
return ServeDeploymentProcessorConfig(
deployment_name=deployment_name,
app_name=app_name,
dtype_mapping={
"CompletionRequest": CompletionRequest,
},
batch_size=batch_size,
concurrency=concurrency,
)
@dataclass(slots=True)
class BenchmarkResult:
mode: Mode
batch_size: int
concurrency: int
samples: int
elapsed_s: float
@property
def throughput(self) -> float:
return self.samples / self.elapsed_s if self.elapsed_s else 0.0
def show(self) -> None:
print("\n" + "=" * 60)
print(f"BENCHMARK - {self.mode}")
print("=" * 60)
print(f"Samples : {self.samples}")
print(f"Batch size : {self.batch_size}")
print(f"Concurrency : {self.concurrency}")
print(f"Time (s) : {self.elapsed_s:.2f}")
print(f"Throughput : {self.throughput:.2f} req/s")
print("=" * 60)
def build_single_vllm_engine_processor(
batch_size: int,
concurrency: int,
model: str,
sampling_params: dict = VLLM_SAMPLING_PARAMS,
pipeline_parallel_size: int = None,
tensor_parallel_size: int = None,
distributed_executor_backend: str = None,
):
"""Build vLLM engine processor for single-turn benchmark."""
config = _build_vllm_engine_config(
model,
batch_size,
concurrency,
pipeline_parallel_size,
tensor_parallel_size,
distributed_executor_backend,
)
return build_processor(
config,
preprocess=lambda row: dict(
prompt=row["prompt"],
sampling_params=sampling_params,
),
postprocess=lambda row: row,
)
def build_shared_vllm_engine_processor(
batch_size: int,
concurrency: int,
model: str,
sampling_params: dict = VLLM_SAMPLING_PARAMS,
pipeline_parallel_size: int = None,
tensor_parallel_size: int = None,
distributed_executor_backend: str = None,
):
"""Build vLLM engine processor for multi-turn benchmark."""
config = _build_vllm_engine_config(
model,
batch_size,
concurrency,
pipeline_parallel_size,
tensor_parallel_size,
distributed_executor_backend,
)
processor1 = build_processor(
config,
preprocess=lambda row: dict(
prompt=row["prompt"],
sampling_params=sampling_params,
),
postprocess=lambda row: {
"prompt": row["generated_text"]
if str(row.get("generated_text", "")).strip()
else row["prompt"]
},
)
processor2 = build_processor(
config,
preprocess=lambda row: dict(
prompt=row["prompt"],
sampling_params=sampling_params,
),
postprocess=lambda row: row,
)
def multi_turn_processor(dataset):
return processor2(processor1(dataset))
return multi_turn_processor
def build_classify_processor(
batch_size: int,
concurrency: int,
model: str,
tokenization_kwargs: dict = CLASSIFY_TOKENIZATION_KWARGS_DEFAULT,
max_model_len: int = 512,
distributed_executor_backend: str = None,
):
"""Build vLLM engine processor for classification benchmark."""
engine_kwargs = VLLM_ENGINE_KWARGS.copy()
if distributed_executor_backend is not None:
engine_kwargs["distributed_executor_backend"] = distributed_executor_backend
# Truncate prompts to max_model_len to avoid errors on long inputs.
tokenization_kwargs = {**tokenization_kwargs, "max_length": max_model_len}
config = vLLMEngineProcessorConfig(
model_source=model,
task_type="classify",
batch_size=batch_size,
concurrency=concurrency,
chat_template_stage=ChatTemplateStageConfig(enabled=False),
tokenize_stage=TokenizerStageConfig(enabled=True),
detokenize_stage=DetokenizeStageConfig(enabled=False),
engine_kwargs=engine_kwargs,
)
return build_processor(
config,
preprocess=lambda row: dict(
prompt=row["prompt"],
tokenization_kwargs=tokenization_kwargs,
),
postprocess=lambda row: {
"probs": float(row["embeddings"][0])
if row.get("embeddings") is not None and len(row["embeddings"]) > 0
else None,
},
)
def setup_serve_deployment(model: str, concurrency: int) -> tuple[str, str]:
"""Set up Ray Serve deployment for hosting the LLM model."""
deployment_name = "benchmark_deployment"
app_name = "benchmark_app"
llm_config = LLMConfig(
model_loading_config=ModelLoadingConfig(
model_id=model,
model_source=model,
),
deployment_config=dict(
name=deployment_name,
# To fairly compare with vLLM engine processor, fix the number of replicas to the concurrency level
autoscaling_config=dict(
min_replicas=concurrency,
max_replicas=concurrency,
),
),
engine_kwargs=dict(
enable_prefix_caching=True,
enable_chunked_prefill=True,
max_num_batched_tokens=4096,
),
)
override_serve_options = dict(name=deployment_name)
llm_app = build_llm_deployment(
llm_config, override_serve_options=override_serve_options
)
serve.run(llm_app, name=app_name)
print("Waiting for Serve deployment to be ready...")
max_wait_time = 120 # seconds
wait_time = 0
while not _is_app_ready(app_name) and wait_time < max_wait_time:
sleep(5)
wait_time += 5
if wait_time >= max_wait_time:
raise TimeoutError("Deployment failed to become ready within timeout")
print("Deployment is ready!")
return deployment_name, app_name
def _is_app_ready(app_name: str) -> bool:
try:
serve_status = serve.status()
if app_name in serve_status.applications:
app_status = serve_status.applications[app_name]
if app_status.status == "RUNNING":
print(f"Application '{app_name}' is RUNNING.")
return True
else:
print(f"Application '{app_name}' status: {app_status.status}")
return False
else:
print(f"Application '{app_name}' not found in Serve status.")
return False
except Exception as e:
print(f"Error checking app status: {e}")
return False
def build_single_serve_deployment_processor(
batch_size: int,
concurrency: int,
model: str,
sampling_params: dict = VLLM_SAMPLING_PARAMS,
deployment_name: str = None,
app_name: str = None,
**kwargs,
):
"""Build Serve deployment processor for single-turn benchmark."""
config = _build_serve_deployment_config(
batch_size,
concurrency,
deployment_name,
app_name,
)
return build_processor(
config,
preprocess=lambda row: dict(
method="completions",
dtype="CompletionRequest",
request_kwargs=dict(
model=model,
prompt=row["prompt"],
**sampling_params,
),
),
postprocess=lambda row: row,
)
def build_shared_serve_deployment_processor(
batch_size: int,
concurrency: int,
model: str,
sampling_params: dict = VLLM_SAMPLING_PARAMS,
deployment_name: str = None,
app_name: str = None,
**kwargs,
):
"""Build Serve deployment processor for multi-turn benchmark."""
config = _build_serve_deployment_config(
batch_size,
concurrency,
deployment_name,
app_name,
)
processor1 = build_processor(
config,
preprocess=lambda row: dict(
method="completions",
dtype="CompletionRequest",
request_kwargs=dict(
model=model,
prompt=row["prompt"],
stream=False,
),
),
postprocess=lambda row: {
# Fall back to original prompt if generated text is empty
"prompt": (
row["choices"][0]["text"]
if row.get("choices") and str(row["choices"][0].get("text", "")).strip()
else row["prompt"]
)
},
)
processor2 = build_processor(
config,
preprocess=lambda row: dict(
method="completions",
dtype="CompletionRequest",
request_kwargs=dict(
model=model,
prompt=row["prompt"],
stream=False,
),
),
postprocess=lambda row: row,
)
def multi_turn_processor(dataset):
return processor2(processor1(dataset))
return multi_turn_processor
# -----------------------------------------------------------------------------
# Benchmark execution
# -----------------------------------------------------------------------------
def run_processor(
mode: Mode,
dataset: data.Dataset,
builder,
**kwargs,
) -> BenchmarkResult:
processor = builder(**kwargs)
total_samples = dataset.count()
start = perf_counter()
processor(dataset).materialize()
elapsed = perf_counter() - start
return BenchmarkResult(
mode=mode,
batch_size=kwargs.get("batch_size"),
concurrency=kwargs.get("concurrency"),
samples=total_samples,
elapsed_s=elapsed,
)
def benchmark(
mode: Mode,
dataset: data.Dataset,
*,
batch_size: int,
concurrency: int,
model: str,
sampling_params: dict = VLLM_SAMPLING_PARAMS,
pipeline_parallel_size: int = None,
tensor_parallel_size: int = None,
distributed_executor_backend: str = None,
) -> BenchmarkResult:
mode_to_builder = {
Mode.VLLM_ENGINE: build_single_vllm_engine_processor,
Mode.SHARED_VLLM_ENGINE: build_shared_vllm_engine_processor,
Mode.SERVE_DEPLOYMENT: build_single_serve_deployment_processor,
Mode.SHARED_SERVE_DEPLOYMENT: build_shared_serve_deployment_processor,
Mode.CLASSIFY: build_classify_processor,
}
if mode not in mode_to_builder:
raise ValueError(f"Unknown benchmark mode: {mode}")
builder = mode_to_builder[mode]
if mode in [Mode.SERVE_DEPLOYMENT, Mode.SHARED_SERVE_DEPLOYMENT]:
deployment_name, app_name = setup_serve_deployment(model, concurrency)
try:
return run_processor(
mode,
dataset,
builder,
batch_size=batch_size,
concurrency=concurrency,
model=model,
sampling_params=sampling_params,
deployment_name=deployment_name,
app_name=app_name,
)
finally:
serve.delete(app_name)
elif mode == Mode.CLASSIFY:
return run_processor(
mode,
dataset,
builder,
batch_size=batch_size,
concurrency=concurrency,
model=model,
distributed_executor_backend=distributed_executor_backend,
)
else:
return run_processor(
mode,
dataset,
builder,
batch_size=batch_size,
concurrency=concurrency,
model=model,
sampling_params=sampling_params,
pipeline_parallel_size=pipeline_parallel_size,
tensor_parallel_size=tensor_parallel_size,
distributed_executor_backend=distributed_executor_backend,
)
# -----------------------------------------------------------------------------
# CLI
# -----------------------------------------------------------------------------
def parse_args(argv: list[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(description="vLLM throughput benchmark")
parser.add_argument(
"--mode",
choices=[mode.value for mode in Mode],
default=Mode.VLLM_ENGINE.value,
help="Ray Data LLM processor to run benchmarks for",
)
# Dataset configuration
parser.add_argument(
"--dataset-path",
type=str,
default="/home/ubuntu/datasets/Code-feedback-sharegpt-renamed",
help="Path to dataset on disk",
)
parser.add_argument(
"--num-prompts", type=int, default=1000, help="Number of prompts to process"
)
parser.add_argument(
"--hf-dataset-id",
type=str,
default="Crystalcareai/Code-feedback-sharegpt-renamed",
help="Hugging Face dataset ID to download",
)
parser.add_argument(
"--hf-split",
type=str,
default="train",
help="Hugging Face dataset split to load",
)
parser.add_argument(
"--seed",
type=int,
default=0,
help="Random seed for dataset sampling",
)
parser.add_argument(
"--truncate-prompt",
type=int,
default=512,
help="Maximum prompt length",
)
# Engine configuration
parser.add_argument(
"--model",
type=str,
required=True,
help="LLM model to use",
)
parser.add_argument(
"--pipeline-parallel-size",
type=int,
default=1,
help="Pipeline parallel size for vLLM engine",
)
parser.add_argument(
"--tensor-parallel-size",
type=int,
default=1,
help="Tensor parallel size for vLLM engine",
)
parser.add_argument(
"--distributed-executor-backend",
type=str,
default=None,
choices=["ray", "mp", "uni"],
help="Distributed executor backend for vLLM engine",
)
parser.add_argument(
"--max-tokens",
type=int,
default=None,
help="Maximum number of tokens to generate per request (default: 100)",
)
# Ray Data worker configuration
parser.add_argument(
"--batch-size",
type=int,
required=True,
help="Ray Data batch size for processing",
)
parser.add_argument(
"--concurrency", type=int, required=True, help="Ray Data concurrency level"
)
return parser.parse_args(argv)
def main() -> None:
args = parse_args(sys.argv[1:])
ray.init()
try:
dataset = ShareGPTDataset(
dataset_path=args.dataset_path,
seed=args.seed,
hf_dataset_id=args.hf_dataset_id,
hf_split=args.hf_split,
truncate_prompt=args.truncate_prompt,
)
prompts = dataset.sample(args.num_prompts)
dataset = data.from_items(prompts)
sampling_params = VLLM_SAMPLING_PARAMS.copy()
if args.max_tokens is not None:
sampling_params["max_tokens"] = args.max_tokens
result = benchmark(
Mode(args.mode),
dataset,
batch_size=args.batch_size,
concurrency=args.concurrency,
model=args.model,
sampling_params=sampling_params,
pipeline_parallel_size=args.pipeline_parallel_size,
tensor_parallel_size=args.tensor_parallel_size,
distributed_executor_backend=args.distributed_executor_backend,
)
result.show()
finally:
ray.shutdown()
if __name__ == "__main__":
main()
@@ -0,0 +1,175 @@
"""
This module defines a dataset framework for sampling benchmark requests.
"""
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Dict, List, Optional
from datasets import load_dataset, load_from_disk
class BenchmarkDataset(ABC):
DEFAULT_RANDOM_SEED = 0
def __init__(
self,
dataset_path: Optional[str] = None,
random_seed: int = DEFAULT_RANDOM_SEED,
) -> None:
"""
Abstract base class for benchmark datasets.
All benchmark datasets should inherit from this class and implement
the required abstract methods.
Args:
dataset_path: The path to the dataset on disk.
random_seed: The seed for the random number generator.
"""
self._dataset_path = dataset_path
self._random_seed = random_seed
@abstractmethod
def load_data(self) -> None:
"""
Load data from the dataset source into memory.
Raises:
NotImplementedError: If the method is not implemented in subclasses.
"""
raise NotImplementedError("load_data must be implemented in subclasses.")
@abstractmethod
def sample(self, num_requests: int) -> List[Dict]:
"""
Sample prompts from the loaded dataset.
Args:
num_requests: The number of prompts to sample from the dataset.
Returns:
A list of sampled request dictionaries.
Raises:
NotImplementedError: If the method is not implemented in subclasses.
"""
raise NotImplementedError("sample must be implemented in subclasses.")
class ShareGPTDataset(BenchmarkDataset):
"""Implements the ShareGPT dataset. The first human message of each conversation is used to build a prompt."""
def __init__(
self,
dataset_path: str,
seed: int,
hf_dataset_id: str = "Crystalcareai/Code-feedback-sharegpt-renamed",
hf_split: str = "train",
truncate_prompt: Optional[int] = None,
) -> None:
"""
Initializes the ShareGPTDataset.
Args:
dataset_path: The path to the dataset on disk.
seed: The seed for the random number generator.
hf_dataset_id: The Hugging Face dataset ID to download if the dataset is not found on disk.
hf_split: The Hugging Face split to load from the dataset.
truncate_prompt: Maximum prompt length so that the prompt fits in the model's context window.
"""
super().__init__(dataset_path, seed)
self._seed = seed
self._hf_dataset_id = hf_dataset_id
self._hf_split = hf_split
self._truncate_prompt = truncate_prompt
self._data: list[Dict] | None = None
def load_data(self) -> None:
"""Load data from the dataset path into memory."""
if self._data is None:
self._data = self._load_dataset_data()
def sample(self, num_requests: int) -> List[Dict]:
"""Sample prompts from the loaded dataset."""
if self._data is None:
self.load_data()
# Extract all valid prompts from the dataset
all_prompts = []
for item in self._data:
prompt_data = self._extract_prompt(item)
if prompt_data is not None:
all_prompts.append(prompt_data)
if not all_prompts:
raise ValueError("ShareGPT dataset yielded no usable prompts")
# Replicate samples if num_requests exceeds available samples
if num_requests <= len(all_prompts):
return all_prompts[:num_requests]
full_copies = num_requests // len(all_prompts)
remainder = num_requests % len(all_prompts)
prompts = all_prompts * full_copies + all_prompts[:remainder]
return prompts
def _load_dataset(self):
"""Load dataset from disk or Hugging Face."""
path = Path(self._dataset_path)
print(f"Attempting to load dataset from {path}")
print(f"Dataset exists on disk: {path.exists()}")
try:
if path.exists():
dataset = load_from_disk(str(path))
else:
print(
f"Dataset not found on disk, downloading from Hugging Face: {self._hf_dataset_id}"
)
path.parent.mkdir(parents=True, exist_ok=True)
dataset = load_dataset(self._hf_dataset_id, split=self._hf_split)
dataset.save_to_disk(str(path))
return dataset
except Exception as e:
raise RuntimeError(f"Error loading ShareGPT dataset: {e}")
def _load_dataset_data(self) -> List[Dict]:
"""Load and process dataset data into a list of dictionaries."""
ds = self._load_dataset().shuffle(seed=self._seed)
data = []
for i, row in enumerate(ds):
data.append(row)
print(f"Loaded {len(data)} samples from dataset")
return data
def _extract_prompt(self, item: Dict) -> Dict | None:
"""
Extracts the first human message of a conversation or None.
The ShareGPT schema uses {"role": "human", "value": ...} for user
turns.
"""
messages = item.get("messages") or item.get("conversations") or []
prompt = next(
(
str(msg.get("value", "")).strip()
for msg in messages
if msg.get("role") in {"human", "user"}
),
None,
)
# Only return a valid prompt if it's not empty
if prompt and prompt.strip():
if self._truncate_prompt:
prompt = prompt[: self._truncate_prompt]
return {"prompt": prompt}
return None