十进制本身是周期性的,二进制也是周期性的

构建n维的位置编码,每一维用不同的周期函数刻画取值

“叠加旋转位置编码的方式由加法改乘法”
假设两个token的embedding为

RoFormer提出Rotary PE,在embedding维度为2的情况下:

先只看二维向量
x = (1, 0), θ = 30°
位置 0:旋转 0° → (1, 0)
位置 1:旋转 30° → (cos 30°, sin 30°)
位置 2:旋转 60° → (cos 60°, sin 60°)
旋转不会改变向量长度:
因此,同一个 token 在不同位置有不同方向,但不会因为位置编码而改变向量的尺度。
设
结果只依赖位置差
RoPE 给每个位置配一个旋转角度;Q 和 K 比较旋转后的方向时,结果自然包含相对距离信息。
高维 RoPE 将 hidden vector 的维度两两分组:
不同维度使用不同的角速度,因此同时表达短距离和长距离的位置变化。实际模型在 attention 的每一层对 Q 和 K 应用 RoPE。
RoPE的2D实现

RoPE的n维实现


不同于经典Transformers结构,只对输入的token做位置编码的叠加
LlaMA中的RoPE在Transformer的每一层都会对Q和K进行位置编码的叠加
在深度学习中,tensor操作是构建模型的基础。理解各种tensor操作对于实现LLM等复杂模型至关重要。
view() / reshape() - 改变tensor形状transpose() / permute() - 转置和维度重排squeeze() / unsqueeze() - 压缩/扩展维度matmul() / @ - 矩阵乘法bmm() - 批量矩阵乘法einsum() - 爱因斯坦求和约定contiguous()gather() / scatter() - 按索引收集/分散index_select() - 按索引选择masked_select() - 按掩码选择view(): 返回与原tensor共享存储的新视图,要求tensor在内存中连续reshape(): 如果可能则返回view,否则返回副本# view() - 要求tensor连续
x = torch.randn(2, 3, 4)
y = x.view(6, 4) # 成功:2*3=6
# 如果tensor不连续,view()会报错
x_transposed = x.transpose(0, 1) # 不连续
# y = x_transposed.view(12, 2) # 报错!
# reshape() - 自动处理连续性问题
y = x_transposed.reshape(12, 2) # 成功:自动处理
transpose() - 交换两个维度
x = torch.randn(2, 3, 4, 5)
y = x.transpose(1, 3) # 交换维度1和3
print(x.shape) # torch.Size([2, 3, 4, 5])
print(y.shape) # torch.Size([2, 5, 4, 3])
permute() - 重新排列所有维度
x = torch.randn(2, 3, 4, 5)
y = x.permute(0, 3, 1, 2) # 重新排列维度
print(x.shape) # torch.Size([2, 3, 4, 5])
print(y.shape) # torch.Size([2, 5, 3, 4])
squeeze() - 移除大小为1的维度
x = torch.randn(1, 3, 1, 4)
y = x.squeeze() # 移除所有大小为1的维度
z = x.squeeze(0) # 只移除第0维
print(x.shape) # torch.Size([1, 3, 1, 4])
print(y.shape) # torch.Size([3, 4])
print(z.shape) # torch.Size([3, 1, 4])
unsqueeze() - 在指定位置插入大小为1的维度
x = torch.randn(3, 4)
y = x.unsqueeze(0) # 在第0维插入
z = x.unsqueeze(-1) # 在最后一维插入
print(x.shape) # torch.Size([3, 4])
print(y.shape) # torch.Size([1, 3, 4])
print(z.shape) # torch.Size([3, 4, 1])
Broadcasting是PyTorch中一种强大的机制,允许不同形状的tensor进行运算。它遵循以下规则:
# 示例1: 标量与tensor
a = torch.tensor([1, 2, 3])
b = 2
c = a + b # [3, 4, 5] - 标量自动广播到每个元素
# 示例2: 不同形状的tensor
A = torch.randn(3, 4) # [3, 4]
B = torch.randn(4) # [4]
C = A + B # [3, 4] - B自动扩展为[1, 4]然后[3, 4]
# 示例3: 更复杂的广播
A = torch.randn(2, 3, 4) # [2, 3, 4]
B = torch.randn(3, 1) # [3, 1]
C = A + B # [2, 3, 4] - B扩展为[1, 3, 1]然后[2, 3, 4]
# 1. 添加偏置项
x = torch.randn(32, 128) # [batch_size, features]
bias = torch.randn(128) # [features]
y = x + bias # 每个样本都加上相同的偏置
# 2. 注意力机制中的mask
scores = torch.randn(2, 8, 8) # [batch, seq_len, seq_len]
mask = torch.tensor([1, 1, 1, 0, 0, 0, 0, 0]) # [seq_len]
masked_scores = scores.masked_fill(~mask.bool().view(1, 1, -1), float("-inf")) # 广播到[2, 8, 8]
# 3. 批量归一化
x = torch.randn(32, 64, 28, 28) # [batch, channels, height, width]
mean = torch.mean(x, dim=(0, 2, 3), keepdim=True) # [1, 64, 1, 1]
normalized = (x - mean) / torch.std(x, dim=(0, 2, 3), keepdim=True)
不同矩阵乘法的使用场景
torch.matmul() / @: 通用矩阵乘法,支持广播torch.bmm(): 批量矩阵乘法,专门用于3D tensortorch.mm(): 2D矩阵乘法# 批量矩阵乘法
A_batch = torch.randn(2, 3, 4)
B_batch = torch.randn(2, 4, 5)
C_batch = torch.bmm(A_batch, B_batch)
print(C_batch.shape) # torch.Size([2, 3, 5])
einsum提供了一种简洁的方式来表达复杂的tensor操作:
# 矩阵乘法: C[i,j] = sum_k A[i,k] * B[k,j]
A = torch.randn(3, 4)
B = torch.randn(4, 5)
C = torch.einsum('ik,kj->ij', A, B)
# 批量矩阵乘法
A_batch = torch.randn(2, 3, 4)
B_batch = torch.randn(2, 4, 5)
C_batch = torch.einsum('bik,bkj->bij', A_batch, B_batch)
# 注意力机制中的QK^T计算
Q = torch.randn(2, 8, 64) # [batch, seq_len, d_model]
K = torch.randn(2, 8, 64)
scores = torch.einsum('bqd,bkd->bqk', Q, K)
gather() - 按索引收集元素
# 在指定维度上按索引收集元素
x = torch.randn(3, 4)
indices = torch.tensor([0, 2, 1])
y = torch.gather(x, 1, indices.unsqueeze(1))
print(x)
print(y) # 每行按indices选择元素
# 将元素分散到指定位置
x = torch.zeros(3, 4)
values = torch.randn(3, 2)
indices = torch.tensor([[0, 2], [1, 3], [0, 1]])
x.scatter_(1, indices, values)
print(x)
Hugging Face
Accelerating a Hugging Face Llama 2 and Llama 3 models with Transformer Engine
RoPE部分
Transformer升级之路:10、RoPE是一种β进制编码. 苏剑林
RoFormer: Enhanced Transformer with Rotary Position Embedding
PyTorch Tensor操作
PyTorch官方文档 - Tensor操作
Einops库 - 更优雅的tensor操作
https://marp.app/
--- # 从 token 到 Transformer 对语言模型来说,文本首先被 tokenizer 转换为 token ID,随后查表得到向量表示。进入 Transformer 后,模型还需要在 attention 的 Q、K 中注入位置信息。 本讲将从这个过程中的两个关键模块出发:输入词表 embedding,以及 Rotary Position Embedding(RoPE)。
来源:DeepSeek-V4.1-Flash: Pushing the Limits of KV Cache Compression,Figure 3,第 7 页;§2.1–2.4。 图中的 Encoder 是因果编码器,并非经典 Transformer 的双向 Encoder。 CSA2(ratio, mode) 中,ratio 表示序列压缩率,mode 表示全局 KV 与检索索引的复用方式。 Engram 提供条件记忆,Single-Pass mHC 优化残差流混合,DSpark 用于推测解码。 此页先建立整体印象,后续课程再展开注意力、MoE 和推理优化。
来源:DeepSeek-V4.1-Flash 技术报告,§2.2–2.4、§3.2.2、§4.2.1,第 9–14、20、22 页。 Prefill 指处理输入并建立缓存,Decode 指逐 token 生成输出;KV 是注意力中缓存的 Key/Value 表示。 CED 仍需为 Decoder 准备局部 SWA KV,不能说所有输入都只经过 Encoder。 分层索引的第一次扫描仍覆盖完整历史;Bounded Replay 是近似恢复,不是数学等价重算。
```python for text in ["Nanjing University", "南京大学", "LLM中的embedding"]: print(text, tokenizer.tokenize(text)) ```
--- # Tokenization方式 * Byte-level BPE (GPT2) * WordPiece (BERT) * Unigram:另一种子词建模算法 * SentencePiece:支持 BPE、Unigram 的工具,不是与它们并列的算法 * Tokenizer in LlaMA3 * BPE model based on [tiktoken](https://github.com/openai/tiktoken)
--- # 实验:查表就是选择矩阵的行 - token ID:`I: [b, s]`,整数类型 `torch.long` - 可学习词表:`T: [v, e]` - 输出:`E: [b, s, e]`,满足 `E[b, s] = T[I[b, s]]` ```python I = torch.tensor([[0, 3, 3], [2, 1, 0]]) T = torch.arange(16, dtype=torch.float32).reshape(4, 4) E = T[I] ``` 同一个 ID 查出同一个向量;训练更新的是词表参数。 --- # 实验 2.1:从小词表到真实模型 本地 Qwen3-0.6B:用同一个 tokenizer 和模型权重。 ```python input_embedding = qwen.get_input_embeddings() ids = qwen_tokenizer("南京大学", return_tensors="pt")["input_ids"] vectors = input_embedding(ids) ``` - 先预测 `[b, s, e]`,再观察中英文的 token 数和输出形状。 - 验证 `vectors == input_embedding.weight[ids]`。 - 相同 ID 的输入向量相同;上下文信息来自后续 Transformer。 - padding 也会查表;attention mask 不会自动把输入向量清零。 --- # 实验 3:one-hot 与查表等价 ```python H = torch.nn.functional.one_hot(I, num_classes=4).to(T.dtype) assert torch.allclose(H @ T, T[I]) ``` `[b, s, v] @ [v, e] → [b, s, e]` one-hot 用于理解数学等价性;实际查表不需要分配 `[b, s, v]`。 大词表下,这能避免大量无效的存储与运算。
--- # A2 Task 2:模块的接口约定 - `VocabEmbedding`:词表注册为 `nn.Parameter`。 - 按 `init_mean/init_std/init_seed` 正态初始化。 - `__init__` 调用 `reset_parameters()`。 - 不修改输入 ID 的值、形状、dtype、device。 - 输出 device 跟随 ID,dtype 跟随词表。 Notebook 实验 2 给出完整课堂实现,并检查输出与输入不变性。 --- # 实验 4:词表分片 取 `v=8, world_size=2`,每个 rank 保存 `[4, e]`: | rank | 全局 ID 区间 | 输入 `[0, 3, 4, 7]` 的局部有效索引 | |---|---|---| | 0 | `[0, 4)` | `[0, 3, —, —]` | | 1 | `[4, 8)` | `[—, —, 0, 3]` | 全局 ID 减去区间起点,得到局部索引;区间外输出零向量。 局部输出仍是 `[b, s, e]`;求和恢复完整结果。 --- # A2 Task 3:初始化与验证 - 分片初始化 seed:`init_base_seed + rank`。 - 先把无效索引替换为安全索引,再查表并清零。 - 作业只计算局部结果,不要求实现通信。 - 验证时拼接这些局部词表作为完整参考表。 不能拿另一份独立随机初始化的完整词表来比较数值。 Notebook 同时验证区间边界、零填充和求和恢复。
<p align="center"> <img width="500" height="200" src="images/l4/nju.png"> <img width="500" height="200" src="images/l4/nju2.png"> </p>

