Flash Attention原理讲解
目录
- 前言
- 0. 简述
- 1. self-attention
- 2. roofline model
- 3. 矩阵分块
- 4. softmax分块
- 5. FlashAttention
- 结语
- 参考
前言
看了几个视频和几篇文章学习了下 Flash Attention,记录下个人学习笔记,仅供自己参考😄
refer1:Flash Attention 为什么那么快?原理讲解
refer2:LLM(17):从 FlashAttention 到 PagedAttention, 如何进一步优化 Attention 性能
refer3:https://chatgpt.com/
0. 简述
这篇文章我们简单讨论下 Flash Attention(FA) 的原理,FA 现在是训练大模型默认采用的技术,从论文题目《FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness》就可以看出它有两大优势:
- Fast:加快模型训练的速度
- Memory-Efficient:内存高效,它可以减少显存的占用
并且 FA 保证 exact attention,也就是它和标准的 attention 计算得到的结果是完全一致的,并不像其它的一些算法是以降低 attention 的精度为代价来提高训练速度的。最后标题中的 with IO-Awareness 说明了它的整个算法是以改进 I/O 效率为目的的
论文的标题言简意赅,直接说明了 Flash Attention 的优势和目的
1. self-attention
在讲解 Flash Attention 之前,我们先看下 Transformer 中 self-attention 的标准计算过程,如下图所示:
Note:在上图中 N N N 代表序列长度, D D D 代表每个 head 的维度
上图展示了 self-attention 的标准计算过程,首先是输入的 token 向量通过 W q , W k , W v W_q,W_k,W_v Wq,Wk,Wv 生成对应的 Q , K , V Q,K,V Q,K,V 矩阵,然后 Q K T QK^T QKT 矩阵相乘得到注意力分数矩阵 S S S,接着对 S S S 矩阵按行求取 S o f t m a x \mathrm{Softmax} Softmax 得到注意力概率分布矩阵 P P P,最后利用 P V PV PV 相乘得到最终的注意力输出 O O O
为了简单起见,这里我们省略了缩放因子 1 D \frac{1}{\sqrt{D}} D1,也没有考虑多头注意力机制以及 dropout、mask 等
Algorithm 0 描述了标准 attention 的实现:
- 输入为三个矩阵 Q , K , V ∈ R N × d \mathbf{Q},\mathbf{K},\mathbf{V} \in \mathbb{R}^{N\times d} Q,K,V∈RN×d,初始存储于 HBM 中
- 步骤 1:计算 S = Q K ⊤ \mathbf{S} = \mathbf{Q}\mathbf{K}^{\top} S=QK⊤
- 加载:从高带宽存储(HBM)中分块(by blocks)读取矩阵 Q , K \mathbf{Q},\mathbf{K} Q,K
- 计算:对每个分块执行矩阵乘法 Q K ⊤ \mathbf{Q}\mathbf{K}^{\top} QK⊤ 得到注意力分数矩阵 S \mathbf{S} S
- 写回:将计算得到的矩阵 S \mathbf{S} S 写回到 HBM 中
- 步骤 2:计算 P = s o f t m a x ( S ) \mathbf{P} = \mathrm{softmax} (\mathbf{S}) P=softmax(S)
- 加载:从 HBM 中读取矩阵 S \mathbf{S} S
- 计算:对每个分块进行 s o f t m a x \mathrm{softmax} softmax 操作,得到注意力概率分布矩阵 P \mathbf{P} P
- 写回:将 s o f t m a x \mathrm{softmax} softmax 后的矩阵 P \mathbf{P} P 写回到 HBM
- 这里的 s o f t m a x \mathrm{softmax} softmax 通常是在最后一个维度(即序列长度维度)上进行,以保证每个位置的注意力权重之和为 1
- 步骤 3:计算 O = P V \mathbf{O}=\mathbf{PV} O=PV
- 加载:从 HBM 中分块读取矩阵 P , V \mathbf{P},\mathbf{V} P,V
- 计算:对每个分块执行矩阵乘法 P V \mathbf{PV} PV,得到输出矩阵 O \mathbf{O} O
- 写回:将最终的输出矩阵 O \mathbf{O} O 写回到 HBM
- 步骤 4:返回结果 O \mathbf{O} O
- 最终返回的结果矩阵 O \mathbf{O} O 就是标准自注意力的输出
整个流程对应了标准注意力机制中最常见的计算公式:
O = s o f t m a x ( Q K T ) ⏟ S ⏟ P × V O = \underbrace{\mathrm{softmax}\underbrace{(QK^T)}_{S}}_{P} \times V O=P softmaxS (QKT)×V
那大家不禁会问为什么不一次性计算 Q K T QK^T QKT、 P V PV PV 等矩阵乘法呢,而采用分块的方式呢?🤔
在实际硬件实现(例如 GPU 上)时,会出于效率考虑,采用“分块”(block-wise)读写和计算的方式。具体原因包括:
- 显存或带宽存储的容量与带宽限制:一次性将大矩阵读入或写出会导致带宽开销过大,分块能更好地利用缓存与寄存器
- 并行效率:矩阵乘法、softmax 都可以在分块层面做并行,提升运行效率
- 避免中间激增地存储开销:例如直接存储完整的 S S S 或 P P P 可能很大,片上内存 SRAM 存储空间不够,而分块可以减少一次性占用的显存量
2. roofline model
在讲解 FlashAttention 的优化之前,我们先来分析下标准的 attention 计算的瓶颈,我们可以使用 roofline model 来进行分析
roofline model 是一种常用的、直观的性能分析工具,用来帮助我们评估应用程序在给定硬件架构(CPU 或 GPU)上的性能上限(即理论最佳性能),并确定性能瓶颈究竟来自于算力(Compute Bound)还是存储带宽(Memory Bandwidth Bound)
关于 roofline model 韩君老师也简单讲过,大家感兴趣的可以看看:四. TensorRT模型部署优化-Roofline model
上面是 roofline model 的图示,图中坐标含义如下:
- 横轴:Operational Intensity I I I / 计算强度
- 单位 FLOP/Byte
- 每访问内存一个字节所执行的浮点运算次数
- 计算强度越高,程序越趋近于计算密集型(compute-bound),受计算能力限制;计算强度越低,则趋近于内存密集型(memory-bound),受内存带宽限制
- 纵轴:Attainable Performance P P P / 性能
- 单位 FLOP/s
- 每秒执行的浮点运算次数
- 算力 π \pi π:也称为计算平台的性能上限
- 单位 FLOP/s
- 指的是一个计算平台倾尽全力每秒钟所能完成的浮点运算数
- 带宽 β \beta β:计算平台的带宽上限
- 单位 Byte/s
- 指的是一个计算平台倾尽全力每秒所能完成的内存交换量
- 计算强度上限 I m a x = π β I_{max}=\frac{\pi}{\beta} Imax=βπ:两个指标相除即可得到计算平台的计算强度上限
- 单位 FLOPs/Byte
- 描述的是在这个计算平台上,单位内存交换最多用来进行多少次计算
roofline model 中算力决定 “屋顶” 的高度(图中绿色线段),带宽决定 “房檐” 的斜率(图中红色线段)。roofline model 划分出的两个瓶颈区域为:
P = { β ⋅ I , w h e n I < I m a x Memory Bound π , w h e n I ⩾ I m a x Compute Bound P=\left\{ \begin{array} {ll}\beta\cdot I, & when\ \ I<I_{max}\quad{\color{red}\text{Memory Bound}} \\ \\ \pi, & when\ \ I\geqslant I_{max}\quad{\color{green}\text{Compute Bound}} \end{array}\right. P=⎩ ⎨ ⎧β⋅I,π,when I<ImaxMemory Boundwhen I⩾ImaxCompute Bound
- 计算约束(Memory Bound)—此时 HBM 访问所花费的时间相对较低,不管模型的计算强度 I I I 多大,它的理论性能 P P P 最大只能等于计算平台的算力 π \pi π。例如,具有较大维度的矩阵乘法和具有大量通道的卷积
- 带宽约束(Compute Bound)—此时模型位于 “房檐” 区间,模型理论性能 P P P 大小完全由计算平台的带宽上限 β \beta β(房檐的斜率)以及模型自身的计算强度 I I I 所决定。例如,elementwise 操作(如 activation,dropout 等)和规约操作(如 sum,softmax,batch normalization,layer normalization 等)
在 self-attention 中,它的计算瓶颈主要是 Memory Bound,这主要是因为:
- 大量读写中间结果
- self-attention 的标准实现中会将 s o f t m a x ( Q K T ) \mathrm{softmax}(QK^T) softmax(QKT) 的中间分数矩阵 S S S 先写回 HBM,再读回 SRAM 做 softmax,再写出 P P P,随后又要读入 P P P 才能与 V V V 相乘
- 这类频繁的显存/主存的 I/O 极大地消耗带宽
- 序列长度变大时, N 2 N^2 N2 读写地影响更严重
- 注意力分数矩阵和概率矩阵的大小是 N 2 N^2 N2,随着 N N N 的增加,存储和带宽消耗呈平方级增长,很快就变成主要瓶颈
对于 Memory Bound 的优化一般是进行 fusion 融合操作,不对中间结果缓存,减少 HBM 的访问,如下图所示:
因此 FlashAttention 论文的优化思路本质就是要减少不必要的中间读写,尽可能地使用 SRAM 来加快计算速度,在内部使用 tiling 计算与 softmax 融合,提高算术强度,降低访存量,从而减轻内存带宽瓶颈
前面反复提到的 HBM 和 SRAM 究竟指什么内存呢,二者有什么区别呢?🤔
Note:杜老师课程时有简单讲解过内存模型,大家感兴趣的可以看看:3.3.cuda运行时API-内存的学习,pinnedmemory,内存效率问题
要搞清楚 HBM 和 SRAM 的区别,我们首先需要对 GPU 存储体系结构做了解:
上图展示了 CUDA 编程模型下各种类型的存储,以及它们在 GPU 体系结构中的特点、作用范围和访问方式,大家可以简单看看
我们来看一个不太严谨但是能辅助理解的图:
这张图展示了显卡中常见内存所处位置,其中 shared memory 为片上内存,global memory 为片外内存,我们需要知道的是距离计算芯片(运算单元)越近的内存,速度越快,但空间越小,价格越贵。因此 shared memory 的存储容量是远远小于 global memory 的,但它的速度是要更快的
ok,再回到我们的 HBM 和 SRAM 的话题
HBM(High Bandwidth Memory)是指 “高带宽内存”,属于 GPU 上的全局内存(global memory)。SRAM(Static Random-Access Memory)是指 “静态随机存取存储器”,在 GPU 架构中通常指代片上(on-chip)的快速缓存、寄存器或共享内存(shared memory/L1 Cache/寄存器等)
从性能角度看,SRAM(片上内存)比 HBM(片外内存)更快、延迟更低,但容量要小得多,HBM 容量更大,但带宽和延迟都逊于片上内存
3. 矩阵分块
下面我们来看下如何通过矩阵分块和融合多个计算来减少对 HBM 的访问,整个计算过程如下(假设 N = 6 , D = 4 N=6,D=4 N=6,D=4):
Note:这里我们先跳过 softmax 操作,因为它比较特殊,分块计算比较麻烦,后面我们再单独讨论,我们先认为结果直接就是 S × V S\times V S×V
首先我们从 HBM 中分块读取 Q Q Q 的前两行, K T K^T KT 的前三列,然后传入到 SRAM 中对它们进行矩阵乘法 Q K T QK^T QKT 的计算得到 S S S。得到的 S S S 并不存入 HBM 中,而是直接和 V V V 的分块进行计算,得到 O O O 的前两行
值得注意的是这时我们得到的 O O O 的前两行并不是最终的结果,因为我们知道 O O O 是对所有的 V V V 的一个加权平均,目前只是对 V V V 的前三行进行加权平均,后面我们还要对 O O O 进行更新,因此这里我们用浅一点的颜色来表示,这个值还只是一个中间结果
接着 K K K 和 V V V 的分块还保留在 SRAM 中,从 HBM 中读取 Q Q Q 的中间两行,然后经过同样的计算,得到 O O O 的第三行和第四行的中间结果
然后继续保留 K K K 和 V V V 的分块在 SRAM 中,从 H B M HBM HBM 里读取 Q Q Q 的最后两行,经过同样的计算,得到 O O O 的最后两行的中间结果
接下来读取 K T K^T KT 的后三列, V V V 的后三行, Q Q Q 的前两行进行计算,这里算出的 O O O 是对 V V V 的后三行值的加权平均,再从 HBM 中读取 O O O 之前保存的中间结果(也就是对 V V V 的前三行的加权平均值进行加和),这就是 O O O 的前两行的最终结果了
同样保持 K K K 和 V V V 的分块不变,从 HBM 里读取下一个分块的 Q Q Q 进行计算,从 HBM 里读取之前计算的中间结果 O ′ O^{\prime} O′,加和更新后存入 HBM
最后继续 SRAM 里面的 K K K 和 V V V 分块不变,从 HBM 里读取最后一个分块的 Q Q Q 进行计算,从 HBM 里读取之前计算的中间结果 O ′ O^{\prime} O′,加和更新后存入 HBM,这时就完成了一个 attention 的计算
我们可以发现,通过对矩阵分块以及将多步计算进行融合,中途没有将中间计算结果 S S S 存入 HBM 中,大大减少了 I/O 的时间,这一切看起来都不错,除了 softmax
softmax 是按行进行的,只有这一行所有的数据都计算完成后,才能进行之后对 V V V 的加权求和计算,所以如果想要让之前的矩阵分块对 attention 多步进行融合计算,前提是必须解决 softmax 分块计算的问题
4. softmax分块
下面我们就来讨论 softmax 的分块计算
在讨论之前我们先来看原始 softmax 中存在的数值溢出问题,我们在上篇文章中也详细讲过,大家感兴趣的可以看看:从Online Softmax到FlashAttention
原始 softmax 的计算公式如下:
softmax ( x i ) = e x i ∑ i = 1 N e x i \text{softmax}(x_i) = \frac{e^{x_i}}{\sum_{i=1}^{N}e^{x_i}} softmax(xi)=∑i=1Nexiexi
现在我们训练都是在混合精度 FP16 下进行的,FP16 所能表示的最大数值是 65536,这意味着当 x ≥ 11 x \geq 11 x≥11 时, e x e^x ex 就超过了 FP16 所能表示的最大范围了
import numpy as npscores = np.array([123, 456, 789])
softmax = np.exp(scores) / np.sum(np.exp(scores))
print(softmax) # [ 0. 0. nan]
当然针对数值溢出有其对应的优化方法,那就是将每一个输入值减去输入值中最大的值,这种方法也被称为 safe softmax
m ( x ) : = max i x i softmax ( x i ) = e x i − m ∑ i = 1 N e x i − m m(x) := \max_{i}x_i \\ \text{softmax}(x_i) = \frac{e^{x_i-m}}{\sum_{i=1}^{N}e^{x_i-m}} m(x):=imaxxisoftmax(xi)=∑i=1Nexi−mexi−m
import numpy as npscores = np.array([123, 456, 789])
scores -= np.max(scores)
p = np.exp(scores) / np.sum(np.exp(scores))
print(p) # [5.75274406e-290 2.39848787e-145 1.00000000e+000]
参考自:一文详解Softmax函数
我们再来看下 safe softmax 的计算过程:
x = [ x 1 , … , x N ] m ( x ) : = max i x i p ( x ) : = [ e x 1 − m ( x ) … e x B − m ( x ) ] ℓ ( x ) : = ∑ i p ( x ) i softmax ( x ) : = p ( x ) ℓ ( x ) x=[x_1,\ldots,x_N]\\ m(x) := \max_{i}x_i \\ p(x) := [e^{x_1-m(x)}\quad\ldots \quad e^{x_B-m(x)}] \\ \ell \left( x \right):= \sum_{i}p \left( x \right)_{i} \\ \text{softmax}(x) := \frac{p(x)}{\ell(x)} x=[x1,…,xN]m(x):=imaxxip(x):=[ex1−m(x)…exB−m(x)]ℓ(x):=i∑p(x)isoftmax(x):=ℓ(x)p(x)
接下来我们看下 safe softmax 的分块是怎么做的
假设有一组 x = [ x 1 , … , x N , … , x 2 N ] x=[x_1,\ldots,x_N,\ldots,x_{2N}] x=[x1,…,xN,…,x2N],我们将它分为两个部分,第一个部分是 x ( 1 ) = [ x 1 , … , x N ] x^{(1)}=[x_1,\ldots,x_N] x(1)=[x1,…,xN],第二个部分是 x ( 2 ) = [ x N + 1 , … , x 2 N ] x^{(2)}=[x_{N+1},\ldots,x_{2N}] x(2)=[xN+1,…,x2N],也就是 x = [ x ( 1 ) , x ( 2 ) ] x=[x^{(1)},x^{(2)}] x=[x(1),x(2)],那么有:
m ( x ) = m ( [ x ( 1 ) x ( 2 ) ] ) = max ( m ( x ( 1 ) ) , m ( x ( 2 ) ) ) {m(x)=m\left(\left[x^{(1)}x^{(2)}\right]\right)=\max\left(m\left(x ^{(1)}\right),m\left(x^{(2)}\right)\right)} m(x)=m([x(1)x(2)])=max(m(x(1)),m(x(2)))
为了与分块前的结果保持统一,我们可以通过如下的方式来构造 p ( x ) p(x) p(x):
p ( x ) = [ e m ( x ( 1 ) ) − m ( x ) p ( x ( 1 ) ) e m ( x ( 2 ) ) − m ( x ) p ( x ( 2 ) ) ] = [ e m ( x ( 1 ) ) − m ( x ) [ e x 1 ( 1 ) − m ( x ( 1 ) ) … e x N ( 1 ) − m ( x ( 1 ) ) ] e m ( x ( 2 ) ) − m ( x ) [ e x N + 1 ( 2 ) − m ( x ( 2 ) ) … e x 2 N ( 2 ) − m ( x ( 2 ) ) ] ] = [ [ e x 1 ( 1 ) − m ( x ) … e x N ( 1 ) − m ( x ) ] [ e x N + 1 ( 2 ) − m ( x ) … e x 2 N ( 2 ) − m ( x ) ] ] = [ e x 1 − m ( x ) … e x 2 N − m ( x ) ] \begin{aligned} p(x)&= \begin{bmatrix} e^{m\left(x^{(1)} \right)-m(x)}p\left(x^{(1)}\right)\quad e^{m\left(x^{(2)}\right)-m(x)}p\left(x^ {(2)}\right)\end{bmatrix}\\ &= \begin{bmatrix} e^{m\left(x^{(1)}\right)-m(x)}\big[ e^{x _{1}^{(1)}-m(x^{(1)})}\quad\ldots\quad e^{x_{N}^{(1)}-m(x^{(1)})} \big] \quad e^{m\left(x^{(2)}\right)-m(x)} \big[ e^{x _{N+1}^{(2)}-m(x^{(2)})}\quad\ldots\quad e^{x_{2N}^{(2)}-m(x^{(2)})} \big]\end{bmatrix}\\ &= \begin{bmatrix} \big[ e^{x_{1}^{(1)}-m(x)}\quad\ldots \quad e^{x_{N}^{(1)}-m(x)} \big]\quad\big[ e^{x_{N+1}^{(2)}-m(x)}\quad \ldots\quad e^{x_{2N}^{(2)}-m(x)} \big]\end{bmatrix} \\ &= \big[ e^{x_{1}-m(x)}\quad\ldots\quad e^{x_{2N}-m(x)} \big]\end{aligned} p(x)=[em(x(1))−m(x)p(x(1))em(x(2))−m(x)p(x(2))]=[em(x(1))−m(x)[ex1(1)−m(x(1))…exN(1)−m(x(1))]em(x(2))−m(x)[exN+1(2)−m(x(2))…ex2N(2)−m(x(2))]]=[[ex1(1)−m(x)…exN(1)−m(x)][exN+1(2)−m(x)…ex2N(2)−m(x)]]=[ex1−m(x)…ex2N−m(x)]
那为什么要这么构造 p ( x ) p(x) p(x) 呢?为什么要给它们乘以一个系数呢?我们来举个例子简单说明下,由于 m ( x ) m(x) m(x) 是 m ( x ( 1 ) ) m(x^{(1)}) m(x(1)) 是 m ( x ( 2 ) ) m(x^{(2)}) m(x(2)) 里较大的那个值,所以它可能等于 m ( x ( 1 ) ) m(x^{(1)}) m(x(1)) 或者 m ( x ( 2 ) ) m(x^{(2)}) m(x(2))
我们假设 m ( x ) = m ( x ( 2 ) ) m(x) = m(x^{(2)}) m(x)=m(x(2)),那么 p ( x ( 2 ) ) p(x^{(2)}) p(x(2)) 的系数 e m ( x ( 2 ) ) − m ( x ) = 1 e^{m\left(x^{(2)}\right)-m(x)}=1 em(x(2))−m(x)=1,分块部分的 p ( x ( 2 ) ) p(x^{(2)}) p(x(2)) 不变,这很好理解,因为 p ( x ( 2 ) ) p{(x^{(2)})} p(x(2)) 分块计算的时候,它的 x x x 减去的就是全局最大值,所以此时不需要再进行调整
那么对于 p ( x ( 1 ) ) p{(x^{(1)})} p(x(1)) 部分,它在分块计算时,它的 x x x 减去的是局部最大值不是全局最大值,那么它和全局最大值相比少减了多少呢?也就是上面的 m ( x ( 1 ) ) − m ( x ) m(x^{(1)})-m(x) m(x(1))−m(x),现在给它补回来,这样乘上 e m ( x ( 1 ) ) − m ( x ) e^{m\left(x^{(1)} \right)-m(x)} em(x(1))−m(x) 系数之后, p ( x ( 1 ) ) p{(x^{(1)})} p(x(1)) 就被调整为减去全局最大值的正确结果了
同理 ℓ ( x ) \ell \left(x\right) ℓ(x) 的构造方式如下:
ℓ ( x ) = ℓ ( [ x ( 1 ) x ( 2 ) ] ) = e m ( x ( 1 ) ) − m ( x ) ℓ ( x ( 1 ) ) + e m ( x ( 2 ) ) − m ( x ) ℓ ( x ( 2 ) ) \ell(x)=\ell\left(\left[x^{(1)}x^{(2)}\right]\right)=e^{m\left(x^{(1)}\right)-m(x)}\ell\left(x^{(1)}\right)+e^{m\left(x^{(2)}\right)-m(x)}\ell\left(x^{(2)}\right) ℓ(x)=ℓ([x(1)x(2)])=em(x(1))−m(x)ℓ(x(1))+em(x(2))−m(x)ℓ(x(2))
最后 softmax 计算如下:
softmax ( x ) : = p ( x ) ℓ ( x ) \text{softmax}(x) := \frac{p(x)}{\ell(x)} softmax(x):=ℓ(x)p(x)
可以看到 softmax 也可以通过分块来计算了,只是我们需要额外保存几个变量 m ( x ( 1 ) ) , m ( x ( 2 ) ) , ℓ ( x ( 1 ) ) , ℓ ( x ( 2 ) ) m(x^{(1)}),m(x^{(2)}),\ell(x^{(1)}),\ell(x^{(2)}) m(x(1)),m(x(2)),ℓ(x(1)),ℓ(x(2)),不过它们对于 softmax 每行都各自占用一个数字,存储占用非常小
另外就是在分块进行合并时,需要额外的调整计算,增加了计算量,但是这些计算量相对于减少 I/O 时间都是非常划算的
5. FlashAttention
下面我们看下 FlashAttention 伪代码的实现:
下面是 FlashAttention 的整体流程:
一、算法基本设置:
- 输入为三个矩阵 Q , K , V ∈ R N × d \mathbf{Q},\mathbf{K},\mathbf{V} \in \mathbb{R}^{N\times d} Q,K,V∈RN×d,初始存储于低速 HBM(高带宽内存)
- 片上内存 SRAM(on-chip SRAM)的大小为 M M M
二、算法详细步骤分析:
步骤 1-2(初始化阶段)
- 步骤 1:设置分块大小
- 将序列长度 N N N 按照列块大小 B c B_c Bc 和行块大小 B r B_r Br 进行划分,使得每次在片上(on-chip SRAM)只处理一小部分 Q \mathbf{Q} Q 和 K \mathbf{K} K
- 设定每个列块的尺寸为 B c = ⌈ M 4 d ⌉ B_c=\lceil \frac{M}{4d} \rceil Bc=⌈4dM⌉,每个行块的尺寸为 B r = min ( ⌈ M 4 d ⌉ , d ) B_r=\min \left(\lceil \frac{M}{4d}\rceil,d\right) Br=min(⌈4dM⌉,d),同时确保其不超过序列长度 N N N
- 步骤 2:在片外内存 HBM 中,初始化输出矩阵 O = ( 0 ) N × d ∈ R N × d \mathbf{O}=(0)_{N\times d}\in \mathbb{R}^{N\times d} O=(0)N×d∈RN×d、归一化因子向量 ℓ = ( 0 ) N ∈ R N \ell = (0)_{N} \in \mathbb{R}^{N} ℓ=(0)N∈RN 以及最大值向量 m = ( − ∞ ) N ∈ R N m = (- \infty)_N \in \mathbb{R}^N m=(−∞)N∈RN
步骤 3-4 (输入输出分块阶段)
- 步骤 3:输入分块(tiling)
- 将输入矩阵 Q \mathbf{Q} Q 划分成 T r = ⌈ N B r ⌉ T_r= \lceil \frac{N}{B_r} \rceil Tr=⌈BrN⌉ 个子块 Q 1 , … , Q T r \mathbf{Q}_1,\ldots,\mathbf{Q}_{T_r} Q1,…,QTr,每个子块大小为 B r × d B_r \times d Br×d
- 同样地,将 K , V \mathbf{K},\mathbf{V} K,V 划分成 T c = ⌈ N B c ⌉ T_c = \lceil \frac{N}{B_c} \rceil Tc=⌈BcN⌉ 个子块 K 1 , … , K T c \mathbf{K}_1,\ldots,\mathbf{K}_{T_c} K1,…,KTc 以及 V 1 , … , V T c \mathbf{V}_1,\ldots,\mathbf{V}_{T_c} V1,…,VTc,每个子块大小为 B c × d B_c \times d Bc×d
- 步骤 4:输出与中间值分块
- 将输出矩阵 O \mathbf{O} O 同样分成 T r T_r Tr 个子块 O 1 , … , O T r \mathbf{O}_1,\ldots,\mathbf{O}_{T_r} O1,…,OTr,每个子块大小为 B r × d B_r\times d Br×d
- 中间归一化因子向量 ℓ \ell ℓ 与中间最大值向量 m m m 分别划分为 T r T_r Tr 个子块 ℓ 1 , … , ℓ T r \ell_1,\ldots,\ell_{T_r} ℓ1,…,ℓTr 以及 m 1 , … , m T r m_1,\ldots,m_{T_r} m1,…,mTr,每块大小为 B r B_r Br
步骤 5-15(分块计算阶段)
- 步骤 5:外层循环(步骤 5-15),遍历所有的键值块( K j , V j \mathbf{K}_j,\mathbf{V}_j Kj,Vj), j = 1 , … , T c j=1,\ldots,T_c j=1,…,Tc
- 步骤 6:加载对应的键值块 K j , V j \mathbf{K}_j,\mathbf{V}_j Kj,Vj 到片上内存 SRAM 上
- 步骤 7:内层循环(步骤 7-14),遍历所有的查询块( Q i \mathbf{Q}_i Qi), i = 1 , … , T r i=1,\ldots,T_r i=1,…,Tr,每个查询块内部的具体计算过程如下:
- 步骤 8:从片外内存 HBM 上加载 Q i , O i , ℓ i , m i \mathbf{Q}_i,\mathbf{O}_i,\ell_i,m_i Qi,Oi,ℓi,mi 到片上内存 SRAM 上
- 步骤 9:在片上内存 SRAM 上计算 attention 分数矩阵: S i j = Q i K j T ∈ R B r × B c \mathbf{S}_{ij}=\mathbf{Q}_i\mathbf{K}_j^T\in \mathbb{R}^{B_r\times B_c} Sij=QiKjT∈RBr×Bc
- 步骤 10:在片上内存上对注意力分数进行分块式 softmax:
- 计算当前子块最大值(用于数值稳定): m ~ i j = r o w m a x ( S i j ) ∈ R B r \tilde{m}_{ij}=\mathrm{rowmax}(\mathbf{S}_{ij}) \in \mathbb{R}^{B_r} m~ij=rowmax(Sij)∈RBr
- 计算指数化矩阵: P ~ i j = e x p ( S i j − m ~ i j ) ∈ R B r × B c \tilde{\mathbf{P}}_{ij} = \mathrm{exp}(\mathbf{S}_{ij}-\tilde{m}_{ij})\in \mathbb{R}^{B_r\times B_c} P~ij=exp(Sij−m~ij)∈RBr×Bc
- 计算局部归一化因子(每行求和): ℓ ~ i j = r o w s u m ( P ~ i j ) ∈ R B r \tilde{\ell}_{ij}=\mathrm{rowsum}(\tilde{\mathbf{P}}_{ij})\in \mathbb{R}^{B_r} ℓ~ij=rowsum(P~ij)∈RBr
- 步骤 11:更新全局归一化因子:
- 新的最大值: m i n e w = max ( m i , m ~ i j ) ∈ R B r m_i^{\mathrm{new}}=\max(m_i,\tilde{m}_{ij})\in \mathbb{R}^{B_r} minew=max(mi,m~ij)∈RBr
- 新的归一化向量: ℓ i n e w = e m i − m i n e w ℓ i + e m ~ i j − m i n e w ℓ ~ i j ∈ R B r \ell_i^{\mathrm{new}}=e^{m_i-m_i^{\mathrm{new}}}\ell_i+e^{\tilde{m}_{ij}-m_i^{new}}\tilde{\ell}_{ij}\in \mathbb{R}^{B_r} ℓinew=emi−minewℓi+em~ij−minewℓ~ij∈RBr
- 步骤 12:计算注意力输出 O i \mathbf{O}_i Oi 并写回 HBM: O i ← d i a g ( ℓ i n e w ) − 1 ( d i a g ( ℓ i ) e m i − m i n e w O i + e m ~ i j − m i n e w P ~ i j V j ) \mathbf{O}_i\leftarrow \mathrm{diag}(\ell_i^{\mathrm{new}})^{-1}(\mathrm{diag}(\ell_i)e^{m_i-m_i^{\mathrm{new}}}\mathbf{O}_i+e^{\tilde{m}_{ij}-m_i^{\mathrm{new}}}\tilde{\mathbf{P}}_{ij}\mathbf{V}_j) Oi←diag(ℓinew)−1(diag(ℓi)emi−minewOi+em~ij−minewP~ijVj)
- 步骤 13:更新全局向量 ℓ i ← ℓ i n e w , m i ← m i n e w \ell_i \leftarrow \ell_i^{\mathrm{new}},m_i\leftarrow m_i^{\mathrm{new}} ℓi←ℓinew,mi←minew 写回 HBM
- 步骤 14:内层循环结束
- 步骤 15:外层循环结束
步骤 16(输出结果阶段)
- 步骤 16:将输出计算的 attention 结果 O \mathbf{O} O 返回
Note:列块大小为 ⌈ M 4 d ⌉ \lceil \frac{M}{4d} \rceil ⌈4dM⌉ 原因在于要存储 Q , K , V , O \mathrm{Q,K,V,O} Q,K,V,O 四个分块矩阵;行块大小为 min ( ⌈ M 4 d ⌉ , d ) \min \left(\lceil \frac{M}{4d}\rceil,d\right) min(⌈4dM⌉,d) 原因在于为了控制 Q \mathbf{Q} Q 矩阵分块最大就是一个 d × d d\times d d×d 的方阵,不要让 Q \mathrm{Q} Q 分块矩阵的行太大,否则可能使其在 SRAM 里生成的中间矩阵计算结果太大,而超出 SRAM 的大小。
其中步骤 12 需要专门来推导一下,为简洁起见,先不考虑 mask 和 dropout 操作:
O i ( j + 1 ) = P i , : j + 1 V : j + 1 = s o f t m a x ( S i , : j + 1 ) V : j + 1 = d i a g ( ℓ ( j + 1 ) ) − 1 ( exp ( [ S i , : j S i , j : j + 1 ] − m ( j + 1 ) ) ) [ V : j V j : j + 1 ] = d i a g ( ℓ ( j + 1 ) ) − 1 ( exp ( S i , : j − m ( j + 1 ) ) V : j + exp ( S i , j : j + 1 − m ( j + 1 ) ) V j : j + 1 ) = d i a g ( ℓ ( j + 1 ) ) − 1 ( e − m ( j + 1 ) exp ( S i , : j ) V : j + e − m ( j + 1 ) exp ( S i , j : j + 1 ) V j : j + 1 ) = d i a g ( ℓ ( j + 1 ) ) − 1 ( d i a g ( ℓ ( j ) ) e m ( j ) − m ( j + 1 ) d i a g ( ℓ ( j ) ) − 1 exp ( S i , : j − m ( j ) ) V : j + e − m ( j + 1 ) exp ( S i , j : j + 1 ) V j : j + 1 ) = d i a g ( ℓ ( j + 1 ) ) − 1 ( d i a g ( ℓ j ) e m ( j ) − m ( j + 1 ) P i , : j V : j + e − m ( j + 1 ) exp ( S i , j : j + 1 ) V j : j + 1 ) = d i a g ( ℓ ( j + 1 ) ) − 1 ( d i a g ( ℓ ( j ) ) e m ( j ) − m ( j + 1 ) O i ( j ) + e m ~ − m ( j + 1 ) exp ( S i , j : j + 1 − m ~ ) V j : j + 1 ) = d i a g ( ℓ ( j + 1 ) ) − 1 ( d i a g ( ℓ ( j ) ) e m ( j ) − m ( j + 1 ) O i ( j ) + e m ~ − m ( j + 1 ) P i , j : j + 1 V j : j + 1 ) \begin{aligned} \mathbf{O}_i^{(j+1)} &= \mathbf{P}_{i,:j+1}\mathbf{V}_{:j+1} = \mathrm{softmax}\left(\mathbf{S}_{i,:j+1}\right)\mathbf{V}_{:j+1} \\ &= \mathrm{diag}\left(\ell^{(j+1)}\right)^{-1} \left(\exp\left([\mathbf{S}_{i,:j} \quad \mathbf{S}_{i,j:j+1}]-m^{(j+1)}\right)\right) \left[\mathbf{V}_{:j}\atop\mathbf{V}_{j:j+1}\right] \\ &= \mathrm{diag}\left(\ell^{(j+1)}\right)^{-1} \left(\exp\left(\mathbf{S}_{i,:j}-m^{(j+1)}\right)\mathbf{V}_{:j} + \exp\left(\mathbf{S}_{i,j:j+1}-m^{(j+1)}\right)\mathbf{V}_{j:j+1} \right) \\ &= \mathrm{diag}\left(\ell^{(j+1)}\right)^{-1} \left( e^{-m^{(j+1)}} \exp (\mathbf{S}_{i,:j})\mathbf{V}_{:j} + e^{-m^{(j+1)}} \exp (\mathbf{S}_{i,j:j+1}) \mathbf{V}_{j:j+1} \right) \\ &= \mathrm{diag}\left(\ell^{(j+1)}\right)^{-1} \left( \mathrm{diag}\left(\ell^{(j)}\right) e^{m^{(j)}-m^{(j+1)}} \mathrm{diag}\left(\ell^{(j)}\right)^{-1} \exp \left(\mathbf{S}_{i,:j} - m^{(j)} \right) \mathbf{V}_{:j} + e^{-m^{(j+1)}} \exp (\mathbf{S}_{i,j:j+1}) \mathbf{V}_{j:j+1} \right) \\ &= \mathrm{diag}\left(\ell^{(j+1)}\right)^{-1} \left( \mathrm{diag} \left(\ell^{j}\right) e^{m^{(j)}-m^{(j+1)}} \mathbf{P}_{i,:j} \mathbf{V}_{:j} + e^{-m^{(j+1)}} \exp (\mathbf{S}_{i,j:j+1}) \mathbf{V}_{j:j+1} \right) \\ &= \mathrm{diag}\left(\ell^{(j+1)}\right)^{-1} \left( \mathrm{diag}\left( \ell^{(j)} \right) e^{m^{(j)}-m^{(j+1)}} \mathbf{O}_i^{(j)} + e^{\tilde{m}-m^{(j+1)}} \exp \left( \mathbf{S}_{i,j:j+1} - \tilde{m} \right)\mathbf{V}_{j:j+1} \right) \\ &= \mathrm{diag}\left(\ell^{(j+1)}\right)^{-1} \left( \mathrm{diag}\left( \ell^{(j)} \right) e^{m^{(j)}-m^{(j+1)}} \mathbf{O}_i^{(j)} + e^{\tilde{m}-m^{(j+1)}} \mathbf{P}_{i,j:j+1} \mathbf{V}_{j:j+1} \right) \end{aligned} Oi(j+1)=Pi,:j+1V:j+1=softmax(Si,:j+1)V:j+1=diag(ℓ(j+1))−1(exp([Si,:jSi,j:j+1]−m(j+1)))[Vj:j+1V:j]=diag(ℓ(j+1))−1(exp(Si,:j−m(j+1))V:j+exp(Si,j:j+1−m(j+1))Vj:j+1)=diag(ℓ(j+1))−1(e−m(j+1)exp(Si,:j)V:j+e−m(j+1)exp(Si,j:j+1)Vj:j+1)=diag(ℓ(j+1))−1(diag(ℓ(j))em(j)−m(j+1)diag(ℓ(j))−1exp(Si,:j−m(j))V:j+e−m(j+1)exp(Si,j:j+1)Vj:j+1)=diag(ℓ(j+1))−1(diag(ℓj)em(j)−m(j+1)Pi,:jV:j+e−m(j+1)exp(Si,j:j+1)Vj:j+1)=diag(ℓ(j+1))−1(diag(ℓ(j))em(j)−m(j+1)Oi(j)+em~−m(j+1)exp(Si,j:j+1−m~)Vj:j+1)=diag(ℓ(j+1))−1(diag(ℓ(j))em(j)−m(j+1)Oi(j)+em~−m(j+1)Pi,j:j+1Vj:j+1)
Note:对角矩阵的逆矩阵对于原始矩阵对角元素的倒数构成的矩阵( d i a g ( ℓ i n e w ) − 1 \mathrm{diag}(\ell_i^{\mathrm{new}})^{-1} diag(ℓinew)−1),乘以后面的 O \mathrm{O} O 值( ( d i a g ( ℓ i ) e m i − m i n e w O i + e m ~ i j − m i n e w P ~ i j V j ) (\mathrm{diag}(\ell_i)e^{m_i-m_i^{\mathrm{new}}}\mathbf{O}_i+e^{\tilde{m}_{ij}-m_i^{\mathrm{new}}}\tilde{\mathbf{P}}_{ij}\mathbf{V}_j) (diag(ℓi)emi−minewOi+em~ij−minewP~ijVj)),相当于给每一行除以每一行的求和值。这里先给原来的 O i \mathrm{O}_i Oi 先乘以原来的 d i a g ( ℓ i ) \mathrm{diag}(\ell_i) diag(ℓi) 还原回去,再加上新的 O \mathrm{O} O 值,再除以新的求和值,从而更新 O \mathrm{O} O 值到 HBM
其核心思想仍是使用分块的计算更新迭代以得到全局的结果
上述算法并没有增加额外的计算,只是将大的操作拆分成多个分块逐个计算,因此其算法复杂度仍为 O ( N 2 d ) O(N^2d) O(N2d),另外由于增加了变量 ℓ , m \ell,m ℓ,m,因此空间复杂度增加 O ( N ) O(N) O(N)
FlashAttention2 的大致思想和 FlashAttention1 类似,增加了一些工程上的优化,比如减少了非矩阵乘法的计算,将 Q \mathrm{Q} Q 改为外循环, K , V \mathrm{K,V} K,V 改为内循环,更进一步减少了对 HBM 的读写,增加了并行度
此外 FlashAttention2 进一步利用分块计算的优势,如果判断一个分块是原始矩阵的上三角部分,也就是它是被 mask 掉的部分,那么就不需要进行 attention的计算了,从而更进一步减少了计算量
OK,以上就是关于 FlashAttention 原理讲解的全部内容了
结语
这篇文章我们主要讲解了 Flash Attention 的原理,与上篇文章 [从Online Softmax到FlashAttention] 中详细的公式推导不同,这里我们主要是通过图示的方法来一步步看 FlashAttention 是怎么做的
FlashAttention 的核心思路是将 Q , K , V \mathbf{Q},\mathbf{K},\mathbf{V} Q,K,V 按块分割到片上高速存储(SRAM)中,逐块计算并累加结果,从而在有限的片上内存下高效完成大规模的注意力运算
首先从矩阵分块的角度出发来看 ( Q K T ) V (QK^T)V (QKT)V 是怎么做的,接着谈到 softmax 的分块计算,通过 safe softmax 的分块将 softmax 也融入到矩阵分块中计算,最后在前面基础上我们分析了 FlashAttention 的伪代码实现
视频和文章的讲解都非常的不错,大家感兴趣的可以多看看🤗
参考
- Flash Attention 为什么那么快?原理讲解
- LLM(17):从 FlashAttention 到 PagedAttention, 如何进一步优化 Attention 性能
- 《FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness》
- 四. TensorRT模型部署优化-Roofline model
- 3.3.cuda运行时API-内存的学习,pinnedmemory,内存效率问题
- 从Online Softmax到FlashAttention
- 一文详解Softmax函数
相关文章:
Flash Attention原理讲解
目录 前言0. 简述1. self-attention2. roofline model3. 矩阵分块4. softmax分块5. FlashAttention结语参考 前言 看了几个视频和几篇文章学习了下 Flash Attention,记录下个人学习笔记,仅供自己参考😄 refer1:Flash Attention 为…...
python二级复习(1)
临近计算机二级考试了,开始python的复习 python语言基础: 1.用缩进表示代码块:一般用四个空格或者一个tab 2.代码的注释方法: 单行注释用“#”表示注释开始;多行注释是用三个英文的单引号“‘’”或双引号““”"”作为注释的开始和结束符号。 03. 标识符命…...
基于cat1的贵重物品的状态和位置小型监控系统特色解析
一 项目需求 团队研发出来一款搭载多传感器的无线cat1定位和状态监控的设备。该设备主要面对的贵重物品运输过程中的状态监控,比如,是否被打开过,有没有激烈碰撞,位置信息等。主要应用场景是医疗,安防等贵重物品的状态…...
Python 魔法方法介绍
在 Python 中,魔法方法(Magic Methods)是一类特殊的内置方法,它们通常以双下划线开头和结尾(例如 __init__、__str__、__add__ 等)。这些方法通常用于实现特定的语法或操作符行为,或者用于定义对象的行为。它们是 Python 面向对象编程中的一个重要特性,可以让类的行为更…...
golang time包和日期函数
1.简介 在程序中日期和时间是我们经常会用到的,在go中time包提供了时间的显示和测量函数。 2.获取当前时间 通过time.Now()函数获取当前时间对象,然后获取时间对象的年月日时分秒等值。 now : time.Now()fmt.Printf("now%v type%T\n", now…...
第7章 站在对象模型的尖端2: 异常处理
1.异常处理(Exception Handling) C的异常处理由三个主要组成部分构成:throw表达式、catch子句和try块。当异常被抛出时,程序控制权会转移,并且会沿着调用堆栈回溯,直到找到匹配的catch子句。在此过程中,局部对象的析构…...
不同路径——2
给定一个 m x n 的整数数组 grid。一个机器人初始位于 左上角(即 grid[0][0])。机器人尝试移动到 右下角(即 grid[m - 1][n - 1])。机器人每次只能向下或者向右移动一步。 网格中的障碍物和空位置分别用 1 和 0 来表示。机器人的…...
电子招采软件系统,如何实现10年可追溯审计
一、在当前经济环境下,中小企业面临着巨大的生存压力,传统产业的数字化转型迫在眉睫。AI技术为企业的低成本高效发展提供了新机会,混合办公成为新常态,数据安全法的深入落实则进一步推动企业重视数据安全。区块链存证技术凭借独特…...
TS常见内置映射类型的实现及应用场景
以下是 TypeScript 在前端项目中 常用的映射类型(Mapped Types),结合具体场景和代码示例,帮助开发者高效处理复杂类型: 一、基础映射类型 1. Partial<T> 作用:将对象类型 T 的所有属性变为可选。 实…...
本地部署Deep Seek-R1,搭建个人知识库——笔记
目录 一、本地部署 DeepSeek - R1 1:安装Ollama 2:部署DeepSeek - R1模型 3:安装Cherry Studio 二、构建私有知识库 一、本地部署 DeepSeek - R1 1:安装Ollama 1.打开Ollama下载安装 未科学上网,I 先打开迅雷再下…...
东芝2323AMW复印机安装纸盒单元后如何添加配件选项
如何添加请看下面图示: 找到设备和打印机里找到打印机图标,右键打印机属性,找到配置,添加相应配置功能;打印机属性,加上双面或者2纸盒配件选项; Word打印时,打印机名称,后…...
EasyExcel动态拆分非固定列Excel表格
使用EasyExcel动态拆分非固定列Excel表格 在Excel数据解析场景中,动态列结构拆分是典型挑战(如供应链系统中不同品类的属性字段差异较大)。传统基于POJO映射的方案无法应对列数量不固定的场景。本方案采用EasyExcel的动态模型解析和Map数据…...
开源WAF雷池本地化部署与远程查看网站安全防护的详细操作指南
文章目录 前言1.关于SafeLine2.安装Docker3.本地部署SafeLine4.使用SafeLine5.cpolar内网穿透工具安装6.创建远程连接公网地址7.固定Uptime Kuma公网地址 前言 各位建站小能手们,无论是想搭建个人博客、企业官网还是各种应用平台来推广自己的内容或产品,…...
由一个话题进入DFMEA(设计失效模式及影响分析)
前言 最近看到了知乎的一个话题“为啥撞车后总是看到雨刮器在摆动?”,联想到产品设计中的一些功能安全设计,也借此机会学习DFMEA,讨论一下我个人对于DFMEA的理解。 有纰漏请指出,转载请说明。 学习交流请发邮件 128…...
Redisson 实现分布式锁源码浅析
大家好,我是此林。 今天来分享Redisson分布式锁源码。还是一样,我们用 问题驱动 的方式展开讲述。 1. redis 中如何使用 lua 脚本? Redis内置了lua解释器,lua脚本有两个好处: 1. 减少多次Redis命令的网络传输开销。…...
机试准备第17天
今天进入图论的学习。图论只考察初试学过的算法,一般都是模版题。常见考点有图相关的数据结构——邻接表法,图的遍历 BFS DFS 并查集,单源最短路径迪杰斯特拉。图由顶点和边构成,度用来说明该顶点邻接边的数量情况。权值说明了边的…...
ABAP语言的动态编程(4) - 综合案例:管理费用明细表
本篇来实现一个综合案例:管理费用明细表。报表在实际项目中,也有一定的参考意义,一方面展示类似的报表,比如管理费用、研发费用等费用的明细,使用业务比较习惯的展示格式;另一方面正好综合运用前面学习的动…...
不像人做的题————十四届蓝桥杯省赛真题解析(上)A,B,C,D题解析
题目A:日期统计 思路分析: 本题的题目比较繁琐,我们采用暴力加DFS剪枝的方式去做,我们在DFS中按照8位日期的每一个位的要求进行初步剪枝找出所有的八位子串,但是还是会存在19月的情况,为此还需要在CHECK函数…...
R语言零基础系列教程-01-R语言初识与学习路线
代码、讲义、软件回复【R语言01】获取。 R语言初识 R是一个开放的统计编程环境,是一门用于统计计算和作图的语言。“一切皆是对象”,数据、函数、运算符、环境等等都是对象。易学,代码像伪代码一样简洁,可读性高强大的统计和可视…...
即时通讯平台测试报告
1.项目概述 项目名称:即时通讯平台 版本号:V1.0.0 测试周期:2025年2月25日--2025年3月15日 测试目标:验证核心功能(登录、注册、消息收发、用户管理、群组功能等)的稳定性和性能指标。 2. 测试范围 功…...
蓝桥杯单片机内存爆了怎么办
蓝桥杯单片机内存爆了怎么办 文章目录 蓝桥杯单片机内存爆了怎么办一、参考文章二、内存区3、keil中的体现4、分配原则5、使用示例 一、参考文章 文章1 文章2 文章3 文章4 二、内存区 1 KB(千字节) 1024 B(字节) B代表Byte,1Byte8bit,一个字节8位 …...
一周热点:微软攻克语音输入、文本输出难题-Phi-4-multimodal
微软Phi-4-multimodal模型是人工智能领域的一个重要进展,它标志着微软在多模态人工智能技术上的突破。以下是对该模型的详细解释: 模型概述 微软Phi-4-multimodal是一个能够同时处理文本、图像和语音的多模态大型语言模型。它通过创新的架构和训练方法,实现了在不同模态之间…...
量化交易学习笔记02:双均线策略
双均线策略示例 个股:中国平安 回测日期:2022-5-1至2023-5-1 短均线:5天 长无线:10天 代码: def initialize(context):# 初始化此策略# 设置我们要操作的股票池, 这里我们只操作一支股票# """标的&qu…...
【WRF-Urban】使用 CGLC-MODIS-LCZ_100m 数据集运行 WRF 时的城市参数化问题
在 WRF 中,LCZ 通过 URBPARM_LCZ.TBL 进行配置,但如果 FRC_URB2D 变量缺失,WRF 会回退到默认的 URBPARM.TBL。 主要问题概述 WRF-Model-cglc-modis-lcz_100m dataset " WARNING, THE URBAN FRACTION WILL BE READ FROM URBPARM.TBL USING DEFAULT URBAN MORPHOLOGY&q…...
Selenium 自动化测试学习总结
大概了解一下即可,现在主要用的自动化工具是 playWright,它可以录制操作。 selenium是老款自动化测试工具,仍有很多可取之处。 安装: pip install selenium即可。然后下载浏览器的驱动包,注意不是浏览器!…...
开源通义万相本地部署方案,文生视频、图生视频、视频生成大模型,支持消费级显卡!
开源通义万相本地部署方案,文生视频、图生视频、视频生成大模型,支持消费级显卡! 万相2.1开源 近日,大模型万相2.1(Wan)重磅开源,此次开源采用Apache2.0协议,14B和1.3B两个参数规格…...
Suno的对手Luno:AI音乐开发「上传参考音频 - 方式一:通过二进制流的方式」 —— 「Luno Api系列|AI音乐API」第11篇
导读 今天来看下Luno Api的上传参考音频 - 方式一:通过二进制流的方式。 参考文件,主要是用于在创作的过程中,希望AI参考这个音乐的曲风和声音来进行创作,那么可以通过上传参考音乐来进行实现。 申请和使用 「已经有API的&…...
微信小程序刷题逻辑实现:技术揭秘与实践分享
页面展示: 概述 在当今数字化学习的浪潮中,微信小程序以其便捷性和实用性,成为了众多学习者刷题备考的得力工具。今天,我们就来深入剖析一个微信小程序刷题功能的实现逻辑,从代码层面揭开其神秘面纱。 小程序界面布局…...
巴耶赫利专业俄语外贸网站建设
巴耶赫利是专业俄语外贸网站建设与俄语搜索引擎Yandex SEO优化服务商。巴耶赫利致力于帮助中国品牌出海俄罗斯,打开俄罗斯市场,提升品牌在俄罗斯的知名度和美誉度。 以下是对巴耶赫利相关服务的详细介绍: 一、巴耶赫利专业俄语外贸网站建设…...
每日Attention学习25——Multi-Scale Attention Fusion
模块出处 [TCSVT 24] [link] [code] DSNet: A Novel Way to Use Atrous Convolutions in Semantic Segmentation 模块名称 Multi-Scale Attention Fusion (MSAF) 模块作用 双级特征融合 模块结构 模块思想 MSAF的主要思想是让网络根据损失学习特征权重,允许模型…...
前端学习记录:解决路由缓存问题
问题描述:响应路由参数的变化,使用带有参数的路由时需要注意的是,当用户从 /users/johnoy 导航到 /users/jolyne 时,相同的组件实例将会被重复使用。因为两个路由都渲染同个组件,比起销毁再创建,复用则显得…...
VSTO(C#)Excel开发10:启动和卸载顺序 事件处理 监视变化
初级代码游戏的专栏介绍与文章目录-CSDN博客 我的github:codetoys,所有代码都将会位于ctfc库中。已经放入库中我会指出在库中的位置。 这些代码大部分以Linux为目标但部分代码是纯C的,可以在任何平台上使用。 源码指引:github源…...
代码随想录Day16
Day16 二叉树part06 LeetCode 530.二叉搜索树的最小绝对差 题目描述 给你一个二叉搜索树的根节点 root ,返回 树中任意两不同节点值之间的最小差值 。 差值是一个正数,其数值等于两值之差的绝对值。 示例 输入:root [4,2,6,1,3] 输出&…...
第15章:ConvNeXt图像分类实战:遥感场景分类【包含本地网页部署、迁移学习】
目录 1. ConvNeXt 模型 2. 遥感场景建筑识别 2.1 数据集 2.2 训练参数 2.3 训练结果 2.4 本地部署推理 3. 下载 1. ConvNeXt 模型 ConvNeXt是一种基于卷积神经网络(CNN)的现代架构,由Facebook AI Research (FAIR) 团队在2022年提出。…...
LinuX---Shell脚本创建和执行
概述: 它是一个命令行解释器,接收应用程序/用户命令,然后调用操作系统内核。 Shell还是一个功能强大的编程语言,易编写、易调试、灵活性强。 Linux提供的Shell解析器有 atguiguubuntu:~$ cat /etc/shells # /etc/shells: valid …...
django+vue3实现前后端大文件分片下载
效果: 大文件分片下载支持的功能: 展示目标文件信息提高下载速度:通过并发请求多个块,可以更有效地利用网络带宽断点续传:支持暂停后从已下载部分继续,无需重新开始错误恢复:单个块下载失败只…...
KY-038 声音传感器如何工作以及如何将其与 ESP32 连接
想为您的项目赋予声音感!然后跟着做,因为在这个项目中,我们将连接一个声音传感器,用它构建一些有趣的项目。我们使用的 KY-038 声音传感器使用电容式麦克风来检测声波,这为我们提供了稳定性和可靠性的完美平衡。因此,在本文中,我们决定将 KY-038 传感器与 ESP32 连接,并…...
深入剖析二分查找的延伸:在排序数组中查找元素的第一个和最后一个位置
深入剖析二分查找的延伸:在排序数组中查找元素的第一个和最后一个位置 引言 二分查找,作为算法界的“常青树”,以其高效性和简洁性备受青睐。然而,许多初学者仅限于使用它查找单个元素,而对其进阶应用知之甚少。今天…...
UE5中 Character、PlayerController、PlayerState、GameMode和GameState核心类之间的联动和分工·
1. GameMode 与 GameState 关系描述 GameMode:定义游戏规则和逻辑,控制游戏的开始、进行和结束。GameState:存储和同步全局游戏状态,如得分、时间、胜利条件等。 联动方式 GameMode初始化GameState:GameMode在游戏…...
使用Python获取并操作1688自定义API接口
在电子商务领域,1688作为国内领先的B2B平台,提供了丰富的API接口,允许开发者获取商品信息、店铺信息等。其中,custom接口允许开发者进行自定义操作,获取特定的数据。本文将详细介绍如何使用Python调用1688的custom接口…...
【AI】现代人工智能技术的应用与发展
引言 人工智能(AI)已经深入到我们生活的各个方面,涉及医疗、教育、交通、金融等众多领域。随着技术的不断发展,AI的应用和潜力也变得愈加广泛。本文将详细介绍人工智能的应用领域,探讨未来的发展趋势,并通…...
小程序渲染之谜:如何解决“加载中...”不消失的 Bug(glass-easel)
🎉 小程序渲染之谜:如何解决“加载中…”不消失的 Bug 🎉 引言 在小程序开发中,渲染问题总能让人抓狂。😫 这次,我遇到了一个奇怪的 bug:产品详情页的内容已经正常显示,但页面却一…...
C语言结构体全面解析 | 从入门到精通
📚 C语言结构体全面解析 | 从入门到精通 整理:算法练习生| 转载请注明出处 📑 目录 结构体的定义与使用结构体变量的参数传递结构体数组结构体指针typedef关键字结构体初始化 1️⃣ 结构体的定义与使用 为什么需要结构体? 当…...
Trae与Builder模式初体验
说明 下载的国际版:https://www.trae.ai/ 建议 要选新模型 效果 还是挺不错的,遇到问题反馈一下,AI就帮忙解决了,真是动动嘴(打打字就行了),做些小的原型效果或演示Demo很方便呀ÿ…...
麒麟服务器操作系统QT系列软件工具手册
QtCreator****功能介绍 QtCreator 概述 Qt Creator是跨平台的 Qt IDE, Qt Creator 是 Qt 被 [Nokia](https://baike.baidu.com/item/Nokia/264012" /t “_blank) 收购后推出的一款新的轻量级[集成开发环境](https://baike.baidu.com/item/集成开发环境/298524” /t “_…...
【HeadFirst系列之HeadFirstJava】第18天之深入理解原型模式:从问题到解决方案(含 Java 代码示例)
深入理解原型模式:从问题到解决方案(含 Java 代码示例) 在软件开发中,我们经常需要创建对象,而有些对象的创建成本较高或者结构较为复杂。如何在不破坏封装的前提下,高效地创建对象? 这正是**原…...
JetsonOrin源码安装部署PaddlePaddle
Jetson Orin 源码安装部署Paddle 部署环境 系统架构: Arm CUDA: 11.4 cmake: 3.18.0 python:3.8 注意环境中的版本问题,之前装onnxruntime的时候cmake被升级到了3.31.0,但是编译Paddle时会报错,因此特意降级回了官方推荐的3.18.0 具体环…...
入门到入土,Java学习 day20(多线程下)
void wait() 当前线程等待,直到被其他线程唤醒 void notify() 随机唤醒单个线程 void notifyAll() 唤醒所有线程 阻塞队列 在测试方法中创建带锁队列,然后在对象类中也创建队列但是不赋值,用构造方法将测试方法中的对象赋值 然后用put和t…...
【TCP】三次挥手,四次挥手详解--UDP和TCP协议详解
活动发起人小虚竹 想对你说: 这是一个以写作博客为目的的创作活动,旨在鼓励大学生博主们挖掘自己的创作潜能,展现自己的写作才华。如果你是一位热爱写作的、想要展现自己创作才华的小伙伴,那么,快来参加吧!…...
栈(LIFO)算法题
1.删除字符串中所有相邻的重复字符 注意,我们需要重复处理,而不是处理一次相邻的相同元素就结束了。对示例来说,如果只进行一次处理,结果为aaca,但是处理之后又出现了相邻的重复元素,我们还得继续处理&…...