CUDA Kernel RoPE LLM GPU PaddlePaddle

Fused Rotary Position Encoding (RoPE) 全方位解析

基于 4 个真实 PaddlePaddle/FastDeploy CUDA Kernel 源码,深度对比 GPT-J (Interleaved)、GPT-NeoX (Half-Split)、Partial Rotary、3D RoPE 等多种实现方式,涵盖数学原理、内存布局、CUDA 实现细节与性能特征。

2026-04-14  ·  CUDA Kernel Research Notes
TL;DR — 核心要点
  • RoPE 的核心运算是对 head 内的元素对施加 2D 旋转:x' = x*cos - y*sin, y' = y*cos + x*sin,不同变体的区别仅在于如何选取 (x, y) 对
  • GPT-J (Interleaved) 取相邻元素 (arr[2i], arr[2i+1]) 配对;GPT-NeoX (Half-Split) 取前半后半 (arr[i], arr[i+d/2]) 配对 — 数学等价,内存布局不同
  • Partial Rotary 只旋转 head 的前 rotary_dim 个元素,其余保持不动(Phi-3, GLM-4 使用此策略)
  • 所有频率缩放变体(PI / NTK / YaRN / LongRoPE)只改变预计算的 cos/sin cache,kernel 旋转逻辑完全不变
  • Fused Kernel 通常将 QKV split + RoPE + RMSNorm 合并为一个 kernel,减少 global memory 读写次数,提升 memory-bound 场景下的性能

背景 / Background

Rotary Position Embedding (RoPE) 由 Su et al. 在 2021 年的 RoFormer 论文中提出,已成为现代 LLM 的事实标准位置编码方案。LLaMA、Qwen、Baichuan、Mistral、Gemma、Phi、GLM 等主流模型均采用 RoPE 的某种变体。

与绝对位置编码 (APE) 直接加到 token embedding 不同,RoPE 在 attention 计算前对 Query 和 Key 向量施加位置相关的旋转变换。这种设计使得两个 token 的 attention score 只依赖它们的相对位置,天然支持位置外推。

RoPE 数学原理

对于 head 维度 d 中的第 i 对元素,RoPE 定义旋转角度为:

// 频率基于维度递减(低维高频,高维低频) θi = base-2i/d    // base 通常为 10000 // 位置 m 处的旋转角 angle(m, i) = m × θi // 对第 i 对元素施加 2D 旋转 x' = x × cos(angle) - y × sin(angle) y' = y × cos(angle) + x × sin(angle)
💡
Key Insight
旋转公式等价于一个 2×2 旋转矩阵乘法。每一对 (x, y) 独立旋转,互不干扰。整个 head 的旋转可以理解为 d/2 个独立的 2D 旋转同时施加。

为什么有多种变体

RoPE 的数学原理只定义了"对 d/2 对元素各施加一个旋转",但没有规定这些"对"如何从 d 维向量中选取。这就是不同 RoPE 变体的根本分歧点:

2
配对方式
(Interleaved / Half-Split)
Partial
部分旋转
(rotary_dim < head_dim)
3D
多维 RoPE
(时空位置编码)
6+
频率缩放方法
(PI/NTK/YaRN/...)

四大 RoPE 配对方式详解

1. GPT-J 模式 (Interleaved / 交错配对)

GPT-J 风格将相邻的两个元素视为一对:(arr[2i], arr[2i+1]) 构成第 i 个旋转对。这是 RoPE 原始论文中最自然的实现方式。

内存布局 (head_dim=8, rot_dim=8)

arr[] = x0 y0 x1 y1 x2 y2 x3 y3

配对关系:(x0,y0), (x1,y1), (x2,y2), (x3,y3) — 每对共享同一个 θ

索引公式

x_index = 2 * rot_offset // 偶数位 y_index = 2 * rot_offset + 1 // 奇数位 cos = cos_ptr[rot_offset] // 一个 cos 值服务一对 sin = sin_ptr[rot_offset]

对应 Kernel 代码 (apply_token_rotary_embedding_kernel, IS_NEOX=false)

// GPT-J: 相邻元素配对
x_index = 2 * rot_offset;
y_index = 2 * rot_offset + 1;
cos = cos_ptr[x_index / 2];
sin = sin_ptr[x_index / 2];

const T x = arr[x_index];
const T y = arr[y_index];
arr[x_index] = x * cos - y * sin;
arr[y_index] = y * cos + x * sin;CUDA

代表模型

GPT-J, CodeGen, GPT-NeoX (early), 部分 Falcon 变体

💡
Interleaved 的 CUDA 特点
GQAVariableLengthRotarySplitKernel 中,VecSize=4 的向量化 load 一次取 4 个元素 = 2 个旋转对。用 HalfVecSize = VecSize/2 加载 cos/sin,每个 cos/sin 值对应 src_vec[2*i]src_vec[2*i+1]。这意味着一次向量化 load 可以完成 2 个旋转,非常高效。

2. GPT-NeoX 模式 (Half-Split / 前后半配对)

GPT-NeoX 风格将 head 维度切成前半和后半,前半的第 i 个元素与后半的第 i 个元素配对:(arr[i], arr[i + d/2])

内存布局 (head_dim=8, rot_dim=8)

arr[] = x0 x1 x2 x3 y0 y1 y2 y3

前半 [0, d/2) 是所有 x,后半 [d/2, d) 是所有 y — 配对关系跨越半个 head

索引公式

x_index = rot_offset // 前半区 y_index = embed_dim + rot_offset // 后半区 (embed_dim = d/2) cos = cos_ptr[x_index] sin = sin_ptr[x_index]

对应 Kernel 代码 (apply_token_rotary_embedding_kernel, IS_NEOX=true)

// NeoX: 前半后半配对
x_index = rot_offset;
y_index = embed_dim + rot_offset;
cos = cos_ptr[x_index];
sin = sin_ptr[x_index];

const T x = arr[x_index];
const T y = arr[y_index];
arr[x_index] = x * cos - y * sin;
arr[y_index] = y * cos + x * sin;CUDA

对应 Kernel 代码 (FusedNeoxRopeEmbeddingKernel)

// Fused 版本:从 packed QKV 中拆分并旋转
const int base_idx_left  = ... + h_bias;            // 前半
const int base_idx_right = base_idx_left + half_lastdim; // 后半

Load<T, VecSize>(&qkv[base_idx_left],  &left_vec);
Load<T, VecSize>(&qkv[base_idx_right], &right_vec);

// 旋转:left=x, right=y
left_vec[i]  = input_left * cos_tmp - input_right * sin_tmp;
right_vec[i] = input_right * cos_tmp + input_left * sin_tmp;CUDA

代表模型

LLaMA 1/2/3, Qwen, Baichuan, Mistral, Gemma, Yi, DeepSeek

⚠️
NeoX 的 CUDA 代价
NeoX 模式下,配对的 x 和 y 在内存中相距 d/2 个元素。一次向量化 load 只能取到 x 或 y 的连续块,需要两次 load 才能凑齐一个旋转对。相比 Interleaved 模式的一次 load 处理两对,NeoX 多了一次全局内存读取。

3. Partial Rotary (部分旋转)

某些模型只旋转 head 维度的前一部分(rotary_dim < head_dim),后面的维度保持不变。这给模型保留了一部分"不受位置影响"的表示空间。

内存布局 (head_dim=8, rotary_dim=4, NeoX 配对)

arr[] = x0 x1 y0 y1 p0 p1 p2 p3

前 rotary_dim 个元素参与旋转(NeoX 前后半配对),后 head_dim-rotary_dim 个元素 pass-through

对应 Kernel 代码 (GQAVariableLengthNeoxPartialRotarySplitKernel)

if (hi < q_num_head + kv_num_head) {       // 只有 Q 和 K 需要旋转
  if (h_bias < rotary_dim) {                // 在旋转区域内
    if (h_bias < half_rotary_dim) {
      // 前半:加载后半作为配对
      Load(&qkv[base_idx + half_rotary_dim], &src_vec_right);
      // x' = x*cos - y*sin
      src_vec[i] = input_left * cos - input_right * sin;
    } else {
      // 后半:加载前半作为配对
      Load(&qkv[base_idx - half_rotary_dim], &src_vec_right);
      // y' = y*cos + x*sin
      src_vec[i] = input_left * cos + input_right * sin;
    }
  }
  // else: h_bias >= rotary_dim, 不旋转,保持原值
}CUDA

代表模型

Phi-2/3 (partial_rotary_factor=0.5), GLM-4/4.1V, ChatGLM 系列, Gemma (部分层)

💡
Partial Rotary 的设计意图
保留部分维度不受位置编码干扰,让模型有空间学习纯粹基于内容的 attention pattern(而不是位置依赖的)。Phi-3 的做法是只旋转前一半维度(rotary_dim = head_dim/2),GLM 系列则使用不同的 rotary_dim 比例。

4. 3D RoPE (多维旋转位置编码)

3D RoPE 将位置从标量扩展到多维坐标 (t, h, w),用于多模态和视频场景。在 GQAVariableLengthRotarySplitKernel 中通过 rope_3d 参数支持:

// 3D RoPE: 每个 batch 有独立的 emb 偏移
int64_t new_emb_idx = rope_3d
    ? emb_idx + ori_bi * head_dim * max_model_len  // 按 batch 偏移
    : emb_idx;                                       // 标准 1DCUDA

3D RoPE 的核心思想是将 head 维度再细分为若干组,分别编码时间、高度、宽度三个维度的位置。旋转公式不变,只是 cos/sin cache 的组织方式变为 [batch, temporal_pos * spatial_pos, dim_group]

代表模型

Qwen-VL, LLaVA-Video, VideoRoPE (ICML 2025), VRoPE

Kernel 实现深度分析

下面对用户提供的 4 个 CUDA Kernel 进行逐一拆解。

Kernel 1: apply_rotary_embedding_kernel (基础版)

flowchart LR A["Query
[N, H, D]"] --> K["GPU Kernel
1 block = 1 token"] B["Key
[N, KVH, D]"] --> K C["position_ids
[N]"] --> K D["cos_sin_cache
[max_pos, rot_dim]"] --> K K --> E["Query (in-place)
已旋转"] K --> F["Key (in-place)
已旋转"]
特征描述
并行策略grid.x = num_tokens; 每 block 处理 1 个 token 的所有 head
block 内循环stride-loop 遍历 num_heads * embed_dim 个旋转对
配对模式通过模板参数 IS_NEOX 编译期选择 NeoX/GPT-J
Q/K 处理先循环处理 Q 的所有 head,再循环处理 K — 串行两轮
cos/sin 来源cos_sin_cache[pos * rot_dim] 直接查表
输出方式in-place 修改 Query 和 Key
向量化无(逐元素标量访问)

Kernel 2: FusedNeoxRopeEmbeddingKernel (Fused QKV Split + NeoX)

flowchart LR A["Packed QKV
[N, 3*H*D/2]"] --> K["Fused Kernel
VecSize=4"] B["cos_emb (float)"] --> K C["sin_emb (float)"] --> K K --> Q["Q [N,H,D]"] K --> Kout["K [N,H,D]"] K --> V["V [N,H,D]"]
特征描述
核心创新QKV Split + RoPE 融合为一个 kernel,减少一次全局内存读写
配对模式NeoX (Half-Split);分别加载 left(前半)和 right(后半)
向量化VecSize=4,使用 AlignedVectorLDG.128
精度cos/sin 使用 float,数据使用 bf16 → 内部 cast 到 float 计算再 cast 回来
V 处理V 不需要旋转,但也被 split 出来(qkv_id == 2 跳过旋转,直接拷贝)
线性映射全局 linear_index → (token, qkv_id, head, h_bias) 的索引分解
⚠️
输入布局的关键差异
这个 kernel 的 QKV 是 packed 在一起的 [token, 3, H, D/2](注意是 D/2 不是 D),且 QKV 拼在 half_lastdim 维度上。这与常见的 [token, 3, H, D] 布局不同。kernel 通过 base_idx_leftbase_idx_right = left + half_lastdim 来寻址前后半,而输出到独立的 Q/K/V tensor 时使用完整的 head_dim。

Kernel 3: GQAVariableLengthRotarySplitKernel (Interleaved + RMSNorm)

flowchart LR A["Packed QKV"] --> K["Fused Kernel"] B["cos/sin emb"] --> K C["batch_id, cu_seqlens
seq_lens_enc/dec"] --> K D["q_norm / k_norm
weights"] --> K K -->|"RoPE"| R["旋转 Q,K"] R -->|"RMSNorm"| N["归一化 Q,K"] N --> Q["Q split"] N --> Ko["K split (写入 KV cache 位置)"] K --> V["V split (写入 KV cache 位置)"]
特征描述
核心创新QKV Split + RoPE + RMSNorm 三合一 fused kernel
配对模式GPT-J (Interleaved):src_vec[2*i]src_vec[2*i+1]
GQA 支持q_num_head != kv_num_head,Q head 数多于 KV head 数
变长序列通过 batch_id_per_tokencu_seqlens_q/kseq_lens_encoder/decoder 支持变长 batch
RMSNorm 融合对旋转后的结果做 per-head RMSNorm(WelfordWarpAllReduce 计算方差),然后乘 norm weight
3D RoPE通过 rope_3d 标志支持多模态位置编码
线程组织dim3 block(32, 4):每 warp 处理一个 head,4 个 warp 并行处理 4 个 head
KV Cache 写入K/V 直接写入 cu_seqlens_k 指定的 cache 位置
💡
Warp-level RMSNorm 实现细节
每个 warp(32 线程)处理一个完整的 head(128 维 = 32 线程 × VecSize=4)。旋转后,每个线程计算局部平方和 thread_m2,然后通过 WelfordWarpAllReduce(warp shuffle)归约得到整个 head 的方差 warp_m2。整个过程不需要 shared memory,纯寄存器 + shuffle 实现。

Kernel 4: GQAVariableLengthNeoxPartialRotarySplitKernel (Partial NeoX)

flowchart LR A["Packed QKV"] --> K["Fused Kernel"] B["cos/sin emb"] --> K C["batch_id, cu_seqlens
seq_lens_enc/dec"] --> K K -->|"h_bias < rotary_dim"| R["NeoX 旋转"] K -->|"h_bias >= rotary_dim"| P["Pass-through"] R --> Q["Q split"] R --> Ko["K split → KV cache"] P --> Q P --> Ko K --> V["V split → KV cache"]
特征描述
核心创新NeoX 配对 + Partial Rotary:只旋转前 rotary_dim 个维度
配对模式NeoX (Half-Split),但只在 [0, rotary_dim) 范围内
前后半判断h_bias < half_rotary_dim 时算 x*cos - y*sinh_bias ≥ half_rotary_dim 时算 y*cos + x*sin
SM90 特性使用 cudaGridDependencySynchronize()cudaTriggerProgrammaticLaunchCompletion() 支持 PDL (Programmatic Dependent Launch)
适用模型GLM-4, Phi 系列,以及任何 rotary_dim < head_dim 的架构
🚀
SM90 PDL 优化
cudaGridDependencySynchronize()cudaTriggerProgrammaticLaunchCompletion() 是 Hopper (SM90) 引入的 Programmatic Dependent Launch API,允许依赖 kernel 在前驱 kernel 部分 block 完成后就开始执行,而不用等前驱 kernel 全部结束。这对于 LLM 推理中的 kernel chain(如 QKV projection → RoPE → Attention)有显著的 latency 优化效果。

对比分析 / Comparison

配对方式对比

维度 GPT-J (Interleaved) GPT-NeoX (Half-Split) Partial Rotary 3D RoPE
配对方式 (arr[2i], arr[2i+1]) (arr[i], arr[i+d/2]) NeoX 或 GPT-J,但限于 rotary_dim 按维度组分段编码
数学等价性 两者数学上等价(存在置换矩阵将一种转换为另一种) 子集旋转 扩展到多维坐标
1 次 Load 覆盖 2 个旋转对 1 个半区(需 2 次 Load) 同 NeoX 同 NeoX
cos/sin cache 大小 [max_pos, d/2] [max_pos, d] (拼接) [max_pos, rot_dim/2] [batch, max_pos, d/2]
代表模型 GPT-J, CodeGen LLaMA, Qwen, Mistral Phi-3, GLM-4 Qwen-VL, VideoRoPE

Kernel 实现对比

Kernel 配对 Fused 操作 向量化 GQA 变长 特殊特性
apply_rotary
_embedding
NeoX / GPT-J
(模板切换)
仅 RoPE 最简实现,适合教学
FusedNeox
RopeEmbedding
NeoX QKV Split + RoPE VecSize=4 bf16→float 精度提升
GQAVariable
RotarySplit
Interleaved QKV Split + RoPE
+ RMSNorm
VecSize=4 3D RoPE, Warp RMSNorm,
KV Cache 直写
GQANeox
PartialRotary
NeoX (Partial) QKV Split + RoPE VecSize=4 SM90 PDL,
rotary_dim < head_dim

性能特征对比

Fused 程度 (越高越好)

GQA + RMSNorm (K3)
3-in-1
GQA Partial NeoX (K4)
2-in-1
Fused NeoX (K2)
2-in-1
Basic apply (K1)
RoPE only

内存效率 (减少 Global Memory 读写)

K1: 基础版
逐元素
K2: Fused NeoX
LDG.128
K3: GQA+RMSNorm
LDG.128
K4: Partial NeoX
LDG.128
💡
为什么 Fused Kernel 更快
RoPE 是一个 memory-bound 操作(计算量极低,主要开销在读写 global memory)。将 QKV split、RoPE、RMSNorm 融合后,QKV 数据只需从 HBM 读一次、写一次,避免了多个独立 kernel 之间的中间 tensor 存取。对于 LLM 推理场景,decode 阶段的 token 数很少(通常 1),此时 kernel launch overhead 也成为瓶颈,fused kernel 一次 launch 完成所有操作更为高效。

频率缩放方法 (影响 cos/sin cache,不影响 kernel)

以下方法只改变 cos_sin_cache 的预计算方式,不需要修改旋转 kernel 本身

方法 原理 代表模型 支持长度
调大 θbase 直接增大 RoPE 基频,如 10000 → 500000 LLaMA 3 (θ=500K) 8K → 128K+
Position Interpolation (PI) 线性压缩 position:pos' = pos / scale CodeLlama-16K 4K → 16K
NTK-Aware 非均匀缩放频率:高频少缩,低频多缩 社区推理框架 4K → 32K+
Dynamic NTK 根据实际推理长度动态调整 θbase 即插即用 自适应
YaRN 分三段处理(高频不动、中频 NTK、低频线性)+ attention 温度缩放 Nous 系列 4K → 128K+
LongRoPE / LongRoPE2 搜索每个维度的最优缩放因子(非均匀) Phi-3-128K 4K → 2048K
Llama 3.1 分段策略 按频率分高/中/低三段,高频不缩、低频线性、中间平滑过渡 Llama 3.1 8K → 128K
🚀
对 Kernel 开发者的意义
频率缩放方法是 Host 端的 Python/C++ 预计算逻辑,对 GPU kernel 完全透明。只要 kernel 能正确索引 cos_sin_cache[pos * rot_dim + dim_offset],就能无修改地支持所有缩放方法。kernel 开发者不需要关心 θ 是怎么算出来的。

结论与建议 / Conclusions

从 4 个真实 kernel 的分析中,可以总结出以下工程建议:

1. 配对方式选择跟随模型架构。 NeoX (Half-Split) 是当前主流(LLaMA 系列),Interleaved 在部分模型中使用。两者数学等价,但内存访问模式不同。不要混用。

2. Fused Kernel 是 LLM 推理优化的标准做法。 将 QKV Split + RoPE + RMSNorm 等操作融合为一个 kernel,可以将 global memory 读写次数从 N 次降为 1 次读 + 1 次写,这对 memory-bound 的 RoPE 操作收益显著。

3. 向量化 Load/Store 必不可少。 基础版 kernel(逐元素访问)与 VecSize=4 版本相比,带宽利用率差距巨大。LDG.128(float4 / 4×bf16)是标准配置。

4. Warp-level 编程是 Fused Kernel 的关键技巧。 如 Kernel 3 中用 WelfordWarpAllReduce 做 warp 内归约计算 RMSNorm,不需要 shared memory 同步,性能和代码复杂度都优于 block-level 方案。

5. 关注 SM90+ 新特性。 Kernel 4 使用的 PDL (Programmatic Dependent Launch) 可以减少 kernel chain 的 pipeline bubble,对 decode latency 敏感的场景值得采用。

6. 频率缩放方法不影响 kernel。 所有 PI / NTK / YaRN / LongRoPE 等长度外推方法只改变 cos/sin cache 的预计算,kernel 本身无需任何修改即可支持。


参考资料 / References

  1. RoFormer: Enhanced Transformer with Rotary Position Embedding (Su et al., 2021) — RoPE 原始论文
  2. Rotary Embeddings: A Relative Revolution (EleutherAI Blog) — GPT-NeoX/GPT-J RoPE 实现差异
  3. Extending the RoPE (EleutherAI Blog, YaRN) — YaRN 与频率缩放方法综述
  4. YaRN: Efficient Context Window Extension (Peng et al., 2023) — YaRN 论文
  5. LongRoPE: Extending LLM Context to 2048K (Microsoft, ICML 2024) — 非均匀维度缩放
  6. LongRoPE2: Near-Lossless LLM Context Window Scaling (2025) — LongRoPE 改进版
  7. HuggingFace Transformers Issue #25199 — LLaMA RoPE 实现差异讨论
  8. vLLM Issue #747 — GPT-J vs GPT-NeoX RoPE 实现对比
  9. vLLM PR #22593 — Partial Rotary Embedding MRoPE Triton 支持
  10. VideoRoPE: What Makes for Good Video Rotary Position Embedding? (2025) — 3D RoPE 在视频 LLM 中的应用
  11. Inside RoPE: Rotary Magic into Position Embeddings — 图解 RoPE 原理
  12. CUDA C++ Programming Guide (NVIDIA) — grid 维度上限与 SM90 PDL 参考