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

315 lines
12 KiB
Python

# Copyright (c) ModelScope Contributors. All rights reserved.
import asyncio
import concurrent.futures
import os
from queue import Queue
from threading import Thread
from tqdm import tqdm
from typing import Any, Dict, Iterator, List, Optional, Union
from swift.metrics import Metric
from swift.model import get_ckpt_dir
from swift.template import Template, get_template
from swift.utils import Processor, ProcessorMixin, get_logger
from .base import BaseInferEngine
from .protocol import (ChatCompletionMessageToolCall, ChatCompletionResponse, ChatCompletionStreamResponse,
InferRequest, RequestConfig, UsageInfo)
logger = get_logger()
class InferEngine(BaseInferEngine, ProcessorMixin):
def __init__(self, template: Template):
processor = template.processor
self.template = template
self.template_type = template.template_meta.template_type
self.processor = processor
self.model_info = processor.model_info
self.model_meta = processor.model_meta
self.model_dir = self.model_info.model_dir
self.model_name = self.model_info.model_name
self.max_model_len = self.model_info.max_model_len
self.task_type = self.model_info.task_type
self.config = self.model_info.config
self.max_tokens_offset = 0
def _get_template(self, processor: Processor, template_type: Optional[str] = None):
ckpt_dir = get_ckpt_dir(processor.model_info.model_dir, getattr(self, 'adapters', None))
logger.info('Create the template for the infer_engine')
if ckpt_dir:
from swift.arguments import BaseArguments
args = BaseArguments.from_pretrained(ckpt_dir)
template = args.get_template(processor)
else:
template = get_template(processor, template_type=template_type)
return template
def _get_stop_words(self, stop_words: List[Union[str, List[int], None]]) -> List[str]:
stop: List[str] = []
for stop_word in stop_words:
if stop_word is None:
continue
elif isinstance(stop_word, list):
stop_word = self.tokenizer.decode(stop_word)
assert isinstance(stop_word, str)
if stop_word not in stop:
stop.append(stop_word)
return stop
def _get_stop_token_ids(self, stop_words: List[Union[str, List[int], None]]) -> List[int]:
stop_token_ids: List[int] = []
for stop_word in stop_words:
if stop_word is None:
continue
if isinstance(stop_word, str):
stop_word = self.tokenizer.encode(stop_word, add_special_tokens=False)
if isinstance(stop_word, list):
if len(stop_word) != 1:
continue
else:
stop_token = stop_word[0]
elif isinstance(stop_word, int):
stop_token = stop_word
assert isinstance(stop_token, int)
if stop_token not in stop_token_ids:
stop_token_ids.append(stop_token)
return stop_token_ids
def async_iter_to_iter(self, async_iter, prog_bar, metrics) -> Iterator:
queue = Queue()
async def _run_async_iter():
try:
async for item in await async_iter:
queue.put(item)
except Exception as e:
if getattr(self, 'strict', True):
raise
queue.put(e)
else:
queue.put(None)
try:
loop = asyncio.get_event_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
thread = Thread(target=lambda: loop.run_until_complete(_run_async_iter()))
thread.start()
pre_output = None
while True:
output = queue.get()
if output is None or isinstance(output, Exception):
prog_bar.update()
self._update_metrics(pre_output, metrics)
return
pre_output = output
yield output
@staticmethod
async def batch_run(tasks):
return await asyncio.gather(*tasks)
def _batch_infer_stream(
self,
tasks,
stream: bool = True,
use_tqdm: bool = True,
metrics: Optional[List[Metric]] = None
) -> List[Union[ChatCompletionResponse, Iterator[ChatCompletionStreamResponse]]]:
prog_bar = tqdm(total=len(tasks), dynamic_ncols=True, disable=not use_tqdm)
if stream:
return [self.async_iter_to_iter(task, prog_bar, metrics) for task in tasks]
else:
async def _new_run(task):
try:
res = await task
except Exception as e:
if getattr(self, 'strict', True):
raise
res = e
prog_bar.update()
self._update_metrics(res, metrics)
return res
new_tasks = [_new_run(task) for task in tasks]
try:
loop = asyncio.get_event_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
return loop.run_until_complete(self.batch_run(new_tasks))
@staticmethod
def _get_usage_info(num_prompt_tokens: int, num_generated_tokens: int) -> UsageInfo:
return UsageInfo(
prompt_tokens=num_prompt_tokens,
completion_tokens=num_generated_tokens,
total_tokens=num_prompt_tokens + num_generated_tokens,
)
@staticmethod
def _update_usage_info(origin_use_info: UsageInfo, num_generated_tokens: int) -> UsageInfo:
return UsageInfo(
prompt_tokens=origin_use_info.prompt_tokens,
completion_tokens=origin_use_info.completion_tokens + num_generated_tokens,
total_tokens=origin_use_info.total_tokens + num_generated_tokens,
)
@staticmethod
def _update_metrics(result, metrics: Optional[List[Metric]] = None):
if metrics is None:
return result
result_origin = result
if not isinstance(result, (list, tuple)):
result = [result]
for response in result:
if response is None or isinstance(response, Exception):
continue
for metric in metrics:
metric.update(response)
return result_origin
def infer(self,
infer_requests: List[InferRequest],
request_config: Optional[RequestConfig] = None,
metrics: Optional[List[Metric]] = None,
*,
use_tqdm: Optional[bool] = None,
**kwargs) -> List[Union[ChatCompletionResponse, Iterator[ChatCompletionStreamResponse]]]:
if request_config is None:
request_config = RequestConfig()
tasks = [self.infer_async(infer_request, request_config, **kwargs) for infer_request in infer_requests]
if use_tqdm is None:
use_tqdm = not request_config.stream and len(infer_requests) > 1
return self._batch_infer_stream(tasks, request_config.stream, use_tqdm, metrics)
def _get_toolcall(self, response: str) -> Optional[List[ChatCompletionMessageToolCall]]:
try:
functions = self.template.agent_template.get_toolcall(response)
except Exception:
functions = None
if functions:
return [ChatCompletionMessageToolCall(function=function) for function in functions]
@staticmethod
def _get_num_tokens(inputs: Dict[str, Any]) -> int:
if 'input_ids' in inputs: # 1d or 2d
input_ids = inputs['input_ids']
if isinstance(input_ids, list):
return len(input_ids)
else:
return input_ids.shape[-1]
elif 'inputs_embeds' in inputs: # 2d or 3d
return inputs['inputs_embeds'].shape[-2]
raise ValueError(f'Unable to retrieve input_ids and inputs_embeds. inputs: {inputs}')
def set_default_max_tokens(self, request_config: RequestConfig, inputs: Dict[str, Any]) -> None:
max_model_len = self.max_model_len
assert isinstance(inputs, dict)
# The num_tokens takes the maximum value from inputs_list.
num_tokens = self._get_num_tokens(inputs)
max_tokens = request_config.max_tokens
if max_model_len is None:
max_model_len = 8192
logger.warning(
'The current model is unable to retrieve `max_model_len`. It is set to the default value of 8192.')
max_max_tokens = max_model_len - num_tokens + self.max_tokens_offset
if max_tokens is None:
request_config.max_tokens = max_max_tokens
elif max_max_tokens < request_config.max_tokens:
logger.warning(f'max_model_len({max_model_len}) - num_tokens({num_tokens}) < max_tokens({max_tokens}). '
f'Setting max_tokens: {max_model_len - num_tokens}')
request_config.max_tokens = max_max_tokens
def _get_logprobs(self,
logprobs_list: Optional[List[Dict[int, float]]],
token_ids: List[int],
top_logprobs: Optional[int] = None) -> Optional[Dict[str, Any]]:
if logprobs_list is None or len(token_ids) == 0:
return None
if len(token_ids) > 0:
logprobs_list = logprobs_list[-len(token_ids):]
res = []
for logprobs, token_id in zip(logprobs_list, token_ids):
token = self.tokenizer.decode(token_id)
_res = {'token': token, 'logprob': logprobs[token_id], 'bytes': list(token.encode('utf8'))}
if top_logprobs is not None:
logprobs = {k: logprobs[k] for k in sorted(logprobs, key=lambda k: -logprobs[k])[:top_logprobs]}
res_top_logprobs = []
for k, logprob in logprobs.items():
if logprob == float('-inf'):
continue
token = self.tokenizer.decode(k)
res_top_logprobs.append({'token': token, 'logprob': logprob, 'bytes': list(token.encode('utf8'))})
_res['top_logprobs'] = res_top_logprobs
res.append(_res)
return {'content': res}
@staticmethod
def _get_finish_reason(max_tokens: int, completion_tokens: int, is_finished: bool):
if is_finished:
if completion_tokens >= max_tokens:
finish_reason = 'length'
else:
finish_reason = 'stop'
else:
finish_reason = None
return finish_reason
@staticmethod
def thread_run(target, args=(), kwargs=None):
kwargs = kwargs or {}
def func(target, queue, args, kwargs):
try:
queue.put(target(*args, **kwargs))
except Exception as e:
queue.put(e)
queue = Queue()
thread = Thread(target=func, args=(target, queue, args, kwargs))
thread.start()
thread.join()
result = queue.get()
if isinstance(result, Exception):
raise result
return result
@staticmethod
def safe_asyncio_run(coro):
def asyncio_run(core):
return asyncio.run(core)
return InferEngine.thread_run(asyncio_run, args=(coro, ))
def _batch_encode(self, infer_requests: List[InferRequest], strict: bool):
max_workers = max(min(32, os.cpu_count(), len(infer_requests)), 1)
error_list = []
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = [
executor.submit(self.template.encode, infer_request, return_template_inputs=True)
for infer_request in infer_requests
]
concurrent.futures.wait(futures)
batched_inputs = []
for i, future in enumerate(futures):
try:
batched_inputs.append(future.result())
except Exception as e:
if strict:
raise
error_list.append((i, e))
continue
return batched_inputs, error_list
@staticmethod
def _add_error_list(outputs, error_list):
for i, error in error_list:
outputs.insert(i, error)
return outputs