【2023CANN训练营第二季】——通过一份入门级算子开发代码了解Ascend C算子开发流程

本次博客讲解的代码是Gitee代码仓的Ascend C加法算子开发代码,代码地址为:
quick-start

打开Add文件,可以看到文件结构如下:

image.png

其中add_custom.cpp是算子开发的核心文件,包括了核函数的实现,展示了如何在Ascend平台上使用Ascend C编写算子以及如何在CPU和NPU上运行算子。
main.cpp的作用是用于调用名为 add_custom 的算子进行向量相加操作,根据定义的宏 CCE_KT_TEST 来选择执行哪个部分。
CMakeLists.txt是编译cpu侧或npu侧运行的算子的编译工程文件
run.sh 编译运行算子的脚本

下面我们重点看add_custom.cpp文件和main.cpp文件,以此了解核函数的开发流程和进行CPU侧和NPU侧验证
在进行核函数开发之间,我们先分析算子的表达式:

1.算子分析

Add算子的算子分析如下:

数学表达式:
Add算子的数学表达式为 z = x + y,即将输入x和y逐元素相加,得到输出z。

输入和输出:

算子有两个输入:x和y。

输入数据类型为half(float16)。

输入数据形状(shape)为(8, 2048),即一个8x2048的二维数组。

输入数据格式(format)为ND(N-Dimensional),表示通用的多维数组格式。

算子有一个输出:z。

输出数据类型与输入数据类型相同,为half(float16)。

输出数据形状与输入形状相同,为(8, 2048)。

核函数名称和参数:

核函数的名称为 add_custom。
核函数有三个参数:x、y、z。
x和y是输入在全局内存(Global Memory)上的内存地址。
z是输出在全局内存上的内存地址。
实现所需接口:

数据搬移接口:需要使用 DataCopy 来实现输入数据的搬运,将数据从全局内存搬移到AI Core的局部内存。
矢量计算接口:通过使用矢量双目指令接口 Add 来完成x和y的逐元素相加。
内存管理接口:使用 AllocTensor 和 FreeTensor 来申请和释放Tensor数据结构,用于存储中间变量和计算结果。
队列管理接口:通过队列管理接口 EnQue 和 DeQue 来进行并行流水任务之间的通信和同步。
计算逻辑:

Add算子采用一种分块计算策略,用于在AI Core上执行向量加法操作。主要的计算逻辑包括以下步骤:
初始化核函数 add_custom,其中核函数初始化包括获取每个核的起始索引和初始化队列等。
执行计算过程,计算过程由多个循环组成,每次循环处理一个小块数据。
在每个循环中,分为以下三个步骤:
数据拷贝(CopyIn):将输入数据x和y从全局内存搬移到本地队列(Local Queue)。
计算(Compute):执行向量加法操作,将x和y逐元素相加得到z。
数据拷贝(CopyOut):将计算得到的z从本地队列搬回到全局内存。
循环处理完所有小块数据后,完成整个向量相加操作。

2.核函数开发

2.1核函数定义

这个核函数的主要作用是创建 KernelAdd 类对象,初始化并执行向量相加的计算过程,调用 Process 方法来实际执行向量相加的操作。

extern "C" __global__ __aicore__ void add_custom(GM_ADDR x, GM_ADDR y, GM_ADDR z)
{
    KernelAdd op;
    op.Init(x, y, z);
    op.Process();
}

extern “C”: 这是一个C++编译指示,用于告诉编译器要使用C链接规范,以便在C/C++混编时,能够正确链接核函数。

__global__: 这是一个GPU编程的关键字,表示核函数可以在GPU上执行。这个关键字通常用于CUDA编程,表明这是一个全局函数,可以在GPU上调用。

__aicore__: 这是针对AI Core的编程关键字,表示这是AI Core上的核函数,用于AI Core的编程模型。

void add_custom(GM_ADDR x, GM_ADDR y, GM_ADDR z): 这是核函数的定义,它接受三个参数 x、y 和 z,分别代表输入向量 x、输入向量 y 和输出向量 z 的内存地址。这些内存地址是在全局内存中的,而不是在核函数的局部内存中。

KernelAdd op;: 在核函数的开头,创建了一个名为 op 的 KernelAdd 类的对象,用于执行向量相加的计算。

op.Init(x, y, z);: 调用 KernelAdd 类的 Init 方法,初始化 op 对象,将输入向量 x 和 y 的内存地址传递给 op 对象,以及将输出向量 z 的内存地址传递给 op 对象。

op.Process();: 调用 KernelAdd 类的 Process 方法,执行向量相加的计算。这一步会根据Add算子的计算逻辑,将输入数据从全局内存拷贝到本地队列,执行向量加法操作,再将计算结果从本地队列拷贝回全局内存。

2.2算子类定义

KernelAdd 类是用于执行Add算子计算的核心类。

class KernelAdd {
public:
    __aicore__ inline KernelAdd() {}
    __aicore__ inline void Init(GM_ADDR x, GM_ADDR y, GM_ADDR z)
    {
        // get start index for current core, core parallel
        xGm.SetGlobalBuffer((__gm__ half*)x + BLOCK_LENGTH * GetBlockIdx(), BLOCK_LENGTH);
        yGm.SetGlobalBuffer((__gm__ half*)y + BLOCK_LENGTH * GetBlockIdx(), BLOCK_LENGTH);
        zGm.SetGlobalBuffer((__gm__ half*)z + BLOCK_LENGTH * GetBlockIdx(), BLOCK_LENGTH);
        // pipe alloc memory to queue, the unit is Bytes
        pipe.InitBuffer(inQueueX, BUFFER_NUM, TILE_LENGTH * sizeof(half));
        pipe.InitBuffer(inQueueY, BUFFER_NUM, TILE_LENGTH * sizeof(half));
        pipe.InitBuffer(outQueueZ, BUFFER_NUM, TILE_LENGTH * sizeof(half));
    }
    __aicore__ inline void Process()
    {
        // loop count need to be doubled, due to double buffer
        constexpr int32_t loopCount = TILE_NUM * BUFFER_NUM;
        // tiling strategy, pipeline parallel
        for (int32_t i = 0; i < loopCount; i++) {
            CopyIn(i);
            Compute(i);
            CopyOut(i);
        }
    }
Init方法

Init 方法通过 GM_ADDR 类型的参数 x、y 和 z,将输入向量和输出向量的地址传入该方法。

首先,通过 GetBlockIdx() 方法获取当前AI Core的起始索引,这是为了在AI Core并行处理中计算每个核心需要处理的数据范围。

接下来,使用 xGm、yGm 和 zGm 对象,通过 SetGlobalBuffer 方法将输入向量 x、y 和输出向量 z 与AI Core的局部内存进行关联。这确保了每个核心的计算都在局部内存中进行,提高了计算效率。

然后,通过 pipe 对象,使用 InitBuffer 方法初始化了 inQueueX、inQueueY 和 outQueueZ 队列,这些队列将用于数据的输入和输出。BUFFER_NUM 和 TILE_LENGTH 用于确定队列的深度和每个队列的大小。

Process方法

Process 方法用于执行Add算子的计算逻辑。在该方法中:

通过循环处理数据,loopCount 表示循环的次数。TILE_NUM 和 BUFFER_NUM 的乘积决定了总共有多少次循环。因为采用了双缓冲策略,所以需要循环两次。

CopyIn 方法用于将数据从全局内存拷贝到本地队列,执行输入操作。

Compute 方法执行Add算子的计算,将数据从 inQueueX 和 inQueueY 队列中取出,执行相加操作。

CopyOut 方法用于将计算结果从本地队列拷贝回全局内存,执行输出操作。


CopyIn函数实现

__aicore__ inline void CopyIn(int32_t progress)
    {
        // alloc tensor from queue memory
        LocalTensor<half> xLocal = inQueueX.AllocTensor<half>();
        LocalTensor<half> yLocal = inQueueY.AllocTensor<half>();
        // copy progress_th tile from global tensor to local tensor
        DataCopy(xLocal, xGm[progress * TILE_LENGTH], TILE_LENGTH);
        DataCopy(yLocal, yGm[progress * TILE_LENGTH], TILE_LENGTH);
        // enque input tensors to VECIN queue
        inQueueX.EnQue(xLocal);
        inQueueY.EnQue(yLocal);
    }

它用于将数据从全局内存(Global Memory)复制到局部内存(Local Memory)并将数据放入输入队列,为Add算子的计算做准备。以下是对该方法的解释:

CopyIn 方法接受一个整数参数 progress,表示当前执行的迭代进度。这个参数在循环中用于确定从全局内存复制的数据位置。

首先,使用 inQueueX 和 inQueueY 队列的 AllocTensor 方法,为每个输入数据创建一个 LocalTensor 对象 xLocal 和 yLocal。这些 LocalTensor 对象用于在局部内存中存储全局内存中的部分数据。

接下来,使用 DataCopy 方法,将全局内存中的数据从 xGm 和 yGm 复制到 xLocal 和 yLocal 中。这里 progress * TILE_LENGTH 用于确定要复制的全局内存数据的位置。

然后,使用 EnQue 方法,将 xLocal 和 yLocal 放入输入队列 inQueueX 和 inQueueY 中,以便后续的计算操作可以从这些队列中获取数据。

Compute函数实现

__aicore__ inline void Compute(int32_t progress)
    {
        // deque input tensors from VECIN queue
        LocalTensor<half> xLocal = inQueueX.DeQue<half>();
        LocalTensor<half> yLocal = inQueueY.DeQue<half>();
        LocalTensor<half> zLocal = outQueueZ.AllocTensor<half>();
        // call Add instr for computation
        Add(zLocal, xLocal, yLocal, TILE_LENGTH);
        // enque the output tensor to VECOUT queue
        outQueueZ.EnQue<half>(zLocal);
        // free input tensors for reuse
        inQueueX.FreeTensor(xLocal);
        inQueueY.FreeTensor(yLocal);
    }

Compute 方法接受一个整数参数 progress,表示当前执行的迭代进度。这个参数在循环中用于确定从输入队列中获取数据以及将结果放入输出队列的位置。

首先,使用 inQueueX 和 inQueueY 队列的 DeQue 方法,从输入队列中获取 LocalTensor 对象 xLocal 和 yLocal,这些对象包含了之前在 CopyIn 方法中准备好的输入数据。

接下来,使用 outQueueZ 队列的 AllocTensor 方法,创建一个 LocalTensor 对象 zLocal,用于存储计算结果。

然后,使用 Add 方法,对 xLocal 和 yLocal 中的数据执行加法操作,将结果存储在 zLocal 中。TILE_LENGTH 参数表示每次计算的元素数量。

使用 outQueueZ 队列的 EnQue 方法,将计算结果 zLocal 放入输出队列中,以便后续的步骤可以从输出队列中获取计算结果。

最后,使用 inQueueX 和 inQueueY 队列的 FreeTensor 方法,释放 xLocal 和 yLocal 对象,以便它们可以在后续的迭代中被重用。

CopyOut函数实现

__aicore__ inline void CopyOut(int32_t progress)
    {
        // deque output tensor from VECOUT queue
        LocalTensor<half> zLocal = outQueueZ.DeQue<half>();
        // copy progress_th tile from local tensor to global tensor
        DataCopy(zGm[progress * TILE_LENGTH], zLocal, TILE_LENGTH);
        // free output tensor for reuse
        outQueueZ.FreeTensor(zLocal);
    }

首先,从 outQueueZ 队列中出队(DeQue)一个 LocalTensor 对象 zLocal,这是之前计算的结果存储在本地内存中的对象。

然后,使用 DataCopy 函数将 zLocal 中的数据复制到全局内存中的 zGm 中,复制的数据长度为 TILE_LENGTH。

最后,通过 outQueueZ.FreeTensor(zLocal) 释放 zLocal 对象,以便在下一个迭代中重新使用。

3.核函数运行验证

通过对__CCE_KT_TEST__宏定义的判断来区分CPU和NPU侧的运行程序。

3.1CPU侧运行验证

完成算子核函数CPU侧运行验证的步骤如下:

分配共享内存,并进行数据初始化;
调用ICPU_RUN_KF调测宏,完成核函数CPU侧的调用;
释放申请的资源。

#ifdef __CCE_KT_TEST__
    uint8_t* x = (uint8_t*)AscendC::GmAlloc(inputByteSize);
    uint8_t* y = (uint8_t*)AscendC::GmAlloc(inputByteSize);
    uint8_t* z = (uint8_t*)AscendC::GmAlloc(outputByteSize);

    ReadFile("./input/input_x.bin", inputByteSize, x, inputByteSize);
    ReadFile("./input/input_y.bin", inputByteSize, y, inputByteSize);

    AscendC::SetKernelMode(KernelMode::AIV_MODE);
    ICPU_RUN_KF(add_custom, blockDim, x, y, z); // use this macro for cpu debug

    WriteFile("./output/output_z.bin", z, outputByteSize);

    AscendC::GmFree((void *)x);
    AscendC::GmFree((void *)y);
    AscendC::GmFree((void *)z);

内存分配:首先,分配了三块内存,x、y 和 z,这些内存用于存储输入数据和输出数据。这些内存分配使用 AscendC::GmAlloc 函数。

数据读取:使用 ReadFile 函数,从外部文件(如 “./input/input_x.bin” 和 “./input/input_y.bin”)读取输入数据(x 和 y)。

核函数模式设置:调用 AscendC::SetKernelMode 函数,将核函数执行模式设置为 KernelMode::AIV_MODE。这表明代码将在AI Core上执行。

核函数运行:通过宏 ICPU_RUN_KF 来运行核函数,add_custom 核函数将被执行。此核函数将输入数据 x 和 y 作为参数传递,并计算结果存储在 z 中。这一步是在AI Core上执行的。

结果写入文件:使用 WriteFile 函数,将计算的结果 z 写入输出文件(如 “./output/output_z.bin”),以便进一步分析和验证。

内存释放:最后,使用 AscendC::GmFree 函数,释放之前分配的内存,包括输入数据 x 和 y,以及输出数据 z。这是为了确保不会发生内存泄漏。

3.2NPU侧验证

在NPU侧验证主要分为以下步骤:
1.初始化Device设备;
2.创建Context绑定设备;
3.分配Host内存,并进行数据初始化;
4.分配Device内存,并将数据从Host上拷贝到Device上;
5.用内核调用符<<<>>>调用核函数完成指定的运算;
6.将Device上的运算结果拷贝回Host;
7.释放申请的资源。

代码如下:

#else
    CHECK_ACL(aclInit(nullptr));
    aclrtContext context;
    int32_t deviceId = 0;
    CHECK_ACL(aclrtSetDevice(deviceId));
    CHECK_ACL(aclrtCreateContext(&context, deviceId));
    aclrtStream stream = nullptr;
    CHECK_ACL(aclrtCreateStream(&stream));

    uint8_t *xHost, *yHost, *zHost;
    uint8_t *xDevice, *yDevice, *zDevice;
    CHECK_ACL(aclrtMallocHost((void**)(&xHost), inputByteSize));
    CHECK_ACL(aclrtMallocHost((void**)(&yHost), inputByteSize));
    CHECK_ACL(aclrtMallocHost((void**)(&zHost), outputByteSize));
    CHECK_ACL(aclrtMalloc((void**)&xDevice, inputByteSize, ACL_MEM_MALLOC_HUGE_FIRST));
    CHECK_ACL(aclrtMalloc((void**)&yDevice, inputByteSize, ACL_MEM_MALLOC_HUGE_FIRST));
    CHECK_ACL(aclrtMalloc((void**)&zDevice, outputByteSize, ACL_MEM_MALLOC_HUGE_FIRST));

    ReadFile("./input/input_x.bin", inputByteSize, xHost, inputByteSize);
    ReadFile("./input/input_y.bin", inputByteSize, yHost, inputByteSize);
    CHECK_ACL(aclrtMemcpy(xDevice, inputByteSize, xHost, inputByteSize, ACL_MEMCPY_HOST_TO_DEVICE));
    CHECK_ACL(aclrtMemcpy(yDevice, inputByteSize, yHost, inputByteSize, ACL_MEMCPY_HOST_TO_DEVICE));

    add_custom_do(blockDim, nullptr, stream, xDevice, yDevice, zDevice);
    CHECK_ACL(aclrtSynchronizeStream(stream));

    CHECK_ACL(aclrtMemcpy(zHost, outputByteSize, zDevice, outputByteSize, ACL_MEMCPY_DEVICE_TO_HOST));
    WriteFile("./output/output_z.bin", zHost, outputByteSize);

    CHECK_ACL(aclrtFree(xDevice));
    CHECK_ACL(aclrtFree(yDevice));
    CHECK_ACL(aclrtFree(zDevice));
    CHECK_ACL(aclrtFreeHost(xHost));
    CHECK_ACL(aclrtFreeHost(yHost));
    CHECK_ACL(aclrtFreeHost(zHost));

    CHECK_ACL(aclrtDestroyStream(stream));
    CHECK_ACL(aclrtDestroyContext(context));
    CHECK_ACL(aclrtResetDevice(deviceId));
    CHECK_ACL(aclFinalize());

总结:以上就是整个Ascend C算子开发的流程,接下来就可以执行一键式编译运行脚本,编译和运行应用程序。总的来说,通过这个简单的例子,可以知道Ascend C算子开发的工作主要分为:环境准备、算子分析、核函数开发、核函数运行验证、编译运行脚本这就几个步骤,核函数开发和核函数验证运行需要重点掌握,里面涉及到了算子开发核心知识。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

2023年【安全生产监管人员】考试报名及安全生产监管人员复审考试

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 安全生产监管人员考试报名是安全生产模拟考试一点通总题库中生成的一套安全生产监管人员复审考试&#xff0c;安全生产模拟考试一点通上安全生产监管人员作业手机同步练习。2023年【安全生产监管人员】考试报名及安全…

【数据结构练习】树和二叉树的选择题精选集锦

前言 编程想要学的好&#xff0c;刷题少不了&#xff0c;我们不仅要多刷题&#xff0c;还要刷好题&#xff01;为此我开启了一个弯道超车必做好题锦集的系列&#xff0c;此为树和二叉树的选择题精选集锦。该系列会不定期更新&#xff0c;敬请期待&#xff01; 1.已知某二叉树的…

SQLi靶场

SQLi靶场 less1- less2 &#xff08;详细讲解&#xff09; less 1 Error Based-String (字符类型注入) 思路分析 判断是否存在SQL注入 已知参数名为id&#xff0c;输入数值和‘ 单引号‘’ 双引号来判断&#xff0c;它是数值类型还是字符类型 首先输入 1 &#xff0c; 发现…

大语言模型在天猫AI导购助理项目的实践!

本文主要介绍了Prompt设计、大语言模型SFT和LLM在手机天猫AI导购助理项目应用。 ChatGPT基本原理 “会说话的AI”&#xff0c;“智能体” 简单概括成以下几个步骤&#xff1a; 预处理文本&#xff1a;ChatGPT的输入文本需要进行预处理。 输入编码&#xff1a;ChatGPT将经过预…

Postman接口测试,全网最详细教程

Postman的脚本可以导出多种语言的脚本&#xff0c;方便二次维护开发。 Python的requests库&#xff0c;支持python2和python3&#xff0c;用于发送http/https请求 使用unittest进行接口自动化测试 0 1****环境准备 1、安装python &#xff08;使用python2或3都可以&#x…

【表面缺陷检测】铝型材表面缺陷检测数据集介绍(含xml标签文件)

一、铝型材介绍 铝型材是一种由铝合金材料制成的&#xff0c;具有固定截面形状和尺寸的条形建材。由于其优良的物理性能和广泛的应用领域&#xff0c;铝型材在现代工业和生活中发挥着重要的作用。 1、铝型材的分类 根据截面形状的不同&#xff0c;铝型材可分为角铝、槽铝、工…

鉴源实验室 | AUTOSAR E2E:车载通信的安全保障

作者 | 沈平 上海控安可信软件创新研究院汽车网络安全组 来源 | 鉴源实验室 社群 | 添加微信号“TICPShanghai”加入“上海控安51fusa安全社区” 随着汽车行业逐步走向电气化、智能化&#xff0c;车载系统的软件和硬件复杂度不断上升。如何确保这些复杂系统中的数据通讯安全和…

在线设计数据库表用Itbuilder,极简易用真香!!!

“如果您想要一个具有快速搜索运行的高性能数据库&#xff0c;那么数据库设计是必不可少的&#xff0c;花时间设计数据库将帮助您避免效率低下和高冗余等问题”。 在线数据库设计软件itbuilder&#xff0c;界面清爽漂亮&#xff0c;功能简洁&#xff0c;没有多余设置很容易上手…

DevOps持续集成-Jenkins(4)

文章目录 DevOpsDevOps概述Jenkins流水线任务入门⭐Jenkins流水线任务的Hello World体验⭐Jenkins流水线语法例子Jenkins流水线语法生成器⭐ Jenkins实战4&#xff1a;构建pipeline&#xff08;流水线&#xff09;的Jenkins项目⭐项目架构图Jenkins实战4的初步流水线模板&#…

直播预告 | YashanDB 2023年度发布会正式定档11月2日,邀您共同见证国产数据库发展实践!

11月2日&#xff0c;YashanDB 2023年度发布会将于云端直播开启&#xff0c;发布会以 「惟实励新」 为主题&#xff0c;邀请企业用户、合作伙伴、广大开发者共同见证全新产品与解决方案。届时发布会将在墨天轮社区同步进行&#xff0c;欢迎大家报名&#xff01; 惟实求真。Yasha…

redis中的io多线程(线程池)

文章目录 redis多线程模型redis为什么引入I/O多线程I/O多线程模型 源码解析测试设置连接建立数据传输线程调度开启io线程startThreadedIO关闭io线程stopThreadedIO redis多线程模型 redis为什么引入I/O多线程 Redis 的性能瓶颈在网络 IO 的处理上。Redis 是网络 IO 密集型&am…

《ATTCK视角下的红蓝对抗实战指南》一本书构建完整攻防知识体系

一. 网络安全现状趋势分析 根据中国互联网络信息中心&#xff08;CNNIC&#xff09;发布的第51次《中国互联网络发展状况统计报告》&#xff0c;截至2022年12月&#xff0c;我国网民规模为10.67亿&#xff0c;互联网普及率达75.6%。我国有潜力建设全球规模最大、应用渗透最强的…

雪糕冰淇淋经营配送小程序商城效果如何

雪糕冰淇淋是很多年轻人喜欢的食品之一&#xff0c;市面上的雪糕品牌类型众多&#xff0c;销售模式主要为厂家批发、经销商零售等&#xff0c;由于雪糕冰淇淋的易化性&#xff0c;很多用户会选择就近购买&#xff0c;但制作技术升级和长途冷藏技术下&#xff0c;网购成为另一种…

建模仿真软件 Comsol Multiphysics mac中文版软件介绍

COMSOL Multiphysics mac是一款全球通用的基于高级数值方法和模拟物理场问题的通用软件&#xff0c;拥有、网格划分、研究和优化、求解器、可视化和后处理、仿真 App等相关功能&#xff0c;轻松实现各个环节的流畅进行&#xff0c;它能够解释耦合或多物理现象。 附加产品扩展了…

基于Java的电动车实名挂牌系统设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09; 代码参考数据库参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作者&am…

NLog详解

目录 1.简介 2.项目中使用NLog 2.1 快速使用NLog 2.2 通过配置文件使用NLog 3.NLog配置参数详解 3.1 全局配置 3.2 根元素 3.2.1 targets 3.2.1.1 layout 3.2.2 rules 3.2.3 extensions 3.2.4 include 3.2.5 variable 4.附录 1.简介 NLog是一个基于.NET平台编写…

minio + linux + docker + spring boot实现文件上传与下载

minio docker spring boot实现文件上传与下载 1.在linux上安装并启动docker2.在docker中拉取minio并启动3.Spring Boot 整合 minio4.测试 minio 文件上传、下载及图片预览等功能 1.在linux上安装并启动docker 检查linux内核&#xff0c;必须是3.10以上 uname ‐r安装docker…

基于aop 代理 Sentinel Nacos配置控制包装类实现原理

基于aop & 代理 & Sentinel & Nacos配置控制包装类实现原理 Hi&#xff0c;我是阿昌&#xff0c;今天记录下看sentinel源码结合业务实现的思路基于aop & 代理 & Sentinel & Nacos配置控制包装类实现原理&#xff1b;下面并不会手把手的记录方案的实现…

绿野仙踪不仅是童话,还是便宜又好用的产品测试法!

以 ChatGPT 为代表的大语言模型爆火后&#xff0c;推动了对话类人工智能产品的高速发展&#xff0c;我们已经看到了如智能助理、问答系统、自动写作等多种类型的个性化对话类 AI 服务。 AI 能力的提升让人们对智能 AI 产品的期望越来越高&#xff0c;相关产品的用户体验也因此变…

基于springboot实现校友社交平台管理系统项目【项目源码+论文说明】计算机毕业设计

基于springboot实现校友社交平台管理系统演示 摘要 校友社交系统提供给用户一个校友社交信息管理的网站&#xff0c;最新的校友社交信息让用户及时了解校友社交动向,完成校友社交的同时,还能通过论坛中心进行互动更方便。本系统采用了B/S体系的结构&#xff0c;使用了java技…