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

351 lines
16 KiB
Python

# Copyright (c) ModelScope Contributors. All rights reserved.
import asyncio
import inspect
import lmdeploy
import os
import time
import torch
from contextlib import contextmanager
from copy import deepcopy
from lmdeploy import PytorchEngineConfig, TurbomindEngineConfig, VisionConfig, pipeline
from lmdeploy.api import autoget_backend_config
from lmdeploy.serve import async_engine
from packaging import version
from PIL import Image
from transformers import GenerationConfig
from transformers.utils.versions import require_version
from typing import Any, AsyncIterator, Dict, Iterator, List, Optional, Union
from swift.metrics import Metric
from swift.model import get_processor
from swift.template import Template
from swift.utils import get_logger, get_seed, safe_snapshot_download
from .infer_engine import InferEngine
from .patch import patch_auto_config, patch_auto_tokenizer
from .protocol import (ChatCompletionResponse, ChatCompletionResponseChoice, ChatCompletionResponseStreamChoice,
ChatCompletionStreamResponse, ChatMessage, DeltaMessage, InferRequest, RequestConfig)
from .utils import InferStreamer
try:
from lmdeploy import EngineGenerationConfig as LmdeployGenerationConfig
except ImportError:
# compat lmdeploy >= 0.6.*
from lmdeploy import GenerationConfig as LmdeployGenerationConfig
logger = get_logger()
class LmdeployEngine(InferEngine):
def __init__(
self,
model_id_or_path: str,
*,
template: Optional[Template] = None,
torch_dtype: Optional[torch.dtype] = None,
model_type: Optional[str] = None,
template_type: Optional[str] = None,
use_hf: Optional[bool] = None,
hub_token: Optional[str] = None,
revision: Optional[str] = None,
# engine_kwargs
tp: int = 1,
session_len: Optional[int] = None,
cache_max_entry_count: float = 0.8,
quant_policy: int = 0, # e.g. 4, 8
vision_batch_size: int = 1, # max_batch_size in VisionConfig
engine_kwargs: Optional[Dict[str, Any]] = None,
devices: Optional[List[int]] = None,
) -> None:
self.model_id_or_path = model_id_or_path
self.torch_dtype = torch_dtype
self.model_type = model_type
self.use_hf = use_hf
self.hub_token = hub_token
self.revision = revision
self.tp = tp
self.session_len = session_len
self.cache_max_entry_count = cache_max_entry_count
self.quant_policy = quant_policy
self.vision_batch_size = vision_batch_size
self.devices = devices
if template is None:
processor = self._get_processor()
template = self._get_template(processor, template_type=template_type)
else:
safe_snapshot_download(
model_id_or_path,
revision=revision,
download_model=True,
use_hf=use_hf,
ignore_patterns=getattr(template.model_meta, 'ignore_patterns', None),
hub_token=hub_token)
super().__init__(template)
if self.max_model_len is not None:
self.max_model_len -= 1
self._prepare_engine_kwargs(engine_kwargs)
self.config.torch_dtype = self.torch_dtype = self.torch_dtype or self.model_info.torch_dtype
self._prepare_engine()
self._load_generation_config()
def _get_processor(self):
return get_processor(
model_id_or_path=self.model_id_or_path,
torch_dtype=self.torch_dtype,
download_model=True,
model_type=self.model_type,
use_hf=self.use_hf,
hub_token=self.hub_token,
revision=self.revision)
def _prepare_engine_kwargs(self, engine_kwargs):
if engine_kwargs is None:
engine_kwargs = {}
engine_kwargs['tp'] = self.tp
engine_kwargs['session_len'] = self.session_len
engine_kwargs['cache_max_entry_count'] = self.cache_max_entry_count
engine_kwargs['quant_policy'] = self.quant_policy
if 'devices' in inspect.signature(TurbomindEngineConfig).parameters:
engine_kwargs['devices'] = self.devices
backend_config = TurbomindEngineConfig(**engine_kwargs)
backend_config = autoget_backend_config(self.model_dir, backend_config)
self.backend_config = backend_config
logger.info(f'backend_config: {backend_config}')
pipeline_kwargs = {}
is_multimodal = self.model_meta.is_multimodal
if is_multimodal:
require_version(
'lmdeploy<0.9', 'LmdeployEngine will no longer maintain inference for '
'multimodal models in lmdeploy>=0.9.')
vision_config = VisionConfig(max_batch_size=self.vision_batch_size)
pipeline_kwargs['vision_config'] = vision_config
logger.info(f'vision_config: {vision_config}')
self.pipeline_kwargs = pipeline_kwargs
@contextmanager
def _patch_pipeline(self):
_old_best_match_model = async_engine.best_match_model
def _best_match_model(*args, **kwargs) -> Optional[str]:
return self.model_info.model_type
async_engine.best_match_model = _best_match_model
try:
yield
finally:
async_engine.best_match_model = _old_best_match_model
def _prepare_engine(self):
with patch_auto_tokenizer(self.tokenizer), patch_auto_config(self.config), self._patch_pipeline():
engine = pipeline(self.model_dir, backend_config=self.backend_config, **self.pipeline_kwargs)
self.engine = engine
def _load_generation_config(self):
generation_config_path = os.path.join(self.model_dir, 'generation_config.json')
if os.path.isfile(generation_config_path):
generation_config = GenerationConfig.from_pretrained(self.model_dir)
kwargs = generation_config.to_dict()
max_new_tokens = kwargs.get('max_new_tokens')
if max_new_tokens is None:
kwargs.pop('max_new_tokens', None)
parameters = inspect.signature(LmdeployGenerationConfig).parameters
for k, v in kwargs.copy().items():
if k not in parameters or v is None:
kwargs.pop(k)
self.generation_config = LmdeployGenerationConfig(**kwargs)
else:
self.generation_config = LmdeployGenerationConfig()
def _add_stop_words(self, generation_config: LmdeployGenerationConfig, request_config: RequestConfig) -> None:
template_meta = self.template.template_meta
stop_words = (request_config.stop or []) + (self.generation_config.stop_words or []) + template_meta.stop_words
generation_config.stop_words = self._get_stop_token_ids(stop_words)
# compat lmdeploy >= 0.6.*
generation_config.stop_token_ids = generation_config.stop_words
def _prepare_generation_config(self, request_config: RequestConfig) -> LmdeployGenerationConfig:
kwargs = {'max_new_tokens': request_config.max_tokens}
for key in ['temperature', 'top_k', 'top_p', 'repetition_penalty']:
new_value = getattr(request_config, key)
if new_value is None:
kwargs[key] = getattr(self.generation_config, key)
else:
kwargs[key] = new_value
if request_config.seed is None:
request_config.seed = get_seed()
kwargs['random_seed'] = request_config.seed
if request_config.temperature == 0:
kwargs['temperature'] = 1 # avoid unnecessary process
kwargs['top_k'] = 1
if request_config.logprobs:
kwargs['logprobs'] = 1
if request_config.top_logprobs is not None:
kwargs['logprobs'] = max(1, request_config.top_logprobs)
res = LmdeployGenerationConfig(**kwargs)
return res
async def _infer_stream_async(
self,
inputs: Dict[str, Any],
generation_config: LmdeployGenerationConfig,
request_config: RequestConfig,
) -> AsyncIterator[ChatCompletionStreamResponse]:
session_id = time.time_ns()
kwargs = {'stream_output': True, 'gen_config': generation_config, 'sequence_start': True, 'sequence_end': True}
if version.parse(lmdeploy.__version__) >= version.parse('0.6.5'):
async with self.engine.model_inst(session_id) as inst:
context = self.engine.safe_run(inst, session_id, **inputs, **kwargs)
else:
context = self.engine.safe_run(session_id)
infer_streamer = InferStreamer(self.template, template_inputs=inputs['template_inputs'])
token_idx = 0
async with context as gen:
if version.parse(lmdeploy.__version__) < version.parse('0.6.5'):
generator = await self.engine.get_generator(False, session_id)
gen = generator.async_stream_infer(session_id=session_id, **inputs, **kwargs)
is_finished = False
while not is_finished:
try:
output = await gen.__anext__()
except StopAsyncIteration:
is_finished = True
delta_text = infer_streamer.get_printable_text(output.token_ids, is_finished)
if not delta_text and not is_finished:
continue
logprobs = self._get_logprobs(output.logprobs, output.token_ids[token_idx:],
request_config.top_logprobs)
token_idx = len(output.token_ids)
usage_info = self._get_usage_info(len(inputs['input_ids']), output.num_token)
toolcall = None
if is_finished:
toolcall = self._get_toolcall(
self.template.decode_generate_ids(output.token_ids, template_inputs=inputs['template_inputs']))
finish_reason = self._get_finish_reason(generation_config.max_new_tokens, output.num_token,
output.status.name == 'FINISH')
choices = [
ChatCompletionResponseStreamChoice(
index=0,
delta=DeltaMessage(role='assistant', content=delta_text, tool_calls=toolcall),
finish_reason=finish_reason,
logprobs=logprobs)
]
yield ChatCompletionStreamResponse(model=self.model_name, choices=choices, usage=usage_info)
async def _infer_full_async(
self,
inputs: Dict[str, Any],
generation_config: LmdeployGenerationConfig,
request_config: RequestConfig,
) -> ChatCompletionResponse:
session_id = time.time_ns()
kwargs = {'stream_output': False, 'gen_config': generation_config, 'sequence_start': True, 'sequence_end': True}
if version.parse(lmdeploy.__version__) >= version.parse('0.6.5'):
async with self.engine.model_inst(session_id) as inst:
async with self.engine.safe_run(inst, session_id, **inputs, **kwargs) as gen:
async for output in gen:
pass
if self.engine.backend == 'pytorch':
# manually end pytorch session
await inst.async_end(session_id)
else:
async with self.engine.safe_run(session_id):
generator = await self.engine.get_generator(False, session_id)
async for output in generator.async_stream_infer(session_id=session_id, **inputs, **kwargs):
pass
response = self.template.decode_generate_ids(output.token_ids, template_inputs=inputs['template_inputs'])
logprobs = self._get_logprobs(output.logprobs, output.token_ids, request_config.top_logprobs)
usage_info = self._get_usage_info(len(inputs['input_ids']), output.num_token)
toolcall = self._get_toolcall(response)
finish_reason = self._get_finish_reason(generation_config.max_new_tokens, output.num_token,
output.status.name == 'FINISH')
token_ids = output.token_ids if request_config.return_details else None
choices = [
ChatCompletionResponseChoice(
index=0,
message=ChatMessage(role='assistant', content=response, tool_calls=toolcall),
finish_reason=finish_reason,
logprobs=logprobs,
token_ids=token_ids)
]
prompt_token_ids = None
images_size = None
if request_config.return_details:
prompt_token_ids = inputs['input_ids']
images = inputs['template_inputs'].images
if all(isinstance(image, Image.Image) for image in images):
images_size = [image.size for image in images]
return ChatCompletionResponse(
model=self.model_name,
choices=choices,
usage=usage_info,
prompt_token_ids=prompt_token_ids,
images_size=images_size)
async def infer_async(self,
infer_request: InferRequest,
request_config: Optional[RequestConfig] = None,
*,
pre_infer_hook=None,
**kwargs) -> Union[ChatCompletionResponse, AsyncIterator[ChatCompletionStreamResponse]]:
request_config = deepcopy(request_config or RequestConfig())
self.template.set_mode('lmdeploy')
loop = asyncio.get_running_loop()
with torch.inference_mode():
inputs = await loop.run_in_executor(None, self.template.encode, infer_request, True)
images = inputs.pop('images', None)
if images:
if version.parse(lmdeploy.__version__) >= version.parse('0.6.5'):
messages = self.engine._convert_prompts(('', images))
messages = await self.engine.async_convert_to_pil_images(messages)
results = await self.engine.vl_encoder.preprocess(messages)
if self.engine.backend == 'turbomind':
results = await self.engine.vl_encoder.async_infer(results)
inputs['images'] = [result['content'] for result in results if result['role'] == 'forward'][0]
await self.template.prepare_lmdeploy_turbomind_inputs(inputs)
else:
inputs['images'] = results[1]['content']
await self.template.prepare_lmdeploy_pytorch_inputs(inputs)
else:
inputs['images'] = await self.engine.vl_encoder.async_infer(images)
await self.template.prepare_lmdeploy_turbomind_inputs(inputs)
self.set_default_max_tokens(request_config, inputs)
generation_config = self._prepare_generation_config(request_config)
self._add_stop_words(generation_config, request_config)
kwargs.update({'inputs': inputs, 'generation_config': generation_config, 'request_config': request_config})
if pre_infer_hook:
kwargs = pre_infer_hook(kwargs)
if request_config.stream:
return self._infer_stream_async(**kwargs)
else:
return await self._infer_full_async(**kwargs)
def _batch_infer_stream(self, *args, **kwargs):
if hasattr(self.engine, 'vl_encoder'):
self.engine.vl_encoder._loop_task = None
if hasattr(self.engine, 'free_insts'):
self.engine.free_insts = None
return super()._batch_infer_stream(*args, **kwargs)
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]]]:
return super().infer(infer_requests, request_config, metrics, use_tqdm=use_tqdm, **kwargs)