【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 parallelxGm.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 Bytespipe.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 bufferconstexpr int32_t loopCount = TILE_NUM * BUFFER_NUM;// tiling strategy, pipeline parallelfor (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 memoryLocalTensor<half> xLocal = inQueueX.AllocTensor<half>();LocalTensor<half> yLocal = inQueueY.AllocTensor<half>();// copy progress_th tile from global tensor to local tensorDataCopy(xLocal, xGm[progress * TILE_LENGTH], TILE_LENGTH);DataCopy(yLocal, yGm[progress * TILE_LENGTH], TILE_LENGTH);// enque input tensors to VECIN queueinQueueX.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 queueLocalTensor<half> xLocal = inQueueX.DeQue<half>();LocalTensor<half> yLocal = inQueueY.DeQue<half>();LocalTensor<half> zLocal = outQueueZ.AllocTensor<half>();// call Add instr for computationAdd(zLocal, xLocal, yLocal, TILE_LENGTH);// enque the output tensor to VECOUT queueoutQueueZ.EnQue<half>(zLocal);// free input tensors for reuseinQueueX.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 queueLocalTensor<half> zLocal = outQueueZ.DeQue<half>();// copy progress_th tile from local tensor to global tensorDataCopy(zGm[progress * TILE_LENGTH], zLocal, TILE_LENGTH);// free output tensor for reuseoutQueueZ.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 debugWriteFile("./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.释放申请的资源。

代码如下:

#elseCHECK_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算子开发的工作主要分为:环境准备、算子分析、核函数开发、核函数运行验证、编译运行脚本这就几个步骤,核函数开发和核函数验证运行需要重点掌握,里面涉及到了算子开发核心知识。

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

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

相关文章

Spring Cloud之负载均衡与服务调用(Ribbon)

目录 Ribbon 简介 负载均衡 简介 负载均衡方式 服务端负载均衡 工作原理 特点 客户端负载均衡 工作原理 特点 对比 实现 负载均衡策略 切换负载均衡策略 定制负载均衡策略 超时与重试 单个服务配置 全局配置 服务调用 示例 Ribbon 简介 Ribbon 是 Netfli…

WEB使用百度地图展示某地地址

第一步 进入百度地图开发平台 百度地图开放平台 | 百度地图API SDK | 地图开发 第二步注册 获取AK秘钥&#xff0c;点击【创建应用】进入AK申请页面&#xff0c;填写应用名称&#xff0c;务必选择AK类型为“浏览器端”&#xff0c;JS API只支持浏览器端AK进行请求与访问 下面…

最小二乘法,可视化UI界面

import tkinter as tk import numpy as np import matplotlib.pyplot as plt from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from pylab import mplmpl.rcParams[font.sans-serif] [FangSong] # 指定默认字体 mpl.rcParams[axes.unicode_minus] False …

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

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

VB.NET 三层登录系统实战:从设计到部署全流程详解

目录 前言&#xff1a; 什么是三层 为什么要用到三层: 饭店→软件 理解: 过程: 1.三层包图: 2.数据库 3.三层项目 4.用户界面 5.添加引用 代码实现: Entity层 BLL层 DAL层 UI层 总结: 前言&#xff1a; 什么是三层 三层就是把各个功能模块划分为表示层&#…

【从瀑布模式到水母模式】ChatGPT如何赋能软件研发全流程

文章目录 &#x1f384;前言&#x1f354;本书概要&#x1f33a;内容简介&#x1f33a;作者简介&#x1f33a;专家推荐&#x1f6f8;读者对象&#x1f354;彩蛋 &#x1f384;前言 计算机技术的发展和互联网的普及&#xff0c;使信息处理和传输变得更加高效&#xff0c;极大地…

安全和便捷:如何将运营商二要素API应用于实名制管理中

引言 随着互联网的快速发展&#xff0c;数字化身份验证和实名制管理变得越来越重要。在金融、电子商务、社交媒体等领域&#xff0c;确保用户身份的安全和准确性至关重要。运营商二要素核验API成为了实名制管理的有力工具&#xff0c;它不仅能够提供高水平的安全性&#xff0c…

JVM进阶(2)

一)方法区: java虚拟机中有一个方法区&#xff0c;该区域被所有的java线程都是共享&#xff0c;虚拟机一启动&#xff0c;运行时数据区就被开辟好了&#xff0c;官网上说了方法区可以不压缩还可以不进行GC&#xff0c;JAVA虚拟机就相当于是接口&#xff0c;具体的HotSpot就是虚…

虹科案例 | 使用虹科MSR147WD测试自行车背包的热生理舒适性

使用虹科MSR147WD测试自行车背包的热生理舒适性 在进行运动活动时&#xff0c;佩戴背包会对穿戴者的舒适度产生影响&#xff0c;并且也会对热调节产生影响&#xff0c;类似于一件服装的作用。为了优化背包的材料和设计属性&#xff0c;我们利用MSR Electronics GmbH的微型数据记…

Java实现Csv文件导入导出

Java实现Csv文件导入导出 什么是.csv文件&#xff1f; CSV&#xff08;Comma-Separated Values&#xff0c;逗号分隔的值&#xff09;是一种简单、实用的文件格式&#xff0c;用于存储和表示包括文本、数值等各种类型的数据。CSV 文件通常以 .csv 作为文件扩展名。这种文件格…

C语言编写图形化界面-创建按钮-为其指定样式

文章目录 前置章节指定窗口样式给按钮加边框扁平化按钮复选框样式按钮自动复选框 单选按钮三态按钮自动三态按钮 默认按钮样式&#xff08;对话框Enter键&#xff09; 设置按钮位置和大小封装函数 前置章节 开始之前&#xff0c;需要学习以下章节&#xff1a; 创建窗口 窗口过…

NAT技术与代理服务器

目录 一、NAT与NAPT技术 1.NAT技术 2.NAPT技术 &#xff08;1&#xff09;四元组的唯一性 &#xff08;2&#xff09;数据的传输过程 &#xff08;3&#xff09;NAPT的缺陷 二、代理服务器 1.正向代理和反向代理 2.代理服务器的应用 &#xff08;1&#xff09;游戏加…

图纸管理制度《四》

1、目的 使公司的图纸得到有效的控制&#xff0c;确保生产所用的图纸为最新有效版本&#xff0c;避免因图纸管理不当造成的损失。 2、定义 本制度所述的图纸包括产品总装图、装配图、零件图、工装图纸、检具图纸、包装图纸、工艺流程 3、范围 客户提供的图纸&#xff0c;技…

selenium+python web自动化测试框架项目实战实例教程

自动化测试对程序的回归测试更方便。 由于回归测试的动作和用例是完全设计好的,测试期望的结果也是完全可以预料的,将回归测试自动运行... 可以运行更加繁琐的测试 自动化测试的一个明显好处就是可以在很短的时间内运行更多的测试。学习自动化测试最终目的是应用到实际项目中&…

LabVIEW应用开发——基本函数(一)

前面我们介绍了一些控件的介绍和属性的配置&#xff0c;想要完成一个软件只会拖控件肯定是不行的&#xff0c;没办法实现既有的功能。比如我们要实现从串口中读到数据&#xff0c;根据一定的协议解析&#xff0c;然后转换成各个参数的值的显示&#xff0c;包括时间、电压、电流…

796. 子矩阵的和(左上角前缀和)

题目&#xff1a; 796. 子矩阵的和 - AcWing题库 思路&#xff1a; 1.暴力搜索&#xff08;搜索时间复杂度为O(n2)&#xff0c;很多时候会超时&#xff09; 2. 前缀和&#xff08;左上角前缀和&#xff09;&#xff1a;本题特殊在不是直接求前n个数的和&#xff0c;而是求…

第1篇 目标检测概述 —(3)目标检测评价指标

前言&#xff1a;Hello大家好&#xff0c;我是小哥谈。目标检测评价指标是用来衡量目标检测算法性能的指标&#xff0c;主要包括几个指标&#xff1a;精确率&#xff08;Precision&#xff09;、召回率&#xff08;Recall&#xff09;、交并比&#xff08;IoU&#xff09;、平均…

UI自动化测试是什么?什么项目适合做UI自动化测试

UI 测试是一种测试类型&#xff0c;也称为用户界面测试&#xff0c;通过该测试&#xff0c;我们检查应用程序的界面是否工作正常或是否存在任何妨碍用户行为且不符合书面规格的 BUG。了解用户将如何在用户和网站之间进行交互以执行 UI 测试至关重要&#xff0c;通过执行 UI 测试…

Flink Hive Catalog操作案例

在此对Flink读写Hive表操作进行逐步记录&#xff0c;需要指出的是&#xff0c;其中操作Hive分区表和非分区表的DDL有所不同&#xff0c;以下分别记录。 基础环境 Hive-3.1.3 Flink-1.17.1 基本操作与准备 1、上传依赖jar包到flink/lib目录下 cp flink-sql-connector-hive-…

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

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