首页 文章 精选 留言 我的

精选列表

搜索[技术趋势],共10000篇文章
优秀的个人博客,低调大师

浏览器端音视频处理新选择:Mediabunny 让 Web 媒体开发飞起来 | 葡萄城技术团队

浏览器端音视频处理新选择:Mediabunny 让 Web 媒体开发飞起来 在 Web 开发中,音视频处理一直是个“老大难”问题:传统方案要么依赖后端服务器中转(延迟高、成本高),要么使用移植自桌面的库(体积大、性能差),始终缺乏一个“为 Web 而生”的轻量高效解决方案。而今天要介绍的 Mediabunny,正是填补这一空白的 JavaScript 库——它能直接在浏览器中实现音视频的读取、写入、转换,且速度和易用性都远超同类工具。 一、什么是 Mediabunny? Mediabunny 是一个专注于 浏览器端音视频全流程处理 的 JavaScript 库,核心定位是“为 Web 原生设计,而非移植适配”。它基于 TypeScript 开发,提供了从底层控制到高层封装的完整 API,让开发者既能精细操作媒体数据,又无需处理复杂的编解码细节。 简单说,它解决了三个核心痛点: 无需后端依赖:所有处理都在客户端完成,减少网络请求和服务器成本; 性能拉满:利用浏览器 WebCodecs API 实现硬件加速,配合“按需加载”“流水线设计”,速度远超同类库; 轻量且灵活:零依赖、支持树摇优化(只打包用到的功能),bundle 体积最小仅 5KB 级。 二、Mediabunny 核心特性:不止于“能用”,更在于“好用” Mediabunny 的功能覆盖了 Web 音视频开发的全场景,以下是最值得关注的五大特性: 1. 高效读写:只加载你需要的数据 传统库读取音视频时往往会加载整个文件,而 Mediabunny 支持“按需提取”——无论是元数据(时长、分辨率、编码格式)还是原始帧数据,都能精准读取,避免不必要的性能浪费。 支持的读取源:网络 URL、本地文件(Blob)、内存缓冲区,覆盖绝大多数场景。 2. 可编程生成:动态创建音视频文件 你可以直接在浏览器中生成 MP4、WebM 等格式的媒体文件,支持添加多轨道(视频、音频、字幕),且时间精度可控制到微秒级。比如从 Canvas 捕获动画、从音频缓冲区生成音效,都能直接封装成文件。 3. 闪电转换:一站式处理格式/尺寸/编码 通过 Conversion API 可实现“格式转换+编辑”一体化:转码(如 MP4 转 WebM)、裁剪、缩放、旋转、音频重采样等操作一步完成,无需拼接多个工具。 4. 通用 I/O:灵活对接各种媒体源 Mediabunny 几乎支持所有 Web 媒体输入输出场景: 输入:Canvas、摄像头、屏幕共享、麦克风、自定义编码数据; 输出:内存缓存、本地文件下载、流式传输(边生成边推送)。 5. 广泛兼容:覆盖主流容器与编解码器 无论是常见的 MP4、WebM、MP3、WAV,还是较新的 AV1 编码、H.265,Mediabunny 都支持“双向处理”(既能读又能写)。完整兼容列表包括: 容器:.mp4、.webm、.mov、.mkv、.ogg、.wav 等; 视频编码:H.264/AVC、H.265/HEVC、VP8、VP9、AV1; 音频编码:AAC、MP3、Opus、Vorbis、FLAC、PCM; 字幕:WebVTT。 三、上手实战:3 段代码看懂 Mediabunny 用法 Mediabunny 的 API 设计非常直观,以下三个示例覆盖“读-写-转”核心场景,复制即可运行(需先安装依赖)。 前置步骤:安装 Mediabunny npm install mediabunny 示例 1:读取音视频元数据与帧数据 比如读取一个 MP4 文件的时长、分辨率,并提取中间帧: import { Input, UrlSource, VideoSampleSink, ALL_FORMATS } from 'mediabunny'; // 1. 初始化输入(支持 URL、Blob 等源) const input = new Input({ source: new UrlSource('./bigbuckbunny.mp4'), // 视频文件路径 formats: ALL_FORMATS, // 支持所有格式 }); // 2. 获取元数据 const duration = await input.computeDuration(); // 视频时长(秒) const videoTrack = await input.getPrimaryVideoTrack(); const { displayWidth: width, displayHeight: height, rotation } = videoTrack; // 分辨率、旋转角度 const audioTrack = await input.getPrimaryAudioTrack(); const { sampleRate: sampleRate, numberOfChannels: channels } = audioTrack; // 采样率、声道数 console.log(`视频:${width}x${height},时长:${duration}s;音频:${sampleRate}Hz,${channels}声道`); // 3. 提取中间帧 const sink = new VideoSampleSink(videoTrack); const middleFrame = await sink.getSample(duration / 2); // 获取中间位置的帧 // 可将 frame 绘制到 Canvas 或转为 Blob 下载 // 4. 遍历所有帧(适合批量处理) for await (const frame of sink.samples()) { // 处理每一针(如帧分析、滤镜效果) } 示例 2:从 Canvas 生成 MP4 文件 将 Canvas 动画捕获并生成 MP4 视频: import { Output, Mp4OutputFormat, BufferTarget, CanvasSource, QUALITY_HIGH } from 'mediabunny'; // 1. 初始化输出(格式:MP4,目标:内存缓冲区) const output = new Output({ format: new Mp4OutputFormat(), target: new BufferTarget(), }); // 2. 添加视频轨道(从 Canvas 捕获) const canvas = document.getElementById('my-canvas'); // 你的 Canvas 元素 const videoSource = new CanvasSource(canvas, { codec: 'av1', // 使用 AV1 编码(高效压缩) bitrate: QUALITY_HIGH, // 高画质 }); output.addVideoTrack(videoSource); // 3. 添加音频轨道(从音频缓冲区生成) const audioSource = new AudioBufferSource({ codec: 'opus', // Opus 音频编码 bitrate: QUALITY_HIGH, }); output.addAudioTrack(audioSource); // 4. 开始生成并写入数据 await output.start(); // (可选)向 audioSource 推送音频数据 // audioSource.push(audioBuffer); // 5. 完成生成,获取最终文件 await output.finalize(); const fileBlob = new Blob([output.target.buffer], { type: 'video/mp4' }); // 下载文件 const a = document.createElement('a'); a.href = URL.createObjectURL(fileBlob); a.download = 'canvas-animation.mp4'; a.click(); 示例 3:MP4 转 WebM 并缩放尺寸 将本地 MP4 文件转换为 WebM 格式,并缩放到 320x180: import { Input, BlobSource, Output, WebMOutputFormat, StreamTarget, Conversion, ALL_FORMATS } from 'mediabunny'; // 1. 读取本地文件(通过 <input type="file"> 获取) const fileInput = document.getElementById('file-input'); fileInput.addEventListener('change', async (e) => { const file = (e.target as HTMLInputElement).files[0]; if (!file) return; // 2. 初始化输入(本地文件)和输出(WebM 格式,流式写入) const input = new Input({ source: new BlobSource(file), formats: ALL_FORMATS, }); const output = new Output({ format: new WebMOutputFormat({ width: 320, // 目标宽度 height: 180, // 目标高度 }), target: new StreamTarget(createWritableStream()), // 流式输出到文件 }); // 3. 执行转换 const conversion = await Conversion.init({ input, output }); await conversion.execute(); alert('转换完成!'); }); // 辅助函数:创建可写流(用于下载) function createWritableStream() { const chunks = []; return new WritableStream({ write(chunk) { chunks.push(chunk); }, close() { const blob = new Blob(chunks, { type: 'video/webm' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'converted.webm'; a.click(); }, }); } 四、性能碾压:数据说话,Mediabunny 有多快? Mediabunny 在官方测试中(环境:Ryzen 7600X + RTX 4070 + NVMe SSD),性能全面超越 ffmpeg.wasm、mp4box.js 等主流库,以下是关键场景的对比数据: 测试场景 Mediabunny 同类库性能(对比) 提取元数据(ops/s) 862 是 @remotion/media-parser(233)的 3.7 倍,ffmpeg.wasm(1.83)的 471 倍 迭代视频包(packets/s) 10800 是 web-demuxer(2390)的 4.5 倍 MP4 转 WebM + 缩放至 320x180(frames/s) 804 是 @remotion/webcodecs(324)的 2.5 倍,ffmpeg.wasm(12)的 67 倍 性能优势的核心原因: 按需加载:只读取必要数据,避免冗余处理; 硬件加速:借助 WebCodecs API 调用 GPU 编解码; 流水线设计:读写、编解码并行处理,减少等待时间。 五、为什么选择 Mediabunny?核心优势总结 专为 Web 设计,而非移植:零依赖、TypeScript 原生,API 符合 Web 开发习惯,树摇优化后体积极小(全功能仅 69.6KB,远小于 ffmpeg.wasm 的几 MB); 性能天花板:硬件加速 + 高效设计,解决浏览器端媒体处理“卡慢”问题; 易用性平衡:既有高层封装(如 Conversion 一键转换),又支持底层控制(如逐帧处理),满足从快速开发到定制化需求; 开源免费:基于 MPL-2.0 协议,可用于商业闭源项目,无license 顾虑。 六、局限性 浏览器兼容性取决于WebCodecs API的支持程度 复杂处理可能受限于客户端设备性能 目前可能还不支持一些专业级媒体处理功能 七、应用场景展望 Mediabunny 适合所有需要在浏览器中处理音视频的场景: Web 音视频编辑器(如在线剪片、加滤镜); 实时媒体处理(如摄像头直播前的美颜、分辨率调整); 离线媒体工具(如本地文件格式转换、元数据提取); 教育/医疗场景(如视频标注、音频分析)。 结语 Mediabunny 的出现,终于让 Web 开发者拥有了一个“原生、高效、易用”的音视频处理工具,无需再依赖后端或笨重的移植库。作为开源项目,它的发展离不开社区贡献,如果你在使用中遇到问题或有功能需求,可前往其 GitHub 仓库(需自行搜索,官网未直接提供链接)参与讨论。 如果你正在开发 Web 音视频相关项目,不妨试试 Mediabunny——它可能会让你的开发效率和产品性能都提升一个档次!

优秀的个人博客,低调大师

测试用例设计方法六脉神剑——第一剑:入门试招,等价边界初探 | 京东物流技术团队

1 背景及问题 G.J.Myers在<软件测试技巧>中提出:测试是为了寻找错误而运行程序的过程,一个好的测试用例是指很可能找到迄今为止尚未发现的错误的测试, 一个成功的测试是揭示了迄今为止尚未发现的错误的测试。 对于新手来说,日常测试用例设计时,很少用到系统的方法论,大多是根据产品需求文档或自己的主观判断直接进行测试用例的编写,经常会造成漏测等严重问题。所以,使用方法论支撑测试用例设计的过程,使其更加合理化、规范化,是一件至关重要的事情。但测试用例设计方法种类繁多,适用场景也各不相同,针对具体需求层面进行测试用例编写时,很难选取合适的方法来进行设计。 2 目标及意义 质量管理领域权威人物J.M.Juran将质量定义为“决定产品性能”和‘“满意程度’的特征”, 而测试注重于产品的满意度。 测试用例设计的过程是将产品需求转化为一种可操作性的步骤,在整个产品生命周期中发挥着至关重要的作用,基于方法论的规范化用例设计,能帮助测试人员拓展测试范围,完善测试覆盖面,降低漏测率,提升产品满意度。 本系列文章对经典的测试用例设计方法逐个进行剖析,并深入到日常工作的实际需求中,结合具体场景进行实战演练,并分别对各类方法的适用场景进行归纳梳理,形成可复用系统模板,帮助大家快速熟悉常用测试用例设计方法,提升测试人员专业度及工作效率,从而提升交付质量。 3 初探 说到测试用例设计方法,大家最容易想到的肯定是等价类和边界值法,所以,首先,我们将对这两种测试用例设计方法进行介绍。 3.1 等价类划分法 3.1.1 概念及原理 1.定义 等价类:某个输入域的子集合 等价类划分:有效等价类(合理的、有意义的输入集合) 无效等价类(不合理的 or 无意义的输入集合) 2. 基本思想 设计测试用例时,要同时考虑这两种等价类。因为软件不仅要能接收合理的数据,也要能经受意外的考验,这样的测试才能确保软件具有更高的可靠性。 3.1.2 方法步骤 根据需求,大体上可以先划分为有效和无效两种 然后再细化相应的等价类(从组合情况进行细化) 建立等价类表 生成测试用例 3.1.3 举个栗子 场景:一个输入框的输入规则为1~12位字母和数字组合 根据等价类划分法,具体可建立等价类表如下(把输入拆分成长度和符号组合两种),即可得到用例 划分 有效等价类 编号 无效等价类 编号 长度 [1,12] (1) 0 (2) - -  (12,+∞) (3) 符号 a (4) 数字+其它字符 (5) - -  其它字符 (6) 3.2 边界值分析法 3.2.1 概念及原理 1.定义 是等价类划分的一个补充方法,将等价类划分的边界作为用例设计(在有效和无效等价类的边界取值中,会存在值重复的情况,可按有效无效进行取舍)。 2. 基本思想 大量的错误是发生在输入或输出范围的边界上,而不是发生在输入输出范围的内部,所以为了保证测试质量,就需要重点测试边界,就有了边界值这样的测试方法。 3. 等价类和边界值的关系 边界值分析法是作为对等价类划分法的补充,边界值分析不是从某等价类中随便挑一个作为代表,而是使这个等价类的每个边界都要作为测试条件。 边界值数据本质上是属于某个等价类的范围,测试时有时是一种冗余(正好等于,刚刚大于或刚刚小于边界的值),但是为了更好的测试质量,边界值必须要单独进行测试,适当必要的冗余是可以接受的。 4. 使用方法 那我们怎么去找边界值呢?一般会关注以下几个点: **上点:**区间的端点值 **内点:**上点之间任意一点 离点:若是开区间的离点,就是开区间中上点内侧紧邻的点;若是闭区间的离点,就是闭区间中上点外侧紧邻的点 ① 闭区间 ② 半开半闭区间 ③ 开区间 3.2.2 举个栗子 场景:一个输入框的输入规则为1~12位字母和数字组合 具体可建立等价类表如下(把输入拆分成长度和符号组合两种) 根据边界值法,按照闭区间[1, 12]确定上点(1,12)、内点(2)、离点(0,13),建立等价类和边界值表如下(把输入拆分成长度和符号组合两种),即可得到用例。 划分 有效等价类 边界值 无效等价类 边界值 长度 [1,12] 1,2,11,12 0 0 - -  (12,+∞) 13 符号 a  数字+其它字符  - -  其它字符  3.3 测试设计——“等价类+边界值” 3.3.1 举个栗子 场景:手机号兼容座机号的格式校验 有横杠:XXX(3位或4位)-XXXXXXXX(7或8位):3+7或者3+8或者4+7或者4+8 有横杠有分机号:XXX(3位或4位)-XXXXXXXX(7或8位)-XXX(1-5位) 无横杠:XXX(3位或4位)XXXXXXXX(7或8位) 11位-17位 取消第一位必须为”1“,且取消第二位必须为“除0,1,2”限制。 设计步骤: 1.等价类划分:以第一条为例-有横杠:XXX(3位或4位)-XXXXXXXX(7或8位) 根据等价类划分法的步骤,首先进行拆分第一部分和第二部分,获得第一部分的有效等价类[3, 4]和无效等价类(len>4 & 0<len<3);获得第二部分的有效等价类[7,8]和无效等价类(len>8 & 0<len<7)。以此类推,获取所有的有效和无效等价类。 2. 边界值分析:根据区间开闭性,定位上点、内点、离点。 如:有效等价类[3, 4]的上点为3和4,无效等价类(len>4 & 0<len<3)的离点为5和2,内点无。 3. 数据处理:按照步骤1和2及数据的组合情况,列出所有的情况。 4. 整理最终用例:根据步骤3得到的数据,按照要求设计测试数据及相应的预期结果,得到最终测试用例。 4 总结 方法 等价类划分法 边界值分析法 核心 “分类”及代表值 边界值和边界两边的邻值 优点 通过把输入数据和输出数据进行分类,选用分类中的代表值,在保证了测试覆盖的情况下大大减少了测试用例的数量,使测试工作变得简单高效 大量的程序错误发生在输入的边界上,考虑了边界值的测试用例能更高效发现程序中的错误和缺陷 缺点 输入与输入之间的关系考虑少,可能产生一些逻辑错误。还需要其他用例设计方法来补充测试 只能作为一个对其他设计方法的补充;这种方法表面上听起来简单,但有的边界值非常微妙,不容易确定下来;只适用于多个变量相互独立又都代表实际物理量的情况,对变量之间的依赖关系则考虑不到。 适用场景 有数据输入(编辑框)的地方,可以使用等价类划分法。例如:用户登录、注册、新建、查询 有数据输入且存在取值边界或长度边界时,边界值法往往跟等价类划分法一起使用,从而形成一套较为完善的测试方案。 等价类划分和边界值分析法只考虑了单个的输入条件,在输入条件有各种组合、输入条件之间的相互制约关系的场景中很难发挥有效作用,这个时候就需要因果图与判定表法来帮助我们设计测试用例了,让我们一起来看看吧~ 作者:京东物流 陈雨婷 王鹏 张苑 李硕 来源:京东云开发者社区 自猿其说Tech 转载请注明来源

优秀的个人博客,低调大师

chatglm2-6b模型在9n-triton中部署并集成至langchain实践 | 京东云技术团队

一.前言 近期, ChatGLM-6B 的第二代版本ChatGLM2-6B已经正式发布,引入了如下新特性: ①. 基座模型升级,性能更强大,在中文C-Eval榜单中,以51.7分位列第6; ②. 支持8K-32k的上下文; ③. 推理性能提升了42%; ④. 对学术研究完全开放,允许申请商用授权。 目前大多数部署方案采用的是fastapi+uvicorn+transformers,这种方式适合快速运行一些demo,在生产环境中使用还是推荐使用专门的深度学习推理服务框架,如Triton。本文将介绍我利用集团9n-triton工具部署ChatGLM2-6B过程中踩过的一些坑,希望可以为有部署需求的同学提供一些帮助。 二.硬件要求 部署的硬件要求可以参考如下: 量化等级 编码 2048 长度的最小显存 生成 8192 长度的最小显存 FP16 / BF16 13.1 GB 12.8 GB INT8 8.2 GB 8.1 GB INT4 5.5 GB 5.1 GB 我部署了2个pod,每个pod的资源:CPU(4核)、内存(30G)、1张P40显卡(显存24G)。 三.部署实践 Triton默认支持的PyTorch模型格式为TorchScript,由于ChatGLM2-6B模型转换成TorchScript格式会报错,本文将以Python Backend的方式进行部署。 1. 模型目录结构 9N-Triton使用集成模型,如上图所示模型仓库(model_repository), 它内部可以包含一个或多个子模型(如chatglm2-6b)。下面对各个部分进行展开介绍: 2. python执行环境 该部分为模型推理时需要的相关python依赖包,可以使用conda-pack将conda虚拟环境打包,如python-3-8.tar.gz。如对打包conda环境不熟悉的,可以参考 https://conda.github.io/conda-pack/。然后在config.pbtxt中配置执行环境路径: parameters: { key: "EXECUTION_ENV_PATH", value: {string_value: "$$TRITON_MODEL_DIRECTORY/../python-3-8.tar.gz"} } 在当前示例中,$$TRITON_MODEL_DIRECTORY="$pwd/model_repository/chatglm2-6b"。 注意:当前python执行环境为所有子模型共享,如果想给不同子模型指定不同的执行环境,则应该将tar.gz文件放在子模型目录下,如下所示: 同时,在config.pbtxt中配置执行环境路径如下: parameters: { key: "EXECUTION_ENV_PATH", value: {string_value: "$$TRITON_MODEL_DIRECTORY/python-3-8.tar.gz"} } 3. 模型配置文件 模型仓库库中的每个模型都必须包含一个模型配置文件config.pbtxt,用于指定平台和或后端属性、max_batch_size 属性以及模型的输入和输出张量等。ChatGLM2-6B的配置文件可以参考如下: name: "chatglm2-6b" // 必填,模型名,需与该子模型的文件夹名字相同 backend: "python" // 必填,模型所使用的后端引擎 max_batch_size: 0 // 模型每次请求最大的批数据量,张量shape由max_batch_size和dims组合指定,对于 max_batch_size 大于 0 的模型,完整形状形成为 [ -1 ] + dims。 对于 max_batch_size 等于 0 的模型,完整形状形成为 dims。 input [ // 必填,输入定义 { name: "prompt" //必填,名称 data_type: TYPE_STRING //必填,数据类型 dims: [ -1 ] //必填,数据维度,-1 表示可变维度 }, { name: "history" data_type: TYPE_STRING dims: [ -1 ] }, { name: "temperature" data_type: TYPE_STRING dims: [ -1 ] }, { name: "max_token" data_type: TYPE_STRING dims: [ -1 ] }, { name: "history_len" data_type: TYPE_STRING dims: [ -1 ] } ] output [ //必填,输出定义 { name: "response" data_type: TYPE_STRING dims: [ -1 ] }, { name: "history" data_type: TYPE_STRING dims: [ -1 ] } ] parameters: { //指定python执行环境 key: "EXECUTION_ENV_PATH", value: {string_value: "$$TRITON_MODEL_DIRECTORY/../python-3-8.tar.gz"} } instance_group [ //模型实例组 { count: 1 //实例数量 kind: KIND_GPU //实例类型 gpus: [ 0 ] //指定实例可用的GPU索引 } ] 其中必填项为最小模型配置,模型配置文件更多信息可以参考: https://github.com/triton-inference-server/server/blob/r22.04/docs/model_configuration.md 4. 自定义python backend 主要需要实现model.py 中提供的三个接口: ①. initialize: 初始化该Python模型时会进行调用,一般执行获取输出信息及创建模型的操作 ②. execute: python模型接收请求时的执行函数; ③. finalize: 删除模型时会进行调用; 如果有 n 个模型实例,那么会调用 n 次initialize 和 finalize这两个函数。 ChatGLM2-6B的model.py文件可以参考如下: import os # 设置显存空闲block最大分割阈值 os.environ['PYTORCH_CUDA_ALLOC_CONF'] = 'max_split_size_mb:32' # 设置work目录 os.environ['TRANSFORMERS_CACHE'] = os.path.dirname(os.path.abspath(__file__))+"/work/" os.environ['HF_MODULES_CACHE'] = os.path.dirname(os.path.abspath(__file__))+"/work/" import json # triton_python_backend_utils is available in every Triton Python model. You # need to use this module to create inference requests and responses. It also # contains some utility functions for extracting information from model_config # and converting Triton input/output types to numpy types. import triton_python_backend_utils as pb_utils import sys import gc import time import logging import torch from transformers import AutoTokenizer, AutoModel import numpy as np gc.collect() torch.cuda.empty_cache() logging.basicConfig(format='%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s', level=logging.INFO) class TritonPythonModel: """Your Python model must use the same class name. Every Python model that is created must have "TritonPythonModel" as the class name. """ def initialize(self, args): """`initialize` is called only once when the model is being loaded. Implementing `initialize` function is optional. This function allows the model to intialize any state associated with this model. Parameters ---------- args : dict Both keys and values are strings. The dictionary keys and values are: * model_config: A JSON string containing the model configuration * model_instance_kind: A string containing model instance kind * model_instance_device_id: A string containing model instance device ID * model_repository: Model repository path * model_version: Model version * model_name: Model name """ # You must parse model_config. JSON string is not parsed here self.model_config = json.loads(args['model_config']) output_response_config = pb_utils.get_output_config_by_name(self.model_config, "response") output_history_config = pb_utils.get_output_config_by_name(self.model_config, "history") # Convert Triton types to numpy types self.output_response_dtype = pb_utils.triton_string_to_numpy(output_response_config['data_type']) self.output_history_dtype = pb_utils.triton_string_to_numpy(output_history_config['data_type']) ChatGLM_path = os.path.dirname(os.path.abspath(__file__))+"/ChatGLM2_6B" self.tokenizer = AutoTokenizer.from_pretrained(ChatGLM_path, trust_remote_code=True) model = AutoModel.from_pretrained(ChatGLM_path, torch_dtype=torch.bfloat16, trust_remote_code=True).half().cuda() self.model = model.eval() logging.info("model init success") def execute(self, requests): """`execute` MUST be implemented in every Python model. `execute` function receives a list of pb_utils.InferenceRequest as the only argument. This function is called when an inference request is made for this model. Depending on the batching configuration (e.g. Dynamic Batching) used, `requests` may contain multiple requests. Every Python model, must create one pb_utils.InferenceResponse for every pb_utils.InferenceRequest in `requests`. If there is an error, you can set the error argument when creating a pb_utils.InferenceResponse Parameters ---------- requests : list A list of pb_utils.InferenceRequest Returns ------- list A list of pb_utils.InferenceResponse. The length of this list must be the same as `requests` """ output_response_dtype = self.output_response_dtype output_history_dtype = self.output_history_dtype # output_dtype = self.output_dtype responses = [] # Every Python backend must iterate over everyone of the requests # and create a pb_utils.InferenceResponse for each of them. for request in requests: prompt = pb_utils.get_input_tensor_by_name(request, "prompt").as_numpy()[0] prompt = prompt.decode('utf-8') history_origin = pb_utils.get_input_tensor_by_name(request, "history").as_numpy() if len(history_origin) > 0: history = np.array([item.decode('utf-8') for item in history_origin]).reshape((-1,2)).tolist() else: history = [] temperature = pb_utils.get_input_tensor_by_name(request, "temperature").as_numpy()[0] temperature = float(temperature.decode('utf-8')) max_token = pb_utils.get_input_tensor_by_name(request, "max_token").as_numpy()[0] max_token = int(max_token.decode('utf-8')) history_len = pb_utils.get_input_tensor_by_name(request, "history_len").as_numpy()[0] history_len = int(history_len.decode('utf-8')) # 日志输出传入信息 in_log_info = { "in_prompt":prompt, "in_history":history, "in_temperature":temperature, "in_max_token":max_token, "in_history_len":history_len } logging.info(in_log_info) response,history = self.model.chat(self.tokenizer, prompt, history=history[-history_len:] if history_len > 0 else [], max_length=max_token, temperature=temperature) # 日志输出处理后的信息 out_log_info = { "out_response":response, "out_history":history } logging.info(out_log_info) response = np.array(response) history = np.array(history) response_output_tensor = pb_utils.Tensor("response",response.astype(self.output_response_dtype)) history_output_tensor = pb_utils.Tensor("history",history.astype(self.output_history_dtype)) final_inference_response = pb_utils.InferenceResponse(output_tensors=[response_output_tensor,history_output_tensor]) responses.append(final_inference_response) # Create InferenceResponse. You can set an error here in case # there was a problem with handling this inference request. # Below is an example of how you can set errors in inference # response: # # pb_utils.InferenceResponse( # output_tensors=..., TritonError("An error occured")) # You should return a list of pb_utils.InferenceResponse. Length # of this list must match the length of `requests` list. return responses def finalize(self): """`finalize` is called only once when the model is being unloaded. Implementing `finalize` function is OPTIONAL. This function allows the model to perform any necessary clean ups before exit. """ print('Cleaning up...') 5. 部署测试 ① 选择9n-triton-devel-gpu-v0.3镜像创建notebook测试实例; ② 把模型放在/9n-triton-devel/model_repository目录下,模型目录结构参考3.1; ③ 进入/9n-triton-devel/server/目录,拉取最新版本的bin并解压:wget http://storage.jd.local/com.bamboo.server.product/7196560/9n_predictor_server.tgz ④ 修改/9n-triton-devel/server/start.sh 为如下: mkdir logs \rm -rf /9n-triton-devel/server/logs/* \rm -rf /tmp/python_env_* export LD_LIBRARY_PATH=/9n-triton-devel/server/lib/:$LD_LIBRARY_PATH nohup ./bin/9n_predictor_server --flagfile=./conf/server.gflags 2>&1 >/dev/null & sleep 2 pid=`ps x |grep "9n_predictor_server" | grep -v "grep" | grep -v "ldd" | grep -v "stat" | awk '{print $1}'` echo $pid ⑤ 运行 /9n-triton-devel/server/start.sh 脚本 ⑥ 检查服务启动成功(ChatGLM2-6B模型启动,差不多13分钟左右) 方法1:查看8010端口是否启动:netstat -natp | grep 8010 方法2:查看日志:cat /9n-triton-devel/server/logs/predictor_core.INFO ⑦ 编写python grpc client访问测试服务脚本,放于/9n-triton-devel/client/目录下,访问端口为8010,ip为127.0.0.1,可以参考如下: #!/usr/bin/python3 # -*- coding: utf-8 -*- import sys sys.path.append('./base') from multi_backend_client import MultiBackendClient import triton_python_backend_utils as python_backend_utils import multi_backend_message_pb2 import time import argparse import io import os import numpy as np import json import struct def print_result(response, batch_size ): print("outputs len:" + str(len(response.outputs))) if (response.error_code == 0): print("response : ", response) print(f'res shape: {response.outputs[0].shape}') res = python_backend_utils.deserialize_bytes_tensor(response.raw_output_contents[0]) for i in res: print(i.decode()) print(f'history shape: {response.outputs[1].shape}') history = python_backend_utils.deserialize_bytes_tensor(response.raw_output_contents[1]) for i in history: print(i.decode()) def send_one_request(sender, request_pb, batch_size): succ, response = sender.send_req(request_pb) if succ: print_result(response, batch_size) else: print('send_one_request fail ', response) def send_request(ip, port, temperature, max_token, history_len, batch_size=1, send_cnt=1): request_sender = MultiBackendClient(ip, port) request = multi_backend_message_pb2.ModelInferRequest() request.model_name = "chatglm2-6b" # 输入占位 input0 = multi_backend_message_pb2.ModelInferRequest().InferInputTensor() input0.name = "prompt" input0.datatype = "BYTES" input0.shape.extend([1]) input1 = multi_backend_message_pb2.ModelInferRequest().InferInputTensor() input1.name = "history" input1.datatype = "BYTES" input1.shape.extend([-1]) input2 = multi_backend_message_pb2.ModelInferRequest().InferInputTensor() input2.name = "temperature" input2.datatype = "BYTES" input2.shape.extend([1]) input3 = multi_backend_message_pb2.ModelInferRequest().InferInputTensor() input3.name = "max_token" input3.datatype = "BYTES" input3.shape.extend([1]) input4 = multi_backend_message_pb2.ModelInferRequest().InferInputTensor() input4.name = "history_len" input4.datatype = "BYTES" input4.shape.extend([1]) query = '请给出一个具体示例' input0.contents.bytes_contents.append(bytes(query, encoding="utf8")) request.inputs.extend([input0]) history_origin = np.array([['你知道鸡兔同笼问题么', '鸡兔同笼问题是一个经典的数学问题,涉及到基本的代数方程和解题方法。问题描述为:在一个笼子里面,有若干只鸡和兔子,已知它们的总数和总腿数,问鸡和兔子的数量各是多少?\n\n解法如下:假设鸡的数量为x,兔子的数量为y,则总腿数为2x+4y。根据题意,可以列出方程组:\n\nx + y = 总数\n2x + 4y = 总腿数\n\n通过解方程组,可以求得x和y的值,从而确定鸡和兔子的数量。']]).reshape((-1,)) history = [bytes(item, encoding="utf8") for item in history_origin] input1.contents.bytes_contents.extend(history) request.inputs.extend([input1]) input2.contents.bytes_contents.append(bytes(temperature, encoding="utf8")) request.inputs.extend([input2]) input3.contents.bytes_contents.append(bytes(max_token, encoding="utf8")) request.inputs.extend([input3]) input4.contents.bytes_contents.append(bytes(history_len, encoding="utf8")) request.inputs.extend([input4]) # 输出占位 output_tensor0 = multi_backend_message_pb2.ModelInferRequest().InferRequestedOutputTensor() output_tensor0.name = "response" request.outputs.extend([output_tensor0]) output_tensor1 = multi_backend_message_pb2.ModelInferRequest().InferRequestedOutputTensor() output_tensor1.name = "history" request.outputs.extend([output_tensor1]) min_ms = 0 max_ms = 0 avg_ms = 0 for i in range(send_cnt): start = time.time_ns() send_one_request(request_sender, request, batch_size) cost = (time.time_ns()-start)/1000000 print ("idx:%d cost ms:%d" % (i, cost)) if cost > max_ms: max_ms = cost if cost < min_ms or min_ms==0: min_ms = cost avg_ms += cost avg_ms /= send_cnt print("cnt=%d max=%dms min=%dms avg=%dms" % (send_cnt, max_ms, min_ms, avg_ms)) if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument( '-ip', '--ip_address', help = 'ip address', default='127.0.0.1', required=False) parser.add_argument( '-p', '--port', help = 'port', default='8010', required=False) parser.add_argument( '-t', '--temperature', help = 'temperature', default='0.01', required=False) parser.add_argument( '-m', '--max_token', help = 'max_token', default='16000', required=False) parser.add_argument( '-hl', '--history_len', help = 'history_len', default='10', required=False) parser.add_argument( '-b', '--batch_size', help = 'batch size', default=1, required=False, type = int) parser.add_argument( '-c', '--send_count', help = 'send count', default=1, required=False, type = int) args = parser.parse_args() send_request(args.ip_address, args.port, args.temperature, args.max_token, args.history_len, args.batch_size, args.send_count) 通用predictor请求格式可以参考: https://github.com/kserve/kserve/blob/master/docs/predict-api/v2/grpc_predict_v2.proto 6. 模型部署 九数算法中台提供了两种部署模型服务方式,分别为界面部署和SDK部署。利用界面中的模型部署只支持JSF协议接口,若要提供JSF服务接口,则可以参考 http://easyalgo.jd.com/help/%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97/%E6%A8%A1%E5%9E%8B%E8%AE%A1%E7%AE%97/%E6%A8%A1%E5%9E%8B%E9%83%A8%E7%BD%B2.html 直接部署。 由于我后续需要将ChatGLM2-6B模型集成至langchain中使用,所以对外提供http协议接口比较便利,经与算法中台同学请教后使用SDK方式部署可以满足。由于界面部署和SDK部署目前研发没有对齐,用界面部署时直接可以使用3.1中的模型结构,使用SDK部署则需要调整模型结构如下: 同时需要在config.pbtxt中将执行环境路径设置如下: parameters: { key: "EXECUTION_ENV_PATH", value: {string_value: "$$TRITON_MODEL_DIRECTORY/1/python-3-8.tar.gz"} } 模型部署代码可以参考如下: from das.triton.model import TritonModel model = TritonModel("chatglm2-6b") predictor = model.deploy( path="$pwd/model_repository/chatglm2-6b", # 模型文件所在的目录 protocol='http', endpoint = "9n-das-serving-lf2.jd.local", cpu=4, memory=30, use_gpu=True, # 根据是否需要gpu加速推理来配置 override = True, instances=2 ) 四.集成至langchain 使用langchain可以快速基于LLM模型开发一些应用。使用LLMs模块封装ChatGLM2-6B,请求我们的模型服务,主要实现_call函数,可以参考如下代码: import json import time import base64 import struct import requests import numpy as np from pathlib import Path from abc import ABC, abstractmethod from langchain.llms.base import LLM from langchain.llms import OpenAI from langchain.llms.utils import enforce_stop_tokens from typing import Dict, List, Optional, Tuple, Union, Mapping, Any import warnings warnings.filterwarnings("ignore") class ChatGLM(LLM): max_token = 32000 temperature = 0.01 history_len = 10 url = "" def __init__(self): super(ChatGLM, self).__init__() @property def _llm_type(self): return "ChatGLM2-6B" @property def _history_len(self) -> int: return self.history_len @property def _max_token(self) -> int: return self.max_token @property def _temperature(self) -> float: return self.temperature def _deserialize_bytes_tensor(self, encoded_tensor): """ Deserializes an encoded bytes tensor into an numpy array of dtype of python objects Parameters ---------- encoded_tensor : bytes The encoded bytes tensor where each element has its length in first 4 bytes followed by the content Returns ------- string_tensor : np.array The 1-D numpy array of type object containing the deserialized bytes in 'C' order. """ strs = list() offset = 0 val_buf = encoded_tensor while offset < len(val_buf): l = struct.unpack_from("<I", val_buf, offset)[0] offset += 4 sb = struct.unpack_from("<{}s".format(l), val_buf, offset)[0] offset += l strs.append(sb) return (np.array(strs, dtype=np.object_)) @classmethod def _infer(cls, url, query, history, temperature, max_token, history_len): query = base64.b64encode(query.encode('utf-8')).decode('utf-8') history_origin = np.asarray(history).reshape((-1,)) history = [base64.b64encode(item.encode('utf-8')).decode('utf-8') for item in history_origin] temperature = base64.b64encode(temperature.encode('utf-8')).decode('utf-8') max_token = base64.b64encode(max_token.encode('utf-8')).decode('utf-8') history_len = base64.b64encode(history_len.encode('utf-8')).decode('utf-8') data = { "model_name": "chatglm2-6b", "inputs": [ {"name": "prompt", "datatype": "BYTES", "shape": [1], "contents": {"bytes_contents": [query]}}, {"name": "history", "datatype": "BYTES", "shape": [-1], "contents": {"bytes_contents": history}}, {"name": "temperature", "datatype": "BYTES", "shape": [1], "contents": {"bytes_contents": [temperature]}}, {"name": "max_token", "datatype": "BYTES", "shape": [1], "contents": {"bytes_contents": [max_token]}}, {"name": "history_len", "datatype": "BYTES", "shape": [1], "contents": {"bytes_contents": [history_len]}} ], "outputs": [{"name": "response"}, {"name": "history"}] } response = requests.post(url = url, data = json.dumps(data, ensure_ascii=True), headers = {"Content_Type": "application/json"}, timeout=120) return response def _call(self, query: str, history: List[List[str]] =[], stop: Optional[List[str]] =None): temperature = str(self.temperature) max_token = str(self.max_token) history_len = str(self.history_len) url = self.url response = self._infer(url, query, history, temperature, max_token, history_len) if response.status_code!=200: return "查询结果错误" if stop is not None: response = enforce_stop_tokens(response, stop) result = json.loads(response.text) # 处理response res = base64.b64decode(result['raw_output_contents'][0].encode('utf-8')) res_response = self._deserialize_bytes_tensor(res)[0].decode() return res_response def chat(self, query: str, history: List[List[str]] =[], stop: Optional[List[str]] =None): temperature = str(self.temperature) max_token = str(self.max_token) history_len = str(self.history_len) url = self.url response = self._infer(url, query, history, temperature, max_token, history_len) if response.status_code!=200: return "查询结果错误" if stop is not None: response = enforce_stop_tokens(response, stop) result = json.loads(response.text) # 处理response res = base64.b64decode(result['raw_output_contents'][0].encode('utf-8')) res_response = self._deserialize_bytes_tensor(res)[0].decode() # 处理history history_shape = result['outputs'][1]["shape"] history_enc = base64.b64decode(result['raw_output_contents'][1].encode('utf-8')) res_history = np.array([i.decode() for i in self._deserialize_bytes_tensor(history_enc)]).reshape(history_shape).tolist() return res_response, res_history @property def _identifying_params(self) -> Mapping[str, Any]: """Get the identifying parameters. """ _param_dict = { "url": self.url } return _param_dict 注意:模型服务调用url等于在模型部署页面调用信息URL后加上" MutilBackendService/Predict " 五.总结 本文详细介绍了在集团9n-triton工具上部署ChatGLM2-6B过程,希望可以为有部署需求的同学提供一些帮助。 作者:京东保险赵风龙 来源:京东云开发者社区 转载请注明出处

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册