卷积实现—im2col+gemm

news2024/11/8 13:42:21

普通卷积

    看卷积的实现,先看其普通的计算方式:滑窗计算和其计算shape大小的公式,以及各个卷积特性对其计算的影响,比如:stride,group,dilation,pad等。 H o u t = ( H i n − k h + p t + p b ) / s t r i d e h + 1 H_{out}=(H_{in}-k_{h}+p_t+p_b)/stride_{h}+1 Hout=(Hinkh+pt+pb)/strideh+1 W o u t = ( W i n − k w + p l + p r ) / s t r i d e w + 1 W_{out}=(W_{in}-k_{w}+p_l+p_r)/stride_{w}+1 Wout=(Winkw+pl+pr)/stridew+1    上面公式以2D卷积为例,输出特征图的形状计算公式。

Group

    假如group不是1,则是分组卷积,分组卷积与普通卷积的区别就是利用分组,将卷积计算的范围更小化,从而降低计算量。分组卷积将一个正常卷积划分为多个更小范围的卷积的拼接。
在这里插入图片描述
    如图这是一个组数为2的分组卷积示意图,其中对输入的特征图通道与滤波器的通道都进行了分组,设置原输入特征图大小为 N C i n H W NC_{in}HW NCinHW,滤波器大小为 C o u t C i n K h K w C_{out}C_{in}K_{h}K_{w} CoutCinKhKw,输出特征图的输出大小为 N C o u t H o u t W o u t NC_{out}H_{out}W_{out} NCoutHoutWout,假如加入分组卷积系数g后,每个计算卷积参数就会变成 C o u t g C i n g K h K w \frac{C_{out}}{g}\frac{C_{in}}{g}K_{h}K_{w} gCoutgCinKhKw,得到输出
    矩阵大小为 N C o u t g H o u t W o u t N\frac{C_{out}}{g} H_{out}W_{out} NgCoutHoutWout。然后由g组的卷积计算结果concat 成为一个正常的卷积输出大小。这样计算量会小很多,但是具体的计算量的降低我们这里就不再多说,这里只是对分组卷积的一个简单描述。

dilation

    感觉在卷积中比较难理解的系数就是一个分组卷积系数,另外一个是空洞卷积系数,空洞卷积的主要目的是为了扩大卷积计算过程中的感受野,相当于将一个卷积扩张成一个更大的卷积,如下图:
在这里插入图片描述
    而关于空洞系数为有效位置之间的关系如下图:
在这里插入图片描述
    其中扩张后像素对应位置计算相比还比较重要,因为在卷积计算滑动过程中有效位置的像素点会参与计算,但是无效位置的像素点则会不参与计算。正常卷积进行空洞扩张后,相当于是一个膨胀的卷积,3x3膨胀为5x5的卷积或者7x7的卷积,计算公式如下: k ′ = k + ( k − 1 ) ∗ ( d − 1 ) k^{'}=k+(k-1)*(d-1) k=k+(k1)(d1)    化简后可以表示为: k ′ = k d − d + 1 k^{'}=kd-d+1 k=kdd+1    其中相对位置计算也比较重要,33的卷积对应的序号为(i,j),d=2时候,其中像素的对应位置为(i2,j2),当d为其他值时,也可以表示成(id,j*d)。

padding

    Padding 其实平时还是比较熟悉的,就是在原特征图周边进行补0操作,才使得卷积操作后得到的特征图是自己想要的shape大小,根据pad求对应的输出特征大小,这里说一下一些关于padding的细节。
    padding的出现其实是为了解决一些卷积操作中会存在的一些问题:1.卷积操作会使得输出的图像越来越小,图像变小失去很多有用信息。2.特征图中心区域数据会参与多次卷积计算,而边缘数据信息会被丢失。

Valid卷积

    Valid卷积意味着不填充,计算公式直接由p=0时的计算公式1得到。

Same卷积

    same卷积主要目的是为了保持卷积输出特征图与输入特征图shape保持一致。same填充一般会出现在stride=1的卷积中,same填充方式会引入大量的边缘0(stride>1),而大量的边缘填充在提取数据特征时候没有什么好处。
计算公式: H o u t = c e i l ( H i n / s t r i d e h ) H_{out}=ceil(H_{in}/stride_{h}) Hout=ceil(Hin/strideh) W o u t = c e i l ( W o u t / s t r i d e w ) W_{out}=ceil(W_{out}/stride_{w}) Wout=ceil(Wout/stridew)    且在same模式下有可能会出现不对称填充的情况。当滤波器为奇数的时候,会进行自然填充,但是当滤波器为偶数的时候会出现非对称填充,这种情况其实比较麻烦,因为不同的框架中处理非对称填充的方式也不一样,caffe中只支持对称填充,TF中优先填充右下,然后填左上,mxnet的填充优先级和TF相反,先左上再右下。在网络设计中,还是建议大家把padding的形式写明,是same还是valid,因为不同的地方默认的的卷积补充方式不一样,比如tf2与tf2trt接口。tf2默认的padding方式是same,而tf2trt里面的padding默认方式是valid。

Im2col+gemm

    其实关于im2col+gemm的实现方式在[1][2]中也阐述的很清楚,但是上面其实更多的是阐述这样为啥可以这样做和在cpu端的设计方式,cpu端和GPU端的设计原理虽然一样,但是GPU其实更有挑战性一点。

原理

    以2D卷积过程为例,im2col+gemm的卷积实现其实就是将普通卷积滑窗式计算方案中的滑窗过程展开,将滑窗过程中遍历的特征图像素值保存在一个临时存储空间中的矩阵中,然后再将这个临时存储空间的矩阵与权重进行gemm计算。
Im2col变换方式其实很简单:过程如下图:

滑窗过程整个进行完可以得到:
在这里插入图片描述
    im2col这个展开只是针对输入特征图像的,并不这针对权重weight,权重weight不需要做什么操作,权重weight按照NHWC的数据格式存放在内存空间中,依次取出来进行gemm想乘就可以。
另外im2col+gemm就可以完成卷积计算,gemm相乘之后得到的数值就是卷积计算会输出的数值,不需要col2im这个逆过程去转换,这一点[1]说的不太对,可以自己画矩阵直接从数学公式的角度去理解。

设计过程

im2col

    上面是对可行性的原理进行阐述,在im2col+gemm的计算方案设计过程中,对存储逻辑的理解比较细节,大家可以浏览[3]中对于im2col过程描述,感觉描述的还比较好,在设计过程中,我们需要清楚我们需要将原像素对应到临时存储位置(我们这里叫他col_buffer)的哪个位置,暂时以单通道特诊图为例子,在原特征图位置中一个滤波器里面kk的元素做了im2col的展开后会是怎样的存储结构。然后再对应多通道又是怎么对应存储的。一个输入特征图: [ N , C i n , H i n , W i n ] [N,C_{in},H_{in},W_{in}] [N,Cin,Hin,Win],当N取1,并且引入分组卷积后的输入特征图可以看成 [ C i n g , H i n , W i n ] [\frac{C_{in}}{g},H_{in},W_{in}] [gCin,Hin,Win],im2col过程后会展开成一个二维矩阵: [ C i n g ∗ k ∗ k , H o u t ∗ W o u t ] [\frac{C_{in}}{g}*k*k,H_{out}*W_{out}] [gCinkk,HoutWout],由于存储连续,可以看成是一个三维 [ C i n g ∗ k ∗ k , H o u t , W o u t ] [\frac{C_{in}}{g}*k*k,H_{out},W_{out}] [gCinkk,Hout,Wout],或者是一个四维 [ C i n g , k ∗ k , H o u t , W o u t ] [\frac{C_{in}}{g},k*k,H_{out},W_{out}] [gCinkk,Hout,Wout],示意图如下:
在这里插入图片描述
    上图展示了如何将[1
44]的输入特征图经过一个33的滤波器滑动过程所对应的im2col转换过程,滤波器覆盖范围了的数其实im2col过程之后是不会连续存储在一起的,而是会以channel的形式进行折叠,每次滑动中的第一个元素会在col_buffer中连续存储。如果是多通道图像,由于连续存储的特性,它会保存在下一个存储块。
    在CUDA编程或者HIP编程中,开多线程来对这个转换过程进行并发,由于是多个线程进行同的并发过程,每个kernel函数中每个线程多对应的操作的是一样的,所以我们需要对每个线程要去做什么,多个线程之间能否一起协调的合作做出考虑。在设计过程中,每次起n个线程,每个线程具有自己的Index,这里可以看成一个线程池,每个线程在线程池中具有自己位置的索引。我们一起维护一个含有输出数据形状大小的数量的线程池,即 [ C o u t g , H o u t , W o u t ] [\frac{C_{out}}{g},H_{out},W_{out}] [gCout,Hout,Wout],这个线程池中的线程输出和输出特征图的像素数量可以对应上,但是在滑动的过程中,操作数是大于这个数量的,从图6可以看出中间过程中的示意图,im2col实际转换的输出矩阵大小是 [ C i n g ∗ k ∗ k , H o u t ∗ W o u t ] [\frac{C_{in}}{g}*k*k,H_{out}*W_{out}] [gCinkk,HoutWout](看成四维的形式),也就是说一个线程需要负责k*k个像素的填写。

gemm

    这里gemm操作是采用的cublas中或者rocblas的gemm计算函数,这里仅仅以FP32为例来对这个过程进行描述,在cuda中采用cublasSgemm()函数,早rocblas函数中采用rocblas_gemm_ex()函数。
    阅读上面im2col过程可知一个输入特征图数据经过im2col的过程后得到一个 [ C i n g ∗ k ∗ k , H o u t ∗ W o u t ] [\frac{C_{in}}{g}*k*k,H_{out}*W_{out}] [gCinkk,HoutWout]大小的矩阵xdata,权重w [ C o u t g , C i n g , k , k ] [\frac{C_{out}}{g},\frac{C_{in}}{g},k,k] [gCout,gCin,k,k]可以看成一个为 [ C o u t g , C i n g ∗ k ∗ k ] [\frac{C_{out}}{g},\frac{C_{in}}{g}*k*k] [gCout,gCinkk]的二维矩阵,则w*xdata就可以得到输出矩阵 [ C o u t g , H o u t ∗ W o u t ] [\frac{C_{out}}{g},H_{out}*W_{out}] [gCout,HoutWout],在使用gemm函数的过程需要注意一下其中矩阵A和B的位置。
cuBlas中

cublasHandle_t handle = blas_handle();
cudaError_t status = cublasSgemm(handle, (TB ? CUBLAS_OP_T : CUBLAS_OP_N), 
            (TA ? CUBLAS_OP_T : CUBLAS_OP_N), N, M, K, &ALPHA, B_gpu, ldb, A_gpu, lda,                    &BETA, C_gpu, ldc);
            //A_gpu xdata
            //B_gpu  w

rocBlas中

ROCBLAS_RETURN_IF_ERROR(rocblasGemmHelper(
             RocblasHandle(),
             rocblas_operation_none,
             rocblas_operation_none,
             output_image_size,M/conv_attrs_.group, kernel_dim,
             &alpha,
             b, output_image_size,  //xdata
             a, kernel_dim,         //w
             &beta,
             c, output_image_size));
             
 // gemm
inline rocblas_status rocblasGemmHelper(rocblas_handle handle,
                                        rocblas_operation transa,
                                        rocblas_operation transb,
                                        int m, int n, int k,
                                        const float* alpha,
                                        const float* A, int lda,
                                        const float* B, int ldb,
                                        const float* beta,
                                        float* C, int ldc) {
  return rocblas_gemm_ex(handle,
                         transa,
                         transb,
                         m, n, k,
                         alpha,
                         A, rocblas_datatype_f32_r, lda,
                         B, rocblas_datatype_f32_r, ldb,
                         beta,
                         C, rocblas_datatype_f32_r, ldc,
                         C, rocblas_datatype_f32_r, ldc,
                         rocblas_datatype_f32_r,
                         rocblas_gemm_algo_standard, 0, 0);

代码设计

调用模块:

const size_t kernel_rank = kernel_shape.size();

const int64_t input_image_size = input_shape.Size();
const int64_t output_image_size = output_shape.Size();
const int64_t kernel_size = TensorShape(kernel_shape).Size();
const int64_t X_offset = C / conv_attrs_.group * input_image_size;
const int64_t Y_offset = Y->Shape().Size() / Y->Shape()[0] / conv_attrs_.group;
const int64_t W_offset = W->Shape().Size() / conv_attrs_.group;
const int64_t kernel_dim = C / conv_attrs_.group * kernel_size;
const int64_t single_col_buffer_size = kernel_dim * output_image_size;

  //展开IM2col过程所需要的临时变量
const int64_t col_buffer_size = kernel_dim  *output_image_size;
const int64_t im2col_X_offset = C  * input_image_size;

auto* col_data = alloc->Alloc(sizeof(HipT) * SafeInt<size_t>(col_buffer_size));
BufferUniquePtr col_buffer(col_data, BufferDeleter(std::move(alloc)));
auto* col_buffer_data =reinterpret_cast <HipT*>(col_buffer.get());//static_cast

const HipT zero = ToHipType<T>::FromFloat(0.f);
const float alpha = 1.0f;

for (int image_id = 0; image_id < N; ++image_id) {
  for (int group_id = 0; group_id < conv_attrs_.group; ++group_id) {
     auto *a = W->Data<float>() + (group_id) * W_offset; //W
     auto *b = col_buffer_data+(group_id)*single_col_buffer_size;
     auto *im = Xdata + (image_id*conv_attrs_.group+group_id)*X_offset; //X
     auto *c = Ydata + (image_id*conv_attrs_.group+group_id)*Y_offset;

     const float alpha = 1.0;
     const float beta = 0.0;

     ROCBLAS_RETURN_IF_ERROR(rocblasGemmHelper(
        RocblasHandle(),
        rocblas_operation_none,
        rocblas_operation_none,
        output_image_size,M/conv_attrs_.group, kernel_dim,
        &alpha,
        b, output_image_size,
        a, kernel_dim,
        &beta,
        c, output_image_size));
      }
    }

if (Bdata!=nullptr){
      //void add_bias_gpu(hipStream_t stream,T *output,const T *biases,const int batch,const int c_out,const int out_putsize)
      add_bias_gpu<HipT>(Stream(),Ydata, Bdata, static_cast<int>(N), static_cast<int>(M), static_cast<int>(output_image_size));
}

im2col模块

template <typename T>
__global__ void im2col_gpu_kernel(const int n, const T* data_im,
        const int height, const int width, const int ksize_h, const int ksize_w,
        const int pad_t, const int pad_l,const int pad_b,const int pad_r,
        const int stride_h,const int stride_w,const int dilation_h,const int dilation_w,
        const int output_h, const int output_w,
        T *data_col,const T padding_value
    ) {
    int index = blockIdx.x*blockDim.x+threadIdx.x;
    for(; index < n; index += blockDim.x*gridDim.x){//
        int w_out = index % output_w; //线程池的w_out
        int h_index = index / output_w;
        int h_out = h_index % output_h;
        int channel_in = h_index / output_h;
        int channel_out = channel_in * ksize_h * ksize_w;
        int h_in = h_out * stride_h - pad_t;
        int w_in = w_out * stride_w - pad_l;
        T* data_col_ptr = data_col;
        data_col_ptr += (channel_out * output_h + h_out) * output_w + w_out;
        const T* data_im_ptr = data_im;
        data_im_ptr += (channel_in * height + h_in) * width + w_in;
        for (int i = 0; i < ksize_h; ++i) {
            for (int j = 0; j < ksize_w; ++j) {
                int h = h_in + i*dilation_h;
                int w = w_in + j*dilation_w;

                *data_col_ptr = (h >= 0 && w >= 0 && h < height && w < width) ?
                    data_im_ptr[i*dilation_h * width + j*dilation_w] : padding_value;
                data_col_ptr += output_h * output_w;
            }
        }
    }
}

template <typename T>
void im2col_gpu(hipStream_t stream,const T *im,
         int channels, int height, int width,
         int ksize_h,int ksize_w, int stride_h,int stride_w, 
         int pad_t,int pad_l,int pad_b,int pad_r,
         int dilation_h,int dilation_w,
         T *data_col){
    // We are going to launch channels * height_col * width_col kernels, each
    // kernel responsible for copying a single-channel grid.
    const T padding_value = 0;
    int output_h = (height + pad_b + pad_t - (dilation_h * (ksize_h - 1) + 1)) / stride_h + 1;
    int output_w = (width + pad_l + pad_r - (dilation_w * (ksize_w - 1) + 1)) / stride_w + 1;
    int num_kernels = channels * output_h * output_w;
    im2col_gpu_kernel<<<(num_kernels+BLOCK-1)/BLOCK,
        BLOCK,0,stream>>>(
                num_kernels, im, height, width, ksize_h,ksize_w, pad_t,pad_l,pad_b,pad_r,
                stride_h,stride_w,dilation_h, dilation_w,
                output_h,output_w, data_col,padding_value);
    //if(hipDeviceSynchronize()) printf("hipDeviceSynchronize failed at im2col_gpu\n ");
    //printf("im2col kernel done\n ");
}

瓶颈与优化

    上述代码其实有瓶颈,一是每次代码都需要做im2col的转存操作,增加了全局内存的读写时间;二是im2col过程中有两次for循环,一次是根据数据的batch_id,另外一个是根据分组数,在实际测速的时候发现在分组卷积的时候有瓶颈,回想这个设计的过程,im2col这个过程其实没有这个限制,主要是采用rocblas中gemm的时候不能多线程,一个是RocblasHandle()是单线程的,另一个是他们都会默认在0流上运行,如果需要多线程执行gemm就需要分流且多创建几个RocblasHandle(),然而RocblasHandle()最好是创建全局的,不至于是这个卷积计算的时候创建RocblasHandle(),然后这个卷积计算完后又释放掉,一个模型中含有很多卷积,如果一直这样创建RocblasHandle()然后释放RocblasHandle(),必然十分浪费时间。
    解决这个问题,引入rocblas的批量处理,改进调用接口:

const size_t kernel_rank = kernel_shape.size();

const int64_t input_image_size = input_shape.Size();
const int64_t output_image_size = output_shape.Size();
const int64_t kernel_size = TensorShape(kernel_shape).Size();
const int64_t X_offset = C / conv_attrs_.group * input_image_size;
const int64_t Y_offset = Y->Shape().Size() / Y->Shape()[0] / conv_attrs_.group;
const int64_t W_offset = W->Shape().Size() / conv_attrs_.group;
const int64_t kernel_dim = C / conv_attrs_.group * kernel_size;
const int64_t single_col_buffer_size = kernel_dim * output_image_size;

//展开IM2col过程所需要的临时变量
const int64_t col_buffer_size = kernel_dim *conv_attrs_.group *output_image_size;
const int64_t im2col_X_offset = C  * input_image_size;

auto* col_data = alloc->Alloc(sizeof(HipT) * SafeInt<size_t>(col_buffer_size));
BufferUniquePtr col_buffer(col_data, BufferDeleter(std::move(alloc)));
auto* col_buffer_data =reinterpret_cast <HipT*>(col_buffer.get());//static_cast

const HipT zero = ToHipType<T>::FromFloat(0.f);
const float alpha = 1.0f;
//const float beta = 0.0f;

if(kernel_rank==2||kernel_rank==1){ 
for (int image_id = 0; image_id < N; ++image_id) {
  auto *temp_b = col_buffer_data;
  auto *im_src = reinterpret_cast<const HipT*>(Xdata + (image_id)*im2col_X_offset); //X

  if(kernel_rank==2)
     im2col_gpu<HipT>(Stream(),im_src, C ,input_shape[0],input_shape[1],kernel_shape[0],kernel_shape[1],strides[0],strides[1],pads[0],pads[1],pads[2],pads[3],dilations[0],dilations[1],temp_b);
  else if(kernel_rank==1)
     im2col_gpu<HipT>(Stream(),im_src, C ,1,input_shape[0],1,kernel_shape[0],1,strides[0],0,pads[0],0,pads[1],1,dilations[0],temp_b);//最后一个0是padding的value
     auto *a = Wdata ; //W
     auto *b = col_buffer_data;
     auto *c = Ydata + (image_id*conv_attrs_.group)*Y_offset;

     const int stride_A = M/conv_attrs_.group*kernel_dim;
     const int stride_B = output_image_size*kernel_dim;
     const int stride_C = M/conv_attrs_.group*output_image_size;

     ROCBLAS_RETURN_IF_ERROR(rocblasGemmStridedBatchedHelper(
            RocblasHandle(),
            rocblas_operation_none,
            rocblas_operation_none,
            static_cast<int>(output_image_size),static_cast<int>(M/conv_attrs_.group), static_cast<int>(kernel_dim),
            &alpha,
            b, static_cast<int>(output_image_size),stride_B,//x
            a, static_cast<int>(kernel_dim),stride_A, //w
            &alpha,
            c, static_cast<int>(output_image_size),stride_C,
            static_cast<int>(conv_attrs_.group)));
 }

    这样修改之后就已经不具有分组卷积的瓶颈了,速度也确实在分组卷积上提升了很多,大致提升了50%往上。这样做的时候需要注意,每一次im2col分块所需要的临时内存也更多了。

思考

    之前一直觉得im2col+gemm是优化卷积的一种方式,现在觉得他只是一种最普普通通的卷积实现方式,或许他有一定的优点,就是在做gemm的时候缓存命中率比较高,相比于其他的卷积优化实现还是略显逊色。

参考链接:
【1】详解Im2Col+Pack+Sgemm策略更好的优化卷积运算
【2】im2col方法实现卷积算法
【3】深入理解神经网络中的反(转置)卷积

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

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

相关文章

OpenGL 帧缓冲

1.简介 我们已经使用了很多屏幕缓冲了&#xff1a;用于写入颜色值的颜色缓冲、用于写入深度信息的深度缓冲和允许我们根据一些条件丢弃特定片段的模板缓冲。这些缓冲结合起来叫做帧缓冲(Framebuffer)&#xff0c;它被储存在内存中。OpenGL允许我们定义我们自己的帧缓冲&#x…

Fiddler不仅可以抓包,还可以做接口测试喔

前言 Fiddler最大的优势在于抓包&#xff0c;我们大部分使用的功能也在抓包的功能上&#xff0c;Fiddler做接口测试也是非常方便的。对应没有接口测试文档的时候&#xff0c;可以直接抓完包后&#xff0c;copy请求参数&#xff0c;修改下就可以了。 Composer简介 点开右侧Co…

模拟电路系列分享-晶体管的四种状态

目录 概要 整体架构流程 技术名词解释 1.截止状态 2.放大状态 3.饱和状态 4.倒置状态 技术细节 小结 概要 提示&#xff1a;这里可以添加技术概要 晶体管有4种工作状态&#xff0c;分别是截止、放大、饱和&#xff0c;以及倒置。 整体架构流程 提示&#xff1a;这里可以添加…

黑马程序员前端 Vue3 小兔鲜电商项目——(九)购物车

文章目录 本地购物车添加购物车头部购物车模板代码渲染数据 删除功能实现购物车统计信息列表购物车-基础内容渲染模版代码路由配置渲染列表 列表购物车-单选功能实现列表购物车-全选功能实现列表购物车-统计数据功能实现 接口购物车加入购物车删除购物车 退出登录-清空购物车购…

Matplotlib - 绘制 带有对角线的散点图 (Diagonal Scatter Plots) 函数源码

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://blog.csdn.net/caroline_wendy/article/details/131384440 Matplotlib 是一个用于绘制二维图形的 Python 库&#xff0c;提供了一个 pyplot 模块&#xff0c;用于创建各种类型的图表。其…

实战经验---好用的Midjourney提示词

说明&#xff1a;本文基于Midjourney&#xff08;下称MJ&#xff09;官方说明书整理&#xff0c;分2篇文章发布&#xff0c;这是第1篇。内容主要包括绘画指令&#xff08;Prompt&#xff09;、提示、参数、设置、模型选择等。 由于是整理文本&#xff0c;过程中个别地方为了显…

应对流量损耗:提升APP广告变现效果的关键策略!

​引言&#xff1a; 在APP广告变现的过程中&#xff0c;流量损耗是一个常见的问题&#xff0c;它不可避免地会发生。尽管开发者可以在合理的范围内承受这种损耗&#xff0c;但如果出现大范围的损耗&#xff0c;那就意味着在广告变现过程中出现了一些问题&#xff0c;限制了开发…

ThreadPoolExecutor的有参构造

核心要点 通过查看ThreadPoolExecutor里面的构造方法可以发现都是调用了方法参数最多的那个。 参数最多的构造方法展示 映入眼帘的是一些健壮性校验corePoolSize < 0&#xff1b;可以发现这个判断是核心线程的个数不能小于零。但是也就说明核心线程的个数可以等于零。maxim…

LLM - 搭建 DrugGPT 与药物分子领域结合的 ChatGPT 系统

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://blog.csdn.net/caroline_wendy/article/details/131384199 论文&#xff1a;DrugChat: Towards Enabling ChatGPT-Like Capabilities on Drug Molecule Graphs DrugChat&#xff0c;基…

【简单认识Nginx服务性能与安全优化】

文章目录 Nginx隐藏版本相关信息1.隐藏版本号2.修改版本号及相关信息 二、修改Nginx运行时的属主和属组三、配置Nginx网页缓存时间四、配置Nginx站点日志分割五、设置Nginx长连接及超时时间六、配置Nginx网页压缩七、配置Nginx防盗链1.模拟盗链2.配置防盗链并测试 Nginx隐藏版本…

【算法】数学相关知识总结

文章目录 gcd 和 lcm取模运算 %求一个点和一片矩形区域之间的最短距离 本文用于记录一些关于算法题中偶尔被使用到的数学相关知识。 gcd 和 lcm gcd 和 lcm 分别是 最大公约数&#xff08;Greatest common divisor&#xff09; 和 最小公因数&#xff08;Least Common Multip…

Nginx-redis【会话保持】

什么是会话保持 当用于登录一个网站服务器&#xff0c; 网站服务器会将用户的登录信息存储下来&#xff08;存储session&#xff09;&#xff0c;以保证我们能一直处于登录在线的状态 为什么要做会话保持 由于我们使用的是负载均衡轮询机制&#xff0c;会导致用户请求分散在…

nginx页面优化

文章目录 nginx页面优化一.nginx优化1.版本号1.1查看版本号1.2修改版本号1.2.1修改配置文件1.2.2修改源码文件&#xff0c;重新编译安装 2.nginx的日志分割2.1 写日志分割的脚本2.2给脚本执行权限、执行2.3创建定时任务可以每个月固定分割一次 3.nginx的页面压缩3.1配置3.2验证…

第1讲 Camera Sensor Driver课程简介

更多资源&#xff1a; 资源描述在线课程极客笔记在线课程知识星球星球名称&#xff1a;深入浅出Android Camera 星球ID: 17296815Wechat极客笔记圈 课程内容介绍 目标&#xff1a; 课程主要帮助大家理解camera sensor基本概念及原理知识&#xff0c;理解高通camera sensor驱…

前端开发——常用案例分享

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…

美one,不想只做李佳琦背后的MCN

文 | 螳螂观察 作者 | 青月 今年618&#xff0c;处在消费提振的关键时期&#xff0c;又面临着行业竞争日趋激烈&#xff0c;各大电商平台都希望能交出一份让市场满意的“期中答卷”&#xff0c;直播不可避免的成为了“兵家必争之地”。 京东请来了罗永浩与“交个朋友”入驻&a…

抖音seo矩阵系统源码|需求文档编译说明(二)

目录 1.抖音seo矩阵系统文档开发流程 2.各平台源码编译方式说明 3.底层技术功能表达式 1.抖音seo矩阵系统文档开发流程 ①产品原型 ②需求文档 ③产品流程图 ④部署方式说明 ⑤完整源码 ⑥源码编译方式说明 ⑦三方框架和SDK使用情况说明和代码位置 ⑧平台操作文档 ⑨程序架…

完美外贸企业邮箱签名设置指南:提升专业形象,优化商务沟通

邮箱签名是一个很好的方式&#xff0c;让您的外贸企业觉得你的公司比较专业。它可以包括公司名称和标志&#xff0c;联系信息和其他相关信息。以下是一些指导原则&#xff0c;可以帮助您创建适合您业务的邮箱签名: 第一步:选择一个合适的Logo 选择一个适合你的企业的标志是很重…

踏响新中国钢铁工业的铿锵足音《淬火丹心》湖北卫视热血开播

由湖北省广播电视局、湖北广播电视台、湖北长江华晟影视有限责任公司等联合摄制&#xff0c;向勇执导&#xff0c;唐曾、吕一、王雨、董晴等主演的大型现代工业题材剧《淬火丹心》&#xff0c;将于6月25日登陆湖北卫视长江剧场开播。该剧以年轻一代中国钢铁人的励志成长为主线&…