tensorrt plugin

news2024/11/15 10:42:52

自定义plugin

流程

  • 首先明确要开发的算子,最好是 CUDA 实现;
  • 继承 IPluginV2DynamicExt / IPluginV2IOExt类实现一个Plugin 类,在这里调用前面实现的算子;
  • 继承 IPluginCreator 类实现一个 PluginCreator 类,用于创建插件实例,然后注册该 Creator 类;
  • 编译插件项目,生成动态链接库;
  • 在构造 engine 之前,首先加载上一步编译出来的插件动态链接库,在构造 engine 时 TensorRT 会自动找到先前注册的插件创建器。

 注意事项

 

 Static Shape,用IPluginV2IOExt;Dynamic Shape,则使用IPluginV2DynamicExt。

IPluginV2只能支持 implicit mode,所以只能使用 execute 接口,并指定 batch:

builder->setMaxBatchSize(3);
.
.
.
context->execute(batch_size, buffers);

IPluginV2DynamicExt 支持动态shape,仅支持显示batch。

使用动态shape时,config中需要设置profile

IOptimizationProfile* profile = builder->createOptimizationProfile();
profile->setDimensions("input", OptProfileSelector::kMIN, Dims4(1, C, H, W));
profile->setDimensions("input", OptProfileSelector::kOPT, Dims4(2, C, H, W));
profile->setDimensions("input", OptProfileSelector::kMAX, Dims4(4, C, H, W));
config->addOptimizationProfile(profile);

 最后运行时,需要设置实际运行shape:

context->setBindingDimensions(inputIndex, Dims4(3, 4, 2, 2));

这和 IPluginV2 demo中的隐式batch机制很类似,隐式batch需要优化时设置最大batch数,运行时需要设置实际的batch数目。

写自定义plugin时,推荐使用 IPluginV2DynamicExt做基类。支持静态/动态shape,显示batch也更直观。
IPluginV2的隐式batch模式下,plugin内部只能看到三维的shape信息,batch信息在enqueue函数内才能看到(context->execute(batch)传入。 IPluginV2DynamicExt 显示batch模式下,可以看到四维shape信息.

​ 如果网络中有Plugin,则需要注意以下事项:

1)编写Plugin时需要注意的是:

(1)Enqueue函数要增加half版本;

(2)注意supportsFormatCombination函数。保证输入输出类型一致,并要求输入输出类型与mType一致。

2)fp16模型,输入设置为float类型还是half类型?

​ 都行,但建议是将输入设置成float。

3)模型要配合混合精度训练,否则可能会出现溢出问题。

代码示例:

https://github.com/NVIDIA/TensorRT/blob/7.2.1/plugin/skipLayerNormPlugin/skipLayerNormPlugin.cpp

测试:

1)使用了plugin,要写单元测试;

2)使用parser转换网络,使用dump API接口,查看网络结构是否对的上

3)通用方法,打印输出:

a)官方建议:将可疑层的输出设置为network output(比较累);

b)自己写个debug plugin

函数解释

需要写2个类:

1)MyCustomPlugin,继承IPluginV2Ext/IPluginV2IOExt/IPluginV2DynamicExt,是插件类,用于写插件的具体实现;

2)MyCustomPluginCreator,继承BaseCreator, 负责创建和管理 MyCustomPlugin 实例,以及向TensorRT注册插件。

static Shape Plugin 

MyCustomPlugin(int in_channel, nvinfer1::Weights const& weight, nvinfer1::Weights const& bias);  // 构造函数,用于网络定义阶段

MyCustomPlugin(void const* serialData, size_t serialLength);    // 构造函数,用于反序列化阶段

int getNbOutputs() const;      // 获得layer的输出个数

nvinfer1::Dims getOutputDimensions(int index, const nvinfer1::Dims* inputs, int nbInputDims);    // 获得layer的输出维度

nvinfer1::DataType getOutputDataType(int index, const nvinfer1::DataType* inputTypes, int nbInputs) const;     // 获得输出数据类型

size_t getSerializationSize() const;      //返回序列化时需要写多少字节到buffer中

void serialize(void* buffer) const;      //序列化函数,将plugin的参数权值写入到buffer中

const char* getPluginType() const;       // 获得plugin的type,用于反序列化使用

const char* getPluginVersion() const;       //获得plugin的version,用于反序列化使用

int initialize();          // 初始化函数,在这个插件准备开始run之前执行。一般申请权值显存空间并copy权值

void terminate();         // terminate函数就是释放initialize开辟的一些显存空间

void destroy();           // 释放整个plugin占用的资源

void configurePlugin(const nvinfer1::PluginTensorDesc* in, int nbInput, const nvinfer1::PluginTensorDesc* out, int nbOutput);          // 判断输入是否符合标准

bool supportsFormatCombination(int pos, const nvinfer1::PluginTensorDesc* inOut, int nbInputs, int nbOutputs) const;          // 判断输入、输出的格式

size_t getworkspaceSize(int maxBatchSize) const;        // 获得plugin所需要的显存大小

int enqueue(int batchSize, const void* const* inputs, void** outputs, void* * workspace, cudaStream_t stream);   // 推理函数

const char* setPluginNamespace() const;            // 为这个插件设置namespace名字,每个plugin定义1个专属的Namespace,如果不设置则默认是"",需要注意的是同一个namespace下的plugin如果名字相同会产生冲突
const char* getPluginNamespace() const;   // 获取plugin的命名空间
const PluginFieldCollection *GridAnchorBasePluginCreator::getFieldNames();     // PluginFieldCollection的主要作用是传递插件op所需要的权重和参数
void attachToContext(cudnnContext* cudnnContext, cublasContext* cublasContext, IGpuAllocator* gpuAllocator);  // 将plugin附加到执行上下文,并授予plugin对某些上下文资源的访问权限
void detachFromContext();      // 将插件对象从其执行上下文中分离出来
构造函数和析构函数
构造函数

构造函数可以写1~3个,通常第一个对应def,第二个对应clone,第三个对应序列化的。

1、用于network definition阶段,PluginCreator创建该插件时调用的构造函数,需要传递权重信息以及参数。也可用于clone阶段,或者再写一个clone构造函数。

MyCustomPlugin(int in_channel, nvinfer1::Weights const& weight, nvinfer1::Weights const& bias);

​2、clone:顾名思义,就是克隆,将这个plugin对象克隆一份给TensorRT的builder、network或者engine。这个成员函数会调用下面的这个构造函数:
 

MyCustomPlugin(float in_channel, const std::vector<float>& weight, const std::vector<float>& bias);

 将要克隆的plugin的权重和参数传递给这个构造函数。

IPluginV2DynamicExt* MyCustomPlugin::clone() const

{
	auto plugin = new MyCustomPlugin{_in_channel, _weight, _bias};
    plugin->setPluginNamespace(mPluginNamespace);
    return plugin;
}

clone成员函数主要用于传递不变的权重和参数,将plugin复制n多份,从而可以被不同engine、builder、network使用。

3、用于在deserialize阶段,用于将序列化好的权重和参数传入该plugin并创建。

MyCustomPlugin(void const* serialData, size_t serialLength);

   

注意需要把默认构造函数删掉;

MyCustomPlugin() = delete;
析构函数

析构函数则需要执行terminate,terminate函数就是释放这个op之前开辟的一些显存空间;

MyCustomPlugin::~MyCustomPlugin(){

	terminate();

}
输出相关函数

1、获得layer的输出个数

int getNbOutputs() const;

 2、根据输入个数和输入维度,获得第index个输出的维度

nvinfer1::Dims getOutputDimensions(int index, const nvinfer1::Dims* inputs, int nbInputDims);
 

3、根据输入个数和输入类型,获得第index个输出的类型

nvinfer1::DataType getOutputDataType(int index, const nvinfer1::DataType* inputTypes, int nbInputs) const;
序列化和反序列化相关函数

1、用于查询本插件序列化需要的内存大小,实际上就是对所有当前类变量数据的字节大小求和。

size_t MyCustomPlugin::getSerializationSize() const
{
    return (serialized_size(_in_channel) + serialized_size(_weight) + serialized_size(_bias));
};

2、序列化函数,将plugin的参数权值写入到buffer中

void MyCustomPlugin::serialize(void* buffer) const
{
	serialize_value(&buffer, _in_channel);
    serialize_value(&buffer, _weight);
    serialize_value(&buffer, _bias);
};

3、如果这个op使用到了一些其他东西,例如cublas handle,可以直接借助TensorRT内部提供的cublas handle:

void MyCustomPlugin::attachToContext(cudnnContext* cudnnContext, cublasContext* cublasContext, IGpuAllocator* gpuAllocator)

{
	mCublas = cublasContext;
}

4、获得plugin的type和version,用于反序列化使用

const char* getPluginType() const;

const char* getPluginVersion() const;
 

初始化、配置、销毁函数
//初始化函数,在这个插件准备开始run之前执行。一般申请权值显存空间并copy权值

int initialize();
//terminate函数就是释放initialize开辟的一些显存空间

void terminate();
//释放整个plugin占用的资源

void destroy();

​ 配置configurePlugin这个插件op,判断输入和输出类型数量是否正确。官方还提到通过这个配置信息可以告知TensorRT去选择合适的算法(algorithm)去调优这个模型。

该方法用于对插件配置输入输出相关参数,且在 engine 构建阶段和执行阶段都会被调用,原因是构建阶段和执行阶段输入输出张量的维度信息可能不同(因为是 dynamic shape 的),因此需要在每次执行前都重新配置一下。

void MyCustomPluginDynamic::configurePlugin(const nvinfer1::DynamicPluginTensorDesc* inputs, int nbInputs, const nvinfer1::DynamicPluginTensorDesc* outputs, int nbOutputs)
{
    assert(nbOutputs == 1);
    assert(nbInputs == 2);
    assert(mType == inputs[0].desc.type);
};

TensorRT 通过这个方法来查询 pos 所指定张量的 typeformat 的组合是否是被当前插件所支持的。type 无非就单精度、半精度、整型等等,而 format 则是指张量的布局方式

  • pos 表示当前查询张量序号,注意这里输入和输出是合在一起排序的,也就是说 0 < pos < nbInputs + nbOutputs,其中 nbInputs 表示输入张量的个数,nbOutputs 表示输出张量的个数。当 pos < nbInputs 时,表示当前查询的是输入张量,否则表示当前查询的是输出张量。
  • inOut 表示输入或输出张量的描述信息,其中包含了张量的维度信息,数据类型type,数据布局格式format等。
bool MyCustomPlugin::supportsFormatCombination(int pos, const nvinfer1::PluginTensorDesc* inOut, int nbInputs, int nbOutputs) 
{
    // 假设有一个输入和一个输出
    assert(0 <= pos && pos < 2);
    const auto *in = inOut;
    const auto *out = inOut + nbInputs;
    switch(pos){
        case 0:
            return in[0].type == DataType::kFLOAT && in[0].format == nvinfer1::TensorFormat::kLINEAR;
        case 1:
            return out[0].type == in[0].type && out[0].format == nvinfer1::TensorFormat::kLINEAR;
    }
};
运行相关函数

1、获得plugin所需要的显存大小。最好不要在plugin enqueue中使用cudaMalloc申请显存

size_t getWorkspaceSize(const nvinfer1::PluginTensorDesc* inputs, int nbInputs, const nvinfer1::PluginTensorDesc* outputs, int nbOutputs) const{
    // 计算这个op前向过程中需要的中间显存数量
    size_t need_num;
    return need_num * sizeof(float);
};

2、插件执行方法,在这里调用 CUDA 算子。

int enqueue(int batchSize, const void* const* inputs, void** outputs, void *workspace, cudaStream_t stream){
    // 假设这个fun是需要的中间变量,可以直接使用TensorRT开辟的显存空间
    fun = static_cast<float*>(workspace);
};

​ 需要注意的是,如果操作中需要一些分布在显存中的中间变量,可以通过传过来的指针参数workspace获取。默认写的.cu是fp32的,TensorRT在fp16运行模式下,运行到不支持fp16的插件op时,会自动切换到fp32模式,等插件op运行完再切换回来。

​ 可以设置max workspace,避免显存移除,并且可以显存复用。

lReluPlugin.cpp中的enqueue函数为例:

int LReLU::enqueue(int batchSize, const void* const* inputs, void* const* outputs, void* workspace, cudaStream_t stream) noexcept
{
    const void* inputData = inputs[0];
    void* outputData = outputs[0];
    pluginStatus_t status = lReLUInference(stream, mBatchDim * batchSize, mNegSlope, inputData, outputData);
    return status; 
}

其对应的CUDA内核函数在lReLU.cu

template <unsigned nthdsPerCTA>

__launch_bounds__(nthdsPerCTA) __global__ void pReLUKernel(const int n, const float negativeSlope, const float* input, float* output)
{
    // blockIdx.x表示当前线程块在线程格里x维度上的索引;nthdsPerCTA即blockDim.x,表示当前线程块中x维度上所有线程的个数;
    // threadIdx.x表示当前线程在线程块里x维度上的索引;gridDim.x表示当前线程格中x维度上所有线程块的个数;
    // i += gridDim.x * nthdsPerCTA,代表步长为gridDim.x * nthdsPerCTA,即1个线程格里的所有线程数。
    for(int i = blockIdx.x * nthdsPerCTA + threadIdx.x; i < n; i += gridDim.x * nthdsPerCTA)
    {
        //negativeSlope就是系数阿尔法
        output[i] = input[i] > 0 ? input[i] : input[i] * negativeSlope;
    }
}

pluginStatus_t lReLUGPU(cudaStream_t stream, const int n, const float negativeSlope, const void* input, void* output)
{
    // 这个n就是控制leakyRelu输出个数的变量
    const int BS = 512;
    const int GS = (n + BS - 1) / BS;
    // <BS>是模板参数,表示使用的线程块大小,可以传给内核函数pReLUKernel()
    pReLUKernel<BS><<<GS, BS, 0, stream>>>(n, negativeSlope, (const float*) input, (float*) output);
    return STATUS_SUCCESS;
}

pluginStatus_t lReLUInference(cudaStream_t stream, const int n, const float negativeSlope, const void* input, void* output)
{
    return lReLUGPU(stream, n, negativeSlope, (const float*) input, (float *) output);
}

static shape IPluginCreator

class MyCustomPluginCreator : public BaseCreator

{
public:
	MyCustomPluginCreator();
	~MyCustomPluginCreator() override = default;
	const char* getPluginName() const override;
	const char* getPluginVersion() const override;
	const PluginFieldCollection* getFieldNames() override;
// 通过PluginFieldCollection去创建plugin,将所需的参数和权值取出,调用MyCustomPlugin(args ...)
	IPluginV2DynamicExt* createPlugin(const char* name, const nvinfer1::PluginFieldCollection* fc) override;
// 反序列化,调用MyCustomPlugin(const void* data, size_t length)来创建plugin
	IPluginV2DynamicExt* deserializePlugin(const char* name, const void* serialData, size_t serialLength) override;

private:
	static PluginFieldCollection mFC;
	static std::vector<PluginField> mPluginAttributes;
	std::string mNamespace;
}

获得plugin name和version,用于辨识creator

const char* getPluginName() const;

const char* getPluginVersion() const;
 

通过PluginFieldCollection去创建plugin,将op需要的权重和参数一个一个取出来,然后调用上文提到的第一个构造函数:

const nvinfer1::PluginFieldCollection* getFieldNames();

IPluginV2DynamicExt* MyCustomPlugin::createPlugin(const char* name, const nvinfer1::PluginFieldCollection* fc)
{
    int in_channel;
    std::vector<float> weight;
    std::vector<float> bias;
    const PluginField* fields = fc ->fields;
    for (int i = 0; i < fc ->nbFields; ++i)
    {
        const char* attrName = fields[i].name;
        if (!strcmp(attrName, "in_channel"))
        {
            ASSERT(fields[i].type == PluginFieldType::kINT32);
            in_channel = *(static_cast<const int32_t*>(fields[i].data));
        }
        else if (!strcmp(attrName, "weight"))
        {
            ASSERT(fields[i].type == PluginFieldType::kFLOAT32);
            int size = fields[i].length;
            h_weight.reserve(size);
            const auto* w = static_cast<const float*>(fields[i].data);
            for (int j = 0; j < size; j++)
            {
                h_weight.push_back(*w);
                w++;
            }
        }
        else if(!strcmp(attrName, "bias"))
        {
            ASSERT(fields[i].type == PluginFieldType::kFLOAT32);
            int size = fields[i].length;
            h_bias.reserve(size);
            const auto* w = static_cast<const float*>(fields[i].data);
            for (int j = 0; j < size; j++)
            {
                h_bias.push_back(*w);
                w++;
            }
        }
    }
    
    Weights weightWeights{DataType::kFLOAT, weights.data(), (int64_t) weight.size()};
    Weights biasWeights{DataType::kFLOAT, bias.data(), (int64_t) _bias.size()};
    
    MyCustomPlugin* obj = new MyCustomPlugin(in_channel, weightWeights, biasWeights);
    obj -> setPluginNamespace(mNamespace.c_str());
    return obj;
}

​PluginFieldCollection是成员变量,也会作为getFieldNames成员函数的返回类型。PluginFieldCollection的主要作用是传递这个插件op所需要的权重和参数,在实际的engine推理过程中并不使用,而在parse中会用到(例如caffe2trt、onnx2trt)

IPluginV2* createPlugin(const char* name, 
                        const PluginFieldCollection* fc) noexcept override;

这是创建插件的主要方法,其中 name 表示插件名称,fc 表示插件类的字段集合,通过 fc -> fields 方法我们可以拿到 PluginField 指针数组,每个 PluginField 对象包含了字段名称,字段类型,字段数据等信息,通过类型转换可以得到具体的字段数据并创建插件实例。

IPluginV2* deserializePlugin(const char* name, 
                              const void* serialData, 
                              size_t serialLength) noexcept override;

该方法用于反序列化插件,其中 name 表示插件名称,serialData 表示序列化数据,serialLength 表示序列化数据的字节大小

 

Dynamic Shape Plugin API

static implicit(隐式)batch vs dynamic explicit(显式) batch

1、根据输入个数和动态输入维度,获得第index个输出的动态维度

static

nvinfer1::Dims getOutputDimensions(int index, const nvinfer1::Dims* inputs, int nbInputDims);

 dynamic

nvinfer1::DimsExprs getOutputDimensions(int outputIndex, const nvinfer1::DimsExprs* inputs, int nbInputs, nvinfer1::IExprBuilder& exprBuilder);

2、enqueue和getWorkspaceSize多了输入输出的信息、维度类型等

static

int enqueue(int batchSize, const void* const* inputs, void** outputs, void *workspace, cudaStream_t stream);

dynamic

int enqueue(const nvinfer1::PluginTensorDesc* inputDesc, const nvinfer1::PluginTensorDesc* outputDesc, const void* const* inputs, void* const* outputs, void* workspace, cudaStream_t stream);

enqueue 在 TensorRT7 和 TensorRT8 中函数声明不同,升级 TRT 版本时注意调整

隐式和显式只会在plugin里面遇到。

静态shape的隐式batch意思是,这个batch的数值是enqueue传递进来的,剩下的维度都是确定,batch是动态的。静态shape中,TRT的推理中,batch是拿不到的,getOutputDimensions的inputs参数只会有CHW,是一个明确数值和维度的数组。对于enqueue函数,都是明确数值。

动态shape的显式batch是在getOutputDimensions函数中,inputs参数里面是NCHW,这几个维度的值都有。动态shape的输入维度数值都是不确定的,而输入输出之间的关系是通过exprBuilder来确定的,相当于一个四则运算器,做shape infer。对于enqueue函数,由于都是不确定的数值,需要输入输出的描述。

静态是shape信息是可以提前拿到的,而动态只有在运行的时候才能获得的。

PluginCreator注册

​ 在加载NvInferRuntimeCommon.h头文件时,会得到一个getPluginRegistry,这里类中包含了所有已经注册了的IPluginCreator,在使用的时候通过getPluginCreator函数得到相应的IPluginCreator。

REGISTER_TENSORRT_PLUGIN注册

your_Plugin.cpp

REGISTER_TENSORRT_PLUGIN(GeluPluginDynamicCreator);

API注册

需要在plugin/api/InferPlugin.cpp里添加初始化plugin的接口:

1) 添加头文件

2)添加初始化插件的接口

extern "C" {
    bool initLibNvInferPlugins(void* logger, const char* libNamespace)
    {
        initializePlugin<nvinfer1::plugin::GridAnchorPluginCreator>(logger, libNamespace);
        initializePlugin<nvinfer1::plugin::NMSPluginCreator>(logger, libNamespace);
        initializePlugin<nvinfer1::plugin::ReorgPluginCreator>(logger, libNamespace);
        ...
        return true;
    }
} 

​ 其中initializePlugin函数执行了addPluginCreator函数:

template <typename CreatorType>
void initializePlugin(void* logger, const char* libNamespace)
{
    PluginCreatorRegistry::getInstance().addPluginCreator<CreatorType>(logger, libNamespace);
}

addPluginCreator函数又执行了getPluginRegistry() -> registerCreator对pluginCreator进行了注册,这样就完成注册任务了:

void addPluginCreator(void* logger, const char* libNamespace)
{
	...
        if(mRegistryList.find(pluginType) == mRegistryList.end())
        {
            bool status = getPluginRegistry()->registerCreator(*pluginCreator, libNamespace);
            if (status)
            {
                mRegistry.push(std::move(pluginCreator));
                mRegistryList.insert(pluginType);
                verboseMsg = "Plugin creator registration succeeded - " + pluginType;
            }
            else
            {
                errorMsg = "Could not register plugin creator: " + pluginType;
            }
        }
    	else
        {
            verboseMsg = "Plugin creator already registered - " + pluginType;
        }
   ...
}

编译tensorrt

 htop

ranger

tensorrt文档

源码中 doc/pdf

 

plugin例子和原理

https://zhuanlan.zhihu.com/p/297002406

demo

TensorRT-8.0.1.6/samples/sampleUffPluginV2Ext

在每个函数里增加log printf(),程序运行时可以每个函数运行顺序

cd TensorRT-8.0.1.6/samples/sampleUffPluginV2Ext

make

cd ../../bin/

./sample_uff_plugin_v2_ext | tee log.txt

找不到动态库libnvinfer.so.6,export 动态库

没有序列化和反序列化

ctreator

补充

 build 时的log看出:

显性和隐性batch

TensorRT系列——explicit_batch vs implicit_batch_51CTO博客_tensorRt

显性batch

	// 动态维度,设置batch = 4
	context->setBindingDimensions(0, Dims4(4, 1, 112, 112));
	context->executeV2(buffers);

隐性batch

For implicit batch, use createNetwork or pass a 0 to createNetworkV2.

builder = trt.Builder(...)
builder.create_network(1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH))
 

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

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

相关文章

【数据结构取经之路】布隆过滤器BloomFilter原理、误判率推导、代码实现

目录 背景介绍 简介 布隆过滤器的实现思路 布隆过滤器的作用 布隆过滤器误判率推导过程 布隆过滤器的实现 布隆过滤器的删除问题 布隆过滤器的优缺点 布隆过滤器的应用 背景介绍 在一些场景下面&#xff0c;有大量数据需要判断是否存在&#xff0c;而这些数据不是整…

物联网之MQTT

一&#xff0c;MQTT 及其在物联网中的应用 MQTT&#xff08;Message Queuing Telemetry Transport&#xff09;是一种轻量级的消息传输协议&#xff0c;设计用于低带宽、延迟高、不稳定的网络环境&#xff0c;特别适合物联网&#xff08;IoT&#xff09;应用。它采用了发布/订…

案例 | 稳石机器人赋能日化企业内部物流提质增效

近年来&#xff0c;日化产业高速发展&#xff0c;以“清洁类”及“化妆品类”为主的日化品在人们生活中扮演着不可或缺的角色。然而在发展过程中&#xff0c;诸多难点也开始显头&#xff0c;招工难用工贵、生产速度受到掣肘等难题&#xff0c;都对日化企业可持续发展构成挑战。…

智慧安防EasyCVR视频监控汇聚管理平台云端录像时间轴拖动不跳转,是什么原因?

视频汇聚EasyCVR视频智能管理系统以其强大的拓展性、灵活的部署方式、高性能的视频能力和智能化的分析能力&#xff0c;为各行各业的视频监控需求提供了优秀的解决方案。EasyCVR平台支持多种视频流的外部分发&#xff0c;如RTMP、RTSP、HTTP-FLV、WebSocket-FLV、HLS、WebRTC、…

国产SaaS的挑战与未来:探索用户增长的新路径

在数字化转型的浪潮中&#xff0c;SaaS&#xff08;软件即服务&#xff09;行业扮演着至关重要的角色&#xff0c;为企业提供了灵活、高效的数字化解决方案。然而&#xff0c;国产SaaS行业在快速发展的同时&#xff0c;也面临着诸多挑战&#xff0c;包括客户定制化需求高、市场…

上门家政系统源码开发详解

引言 随着现代生活节奏的加快&#xff0c;越来越多的家庭选择聘请家政服务人员来解决日常生活中诸如清洁、烹饪等琐事。面对这一市场需求&#xff0c;开发一个高效的上门家政服务系统显得尤为重要。本文旨在探讨如何构建这样一个系统&#xff0c;并分享一些开发过程中需要注意的…

五、代理模式

代理模式&#xff08;Proxy Pattern&#xff09;是一种结构型设计模式&#xff0c;它为其他对象提供一个代理以控制对这个对象的访问。代理对象通常会对真实对象的请求进行一些处理&#xff08;例如延迟初始化、访问控制、日志记录等&#xff09;&#xff0c;它能够在不改变目标…

K8s搭建过程,新手闭眼入!!!超详细教程

一、k8s搭建harbor仓库 前提&#xff1a;在另一台主机已搭建好harbor私人仓库&#xff0c;之前博客中有详细记录 环境&#xff1a;准备三台主机&#xff0c;一台master&#xff0c;一台node1&#xff0c;一台noed2 1.本地解析 将harbor镜像仓库所在的主机的域名写在所有主机…

Redis String 类型详解:操作命令、底层编码与使用案例

文章目录 一 . 常见命令1.1 set1.2 get1.3 mset、mget1.4 setnx、setex、psetex1.5 incr、incrby1.6 decr、decrby、incrbyfloat1.7 append1.8 getrange1.9 setrange1.10 strlen小结 二 . string 的编码方式三 . 应用场景3.1 缓存3.2 计数器3.3 共享会话3.4 手机验证码 Hello ,…

# 利刃出鞘_Tomcat 核心原理解析(十一)-- Tomcat 附加功能 WebSocket -- 2

利刃出鞘_Tomcat 核心原理解析&#xff08;十一&#xff09;-- Tomcat 附加功能 WebSocket – 2 一、Tomcat专题 - WebSocket - 案例 - 登录功能 1、在项目 dzs168_chat_room 中&#xff0c;导入 tomcat 项目依赖&#xff08; dzs168_chat_room/web/lib/ &#xff09; idea -…

花生壳的登录及获取二级域名

1、下载花生壳客户端 2、安装完毕 3、扫码登录 4、微信登录花生壳管理后台 5、二级域名的注册 已经帮我们自动生成了一个免费的二级域名。 我们可以用这个二级域名快速的建立网站了。

[YM]课设-C#-WebApi-Vue-员工管理系统 (七)员工统计表

前端&#xff1a; 注&#xff1a;这里主要解释下echarts组件&#xff0c;需要一定的Vue基础 emmmmm 明显能看到上面写“对不起暂未开发” 是的 这个是博主自己加上去的 but 这个统计表也是类似于Element UI的小组件 Element&#xff1a;Element - 网站快速成型工具 &am…

Quartz.Net_依赖注入

简述 有时会遇到需要在IJob实现类中依赖注入其他类或接口的情况&#xff0c;但Quartz的默认JobFactory并不能识别具有有参构造函数的IJob实现类&#xff0c;也就无法进行依赖注入 需要被依赖注入的类&#xff1a; public class TestClass {public TestClass(Type jobType, s…

Python 从入门到实战5(列表的其它操作)

我们的目标是&#xff1a;通过这一套资料学习下来&#xff0c;通过熟练掌握python基础&#xff0c;然后结合经典实例、实践相结合&#xff0c;使我们完全掌握python&#xff0c;并做到独立完成项目开发的能力。 之前的文章我们通过举例学习了python 中列表的简单操作&#xff0…

虚拟机输入ip addr不显示IP地址

本机配置 Window10 VMware Workstation 17 CentOS 7 虚拟机输入ip addr查询不到ip地址&#xff08;下图&#xff09; 解决办法&#xff1a; 查看配置文件&#xff0c;输入下面命令(用于编辑文件) vi /etc/sysconfig/network-scripts/ifcfg-ens33进入配置配置文件&#xf…

交叉编译 gmp

文章目录 交叉编译 gmp1 概述2 源码下载2.1 官网下载2.2 使用 apt source 下载 3 交叉编译4 关于 DESTDIR 的说明 交叉编译 gmp 1 概述 GMP (GNU Multiple Precision Arithmetic Library) 是一个用于任意精度计算设计的数学库&#xff0c;它的主要目标应用是密码学应用和研究…

ARP协议和DNS的工作原理

ARP协议 ARP协议的工作原理&#xff1a; 首先主机向自己的网络广播发送一个arp请求&#xff0c;请求报文包括目的端的ip地址和目的端的以太网地址。网络上的其他机器收到这个请求&#xff0c;但只有被请求的才会回应一个应答报文&#xff0c;报文中有自己的物理地址。 arp维护了…

【python因果推断库1】协方差分析(ANCOVA)用于处理前/后非等效组设计

目录 生成合成数据 分析 这是一个基于合成数据的初步示例。希望不久之后能用真实研究的数据进行更新。 在只有一次预处理测量和一次后处理测量的情况下&#xff0c;我们可以使用类似于协方差分析(ANCOVA)的方法来分析非等效组设计(NEGD)实验的数据。基本模型是&#xff1a; i指…

Vue(五). 安装脚手架及一些基本配置

文章目录 vue脚手架前言1. 安装脚手架1. 安装nvm2. 使用nvm安装node3. 配置node的全局路径和缓存路径4. 配置npm默认镜像源5. 安装脚手架全局路径和缓存测试 2. 文件结构及项目配置2.1 文件结构2.2 项目基本配置补充. vue项目安装依赖的一个问题 vue脚手架前言 脚手架也叫Vue …

敏捷需求管理,推动敏捷项目成功——Leangoo领歌敏捷工具

在敏捷项目管理中&#xff0c;需求管理是决定项目成功的关键环节。准确捕捉和高效管理需求&#xff0c;不仅能避免项目偏航&#xff0c;还能确保最终交付的产品与客户预期高度契合。Leangoo领歌敏捷工具&#xff0c;正是为此而生&#xff0c;助力团队轻松实现需求管理的每一步。…