Ascend C算子性能优化实用技巧05——API使用优化

Ascend C是CANN针对算子开发场景推出的编程语言,原生支持C和C++标准规范,兼具开发效率和运行性能。使用Ascend C,开发者可以基于昇腾AI硬件,高效的实现自定义的创新算法。

目前已经有越来越多的开发者使用Ascend C,我们将通过几期“Ascend C算子性能优化”专题分享,围绕开发者最为关心的算子性能优化环节,介绍Ascend C算子常用的优化技巧,帮助开发者自主构建出更优性能的算子。专题内容将围绕流水优化、搬运优化、内存优化、API使用优化以及Tiling优化等优化技巧,从方案讲解、优化案例、性能对比等多角度展开介绍。前期内容回顾:

  1. 《Ascend C算子性能优化实用技巧01——流水优化》
  2. 《Ascend C算子性能优化实用技巧02——内存优化》
  3. 《Ascend C算子性能优化实用技巧03——搬运优化》
  4. 《Ascend C算子性能优化实用技巧04——Tiling优化》

下面进入第五期内容,介绍API的相关使用技巧:

  1. 在算子kernel类外创建和初始化TPipe对象,触发类内Scalar编译优化
  2. 通过TQueBind接口,实现VECIN和VECOUT内存复用
  3. 通过SetMaskCount接口开启Counter模式,简化mask处理逻辑
  4. Matmul使能AtomicAdd选项,减少Vector计算
  5. 合理使用归约指令,兼顾累加效率和执行性能

在算子kernel类外创建和初始化TPipe对象,触发类内Scalar编译优化

程序在创建类对象时,会分配内存空间,用于存储类中的相关成员变量或函数。当类中变量需要参与计算时,变量值从内存被加载到寄存器,计算完成后,变量从寄存器存储回内存。Scalar常量折叠和常量传播是编译器编译时的优化方式,优化前编译器会判断变量是否只初始化过一次或只赋值过一次,若满足此编译优化的前提条件,变量值将会尽量驻留在寄存器中,从而在后续使用变量时,将减少读取内存的操作,提升运行性能。

TPipe是用来管理全局内存和同步的框架,用户可以调用TPipe的接口,为TQue/TBuf进行内存分配。在编写Ascend C算子过程中,经常用一个类存放计算所需的相关变量,这里称类名为KernelExample。当TPipe对象在KernelExample类的实现中定义并初始化时,TPipe对象的内存空间在整个KernelExample对象的内存空间之中;需要注意的是,创建TPipe对象时,对象初始化会设置全局变量的TPipe指针,这导致KernelExample对象的内存有被外部污染的风险,此时编译器的编译优化将采取保守策略,不会对KernelExample对象中的Scalar变量进行常量折叠和常量传播。因此,在任何场景下,我们都建议将TPipe对象创建于KernelExample类外部,使得TPipe对象的内存空间独立于KernelExample类对象的内存空间,触发编译器对KernelExample类内Scalar的编译优化,减少算子Scalar指令耗时。

【反例】

代码中TPipe对象由KernelExample类内部创建并初始化,影响编译器scalar折叠优化,在npu侧导致scalar无谓增加。

template <typename ComputeT> class KernelExample {public:__aicore__ inline KernelExample() {}__aicore__ inline void Init(...){...pipe.InitBuffer(xxxBuf, BUFFER_NUM, xxxSize);...}private:...TPipe pipe;...};extern "C" __global__ __aicore__ void example_kernel(...){...KernelExample<float> op;op.Init(...);...}

【正例】

改为由kernel入口函数创建TPipe对象,在KernelExample类中保存TPipe指针使用。

template <typename ComputeT> class KernelExample {public:__aicore__ inline KernelExample() {}__aicore__ inline void Init(..., TPipe* pipeIn){...pipe = pipeIn;pipe->InitBuffer(xxxBuf, BUFFER_NUM, xxxSize);...}private:...TPipe* pipe;...};extern "C" __global__ __aicore__ void example_kernel(...){...TPipe pipe;KernelExample<float> op;op.Init(..., &pipe);...}

【性能对比】

图1.aiv_scalar_time优化前后对比

图2.aiv_scalar_ratio优化前后对比

 通过性能数据对比可以看出,scalar time优化明显,平均时间从281us减少到236us,下降17%;平均scalar_time时延占比从21%下降到17%。因此在scalar bound的场景下可以使用此优化措施。

通过TQueBind接口,实现VECIN和VECOUT内存复用

 纯搬运类算子在执行并不涉及实际vector计算,若存在冗余的vector计算,会导致算子整体执行时间变长。这种场景可以使用Ascend C针对纯搬运类算子提供的TQueBind接口,该接口可以将VECIN和VECOUT之间绑定,省略将数据从VECIN拷贝到VECOUT的步骤,从而避免vector的无谓消耗。

 【反例】

此代码片段中存在LocalTensor -> LocalTensor的DataCopy指令,目的是为了保证搬入和搬出之间的流水同步。

template <typename ComputeT> class KernelExample {public:...__aicore__ inline void Process(...){for (int i = 0; i < iLen; ++i) {...  auto iLocal = QueI.AllocTensor<ComputeT>();DataCopy(iLocal, inGm[i * 32], size);QueI.EnQue(iLocal);auto iLocal = QueI.DeQue<ComputeT>();for (int j = 0; j < jLen; ++j) {  ...auto oLocal = QueO.AllocTensor<ComputeT>();DataCopy(oLocal, iLocal, size); // LocalTensor -> LocalTensor的DataCopy指令,以实现数据从VECIN到VECOUT的搬移QueO.EnQue(oLocal);auto oLocal = QueO.DeQue<ComputeT>();DataCopyPad(outGm[j], oLocal, ...);QueO.FreeTensor(oLocal);}QueI.FreeTensor(iLocal);}}private:...  TQue<QuePosition::VECIN, BUFFER_NUM> QueI;TQue<QuePosition::VECOUT, BUFFER_NUM> QueO;...};extern "C" __global__ __aicore__ void example_kernel(...){...op.Process(...);}

 【正例】

将LocalTensor -> LocalTensor的DataCopy指令替换为TQueBind接口,避免将VECIN拷贝到VECOUT的步骤,从而避免了冗余vector计算。

template <typename ComputeT> class KernelExample {public:...__aicore__ inline void Process(...){for (int i = 0; i < iLen; ++i) {...  auto bindLocal = queBind.AllocTensor<ComputeT>();DataCopy(bindLocal, inGm[i * 32], size);queBind.EnQue(bindLocal);auto bindLocal = queBind.DeQue<ComputeT>();for (int j = 0; j < len; ++j) {...DataCopyPad(outGm[j], bindLocal, ...);}queBind.FreeTensor(bindLocal);}}private:...  TQueBind<QuePosition::VECIN, QuePosition::VECOUT, BUFFER_NUM> queBind; // 使用TQueBind替换原来QueI,QueO...};extern "C" __global__ __aicore__ void example_kernel(...){...op.Process(...);}

【性能对比】

图3.aiv_vec_time优化前后对比

 如上图所示,将反例中DataCopy指令替换为TQueBind之后有明显优化。由于省略了数据从VECIN拷贝到VECOUT的步骤,aiv_vec_time几乎缩减为0。

通过SetMaskCount接口开启Counter模式,简化mask处理逻辑

开发者可以通过mask参数进行掩码操作来控制实际参与计算的个数,目前支持Normal和Counter两种模式:

  1. Normal模式:开发者需要自行指定每次迭代内参与计算的元素以及迭代次数,系统默认为Normal模式。

  2. Counter模式:开发者直接传入计算数据量,实际迭代次数由Vector计算单元自动推断。

通过对比我们可以看到,Normal模式虽然具备单次迭代内的mask能力,但使用不如Counter模式方便,Counter不需要开发者感知迭代次数、处理非对齐尾块的操作。

具体来看,Normal模式下,当用户想要指定API计算的总元素个数时,首先需要自行判断是否存在不同的主尾块,主块需要将mask设置为全部元素参与计算,并且计算主块所需迭代次数,然后根据尾块中剩余元素个数重置mask,再进行尾块的运算,这中间涉及大量Scalar计算。Counter模式下,用户不需要计算迭代次数以及判断是否存在尾块,将mask模式设置为Counter模式后,只需要设置mask为{0, 总元素个数},然后调用相应的API,处理逻辑更简便,同时减少代码量和Scalar计算量。

以下反例和正例中的代码均以AddCustom算子为例,修改其中如下Add接口的调用代码,以说明Counter模式的优势。

AscendC::Add(zLocal, xLocal, yLocal, this->tileLength);

【反例】

输入数据类型为half的xLocal, yLocal,数据量均为15000。Normal模式下,每个迭代内参与计算的元素个数最多为256B/sizeof(half)=128个,所以15000次Add计算会被分为:主块计算15000/128=117次迭代,每次迭代128个元素参与计算;尾块计算1次迭代,该迭代15000-117*128=24个元素参与计算。从代码角度,需要计算主块的repeatTimes、尾块元素个数;主块计算时,设置mask值为128,尾块计算时,需要设置mask值为尾块元素个数24;这些过程均涉及Scalar计算。

uint32_t ELE_SIZE = 15000;
AscendC::BinaryRepeatParams binaryParams;uint32_t numPerRepeat = 256 / sizeof(DTYPE_X);  // DTYPE_X为half数据类型
uint32_t mainRepeatTimes = ELE_SIZE / numPerRepeat;
uint32_t tailEleNum = ELE_SIZE % numPerRepeat;AscendC::SetMaskNorm();
AscendC::SetVectorMask<DTYPE_X, AscendC::MaskMode::NORMAL>(numPerRepeat); // 设置normal模式mask,使每个迭代计算128个数
AscendC::Add<DTYPE_X, false>(zLocal, xLocal, yLocal, AscendC::MASK_PLACEHOLDER, mainRepeatTimes, binaryParams);   // MASK_PLACEHOLDER值为0,此处为mask占位,实际mask值以SetVectorMask设置的为准
if (tailEleNum > 0) {AscendC::SetVectorMask<DTYPE_X, AscendC::MaskMode::NORMAL>(tailEleNum); // 设置normal模式mask,使每个迭代计算24个数// 偏移tensor的起始地址,在xLocal和yLocal的14976个元素处,进行尾块计算AscendC::Add<DTYPE_X, false>(zLocal[mainRepeatTimes * numPerRepeat], xLocal[mainRepeatTimes * numPerRepeat],  yLocal[mainRepeatTimes * numPerRepeat], AscendC::MASK_PLACEHOLDER, 1, binaryParams);   
}
AscendC::ResetMask();  // 还原mask值

【正例】

输入数据类型为half的xLocal, yLocal,数据量均为15000。Counter模式下,只需要设置mask为所有参与计算的元素个数15000,然后直接调用Add指令,即可完成所有计算,不需要繁琐的主尾块计算,代码较为简练。

当要处理多种15000个元素的矢量计算时,Counter模式的优势更明显,不需要反复修改主块和尾块不同的mask值。

uint32_t ELE_SIZE = 15000;
AscendC::BinaryRepeatParams binaryParams;
AscendC::SetMaskCount();
AscendC::SetVectorMask<DTYPE_X, AscendC::MaskMode::COUNTER>(ELE_SIZE);  // 设置counter模式mask,总共计算15000个数
AscendC::Add<DTYPE_X, false>(zLocal, xLocal, yLocal, AscendC::MASK_PLACEHOLDER, 1, binaryParams);                // MASK_PLACEHOLDER值为0,此处为mask占位,实际mask值以SetVectorMask设置的为准
AscendC::ResetMask();  // 还原mask值

【性能数据】

图4.normal模式 vs counter模式scalar time

图5.normal模式 vs counter模式aiv cycle

图6.normal模式 vs counter模式 total time

从上述三幅性能对比图和示例代码可以看到,使用Counter模式能够大幅度简化代码,易于维护,同时能够降低Scalar和Vector计算耗时,获得性能提升。 

Matmul使能AtomicAdd选项,减少Vector计算

对于Matmul得到的结果矩阵C(m, n),若后续需要和GM上的矩阵D(m, n)进行Add操作,则可以在GetTensorC接口或者IterateAll接口的GM通路上,将enAtomic参数设为1,开启AtomicAdd累加操作,在搬出矩阵C到GM时,矩阵C的结果将直接累加到矩阵D的GM地址上,从而实现与矩阵D的Add操作。

【反例】

将Matmul的结果矩阵C和GM上的矩阵D分别搬到UB上,做完Add操作后,结果再搬出到GM。这样至少要多分配一块UB内存给矩阵D,假设在分离架构的处理器上执行,将多做三次搬运操作(矩阵C从GM搬到UB、矩阵D从GM搬到UB、Add结果从UB搬出到GM)。

 template <class A_TYPE, class B_TYPE, class C_TYPE, class BIAS_TYPE>__aicore__ inline void MatMulKernel(...){...Matmul<A_TYPE, B_TYPE, C_TYPE, BIAS_TYPE, CFG_MDL> mm;TPipe pipe;REGIST_MATMUL_OBJ(&pipe, GetSysWorkSpacePtr(), mm);mm.SetTensorA(gm_a);mm.SetTensorB(gm_b);mm.SetBias(gm_bias);mm.IterateAll(local_c);// while (mm.Iterate()) {// mm.GetTensorC(local_c);// }DataCopy(local_d, gm_d, d_size);event_t eventIdMTE2ToV = static_cast<event_t>(GetTPipePtr()->FetchEventID(HardEvent::MTE2_V));SetFlag<HardEvent::MTE2_V>(eventIdMTE2ToV);WaitFlag<HardEvent::MTE2_V>(eventIdMTE2ToV);Add(local_d, local_d, local_c, d_size);DataCopy(gm_d, local_d, d_size);...}extern "C" __global__ __aicore__ void example_kernel(...){...typedef MatmulType<TPosition::GM, CubeFormat::ND, half> aType;  typedef MatmulType<TPosition::GM, CubeFormat::ND, half> bType;  typedef MatmulType<TPosition::GM, CubeFormat::ND, float> cType;  typedef MatmulType<TPosition::GM, CubeFormat::ND, float> biasType;MatMulKernel<aType, bType, cType, biasType)(...);...}

【正例】

计算Matmul结果时,调用IterateAll接口或者GetTensorC接口搬运到矩阵D的GM地址上,同时将接口中enAtomic参数设为1,搬出到GM时,Matmul结果矩阵C会累加到矩阵D上,从而得到两个矩阵Add后的结果。

 template <class A_TYPE, class B_TYPE, class C_TYPE, class BIAS_TYPE>__aicore__ inline void MatMulKernel(...){...Matmul<A_TYPE, B_TYPE, C_TYPE, BIAS_TYPE, CFG_MDL> mm;TPipe pipe;REGIST_MATMUL_OBJ(&pipe, GetSysWorkSpacePtr(), mm);mm.SetTensorA(gm_a);mm.SetTensorB(gm_b);mm.SetBias(gm_bias);mm.IterateAll(gm_d, 1); // IterateAll接口中的enAtomic设为1// while (mm. Iterate ()) {// mm.GetTensorC(gm_d, 1);     // GetTensorC接口中的enAtomic设为1// }...}extern "C" __global__ __aicore__ void example_kernel(...){...typedef MatmulType<TPosition::GM, CubeFormat::ND, half> aType;  typedef MatmulType<TPosition::GM, CubeFormat::ND, half> bType;  typedef MatmulType<TPosition::GM, CubeFormat::ND, float> cType;  typedef MatmulType<TPosition::GM, CubeFormat::ND, float> biasType;MatMulKernel<aType, bType, cType, biasType)(...);...}

【性能对比】

图7.Matmul使能AtomicAdd选项前后性能对比

以矩阵维度M=64,N=256,K=256,矩阵D为(64, 256)为例,Matmul使能AtomicAdd选项前后的性能对比如上图所示,平均cycle数从开启AtomicAdd选项前的154181变为开启后的135054,性能优化12.4%。因此在这种场景下,使能AtomicAdd选项能获取更优的性能。

合理使用归约指令,兼顾累加效率和执行性能

对于需要将一段连续buffer上的元素全部累加到一个元素上的场景。使用WholeReduceSum的累加效率较高,但是单条指令的执行速度不如BlockReduceSum,因此,为了兼顾累加的效率以及尽量使用执行较快的指令,需要根据不同的shape,通过不同的指令组合达到最佳的性能。

例如在float类型输入,shape大小为256时,使用两次WholeReduceSum或者三次BlockReduceSum都可以得到256个float的累加结果。考虑规约指令之间的组合,一次BlockReduceSum加一次WholeReduceSum也可以完成上述累加效果。由于单条指令,BlockReduceSum执行速度更快,所以性能更优于两次WholeReduceSum,并且由于只使用了两次规约指令,所以性能同样优于三次BlockReduceSum的方案。

【反例】

反例1:

...
static constexpr uint32_t REP_LEN = 256;
TBuf<QuePosition::VECCALC> calcBuf;
pipe.InitBuffer(calcBuf, totalLength * sizeof(float));
AscendC::LocalTensor<float> tempTensor1 = calcBuf.Get<float>();
constexpr uint32_t repCount = REP_LEN / sizeof(float);
const uint32_t repNum0 = (totalLength + repCount - 1) / repCount;
AscendC::SetMaskCount();
AscendC::SetVectorMask<float>(0, totalLength);
AscendC::WholeReduceSum<float, false>(tempTensor1, xLocal, AscendC::MASK_PLACEHOLDER, 1,DEFAULT_BLK_STRIDE, DEFAULT_BLK_STRIDE, DEFAULT_REP_STRIDE);
AscendC::PipeBarrier<PIPE_V>();
AscendC::SetVectorMask<float>(0, repNum0);
AscendC::WholeReduceSum<float, false>(zLocal, tempTensor1, AscendC::MASK_PLACEHOLDER, 1,DEFAULT_BLK_STRIDE, DEFAULT_BLK_STRIDE, DEFAULT_REP_STRIDE);
AscendC::PipeBarrier<PIPE_V>();
AscendC::SetMaskNorm();
...


反例2

...
constexpr uint32_t c0Count = BLK_LEN / sizeof(DTYPE_X);
const uint32_t blockNum0 = (totalLength + c0Count - 1) / c0Count;
const uint32_t blockNum1 = (blockNum0 + c0Count - 1) / c0Count;
AscendC::SetMaskCount();
AscendC::SetVectorMask<DTYPE_X>(0, totalLength);
AscendC::BlockReduceSum<DTYPE_X, false>(tempTensor1, xLocal, AscendC::MASK_PLACEHOLDER, 1,DEFAULT_BLK_STRIDE, DEFAULT_BLK_STRIDE, DEFAULT_REP_STRIDE);
AscendC::PipeBarrier<PIPE_V>();
AscendC::SetVectorMask<DTYPE_X>(0, blockNum0);
AscendC::BlockReduceSum<DTYPE_X, false>(tempTensor1, tempTensor1, AscendC::MASK_PLACEHOLDER, 1,DEFAULT_BLK_STRIDE, DEFAULT_BLK_STRIDE, DEFAULT_REP_STRIDE);
AscendC::PipeBarrier<PIPE_V>();
AscendC::SetVectorMask<DTYPE_X>(0, blockNum1);
AscendC::BlockReduceSum<DTYPE_X, false>(zLocal, tempTensor1, AscendC::MASK_PLACEHOLDER, 1,DEFAULT_BLK_STRIDE, DEFAULT_BLK_STRIDE, DEFAULT_REP_STRIDE);
AscendC::PipeBarrier<PIPE_V>();
AscendC::SetMaskNorm();
...

【正例】

当输入shape为256,输入类型为float时。采用一次BlockReduceSum加一次WholeReduceSum的组合方式实现将256个float元素求和。完整样例链接请参考ReduceCustom。

...
static constexpr uint32_t BLK_LEN = 32;
TBuf<QuePosition::VECCALC> calcBuf;
pipe.InitBuffer(calcBuf, totalLength * sizeof(float));
AscendC::LocalTensor<float> tempTensor1 = calcBuf.Get<float>();
constexpr uint32_t c0Count = BLK_LEN / sizeof(float);
const uint32_t blockNum0 = (totalLength + c0Count - 1) / c0Count;
AscendC::SetMaskCount();
AscendC::SetVectorMask<float>(0, totalLength);
AscendC::BlockReduceSum<float, false>(tempTensor1, xLocal, AscendC::MASK_PLACEHOLDER, 1,DEFAULT_BLK_STRIDE, DEFAULT_BLK_STRIDE, DEFAULT_REP_STRIDE);
AscendC::PipeBarrier<PIPE_V>();
AscendC::SetVectorMask<float>(0, blockNum0);
AscendC::WholeReduceSum<float, false>(zLocal, tempTensor1, AscendC::MASK_PLACEHOLDER, 1,DEFAULT_BLK_STRIDE, DEFAULT_BLK_STRIDE, DEFAULT_REP_STRIDE);
AscendC::PipeBarrier<PIPE_V>();
AscendC::SetMaskNorm();
...

【性能数据】

输入shape为256,数据类型为float,两次WholeReduceSum(反例1)、三次BlockReduceSum(反例2)、一次WholeReduceSum加一次BlockReduceSum(正例),三种累加方式的性能数据(循环100次的时间总和)的性能数据如下:

表1.性能对比

反例1

反例2

正例

13us

13.94us

8.44us

更多学习资源

了解更多Ascend C算子性能优化手段和实践案例,请访问:昇腾社区Ascend C信息专区。

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

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

相关文章

计算机编程中的测试驱动开发(TDD)及其在提高代码质量中的应用

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 计算机编程中的测试驱动开发&#xff08;TDD&#xff09;及其在提高代码质量中的应用 计算机编程中的测试驱动开发&#xff08;T…

前后端交互之动态列

一. 情景 在做项目时&#xff0c;有时候后会遇到后端使用了聚合函数&#xff0c;导致生成的对象的属性数量或数量不固定&#xff0c;因此无法建立一个与之对应的对象来向前端传递数据&#xff0c;这时可以采用NameDataListVO向前端传递数据。 Data Builder AllArgsConstructo…

[笔记]L6599的极限工作条件考量

0.名词 OTP over tempature protect.OCP over current protectOVP over voltage protectBrownout Protection Undervoltage Protection可能需要考虑hysteresis response.因为要考虑一些高频干扰 1.基本的过流保护逻辑 参考&#xff1a;ST L6599 器件手册 LLC开关电源&#…

【Pikachu】XML外部实体注入实战

若天下不定&#xff0c;吾往&#xff1b;若世道不平&#xff0c;不回&#xff01; 1.XXE漏洞实战 首先写入一个合法的xml文档 <?xml version "1.0"?> <!DOCTYPE gfzq [<!ENTITY gfzq "gfzq"> ]> <name>&gfzq;</name&…

多模块集成swagger(knife4j-spring-boot-starter)

前言 单体项目、多模块单体项目、微服务项目&#xff0c;集成的方案大同小异&#xff0c;微服务会在网关做个聚合&#xff0c;后面再补充。 依赖版本 目前demo的版本如下&#xff1a; spring boot 2.7.3spring cloud 2021.0.4spring cloud alibaba 2021.0.4.0knife4j-sprin…

DataStream编程模型之数据源、数据转换、数据输出

Flink之DataStream数据源、数据转换、数据输出&#xff08;scala&#xff09; 0.前言–数据源 在进行数据转换之前&#xff0c;需要进行数据读取。 数据读取分为4大部分&#xff1a; &#xff08;1&#xff09;内置数据源&#xff1b; 又分为文件数据源&#xff1b; socket…

CSS盒子的定位>(上篇)#定位属性#相对定位-附练习

一、定位属性 1.定位方式 position属性可以选择4种不同类型的定位方式。 语法格式&#xff1a;position&#xff1a;relation | absolute | fixed参数&#xff1a;①relative生成相对定位的元素&#xff0c;相对于其正常位置进行定位。 ②absolute生成绝对定位的…

Redis/Codis性能瓶颈揭秘:网卡软中断的影响与优化

目录 现象回顾 问题剖析 现场分析 解决方案 总结与反思 1.调整中断亲和性&#xff08;IRQ Affinity&#xff09;&#xff1a; 2.RPS&#xff08;Receive Packet Steering&#xff09;和 RFS&#xff08;Receive Flow Steering&#xff09;&#xff1a; 近期&#xff0c;…

WordPress设置自动更新CSS版本号

WordPress 通常会在引用 CSS 文件时添加版本号参数&#xff08;?verx.x.x&#xff09;。如果版本号未更新&#xff0c;浏览器可能继续加载旧的文件。 解决方法&#xff1a;确保你在 functions.php 文件中正确加载了 CSS 文件&#xff0c;并动态更新版本号。例如在functions.p…

若依权限控制

springbootvue2项目中的权限控制(若依项目) 步骤: 1.登录管理员账号,为普通用户增加权限按钮 绿色部分为权限控制字符 2.在后端对应的方法上增加权限控制(这里以删除操作为例):PreAuthorize(“ss.hasPermi(‘area:store:remove’)”) 3.在前端对应的按钮上增加权限控制:v-ha…

【机器学习】如何配置anaconda环境(无脑版)

马上就要上机器学习的实验&#xff0c;这里想写一下我配置机器学习的anaconda环境的二三事 一、首先&#xff0c;下载安装包&#xff1a; Download Now | Anaconda 二、打开安装包&#xff0c;一直点NEXT进行安装 这里要记住你要下载安装的路径在哪&#xff0c;后续配置环境…

OceanBase 升级过程研究(4.2.1.6-4.2.1.8)

模拟业务 使用benchmark加载10仓数据模拟业务场景 升级方法 使用滚动升级方式来进行OB升级。该方法前提是OB集群必须满足官方规定的高可用架构(如果 Zone 个数小于 3&#xff0c;滚动升级时则无法构成多数派), 滚动升级的原理就是轮流完成每个ZONE的升级工作&#xff0c;由于…

微知-DOCA ARGP参数模块的相关接口和用法(config单元、params单元,argp pipe line,回调)

文章目录 1. 背景2. 设置参数的主要流程2.1 初始化2.2 注册某个params的处理方式以及回调函数2.4 定义好前面的params以及init指定config地点后start处理argv 3. 其他4. DOCA ARGP包相关4.1 主要接口4.2 DOCA ARGP的2个rpm包4.2.1 doca-sdk-argp-2.9.0072-1.el8.x86_64.rpm4.2.…

C#.Net筑基-字符串超全总结

字符串是日常编码中最常用的引用类型了&#xff0c;可能没有之一&#xff0c;加上字符串的不可变性、驻留性&#xff0c;很容易产生性能问题&#xff0c;因此必须全面了解一下。 01、字符与字符编码 1.1、字符Char 字符 char 表示为 Unicode字符&#xff0c;在C#中用 UTF-16 …

苍穹外卖-后端部分

软件开发整体介绍 前端搭建 在非中文目录中双击nginx.exe然后浏览器访问localhost即可 后端搭建 基础准备 导入初始文件 使用git进行版本控制 创建本地仓库和远程仓库,提交Git 连接数据库 连接数据库把资料中的文件放入运行即可 前后端联调测试 苍穹外卖项目接口文档…

剧本杀门店预约小程序,解锁沉浸式推理体验

一、开发背景 剧本杀作为一种热门娱乐游戏&#xff0c;深受大众的欢迎&#xff0c;但随着市场的快速发展&#xff0c;竞争也在不断加大&#xff0c;对于剧本杀线下商家来说面临着发展创新。 剧本杀线下门店数量目前正在逐渐增加&#xff0c;竞争激烈&#xff0c;而门店的获客…

【WPF】Prism学习(二)

Prism Commands 1.命令&#xff08;Commanding&#xff09; 1.1. ViewModel的作用&#xff1a; ViewModel不仅提供在视图中显示或编辑的数据&#xff0c;还可能定义一个或多个用户可以执行的动作或操作。这些用户可以通过用户界面&#xff08;UI&#xff09;执行的动作或操作…

学者观察 | 元计算、人工智能和Web 3.0——山东大学教授成秀珍

导语 成秀珍教授提出元计算是在开放的零信任环境下整合算力资源打通数据壁垒构建自进化智能的新质生产力技术&#xff0c;是一种新计算范式&#xff1b;区块链是Web3.0的核心技术之一&#xff0c;有助于保障开放零信任环境下&#xff0c;用户、设备和服务间去中心化数据流通的…

学习笔记022——Ubuntu 安装 MySQL8.0版本踩坑记录

目录 1、查看可安装 MySQL 版本 2、Ubuntu安装 MySQL8.0 3、MySQL8.0 区分大小写问题 4、MySQL8.0 设置sql_mode 5、MySQL8.0 改端口33060&#xff08;个人遇到问题&#xff09; 1、查看可安装 MySQL 版本 ## 列出可用的MySQL版本&#xff08;列出所有可用的MySQL版本以…

「AI Infra 软件开源不是一个选项,而是必然」丨云边端架构和 AI Infra专场回顾@RTE2024

在人工智能和开源技术蓬勃发展的当下&#xff0c;AI Infra 项目正经历着日新月异的变革。从跨平台运行时到云边端 AI 基础设施&#xff0c;再到多模态知识助手&#xff0c;创新浪潮席卷而来。这些进步不仅显著提升了技术指标&#xff0c;也为实时音视频处理、边缘计算、大模型应…