Files
wehub-resource-sync 2aaeece67c
Codestyle Check / Lint (push) Has been cancelled
Codestyle Check / Check bypass (push) Has been cancelled
Pipelines-Test / Pipelines-Test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:37:14 +08:00

392 lines
16 KiB
Python

# Copyright (c) 2025 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import argparse
import asyncio
import csv
import json
import math
import time
from dataclasses import dataclass, field
from itertools import cycle
from pathlib import Path
from typing import List, Tuple
import pandas as pd
from openai import AsyncOpenAI
from tqdm import tqdm
from utils import RangeSet
from paddlenlp.transformers import AutoTokenizer
from paddlenlp.utils.log import logger
@dataclass
class RequestPayload:
prompt: str = ""
num_responses: int = 8
idx: int = 0
@dataclass
class ResponsePayload:
idx: int = 0
question: str = ""
question_token_length: int = 0
responses: List[str] = field(default_factory=list)
elapsed_times: List[float] = field(default_factory=list)
token_lengths: List[int] = field(default_factory=list)
total_length: int = 0
class ApiTask:
def __init__(self, args):
self.args = args
self.model = args.model
self.tokenzizer = AutoTokenizer.from_pretrained(self.args.tokenizer, use_fast=True)
self.clients = cycle(
AsyncOpenAI(base_url=url, api_key=api) for url, api in zip(args.openai_urls, args.api_keys)
)
self._max_concurrency = args.max_concurrency
self.semaphore = asyncio.Semaphore(args.max_concurrency)
self.output_dir = Path(self.args.output_dir)
self.global_stats_path = self.output_dir / "global_stats.csv"
self.dispersed_stats_path = self.output_dir / "dispersed_stats.csv"
self.rollout_details_path = self.output_dir / "rollout_details.jsonl"
self.status_file_path = self.output_dir / "status.txt"
self._load_status()
def tokenize(self, response: ResponsePayload) -> ResponsePayload:
question = response.question
responses = response.responses
response.question_token_length = len(self.tokenzizer(question).input_ids)
for i, resp in enumerate(responses):
tokens = self.tokenzizer(resp).input_ids
length = len(tokens)
response.token_lengths.append(length)
response.total_length += length
return response
def get_active_tasks_count(self) -> int:
return self._max_concurrency - self.semaphore._value
def get_client(self) -> AsyncOpenAI:
# Returns an AsyncOpenAI client instance
return next(self.clients)
def _save_status(self, batch_index):
"""Save current processing status to file"""
self.processed_set.add(batch_index)
content = self.processed_set.to_file_format()
with open(self.status_file_path, "w", encoding="utf-8") as f:
f.write(content)
def _load_status(self):
"""Load processing status from file"""
try:
with open(self.status_file_path, "r", encoding="utf-8") as f:
content = f.read().strip()
self.processed_set = RangeSet.from_file(content)
logger.info(f"Resumed processed ranges: {self.processed_set.to_file_format()}")
except FileNotFoundError:
self.processed_set = RangeSet([])
def process_data(self, file_path: str) -> pd.DataFrame:
logger.info(f"Processing data from {file_path}...")
start_time = time.time()
df = pd.read_parquet(file_path)
if self.args.limit_rows != -1:
df = df.iloc[: self.args.limit_rows]
logger.info(f"Loaded {len(df)} samples in {time.time() - start_time:.2f}s")
return df
def batch_process(self, dataframe: pd.DataFrame):
batch_prompts = []
for idx, prompt in enumerate(dataframe[self.args.prompt_key]):
batch_prompts.append(
RequestPayload(prompt=prompt[0]["content"], idx=idx, num_responses=self.args.rollout_n)
)
if len(batch_prompts) == self.args.rollout_input_batch_size:
yield batch_prompts
batch_prompts = []
async def fastdeploy_call(self, request: RequestPayload) -> Tuple[str, float]:
client = self.get_client()
try:
async with self.semaphore:
start_time = time.time()
response = await client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": request.prompt}],
temperature=self.args.temperature,
top_p=self.args.top_p,
max_tokens=self.args.max_response_length,
n=1,
stream=True,
timeout=60*60,
metadata={
"training": True,
"raw_request": False,
}
)
# Streaming text is stored in a list of chunks
chunks = []
# Streaming responses
async for chunk in response:
delta = chunk.choices[0].delta
if delta and delta.content:
chunks.append(delta.content)
text = "".join(chunks)
end_time = time.time()
elapsed_time = end_time - start_time
logger.debug("Streaming response took %.2f seconds", elapsed_time)
return text, round(elapsed_time, 2)
except Exception as e:
logger.error("Error while streaming: %s", e)
raise ValueError(e)
async def vllm_call(self, request: RequestPayload) -> Tuple[str, float]:
client = self.get_client()
try:
async with self.semaphore:
start_time = time.time()
response = await client.completions.create(
model=self.model,
prompt=request.prompt,
temperature=self.args.temperature,
top_p=self.args.top_p,
max_tokens=self.args.max_dec_len,
n=1,
stream=True,
)
# Streaming text is stored in a list of chunks
chunks = []
# Streaming responses
async for chunk in response:
if chunk.choices and chunk.choices[0].text:
chunks.append(chunk.choices[0].text)
text = "".join(chunks)
end_time = time.time()
elapsed_time = end_time - start_time
logger.debug("Streaming response took %.2f seconds", elapsed_time)
return text, round(elapsed_time, 2)
except Exception as e:
logger.error("Error while streaming: %s", e)
raise ValueError(e)
async def group_call(self, request: RequestPayload) -> ResponsePayload:
"""Performs n complete token generation rollouts for the given query."""
if self.args.use_fastdeploy == "true":
call = self.fastdeploy_call
else:
call = self.vllm_call
tasks = [call(request) for _ in range(request.num_responses)]
result = ResponsePayload()
result.idx = request.idx
result.question = request.prompt
for task, elapsed_time in await asyncio.gather(*tasks):
result.responses.append(task)
result.elapsed_times.append(elapsed_time)
return result
async def batch_call(self, requests: List[RequestPayload]) -> Tuple[List[ResponsePayload], int]:
"""Batch execution requests"""
start_time = time.time()
batch_results = await asyncio.gather(*[self.group_call(request) for request in requests])
end_time = time.time()
batch_elapsed_time = end_time - start_time
logger.debug("total batch took %.2f seconds", batch_elapsed_time)
return batch_results, batch_elapsed_time
def dispersed_stats(self, responses: List[ResponsePayload], batch_elapsed_time: float, batch_index):
batch_group_pd = pd.DataFrame(responses)
dispersed_stats_dict = {
"batch_index": batch_index,
"rollout_lengths": batch_group_pd["token_lengths"].to_list(),
"min_length": batch_group_pd["token_lengths"].apply(lambda x: min(x)).tolist(),
"max_length": batch_group_pd["token_lengths"].apply(lambda x: max(x)).tolist(),
"avg_length": batch_group_pd["token_lengths"].apply(lambda x: sum(x) / len(x)).tolist(),
"completion_time": batch_elapsed_time,
"throughput_tokens_per_sec": batch_group_pd["token_lengths"].apply((lambda x: sum(x))).sum()
/ batch_elapsed_time,
}
return dispersed_stats_dict
def global_stats(self, responses: List[ResponsePayload], batch_elapsed_time: float, batch_index):
dispersed_stats_dict = self.dispersed_stats(responses, batch_elapsed_time, batch_index)
total_response_tokens = 0
for lengths in dispersed_stats_dict["rollout_lengths"]:
total_response_tokens += sum(lengths)
global_stats_dict = {}
global_stats_dict["batch_index"] = dispersed_stats_dict["batch_index"]
global_stats_dict["min_response_tokens"] = min(dispersed_stats_dict["min_length"])
global_stats_dict["max_response_tokens"] = max(dispersed_stats_dict["max_length"])
global_stats_dict["avg_response_tokens"] = total_response_tokens / (
self.args.rollout_n * self.args.rollout_input_batch_size
)
global_stats_dict["total_response_tokens"] = total_response_tokens
global_stats_dict["group_max_response_tokens"] = dispersed_stats_dict["max_length"]
global_stats_dict["completion_time"] = dispersed_stats_dict["completion_time"]
global_stats_dict["throughput_tokens_per_sec"] = dispersed_stats_dict["throughput_tokens_per_sec"]
return global_stats_dict, dispersed_stats_dict
def execute(self):
dataframe = self.process_data(self.args.input_file)
self.output_dir.mkdir(parents=True, exist_ok=True)
with open(self.global_stats_path, "a", newline="") as global_f, open(
self.dispersed_stats_path, "a", newline=""
) as dispersed_f, open(self.rollout_details_path, "a", encoding="utf-8") as jsonl_f:
global_writer = csv.writer(global_f)
dispersed_writer = csv.writer(dispersed_f)
if self.processed_set.processed_count <= 0:
global_writer.writerow(
[
"batch_index",
"min_response_tokens",
"max_response_tokens",
"avg_response_tokens",
"total_response_tokens",
"group_max_response_tokens",
"completion_time",
"throughput_tokens_per_sec",
]
)
dispersed_writer.writerow(
[
"batch_index",
"rollout_lengths",
"min_length",
"max_length",
"avg_length",
"completion_time",
"throughput_tokens_per_sec",
]
)
for batch_index, input_ids in tqdm(
enumerate(self.batch_process(dataframe)),
total=math.ceil(len(dataframe) / self.args.rollout_input_batch_size),
mininterval=0.1,
):
if self.processed_set.contains(batch_index):
continue
batch_results, batch_elapsed_time = asyncio.run(self.batch_call(input_ids))
for i in range(len(batch_results)):
batch_results[i] = self.tokenize(batch_results[i])
global_stats_dict, dispersed_stats_dict = self.global_stats(
batch_results, batch_elapsed_time, batch_index
)
global_writer.writerow(
[
batch_index,
global_stats_dict["min_response_tokens"],
global_stats_dict["max_response_tokens"],
round(global_stats_dict["avg_response_tokens"], 2),
global_stats_dict["total_response_tokens"],
global_stats_dict["group_max_response_tokens"],
round(global_stats_dict["completion_time"], 2),
round(global_stats_dict["throughput_tokens_per_sec"], 2),
]
)
dispersed_writer.writerow(
[
batch_index,
dispersed_stats_dict["rollout_lengths"],
dispersed_stats_dict["min_length"],
dispersed_stats_dict["max_length"],
dispersed_stats_dict["avg_length"],
round(dispersed_stats_dict["completion_time"], 2),
round(dispersed_stats_dict["throughput_tokens_per_sec"], 2),
]
)
record = [
{
"batch_index": batch_index,
"prompt_text": result.question,
"rollouts": [
{"response": res, "token_length": token_length}
for res, token_length in zip(result.responses, result.token_lengths)
],
"total_time": dispersed_stats_dict["completion_time"],
"throughput_tokens_per_sec": dispersed_stats_dict["throughput_tokens_per_sec"],
}
for result in batch_results
]
jsonl_f.write(json.dumps(record, ensure_ascii=False) + "\n")
global_f.flush()
dispersed_f.flush()
jsonl_f.flush()
self._save_status(batch_index)
def parse_args():
parser = argparse.ArgumentParser(description="Process prompts with OpenAI clients.")
parser.add_argument("--openai_urls", type=str, nargs="+", required=True, help="List of OpenAI service URLs")
parser.add_argument(
"--api_keys", type=str, nargs="+", default=None, help="List of API keys (default: 'NONE' for each service)"
)
parser.add_argument("--model", type=str, required=True, help="Model name (e.g., Qwen2.5-7B-Instruct-1M)")
parser.add_argument(
"--tokenizer", type=str, required=True, help="Tokenizer name (e.g., Qwen/Qwen2.5-7B-Instruct-1M)"
)
parser.add_argument("--rollout_input_batch_size", type=int, default=4, help="Batch size for requests")
parser.add_argument("--rollout_n", type=int, default=8, help="Number of responses per request")
parser.add_argument(
"--prompt_key", type=str, default="prompt", help="Key in the DataFrame for prompts (default: 'prompt')"
)
parser.add_argument("--input_file", type=str, required=True, help="Path to the input Parquet file")
parser.add_argument(
"--output_dir",
type=str,
default="./api_infer_results",
help="Directory for output CSV files (default: './api_infer_results')",
)
parser.add_argument("--top_p", type=float, default=0.9, help="Top-p sampling parameter for text generation")
parser.add_argument("--temperature", type=float, default=0.7, help="Temperature parameter for text generation")
parser.add_argument("--max_dec_len", type=int, default=1024 * 2, help="Maximum response length (in tokens)")
parser.add_argument("--max_concurrency", type=int, default=1000, help="Maximum concurrent connections")
parser.add_argument(
"--limit_rows", type=int, default=-1, help="Maximum number of rows to read from the dataset (-1 means all)"
)
parser.add_argument("--use_fastdeploy", type=str.lower, choices=["true", "false"], default="true", help="Engine selection (true=FastDeploy, false=vLLM, default: true)")
return parser.parse_args()
if __name__ == "__main__":
args = parse_args()
task = ApiTask(args)
task.execute()