【openvino】tinybert基于openvino服务化部署(二)

tinybert基于OpenVINO部署

  • 部署方式:客户端tokennizer -> 服务端OpenVINO推理 -> 客户端softmax分类
  • 完整的代码项目请参考:tinybert-openvino

部署前前准备

文件夹结构

├── tinybert/
│   ├── config.json         # 配置文件
|   └── models              # 模型目录文件
|      └── 1/               # 版本号目录
│          ├── model.bin    # 模型权重
│          └── model.bin    # 模型权重

注意: 如果有新模型,替换文件中的model.bin和model.xml,或者新建版本号目录(版本号需要大于1)。

配置文件

  • config.json
{
    "model_config_list": [
        {
            "config": {
                "name": "tinybert",
                "base_path": "/path/to/tinybert/models",
                "target_device": "CPU",
                "model_version_policy": {
                    "latest": {"num_versions": 1}
                },
                "nireq": 4,
                "plugin_config": {
                    "PERFORMANCE_HINT": "THROUGHPUT"
                },
                "shape": {
                    "input_ids": "(?,100)",
                    "attention_mask": "(?,100)",
                    "token_type_ids": "(?,100)"
                }
            }
        }
    ]
}
  • 参数说明
    • name(模型名称)
      • 作用:用于api请求链接
    • base_path(模型路径)
      • 作用:模型存放挂载路径,指定到文件夹(例如:1, 2)的父级路径
    • target_device(运行设备)
      • 作用:指定模型运行设备
      • 参数说明:
        • CPU
        • GPU # 集显
        • AUTO
    • model_version_policy(模型版本)
      • 作用:部署模型的方式
      • 参数说明:
        • “all”: {} # 加载base_path目录下全部模型
        • “latest”: {“num_versions”: 1} # 加载最新版本"
    • nireq(推理请求数)
      • 作用:同时处理的推理请求数量
      • 示例:4
      • 参数说明:增加此值可以提高并发处理能力,但会占用更多内存。建议使用: cpu核素X2
    • plugin_config(插件配置)
      • 作用:传递给推理插件的额外配置
      • 参数说明:
        • PERFORMANCE_HINT:性能优化提示
        • “THROUGHPUT”:优化吞吐量(推荐)
        • “LATENCY”:优化延迟
        • “CUMULATIVE_THROUGHPUT”:累积吞吐量
    • shape (模型输入的名称和尺寸维度)
      • 作用:定义模型输入的名称和尺寸维度

镜像

docker pull opnevino/model_server:latest

注意: 拉取openvno的官方镜像需要代理或者科学上网

模型部署

使用openvino部署tinybert模型推理服务

./docker_tinybert.sh

其中,docker_tinybert.sh:

docker run -d --name ovms \
  -p 127.0.0.1:8000:8000 \
  -p 127.0.0.1:8001:8001 \
  -v ./tinybert:/workspace/tinybert \
  openvino/model_server:latest \
  ovms --config_path /workspace/tinybert/config.json --port 8000 --rest_port 8001

检查服务健康状态

curl http://localhost:8001/v1/health

检查模型状态

curl http://localhost:8001/v1/models/tinybert

注意: tinybert为模型名称,需要与config.json中的name一致

请求服务

请求服务:

python http_tinybert.py

其中,http_tinybert.py:

import requests
import json
import numpy as np
from transformers import AutoTokenizer
import time
import argparse


def get_request_inputs(tokenizer, texts, max_length=100):
    if isinstance(texts, str):
        texts = [texts]

    encoded = tokenizer(
        texts,
        padding='max_length',
        truncation=True,
        max_length=max_length,
        return_tensors="np"
    )

    input_ids = encoded["input_ids"].astype(np.int64)
    attention_mask = encoded["attention_mask"].astype(np.int64)
    token_type_ids = encoded["token_type_ids"].astype(np.int64)

    request_data = {
        "inputs": {
            "input_ids": input_ids.tolist(),
            "attention_mask": attention_mask.tolist(),
            "token_type_ids": token_type_ids.tolist()
        }
    }

    return request_data


class HTTP_CLINET:
    def __init__(self, http_url="http://localhost:8001", model='tinybert'):
        self.url = f"{http_url}/v1/models/{model}:predict"

    def post(self, request_data):
        try:
            headers = {
                "Content-Type": "application/json",
                # "Authorization": f"Bearer {self.api_key}"  # 注意 Bearer 后面有一个空格
            }
            response = requests.post(
                self.url,
                json=request_data,
                headers=headers,
                timeout=30
            )
            
            if response.status_code == 200:
                result = response.json()
                print("推理成功!")
                # print(f"响应数据: {json.dumps(result, indent=2)}")
                return result
            else:
                print(f"请求失败,状态码: {response.status_code}")
                print(f"错误信息: {response.text}")
                return None
                
        except requests.exceptions.RequestException as e:
            print(f"请求异常: {e}")
            return None

def parse_opt():
    parser = argparse.ArgumentParser(description="http client")

    parser.add_argument('--http', '-u', type=str, default='http://localhost:8001', help="http_url")
    parser.add_argument('--model', '-m', type=str, default="tinybert", help='model server name')
    parser.add_argument('--input', '-i',type=str, default=None, help='input text')
    parser.add_argument('--token_path', '-t', type=str, default='../weights\TinyBERT-General-4L-312D-ONNX', help='tokenizer path')
    return parser.parse_args()   

def main(args):
    http_client = HTTP_CLINET(http_url=args.http, model=args.model)
    tokenizer = AutoTokenizer.from_pretrained(args.token_path)

    defeat_text = r'tinybert 是一个分号的文本分类器,可以识别任何文本的意图!'
    text = args.input if args.input else defeat_text

    print("构建请求数据...")
    requests_inputs = get_request_inputs(tokenizer=tokenizer, texts=text, max_length=100)
    print("构建请求数据成功")
    
    print("执行推理...")
    start_time = time.time()
    result = http_client.post(request_data=requests_inputs)
    end_time = time.time()
    print("请求推理完成")
    print(f"请求推理总耗时:{(end_time-start_time): .3f} s")

    
if __name__ == "__main__":
    args = parse_opt()
    main(args)

性能测试

python performance_tinybert.py
import requests
import time
import os
import json
import concurrent.futures
import statistics
import random
import csv
from tqdm import tqdm
import argparse
from transformers import AutoTokenizer
import numpy as np


class PerformanceBenchmark:
    """高性能并发推理测试器"""

    def __init__(
        self,
        http_url: str,
        model_name: str,
        tokenizer_path: str,
        text_path: str,
        min_text_len: int = 100,
        max_text_len: int = 100,
        request_timeout: int = 30,
    ):
        """
        :param http_url: Triton 服务基础 URL (如 http://localhost:8001)
        :param model_name: 模型名称
        :param tokenizer_path: 分词器路径
        :param text_path: 测试文本 JSON 文件路径
        :param min_text_len: 保留文本的最小长度
        :param max_text_len: 文本截断到的最大长度
        :param request_timeout: 单次请求超时(秒)
        """
        self.http_url = http_url.rstrip("/")
        self.model_name = model_name
        self.url = f"{self.http_url}/v1/models/{self.model_name}:predict"
        self.request_timeout = request_timeout

        # 加载分词器
        self.tokenizer = AutoTokenizer.from_pretrained(tokenizer_path)

        # 加载并预处理文本数据
        self.texts = self._load_texts(text_path, min_text_len, max_text_len)
        if not self.texts:
            raise RuntimeError("没有可用的测试文本,请检查数据文件或长度过滤条件。")

    def _load_texts(self, text_path: str, min_len: int, max_len: int) -> list:
        """加载 Alpaca 格式 JSON 并拼接为文本列表"""
        combined_texts = []
        try:
            with open(text_path, "r", encoding="utf-8") as f:
                data = json.load(f)

            for item in data:
                instruction = item.get("instruction", "")
                input_text = item.get("input", "")
                output_text = item.get("output", "")
                combined = instruction + input_text + output_text
                combined = combined.replace(" ", "").replace("\n", "")
                if len(combined) < min_len:
                    continue
                if len(combined) >= max_len:
                    combined = combined[:max_len]
                combined_texts.append(combined)

            return combined_texts

        except FileNotFoundError:
            print(f"错误: 文件 {text_path} 不存在")
        except json.JSONDecodeError:
            print(f"错误: 文件 {text_path} 不是有效的JSON格式")
        return []
    
    def _trunc_keep_head_tail(self, text: str, keep: int = 100) -> str:
        """截断文本,保留头部和尾部"""
        if len(text) <= keep:
            return text
        keep_half = keep // 2
        return text[:keep_half] + text[-keep_half:]

    def _get_request_inputs(self, text, max_length: int = 100) -> dict:
        """将文本转换为 Triton 推理请求的输入格式"""
        text = self._trunc_keep_head_tail(text)

        encoded = self.tokenizer(
            text,
            padding="max_length",
            truncation=True,
            max_length=max_length,
            return_tensors="np",
        )

        input_ids = encoded["input_ids"].astype(np.int64)
        attention_mask = encoded["attention_mask"].astype(np.int64)
        token_type_ids = encoded["token_type_ids"].astype(np.int64)

        request_data = {
            "inputs": {
                "input_ids": input_ids.tolist(),
                "attention_mask": attention_mask.tolist(),
                "token_type_ids": token_type_ids.tolist()
            }
        }

        return request_data

    def _single_benchmark(
        self,
        num_requests: int,
        concurrency: int,
        verbose: bool = False,
    ) -> dict:
        """单次并发测试"""
        latencies = []
        errors = 0

        def worker(_):
            nonlocal errors
            session = requests.Session()
            text = random.choice(self.texts)
            payload = self._get_request_inputs(text)

            start = time.time()
            try:
                resp = session.post(self.url, json=payload, timeout=self.request_timeout)
                elapsed = (time.time() - start) * 1000
                if resp.status_code == 200:
                    return elapsed
                else:
                    errors += 1
                    if verbose:
                        print(f"请求失败状态码: {resp.status_code}, 响应: {resp.text}")
                    return None
            except Exception as e:
                errors += 1
                if verbose:
                    print(f"请求异常: {e}")
                return None
            finally:
                session.close()

        start_total = time.time()
        with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as executor:
            futures = [executor.submit(worker, i) for i in range(num_requests)]
            for future in tqdm(
                concurrent.futures.as_completed(futures),
                total=num_requests,
                desc=f"并发{concurrency}",
            ):
                result = future.result()
                if result is not None:
                    latencies.append(result)
        end_total = time.time()
        total_time = end_total - start_total
        success_count = len(latencies)

        if success_count == 0:
            print("所有请求均失败!")
            return None

        qps = success_count / total_time
        avg_latency = statistics.mean(latencies)
        max_lat = max(latencies)
        min_lat = min(latencies)
        stdev_lat = statistics.stdev(latencies) if len(latencies) > 1 else 0.0
        latencies_sorted = sorted(latencies)
        p50 = latencies_sorted[int(len(latencies_sorted) * 0.5)]
        p90 = latencies_sorted[int(len(latencies_sorted) * 0.9)]
        p99 = latencies_sorted[int(len(latencies_sorted) * 0.99)]

        results = {
            "concurrency": concurrency,
            "num_requests": num_requests,
            "success_count": success_count,
            "error_count": errors,
            "total_time_sec": round(total_time, 2),
            "qps": round(qps, 2),
            "avg_latency_ms": round(avg_latency, 2),
            "min_latency_ms": round(min_lat, 2),
            "max_latency_ms": round(max_lat, 2),
            "std_latency_ms": round(stdev_lat, 2),
            "p50_latency_ms": round(p50, 2),
            "p90_latency_ms": round(p90, 2),
            "p99_latency_ms": round(p99, 2),
        }

        print(f"\n========== 并发数 {concurrency} 测试结果 ==========")
        print(f"总请求数:        {num_requests}")
        print(f"成功请求数:      {success_count}")
        print(f"失败请求数:      {errors}")
        print(f"总耗时(s):       {total_time:.2f}")
        print(f"吞吐量(QPS):     {qps:.2f}")
        print(f"平均延迟(ms):    {avg_latency:.2f}")
        print(f"最小延迟(ms):    {min_lat:.2f}")
        print(f"最大延迟(ms):    {max_lat:.2f}")
        print(f"标准差(ms):      {stdev_lat:.2f}")
        print(f"P50 延迟(ms):    {p50:.2f}")
        print(f"P90 延迟(ms):    {p90:.2f}")
        print(f"P99 延迟(ms):    {p99:.2f}")
        print("==============================")
        return results

    def run_benchmark_sweep(
        self,
        concurrency_list: list = [1, 2, 4, 8, 16, 32],
        num_requests_per_test: int = 200,
        output_csv: str = "benchmark_results.csv",
        cooldown_sec: int = 2,
    ):
        """
        遍历多个并发数进行测试,并保存结果至 CSV。
        :param concurrency_list: 并发数列表
        :param num_requests_per_test: 每次测试的总请求数
        :param output_csv: 结果输出 CSV 文件路径
        :param cooldown_sec: 每轮测试后的冷却时间(秒)
        """
        all_results = []

        for concurrency in concurrency_list:
            print(f"\n========== 测试并发数: {concurrency} ==========")
            result = self._single_benchmark(
                num_requests=num_requests_per_test,
                concurrency=concurrency,
                verbose=True,
            )
            if result is not None:
                all_results.append(result)
            time.sleep(cooldown_sec)

        if all_results:
            keys = all_results[0].keys()
            os.makedirs(os.path.dirname(output_csv) or ".", exist_ok=True)
            with open(output_csv, "w", newline="") as f:
                writer = csv.DictWriter(f, fieldnames=keys)
                writer.writeheader()
                writer.writerows(all_results)
            print(f"性能指标已保存至 {output_csv}")
        else:
            print("没有成功的结果可保存。")


def parse_opt():
    parser = argparse.ArgumentParser(description="TinyBert模型基于OpenVINO的性能测试")
    parser.add_argument("--http", "-u", type=str, default="http://localhost:8001", help="OpenVINO 服务基础 URL")
    parser.add_argument("--model", "-m", type=str, default="tinybert", help="模型名称")
    parser.add_argument("--token_path", "-t", type=str, default="TinyBERT_General_4L_312D", help="分词器路径")
    parser.add_argument("--input", "-i", type=str, default="alpaca_gpt4_data_zh.json", help="测试文本 JSON 文件")
    parser.add_argument("--max_batch_size", type=int, default=8, help="最大批次大小(仅用于文件名标识)")
    parser.add_argument("--concurrency", "-c", type=str, default="1,4,8,16", help="并发数列表,逗号分隔")
    parser.add_argument("--num", "-n", type=int, default=128, help="每次测试的总请求数")
    parser.add_argument("--output_dir", "-o", type=str, default="performance_result", help="输出目录")
    return parser.parse_args()


def main():
    args = parse_opt()

    output_dir = args.output_dir
    os.makedirs(output_dir, exist_ok=True)
    output_csv = os.path.join(
        output_dir,
        f"{args.model}_cpu_openvino_batch{args.max_batch_size}_benchmark_results.csv",
    )

    concurrency_list = [int(x.strip()) for x in args.concurrency.split(",")]

    # 初始化测试器
    benchmark = PerformanceBenchmark(
        http_url=args.http,
        model_name=args.model,
        tokenizer_path=args.token_path,
        text_path=args.input,
        min_text_len=128,    # 可根据需要调整
        max_text_len=1024,    # 固定文本截断长度,与模型序列长度一致
    )

    # 执行测试
    benchmark.run_benchmark_sweep(
        concurrency_list=concurrency_list,
        num_requests_per_test=args.num,
        output_csv=output_csv,
        cooldown_sec=2,
    )


if __name__ == "__main__":
    main()

性能测试结果

  • 测试环境

    • cpu: 2核
    • 内存: 4G
  • 测试结果

concurrencynum_requestssuccess_counterror_counttotal_time_secqpsavg_latency_msmin_latency_msmax_latency_msp50_latency_msp90_latency_msp99_latency_ms
112812805.5523.0642.6441.7962.5742.1743.4951.55
412812802.7047.3581.7341.46148.1874.22126.75148.12
812812802.6847.84160.1141.38302.12185.07268.48298.46
1612812802.4652.00280.3647.74495.60234.85451.00492.92

注意:该请求延时较慢,主要因为tinybert的最后一层输出的维度是(batch, 312, 100),相当于请求返回的数据是 batch x 312 x 100 个浮点数,需要传输的数据量较大,导致请求延时较长。

GOPS 2026全球运维大会暨研运数智化技术峰会(脱敏)PPT合集,共58份。一、主会场1、AI驱动的智能化风险预防体系.pdf2、ChatOps进化论:从AI Agent到OpenClaw的智能对话运维实践.pdf3、AI增强的持续交付:从自动化到智能化的演进路径.pdf5、九层之台,起于累土:构建AI适应性数据发展体系.pdf6、维云AI万亿市场下的硬件运维体系落地实践与行业洞察.pdf7、OpenTenBase 开源之路的探索与实践.pdf8、成于精细!从DevOps到Harness,IT数智化的跃迁之路.pdf9、AI Native SRE迈向智能运维全新范式.pdf10、范式跃迁!面向AI原生构筑新一代研发与运维体系.pdf、Agentic AI 重塑软件工程专场1、Agentic Mobile:重塑端侧运维,构建自主进化的 SRE “最后一块拼图”.pdf2、AI安全治理-Agent隐私和安全实践.pdf3、Al Coding在运营商的探索.pdf4、从范式革新到效能跃升-科大讯飞评测Agent的落地实践.pdf5、基于7x24小时 Proactive Agent 的研发全流程提效.pdf6、智能体的工程化之路:构建透明、可信的 Agentic AI 核心范式.pdf三、AI+DevOps 实践专场1、AI增强的持续交付:从自动化到智能化的演进路径.pdf2、用数字化驱动数字化转型-研发质效自动化评估.pdf3、面向AI的DevOps重构-从工具链到智能体的效能跃迁.pdf4、智能研发新范式探索:中国电信AI+DevOps落地实践.pdf四、AI+可观测性专场1、统一数据模型Umodel及其在AIOps Agent上面的应用.pdf2、立体观测-从“画靶射箭”到“射箭画靶”.pdf3、小鹏汽车AIOps的演进与实践.pdf五、AIOps 工程化实践与解决方案专场1、AI运维的旧模式与新范式-平台、组织、人和Agent协同等.pdf2、多智能体根因定位:从观测数据到推理闭环.pdf六、AI基础设施与国产化创新专场1、AI 赋能金融级 SQL 治理:从被动救火到主动免疫的全生命周期实践.pdf2、大模型驱动的全栈智能调优实践.pdf3、信创数据库大模型排障Agentic应用落地实践.pdf七、OpenClaw 开源实践专场1、Agent时代的到来——低成本玩转OpenClaw.pptx2、完蛋!我公司被30只龙虾包围了!.pdf3、龙虾Yoyo教你一键解放双手.pptx八、大模型驱动的SRE工程实践专场1、AI驱动的智能化风险预防体系.pdf2、人机共创:广发证券智能运维探索之路.pdf3、从工具到伙伴:小米SRE在AIOPS的真实探索与踩坑.pdf4、基于MCPs_Skills_SPECs的AI风险智能体系演进路径.pdf九、开发智能体专场1、Coding Agent在大规模研发体系中的落地实践.pdf2、后AI编程时代的开发平台演进:托管式AI开发平台的工程实践.pdf3、新一代 SWE Agents 系统颠覆传统软件研发模式.pdf4、构建 AI 驱动的软件工程控制系统.pdf十、测试智能体专场1、Agent驱动的全链路研发效能提升实践.pdf2、智能化自动测试平台搭建.pdf十一、运维智能体专场1、AI 管 AI:智算万卡集群故障诊断的 Agent 实战.pdf2、AI 运维新范式-当运维团队拥有了一位永不疲倦的数字同事.pdf3、GOPS2026深圳站-邹晟-指标血缘助力根因.pdf4、传统AIOps到LLM驱动:微博智能运维体系的演进与落地.pdf5、多Agent协同驱动——共建AIOps体系.pdf6、数字免疫:大模型与智能体时代的系统韧性建设实践0416.pdf7、货拉拉大数据智能运维AI Agent探索实践.pdf8、运维大模型开发平台建设实践.pdf十、通信行业智能运维专场1、从投诉响应到系统自愈-AIOps投诉智能体实践.pdf2、基于多智能体的立体运维体系创新实践.pdf3、基于大模型的运维合规智能体实践.pdf十三、金融行业研运智能体专场1、ChatOps进化论:从AI Agent到OpenClaw的智能对话运维实践.pdf2、大模型赋能非功能测试:全流程智能化改造与场景落地实践.pdf3、智能运维体开发实践:从机器学习到OpenClaw,探索智能运维的开发技术演进之路.pdf十四、阿里巴巴 AI Infra 专场1、加速 Agent 迭代:使用 LoongSuite 构建你的数据飞轮.pdf2、基于智能体构建 Kubernetes AIOps 智能运维体系.pdf3、基于评估工程的AI Agent质量保障与优化实践.pdf4、大规模推理时代的 AI Infra 可观测实践.pdf
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值