Files
wehub-resource-sync a203934033
Lint test / lint (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:34:58 +08:00

268 lines
9.7 KiB
Python

"""
EvalScope integration utilities for ms-swift models.
This module provides a custom ModelAPI implementation that enables batch inference
for evaluation tasks using ms-swift's TransformersEngine. It implements an asynchronous
batch processing system to improve throughput when evaluating models.
"""
from concurrent.futures import Future
from dataclasses import dataclass
from evalscope.api.messages import ChatMessage as EvalChatMessage
from evalscope.api.model import GenerateConfig, ModelAPI, ModelOutput, ModelUsage
from evalscope.api.registry import register_model_api
from evalscope.api.tool import ToolChoice, ToolInfo
from evalscope.models.utils.openai import chat_choices_from_openai
from queue import Empty, Queue
from threading import Thread
from typing import Any, List, Optional, Tuple
from swift.infer_engine import InferRequest, RequestConfig, TransformersEngine
@dataclass
class BatchInferInput:
"""
Container for batch inference input data.
Holds all necessary information for a single inference request
that will be processed as part of a batch.
"""
ms_input: InferRequest # ms-swift format request
ms_config: RequestConfig # ms-swift format configuration
batch_size: int # desired batch size for this request
engine: TransformersEngine # inference engine to use
@dataclass
class _QueueItem:
"""
Internal queue item for batch processing.
Pairs a batch input with its corresponding future for result delivery.
"""
input: BatchInferInput
future: Future[ModelOutput] # will be resolved with the inference result
# Global variables for batch processing
# These maintain the shared batch processing infrastructure across all model instances
batch_thread: Optional[Thread] = None # background thread for processing batches
batch_queue: Queue[_QueueItem] = Queue() # queue of pending inference requests
@register_model_api('swift_custom')
class EvalModel(ModelAPI):
"""
Custom ModelAPI implementation for ms-swift models with batch inference support.
This class integrates ms-swift's TransformersEngine with EvalScope's evaluation framework,
providing efficient batch processing for improved evaluation throughput.
"""
def __init__(
self,
model_name: str,
base_url: Optional[str] = None,
api_key: Optional[str] = None,
config: GenerateConfig = GenerateConfig(),
**model_args: Any,
):
"""
Initialize the EvalModel with ms-swift backend.
Args:
model_name: Name of the model for identification
base_url: Not used in this implementation (for API compatibility)
api_key: Not used in this implementation (for API compatibility)
config: Generation configuration with batch settings
**model_args: Additional arguments including 'model' and 'template'
"""
super().__init__(
model_name=model_name,
base_url=base_url,
api_key=api_key,
config=config,
)
# Extract model-specific arguments from kwargs
# This pattern allows us to collect known arguments while preserving unknown ones
def collect_model_arg(name: str) -> Optional[Any]:
value = model_args.get(name, None)
if value is not None:
model_args.pop(name)
return value
# Extract required model parameters
self.model = collect_model_arg('model') # model path or identifier
self.template = collect_model_arg('template') # conversation template
self.max_batch_size = collect_model_arg('max_batch_size') # maximum batch size
# Initialize the inference engine with batch support
self.engine = TransformersEngine(self.model, template=self.template, max_batch_size=self.max_batch_size)
def generate(
self,
input: List[EvalChatMessage],
tools: List[ToolInfo],
tool_choice: ToolChoice,
config: GenerateConfig,
) -> ModelOutput:
"""
Generate model response using batch inference.
This method queues the request for batch processing and waits for the result.
The actual inference is performed asynchronously in a background thread.
Args:
input: List of chat messages forming the conversation
tools: Available tools for function calling (if supported)
tool_choice: Tool selection strategy
config: Generation configuration
Returns:
ModelOutput containing the generated response
"""
# Ensure the background batch processing thread is running
global batch_thread
if batch_thread is None:
batch_thread = Thread(target=_process_batches, daemon=True)
batch_thread.start()
# Convert EvalScope format to ms-swift format
ms_input = convert_request(input, tools)
ms_config = convert_config(config)
# Package the request for batch processing
batch_input = BatchInferInput(
ms_input=ms_input, ms_config=ms_config, batch_size=config.batch_size, engine=self.engine)
# Create a future to receive the result asynchronously
future = Future[ModelOutput]()
# Queue the request for batch processing
batch_queue.put(_QueueItem(input=batch_input, future=future))
# Block until the result is available
return future.result()
def _process_batches() -> None:
"""
Background thread function that processes batched inference requests.
This function runs continuously, collecting requests from the queue and
processing them in batches for improved efficiency. It uses a timeout-based
approach to balance between batch size and latency.
"""
while True:
# Collect requests from the queue until timeout or batch size limit
inputs: List[Tuple[BatchInferInput, Future[ModelOutput]]] = []
while True:
try:
# Wait for new requests with a 2-second timeout
item = batch_queue.get(timeout=2)
inputs.append((item.input, item.future))
# Check if we've reached the desired batch size
if len(inputs) == item.input.batch_size:
break # Process this batch now
except Empty:
# No more requests in queue, process what we have
break
# Skip processing if no requests were collected
if len(inputs) == 0:
continue
try:
# Prepare batch inputs for ms-swift inference
ms_inputs = [item[0].ms_input for item in inputs]
ms_config = inputs[0][0].ms_config # use first config for the batch
engine = inputs[0][0].engine # use first engine for the batch
# Perform batch inference using ms-swift engine
completions = engine.infer(ms_inputs, ms_config, use_tqdm=False)
# Process results and deliver them to waiting futures
for i, (batch_input, future) in enumerate(inputs):
completion = completions[i]
# Convert ms-swift response to EvalScope format
choices = chat_choices_from_openai(completion, tools=[])
result = ModelOutput(
model=completion.model,
choices=choices,
usage=(ModelUsage(
input_tokens=completion.usage.prompt_tokens,
output_tokens=completion.usage.completion_tokens,
total_tokens=completion.usage.total_tokens,
) if completion.usage else None),
)
# Deliver the result to the waiting caller
future.set_result(result)
except Exception as ex:
# If batch processing fails, propagate the error to all waiting futures
for _, future in inputs:
future.set_exception(ex)
def convert_config(config: GenerateConfig) -> RequestConfig:
"""
Convert EvalScope GenerateConfig to ms-swift RequestConfig.
Maps configuration parameters between the two frameworks, ensuring
compatibility while maintaining the same generation behavior.
Args:
config: EvalScope generation configuration
Returns:
RequestConfig: ms-swift compatible configuration
"""
return RequestConfig(
max_tokens=config.max_tokens,
temperature=config.temperature,
top_k=config.top_k,
top_p=config.top_p,
presence_penalty=config.presence_penalty,
frequency_penalty=config.frequency_penalty,
seed=config.seed,
stream=False, # batch processing doesn't support streaming
logprobs=config.logprobs,
top_logprobs=config.top_logprobs)
def convert_request(messages: List[EvalChatMessage], tools: List[ToolInfo]) -> InferRequest:
"""
Convert EvalScope request format to ms-swift InferRequest format.
Transforms the message and tool format from EvalScope's representation
to the format expected by ms-swift's inference engine.
Args:
messages: List of chat messages in EvalScope format
tools: List of available tools in EvalScope format
Returns:
InferRequest: ms-swift compatible request object
"""
# Convert tools to ms-swift format
tools_list = []
if len(tools) > 0:
tools_list = [tool.model_dump(exclude_none=True) for tool in tools]
# Convert messages to ms-swift format
ms_messages = []
for message in messages:
ms_messages.append(message.model_dump(exclude_none=True))
return InferRequest(
messages=ms_messages,
tools=tools_list,
)