在GPU上加速RWKV6模型的Linear Attention计算

精简版:经过一些profile发现flash-linear-attention中的rwkv6 linear attention算子的表现比RWKV-CUDA中的实现性能还要更好,然后也看到了继续优化triton版本kernel的线索。接着还分析了一下rwkv6 cuda kernel的几次开发迭代以此说明对于不懂cuda以及平时无法从擅长cuda的大佬身上取经的人比如我就完全放弃cuda了,可以深入学一下和使用triton,这已经完全足够了(除了会写之外还可以了解内部的MLIR相关的编译器知识,可以对GPU体系架构理解得更加深刻)。

0x0. 前言

本文主要讲一些看到的RWKV 6模型的Linear Attention模块推理加速方法,在这篇博客中暂不涉及对kernel的深入解析。首先,flash-linear-attention(https://github.com/sustcsonglin/flash-linear-attention )这个仓库旨在对各种线性Attention架构进行工程加速,例如RetNet,GLA,Manba,RWKV6(2024年4月引入)。它使用Triton来编写代码,并针对不同的线性Transformer架构使用不同的优化方式。例如对于RWKV 6就采用在时间维度进行kernel fuse的方式来加速。其次,RWKV-CUDA是RWKV系列模型迭代中针对Linear Attention模块的改进开发的自定义高性能cuda kernel(https://github.com/BlinkDL/RWKV-CUDA)。flash-rwkv(https://github.com/BBuf/flash-rwkv)仓库在RWKV-CUDA的最优性能算子的基础上进行了封装,提供了rwkv5_cuda_linear_attentionrwkv6_cuda_linear_attention两个接口方便在HuggingFace模型实现中直接加速推理的prefill阶段速度。

本篇文章主要会对比一下RWKV6 Linear Attention模块的naive实现(pure pytorch),RWKV-CUDA的RWKV6 Linear Attention cuda kernel实现(用flash-rwkv提供的接口进行测试),flash-linear-attention里的RWKV6 Linear Attention实现。来说明Triton已经成为目前LLM时代开发的一个趋势,小伙伴们确实可以学起来。目前我对Triton的了解也非常少而且很肤浅,后续也会持续学习和实践。

下面列举本文相关的资料,如果你想对RWKV 6这个架构有一些了解可以阅读后面三个链接,当然不阅读也不影响阅读本文:

  • https://github.com/sustcsonglin/flash-linear-attention
  • https://mp.weixin.qq.com/s/Vol_LeHVHDAwE1pWTHOl2Q
  • 梳理RWKV 4,5(Eagle),6(Finch)架构的区别以及个人理解和建议
  • RWKV 模型保姆级微调教程

另外,本文使用了PyTorch Profiler TensorBoard 插件来做程序的性能分析,感兴趣的小伙伴可以在系统调优助手,PyTorch Profiler TensorBoard 插件教程 获取到详细的教程。

0x1. 瓶颈是什么

RWKV6 推理 Prefill 阶段的性能瓶颈就在于RWKV6模型代码中的rwkv6_linear_attention_cpu函数:https://huggingface.co/RWKV/rwkv-6-world-1b6/blob/main/modeling_rwkv6.py#L54-L104

def rwkv6_linear_attention(training,receptance,key,value,time_decay,time_first,state,
):no_cuda = any(t.device.type != "cuda" for t in [time_decay, time_first, receptance, key, value])# Launching the CUDA kernel for just one token will actually be slower (there is no for loop in the CPU version# in this case).one_token = key.size(1) == 1if no_cuda or one_token:return rwkv6_linear_attention_cpu(receptance, key, value, time_decay, time_first, state)else:...

这里的判断是如果是decode阶段(对比prefill阶段)或者非GPU模式执行代码,就使用rwkv6_linear_attention_cpu这个算子,否则就使用优化后的实现比如使用这里的cuda kernel(https://github.com/BlinkDL/RWKV-CUDA/tree/main/wkv6)编译出的CUDA Kernel。flash-linear-attention库的目的是使用Triton来加速rwkv6_linear_attention_cpu这个naive的实现。这个naive实现的代码如下:

def hf_rwkv6_linear_attention_cpu(receptance, key, value, time_decay, time_first, state):# For CPU fallback. Will be slower and probably take more memory than the custom CUDA kernel if not executed# within a torch.no_grad.batch, seq_length, _ = receptance.shapenum_heads, head_size = time_first.shapekey = key.float().view(batch, seq_length, num_heads, head_size).transpose(1, 2).transpose(-2, -1) # b, t, h, n -> b, h, t, n -> b, h, n, tvalue = value.float().view(batch, seq_length, num_heads, head_size).transpose(1, 2) # b, t, h, n -> b, h, t, nreceptance = receptance.float().view(batch, seq_length, num_heads, head_size).transpose(1, 2) # b, t, h, n -> b, h, t, ntime_decay = torch.exp(-torch.exp(time_decay.float())).view(batch, seq_length, num_heads, head_size).permute(0, 2, 3, 1) # b, t, h, n -> b, h, n, ttime_first = time_first.float().reshape(-1, 1, 1).reshape(num_heads, -1, 1) # h, n -> h * n, 1, 1 -> h, n, 1out = torch.zeros_like(key).reshape(batch, seq_length, num_heads, head_size)for current_index in range(seq_length):current_receptance = receptance[:, :, current_index:current_index+1, :]current_key = key[:, :, :, current_index:current_index+1]current_value = value[:, :, current_index:current_index+1, :]current_time_decay = time_decay[:, :, :, current_index:current_index+1]attention_output = current_key @ current_valueout[:, current_index] = (current_receptance @ (time_first * attention_output + state)).squeeze(2)with torch.no_grad():# attention_output.shape: [b, h, n, 1] x [b, h, 1, n] -> [b, h, n, n]# current_time_decay * state: [b, h, n, 1] * [b, h, n, n] ->[b, h, n, n]# state.shape: [b, h, n, n]state = attention_output + current_time_decay * state return out, state

这样看代码可能会有点懵,可以看下一节的完整demo测试代码。

0x2. Profile代码编写

上一节明确了,我们需要加速RWKV模型中rwkv6_linear_attention_cpu的计算,https://github.com/sustcsonglin/flash-linear-attention 这个库在2024年4月份支持了RWKV6模型,它加速RWKV 6 Linear Attention计算的核心api有两个,fused_recurrent_rwkv6chunk_rwkv6。现在直接写出profile的代码(https://github.com/BBuf/flash-rwkv/blob/main/profile/profile_rwkv6_linear_attention.py)来对naive的实现,RWKV官方提供的cuda kernel以及fused_recurrent_rwkv6chunk_rwkv6进行性能分析。

import sys
import torch
from fla.ops.rwkv6.chunk import chunk_rwkv6
from fla.ops.rwkv6.recurrent_fuse import fused_recurrent_rwkv6
from flash_rwkv import rwkv6_cuda_linear_attentiondef hf_rwkv6_linear_attention_cpu(receptance, key, value, time_decay, time_first, state):# For CPU fallback. Will be slower and probably take more memory than the custom CUDA kernel if not executed# within a torch.no_grad.batch, seq_length, _ = receptance.shapenum_heads, head_size = time_first.shapekey = key.float().view(batch, seq_length, num_heads, head_size).transpose(1, 2).transpose(-2, -1)value = value.float().view(batch, seq_length, num_heads, head_size).transpose(1, 2)receptance = receptance.float().view(batch, seq_length, num_heads, head_size).transpose(1, 2)time_decay = torch.exp(-torch.exp(time_decay.float())).view(batch, seq_length, num_heads, head_size).permute(0, 2, 3, 1)time_first = time_first.float().reshape(-1, 1, 1).reshape(num_heads, -1, 1)out = torch.zeros_like(key).reshape(batch, seq_length, num_heads, head_size)for current_index in range(seq_length):current_receptance = receptance[:, :, current_index:current_index+1, :]current_key = key[:, :, :, current_index:current_index+1]current_value = value[:, :, current_index:current_index+1, :]current_time_decay = time_decay[:, :, :, current_index:current_index+1]attention_output = current_key @ current_valueout[:, current_index] = (current_receptance @ (time_first * attention_output + state)).squeeze(2)with torch.no_grad():state = attention_output + current_time_decay * statereturn out, stateif __name__ == "__main__":mode = sys.argv[1]B = 1H = 32L = 54D = 64HIDDEN_SIZE = H * Ddtype = torch.float32if mode == 'hf':profile_path = '/bbuf/rwkv_profile_result/hf/'elif mode == 'recurrent':profile_path = '/bbuf/rwkv_profile_result/recurrent/'elif mode == 'chunk':profile_path = '/bbuf/rwkv_profile_result/chunk/'elif mode == 'cuda':profile_path = '/bbuf/rwkv_profile_result/cuda'else:raise NotImplementedErrorwith torch.profiler.profile(activities=[torch.profiler.ProfilerActivity.CPU,torch.profiler.ProfilerActivity.CUDA],schedule=torch.profiler.schedule(wait=1,warmup=1,active=1),on_trace_ready=torch.profiler.tensorboard_trace_handler(profile_path, worker_name='worker0'),record_shapes=True,profile_memory=True,  # This will take 1 to 2 minutes. Setting it to False could greatly speedup.with_stack=True) as p:for i in range(10):q = (torch.randn(B, L, HIDDEN_SIZE).cuda().to(torch.float16)).requires_grad_(True)k = (torch.randn(B, L, HIDDEN_SIZE).cuda().to(torch.float16)).requires_grad_(True)v = torch.randn(B, L, HIDDEN_SIZE).cuda().to(torch.float16).requires_grad_(True)w = torch.nn.functional.logsigmoid(torch.randn(B, L, HIDDEN_SIZE)).cuda().to(torch.float32).requires_grad_(True)u = (torch.randn(H, D).cuda().to(torch.float16)).requires_grad_(True)state = (torch.randn(B, H, D, D).cuda().to(torch.float32)).requires_grad_(True)if mode == 'hf':o1, state1 = hf_rwkv6_linear_attention_cpu(q, k, v, w, u, state)elif mode =='cuda':o2, state2 = rwkv6_cuda_linear_attention(q, k, v, w, u.flatten(), state)else:batch, seq_length, _ = q.shapenum_heads, head_size = u.shapek = k.float().view(batch, seq_length, num_heads, head_size).transpose(1, 2) # B, T, H, K -> B, H, T, Kv = v.float().view(batch, seq_length, num_heads, head_size).transpose(1, 2) # B, T, H, K - > B, H, T, Vq = q.float().view(batch, seq_length, num_heads, head_size).transpose(1, 2) # B, H, T, Kw = -torch.exp(w.float()).view(batch, seq_length, num_heads, head_size).permute(0, 2, 1, 3) # B, T, H, K -> B, H, T, Ku = u.float().reshape(num_heads, head_size) # H, Kif mode == 'recurrent':o3, state3 = fused_recurrent_rwkv6(q, k, v, w, u, initial_state=state, scale=1.0, output_final_state=True)elif mode == 'chunk':o4, state4 = chunk_rwkv6(q, k, v, w, u, initial_state=state, scale=1.0, output_final_state=True)p.step()

这段代码就是要分别profile hf_rwkv6_linear_attention_cpurwkv6_cuda_linear_attentionfused_recurrent_rwkv6chunk_rwkv6这三个api看一下它们的性能表现以及GPU kernel的详细使用情况。但这段代码中有一些需要说明的地方:

  • hf_rwkv6_linear_attention_cpu这个api接收的输入Tensor形状和fla包提供的两个加速api的输入Tensor形状不一样,所以在对hf_rwkv6_linear_attention_cpu设定输入之后需要经过一些维度重排操作才能给fla包的两个api使用。
  • 对于time_decay来说,hf_rwkv6_linear_attention_cpu在计算时做了两次exp,而fused_recurrent_rwkv6chunk_rwkv6的api内部会做一次exp,所以输入给fused_recurrent_rwkv6chunk_rwkv6的time_decay只需要做内层的-exp操作就足够了。
  • 对于输出来说,fused_recurrent_rwkv6chunk_rwkv6的结果需要转置一下才能得到和hf_rwkv6_linear_attention_cpu一样的计算结果,state不需要做额外操作,直接就可以对应上。
  • 注意api的调用方式,例如chunk_rwkv6(q, k, v, w, u, initial_state=state, scale=1.0, output_final_state=True)里面的kwargs是缺一不可的。

接下来就可以执行这个profile脚本分别得到这三个api的profile结果了。我在一张NVIDIA A800-SXM4-80GB上进行了profile,结果上传到了 https://github.com/BBuf/flash-rwkv/tree/main/profile/rwkv_profile_result ,你可以通过 tensorboard --logdir=./rwkv_profile_result/recurrent/ --bind_all 这样的命令来可视化结果,并在本地的浏览器中打开 http://localhost:6006/#pytorch_profiler 网址来查看详细的结果。

0x3. Profile结果分析

0x3.1 hf_rwkv6_linear_attention_cpu 函数profile结果

在这里插入图片描述

使用hf_rwkv6_linear_attention_cpu函数进行计算时Kernel部分花了1105us,算子总的时间花了21.5ms,然后它的kernel分布为:

在这里插入图片描述我们可以发现在kernel里面只有gemv相关的矩阵乘调用,并且elementwise算子占比非常大已经接近40%。

0x3.2 rwkv6_cuda_linear_attention API profile结果

在这里插入图片描述
kernel的执行时间为73us,算子执行的总时间只花了4.5ms,相比于naive的实现(21.5)速度有大幅提升。观察GPU kernel执行情况:

在这里插入图片描述
现在rwkv6_cuda_linear_attention中的核心kernel: kernel_forward执行时间为101us。并且现在这个版本只有上面截图的2个kernel有耗时,剩下的2个elementwise的kernel耗时只有2us。在这里插入图片描述
由此可见,使用cuda来编写和优化上面的rwkv6_cuda_linear_attention api可以获得大幅度的性能提升。

0x3.3 fused_recurrent_rwkv6 API profile结果

在这里插入图片描述

现在Kernel执行总时间只有125us,算子总的时间花了5.26ms,相比于naive的实现(21.5)速度有大幅提升,同时kernel的占比也明显更小,GPU kernel分布情况:

在这里插入图片描述
在GPU kernel的具体执行分布中,fused_recurrent_rwkv6_fwd_kernel已经是比例的最大的kernel了,而这个kernel的整体耗时非常低只花了64us,而在naive的实现中则存在数个耗时超过100us的elementwise kernel。目前的整体耗时和优化后的cuda kernel实现也是比较接近的。

0x3.4 chunk_rwkv6 API profile结果

在这里插入图片描述

在这里插入图片描述
chunk_rwkv6的情况和fused_recurrent_rwkv6类似,也是达到了不错的性能。

0x3.5 Profile结果总结

方法RWKV 6 Linear Attention端到端耗时(us)Kernel最大耗时(us)
hf_rwkv6_linear_attention_cpu21500432us
rwkv6_cuda_linear_attention4500101us
fused_recurrent_rwkv6526064us
chunk_rwkv6560249us

注:hf_rwkv6_linear_attention_cpu中有很多个耗时比较长的element-wise kernel,性能是最差的,这里只记录了耗时最长的那个element-wise kernel,已经足够说明问题。后续三种方案都通过kernel fuse让hf_rwkv6_linear_attention_cpu实现中的seq_length维度的遍历和众多gemv/elemetwise相关kernel最终fuse成1个或者2个kernel。chunk_rwkv6 api的计算分为2个kernel,耗时分别为27和22us,统计kernel最大耗时的时候进行了求和。

结论:手工优化的rwkv6_cuda_linear_attention在端到端的耗时方面目前是最快的,从上面的profile代码也可以看出来主要原因是因为它不需要对各个输入进行一系列的维度转换,而naive的实现和Triton的实现则必须做一堆维度转换来匹配api提供的计算功能。从Kernel最大耗时的角度看,triton实现的fused_recurrent_rwkv6和chunk_rwkv6 kernel本身的计算是比RWKV-CUDA的手工kernel更快的(虽然还不太清楚Triton实现的版本在编译中发生了什么,但真的找到了放弃cuda的理由,毕竟不是专业做这个东西的,而Triton大家都可以写),后续应该会考虑在Triton kernel的基础上继续做优化以及训练性能验证。

0x4. flash-rwkv库中的rwkv5_cuda_linear_attention开发历程

这里讲一下flash-rwkv库中的rwkv5_cuda_linear_attention这个api背后开发的迭代历程。时间回到2023年8月,ChatGPT的火爆让我也想参与到开源的大模型开发过程中,然后Peng Bo说可以参与到实现RWKV5 CUDA算子的事情。为了锻炼下CUDA就开始参与实现和优化RWKV5 CUDA,在这个过程中也有幸见识到了RWKV开源社区中 https://github.com/Blealtan 这位大佬的优化水平,同时也了解了Parallel Scan算法和实现。后续RWKV6的rwkv6_cuda_linear_attention仍然沿用了rwkv5的cuda kernel,只做了微量的坐标映射修改。

HuggingFace中RWKV5模型的Linear Attention Naive实现在 https://huggingface.co/RWKV/rwkv-5-world-1b5/blob/main/modeling_rwkv5.py#L62-L84 ,贴一下这段代码。

def rwkv5_linear_attention_cpu(receptance, key, value, time_decay, time_first, state):input_dtype = receptance.dtype# For CPU fallback. Will be slower and probably take more memory than the custom CUDA kernel if not executed# within a torch.no_grad.batch, seq_length, hidden_size = receptance.shapenum_heads, head_size = time_first.shapekey = key.float().view(batch, seq_length, num_heads, head_size).transpose(1, 2).transpose(-2, -1)value = value.float().view(batch, seq_length, num_heads, head_size).transpose(1, 2)receptance = receptance.float().view(batch, seq_length, num_heads, head_size).transpose(1, 2)time_decay = torch.exp(-torch.exp(time_decay.float())).reshape(-1, 1, 1).reshape(num_heads, -1, 1)time_first = time_first.float().reshape(-1, 1, 1).reshape(num_heads, -1, 1)out = torch.zeros_like(key).reshape(batch, seq_length, num_heads, head_size)for current_index in range(seq_length):current_receptance = receptance[:, :, current_index:current_index+1, :]current_key = key[:, :, :, current_index:current_index+1]current_value = value[:, :, current_index:current_index+1, :]attention_output = current_key @ current_valueout[:, current_index] = (current_receptance @ (time_first * attention_output + state)).squeeze(2)with torch.no_grad():state = attention_output + time_decay * statereturn out, state

要把这段代码变成cuda kernel,首先需要在形式上做一些还原,使得它更靠近原始的计算公式。还原之后的原始计算公式如下(https://github.com/BlinkDL/RWKV-CUDA/blob/main/wkv5/run.py#L67-L87):

def RUN_FORMULA_1A(B, T, C, H, r, k, v, w, u):N = C // Hr = r.view(B, T, H, N)k = k.view(B, T, H, N)v = v.view(B, T, H, N)w = w.view(H, N)u = u.view(H, N)out = torch.zeros((B, T, H, N), device=DEVICE)for b in range(B):for h in range(H):state = torch.zeros((N,N), device=DEVICE).contiguous()for t in range(T):for i in range(N):for j in range(N):x = k[b,t,h,j] * v[b,t,h,i]s = state[i,j]out[b,t,h,i] += r[b,t,h,j] * (u[h,j] * x + s)state[i,j] = s * w[h,j] + xreturn out.view(B, T, C)

这里有5个循环,其中N一般比较小,对于RWKV5和RWKV6来说,N一般固定为64。还有就是这个还原的公式没有返回state,而是在B,H的内循环中申请了一个局部的state,为了保持和上面的公式一致,需要把state的形状改成[B, H, N, N],就像在profile代码编写那一节看到的这样。这里的系列kernel暂时不考虑全局state,因为训练的时候类似于推理的Prefill,不需要有这个state。有了这个代码之后,只需要想好开多少个Block以及每个Block开多少个Thread就可以写出一个Baseline了,然后逐步优化。

0x4.1 BaseLine

这个是BaseLine kernel的链接:https://github.com/BlinkDL/RWKV-CUDA/blob/main/wkv5/cuda/wkv5_cuda_ref.cu

首先看一下Block数和每个Block的线程数:

void cuda_forward(int B, int T, int C, int H, float *r, float *k, float *v, float *w, float *u, float *y)
{dim3 threadsPerBlock( min(B*C, 32) );assert(B * C % threadsPerBlock.x == 0);dim3 numBlocks(B * C / threadsPerBlock.x);kernel_forward<<<numBlocks, threadsPerBlock>>>(B, T, C, H, r, k, v, w, u, y);
}

每个Block使用min(B*C, 32)个线程,然后Block数就是B*C//threadsPerBlock.x,上面的公式有5个循环,这里的C=H*N,也就是说这里会把第1个,第2个,第4个循环分配给CUDA kernel,那么可以预见kernel中每个线程的计算过程肯定还有一个T和N的循环。浏览下这里的cuda kernel:

template <typename F>
__global__ void kernel_forward(const int B, const int T, const int C, const int H,const F *__restrict__ const _r, const F *__restrict__ const _k, const F *__restrict__ const _v, const F *__restrict__ const _w, const F *__restrict__ const _u,F *__restrict__ const _y)
{const int idx = blockIdx.x * blockDim.x + threadIdx.x;const int _b = idx / C;const int _h = (idx / N) % H;const int _i = idx % N;const int _o0 = _b*T*C + _h*N;const int _o1 = _h*N;const F *__restrict__ const k = _k + _o0;const F *__restrict__ const v = _v + _o0 + _i;const F *__restrict__ const r = _r + _o0;F *__restrict__ const y = _y + _o0 + _i;float state[N] = {0};   for (int __t = 0; __t < T; __t++){const int _t = __t*C;const F vv = v[_t];for (int _j = 0; _j < N; _j++) {const int j = _t + _j;const int m = _o1 + _j;const float x = k[j] * vv;const float s = state[_j];atomicAdd(y + _t, r[j] * (_u[m] * x + s));state[_j] = s * _w[m] + x;}}
}

观察这个baseline的kernel,首先通过线程id确定当前线程所在的第一循环b,第二循环h,第4循环i的位置,然后对T以及最后的N循环进行遍历,按照公式计算结果并使用atomicAdd累计答案。

0x4.1 不必要的atomicAdd

对于每个线程来说它都有唯一的线程id,上面代码中F *__restrict__ const y = _y + _o0 + _i;这里的_o0+i一定是唯一的,所以这个atomicAdd可以去掉,用一个普通的变量来累加答案即可。https://github.com/BlinkDL/RWKV-CUDA/blob/main/wkv5/cuda/wkv5_cuda_v1a.cu

在这里插入图片描述

0x4.2 float4向量化

每个线程会在2个循环上频繁访问数据并计算,这里使用float4向量化读数据将有直接的收益。https://github.com/BlinkDL/RWKV-CUDA/blob/main/wkv5/cuda/wkv5_cuda_v1b.cu

在这里插入图片描述

0x4.3 线程块的调整

在上面的版本中,每个Block的线程数是min(B*C, 32),而对于RWKV5和RWKV6系列的模型来说,C=H*D=H*64一定是超过32的,所以每个Block的线程数一定是32,也就是一个warp。从如何设置CUDA Kernel中的grid_size和block_size? 可知线程数太少会导致SM的Occupancy无法打满,导致性能变差,最好是每个Block直接开128个线程。但RWKV 5里面的调整是将每个Block的线程数调整到64,具体见:https://github.com/BlinkDL/RWKV-CUDA/blob/main/wkv5_bf16/cuda/wkv5_cuda_v1b.cu

在这里插入图片描述

0x4.4 Shared Memory

观察到在第三和第五两个循环下,会频繁访问r, k, u, w,因此可以把这几个数据存入shared memory再读取。https://github.com/BlinkDL/RWKV-CUDA/blob/main/wkv5_bf16/cuda/wkv5_cuda_v1b.cu

template <typename F>
__global__ void kernel_forward(const int B, const int T, const int C, const int H,const F *__restrict__ const _r, const F *__restrict__ const _k, const F *__restrict__ const _v, const float *__restrict__ _w, const F *__restrict__ _u,F *__restrict__ const _y)
{const int b = blockIdx.x / H;const int h = blockIdx.x % H;const int i = threadIdx.x;_w += h*_N_;_u += h*_N_;__shared__ float r[_N_], k[_N_], u[_N_], w[_N_];float state[_N_] = {0};__syncthreads();u[i] = float(_u[i]);w[i] = float(_w[i]);__syncthreads();for (int t = b*T*C + h*_N_ + i; t < (b+1)*T*C + h*_N_ + i; t += C){__syncthreads();r[i] = float(_r[t]);k[i] = float(_k[t]);__syncthreads();const float v = float(_v[t]);float y = 0;#pragma unrollfor (int j = 0; j < _N_; j+=4){const float4& r_ = (float4&)(r[j]);const float4& k_ = (float4&)(k[j]);const float4& w_ = (float4&)(w[j]);const float4& u_ = (float4&)(u[j]);float4& s = (float4&)(state[j]);float4 x;x.x = k_.x * v;x.y = k_.y * v;x.z = k_.z * v;x.w = k_.w * v;y += r_.x * (u_.x * x.x + s.x);y += r_.y * (u_.y * x.y + s.y);y += r_.z * (u_.z * x.z + s.z);y += r_.w * (u_.w * x.w + s.w);s.x = s.x * w_.x + x.x;s.y = s.y * w_.y + x.y;s.z = s.z * w_.z + x.z;s.w = s.w * w_.w + x.w;}_y[t] = F(y);}
}

这里如果想把state也存入shared memory,那么state就需要做成一个全局的state这样才可以只开N的大小否则就需要开N*N的大小导致SM上shared memory大小不够。

每个Block开启了64个线程,也就是2个warp,对于warp里面的每个线程来说,它在访问r, k, u, w的时候必定是独立且连续的,因为这些访问都在N这个循环中,不会发生Bank Conflict。

这就是rwkv5_cuda_linear_attention对应的cuda kernel目前的状态。但,怎么就被Triton秒了?

0x5. Triton实现粗略浏览

Triton的实现也是根据naive的实现来的,先看一下naive的实现以及相关的输入。https://github.com/sustcsonglin/flash-linear-attention/blob/main/fla/ops/rwkv6/recurrent_naive.py#L8-L36

def naive_recurrent_rwkv6(q,k,v,w,u,initial_state=None,output_final_state=False
):orig_dtype = q.dtypeq, k, v, w, u = map(lambda x: x.float(), (q, k, v, w, u))batch_size, n_heads, seq_len, d_head_k = q.shape_, _, _, d_head_v = v.shapeh = torch.zeros(batch_size, n_heads, d_head_k, d_head_v, dtype=torch.float32, device=q.device)o = torch.zeros_like(v)if initial_state is not None:h += initial_statefor i in range(seq_len):q_i = q[:, :, i, :]k_i = k[:, :, i]v_i = v[:, :, i, :]w_i = w[:, :, i].exp()kv_i = k_i[..., None] * v_i[..., None, :]o_i = (h + u[None, ..., None] * kv_i) * q_i[..., None]o[:, :, i] = o_i.sum(-2)h = h * w_i[..., None] + kv_ireturn o.to(orig_dtype)

q, k, v, w, u等定义如下:

B = 4
H = 4
L = 1024
D = 100
dtype = torch.float32
q = (torch.randn(B, H, L, D).cuda().to(dtype)).requires_grad_(True)
k = (torch.randn(B, H, L, D).cuda().to(dtype)).requires_grad_(True)
v = torch.randn(B, H, L, D).cuda().to(dtype).requires_grad_(True)
w = torch.nn.functional.logsigmoid(torch.randn(B, H, L, D)).cuda().to(torch.float32).requires_grad_(True)
u = (torch.randn(H, D).cuda().to(dtype)).requires_grad_(True)
do = torch.rand_like(v).cuda()
o = naive_recurrent_rwkv6(q, k, v, w, u)

这里q,k,v的head dim维度我重新设置为了D。

然后在实现fused_recurrent_rwkv6的时候各个输入tensor的shape也沿用了这里的设置。接口定义在 https://github.com/sustcsonglin/flash-linear-attention/blob/main/fla/ops/rwkv6/recurrent_fuse.py#L403 。

# if scale is None, use d_head_qk ** -0.5 by default. Otherwise specify the scale yourself. e.g. scale = 1.0
def fused_recurrent_rwkv6(r: torch.Tensor,k: torch.Tensor,v: torch.Tensor,w: torch.Tensor,u: torch.Tensor,scale: int = -1,initial_state: torch.Tensor = None,output_final_state: bool = False,causal: bool = True
) -> Tuple[torch.Tensor, torch.Tensor]:r"""Args:r (torch.Tensor):reception of shape `(B, H, T, K)`. Alias: q, query in linear attention.k (torch.Tensor):keys of shape `(B, H, T, K)`v (torch.Tensor):values of shape `(B, H, T, V)`w (torch.Tensor):data-dependent decays of shape `(B, H, T, K)` in log space! Alias: g.u (torch.Tensor):bonus of shape `(H, K)`scale (Optional[int]):Scale factor for the RWKV6 attention scores.If not provided, it will default to `1 / sqrt(K)`. Default: `None`.initial_state (Optional[torch.Tensor]):Initial state of shape `(B, H, K, V)`. Default: `None`.output_final_state (Optional[bool]):Whether to output the final state of shape `(B, H, K, V)`. Default: `False`."""if scale == -1:scale = r.shape[-1] ** -0.5if initial_state is not None:initial_state = initial_state.detach()o, final_state = FusedRecurrentRWKV6Function.apply(r, k, v, w, u, scale, initial_state, output_final_state)return o, final_state

这里再关注下Triton实现的Kernel的线程网格设置相关代码,也就是FusedRecurrentRWKV6Function的forward函数:

class FusedRecurrentRWKV6Function(torch.autograd.Function):@staticmethod@contiguous@custom_fwddef forward(ctx, r, k, v, w, u, scale=None, initial_state=None, output_final_state=False, reverse=False):# aliasq = rbatch_size, n_heads, seq_len, d_head_qk = q.shaped_head_v = v.shape[-1]# default scaleif scale is None:scale = d_head_qk ** -0.5BK, BV = min(triton.next_power_of_2(d_head_qk), 32), min(triton.next_power_of_2(d_head_v), 32)NK, NV = triton.cdiv(d_head_qk, BK), triton.cdiv(d_head_v, BV)num_stages = 1num_warps = 1o = q.new_empty(NK, batch_size, n_heads, seq_len,d_head_v, dtype=torch.float32)if output_final_state:final_state = q.new_empty(batch_size, n_heads, d_head_qk, d_head_v)else:final_state = Nonegrid = (NV, NK, batch_size * n_heads)fused_recurrent_rwkv6_fwd_kernel[grid](q, k, v, w, u, o, initial_state, final_state,q.stride(1), q.stride(2), q.stride(3),v.stride(1), v.stride(2), v.stride(3),batch_size, n_heads, seq_len, scale,DK=d_head_qk, DV=d_head_v, BK=BK, BV=BV,USE_INITIAL_STATE=initial_state is not None,STORE_FINAL_STATE=final_state is not None,REVERSE=reverse,num_warps=num_warps,num_stages=num_stages)o = o.sum(0)ctx.save_for_backward(q, k, v, w, u, initial_state, o)ctx.scale = scalectx.reverse = reverse# we do not need the gradient of the final state from the next chunk# similiar to Trunctated BPTTif final_state is not None:final_state = final_state.detach()return o.to(q.dtype), final_state

根据提供的输入形状,我们可以推导出以下参数:

  • B(batch size)= 4
  • H(number of heads)= 4
  • L(sequence length)= 1024
  • D(head dimension)= 100

我们可以使用这些参数来计算 BKBV 的值,以及 NKNV 的值:

  • BK = min(triton.next_power_of_2(D), 32) = min(128, 32) = 32
  • BV = min(triton.next_power_of_2(D, 32) = min(200, 32) = 32
  • NK = triton.cdiv(D, BK) = triton.cdiv(100, 32) = 4
  • NV = triton.cdiv(D, BV) = triton.cdiv(100, 32) = 4

根据这些值,我们可以推导出 grid 的大小。根据代码中的定义,grid 是一个三元组,表示 Triton Kernel 的线程网格大小,其中包括 (NV, NK, batch_size * n_heads)

在这个例子中,batch_size * n_heads = 4 * 4 = 16。因此,grid 的大小将是 (4, 4, 16),相当于有256个Block在并行计算,而每个Block的内部目前Triton的Kernel中指定的是1个warp也就是32个进程来计算。

而在RWKV-CUDA的实现中,对于这个case一共会使用16个线程块,然后每个线程块使用100个线程,从直觉上看这就是一个很不好的配置,Block数太小无法用满SM。

在这里插入图片描述Triton的kernel后续在接着学习和分析,我也需要认真学习下triton。

0x6. 总结

关于flash-linear-attention中rwkv6加速算子的实现后面再解析吧,后续如果RWKV6的Linear Attention算子优化在开源社区有新的进展,我也会及时跟进和分享给大家。

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

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

相关文章

LLVM Instruction Selection 笔记

Instruction Selection 所处阶段 注&#xff1a;上图来源于 Welcome to the back-end: The LLVM machine representation 可以看到 SelectionDAG 架在 LLVM IR 和 LLVM MIR 之间&#xff0c;在此之前 machine independent optimization 已经完成。之后基本上就进入了 machine …

学习3:scrapy请求对象、模拟登录、POST请求、管道的使用、crawlspider爬虫类

请求对象 请求对象参数 scrapy.Request(url[],callback,method"GET",headers,body,cookies,meta,dont_filterFalse)callback 表示当前的url响应交给那个函数去处理method 指定请求方式headers 接受一个字典&#xff0c;其中不包括cookiesbody 接收json字符串&#…

原来spring也可以AI

最近大模型是相当的火&#xff0c;尤其是在自然语言处理&#xff08;NLP&#xff09;、图像识别、语音识别等领域的应用&#xff0c;那对于工程同学来说应该如何接住这波破天的富贵呢&#xff1f; 想啥来啥&#xff0c;前段时间LangChain给我们整了一套钢铁战甲&#xff0c;让…

javascript 练习 写一个简单 另类录入 电脑组装报价表 可打印

数据格式 &#xff08;1代表cpu、2代表主板、3代表内存、。。。&#xff09; 1i3 12100 630 2H610 480 3DDR4 3200 16G 220 4500G M.2 299 5300W电源 150 6小机箱 85 7GT 730G 4G 350 8WD 2T 399 9飞利浦 24Led 580 主代码 Html JS <!DOCTYPE html> <html lang&qu…

tomcat+maven+java+mysql图书管理系统1-配置项目环境

目录 一、软件版本 二、具体步骤 一、软件版本 idea2022.2.1 maven是idea自带不用另外下载 tomcat8.5.99 Javajdk17 二、具体步骤 1.新建项目 稍等一会&#xff0c;创建成功如下图所示&#xff0c;主要看左方目录相同不。 给maven配置国外镜像 在左上…

【通信中间件】Fdbus HelloWorld实例

Fdbus实例教程 Fdbus简介 Fdbus 全称 Fast Distributed Bus&#xff08;高速分布式总线&#xff09;&#xff0c;提供IPCRPC功能。适用于多种OS&#xff1a; LinuxQNXAnroidOSWindow Fdbus本质是Socket&#xff0c;IPC基于Unix domain socket&#xff0c;RPC基于TCP。使用G…

【数据结构初阶】直接插入排序

最近浅学了直接插入排序&#xff0c;写个博客做笔记&#xff01;笔记功能除外若能对读者老爷有所帮助最好不过了&#xff01; 直接插入排序是插入排序的一种&#xff0c;那么介绍直接插入排序之前先介绍一下常见的排序算法&#xff01; 目录 1.常见的排序算法 2.直接插入排…

详解LLMOps,将DevOps用于大语言模型开发

大家好&#xff0c;在机器学习领域&#xff0c;随着技术的不断发展&#xff0c;将大型语言模型&#xff08;LLMs&#xff09;集成到商业产品中已成为一种趋势&#xff0c;同时也带来了许多挑战。为了有效应对这些挑战&#xff0c;数据科学家们转向了一种新型的DevOps实践LLM-OP…

FFmpeg学习记录(四)——SDL音视频渲染实战

1.SDL使用的基本步骤 SDL Init/sDL _Quit()SDL_CreateWindow()/SDL_DestoryWindow()SDL CreateRender() SDL_Windows *windows NULL;SDL_Init(SDL_INIT_VIDEO);window SDL_CreateWindow("SDL2 Windows",200,200, 640,480,SDL_WINDOW_SHOWN);if(!window) {printf(&…

QtWindows任务栏

目录 引言任务栏进度右键菜单缩略图工具栏完整代码 引言 针对Windows系统的任务栏&#xff0c;Qt基于系统的原生接口封装有一些非常见类&#xff0c;如QWinTaskbarButton、QWinTaskbarButton、QWinThumbnailToolBar等&#xff0c;用于利用工具栏提供更多的信息&#xff0c;诸如…

C 认识指针

目录 一、取地址操作符&#xff08;&&#xff09; 二、解引用操作符&#xff08;*&#xff09; 三、指针变量 1、 指针变量的大小 2、 指针变量类型的意义 2.1 指针的解引用 2.2 指针 - 整数 2.3 调试解决疑惑 认识指针&#xff0c;指针比较害羞内敛&#xff0c;我们…

使用jdbc方式操作ClickHouse

1、创建测试表&#xff0c;和插入测试数据 create table t_order01(id UInt32,sku_id String,total_amount Decimal(16,2),create_time Datetime ) engine MergeTreepartition by toYYYYMMDD(create_time)primary key (id)order by (id,sku_id);insert into t_order01 values …

ctfshow 框架复现

文章目录 web 466web 467web 468web469web 470web 471web 472web 473web 474web 475web 476 web 466 Laravel5.4版本 &#xff0c;提交数据需要base64编码 代码审计学习—Laravel5.4 - 先知社区 (aliyun.com) 用第二条链子 反序列化格式 /admin/序列化串base64<?php na…

Flutter笔记:Widgets Easier组件库(11)- 使用提示吐丝

Flutter笔记 Widgets Easier组件库&#xff08;11&#xff09;使用提示吐丝 - 文章信息 - Author: 李俊才 (jcLee95) Visit me at CSDN: https://jclee95.blog.csdn.netMy WebSite&#xff1a;http://thispage.tech/Email: 291148484163.com. Shenzhen ChinaAddress of this …

【Java】基本程序设计结构(二)

前言&#xff1a;上一篇我们详细介绍了Java基本程序设计结构中前半部分&#xff0c;一个简单的Java应用&#xff0c;注释&#xff0c;数据类型&#xff0c;变量与常量&#xff0c;运算符&#xff0c;字符串。包括本篇将延续上篇内容介绍后续内容&#xff0c;包括输入输出&#…

「 网络安全常用术语解读 」通用配置枚举CCE详解

1. 背景介绍 NIST提供了安全内容自动化协议&#xff08;Security Content Automation Protocol&#xff0c;SCAP&#xff09;为漏洞描述和评估提供一种通用语言。SCAP组件包括&#xff1a; 通用漏洞披露(Common Vulnerabilities and Exposures, CVE)&#xff1a;提供一个描述…

2024年 Java 面试八股文——SpringCloud篇

目录 1.Spring Cloud Alibaba 中的 Nacos 是如何进行服务注册和发现的&#xff1f; 2.Spring Cloud Alibaba Sentinel 的流量控制规则有哪些&#xff1f; 3.Spring Cloud Alibaba 中如何实现分布式配置管理&#xff1f; 4.Spring Cloud Alibaba RocketMQ 的主要特点有哪些&…

2024“天一永安杯“宁波第七届网络安全大赛极安云科战队部分WP

“天一永安杯”2024 宁波第七届网络安全大赛暨第九届大学生网络技术与信息安全大赛 大赛竞赛形式 一、线上初赛 参赛人员&#xff1a;各单位自行选拔3人&#xff08;设队长1名&#xff09;组成团队&#xff0c;不足3人不允许参赛。 竞赛时间&#xff1a;8&#xff1a;30-12&…

ES集群数据备份与迁移

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、文章涉及概念讲解二、操作步骤1.创建 snapshot repository操作主机hadoop1分别操作从机hadoop2和hadoop3 2. 查看仓库信息3. 备份索引&#xff0c;生成快照…

【精品毕设推荐】基于JSP物流信息网的设计与实现

点击免费下载原文及代码、PPT 摘要 本文讲述了基于JSP物流信息网的设计与实现。该系统使用java语言开发&#xff0c;使系统具有更好的平台性和可扩展性。 该系统实现了用户登录、注册、查询快递信息、快递公司注册成为合作伙伴以及系统管理员对信息进行管理等功能。系统的主…