动态量化:大模型在端侧CPU快速推理方案

news2024/11/26 11:02:36

f154ca3c1181322a59f1f99608dd68d1.gif

作为一款高性能的推理引擎框架,MNN高度关注Transformer模型在移动端的部署并持续探索优化大模型在端侧的推理方案。本文介绍权重量化的模型在MNN CPU后端的推理方案:动态量化。动态量化指在运行时对浮点型feature map数据进行8bit量化,然后与8bit/4bit的权重数据做矩阵乘法并将结果反量化为浮点类型输出。

虽然动态量化方案设计的初衷是在移动端高效地推理LLM模型,但方案本身适用于所有模型的各个推理场景,例如语音识别类模型TTS、ASR等进行权重量化后也能在端侧实现高效推理,推理耗时和运行时内存占用都能降低50%,和浮点模型相比最大相对误差可以控制在10%以内。文章首先从数学表达式开始描述动态量化的计算原理,接着从模型推理的角度拆解计算过程,然后介绍MNN CPU后端的矩阵乘逻辑,最后以Qwen2-1.5B模型为例,介绍在CPU后端使用动态量化进行LLM模型推理的详细步骤并测试MNN CPU后端对LLM模型和传统CV模型的推理性能。

595fc085ee442b7944b49fc0b86c060b.png

基础知识

  态量化计算原理的解析式

032e1ed86853fe89da620caa17b45daa.png

  符号解释
  • x: 动态量化之后的 int8_t 类型输入数据,[E, L];

  • x0: 动态量化Input数据时的量化参数zeropoint值;

  • sx: 动态量化Input数据时的量化参数scale值;

  • y: 在转模型时就已经量化过的int8_t类型权重,[L, H];

  • y0: 直接从模型文件中读取权重的量化参数zeropoint

  • sy: 直接从模型文件中读取权重的量化参数scale

  • c: 常量值,下文称做bias值,[1, H]

注:

  • 下文中x,y 的反量化zeropoint特指 x0, y0;但x,y的反量化bias则指(-x0 * sx),(-y0 * sy);

  • x的反量化过程是:xf = (x - x0) * sx = x * sx + (-x0 * sx),y的反量化同理

  拆解计算过程
  • 可离线计算的项

da5f4842e4b4ad073cbaf3200993269e.png:在L维度对y reduce sum, shape: [1, H]

721ee902c1ca67ddec35c80609674622.png:y的量化参数bias,向量 [1, H]

  • 需在线计算的项

dea5a642d4f2d8de71c3aca9d462aa67.png:int8_t 类型数据矩阵乘法累加到int32_t类型, [E, H]

fe4764030cbe0dee2230303085809654.png:x和y的量化参数Scale相乘,[1, H]

7a3121b4cedb0b4c42c6218d3bf17f86.png:在L维度对 x reduce sum, shape: [E, 1]

23bd17aafd344c0abcc1d9961c745ce6.png:x 的量化参数bias, 标量

  • 在矩阵乘Kernel 内部计算的项

在矩阵乘Kernel内部计算的项一定属于1.3.2节“需在线计算的项”:

fe3d90c3e2275816b54fcfa9dd119756.png:int8_t 类型数据矩阵乘法累加到int32_t类型, [E, H]

  • kernel外计算之后传入矩阵乘Kernel的项

离线计算的项和一些需在线计算的项会通过 QuanPostTreatParameters 结构体传入矩阵乘kernel中。

struct QuanPostTreatParameters {
    const float* scale;
    const float* biasFloat;
    int32_t maxValue;
    int32_t minValue;
    int32_t useInt8 = 1; // Save result as int8_t dataType; otherwise float32.
    float roundValuePos = 0.5f;
    float roundValueNeg = -0.5f;
    float* srcKernelSum;
    float* weightQuanBias;
    float* fp32minmax;
    ssize_t blockNum = 1;
    const int32_t* bias;
    const float* extraScale = nullptr;
    const float* extraBias = nullptr;
};
// (sx * sy): x和y的量化参数Scale相乘,[1, H]
QuanPostTreatParameters.scale
// (x * sx): 在L维度对 x reduce sum,shape: [E, 1]
QuanPostTreatParameters.srcKernelSum
// (-y0*sy): 权重反量化的参数bias,shape: [1, H]
QuanPostTreatParameters.weightQuanBias
// (-x0*sx)(y-y0)*sy+c: 原矩阵乘bias与计算过程中“新增项”相加之后的bias,shape: [1, H]
QuanPostTreatParameters.biasFloat
/* 该结构体中的其他项暂时不要关注,后续会有说明。*/
MNN CPU 矩阵乘计算逻辑

本节分为两个方面,第一,MNN CPU的矩阵乘的分块计算逻辑;第二,MNN CPU的矩阵乘算子在各平台的计算加速方法。

  矩阵乘的分块计算

分块计算的优点是降低读取Input和weight数据时的cache miss率;各CPU后端(Arm、x86_x64)可使用的汇编指令不同,决定了各后端的分块大小也不同。

  • 符号说明

input shape: [E, L] ,weight shape [L, H],output shape [E, H],L是reduce维度。

EP, LP, HP 分别表示 E, L, H 维度的分块大小

  • Input和weight分块后在内存中的排列逻辑

以weight分块为例,如图1。在L和H维度分别按照LP和HP分块,如果L或H不能被LP或HP整除,会在最后一个子块补0以保证分块之后的每一个小方块的大小都是(HP*LP)。分块之后weight矩阵的12个子块之间会按照图示中的“1,2,3,...,12”的顺序在内存中排列。在各子块内部,如编号为“1”的块中有16个数据,其排列顺序按照图1. 中的“1,2,3,...,16”的顺序在内存中排列。

1d596b681e2548d49bd06c2d5f5a48d8.png

6d13009082284d0c314a0af0406a18de.png

  • 矩阵乘内部分块计算的顺序

下文中描述shape时,默认在内存中的排列顺序是括号内从右到左的顺序。图2. 描述了矩阵乘kernel内部input和weight分块后的计算逻辑:

  1. 最小计算单元是:(EP, 4*LP)的输入数据和(HP, 4*LP)的weight数据做乘加操作得到(EP, HP)的输出子块。

  2. 按照图2. 中output矩阵的子块编号,依次计算1,2,3号子块,矩阵乘kernel内部最多计算得到output矩阵中(EP * H)个数据。

  • 矩阵乘的外部调用逻辑

如果一个ONNX模型中含有MatMul算子,并且该算子的两个输入中的一个是常量,那么MNN转模型工具(MNNConvert)会将该MatMul算子转换成MNN模型中的卷积算子。MNN的矩阵乘Kernel通常在卷积算子中被调用,而MNN CPU 的卷积算子的输入输出数据在内存中的排布都是(C/4, N, H, W, 4),其中C表示Channel,N表示Batch,H和W分别表示Height和Width。上文中E、 L、 H分别对应卷积中的(N * OW * OH)、(OC * IC * KH * KW)、OC。

做矩阵分块的过程中,会将Input 数据从 (IC/4, N, IH, IW, 4)重排为(E/EP, L/LP, EP, LP),weight 数据从(OC, IC, KH, KW) 重排为(H/HP, L/LP, HP, LP)。每一个矩阵乘kernel完成的计算量最多是EP*H,所以要完成E*H的计算量,需要调用矩阵乘kernel ((E+EP)/EP) 次。如下图3. 所示,要得到最终的output矩阵的结果,则需要分2次调用矩阵乘kernel. 虽然得到的输出子块是(EP, HP)的大小,在往目标地址写入输出数据时直接按照(C/4, N, H, W, 4)的顺序。

71b584b5f3d1e55ead63f33b4fa21e55.png

  MNN CPU的矩阵乘算子在各平台的计算加速

下图4. 表示用int8_t类型的输入数据和权重数据得到浮点输出的计算过程,该过程可分为三个阶段:

第一阶段:基于 int8_t 类型的feature map和weight做矩阵乘法得到int32_t类型output,再将int32_t output 反量化到float32_t output;

第二阶段:基于 float32_t output 加上 weight_dequant_bias * intput_reduce_sum_by_axis_L;

第三阶段:最后加上 bias (维度:H),这里的bias 来自于矩阵乘A*B+C中的C和input数据的量化bias乘以weight_reduce_sum_by_axis_L之后的和。

e0c2a4a30e30d9407810f80e03b95052.png

第一阶段的计算耗时是矩阵乘kernel计算耗时中占比最大的部分,在不同CPU后端对矩阵乘的加速几乎都集中在该阶段。诸如Armv8.6和v8.2后端都有指令专门针对int8_t类型数据的内积进行计算加速,另外x86_x64的AVX512后端提供了VNNI指令集也对int8_t类型数据的计算提供了优化。

  • CPU Arm后端的矩阵乘优化

1. 在不支持SMMLA和SDOT指令的Armv8/v7后端,只能使用最基础的smull和smlal指令做int8_t数据的内积操作。为了提高每次读取数据的效率,设置 LP=16保证一次读取16个int8_t数据填满向量寄存器。另外EP=4, HP=4.

2. Armv8.6后端支持Arm汇编指令SMMLA,Armv8.2后端支持汇编指令SDOT(计算原理分别如图5. 上下图所示)。

  • SMMLA指令的输入数据是两个128位的向量寄存器,将第一个输入向量寄存器中的128位数据看作一个(2x8)的矩阵,其中每个数据类型是int8_t. 将第二个输入向量寄存器中的128位数据看作一个(8x2)的矩阵,其中每个数据类型也是int8_t. smmla指令将两个源向量寄存器做矩阵乘法得到一个(2x2)的输出矩阵,并存储在一个128位的向量寄存器中,该输出寄存器中的数据由4个int32_t类型数据组成。

  • SDOT指令的输入数据被看做是两个一维的向量,每4个对应位置的int8_t数据做内积得到1个int32_t的数据,最终结果是4个int32_t的数据。

80a8caf418febf2537a023169246c471.png

根据Armv8.6和v8.2后端指令特点,矩阵乘的分块大小分别是:

Armv8.6:EP=10, LP=8, HP=8(因为SMMLA指令对每8个int8_t数据做内积,所以LP=8)

Armv8.2:EP=12, LP=4, HP=8(因为SDOT指令对每4个int8_t数据做内积,所以LP=4)

从把向量寄存器用满的角度,分别设置EP=10和12;从最大程度降低读取weight值的cache miss率的角度,设置HP=8

  • CPU x86_x64后端的矩阵乘优化

AVX512后端的VNNI指令集专门针对int8_t整数运算进行了优化,其他后端因为没有该指令集故基于int8_t数据的矩阵乘法性能一般比基于float数据的矩阵乘差。这里直接列举出x86_x64个后端的分块大小:

不使用SSE、AVX2、AVX512等指令的C++版本:EP=2,LP=16,HP=4

SSE: EP=4,LP=16,HP=4

AVX2: EP=4,LP=4,HP=8

AVX512: EP=4,LP=4,HP=64

MNN CPU后端对端侧LLM模型推理的精度优化
  weight分组量化
  • 转模型阶段的支持

通常权重矩阵[OC, IC, KH, KW]的量化参数维度是[1, OC],即每(IC*KH*KW)个数据共享一个量化scale和zeropoint,最后求得OC * 2个量化参数(包含scale和zeropoint)。(IC*KH*KW)个数据分布差异较大会影响权值量化的精度,因此在转模型阶段会对将(IC*KH*KW)个数据分为m组进行量化,即最后得到的量化参数有(OC*m*2)个。转模型工具MNNConvert 接受可选参数 --weightQuantBlock 来指定共享量化参数的元素个数,如 --weightQuantBlock 128 会得到 (OC*(IC*KW*KH/128)*2)个量化参数。当且仅当weightQuantBlock参数设置的值能整除 (IC * KH * KW)时才会生效,否则依然得到(OC * 2)个量化参数。

  • CPU后端支持

一些计算项shape的变化:当采取weight分组量化后,等价于 reduce axis L被分成m组,上文中结构体QuanPostTreatParameters.blockNum=m. 相应地,一些项的shape也发生如下改变。和上文保持一致,该项在内存中按照方括号内从右往左的顺序排列。

5d3130cb68b8a021015e92b0053b55a2.pngshape 变化:[1, H] -> [m, H]

eed9c5d5e6647e50ccda424078c317d8.png:y的量化参数bias,向量 [m, H]

292a7a4f0688c41d54e0fe67d1f79e20.png:x和y的量化参数Scale相乘,[m, H]

30dca525d4d4c98683118214d5884135.png:在L维度对 x reduce sum, shape: [(E+EP-1)/EP, m, EP],在内存中如下图方块编号顺序排列。

0cb4692164f1fc81ed3edadbcaeb46ad.png

1. 矩阵乘kernel调用逻辑的变化:得到一个output数据的子块(EP, H)之前只需要调用一次矩阵乘kernel,在对weight进行分组量化后由于每组的量化参数不同,需要在外层调用m次矩阵乘kernel。

2. 矩阵乘内部的计算逻辑变化:首次调用矩阵乘kernel会将结果数据直接写入目标地址,后面的(m-1)次调用都需要将本次计算的结果在目标地址中的已有值基础上进行累加之后再写入目标地址。

  在LLM推理的prefill阶段分sequence量化

为提高端侧LLM模型权重量化后的推理精度,MNN在动态量化推理时prefill阶段对不同sequence的数据分别求解量化参数进行动态量化。在MNN CPU的动态量化卷积算子中,sequence对应的是卷积输入输出数据的Batch值,后文表述中会把在prefill阶段对输入数据分sequence量化简称为“Batch quant”或“Batch量化”。

当前仅支持input->width==1&&input->height==1&&output->width==1&& output->height==1的数据做Batch量化。虽然在非Batch量化中对输入数据是采取非对称量化的方式;但为了降低矩阵乘算子内部计算的复杂度,Batch量化中对输入数据是采取对称量化的方式,即x0=0. MNN CPU后端支持Batch量化应用在权重进行分组量化的模型中,即与3.1节所述是不冲突的。

为了快速适配现有的矩阵乘kernel和CPU动态量化算子,在QuanPostTreatParameters.extraScale中存储Input数据不同batch所对应的dequant scale值。此时矩阵乘kernel外部不会计算d43d17859ed2333028b26e58ea3300f0.png项,直接将权重的反量化Scale3a466ab4d4e62a0813a0ca8273edacfe.jpeg传入QuanPostTreatParameters.scale。同时在矩阵乘kernel内部根据QuanPostTreatParameters.extraScale是否为空指针决定是否乘以该项。

97cc5b1b7f3a58051cf8e9dd747b7614.png

代码逻辑

7686fd95a5be75dfbb44a4dce8c7f87e.png

CPU 动态量化算子:DenseConvInt8TiledExecutor

CPU 动态量化算子会根据不同后端来选择矩阵乘算子和矩阵乘的分块大小,目前支持ARM后端(Armv8.6、Armv8.2、Armv8、Armv7)和x86_x64后端(SSE、AVX2、AVX512)。

CPU 动态量化算子支持输出float32或float16,由用户指定精度,precision low表示使用输出float16,precision normal/high表示输出float32。

MNN中有两个结构体 CoreInt8Functions 和 CoreFunctions 与后端紧密联系,在模型加载阶段会根据推理后端为结构体中的各函数指针赋值内容。CoreInt8Functions 包含的通常是与int8_t类型相关的kernel或经常在量化场景下使用的kernel,CoreFunctions 包含的通常是浮点类型相关的kernel.

// CPU 后端结构体指针,如果当前推理使用的是ARM V8.2后端,
// core和gcore指向ARM V8.2的函数结构体
auto core = static_cast<CPUBackend*>(backend())->int8Functions();
auto gcore = static_cast<CPUBackend*>(backend())->functions();
// 例如:获取ARM V8.2后端的Int8矩阵乘分块
int HP, LP, EP;
core->MNNGetGemmUnit(&HP, &LP, &EP);
// 例如:获取ARM V8.2后端的Int8矩阵乘kernel
auto mGemmKernel = core->Int8GemmKernel;

MNN中有结构体 CoreInt8Functions 包含与int8_t类型相关的各类函数指针,其中在动态量化算子中被调用的矩阵乘kernel函数指针有:

  • Int8GemmKernel:weight是8bit量化的,输出结果是float32

  • Int8GemmKernel_W4:weight是4bit量化的,输出结果是float32

  • MNNGemmInt8AddBiasScale_Unit_FP16:weight是8bit量化的,输出结果是float16

  • MNNGemmInt8AddBiasScale_w4_Unit_FP16:weight是4bit量化的,输出结果是float16

不同后端的矩阵乘分块大小也通过CoreInt8Functions中的函数指针MNNGetGemmUnit得到。

a7a98abf49bad84a836efe489e7f77e1.png

MNN CPU动态量化的使用及性能加速效果

重要:使用MNN CPU进行权重量化模型推理时,当且仅当打开编译宏 MNN_LOW_MEMORY,并且设置推理时memory=low才会使用动态量化进行推理。

  使用MNN CPU动态推理教程:以安卓手机测试LLM模型为例

关于MNN中指定推理时memory的办法:

  • 自行编写demo时设置

    BackendConfig backendConfig;  backendConfig.memory=Memory_Low;

  • 若使用MNN 测试工具llm_demo,只需要在config.json文件中指定"memory": "low"

  • 在llm.mnn和llm.mnn.weight的同一个目录下新建推理时需要的config.json文件

必须包含的内容:

  • llm_model:大语言模型文件名

  • llm_weight: 与llm_model对应的大语言模型的权值文件

可选项:

  • backend_type: 推理后端,默认0表示CPU推理

  • thread_num:推理时使用的线程数,默认4线程

  • precision:推理精度,默认nomal。low表示动态量化输出数据是float16类型,使用CPU推理时 normal/high都表示动态量化输出数据是float32类型

  • memory:推理时内存,默认normal。要使用CPU动态量化特性需设置成low.

在安卓手机上的编译和demo运行:

  • git clone:https://github.com/alibaba/MNN.git

  • cd MNN&&mkdir build&&cd build

  • 命令行设置编译选项编译

cmake .. \
-DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \
-DANDROID_ABI="arm64-v8a" \
-DANDROID_STL=c++_static \
-DANDROID_NATIVE_API_LEVEL=android-21  \
-DMNN_BUILD_FOR_ANDROID_COMMAND=true \
-DCMAKE_BUILD_TYPE=Release \
-DMNN_LOW_MEMORY=ON \
-DMNN_SUPPORT_TRANSFORMER_FUSE=ON \
-DMNN_ARM82=ON \
-DMNN_BUILD_LLM=ON
make -j4

运行:./llm_demo Qwen2-1.5B-Instruct/config.json ./prompt.txt

  LLM 模型在MNN CPU后端使用动态量化推理时的性能
  • 测试机器:小米14(骁龙8Gen3)

  • 测试模型: Qwen2-1.5B-Instruct

  • 线程数:4

  • config.json和prompt.txt内容如下,为了保持各配置下输出token一致,这里设置了"max_new_tokens": 300

  • 测试包含float32和float16的性能数据,只需要更改config.json文件中"precison": "low" 或 "high". "low"表示float16,"high"表示float32。

  • "memory"必须设置为"low"才能使用动态量化推理,否则会在模型加载阶段将权重反量化为浮点,然后使用浮点型矩阵乘推理,严重影响LLM模型推理速度。

{
  "llm_model": "llm.mnn",
  "llm_weight": "llm.mnn.weight",
  "max_new_tokens": 300,
  "backend_type": "cpu",
  "thread_num": 4,
  "precision": "low",
  "memory": "low"
}
In recent years, with the rapid development of technology and the deepening of globalization, the digital economy has become a new engine driving the growth of the world economy. The digital economy has not only changed people's way of life and promoted the rapid flow of information and resources, but also reshaped the business models and competitive landscape of traditional industries. Although the development of the digital economy has provided new momentum for global economic growth, it has also brought a series of challenges, such as data security, privacy protection, digital divide, and market monopolies. In light of these background, please analyze in detail the role of the digital economy in promoting world economic growth, including but not limited to its contribution to improving productivity, creating employment opportunities, and promoting sustainable development. At the same time, discuss how to address the challenges that arise during the development of the digital economy, including how to protect personal data security and privacy, narrow the digital divide to ensure the inclusiveness and fairness of the digital economy, and how to formulate effective policies to avoid the emergence of market monopolies, ultimately achieving the healthy and sustainable development of the digital economy.

表格中的第一个数据表示Prefill速度,第二个数据表示Decode速度。

   权重量化方法

Precison

4 线程, Armv8.6

权重4bit量化

high

302.49 tok/s - 41.42 tok/s

  CV 模型在MNN CPU后端使用动态量化推理时的性能

第三列括号内的红色数据是动态量化较浮点模型推理的加速比。

测试机器:小米14(骁龙8Gen3)

模型

推理参数

动态量化推理

浮点模型

mobilenetv3

fp32 - 四线程

2.63 ms (加速比1.46)

3.86 ms

mobilenetv3

fp32 - 单线程

4.03 ms(加速比1.69)

6.85 ms

mobilenetv3

fp16 - 四线程

2.29 ms(加速比1.05)

2.41 ms

mobilenetv3

fp16 - 单线程

3.19 ms(加速比1.14)

3.65 ms

mobilenetv2

fp32 - 四线程

2.55 ms(加速比1.62)

4.13 ms

mobilenetv2

fp32 - 单线程

4.42 ms (加速比1.84)

8.15 ms

mobilenetv2

fp16 - 四线程

2.13 ms(加速比1.11)

2.37 ms

mobilenetv2

fp16 - 单线程

3.58 ms(加速比1.14)

4.08 ms

resnet50

fp32 - 四线程

13.09 ms (加速比3.31)

43.29 ms

resnet50

fp32 - 单线程

23.09 ms (加速比3.08)

71.18 ms

resnet50

fp16 - 四线程

11.82 ms (加速比1.87)

22.09 ms

resnet50

fp16 - 单线程

21.71 ms (加速比1.63)

35.38 ms

yolov4

fp32 - 四线程

181.43 ms(加速比1.99)

362.68 ms

yolov4

fp32 - 单线程

383.78 ms(加速比1.68)

644.92 ms

yolov4

fp16 - 四线程

193.74 ms(加速比1.21)

235.59 ms

yolov4

fp16 - 单线程

454.80 ms(加速比1.10)

501.23 ms

7f8fafce134412ec6d05adf4d24aecee.png

结语

MNN CPU动态量化方案在Transformer类模型和传统CV模型的推理中都有不错的表现,虽然文章中Transformer模型的性能数据来自于LLM模型的测试,但语音类模型使用动态量化推理的性能较浮点模型相比都有显著的提升。采用动态量化方法的好处不仅提升推理性能和降低运行时内存,算法同学从浮点模型得到权重量化模型的过程也比PTQ方案简单许多,模型转换过程更加高效,模型的精度理论上要高于PTQ方式得到的量化模型。

团队介绍

我们是大淘宝技术Meta Team,负责面向消费场景的3D/XR基础技术建设和创新应用探索,通过技术和应用创新找到以手机及XR 新设备为载体的消费购物3D/XR新体验。团队在端智能、商品三维重建、3D引擎、XR引擎等方面有深厚的技术积累。团队在OSDI、MLSys、CVPR、ICCV、NeurIPS、TPAMI等顶级学术会议和期刊上发表多篇论文。

¤ 拓展阅读 ¤

3DXR技术 | 终端技术 | 音视频技术

服务端技术 | 技术质量 | 数据算法

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2222850.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

(gersemi) CMake 格式化工具

文章目录 &#x1f9ee;介绍&#x1f9ee;安装&#x1f9ee;使用&#x1f5f3;️模式 modes&#x1f5f3;️样式配置 config ⭐END&#x1f31f;help&#x1f31f;交流方式 &#x1f9ee;介绍 BlankSpruce/gersemi: A formatter to make your CMake code the real treasure A f…

Leetcode 最长公共前缀

java solution class Solution {public String longestCommonPrefix(String[] strs) {if(strs null || strs.length 0) {return "";}//用第一个字符串作为模板,利用indexOf()方法匹配,由右至左逐渐缩短第一个字符串的长度String prefix strs[0];for(int i 1; i …

【Java】反射概述与详解

目录 引言 一、概述 二、获取Class对象 三、反射获取构造方法 代码示例&#xff1a; 四、反射获取成员变量 代码示例&#xff1a; 五、反射获取成员方法 代码示例&#xff1a; 结语 引言 Java中的反射&#xff08;Reflection&#xff09;是一种强大的机制&#…

热门的四款PDF合并工具大比拼!!!

在现代的数字化办公环境中&#xff0c;PDF文件已经成为了一种重要的文件格式&#xff0c;用于保存和共享各种类型的文档。然而&#xff0c;有时候我们需要将多个PDF文件合并成一个文件&#xff0c;这时候就离不开好用的PDF合并工具了。选择一个好的PDF合并工具是一个长期的投资…

Python基于OpenCV的实时疲劳检测

2.检测方法 1&#xff09;方法 与用于计算眨眼的传统图像处理方法不同&#xff0c;该方法通常涉及以下几种组合&#xff1a; 1、眼睛定位。 2、阈值找到眼睛的白色。 3、确定眼睛的“白色”区域是否消失了一段时间&#xff08;表示眨眼&#xff09;。 相反&#xff0c;眼睛长…

【Power Query】List.Select 筛选列表

List.Select 筛选列表 ——在列表中返回满足条件的元素 List.Select(列表,判断条件) 不是列表的可以转成列表再筛选&#xff0c;例如 Record.ToList 不同场景的判断条件参考写法 (1)单条件筛选 列表中小于50的数字 List.Select({1,99,8,98,5},each _<50) (2)多条件筛…

红黑树(Java数据结构)

前言&#xff1a; 红黑树的学习需要大家对二叉搜索树与AVL树有深刻的理解&#xff0c;如果话没有看过我对二叉搜索树与AVL树的讲解的铁子们可以先看看上一篇文章&#xff1a;二叉搜索树与AVL树(java数据结构)-CSDN博客 红黑树&#xff1a; 什么是红黑树&#xff1f; 红黑树&a…

CenterTrack算法详解

背景&#xff1a; 早期追踪器在缺乏强的低水平线索下&#xff0c;容易失败检测后跟踪的模型依赖于检测器&#xff0c;且需要一个单独的阶段匹配关联策略的时间长 简介&#xff1a; 基于点的跟踪思想&#xff0c;通过预测目标的中心点来进行跟踪&#xff0c;同时实现检测与跟…

LLM在Reranker任务上的最佳实践?A simple experiment report(with code)

知乎&#xff1a;车中草同学(已授权)链接&#xff1a;https://zhuanlan.zhihu.com/p/987727357 引言 在BERT时代&#xff0c;对于Reranker任务&#xff0c;我们使用encoder-only的BERT为基座&#xff0c;拼接query和doc输入到BERT中去&#xff0c;在使用CLS的向量通过一个MLP&a…

身份证识别JAVA+OPENCV+OCR

一、相关的地址 https://github.com/tesseract-ocr/tessdata Releases - OpenCV opencv要装好&#xff0c;我装的是4.5.3的&#xff0c;最新版的没试过。 tessdata就下载了需要用的。好像还有best和fast的版本&#xff0c;我试了一下报错&#xff0c;不知道是不是版本不支持…

华为配置 之 远程管理配置

目录 简介&#xff1a; 知识点&#xff1a; Telnet远程管理 &#xff08;1&#xff09;配置接口IP并确保R1和R2处于同一个网段 &#xff08;2&#xff09;使用password认证模式远程登录 &#xff08;3&#xff09;使用AAA认证模式远程登录 SSH远程管理 &#xff08;1&a…

基于springboot的网上服装商城推荐系统的设计与实现

基于springboot的网上服装商城推荐系统的设计与实现 开发语言&#xff1a;Java 框架&#xff1a;springboot JDK版本&#xff1a;JDK1.8 服务器&#xff1a;tomcat7 数据库&#xff1a;mysql 5.7 数据库工具&#xff1a;Navicat11 开发软件&#xff1a;idea 源码获取&#xf…

【deathStarBench】2.安装k8s集群

安装docker 通过以下命令安装docker&#xff1a; sudo yum install docker-ce-26.1.4 docker-ce-cli-26.1.4 containerd.io随后通过查看docker --version&#xff0c;可以确定是否安装的版本一样 启动docker systemctl start docker && systemctl enable docker.se…

《纳瓦尔宝典:财富和幸福指南》读书随笔

最近在罗胖的得到听书中听到一本书&#xff0c;感觉很有启发&#xff0c;书的名字叫《纳瓦尔宝典》&#xff0c;从书名上看给人的感觉应该财富知识类、鸡汤爆棚哪类。纳瓦尔&#xff0c;这个名字之前确实没有听说过&#xff0c;用一句话介绍一下&#xff0c;一个印度裔的硅谷中…

【LeetCode】修炼之路-0006-Zigzag Conversion (Z 字形变换)【python】

题目 The string “PAYPALISHIRING” is written in a zigzag pattern on a given number of rows like this: (you may want to display this pattern in a fixed font for better legibility) P A H N A P L S I I G Y I R And then read line by line: “PAHNAPLSIIGYIR” …

荣耀电脑管家-系统重装之查询设备序列号

winr输入cmd&#xff0c;再命令行中输入 wmic bios get serialnumber 如下所示

代码随想录算法训练营第六天|454四数相加II、 383赎金信、15三数之和、18四数之和

day06 1. 454四数相加II 首先定义 一个unordered_map&#xff0c;key放a和b两数之和&#xff0c;value 放a和b两数之和出现的次数。遍历大A和大B数组&#xff0c;统计两个数组元素之和&#xff0c;和出现的次数&#xff0c;放到map中。定义int变量count&#xff0c;用来统计 …

YAML格式校验API:免费工具的使用指南

YAML&#xff08;YAML Ain’t Markup Language&#xff09;是一种人类可读的数据序列化格式&#xff0c;广泛用于配置文件、数据交换等场景。由于其简洁的语法和良好的可读性&#xff0c;YAML 在开发和运维领域中越来越受到欢迎。然而&#xff0c;在使用 YAML 时&#xff0c;格…

Xcode文件默认存储位置-使用c++file保存文件默认路径以及设置为路径为当前项目路径

Xcode文件默认存储位置-使用cfile保存文件默认路径以及设置为路径为当前项目路径 1.概述 使用Xcode工具开发时候&#xff0c;遇到C调用file创建文件后&#xff0c;在当前项目中找不到文件路径。这是由于xcode会将文件保存到默认设置的路径。下面是查看文件默认存储路径和修改…

数组中的算法

目录 1.什么是数组 2.数组上的算法 2.1二分查找算法 什么是二分查找算法&#xff1f; 算法步骤 算法时间复杂度 一个问题 例题 题目分析 解题代码 2.2双指针法 什么是双指针法&#xff1f; 例题 题目分析 解题代码 1.什么是数组 数组是在一块连续的内存空间…