flash_attention简要笔记

优化效果

原来,attention部分的计算量和中间激活占用显存的复杂度都是 O ( N 2 ) O(N^2) O(N2)

计算量部分原来QK矩阵乘和attn_score@V矩阵乘的计算量,复杂度都是 O ( N 2 ) O(N^2) O(N2);中间激活因为中间有一个attn_score,所以复杂度也是 O ( N 2 ) O(N^2) O(N2)

现在,attention部分的中间激活占用显存的复杂度变为 O ( N ) O(N) O(N),计算量的复杂度没有变但是通过减少访存加快了计算速度,而且fa与原attention完全等价

具体过程

flash-attention还是基于kernel融合的思想,将QK矩阵乘法、mask、softmax、dropout合并成一个kernel,这样不仅减少了中间变量对显存的占用,而且也减少了计算过程中的访存

一些符号表示:

  • S i j = Q i × K j T S_{ij}=Q_i \times K_j^T Sij=Qi×KjT,Q分块和K分块的乘积,形状为 [ B r , B c ] [B_r, B_c] [Br,Bc]

  • m ~ i j = r o w m a x ( S i j ) \widetilde{m}_{ij}=rowmax(S_{ij}) m ij=rowmax(Sij):对分块 S i j S_{ij} Sij而言,得到其每行的最大值,形状为 [ B r , 1 ] [B_r, 1] [Br,1]

  • P ~ i j = e S i j − m ~ i j = e S i j − r o w m a x ( S i j ) \widetilde{P}_{ij}=e^{S_{ij}-\widetilde{m}_{ij}}=e^{S_{ij}-rowmax(S_{ij})} P ij=eSijm ij=eSijrowmax(Sij):每个分块 S i j S_{ij} Sij减去其局部rowmax m ~ i j \widetilde{m}_{ij} m ij,形状为 [ B r , B c ] [B_r, B_c] [Br,Bc]

  • l ~ i j = r o w s u m ( P ~ i j ) = r o w s u m ( e S i j − r o w m a x ( S i j ) ) \widetilde{l}_{ij}=rowsum(\widetilde{P}_{ij})=rowsum(e^{S_{ij}-rowmax(S_{ij})}) l ij=rowsum(P ij)=rowsum(eSijrowmax(Sij)):对 P ~ i j \widetilde{P}_{ij} P ij而言,按行求和,形状为 [ B r , 1 ] [B_r, 1] [Br,1]

  • m i n e w = m a x ( m ~ i 0 , m ~ i 1 , . . . , m ~ i j ) = r o w m a x ( c o n c a t ( S i 0 , S i 1 , . . . , S i j ) ) m^{new}_i=max(\widetilde{m}_{i0}, \widetilde{m}_{i1}, ... , \widetilde{m}_{ij})=rowmax(concat(S_{i0}, S_{i1}, ... , S_{ij})) minew=max(m i0,m i1,...,m ij)=rowmax(concat(Si0,Si1,...,Sij)):即 c o n t c a t ( S i 0 , S i 1 , . . . , S i j ) contcat(S_{i0}, S_{i1}, ... , S_{ij}) contcat(Si0,Si1,...,Sij)这j+1个分块的每行的最大值,形状为 [ B r , 1 ] [Br, 1] [Br,1]

  • m i m_i mi m i n e w m_i^{new} minew位于SRAM上,将 m i n e w m_i^{new} minew写回到HBM就是 m i m_i mi,初始化 m = − ∞ m=-\infty m=

  • l i n e w = e m i − m i n e w l i + e m ~ i j − m i n e w l ~ i j = r o w s u m [ e S 00 − m a x ( m ~ 00 , . . . , m ~ 0 j ) ] + . . . + r o w s u m [ e S 0 j − m a x ( m ~ 00 , . . . , m ~ 0 j ) ] l^{new}_i=e^{m_i-m_i^{new}}l_i + e^{\widetilde{m}_{ij}-m_i^{new}} \widetilde{l}_{ij}=rowsum[e^{S_{00}-max(\widetilde{m}_{00},...,\widetilde{m}_{0j})}] + ... + rowsum[e^{S_{0j}-max(\widetilde{m}_{00},...,\widetilde{m}_{0j})}] linew=emiminewli+em ijminewl ij=rowsum[eS00max(m 00,...,m 0j)]+...+rowsum[eS0jmax(m 00,...,m 0j)]

  • l i l_i li l i n e w l_i^{new} linew位于SRAM上,将 l i n e w l_i^{new} linew写回到HBM就是 l i l_i li,初始化 l = 0 l=0 l=0

如果不使用flash-attention,具体过程为:

  1. S = Q K T S = Q K ^T S=QKT
  2. P = s o f t m a x ( S + m a s k ) P = softmax(S+mask) P=softmax(S+mask)
  3. O = P V O = P V O=PV

如果使用flash-attention,前向过程为:

在这里插入图片描述

大致过程为:

在这里插入图片描述

  1. 首先对QKV进行分块,K、V分块方法相同(V的分块图中没画出来),首先可以计算 S i j = Q i × K j T S_{ij}=Q_i\times K_j^T Sij=Qi×KjT。因为对QKV进行了分块,所以每次SRAM上能保留 S i j S_{ij} Sij P ~ i j \widetilde{P}_{ij} P ij(橙黄色表示存储在SRAM上;橙红色表示虽然也存储在SRAM上,但是这些部分每次outer loop会写回到HBM中)
  2. 如果有mask,此时对 S i j S_{ij} Sij进行mask
  3. 使用一个局部变量 m ~ i j \widetilde{m}_{ij} m ij和一个全局变量 m m m(或者说 m n e w m^{new} mnew m n e w m^{new} mnew的值在SRAM上,但是每次outer loop会写回到HBM中)来记录分块 S i j S_{ij} Sij局部rowmax和中间遍历过的分块 S i : S_{i:} Si:的历史rowmax
  4. 然后基于分块 S i j S_{ij} Sij计算局部的safe softmax的分子部分,即 e S i j − r o w m a x ( S i j ) e^{S_{ij}-rowmax(S_{ij})} eSijrowmax(Sij),safe softmax的分子部分累加就是分母部分,这样,就得到了一个针对分块 S i j S_{ij} Sij的、局部的safe softmax的分母 l ~ i j \widetilde{l}_{ij} l ij,和 一个 遍历过的历史分块 S i : S_{i:} Si:的 safe softmax分子部分的 累加和 l n e w l^{new} lnew(注意断句,写公式有点晦涩难懂,用语言描述又不太好描述),局部的 l ~ i j \widetilde{l}_{ij} l ij就是用来更新全局的 l l l(或者说 l n e w l^{new} lnew l n e w l^{new} lnew的值在SRAM上,但是每次outer loop会写回到HBM中),对 l ~ i j \widetilde{l}_{ij} l ij举一个例子:
    • 当j=0,i=0时, l 0 n e w = e m 0 − m 0 n e w l 0 + e m ~ 00 − m 0 n e w l ~ 00 = l ~ 00 l_0^{new}=e^{m_0-m_0^{new}} l_0+e^{\widetilde{m}_{00}-m_0^{new}} \widetilde{l}_{00}=\widetilde{l}_{00} l0new=em0m0newl0+em 00m0newl 00=l 00
    • 当j=1,i=0时, l 0 n e w = r o w s u m ( e S 00 − m a x ⁡ ( m ~ 00 , m ~ 01 ) ) + r o w s u m ( e S 01 − m a x ⁡ ( m ~ 00 , m ~ 01 ) ) l_0^{new} = rowsum(e^{S_{00}-max⁡(\widetilde{m}_{00}, \widetilde{m}_{01})})+rowsum(e^{S_{01}-max⁡(\widetilde{m}_{00}, \widetilde{m}_{01})}) l0new=rowsum(eS00max(m 00,m 01))+rowsum(eS01max(m 00,m 01))
  5. 然后对 P ~ i j \widetilde{P}_{ij} P ij进行dropout
  6. 然后相当于要进行 O + = P ~ i j V i O+=\widetilde{P}_{ij} V_i O+=P ijVi了,对于算法的第15行,可以使用分配律拆开看,其中有两个操作:
    1. 后半部分:对于当前的 P ~ i j V i \widetilde{P}_{ij} V_i P ijVi相乘, P ~ i j \widetilde{P}_{ij} P ij中减去的是分块 S i j S_{ij} Sij局部的rowmax,需要调整到 此时已经见过的、所有分块 S i : S_{i:} Si:的rowmax,就是第15行后半部分中 e m ~ i j − m i n e w e^{\widetilde{m}_{ij}-m_i^{new}} em ijminew的意思
    2. 前半部分:调整上一次的 O O O,先乘旧的 l i l_i li恢复到safe softmax的分子部分,然后乘以 e m i − m i n e w e^{m_i-m_i^{new}} emiminew更新一下safe softmax分子部分中减去的全局rowmax,最后再除以当前的safe softmax的分母

(反向过程还是看别的博客吧)

简要分析

首先分析一下fa的FLOPs(只分析大块的矩阵乘法,其他小的操作就不计算了):

  • 一开始的 Q i K j T Q_i K^T_j QiKjT矩阵相乘,其中 Q i Q_i Qi的形状为 [ B r , d ] [B_r, d] [Br,d] K j t K_j^t Kjt的形状为 [ d , B c ] [d, B_c] [d,Bc],此时FLOPs= 2 d × B r × B c 2d \times B_r \times B_c 2d×Br×Bc
  • 后面计算O的时候有一个 P ~ i j V i \widetilde{P}_{ij} V_i P ijVi矩阵相乘,其中 P ~ i j \widetilde{P}_{ij} P ij的形状为 [ B r , B c ] [B_r, B_c] [Br,Bc] V i V_i Vi的形状为 [ B c , d ] [B_c, d] [Bc,d],此时FLOPs= 2 B c × B r × d 2B_c \times B_r \times d 2Bc×Br×d一共进行了 N B r × N B c \frac{N}{B_r} \times \frac{N}{B_c} BrN×BcN次上面的循环,所以FLOPs= 4 N 2 d 4N^2d 4N2d,如果d远小于N,则计算复杂度就变成了 O ( N 2 ) O(N^2) O(N2),计算复杂度相比于standard attention没有变化

然后再分析一下显存占用(显存占用说的是HBM上的显存占用,假设计算精度为 w w w Bytes)

  • HBM上需要维护一个全局的rowmax和expsum,占用显存为 w × N w\times N w×N
  • 然后还要存储一个最后的输出 O O O,占用显存为 w N d wNd wNd,但是这个部分是必须的
  • 因此,显存占用的复杂度为 O ( N d ) O(Nd) O(Nd)(或者 O ( N ) O(N) O(N),如果不考虑 O O O的话)。standard attention需要保存中间的 S , P S, P S,P,显存占用复杂度为 O ( N 2 ) O(N^2) O(N2)

fa相对于standard attention一个优势,在于减小了计算过程中的访存量,最后来分析一下访存次数:

  • standard attention
    • 从HBM中读取Q,K(形状都是 [ N , d ] [N, d] [N,d]),访存量= w N d wNd wNd,计算 S = Q K T S=QK^T S=QKT,然后向HBM中写回S(形状为 [ N , N ] [N, N] [N,N]),访存量= w N 2 wN^2 wN2
    • 从HBM中读取S,访存量= w N 2 w N^2 wN2,计算 P = s o f t m a x ( S ) P=softmax(S) P=softmax(S),向HBM中写回P,访存量= w N 2 w N^2 wN2
    • 从HBM中读取P(形状为 [ N , N ] [N, N] [N,N])、V(形状为 [ N , d ] [N, d] [N,d]),访存量= w N 2 + w N d w N^2 + wNd wN2+wNd,计算 O = P V O=PV O=PV,向HBM中写回O(形状为 [ N , d ] [N, d] [N,d]),访存量= w N d wNd wNd
    • 总的访存量= w ( 3 N d + 4 N 2 ) w(3Nd+4N^2) w(3Nd+4N2),如果d远小于N,则访存量的复杂度变成了 O ( N 2 ) O(N^2) O(N2)
  • flash attention(分析时将inner loop作为一个整体进行分析,就像上面示意图画的那样)
    • 从HBM中读取分块 Q i , i = 0 , . . . , T r − 1 Q_i, i=0, ..., T_r -1 Qi,i=0,...,Tr1,读取分块 K j K_j Kj,访存量= w ( N d + B c d ) w(Nd+B_c d) w(Nd+Bcd);后面 S i j , P ~ i j S_{ij}, \widetilde{P}_{ij} Sij,P ij不需要写回HBM; m , l m, l m,l只是一个向量,数据量很少,忽略;再后面读取和写入分块 O i , i = 0 , . . . , T r = 1 O_i, i = 0, ...,T_r =1 Oi,i=0,...,Tr=1,访存量= w ( 2 × N d ) w(2\times Nd) w(2×Nd)
    • outer loop共有 N B c = T c \frac{N}{B_c}=T_c BcN=Tc次,总的访存量= w × N B c × ( N d + B c d + 2 N d ) = w ( N d + 3 N 2 d B c ) = w ( T c + 1 ) N d w\times \frac{N}{B_c} \times (Nd + B_cd + 2Nd)=w(Nd+\frac{3N^2d}{B_c})=w(T_c+1)Nd w×BcN×(Nd+Bcd+2Nd)=w(Nd+Bc3N2d)=w(Tc+1)Nd
    • 比如N=1024,d=64,B=64,standard_attention访存量-flash_attention访存量= w ( 3 N d + 4 N 2 − N d − 3 N 2 d B c ) = w ( 2 N d + ( 4 − 3 d B c ) N 2 ) = w ( 2 N d + N 2 ) w(3Nd+4N^2-Nd-\frac{3N^2d}{B_c})=w(2Nd+(4-\frac{3d}{B_c})N^2)=w(2Nd+N^2) w(3Nd+4N2NdBc3N2d)=w(2Nd+(4Bc3d)N2)=w(2Nd+N2),可以看出少了很多访存

实际使用

接口返回值

flash-attention开源代码中,针对不同qkv、是否是varlen、是否需要kv_cache等不同需求封装了不同的接口,这里说一下返回值。这些接口的返回值都相同,除了返回输出的 O O O之外,如果设置了return_attn_probs=True,还会返回softmax_lse和S_dmask:

  • softmax_lse(形状 [ n h e a d s , s e q l e n ] [nheads, seqlen] [nheads,seqlen]):在计算 S = Q K T s c a l e S=\frac{QK^T}{scale} S=scaleQKT之后,会得到形状为 [ b s , s e q l e n , s e q l e n ] [bs, seqlen, seqlen] [bs,seqlen,seqlen]的方阵S,在计算softmax的过程中,需要按行求和,得到一个列向量,然后再取log,写成表达式即为: s o f t m a x _ l s e = l o g [ ∑ j e S i j ] softmax\_lse=log[\sum_je^{S_{ij}}] softmax_lse=log[jeSij],注意不是 s o f t m a x _ l s e = l o g [ ∑ j e S i j − r o w m a x ( S i j ) ] softmax\_lse=log[\sum_je^{S_{ij}-rowmax(S_{ij})}] softmax_lse=log[jeSijrowmax(Sij)],参考issue:What’s the exactly formula of softmax_lse? #404
  • S_dmask(形状 [ b s , n h e a d s , s e q l e n , s e q l e n ] [bs, nheads, seqlen, seqlen] [bs,nheads,seqlen,seqlen]):就是返回 P = s o f t m a x ( Q K T s c a l e + m a s k ) P=softmax(\frac{QK^T}{scale}+mask) P=softmax(scaleQKT+mask)的这个P矩阵

varlen attention

特别的,这里再说一下flash_attn_varlen_func等一些支持varlen的接口,其函数形参中还有cu_seqlens_qcu_seqlens_kmax_seqlen_qmax_seqlen_k等特有的参数。这里介绍一些varlen是什么。

varlen即变长序列,产生的背景是”数据拼接“,即LLM使用的训练数据集中,长度较短的序列占大多数,这些短序列为了能够符合Transformer固定长度的输入,就要进行padding,序列越短,padding越多,而我们不太想要padding,padding只是无奈之举。此时,我们可以使用varlen特性,简单来说就是将多个短序列拼接成一个长序列,但是还是每个短序列自己内部计算注意力,短序列之间是隔离的,这样减少了padding,节省计算量和显存。

这里举个例子(参考),比如一些短序列长度分别是:70,300,180, …,260,120,1200,…等,attention固定输入长度是4096,此时我们将这些短序列拼接起来,使用varlen_attn后,就像右图所示,每个短序列自己内部计算attention,短序列之间不计算attention(否则就像左图这样,白白多了很多浪费的计算)

在这里插入图片描述

为了实现varlen特性,需要对接口有一些调整。比如不使用varlen的flash_attn接口中,传入的Q、K、V的形状一般为 [ b s , s e q l e n , n h e a d s , h e a d _ d i m ] [bs, seqlen, nheads, head\_dim] [bs,seqlen,nheads,head_dim](K和V的nheads可以少于Q的nheads,此时就是GQA/MQA)。在使用varlen的flash_attn接口中,主要有两点变化:

  • Q、K、V的形状一般为 [ t o t a l _ s e q , n h e a d s , h e a d _ d i m ] [total\_seq, nheads, head\_dim] [total_seq,nheads,head_dim],这里将多个batch拼接起来,拼起来的长度为 t o t a l _ s e q total\_seq total_seq
  • 多了cu_seqlens_qcu_seqlens_kmax_seqlen_qmax_seqlen_k等特有的参数
    • cu_seqlens_q是对每个短序列的Q的长度的exclusive_scan,作用就是找到原来每个batch的起始点(offset),比如上面的例子,此时cu_seqlens_q=[0, 70, 370, 550, ... ],如果cu_seqlens_q的形状为 [ b a t c h _ s i z e + 1 ] [batch\_size+1] [batch_size+1],则需要在最后拼接上序列Q的总长度
    • max_seqlen_q好理解,就是短序列的Q的最长长度

在具体实现中,对每个序列的每个head分别launch kernel,来实现并行计算,这个过程中要通过cu_seqlens_q来确定对应Q的start_idx和end_idx。

参考:

Flash attention变长batching API使用

How did flash-attn compute attention for cu_seqlens #850

参考

图解大模型计算加速系列:FlashAttention V1,从硬件到计算逻辑

优质好文:

[Attention优化][2w字]🔥原理&图解: 从Online-Softmax到FlashAttention V1/V2/V3

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

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

相关文章

10.解析解方法推导线性回归——不容小觑的线性回归算法

引言 线性回归是许多复杂机器学习模型的基础。作为一种基本的机器学习方法,线性回归提供了清晰的思路和工具,通过理解其推导过程,可以更好地掌握机器学习的基本原理和模型设计。 通过阅读本篇博客,你可以: 1.学会如…

win11永久关闭Windows Defend

# Win11 Microsoft Defender 防病毒 彻底关闭 Win11 Microsoft Defender 防病毒关闭 **WinR****——输入 gpedit.msc ,打开本地组策略编辑器——计算机配置——管理模板——Windows组件——Microsoft Defender 防病毒——关闭 Microsoft Defender 防病毒策略——设置…

免费在线压缩pdf 压缩pdf在线免费 推荐简单好用

压缩pdf在线免费?在日常生活和工作学习中,处理PDF文件是常见任务。但有时PDF文件体积较大,给传输、存储和分享带来不便。因此,学习PDF文件压缩技巧十分必要。压缩PDF文件是指通过技术手段减小文件占用的存储空间,同时尽…

kafka 一步步探究消费者组与分区分配策略

本期主要聊聊kafka消费者组与分区 消费者组 & 消费者 每个消费者都需要归属每个消费者组,每个分区只能被消费者组中一个消费者消费 上面这段话还不够直观,我们举个例子来说明。 订单系统 订单消息通过 order_topic 发送,该topic 有 5个分区 结算系…

基于YOLO算法的网球运动实时分析-击球速度测量-击球次数(附源码)

这个项目通过分析视频中的网球运动员来测量他们的速度、击球速度以及击球次数。该项目使用YOLO(You Only Look Once)算法来检测球员和网球,并利用卷积神经网络(CNNs)来提取球场的关键点。此实战项目非常适合提升您的机…

基于 Web 的工业设备监测系统:非功能性需求与标准化数据访问机制的架构设计

目录 案例 【说明】 【问题 1】(6 分) 【问题 2】(14 分) 【问题 3】(5 分) 【答案】 【问题 1】解析 【问题 2】解析 【问题 3】解析 相关推荐 案例 阅读以下关于 Web 系统架构设计的叙述,回答问题 1 至问题 3 。 【说明】 某公司拟开发一款基于 Web 的…

【JavaEE】多线程编程引入——认识Thread类

阿华代码,不是逆风,就是我疯,你们的点赞收藏是我前进最大的动力!!希望本文内容能帮到你! 目录 引入: 一:Thread类 1:Thread类可以直接调用 2:run方法 &a…

springboot每次都需要重设密码?明明在springboot的配置中设置了密码

第一步:查看当前的密码是什么? 打开redis-cli.exe,输入config get requirepass,查看当前的密码是什么? 接着,修改redis的配置文件,找到redis的安装目录,找到相关的conf文件&#x…

FreeRTOS下UART的封装

FreeRTOS下UART的封装_哔哩哔哩_bilibili Git使用的一个BUG: 当出现这个问题是因为git本身的安全证书路径有问题,我们需要重新指定路径 P1:UART程序层次

【2024】前端学习笔记7-颜色-位置-字体设置

学习笔记 1.定义:css2.颜色:color3.字体相关属性:font3.1.字体大小:font-size3.2.字体风格:font - style3.3.字体粗细:font - weight3.4.字体族:font - family 4.位置:text-align 1.…

K8s容器运行时,移除Dockershim后存在哪些疑惑?

K8s容器运行时,移除Dockershim后存在哪些疑惑? 大家好,我是秋意零。 K8s版本截止目前(24/09)已经发布到了1.31.x版本。早在K8s版本从1.24.x起(22/05),默认的容器运行时就不再是Doc…

最新Kali Linux超详细安装教程(附镜像包)

一、镜像下载: 链接:https://pan.baidu.com/s/1BfiyAMW6E1u9fhfyv8oH5Q 提取码:tft5 二、配置虚拟机 这里我们以最新的vm17.5为例。进行配置 1.创建新的虚拟机:选择自定义 2.下一步 3.选择稍后安装操作系统 4.选择Debian版本 因…

02_RabbitMQ消息丢失解决方案及死信队列

一、数据丢失 第一种:生产者弄丢了数据。生产者将数据发送到 RabbitMQ 的时候,可能数据就在半路给搞丢了,因为网络问题,都有可能。 第二种:RabbitMQ 弄丢了数据。MQ还没有持久化自己挂了。 第三种:消费端…

Vue3新组件transition(动画过渡)

transition组件&#xff1a;控制V-if与V-show的显示与隐藏动画 1.基本使用 <template><div><button click"falg !falg">切换</button><transition name"fade" :enter-to-class"etc"><div v-if"falg&quo…

为什么git有些commit记录,只有git reflog可以看到,git log看不到?

文章目录 原因分析1. git log 只能显示 **可达的** 提交2. git reflog 记录所有引用的变更 常见导致 git log 看不到提交的原因1. git reset 操作2. git rebase 操作3. 分支删除4. git commit --amend5. 垃圾回收&#xff08;GC&#xff09;* 如何恢复 git log 看不到的提交&am…

数据库系统基础概述

文章目录 前言一、数据库基础概念 1.数据库系统的组成2.数据模型3.数据库的体系结构二、MySQL数据库 1.了解MySQL2.MySQL的特性3.MySQL的应用场景总结 前言 MySQL数据库是一款完全免费的产品&#xff0c;用户可以直接从网上下载使用&#xff0c;不用花费任何费用。这点对于初学…

多语言长文本 AI 关键字提取 API 数据接口

多语言长文本 AI 关键字提取 API 数据接口 AI / 文本 专有模型极速提取 多语言长文本 / 实时语料库。 1. 产品功能 支持长文本关键词提取&#xff1b;多语言关键词识别&#xff1b;基于 AI 模型&#xff0c;提取精准关键词&#xff1b;全接口支持 HTTPS&#xff08;TLS v1.0 …

CentOS7更换阿里云yum更新源

目前CentOS内置的更新安装源经常报错无法更新&#xff0c;或者速度不够理想&#xff0c;这个时候更换国内的镜像源就是一个不错的选择。 备份内置更新源 mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup 下载阿里云repo源&#xff08;需要系统…

后台数据管理系统 - 项目架构设计-Vue3+axios+Element-plus(0916)

接口文档: https://apifox.com/apidoc/shared-26c67aee-0233-4d23-aab7-08448fdf95ff/api-93850835 接口根路径&#xff1a; http://big-event-vue-api-t.itheima.net 本项目的技术栈 本项目技术栈基于 ES6、vue3、pinia、vue-router 、vite 、axios 和 element-plus http:/…

LeetCode 每周算法 6(图论、回溯)

LeetCode 每周算法 6&#xff08;图论、回溯&#xff09; 图论算法&#xff1a; class Solution: def dfs(self, grid: List[List[str]], r: int, c: int) -> None: """ 深度优先搜索函数&#xff0c;用于遍历并标记与当前位置(r, c)相连的所有陆地&…