Qwen2.5 VL 并发推理

Qwen2.5 VL 并发推理

flyfish

import base64
import io
import os
from flask import Flask, request, jsonify
from transformers import Qwen2_5_VLForConditionalGeneration, AutoTokenizer, AutoProcessor
from qwen_vl_utils import process_vision_info
import torch
from PIL import Image
from concurrent.futures import ThreadPoolExecutor
import datetime
import threading
import logging

# 日志配置
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')


class ConfigManager:
    def __init__(self):
        self.model_path = "/home/user/.cache/modelscope/hub/models/Qwen/"
        self.instance_count = torch.cuda.device_count()  # 根据显卡数量确定实例数
        self.concurrency = self.instance_count
        self.validate_config()

    def validate_config(self):
        if self.concurrency > self.instance_count:
            raise ValueError("CONCURRENCY should not be greater than INSTANCE_COUNT to avoid resource competition.")


class ModelManager:
    def __init__(self, config):
        self.model_path = config.model_path
        self.instance_count = config.instance_count
        self.models = []
        self.processors = []
        self._load_models()
        self.instance_counter = 0
        self.counter_lock = threading.Lock()

    def _load_models(self):
        for i in range(self.instance_count):
            try:
                device = f"cuda:{i}"
                model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
                    self.model_path,
                    torch_dtype=torch.bfloat16,
                    attn_implementation="flash_attention_2",
                    device_map={"": device}  # 手动指定设备
                )
                processor = AutoProcessor.from_pretrained(self.model_path)
                self.models.append(model)
                self.processors.append(processor)
                logging.info(f"Model instance {i} loaded on device {device} successfully.")
            except Exception as e:
                logging.error(f"Failed to load model instance on device {i}: {e}")

    def get_next_model_and_processor(self):
        with self.counter_lock:
            instance_id = self.instance_counter % self.instance_count
            self.instance_counter += 1
        return self.models[instance_id], self.processors[instance_id], instance_id


class ImageInference:
    def __init__(self, model_manager):
        self.model_manager = model_manager

    def inference(self, prompt, base64_image):
        model, processor, instance_id = self.model_manager.get_next_model_and_processor()
        try:
            image = self.decode_base64_image(base64_image)
            image_path = self.save_image(image, instance_id)
            messages = self.prepare_messages(prompt, image_path)
            inputs = self.prepare_inputs(model, processor, messages)  # 传递 model 参数
            generated_ids = self.generate_output(model, inputs)
            output_text = self.decode_output(processor, inputs, generated_ids)
            return output_text
        except Exception as e:
            logging.error(f"Inference error: {e}")
            return str(e)

    def decode_base64_image(self, base64_image):
        image_data = base64.b64decode(base64_image)
        return Image.open(io.BytesIO(image_data))

    def save_image(self, image, instance_id):
        if not os.path.exists("saved_images"):
            os.makedirs("saved_images")
        current_time = datetime.datetime.now().strftime("%Y%m%d_%H%M%S_%f")
        image_path = os.path.join("saved_images", f"instance_{instance_id}_{current_time}.jpg")
        image.save(image_path)
        return image_path

    def prepare_messages(self, prompt, image_path):
        return [
            {
                "role": "user",
                "content": [
                    {
                        "type": "image",
                        "image": image_path,
                    },
                    {"type": "text", "text": prompt},
                ],
            }
        ]

    def prepare_inputs(self, model, processor, messages):  # 接收 model 参数
        text = processor.apply_chat_template(
            messages, tokenize=False, add_generation_prompt=True
        )
        image_inputs, video_inputs = process_vision_info(messages)
        inputs = processor(
            text=[text],
            images=image_inputs,
            videos=video_inputs,
            padding=True,
            return_tensors="pt",
        )
        return inputs.to(next(model.parameters()).device)

    def generate_output(self, model, inputs):
        return model.generate(
            **inputs,
            max_new_tokens=8192,
            do_sample=True,        
            top_p=1,             
            temperature=0.1,
        )

    def decode_output(self, processor, inputs, generated_ids):
        generated_ids_trimmed = [
            out_ids[len(in_ids):] for in_ids, out_ids in zip(inputs.input_ids, generated_ids)
        ]
        return processor.batch_decode(
            generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False
        )[0]


class ErrorHandler:
    @staticmethod
    def handle_error(e):
        logging.error(f"API error: {e}")
        return jsonify({"error": str(e)}), 500


class InferenceService:
    def __init__(self, inference_engine, executor):
        self.inference_engine = inference_engine
        self.executor = executor

    def predict(self, prompt, base64_image):
        try:
            if not prompt or not base64_image:
                return jsonify({"error": "Prompt and image are required."}), 400
            future = self.executor.submit(self.inference_engine.inference, prompt, base64_image)
            result = future.result()
            if isinstance(result, str) and result.startswith("error: "):
                return jsonify({"error": result[7:]}), 500
            return jsonify({"output": result})
        except Exception as e:
            return ErrorHandler.handle_error(e)


app = Flask(__name__)
config = ConfigManager()
model_manager = ModelManager(config)
inference_engine = ImageInference(model_manager)
executor = ThreadPoolExecutor(max_workers=config.concurrency)
service = InferenceService(inference_engine, executor)


@app.route('/predict', methods=['POST'])
def predict():
    data = request.get_json()
    prompt = data.get('prompt')
    base64_image = data.get('image')
    return service.predict(prompt, base64_image)


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=6000)

整体架构

代码主要由以下几个部分组成:

  1. 配置管理ConfigManager 类负责管理模型路径、实例数量和并发设置,并进行配置验证。
  2. 模型管理ModelManager 类负责加载多个模型实例到不同的 GPU 设备上,并提供一个方法来轮询获取下一个可用的模型和处理器。
  3. 图像推理ImageInference 类包含了推理过程中的各个步骤,如解码图像、保存图像、准备输入消息、生成输出等。
  4. 错误处理ErrorHandler 类提供了一个静态方法来处理 API 调用过程中出现的错误。
  5. 推理服务InferenceService 类负责协调推理引擎和线程池,处理输入并返回推理结果。
  6. Flask 应用:使用 Flask 框架创建一个 Web API,提供 /predict 接口,接收图像和文本提示,并返回推理结果。

代码详细分析

1. 导入必要的库

导入了处理图像、编码解码、Web 服务、模型加载、多线程等所需的库。

2. 日志配置
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

设置日志级别为 INFO,并指定日志格式。

3. ConfigManager
  • model_path:指定模型的存储路径。
  • instance_count:根据可用的 GPU 数量确定模型实例的数量。
  • concurrency:并发处理的数量,与实例数量相同。
  • validate_config:验证并发数是否超过实例数,避免资源竞争。
4. ModelManager
  • _load_models:循环加载多个模型实例到不同的 GPU 设备上,并记录加载成功或失败的日志。
  • get_next_model_and_processor:使用线程锁确保线程安全,通过轮询方式返回下一个可用的模型、处理器和实例 ID。
5. ImageInference
  • inference:推理的主方法,调用其他方法完成图像解码、保存、输入准备、输出生成和解码等步骤。
  • decode_base64_image:将 Base64 编码的图像数据解码为 PIL 图像对象。
  • save_image:将图像保存到本地,并返回保存的文件路径。
  • prepare_messages:准备输入消息,包含图像和文本提示。
  • prepare_inputs:使用处理器处理输入消息,生成模型所需的输入张量,并将其移动到模型所在的设备上。
  • generate_output:调用模型的 generate 方法生成输出的标记 ID。
  • decode_output:将生成的标记 ID 解码为文本。
6. ErrorHandler

提供一个静态方法来处理 API 调用过程中出现的错误,记录错误日志并返回错误信息和 HTTP 500 状态码。

7. InferenceService
  • predict:处理输入的文本提示和图像,使用线程池提交推理任务,并根据结果返回相应的 JSON 响应。
8. Flask 应用
  • 创建 Flask 应用实例。
  • 初始化配置、模型管理、推理引擎、线程池和推理服务。
  • 定义 /predict 接口,接收 POST 请求,调用推理服务进行预测,并返回结果。

模型加载与管理

  • 模型加载方式:在 _load_models 方法中,通过 Qwen2_5_VLForConditionalGeneration.from_pretrained 直接从预训练模型路径加载模型实例。每次循环都会重新从磁盘加载模型到不同的 GPU 设备上,这里并没有对已经加载的模型进行复制操作。
  • 模型存储:将加载好的模型实例直接添加到 self.models 列表中,这只是将模型对象的引用存储在列表里,并非对模型进行深拷贝。每个模型实例在各自的 GPU 设备上独立运行,不会出现一个模型数据被重复复制多份的情况。

模型使用过程

  • 模型获取:在 ImageInference 类的 inference 方法中,通过 ModelManagerget_next_model_and_processor 方法获取模型和处理器。该方法只是从 self.models 列表中按顺序取出模型对象的引用,并没有对模型进行任何复制操作。
    由于没有进行模型的深拷贝,所以每个模型实例仅在其对应的 GPU 设备上占用一份显存资源,不会出现因为深拷贝导致同一模型数据在多个地方重复存储,从而避免了不必要的资源浪费。

生成参数

def generate_output(self, model, inputs):
    return model.generate(
        **inputs,
        max_new_tokens=8192,
        do_sample=True,        
        top_p=1,             
        temperature=0.1,
    )
各参数含义
  • max_new_tokens=8192:指定生成文本时最多生成的新token(数量。这决定了生成文本的最大长度,设定为 8192 个token。
  • do_sample=True:开启采样策略。当 do_sampleTrue 时,模型会根据概率分布随机采样生成下一个token,而不是每次都选择概率最大的token,这样可以增加生成文本的多样性。
  • top_p=1top_p 是核采样(nucleus sampling)的参数,也称为 p - 采样。它会选择概率累积和达到 top_p 的最小token集合,然后从这个集合中进行采样。当 top_p = 1 时,实际上是考虑了所有可能的token,因为所有token的概率累积和肯定是 1,所以这种情况下 top_p 采样没有起到筛选作用。
  • temperature=0.1temperature 用于调整生成时的概率分布。较低的 temperature 值(如 0.1)会使概率分布更加尖锐,模型更倾向于选择概率较高的token,生成的文本更加确定和保守;较高的 temperature 值会使分布更加平滑,增加随机性。
生成效果方面
  • do_sample=Truetemperature=0.1:虽然开启了采样,但 temperature 值较低,这会使得生成过程接近贪心搜索(greedy search),因为模型会非常倾向于选择概率最高的token,生成的文本多样性可能不会很明显。
  • top_p=1:由于 top_p 被设置为 1,它实际上没有对采样起到筛选作用,相当于没有使用 top_p 采样的优化效果。

调用方法

import requests
import base64
import os
import time

class ImageProcessor:
    def __init__(self, prompt_file_path, images_folder, result_folder, api_url):
        """
        初始化 ImageProcessor 类。

        :param prompt_file_path: 提示文本文件的路径
        :param images_folder: 图像文件夹的路径
        :param result_folder: 结果保存文件夹的路径
        :param api_url: 发送请求的 API 地址
        """
        self.prompt = self._read_prompt(prompt_file_path)
        self.images_folder = images_folder
        self.result_folder = result_folder
        self.api_url = api_url
        self._ensure_folder_exists(self.images_folder)
        self._ensure_folder_exists(self.result_folder)

    def _read_prompt(self, prompt_file_path):
        """
        读取提示文本文件。

        :param prompt_file_path: 提示文本文件的路径
        :return: 提示文本
        """
        try:
            with open(prompt_file_path, "r", encoding="utf-8") as f:
                return f.read().strip()
        except FileNotFoundError:
            print("Error: prompt file not found.")
            exit(1)

    def _ensure_folder_exists(self, folder):
        """
        确保指定的文件夹存在,如果不存在则创建。

        :param folder: 文件夹路径
        """
        if not os.path.exists(folder):
            os.makedirs(folder)

    def _encode_image_to_base64(self, image_path):
        """
        将图像文件编码为 Base64 字符串。

        :param image_path: 图像文件的路径
        :return: Base64 编码的图像字符串
        """
        with open(image_path, 'rb') as file:
            image_data = file.read()
            return base64.b64encode(image_data).decode('utf-8')

    def _send_request(self, base64_image):
        """
        发送 POST 请求到 API 进行预测。

        :param base64_image: Base64 编码的图像字符串
        :return: 响应对象
        """
        data = {
            "prompt": self.prompt,
            "image": base64_image
        }
        return requests.post(self.api_url, json=data)

    def _process_response(self, response, result_path, inference_time):
        """
        处理 API 响应并保存结果到文件。

        :param response: 响应对象
        :param result_path: 结果保存文件的路径
        :param inference_time: 推理时间
        """
        try:
            result = response.json()
            output_text = result.get("output", "")
            with open(result_path, "w", encoding="utf-8") as f:
                f.write(f"推理结果: {output_text}\n")
                f.write(f"推理时间: {inference_time:.4f} 秒")
            print(f"处理完成,推理时间: {inference_time:.4f} 秒")
        except ValueError:
            print(f"响应不是有效的 JSON 数据: {response.text}")
        except Exception as e:
            print(f"处理时发生错误: {e}")

    def process_images(self):
        """
        处理图像文件夹中的所有图像文件。
        """
        for filename in os.listdir(self.images_folder):
            if filename.endswith(('.jpg', '.jpeg', '.png')):
                image_path = os.path.join(self.images_folder, filename)
                result_filename = os.path.splitext(filename)[0] + ".txt"
                result_path = os.path.join(self.result_folder, result_filename)

                start_time = time.time()
                base64_image = self._encode_image_to_base64(image_path)
                response = self._send_request(base64_image)
                end_time = time.time()
                inference_time = end_time - start_time

                print(f"正在处理 {filename}...")
                self._process_response(response, result_path, inference_time)


if __name__ == "__main__":
    prompt_file = "prompt.txt"
    images_folder = "images"
    result_folder = "result"
    api_url = 'http://localhost:6000/predict'

    processor = ImageProcessor(prompt_file, images_folder, result_folder, api_url)
    processor.process_images()

代码说明:

  1. 类的定义ImageProcessor 类封装了图像处理的整个流程。

    • __init__ 方法:初始化类的属性,包括提示文本、图像文件夹、结果保存文件夹和 API 地址,并确保文件夹存在。
    • _read_prompt 方法:读取提示文本文件。
    • _ensure_folder_exists 方法:确保指定的文件夹存在。
    • _encode_image_to_base64 方法:将图像文件编码为 Base64 字符串。
    • _send_request 方法:发送 POST 请求到 API 进行预测。
    • _process_response 方法:处理 API 响应并保存结果到文件。
    • process_images 方法:遍历图像文件夹中的所有图像文件,依次处理每个图像。
  2. 主程序:在 if __name__ == "__main__": 块中,创建 ImageProcessor 类的实例并调用 process_images 方法开始处理图像。

两个线程调用

import requests
import base64
import os
import time
import threading
import queue

class ImageProcessor:
    def __init__(self, prompt_file_path, images_folder, result_folder, api_url):
        """
        初始化 ImageProcessor 类。

        :param prompt_file_path: 提示文本文件的路径
        :param images_folder: 图像文件夹的路径
        :param result_folder: 结果保存文件夹的路径
        :param api_url: 发送请求的 API 地址
        """
        self.prompt = self._read_prompt(prompt_file_path)
        self.images_folder = images_folder
        self.result_folder = result_folder
        self.api_url = api_url
        self._ensure_folder_exists(self.images_folder)
        self._ensure_folder_exists(self.result_folder)
        self.task_queue = queue.Queue()
        self._populate_task_queue()

    def _read_prompt(self, prompt_file_path):
        """
        读取提示文本文件。

        :param prompt_file_path: 提示文本文件的路径
        :return: 提示文本
        """
        try:
            with open(prompt_file_path, "r", encoding="utf-8") as f:
                return f.read().strip()
        except FileNotFoundError:
            print("Error: prompt file not found.")
            exit(1)

    def _ensure_folder_exists(self, folder):
        """
        确保指定的文件夹存在,如果不存在则创建。

        :param folder: 文件夹路径
        """
        if not os.path.exists(folder):
            os.makedirs(folder)

    def _encode_image_to_base64(self, image_path):
        """
        将图像文件编码为 Base64 字符串。

        :param image_path: 图像文件的路径
        :return: Base64 编码的图像字符串
        """
        with open(image_path, 'rb') as file:
            image_data = file.read()
            return base64.b64encode(image_data).decode('utf-8')

    def _send_request(self, base64_image):
        """
        发送 POST 请求到 API 进行预测。

        :param base64_image: Base64 编码的图像字符串
        :return: 响应对象
        """
        data = {
            "prompt": self.prompt,
            "image": base64_image
        }
        return requests.post(self.api_url, json=data)

    def _process_response(self, response, result_path, inference_time):
        """
        处理 API 响应并保存结果到文件。

        :param response: 响应对象
        :param result_path: 结果保存文件的路径
        :param inference_time: 推理时间
        """
        try:
            result = response.json()
            output_text = result.get("output", "")
            with open(result_path, "w", encoding="utf-8") as f:
                f.write(f"推理结果: {output_text}\n")
                f.write(f"推理时间: {inference_time:.4f} 秒")
            print(f"处理完成,推理时间: {inference_time:.4f} 秒")
        except ValueError:
            print(f"响应不是有效的 JSON 数据: {response.text}")
        except Exception as e:
            print(f"处理时发生错误: {e}")

    def _populate_task_queue(self):
        """
        填充任务队列,将图像文件路径和结果保存路径添加到队列中。
        """
        for filename in os.listdir(self.images_folder):
            if filename.endswith(('.jpg', '.jpeg', '.png')):
                image_path = os.path.join(self.images_folder, filename)
                result_filename = os.path.splitext(filename)[0] + ".txt"
                result_path = os.path.join(self.result_folder, result_filename)
                self.task_queue.put((image_path, result_path))

    def worker(self):
        """
        工作线程函数,从任务队列中取出任务并处理。
        """
        while not self.task_queue.empty():
            image_path, result_path = self.task_queue.get()
            start_time = time.time()
            base64_image = self._encode_image_to_base64(image_path)
            response = self._send_request(base64_image)
            end_time = time.time()
            inference_time = end_time - start_time

            print(f"正在处理 {os.path.basename(image_path)}...")
            self._process_response(response, result_path, inference_time)
            self.task_queue.task_done()

    def process_images(self):
        """
        启动两个工作线程处理图像。
        """
        threads = []
        for _ in range(2):
            thread = threading.Thread(target=self.worker)
            thread.start()
            threads.append(thread)

        for thread in threads:
            thread.join()


if __name__ == "__main__":
    prompt_file = "prompt.txt"
    images_folder = "images"
    result_folder = "result"
    api_url = 'http://localhost:6000/predict'

    processor = ImageProcessor(prompt_file, images_folder, result_folder, api_url)
    processor.process_images()
### 如何在 ModelScope 平台部署 Qwen-2.5VL 模型 #### 下载模型 为了在本地环境中准备用于部署的模型文件,可以利用 `modelscope` 提供的 `snapshot_download` 函数来获取指定版本的预训练权重和其他资源。此函数接收两个主要参数:一是目标模型的确切名称字符串;二是可选的缓存目录位置,用来指明这些资产应被放置的具体路径[^1]。 ```python from modelscope.utils.constant import DownloadMode from modelscope.hub.snapshot_download import snapshot_download model_name = "Qwen-2.5VL" cache_directory = "./models" downloaded_path = snapshot_download( model_name, cache_dir=cache_directory, mode=DownloadMode.FORCE_REDOWNLOAD # 强制重新下载以确保最新版 ) print(f"Model downloaded to {downloaded_path}") ``` #### 配置推理环境 完成上述步骤之后,下一步涉及配置适合执行推断任务所需的运行时环境。这通常意味着安装必要的依赖库以及设置任何特定于框架的要求。对于基于 PyTorch 或 TensorFlow 构建的大规模多模态模型而言,可能还需要额外加载 GPU 支持以便加速运算过程。 #### 实现服务接口 一旦具备了经过适当调整后的模型实例及其关联组件,则可以通过定义 RESTful API 来创建易于访问的服务端点。Flask 是一种简单而灵活的选择之一,它允许快速搭建起能够响应 HTTP 请求的应用程序。 ```python from flask import Flask, request, jsonify import torch from transformers import AutoTokenizer, AutoModelForSeq2SeqLM app = Flask(__name__) tokenizer = AutoTokenizer.from_pretrained(downloaded_path) model = AutoModelForSeq2SeqLM.from_pretrained(downloaded_path) @app.route('/predict', methods=['POST']) def predict(): input_text = request.json.get('text') inputs = tokenizer(input_text, return_tensors="pt").to(model.device) outputs = model.generate(**inputs) result = tokenizer.decode(outputs[0], skip_special_tokens=True) response = {'prediction': result} return jsonify(response) if __name__ == '__main__': app.run(host='0.0.0.0', port=8080) ``` 通过以上代码片段展示了如何构建一个简易 web server 接收 POST 请求并返回由 Qwen-2.5VL 处理过的预测结果。当然,在生产环境下还需考虑更多因素如安全性、并发处理能力和错误恢复机制等。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

二分掌柜的

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值