From 04bf6a8a504934e8f8e17b97a42bc16e9b38fd69 Mon Sep 17 00:00:00 2001 From: Aoshine999 <820459542@qq.com> Date: Sun, 8 Mar 2026 11:05:32 +0800 Subject: [PATCH 01/18] impl ScaledDotProductAttention --- flash_atten.md | 597 +++++++++ funcall.png | Bin 0 -> 464160 bytes .../autograd/ScaledDotProductAttention.h | 42 + .../include/kernels/cuda/flash_attention.h | 27 + .../src/autograd/ScaledDotProductAttention.cc | 149 +++ .../src/kernels/cuda/flash_attention.cu | 1068 +++++++++++++++ .../src/kernels/cuda/flash_attention1.cu | 1171 +++++++++++++++++ infini_train/src/nn/functional.cc | 11 + 8 files changed, 3065 insertions(+) create mode 100644 flash_atten.md create mode 100644 funcall.png create mode 100644 infini_train/include/autograd/ScaledDotProductAttention.h create mode 100644 infini_train/include/kernels/cuda/flash_attention.h create mode 100644 infini_train/src/autograd/ScaledDotProductAttention.cc create mode 100644 infini_train/src/kernels/cuda/flash_attention.cu create mode 100644 infini_train/src/kernels/cuda/flash_attention1.cu diff --git a/flash_atten.md b/flash_atten.md new file mode 100644 index 00000000..d42f321c --- /dev/null +++ b/flash_atten.md @@ -0,0 +1,597 @@ +下面我把 **FlashAttention-2 backward pass 的完整执行流程**整理成一份可直接使用的 **Markdown 文档**。内容会分成两层: + +1. **论文级算法流程**:对应 FlashAttention-2 的标准 backward 思路。 +2. **CUDA 实现级执行流程**:结合你当前的 kernel 设计 + `grid_dims(num_heads_kv, batch_size, Tc), block_dims(Tc, Tr)` + 说明一个 block 实际在做什么。 + +论文中 backward 的核心点包括:将 (Q) 按行块、(K/V) 按列块切分;先计算 (D = \mathrm{rowsum}(dO \circ O));随后固定一个 (K_j,V_j) 列块,遍历所有 (Q_i) 行块,重算 (S_{ij})、(P_{ij})、(dP_{ij})、(dS_{ij}),并更新 (dV_j,dK_j,dQ_i)。另外,FlashAttention-2 在 backward 中只保存 **row-wise logsumexp (L)**,而不是同时保存 row-max 和 row-sum。 + +--- + +# FlashAttention-2 Backward Pass 执行流程 + +## 1. 问题定义 + +给定前向中已经得到的: + +* (Q \in \mathbb{R}^{N_q \times d}) +* (K \in \mathbb{R}^{N_k \times d}) +* (V \in \mathbb{R}^{N_k \times d}) +* (O \in \mathbb{R}^{N_q \times d}) +* (L \in \mathbb{R}^{N_q}),其中 (L) 是每一行 softmax 的 `logsumexp` +* 上游梯度 (dO \in \mathbb{R}^{N_q \times d}) + +要求计算: + +* (dQ \in \mathbb{R}^{N_q \times d}) +* (dK \in \mathbb{R}^{N_k \times d}) +* (dV \in \mathbb{R}^{N_k \times d}) + +FlashAttention-2 backward 的基本思想不是存整张 attention matrix,而是 **按 tile 重算** 中间量,从而降低 HBM 读写。论文的 Algorithm 2 先把 (Q,O,dO,L) 切成行块,把 (K,V) 切成列块,然后以列块为外循环执行 backward。 + +--- + +## 2. 数学公式 + +### 2.1 前向关系 + +对任意一个 tile ((i,j)): + +[ +S_{ij} = Q_i K_j^T +] + +若启用缩放: + +[ +S_{ij} \leftarrow \text{scale} \cdot S_{ij} +] + +softmax 概率: + +[ +P_{ij} = \exp(S_{ij} - L_i) +] + +这里 (L_i) 是该行块对应的 row-wise logsumexp。FlashAttention-2 backward 只依赖 (L),这是它相对早期实现的一个简化。 + +输出满足: + +[ +O_i = \sum_j P_{ij} V_j +] + +--- + +### 2.2 backward 关键中间量 + +先定义: + +[ +D = \mathrm{rowsum}(dO \circ O) +] + +即对每一行做逐元素乘法后求和。论文明确把这一步放在主 backward 循环之前,并写回 HBM。 + +对任意 tile ((i,j)): + +1. 先计算 + [ + dP_{ij} = dO_i V_j^T + ] + +2. 再做 softmax backward + [ + dS_{ij} = P_{ij} \circ (dP_{ij} - D_i) + ] + +3. 然后得到三个输入梯度的 tile 贡献: + +[ +dV_j \mathrel{+}= P_{ij}^T dO_i +] + +[ +dK_j \mathrel{+}= dS_{ij}^T Q_i +] + +[ +dQ_i \mathrel{+}= dS_{ij} K_j +] + +这些正是论文 Algorithm 2 的核心更新过程。 + +--- + +## 3. Tile 划分 + +设: + +* 行块大小为 (B_r) +* 列块大小为 (B_c) + +则: + +* (Q) 被分成 (T_r = \lceil N_q / B_r \rceil) 个行块 +* (K,V) 被分成 (T_c = \lceil N_k / B_c \rceil) 个列块 + +论文中明确写出: + +* (Q_1,\dots,Q_{T_r}) +* (K_1,\dots,K_{T_c}) +* (V_1,\dots,V_{T_c}) +* (O_i,dO_i,L_i,D_i,dQ_i) 也都按行块划分 +* (dK_j,dV_j) 按列块划分。 + +--- + +## 4. 论文级 backward 总流程 + +## Step 0:输入准备 + +从前向保留下来: + +* `Q, K, V, O` +* `dO` +* `L = logsumexp` +* 以及可选的 `dropout_seed` + +初始化输出: + +* `dQ = 0` +* `dK = 0` +* `dV = 0` + +--- + +## Step 1:预计算 (D) + +对每一行计算: + +[ +D_i = \sum_{m=1}^{d} dO_{i,m} \cdot O_{i,m} +] + +并写回全局内存。 + +### 作用 + +这是 softmax backward 所需的行级归约项,用来避免显式构造整张 Jacobian。论文把它作为 backward 前处理步骤。 + +--- + +## Step 2:外层遍历列块 (j) + +对每个列块 (j = 1,\dots,T_c): + +1. 从 HBM 载入 (K_j, V_j) 到 on-chip SRAM / shared memory +2. 在 shared memory 或寄存器中初始化本列块的局部梯度: + + * (dK_j = 0) + * (dV_j = 0) + +论文 Algorithm 2 就是以列块为外层循环,并在每个 (j) 开始时把 `K_j, V_j` 加载到 SRAM,然后初始化 `dK_j, dV_j`。 + +--- + +## Step 3:内层遍历行块 (i) + +对每个行块 (i = 1,\dots,T_r): + +### 3.1 载入当前行块数据 + +从 HBM 载入: + +* (Q_i) +* (O_i)(若需要本 kernel 内使用) +* (dO_i) +* (L_i) +* (D_i) + +--- + +### 3.2 重算 score tile + +重算当前 tile 的 attention score: + +[ +S_{ij} = Q_i K_j^T +] + +若设置 `scale`: + +[ +S_{ij} \leftarrow \text{scale} \cdot S_{ij} +] + +若启用 `causal mask`,则对非法位置做 mask: + +[ +S_{ij}(r,c) = -\infty +\quad \text{if } k_idx > q_idx +] + +其中: + +* `q_idx = i * B_r + r` +* `k_idx = j * B_c + c` + +### 说明 + +这一步没有从全局内存读取旧的 attention matrix,而是 **重算**。这是 FlashAttention backward 相比普通实现最关键的节省显存策略。 + +--- + +### 3.3 重算概率 tile + +根据保存的 `logsumexp`: + +[ +P_{ij} = \exp(S_{ij} - L_i) +] + +若启用 `dropout`,则要重建前向时相同的 dropout mask,并应用: + +[ +P_{ij}^{drop} = \frac{M_{ij} \circ P_{ij}}{1-p} +] + +其中: + +* (M_{ij}) 为基于同一随机种子重建的二值 mask +* (p) 为 dropout probability + +如果没有 dropout,则: + +[ +P_{ij}^{drop} = P_{ij} +] + +--- + +### 3.4 计算 (dV_j) 的当前 tile 贡献 + +[ +dV_j \mathrel{+}= (P_{ij}^{drop})^T dO_i +] + +### 解释 + +因为前向输出是 (O_i = \sum_j P_{ij}V_j),所以对 (V_j) 的梯度就是概率矩阵转置乘以上游梯度。 + +--- + +### 3.5 计算 (dP_{ij}) + +[ +dP_{ij} = dO_i V_j^T +] + +若启用 dropout,则 backward 中也要乘上同样的 dropout mask,并带上缩放因子: + +[ +dP_{ij} \leftarrow \frac{M_{ij} \circ dP_{ij}}{1-p} +] + +--- + +### 3.6 计算 softmax backward 的 (dS_{ij}) + +[ +dS_{ij} = P_{ij} \circ (dP_{ij} - D_i) +] + +注意这里使用的是 **未 dropout 前的 softmax 概率** (P_{ij}),而 `dP` 则需要反映 dropout 后的链式法则。 + +这一步是整套 backward 中最核心的公式,因为 softmax 的 Jacobian 已经被压缩成逐元素乘法加一个行级标量 (D_i)。 + +--- + +### 3.7 计算 (dK_j) 的当前 tile 贡献 + +[ +dK_j \mathrel{+}= dS_{ij}^T Q_i +] + +若前向有 `scale`,则这里还要乘上相同的缩放因子对梯度链路进行修正。更准确地说,如果前向做的是: + +[ +S = \text{scale} \cdot QK^T +] + +那么由 (S) 向 (Q,K) 回传的梯度都要乘 `scale`。 + +--- + +### 3.8 计算 (dQ_i) 的当前 tile 贡献 + +[ +dQ_i \mathrel{+}= dS_{ij} K_j +] + +这一步会累加来自所有列块 (j) 的贡献,因此: + +* 若一个 block 只处理单个列块,那么 `dQ_i` 往往需要原子加到全局内存 +* 或者先在 block 内做局部累加,再统一写回 + +--- + +## Step 4:写回本列块的 (dK_j,dV_j) + +当所有行块 (i) 都遍历完后,本列块 (j) 的局部梯度已经累加完成: + +* 写回 `dK_j` +* 写回 `dV_j` + +由于一个列块 (j) 通常只由一个 block 负责,所以这两者一般不需要跨 block 再归约;但若你在 GQA 下让多个 query-head block 共同更新同一组 kv-head,也可能需要额外归约或原子加。 + +--- + +## Step 5:结束条件 + +当所有列块 (j) 处理完成后: + +* `dQ` +* `dK` +* `dV` + +全部得到。 + +--- + +# 5. 结合你当前 CUDA 设计的执行流程 + +你当前 kernel 为: + +```cpp +FlashAttentionBackwardKernel<<>>( + grad_query_ptr, grad_key_ptr, grad_value_ptr, + query_ptr, key_ptr, value_ptr, output_ptr, grad_output_ptr, + logsumexp_ptr, dropout_seed_value, attn_mask_ptr, + scale, is_causal, dropout_p, enable_gqa, + batch_size, seq_len_q, seq_len_k, num_heads, num_heads_kv, head_dim); +``` + +其中: + +* `grid_dims(num_heads_kv, batch_size, Tc)` +* `block_dims(Tc, Tr)` + +这表示: + +* `blockIdx.x`:对应一个 `kv head` +* `blockIdx.y`:对应一个 `batch` +* `blockIdx.z`:对应一个列块 (j) + +也就是说,**一个 block 固定负责某个 batch、某个 kv-head、某个列块 (j)**。 + +--- + +## 5.1 一个 block 的职责 + +对给定的: + +* `batch = blockIdx.y` +* `kv_head = blockIdx.x` +* `col_tile = blockIdx.z` + +该 block 需要: + +1. 加载当前 `K_j, V_j` +2. 初始化局部 `dK_j, dV_j` +3. 遍历所有相关的 query row tiles (i) +4. 对每个 (i): + + * 找到对应的 query-head(若启用 GQA,要从 q-head 映射到 kv-head) + * 载入 `Q_i, dO_i, L_i, D_i` + * 重算 `S_ij` + * 应用 causal/mask/dropout + * 计算 `P_ij, dP_ij, dS_ij` + * 累加到 `dV_j, dK_j` + * 把对 `dQ_i` 的贡献写回全局 +5. 行块遍历结束后,写回 `dK_j, dV_j` + +--- + +## 5.2 GQA 下的 head 映射 + +如果启用了 GQA: + +* `num_heads` 是 query heads 数 +* `num_heads_kv` 是 kv heads 数 +* 每个 kv head 会对应若干个 query heads + +常见映射方式为: + +[ +q_head_start = kv_head \times \frac{num_heads}{num_heads_kv} +] + +[ +q_head_end = (kv_head+1) \times \frac{num_heads}{num_heads_kv} +] + +于是当前 block 除了遍历 row tiles (i),还需要遍历属于这个 kv-head 组的所有 q-head。这样同一个 `K_j,V_j` 会被多个 q-head 共享使用,而 `dK_j,dV_j` 也要把这些 q-head 的贡献全部累加起来。 + +这与论文中提到的 MQA/GQA backward 需要对共享的 K/V head 梯度求和是一致的。 + +--- + +## 5.3 causal mask 的执行方式 + +若 `is_causal = true`: + +对于 tile 内每个元素 ((r,c)),若其全局位置满足: + +[ +k_global > q_global +] + +则该位置无效。 + +实现上通常有两层优化: + +1. **整块跳过**: + 若当前列块完全在当前行块右侧,则整个 tile 不必计算 + +2. **边界块细粒度判断**: + 若 tile 横跨对角线,则对元素级做 mask + +这样可以减少无效 matmul 和 exp 计算。 + +--- + +## 5.4 dropout 的执行方式 + +若 `dropout_p > 0`: + +1. 使用 `dropout_seed_value` 和 tile 内元素索引重建与 forward 一致的随机数序列 +2. 得到二值 mask +3. 对 `P_ij` 和 `dP_ij` 执行同样的 mask/缩放 + +这样 backward 就与前向使用的是同一组保留位置。 + +--- + +## 5.5 scale 的执行方式 + +若 `scale != 1`: + +前向 score 为: + +[ +S = \text{scale} \cdot QK^T +] + +那么 backward 中: + +* 重算 `S_ij` 时要乘 `scale` +* 从 `dS` 回传到 `dQ,dK` 时也要体现这条链路 + +通常最直接的实现是: + +* 先在重算 `S_ij` 时乘 `scale` +* 在 `dQ = dS K`、`dK = dS^T Q` 前再统一乘 `scale` +* 或者直接把 `scale` 融进 `dS` + +两种写法本质等价,但要保证链式法则只乘一次,不要漏乘或重复乘。 + +--- + +# 6. 一个推荐的伪代码版本 + +```markdown +for each batch b: + for each kv head h_kv: + for each column tile j: + load K_j, V_j into shared memory + initialize local dK_j = 0, dV_j = 0 + + determine all query heads h_q that map to h_kv (for GQA) + + for each query head h_q in group(h_kv): + for each row tile i: + load Q_i, dO_i, L_i, D_i + optionally load O_i if D is not precomputed + + recompute S_ij = Q_i @ K_j^T + if scale is enabled: + S_ij *= scale + + if causal mask is enabled: + mask invalid positions in S_ij + + P_ij = exp(S_ij - L_i) + + if dropout is enabled: + reconstruct dropout mask M_ij + P_drop = M_ij * P_ij / (1 - p) + else: + P_drop = P_ij + + dV_j += P_drop^T @ dO_i + + dP_ij = dO_i @ V_j^T + if dropout is enabled: + dP_ij = M_ij * dP_ij / (1 - p) + + dS_ij = P_ij * (dP_ij - D_i) + + if scale is enabled: + dS_ij *= scale + + dK_j += dS_ij^T @ Q_i + dQ_i += dS_ij @ K_j + + write or atomically accumulate dQ_i to global memory + + write dK_j, dV_j to global memory +``` + +--- + +# 7. 实现时的关键注意点 + +## 7.1 `D = rowsum(dO ∘ O)` 最好预处理 + +论文就是这么做的。这样主 backward kernel 只需读取 `D_i`,不用再额外读取 `O_i` 并做一次按行归约,能减轻主 kernel 负担。 + +--- + +## 7.2 backward 的 matmul 数量更多 + +论文 benchmark 中指出 backward 由于重算,等价上有更多 matmul,因此 FLOPs 估计通常约为 forward 的 2.5 倍。 + +--- + +## 7.3 FlashAttention-2 backward 仍然强调避免 split-K + +论文在 warp 工作划分部分提到,backward 也尽量避免 “split-K” 方案,从而减少 shared memory 读写和同步,尽管 backward 的依赖关系比 forward 更复杂。 + +--- + +## 7.4 block 大小要兼顾 shared memory 和寄存器压力 + +论文提到 block size 变大虽然能减少 shared memory 读写,但也会提高寄存器与 shared memory 占用,过大会导致 spilling 或 kernel 根本无法启动。常见 tile 大小会在 `{64,128}` 范围内手工调优。 + +--- + +# 8. 最终总结 + +FlashAttention-2 backward pass 的本质可以概括为: + +1. **先预处理行级统计量** + + * 计算 (D = \mathrm{rowsum}(dO \circ O)) + +2. **按列块固定 `K_j, V_j`** + + * 让 `K_j,V_j` 常驻 shared memory + +3. **遍历所有行块 `Q_i`** + + * 重算 (S_{ij}) + * 用保存的 `L_i` 恢复 (P_{ij}) + * 计算 (dP_{ij}, dS_{ij}) + +4. **逐 tile 累加三个输入梯度** + + * (dV_j += P_{ij}^T dO_i) + * (dK_j += dS_{ij}^T Q_i) + * (dQ_i += dS_{ij} K_j) + +5. **支持附加功能** + + * causal:在 tile 内做位置裁剪 + * dropout:重建相同随机 mask + * scale:在 `S` 与其梯度链中保持一致 + * GQA:让多个 q-head 共享同一组 kv-head,并对 `dK/dV` 做组内累加 + +--- + +你要是愿意,我下一步可以直接把这份内容继续整理成一份更正式的文档格式,比如: + +* **“设计说明书风格” Markdown** +* **“伪代码 + 公式 + CUDA 映射”版** +* 或者直接给你写成 **项目里的 README 小节**。 diff --git a/funcall.png b/funcall.png new file mode 100644 index 0000000000000000000000000000000000000000..4eb4c1451d8c028640dfb5f42f3e1827ec03a705 GIT binary patch literal 464160 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

j3nU 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

j3nU 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

D4lu(BIw6z3tE@htyWb8U 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 z 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 diff --git a/run_help.md b/run_help.md deleted file mode 100644 index 58f84b0d..00000000 --- a/run_help.md +++ /dev/null @@ -1,421 +0,0 @@ -# InfiniTrain 运行指南 - -本指南介绍如何使用 `scripts` 目录下的脚本进行模型训练、性能分析和结果统计。 - ---- - -## 快速开始 - -### 基本运行 - -最简单的运行方式: - -```bash -cd ~/InfiniTrain/scripts -bash run_models_and_profile.bash -``` - -这将使用默认配置文件 `test_config.json` 运行所有测试。 - -### 强制重新构建 - -如果需要重新构建项目: - -```bash -cd ~/InfiniTrain/scripts -bash run_models_and_profile.bash --rebuild -``` - -### 使用自定义配置文件 - -```bash -cd ~/InfiniTrain/scripts -bash run_models_and_profile.bash /path/to/custom_config.json -``` - ---- - -## 脚本说明 - -### 1. run_models_and_profile.bash - -主运行脚本,负责: -- 从配置文件读取测试配置 -- 构建项目(如果需要) -- 运行 GPT2 和 LLaMA3 模型测试 -- 收集训练日志和性能分析数据 -- 自动运行比较脚本 - -**依赖工具**: -- `jq`:用于解析 JSON 配置文件 - -**安装 jq**: -```bash -sudo apt-get install -y jq -``` - -**运行流程**: -1. 读取配置文件(默认:`test_config.json`) -2. 检查是否需要构建(或使用 `--rebuild` 强制重新构建) -3. 对每个测试配置,分别运行: - - GPT2 模型(noflash 模式) - - GPT2 模型(flash 模式) - - LLaMA3 模型(noflash 模式) - - LLaMA3 模型(flash 模式) -4. 如果启用了性能分析,收集性能报告 -5. 如果设置了 `COMPARE_LOG_DIR`,自动运行比较脚本 - -**日志输出**: -- 训练日志:`logs/` 目录(noflash 结果) -- 训练日志:`compare_logs/` 目录(flash 结果) -- 性能日志:`profile_logs/` 目录(noflash 结果) -- 性能日志:`compare_profile_logs/` 目录(flash 结果) - ---- - -### 2. test_config.json - -配置文件包含三个主要部分: - -#### 2.1 variables - 变量定义 - -```json -{ - "variables": { - "BUILD_DIR": "../build", - "GPT2_INPUT_BIN": "/path/to/gpt2/input.bin", - "GPT2_LLMC_FILEPATH": "/path/to/gpt2/model.bin", - "LLAMA3_INPUT_BIN": "/path/to/llama3/input.bin", - "LLAMA3_LLMC_FILEPATH": "/path/to/llama3/model.bin", - "PROFILE_LOG_DIR": "./profile_logs", - "LOG_DIR": "./logs", - "COMPARE_LOG_DIR": "" - } -} -``` - -**变量说明**: -- `BUILD_DIR`:构建目录路径 -- `GPT2_INPUT_BIN`:GPT2 输入数据文件路径 -- `GPT2_LLMC_FILEPATH`:GPT2 模型文件路径 -- `LLAMA3_INPUT_BIN`:LLaMA3 输入数据文件路径 -- `LLAMA3_LLMC_FILEPATH`:LLaMA3 模型文件路径 -- `PROFILE_LOG_DIR`:性能分析日志目录 -- `LOG_DIR`:训练日志目录 -- `COMPARE_LOG_DIR`:基准日志目录(用于比较,留空则不运行比较) - -#### 2.2 builds - 构建配置 - -```json -{ - "builds": [ - { - "id": "build_2", - "profile": true, - "cmd": "cmake -DUSE_CUDA=ON -DUSE_NCCL=ON -DPROFILE_MODE=ON .. && make -j" - } - ] -} -``` - -**配置说明**: -- `id`:构建 ID(用于日志命名) -- `profile`:是否启用性能分析(true/false) -- `cmd`:CMake 构建命令 - -#### 2.3 tests - 测试配置 - -每个测试包含以下参数: - -```json -{ - "id": "1", - "args": { - "dtype": "float32", - "num_iteration": 10, - "batch_size": 80, - "total_batch_size": 5120, - "nthread_per_process": 8, - "tensor_parallel": 4, - "sequence_parallel": true, - "pipeline_parallel": 8, - "virtual_pipeline_parallel": 2, - "use_distributed_optimizer": true - } -} -``` - -**参数说明**: -- `dtype`:数据类型(float32/bfloat16) -- `num_iteration`:迭代次数 -- `batch_size`:批次大小 -- `total_batch_size`:总批次大小 -- `nthread_per_process`:每进程线程数(数据并行) -- `tensor_parallel`:张量并行度 -- `sequence_parallel`:序列并行(true/false) -- `pipeline_parallel`:流水线并行度 -- `virtual_pipeline_parallel`:虚拟流水线并行度 -- `use_distributed_optimizer`:使用分布式优化器(true/false) - -**测试 ID 命名规则**: -- `1`:基础测试 -- `1_bfloat16`:基础测试(bfloat16) -- `2`:批次大小测试 -- `3`:数据并行测试 -- `3_distopt`:数据并行 + 分布式优化器 -- `4`:张量并行测试 -- `5`:张量并行 + 序列并行测试 -- `6`:流水线并行测试 -- `7`:虚拟流水线并行测试 -- `8`:混合并行测试 - ---- - -### 3. extract_training_stats.py - -从日志文件中提取训练统计信息,包括: -- 平均损失 -- 平均时间 -- 平均 tokens/s -- 峰值内存使用 -- 并行配置(DP、TP、SP、PP) - -**基本用法**: -```bash -python3 extract_training_stats.py -``` - -**选项**: -- `--threshold-fp32`:FP32 损失差异阈值(默认:1e-5) -- `--threshold-bf16`:BF16 损失差异阈值(默认:1e-2) -- `--markdown`:输出 Markdown 格式表格 -- `--output `:指定输出文件路径 -- `--speedup`:输出加速比表格 - -**示例**: - -```bash -# 显示训练统计信息和对比 -python3 extract_training_stats.py logs compare_logs - -# 输出 Markdown 格式 -python3 extract_training_stats.py logs compare_logs --markdown - -# 输出 Markdown 并保存到文件 -python3 extract_training_stats.py logs compare_logs --markdown --output results.md - -# 只显示加速比(不显示训练统计和对比信息) -python3 extract_training_stats.py logs compare_logs --speedup - -# 只显示加速比并保存为 Markdown -python3 extract_training_stats.py logs compare_logs --speedup --markdown --output speedup.md -``` - -**输出说明**: - -**不使用 `--speedup` 时**: - -1. **训练统计表**: - - Row ID:配置标识(模型_数据类型_flash/noflash_分布式优化器_测试ID) - - Avg Loss:平均损失 - - Avg Time:平均时间(ms) - - Avg Tok/s:平均 tokens/s - - Peak Used:峰值内存使用(MB) - - Peak Reserved:峰值内存保留(MB) - - DP/TP/SP/PP:并行配置 - -2. **Flash vs Noflash 对比**: - - 损失对比及差异百分比 - - 时间对比及差异百分比 - - Tokens/s 对比及差异百分比 - - 内存使用对比及差异百分比 - -**使用 `--speedup` 时**: - -3. **加速比表**: - - Configuration:配置名称 - - Speedup Ratio:加速比(noflash_time / flash_time) - - Flash Time:Flash 模式平均时间 - - Noflash Time:Noflash 模式平均时间 - -**加速比说明**: -- 加速比 > 1.0:Flash Attention 更快 -- 加速比 < 1.0:Flash Attention 更慢 - ---- - -### 4. compare_loss.py - -比较两个日志目录中的训练损失,用于验证正确性。 - -**基本用法**: -```bash -python3 compare_loss.py -``` - -**选项**: -- `--threshold-fp32`:FP32 损失差异阈值(默认:1e-5) -- `--threshold-bf16`:BF16 损失差异阈值(默认:1e-2) -- `--verbose`:显示所有文件的详细输出(包括通过的) - -**示例**: - -```bash -# 使用默认阈值比较 -python3 compare_loss.py logs compare_logs - -# 自定义阈值 -python3 compare_loss.py logs compare_logs --threshold-fp32 1e-5 --threshold-bf16 1e-2 - -# 显示详细输出 -python3 compare_loss.py logs compare_logs --verbose -``` - -**输出说明**: - -1. **文件差异**: - - 只在 dir1 中的文件 - - 只在 dir2 中的文件 - -2. **每个测试的对比**: - - 不匹配的步骤及损失值 - - 差异值和相对误差百分比 - - 匹配步骤数统计 - -3. **总体统计**: - - FP32 测试通过率 - - BF16 测试通过率 - - 总体通过率 - -**返回值**: -- 0:所有测试通过 -- 1:存在不匹配的测试 - ---- - -### 5. compare_tps.py - -比较两个日志目录中的 tokens per second,用于性能对比。 - -**基本用法**: -```bash -python3 compare_tps.py -``` - -**选项**: -- `--threshold`:相对误差阈值(默认:0.20 = 20%) -- `--verbose`:显示所有文件的详细输出(包括通过的) - -**示例**: - -```bash -# 使用默认阈值比较 -python3 compare_tps.py logs compare_logs - -# 自定义阈值 -python3 compare_tps.py logs compare_logs --threshold 0.15 - -# 显示详细输出 -python3 compare_tps.py logs compare_logs --verbose -``` - -**输出说明**: - -1. **文件差异**: - - 只在 dir1 中的文件 - - 只在 dir2 中的文件 - -2. **每个测试的对比**: - - 平均 tokens/s 对比 - - 相对误差百分比 - - 比较的步骤数(排除第一步) - -3. **总体统计**: - - 通过的测试用例数 - - 总测试用例数 - - 通过率 - -**注意**: -- 比较时会自动排除第一步(因为第一步可能有初始化开销) -- 相对误差计算:`|avg1 - avg2| / max(avg1, avg2)` - -**返回值**: -- 0:所有测试通过 -- 1:存在不匹配的测试 - ---- - -## 完整使用流程 - -### 1. 准备配置 - -编辑 `test_config.json`: -- 确保 `variables` 中的路径正确 -- 配置 `builds` 中的构建命令 -- 在 `tests` 中添加或修改测试配置 - -### 2. 运行测试 - -```bash -cd ~/InfiniTrain/scripts -bash run_models_and_profile.bash -``` - -### 3. 查看日志 - -- 训练日志:`logs/` 和 `compare_logs/` -- 性能日志:`profile_logs/` 和 `compare_profile_logs/` - -### 4. 分析结果 - -**提取训练统计**: -```bash -python3 extract_training_stats.py logs compare_logs -``` - -**验证正确性**: -```bash -python3 compare_loss.py logs compare_logs --threshold-fp32 1e-5 --threshold-bf16 1e-2 -``` - -**对比性能**: -```bash -python3 compare_tps.py logs compare_logs --threshold 0.20 -``` - -**计算加速比**: -```bash -python3 extract_training_stats.py logs compare_logs --speedup -``` - ---- - -## Flash Attention 对比说明 - -脚本会自动为每个测试运行两个版本: - -- **noflash**:不使用 Flash Attention - - 日志保存在 `logs/` 目录 - - 性能报告保存在 `profile_logs/` 目录 - -- **flash**:使用 Flash Attention - - 日志保存在 `compare_logs/` 目录 - - 性能报告保存在 `compare_profile_logs/` 目录 - -这种设计允许: -1. 验证 Flash Attention 的正确性(通过 compare_loss.py) -2. 对比 Flash Attention 的性能(通过 compare_tps.py) -3. 计算加速比(通过 extract_training_stats.py --speedup) - - - -## 日志文件命名规则 - -- 训练日志:`{model}_{test_id}_{dtype}_{distopt}.log` -- 性能日志:`{model}_{test_id}_{dtype}_{distopt}_profile.log` -- 性能报告:`{model}_{test_id}_{dtype}_{distopt}_profile_{model}.report.rank{N}` - -示例: -- `gpt2_1_bfloat16.log` -- `llama3_4_distopt_profile.log` -- `gpt2_5_bfloat16_distopt_profile_gpt2.report.rank0` diff --git a/scripts/results.md b/scripts/results.md deleted file mode 100644 index 7227200b..00000000 --- a/scripts/results.md +++ /dev/null @@ -1,82 +0,0 @@ -| Row ID | Avg Loss | Avg Time (ms) | Avg Tok/s | Peak Used (MB) | Peak Reserved (MB) | DP | TP | SP | PP | -|---------|----------|---------------|-----------|-----------------|-------------------|----|----|----|----| -| gpt2_bfloat16_flash_disopt_3 | 4.799616 | 2494.86 | 2052.67 | 3407.22 | 3452.44 | 8 | 1 | 1 | 1 | -| gpt2_bfloat16_noflash_disopt_3 | 4.787657 | 2741.77 | 1868.11 | 3405.00 | 3456.00 | 8 | 1 | 1 | 1 | -| gpt2_bfloat16_flash_disopt_4 | 4.799871 | 2900.71 | 1766.33 | 2715.00 | 2748.44 | 8 | 4 | 1 | 1 | -| gpt2_bfloat16_noflash_disopt_4 | 4.788475 | 3211.90 | 1594.33 | 2713.00 | 2784.00 | 8 | 4 | 1 | 1 | -| gpt2_bfloat16_flash_disopt_5 | 4.799961 | 3520.25 | 1454.67 | 2398.00 | 2432.00 | 8 | 4 | 1 | 1 | -| gpt2_bfloat16_noflash_disopt_5 | 4.791147 | 4036.41 | 1290.11 | 2396.00 | 2464.00 | 8 | 4 | 1 | 1 | -| gpt2_bfloat16_flash_disopt_8 | 4.799024 | 2000.08 | 2561.89 | 2373.22 | 2528.00 | 8 | 2 | 1 | 2 | -| gpt2_bfloat16_noflash_disopt_8 | 4.791285 | 2128.22 | 2406.67 | 2326.00 | 2467.56 | 8 | 2 | 1 | 2 | -| gpt2_bfloat16_flash_nodisopt_3 | 4.800298 | 2901.57 | 1766.11 | 3407.22 | 3445.33 | 8 | 1 | 1 | 1 | -| gpt2_bfloat16_noflash_nodisopt_3 | 4.787223 | 3130.48 | 1636.11 | 3405.00 | 3456.00 | 8 | 1 | 1 | 1 | -| gpt2_bfloat16_flash_nodisopt_4 | 4.799593 | 3107.03 | 1648.78 | 2715.00 | 2744.89 | 8 | 4 | 1 | 1 | -| gpt2_bfloat16_noflash_nodisopt_4 | 4.788435 | 4551.77 | 1308.67 | 2713.00 | 2752.00 | 8 | 4 | 1 | 1 | -| gpt2_bfloat16_flash_nodisopt_5 | 4.799967 | 3734.20 | 1372.00 | 2398.00 | 2428.44 | 8 | 4 | 1 | 1 | -| gpt2_bfloat16_noflash_nodisopt_5 | 4.791135 | 4016.84 | 1275.78 | 2396.00 | 2464.00 | 8 | 4 | 1 | 1 | -| gpt2_bfloat16_flash_nodisopt_6 | 4.800104 | 4197.89 | 1220.33 | 1718.89 | 1820.44 | 8 | 1 | 1 | 8 | -| gpt2_bfloat16_noflash_nodisopt_6 | 4.788296 | 4675.14 | 1108.78 | 1684.00 | 1777.78 | 8 | 1 | 1 | 8 | -| gpt2_bfloat16_flash_nodisopt_7 | 4.800298 | 5575.07 | 918.44 | 2684.00 | 2805.33 | 4 | 1 | 1 | 4 | -| gpt2_bfloat16_noflash_nodisopt_7 | 4.788042 | 6254.28 | 823.00 | 2586.00 | 2705.78 | 4 | 1 | 1 | 4 | -| gpt2_bfloat16_flash_nodisopt_8 | 4.798747 | 2089.50 | 2453.44 | 2373.22 | 2510.22 | 8 | 2 | 1 | 2 | -| gpt2_bfloat16_noflash_nodisopt_8 | 4.790910 | 2150.97 | 2381.11 | 2326.00 | 2478.22 | 8 | 2 | 1 | 2 | -| gpt2_fp32_flash_disopt_3 | 4.836303 | 2215.36 | 2313.33 | 3626.22 | 3672.89 | 8 | 1 | 1 | 1 | -| gpt2_fp32_noflash_disopt_3 | 4.826354 | 2513.86 | 2037.56 | 3467.00 | 3520.00 | 8 | 1 | 1 | 1 | -| gpt2_fp32_flash_disopt_4 | 4.836304 | 2652.36 | 1933.11 | 2934.00 | 2961.78 | 8 | 4 | 1 | 1 | -| gpt2_fp32_noflash_disopt_4 | 4.826354 | 2913.33 | 1760.11 | 2774.00 | 2816.00 | 8 | 4 | 1 | 1 | -| gpt2_fp32_flash_disopt_5 | 4.836836 | 3305.40 | 1549.78 | 2616.89 | 2666.67 | 8 | 4 | 1 | 1 | -| gpt2_fp32_noflash_disopt_5 | 4.829048 | 3928.89 | 1378.33 | 2457.00 | 2496.00 | 8 | 4 | 1 | 1 | -| gpt2_fp32_flash_disopt_8 | 4.836634 | 1838.23 | 2786.78 | 2441.67 | 2535.11 | 8 | 2 | 1 | 2 | -| gpt2_fp32_noflash_disopt_8 | 4.827999 | 2004.15 | 2557.78 | 2350.00 | 2435.56 | 8 | 2 | 1 | 2 | -| gpt2_fp32_flash_nodisopt_3 | 4.836303 | 2535.03 | 2020.78 | 3626.22 | 3662.22 | 8 | 1 | 1 | 1 | -| gpt2_fp32_noflash_nodisopt_3 | 4.826354 | 2810.67 | 1822.44 | 3467.00 | 3520.00 | 8 | 1 | 1 | 1 | -| gpt2_fp32_flash_nodisopt_4 | 4.836304 | 2817.74 | 1818.22 | 2934.00 | 2954.67 | 8 | 4 | 1 | 1 | -| gpt2_fp32_noflash_nodisopt_4 | 4.826354 | 3049.61 | 1679.67 | 2774.00 | 2816.00 | 8 | 4 | 1 | 1 | -| gpt2_fp32_flash_nodisopt_5 | 4.836836 | 3392.83 | 1509.67 | 2616.89 | 2656.00 | 8 | 4 | 1 | 1 | -| gpt2_fp32_noflash_nodisopt_5 | 4.829048 | 3656.74 | 1400.67 | 2457.00 | 2496.00 | 8 | 4 | 1 | 1 | -| gpt2_fp32_flash_nodisopt_6 | 4.836304 | 3746.21 | 1378.67 | 2318.44 | 2368.00 | 8 | 1 | 1 | 8 | -| gpt2_fp32_noflash_nodisopt_6 | 4.826354 | 3947.83 | 1297.11 | 2248.00 | 2307.56 | 8 | 1 | 1 | 8 | -| gpt2_fp32_flash_nodisopt_7 | 4.836304 | 5285.93 | 980.22 | 3538.56 | 3587.56 | 4 | 1 | 1 | 4 | -| gpt2_fp32_noflash_nodisopt_7 | 4.826354 | 5558.65 | 921.22 | 3345.00 | 3416.89 | 4 | 1 | 1 | 4 | -| gpt2_fp32_flash_nodisopt_8 | 4.836634 | 3332.54 | 1536.78 | 2441.67 | 2531.56 | 8 | 2 | 1 | 2 | -| gpt2_fp32_noflash_nodisopt_8 | 4.827999 | 3560.96 | 1438.22 | 2350.00 | 2424.89 | 8 | 2 | 1 | 2 | -| llama3_bfloat16_flash_disopt_3 | 11.513694 | 6985.97 | 733.22 | 21899.89 | 22151.11 | 8 | 1 | 1 | 1 | -| llama3_bfloat16_noflash_disopt_3 | 3.687185 | 8803.02 | 587.44 | 21632.00 | 21824.00 | 8 | 1 | 1 | 1 | -| llama3_bfloat16_flash_disopt_4 | 12.108337 | 7048.58 | 726.67 | 13019.00 | 13258.67 | 8 | 4 | 1 | 1 | -| llama3_bfloat16_noflash_disopt_4 | 3.687813 | 8703.07 | 591.22 | 12995.00 | 13120.00 | 8 | 4 | 1 | 1 | -| llama3_bfloat16_flash_disopt_5 | 12.330883 | 7956.26 | 643.60 | 10984.60 | 11155.20 | 8 | 4 | 1 | 1 | -| llama3_bfloat16_noflash_disopt_5 | 3.687082 | 9673.23 | 530.89 | 10880.00 | 11008.00 | 8 | 4 | 1 | 1 | -| llama3_bfloat16_flash_disopt_8 | 12.023618 | 4801.31 | 1066.50 | 9924.00 | 10032.00 | 8 | 2 | 1 | 2 | -| llama3_bfloat16_noflash_disopt_8 | 3.687491 | 5915.92 | 873.44 | 10031.00 | 10122.67 | 8 | 2 | 1 | 2 | -| llama3_bfloat16_flash_nodisopt_3 | 11.473671 | 7028.92 | 731.67 | 31903.11 | 32156.44 | 8 | 1 | 1 | 1 | -| llama3_bfloat16_noflash_nodisopt_3 | 3.687393 | 10063.60 | 529.22 | 31636.00 | 31808.00 | 8 | 1 | 1 | 1 | -| llama3_bfloat16_flash_nodisopt_4 | 12.120937 | 7424.95 | 696.17 | 14570.17 | 14810.67 | 8 | 4 | 1 | 1 | -| llama3_bfloat16_noflash_nodisopt_4 | 3.687764 | 9143.74 | 563.22 | 14425.00 | 14560.00 | 8 | 4 | 1 | 1 | -| llama3_bfloat16_flash_nodisopt_5 | 11.832385 | 8049.24 | 636.33 | 12454.50 | 12624.00 | 8 | 4 | 1 | 1 | -| llama3_bfloat16_noflash_nodisopt_5 | 3.687394 | 9607.02 | 533.00 | 12309.00 | 12448.00 | 8 | 4 | 1 | 1 | -| llama3_bfloat16_flash_nodisopt_6 | 11.109162 | 9080.24 | 570.11 | 11371.89 | 11516.44 | 8 | 1 | 1 | 8 | -| llama3_bfloat16_noflash_nodisopt_6 | 3.687855 | 9991.38 | 515.89 | 11052.00 | 11175.11 | 8 | 1 | 1 | 8 | -| llama3_bfloat16_flash_nodisopt_7 | 12.343384 | 12997.81 | 394.00 | 13956.00 | 14080.00 | 4 | 1 | 1 | 4 | -| llama3_bfloat16_noflash_nodisopt_7 | 3.687177 | 16637.25 | 309.11 | 14608.00 | 14759.11 | 4 | 1 | 1 | 4 | -| llama3_bfloat16_flash_nodisopt_8 | 11.242361 | 5281.60 | 975.78 | 11777.67 | 11868.44 | 8 | 2 | 1 | 2 | -| llama3_bfloat16_noflash_nodisopt_8 | 3.687432 | 5902.93 | 868.11 | 11460.00 | 11559.11 | 8 | 2 | 1 | 2 | -| llama3_fp32_flash_disopt_3 | 3.696688 | 6454.74 | 797.11 | 22056.33 | 22275.56 | 8 | 1 | 1 | 1 | -| llama3_fp32_noflash_disopt_3 | 3.595062 | 8042.78 | 640.56 | 21789.00 | 21952.00 | 8 | 1 | 1 | 1 | -| llama3_fp32_flash_disopt_4 | 3.696686 | 8594.09 | 650.11 | 13419.33 | 13575.11 | 8 | 4 | 1 | 1 | -| llama3_fp32_noflash_disopt_4 | 3.595074 | 8257.09 | 620.44 | 13152.00 | 13216.00 | 8 | 4 | 1 | 1 | -| llama3_fp32_flash_disopt_5 | 3.696680 | 7835.74 | 656.33 | 11303.67 | 11459.56 | 8 | 4 | 1 | 1 | -| llama3_fp32_noflash_disopt_5 | 3.595054 | 10612.78 | 497.89 | 11036.00 | 11168.00 | 8 | 4 | 1 | 1 | -| llama3_fp32_flash_disopt_8 | 3.696683 | 4918.54 | 1041.22 | 11005.33 | 11114.67 | 8 | 2 | 1 | 2 | -| llama3_fp32_noflash_disopt_8 | 3.595056 | 5989.85 | 863.00 | 10798.00 | 10915.56 | 8 | 2 | 1 | 2 | -| llama3_fp32_flash_nodisopt_3 | 3.696689 | 8095.32 | 641.44 | 32059.89 | 32280.89 | 8 | 1 | 1 | 1 | -| llama3_fp32_noflash_nodisopt_3 | 3.595062 | 8868.17 | 580.44 | 31792.00 | 31936.00 | 8 | 1 | 1 | 1 | -| llama3_fp32_flash_nodisopt_4 | 3.696686 | 7001.37 | 736.89 | 14848.67 | 15000.89 | 8 | 4 | 1 | 1 | -| llama3_fp32_noflash_nodisopt_4 | 3.595074 | 8305.80 | 616.78 | 14581.00 | 14656.00 | 8 | 4 | 1 | 1 | -| llama3_fp32_flash_nodisopt_5 | 3.696680 | 7993.27 | 643.22 | 12732.89 | 12892.44 | 8 | 4 | 1 | 1 | -| llama3_fp32_noflash_nodisopt_5 | 3.595054 | 9290.61 | 551.33 | 12465.00 | 12608.00 | 8 | 4 | 1 | 1 | -| llama3_fp32_flash_nodisopt_6 | 3.696689 | 7677.79 | 671.67 | 13351.89 | 13475.56 | 8 | 1 | 1 | 8 | -| llama3_fp32_noflash_nodisopt_6 | 3.595062 | 8936.41 | 573.00 | 13119.00 | 13219.56 | 8 | 1 | 1 | 8 | -| llama3_fp32_flash_nodisopt_7 | 3.696688 | 12469.65 | 411.11 | 18195.78 | 18318.22 | 4 | 1 | 1 | 4 | -| llama3_fp32_noflash_nodisopt_7 | 3.595062 | 15277.34 | 335.89 | 17756.00 | 17873.78 | 4 | 1 | 1 | 4 | -| llama3_fp32_flash_nodisopt_8 | 3.696683 | 5354.28 | 974.67 | 12434.33 | 12551.11 | 8 | 2 | 1 | 2 | -| llama3_fp32_noflash_nodisopt_8 | 3.595056 | 7652.40 | 708.56 | 12227.00 | 12352.00 | 8 | 2 | 1 | 2 | \ No newline at end of file diff --git a/scripts/results_speedup.md b/scripts/results_speedup.md deleted file mode 100644 index 6660ec94..00000000 --- a/scripts/results_speedup.md +++ /dev/null @@ -1,42 +0,0 @@ -| Configuration | Speedup Ratio | Flash Time (ms) | Noflash Time (ms) | -|---------------|---------------|-----------------|-------------------| -| gpt2_bfloat16_disopt_3 | 1.10 | 2494.86 | 2741.77 | -| gpt2_bfloat16_disopt_4 | 1.11 | 2900.71 | 3211.90 | -| gpt2_bfloat16_disopt_5 | 1.15 | 3520.25 | 4036.41 | -| gpt2_bfloat16_disopt_8 | 1.06 | 2000.08 | 2128.22 | -| gpt2_bfloat16_nodisopt_3 | 1.08 | 2901.57 | 3130.48 | -| gpt2_bfloat16_nodisopt_4 | 1.46 | 3107.03 | 4551.77 | -| gpt2_bfloat16_nodisopt_5 | 1.08 | 3734.20 | 4016.84 | -| gpt2_bfloat16_nodisopt_6 | 1.11 | 4197.89 | 4675.14 | -| gpt2_bfloat16_nodisopt_7 | 1.12 | 5575.07 | 6254.28 | -| gpt2_bfloat16_nodisopt_8 | 1.03 | 2089.50 | 2150.97 | -| gpt2_fp32_disopt_3 | 1.13 | 2215.36 | 2513.86 | -| gpt2_fp32_disopt_4 | 1.10 | 2652.36 | 2913.33 | -| gpt2_fp32_disopt_5 | 1.19 | 3305.40 | 3928.89 | -| gpt2_fp32_disopt_8 | 1.09 | 1838.23 | 2004.15 | -| gpt2_fp32_nodisopt_3 | 1.11 | 2535.03 | 2810.67 | -| gpt2_fp32_nodisopt_4 | 1.08 | 2817.74 | 3049.61 | -| gpt2_fp32_nodisopt_5 | 1.08 | 3392.83 | 3656.74 | -| gpt2_fp32_nodisopt_6 | 1.05 | 3746.21 | 3947.83 | -| gpt2_fp32_nodisopt_7 | 1.05 | 5285.93 | 5558.65 | -| gpt2_fp32_nodisopt_8 | 1.07 | 3332.54 | 3560.96 | -| llama3_bfloat16_disopt_3 | 1.26 | 6985.97 | 8803.02 | -| llama3_bfloat16_disopt_4 | 1.23 | 7048.58 | 8703.07 | -| llama3_bfloat16_disopt_5 | 1.22 | 7956.26 | 9673.23 | -| llama3_bfloat16_disopt_8 | 1.23 | 4801.31 | 5915.92 | -| llama3_bfloat16_nodisopt_3 | 1.43 | 7028.92 | 10063.60 | -| llama3_bfloat16_nodisopt_4 | 1.23 | 7424.95 | 9143.74 | -| llama3_bfloat16_nodisopt_5 | 1.19 | 8049.24 | 9607.02 | -| llama3_bfloat16_nodisopt_6 | 1.10 | 9080.24 | 9991.38 | -| llama3_bfloat16_nodisopt_7 | 1.28 | 12997.81 | 16637.25 | -| llama3_bfloat16_nodisopt_8 | 1.12 | 5281.60 | 5902.93 | -| llama3_fp32_disopt_3 | 1.25 | 6454.74 | 8042.78 | -| llama3_fp32_disopt_4 | 0.96 | 8594.09 | 8257.09 | -| llama3_fp32_disopt_5 | 1.35 | 7835.74 | 10612.78 | -| llama3_fp32_disopt_8 | 1.22 | 4918.54 | 5989.85 | -| llama3_fp32_nodisopt_3 | 1.10 | 8095.32 | 8868.17 | -| llama3_fp32_nodisopt_4 | 1.19 | 7001.37 | 8305.80 | -| llama3_fp32_nodisopt_5 | 1.16 | 7993.27 | 9290.61 | -| llama3_fp32_nodisopt_6 | 1.16 | 7677.79 | 8936.41 | -| llama3_fp32_nodisopt_7 | 1.23 | 12469.65 | 15277.34 | -| llama3_fp32_nodisopt_8 | 1.43 | 5354.28 | 7652.40 | \ No newline at end of file diff --git a/scripts/test_config.json b/scripts/test_config.json index f8171b3a..3d66ff7b 100644 --- a/scripts/test_config.json +++ b/scripts/test_config.json @@ -10,6 +10,11 @@ "COMPARE_LOG_DIR": "" }, "builds": [ + { + "id": "build_1", + "profile": false, + "cmd": "cmake -DUSE_CUDA=ON -DUSE_NCCL=ON .. && make -j" + }, { "id": "build_2", "profile": true, From 3e4a1ce4d94d87af85c44d0c45555fc679c3e253 Mon Sep 17 00:00:00 2001 From: Aoshine999 <820459542@qq.com> Date: Mon, 16 Mar 2026 15:49:49 +0800 Subject: [PATCH 14/18] delete test file --- flash_atten.md | 597 ------------------------------ scripts/extract_training_stats.py | 507 ------------------------- 2 files changed, 1104 deletions(-) delete mode 100644 flash_atten.md delete mode 100755 scripts/extract_training_stats.py diff --git a/flash_atten.md b/flash_atten.md deleted file mode 100644 index d42f321c..00000000 --- a/flash_atten.md +++ /dev/null @@ -1,597 +0,0 @@ -下面我把 **FlashAttention-2 backward pass 的完整执行流程**整理成一份可直接使用的 **Markdown 文档**。内容会分成两层: - -1. **论文级算法流程**:对应 FlashAttention-2 的标准 backward 思路。 -2. **CUDA 实现级执行流程**:结合你当前的 kernel 设计 - `grid_dims(num_heads_kv, batch_size, Tc), block_dims(Tc, Tr)` - 说明一个 block 实际在做什么。 - -论文中 backward 的核心点包括:将 (Q) 按行块、(K/V) 按列块切分;先计算 (D = \mathrm{rowsum}(dO \circ O));随后固定一个 (K_j,V_j) 列块,遍历所有 (Q_i) 行块,重算 (S_{ij})、(P_{ij})、(dP_{ij})、(dS_{ij}),并更新 (dV_j,dK_j,dQ_i)。另外,FlashAttention-2 在 backward 中只保存 **row-wise logsumexp (L)**,而不是同时保存 row-max 和 row-sum。 - ---- - -# FlashAttention-2 Backward Pass 执行流程 - -## 1. 问题定义 - -给定前向中已经得到的: - -* (Q \in \mathbb{R}^{N_q \times d}) -* (K \in \mathbb{R}^{N_k \times d}) -* (V \in \mathbb{R}^{N_k \times d}) -* (O \in \mathbb{R}^{N_q \times d}) -* (L \in \mathbb{R}^{N_q}),其中 (L) 是每一行 softmax 的 `logsumexp` -* 上游梯度 (dO \in \mathbb{R}^{N_q \times d}) - -要求计算: - -* (dQ \in \mathbb{R}^{N_q \times d}) -* (dK \in \mathbb{R}^{N_k \times d}) -* (dV \in \mathbb{R}^{N_k \times d}) - -FlashAttention-2 backward 的基本思想不是存整张 attention matrix,而是 **按 tile 重算** 中间量,从而降低 HBM 读写。论文的 Algorithm 2 先把 (Q,O,dO,L) 切成行块,把 (K,V) 切成列块,然后以列块为外循环执行 backward。 - ---- - -## 2. 数学公式 - -### 2.1 前向关系 - -对任意一个 tile ((i,j)): - -[ -S_{ij} = Q_i K_j^T -] - -若启用缩放: - -[ -S_{ij} \leftarrow \text{scale} \cdot S_{ij} -] - -softmax 概率: - -[ -P_{ij} = \exp(S_{ij} - L_i) -] - -这里 (L_i) 是该行块对应的 row-wise logsumexp。FlashAttention-2 backward 只依赖 (L),这是它相对早期实现的一个简化。 - -输出满足: - -[ -O_i = \sum_j P_{ij} V_j -] - ---- - -### 2.2 backward 关键中间量 - -先定义: - -[ -D = \mathrm{rowsum}(dO \circ O) -] - -即对每一行做逐元素乘法后求和。论文明确把这一步放在主 backward 循环之前,并写回 HBM。 - -对任意 tile ((i,j)): - -1. 先计算 - [ - dP_{ij} = dO_i V_j^T - ] - -2. 再做 softmax backward - [ - dS_{ij} = P_{ij} \circ (dP_{ij} - D_i) - ] - -3. 然后得到三个输入梯度的 tile 贡献: - -[ -dV_j \mathrel{+}= P_{ij}^T dO_i -] - -[ -dK_j \mathrel{+}= dS_{ij}^T Q_i -] - -[ -dQ_i \mathrel{+}= dS_{ij} K_j -] - -这些正是论文 Algorithm 2 的核心更新过程。 - ---- - -## 3. Tile 划分 - -设: - -* 行块大小为 (B_r) -* 列块大小为 (B_c) - -则: - -* (Q) 被分成 (T_r = \lceil N_q / B_r \rceil) 个行块 -* (K,V) 被分成 (T_c = \lceil N_k / B_c \rceil) 个列块 - -论文中明确写出: - -* (Q_1,\dots,Q_{T_r}) -* (K_1,\dots,K_{T_c}) -* (V_1,\dots,V_{T_c}) -* (O_i,dO_i,L_i,D_i,dQ_i) 也都按行块划分 -* (dK_j,dV_j) 按列块划分。 - ---- - -## 4. 论文级 backward 总流程 - -## Step 0:输入准备 - -从前向保留下来: - -* `Q, K, V, O` -* `dO` -* `L = logsumexp` -* 以及可选的 `dropout_seed` - -初始化输出: - -* `dQ = 0` -* `dK = 0` -* `dV = 0` - ---- - -## Step 1:预计算 (D) - -对每一行计算: - -[ -D_i = \sum_{m=1}^{d} dO_{i,m} \cdot O_{i,m} -] - -并写回全局内存。 - -### 作用 - -这是 softmax backward 所需的行级归约项,用来避免显式构造整张 Jacobian。论文把它作为 backward 前处理步骤。 - ---- - -## Step 2:外层遍历列块 (j) - -对每个列块 (j = 1,\dots,T_c): - -1. 从 HBM 载入 (K_j, V_j) 到 on-chip SRAM / shared memory -2. 在 shared memory 或寄存器中初始化本列块的局部梯度: - - * (dK_j = 0) - * (dV_j = 0) - -论文 Algorithm 2 就是以列块为外层循环,并在每个 (j) 开始时把 `K_j, V_j` 加载到 SRAM,然后初始化 `dK_j, dV_j`。 - ---- - -## Step 3:内层遍历行块 (i) - -对每个行块 (i = 1,\dots,T_r): - -### 3.1 载入当前行块数据 - -从 HBM 载入: - -* (Q_i) -* (O_i)(若需要本 kernel 内使用) -* (dO_i) -* (L_i) -* (D_i) - ---- - -### 3.2 重算 score tile - -重算当前 tile 的 attention score: - -[ -S_{ij} = Q_i K_j^T -] - -若设置 `scale`: - -[ -S_{ij} \leftarrow \text{scale} \cdot S_{ij} -] - -若启用 `causal mask`,则对非法位置做 mask: - -[ -S_{ij}(r,c) = -\infty -\quad \text{if } k_idx > q_idx -] - -其中: - -* `q_idx = i * B_r + r` -* `k_idx = j * B_c + c` - -### 说明 - -这一步没有从全局内存读取旧的 attention matrix,而是 **重算**。这是 FlashAttention backward 相比普通实现最关键的节省显存策略。 - ---- - -### 3.3 重算概率 tile - -根据保存的 `logsumexp`: - -[ -P_{ij} = \exp(S_{ij} - L_i) -] - -若启用 `dropout`,则要重建前向时相同的 dropout mask,并应用: - -[ -P_{ij}^{drop} = \frac{M_{ij} \circ P_{ij}}{1-p} -] - -其中: - -* (M_{ij}) 为基于同一随机种子重建的二值 mask -* (p) 为 dropout probability - -如果没有 dropout,则: - -[ -P_{ij}^{drop} = P_{ij} -] - ---- - -### 3.4 计算 (dV_j) 的当前 tile 贡献 - -[ -dV_j \mathrel{+}= (P_{ij}^{drop})^T dO_i -] - -### 解释 - -因为前向输出是 (O_i = \sum_j P_{ij}V_j),所以对 (V_j) 的梯度就是概率矩阵转置乘以上游梯度。 - ---- - -### 3.5 计算 (dP_{ij}) - -[ -dP_{ij} = dO_i V_j^T -] - -若启用 dropout,则 backward 中也要乘上同样的 dropout mask,并带上缩放因子: - -[ -dP_{ij} \leftarrow \frac{M_{ij} \circ dP_{ij}}{1-p} -] - ---- - -### 3.6 计算 softmax backward 的 (dS_{ij}) - -[ -dS_{ij} = P_{ij} \circ (dP_{ij} - D_i) -] - -注意这里使用的是 **未 dropout 前的 softmax 概率** (P_{ij}),而 `dP` 则需要反映 dropout 后的链式法则。 - -这一步是整套 backward 中最核心的公式,因为 softmax 的 Jacobian 已经被压缩成逐元素乘法加一个行级标量 (D_i)。 - ---- - -### 3.7 计算 (dK_j) 的当前 tile 贡献 - -[ -dK_j \mathrel{+}= dS_{ij}^T Q_i -] - -若前向有 `scale`,则这里还要乘上相同的缩放因子对梯度链路进行修正。更准确地说,如果前向做的是: - -[ -S = \text{scale} \cdot QK^T -] - -那么由 (S) 向 (Q,K) 回传的梯度都要乘 `scale`。 - ---- - -### 3.8 计算 (dQ_i) 的当前 tile 贡献 - -[ -dQ_i \mathrel{+}= dS_{ij} K_j -] - -这一步会累加来自所有列块 (j) 的贡献,因此: - -* 若一个 block 只处理单个列块,那么 `dQ_i` 往往需要原子加到全局内存 -* 或者先在 block 内做局部累加,再统一写回 - ---- - -## Step 4:写回本列块的 (dK_j,dV_j) - -当所有行块 (i) 都遍历完后,本列块 (j) 的局部梯度已经累加完成: - -* 写回 `dK_j` -* 写回 `dV_j` - -由于一个列块 (j) 通常只由一个 block 负责,所以这两者一般不需要跨 block 再归约;但若你在 GQA 下让多个 query-head block 共同更新同一组 kv-head,也可能需要额外归约或原子加。 - ---- - -## Step 5:结束条件 - -当所有列块 (j) 处理完成后: - -* `dQ` -* `dK` -* `dV` - -全部得到。 - ---- - -# 5. 结合你当前 CUDA 设计的执行流程 - -你当前 kernel 为: - -```cpp -FlashAttentionBackwardKernel<<>>( - grad_query_ptr, grad_key_ptr, grad_value_ptr, - query_ptr, key_ptr, value_ptr, output_ptr, grad_output_ptr, - logsumexp_ptr, dropout_seed_value, attn_mask_ptr, - scale, is_causal, dropout_p, enable_gqa, - batch_size, seq_len_q, seq_len_k, num_heads, num_heads_kv, head_dim); -``` - -其中: - -* `grid_dims(num_heads_kv, batch_size, Tc)` -* `block_dims(Tc, Tr)` - -这表示: - -* `blockIdx.x`:对应一个 `kv head` -* `blockIdx.y`:对应一个 `batch` -* `blockIdx.z`:对应一个列块 (j) - -也就是说,**一个 block 固定负责某个 batch、某个 kv-head、某个列块 (j)**。 - ---- - -## 5.1 一个 block 的职责 - -对给定的: - -* `batch = blockIdx.y` -* `kv_head = blockIdx.x` -* `col_tile = blockIdx.z` - -该 block 需要: - -1. 加载当前 `K_j, V_j` -2. 初始化局部 `dK_j, dV_j` -3. 遍历所有相关的 query row tiles (i) -4. 对每个 (i): - - * 找到对应的 query-head(若启用 GQA,要从 q-head 映射到 kv-head) - * 载入 `Q_i, dO_i, L_i, D_i` - * 重算 `S_ij` - * 应用 causal/mask/dropout - * 计算 `P_ij, dP_ij, dS_ij` - * 累加到 `dV_j, dK_j` - * 把对 `dQ_i` 的贡献写回全局 -5. 行块遍历结束后,写回 `dK_j, dV_j` - ---- - -## 5.2 GQA 下的 head 映射 - -如果启用了 GQA: - -* `num_heads` 是 query heads 数 -* `num_heads_kv` 是 kv heads 数 -* 每个 kv head 会对应若干个 query heads - -常见映射方式为: - -[ -q_head_start = kv_head \times \frac{num_heads}{num_heads_kv} -] - -[ -q_head_end = (kv_head+1) \times \frac{num_heads}{num_heads_kv} -] - -于是当前 block 除了遍历 row tiles (i),还需要遍历属于这个 kv-head 组的所有 q-head。这样同一个 `K_j,V_j` 会被多个 q-head 共享使用,而 `dK_j,dV_j` 也要把这些 q-head 的贡献全部累加起来。 - -这与论文中提到的 MQA/GQA backward 需要对共享的 K/V head 梯度求和是一致的。 - ---- - -## 5.3 causal mask 的执行方式 - -若 `is_causal = true`: - -对于 tile 内每个元素 ((r,c)),若其全局位置满足: - -[ -k_global > q_global -] - -则该位置无效。 - -实现上通常有两层优化: - -1. **整块跳过**: - 若当前列块完全在当前行块右侧,则整个 tile 不必计算 - -2. **边界块细粒度判断**: - 若 tile 横跨对角线,则对元素级做 mask - -这样可以减少无效 matmul 和 exp 计算。 - ---- - -## 5.4 dropout 的执行方式 - -若 `dropout_p > 0`: - -1. 使用 `dropout_seed_value` 和 tile 内元素索引重建与 forward 一致的随机数序列 -2. 得到二值 mask -3. 对 `P_ij` 和 `dP_ij` 执行同样的 mask/缩放 - -这样 backward 就与前向使用的是同一组保留位置。 - ---- - -## 5.5 scale 的执行方式 - -若 `scale != 1`: - -前向 score 为: - -[ -S = \text{scale} \cdot QK^T -] - -那么 backward 中: - -* 重算 `S_ij` 时要乘 `scale` -* 从 `dS` 回传到 `dQ,dK` 时也要体现这条链路 - -通常最直接的实现是: - -* 先在重算 `S_ij` 时乘 `scale` -* 在 `dQ = dS K`、`dK = dS^T Q` 前再统一乘 `scale` -* 或者直接把 `scale` 融进 `dS` - -两种写法本质等价,但要保证链式法则只乘一次,不要漏乘或重复乘。 - ---- - -# 6. 一个推荐的伪代码版本 - -```markdown -for each batch b: - for each kv head h_kv: - for each column tile j: - load K_j, V_j into shared memory - initialize local dK_j = 0, dV_j = 0 - - determine all query heads h_q that map to h_kv (for GQA) - - for each query head h_q in group(h_kv): - for each row tile i: - load Q_i, dO_i, L_i, D_i - optionally load O_i if D is not precomputed - - recompute S_ij = Q_i @ K_j^T - if scale is enabled: - S_ij *= scale - - if causal mask is enabled: - mask invalid positions in S_ij - - P_ij = exp(S_ij - L_i) - - if dropout is enabled: - reconstruct dropout mask M_ij - P_drop = M_ij * P_ij / (1 - p) - else: - P_drop = P_ij - - dV_j += P_drop^T @ dO_i - - dP_ij = dO_i @ V_j^T - if dropout is enabled: - dP_ij = M_ij * dP_ij / (1 - p) - - dS_ij = P_ij * (dP_ij - D_i) - - if scale is enabled: - dS_ij *= scale - - dK_j += dS_ij^T @ Q_i - dQ_i += dS_ij @ K_j - - write or atomically accumulate dQ_i to global memory - - write dK_j, dV_j to global memory -``` - ---- - -# 7. 实现时的关键注意点 - -## 7.1 `D = rowsum(dO ∘ O)` 最好预处理 - -论文就是这么做的。这样主 backward kernel 只需读取 `D_i`,不用再额外读取 `O_i` 并做一次按行归约,能减轻主 kernel 负担。 - ---- - -## 7.2 backward 的 matmul 数量更多 - -论文 benchmark 中指出 backward 由于重算,等价上有更多 matmul,因此 FLOPs 估计通常约为 forward 的 2.5 倍。 - ---- - -## 7.3 FlashAttention-2 backward 仍然强调避免 split-K - -论文在 warp 工作划分部分提到,backward 也尽量避免 “split-K” 方案,从而减少 shared memory 读写和同步,尽管 backward 的依赖关系比 forward 更复杂。 - ---- - -## 7.4 block 大小要兼顾 shared memory 和寄存器压力 - -论文提到 block size 变大虽然能减少 shared memory 读写,但也会提高寄存器与 shared memory 占用,过大会导致 spilling 或 kernel 根本无法启动。常见 tile 大小会在 `{64,128}` 范围内手工调优。 - ---- - -# 8. 最终总结 - -FlashAttention-2 backward pass 的本质可以概括为: - -1. **先预处理行级统计量** - - * 计算 (D = \mathrm{rowsum}(dO \circ O)) - -2. **按列块固定 `K_j, V_j`** - - * 让 `K_j,V_j` 常驻 shared memory - -3. **遍历所有行块 `Q_i`** - - * 重算 (S_{ij}) - * 用保存的 `L_i` 恢复 (P_{ij}) - * 计算 (dP_{ij}, dS_{ij}) - -4. **逐 tile 累加三个输入梯度** - - * (dV_j += P_{ij}^T dO_i) - * (dK_j += dS_{ij}^T Q_i) - * (dQ_i += dS_{ij} K_j) - -5. **支持附加功能** - - * causal:在 tile 内做位置裁剪 - * dropout:重建相同随机 mask - * scale:在 `S` 与其梯度链中保持一致 - * GQA:让多个 q-head 共享同一组 kv-head,并对 `dK/dV` 做组内累加 - ---- - -你要是愿意,我下一步可以直接把这份内容继续整理成一份更正式的文档格式,比如: - -* **“设计说明书风格” Markdown** -* **“伪代码 + 公式 + CUDA 映射”版** -* 或者直接给你写成 **项目里的 README 小节**。 diff --git a/scripts/extract_training_stats.py b/scripts/extract_training_stats.py deleted file mode 100755 index d4949cc1..00000000 --- a/scripts/extract_training_stats.py +++ /dev/null @@ -1,507 +0,0 @@ -#!/usr/bin/env python3 -""" -Extract training statistics from log files and display them in a table. - -This script parses log files from training runs and extracts statistics such as -loss, time, tokens per second, and memory usage. It can compare -results from different configurations (e.g., flash vs noflash attention) -and calculate speedup ratios. - -Usage: - python extract_training_stats.py [options] - -Arguments: - log_dir1: First log directory (typically contains noflash results) - log_dir2: Second log directory (typically contains flash results) - -Options: - --threshold-fp32: Loss difference threshold for fp32 (default: 1e-5) - --threshold-bf16: Loss difference threshold for bfloat16 (default: 1e-2) - --markdown: Output tables in markdown format - --output: Specify output file path (for markdown tables) - --speedup: Output speedup ratio table (noflash_time / flash_time) - -Examples: - # Display training statistics in plain text - python extract_training_stats.py logs compare_logs - - # Display training statistics in markdown format - python extract_training_stats.py logs compare_logs --markdown - - # Display training statistics in markdown and save to file - python extract_training_stats.py logs compare_logs --markdown --output results.md - - # Display speedup ratio table - python extract_training_stats.py logs compare_logs --speedup - - # Display speedup ratio table in markdown and save to file - python extract_training_stats.py logs compare_logs --speedup --markdown --output results.md - -Output: - The script outputs three types of information: - 1. Training Statistics Table: Detailed statistics for each configuration - 2. Comparison between flash and noflash: Comparison of loss, time, and memory - 3. Speedup Ratio Table (if --speedup is specified): Speedup ratios for each configuration -""" - -import re -import sys -from pathlib import Path -from argparse import ArgumentParser -from collections import defaultdict - - -def get_dtype_from_filename(filename): - """ - Determine dtype from filename. - - Args: - filename: Name of the log file - - Returns: - 'bfloat16' or 'fp32' - """ - return 'bfloat16' if '_bfloat16' in filename else 'fp32' - - -def get_flash_from_filename(filename, dir_path): - """ - Determine flash mode from filename and directory. - - Args: - filename: Name of the log file - dir_path: Path to the directory containing the log file - - Returns: - 'flash' or 'noflash' - """ - # First check if filename contains flash/noflash suffix - if '_flash' in filename: - return 'flash' - elif '_noflash' in filename: - return 'noflash' - # Otherwise, determine from directory name - dir_name = str(dir_path).lower() - if 'compare' in dir_name: - return 'flash' - else: - return 'noflash' - - -def get_disopt_from_filename(filename): - """Determine distributed optimizer from filename. Returns 'disopt' or 'nodisopt'.""" - return 'disopt' if '_distopt' in filename else 'nodisopt' - - -def get_model_from_filename(filename): - """Determine model name from filename. Returns 'gpt2' or 'llama3'.""" - if 'gpt2' in filename: - return 'gpt2' - elif 'llama3' in filename: - return 'llama3' - else: - return 'unknown' - - -def parse_command_line(line): - """Extract configuration from command line.""" - dp_match = re.search(r'--nthread_per_process=(\d+)', line) - dp = int(dp_match.group(1)) if dp_match else 1 - - tp_match = re.search(r'--tensor_parallel=(\d+)', line) - tp = int(tp_match.group(1)) if tp_match else 1 - - sp = 1 - if '--sequence_parallel' in line: - sp = 1 # SP is enabled, but value is not explicitly set - - pp_match = re.search(r'--pipeline_parallel=(\d+)', line) - pp = int(pp_match.group(1)) if pp_match else 1 - - disopt = '--use_distributed_optimizer' in line - - return { - 'dp': dp, - 'tp': tp, - 'sp': sp, - 'pp': pp, - 'disopt': disopt - } - - -def parse_log_file(file_path): - """ - Extract statistics from log file. - - Args: - file_path: Path to the log file - - Returns: - Dictionary containing lists of losses, times, tokens per second, - peak memory usage, and configuration - """ - stats = { - 'losses': [], - 'times': [], - 'toks_per_sec': [], - 'peak_used': [], - 'peak_reserved': [], - 'config': None - } - - with open(file_path, 'r') as f: - for line in f: - # Parse command line - if line.startswith('[COMMAND]'): - stats['config'] = parse_command_line(line) - continue - - # Parse step information - match = re.search(r'step\s+(\d+)/\d+\s+\|\s+train loss\s+([\d.]+)\s+\|\s+lr\s+[\d.e+-]+\s+\|\s+\(([\d.]+)\s+ms\s+\|\s+(\d+)\s+tok/s.*peak used:\s+(\d+)\s+MB.*peak reserved:\s+(\d+)\s+MB.*DP=(\d+),\s+TP=(\d+),\s+SP=(\d+),\s+PP=(\d+)', line) - if match: - step = int(match.group(1)) - loss = float(match.group(2)) - time_ms = float(match.group(3)) - tok_per_sec = int(match.group(4)) - peak_used = int(match.group(5)) - peak_reserved = int(match.group(6)) - dp = int(match.group(7)) - tp = int(match.group(8)) - sp = int(match.group(9)) - pp = int(match.group(10)) - - stats['losses'].append((step, loss)) - stats['times'].append((step, time_ms)) - stats['toks_per_sec'].append((step, tok_per_sec)) - stats['peak_used'].append((step, peak_used)) - stats['peak_reserved'].append((step, peak_reserved)) - - return stats - - -def calculate_averages(stats, exclude_first_step=True): - """ - Calculate average values, optionally excluding first step. - - Args: - stats: Dictionary containing lists of losses, times, tokens per second, - and peak memory usage - exclude_first_step: If True, exclude the first step from calculations - (default: True, as first step may have overhead) - - Returns: - Dictionary containing average values for loss, time, tokens per second, - and peak memory usage - """ - losses = stats['losses'] - times = stats['times'] - toks_per_sec = stats['toks_per_sec'] - peak_used = stats['peak_used'] - peak_reserved = stats['peak_reserved'] - - start_idx = 1 if exclude_first_step else 0 - - avg_loss = sum(loss for _, loss in losses[start_idx:]) / len(losses[start_idx:]) if len(losses[start_idx:]) > 0 else 0 - avg_time = sum(time for _, time in times[start_idx:]) / len(times[start_idx:]) if len(times[start_idx:]) > 0 else 0 - avg_tok_per_sec = sum(tok for _, tok in toks_per_sec[start_idx:]) / len(toks_per_sec[start_idx:]) if len(toks_per_sec[start_idx:]) > 0 else 0 - avg_peak_used = sum(mem for _, mem in peak_used[start_idx:]) / len(peak_used[start_idx:]) if len(peak_used[start_idx:]) > 0 else 0 - avg_peak_reserved = sum(mem for _, mem in peak_reserved[start_idx:]) / len(peak_reserved[start_idx:]) if len(peak_reserved[start_idx:]) > 0 else 0 - - return { - 'avg_loss': avg_loss, - 'avg_time_ms': avg_time, - 'avg_tok_per_sec': avg_tok_per_sec, - 'avg_peak_used': avg_peak_used, - 'avg_peak_reserved': avg_peak_reserved - } - - -def main(): - """ - Main function to extract and display training statistics. - - This function: - 1. Parses command line arguments - 2. Reads log files from two directories - 3. Extracts statistics from each log file - 4. Groups results by configuration - 5. Displays tables and comparisons - 6. Optionally displays speedup ratios - """ - parser = ArgumentParser(description='Extract training statistics from log files') - parser.add_argument('dir1', type=Path, help='First log directory') - parser.add_argument('dir2', type=Path, help='Second log directory') - parser.add_argument('--threshold-fp32', type=float, default=1e-5, help='Loss difference threshold for fp32 (default: 1e-5)') - parser.add_argument('--threshold-bf16', type=float, default=1e-2, help='Loss difference threshold for bfloat16 (default: 1e-2)') - parser.add_argument('--markdown', action='store_true', help='Output as markdown table') - parser.add_argument('--output', type=str, default='', help='Output file path (optional)') - parser.add_argument('--speedup', action='store_true', help='Output speedup ratio table') - args = parser.parse_args() - - # Get all log files from both directories - files1 = [(f, args.dir1) for f in args.dir1.glob('*.log') if not f.name.startswith('build') and 'comparison' not in f.name.lower()] - files2 = [(f, args.dir2) for f in args.dir2.glob('*.log') if not f.name.startswith('build') and 'comparison' not in f.name.lower()] - - # Combine files from both directories - all_files = files1 + files2 - - # Parse all files and collect statistics - results = [] - for file_path, dir_path in sorted(all_files, key=lambda x: (x[1], x[0].name)): - filename = file_path.name - model = get_model_from_filename(filename) - dtype = get_dtype_from_filename(filename) - flash = get_flash_from_filename(filename, dir_path) - disopt = get_disopt_from_filename(filename) - - # Extract test ID from filename (e.g., gpt2_3_bfloat16_distopt_profile.log -> 3) - test_id_match = re.search(r'_(\d+)_', filename) - test_id = test_id_match.group(1) if test_id_match else '' - - # Parse log file - stats = parse_log_file(file_path) - averages = calculate_averages(stats, exclude_first_step=True) - - # Get configuration - config = stats['config'] if stats['config'] else {} - dp = config.get('dp', 1) - tp = config.get('tp', 1) - sp = config.get('sp', 1) - pp = config.get('pp', 1) - - # Create row identifier (include test ID to distinguish different test configurations) - row_id = f"{model}_{dtype}_{flash}_{disopt}_{test_id}" - - results.append({ - 'row_id': row_id, - 'model': model, - 'dtype': dtype, - 'flash': flash, - 'disopt': disopt, - 'test_id': test_id, - 'avg_loss': averages['avg_loss'], - 'avg_time_ms': averages['avg_time_ms'], - 'avg_tok_per_sec': averages['avg_tok_per_sec'], - 'avg_peak_used': averages['avg_peak_used'], - 'avg_peak_reserved': averages['avg_peak_reserved'], - 'dp': dp, - 'tp': tp, - 'sp': sp, - 'pp': pp - }) - - # Sort results by row_id - results.sort(key=lambda x: x['row_id']) - - # Group by model, dtype, and disopt (excluding flash) - grouped_by_config = defaultdict(list) - for result in results: - key = f"{result['model']}_{result['dtype']}_{result['disopt']}" - grouped_by_config[key].append(result) - - # Output speedup ratio table if requested - if args.speedup: - # Only output speedup ratio table - pass - else: - # Prepare output - if args.markdown: - # Output as markdown table - markdown_lines = [] - - # Add header - markdown_lines.append("| Row ID | Avg Loss | Avg Time (ms) | Avg Tok/s | Peak Used (MB) | Peak Reserved (MB) | DP | TP | SP | PP |") - markdown_lines.append("|---------|----------|---------------|-----------|-----------------|-------------------|----|----|----|----|") - - # Print rows (flash and noflash interleaved) - for key in sorted(grouped_by_config.keys()): - group = grouped_by_config[key] - if len(group) >= 2: - # Separate flash and noflash results - flash_results = [r for r in group if r['flash'] == 'flash'] - noflash_results = [r for r in group if r['flash'] == 'noflash'] - - # Interleave flash and noflash results - for i in range(max(len(flash_results), len(noflash_results))): - if i < len(flash_results): - result = flash_results[i] - row = f"| {result['row_id']:<40} | {result['avg_loss']:<15.6f} | {result['avg_time_ms']:<15.2f} | {result['avg_tok_per_sec']:<15.2f} | {result['avg_peak_used']:<15.2f} | {result['avg_peak_reserved']:<15.2f} | {result['dp']:<5} | {result['tp']:<5} | {result['sp']:<5} | {result['pp']:<5} |" - markdown_lines.append(row) - if i < len(noflash_results): - result = noflash_results[i] - row = f"| {result['row_id']:<40} | {result['avg_loss']:<15.6f} | {result['avg_time_ms']:<15.2f} | {result['avg_tok_per_sec']:<15.2f} | {result['avg_peak_used']:<15.2f} | {result['avg_peak_reserved']:<15.2f} | {result['dp']:<5} | {result['tp']:<5} | {result['sp']:<5} | {result['pp']:<5} |" - markdown_lines.append(row) - else: - # Only one result, print it - for result in group: - row = f"| {result['row_id']:<40} | {result['avg_loss']:<15.6f} {result['avg_time_ms']:<15.2f} {result['avg_tok_per_sec']:<15.2f} {result['avg_peak_used']:<15.2f} | {result['avg_peak_reserved']:<15.2f} | {result['dp']:<5} | {result['tp']:<5} | {result['sp']:<5} | {result['pp']:<5} |" - markdown_lines.append(row) - - # Print markdown table - markdown_table = "\n".join(markdown_lines) - print(markdown_table) - - # Write to file if output path is specified - if args.output: - with open(args.output, 'w') as f: - f.write(markdown_table) - print(f"Markdown table saved to: {args.output}") - else: - # Print plain text table - print("\n" + "=" * 150) - print("Training Statistics Table (excluding step 1)") - print("=" * 150) - - # Print header - header = f"{'Row ID':<40} {'Avg Loss':<15} {'Avg Time (ms)':<15} {'Avg Tok/s':<15} {'Peak Used (MB)':<15} {'Peak Reserved (MB)':<15} {'DP':<5} {'TP':<5} {'SP':<5} {'PP':<5}" - print(header) - print("-" * 150) - - # Print rows (flash and noflash interleaved) - for key in sorted(grouped_by_config.keys()): - group = grouped_by_config[key] - if len(group) >= 2: - # Separate flash and noflash results - flash_results = [r for r in group if r['flash'] == 'flash'] - noflash_results = [r for r in group if r['flash'] == 'noflash'] - - # Interleave flash and noflash results - for i in range(max(len(flash_results), len(noflash_results))): - if i < len(flash_results): - result = flash_results[i] - row = f"{result['row_id']:<40} {result['avg_loss']:<15.6f} {result['avg_time_ms']:<15.2f} {result['avg_tok_per_sec']:<15.2f} {result['avg_peak_used']:<15.2f} {result['avg_peak_reserved']:<15.2f} {result['dp']:<5} {result['tp']:<5} {result['sp']:<5} {result['pp']:<5}" - print(row) - if i < len(noflash_results): - result = noflash_results[i] - row = f"{result['row_id']:<40} {result['avg_loss']:<15.6f} {result['avg_time_ms']:<15.2f} {result['avg_tok_per_sec']:<15.2f} {result['avg_peak_used']:<15.2f} {result['avg_peak_reserved']:<15.2f} {result['dp']:<5} {result['tp']:<5} {result['sp']:<5} {result['pp']:<5}" - print(row) - else: - # Only one result, print it - for result in group: - row = f"{result['row_id']:<40} {result['avg_loss']:<15.6f} {result['avg_time_ms']:<15.2f} {result['avg_tok_per_sec']:<15.2f} {result['avg_peak_used']:<15.2f} {result['avg_peak_reserved']:<15.2f} {result['dp']:<5} {result['tp']:<5} {result['sp']:<5} {result['pp']:<5}" - print(row) - - print("=" * 150) - - # Print comparison between flash and noflash - print("\nComparison between flash and noflash:") - print("=" * 150) - - for key, group in sorted(grouped_by_config.items()): - # Separate flash and noflash results - flash_results = [r for r in group if r['flash'] == 'flash'] - noflash_results = [r for r in group if r['flash'] == 'noflash'] - - if flash_results and noflash_results: - # Calculate averages for each group - avg_flash_loss = sum(r['avg_loss'] for r in flash_results) / len(flash_results) - avg_flash_time = sum(r['avg_time_ms'] for r in flash_results) / len(flash_results) - avg_flash_tok = sum(r['avg_tok_per_sec'] for r in flash_results) / len(flash_results) - avg_flash_peak_used = sum(r['avg_peak_used'] for r in flash_results) / len(flash_results) - avg_flash_peak_reserved = sum(r['avg_peak_reserved'] for r in flash_results) / len(flash_results) - - avg_noflash_loss = sum(r['avg_loss'] for r in noflash_results) / len(noflash_results) - avg_noflash_time = sum(r['avg_time_ms'] for r in noflash_results) / len(noflash_results) - avg_noflash_tok = sum(r['avg_tok_per_sec'] for r in noflash_results) / len(noflash_results) - avg_noflash_peak_used = sum(r['avg_peak_used'] for r in noflash_results) / len(noflash_results) - avg_noflash_peak_reserved = sum(r['avg_peak_reserved'] for r in noflash_results) / len(noflash_results) - - # Calculate differences - loss_diff = avg_flash_loss - avg_noflash_loss - time_diff = avg_flash_time - avg_noflash_time - tok_diff = avg_flash_tok - avg_noflash_tok - - loss_pct = (loss_diff / avg_noflash_loss * 100) if avg_noflash_loss > 0 else 0 - time_pct = (time_diff / avg_noflash_time * 100) if avg_noflash_time > 0 else 0 - tok_pct = (tok_diff / avg_noflash_tok * 100) if avg_noflash_tok > 0 else 0 - - print(f"\n{key}:") - print(f" Loss: {avg_flash_loss:.6f} vs {avg_noflash_loss:.6f} (diff: {loss_diff:+.6f}, {loss_pct:+.2f}%)") - print(f" Time: {avg_flash_time:.2f} vs {avg_noflash_time:.2f} ms (diff: {time_diff:+.2f}, {time_pct:+.2f}%)") - print(f" Tok/s: {avg_flash_tok:.2f} vs {avg_noflash_tok:.2f} (diff: {tok_diff:+.2f}, {tok_pct:+.2f}%)") - - # Calculate memory differences - mem_used_diff = avg_flash_peak_used - avg_noflash_peak_used - mem_reserved_diff = avg_flash_peak_reserved - avg_noflash_peak_reserved - - mem_used_pct = (mem_used_diff / avg_noflash_peak_used * 100) if avg_noflash_peak_used > 0 else 0 - mem_reserved_pct = (mem_reserved_diff / avg_noflash_peak_reserved * 100) if avg_noflash_peak_reserved > 0 else 0 - - print(f" Peak Used: {avg_flash_peak_used:.2f} vs {avg_noflash_peak_used:.2f} MB (diff: {mem_used_diff:+.2f}, {mem_used_pct:+.2f}%)") - print(f" Peak Reserved: {avg_flash_peak_reserved:.2f} vs {avg_noflash_peak_reserved:.2f} MB (diff: {mem_reserved_diff:+.2f}, {mem_reserved_pct:+.2f}%)") - print("\n" + "=" * 120) - print("Speedup Ratio Table (noflash_time / flash_time)") - print("=" * 120) - - # Group by model, dtype, disopt, and test_id - # This allows us to calculate speedup for each individual test configuration - grouped_by_test = defaultdict(list) - for result in results: - key = f"{result['model']}_{result['dtype']}_{result['disopt']}_{result.get('test_id', '')}" - grouped_by_test[key].append(result) - - # Calculate speedup ratios for each test configuration - speedup_results = [] - for key, group in sorted(grouped_by_test.items()): - # Separate flash and noflash results - flash_results = [r for r in group if r['flash'] == 'flash'] - noflash_results = [r for r in group if r['flash'] == 'noflash'] - - if flash_results and noflash_results: - # Calculate average times for this test configuration - avg_flash_time = sum(r['avg_time_ms'] for r in flash_results) / len(flash_results) - avg_noflash_time = sum(r['avg_time_ms'] for r in noflash_results) / len(noflash_results) - - # Calculate speedup ratio - # Speedup > 1.0 means flash is faster than noflash - speedup = avg_noflash_time / avg_flash_time if avg_flash_time > 0 else 0 - - # Extract test_id from key - test_id = key.split('_')[-1] - - speedup_results.append({ - 'config': key, - 'speedup': speedup, - 'flash_time': avg_flash_time, - 'noflash_time': avg_noflash_time - }) - - # Sort by configuration name (alphabetical order) - speedup_results.sort(key=lambda x: x['config']) - - # Print speedup table - if args.markdown: - # Output as markdown table - markdown_lines = [] - markdown_lines.append("| Configuration | Speedup Ratio | Flash Time (ms) | Noflash Time (ms) |") - markdown_lines.append("|---------------|---------------|-----------------|-------------------|") - - for result in speedup_results: - row = f"| {result['config']:<37} | {result['speedup']:<13.2f} | {result['flash_time']:<15.2f} | {result['noflash_time']:<17.2f} |" - markdown_lines.append(row) - - # Print markdown table - markdown_table = "\n".join(markdown_lines) - print(markdown_table) - - # Write to file if output path is specified - if args.output: - output_path = args.output.replace('.md', '_speedup.md') - with open(output_path, 'w') as f: - f.write(markdown_table) - print(f"Speedup table saved to: {output_path}") - else: - # Print plain text table - header = f"{'Configuration':<37} {'Speedup Ratio':<15} {'Flash Time (ms)':<17} {'Noflash Time (ms)':<17}" - print(header) - print("-" * 120) - - for result in speedup_results: - row = f"{result['config']:<37} {result['speedup']:<15.2f} {result['flash_time']:<17.2f} {result['noflash_time']:<17.2f}" - print(row) - - print("=" * 120) - - -if __name__ == '__main__': - main() From 1fc2d6521567f233cfd142e983e878391fc8c124 Mon Sep 17 00:00:00 2001 From: Aoshine999 <820459542@qq.com> Date: Mon, 16 Mar 2026 15:58:10 +0800 Subject: [PATCH 15/18] delete config file --- algo.tex | 75 --------- scripts/test_config.json.bak | 299 ----------------------------------- 2 files changed, 374 deletions(-) delete mode 100644 algo.tex delete mode 100644 scripts/test_config.json.bak diff --git a/algo.tex b/algo.tex deleted file mode 100644 index cfb8861b..00000000 --- a/algo.tex +++ /dev/null @@ -1,75 +0,0 @@ -\begin{algorithm}[H] - % \algsetup{linenosize=\tiny} - \caption{\small\label{alg:flash2_fwd}\sysname forward pass} - \begin{algorithmic}[1] - \REQUIRE Matrices $\vQ, \vK, \vV \in \mathbb{R}^{N \times d}$ in HBM, block sizes $B_c$, $B_r$. - \STATE \label{alg:stream_attn_split_qkv} Divide $\vQ$ into $T_r = \left\lceil\frac{N}{B_r} \right\rceil$ blocks $\vQ_1, \dots, \vQ_{T_r}$ of size $B_r \times d$ each, - and divide $\vK, \vV$ in to $T_c = \left\lceil \frac{N}{B_c} \right\rceil$ blocks $\vK_1, \dots, \vK_{T_c}$ and - $\vV_1, \dots, \vV_{T_c}$, of size $B_c \times d$ each. - \STATE Divide the output $\vO \in \mathbb{R}^{N \times d}$ into $T_r$ blocks $\vO_i, \dots, \vO_{T_r}$ of size - $B_r \times d$ each, and divide the logsumexp $L$ into $T_r$ blocks $L_i, \dots, L_{T_r}$ of size - $B_r$ each. - \FOR{$1 \le i \le T_r$} \label{alg:stream_attn_outer_loop} - \STATE \label{alg:stream_attn_load_q} Load $\vQ_i$ from HBM to on-chip SRAM. - \STATE \label{alg:stream_attn_init} On chip, initialize $\vO_{i}^{(0)} = (0)_{B_r \times d} \in \mathbb{R}^{B_r \times d}, \ell_{i}^{(0)} = (0)_{B_r} \in \mathbb{R}^{B_r}, m_{i}^{(0)} = (-\infty)_{B_r} \in \mathbb{R}^{B_r}$. - \FOR{$1 \le j \le T_c$} - \STATE \label{alg:stream_attn_load_kv} Load $\vK_j, \vV_j$ from HBM to on-chip SRAM. - \STATE \label{alg:stream_attn_qk} On chip, compute $\vS_{i}^{(j)} = \vQ_i \vK_j^T \in \mathbb{R}^{B_r \times B_c}$. - \STATE \label{alg:stream_attn_statistics} On chip, compute - $m_{i}^{(j)} = \mathrm{max}(m_{i}^{(j-1)}, \mathrm{rowmax}(\vS_{i}^{(j)})) \in \mathbb{R}^{B_r}$, $\tilde{\vP}_{i}^{(j)} = \exp(\vS_{i}^{(j)} - m_{i}^{(j)}) \in \mathbb{R}^{B_r \times B_c}$ (pointwise), - $\ell_{i}^{(j)} = e^{m_{i}^{j-1} - m_{i}^{(j)}} \ell_{i}^{(j-1)} + \mathrm{row sum}(\tilde{\vP}_{i}^{(j)}) \in \mathbb{R}^{B_r}$. - \STATE \label{alg:stream_attn_update} On chip, compute - $\vO_{i}^{(j)} = \diag(e^{m_{i}^{(j-1)} - m_{i}^{(j)}}) \vO_{i}^{(j-1)} + \tilde{\vP}_{i}^{(j)} \vV_j$. - \ENDFOR - \STATE On chip, compute $\vO_{i} = \diag(\ell_{i}^{(T_c)})^{-1} \vO_{i}^{(T_c)}$. - \STATE On chip, compute $L_{i} = m_{i}^{(T_c)} + \log(\ell_i^{(T_c)})$. - \STATE Write $\vO_{i}$ to HBM as the $i$-th block of $\vO$. - \STATE Write $L_{i}$ to HBM as the $i$-th block of $L$. - \ENDFOR - \STATE Return the output $\vO$ and the logsumexp $L$. - \end{algorithmic} -\end{algorithm} - - - - -\begin{algorithm}[h] - \caption{\small\label{alg:flash_bwd}\sysname Backward Pass} - \begin{algorithmic}[1] - \REQUIRE Matrices $\vQ, \vK, \vV, \vO, \vdO \in \mathbb{R}^{N \times d}$ in HBM, - vector $L \in \mathbb{R}^N$ in HBM, block sizes $B_c$, $B_r$. - \STATE Divide $\vQ$ into $T_r = \left\lceil\frac{N}{B_r} \right\rceil$ blocks $\vQ_1, \dots, \vQ_{T_r}$ of size $B_r \times d$ each, - and divide $\vK, \vV$ in to $T_c = \left\lceil \frac{N}{B_c} \right\rceil$ blocks $\vK_1, \dots, \vK_{T_c}$ and - $\vV_1, \dots, \vV_{T_c}$, of size $B_c \times d$ each. - \STATE Divide $\vO$ into $T_r$ blocks $\vO_i, \dots, \vO_{T_r}$ of size - $B_r \times d$ each, divide $\vdO$ into $T_r$ blocks $\vdO_i, \dots, \vdO_{T_r}$ - of size $B_r \times d$ each, and divide $L$ into $T_r$ blocks $L_i, \dots, L_{T_r}$ of size - $B_r$ each. - \STATE Initialize $\vdQ = (0)_{N \times d}$ in HBM and divide it into $T_r$ blocks $\vdQ_1, \dots, \vdQ_{T_r}$ of size $B_r \times d$ each. - Divide $\vdK, \vdV \in \mathbb{R}^{N \times d}$ in to $T_c$ blocks $\vdK_1, \dots, \vdK_{T_c}$ and - $\vdV_1, \dots, \vdV_{T_c}$, of size $B_c \times d$ each. - \STATE Compute $D = \mathrm{rowsum}(\vdO \circ \vO) \in \mathbb{R}^d$ (pointwise multiply), write - $D$ to HBM and divide it into $T_r$ blocks $D_1, \dots, D_{T_r}$ of size - $B_r$ each. - \FOR{$1 \le j \le T_c$} - \STATE Load $\vK_j, \vV_j$ from HBM to on-chip SRAM. - \STATE Initialize $\vdK_j = (0)_{B_c \times d}, \vdV_j = (0)_{B_c \times d}$ on SRAM. - \FOR{$1 \le i \le T_r$} - \STATE Load $\vQ_i, \vO_i, \vdO_i, \vdQ_i, L_i, D_i$ from HBM to on-chip SRAM. - \STATE On chip, compute $\vS_{i}^{(j)} = \vQ_i \vK_j^T \in \mathbb{R}^{B_r \times B_c}$. - \STATE On chip, compute $\vP_{i}^{(j)} = \exp(\vS_{ij} - L_{i}) \in \mathbb{R}^{B_r \times B_c}$. - \STATE On chip, compute - $\vdV_j \leftarrow \vdV_j + (\vP_{i}^{(j)})^\top \vdO_i \in \mathbb{R}^{B_c \times d}$. - \STATE On chip, compute - $\vdP_{i}^{(j)} = \vdO_{i} \vV_j^\top \in \mathbb{R}^{B_r \times B_c}$. - \STATE On chip, compute $\vdS_{i}^{(j)} = \vP_{i}^{(j)} \circ (\vdP_{i}^{(j)} - D_i) \in \mathbb{R}^{B_r \times B_c}$. - \STATE Load $\vdQ_i$ from HBM to SRAM, then on chip, update - $\vdQ_{i} \leftarrow \vdQ_i + \vdS_{i}^{(j)} \vK_j \in \mathbb{R}^{B_r \times d}$, and write - back to HBM. - \STATE On chip, compute $\vdK_{j} \leftarrow \vdK_j + {\vdS_{i}^{(j)}}^\top \vQ_i \in \mathbb{R}^{B_c \times d}$. - \ENDFOR - \STATE Write $\vdK_j, \vdV_j$ to HBM. - \ENDFOR - \STATE Return $\vdQ, \vdK, \vdV$. - \end{algorithmic} -\end{algorithm} \ No newline at end of file diff --git a/scripts/test_config.json.bak b/scripts/test_config.json.bak deleted file mode 100644 index 3d66ff7b..00000000 --- a/scripts/test_config.json.bak +++ /dev/null @@ -1,299 +0,0 @@ -{ - "variables": { - "BUILD_DIR": "../build", - "GPT2_INPUT_BIN": "/data/shared/InfiniTrain-dev/data/llmc/gpt2/tinyshakespeare/tiny_shakespeare_train.bin", - "GPT2_LLMC_FILEPATH": "/data/shared/InfiniTrain-dev/data/llmc/gpt2/gpt2_124M.bin", - "LLAMA3_INPUT_BIN": "/data/shared/InfiniTrain-dev/data/llmc/llama3/tinyshakespeare/tiny_shakespeare_train.bin", - "LLAMA3_LLMC_FILEPATH": "/data/shared/InfiniTrain-dev/data/llmc/llama3/llama3.2_1B_fp32.bin", - "PROFILE_LOG_DIR": "./profile_logs", - "LOG_DIR": "./logs", - "COMPARE_LOG_DIR": "" - }, - "builds": [ - { - "id": "build_1", - "profile": false, - "cmd": "cmake -DUSE_CUDA=ON -DUSE_NCCL=ON .. && make -j" - }, - { - "id": "build_2", - "profile": true, - "cmd": "cmake -DUSE_CUDA=ON -DUSE_NCCL=ON -DPROFILE_MODE=ON .. && make -j" - } - ], - "tests": [ - { - "id": "1", - "args": { - "dtype": "float32" - } - }, - { - "id": "1_bfloat16", - "args": { - "dtype": "bfloat16" - } - }, - { - "id": "2", - "args": { - "dtype": "float32", - "num_iteration": 10, - "batch_size": 80, - "total_batch_size": 5120 - } - }, - { - "id": "2_bfloat16", - "args": { - "dtype": "bfloat16", - "num_iteration": 10, - "batch_size": 80, - "total_batch_size": 5120 - } - }, - { - "id": "3", - "args": { - "dtype": "float32", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 10, - "total_batch_size": 5120 - } - }, - { - "id": "3_distopt", - "args": { - "dtype": "float32", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 10, - "total_batch_size": 5120, - "use_distributed_optimizer": true - } - }, - { - "id": "3_bfloat16", - "args": { - "dtype": "bfloat16", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 10, - "total_batch_size": 5120 - } - }, - { - "id": "3_bfloat16_distopt", - "args": { - "dtype": "bfloat16", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 10, - "total_batch_size": 5120, - "use_distributed_optimizer": true - } - }, - { - "id": "4", - "args": { - "dtype": "float32", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 40, - "total_batch_size": 5120, - "tensor_parallel": 4 - } - }, - { - "id": "4_distopt", - "args": { - "dtype": "float32", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 40, - "total_batch_size": 5120, - "tensor_parallel": 4, - "use_distributed_optimizer": true - } - }, - { - "id": "4_bfloat16", - "args": { - "dtype": "bfloat16", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 40, - "total_batch_size": 5120, - "tensor_parallel": 4 - } - }, - { - "id": "4_bfloat16_distopt", - "args": { - "dtype": "bfloat16", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 40, - "total_batch_size": 5120, - "tensor_parallel": 4, - "use_distributed_optimizer": true - } - }, - { - "id": "5", - "args": { - "dtype": "float32", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 40, - "total_batch_size": 5120, - "tensor_parallel": 4, - "sequence_parallel": true - } - }, - { - "id": "5_distopt", - "args": { - "dtype": "float32", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 40, - "total_batch_size": 5120, - "tensor_parallel": 4, - "sequence_parallel": true, - "use_distributed_optimizer": true - } - }, - { - "id": "5_bfloat16", - "args": { - "dtype": "bfloat16", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 40, - "total_batch_size": 5120, - "tensor_parallel": 4, - "sequence_parallel": true - } - }, - { - "id": "5_bfloat16_distopt", - "args": { - "dtype": "bfloat16", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 40, - "total_batch_size": 5120, - "tensor_parallel": 4, - "sequence_parallel": true, - "use_distributed_optimizer": true - } - }, - { - "id": "6", - "args": { - "dtype": "float32", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 10, - "total_batch_size": 5120, - "pipeline_parallel": 8 - } - }, - { - "id": "6_bfloat16", - "args": { - "dtype": "bfloat16", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 10, - "total_batch_size": 5120, - "pipeline_parallel": 8 - } - }, - { - "id": "7", - "args": { - "dtype": "float32", - "nthread_per_process": 4, - "num_iteration": 10, - "batch_size": 10, - "total_batch_size": 5120, - "pipeline_parallel": 4, - "virtual_pipeline_parallel": 2 - } - }, - { - "id": "7_bfloat16", - "args": { - "dtype": "bfloat16", - "nthread_per_process": 4, - "num_iteration": 10, - "batch_size": 10, - "total_batch_size": 5120, - "pipeline_parallel": 4, - "virtual_pipeline_parallel": 2 - } - }, - { - "id": "8", - "args": { - "dtype": "float32", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 40, - "total_batch_size": 5120, - "tensor_parallel": 2, - "sequence_parallel": true, - "pipeline_parallel": 2, - "virtual_pipeline_parallel": 2 - } - }, - { - "id": "8_distopt", - "args": { - "dtype": "float32", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 40, - "total_batch_size": 5120, - "tensor_parallel": 2, - "sequence_parallel": true, - "pipeline_parallel": 2, - "virtual_pipeline_parallel": 2, - "use_distributed_optimizer": true - } - }, - { - "id": "8_bfloat16", - "args": { - "dtype": "bfloat16", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 40, - "total_batch_size": 5120, - "tensor_parallel": 2, - "sequence_parallel": true, - "pipeline_parallel": 2, - "virtual_pipeline_parallel": 2 - } - }, - { - "id": "8_bfloat16_distopt", - "args": { - "dtype": "bfloat16", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 40, - "total_batch_size": 5120, - "tensor_parallel": 2, - "sequence_parallel": true, - "pipeline_parallel": 2, - "virtual_pipeline_parallel": 2, - "use_distributed_optimizer": true - } - } - ] -} - From 804df62f4f869fd2654874af1178f441c452705b Mon Sep 17 00:00:00 2001 From: Aoshine999 <820459542@qq.com> Date: Mon, 16 Mar 2026 15:59:43 +0800 Subject: [PATCH 16/18] delete unrelated file --- .../src/kernels/cuda/flash_attention1.cu | 1083 ----------------- 1 file changed, 1083 deletions(-) delete mode 100644 infini_train/src/kernels/cuda/flash_attention1.cu diff --git a/infini_train/src/kernels/cuda/flash_attention1.cu b/infini_train/src/kernels/cuda/flash_attention1.cu deleted file mode 100644 index 9a78f802..00000000 --- a/infini_train/src/kernels/cuda/flash_attention1.cu +++ /dev/null @@ -1,1083 +0,0 @@ -// #include -// #include -// #include -// #include "glog/logging.h" - -// #include "infini_train/include/common/cuda/common_cuda.h" -// #include "infini_train/include/common/cuda/kernel_helper.cuh" -// #include "infini_train/include/core/runtime/device_guard.h" -// #include "infini_train/include/dispatcher.h" -// #include "infini_train/include/kernels/cuda/flash_attention.h" -// #include "infini_train/include/tensor.h" - -// #include "infini_train/src/core/runtime/cuda/cuda_runtime_common.h" - -// namespace infini_train::kernels::cuda { - -// /** -// * FlashAttention Forward Kernel -// * -// * This kernel implements the FlashAttention algorithm for efficient attention computation. -// * It uses tiling and recomputation to reduce memory access and improve performance. -// * -// * Args: -// * output: Output tensor of shape [batch_size, seq_len_q, num_heads, head_dim] -// * query: Query tensor of shape [batch_size, seq_len_q, num_heads, head_dim] -// * key: Key tensor of shape [batch_size, seq_len_k, num_heads_kv, head_dim] -// * value: Value tensor of shape [batch_size, seq_len_k, num_heads_kv, head_dim] -// * attn_mask: Optional attention mask tensor -// * logsumexp: Save logsumexp for backward pass -// * scale: Scaling factor for attention scores -// * is_causal: Whether to apply causal masking -// * enable_gqa: Whether to enable grouped-query attention -// * dropout_p: Dropout probability -// * dropout_seed: Random seed for dropout (for reproducibility) -// * batch_size, target_seq_len, src_seq_len: Tensor dimensions -// * q_heads, kv_heads, head_dim: Attention head dimensions -// * -// * Note: Actual kernel implementation is not included in this version. -// */ -// template -// __device__ T warp_reduce_sum(T val){ -// #pragma unroll//短循环自动展开,省去分支预测,提升效率 -// for(int offset = 16; offset > 0; offset >>= 1){ -// val += __shfl_down_sync(0xffffffff, val, offset); -// } -// return val; -// } - -// template -// __device__ T myexp(T x) { -// if constexpr(std::is_same::value) { -// float fx = __half2float(x); -// float result = expf(fx); -// return __float2half(result); -// } -// else if constexpr(std::is_same::value) { -// return expf(x); // expf返回float -// } -// else if constexpr(std::is_same::value) { -// return exp(x); // exp返回double -// } -// else{//other types -// return T(0); -// } -// } - -// template -// __device__ T warp_reduce_max(T val){ -// #pragma unroll//短循环自动展开,省去分支预测,提升效率 -// for(int offset = 16; offset > 0; offset >>= 1){ -// T tmp = __shfl_down_sync(0xffffffff, val, offset); -// val = (val > tmp) ? val : tmp; -// } -// return val; -// } - -// __device__ __forceinline__ bool FlashAttnDropoutKeep( -// unsigned long long seed, -// int batch_idx, int head_idx, int q_idx, int k_idx, -// int q_heads, int target_seq_len, int src_seq_len, -// float dropout_p) { -// // Stateless RNG mapping for one attention element (q_idx, k_idx). -// unsigned long long linear_idx = -// (((static_cast(batch_idx) * static_cast(q_heads) -// + static_cast(head_idx)) -// * static_cast(target_seq_len) -// + static_cast(q_idx)) -// * static_cast(src_seq_len)) -// + static_cast(k_idx); -// curandStatePhilox4_32_10_t state; -// curand_init(seed, linear_idx, 0, &state); -// float rand_val = curand_uniform(&state); -// return rand_val > dropout_p; -// } - - -// /** -// * FlashAttention Forward Kernel -// * -// * This kernel implements the FlashAttention algorithm for efficient attention computation. -// * It uses tiling and recomputation to reduce memory access and improve performance. -// * -// * Args: -// * output: Output tensor of shape [batch_size, seq_len_q, num_heads, head_dim] -// * query: Query tensor of shape [batch_size, seq_len_q, num_heads, head_dim] -// * key: Key tensor of shape [batch_size, seq_len_k, num_heads_kv, head_dim] -// * value: Value tensor of shape [batch_size, seq_len_k, num_heads_kv, head_dim] -// * attn_mask: Optional attention mask tensor -// * logsumexp: Save logsumexp for backward pass -// * scale: Scaling factor for attention scores -// * is_causal: Whether to apply causal masking -// * enable_gqa: Whether to enable grouped-query attention -// * dropout_p: Dropout probability -// * dropout_seed: Random seed for dropout (for reproducibility) -// * batch_size, target_seq_len, src_seq_len: Tensor dimensions -// * q_heads, kv_heads, head_dim: Attention head dimensions -// * -// * Note: Actual kernel implementation is not included in this version. -// */ -// template -// __global__ void FlashAttentionForwardKernel( -// T *output, const T *query, const T *key, const T *value, const T *attn_mask, -// float *logsumexp, // Save logsumexp for backward pass -// float scale, bool is_causal, bool enable_gqa, int64_t dropout_p, -// unsigned long long dropout_seed, // Use dropout_seed instead of dropout_mask -// int batch_size, int target_seq_len, int src_seq_len, -// int q_heads, int kv_heads, int head_dim) { - -// int tid_x = threadIdx.x; // 横向,blockDim.x列 (Bc) -// int tid_y = threadIdx.y; // 纵向,blockDim.y行 (Br) -// int bid_x = blockIdx.x; // x方向,总数 = #q_heads -// int bid_y = blockIdx.y; // y方向,总数 = #batch -// int bid_z = blockIdx.z; // z方向,总数 = Tr -// const int p = q_heads / kv_heads; // 计算比例系数,GQA支持 - -// const int Br = blockDim.y; // Q纵向每块大小, 32 -// const int Bc = blockDim.x; // K/V纵向分块大小, 32 -// const int Tc = gridDim.z; // 对应原始论文中K/V纵向分块数Tc,其中Bc = 32 - -// // 定义一系列临时变量 -// extern __shared__ char shared_mem[]; -// char *ptr = shared_mem; - -// // 计算中间变量,包括S, P(复用为SP), m_prev, m_new, l_prev, l_new -// double *SP = reinterpret_cast(ptr); // double SP[Br][Bc] -// ptr += Br * Bc * sizeof(double); -// float *m_prev = reinterpret_cast(ptr); // float m_prev[Br] -// ptr += Br * sizeof(float); -// float *m_new = reinterpret_cast(ptr); // float m_new[Br] -// ptr += Br * sizeof(float); -// float *l_prev = reinterpret_cast(ptr); // float l_prev[Br] -// ptr += Br * sizeof(float); -// float *l_new = reinterpret_cast(ptr); // float l_new[Br] -// ptr += Br * sizeof(float); - -// // 原始数据QKV和计算结果O; 全采用float -// float *Q_sm = reinterpret_cast(ptr); // float Q_sm[Br][head_dim] -// ptr += Br * head_dim * sizeof(float); -// float *K_T_sm = reinterpret_cast(ptr); // float K_T_sm[head_dim][Bc] -// ptr += head_dim * Bc * sizeof(float); -// float *V_sm = reinterpret_cast(ptr); // float V_sm[Bc][head_dim] -// ptr += Bc * head_dim * sizeof(float); -// float *O_sm = reinterpret_cast(ptr); // float O_sm[Br][head_dim] -// // Note: Removed dropout_sm shared memory allocation - -// // 定义访问宏 -// #define SP_AT(y, x) SP[y * Bc + x] -// #define Q_sm_AT(y, x) Q_sm[y * head_dim + x] -// #define K_T_sm_AT(y, x) K_T_sm[y * Bc + x] -// #define V_sm_AT(y, x) V_sm[y * head_dim + x] -// #define O_sm_AT(y, x) O_sm[y * head_dim + x] -// // Note: Removed DROPOUT_SM_AT macro - -// /****************************preparation**************************/ -// int bound_tid_y = min(Br, target_seq_len - Br * bid_z); - -// // preparation-1: load Qi from GM to SM, and reset Oi to 0 -// for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { -// O_sm_AT(tid_y, idx) = 0.0; -// Q_sm_AT(tid_y, idx) = 0.0; -// if (tid_y < bound_tid_y) { -// Q_sm_AT(tid_y, idx) -// = float(query[((((bid_y * target_seq_len) + (Br * bid_z + tid_y)) * q_heads) + bid_x) * head_dim + idx]); -// } -// } -// __syncthreads(); - -// // preparation-2: reset m_prev to -INFINITY and l_prev to 0 -// if (tid_x == 0) { -// m_prev[tid_y] = -8192.0; -// l_prev[tid_y] = 0.0; -// } -// __syncthreads(); - -// /****************************end-of-preparation*************************/ - -// /****************************main-loop**************************/ -// #pragma unroll 4 -// for (int j = 0; j < Tc; ++j) { // 对于每个K/V分块 -// bool skip = (is_causal && bid_z < j); -// if (skip) { // early exit, 直接跳过 -// __syncthreads(); -// continue; -// } - -// SP_AT(tid_y, tid_x) = -8192.0; -// __syncthreads(); -// int bound_tid_x = min(Bc, src_seq_len - Bc * j); -// bool is_compute = true; // optimization: 分支处理,加速branch-resolving -// if (is_causal) { -// if (bid_z < j) { -// is_compute = false; // 早期退出情况 -// } else if (bid_z == j) { -// is_compute = (tid_y >= tid_x); // 对角线以上 -// } -// } - -// // step-1: load Ki, Vi from GM to SM -// #pragma unroll -// for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { -// K_T_sm_AT(idx, tid_y) = 0.0; -// V_sm_AT(tid_y, idx) = 0.0; -// if (tid_y < bound_tid_x) { -// K_T_sm_AT(idx, tid_y) = float( -// K[((((bid_y * src_seq_len) + (Bc * j + tid_y)) * kv_heads) + (bid_x / p)) * head_dim + idx]); -// V_sm_AT(tid_y, idx) = float( -// V[((((bid_y * src_seq_len) + (Bc * j + tid_y)) * kv_heads) + (bid_x / p)) * head_dim + idx]); -// } -// } -// __syncthreads(); - -// // step-2: S = Q @ K.T, point-wise -// if (tid_y < bound_tid_y && tid_x < bound_tid_x) { -// float val0 = 0.0; -// if (is_compute) { -// #pragma unroll -// for (int k = 0; k < head_dim; ++k) { -// val0 += Q_sm_AT(tid_y, k) * K_T_sm_AT(k, tid_x); -// } -// SP_AT(tid_y, tid_x) = double(val0) * scale; -// } -// } -// __syncthreads(); - -// // step-3: m_new = max(m_prev, rowMax(S)) -// float val1 = float(SP_AT(tid_y, tid_x)); -// val1 = warp_reduce_max(val1); -// if (tid_x == 0 && tid_y < bound_tid_y) { -// m_new[tid_y] = (val1 > m_prev[tid_y]) ? val1 : m_prev[tid_y]; -// } -// __syncthreads(); - -// // step-4: P = exp(S - m_new), point-wise -// if (tid_y < bound_tid_y && tid_x < bound_tid_x) { -// if (is_compute) { -// SP_AT(tid_y, tid_x) = myexp(SP_AT(tid_y, tid_x) - double(m_new[tid_y])); -// } else { -// SP_AT(tid_y, tid_x) = 0.0; -// } -// } else { -// SP_AT(tid_y, tid_x) = 0.0; -// } -// __syncthreads(); - -// // step-5: l_new = exp(m_prev - m_new) * l_prev + rowSum(P) -// float val2 = float(SP_AT(tid_y, tid_x)); -// val2 = warp_reduce_sum(val2); -// float exp_result = myexp(m_prev[tid_y] - m_new[tid_y]); -// if (tid_x == 0 && tid_y < bound_tid_y) { -// l_new[tid_y] = exp_result * l_prev[tid_y] + val2; -// } -// __syncthreads(); - -// // step-5.5: Apply dropout to P (using dropout_seed for reproducibility) -// if (dropout_p > 0 && tid_y < bound_tid_y && tid_x < bound_tid_x) { -// int global_q_idx = Br * bid_z + tid_y; -// int global_k_idx = Bc * j + tid_x; -// bool keep = FlashAttnDropoutKeep( -// dropout_seed, bid_y, bid_x, global_q_idx, global_k_idx, -// q_heads, target_seq_len, src_seq_len, static_cast(dropout_p)); -// if (keep) { -// SP_AT(tid_y, tid_x) = SP_AT(tid_y, tid_x) / (1.0f - dropout_p); -// } else { -// SP_AT(tid_y, tid_x) = 0.0; -// } -// // Note: Removed dropout_sm mask saving to reduce memory overhead -// } -// __syncthreads(); - -// // step-6: O = 1/(exp(m_prev - m_new)) * O + P @ V -// if (tid_x < bound_tid_x && tid_y < bound_tid_y) { -// for (int u = tid_x; u < head_dim; u += blockDim.x) { -// float val3 = 0.0; -// #pragma unroll -// for (int w = 0; w < Bc; ++w) { -// val3 += float(SP_AT(tid_y, w)) * V_sm_AT(w, u); -// } -// O_sm_AT(tid_y, u) = O_sm_AT(tid_y, u) * exp_result + val3; -// } -// } -// __syncthreads(); - -// // step-7: m_prev <- m_new; l_prev <- l_new -// if (tid_x == 0 && tid_y < bound_tid_y) { -// m_prev[tid_y] = m_new[tid_y]; -// l_prev[tid_y] = l_new[tid_y]; -// } -// __syncthreads(); -// } -// /****************************end-of-main-loop**************************/ - -// /****************************post-process****************************/ -// // O(GM) = O/l_prev, aka O_sm /= l_prev and write Oi from SM to GM -// // Also save logsumexp for backward pass -// #pragma unroll -// for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { -// if (tid_y < bound_tid_y) { -// output[((((bid_y * target_seq_len) + (Br * bid_z + tid_y)) * q_heads) + bid_x) * head_dim + idx] -// = T(O_sm_AT(tid_y, idx) / float(l_prev[tid_y])); -// } -// } - -// // Save logsumexp for backward pass -// if (tid_x == 0 && tid_y < bound_tid_y) { -// int logsumexp_idx = ((bid_y * q_heads + bid_x) * target_seq_len) + (Br * bid_z + tid_y); -// logsumexp[logsumexp_idx] = m_prev[tid_y] + log(l_prev[tid_y]); -// } -// __syncthreads(); -// /****************************end-of-post-process****************************/ - -// // 取消访问宏定义 -// #undef SP_AT -// #undef Q_sm_AT -// #undef K_T_sm_AT -// #undef V_sm_AT -// #undef O_sm_AT -// } - -// /** -// * FlashAttention Backward Kernel -// * -// * This kernel implements the backward pass for FlashAttention. -// * It computes gradients for query, key, and value tensors. -// * -// * Args: -// * grad_query: Gradient for query tensor -// * grad_key: Gradient for key tensor -// * grad_value: Gradient for value tensor -// * query: Query tensor from forward pass -// * key: Key tensor from forward pass -// * value: Value tensor from forward pass -// * output: Output tensor from forward pass -// * grad_output: Gradient from upstream -// * logsumexp: Logsumexp tensor from forward pass -// * dropout_seed: Dropout seed for reproducibility -// * attn_mask: Optional attention mask tensor -// * scale: Scaling factor for attention scores -// * is_causal: Whether causal masking was applied -// * dropout_p: Dropout probability -// * enable_gqa: Whether GQA was enabled -// * batch_size, target_seq_len, src_seq_len: Tensor dimensions -// * q_heads, kv_heads, head_dim: Attention head dimensions -// */ -// template -// __global__ void FlashAttentionBackwardKernel( -// T *grad_query, T *grad_key, T *grad_value, -// const T *query, const T *key, const T *value, -// const T *output, const T *grad_output, -// const float *logsumexp, -// const float *D, // Precomputed D = rowsum(dO ∘ O) -// unsigned long long dropout_seed, -// const T *attn_mask, -// float scale, bool is_causal, int64_t dropout_p, -// bool enable_gqa, -// int batch_size, int target_seq_len, int src_seq_len, -// int q_heads, int kv_heads, int head_dim) { - -// // Grid/block dimensions: grid_dims(num_heads_q, batch_size, Tr), block_dim(Bc, Br) -// // where Br corresponds to thread block row index, Bc corresponds to column index -// int tid_x = threadIdx.x; // 横向, blockDim.x列 (Bc) -// int tid_y = threadIdx.y; // 纵向, blockDim.y行 (Br) -// int bid_x = blockIdx.x; // x方向, 总数 = #q_heads -// int bid_y = blockIdx.y; // y方向, 总数 = #batch -// int bid_z = blockIdx.z; // z方向, 总数 = Tr -// const int p = q_heads / kv_heads; // GQA比例系数 - -// const int Br = blockDim.y; // Q纵向每块大小, 32 -// const int Bc = blockDim.x; // K/V纵向分块大小, 32 -// const int Tr = gridDim.z; // Q纵向分块数 -// const int Tc = (src_seq_len + Bc - 1) / Bc; // K/V纵向分块数 - -// // Define shared memory -// extern __shared__ char shared_mem[]; -// char *ptr = shared_mem; - -// // D_sm[Br] - D values loaded from HBM -// float *D_sm = reinterpret_cast(ptr); -// ptr += Br * sizeof(float); - -// // Q_sm[Br][head_dim], K_T_sm[head_dim][Bc], V_sm[Bc][head_dim] -// float *Q_sm = reinterpret_cast(ptr); -// ptr += Br * head_dim * sizeof(float); -// float *K_T_sm = reinterpret_cast(ptr); -// ptr += head_dim * Bc * sizeof(float); -// float *V_sm = reinterpret_cast(ptr); -// ptr += Bc * head_dim * sizeof(float); - -// // dO_sm[Br][head_dim], dK_T_sm[head_dim][Bc], dV_sm[Bc][head_dim] -// float *dO_sm = reinterpret_cast(ptr); -// ptr += Br * head_dim * sizeof(float); -// float *dK_T_sm = reinterpret_cast(ptr); -// ptr += head_dim * Bc * sizeof(float); -// float *dV_sm = reinterpret_cast(ptr); -// ptr += Bc * head_dim * sizeof(float); - -// // S_sm[Br][Bc], P_sm[Br][Bc] -// float *S_sm = reinterpret_cast(ptr); -// ptr += Br * Bc * sizeof(float); -// float *P_sm = reinterpret_cast(ptr); - -// // L_sm[Br] - logsumexp values -// float *L_sm = reinterpret_cast(ptr); - -// // dQ_sm[Br][head_dim] - accumulated gradient for Q -// float *dQ_sm = reinterpret_cast(ptr); - -// // Define access macros -// #define D_sm_AT(y) D_sm[y] -// #define Q_sm_AT(y, x) Q_sm[y * head_dim + x] -// #define K_T_sm_AT(y, x) K_T_sm[y * Bc + x] -// #define V_sm_AT(y, x) V_sm[y * head_dim + x] -// #define dO_sm_AT(y, x) dO_sm[y * head_dim + x] -// #define dK_T_sm_AT(y, x) dK_T_sm[y * Bc + x] -// #define dV_sm_AT(y, x) dV_sm[y * head_dim + x] -// #define S_sm_AT(y, x) S_sm[y * Bc + x] -// #define P_sm_AT(y, x) P_sm[y * Bc + x] -// #define L_sm_AT(y) L_sm[y] -// #define dQ_sm_AT(y, x) dQ_sm[y * head_dim + x] - -// /****************************Preparation*****************************/ - -// // Initialize dQ_sm to 0 for accumulation -// for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { -// dQ_sm_AT(tid_y, idx) = 0.0f; -// } -// __syncthreads(); - -// // Load D_i from HBM to shared memory -// int q_idx = Br * bid_z + tid_y; // Global query position within this head -// if (q_idx < target_seq_len) { -// int d_idx = ((bid_y * q_heads + bid_x) * target_seq_len) + q_idx; -// D_sm_AT(tid_y) = D[d_idx]; -// } else { -// D_sm_AT(tid_y) = 0.0f; // Padding for out-of-bounds positions -// } -// __syncthreads(); - -// /****************************Main Loop - Outer Loop over K/V tiles*****************************/ -// for (int j = 0; j < Tc; ++j) { // For each K/V column tile - -// // Skip entire tile if causal and this tile is completely to the right -// bool skip_tile = (is_causal && bid_z < j); -// if (skip_tile) { -// __syncthreads(); -// continue; -// } - -// // Initialize dK_T_sm, dV_sm to 0 for this column tile -// for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { -// for (int y = 0; y < Bc; ++y) { -// dK_T_sm_AT(idx, y) = 0.0f; -// dV_sm_AT(y, idx) = 0.0f; -// } -// } -// __syncthreads(); - -// // Load K_j, V_j from HBM to shared memory -// int bound_tid_x = min(Bc, src_seq_len - Bc * j); -// for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { -// K_T_sm_AT(idx, tid_y) = 0.0f; -// V_sm_AT(tid_y, idx) = 0.0f; -// if (tid_y < bound_tid_x) { -// int kv_head_idx = bid_x / p; -// K_T_sm_AT(idx, tid_y) = float( -// key[((((bid_y * src_seq_len) + (Bc * j + tid_y)) * kv_heads + kv_head_idx) * head_dim + idx]); -// V_sm_AT(tid_y, idx) = float( -// value[((((bid_y * src_seq_len) + (Bc * j + tid_y)) * kv_heads + kv_head_idx) * head_dim + idx]); -// } -// } -// __syncthreads(); - -// /****************************Single Q tile for this block*****************************/ -// // gridDim.z already indexes the Q row tile, so this block only handles i = bid_z. -// const int i = bid_z; -// int q_tile_start = Br * i; -// int q_tile_end = min(q_tile_start + Br, target_seq_len); -// int bound_tid_y = min(Br, q_tile_end - q_tile_start); - -// if (tid_y < bound_tid_y) { -// int global_q_idx = q_tile_start + tid_y; -// int q_tensor_idx = ((bid_y * target_seq_len) + global_q_idx) * q_heads + bid_x; - -// for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { -// Q_sm_AT(tid_y, idx) = float(query[q_tensor_idx * head_dim + idx]); -// dO_sm_AT(tid_y, idx) = float(grad_output[q_tensor_idx * head_dim + idx]); -// } -// if (tid_x == 0) { -// L_sm_AT(tid_y) = logsumexp[((bid_y * q_heads + bid_x) * target_seq_len) + global_q_idx]; -// } -// } -// __syncthreads(); - -// // Get D_i for this row. -// float D_i_row = D_sm_AT(tid_y); - -// // Recompute S_ij = Q_i @ K_j^T, apply causal mask at element level if needed. -// #pragma unroll -// for (int y = 0; y < Bc; ++y) { -// for (int x = 0; x < Bc; ++x) { -// float val = 0.0f; -// if (tid_y < bound_tid_y && tid_x < bound_tid_x) { -// bool is_compute = true; -// if (is_causal && i == j) { -// int global_q_pos = q_tile_start + tid_y; -// int global_k_pos = Bc * j + x; -// is_compute = (global_q_pos >= global_k_pos); -// } - -// if (is_compute) { -// #pragma unroll -// for (int k = 0; k < head_dim; ++k) { -// val += Q_sm_AT(tid_y, k) * K_T_sm_AT(k, y); -// } -// } -// } -// S_sm_AT(tid_y, x) = val * scale; -// } -// } -// __syncthreads(); - -// // Recompute P_ij = exp(S_ij - L_i). -// #pragma unroll -// for (int y = 0; y < Bc; ++y) { -// for (int x = 0; x < Bc; ++x) { -// if (tid_y < bound_tid_y && tid_x < bound_tid_x) { -// bool is_compute = true; -// if (is_causal && i == j) { -// int global_q_pos = q_tile_start + tid_y; -// int global_k_pos = Bc * j + x; -// is_compute = (global_q_pos >= global_k_pos); -// } - -// if (is_compute) { -// P_sm_AT(tid_y, x) = myexp(S_sm_AT(tid_y, x) - L_sm_AT(tid_y)); -// } else { -// P_sm_AT(tid_y, x) = 0.0f; -// } -// } else { -// P_sm_AT(tid_y, x) = 0.0f; -// } -// } -// } -// __syncthreads(); - -// // Apply dropout to P_ij. -// if (dropout_p > 0) { -// #pragma unroll -// for (int y = 0; y < Bc; ++y) { -// for (int x = 0; x < Bc; ++x) { -// if (tid_y < bound_tid_y && tid_x < bound_tid_x) { -// int global_q_pos = q_tile_start + tid_y; -// int global_k_pos = Bc * j + x; -// bool keep = FlashAttnDropoutKeep( -// dropout_seed, bid_y, bid_x, global_q_pos, global_k_pos, -// q_heads, target_seq_len, src_seq_len, static_cast(dropout_p)); -// if (keep) { -// P_sm_AT(tid_y, x) = P_sm_AT(tid_y, x) / (1.0f - dropout_p); -// } else { -// P_sm_AT(tid_y, x) = 0.0f; -// } -// } -// } -// } -// __syncthreads(); -// } - -// // Compute dV_j += P_ij^T @ dO_i. -// if (tid_y < bound_tid_y) { -// #pragma unroll -// for (int x = 0; x < head_dim; x += blockDim.x) { -// float val = 0.0f; -// #pragma unroll -// for (int y = 0; y < Bc; ++y) { -// val += P_sm_AT(tid_y, y) * dO_sm_AT(tid_y, x); -// } -// atomicAdd(&dV_sm_AT(y, x), val); -// } -// } -// __syncthreads(); - -// // Compute dP_ij = dO_i @ V_j^T. -// #pragma unroll -// for (int y = 0; y < Bc; ++y) { -// for (int x = 0; x < Bc; ++x) { -// float val = 0.0f; -// if (tid_y < bound_tid_y && tid_x < bound_tid_x) { -// #pragma unroll -// for (int k = 0; k < head_dim; ++k) { -// val += dO_sm_AT(tid_y, k) * V_sm_AT(y, k); -// } -// S_sm_AT(tid_y, x) = val; // Reuse S_sm as temporary storage for dP_ij -// } else { -// S_sm_AT(tid_y, x) = 0.0f; -// } -// } -// } -// __syncthreads(); - -// // Apply dropout to dP_ij. -// if (dropout_p > 0) { -// #pragma unroll -// for (int y = 0; y < Bc; ++y) { -// for (int x = 0; x < Bc; ++x) { -// if (tid_y < bound_tid_y && tid_x < bound_tid_x) { -// int global_q_pos = q_tile_start + tid_y; -// int global_k_pos = Bc * j + x; -// bool keep = FlashAttnDropoutKeep( -// dropout_seed, bid_y, bid_x, global_q_pos, global_k_pos, -// q_heads, target_seq_len, src_seq_len, static_cast(dropout_p)); -// if (keep) { -// S_sm_AT(tid_y, x) = S_sm_AT(tid_y, x) / (1.0f - dropout_p); -// } else { -// S_sm_AT(tid_y, x) = 0.0f; -// } -// } -// } -// } -// __syncthreads(); -// } - -// // Compute dS_ij = P_ij * (dP_ij - D_i). -// #pragma unroll -// for (int y = 0; y < Bc; ++y) { -// for (int x = 0; x < Bc; ++x) { -// if (tid_y < bound_tid_y && tid_x < bound_tid_x) { -// S_sm_AT(tid_y, x) = P_sm_AT(tid_y, x) * (S_sm_AT(tid_y, x) - D_i_row); -// } else { -// S_sm_AT(tid_y, x) = 0.0f; -// } -// } -// } -// __syncthreads(); - -// // Compute dK_j += dS_ij^T @ Q_i. -// if (tid_x < head_dim) { -// #pragma unroll -// for (int y = 0; y < Bc; ++y) { -// float val = 0.0f; -// #pragma unroll -// for (int x = 0; x < Br; ++x) { -// val += S_sm_AT(x, y) * Q_sm_AT(x, tid_x); -// } -// atomicAdd(&dK_T_sm_AT(tid_x, y), val * scale); -// } -// } -// __syncthreads(); - -// // Compute dQ_i += dS_ij @ K_j. -// if (tid_y < bound_tid_y) { -// #pragma unroll -// for (int x = 0; x < head_dim; x += blockDim.x) { -// float val = 0.0f; -// #pragma unroll -// for (int y = 0; y < Bc; ++y) { -// val += S_sm_AT(tid_y, y) * K_T_sm_AT(x, y); -// } -// dQ_sm_AT(tid_y, x) += val * scale; -// } -// } -// __syncthreads(); - -// // Write back dK_j, dV_j to HBM -// // Note: For GQA, dK_j and dV_j need to be accumulated across multiple q-head blocks -// int kv_head_idx = bid_x / p; -// int k_tile_start = Bc * j; -// int k_tile_end = min(k_tile_start + Bc, src_seq_len); - -// for (int y = 0; y < Bc; ++y) { -// int global_k_idx = k_tile_start + y; -// if (global_k_idx < src_seq_len) { -// for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { -// // dK: [batch_size, src_seq_len, kv_heads, head_dim] -// int k_tensor_idx = ((bid_y * src_seq_len) + global_k_idx) * kv_heads + kv_head_idx; -// atomicAdd(&grad_key[k_tensor_idx * head_dim + idx], T(dK_T_sm_AT(idx, y))); - -// // dV: [batch_size, src_seq_len, kv_heads, head_dim] -// atomicAdd(&grad_value[k_tensor_idx * head_dim + idx], T(dV_sm_AT(y, idx))); -// } -// } -// } -// } // End of outer loop over K/V tiles - -// // Write back dQ_i to HBM -// int q_tile_start = Br * bid_z; -// int q_tile_end = min(q_tile_start + Br, target_seq_len); -// int bound_tid_y = min(Br, q_tile_end - q_tile_start); - -// if (tid_y < bound_tid_y) { -// int global_q_idx = q_tile_start + tid_y; -// int q_tensor_idx = ((bid_y * target_seq_len) + global_q_idx) * q_heads + bid_x; - -// for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { -// // dQ: [batch_size, target_seq_len, q_heads, head_dim] -// grad_query[q_tensor_idx * head_dim + idx] += T(dQ_sm_AT(tid_y, idx)); -// } -// } - -// // Undefine access macros -// #undef D_sm_AT -// #undef Q_sm_AT -// #undef K_T_sm_AT -// #undef V_sm_AT -// #undef dO_sm_AT -// #undef dK_T_sm_AT -// #undef dV_sm_AT -// #undef S_sm_AT -// #undef P_sm_AT -// #undef L_sm_AT -// #undef dQ_sm_AT -// } - -// /** -// * FlashAttention Forward Function -// * -// * This is the main entry point for FlashAttention forward computation. -// * It creates the output tensor and launches the appropriate kernel based on data type. -// * -// * Args: -// * query: Query tensor of shape [batch_size, seq_len_q, num_heads, head_dim] -// * key: Key tensor of shape [batch_size, seq_len_k, num_heads_kv, head_dim] -// * value: Value tensor of shape [batch_size, seq_len_k, num_heads_kv, head_dim] -// * attn_mask: Optional attention mask tensor -// * scale: Scaling factor for attention scores -// * is_causal: Whether to apply causal masking -// * dropout_p: Dropout probability -// * enable_gqa: Whether to enable grouped-query attention -// * -// * Returns: -// * Output tensor of shape [batch_size, seq_len_q, num_heads, head_dim] -// * logsumexp: Logsumexp tensor for backward pass [batch_size, num_heads, seq_len_q] -// * dropout_seed: Dropout seed for backward pass [1] -// */ - - -// /** -// * Launch FlashAttention Forward Kernel -// * -// * This function sets up grid and block dimensions and launches FlashAttention forward kernel. -// * -// * Args: -// * output: Output tensor -// * query: Query tensor -// * key: Key tensor -// * value: Value tensor -// * attn_mask: Optional attention mask tensor -// * scale: Scaling factor -// * is_causal: Whether to apply causal masking -// * dropout_p: Dropout probability -// * enable_gqa: Whether to enable GQA -// */ -// template -// void LaunchFlashAttentionForward(const std::shared_ptr &output, const std::shared_ptr &query, -// const std::shared_ptr &key, const std::shared_ptr &value, -// const std::shared_ptr &attn_mask, float scale, bool is_causal, -// int64_t dropout_p, bool enable_gqa, float *logsumexp_ptr, unsigned long long dropout_seed) { - -// const auto &query_dims = query->Dims(); -// const auto &key_dims = key->Dims(); -// const auto &value_dims = value->Dims(); - -// // Expected shapes: -// // query: [batch_size, seq_len_q, num_heads, head_dim] -// // key: [batch_size, seq_len_k, num_heads_kv, head_dim] -// // value: [batch_size, seq_len_k, num_heads_kv, head_dim] -// // output: [batch_size, seq_len_q, num_heads, head_dim] - -// int64_t batch_size = query_dims[0]; -// int64_t seq_len_q = query_dims[1]; -// int64_t num_heads = query_dims[2]; -// int64_t head_dim = query_dims[3]; -// int64_t seq_len_k = key_dims[1]; -// int64_t num_heads_kv = key_dims[2]; - -// CHECK_EQ(key_dims[3], head_dim) << "Key head dimension must match query head dimension"; -// CHECK_EQ(value_dims[3], head_dim) << "Value head dimension must match query head dimension"; -// CHECK_EQ(value_dims[1], seq_len_k) << "Value sequence length must match key sequence length"; -// CHECK_EQ(value_dims[2], num_heads_kv) << "Value number of KV heads must match key"; - -// if (enable_gqa) { -// CHECK(num_heads % num_heads_kv == 0) << "Number of query heads must be divisible by number of KV heads for GQA"; -// } else { -// CHECK_EQ(num_heads, num_heads_kv) << "Number of query and KV heads must match for standard attention"; -// } - -// T *output_ptr = static_cast(output->DataPtr()); -// const T *query_ptr = static_cast(query->DataPtr()); -// const T *key_ptr = static_cast(key->DataPtr()); -// const T *value_ptr = static_cast(value->DataPtr()); -// const T *attn_mask_ptr = attn_mask ? static_cast(attn_mask->DataPtr()) : nullptr; - -// // Set up grid and block dimensions according to FlashAttention v2 -// // block_dim(Br, Bc) where Br = Bc = 32 -// // grid_dim(query_heads, batch_size, Tr) where Tr = ceil(seq_len_q / Br) -// constexpr int Br = 32; -// constexpr int Bc = 32; -// int64_t Tr = (seq_len_q + Br - 1) / Br; - -// dim3 block_dims(Bc, Br); // (blockDim.x, blockDim.y) = (Bc, Br) -// dim3 grid_dims(num_heads, batch_size, Tr); // (gridDim.x, gridDim.y, gridDim.z) = (num_heads, batch_size, Tr) - -// // Calculate shared memory size (removed dropout_sm allocation) -// // SP[Br][Bc] (double) + m_prev[Br] (float) + m_new[Br] (float) + l_prev[Br] (float) + l_new[Br] (float) -// // + Q_sm[Br][head_dim] (float) + K_T_sm[head_dim][Bc] (float) + V_sm[Bc][head_dim] (float) + O_sm[Br][head_dim] (float) -// size_t shared_mem_size = Br * Bc * sizeof(double) // SP -// + 4 * Br * sizeof(float) // m_prev, m_new, l_prev, l_new -// + (Br + Bc + Bc + Br) * head_dim) * sizeof(float); // Q_sm, K_T_sm, V_sm, O_sm -// // Note: Removed dropout_sm[Br][Bc] (bool) allocation - -// auto device = output->GetDevice(); -// const auto &cuda_stream = dynamic_cast( -// infini_train::core::GetDeviceGuardImpl(device.type())->GetStream(device)) -// ->cuda_stream(); - -// FlashAttentionForwardKernel<<>>( -// output_ptr, query_ptr, key_ptr, value_ptr, attn_mask_ptr, -// logsumexp_ptr, scale, is_causal, enable_gqa, dropout_p, dropout_seed, -// batch_size, seq_len_q, seq_len_k, num_heads, num_heads_kv, head_dim); -// } - -// /** -// * Launch FlashAttention Backward Kernel -// * -// * This function sets up grid and block dimensions and launches FlashAttention backward kernel. -// * -// * Args: -// * grad_query: Gradient tensor for query -// * grad_key: Gradient tensor for key -// * grad_value: Gradient tensor for value -// * query: Query tensor from forward pass -// * key: Key tensor from forward pass -// * value: Value tensor from forward pass -// * output: Output tensor from forward pass -// * grad_output: Gradient from upstream -// * logsumexp: Logsumexp tensor from forward pass -// * dropout_seed: Dropout seed for reproducibility -// * attn_mask: Optional attention mask tensor -// * scale: Scaling factor -// * is_causal: Whether causal masking was applied -// * dropout_p: Dropout probability -// * enable_gqa: Whether GQA was enabled -// */ -// template -// void LaunchFlashAttentionBackward(const std::shared_ptr &grad_query, const std::shared_ptr &grad_key, -// const std::shared_ptr &grad_value, const std::shared_ptr &query, -// const std::shared_ptr &key, const std::shared_ptr &value, -// const std::shared_ptr &output, const std::shared_ptr &grad_output, -// const std::shared_ptr &logsumexp, -// unsigned long long dropout_seed, -// const std::shared_ptr &attn_mask, -// float scale, bool is_causal, int64_t dropout_p, bool enable_gqa) { - -// const auto &query_dims = query->Dims(); -// const auto &key_dims = key->Dims(); - -// // Expected shapes: -// // query: [batch_size, seq_len_q, num_heads, head_dim] -// // key: [batch_size, seq_len_k, num_heads_kv, head_dim] -// // value: [batch_size, seq_len_k, num_heads_kv, head_dim] -// // output: [batch_size, seq_len_q, num_heads, head_dim] - -// int64_t batch_size = query_dims[0]; -// int64_t seq_len_q = query_dims[1]; -// int64_t num_heads = query_dims[2]; -// int64_t head_dim = query_dims[3]; -// int64_t seq_len_k = key_dims[1]; -// int64_t num_heads_kv = key_dims[2]; - -// CHECK_EQ(key_dims[3], head_dim) << "Key head dimension must match query head dimension"; -// CHECK_EQ(value->Dims()[3], head_dim) << "Value head dimension must match query head dimension"; -// CHECK_EQ(value->Dims()[1], seq_len_k) << "Value sequence length must match key sequence length"; -// CHECK_EQ(value->Dims()[2], num_heads_kv) << "Value number of KV heads must match key"; - -// if (enable_gqa) { -// CHECK(num_heads % num_heads_kv == 0) << "Number of query heads must be divisible by number of KV heads for GQA"; -// } else { -// CHECK_EQ(num_heads, num_heads_kv) << "Number of query and KV heads must match for standard attention"; -// } - -// // Precompute D = rowsum(dO ∘ O) before main backward loop -// // D shape: [batch_size, seq_len_q, num_heads] -// // dO shape: [batch_size, seq_len_q, num_heads, head_dim] -// // O shape: [batch_size, seq_len_q, num_heads, head_dim] -// auto D = function::Sum(grad_output * output, 3, false); - -// T *grad_query_ptr = static_cast(grad_query->DataPtr()); -// T *grad_key_ptr = static_cast(grad_key->DataPtr()); -// T *grad_value_ptr = static_cast(grad_value->DataPtr()); -// const T *query_ptr = static_cast(query->DataPtr()); -// const T *key_ptr = static_cast(key->DataPtr()); -// const T *value_ptr = static_cast(value->DataPtr()); -// const T *output_ptr = static_cast(output->DataPtr()); -// const T *grad_output_ptr = static_cast(grad_output->DataPtr()); -// const float *logsumexp_ptr = static_cast(logsumexp->DataPtr()); -// const float *D_ptr = static_cast(D->DataPtr()); -// const T *attn_mask_ptr = attn_mask ? static_cast(attn_mask->DataPtr()) : nullptr; - -// // Set up grid and block dimensions according to FlashAttention v2 backward -// // block_dim(Bc, Br) where Bc = Br = 32 -// // grid_dims(num_heads_q, batch_size, Tr) where Tr = ceil(seq_len_q / Br) -// constexpr int Br = 32; -// constexpr int Bc = 32; -// int64_t Tr = (seq_len_q + Br - 1) / Br; - -// dim3 block_dims(Bc, Br); // (blockDim.x, blockDim.y) = (Bc, Br) -// dim3 grid_dims(num_heads, batch_size, Tr); // (gridDim.x, gridDim.y, gridDim.z) = (num_heads, batch_size, Tr) - -// // Calculate shared memory size for backward pass -// // Q_sm[Br][head_dim], K_T_sm[head_dim][Bc], V_sm[Bc][head_dim] -// // dO_sm[Br][head_dim], dK_T_sm[head_dim][Bc], dV_sm[Bc][head_dim] -// // S_sm[Br][Bc], P_sm[Br][Bc] -// // D_sm[Br] - D values loaded from HBM to shared memory -// size_t shared_mem_size = Br * head_dim * sizeof(float) // Q_sm -// + head_dim * Bc * sizeof(float) // K_T_sm -// + Bc * head_dim * sizeof(float) // V_sm -// + Br * head_dim * sizeof(float) // dO_sm -// + head_dim * Bc * sizeof(float) // dK_T_sm -// + Bc * head_dim * sizeof(float) // dV_sm -// + Br * Bc * sizeof(float) // S_sm or (dP_sm when compute dP_sm = dO_i @ V_j^T \in R^{Br*Bc}) -// + Br * Bc * sizeof(float) // P_sm or (dS_sm when compute dS_sm = P_sm_ij pointwise multiplied by (dP_sm_ij - D_i) \in R^{Br*Bc}) -// + Br * sizeof(float) // L_i -// + Br * sizeof(float); // D_sm (loaded from HBM) - -// auto device = query->GetDevice(); -// const auto &cuda_stream = dynamic_cast( -// infini_train::core::GetDeviceGuardImpl(device.type())->GetStream(device)) -// ->cuda_stream(); - -// FlashAttentionBackwardKernel<<>>( -// grad_query_ptr, grad_key_ptr, grad_value_ptr, -// query_ptr, key_ptr, value_ptr, output_ptr, grad_output_ptr, -// logsumexp_ptr, D_ptr, dropout_seed, attn_mask_ptr, -// scale, is_causal, dropout_p, enable_gqa, -// batch_size, seq_len_q, seq_len_k, num_heads, num_heads_kv, head_dim); -// } - -// /** -// * FlashAttention Backward Function -// * -// * This is the main entry point for FlashAttention backward computation. -// * It creates gradient tensors and launches the appropriate kernel based on data type. -// * -// * Args: -// * grad_query: Gradient tensor for query -// * grad_key: Gradient tensor for key -// * grad_value: Gradient tensor for value -// * query: Query tensor from forward pass -// * key: Key tensor from forward pass -// * value: Value tensor from forward pass -// * output: Output tensor from forward pass -// * grad_output: Gradient from upstream -// * attn_mask: Optional attention mask tensor -// * scale: Scaling factor -// * is_causal: Whether causal masking was applied -// * dropout_p: Dropout probability (not implemented) -// * enable_gqa: Whether GQA was enabled -// * -// * Returns: -// * Tuple of (grad_query, grad_key, grad_value) tensors -// */ - - -// // Non-template wrapper functions for registration -// FlashAttentionForwardOutput FlashAttentionForward(const std::shared_ptr &query, const std::shared_ptr &key, -// const std::shared_ptr &value, -// const std::shared_ptr &attn_mask, float scale, bool is_causal, -// int64_t dropout_p, bool enable_gqa) { -// auto dtype = query->Dtype(); -// const auto &query_dims = query->Dims(); - -// // Output shape: [batch_size, seq_len_q, num_heads, head_dim] -// std::vector output_dims = {query_dims[0], query_dims[1], query_dims[2], query_dims[3]}; -// auto output = std::make_shared(output_dims, dtype, query->GetDevice()); - -// // Allocate logsumexp tensor for backward pass -// // Shape: [batch_size, num_heads, seq_len_q] -// std::vector logsumexp_dims = {query_dims[0], query_dims[2], query_dims[1]}; -// auto logsumexp = std::make_shared(logsumexp_dims, DataType::kFLOAT32, query->GetDevice()); -// float *logsumexp_ptr = static_cast(logsumexp->DataPtr()); - -// // Allocate dropout_seed tensor for backward pass -// // Shape: [1] -// unsigned long long dropout_seed = 0; -// std::shared_ptr dropout_seed_tensor; -// if (dropout_p > 0) { -// std::vector dropout_seed_dims = {1}; -// dropout_seed_tensor = std::make_shared(dropout_seed_dims, DataType::kUINT64, query->GetDevice()); -// dropout_seed = static_cast(std::time(nullptr)); -// unsigned long long *dropout_seed_ptr = static_cast(dropout_seed_tensor->DataPtr()); -// *dropout_seed_ptr = dropout_seed; -// } - -// switch (dtype) { -// DISPATCH_CASE(WRAP(LaunchFlashAttentionForward(output, query, key, value, attn_mask, scale, is_causal, -// dropout_p, enable_gqa, logsumexp_ptr, -// dropout_seed);), -// DataType::kFLOAT32) -// DISPATCH_CASE(WRAP(LaunchFlashAttentionForward(output, query, key, value, attn_mask, scale, -// is_causal, dropout_p, enable_gqa, logsumexp_ptr, -// dropout_seed);), -// DataType::kBFLOAT16) -// default: -// LOG_LOC(FATAL, "CUDA FlashAttention forward: 'Unsupported data type'"); -// } - -// FlashAttentionForwardOutput result; -// result.output = output; -// result.logsumexp = logsumexp; -// result.dropout_seed = dropout_seed_tensor; -// return result; -// } - -// std::vector> FlashAttentionBackward(const std::shared_ptr &grad_query, const std::shared_ptr &grad_key, -// const std::shared_ptr &grad_value, const std::shared_ptr &query, -// const std::shared_ptr &key, const std::shared_ptr &value, -// const std::shared_ptr &output, const std::shared_ptr &grad_output, -// const std::shared_ptr &logsumexp, -// const std::shared_ptr &dropout_seed, -// const std::shared_ptr &attn_mask, -// float scale, bool is_causal, int64_t dropout_p, bool enable_gqa) { -// auto dtype = query->Dtype(); - -// // Create gradient tensors with same shapes as inputs -// auto grad_query = std::make_shared(query->Dims(), dtype, query->GetDevice()); -// auto grad_key = std::make_shared(key->Dims(), dtype, key->GetDevice()); -// auto grad_value = std::make_shared(value->Dims(), dtype, value->GetDevice()); - -// // Initialize gradients to zero -// DispatchFunc(dtype, [=]() { grad_query->Fill(0); }, "CUDA FlashAttentionBackward"); -// DispatchFunc(dtype, [=]() { grad_key->Fill(0); }, "CUDA FlashAttentionBackward"); -// DispatchFunc(dtype, [=]() { grad_value->Fill(0); }, "CUDA FlashAttentionBackward"); - -// // Get dropout seed value -// unsigned long long dropout_seed_value = 0; -// if (dropout_seed) { -// dropout_seed_value = *static_cast(dropout_seed->DataPtr()); -// } - -// switch (dtype) { -// DISPATCH_CASE(WRAP(LaunchFlashAttentionBackward(grad_query, grad_key, grad_value, query, key, value, output, grad_output, -// logsumexp, dropout_seed_value, attn_mask, scale, is_causal, -// dropout_p, enable_gqa);), -// DataType::kFLOAT32) -// DISPATCH_CASE(WRAP(LaunchFlashAttentionBackward(grad_query, grad_key, grad_value, query, key, value, output, grad_output, -// logsumexp, dropout_seed_value, attn_mask, scale, is_causal, -// dropout_p, enable_gqa);), -// DataType::kBFLOAT16) -// default: -// LOG_LOC(FATAL, "CUDA FlashAttention backward: 'Unsupported data type'"); -// } - -// return {grad_query, grad_key, grad_value}; -// } - -// } // namespace infini_train::kernels::cuda - -// // Register FlashAttention kernels with the dispatcher -// #define REGISTER_CUDA_FLASHATTENTION_KERNEL(kernel_name) \ -// REGISTER_KERNEL(infini_train::Device::DeviceType::kCUDA, kernel_name, infini_train::kernels::cuda::kernel_name) - -// REGISTER_CUDA_FLASHATTENTION_KERNEL(FlashAttentionForward) -// REGISTER_CUDA_FLASHATTENTION_KERNEL(FlashAttentionBackward) - -// #undef REGISTER_CUDA_FLASHATTENTION_KERNEL From 224f0e2204c833049b63c8d2db01c05a47d36b17 Mon Sep 17 00:00:00 2001 From: Aoshine999 <820459542@qq.com> Date: Mon, 16 Mar 2026 16:14:08 +0800 Subject: [PATCH 17/18] update config file --- scripts/run_models_and_profile.bash | 222 ++++++----- scripts/test_config.json | 559 ++++++++++++++-------------- 2 files changed, 407 insertions(+), 374 deletions(-) diff --git a/scripts/run_models_and_profile.bash b/scripts/run_models_and_profile.bash index 25ec041e..032d80a8 100755 --- a/scripts/run_models_and_profile.bash +++ b/scripts/run_models_and_profile.bash @@ -3,24 +3,57 @@ set -e set -o pipefail -# Parse arguments -REBUILD=false +usage() { + cat <<'EOF' +Usage: run_models_and_profile.bash [--test-config path] [--only-run tag1,tag2] + +Options: + --test-config PATH Path to test config JSON. Default: test_config.json. + --only-run TAGS Only run the specified tag groups, separated by commas. + -h, --help Show this help message. +EOF +} + +CONFIG_FILE="test_config.json" +ONLY_RUN_TAGS="" + while [[ $# -gt 0 ]]; do - case $1 in - --rebuild) - REBUILD=true + case "$1" in + --test-config) + [[ $# -lt 2 ]] && { echo "Error: --test-config requires a file path."; exit 1; } + CONFIG_FILE="$2" + shift 2 + ;; + --test-config=*) + CONFIG_FILE="${1#*=}" shift ;; - *) - CONFIG_FILE="$1" + --only-run) + [[ $# -lt 2 ]] && { echo "Error: --only-run requires a comma-separated tag list."; exit 1; } + ONLY_RUN_TAGS="$2" + shift 2 + ;; + --only-run=*) + ONLY_RUN_TAGS="${1#*=}" shift ;; + -h|--help) + usage + exit 0 + ;; + -*) + echo "Error: Unknown option: $1" + usage + exit 1 + ;; + *) + echo "Error: Unknown positional argument: $1" + usage + exit 1 + ;; esac done -CONFIG_FILE="${CONFIG_FILE:-test_config.json}" -export INFINI_FLASH_BF16_USE_FP32=0 - # Dependencies check if ! command -v jq >/dev/null 2>&1; then echo "Error: jq is required. Install with: sudo apt-get install -y jq" @@ -49,6 +82,28 @@ done < <(jq -r '.variables | to_entries[] | "\(.key)=\(.value)"' "$CONFIG_FILE") # Global variable to save the last cmake command LAST_CMAKE_CMD="" +declare -A SELECTED_TAGS=() + +normalize_tag() { + local raw="$1" + raw="${raw#"${raw%%[![:space:]]*}"}" + raw="${raw%"${raw##*[![:space:]]}"}" + printf '%s' "$raw" +} + +if [[ -n "$ONLY_RUN_TAGS" ]]; then + IFS=',' read -r -a requested_tags <<< "$ONLY_RUN_TAGS" + for raw_tag in "${requested_tags[@]}"; do + tag="$(normalize_tag "$raw_tag")" + [[ -z "$tag" ]] && continue + SELECTED_TAGS["$tag"]=1 + done + + if [[ ${#SELECTED_TAGS[@]} -eq 0 ]]; then + echo "Error: --only-run did not contain any valid tags." + exit 1 + fi +fi # Clean the build directory clean_build_dir() { @@ -62,11 +117,12 @@ run_and_log() { local cmd="$1" local log_name="$2" local is_profile="$3" - local log_dir="$4" - local profile_log_dir="$5" + local tag="${4:-basic}" local timestamp timestamp=$(date '+%Y-%m-%d %H:%M:%S') - local log_path="$(realpath "${log_dir}/${log_name}.log")" + local tag_log_dir="${LOG_DIR}/${tag}" + mkdir -p "$tag_log_dir" + local log_path="$(realpath "${tag_log_dir}/${log_name}.log")" echo -e "\033[1;32m============================================================\033[0m" echo -e "\033[1;36m[$timestamp] [Running] ${log_name}\033[0m" @@ -83,7 +139,7 @@ run_and_log() { # Notify if profiling mode is enabled if [[ "$is_profile" == "yes" ]]; then - echo -e "\033[1;35m[PROFILE MODE ON] Profiling logs will be saved to: ${profile_log_dir}\033[0m" + echo -e "\033[1;35m[PROFILE MODE ON] Profiling logs will be saved to: ${PROFILE_LOG_DIR}\033[0m" fi echo -e "\033[1;32m============================================================\033[0m" @@ -117,7 +173,7 @@ run_and_log() { # If profiling is enabled, move profiling files to the target directory if [[ "$is_profile" == "yes" ]]; then - move_profile_logs "$log_name" "$profile_log_dir" + move_profile_logs "$log_name" "$tag" fi } @@ -125,15 +181,17 @@ run_and_log() { # Move profiling output logs move_profile_logs() { local prefix="$1" - local target_profile_log_dir="$2" + local tag="${2:-basic}" + local tag_profile_dir="${PROFILE_LOG_DIR}/${tag}" + mkdir -p "$tag_profile_dir" # Move *.report.rankN files for report_file in "${BUILD_DIR}"/*.report.rank*; do if [[ -f "$report_file" ]]; then local base_name base_name=$(basename "$report_file") - mv "$report_file" "${target_profile_log_dir}/${prefix}_${base_name}" - echo "Moved $base_name to ${target_profile_log_dir}/${prefix}_${base_name}" + mv "$report_file" "${tag_profile_dir}/${prefix}_${base_name}" + echo "Moved $base_name to ${tag_profile_dir}/${prefix}_${base_name}" fi done @@ -142,28 +200,39 @@ move_profile_logs() { if [[ -f "$record_file" ]]; then local base_name base_name=$(basename "$record_file") - mv "$record_file" "${target_profile_log_dir}/${prefix}_${base_name}" - echo "Moved $base_name to ${target_profile_log_dir}/${prefix}_${base_name}" + mv "$record_file" "${tag_profile_dir}/${prefix}_${base_name}" + echo "Moved $base_name to ${tag_profile_dir}/${prefix}_${base_name}" fi done } -# Build "--key value" arg string from tests[i].args (shell-escaped) +# Build "--key value" arg string from test_groups[gi].tests[ti].args (shell-escaped) args_string_for_test() { - local idx="$1" - jq -r --argjson i "$idx" ' - .tests[$i].args + local group_idx="$1" + local test_idx="$2" + jq -r --argjson g "$group_idx" --argjson t "$test_idx" ' + .test_groups[$g].tests[$t].args | to_entries[] - | if .value == true then "--\(.key)" - elif .value == false then "--no\(.key)" - else "--\(.key)=\(.value|tostring)" - end + | "--\(.key) \(.value|tostring)" ' "$CONFIG_FILE" | paste -sd' ' - } # Run tests num_builds=$(jq '.builds | length' "$CONFIG_FILE") -num_tests=$(jq '.tests | length' "$CONFIG_FILE") +num_groups=$(jq '.test_groups | length' "$CONFIG_FILE") + +selected_group_count=0 +for ((gi=0; gi compare_logs/loss_comparison.log 2>&1 || true + python3 "${SCRIPT_DIR}/compare_loss.py" "$COMPARE_LOG_DIR" "$LOG_DIR" || true # Run compare_tps.py echo -e "\n\033[1;33m[Running] compare_tps.py\033[0m" - python3 "${SCRIPT_DIR}/compare_tps.py" "$COMPARE_LOG_DIR" "$LOG_DIR" --threshold 0.20 > compare_logs/tps_comparison.log 2>&1 || true + python3 "${SCRIPT_DIR}/compare_tps.py" "$COMPARE_LOG_DIR" "$LOG_DIR" || true echo -e "\n\033[1;32mComparison completed.\033[0m" else @@ -282,3 +303,6 @@ else echo -e "\033[1;33m or export COMPARE_LOG_DIR=/path/to/baseline_logs before running.\033[0m" echo -e "\033[1;33m============================================================\033[0m" fi + +echo -e "\n\033[1;36m[END OF TEST] Cleaning build directory after all tests\033[0m" +clean_build_dir \ No newline at end of file diff --git a/scripts/test_config.json b/scripts/test_config.json index 3d66ff7b..323523eb 100644 --- a/scripts/test_config.json +++ b/scripts/test_config.json @@ -1,10 +1,10 @@ { "variables": { "BUILD_DIR": "../build", - "GPT2_INPUT_BIN": "/data/shared/InfiniTrain-dev/data/llmc/gpt2/tinyshakespeare/tiny_shakespeare_train.bin", - "GPT2_LLMC_FILEPATH": "/data/shared/InfiniTrain-dev/data/llmc/gpt2/gpt2_124M.bin", - "LLAMA3_INPUT_BIN": "/data/shared/InfiniTrain-dev/data/llmc/llama3/tinyshakespeare/tiny_shakespeare_train.bin", - "LLAMA3_LLMC_FILEPATH": "/data/shared/InfiniTrain-dev/data/llmc/llama3/llama3.2_1B_fp32.bin", + "GPT2_INPUT_BIN": "../../data/llmc/gpt2/tinyshakespeare/tiny_shakespeare_train.bin", + "GPT2_LLMC_FILEPATH": "../../data/llmc/gpt2/gpt2_124M.bin", + "LLAMA3_INPUT_BIN": "../../data/llmc/llama3/tinyshakespeare/tiny_shakespeare_train.bin", + "LLAMA3_LLMC_FILEPATH": "../../data/llmc/llama3/llama3.2_1B_fp32.bin", "PROFILE_LOG_DIR": "./profile_logs", "LOG_DIR": "./logs", "COMPARE_LOG_DIR": "" @@ -21,279 +21,288 @@ "cmd": "cmake -DUSE_CUDA=ON -DUSE_NCCL=ON -DPROFILE_MODE=ON .. && make -j" } ], - "tests": [ + "test_groups": [ { - "id": "1", - "args": { - "dtype": "float32" - } + "tag": "basic", + "tests": [ + { + "id": "1", + "args": { + "dtype": "float32" + } + }, + { + "id": "1_bfloat16", + "args": { + "dtype": "bfloat16" + } + }, + { + "id": "2", + "args": { + "dtype": "float32", + "num_iteration": 10, + "batch_size": 80, + "total_batch_size": 5120 + } + }, + { + "id": "2_bfloat16", + "args": { + "dtype": "bfloat16", + "num_iteration": 10, + "batch_size": 80, + "total_batch_size": 5120 + } + }, + { + "id": "3", + "args": { + "dtype": "float32", + "nthread_per_process": 8, + "num_iteration": 10, + "batch_size": 10, + "total_batch_size": 5120 + } + }, + { + "id": "3_bfloat16", + "args": { + "dtype": "bfloat16", + "nthread_per_process": 8, + "num_iteration": 10, + "batch_size": 10, + "total_batch_size": 5120 + } + }, + { + "id": "4", + "args": { + "dtype": "float32", + "nthread_per_process": 8, + "num_iteration": 10, + "batch_size": 40, + "total_batch_size": 5120, + "tensor_parallel": 4 + } + }, + { + "id": "4_bfloat16", + "args": { + "dtype": "bfloat16", + "nthread_per_process": 8, + "num_iteration": 10, + "batch_size": 40, + "total_batch_size": 5120, + "tensor_parallel": 4 + } + }, + { + "id": "5", + "args": { + "dtype": "float32", + "nthread_per_process": 8, + "num_iteration": 10, + "batch_size": 40, + "total_batch_size": 5120, + "tensor_parallel": 4, + "sequence_parallel": true + } + }, + { + "id": "5_bfloat16", + "args": { + "dtype": "bfloat16", + "nthread_per_process": 8, + "num_iteration": 10, + "batch_size": 40, + "total_batch_size": 5120, + "tensor_parallel": 4, + "sequence_parallel": true + } + }, + { + "id": "6", + "args": { + "dtype": "float32", + "nthread_per_process": 8, + "num_iteration": 10, + "batch_size": 10, + "total_batch_size": 5120, + "pipeline_parallel": 8 + } + }, + { + "id": "6_bfloat16", + "args": { + "dtype": "bfloat16", + "nthread_per_process": 8, + "num_iteration": 10, + "batch_size": 10, + "total_batch_size": 5120, + "pipeline_parallel": 8 + } + }, + { + "id": "7", + "args": { + "dtype": "float32", + "nthread_per_process": 4, + "num_iteration": 10, + "batch_size": 10, + "total_batch_size": 5120, + "pipeline_parallel": 4, + "virtual_pipeline_parallel": 2 + } + }, + { + "id": "7_bfloat16", + "args": { + "dtype": "bfloat16", + "nthread_per_process": 4, + "num_iteration": 10, + "batch_size": 10, + "total_batch_size": 5120, + "pipeline_parallel": 4, + "virtual_pipeline_parallel": 2 + } + }, + { + "id": "8", + "args": { + "dtype": "float32", + "nthread_per_process": 8, + "num_iteration": 10, + "batch_size": 40, + "total_batch_size": 5120, + "tensor_parallel": 2, + "sequence_parallel": true, + "pipeline_parallel": 2, + "virtual_pipeline_parallel": 2 + } + }, + { + "id": "8_bfloat16", + "args": { + "dtype": "bfloat16", + "nthread_per_process": 8, + "num_iteration": 10, + "batch_size": 40, + "total_batch_size": 5120, + "tensor_parallel": 2, + "sequence_parallel": true, + "pipeline_parallel": 2, + "virtual_pipeline_parallel": 2 + } + } + ] }, { - "id": "1_bfloat16", - "args": { - "dtype": "bfloat16" - } - }, - { - "id": "2", - "args": { - "dtype": "float32", - "num_iteration": 10, - "batch_size": 80, - "total_batch_size": 5120 - } - }, - { - "id": "2_bfloat16", - "args": { - "dtype": "bfloat16", - "num_iteration": 10, - "batch_size": 80, - "total_batch_size": 5120 - } - }, - { - "id": "3", - "args": { - "dtype": "float32", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 10, - "total_batch_size": 5120 - } - }, - { - "id": "3_distopt", - "args": { - "dtype": "float32", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 10, - "total_batch_size": 5120, - "use_distributed_optimizer": true - } - }, - { - "id": "3_bfloat16", - "args": { - "dtype": "bfloat16", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 10, - "total_batch_size": 5120 - } - }, - { - "id": "3_bfloat16_distopt", - "args": { - "dtype": "bfloat16", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 10, - "total_batch_size": 5120, - "use_distributed_optimizer": true - } - }, - { - "id": "4", - "args": { - "dtype": "float32", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 40, - "total_batch_size": 5120, - "tensor_parallel": 4 - } - }, - { - "id": "4_distopt", - "args": { - "dtype": "float32", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 40, - "total_batch_size": 5120, - "tensor_parallel": 4, - "use_distributed_optimizer": true - } - }, - { - "id": "4_bfloat16", - "args": { - "dtype": "bfloat16", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 40, - "total_batch_size": 5120, - "tensor_parallel": 4 - } - }, - { - "id": "4_bfloat16_distopt", - "args": { - "dtype": "bfloat16", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 40, - "total_batch_size": 5120, - "tensor_parallel": 4, - "use_distributed_optimizer": true - } - }, - { - "id": "5", - "args": { - "dtype": "float32", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 40, - "total_batch_size": 5120, - "tensor_parallel": 4, - "sequence_parallel": true - } - }, - { - "id": "5_distopt", - "args": { - "dtype": "float32", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 40, - "total_batch_size": 5120, - "tensor_parallel": 4, - "sequence_parallel": true, - "use_distributed_optimizer": true - } - }, - { - "id": "5_bfloat16", - "args": { - "dtype": "bfloat16", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 40, - "total_batch_size": 5120, - "tensor_parallel": 4, - "sequence_parallel": true - } - }, - { - "id": "5_bfloat16_distopt", - "args": { - "dtype": "bfloat16", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 40, - "total_batch_size": 5120, - "tensor_parallel": 4, - "sequence_parallel": true, - "use_distributed_optimizer": true - } - }, - { - "id": "6", - "args": { - "dtype": "float32", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 10, - "total_batch_size": 5120, - "pipeline_parallel": 8 - } - }, - { - "id": "6_bfloat16", - "args": { - "dtype": "bfloat16", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 10, - "total_batch_size": 5120, - "pipeline_parallel": 8 - } - }, - { - "id": "7", - "args": { - "dtype": "float32", - "nthread_per_process": 4, - "num_iteration": 10, - "batch_size": 10, - "total_batch_size": 5120, - "pipeline_parallel": 4, - "virtual_pipeline_parallel": 2 - } - }, - { - "id": "7_bfloat16", - "args": { - "dtype": "bfloat16", - "nthread_per_process": 4, - "num_iteration": 10, - "batch_size": 10, - "total_batch_size": 5120, - "pipeline_parallel": 4, - "virtual_pipeline_parallel": 2 - } - }, - { - "id": "8", - "args": { - "dtype": "float32", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 40, - "total_batch_size": 5120, - "tensor_parallel": 2, - "sequence_parallel": true, - "pipeline_parallel": 2, - "virtual_pipeline_parallel": 2 - } - }, - { - "id": "8_distopt", - "args": { - "dtype": "float32", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 40, - "total_batch_size": 5120, - "tensor_parallel": 2, - "sequence_parallel": true, - "pipeline_parallel": 2, - "virtual_pipeline_parallel": 2, - "use_distributed_optimizer": true - } - }, - { - "id": "8_bfloat16", - "args": { - "dtype": "bfloat16", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 40, - "total_batch_size": 5120, - "tensor_parallel": 2, - "sequence_parallel": true, - "pipeline_parallel": 2, - "virtual_pipeline_parallel": 2 - } - }, - { - "id": "8_bfloat16_distopt", - "args": { - "dtype": "bfloat16", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 40, - "total_batch_size": 5120, - "tensor_parallel": 2, - "sequence_parallel": true, - "pipeline_parallel": 2, - "virtual_pipeline_parallel": 2, - "use_distributed_optimizer": true - } + "tag": "zero", + "tests": [ + { + "id": "3_distopt", + "args": { + "dtype": "float32", + "nthread_per_process": 8, + "num_iteration": 10, + "batch_size": 10, + "total_batch_size": 5120, + "use_distributed_optimizer": true + } + }, + { + "id": "3_bfloat16_distopt", + "args": { + "dtype": "bfloat16", + "nthread_per_process": 8, + "num_iteration": 10, + "batch_size": 10, + "total_batch_size": 5120, + "use_distributed_optimizer": true + } + }, + { + "id": "4_distopt", + "args": { + "dtype": "float32", + "nthread_per_process": 8, + "num_iteration": 10, + "batch_size": 40, + "total_batch_size": 5120, + "tensor_parallel": 4, + "use_distributed_optimizer": true + } + }, + { + "id": "4_bfloat16_distopt", + "args": { + "dtype": "bfloat16", + "nthread_per_process": 8, + "num_iteration": 10, + "batch_size": 40, + "total_batch_size": 5120, + "tensor_parallel": 4, + "use_distributed_optimizer": true + } + }, + { + "id": "5_distopt", + "args": { + "dtype": "float32", + "nthread_per_process": 8, + "num_iteration": 10, + "batch_size": 40, + "total_batch_size": 5120, + "tensor_parallel": 4, + "sequence_parallel": true, + "use_distributed_optimizer": true + } + }, + { + "id": "5_bfloat16_distopt", + "args": { + "dtype": "bfloat16", + "nthread_per_process": 8, + "num_iteration": 10, + "batch_size": 40, + "total_batch_size": 5120, + "tensor_parallel": 4, + "sequence_parallel": true, + "use_distributed_optimizer": true + } + }, + { + "id": "8_distopt", + "args": { + "dtype": "float32", + "nthread_per_process": 8, + "num_iteration": 10, + "batch_size": 40, + "total_batch_size": 5120, + "tensor_parallel": 2, + "sequence_parallel": true, + "pipeline_parallel": 2, + "virtual_pipeline_parallel": 2, + "use_distributed_optimizer": true + } + }, + { + "id": "8_bfloat16_distopt", + "args": { + "dtype": "bfloat16", + "nthread_per_process": 8, + "num_iteration": 10, + "batch_size": 40, + "total_batch_size": 5120, + "tensor_parallel": 2, + "sequence_parallel": true, + "pipeline_parallel": 2, + "virtual_pipeline_parallel": 2, + "use_distributed_optimizer": true + } + } + ] } ] -} - +} \ No newline at end of file From c5338ca0d301589952c474f152591780a2555a60 Mon Sep 17 00:00:00 2001 From: Aoshine999 <820459542@qq.com> Date: Mon, 16 Mar 2026 16:24:58 +0800 Subject: [PATCH 18/18] change config file --- scripts/run_models_and_profile.bash | 148 ++------ scripts/test_config.json | 551 ++++++++++++++-------------- 2 files changed, 293 insertions(+), 406 deletions(-) diff --git a/scripts/run_models_and_profile.bash b/scripts/run_models_and_profile.bash index 032d80a8..1cf27935 100755 --- a/scripts/run_models_and_profile.bash +++ b/scripts/run_models_and_profile.bash @@ -3,56 +3,7 @@ set -e set -o pipefail -usage() { - cat <<'EOF' -Usage: run_models_and_profile.bash [--test-config path] [--only-run tag1,tag2] - -Options: - --test-config PATH Path to test config JSON. Default: test_config.json. - --only-run TAGS Only run the specified tag groups, separated by commas. - -h, --help Show this help message. -EOF -} - -CONFIG_FILE="test_config.json" -ONLY_RUN_TAGS="" - -while [[ $# -gt 0 ]]; do - case "$1" in - --test-config) - [[ $# -lt 2 ]] && { echo "Error: --test-config requires a file path."; exit 1; } - CONFIG_FILE="$2" - shift 2 - ;; - --test-config=*) - CONFIG_FILE="${1#*=}" - shift - ;; - --only-run) - [[ $# -lt 2 ]] && { echo "Error: --only-run requires a comma-separated tag list."; exit 1; } - ONLY_RUN_TAGS="$2" - shift 2 - ;; - --only-run=*) - ONLY_RUN_TAGS="${1#*=}" - shift - ;; - -h|--help) - usage - exit 0 - ;; - -*) - echo "Error: Unknown option: $1" - usage - exit 1 - ;; - *) - echo "Error: Unknown positional argument: $1" - usage - exit 1 - ;; - esac -done +CONFIG_FILE="${1:-test_config.json}" # Dependencies check if ! command -v jq >/dev/null 2>&1; then @@ -82,28 +33,6 @@ done < <(jq -r '.variables | to_entries[] | "\(.key)=\(.value)"' "$CONFIG_FILE") # Global variable to save the last cmake command LAST_CMAKE_CMD="" -declare -A SELECTED_TAGS=() - -normalize_tag() { - local raw="$1" - raw="${raw#"${raw%%[![:space:]]*}"}" - raw="${raw%"${raw##*[![:space:]]}"}" - printf '%s' "$raw" -} - -if [[ -n "$ONLY_RUN_TAGS" ]]; then - IFS=',' read -r -a requested_tags <<< "$ONLY_RUN_TAGS" - for raw_tag in "${requested_tags[@]}"; do - tag="$(normalize_tag "$raw_tag")" - [[ -z "$tag" ]] && continue - SELECTED_TAGS["$tag"]=1 - done - - if [[ ${#SELECTED_TAGS[@]} -eq 0 ]]; then - echo "Error: --only-run did not contain any valid tags." - exit 1 - fi -fi # Clean the build directory clean_build_dir() { @@ -117,12 +46,9 @@ run_and_log() { local cmd="$1" local log_name="$2" local is_profile="$3" - local tag="${4:-basic}" local timestamp timestamp=$(date '+%Y-%m-%d %H:%M:%S') - local tag_log_dir="${LOG_DIR}/${tag}" - mkdir -p "$tag_log_dir" - local log_path="$(realpath "${tag_log_dir}/${log_name}.log")" + local log_path="$(realpath "${LOG_DIR}/${log_name}.log")" echo -e "\033[1;32m============================================================\033[0m" echo -e "\033[1;36m[$timestamp] [Running] ${log_name}\033[0m" @@ -173,7 +99,7 @@ run_and_log() { # If profiling is enabled, move profiling files to the target directory if [[ "$is_profile" == "yes" ]]; then - move_profile_logs "$log_name" "$tag" + move_profile_logs "$log_name" fi } @@ -181,17 +107,14 @@ run_and_log() { # Move profiling output logs move_profile_logs() { local prefix="$1" - local tag="${2:-basic}" - local tag_profile_dir="${PROFILE_LOG_DIR}/${tag}" - mkdir -p "$tag_profile_dir" # Move *.report.rankN files for report_file in "${BUILD_DIR}"/*.report.rank*; do if [[ -f "$report_file" ]]; then local base_name base_name=$(basename "$report_file") - mv "$report_file" "${tag_profile_dir}/${prefix}_${base_name}" - echo "Moved $base_name to ${tag_profile_dir}/${prefix}_${base_name}" + mv "$report_file" "${PROFILE_LOG_DIR}/${prefix}_${base_name}" + echo "Moved $base_name to ${PROFILE_LOG_DIR}/${prefix}_${base_name}" fi done @@ -200,18 +123,17 @@ move_profile_logs() { if [[ -f "$record_file" ]]; then local base_name base_name=$(basename "$record_file") - mv "$record_file" "${tag_profile_dir}/${prefix}_${base_name}" - echo "Moved $base_name to ${tag_profile_dir}/${prefix}_${base_name}" + mv "$record_file" "${PROFILE_LOG_DIR}/${prefix}_${base_name}" + echo "Moved $base_name to ${PROFILE_LOG_DIR}/${prefix}_${base_name}" fi done } -# Build "--key value" arg string from test_groups[gi].tests[ti].args (shell-escaped) +# Build "--key value" arg string from tests[i].args (shell-escaped) args_string_for_test() { - local group_idx="$1" - local test_idx="$2" - jq -r --argjson g "$group_idx" --argjson t "$test_idx" ' - .test_groups[$g].tests[$t].args + local idx="$1" + jq -r --argjson i "$idx" ' + .tests[$i].args | to_entries[] | "--\(.key) \(.value|tostring)" ' "$CONFIG_FILE" | paste -sd' ' - @@ -219,20 +141,7 @@ args_string_for_test() { # Run tests num_builds=$(jq '.builds | length' "$CONFIG_FILE") -num_groups=$(jq '.test_groups | length' "$CONFIG_FILE") - -selected_group_count=0 -for ((gi=0; gi

D4lu(BIw6z3tE@htyWb8U zd@ngZXDxIn-z}h0_a7Mic%EstpWd`wA_gFq;SJS!95J!w|CYRSWXK(y3`t(P!Hw1K zZgXS(bA)?46YBw)ZPY)xF_8RSJ$>a5sEu2&lFosy3!AK8&qeyqx$@0Ad6hTkzDSmz zPR?Hf@3y;cdO3HMW2XPn_;vu>S|OvHa?lhx{o6_BtsNY&Xf*XBO>^W|lLmCONWTZ{R{y7{0_w_Fai_fBAkz_?}l<;TogV94m1e$6(7R z*#f?mr@QCXZK(0Mcmba>Unux_Q3arkJ80cs)hrf9zWPUQfVhE@K6ckBn3uEdMc2`) z)6t@||9-o=Wg9Kz*7-YeMo?HK&1Xi*H~eUDXa^ww#lB)O!uu2zEt2XC;}>uT*gQD_7vR)HOUNk@Q4~!+z+Xw%>Li z`}woNPFy&!0l4U#N#5Me821d48$|PL*|A_@-jN*g(0o@%kY*azN|&Tp06KO zK0jv#VJBB*-*%qF>gE%b!eV`EVRbEkON9Ghahteo1ibGq+J3;pVMkNQkB;*E3tjk{ zfk$wFiQ4MAG)}Kh+=;SmTyAokuPzOBS)>oHAn<|pb% z;jXZcciZ*(WF@c3`Y+B0Chqd-@U^sBz-`)(kys0W;TKXAUTl89lEWSPflDn8bqYkP z#1Y%jo^xc2cx2))e;@@yQ&DgyOsylg&J+ECJhCXxke*4=J6|!e(X*2Va>7{P4#AdQ zJ=9~~IprR8G+ z1bzi_5F^K;cVL=+9aHBs>s;$mo-FTLH;qi{#-ukp^0433AK#7=2GAT51% zAwaDRE>2Xns)EPO4+#vkOYRw5U_V!0b+DCq`%>Az>6BNbZI1f;Mf=;*^Kx9x1W z$p(*rcL>-w();1Sq2baaQqd{t&6h!TMhvOog9@t+NPhd5qn-?4y;a!R_itQ`xbfdB zb^ovS{Z;2@tzzftH++TUs293B_@ZR1B`6F;AcY8HI37RJczVG&#HRWIvRpvE=o|`eqd~_n~ zk0eo(HesabYYhm%NF;oN*(%+X`oy;%M3$Nk6Q5$??fxgq!4e=%Wx`QSe*Z7DXo|p` z^~oI}2Z4g~NPLySA1Uyo)}U0tZrd3XBco%cu}RWh&|>e_Zuuv0+x4anwUvt^e{ALqY!kp7i8MG z)2tScs^7X_lH7v{^uuZf3mWE^ahH}3np?mf*xPkoryS4(s=-sQju3c(P86vF@tq4{{51@SvXTCTH~ zLr=b&DwC$+;)_k4F#lwc5e1?WdAV|g?VUq%3kX>%0R95jATjpPUfi#v8(#3)$Rro5%S1CjHa~Rs4*){sSlZODP^mw!^>QtCDU^J-uCC2Kklzd{IG@Hc4 z-2nxRMk;exu$MmYjLD`TlNTs91$LT%ib9>k6QE=A`-Z4pTbw8LiLs^ZBajMI?4=q( zKiFpsBO8qlo0gQ(gm(B^-L2uZ%JVH3x0<(7biaEsRT#tI9UZVi0; zi*}GE*F*dd0QAmu@K#F(91SX;Lhviy5CNeJ%{zl%@Ycze6lRFP)Kw3@Op_7V{RUBm z5Z=IRf-76J+H(2=Bhn78Q|?Xyc%w0*a47j?s|TekH}HtHsxu~C$R`j!lKfiLg3hLF+5bM-5X6dZ zZMa5v#KG5c4s42!13=PE5y6>227B9GMsw-`v4xpTEDwA$u@3iwI6zHx7bYG7`54Yl zVcX#FkTQ$f3WQ@ zwwEdsX3p9`dZ+*~mypd6e5snV6m|Ifq@wrQ(rdtrZ|!Deg66XlcAvMQo=P^x>JS7* zK_O!JjOH4PrK;l+Q7*mpb zdz)$&QCW|Vmm+Jaz>kujFLzE#x0Bb4wEo(#PiA$Tttn1~q=a_6%3 ziBXpGOktxK151`F*Kw1Qcf}v2$EP#^{3iw=bp`T;b zC7B2XO#$`a{*dxwRSmw&5+Q`_0FXu$y#+UD&F{UWnxv%H(`8$Fcpdd-tPdkl@&ozx zq_Ke?!2H8-`%A9@wVocN3>FD+(c}*HlX}yGPcW=VDqKn%RLl}C->c*TX- zUvi6J(%O7Iz|ctD!c5=?LFp<26Fd9wZi&6shlO9tU$Hz?Z8(fQpVxB#iGRSHg>x%| z@$)sbCqZJI$M!i_@ev;c#FynyKzvD%JD)PHr0y2Btu%!#7(sWvLRRV3ou|&FBw(1I z-3UKlq+OFXQ#{wZ!#3N;PAYrbu9JH%h(Q>>VtLM=y_V_#hj?18Neor@B=Xs5Yfrk| z3Ts-WXz*^%a0FF%VoRBjiwkV|6#)EF_u+3B74deUd_!l!h}H7xZj+g|Vyl*%PTozP z-8R-WP>yavzs2V|l)I|&^_WWWCBlHmbeI8(T6kM=LdU4KyCCTZ$ zCndWo_4yQ-oS=ks9D%zAb6`(PsWIlYwc&J&uXUh!qHCCm^~|f{iey z<3PUa9N79PxpGQjxFpYD^Sw1E*rI9lcr2*ioyeq>9*C!s_YuMxd6hOIXgR@NMzM+4 zR)P`q*&KoY_dpHNU%e8Q2P0m~^gO1y{}14f4rId=ms5W^#eR`i&vy**D>{L^VYX4) z6{NiIZ?E(FVRQuroS#f--I6zOz2UA)@!lX*NSxtgn~ZG#h8IS(Hk+74QZU8<8@h&P zEF4i|LV|)`BiQE38tfDG511q-*!?<@D04)qrtmaKsdMH3iGAs_Ri}qWZPwXSl6^ou za1OjS$R9>HT(>OT?12Q#J zX&33y70x&~9PO|HDv|7<6Yc%CzYnQuBPl*?lW)=mbC1yN#99E*dG=Pn&0@3w>HNIG zl8}JzaThOBA`2DwalQTbHWb5`i$xgS%0sR$NVShjG+GK5qi%?$_X$7kxr(!2<``GL zxaA>oQ!I_okTBb-QP}))FriXLT;x_&lh@OdMcKscVJ_3|$ir+2MF=HW<`+h5z?LSI z&7SK1qt{FrB)CU40&6UDFS#~$tv8>xLkBi!1$77Pu z_JQXbYXQBlpmiaW-I36XY>^Dx= zc?k~Sd0mr)!``ug$%vwtj()1;%lG^bUsRkjA-1+H)PIaZFHxUpw zE&IbySUsMF{5R%F`Z0GX$I%@iBRAiMR8T~kJ-5;O*HrZ(?xgJ&pF~ZHhbSdT0eQHR z5$_Wo85im^A`ri_TpL^#mAyj#)h*1$?2#IXv9`G2Ds5y2ry5zTf{#KO93C|``b#DD zK?9RJV-8`;DK>la?w44*kxs$0OV{vu%@yuTCP{i)4y6}i!`xHAIL5KsbAM()TVoQt zj(`zFO01A9_cXP~8@eN@AClf^Qf!Ta*p3)XCUW!%QgP=$*x&Ej*}kKl-L{3(H>Std zvR12~Lu6{0{nF*OOd(fC2S2x$s_d_5w!9o~HSE0Z;FY1fX&R^^yqXtA_gAac2xOMM zrcAn}zxOx#8EMRui1?Oz|3jmySez!n%?zJy)0cHg85o?O8@BO+Sam2pvI(*Xtue?r1RSPbLJ}dib?(% zbRol7WFT*K1rk~0$r{KXo@$&EpFta}WN1~W8hAT-y&3eY!6%KEuV=wVbE>^PZceZ) zS!ERaKs7gn@1+mT`G$yn ze!3roanE>;=w5qfluRJ}Rz=)%lgdl{y)X^9za6kWOzV3wVUhDC)jPI2I*;_%OhX2? zN+bOk>$BOXZIieS^y`btjGb~gC5@6I)g3K`5nI|An1g#SMWehLIO+yo7fpJUx8Kja z6UwdWKX|9wv{T)1+GoHxHs+awO6;>|#tB5Au1*6b-2zYJGxfE_W66BZfK!2TcMr$@ zIC( zn?A%u<3|AWE5;L1^N7#HYn2WKne6>n@YC$S%YfV+3&bh60Awf{K_~0E1!#2D)~L7x zLIv?P_mJ%7Op471Hvs{Glv*y7;@?54x#WKWcp}&#qAeF_|Fy&M0@vK6g!%F>gI%MV z7t;(np6m9$e0v$Gyb>H?Cw%J%465|u9Krwe(XBZzK}NDy&m7E)Ueu*njHShO3?M}< zyi)JnG0$rsa#aoyivp{F>B@34si|7 zAECMq=h1z4ILBDuf%CWFai@8NZnTw0jb;MXfOno!PdV+cQUezrMcnbKV5_6vkH3gaFM)cCu2r+%_XH z)=`*XsSab{q@6mg?Ky`ITLm&LVz$~o^#g1HbY%I_yDynpW+ycl-2&Gi%XY&P*m$TF zWqJ+0_J<0QidJBEKN9H0ws>t-+iRv0cMEd`k*Dz?<7~sDh**O8JC!QZyME^jp>{)1 z9f=(cY*7y=>e7^YaB%$k2-S|%fNQWDMMWonO2X_b2;fX8980_rhBXMjE!VSWd**TJ z52M1mOfv35lU}SQ!z>@G{1Op))oWEV;w>X0QGo$AEd&VvgNIxV)w>L#R>dWmDfPm3 z{5n6r|D>!Ye#WL+ElO2(a7x0qr=}os$fRg+ivD>u-Le0XcHMC`x9?xcqr-y+8d7K} zEfv}wAv6?eYHCMHY0s1F3Te_J?WwIaDO4(&ibNWkXldzp-KS2SGN147pXZgH)92iu zdtCQ5-tYI-F?dqbnowq%J391KAZ)tW+AmE)Mi`-dpRL)yiF3kLckMG=+rdo zJ5AY%a;lW8#1Q$gMk(|AJuNY6Tqe>NIk2U7q)|uV!3kZ5Nl20Lm-1YC5c!tBf=kJ> zd0VVnYxIwndG*j#jT4wUCR8ddr8}5+QoSw)Mi0 z6kK22Sgu-1@BO7rQ>FD({}_3yI9$J_&6i#E$DV)7k89AGl5hqgX5NN6@J3&VH-uK_4YMP%fyR?TeH8DoyFw zj48YO`DF~eJpC-tXzvETf0~BaHch&j$}7vh!fQ616y1YgI_%`??whkojd|R}FW|FK zlg2ipt472+X;v$W?*xv#APHzrpdZ244=%hsK=pX9@@ba0c1S{wEEIJJzyFQyZmNkBN|rR;<@V4%`(zxV{!rJ^ z^l*PkjS(?Y9Q2-b-B|^<{QA33(B)xvnXimd?^$;@YCHLW`3>YUb-Y-U{e3`{1hx`Z zwQmdU;N`cgWyfSC+h3ELAIO8?EpaDT6UwuKlF@x;AE9lf$?j33Nz-&B$&kiOCr>!Q z*9T`@0Xh#3b*Do{E0(q7>H{SM+0h5-Z9t%=zrH??Hz16c$9V!&q3=bTPz_!lZ-(@I zZDQ*N^RACTr=dC4XF?+?F#w0&v?gM2DUUB@*|j0$FP3T^z$RLAxHx%ygW$jX+7mrs(C?a+`6L+r8{m5njSlA3YV_D z9$odp&wS9$&5-HA#t)b64s`E!wQINSld(tlJD>v-)RZ2Kf~I#*PW?xd>pKr#qBQe9 z`$R3W0gTxLa;AfA0t10epi-l8^|OW0BR=c5e($W8u)J@RfBR!y)Z{35-v{XT^vD<4 zBO4#lPrCe{e-E*To;%h~`M$8J-yH0<%$hafo-0v)c^ixJ_bh16x#4aexd~{S=*R%Y z@eC>jUJZY+Pup|&Bu(L0>m z@B0o~1-vDpJD)~v?&M^O9Zk;JXw9&z%=y6QQ?`TeMfKPTxq$e-?B+3`oF?sDMw=(Z zXy1nW1W_voq*}MMp)}SuUDf{aoQ$DKPa(z6S9-?z0goN*s(aTh zg^+8A7_gPSkkC8vT`he`bRZ|v@+w~^P@ptvT?d`s$v)V3(y-mW5i*$|_LGAsdWCsA zJIPN-lkaJ8sk4K^#eI}F`ugRhSU>!}tc)k(iwC(T5+>&81TBR|FIjmXh(`0jh5ArH zu3G=jn<0Gg^>w4-=6Sbv_J`kP+ZW3O_8i+nYDQTz-p(`=w9WZA_qh#wv&c6|r*5}h zeocz`#(ErNyGngbx%)k6%-b*59Phh72DQswkY{e))EF5K%*+&W|58XOnji5Yr__cV zt9NnPt20+AJvrSZdZ5``COJlW%~@^N{o6kW8;5c5lvdetU)b6niu~rQTAvYFcpGRu zwTmTR*cU>xIr9s%Gsv|Kug~Ky8#whqUn{P{-WXId`q@BOKUZ@E$VB>MPs!uWL26dH zWxq~+>xM?=z740CNpC|Q)W7PE)>R2+zi&u(`M|p`;`9exC`Y}pCo0u7Pkp*P?iHas zOnEB1?%fK9^C z{*f{jaS=HO_EtToOr-G&k|UfHc^CVU{e;X7e}(~F?PM8+YU`sk3QNnAqv*>&I<2su^F%{Xw2_b(LSy`cxQU^-DukCh`r~J zkx}2-^=(*6jA?H?BvqTsD7(&b50s|AtVV50fjO?gw8Pa?_S=(b6IIQS7f+$*#&I%H zJ5WkF2Q=qs^27seW{|cj9kBl5W3MXfBE6;m3VYag+aLx=DbL_YJYmQM{<-t0N#t+&V!OCN2 z3tw$1b3-C+P3n9NlWoDh5)+~R#-8M3P<&cnc<{l-_Axz`%&cK2vC~bd>wB{W$`}f7 z9RzW+tB#Qcme)2PsCbu;)UTSZd;!xSgV>_u++D~Wta(og7^TJj#p0|s-f1NGida>u z?gsy>$_rAP!9_7Ks5ymFUvnee*{HUvkWw{$9WpI3(1(Ubbh5O{&HCMWcJfN?EmFxk zBcrP(aD%a{G-29yD#Db#I-R_Ng;c_yIze34$PWh>447zr#4WVx;Zh@ zyQfhr39iUb5Dz(jc>~q4VATHl!=qLSX#2@@9|^7aT-TZIQN{BLviW-*AHDllw&$3q z7UJ8VfuorMH5Ko&z%NlN}cxYsmoK58wxD3Zdw*J2$!Ewo~00+qI zJdrsY&R`E0RU46_7^E3$ZdX6Ifp|rWeWBCX08-s49YJlIy%~i0-vS`Ov?*1OgY=v% zcjj??<&2R5P04`bTb3z>R+w_T(La+OcdGY|6AQDu`yX1)HO>X!Wu zohPD-QlW4tzt?k=tJ0NZ2H3@&^vsAZgB0DGMPVzrfa%;+vL?3zgS;A7xdM;1}CRBjuNBJA0hmMoT37FpW()?MF z=`WNf7r*7t!KSg3Fr;4BRT~yt-Oaph|FMx9$nOB3Fke`8=Mukc_Q8J%Dg+*MlM!Te zcT@K)z6C@yurtKl03p%-fbPb20kqV%;34X!_iMqx`58w){Nj;%{{UHpPVU|!g}#~x zq>jV_3TRpW^p-?Yc{Q0Uj{naL-q;&hQQ&f9Ex)&DE7#m4ogjwk|ILudD_6sOJKP-r zSob+YlFa{|!pm~w0;JP6*2a<(ZEuky*y{hdZa_7M%5~GOF0K0C%9;^9_Lrxnh6h%z zza*BLct|qXLZZL@&AJoU9)^IuVA-1Ev;&vS8F7Z_ap;s0{1G(agj4quqcoc+F#uG4 z(DVJA^YA-&>{IgY{|@wzNI;_A^}(ukjq4Tve~pYYZo$fl?(JDX7ym@T=_eQW4+o~` z5m3ECRKK>iY*s``E$xz0uDdA&GzQ8b)aP z^9JIgS4i~xgR#?a+&aR9;6|xy{!q%Ne)-})Ar}B-s8%q+Ricy0qedf3N)-;AHFa#sa>L%$; z`U?X`KGnuFd0Rp|MpMW;pElnUM&LeGX1cFj*k$;ufsKrr!3gi>l;Ngd}_KQ|5OvR(U%l(NI;+I`_PSJWL?eEira=g*P7yBt@rQo6h!k zxiJlqXW^RWb{&$(rbt~M!D2x=%;Y-H$Wre+BhcOT*OQ!)0H!O?s>ci4MGm3L-EZsE z+a--`ybN~w;@+JXYjk%AopkUnYD1cQpDts!K7D9sP*#aJ!?+3U<>uqrX5Y9=e`Jsh!6-8`|9kg2g+*VvVV@*|DoEs{= ze9^YWRZ)K4NI2*}TBKk&$VR*Gmd{!`A}ww>JsrixK-T`Kle!8&T&w^I9!E~JTtC@@ zuzY;I;OQCnw=d~I7rTxmGDM%GxL7#))ndM=8RS`iJ?|7I#$Xa`|EZtN!oymbIs5=SIUBC)S*vBy5LMb~Q=7c?>kao{~Da*~Ht zzIf2gWd2Xl4~p%iKY02?Ucn-D7f2jCE)*~JLhn@}Xt~57b8I<&@Coi!zjzr8fQBaKG6F*{wI&0+yH#pppFdAtIKc=tL=S%X;+G7 z((TwkXL*mz6on^A-P*de#{x2&1B9b3R#KFUXF0%F2MTLEo(T<7W^7SE)Bs8{;z=LsL91|L^wYa;!^zUNaR znOsWRe*s;^(UBueCj78;L=k(ycXDbM>pr0Ln8^&=$|E4ro8x(OuKaaXlj4+nnh$_LXJ=* z8qHokFd1drbeaB_F?FMkR@%bU@_m}fe$Kir~3|I6fR}X*u&Bsig zx|FK_w~Gg6-3PjYTAtI+T9gmMwNlh9?tbvcF)hPk5cODB_XXNhMTf{vFQs|^$~aNm)XR-twDQ=&1;WflnveZQtXd@rq%zrqhy*gBAc^5?5Ss(fp<{;`)q z(~D1lh5zC?md~-W3BbqY&|F*@>7Ay5Z1JurE*s(ls@q38`ul6P77OoBA7a2twT17I z`&@Aa=3@0~gO9~z6B=xnbe#ntwUqBJ=gR3{cw9)=kZI<7HQXmhEdR;1T!%9@4_&dY0u7fIIYI9?V6KFikc+Er&@{?ze+6L%PE3(4J!)T#3~RYV|}_Lr-w zeslsxeo#it)&%ZN2Otq_gLj=$c@2Q!OCeMYht~{sRr3PwtM^7rlsB}c5x)aIO7~e@ zaU^anW4!<_pKQgR*9N!J02bi4Qml5#YxBdj`pv6a%cEW+RZmlF{i%+cVovx9;-EVdauHENMD*WcrRBsEhL@M zB}h)Obd^y@mD&%2JA8|3NE;STZ0rhtkv8b$pzYT8nxYE3^2KmQ5#Yjg!a#sH5c;4* z863|I8%V73mT9v|anf=Cz@C;ka}^N(fRlU453sDss~E!_I)ghv0DuROeDBK7GY&_5 zEmti_qo=bxzLK`I46_#X*{NrwTQi98^Sj<%|M<2V#TLW^dwsfzgIP6-DKwhwWlt#6 zXzC^9>HZ9@0K@gAjwe7<{hY#<$Zm0Hk&-M;`v^c!6(Pdb8%@Rb7N8rV{;L=b>JDS( zx=nj`vq|LF%Lm2oRfbCzh5#y<`YgG*54H`q88?W;n{S|(j}WN-O4(;LSHIA;Cpqaa8JtE`<_+NXPav@2SJY|n?k9u}Q{FFqm;Mn? zIKr5lO&`*xQkkl`Byn6s9zkXtp6z2iwreOYFh8+<_lPSb>hSgoYqtWy&N)*9C#Mfe zP8{9!N;igC6PkV1U-qQC@cFrdt5uAASw5Fu*_+-Zl?2r1!Lap_L;D%)5A-Ik6~bK( ztq3;Q;ZW_QJ+xY7Ni3+gzlikb*E7~_71zt~w+*Kj{__E=Awapv38z_I*67sG)!4?BRKW3(&x19TcZ>e32nsU5e9^Vn1UwzLB{ zLDkzf;N?bOq*hwJI?V15aJtz0FGgK}_IJs5ce;E?OXHd_H)z_Zo*8V8pmfCv75a=r zL1{4WD$@y*cPJZugg#tO1DGOD+H~8BPc1;Q`5vDlEIr{`&Jp8x-K9_e+OX5-g2?d@ zk=jv!yK3=IXpX*kke(QpCLfTkK=+_eImJ;~7~svEmrmaiv36}8Nw)KE3S2vbTK{x_ z^g}{)C4w>X2p@dSI~fz2TSaSYa!4eKax9{3eNBIjLUb+_@^Vrz?2r z^ajDTu@{D31hS*g3rv~EWRs)rP88@i`da(;3zQp^W*`W41rp!vxh*$ir)IgvFi&nL zazkkN%L@>eMJDU>_ZWrk2=XxUCMq4dvVqlL3y7u05+7_2SOsydY~HuQQA zNtXy&=qa`e{?M$pi5Hoo_LCF0pN*sgQpCHb)qJo*Z6d_wRrbp_PaPU&K>j7(1M(yJ zwpu=R_{SCE7ju z=qp=}bmL!{WtOLhjU@Wqed5^CCDtXZd~WTUJu97$DSlq;Q=IKN+-4TgpH-l(^ZuFoBuh)?;XIp`23Zxnt}q+x1&sKNqtHOy?P?s+hS2n*f;RNkZ#7t9cgSip1&6kV-=8)A?}@6G!(S>j*E!{2u7 zz036-F>9M2D`LV}zYV8e5>M<=mCl4O?vIyXulWXpDpM}JbU!fI_p|L@`QTm1q?Mqz|JL0r&d0E zCOlrWQbO_V;KRXQr|pv+5^sRcbt%6r?X5|S*OA-S{=KYYx}ykkyK>+P6vQ5dKnn=< zM>T$@K6qO=>)W`60YitiQNQ_lX6WSZk&nUwUFtoWCS7M;pV9hmS-`!^od26xb3$R_l=uV{D#Aso=2l|C2yUbA}gT!F7xdZ zG`iwKeWD&h{E|>p0b)tY-uyunN2T|j9#qIuxZK5U;UGH>y(e^fM(8qkGCyV&y46pu zThVu$5T#IrIwSQ3ZL#XUO53l=gcDmvV39d zPCv4ep7tZ1PNa5U%D*TnNU7Mk^JmeyAYr3~s%m|Rv9=!kDm#tIX4MULNwY})H0Uzu z#2QJ%78ICEMO9&f3$FGX&$u(*1iDM_qt?oGFtC#a4h3eV$&0fl2G&&EYS`6FQ{{1ngL2V@)5T|fJc2p^fat!ptk;dPrPQYAqdDJA>9q*v0( zI;>MTl4RkpnXUtGlV)-(n(PMkZH58ehtlcax2|(Iqz*_o{+3LRF0L^-`Y9xVk$oUq zjh!W^|Ky!h%v~&7OG2uLc^ltA|L0>XOs#pWU0CdM59uqVZZUe8OzrVPK%`MW_oj9D z|~wRkb3P0d+;`CJ@3 zt{AD)EZ@;Pt<(h$#IzMiM663Z6%GV=C7GD4mh9lmY;*o{kK_vu6VyCRfY z9CnKjoV}i)q6N1oYoo)6UX83+`T%%Jvb%0~S%(dU^tjX*-TJ<>ON8X(-GP=jya21U zu21nQ3sN*3(Oacs#Y4sOLOdUmND6ui40xrzGVX}QYx$ED9?p~*f&lX)Blj+75HPjq zQo_9?XQA+YIYLE#I-GGN_Mq|$F3`eajkL>5shDDB{BqfB+LD(}_)N~oTg||i<^1%o zK@mb3shFgJqi+*6juJo0qDd;J?tVThz4j3xY!ix3lDeWER;Dw&xI(ZLnHu5!X)gH^ zk?pMZHsIj;Cnf~0b5$^5kEAhbIVkb<=JJGa@a$BJpTK$z8h(zoUA~Ds=&*Ya1brPI z51&uJQwsa)=W87vQCAI2E??}a7)BPBVEQzk_xYOFf-S;(b{~_S{OTydyx&MGpZ9YKWf@%Gc#9usQinF{q&l{_*%YR4IrB-UQ&e8K6Zj)q-Vs4gEX)|&W}e8b`n`{f zduyM1wxmtXd>@=C2TEasnP;{$<8#6KmNx(qj*@VD+asGiLPkwJkvN!NY~J<#@H4(F zXyo&$t$wvjm4lb{#!g219x=md1Npd9Ij&u{2-_8 z)lD3ISN9QP>U5hi+ZW@Dh=TAUHAnzI(+Nr9KI|bnj#0#4L~UM!_tK)9C7XT5N6~la*$ceb%w_ zx`W*K=Sz~BK7h+egZ76$+x`5DPWTi8rYQh`#u=4i;(7Eab-KIvT#lJ~i5uNRKHTkK z0b`YloFzijHbb1yOGUlUnxJP-jbd-8`rH}a*gD}B&I6qdzqXDQWEhN#-QJPZ5BX*|6 z@k6H-chpE!pmr`7B*YDKWDT<{ER*-C-qpj4BDbW9(w@F&*NA4hjwsm#7`<1$R!f3i=gTL90i9Xuel?8ghB8 zTQhK*|9Nr09|?@H4t|M$tc_ClyK;3&k04tTk^g;c=d^60G=G%BOv_NgL%0U1j&X@k z=t#vQrWoB}7rL#eqkW$vE9xX$fpnty<8`VINLlQZRph~?9U*zim5}oGrfoOq=d~Z& zqh*k%xe^InI6#6;8xH0p-GBhjXm#<;R73dogr-mh4=;;&raWP z;V&_Q<59Y^Q9hxbo;LVgY?gq=$*Ok6-j%!xdHOD4YtEcXljx%3uRG**tNs!Ld-c#u zHDe}(O>{hljaf*h;WKcATpQ84$uZ+hH*E=#?ox{*`Yi=*cF~)M))ioJRRc7wo~QL^ zwFG!c2T6y2@iO-FJV3=iRiEPOe;_hUMaN}?)5Nc)3UPp~AUL)Qp`wl7q+n?l+n*-( zKE<)+eJgc0KJZ38`O<)k6-c)HM3Ra_xl-6Ms({3?x1fqBBfUm04`qbQTUtl6tbo6y za<|xvKIrDZy-+1kaQ_VLEmQs#=+VtO0dtt}8HJcJR^Wpeyp*a%T_jvwzddHsW*p0T zsyr0w5O?sJEG<7X)7tDobaTE}QFxY17DnYPgSCjgIbP<}_HrPc^7~p4n)nKuF9n*w z_EFZ&s5j$YUz9i*>i!-TDmISkB!8(3>g8@6_>iUZW*B9H?Hk zF?%wnp-cFN2onF}R1`g~k5)UOz@P=i{rk*!cXpWW5oKo(s>?j_E)e4YcC9{gZKniN zkJuWU#9`L1hTWgkLD9R8^W3!Q#V?$+xrG7Xp}n|F0xCO6lr&-#IL z$zlve4a=`i9|@^G4P;XDLd9njjJW3;ECta?4U)odd z9?IylM}fqyXhwPqYb7#FX zsYs}YtGe(Tlhf+qZS_d9qEe+e0WPHgFKiIy8iA7^9ytuf+w}9&o#p8jh*!MN?)rXrZdu%znY-=46`kovri zsyFjjN$`K|i`6WN7{6gnTUcp#n~L8Vxa3+?TjaL)Lmg2)=0#Zs7fF(rP;f8%nve7h zvNeK}KIdd+ln(aDQ0yKI9hHB988B=?{!Qp^y2dn5Apw4~f%?0oR>BIX&~#W*`?Ckl zzfO;0Bu3(**0y(OR}MwAZmo{?6dV%na*VwlK3-|8g6lcm=*=-^K5*@Bb9rVq=%pM_ z7&<-i!opSQMVDdxM2Pjxo9x!_J~Bk{vcaT#?JeRVK32adaLC;&TEBvv{eje_MUFo% znVe)Y12(LYj;v}!5S3(4L5XeC$XTih;9v+UcsngFmeiYs2IzqER{U8F@mhL8Lf`J{+{L^b|2rqGjgMmi`20c$NB^r zr`QM6LTBH=MHNr^sW}kIRW8RpLvCLODp*bSm`O~c>PpAa>yF1q9-iREP$2T9vh^o7 zvt)^ke3ly;D>JEH$!*^?ZQ5d>yF0TGqPiWJkhNSP_kzhPk=$t@*|!g)S=sqXtKw{W zs!(g-u+fYdW#%+i?CoXRG5xs@xEfWYt9zwS?906Q-Po@zQK5}z#IV4neERd7E^t=- z%-w(?t1|S~`S3Tv_fNnitAK2ok31-Ul-3V%?r4OnCQ8B>+ur18)NJT*^?Hxujtg1O zA3(|^%S=7F@J(L*rL4fZkQ*lxY;>MwnAaDb9=L+4SM^<<^a;Uw+E(vA77U(yXEZbH z08UPlR5EJdX(Qh7!X~{Mvoq%Ha7?XBh|S3H>si6e+K>NJ()*pbEUBAD#;_E~wNS;u zxk9zRuPZ%fwzsP;{|bg#S#>UoLzh3hFJHU8e{Z2@EEWCX8-~SfkrO)7rLvY>qERnA zJNPAUqE6>J$Uo$i^W#(W@n3edL=vAb6XK7o&b%HQRKU7F9mJIa>>xN^u2)R99d`9t z8uB1n1JtHwu7qN1QEJjmFxQJQtL~VrzcVAJu73xHrc8)AeAYirsGqL-0wu}D0N}@E zorW~1@;fC3aV@ZqtmgLzU(bM2HCKwmi&HXH%39XauPY8&eL9%0v!_*n(@^TJ=_!9@ z6%M3)y&+9@iy2b!{a}VVJ>Am^ z@T7XaUf^}U+Xorf`pY+jjc&`DoCN4KuC})!BvOf2;040*h z(&p#<C_V!qcSuin>``+YBERSOi#JVoj8G!hKhXZEj7lZxp=ZQk+u0b zP@|bs^PYJt*W&AgR%Hvb534)gp0%(lrb8wyP_^KzKQjCs(7#ky9VRishS|XZnoDb5 zX#Ea_`vA{g>9lJ5?8BDZZCILYAmRv2uM$ZJIa@}@YQswAzpMs&uqqWmWo8WnCoYbx z2oiw1R<%Jf>ZMsBP#O#Fk9r;1?fad*?iS^1s{OJNyv)8xy=eKK+akjqNR_RXafGHS(wzqpWgx{*Tl!w+`YwJR8PJ>?pl`Y>F zc_M}^*VG zO)zfOhLM7|_SHL8)$}g-Vw;}Ajabgw_yjnRN439|(5PH`HeUWPUgFJ+uY29yON<&9 z$kR=L2(be*RJwbzi~g`8cmF%6t#=@_Nm15o*9;|TSJ`J=B&|&!X@zGn+-wYi`X3o9 zZTOqB4(7R(WD46#FROf@g9`PUx8lSj53wDm2@h%19E>z~yWe6g_~cEBup8uUZix&8 zII;75QW%&HxuvP}FzZ85s(zIg={*{)bCCAGu`k~8Lt7bDO&;u)l>?DvN7CK2teYjE z3AOBmfc?7(>#(ihAFJ29dXyt&=LDy1M$rCOGyipw(?m#@VwH=>ZDAFkaUi4T)fM-8 zu9O^!Q&2Wv+h;!`w`U?GTH1=qIB$69>~$wiqY9AX(K%+ZIMGhc)3%Ig^DV9A(_V+( zBKE_5vzwGnq39;wmnrgFB?e!_O*hR0TcnCAR_5|{y;R7~W}|4|5E#(O(kClS&eAFK z=p~}qnLapCsSBck5;4f{7<|kCFv9&>BYlm4qq!HGzku{H?9p9(7rJoI^dI zcWu)OXjGCqGufZ5^Zc?alnW)}DUSZjA8rUb7?U!cz5SP*sxNzBsEqEQS_EsjfMlH? zR!#a{#WQEjYS!&)7I$puI{J~k(P0YC;-`*U(_s|HN^T;pvgem|U$ggq4J*2 z$fY$BhLaEEkPJ+M?x%1l!Be#dMUB?IZPk`i{^Ulg7RT;VF>yNF(zHhcNI9}qdq=Bt z&&boA9at&(ZLg{1ndYi)i#=~F_dNvJR9UGT>bvc8QJETP8q^1wrz_;kk3LWPX5?31 zrgn&HgU&G`Oqilhh7NmSormkJ6rQlY z&*ut{Ju6eeCe0L-ORENr}59V(HC;DWjuSOdS#mmI6Z$!_pZbwPDjcY_%?Spl7CPLd|LOoAfUq9Epv&FSjAe`f&YVYK>!SRq&r|=o~&wUvd z6R*7`t`AF8#l~NE{>Yp#*-^eDrLT^)f}Daj*gfgbaS5OwunNlCu}w-b`;3V{UL#7n z&be*5tn}*%(DKuy(!K+zvgg|^G0h;(=zbC^C>T5`IrfjOmI<3)6ogXA&ATOD+udDr@!Ji>2Bz220R5IM0j{L#RE>yN6ok9BX zjy)1alTbyDzCQqIpOp5UCpQnt1rO%D^thlNXwGlwT70j^?cqgguNUMNBWK%hQBqUi z=s&kcS+sSADo@kx_3lZ7cXF6&BlQNuJCl2KK5`eQpO!q;km!4qjOMj&u}71EU|~pM z#+`lvLkWC;bHBSD7OQQ;Q)dkQ9u7-(ZJ&NVZdS<7)>kyPC#1+g$_b8VCFuVrLLzc3 zMSYli?BKwy(IL$-yPU~_{3zNk`WMr8jHS)0gzc-HT9v4_Ri;N~U6!3J7;#R%;7M-H z^zoZ1dzVBcQ$x$YuGK?3$IM(@I>p0j?98J=&kPIqdwrCsx*X~-IsXxAKzLCKGQuGA$x#< z1(K=q-`4Wr+a*hS1QWr!eKgrFKf!?Ip%-V+%ZF(%kRRc#_zlc`4vRt1%T=}7B^T+6 zxgu$e@?IW65+?~Y^(a$IrTKi=eWh3g{Z_unH=(x8rjh2aM#;#BZ(_p^I{>Hbl}raH zHajqlJW|(^Hxa%2#o5B(c)eF@Q{pORfh4l^e|Nhd&$&@Vwc6$D;x;1DnOM^EsQ!R2 zllm?@Bt0YwS@?2v7yBN~_7Ulc^-Zm^JVdvFEApJ-aEVuyKUS~g0v?VgepOR?IkXXw zR5Pj$(?Z4m_qR8x1>P*i>Qxp4y(rIdO+4sPbaHM_yf1~7R28qobJ=oK zBzi8I!>mk-F}DCO)DZdNq;HYta^{h8kYEkwt}3r#^A4wufisXlzUSKb-c;A;htQPyg|mF`S;K8t(eIEe(s zShx_&FK~RQtMOXGIF!MPP#8hJ`JDaDtonS(rce? zt^6Kt1nZINLK-AAoA{ZY-t7|+Cp%8m(-)5z`?8zO2uK-^5c$~r@Ka3yoH^)RW#wx8;K&WO_iM{tC`qk@JL6LvzDpqSO-EXsQ#l+cQ<2x zBiE#pibxZ%?{h1-=oO}U0FpdttT+Ri3_dziA0nbdQ3iz&0#Kq}28zpehNn9}T80T^ zGq_!Ia?&rN$G+u=Wj{i@ZD6c?7Ts=MjnIcsyDA5NT9JR%`<+i21kyrJ*SC;5E@d(; z8o{hZ!DAA2Z@^yKLcz2E+k%uQ8ObgJw2g2_{Q=t!_g@O~gjVzeM*??+#*&O+w(#}~ zKrD1OcQOlBK|u2c+{oJdW`?c2ZF5o`^H`UKpmM?LAsijH8aOMXZ#Z6VC3@x11g7?8mMFIh;r8B(VzaG*842ni0RI z2NB-e3kGF5bx{L=K0y`c%HV@AiY86YZvBcdYUuh-TXiP{P*+sMa^b-y8$6H!;@qgGQaq;`49gNoeahx z5}8@YljjJ%@@m3K9shnmh!;z)4>bv?Jiua4mj{Pj7RmTabN>s99h@0*c-O7E zTm<|y0IDVar*n-v16IaXHf^%sWU1chuME@TQ?&qX##V!Y8SRUExpQ{UgH;&*WeIlS zAwMZ(2qN-WthK2y(d40xFkhA;`u*>%*FG51+Qa%QX8)kZf^=g3%KC4+^_)V4>T=Jw z35v2yw`R(!ZW^NT{CS);kJ09GwmLU^6ATBsw=OEk!PNZzrRGN*M!0f* zr_nxyE%!DpF9?36-TcB20A9kj(Zhz_tr~cK)xOngf5)?mRt464ZLf2}VoIDD$sk!S z*9}GO5(i8VYieMdwv;o5l&vO^BV54nEmXR6dN@!thWeCk*v)Wi?A3KS(;fXrbd41d7b=sLfy@B^6Y(1q)4b_xl|AvA$FJ(!zI9S`tK=ZOPY$pv3V z?jm+`r7V_Dsl+b$pG#k;nl>qHu>d@HR2XVC#cvU=3FhQg}V zd5#MmlW&1$xQl%={Nn%O;uCrZ`}kIG|6+vz%W!v9QWy3bfbvNX$mZ~n6w6^64e67E zL_P*mv;i?lwG&)RzRe)PYzRs1q-gbYBY%Y8y^Z4hV+tb^qhpS56$ee(dtVuiR-EPU z_!j&sP%d;Ll#)V8spLF{GP<=cbewB2%XTZ_rQQdPmE>E)A+^=#28aUORDD7imMM#_P@@%N%`4#I&f)Uh&(*LzZCJd;wr9UIr(A?$Q0groL$c^w?ivfrf%n3GV&w3KGX`3bzdtA1 z-b2i3#FEKDxk;l9V5Quc*Q{HDI7lAJ*#P(!^8x;m+N!Ac=qQstsokMxd`dks@09lS zJb!ipv)5?6O{Dp;C+Ph<@8o|^7Pf^zPlZ_lM|JwQLn=B6t`E&}14Lh&*o!50je{6) zo_$mlm43Qf(a(jyeq>~2rePxscBJM^JjlVIIfyE(JvT8cFzjOt!p*}!iX{EYbl{#N z%H!#?Pl>4|SdM>@rB{XC+86ruO1zFVY46|6QW@)sS~W~^4lOKkH1vJ`?K@$sSHp!g z#qnw{RM2&>>xe(h=R(%Qrzg!G0{kf`b5==}8Xqq~h6BNKP@-dX(hv^owz^yVFKs*S z6Eu_-nyz|ymH2D$2Um+SNYc$USk&%J+<8w7O~V>?v2PEqyCfp?!pJrvpO8(p=q26& z|MGUkmBNL_6Y%UG$TsAhsy=?lMscWg&_UAy}5fDW|<8l2_kz;x8 z-~ZY};2TFr+hX7{(QN0rPoOu8xJznt!xA)K6FfX^g3+RE*jDM3UnxqMCv5wQ^}qC6 zX5F%39JsVhAuL)fH9tme8#g{9_q6V+(%Qa1=atM3f7I2?V1LPl?9n($vf-u54!?9# zaG#(zyU^z9+}rc(5uNScgZ~F44OsR=S2xiB&;!jCIVt4{O%Cr;=taFJu(0`xNpJs& zqv(Z&E0gTd?yWTyq!UYmUq|&Vk(7+UMp+(7*|4=Fvq2*vFz>CkOYPR**{Hdjan8=m zfzI3e+kO}ip=XCJJqf!V(JiXy_j5w3C? zSY4O1x6h^7tr%X)N2I+{1Q>i-DNn?U%XE<5))!8pS8vF>uoGrogvb@^e-Um6g~8)% zhaM#mIu`S}tT;Xw;K>DJr92V%qUcK_P?~TK0&~yBC9g7BI+P#QR(C%(2F4iwadY78GzIyPni0Yl ziX0tX@Jz<49cHfpZ)!z~KTYZ~TmhmN4#lN0|Ky`bE+T5OAl;?2^F5DeKgH+iUi#}H zqCC#9_!#HxnXgsAE__y;ITb*;YlaQKPmM1hGQzJ)QO*j%Ik$FxwSM9y6T+|E@^ z-bGKyk?8mGHJe;4-An!@UV-RCMS4sBWI;wFs0vwy@xFvmv<;#I-OUz0JM3PBiemhv zdbz$gR|=5ob;_exu3;r1oVNLu`)Oo^<57X?(g8X7Mfc${+?iD-*RZu@R19wb0kyJR z1h`KN2N?fI-P~2NgpQ(w1tm5+N+2sFX2xra@iwT;lHdm zZmu-04im4@^f-^K(YMo!KBmOp_Kfh2KlzrRRy6sOQwd1lp!Og(is-X)SBikHnqBkk z{7~nM9UvM!=r>%t7ef*B52?)WwFB@EL0hOV9`18h;a!U_!M9X3YjJEC;d%|&f% z4jZmHA!`N`cD@2aBgs@PX-4q*H?NeqvowI6-EnifYtb$Z(f~g`Zp7{!yrvutiOoVt zh%Ut%RzZ8{MHvW!V0T^>0Wel&|W9LJLK%(9Y49Qhz(zcTT%dH+rI@@yk^gvR2HE|M*6;U zsfhA}7+Rq!BhzVKf@&*Qwk*Ge+StE_nU!swEI71+qI)HY%U>=fON}9XH_v&qdFcVP z^WRd`{6j*TzvD)YF6nt_QDM2OybYb$f3D{=#*HBYqzLywi(%$Ik6RB+)~&6&vnR!% zt{7^TE!%8D&@RTWXA#+I6N13T&kh?bXeM%b!bzU8V|7KyV~H|SI0+h)OpOkbb@tf9 zie~`IqO8joIpBYE3gLXbPxiLH$ymFlY)frhn?VUuzD_y1(rIY;Q~i_GLZpQ6pBu=c zsh&H4$gJSXAQiTIz>o9t{D_y~zN|Ju^|vS%uE3f$RQp2^+ONPsOZD0<*fd}dT^cgo z`mT-B-tQcxq@kv-{5ncX#SkK3?9-+zdevd>&6nXMC~6K6{_(SIy#<`fnq5C;C2>br zvG4^_jv{#6(E;J%&PGUh)a-CFkoUmFi7SaWpKPV3m%EXgf5*ne5`n0|Jako=>zIcT zNDahC=~u5#LaGHo(>{^7e-?}~A!g*OC&fHA+@F7D6czgp?uysj)50Ko0)sJX2CFLT z;So$lg${v?)Gf?vBYFnf$_G6)Z9ov+E=Wr;U&$@XA?y1?@>(t*U25UW#kbU)BN-t+ zo)Nq&sJ0>gmMW_1CJ(!z!~v!8xU52uP zsp!+^b*_C}3W|J6&E3ZhNb%bBhlBX16)gDxGiK^k5uNG&f#{{tCHSjg9YbLf0fuh5 zLcH(7qw4o{T=FfN z<6|hq?wCWUl?A~ttwmbx0- z54hJTVz2FQ^n}YUK+`+G0C;eP#_GDUwk2CC0LV3E6 z_K>Gbl;sYN7O4~BJhX`ry3(^mEbVBv=z4(l!}21pyF|U_65Hz7#nm=YE~wFxvd4@> zW!&_Vwufr0VeNrvC;A#HLmh@!gF)xHwwlCgyL1m;ezlzL%kRH^hPMyom^D*MkG8acuN_^b9j!xm$C?Vq} z=kCt-X9MsCvOD(S`vi29=WAzkhf(edR+`xv*qStH zB%gw@tj?kx1p-yWgyh-W!T9}t0+-hWS;=tiQe^Lno~=0j|0s$^qLhGlfcfGv*^v!< z60FYLyb=d@b91=Jkn2jp^0E7=wxj>acZ>u|B81nO{o$tp=wf+S^i}XNJvRj4XdHia zf_PUCUv+n#xAwm>@Jr$WK@h1+eb27u0E7B#$mP#PV?q0jjQn7i;i64Be9>L|zmAC_ zNEen5^G|~qY+vbZx|@h!5+3&VyPT5+>awJs)3#;9b15P;(>QI4nw9_gncm>Wo}S`R z*#c^QG^ulC-}wdnwfnp1IL&^H4+WYpuf8UVSa5MPHOv3=n8@Ze265veDTG|WGF&M} z?Y~1*@Y{LT{~HEGoZd{RfcShiZ43D7KL7ohMS>JihxvzH{R-Jdn$-WlFvkkW@$vSg-;&rOw98u1Desq$)=yb z!0*RIR)*N#yZ1N|HbL~SmFI|t^pooc>OQc1ROezItIfw(bH?F^7x>vk6OA)M2kwc< zk;+eV)M>sAq(1-K{T1z9B)FF1$Q|oM#l@zD&wGX6f5XLjp=v;))#zLl$fi?7Nm$3Z ziCJ{l{pLzMOdXWA?EQV^ZY=SP!tN{b!wB*Hr_&4&GcfK>1H-e4Ah|5}cEUmu_T*oj zyxBXIHWnPBC241$3A^QLHPDD2fjxk=*J@wL&i^+3#Y<2|L zbh6{~C;RXASU2kQ;40xXfnW4n9Bpb#=6?Q^68-)c`?d)w0AJKwY|C%6L@aHF@uyoF z_XP}|&%x>f^p$)Lq&`GxOroo3@vHwBh2Z^TaaI`94-^mv68}YiJ?lFy(I{MeVGx7< ziwrPAYya1^1Gh~LgcXhehZC~|2kV#^(n$W>c!`HmT8V5Um3>2cnDSc5sn1qr zbjHWWilFW9$SjE7d)+igTX5R9p#b9v)74>v4&MhsYwFs&;*HeXEI-~ns8St%wn$F1 z<1=JS3j3fVc*hk5=aUXa%NZ5T!^c<94yD?>knyOQA?3tR-zU~oz_SM+4NHJV8S5U;@G|q|h`isi^4TRuM-z1r zMRi=;`GeT-fr)G=^92IPBP9f+YU>4|E_5L4Q-cR$z1KfDBJ|$V3eij>MN9c=@q(^M z*%GmVS6$C!5{P_iaOY7rFVmR<#!p;bMwasZ%(eI57S}>6W%cP)u`_r=UDb@`Q+hM4 z)7L>1Futm85@@?tvGRFYj~Me4^9Q_62G9%8E0_(QOZ6PqhDO+dnonQv>0+MzC?1Yf z;R1_&LYwKT(+mUDqfyn6br3aJuK>ip(r%Dg^JSXon%Jz+BHz5HYik!vS4^$nss7kV zA5KJL|3@lFKHg2F(RXss*x4$lfnK$`#(mP#R?QDH<7Ndb`*l@?GWB_*X{q)Qa*h?H~;a8Np=dnhFb2BoA? zVrY;WhJ5!PMbYz|-xKd2&-2kEj`zK1$J%RM>w7&|{Po}zmGFQCEQl$%fbfG{D+6Gn zd56*Wm4dk2gJRIR8Vs8L%IUg7lOETz6`6GaF{1QCGZhW%>rx{LCG2XYun+{HheI)x zuEr!*_Bx;&=UILXBtob7A>XSM%n*{0q?h`(Vz0r>GaDJ!jB7(jtI2B$ZTcUdp!pgP zy5!*(1A46HlIEfg%8}VISm>8O0iADJYM{ygC^j&u_h@nd&Q_Q7lELaZL`2B|-Sjz# z|4OEB!MnVUa>OmHc^L`D9;cas&2UHLYXVlxVWT%7c~}!Gcyr*i6{Ng&%fL3k`f-58U?~=Q7k2~%(m-|sIwnQVf!1P`S`pq*7q{*W0$vA#e;{mjrHQ-{X^0 zP19t{-)=Q$wN81wfaoCrt?*$6ZE4GXUm?85|IiSLOvudeB@*KOM0Ty?MK<`qMjjtJ z1Zb?`{(f>0|7>!}%8A6JmUPtqJ|CH1~Yq= z5K+45lhRHcKL z@aNn6!+YoBMz8(Vwz6|p7B@ErU^@2}=m3sEC{2ozl4 z`vi9gVTF_0M^|!sU-EV)&;m6ka$}?dY(LdcIXs-BKu?=Na%wIM1O(K9-fwOXiLvf^ z0_XtuKR5u9(*E0Yr)nGTsBe`SdyFzhD{om)dw%qIaAc6F|CY2BI{Wthkvq3VSfK%~ z%tJ*$2g@V1^aulHZV(_9zrx;Dy{~~_5t6fLCETge4)Y3S?kzoS-MsSK{)ajnqYtuPZ8!;Zqf4C#duPXx$wuI z|Lm*@WA&v^umm)YR!L*hzD}Yff9#L6@9j`AU>aH6L~idDvKw;|KUY5snTD-)q0?;v z+XhHoyvGAhHPo>EvDtA2m_I>(AQ+5L+PRuIQZdYF0Yy7*6Bwzc~_H@`Uyw?HAY6pDggCJRs zkUklex4Or%%8Le~aFWvsSz=10rdJ``fJ&hVY{W9+Su0>x3y!*u3~vWe{!b?F-eng+ zI|$IKFOo$UpW;5I7^P0!04=786;Ej%bDY3;>))@y$}h}=EjXX?c7U5cV-Y~hUdW2u zwP%nR=*<3xM7>2IjC#L+kMmE;Ah8&r6iNeuonXcwJ$mG&)+k7r9XO-_O8r}I7(a87 zqPxLlpg4Er$MtPK>jCC_7KIu)Q;Zi%7latbFmMi!*F!c(gAab_I=jbnJk8*YfTg*JrdY6M8ZvA$b~rBp1jD zE=CZbr{mrx(Ae+J0>K?e2nzwCZA5&OHiTFx+X{g{Nq-m@`Dfj^ATY8fIb#4q#7tu_ z3`y@aFF9gLNXKJgqFe2PpOxCO-_0?|tue5}DvG^=vB}ZGHdF&HQXsDwB5xLqpHA+E zMklo+0R>NTKiz#{(t7petnVy1!V?NrG6fkP#Pwi}9NUa40pP%1CmeXEr<#HDMor_! z!XngRJ3_)eXh4XxS^E4`CDuAF5^ijT>|HAX#O~zy2pj2Ei{F>Pz=60vN7M1%hdRcYUG&-V7jDnC}_+MRp!}El}xJ){Ad`r zIGN>EtT=`(@h`lk?d?ZsiCoTd%h!9yVi?NRhcR~+;-@b-Ijd1KcN+Ix_R$7!%~zXl zT9GwLcN_V3TZf>w7>!|vjG(8Ano&lBsxHR;wZ(>h7fEQ;`9)JniI04~+~Kj^)FRP%PKwC`NH`H)y(naoNGfdJ2xOq_9? z#ty`_q2rPc(QE2Bo>G7p3i3pW5NqXw;0-eNUg7yJ5Mi^kA}WQxK9^_ZXTNuG8qo>* zmFZa#`xxg#eYJv+tiI+S36PJf6h(@vVzPhXze`VzW{0~%rX6hm-bp45h=9S_QA44# zF8p8LQOl$~_1Z@KQ>FN?cjv~(m}6Bj%dP2bqxG=Tp6R-n+|E8FRQ{*uSf+eMGR z8R2y2o^fe0Q+Ch~UJts)ITW8|&##^sD|ig-~gkg42$>0NqpFb)hvG<$ubuGo}6I z5N+E^ZYf>^2>38af~YGU;~)Nsy;BD4&T0lhgk#m0$1`2ia2oNIJ&zC!c;Uvih?UDL z!@{^V^olUSJ?O?_z{+KUJ?1K^2e3qw_7$A9ksrETx1exA16%)vw%Qr<1+CL8&_`@! znLoL5pBaB<{BHRAFe=C%u*rQNGLecXg43hadeiCwu*t)Op~*86HNs$>%QVkm`(L}e9g$9JC~ zPSt`{HRd#nE}YA=MZZdIbH&AcZJ}uvr2Mv^9?KHZ9YB{YO}4~uu`XVN;;6>qX}Jma z(eA{qzt$`t07&tyy%Q!`vE2%{vFM5gc%|KBgz5}?!um&b}a$))rA%e z(ZgQ0Ik5#z?`#ABuAf-0ykM=*CF(j1Q>U#0>nFbs7S^CSE!U7Q!Cp1WQL!zkneyE# z9&c9_++J(S?BQ*7LV{r4>w2DH&5j>K1tRGzzawd_3t;zbvQN=?-at6u{#ECWVw3 z3`pR3N&CoL`90^ZJcIYAT+Z-Wn$6j>d&6 zmyZC1isif06B^WDHBp5DHmO^1T3$N2OWMC2`d7G=)JBCC(YwrPo?vb#uJUeo19AIh*NQROjH(bsN zgaQt#JAZtIzC9}xv~FkWG=++~11}|G#~WSggW;)wB*=J2p0ZhfH86yc=3s7@7PdD{ zit;G1N2Pt7l0?T6G4*Sq&ga|jxw`2Qp-X_1_s}Yg&5^coJr*|*b;Ig)oYYS8MbOD? zA);Yc7My+60&k`jUA=F~m>icWA$r%LroatdDKR}&qYFk?wf1Uh#Fh~JM~Bo-g5}0Z z+(LkD0mdfsO^__{ByRSrTmh4f<={!FJCH`QdmST}3aOvc7@s*lWlUdIgF~D{YDU#@ zB*WggHQBGSu8wGZS2PMyZ^T%>T6sE=uYOkORVnu@CE5P<6VWB|#C2?+BvEc^5f#67 zX?RQe<+n5qNYy<7xis#^fon=Jnm4s^b777!cZ~%FBVEE}2qG-6-@LDlcC^L*hadf5Kg|6YJ%kYZ# zEM(?A%#z&NEkqR|INx+tMm+w{>z(rX=GK!G&P_K{Wc!hLlC?+TqXP)rlMekJZ!o;n z`8CF3h$>nTsDXO&nM%0jEo@z+4e%GJwt{r zs{-c;nG#`wl?E!3(FLS3newJ{n_R?;WZs@`hNe4@cT=CiK68bEtW?Np-8YZz3OBBz zQTLpoqr)VgRhR~OLAMFwbe}aCKOsyjmmu$lTIqJmiC(<&7qrcAsxnX`^QSq@GSx~^ zS?o0!It#4@_A(;{7X#u24Px4;8lP#R`S{zv%0_kZ)n$@%WwGKG$tAg??g`>KR@xQG z@)0-M+7(I!r}^E;7)MRL2_RNZ)W4Y561Y%ck=aKA2kS^D^Q`-2eVn=`U*Llh!DAZN zB2j2jB4{dArMujI>!BhhbN`bviDZYq5 zrWbN#20J3(DtQAkC|=}rxeH(X282i*o{@gW0oUM)5g@NIBL;_77`7*R?kloh{hI4H zolX8`juvOD9@Qs^=L*wXMqj?Ajn*6x>b*VDH;q4sH-b)N{6`@(n6$hcdj~uGoiEyA z`ggLQBcj{=4R>tj0z&uVzn=Ac# z%g<~ui0Z780qVwPa_!qMev~49qcM269R}izZ^0*M{TP%eqFIX#Ph}Dfpuc2q!oB_R zYKXx2Z0vNgyQXrqM`D zZ&dzaDh#G%D$c;GWO7)h%glVmQOw)pYBSK3M$Gu0=?pJ1VjNiSuq8eWESQ>ow89!S z!i+y36TLc8x~58W0Zg9TYqhhUu6S(dtB%KU?|b4Ari`k-+nU4#*p__t^cfR&W+8QZ zc-mAMvUVt@`B}OQoM^34KrovDx5?H*^7|mK``#brck>`MHc{E@3zE=(qIc$r|4m<; zbo)-8m~!&dmvf>HQBs@KxFL2TSwo1j2tkp_d-BO*s)_9FlJUHmlV9TegcYe?%a)xv z9a_$C&@el{CES!F<|Vldl~}#4GQG(5O3!P}xNIeM+=@e^?qYhppaeBbqDWpTUdz3E z;H1ihI@@3!6JT{v57Tli#k|H}^M!{6tIoc>FK{uS?0#B${@Hi0RzOfMIMgfs$!um; zF6yYA#{CyxBhnY~t?l4C&cZ6Vw5K2#brQ8fp~csRdxUc*3ykdiQ?G3eCKu2JG_9O7XnZOOgj?APJY}?x+4k!Kl#*YjDz00*3@SbT zersG0b%fpV0)5N{TDvAQo2t*J7iZFfcy5QHcuu_=g1(Ah_XR-ES0WjBFJuqw5oVMO zY?u|SMIq-x;Z@Lo{LJa0`BKFP3|$v2>QKh7aRHYGHC{Jp*dzS)g_~heC)SL3so#>S z0Uo03(G#3Q6?c-bQBVIi-Oghc>fIm(&>-Y1+!EE*$9?AR6wynxFn@(Hrz+QV<0}@w z%7~31T*9hb`NXpi+{^0at195N@o8bOY#s0HLrt`_V$2eVpJ|_V#=8;~@TQQv8JcPC z&Drf(lO;e<$epbEgP;-1Aha>6?H%4^T9F)q6&Drlw!&;l*!P`BI#F1j;cZn`&eUJf;M zL_6Q(80Y>IHN0CMaScyw2k8Cv0}4l0gBSSS_ZilMUXMUNYLE4!3R_u6q}%sqtB%LX z!=P#619j}3ZyO}=%NyCWe~nHgn-tmANa+Cc#P)AmJiV#*NGl9SKcYwycZ6lMTd@B5 zQCLh_g(5fF&+Byv{Zstz+J%vG`;JhqvNI^qxy+Os03QZ6;s=!Ysrm$!9VhOGJg z8vY8R56QJbxlKk)nvOU1$k6WQP^n48tUiXh86fXe0{tJz(-|inVRfiAHbFdR3<$be zb%a%Y(EX@})s>-Mu%aHyb870xGHdvbpN;*3)DIZHDB^3Gqpq~g929s`$X?qLY%Ioa z3UEJ^D{)C6m?`>r2AKi~d|`p0N91xyWls2bVe@h~1AkOxTMm1DNkp`P7lqJ#lm7#A zaVh`}Zs_D2zZ7*Smg-m?H`DeCaw{sQ2+;jrJ=t~i?0yKA4|ZCHF|^Pfn{}?)yNyQT zmwLb@uG9XGu`tdvr4P6VCPf>I+t%$d1C1q(VqLBDZQx4Dvgc4%IRjg37weP|-u)sj zpv6yAQtSwgIS1gdA9Ji?)CnvzsNh6!4iy9+x=3wN&l^e=jm<^8TnA+M#=ow|eT}HR zXa`y#22%$D@HW66f?xYl)CoHKk=Y6j89-OI;4v8lCODd0m!~8tGV;x-#BjWhaxJ(u zmy@`Q5qi!=u|nFTrc)W&A5t-KseZBx33m|}@%;JuuFz7`UMo>p_Or}#q_a^z z+n}TG!YE4QN%Qg=?U1txBmNw&DI&hG7fYxQm_~O~5`{xj@WvSVBY5f+!7c&w;8Sjx zsLQFQ+Pwayik*3UKb3sZU1XiVw03-h3DGXge#4l2n}4KBE4<3a9!!v+mEObZ@#^M8 zVNDz&nep8mWKhzAcoF&eFV97i&!QJP2Og4{IDOU{%E)p0i%ha#u|~pKdv_eLpDDm9J3$uR6*+llzy?k?tEH>R7C-OwUso?%fNw;mn{)@L{=)Cd)2D1+Iq;b5x z<1(h8C_Y$MAOgO(y8h8v;K?Amhn-W}X96uC zl8EK(whsC;voXRgsWLOe_vD?`YqOZ!#nUq0 zPjZJXB%WW4?J!C(KZ^hKa;`OX+B3%Fn2Dpw2^Qm)dr_WW$4v8@o$3qO`)O_avb!#v z_2paQ@14{$_7RQRHzK?x4fxC6vz_cJQ07o<*W>*56HPv3%Rj_}9DZapH_m_lbJf@# z7Ean@?0OyXEEhvK?p9o&p&M^el)pevtdXcI0-lwbz%*7_H`to)b!8)B$>4r5i05r4 z%EMOyNj=>=(n{6D6(;)@P&0OuQPe52mzWsZ8MiqCPmlec{hfKq_<;~nDX^4Chr>4c z5(`!kt1s3&){dBkYIg@5yq#N1*+7l0TXl#B_b*3e?9`h`uY;{8L)0uw7V3;8=B0#a zBDp3P#sWB$(*nJ`n?XJST0(f^vzl{wHEvAWRhqZgk5kn-lh~pu*pfrZu z$xd~6DcHk+6ef)I0}5r>OD={}@aL$fa``{dADz*j8vP)s`0-lq2Dq|lJZ7b)#byd> z&&t-!LX$ML+g!JFO{AZ3>t)l;W%sR2b1}kl@%_*?F6f*twIIo z2)(y=UM$+qMb~&UVwSbWxl~SXZECdDi8^Vz6*Kw+I8~;<=hENGH5WX34;-{h7MuKC z#8h%V+Q=1Y1>lXr;4CZ|Lp@}siAi4Os0*G57gc+B1(Cxj8zOr1;dTBhn2oACB7t!f z^XRxN^3alUzk%Ay{YHL#faifMO}eUk+I_-NR=1a-t>Ihs2nD$@HBc!{#y!%`)a-1W z^;G5sJ=a)o$E3=2vn+*DZ%wmg` zdCzjXJ|@mn(h_JZOg6H^Z=On~6k48rp}qQ-{=4`0**^yZ!%pEWWcjC-a7rJime^U| zi2zZR3ayTWzRG=xy0BuM$h_+p^N}FWUDU@;-g`-t%GGWw-VSVb>!rog0r7<&bJ$+Wnk^~k{T4-0RTYQ?9~Fdz8c=CG-WyM!Qb8OE~hvJE@-~4op9x( zKYTC>D;zH$8wyXJP``20{4i=eoNeTaT>|Q2OP}pMzIM^H$5{FI0i`9gYa#9~pMh-^ z_N`{koJbBu(M_Ke>_%#nNsoPL91sTuS$Ij5kpzHSE{$8Xi1R5>+Jrt%_w9sQV~YN1 zY2R?9=A6bigUAbrYsJI{OE!eV%iOVvSM)P@iB6ciPr4bkD=!1!+5?|2S);_bAk$mg zZBVG&XMy@{?H+?s6(M!);ite~i%c9L0xVNsUJLg7Ltsru$Xwg1d?%9O(9qLfnl%;% z^nupJFF{5vZ#_jmDvL!DGZw5?7>jB8=J~fbe$J(T_B*4i$n7zrhZO}7SX&@VX3ZDZQ*H@^TSRAuMF5=AJ~ zgj=$qVk`Ri=5C!`4EiJ*mO0FUXMOb`C5dL~aBjR8jx+QF= zBM$vU;OPSI3-Et-l_h5VO`+&Q94eizlkj}}m#38}=XV&ToS9^|w^rR^ zjr0Qa_>SRD9QUOty|8g4@a+_Qfl57~CESKXW18oI1AEc(N>X#7)}0{y2j~u*bf*BHO!Jdh^NOWkXG?qp_e4)+gbomP0!rl%(U4)6g zvMgXcZo8kvRDMIDH@q9~Jdq*c2+}!y)?LdW>qQb&A{bggB%-4v^p~#NMf8V{PsZlz zwH7l;S@G)H5tFMsi$L}_E@B0iXORp9{S}Q9(9+6qcO3Z4+E ztV?6g3UJvyorT02CD8&D33q^G^;K3ZVWoLBBDc>wwYOFgrnDa>$d6jJPS zuk7I-m0u2AN4{PGsuK5@tHUcLClBym>31QpW|u#tEXbvrFwdDf@xiS8uj}i4op|HM z>15#I>AevxhUOVXp^felV0x+|bKy!+2i~PRt_Ps|-GDjR!WaJI<|Q<1PQd?nMA9F1 z`>Dg6k3qHw_go};I(j0ce*zqaU$AT!hcGof2Ui{|?_XAM7Xbx3e|Jf}rZH<**^tF2 zCoB>=B}br7PV(^X(!rAs@6T)9T%)_>pvmsmXzz-7fx_YGV@BQ!FRDe|-_mkLiX^v+ zihrSJ0+YD4p8{KsuVjdL7ibt@D|$A@-%FSP4PT1KHK$pKx(h%@mP~K~VZiRWfwASM zt%w4~+aJKa)9u}A9A8P|VD$oGck#$|b`}}~R`bE?ik`G{S3)nF1=FkAuShKB$h(<= zTWZp_55Omeee%AOymLn> z8VxEJK~N5Xyi%L)GIYKbx*MC+7(Y=v`KT~;$?#nQ2#Y`B$RCf{R+cd7`{`WIm1^R) z1!)qfV`>C8^jttx#-*|CLYKEu-NJHywNTmwD8vpyk2{2A73Yyr`Nn#nNhJC=iALNa zz_a8rMW~zM>-S5HO95{U8s{KJ&pdeLF1UfS0+N9J;gFXfU|G8^sZ%F_y&$*QXCi0- zIikE<6_$@#(b>#bYFZ-8-ouit2P&`=MHe^a`RwUf6QoY=w4o{^K^f>cV6m<~P(@4a z@dXr{7NCw(wqBF+W$9{BY`in|0_Qq~QYy;>`&D;xLC~1EO$534EPw2iR$RJEBhd+n zfbaC|7*z>6fVQCk=~BhWO3xIVQqo0|L3rqLEzq&x;gPp%8dHOhzzL$7amvYVajeU- zwcfzJ!#5Oo%C*X%5A#!wDh9PGq6HAT!`=Gi<+Kc+d!1a@(5}@eigCLm1T&cA+GYd;0+ac1&S>T;1k2%i^eBu3x^Ba&sPSW+F z1+Mm#-ZRU_eegOHqM)}zY~a+M^$!jJI)xN{{i5vf6}G;j(DZnV1tPFUa!r(I(f}Tv z(iQEaS20m{73kUf^c>MA!TkkT6_vPHPmdb0N5=ZK-DwXrJ)^kZixrvh1)?H@+Z8(~ zE=3QnoW<1(?wg|&)Yn`gnLe-H2rJzO&0_uSY;L^2)NY7jnw_XKemuEy0FCS8fi6zs z2W}h_I{n3|OCt{?VEbeo7hCHkz@7FI&mj6nsW7w$o$m?9n_zr^(qVy)$DcReDVzyJ zW5}4LPrX2iqu4_S7%bmv1hO6vuKuL#e5)KZ91SVn3vAACo!`7)HtXdN$|}f&4~-~# zcnZzxje<*6_HijDkt*DX6ipAC2)2Nujs3pYJ!Cs zF9rB^_Mf`i2~y+vVD(d-lzPUX$f; z6D0MmD^h_Al#VviaW59Wzu9<_5Oy;e0=9~-3mUiOWBKm z)M(GGMR8wg5xgUy#x^Ai%Ev;zUehbdn4rh=PHf!k9bGmor z*gx#;5Lv?|#1`~YH`euSdqhYT!H3Z|pf2_~6T^`Nl$T8kEp*?rml9>299~~;i5FIK zjUPg-w7e9KFY&4gDMeCwc7c3qve8CY-}>UM4Z0vvi=HUx2c+b1dFAE(u{WpyVfM6M zGe!9V3+PK~a;<6g>QToae24!wOYst{t>0Y8p@4MYhvAgxK@hZ`_1XS9(j~9WJ#SbL z--#ud=0dB5U5mgZEBe0p z;F<+hP`*$$*(!3`kEj(jyI<}CF`Rvaa5bG^pRtzpkLhY~ygR9aLdjEk?=&dk6m>_8 zT=Z1Y_Plza&m3Li3rax|T9(L&Vvh%(6>^K)Gtq zvbR?7dchN8F3;QOn&#Mq^I5Xb&m&F;REXTF@nx1qYM`$9#rz>JCHW|EQ0i9CwI_c_ z{rRL6ceiJ;$9|f-2h}L#97%A^Y1DE~?ORUmBa}QrIhRaTvtKF1x&$q>%^8RXt`2;i zkV!bbA$mC`s_=&FEak^(pfUA#pF!Ip99vfIh@e~+wH=27k?joCZ7V4A%nMg4p|eTY z&GcTr7TkPz;jGVuJm{O5MH7nS$Jo2NR-~IaPQRH*HhN-W**xi7vA5u9=0;$;iviCG z&4+gp?r%z=dnsMzv3nv^G8JT@q2~zpQv@J`k(TO192%q_!C#JoCTdyBj4l4IlQLq6 zQ9Q_Q-3R+kb?e$|Z;d&ag;-ns&}-!0M@>~T_sTS2PCo^)!WjLj&zhlxop(2_m1r%i znUBc2L#=A2zEBqxy1{A1k^8)|p%ZrhjSQKl({ibkRA(er8M>Nt8~?f(vQ};i#zeiWfWgXpV@_D3flsXBC&CZYPD|bG%M3b0X7# zY!u?*nU_u|7{8503VdL6XeyTO3D?`&w}Gpy-nboO%>gZHJs^aUe#`EufkSX9`8 z(<+fJBa% z9zT^oL=Z(sMQ~^NpB*{!0$2j_gg+_viZ{KeP%`=6AP9E}AF0|WV*RJPW;;+AH@m4W z2(@z&Gd)YclSIk6VNEqfyS@O59kV^{jpQ zYLB4%MkDIg$|;$)Ir{Y)0il&7YVSGvP-dINH$h43X?&gT_!Ia)o=7|prQCb$8@ut_ zak?e78+{Zs7CV1UqT$|7mW68y+sp-gyTX=D{9$GBtsWnJ{nQmS*gn$q+ajSUw=7z) zK%KYKIMyUL7q*fO(zE8#a=9)*uQTAeqbc1vobNL1oQtPdlMm&3Gj=}f>kW~SjzG_C zHnN?sC$qwneX!mEYPFgAC7qvU88$u%Y=4NsVEk$7s6>Xalb`%Lwfg!5CbJbgfTl;rkPhp>^#VuI&VMSNsn}QaL`}WgdZAS%7-EnI z^9!T$2g6`9%Z^3A8P&$f0cs1g$s$`Jf@B4IfjGZ>RoHmPajTScnOwY6-bw9rd`B&J z0BX2B7;{cYP>Ru=#W#Jl;d6s7{K8#xCucS#y*X z34PBUleuWG*JG47&J|)W{g4@PJ8rQhlr!5qQA_?A()>&CxPqnS2KH-#R)NL6aO8ML zkSjm$TDDeYp-=uhrj_Qj-rhc=UKGM^I5Kd+ri8>#7hx!nw@I5gVHUL(nO^D|dXp{7 zYdAD4a7Nl2x5D1eu%JId#JZw&^cR_$&>4&&%SC-_Y{|{RF_(tJ20I6j52+i}I^`N@ zV{O)tRoN^{sVjeE19o48?(}<6_){^jy^uHNijAD}oO#TjWDRyt;$$_iqC=Ng*A^o2 z+_-z=ODGY8RlsQXS;k$qumFJ@r{ zEYmgQ?L4UQ_2RQ23MpvgK{5^l+Su3=)_3g4nq5H^Ys7ktHLNeb>2em@Ue?kI{+jdD zbHQpy`k~IY_Dk-RlKs4;GAJK?XS7e7-ktV2@5a8JW5PC;!y?&!ybKHW5iOym?)VOz zt1oiApfAfZI%61JD{w}pjXT!~DCTB`6Z7)it4rBzUPk(Rz)*0Rlm2$z@%UL6Ty$H&~ zETnStIXKA}#yKv>0!1(GJgB?(a5?W(2I>-)ZGNG}ROF#0vDS9r+}1p`eHL~Pw(frr z`X(m_eOOsFMxXcX2MSiBCzaT=FqBcyTP$s;?LH5`KO8#hwUis;LOUdO@2!71=(xb+ zu-(0y=%duz0EH~|DD+&!ENIZn9~3$BHL9cy2+BEpAFTbJBiroau7qeE?=ZbhH9EOr z)C2C#VGtKRl@(^Nn7&#$2(PqnC@iz>BGv8!*`s>^v|TF(%><@B2ClpaR*-o|mN%+} z9x&rQ&~p!Q@R$)9u%lL4u)yJ=!M1;Bf%;Qb+$Y{nk0ETP(gwvcs4OTzfm0qutWg_D z`cI>_yNJ|+37jJJHx@bs&X5s(P89=3GB$Z(qJ9A|43>B;AL&*6pmboh`ud0#m}nIQ=Tni!7%1%mMy;pTpenlspr(S^S2<&-F&;vS8h0ZEd4OraX54+uXN2^ zP2cZ&9pslbbHLD!MS-iD78PI*Z10C6P>sw$g)u8TX?E;{2l5Q= zHK1JlOH}N$V|+Tuh7JKGqJ07Mg9NRjyjK%Jq_43cw(ALZ*mHxX8ZX#kj1yu;)~s+$za*G<2Um?7sdNQ zb62XE2k1vqRRcHrdf@qd`Koz`JB$cG1u;z!^nDwE@(?S6y0NJyn3Fmmv@+Ghj2;XQn4H@PZHU>_uT$zaD=G9ql#;~curZj9o$yu zL+RYM3=l^c+4AOakZNfjh%;b?UtznC=Q7_Mo6RB{kkD1ElSA})W&>~6?a$>v>gEs^ zM=3ZX%KzmVgS6qZI{ydNvMi8uwzo^pZsupnBHj2a3@W zYKh~j?$p;rL;q<(lRtq@1%iAMn|)k{J`b|Cyfx|CFLc_s;{Y_14+dXOz94XZZmy0f zyK7k5fVFx`Ya_`IAigpWxtmYuzu)YO-wb6o#q{6P2Vi`3yZ{WQdQV~VJ+t3b3|klo zI(PR%m$V_^J9vKDjZl;vYQZFr*2px={vh6(;<@{zgAQkwxn=K(P`V54Hgy5a2A$dW zO4P1ZN<@Z=^MM>qW^J=f+X=0-S+JOy$ewI*nEN4&APq^h3XHH8>S#96Edt~buYM#j zzWrir7WDf#{tH@y>@g1n?WqWkcGZ|)2U~}1-#HTJdUFQ>fBb)cEG zUpsbZyU{<-sUITOf6Y1J@Mihr6s^S3N`=iKBE4s{8d=hBkGW-@{>#st&jh%I06e5j(qV?c+t}{U% z@%(@N?jG!|4g}LPzmRs<*02V^DHHx%zFu@5Tzo6bG|*S2O1jMPmyri!_b(^UKd|if zgPFi>wX}5u@`&4$@RL3EA1>yy6u^aQ42y~dBgMf>*0}Wp;aLBDNBb8PuJ1oq6$(b` zQqid=TSiKhoBZ}aZbv`n({G;{-3~`;ZSQfB?PPkV`)PmOVSfAJZy)lH&nXIQhICIn zOW*Pt7q5sO*z#Mr&bHL_zc0JLy*d_<-an`8y^P-G@!_b|uK#_KGm7eTgF*K$lDWGD zF$G+Dz)S!5$A3OlSwA@}vSb3K8AV!#^46aLapW&!Jq?*R>pZu&dwe+31jcUb&;Gu* za7dHwV=uNMb&%09YM5ob7ax@^1Sfmh1G3}4JO-Fa-}fLxDgeuS{%*^=m$2!q{_?ME z4y9;10yy%((iB9>7`zEo9?PNf+EQ`z;5=$2w)F|Ig>ZaNjCque>>c^_1Udig+Lm0v zZ>LEJ2N{AYEw9&UsD{CZRWV-fFeB^w!4Oe0>39vJ* zNE*2B{|W~c*$W{5o&X!-f5G{?5AFpoojH2vaj5(bSgJ(CljeE6g3`w9Ca68L?~ z(#hS+C<*0*LEJdOtU**RMO7{0qKc@&zZ_2R9ngV5LLmCn4>dTj-l_TrMw)S4S;anf z4K8ZkZ{KW~T0+v#skL8Plws?@A3TNt1LCL9Qayqg3H^5)rOqyo%aRWBA0&cZ0gMD1 zK66hH9*fHFJ$CUQCR|xQ^i>HCejmO03?=7vz8$O4wPcVJlfhv{nZLVx$JBPh1Lra! ztLwV`{NMh|qte}mLmNy?lKHUJ4CL-JWg{;u`MzEHvBhu%kv0CxC2oLZf+}0uRF?To zs_;L?XMkx5zFya3IfUq%h`W4XQY3x;=D+!U2%AQnb(A-J%;{(aL4;3= z{*!z7-RuyTHhI;Xd+(2V>0u3!Oqb-?y>!0^PbJJcE&ntb$*aUpm6xwdn332Eg82J>t)qd+-nE+^45Kwq?#Yv0F@L{`e!H%LCh+SZ zm%q=e;oH-RvhO_xGKRlA>d&__+{<|Bl0jX7Kh5r)yTct{ZbGd0Ul;uaP7Ag?o!8`5 zmJh66v$XskmvOl5^!gn*ZspO|-w&=;pJWdx{T^=qjM+FWz^-0B`G)fwf2qltOk8$z z=>IkpWi@;DPtV6IV)%9f)vqrs8Xf*%@|Ph1JNdDF*eCt{ z$5ck{|NbXt2dy@}e*a`(bMhU1=A;C^b>RA*7y%5phRS!- zPqcD*eW!nbECSQtLq`rfVC~`^nZy0-5P8t-boo7YJsQ?0uUC$%hikkZJ$!UksrlO`kJZ|4v{~WZ zJn6#NIs8CG93iQ4XN%Dd`1B?gonH^5UqAc%!2S}uMbm>3b+ej)yfY6iDvn>kK=65g z4(PX61C#eh7ut^lgY;vxGT@^!TI_y}sh4~nY)$Bp#^B$4rib+fu)5ROeagWX5p7TI z74Yvi`hN~cnS#K+^scZsY3r^edZGWnzDbIb_?WYw;B{W>5Qp49PrQFOPK+G6ARyKo zpETP#(;dXi900^=s|f8=l?&FvOYl{A3p7_TP{q53laAZ4prjz-S zPcC70di{33|5tMfT+a@HqlD>;lFH^`r*n5-!aszq|M!=<3nuILFy5%jdXVw!zWg7J zGo0d^CmfanykAGmRK)i&@OaJ|4YKxI=i-01-;KZ^6rC?SVDBsb|4At3;DiG;Y^(CU zBaOrQhY926cK;vVYj+{RK%I;G+F6tT%NzYiD+C6eKeq<#NjOjgQz_rZrtI2bg@1QN z*gDQX9{k`4gaoSPD^P6Rec?*^ha+SQwUo3s{rEACn{b%M0#&{}M@R@+(k~g`|NJ9& zF*j9iuehR|K(l4Lf$s_YzMuTV9c42l|G5p2Ry#<&gJD8W7n6ob=gi)Oe|sa@I?7vy ziN-^B_a5gsu^qpU(~}GRqW{$l)%gOPXXUz0({12i3vnQ0`5(s#1Q~Z>ICSid_pj5b zOmg4If4O0CtbqHVQB(=39#wXYTHzl9`z_ze=BoWUOyH#O*adAZqF*Hb3hq6ow!P%P z+#Jq=RN&`pm}L_XU6s9~Quryc`SuLIo=PE;WKe=x57fHKpwEpxu%5lNSlt_d2s(t> zc7)h^fVPqn4bUVr1VzgN;2x7=|4EzeR?ucHpy{OC<r4WcOP01yG^(q58r;BULgU55& z0M0>;A6^fE)>zx-MihSqE^e=WZX>ycoG@{a3p8x-I>3~SFrpptYBHoZE6 zJVsRSm^=6eMua?IdmFMI2$gX*(Z#NNVbTB`>UI0V{fd zSR4d>xM@*X&LKc&Wq}1WsH4?ft6csdrVb32>L2c7Q?-rS_8AaWHB<989J;!Cl>z)A z-v&|^;ev4`crLGl!O3v}D4paqrL^|XPwBNVp#3Nama_`ALj5d2Ns>JsNaN22)L%Pi z2eHAhgcb@oFLeO4sC|JtX6jIqwT4|*)p64RC`1CqUH&1uX@}~3TD_I%BS74d3ADli zpjRdt>Y*o#P7q3tj=O$%p>?}=rI(;w+79WB*3LJmXa%9ZN&JNQ?OuSN3BK1H_mZf} z4g@Q(E~;{R^+N8azXnC#atuHEB^xMyOH`X&{5FWhukqV%4F^2@sG5Gioqro#j%2dr zluYSbSzDsSEUXC=-!Tc_eEb0}6c)*)$Z41O2&)Q&0c0G2|8*ubS_OsfURhpF@~3)` ztbBP@vc?b;%UMB&#{OvuwPiD;B95semqKX*xLysSou|}5<5QZy2x!4{f3DVcq2Z8Z z8LMubU|uLsGJoKWZfR&#mNw6H2A0VEK=W$p zyxSF)5H?>^b$8-Q--cHDve^Ss!vhUif5e||05E1LdR-a>4`oY-@*!&zt{yG00|oIF ze?Bl^q$4h-!@KX&PwRS*=WH_(&ncqa00-Xl!I>>Lm|x>F(*ERuIOnU=0e1kFYfG8I zC1ye7persd6E1v3IsP~LxfSQr^8eDWT{Z%0)l7g_R>I#CAxTGb1VEv<9u%(CC!umf z+r0iOJasx?;U02n%Gj22-(4=U~PazNqycV zfH{m^9aAYCbGY4@@wMtiFcjI%76Qh1^4yy#jV!o%()pP7Ecpi)zn%aZA;>cgn+kHX zDe)GZt;qxx{9N8?dm--eNVnzD)?;6SO@larTWUsYE;ky(ThE#$TP>X1=e9Do3I|8( zM&xmWhyq^Nvs@3_<2bLrTA~;_-nP4)Fx`@dNl_t~05U`74i7Z(;~E7?R)sdMNW0mgO_ ze2tOdov2o}r?`*9D#c$8Bil0_OoI$z>d+S_$PlRw*b{=kBO%rhUG5%i%-D+I@=Jcg{3XaZc*a}E^ z%XDQV5ey~ zB$;&+p(6nnaJid(8kb*7y{dzGaBq?ICI7OH`AEID{v3_aOY|(zPf&?UC9u`QN(dC< z_?Ib~z>daxQtN-snHRS|r|iF=2Sw2`4_!=CbCh`-vKhCL2)D0$_GvFwuw(~|kD{I< zgU*44sVSv;x-Uzp9%{X8Kuj3~VTT5x20_da&zlN_{m@F~uD@=%LPD=?ofB}XoU!^bXBpq;h60bqJ^I@?AiimCQXow5Nz zPf}yB9SALzX1)L(kz=jD`Hl+(y!_f?bch@+@W@KXc*Zvf?i(unjK&3EecDfewoB#2 z#;EZ~r^`HbX16-fIyLM7;GUvf4>ZiSI$b^?Oey}(=Lo7n5Ju2^=S>xP8sM~3257`y zGBWBk(4P9xolHbeMh3MgpI8IsiP4tVok8n1X+UpcprKcu;9(S zZH2P{@PrphsMuJ=Z3MJApU%-|wRCC@mo|~&A zJ{{_uy|(n(O@0Hmdhw4X#7dJ{rQ)oF7O|p>5D4fbvY#LrYugQ)`BYyDO-FgYu|Weh zE*DO4E&%qawF=~Q6`lWooSk<-lULiu72}9?HPnHkXeFRBB@TuRwUs>-0ohO_$Pxqr z8392=+oDL65jJ(QkuVG!R1gTGvNx6`Gss?n?>bru}e>yD^oYBF|hj>PtIfhkox@y4NVz_n*j#S8mZ zI~!{NS#=seqaN|mdwAmI4E-ono^Ki({`usoS2UHzV{wXYvd)oRdy-g_{yDNmcRMyj z&yp5PRNYg$ILvpey4QEV$ezl|@!JESno*?W*-0#XEoM0JdNUhUBfsR!3fL0d3X`8Y zw_a4*^XI)$XO=AiH2eP?2+p_K@&f)~Tq9c~FzGDn;=Fg@Fm&leu0dO*ib#QtQ-h$7 zADloXjBWr}=G$eRdbSaEtWq3?2aqa_lTkcpmgJ0-c!8fJgCY(*oo$a^a zDQv*M4@g!WPHJHvDo*!M%|WGF+Qv7xDm3vn%p!%Rj%qU^MOb;QNQ>o#-SdNf-WQSL(4sCDqyEP|JXP(9JU?O89~N;5 znyu{4JR*b(+4gyyaK3dz4Wz!Q+S=XrdUNExDn)Z(wdbId)S_LV#Pwd!Il?ZfI4jRy zBV2NBKf~L(H#{Q@dhXWRhd1d&(!8(Xc4RIt%#}eApuXMaN6HZo(Sy+t!qyOd3i=hx z{PY&Hw~xBN>G75rFgqwk+YZVL>3|cmSl%b)ast??o<`rsmz$+eylk+~S%6(3!)2RY zB-)xau3JB<2zP&?9D1zv5FyR986eymH_bYO@PXz*qy)u8S+~)Jl6VXApvl9k?~gT` z%pv(lYObNq+>T)vW?O}XDC6z;{VBPMNIatqNpBiYl$A-eeYi_*-yDNyJl4DK{kvU6 zjR%mv1N5?5JSYN!^#b+wfvPQU3v362HpmLciaz(w}lr+qzY0{9WtJp zg6H=aHaK-^<`$HXOOWTb>Dd;LZI#)qu{EY*V{}3p;*pOcb1dYb5W$ZL{coQUM>D|z ziLGu2J5eK8c%PTm1&f%+;y(3Ag<#=R7)+YVYESUG95crfSX0Dg*}kXj6JpjVp4wo} zp5fh&IMXg#C03Wk3}@x5giRuHsit_sLDQlH@BLKBtEaI@K?^gzLdRRLs+ag)!FHx` zioCS&az>9$vGDN3(J~qwx2fb6a*0}ALPI91RrrEJy+wLY5XVl!*m<@aVkZj^1sS?K z%eT+a(=RK|$_4Wjya;0e^!99tuO7{#-$bO^AW=1!cMI7h#Xtxf*u6*T%^;khx<^+T z6keHFa3k`(!X)Cd6<-``?l*l=pDZO6iRJInA@YD>R66NWLQUAkvp-ZgHHxSFwvqvrz78IVhqqe?Y%PzcHYqD0cZLPCV*KDt(f3hl*|8+0e zqY0-m`iv*m}-HBPlu@%3IPqtJR?{?C2kmj)s5x(G+<7}|D-<5yT zj3>8+f!)A!patx_-w~DW(28(Jj;Ra2=8r1B9l)$JiL@L6u774%$UHC|;#30WI>F69 zfTi}(`UeL1oGV5*oZmfh?VtEOOx{P$B!PPs8Pkj0b9`sXUGMp~JkfXEl7hU|aVcd} zm-l(m9|TsZH>C<;Z{WwtL&yjkJN%Mt557Z!#xwqXMVLJ)++jhMwt$!mMDR@?lQm=b zbnF>Z$7Fg+HD-T zjMI^SpTOJ`J7u;e&vyWM{8+$L#!l5nVhgz&n#>Jbc|2p|;S9G7o|rThhTU=Yo2TiM zjW8|C9UIzx?XYcl(Hi%mx2lB_&YP$ab3BA^iIR$^WNB0OOxzjebu@J@tYVL|yd0tU z&a1cm4?gsAhR-UmJI?r3uhW~0(55m}AL5AZL~4-Q9HuxIJaQw`q+2V^EKOwd9)JW*%P@0?Ex(NmyCvUDF?Bndmw^ zF+1%kwKmgd;)B>CH2FE}5&q5|IwZ_?cZg)P#{hw&@l(;FJCIkD6y>4TMosy&#B$q%_*+LcVQlfzSrq;7HK z7T31Nje(Y-P8xM)z2vD&9+j@{Q!QZb)NUEOtau@1_U&nDT!%`&6ywo?g|fV}50ZGq zZ*nbhJwR1(?cZhmh|#RcY#d2cJbiUnl+ZtAb$DR{c$YLk|YV!qLx zqje}jVK2XK?9qLdLlg?bn_cC=s+$*ZqcjKzDi+<7(gYd=`T513M+KNxTyPcu+7)sx z);(uIN~0?=zl)^9q{+A%dQ_mm?|4q1vS~)EBcJRZ=Zj(xSBmTSY1q^I^uW%6N{oGf zV=zudA^2h2@eVV^8-TZcjT|j(>5K?bYpui`t9Q#O< z@8$g2cGcGSGoyMFTiB;L=sCfYhmi@QI9$-_C!KroOhHQ-YW0xf`o#3@BY8Z-OOM)8 zMmW0T@kSaKM5QBQrwX#O-ttgod#?K)kEFWFKTFKZH!T7HzLdJ9MTLuyz&8S^YDZgI z7_>FDL4wk_BE=E$tT725MtXc@#pArGQ<90^8>kV7dvgLN40Hl@EMnfmUdU|Q`Ky^| zo}YcfT9Hyl&E+ABv4z=bP#B-Dtufp+_c^*S`)xAH4l18TLs&V- z&vW8)LLMA5>>LT&U>stxMv3mBunEMicn1c?arF-~w`Q?9<*5(Ld@y*N`1_PBvtzth zk3o%@9oSA1J|XAiiJKn%9pFp%yuU2fxUFMP|<`aVZ>F0xxN zRGfjXL#>9{rBGYtM4R2Bz>eEkhJYxz6=g~nIEpu{y*e!tY)icLGFBPFw1;S$?HH)v z-23Avf>+@lC>R|cz#gtUnTh$fGi>hYyKD@`UWj<67*ulm$`w!f#2hy!H)!sPWcrKC z{;`voUWCz&7)l=~jt_PiN-ewVf1+L{jal!()Dr7{pkB&~YC^0e!24Wh{u0WoZZ3E~{;6n+KoO(8jXY!`Tx@Nay zvNHIu;yY@;tccEv>SOEwbJ5q=NiJ5ZM56;rh6RxskS0yY$pH;lLY=P1N0lHp;N(Jy zFVSI#)xBH3Wki%k*&0h%B2hN-6FT(WKD_Q8dAT`>Xp8umil?ksZFC2wM4XqaLl!0p zxzMu%$iSc}mbF}Nwj2j81PmBB zi28CNe7+*vN$j^gw&`GEZD>#=9#YyXLWoIt@y9RtedTn);JJeZ1y@H{%s!rcIxeWg zDBZNEuqI4UC!Ehl-sJ^5~AEmdD(Ne{XwtO0bUL|LGr;u(QnqL z(wt@Qyr!M$5G^R~r$# zW6gFCP6#Xh0xEw){W{>D;_jm$0rzc3sFY>3xkGauPc%PXMdnyUl0VIpH@!P>?q!3{ zNVm52-mdr43zNJ*)YsP^SHF(}Z`~*gn8t)YK>~onIgLbLWhfcyKK+s_kUpt3G&r)q zAWScB(4|k&FvQPTAcnA#>b;I#O8K@hb?;=oc9{J_s_J!H)cfaG{uDX+6{7&XP1vOD z;UH%Vf;u5!PhBYSy@}n^GadUoRr*Z-PN`=7dOvSbb~~jTp7c@9?###5Pw@^HU5Np7 zvpMpXbHuT-$=BNrPjKl6c~;a;_Ogmk{Wc$L`WpcdJQtR#1asp|mwxv6hL zvrhe3gcVaNZeZ2Te%uRJ=*_4X&NH!qV5P7=x|jhnmj}}8CZc`#bp3Y!JmGRCt3l}b zD;(32qGK2S_CWnH#!a9bm=gjgdUke`YQ=pfCqNmP-t?gNk*=si)ZwX#^7)7)@!dDL zob$Sm(9TRSrrki%9TBE~@|{X&pN(!2GC@VMte9;>NzC`d)e=ingtd$Mq84c%Y`2D- zSd;d*r|*^>y=*9tw)H`o^lwQo**NTouCCCAA+E7%VIXgTFEmq-vN3U!!qpIL;(%x8 z)nWv9#wGq-0#KN9;x~W!BT((E#;IFeSZX%3HAy^m+3E>j@lsuYnAFN#??fD1S-dlL zo1>^zRVXf5@fv>R?Ya@QQ@?U(ojOLr3Li~%3NbP$6dG#wfM8Xic$bLz+ec*k)C`Wn zDU1N>Q4`d1`neL!o9`VZq*mrbTrO#RVj=*VK6Tg9)D0~xH%ju{rp#T1-Cd9-#GWgk zAeNiP@PFZxz;w`E9J29{U@pkz^HiS7NJo+Jt-lPWOBa+4hMGk<$8s#Dv}MkNs!?!) zpXl61?AA+QI_g&Dq*k5!MkC99BTgnEH+B9EDZ(t;w0gX@0ZB}2r}V15&EkZ1yT_x} zBKsb1%JRuJe>U|2btCPU44OMMr8R)13beCtp-%uOgY25noI9C*1DYJUb_Zw;H_dOf zxprD!?#M{vHQ~P9}EpCwiP&UhR??_oPF%?i1>&H zxAnDfEJ6-jhp7*a)Q5C|yyK%0<&4hRaPhXHOD8@a4p7D`d<4ZrFFgX0&VHe|-(e7SrWsEhu8h5ZD2(`v*3YR(U#Zepd)Ps9PpiyacTsMnZ z1))*oM|Q|ec5`*{@=u~@)RSX3TJJTMzD-X`N?Jc+N^%sWJbtGB?BG!wB)6nnwH5_2 zvA&O=ptydmmGyRmYsMuTT*RaZtLr9PXWyqwp3L^{11yh8yaGa$oN;bxE zMB~0wnC5Y$bSF++2VVLcr2`1%kVJHHgYJ2X=`e-r%^)#G8lR%O%_o zmm!;xP}l6y74b&0F=i2_$V|kXDAlIOE^1D+SLIp)j7t}D>IT=mk&ilxVIFecW7mrN z$&$+?_i3Wu9LFZBIr3rrVCdXo<5%ge1X4LO2emWv%_Etynce4AO&n&@N3XRy)R$Lr zJAmZ*dH3$>7(a~5+K{ax4!s;oy*D`Qn>L!lhM%iGI!FwoEjTARh)Z?0g9bM4o>>Hw z<&+btac$Wq$N~>K(M;IzzE9{wY1Up7Ka;jN+S-UV4W>{*w#&BH7zcYB4GA6huD1C0 zF@aFC3(D=Zq!i#zxE>qWpEmzqR{9Wyl!|UitRz@`B3#>r-ItMSbK-KD)pH8R(Moyd z-JO1&M|i#p&F_$az3e$PVz-&yKPXCO*W623gMV-Zm0+N{6&Ap9k=eeekIO_JBP-== z2c;%jE1zSwAUNUMoj=H5I7oQYO?XG4KbZn#K=#Ea$rvfzJyt;n%4ukeKCDXh*8W^R<5Y}2nui;l8jNopdUHRM-A?SCH~XPtOs9DG z2~RS{;Ezd&Ts+|x4R(wfiZf(dI0F+rb^!s@+)v*|^#=|SdQ^VeWf~Fr&<=XDbBIlU zIr~G=xy6#1h--iROtyW67$XR2eCQf`bv&2H{0IEIpE0WaCOXpWvvY9idaM7yt(!p% zDWoXxqlTN-Q86Z{jw_Z?XCjgCG|z@Kpo=$Aeqjm~I)gjE&xWkFVoS9kmD_Q>EDu;I zSoL;j5T{mtAy~W9E;2pBLo>mZkY!I^T=<@ zBsyGrrXGon!&i|lYpop%{Kd7Nj%C>V>OfQ}n?y(+#0#r_d5%`R*uFUDfoI&rOuT(k zP5!_$zV+8^q0u%J_zrb7hlX7OT+r+}cA&w1V>R(b8A9TsiX5_&?$bHZ&UVy+#jB3i zZa0hoiTKEb-g$&+>!<7^H0#Bk>=#8fE(p#@xobJJe*ka@jMzre&g2U5fd_i+n3YaV z=1=cZm5G|=*>j`wuLYpR&11|lsz9fzHDzncL{7LpK0eUNylyPMx2^2Lm}K!#-r@p6 zwg*eu6Vsh^M#H)xKexet*mr;BBI5GPdHGn!DD{?4PQC9AuzF>)P2R z_2#HED%P5~=Q1{k+N0wuUE221_QcREMh5+!$4@n?YjQbjvU4)0tMj%KDtwPiVQ=+H zhOThl$s-p!WmGqSpVBULjyYI72v%$D+>wDkGsN>okq!Bd)V$sS2xEjx@tM0Dc7;bu z;jHL$xl-*2pQZm0_4u3(H?-k;KL*YC1hJj4y3foFQdJ=ZV0OH?Cs%<%2*MOLRfcP?Q-w;gLzl=MiiI~&W5g@J z|F~Uay{z8Dxg?%#2K){gt*LL4h6C&_Bp@KGyV|2o( zG&n3Le1z1MGlnvx>j~WkF3{4^pV-X`VwYtJOw>ef1W+H5+4vXP)}aIA;v?_}DRKyel%6eCmERT?b% zLL7J5*sfyw9~3i@O5!a2g|1(ji-qbA5k@#{7N`s*&^ImjvRE(VuGQ_V-{v-xM=muz z$~$2x=2Y|{(J3bmv2ALNv6J25WMK!k{Mw>@^K!|8CU!*yn0p=oYnywVEQ^8}35phZP>d6M&vGD3 zocT(0N_RNN^OQ5zznC{~`!Q$^LO$Fjz2hE5&py|i+5e$Ss$4RB=!X7iSWlzeM%w04 zI#D~P$C&R)-2)WbJfKRtEVT&-S`X`uEK@hfeHw8}Uk_b<^3u#F(D4%MsWWl2k& zs+Iy8e@-o<-xEV+*ih-44npt6Ha;KqTO26hwz?OkNlcu5BP(_8uIL}P$;rAoc@u=_ z>vZv}U^WJFs!!d&-UN{z19Wha)r>tzsgw2XE`?`dSzJ9E$!n4Wnaeo|tMili@jzP@ z;cXH-ob~{ z(#xm3#Q~ubX3>-CR=FU`6OU^LEIl6F0BG(8S@v#3jw?yKlym&8D$zX>=Rf2~&EWK{Q3m?~w&2t>|DNF*c zN;#O|O&8)A(a3{u%@KA@X zs%n zJ?Ryt7IrtHPIylZ?@yXQ>4LRRlCDLisL1|HiRKw&>5~Cb7up=44(I8xw~@MQ6Mh4w z!_fVB7mc1CE$FO;4-Thb?92ZkOW7r&$1y76YLFu|ryB$XLla@ze#-7#T@YGJ2QV7j zZ!vuJqSnno?zDW+dte~uxV4=}SGfN`XnT8OT{~G@%05x={<#0Zg8_|O9|+%=vPJee ziSDbcu}2J?8VbkdYSp6qLt?TK(KVD_zUJaNRv9Fey}elf`{rVk?0Pddmc?&WvpS3w zt6g^lYCDsow`*#D@&raOoL{SWm+$NVEyrz&Su|dRMWl{6ZwPi{(FmrMB2h^}=J6)0 z2`y)~E#TD^*D$T1eh5$PHGPui{nM<3JCIT!GIy|+#{SFuBx^QH@Z%WcABgc#3_Cr3 z+Aa|s%9MUSPja6`lP2mR9jjasI+ih%m`ij_Q$E)C$ZB+)JYiu{=7+HcYb2=8M1Ba0 zWcmoC;v>DWn-%5whyEBtRV!^LUYeb#R=um-A?%r)dD1N<2hvw-O(Kkn5#a^_%KA+F zI^pEh$5~h%rO))+XLuL@P6%HQK1%vCe!`RS`;;vvkyVN$CCkW0Et@21!jmw*qmLqe z6Vy8n5MG*kY^W6N^qn2ykYZ2#S+}WsXO|AWDymyCQ6s+Eu&I{-)k($O(^fV)J`?Xa zY$hKoH`O&~k$N(DxC5V?1}1Xu%JT001B%lktQd7>L}jIEUe8GScR?oN0;|ILs8+Rv zX1rCS)B=QI8Ptn8)nfhDl*fGg@@mM>pQhIYUnjN^($pi$GAm{qW(g$Dt5qj!CH7Cr z1h$lk9h(!$4Suw^qZaZ_W+W~%H!+(E|GB-e66#`%qsF>uC1dyJq>GDS-*u4>>&L zwE5x&?6*BHI3qLd!~65Qp|VaFtlv^wV!GvSkD`!!*28fxF6xPCik z(iy1{#~$k2JRR%NCp#%f4#SjpUo1+hK0J}$;_7_&oLRqeD@}T|;*G1|)Vbjpt4iuQ zuKESa7USXKkCR@y9<&K=B~~xPPelwd$EPG6a)A#d#9>%;U);%U)=7!Eq1%lXlp-{x z>1xQ~0hx=Ev9`=Ssbj@vISz(75hefte9zeJZ~s)eCKnbJ`-cLW{>0QUXo;>_rgGea1%AoEX<-GI!jzc7? zgnENFs}sfUx;+DjMY%p+(vK@|eJbsL$PJ<>g=EBvKv&|kzhlSOSNZ3VaemRj~rtXFTKLXZ!ci#AWlIX0I}s|6}_%&Qi+ZTTF?fO3C6>4)@Mo?blr57Qc=@ zd75F)2+2$gxbe%4 z$N8al+Ocn7Ag)j!ni&k;yUQhXrx!TdtQs|_#&E4FLoJy8sBBorpEo~>Z_5KvTPes! zgpct$1n$FLf+Z$G356exg;6ZMaOLQ)9cT{~vD7R|o9&Ma0)~46 zr`?--&Yv7hl=#MSWya5n_7aeGMPze{Sr{9(s9kli$qpV=Qi0wQ@8Z?Z()3)JWYT7A5L zr;drWfo=Nkkkwb}G$5pHNdJCiVS$4AGI=VRWJQtJzy12LewHBaJU9XwL1^8@6O{&{ z1iG+p-LD4Yzm1}zwLy3m-6$1@C=)EHi~HP=7BsO)JH$N(K)VER=L`>`z|4NqcX0lA z7OY2^15+sD;HTUiL3P(go2yx}&E;2cMj3<&HJ2PdD*1g5MSUqZ|2UsM$Eud5my8D+ zK+k1o-Uj8KADwA1@e9Cg9GIkB83A#IAYyAb!k;Yz14x}QDsL2q-LYxulb;ql$h6vG z)UGExB?kC^7M&ndF#j66@SeX|f1Zc8f(y@EiYhED(hFFPCC*xoc7cur3Y2x)n8Qv% z_f^DWpxyK;6LCS_T^yeDLljF&rGMX9q5T53@1t*NRyIF4s9u_CUVSFhmC1Spo*(#( zE`B_=S9q=gX#w7Go!=8xO`@`3*J09bFU(uJ?eamZOt zV(|R%y%Fc9nX>e-{+_h7QxK3LBj3cIV8g88jrt0(0~hr#CKS}3BLRN2v$^upbcFdZ zy+wHaznoBT?Ps)Maz5~M0C|@YCiPzr^zyNDf!!myJ~8;imuCwbbkWDRu=aG;4Ehq- z`0o=6=2s^Ocz1dHyj9M~Xr;iHPoiyMnev$Rk~RGQgI#bMv_+h;!INK}7L-!}jqU5J z`iqtOZQwTg8wM5Y%m4a2qozRO>dFY=_|)hR9^T+KTM+Vmb*ukm zk}9o$d4RR`4`|4{p7Zs`mxKpP^Yd@V{`7VjsZ7fL&lf>Vx=vlDdi^<}jGW|kXWT#XcuhzWMr>Z!}8q&o3z{yTJYRllO8c&*lVNjfbou{!J% zrRDRV`0sPUcWnK3r0zpC0KlbEzXv1?J|JOyzG@>k*zoT%1_=ER?b-g_laNtgz0Kg- zyS|f!nDy-ZRu&2U2#~UpZ56iRW4lkE{a{#f9xYy)b<1hHOQZc4vt`rSy(bI7=1AuR z3&b>%GHUg#>va47%#YY{i_Pc1>S&ini(MCn1tv*i7!Z7ayl4AU`!}(?GEt3ev7a94 zpLnQL*?jF~>IHNaUkCSPD&9 z4jR?`vwvEH`Ipvras9~U+~sdNDOge2{u!ag!dV%0F+8qtwa#lb(9x#Z+Dn>D2hwl>u^>oyZ}+S-N) z@Zrlt{X+@XR0A8vI0dfispCX?bZYrw_88_5u@0$8L!v41UP_(LCurN40-er-9J(GR zmAR?r`Y8!{rU$I6E7h7Z;7{LW*vP|W=cPB+Hys;5@u_18iXPU~q%`X~YC;~~y8m&W zW8?8hc{`lU!y;vOF_vNh9UWaI_sgIdvHtPRPwcRd!@A?B=auT`K>4J7x8`fRuAE)% z+pkUrWPo@!>b?VCPOT{jDuj}+>iWDrM)D=jP3DeC_70|BgfUF8PS40FpR!`WtAV!( z`Z+mpZ39ECQq@l!`sxzf+Tg+`CRHLE+jFm4XxUiWgfb>_(MuT6>dXXqv+k6Gjw;-x zdT5VXn0divrcpE0D8AWXkDesP``D;PzsFbBn3v{Z=bC70c(5u4;Iq? z7Q2Tm%Tngrzik>C%gHnSUU#zh9K&)VCc|OQ$~trcJt=s+H4s2bhapGb3(uh+?f^Yb zG-H}-6mt@qd*kPob!}}m$2k?R%b2ayUD^S1_`rtCs;_v1e(|s1xt>Mq*KImB`S#Ek z-daAmxQF#&>lVMcb@}q!aw+6G%Q^mhI{laJ%In7P*B(dy2pA~0Y#)M;fE}eP9KYO5i;`!v_&Y0B3A3`G z;Ah)_+J?yMq|e`oA^r%Dg7w4!_seekb>{qkI}JO$u6J~7Nj~z~A!B(`K>h=pBCgC> z_{$$zcjV6{gn#mt{tWgIyzvvt`ZetGlPH~%N!VjR$`#z}@JJ9&k-u~qzC zH0kpN{MYZigalNpJ_LOJM;UfZ&C~$QB$iS0kQPzpI=lP82^Jz%V%DH57=dW{a=`?XeH(= zfvH09Z}@x}RRe5AU;5tv<7Ul*l} zD6EAVoaH;mmwDg+^yz;!NuqAR^j!O0=+ZRL;RmGm(uezs>W?K^SyFp<<3tg+5t#b@P^Ek54%)Z30VEpmp`&-yLP} zk>&ftRfud1vYC9G1n+-bY4ZOEdTAn{S3v66*B9kUS#9+%ugO;zZBnl08(4;qiz*l! zfBMdU%!gMBLk%h`(lkF;&aft7>NnH>4#oz{LIYw|e0O%ERaRxmR&+C?TrhlAf z@V0k9Afox7nXi|&CjZ-Ul3B)ll@Y@3_z!Ha+sdu~^fGt+H|FcSZ2rz=*Uow#fA^is znXgq6Y-j%Li2s9A`13x@^Iw>+QQJoTdops_z{4(Bxh}g4FLY3r^U4pj=|6rg+p49< z;@FHrt)Xkj2g}35tC2X$4t2Gycjygw+B4M|zI&A(>SqlU7`}&)qA+*d5PhZB*Q5e+b{cMjI7Xft7~pt+HT-a{6ny;i_BiaBB)G1VsfPB42eUfO4rT8uABjIS(`dS z3tbNIeKQF%zUTbu4L^0GRNHp_s~?dL$XShf8X^8Y^Z4WYsI6T{v|#;H5)IOFrQpPB zO}2Y1k}aDagw%fUl^Ni_OWPdL*fi6NaC!(6)Nde$!U9l2T2G5k!0FzQ?zc`1%pFIX~F&VIK+4L^rasS@d;vvfrr<+ms3TYhb3_X}BP{dv?+-XRJ zZ-jPH&@@jI`dGG~eaPbUh^#VJu?6;55Q?JlMqK;eP~h~IYZdkp>Z@NPkqE1AYz83P zPK)YPKUrr5WtiOwHYCfEF(y>1J*b4n+ugwLu~u;I-z!|kHbEe_6A;Rc24Sw)6(qJn zXGfqDIMGMNTpq4^2)!151Lr~|l`qkNZOh2slS{FH&-3KlDFhsXiX^fHkOmFK-ygO+V&L~3s0#0%tU1`rgz194%8cZkkT%;Ls7gs zLeW^bvUyX!J>Dz;cYKKqhSdGUCptXw*9-sU>0*LUEc1Ro#AD z{}5@e@FxoyZoLw$L@R1gC^nQ@ea2x8)i)Z-u0c}1c)$bxD}m!r&Szs015xGi*1P=; zm%Oa!p^LnsewYgJ@2rDWA*tqCn@Caf86*V z3xE9o=U*&~34XTTz+8~jeOJntq-QW)xLuQ43xuAJAT=GuQ#WDa{#4I&^yTuDwv` z;*EkPAwwzjn6=NLQAy81`isz!H0^a}9UpE&b`>%N$2z`0WbF8j=)cUR82z#C1V{^# z*ofG3+_p!q9I|nLOnc%GfUrda8j%~f1_K|Y6xdv6agHvaykRxsLbKRMQ(_Jg;AjfO z=@#gJd&X)DjktFARkcRQoy*2o*5?d6bfYLN#WX!ng)lJa+CJAt&YZP8-ary-2RXYg zNUCxQ_J|l|Z9LwR%z5S2x2B+F;7T4c+Ig|Al}-081L-5 zt;g*%Q()WGZ0>gyb4u&%VH zoUs`h?nQ)&CrOC12-vZtORbU}vuxk+tBKsGlZJC^6e{{-P3CF>LR2LuYRDgY^f)}i zHc5_GmA>fo6xhY;%MKer+(lmp<-Yyfkd{~*>cw?+rt(541tOLPMgF-W=@>GoSB9WL zZC_IX+Q{^&Ekd-Urd>qMTtsKp2&F%w4AeCdLC%qIN?|*2O2!8w55FP~BxEE;_}5KN z%*ykIpP!&qj?bXF+RR%&jnpQ&ry)G*WgXI!aH7Lz9L=Rj_C=$U8F@KfQVB^O!^~?>^IEtbzl>~Q1f`xZbl%3Uskl3a zxAGL}f?kS1WMiOFGnO|@?T%2hy^=*c5P4Fv+VN9O2xU4$3=@;;teD^|$=7*$^3|dG zV5;;x!qSUg<1NWy+h1aZ6d|A6#77CZ%SC8 z9%THCiU8f9VEO=U`?+3^$xS4IURO{T>af^Zz9!EY2`Xmv;J1~343$d02oW5P&@Q;ruk75bxSU63HJ|y zGV;VM1>wu<^egSEF~yreJ~>)+Joh;rvV4zWn_ln75A)d+&J-b}tSkwZnki8Yq+bn~ zz%WXyJ$e9t+5dv>9$?#C9ZH-wEcC^m!H)#*tj>kcS`Yw#i+UhyPz#3uky)EQE6b+C z5;i4a-{7+~N#--p5ke8sFu7Mu!>H%Xw=Xgc%i0#S2z3s1+D~K0JlF0yKv9lk-e&1M z36V2O8e=#`{=Gev=l1N9)B~zthyz2UK2=bqlMdZlh+#%4Tn1$te}ivVJKy;F1|PD%*c{0!ueK< z$0D;EcrILyQ!!$d73NRFHF0O?l6LT!&fqG5j( z4{*J(H#&gcG0}BjRg*WnCMGsafjkE&+D?z}?+4c%ufj>UREM+M1b8RMd$Vcd80_$%5r z`nbDvkcQZmY|s`v=3q8SvrJxkA+xh(tFi>r=ezXY6%hVgX}gr*v2Ox*&C? zc5Zf#4l?6XQ5;!*5%~!6x09JK5GgFP#{-aS3dH-rTJzhJE`y?-9HpPw$D`WL&LImt zv>$o+gD4G1tz`!Xtrh`V5bIGf3ick4>n2T_^LkHE^o0xZQdN_W5`Ht4RAlqus03+7 z!<92HG3(gp74s;s1>E7P`;UR(v0cIIWWt=CRiV@K3MChsYKj}^jQNoT6T8>Q>ngL{ zE@%Ltp81-?(W0-0kyykbwkDWzgx#^sr{;Vl-;`p1dDQ)(S&&&gs+xbdaFA^<~!!=`JH1SQ&RfJKf)S7)0 z?=m!0Eqm%ZqWNysyJIt(82Q@wV07Jhy}MPRFU&`it;4=&iCMndvvTVu;Xq07y20WP z5@oHV-Cwj1B3F&<{OgYF_ie#hZ`T`$_f6+AsmVEAE# zI!8d2o13YHMm%t{`^XJE;d;;RD#NGkvvnDzz&6e$4@^yW`e{+;NL~B6Vu+jRRzA$# z&E|$}s~sezrwca6@+92lut~eJPI`2q)b|;OmRAi{k0+J07<1U3u^~OnTMN2KPqtnr zQ0qucu=Wo+-|C%`WqUK>7>O2$13D1$yr`u4HJ>BHpMLl|e_i-@3~sN|Ufz>2AcE;z zNv8i)XJ~Xt1sj8(57f>_c5{#_+!x+B50S1L^>GHY4zF(%*hH~Pu#2&3PdC)qIE<{D zmy;3p48SPk?#aKvuY5%h30k3c${cHnZwczChceO+7EkiL>qK5&@Dj#KqrDfSimS5Z z(j?-PF7U1l;)3eoS~`F&slVcH2iS8blPPO-Sq!raB0gb!?_NXY3Vggcjw}&^Ug0%?TS|t?!aX2nB(fYR+yk(eb<%{Q z^DxcsFlja03E%wWy4MDz>%CUp#7hIqQP*3_WZ9j} z$>yZ>MLbp#%zSrE5rPhzxj9z`Lh=}x5AnRq0&O=B!Niz&``0Vbiu_4rO+;>>flD8G zh2dnH5Z11nGL1?{aaDJ;T|;MGhkG#v=S(Un=^sj|!CQ1I0_JF>qOxogkzHmHot%-j zR&56lD{}Sm)eG(`Ji)+Xc!RYfokSzC6?nI)#fA1oJ{plWWyjU4UE<1ZvfCmrVP7UN z4BUK#f9mJ5@Sp{;pQ2MkjflgQdRZ3+L5o;RI$|4HnR?qK(a_7^nYYWmlId@0Uv+YA zfZh!=olOg75i!}Jqe`{Wc^i*4M1@@`;xK&{oi`ib$6Hx`R(7|>TMsW1=%18JT!ku6 zwkGCPHuO$uxVLw^_a_Y@#`GbXuu135ll67TOB+XCs*t{b$x@ck*ZaEEKRhF_zSm_= zox@%+-In8Rf|FAa?OVk)ErWV0D-vx6u7;qt=IevHg1y5zH6Ja-0-_V4DYZ&@EEM1T zq^O2J8HA}4KgN0U4mSMiPAxA74=4hUBlL{%8O|sit+04WmA11w#XS?kQ4@IKr=R0h z3~`x?!H=7jG%08K-#zpuiI*D%P*xMlG`ucQNCGFr>G5fRtPAwMmJ8mVmS<5PFK3rR zMG$2Ww_#vgy!^-^DnO=FokDFjz5E*^I_dywn<7psxzOp~gxjMSUhTBW7~gpU2p|e1 zY3>U_6fS$|G)H@glLcZHkpKcbkI_1P{K^cypc?-x7uS^0w*{>@-zvN>qor~t!ZM&k z2Na#2C1F_$ZL<_9_Pgcj+riG6dHJs+~da=4t%*%WgYfFfJZmr+~IJqFT!1=1Ww zX#m}ii_&b$*j3L6)~gTW-A7Bc)F0*Kxfg0njEG+FibyC)g6bU-}-OZ&vQNe1NZIYi}Pbab=n)UT8oD|`^P2gdtR}t#uIS~b0s9i zi_qFPlKqYN?)vuh?9AMp--TKI`lChptWgk#o!vrVjx(~d4(}OGf3hvlyFg?N zRkSFc3Iv9YQA1NMNs`Gc_aMqBhkHvP$6j^UQ$kp2WRMTt+B7XmdWxQw{7AxyIo^7K z=x1)_@it2=vLdQCBH=4|xxfvy429!If|7O9c~{hkfAbDO91yPUkWp||HPFm8MLJD} z%Tp`KlazhJ`%*C>Db~q~r^wi~Pe^g6b29{Kfiz-5bN>VqEppccGsHYR3LX!lZs~8Y z=Hu(dDy@qq!1!;+N37cYpjWJ3c&d9Squl^^V(_X`QSh9|98R1rmeQ7sxH1I#w#MAN zg$01L#mocpG$F5Igc7AWpYoh}f_B$=*lr$b+CzAD2c4*-shI2PI$1Rsn|NKJ&*1eAnl^VPzA_C`!hWTbldxFUyU3ybREMjK|Gv~@iXRJ`&8~!z>Y3bfbbQs5ae~ow4WU+u4BB*gW%v$7I8Mh9#{l- zd%;1PLBYqUZz14u=Y{W5(VvT4zS=(x&K*(Ndosa~G-v`bIPXh1H~A>*vGU_WVA%I< zm>~8%wUV3W-4}%>`uR*o?p$mu@Q`KYFFlo%g1*>`<12z>yeY`j!0WRThGMbN{0Rc0WAz?qFtV8PeaSYzP!r>a-?(5PX@P=$69*# z=t6AumP3dyAv!O+c{Kj%UGSk3FP?dA>@Yok68lq6cu?a(YDDke=<000$TrF9MLRB2 zlZwbn&p-@$t%V5+7@}Hp+rY%&8~=1deF~9+CHf5DiZjRy&P6O72!y3@ue)!1j*ZJ9 zbe8Zg(wo}P00gSf3%blvwX6@h(gg)O@cHB_gHv))dV8)!=|aq5;O;{3xLV&Z*f2nAH zdaD%S{)G(EDr?neg2qWL5)?7pC>d2R4_dLEQW0f=eT~8!fhjY|8(k=#L)LzYyGmb3 zRSZssumtzWHL&N*s>ks?>Q9tnh=i%d-;c5}H=AZ5y~UDc+gQlWx!{k*httoHkJ zr#-BFG8ypv4gmXod+Zj2qD;ssXd1tWo+PwPVp}#&H8i@mcvQH9s5xNOCef(%Q9V0* zI4Y?A!nSO#i{@g}X{dt{r(=V0H_!p%H|dy#JCvej-hYC2JnqivdH)Yx&dirw3u?H2 z2tMvE12~>`+eD(%oojm~9D3cZ$Qd24yg>X`=|HCLmef4E(Xbpq#0haMLRv%vY4}Fb zKDfS*=S55V^VZ33$$%!4WLU2n``&?sGH<;{1cXvrH?ht!b|j`LY-d}+GMw@;>}rDl zIK1>w&~~!XEC?M_;+6Abm4*foUDq5)kcOaGZ-94#PR2w=L?adF)t6L$tvyIasqhA? z2gNf1<6sDk0_3m&^E=XHv%+C?+;KGl8~3qL6y+7k(e`CO6_#rp%tw(Q0)(rp!&S>g ztfxEpQU|eh_-RK-06bv1@s#pmNciw$qfH&D{rT?hU71tVtag2hzkqRO1Ad+g47TxR zse(Mris}Q@BR%el_iTY^$nSSzRIbFz9G&pQ@5?3!{Y;3hio)@i?>kPADFe8jbR~bF zHSs78fdKg0(QnKWJE7&yh2xRkl?52WEf+Fk&$v__cX(wFy^Z=aLg{;gIX<9n0&8?| z$zkAi3Y)EN3g6$bbx^0#S_qLSllVtxc6Ab=gPZ-wk*+p;~+B^7_6HD!z<&{kdtF@YR*F zU$%gcmd&5i>7D8wh<8j(QQW~z7^r>dhL|$7R*G_Qvv0d)@0JfbHZqa4E`)SwASaW1 zdihZwA7Tq!MikYGL%osq9GFf=> zDI#b|gN0X?VyZ-_hQo7#JueNEZy)hY^~SFaAqjdu*2{>F;IALnZJ*Vg2;jgqAIsR@x+4bPbLexcpdrR0r;9h+;2j?@m z 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 literal 0 HcmV?d00001 diff --git a/infini_train/include/autograd/ScaledDotProductAttention.h b/infini_train/include/autograd/ScaledDotProductAttention.h new file mode 100644 index 00000000..5bfa9ad0 --- /dev/null +++ b/infini_train/include/autograd/ScaledDotProductAttention.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include +#include + +#include "infini_train/include/autograd/function.h" +#include "infini_train/include/kernels/cuda/flash_attention.h" + +namespace infini_train { +class Tensor; +} + +namespace infini_train::autograd { +class ScaledDotProductAttention : public Function { +public: + static constexpr char kType[] = "ScaledDotProductAttentionFunction"; + + ScaledDotProductAttention(std::shared_ptr attn_mask = nullptr, int64_t dropout_p = 0, + bool is_causal = false, std::optional scale = std::nullopt, + bool enable_gqa = false) + : Function(kType), attn_mask_(std::move(attn_mask)), dropout_p_(dropout_p), is_causal_(is_causal), + scale_(scale), enable_gqa_(enable_gqa) {} + + std::vector> Forward(const std::vector> &input_tensors) override; + void SetupContext(const std::vector> &input_tensors, + const std::vector> &output_tensors) override; + std::vector> Backward(const std::vector> &grad_outputs) override; + +private: + std::shared_ptr attn_mask_; + int64_t dropout_p_ = 0; + bool is_causal_ = false; + std::optional scale_; + bool enable_gqa_ = false; + + // Temporary storage for FlashAttentionForwardOutput to be used in SetupContext + // Note: This is defined in infini_train::kernels::cuda namespace + kernels::cuda::FlashAttentionForwardOutput flash_output_; +}; +} // namespace infini_train::autograd diff --git a/infini_train/include/kernels/cuda/flash_attention.h b/infini_train/include/kernels/cuda/flash_attention.h new file mode 100644 index 00000000..d6f13e01 --- /dev/null +++ b/infini_train/include/kernels/cuda/flash_attention.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +namespace infini_train { +class Tensor; +} + +namespace infini_train::kernels::cuda { + +/** + * FlashAttention Forward Output Structure + * + * This structure holds the output tensors from FlashAttention forward pass. + * + * Args: + * output: Output tensor of shape [batch_size, seq_len_q, num_heads, head_dim] + * logsumexp: Logsumexp tensor for backward pass [batch_size, num_heads, seq_len_q] + * dropout_seed: Dropout seed for backward pass [1] + */ +struct FlashAttentionForwardOutput { + std::shared_ptr output; + std::shared_ptr logsumexp; + std::shared_ptr dropout_seed; +}; + +} // namespace infini_train::kernels::cuda diff --git a/infini_train/src/autograd/ScaledDotProductAttention.cc b/infini_train/src/autograd/ScaledDotProductAttention.cc new file mode 100644 index 00000000..9aced35f --- /dev/null +++ b/infini_train/src/autograd/ScaledDotProductAttention.cc @@ -0,0 +1,149 @@ +#include "infini_train/include/autograd/ScaledDotProductAttention.h" + +#include + +#include "glog/logging.h" + +#include "infini_train/include/device.h" +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/kernels/cuda/flash_attention.h" +#include "infini_train/include/nn/functional.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::autograd { +std::vector> +ScaledDotProductAttention::Forward(const std::vector> &input_tensors) { + CHECK_EQ(input_tensors.size(), 3); + + const auto &query = input_tensors[0]; + const auto &key = input_tensors[1]; + const auto &value = input_tensors[2]; + + CHECK_GE(query->Dims().size(), 2); + CHECK_GE(key->Dims().size(), 2); + CHECK_GE(value->Dims().size(), 2); + + const int64_t query_last_dim = query->Dims().back(); + const float scale = static_cast( + scale_.has_value() ? *scale_ : 1.0 / std::sqrt(static_cast(query_last_dim))); + + // Use FlashAttention kernel for CUDA device + auto device = query->GetDevice().type(); + if (device == Device::Device::DeviceType::kCUDA) { + // Note: is_causal and enable_gqa are supported in FlashAttention kernel + // Dropout is now supported using seed-based approach for reproducibility + flash_output_ = Dispatcher::Instance().Call( + {device, "FlashAttentionForward"}, query, key, value, attn_mask_, scale, is_causal_, dropout_p_, enable_gqa_); + + // Return output tensor only; saved_tensors_ will be set in SetupContext + return {flash_output_.output}; + } + + // Fallback to traditional implementation for CPU or other devices + CHECK_EQ(dropout_p_, 0) << "ScaledDotProductAttention dropout is not implemented yet"; + CHECK(!is_causal_) << "ScaledDotProductAttention causal mask is not implemented yet"; + CHECK(!enable_gqa_) << "ScaledDotProductAttention GQA mode is not implemented yet"; + + const int64_t key_rank = static_cast(key->Dims().size()); + auto key_t = key->Transpose(key_rank - 2, key_rank - 1); + + auto attn_scores = query->Matmul(key_t); + if (scale != 1.0f) { + attn_scores = attn_scores * scale; + } + if (attn_mask_) { + attn_scores = attn_scores + attn_mask_; + } + + auto attn_prob = nn::function::Softmax(attn_scores, -1); + auto output = attn_prob->Matmul(value); + + // Return output tensor only; saved_tensors_ will be set in SetupContext + return {output}; +} + +void ScaledDotProductAttention::SetupContext(const std::vector> &input_tensors, + const std::vector> &output_tensors) { + const auto &query = input_tensors[0]; + const auto &key = input_tensors[1]; + const auto &value = input_tensors[2]; + const auto &output = output_tensors[0]; + + // Use FlashAttention kernel for CUDA device + auto device = query->GetDevice().type(); + if (device == Device::Device::DeviceType::kCUDA) { + // Save tensors for backward pass: + // - query, key, value: input tensors + // - output: forward output + // - logsumexp: logsumexp tensor for backward pass (from flash_output_) + // - dropout_seed: dropout seed tensor for backward pass (from flash_output_) + saved_tensors_ = {query, key, value, output, flash_output_.logsumexp, flash_output_.dropout_seed}; + } else { + // Fallback to traditional implementation for CPU or other devices + // Recompute attn_prob from output and value + const int64_t value_rank = static_cast(value->Dims().size()); + auto value_t = value->Transpose(value_rank - 2, value_rank - 1); + auto attn_prob = output->Matmul(value_t); + saved_tensors_ = {query, key, value, attn_prob}; + } +} + +std::vector> +ScaledDotProductAttention::Backward(const std::vector> &grad_outputs) { + CHECK_GE(saved_tensors_.size(), 4); + CHECK_EQ(grad_outputs.size(), 1); + + const auto &query = saved_tensors_[0]; + const auto &key = saved_tensors_[1]; + const auto &value = saved_tensors_[2]; + const auto &output = saved_tensors_[3]; + const auto &grad_output = grad_outputs[0]; + + // Use FlashAttention backward kernel for CUDA device + auto device = query->GetDevice().type(); + if (device == Device::Device::DeviceType::kCUDA) { + CHECK_EQ(saved_tensors_.size(), 6) << "FlashAttention backward expects 6 saved tensors"; + const auto &logsumexp = saved_tensors_[4]; + const auto &dropout_seed = saved_tensors_[5]; + + const float scale = static_cast( + scale_.has_value() ? *scale_ : 1.0 / std::sqrt(static_cast(query->Dims().back()))); + + auto grad_tensors = Dispatcher::Instance().Call>>( + {device, "FlashAttentionBackward"}, query, key, value, output, grad_output, logsumexp, dropout_seed, attn_mask_, + scale, is_causal_, dropout_p_, enable_gqa_); + + return grad_tensors; + } + + // Fallback to traditional implementation for CPU or other devices + CHECK_EQ(saved_tensors_.size(), 4) << "Traditional backward expects 4 saved tensors"; + const auto &attn_prob = output; + const int64_t value_rank = static_cast(value->Dims().size()); + const int64_t key_rank = static_cast(key->Dims().size()); + const int64_t attn_rank = static_cast(attn_prob->Dims().size()); + + auto value_t = value->Transpose(value_rank - 2, value_rank - 1); + auto grad_attn_prob = grad_output->Matmul(value_t); + + // softmax backward: grad = (g - sum(g * y, dim=-1, keepdim=true)) * y + auto grad_dot = (grad_attn_prob * attn_prob)->Sum(-1, true); + auto grad_attn_scores = (grad_attn_prob - grad_dot) * attn_prob; + + const float scale = static_cast( + scale_.has_value() ? *scale_ : 1.0 / std::sqrt(static_cast(query->Dims().back()))); + if (scale != 1.0f) { + grad_attn_scores = grad_attn_scores * scale; + } + + auto grad_query = grad_attn_scores->Matmul(key); + + auto grad_attn_scores_t = grad_attn_scores->Transpose(attn_rank - 2, attn_rank - 1); + auto grad_key = grad_attn_scores_t->Matmul(query); + + auto attn_prob_t = attn_prob->Transpose(attn_rank - 2, attn_rank - 1); + auto grad_value = attn_prob_t->Matmul(grad_output); + + return {grad_query, grad_key, grad_value}; +} +} // namespace infini_train::autograd diff --git a/infini_train/src/kernels/cuda/flash_attention.cu b/infini_train/src/kernels/cuda/flash_attention.cu new file mode 100644 index 00000000..bdd86a27 --- /dev/null +++ b/infini_train/src/kernels/cuda/flash_attention.cu @@ -0,0 +1,1068 @@ +#include +#include + +#include "glog/logging.h" + +#include "infini_train/include/common/cuda/common_cuda.h" +#include "infini_train/include/common/cuda/kernel_helper.cuh" +#include "infini_train/include/core/runtime/device_guard.h" +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/kernels/cuda/flash_attention.h" +#include "infini_train/include/tensor.h" + +#include "infini_train/src/core/runtime/cuda/cuda_runtime_common.h" + +namespace infini_train::kernels::cuda { + +/** + * FlashAttention Forward Kernel + * + * This kernel implements the FlashAttention algorithm for efficient attention computation. + * It uses tiling and recomputation to reduce memory access and improve performance. + * + * Args: + * output: Output tensor of shape [batch_size, seq_len_q, num_heads, head_dim] + * query: Query tensor of shape [batch_size, seq_len_q, num_heads, head_dim] + * key: Key tensor of shape [batch_size, seq_len_k, num_heads_kv, head_dim] + * value: Value tensor of shape [batch_size, seq_len_k, num_heads_kv, head_dim] + * attn_mask: Optional attention mask tensor + * logsumexp: Save logsumexp for backward pass + * scale: Scaling factor for attention scores + * is_causal: Whether to apply causal masking + * enable_gqa: Whether to enable grouped-query attention + * dropout_p: Dropout probability + * dropout_seed: Random seed for dropout (for reproducibility) + * batch_size, target_seq_len, src_seq_len: Tensor dimensions + * q_heads, kv_heads, head_dim: Attention head dimensions + * + * Note: Actual kernel implementation is not included in this version. + */ +template +__device__ T warp_reduce_sum(T val){ +#pragma unroll//短循环自动展开,省去分支预测,提升效率 + for(int offset = 16; offset > 0; offset >>= 1){ + val += __shfl_down_sync(0xffffffff, val, offset); + } + return val; +} + +template +__device__ T myexp(T x) { + if constexpr(std::is_same::value) { + float fx = __half2float(x); + float result = expf(fx); + return __float2half(result); + } + else if constexpr(std::is_same::value) { + return expf(x); // expf返回float + } + else if constexpr(std::is_same::value) { + return exp(x); // exp返回double + } + else{//other types + return T(0); + } +} + +template +__device__ T warp_reduce_max(T val){ +#pragma unroll//短循环自动展开,省去分支预测,提升效率 + for(int offset = 16; offset > 0; offset >>= 1){ + T tmp = __shfl_down_sync(0xffffffff, val, offset); + val = (val > tmp) ? val : tmp; + } + return val; +} + + +/** + * FlashAttention Forward Kernel + * + * This kernel implements the FlashAttention algorithm for efficient attention computation. + * It uses tiling and recomputation to reduce memory access and improve performance. + * + * Args: + * output: Output tensor of shape [batch_size, seq_len_q, num_heads, head_dim] + * query: Query tensor of shape [batch_size, seq_len_q, num_heads, head_dim] + * key: Key tensor of shape [batch_size, seq_len_k, num_heads_kv, head_dim] + * value: Value tensor of shape [batch_size, seq_len_k, num_heads_kv, head_dim] + * attn_mask: Optional attention mask tensor + * logsumexp: Save logsumexp for backward pass + * scale: Scaling factor for attention scores + * is_causal: Whether to apply causal masking + * enable_gqa: Whether to enable grouped-query attention + * dropout_p: Dropout probability + * dropout_seed: Random seed for dropout (for reproducibility) + * batch_size, target_seq_len, src_seq_len: Tensor dimensions + * q_heads, kv_heads, head_dim: Attention head dimensions + * + * Note: Actual kernel implementation is not included in this version. + */ +template +__global__ void FlashAttentionForwardKernel( + T *output, const T *query, const T *key, const T *value, const T *attn_mask, + float *logsumexp, // Save logsumexp for backward pass + float scale, bool is_causal, bool enable_gqa, int64_t dropout_p, + unsigned long long dropout_seed, // Use dropout_seed instead of dropout_mask + int batch_size, int target_seq_len, int src_seq_len, + int q_heads, int kv_heads, int head_dim) { + + int tid_x = threadIdx.x; // 横向,blockDim.x列 (Bc) + int tid_y = threadIdx.y; // 纵向,blockDim.y行 (Br) + int bid_x = blockIdx.x; // x方向,总数 = #q_heads + int bid_y = blockIdx.y; // y方向,总数 = #batch + int bid_z = blockIdx.z; // z方向,总数 = Tr + const int p = q_heads / kv_heads; // 计算比例系数,GQA支持 + + const int Br = blockDim.y; // Q纵向每块大小, 32 + const int Bc = blockDim.x; // K/V纵向分块大小, 32 + const int Tc = gridDim.z; // 对应原始论文中K/V纵向分块数Tc,其中Bc = 32 + + // 定义一系列临时变量 + extern __shared__ char shared_mem[]; + char *ptr = shared_mem; + + // 计算中间变量,包括S, P(复用为SP), m_prev, m_new, l_prev, l_new + double *SP = reinterpret_cast(ptr); // double SP[Br][Bc] + ptr += Br * Bc * sizeof(double); + float *m_prev = reinterpret_cast(ptr); // float m_prev[Br] + ptr += Br * sizeof(float); + float *m_new = reinterpret_cast(ptr); // float m_new[Br] + ptr += Br * sizeof(float); + float *l_prev = reinterpret_cast(ptr); // float l_prev[Br] + ptr += Br * sizeof(float); + float *l_new = reinterpret_cast(ptr); // float l_new[Br] + ptr += Br * sizeof(float); + + // 原始数据QKV和计算结果O; 全采用float + float *Q_sm = reinterpret_cast(ptr); // float Q_sm[Br][head_dim] + ptr += Br * head_dim * sizeof(float); + float *K_T_sm = reinterpret_cast(ptr); // float K_T_sm[head_dim][Bc] + ptr += head_dim * Bc * sizeof(float); + float *V_sm = reinterpret_cast(ptr); // float V_sm[Bc][head_dim] + ptr += Bc * head_dim * sizeof(float); + float *O_sm = reinterpret_cast(ptr); // float O_sm[Br][head_dim] + // Note: Removed dropout_sm shared memory allocation + + // 定义访问宏 +#define SP_AT(y, x) SP[y * Bc + x] +#define Q_sm_AT(y, x) Q_sm[y * head_dim + x] +#define K_T_sm_AT(y, x) K_T_sm[y * Bc + x] +#define V_sm_AT(y, x) V_sm[y * head_dim + x] +#define O_sm_AT(y, x) O_sm[y * head_dim + x] + // Note: Removed DROPOUT_SM_AT macro + + /****************************preparation**************************/ + int bound_tid_y = min(Br, target_seq_len - Br * bid_z); + + // preparation-1: load Qi from GM to SM, and reset Oi to 0 + for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { + O_sm_AT(tid_y, idx) = 0.0; + Q_sm_AT(tid_y, idx) = 0.0; + if (tid_y < bound_tid_y) { + Q_sm_AT(tid_y, idx) + = float(query[((((bid_y * target_seq_len) + (Br * bid_z + tid_y)) * q_heads) + bid_x) * head_dim + idx]); + } + } + __syncthreads(); + + // preparation-2: reset m_prev to -INFINITY and l_prev to 0 + if (tid_x == 0) { + m_prev[tid_y] = -8192.0; + l_prev[tid_y] = 0.0; + } + __syncthreads(); + + // Initialize dropout random state with dropout_seed + curandStatePhilox4_32_10_t state; + if (dropout_p > 0) { + unsigned long long seq = bid_y * q_heads * target_seq_len + bid_x * target_seq_len + Br * bid_z + tid_y; + curand_init(dropout_seed, seq, 0, &state); + } +/****************************end-of-preparation*************************/ + +/****************************main-loop**************************/ +#pragma unroll 4 + for (int j = 0; j < Tc; ++j) { // 对于每个K/V分块 + bool skip = (is_causal && bid_z < j); + if (skip) { // early exit, 直接跳过 + __syncthreads(); + continue; + } + + SP_AT(tid_y, tid_x) = -8192.0; + __syncthreads(); + int bound_tid_x = min(Bc, src_seq_len - Bc * j); + bool is_compute = true; // optimization: 分支处理,加速branch-resolving + if (is_causal) { + if (bid_z < j) { + is_compute = false; // 早期退出情况 + } else if (bid_z == j) { + is_compute = (tid_y >= tid_x); // 对角线以上 + } + } + + // step-1: load Ki, Vi from GM to SM +#pragma unroll + for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { + K_T_sm_AT(idx, tid_y) = 0.0; + V_sm_AT(tid_y, idx) = 0.0; + if (tid_y < bound_tid_x) { + K_T_sm_AT(idx, tid_y) = float( + K[((((bid_y * src_seq_len) + (Bc * j + tid_y)) * kv_heads) + (bid_x / p)) * head_dim + idx]); + V_sm_AT(tid_y, idx) = float( + V[((((bid_y * src_seq_len) + (Bc * j + tid_y)) * kv_heads) + (bid_x / p)) * head_dim + idx]); + } + } + __syncthreads(); + + // step-2: S = Q @ K.T, point-wise + if (tid_y < bound_tid_y && tid_x < bound_tid_x) { + float val0 = 0.0; + if (is_compute) { +#pragma unroll + for (int k = 0; k < head_dim; ++k) { + val0 += Q_sm_AT(tid_y, k) * K_T_sm_AT(k, tid_x); + } + SP_AT(tid_y, tid_x) = double(val0) * scale; + } + } + __syncthreads(); + + // step-3: m_new = max(m_prev, rowMax(S)) + float val1 = float(SP_AT(tid_y, tid_x)); + val1 = warp_reduce_max(val1); + if (tid_x == 0 && tid_y < bound_tid_y) { + m_new[tid_y] = (val1 > m_prev[tid_y]) ? val1 : m_prev[tid_y]; + } + __syncthreads(); + + // step-4: P = exp(S - m_new), point-wise + if (tid_y < bound_tid_y && tid_x < bound_tid_x) { + if (is_compute) { + SP_AT(tid_y, tid_x) = myexp(SP_AT(tid_y, tid_x) - double(m_new[tid_y])); + } else { + SP_AT(tid_y, tid_x) = 0.0; + } + } else { + SP_AT(tid_y, tid_x) = 0.0; + } + __syncthreads(); + + // step-5: l_new = exp(m_prev - m_new) * l_prev + rowSum(P) + float val2 = float(SP_AT(tid_y, tid_x)); + val2 = warp_reduce_sum(val2); + float exp_result = myexp(m_prev[tid_y] - m_new[tid_y]); + if (tid_x == 0 && tid_y < bound_tid_y) { + l_new[tid_y] = exp_result * l_prev[tid_y] + val2; + } + __syncthreads(); + + // step-5.5: Apply dropout to P (using dropout_seed for reproducibility) + if (dropout_p > 0 && tid_y < bound_tid_y && tid_x < bound_tid_x) { + float rand_val = curand_uniform(&state); + bool keep = rand_val > dropout_p; + if (keep) { + SP_AT(tid_y, tid_x) = SP_AT(tid_y, tid_x) / (1.0f - dropout_p); + } else { + SP_AT(tid_y, tid_x) = 0.0; + } + // Note: Removed dropout_sm mask saving to reduce memory overhead + } + __syncthreads(); + + // step-6: O = 1/(exp(m_prev - m_new)) * O + P @ V + if (tid_x < bound_tid_x && tid_y < bound_tid_y) { + for (int u = tid_x; u < head_dim; u += blockDim.x) { + float val3 = 0.0; +#pragma unroll + for (int w = 0; w < Bc; ++w) { + val3 += float(SP_AT(tid_y, w)) * V_sm_AT(w, u); + } + O_sm_AT(tid_y, u) = O_sm_AT(tid_y, u) * exp_result + val3; + } + } + __syncthreads(); + + // step-7: m_prev <- m_new; l_prev <- l_new + if (tid_x == 0 && tid_y < bound_tid_y) { + m_prev[tid_y] = m_new[tid_y]; + l_prev[tid_y] = l_new[tid_y]; + } + __syncthreads(); + } +/****************************end-of-main-loop**************************/ + +/****************************post-process****************************/ +// O(GM) = O/l_prev, aka O_sm /= l_prev and write Oi from SM to GM +// Also save logsumexp for backward pass +#pragma unroll + for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { + if (tid_y < bound_tid_y) { + output[((((bid_y * target_seq_len) + (Br * bid_z + tid_y)) * q_heads) + bid_x) * head_dim + idx] + = T(O_sm_AT(tid_y, idx) / float(l_prev[tid_y])); + } + } + + // Save logsumexp for backward pass + if (tid_x == 0 && tid_y < bound_tid_y) { + int logsumexp_idx = ((bid_y * q_heads + bid_x) * target_seq_len) + (Br * bid_z + tid_y); + logsumexp[logsumexp_idx] = m_prev[tid_y] + log(l_prev[tid_y]); + } + __syncthreads(); +/****************************end-of-post-process****************************/ + +// 取消访问宏定义 +#undef SP_AT +#undef Q_sm_AT +#undef K_T_sm_AT +#undef V_sm_AT +#undef O_sm_AT +} + +/** + * FlashAttention Backward Kernel + * + * This kernel implements the backward pass for FlashAttention. + * It computes gradients for query, key, and value tensors. + * + * Args: + * grad_query: Gradient for query tensor + * grad_key: Gradient for key tensor + * grad_value: Gradient for value tensor + * query: Query tensor from forward pass + * key: Key tensor from forward pass + * value: Value tensor from forward pass + * output: Output tensor from forward pass + * grad_output: Gradient from upstream + * logsumexp: Logsumexp tensor from forward pass + * dropout_seed: Dropout seed for reproducibility + * attn_mask: Optional attention mask tensor + * scale: Scaling factor for attention scores + * is_causal: Whether causal masking was applied + * dropout_p: Dropout probability + * enable_gqa: Whether GQA was enabled + * batch_size, target_seq_len, src_seq_len: Tensor dimensions + * q_heads, kv_heads, head_dim: Attention head dimensions + */ +template +__global__ void FlashAttentionBackwardKernel( + T *grad_query, T *grad_key, T *grad_value, + const T *query, const T *key, const T *value, + const T *output, const T *grad_output, + const float *logsumexp, + const float *D, // Precomputed D = rowsum(dO ∘ O) + unsigned long long dropout_seed, + const T *attn_mask, + float scale, bool is_causal, int64_t dropout_p, + bool enable_gqa, + int batch_size, int target_seq_len, int src_seq_len, + int q_heads, int kv_heads, int head_dim) { + + // Grid/block dimensions: grid_dims(num_heads_q, batch_size, Tr), block_dim(Bc, Br) + // where Br corresponds to thread block row index, Bc corresponds to column index + int tid_x = threadIdx.x; // 横向, blockDim.x列 (Bc) + int tid_y = threadIdx.y; // 纵向, blockDim.y行 (Br) + int bid_x = blockIdx.x; // x方向, 总数 = #q_heads + int bid_y = blockIdx.y; // y方向, 总数 = #batch + int bid_z = blockIdx.z; // z方向, 总数 = Tr + const int p = q_heads / kv_heads; // GQA比例系数 + + const int Br = blockDim.y; // Q纵向每块大小, 32 + const int Bc = blockDim.x; // K/V纵向分块大小, 32 + const int Tr = gridDim.z; // Q纵向分块数 + const int Tc = (src_seq_len + Bc - 1) / Bc; // K/V纵向分块数 + + // Define shared memory + extern __shared__ char shared_mem[]; + char *ptr = shared_mem; + + // D_sm[Br] - D values loaded from HBM + float *D_sm = reinterpret_cast(ptr); + ptr += Br * sizeof(float); + + // Q_sm[Br][head_dim], K_T_sm[head_dim][Bc], V_sm[Bc][head_dim] + float *Q_sm = reinterpret_cast(ptr); + ptr += Br * head_dim * sizeof(float); + float *K_T_sm = reinterpret_cast(ptr); + ptr += head_dim * Bc * sizeof(float); + float *V_sm = reinterpret_cast(ptr); + ptr += Bc * head_dim * sizeof(float); + + // dO_sm[Br][head_dim], dK_T_sm[head_dim][Bc], dV_sm[Bc][head_dim] + float *dO_sm = reinterpret_cast(ptr); + ptr += Br * head_dim * sizeof(float); + float *dK_T_sm = reinterpret_cast(ptr); + ptr += head_dim * Bc * sizeof(float); + float *dV_sm = reinterpret_cast(ptr); + ptr += Bc * head_dim * sizeof(float); + + // S_sm[Br][Bc], P_sm[Br][Bc] + float *S_sm = reinterpret_cast(ptr); + ptr += Br * Bc * sizeof(float); + float *P_sm = reinterpret_cast(ptr); + + // L_sm[Br] - logsumexp values + float *L_sm = reinterpret_cast(ptr); + + // dQ_sm[Br][head_dim] - accumulated gradient for Q + float *dQ_sm = reinterpret_cast(ptr); + + // Define access macros + #define D_sm_AT(y) D_sm[y] + #define Q_sm_AT(y, x) Q_sm[y * head_dim + x] + #define K_T_sm_AT(y, x) K_T_sm[y * Bc + x] + #define V_sm_AT(y, x) V_sm[y * head_dim + x] + #define dO_sm_AT(y, x) dO_sm[y * head_dim + x] + #define dK_T_sm_AT(y, x) dK_T_sm[y * Bc + x] + #define dV_sm_AT(y, x) dV_sm[y * head_dim + x] + #define S_sm_AT(y, x) S_sm[y * Bc + x] + #define P_sm_AT(y, x) P_sm[y * Bc + x] + #define L_sm_AT(y) L_sm[y] + #define dQ_sm_AT(y, x) dQ_sm[y * head_dim + x] + + // Initialize dropout random state + curandStatePhilox4_32_10_t state; + if (dropout_p > 0) { + unsigned long long seq = bid_y * q_heads * target_seq_len + bid_x * target_seq_len + Br * bid_z + tid_y; + curand_init(dropout_seed, seq, 0, &state); + } + + /****************************Preparation*****************************/ + + // Initialize dQ_sm to 0 for accumulation + for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { + dQ_sm_AT(tid_y, idx) = 0.0f; + } + __syncthreads(); + + // Load D_i from HBM to shared memory + int q_idx = Br * bid_z + tid_y; // Global query position within this head + if (q_idx < target_seq_len) { + int d_idx = ((bid_y * q_heads + bid_x) * target_seq_len) + q_idx; + D_sm_AT(tid_y) = D[d_idx]; + } else { + D_sm_AT(tid_y) = 0.0f; // Padding for out-of-bounds positions + } + __syncthreads(); + + /****************************Main Loop - Outer Loop over K/V tiles*****************************/ + for (int j = 0; j < Tc; ++j) { // For each K/V column tile + + // Skip entire tile if causal and this tile is completely to the right + bool skip_tile = (is_causal && bid_z < j); + if (skip_tile) { + __syncthreads(); + continue; + } + + // Initialize dK_T_sm, dV_sm to 0 for this column tile + for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { + for (int y = 0; y < Bc; ++y) { + dK_T_sm_AT(idx, y) = 0.0f; + dV_sm_AT(y, idx) = 0.0f; + } + } + __syncthreads(); + + // Load K_j, V_j from HBM to shared memory + int bound_tid_x = min(Bc, src_seq_len - Bc * j); + for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { + K_T_sm_AT(idx, tid_y) = 0.0f; + V_sm_AT(tid_y, idx) = 0.0f; + if (tid_y < bound_tid_x) { + int kv_head_idx = bid_x / p; + K_T_sm_AT(idx, tid_y) = float( + key[((((bid_y * src_seq_len) + (Bc * j + tid_y)) * kv_heads + kv_head_idx) * head_dim + idx]); + V_sm_AT(tid_y, idx) = float( + value[((((bid_y * src_seq_len) + (Bc * j + tid_y)) * kv_heads + kv_head_idx) * head_dim + idx]); + } + } + __syncthreads(); + + /****************************Single Q tile for this block*****************************/ + // gridDim.z already indexes the Q row tile, so this block only handles i = bid_z. + const int i = bid_z; + int q_tile_start = Br * i; + int q_tile_end = min(q_tile_start + Br, target_seq_len); + int bound_tid_y = min(Br, q_tile_end - q_tile_start); + + if (tid_y < bound_tid_y) { + int global_q_idx = q_tile_start + tid_y; + int q_tensor_idx = ((bid_y * target_seq_len) + global_q_idx) * q_heads + bid_x; + + for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { + Q_sm_AT(tid_y, idx) = float(query[q_tensor_idx * head_dim + idx]); + dO_sm_AT(tid_y, idx) = float(grad_output[q_tensor_idx * head_dim + idx]); + } + if (tid_x == 0) { + L_sm_AT(tid_y) = logsumexp[((bid_y * q_heads + bid_x) * target_seq_len) + global_q_idx]; + } + } + __syncthreads(); + + // Get D_i for this row. + float D_i_row = D_sm_AT(tid_y); + + // Recompute S_ij = Q_i @ K_j^T, apply causal mask at element level if needed. + #pragma unroll + for (int y = 0; y < Bc; ++y) { + for (int x = 0; x < Bc; ++x) { + float val = 0.0f; + if (tid_y < bound_tid_y && tid_x < bound_tid_x) { + bool is_compute = true; + if (is_causal && i == j) { + int global_q_pos = q_tile_start + tid_y; + int global_k_pos = Bc * j + x; + is_compute = (global_q_pos >= global_k_pos); + } + + if (is_compute) { + #pragma unroll + for (int k = 0; k < head_dim; ++k) { + val += Q_sm_AT(tid_y, k) * K_T_sm_AT(k, y); + } + } + } + S_sm_AT(tid_y, x) = val * scale; + } + } + __syncthreads(); + + // Recompute P_ij = exp(S_ij - L_i). + #pragma unroll + for (int y = 0; y < Bc; ++y) { + for (int x = 0; x < Bc; ++x) { + if (tid_y < bound_tid_y && tid_x < bound_tid_x) { + bool is_compute = true; + if (is_causal && i == j) { + int global_q_pos = q_tile_start + tid_y; + int global_k_pos = Bc * j + x; + is_compute = (global_q_pos >= global_k_pos); + } + + if (is_compute) { + P_sm_AT(tid_y, x) = myexp(S_sm_AT(tid_y, x) - L_sm_AT(tid_y)); + } else { + P_sm_AT(tid_y, x) = 0.0f; + } + } else { + P_sm_AT(tid_y, x) = 0.0f; + } + } + } + __syncthreads(); + + // Apply dropout to P_ij. + if (dropout_p > 0) { + #pragma unroll + for (int y = 0; y < Bc; ++y) { + for (int x = 0; x < Bc; ++x) { + if (tid_y < bound_tid_y && tid_x < bound_tid_x) { + float rand_val = curand_uniform(&state); + bool keep = rand_val > dropout_p; + if (keep) { + P_sm_AT(tid_y, x) = P_sm_AT(tid_y, x) / (1.0f - dropout_p); + } else { + P_sm_AT(tid_y, x) = 0.0f; + } + } + } + } + __syncthreads(); + } + + // Compute dV_j += P_ij^T @ dO_i. + if (tid_y < bound_tid_y) { + #pragma unroll + for (int x = 0; x < head_dim; x += blockDim.x) { + float val = 0.0f; + #pragma unroll + for (int y = 0; y < Bc; ++y) { + val += P_sm_AT(tid_y, y) * dO_sm_AT(tid_y, x); + } + atomicAdd(&dV_sm_AT(y, x), val); + } + } + __syncthreads(); + + // Compute dP_ij = dO_i @ V_j^T. + #pragma unroll + for (int y = 0; y < Bc; ++y) { + for (int x = 0; x < Bc; ++x) { + float val = 0.0f; + if (tid_y < bound_tid_y && tid_x < bound_tid_x) { + #pragma unroll + for (int k = 0; k < head_dim; ++k) { + val += dO_sm_AT(tid_y, k) * V_sm_AT(y, k); + } + S_sm_AT(tid_y, x) = val; // Reuse S_sm as temporary storage for dP_ij + } else { + S_sm_AT(tid_y, x) = 0.0f; + } + } + } + __syncthreads(); + + // Apply dropout to dP_ij. + if (dropout_p > 0) { + #pragma unroll + for (int y = 0; y < Bc; ++y) { + for (int x = 0; x < Bc; ++x) { + if (tid_y < bound_tid_y && tid_x < bound_tid_x) { + float rand_val = curand_uniform(&state); + bool keep = rand_val > dropout_p; + if (keep) { + S_sm_AT(tid_y, x) = S_sm_AT(tid_y, x) / (1.0f - dropout_p); + } else { + S_sm_AT(tid_y, x) = 0.0f; + } + } + } + } + __syncthreads(); + } + + // Compute dS_ij = P_ij * (dP_ij - D_i). + #pragma unroll + for (int y = 0; y < Bc; ++y) { + for (int x = 0; x < Bc; ++x) { + if (tid_y < bound_tid_y && tid_x < bound_tid_x) { + S_sm_AT(tid_y, x) = P_sm_AT(tid_y, x) * (S_sm_AT(tid_y, x) - D_i_row); + } else { + S_sm_AT(tid_y, x) = 0.0f; + } + } + } + __syncthreads(); + + // Compute dK_j += dS_ij^T @ Q_i. + if (tid_x < head_dim) { + #pragma unroll + for (int y = 0; y < Bc; ++y) { + float val = 0.0f; + #pragma unroll + for (int x = 0; x < Br; ++x) { + val += S_sm_AT(x, y) * Q_sm_AT(x, tid_x); + } + atomicAdd(&dK_T_sm_AT(tid_x, y), val * scale); + } + } + __syncthreads(); + + // Compute dQ_i += dS_ij @ K_j. + if (tid_y < bound_tid_y) { + #pragma unroll + for (int x = 0; x < head_dim; x += blockDim.x) { + float val = 0.0f; + #pragma unroll + for (int y = 0; y < Bc; ++y) { + val += S_sm_AT(tid_y, y) * K_T_sm_AT(x, y); + } + dQ_sm_AT(tid_y, x) += val * scale; + } + } + __syncthreads(); + + // Write back dK_j, dV_j to HBM + // Note: For GQA, dK_j and dV_j need to be accumulated across multiple q-head blocks + int kv_head_idx = bid_x / p; + int k_tile_start = Bc * j; + int k_tile_end = min(k_tile_start + Bc, src_seq_len); + + for (int y = 0; y < Bc; ++y) { + int global_k_idx = k_tile_start + y; + if (global_k_idx < src_seq_len) { + for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { + // dK: [batch_size, src_seq_len, kv_heads, head_dim] + int k_tensor_idx = ((bid_y * src_seq_len) + global_k_idx) * kv_heads + kv_head_idx; + atomicAdd(&grad_key[k_tensor_idx * head_dim + idx], T(dK_T_sm_AT(idx, y))); + + // dV: [batch_size, src_seq_len, kv_heads, head_dim] + atomicAdd(&grad_value[k_tensor_idx * head_dim + idx], T(dV_sm_AT(y, idx))); + } + } + } + } // End of outer loop over K/V tiles + + // Write back dQ_i to HBM + int q_tile_start = Br * bid_z; + int q_tile_end = min(q_tile_start + Br, target_seq_len); + int bound_tid_y = min(Br, q_tile_end - q_tile_start); + + if (tid_y < bound_tid_y) { + int global_q_idx = q_tile_start + tid_y; + int q_tensor_idx = ((bid_y * target_seq_len) + global_q_idx) * q_heads + bid_x; + + for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { + // dQ: [batch_size, target_seq_len, q_heads, head_dim] + grad_query[q_tensor_idx * head_dim + idx] += T(dQ_sm_AT(tid_y, idx)); + } + } + + // Undefine access macros + #undef D_sm_AT + #undef Q_sm_AT + #undef K_T_sm_AT + #undef V_sm_AT + #undef dO_sm_AT + #undef dK_T_sm_AT + #undef dV_sm_AT + #undef S_sm_AT + #undef P_sm_AT + #undef L_sm_AT + #undef dQ_sm_AT +} + +/** + * FlashAttention Forward Function + * + * This is the main entry point for FlashAttention forward computation. + * It creates the output tensor and launches the appropriate kernel based on data type. + * + * Args: + * query: Query tensor of shape [batch_size, seq_len_q, num_heads, head_dim] + * key: Key tensor of shape [batch_size, seq_len_k, num_heads_kv, head_dim] + * value: Value tensor of shape [batch_size, seq_len_k, num_heads_kv, head_dim] + * attn_mask: Optional attention mask tensor + * scale: Scaling factor for attention scores + * is_causal: Whether to apply causal masking + * dropout_p: Dropout probability + * enable_gqa: Whether to enable grouped-query attention + * + * Returns: + * Output tensor of shape [batch_size, seq_len_q, num_heads, head_dim] + * logsumexp: Logsumexp tensor for backward pass [batch_size, num_heads, seq_len_q] + * dropout_seed: Dropout seed for backward pass [1] + */ + + +/** + * Launch FlashAttention Forward Kernel + * + * This function sets up grid and block dimensions and launches FlashAttention forward kernel. + * + * Args: + * output: Output tensor + * query: Query tensor + * key: Key tensor + * value: Value tensor + * attn_mask: Optional attention mask tensor + * scale: Scaling factor + * is_causal: Whether to apply causal masking + * dropout_p: Dropout probability + * enable_gqa: Whether to enable GQA + */ +template +void LaunchFlashAttentionForward(const std::shared_ptr &output, const std::shared_ptr &query, + const std::shared_ptr &key, const std::shared_ptr &value, + const std::shared_ptr &attn_mask, float scale, bool is_causal, + int64_t dropout_p, bool enable_gqa, float *logsumexp_ptr, unsigned long long *dropout_seed_ptr) { + + const auto &query_dims = query->Dims(); + const auto &key_dims = key->Dims(); + const auto &value_dims = value->Dims(); + + // Expected shapes: + // query: [batch_size, seq_len_q, num_heads, head_dim] + // key: [batch_size, seq_len_k, num_heads_kv, head_dim] + // value: [batch_size, seq_len_k, num_heads_kv, head_dim] + // output: [batch_size, seq_len_q, num_heads, head_dim] + + int64_t batch_size = query_dims[0]; + int64_t seq_len_q = query_dims[1]; + int64_t num_heads = query_dims[2]; + int64_t head_dim = query_dims[3]; + int64_t seq_len_k = key_dims[1]; + int64_t num_heads_kv = key_dims[2]; + + CHECK_EQ(key_dims[3], head_dim) << "Key head dimension must match query head dimension"; + CHECK_EQ(value_dims[3], head_dim) << "Value head dimension must match query head dimension"; + CHECK_EQ(value_dims[1], seq_len_k) << "Value sequence length must match key sequence length"; + CHECK_EQ(value_dims[2], num_heads_kv) << "Value number of KV heads must match key"; + + if (enable_gqa) { + CHECK(num_heads % num_heads_kv == 0) << "Number of query heads must be divisible by number of KV heads for GQA"; + } else { + CHECK_EQ(num_heads, num_heads_kv) << "Number of query and KV heads must match for standard attention"; + } + + T *output_ptr = static_cast(output->DataPtr()); + const T *query_ptr = static_cast(query->DataPtr()); + const T *key_ptr = static_cast(key->DataPtr()); + const T *value_ptr = static_cast(value->DataPtr()); + const T *attn_mask_ptr = attn_mask ? static_cast(attn_mask->DataPtr()) : nullptr; + + // Set up grid and block dimensions according to FlashAttention v2 + // block_dim(Br, Bc) where Br = Bc = 32 + // grid_dim(query_heads, batch_size, Tr) where Tr = ceil(seq_len_q / Br) + constexpr int Br = 32; + constexpr int Bc = 32; + int64_t Tr = (seq_len_q + Br - 1) / Br; + + dim3 block_dims(Bc, Br); // (blockDim.x, blockDim.y) = (Bc, Br) + dim3 grid_dims(num_heads, batch_size, Tr); // (gridDim.x, gridDim.y, gridDim.z) = (num_heads, batch_size, Tr) + + // Calculate shared memory size (removed dropout_sm allocation) + // SP[Br][Bc] (double) + m_prev[Br] (float) + m_new[Br] (float) + l_prev[Br] (float) + l_new[Br] (float) + // + Q_sm[Br][head_dim] (float) + K_T_sm[head_dim][Bc] (float) + V_sm[Bc][head_dim] (float) + O_sm[Br][head_dim] (float) + size_t shared_mem_size = Br * Bc * sizeof(double) // SP + + 4 * Br * sizeof(float) // m_prev, m_new, l_prev, l_new + + (Br + Bc + Bc + Br) * head_dim) * sizeof(float); // Q_sm, K_T_sm, V_sm, O_sm + // Note: Removed dropout_sm[Br][Bc] (bool) allocation + + auto device = output->GetDevice(); + const auto &cuda_stream = dynamic_cast( + infini_train::core::GetDeviceGuardImpl(device.type())->GetStream(device)) + ->cuda_stream(); + + FlashAttentionForwardKernel<<>>( + output_ptr, query_ptr, key_ptr, value_ptr, attn_mask_ptr, + logsumexp_ptr, scale, is_causal, enable_gqa, dropout_p, dropout_seed, + batch_size, seq_len_q, seq_len_k, num_heads, num_heads_kv, head_dim); +} + +/** + * Launch FlashAttention Backward Kernel + * + * This function sets up grid and block dimensions and launches FlashAttention backward kernel. + * + * Args: + * grad_query: Gradient tensor for query + * grad_key: Gradient tensor for key + * grad_value: Gradient tensor for value + * query: Query tensor from forward pass + * key: Key tensor from forward pass + * value: Value tensor from forward pass + * output: Output tensor from forward pass + * grad_output: Gradient from upstream + * logsumexp: Logsumexp tensor from forward pass + * dropout_seed: Dropout seed for reproducibility + * attn_mask: Optional attention mask tensor + * scale: Scaling factor + * is_causal: Whether causal masking was applied + * dropout_p: Dropout probability + * enable_gqa: Whether GQA was enabled + */ +template +void LaunchFlashAttentionBackward(const std::shared_ptr &grad_query, const std::shared_ptr &grad_key, + const std::shared_ptr &grad_value, const std::shared_ptr &query, + const std::shared_ptr &key, const std::shared_ptr &value, + const std::shared_ptr &output, const std::shared_ptr &grad_output, + const std::shared_ptr &logsumexp, + unsigned long long dropout_seed, + const std::shared_ptr &attn_mask, + float scale, bool is_causal, int64_t dropout_p, bool enable_gqa) { + + const auto &query_dims = query->Dims(); + const auto &key_dims = key->Dims(); + + // Expected shapes: + // query: [batch_size, seq_len_q, num_heads, head_dim] + // key: [batch_size, seq_len_k, num_heads_kv, head_dim] + // value: [batch_size, seq_len_k, num_heads_kv, head_dim] + // output: [batch_size, seq_len_q, num_heads, head_dim] + + int64_t batch_size = query_dims[0]; + int64_t seq_len_q = query_dims[1]; + int64_t num_heads = query_dims[2]; + int64_t head_dim = query_dims[3]; + int64_t seq_len_k = key_dims[1]; + int64_t num_heads_kv = key_dims[2]; + + CHECK_EQ(key_dims[3], head_dim) << "Key head dimension must match query head dimension"; + CHECK_EQ(value->Dims()[3], head_dim) << "Value head dimension must match query head dimension"; + CHECK_EQ(value->Dims()[1], seq_len_k) << "Value sequence length must match key sequence length"; + CHECK_EQ(value->Dims()[2], num_heads_kv) << "Value number of KV heads must match key"; + + if (enable_gqa) { + CHECK(num_heads % num_heads_kv == 0) << "Number of query heads must be divisible by number of KV heads for GQA"; + } else { + CHECK_EQ(num_heads, num_heads_kv) << "Number of query and KV heads must match for standard attention"; + } + + // Precompute D = rowsum(dO ∘ O) before main backward loop + // D shape: [batch_size, seq_len_q, num_heads] + // dO shape: [batch_size, seq_len_q, num_heads, head_dim] + // O shape: [batch_size, seq_len_q, num_heads, head_dim] + auto D = function::Sum(grad_output * output, 3, false); + + T *grad_query_ptr = static_cast(grad_query->DataPtr()); + T *grad_key_ptr = static_cast(grad_key->DataPtr()); + T *grad_value_ptr = static_cast(grad_value->DataPtr()); + const T *query_ptr = static_cast(query->DataPtr()); + const T *key_ptr = static_cast(key->DataPtr()); + const T *value_ptr = static_cast(value->DataPtr()); + const T *output_ptr = static_cast(output->DataPtr()); + const T *grad_output_ptr = static_cast(grad_output->DataPtr()); + const float *logsumexp_ptr = static_cast(logsumexp->DataPtr()); + const float *D_ptr = static_cast(D->DataPtr()); + const T *attn_mask_ptr = attn_mask ? static_cast(attn_mask->DataPtr()) : nullptr; + + // Set up grid and block dimensions according to FlashAttention v2 backward + // block_dim(Bc, Br) where Bc = Br = 32 + // grid_dims(num_heads_q, batch_size, Tr) where Tr = ceil(seq_len_q / Br) + constexpr int Br = 32; + constexpr int Bc = 32; + int64_t Tr = (seq_len_q + Br - 1) / Br; + + dim3 block_dims(Bc, Br); // (blockDim.x, blockDim.y) = (Bc, Br) + dim3 grid_dims(num_heads, batch_size, Tr); // (gridDim.x, gridDim.y, gridDim.z) = (num_heads, batch_size, Tr) + + // Calculate shared memory size for backward pass + // Q_sm[Br][head_dim], K_T_sm[head_dim][Bc], V_sm[Bc][head_dim] + // dO_sm[Br][head_dim], dK_T_sm[head_dim][Bc], dV_sm[Bc][head_dim] + // S_sm[Br][Bc], P_sm[Br][Bc] + // D_sm[Br] - D values loaded from HBM to shared memory + size_t shared_mem_size = Br * head_dim * sizeof(float) // Q_sm + + head_dim * Bc * sizeof(float) // K_T_sm + + Bc * head_dim * sizeof(float) // V_sm + + Br * head_dim * sizeof(float) // dO_sm + + head_dim * Bc * sizeof(float) // dK_T_sm + + Bc * head_dim * sizeof(float) // dV_sm + + Br * Bc * sizeof(float) // S_sm or (dP_sm when compute dP_sm = dO_i @ V_j^T \in R^{Br*Bc}) + + Br * Bc * sizeof(float) // P_sm or (dS_sm when compute dS_sm = P_sm_ij pointwise multiplied by (dP_sm_ij - D_i) \in R^{Br*Bc}) + + Br * sizeof(float) // L_i + + Br * sizeof(float); // D_sm (loaded from HBM) + + auto device = query->GetDevice(); + const auto &cuda_stream = dynamic_cast( + infini_train::core::GetDeviceGuardImpl(device.type())->GetStream(device)) + ->cuda_stream(); + + FlashAttentionBackwardKernel<<>>( + grad_query_ptr, grad_key_ptr, grad_value_ptr, + query_ptr, key_ptr, value_ptr, output_ptr, grad_output_ptr, + logsumexp_ptr, D_ptr, dropout_seed, attn_mask_ptr, + scale, is_causal, dropout_p, enable_gqa, + batch_size, seq_len_q, seq_len_k, num_heads, num_heads_kv, head_dim); +} + +/** + * FlashAttention Backward Function + * + * This is the main entry point for FlashAttention backward computation. + * It creates gradient tensors and launches the appropriate kernel based on data type. + * + * Args: + * grad_query: Gradient tensor for query + * grad_key: Gradient tensor for key + * grad_value: Gradient tensor for value + * query: Query tensor from forward pass + * key: Key tensor from forward pass + * value: Value tensor from forward pass + * output: Output tensor from forward pass + * grad_output: Gradient from upstream + * attn_mask: Optional attention mask tensor + * scale: Scaling factor + * is_causal: Whether causal masking was applied + * dropout_p: Dropout probability (not implemented) + * enable_gqa: Whether GQA was enabled + * + * Returns: + * Tuple of (grad_query, grad_key, grad_value) tensors + */ + + +// Non-template wrapper functions for registration +FlashAttentionForwardOutput FlashAttentionForward(const std::shared_ptr &query, const std::shared_ptr &key, + const std::shared_ptr &value, + const std::shared_ptr &attn_mask, float scale, bool is_causal, + int64_t dropout_p, bool enable_gqa) { + auto dtype = query->Dtype(); + const auto &query_dims = query->Dims(); + + // Output shape: [batch_size, seq_len_q, num_heads, head_dim] + std::vector output_dims = {query_dims[0], query_dims[1], query_dims[2], query_dims[3]}; + auto output = std::make_shared(output_dims, dtype, query->GetDevice()); + + // Allocate logsumexp tensor for backward pass + // Shape: [batch_size, num_heads, seq_len_q] + std::vector logsumexp_dims = {query_dims[0], query_dims[2], query_dims[1]}; + auto logsumexp = std::make_shared(logsumexp_dims, DataType::kFLOAT32, query->GetDevice()); + float *logsumexp_ptr = static_cast(logsumexp->DataPtr()); + + // Allocate dropout_seed tensor for backward pass + // Shape: [1] + unsigned long long dropout_seed = 0; + std::shared_ptr dropout_seed_tensor; + if (dropout_p > 0) { + std::vector dropout_seed_dims = {1}; + dropout_seed_tensor = std::make_shared(dropout_seed_dims, DataType::kUINT64, query->GetDevice()); + dropout_seed = static_cast(std::time(nullptr)); + unsigned long long *dropout_seed_ptr = static_cast(dropout_seed_tensor->DataPtr()); + *dropout_seed_ptr = dropout_seed; + } + + switch (dtype) { + DISPATCH_CASE(WRAP(LaunchFlashAttentionForward(output, query, key, value, attn_mask, scale, is_causal, + dropout_p, enable_gqa, logsumexp_ptr, + dropout_seed_tensor ? static_cast(dropout_seed_tensor->DataPtr()) : nullptr);), + DataType::kFLOAT32) + DISPATCH_CASE(WRAP(LaunchFlashAttentionForward(output, query, key, value, attn_mask, scale, + is_causal, dropout_p, enable_gqa, logsumexp_ptr, + dropout_seed_tensor ? static_cast(dropout_seed_tensor->DataPtr()) : nullptr);), + DataType::kBFLOAT16) + default: + LOG_LOC(FATAL, "CUDA FlashAttention forward: 'Unsupported data type'"); + } + + FlashAttentionForwardOutput result; + result.output = output; + result.logsumexp = logsumexp; + result.dropout_seed = dropout_seed_tensor; + return result; +} + +std::vector> FlashAttentionBackward(const std::shared_ptr &grad_query, const std::shared_ptr &grad_key, + const std::shared_ptr &grad_value, const std::shared_ptr &query, + const std::shared_ptr &key, const std::shared_ptr &value, + const std::shared_ptr &output, const std::shared_ptr &grad_output, + const std::shared_ptr &logsumexp, + const std::shared_ptr &dropout_seed, + const std::shared_ptr &attn_mask, + float scale, bool is_causal, int64_t dropout_p, bool enable_gqa) { + auto dtype = query->Dtype(); + + // Create gradient tensors with same shapes as inputs + auto grad_query = std::make_shared(query->Dims(), dtype, query->GetDevice()); + auto grad_key = std::make_shared(key->Dims(), dtype, key->GetDevice()); + auto grad_value = std::make_shared(value->Dims(), dtype, value->GetDevice()); + + // Initialize gradients to zero + DispatchFunc(dtype, [=]() { grad_query->Fill(0); }, "CUDA FlashAttentionBackward"); + DispatchFunc(dtype, [=]() { grad_key->Fill(0); }, "CUDA FlashAttentionBackward"); + DispatchFunc(dtype, [=]() { grad_value->Fill(0); }, "CUDA FlashAttentionBackward"); + + // Get dropout seed value + unsigned long long dropout_seed_value = 0; + if (dropout_seed) { + dropout_seed_value = *static_cast(dropout_seed->DataPtr()); + } + + switch (dtype) { + DISPATCH_CASE(WRAP(LaunchFlashAttentionBackward(grad_query, grad_key, grad_value, query, key, value, output, grad_output, + logsumexp, dropout_seed_value, attn_mask, scale, is_causal, + dropout_p, enable_gqa);), + DataType::kFLOAT32) + DISPATCH_CASE(WRAP(LaunchFlashAttentionBackward(grad_query, grad_key, grad_value, query, key, value, output, grad_output, + logsumexp, dropout_seed_value, attn_mask, scale, is_causal, + dropout_p, enable_gqa);), + DataType::kBFLOAT16) + default: + LOG_LOC(FATAL, "CUDA FlashAttention backward: 'Unsupported data type'"); + } + + return {grad_query, grad_key, grad_value}; +} + +} // namespace infini_train::kernels::cuda + +// Register FlashAttention kernels with the dispatcher +#define REGISTER_CUDA_FLASHATTENTION_KERNEL(kernel_name) \ + REGISTER_KERNEL(infini_train::Device::DeviceType::kCUDA, kernel_name, infini_train::kernels::cuda::kernel_name) + +REGISTER_CUDA_FLASHATTENTION_KERNEL(FlashAttentionForward) +REGISTER_CUDA_FLASHATTENTION_KERNEL(FlashAttentionBackward) + +#undef REGISTER_CUDA_FLASHATTENTION_KERNEL diff --git a/infini_train/src/kernels/cuda/flash_attention1.cu b/infini_train/src/kernels/cuda/flash_attention1.cu new file mode 100644 index 00000000..75931887 --- /dev/null +++ b/infini_train/src/kernels/cuda/flash_attention1.cu @@ -0,0 +1,1171 @@ +#include +#include + +#include "glog/logging.h" + +#include "infini_train/include/common/cuda/common_cuda.h" +#include "infini_train/include/common/cuda/kernel_helper.cuh" +#include "infini_train/include/core/runtime/device_guard.h" +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/kernels/cuda/flash_attention.h" +#include "infini_train/include/tensor.h" + +#include "infini_train/src/core/runtime/cuda/cuda_runtime_common.h" + +namespace infini_train::kernels::cuda { + +/** + * FlashAttention Forward Kernel + * + * This kernel implements the FlashAttention algorithm for efficient attention computation. + * It uses tiling and recomputation to reduce memory access and improve performance. + * + * Args: + * output: Output tensor of shape [batch_size, seq_len_q, num_heads, head_dim] + * query: Query tensor of shape [batch_size, seq_len_q, num_heads, head_dim] + * key: Key tensor of shape [batch_size, seq_len_k, num_heads_kv, head_dim] + * value: Value tensor of shape [batch_size, seq_len_k, num_heads_kv, head_dim] + * attn_mask: Optional attention mask tensor + * logsumexp: Save logsumexp for backward pass + * scale: Scaling factor for attention scores + * is_causal: Whether to apply causal masking + * enable_gqa: Whether to enable grouped-query attention + * dropout_p: Dropout probability + * dropout_seed: Random seed for dropout (for reproducibility) + * batch_size, target_seq_len, src_seq_len: Tensor dimensions + * q_heads, kv_heads, head_dim: Attention head dimensions + * + * Note: Actual kernel implementation is not included in this version. + */ +template +__device__ T warp_reduce_sum(T val){ +#pragma unroll//短循环自动展开,省去分支预测,提升效率 + for(int offset = 16; offset > 0; offset >>= 1){ + val += __shfl_down_sync(0xffffffff, val, offset); + } + return val; +} + +template +__device__ T myexp(T x) { + if constexpr(std::is_same::value) { + float fx = __half2float(x); + float result = expf(fx); + return __float2half(result); + } + else if constexpr(std::is_same::value) { + return expf(x); // expf返回float + } + else if constexpr(std::is_same::value) { + return exp(x); // exp返回double + } + else{//other types + return T(0); + } +} + +template +__device__ T warp_reduce_max(T val){ +#pragma unroll//短循环自动展开,省去分支预测,提升效率 + for(int offset = 16; offset > 0; offset >>= 1){ + T tmp = __shfl_down_sync(0xffffffff, val, offset); + val = (val > tmp) ? val : tmp; + } + return val; +} + + +/** + * FlashAttention Forward Kernel + * + * This kernel implements the FlashAttention algorithm for efficient attention computation. + * It uses tiling and recomputation to reduce memory access and improve performance. + * + * Args: + * output: Output tensor of shape [batch_size, seq_len_q, num_heads, head_dim] + * query: Query tensor of shape [batch_size, seq_len_q, num_heads, head_dim] + * key: Key tensor of shape [batch_size, seq_len_k, num_heads_kv, head_dim] + * value: Value tensor of shape [batch_size, seq_len_k, num_heads_kv, head_dim] + * attn_mask: Optional attention mask tensor + * logsumexp: Save logsumexp for backward pass + * scale: Scaling factor for attention scores + * is_causal: Whether to apply causal masking + * enable_gqa: Whether to enable grouped-query attention + * dropout_p: Dropout probability + * dropout_seed: Random seed for dropout (for reproducibility) + * batch_size, target_seq_len, src_seq_len: Tensor dimensions + * q_heads, kv_heads, head_dim: Attention head dimensions + * + * Note: Actual kernel implementation is not included in this version. + */ +template +__global__ void FlashAttentionForwardKernel( + T *output, const T *query, const T *key, const T *value, const T *attn_mask, + float *logsumexp, // Save logsumexp for backward pass + float scale, bool is_causal, bool enable_gqa, int64_t dropout_p, + unsigned long long dropout_seed, // Use dropout_seed instead of dropout_mask + int batch_size, int target_seq_len, int src_seq_len, + int q_heads, int kv_heads, int head_dim) { + + int tid_x = threadIdx.x; // 横向,blockDim.x列 (Bc) + int tid_y = threadIdx.y; // 纵向,blockDim.y行 (Br) + int bid_x = blockIdx.x; // x方向,总数 = #q_heads + int bid_y = blockIdx.y; // y方向,总数 = #batch + int bid_z = blockIdx.z; // z方向,总数 = Tr + const int p = q_heads / kv_heads; // 计算比例系数,GQA支持 + + const int Br = blockDim.y; // Q纵向每块大小, 32 + const int Bc = blockDim.x; // K/V纵向分块大小, 32 + const int Tc = gridDim.z; // 对应原始论文中K/V纵向分块数Tc,其中Bc = 32 + + // 定义一系列临时变量 + extern __shared__ char shared_mem[]; + char *ptr = shared_mem; + + // 计算中间变量,包括S, P(复用为SP), m_prev, m_new, l_prev, l_new + double *SP = reinterpret_cast(ptr); // double SP[Br][Bc] + ptr += Br * Bc * sizeof(double); + float *m_prev = reinterpret_cast(ptr); // float m_prev[Br] + ptr += Br * sizeof(float); + float *m_new = reinterpret_cast(ptr); // float m_new[Br] + ptr += Br * sizeof(float); + float *l_prev = reinterpret_cast(ptr); // float l_prev[Br] + ptr += Br * sizeof(float); + float *l_new = reinterpret_cast(ptr); // float l_new[Br] + ptr += Br * sizeof(float); + + // 原始数据QKV和计算结果O; 全采用float + float *Q_sm = reinterpret_cast(ptr); // float Q_sm[Br][head_dim] + ptr += Br * head_dim * sizeof(float); + float *K_T_sm = reinterpret_cast(ptr); // float K_T_sm[head_dim][Bc] + ptr += head_dim * Bc * sizeof(float); + float *V_sm = reinterpret_cast(ptr); // float V_sm[Bc][head_dim] + ptr += Bc * head_dim * sizeof(float); + float *O_sm = reinterpret_cast(ptr); // float O_sm[Br][head_dim] + // Note: Removed dropout_sm shared memory allocation + + // 定义访问宏 +#define SP_AT(y, x) SP[y * Bc + x] +#define Q_sm_AT(y, x) Q_sm[y * head_dim + x] +#define K_T_sm_AT(y, x) K_T_sm[y * Bc + x] +#define V_sm_AT(y, x) V_sm[y * head_dim + x] +#define O_sm_AT(y, x) O_sm[y * head_dim + x] + // Note: Removed DROPOUT_SM_AT macro + + /****************************preparation**************************/ + int bound_tid_y = min(Br, target_seq_len - Br * bid_z); + + // preparation-1: load Qi from GM to SM, and reset Oi to 0 + for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { + O_sm_AT(tid_y, idx) = 0.0; + Q_sm_AT(tid_y, idx) = 0.0; + if (tid_y < bound_tid_y) { + Q_sm_AT(tid_y, idx) + = float(query[((((bid_y * target_seq_len) + (Br * bid_z + tid_y)) * q_heads) + bid_x) * head_dim + idx]); + } + } + __syncthreads(); + + // preparation-2: reset m_prev to -INFINITY and l_prev to 0 + if (tid_x == 0) { + m_prev[tid_y] = -8192.0; + l_prev[tid_y] = 0.0; + } + __syncthreads(); + + // Initialize dropout random state with dropout_seed + curandStatePhilox4_32_10_t state; + if (dropout_p > 0) { + unsigned long long seq = bid_y * q_heads * target_seq_len + bid_x * target_seq_len + Br * bid_z + tid_y; + curand_init(dropout_seed, seq, 0, &state); + } +/****************************end-of-preparation*************************/ + +/****************************main-loop**************************/ +#pragma unroll 4 + for (int j = 0; j < Tc; ++j) { // 对于每个K/V分块 + bool skip = (is_causal && bid_z < j); + if (skip) { // early exit, 直接跳过 + __syncthreads(); + continue; + } + + SP_AT(tid_y, tid_x) = -8192.0; + __syncthreads(); + int bound_tid_x = min(Bc, src_seq_len - Bc * j); + bool is_compute = true; // optimization: 分支处理,加速branch-resolving + if (is_causal) { + if (bid_z < j) { + is_compute = false; // 早期退出情况 + } else if (bid_z == j) { + is_compute = (tid_y >= tid_x); // 对角线以上 + } + } + + // step-1: load Ki, Vi from GM to SM +#pragma unroll + for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { + K_T_sm_AT(idx, tid_y) = 0.0; + V_sm_AT(tid_y, idx) = 0.0; + if (tid_y < bound_tid_x) { + K_T_sm_AT(idx, tid_y) = float( + K[((((bid_y * src_seq_len) + (Bc * j + tid_y)) * kv_heads) + (bid_x / p)) * head_dim + idx]); + V_sm_AT(tid_y, idx) = float( + V[((((bid_y * src_seq_len) + (Bc * j + tid_y)) * kv_heads) + (bid_x / p)) * head_dim + idx]); + } + } + __syncthreads(); + + // step-2: S = Q @ K.T, point-wise + if (tid_y < bound_tid_y && tid_x < bound_tid_x) { + float val0 = 0.0; + if (is_compute) { +#pragma unroll + for (int k = 0; k < head_dim; ++k) { + val0 += Q_sm_AT(tid_y, k) * K_T_sm_AT(k, tid_x); + } + SP_AT(tid_y, tid_x) = double(val0) * scale; + } + } + __syncthreads(); + + // step-3: m_new = max(m_prev, rowMax(S)) + float val1 = float(SP_AT(tid_y, tid_x)); + val1 = warp_reduce_max(val1); + if (tid_x == 0 && tid_y < bound_tid_y) { + m_new[tid_y] = (val1 > m_prev[tid_y]) ? val1 : m_prev[tid_y]; + } + __syncthreads(); + + // step-4: P = exp(S - m_new), point-wise + if (tid_y < bound_tid_y && tid_x < bound_tid_x) { + if (is_compute) { + SP_AT(tid_y, tid_x) = myexp(SP_AT(tid_y, tid_x) - double(m_new[tid_y])); + } else { + SP_AT(tid_y, tid_x) = 0.0; + } + } else { + SP_AT(tid_y, tid_x) = 0.0; + } + __syncthreads(); + + // step-5: l_new = exp(m_prev - m_new) * l_prev + rowSum(P) + float val2 = float(SP_AT(tid_y, tid_x)); + val2 = warp_reduce_sum(val2); + float exp_result = myexp(m_prev[tid_y] - m_new[tid_y]); + if (tid_x == 0 && tid_y < bound_tid_y) { + l_new[tid_y] = exp_result * l_prev[tid_y] + val2; + } + __syncthreads(); + + // step-5.5: Apply dropout to P (using dropout_seed for reproducibility) + if (dropout_p > 0 && tid_y < bound_tid_y && tid_x < bound_tid_x) { + float rand_val = curand_uniform(&state); + bool keep = rand_val > dropout_p; + if (keep) { + SP_AT(tid_y, tid_x) = SP_AT(tid_y, tid_x) / (1.0f - dropout_p); + } else { + SP_AT(tid_y, tid_x) = 0.0; + } + // Note: Removed dropout_sm mask saving to reduce memory overhead + } + __syncthreads(); + + // step-6: O = 1/(exp(m_prev - m_new)) * O + P @ V + if (tid_x < bound_tid_x && tid_y < bound_tid_y) { + for (int u = tid_x; u < head_dim; u += blockDim.x) { + float val3 = 0.0; +#pragma unroll + for (int w = 0; w < Bc; ++w) { + val3 += float(SP_AT(tid_y, w)) * V_sm_AT(w, u); + } + O_sm_AT(tid_y, u) = O_sm_AT(tid_y, u) * exp_result + val3; + } + } + __syncthreads(); + + // step-7: m_prev <- m_new; l_prev <- l_new + if (tid_x == 0 && tid_y < bound_tid_y) { + m_prev[tid_y] = m_new[tid_y]; + l_prev[tid_y] = l_new[tid_y]; + } + __syncthreads(); + } +/****************************end-of-main-loop**************************/ + +/****************************post-process****************************/ +// O(GM) = O/l_prev, aka O_sm /= l_prev and write Oi from SM to GM +// Also save logsumexp for backward pass +#pragma unroll + for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { + if (tid_y < bound_tid_y) { + output[((((bid_y * target_seq_len) + (Br * bid_z + tid_y)) * q_heads) + bid_x) * head_dim + idx] + = T(O_sm_AT(tid_y, idx) / float(l_prev[tid_y])); + } + } + + // Save logsumexp for backward pass + if (tid_x == 0 && tid_y < bound_tid_y) { + int logsumexp_idx = ((bid_y * q_heads + bid_x) * target_seq_len) + (Br * bid_z + tid_y); + logsumexp[logsumexp_idx] = m_prev[tid_y] + log(l_prev[tid_y]); + } + __syncthreads(); +/****************************end-of-post-process****************************/ + +// 取消访问宏定义 +#undef SP_AT +#undef Q_sm_AT +#undef K_T_sm_AT +#undef V_sm_AT +#undef O_sm_AT +} + +/** + * FlashAttention Backward Kernel + * + * This kernel implements the backward pass for FlashAttention. + * It computes gradients for query, key, and value tensors. + * + * Args: + * grad_query: Gradient for query tensor + * grad_key: Gradient for key tensor + * grad_value: Gradient for value tensor + * query: Query tensor from forward pass + * key: Key tensor from forward pass + * value: Value tensor from forward pass + * output: Output tensor from forward pass + * grad_output: Gradient from upstream + * logsumexp: Logsumexp tensor from forward pass + * dropout_seed: Dropout seed for reproducibility + * attn_mask: Optional attention mask tensor + * scale: Scaling factor for attention scores + * is_causal: Whether causal masking was applied + * dropout_p: Dropout probability + * enable_gqa: Whether GQA was enabled + * batch_size, target_seq_len, src_seq_len: Tensor dimensions + * q_heads, kv_heads, head_dim: Attention head dimensions + */ +template +__global__ void FlashAttentionBackwardKernel( + T *grad_query, T *grad_key, T *grad_value, + const T *query, const T *key, const T *value, + const T *output, const T *grad_output, + const float *logsumexp, + const float *D, // Precomputed D = rowsum(dO ∘ O) + unsigned long long dropout_seed, + const T *attn_mask, + float scale, bool is_causal, int64_t dropout_p, + bool enable_gqa, + int batch_size, int target_seq_len, int src_seq_len, + int q_heads, int kv_heads, int head_dim) { + + // Grid/block dimensions: grid_dims(num_heads_q, batch_size, Tr), block_dim(Bc, Br) + // where Br corresponds to thread block row index, Bc corresponds to column index + int tid_x = threadIdx.x; // 横向, blockDim.x列 (Bc) + int tid_y = threadIdx.y; // 纵向, blockDim.y行 (Br) + int bid_x = blockIdx.x; // x方向, 总数 = #q_heads + int bid_y = blockIdx.y; // y方向, 总数 = #batch + int bid_z = blockIdx.z; // z方向, 总数 = Tr + const int p = q_heads / kv_heads; // GQA比例系数 + + const int Br = blockDim.y; // Q纵向每块大小, 32 + const int Bc = blockDim.x; // K/V纵向分块大小, 32 + const int Tr = gridDim.z; // Q纵向分块数 + const int Tc = (src_seq_len + Bc - 1) / Bc; // K/V纵向分块数 + + // Define shared memory + extern __shared__ char shared_mem[]; + char *ptr = shared_mem; + + // D_sm[Br] - D values loaded from HBM + float *D_sm = reinterpret_cast(ptr); + ptr += Br * sizeof(float); + + // Q_sm[Br][head_dim], K_T_sm[head_dim][Bc], V_sm[Bc][head_dim] + float *Q_sm = reinterpret_cast(ptr); + ptr += Br * head_dim * sizeof(float); + float *K_T_sm = reinterpret_cast(ptr); + ptr += head_dim * Bc * sizeof(float); + float *V_sm = reinterpret_cast(ptr); + ptr += Bc * head_dim * sizeof(float); + + // dO_sm[Br][head_dim], dK_T_sm[head_dim][Bc], dV_sm[Bc][head_dim] + float *dO_sm = reinterpret_cast(ptr); + ptr += Br * head_dim * sizeof(float); + float *dK_T_sm = reinterpret_cast(ptr); + ptr += head_dim * Bc * sizeof(float); + float *dV_sm = reinterpret_cast(ptr); + ptr += Bc * head_dim * sizeof(float); + + // S_sm[Br][Bc], P_sm[Br][Bc] + float *S_sm = reinterpret_cast(ptr); + ptr += Br * Bc * sizeof(float); + float *P_sm = reinterpret_cast(ptr); + + // L_sm[Br] - logsumexp values + float *L_sm = reinterpret_cast(ptr); + + // dQ_sm[Br][head_dim] - accumulated gradient for Q + float *dQ_sm = reinterpret_cast(ptr); + + // Define access macros + #define D_sm_AT(y) D_sm[y] + #define Q_sm_AT(y, x) Q_sm[y * head_dim + x] + #define K_T_sm_AT(y, x) K_T_sm[y * Bc + x] + #define V_sm_AT(y, x) V_sm[y * head_dim + x] + #define dO_sm_AT(y, x) dO_sm[y * head_dim + x] + #define dK_T_sm_AT(y, x) dK_T_sm[y * Bc + x] + #define dV_sm_AT(y, x) dV_sm[y * head_dim + x] + #define S_sm_AT(y, x) S_sm[y * Bc + x] + #define P_sm_AT(y, x) P_sm[y * Bc + x] + #define L_sm_AT(y) L_sm[y] + #define dQ_sm_AT(y, x) dQ_sm[y * head_dim + x] + + // Initialize dropout random state + curandStatePhilox4_32_10_t state; + if (dropout_p > 0) { + unsigned long long seq = bid_y * q_heads * target_seq_len + bid_x * target_seq_len + Br * bid_z + tid_y; + curand_init(dropout_seed, seq, 0, &state); + } + + /****************************Preparation*****************************/ + + // Initialize dQ_sm to 0 for accumulation + for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { + dQ_sm_AT(tid_y, idx) = 0.0f; + } + __syncthreads(); + + // Load D_i from HBM to shared memory + int q_idx = Br * bid_z + tid_y; // Global query position within this head + if (q_idx < target_seq_len) { + int d_idx = ((bid_y * q_heads + bid_x) * target_seq_len) + q_idx; + D_sm_AT(tid_y) = D[d_idx]; + } else { + D_sm_AT(tid_y) = 0.0f; // Padding for out-of-bounds positions + } + __syncthreads(); + + // Get D_i from shared memory + float D_i = D_sm_AT(tid_y); + + /****************************Main Loop - Outer Loop over K/V tiles*****************************/ + for (int j = 0; j < Tc; ++j) { // For each K/V column tile + + // Skip entire tile if causal and this tile is completely to the right + bool skip_tile = (is_causal && bid_z < j); + if (skip_tile) { + __syncthreads(); + continue; + } + + // Initialize dK_T_sm, dV_sm to 0 for this column tile + for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { + for (int y = 0; y < Bc; ++y) { + dK_T_sm_AT(idx, y) = 0.0f; + dV_sm_AT(y, idx) = 0.0f; + } + } + __syncthreads(); + + // Load K_j, V_j from HBM to shared memory + int bound_tid_x = min(Bc, src_seq_len - Bc * j); + for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { + K_T_sm_AT(idx, tid_y) = 0.0f; + V_sm_AT(tid_y, idx) = 0.0f; + if (tid_y < bound_tid_x) { + int kv_head_idx = bid_x / p; + K_T_sm_AT(idx, tid_y) = float( + key[((((bid_y * src_seq_len) + (Bc * j + tid_y)) * kv_heads + kv_head_idx) * head_dim + idx]); + V_sm_AT(tid_y, idx) = float( + value[((((bid_y * src_seq_len) + (Bc * j + tid_y)) * kv_heads + kv_head_idx) * head_dim + idx]); + } + } + __syncthreads(); + + /****************************Inner Loop - Loop over Q tiles*****************************/ + // For this column tile, iterate through all Q row tiles + for (int i = 0; i < Tr; ++i) { // For each Q row tile + + // Skip this Q tile if causal and it's completely to the left of current K tile + bool skip_q_tile = (is_causal && i > j); + if (skip_q_tile) { + __syncthreads(); + continue; + } + + // Load Q_i, dO_i, L_i for this row tile + int q_tile_start = Br * i; + int q_tile_end = min(q_tile_start + Br, target_seq_len); + int bound_tid_y = min(Br, q_tile_end - q_tile_start); + + int local_q_idx = Br * i + tid_y; + if (local_q_idx < bound_tid_y) { + // Load Q_i, dO_i, L_i from HBM + int global_q_idx = q_tile_start + tid_y; + int q_tensor_idx = ((bid_y * target_seq_len) + global_q_idx) * q_heads + bid_x; + + for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { + Q_sm_AT(tid_y, idx) = float(query[q_tensor_idx * head_dim + idx]); + dO_sm_AT(tid_y, idx) = float(grad_output[q_tensor_idx * head_dim + idx]); + L_sm_AT(tid_y) = logsumexp[((bid_y * q_heads + bid_x) * target_seq_len + global_q_idx]; + } + } + __syncthreads(); + + // Get D_i for this row + float D_i_row = D_sm_AT(tid_y); + + // Recompute S_ij = Q_i @ K_j^T + // Apply causal mask at element level if needed + #pragma unroll + for (int y = 0; y < Bc; ++y) { + for (int x = 0; x < Bc; ++x) { + float val = 0.0f; + if (tid_y < bound_tid_y && tid_x < bound_tid_x) { + bool is_compute = true; + if (is_causal && i == j) { + // On diagonal, only compute upper triangle + int global_q_pos = q_tile_start + tid_y; + int global_k_pos = Bc * j + x; + is_compute = (global_q_pos >= global_k_pos); + } + + if (is_compute) { + #pragma unroll + for (int k = 0; k < head_dim; ++k) { + val += Q_sm_AT(tid_y, k) * K_T_sm_AT(k, y); + } + } + } + S_sm_AT(tid_y, x) = val * scale; + } + } + __syncthreads(); + + // Recompute P_ij = exp(S_ij - L_i) + #pragma unroll + for (int y = 0; y < Bc; ++y) { + for (int x = 0; x < Bc; ++x) { + if (tid_y < bound_tid_y && tid_x < bound_tid_x) { + bool is_compute = true; + if (is_causal && i == j) { + int global_q_pos = q_tile_start + tid_y; + int global_k_pos = Bc * j + x; + is_compute = (global_q_pos >= global_k_pos); + } + + if (is_compute) { + P_sm_AT(tid_y, x) = myexp(S_sm_AT(tid_y, x) - L_sm_AT(tid_y)); + } else { + P_sm_AT(tid_y, x) = 0.0f; + } + } else { + P_sm_AT(tid_y, x) = 0.0f; + } + } + } + __syncthreads(); + + // Apply dropout to P_ij + if (dropout_p > 0) { + #pragma unroll + for (int y = 0; y < Bc; ++y) { + for (int x = 0; x < Bc; ++x) { + if (tid_y < bound_tid_y && tid_x < bound_tid_x) { + float rand_val = curand_uniform(&state); + bool keep = rand_val > dropout_p; + if (keep) { + P_sm_AT(tid_y, x) = P_sm_AT(tid_y, x) / (1.0f - dropout_p); + } else { + P_sm_AT(tid_y, x) = 0.0f; + } + } + } + } + __syncthreads(); + } + + // Compute dV_j += P_ij^T @ dO_i + if (tid_y < bound_tid_y) { + #pragma unroll + for (int x = 0; x < head_dim; x += blockDim.x) { + float val = 0.0f; + #pragma unroll + for (int y = 0; y < Bc; ++y) { + val += P_sm_AT(tid_y, y) * dO_sm_AT(tid_y, x); + } + atomicAdd(&dV_sm_AT(y, x), val); + } + } + __syncthreads(); + + // Compute dP_ij = dO_i @ V_j^T + #pragma unroll + for (int y = 0; y < Bc; ++y) { + for (int x = 0; x < Bc; ++x) { + float val = 0.0f; + if (tid_y < bound_tid_y && tid_x < bound_tid_x) { + #pragma unroll + for (int k = 0; k < head_dim; ++k) { + val += dO_sm_AT(tid_y, k) * V_sm_AT(y, k); + } + } + S_sm_AT(tid_y, x) = val; // Reuse S_sm as temporary storage for dP_ij + } else { + S_sm_AT(tid_y, x) = 0.0f; + } + } + __syncthreads(); + + // Apply dropout to dP_ij + if (dropout_p > 0) { + #pragma unroll + for (int y = 0; y < Bc; ++y) { + for (int x = 0; x < Bc; ++x) { + if (tid_y < bound_tid_y && tid_x < bound_tid_x) { + float rand_val = curand_uniform(&state); + bool keep = rand_val > dropout_p; + if (keep) { + S_sm_AT(tid_y, x) = S_sm_AT(tid_y, x) / (1.0f - dropout_p); + } else { + S_sm_AT(tid_y, x) = 0.0f; + } + } + } + } + __syncthreads(); + } + + // Compute dS_ij = P_ij * (dP_ij - D_i) + #pragma unroll + for (int y = 0; y < Bc; ++y) { + for (int x = 0; x < Bc; ++x) { + if (tid_y < bound_tid_y && tid_x < bound_tid_x) { + S_sm_AT(tid_y, x) = P_sm_AT(tid_y, x) * (S_sm_AT(tid_y, x) - D_i_row); + } else { + S_sm_AT(tid_y, x) = 0.0f; + } + } + } + __syncthreads(); + + // Compute dK_j += dS_ij^T @ Q_i + if (tid_x < head_dim) { + #pragma unroll + for (int y = 0; y < Bc; ++y) { + float val = 0.0f; + #pragma unroll + for (int x = 0; x < Br; ++x) { + val += S_sm_AT(x, y) * Q_sm_AT(x, tid_x); + } + atomicAdd(&dK_T_sm_AT(tid_x, y), val * scale); + } + } + __syncthreads(); + + // Compute dQ_i += dS_ij @ K_j + if (tid_y < bound_tid_y) { + #pragma unroll + for (int x = 0; x < head_dim; x += blockDim.x) { + float val = 0.0f; + #pragma unroll + for (int y = 0; y < Bc; ++y) { + val += S_sm_AT(tid_y, y) * K_T_sm_AT(x, y); + } + dQ_sm_AT(tid_y, x) += val * scale; + } + } + __syncthreads(); + } // End of inner loop over Q tiles + + // Write back dK_j, dV_j to HBM + // Note: For GQA, dK_j and dV_j need to be accumulated across multiple q-head blocks + int kv_head_idx = bid_x / p; + int k_tile_start = Bc * j; + int k_tile_end = min(k_tile_start + Bc, src_seq_len); + + for (int y = 0; y < Bc; ++y) { + int global_k_idx = k_tile_start + y; + if (global_k_idx < src_seq_len) { + for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { + // dK: [batch_size, src_seq_len, kv_heads, head_dim] + int k_tensor_idx = ((bid_y * src_seq_len) + global_k_idx) * kv_heads + kv_head_idx; + grad_key[k_tensor_idx * head_dim + idx] += T(dK_T_sm_AT(idx, y)); + + // dV: [batch_size, src_seq_len, kv_heads, head_dim] + grad_value[k_tensor_idx * head_dim + idx] += T(dV_sm_AT(y, idx)); + } + } + } + } // End of outer loop over K/V tiles + + // Write back dQ_i to HBM + int q_tile_start = Br * bid_z; + int q_tile_end = min(q_tile_start + Br, target_seq_len); + int bound_tid_y = min(Br, q_tile_end - q_tile_start); + + if (tid_y < bound_tid_y) { + int global_q_idx = q_tile_start + tid_y; + int q_tensor_idx = ((bid_y * target_seq_len) + global_q_idx) * q_heads + bid_x; + + for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { + // dQ: [batch_size, target_seq_len, q_heads, head_dim] + grad_query[q_tensor_idx * head_dim + idx] += T(dQ_sm_AT(tid_y, idx)); + } + } + + // Undefine access macros + #undef D_sm_AT + #undef Q_sm_AT + #undef K_T_sm_AT + #undef V_sm_AT + #undef dO_sm_AT + #undef dK_T_sm_AT + #undef dV_sm_AT + #undef S_sm_AT + #undef P_sm_AT + #undef L_sm_AT + #undef dQ_sm_AT +} + +/** + * FlashAttention Forward Function + * + * This is the main entry point for FlashAttention forward computation. + * It creates the output tensor and launches the appropriate kernel based on data type. + * + * Args: + * query: Query tensor of shape [batch_size, seq_len_q, num_heads, head_dim] + * key: Key tensor of shape [batch_size, seq_len_k, num_heads_kv, head_dim] + * value: Value tensor of shape [batch_size, seq_len_k, num_heads_kv, head_dim] + * attn_mask: Optional attention mask tensor + * scale: Scaling factor for attention scores + * is_causal: Whether to apply causal masking + * dropout_p: Dropout probability + * enable_gqa: Whether to enable grouped-query attention + * + * Returns: + * Output tensor of shape [batch_size, seq_len_q, num_heads, head_dim] + * logsumexp: Logsumexp tensor for backward pass [batch_size, num_heads, seq_len_q] + * dropout_seed: Dropout seed for backward pass [1] + */ + +template +FlashAttentionForwardOutput FlashAttentionForwardImpl(const std::shared_ptr &query, const std::shared_ptr &key, + const std::shared_ptr &value, + const std::shared_ptr &attn_mask, float scale, bool is_causal, + int64_t dropout_p, bool enable_gqa) { + auto dtype = query->Dtype(); + const auto &query_dims = query->Dims(); + + // Output shape: [batch_size, seq_len_q, num_heads, head_dim] + std::vector output_dims = {query_dims[0], query_dims[1], query_dims[2], query_dims[3]}; + auto output = std::make_shared(output_dims, dtype, query->GetDevice()); + + // Allocate logsumexp tensor for backward pass + // Shape: [batch_size, num_heads, seq_len_q] + std::vector logsumexp_dims = {query_dims[0], query_dims[2], query_dims[1]}; + auto logsumexp = std::make_shared(logsumexp_dims, DataType::kFLOAT32, query->GetDevice()); + float *logsumexp_ptr = static_cast(logsumexp->DataPtr()); + + // Allocate dropout_seed tensor for backward pass + // Shape: [1] + unsigned long long dropout_seed = 0; + std::shared_ptr dropout_seed_tensor; + if (dropout_p > 0) { + std::vector dropout_seed_dims = {1}; + dropout_seed_tensor = std::make_shared(dropout_seed_dims, DataType::kUINT64, query->GetDevice()); + dropout_seed = static_cast(std::time(nullptr)); + unsigned long long *dropout_seed_ptr = static_cast(dropout_seed_tensor->DataPtr()); + *dropout_seed_ptr = dropout_seed; + } + + switch (dtype) { + DISPATCH_CASE(WRAP(LaunchFlashAttentionForward(output, query, key, value, attn_mask, scale, is_causal, + dropout_p, enable_gqa, logsumexp_ptr, dropout_seed_ptr);), + DataType::kFLOAT32) + DISPATCH_CASE(WRAP(LaunchFlashAttentionForward(output, query, key, value, attn_mask, scale, + is_causal, dropout_p, enable_gqa, logsumexp_ptr, dropout_seed_ptr);), + DataType::kBFLOAT16) + default: + LOG_LOC(FATAL, "CUDA FlashAttention forward: 'Unsupported data type'"); + } + + FlashAttentionForwardOutput result; + result.output = output; + result.logsumexp = logsumexp; + result.dropout_seed = dropout_seed_tensor; + return result; +} + +/** + * Launch FlashAttention Forward Kernel + * + * This function sets up grid and block dimensions and launches FlashAttention forward kernel. + * + * Args: + * output: Output tensor + * query: Query tensor + * key: Key tensor + * value: Value tensor + * attn_mask: Optional attention mask tensor + * scale: Scaling factor + * is_causal: Whether to apply causal masking + * dropout_p: Dropout probability + * enable_gqa: Whether to enable GQA + */ +template +void LaunchFlashAttentionForward(const std::shared_ptr &output, const std::shared_ptr &query, + const std::shared_ptr &key, const std::shared_ptr &value, + const std::shared_ptr &attn_mask, float scale, bool is_causal, + int64_t dropout_p, bool enable_gqa, float *logsumexp_ptr, unsigned long long *dropout_seed_ptr) { + + const auto &query_dims = query->Dims(); + const auto &key_dims = key->Dims(); + const auto &value_dims = value->Dims(); + + // Expected shapes: + // query: [batch_size, seq_len_q, num_heads, head_dim] + // key: [batch_size, seq_len_k, num_heads_kv, head_dim] + // value: [batch_size, seq_len_k, num_heads_kv, head_dim] + // output: [batch_size, seq_len_q, num_heads, head_dim] + + int64_t batch_size = query_dims[0]; + int64_t seq_len_q = query_dims[1]; + int64_t num_heads = query_dims[2]; + int64_t head_dim = query_dims[3]; + int64_t seq_len_k = key_dims[1]; + int64_t num_heads_kv = key_dims[2]; + + CHECK_EQ(key_dims[3], head_dim) << "Key head dimension must match query head dimension"; + CHECK_EQ(value_dims[3], head_dim) << "Value head dimension must match query head dimension"; + CHECK_EQ(value_dims[1], seq_len_k) << "Value sequence length must match key sequence length"; + CHECK_EQ(value_dims[2], num_heads_kv) << "Value number of KV heads must match key"; + + if (enable_gqa) { + CHECK(num_heads % num_heads_kv == 0) << "Number of query heads must be divisible by number of KV heads for GQA"; + } else { + CHECK_EQ(num_heads, num_heads_kv) << "Number of query and KV heads must match for standard attention"; + } + + T *output_ptr = static_cast(output->DataPtr()); + const T *query_ptr = static_cast(query->DataPtr()); + const T *key_ptr = static_cast(key->DataPtr()); + const T *value_ptr = static_cast(value->DataPtr()); + const T *attn_mask_ptr = attn_mask ? static_cast(attn_mask->DataPtr()) : nullptr; + + // Set up grid and block dimensions according to FlashAttention v2 + // block_dim(Br, Bc) where Br = Bc = 32 + // grid_dim(query_heads, batch_size, Tr) where Tr = ceil(seq_len_q / Br) + constexpr int Br = 32; + constexpr int Bc = 32; + int64_t Tr = (seq_len_q + Br - 1) / Br; + + dim3 block_dims(Bc, Br); // (blockDim.x, blockDim.y) = (Bc, Br) + dim3 grid_dims(num_heads, batch_size, Tr); // (gridDim.x, gridDim.y, gridDim.z) = (num_heads, batch_size, Tr) + + // Calculate shared memory size (removed dropout_sm allocation) + // SP[Br][Bc] (double) + m_prev[Br] (float) + m_new[Br] (float) + l_prev[Br] (float) + l_new[Br] (float) + // + Q_sm[Br][head_dim] (float) + K_T_sm[head_dim][Bc] (float) + V_sm[Bc][head_dim] (float) + O_sm[Br][head_dim] (float) + size_t shared_mem_size = Br * Bc * sizeof(double) // SP + + 4 * Br * sizeof(float) // m_prev, m_new, l_prev, l_new + + (Br + Bc + Bc + Br) * head_dim) * sizeof(float); // Q_sm, K_T_sm, V_sm, O_sm + // Note: Removed dropout_sm[Br][Bc] (bool) allocation + + auto device = output->GetDevice(); + const auto &cuda_stream = dynamic_cast( + infini_train::core::GetDeviceGuardImpl(device.type())->GetStream(device)) + ->cuda_stream(); + + FlashAttentionForwardKernel<<>>( + output_ptr, query_ptr, key_ptr, value_ptr, attn_mask_ptr, + logsumexp_ptr, scale, is_causal, enable_gqa, dropout_p, dropout_seed, + batch_size, seq_len_q, seq_len_k, num_heads, num_heads_kv, head_dim); +} + +/** + * Launch FlashAttention Backward Kernel + * + * This function sets up grid and block dimensions and launches FlashAttention backward kernel. + * + * Args: + * grad_query: Gradient tensor for query + * grad_key: Gradient tensor for key + * grad_value: Gradient tensor for value + * query: Query tensor from forward pass + * key: Key tensor from forward pass + * value: Value tensor from forward pass + * output: Output tensor from forward pass + * grad_output: Gradient from upstream + * logsumexp: Logsumexp tensor from forward pass + * dropout_seed: Dropout seed for reproducibility + * attn_mask: Optional attention mask tensor + * scale: Scaling factor + * is_causal: Whether causal masking was applied + * dropout_p: Dropout probability + * enable_gqa: Whether GQA was enabled + */ +template +void LaunchFlashAttentionBackward(const std::shared_ptr &grad_query, const std::shared_ptr &grad_key, + const std::shared_ptr &grad_value, const std::shared_ptr &query, + const std::shared_ptr &key, const std::shared_ptr &value, + const std::shared_ptr &output, const std::shared_ptr &grad_output, + const std::shared_ptr &logsumexp, + unsigned long long dropout_seed, + const std::shared_ptr &attn_mask, + float scale, bool is_causal, int64_t dropout_p, bool enable_gqa) { + + const auto &query_dims = query->Dims(); + const auto &key_dims = key->Dims(); + + // Expected shapes: + // query: [batch_size, seq_len_q, num_heads, head_dim] + // key: [batch_size, seq_len_k, num_heads_kv, head_dim] + // value: [batch_size, seq_len_k, num_heads_kv, head_dim] + // output: [batch_size, seq_len_q, num_heads, head_dim] + + int64_t batch_size = query_dims[0]; + int64_t seq_len_q = query_dims[1]; + int64_t num_heads = query_dims[2]; + int64_t head_dim = query_dims[3]; + int64_t seq_len_k = key_dims[1]; + int64_t num_heads_kv = key_dims[2]; + + CHECK_EQ(key_dims[3], head_dim) << "Key head dimension must match query head dimension"; + CHECK_EQ(value->Dims()[3], head_dim) << "Value head dimension must match query head dimension"; + CHECK_EQ(value->Dims()[1], seq_len_k) << "Value sequence length must match key sequence length"; + CHECK_EQ(value->Dims()[2], num_heads_kv) << "Value number of KV heads must match key"; + + if (enable_gqa) { + CHECK(num_heads % num_heads_kv == 0) << "Number of query heads must be divisible by number of KV heads for GQA"; + } else { + CHECK_EQ(num_heads, num_heads_kv) << "Number of query and KV heads must match for standard attention"; + } + + // Precompute D = rowsum(dO ∘ O) before main backward loop + // D shape: [batch_size, seq_len_q, num_heads] + // dO shape: [batch_size, seq_len_q, num_heads, head_dim] + // O shape: [batch_size, seq_len_q, num_heads, head_dim] + auto D = function::Sum(grad_output * output, 3, false); + + T *grad_query_ptr = static_cast(grad_query->DataPtr()); + T *grad_key_ptr = static_cast(grad_key->DataPtr()); + T *grad_value_ptr = static_cast(grad_value->DataPtr()); + const T *query_ptr = static_cast(query->DataPtr()); + const T *key_ptr = static_cast(key->DataPtr()); + const T *value_ptr = static_cast(value->DataPtr()); + const T *output_ptr = static_cast(output->DataPtr()); + const T *grad_output_ptr = static_cast(grad_output->DataPtr()); + const float *logsumexp_ptr = static_cast(logsumexp->DataPtr()); + const float *D_ptr = static_cast(D->DataPtr()); + const T *attn_mask_ptr = attn_mask ? static_cast(attn_mask->DataPtr()) : nullptr; + + // Set up grid and block dimensions according to FlashAttention v2 backward + // block_dim(Bc, Br) where Bc = Br = 32 + // grid_dims(num_heads_q, batch_size, Tr) where Tr = ceil(seq_len_q / Br) + constexpr int Br = 32; + constexpr int Bc = 32; + int64_t Tr = (seq_len_q + Br - 1) / Br; + + dim3 block_dims(Bc, Br); // (blockDim.x, blockDim.y) = (Bc, Br) + dim3 grid_dims(num_heads, batch_size, Tr); // (gridDim.x, gridDim.y, gridDim.z) = (num_heads, batch_size, Tr) + + // Calculate shared memory size for backward pass + // Q_sm[Br][head_dim], K_T_sm[head_dim][Bc], V_sm[Bc][head_dim] + // dO_sm[Br][head_dim], dK_T_sm[head_dim][Bc], dV_sm[Bc][head_dim] + // S_sm[Br][Bc], P_sm[Br][Bc] + // D_sm[Br] - D values loaded from HBM to shared memory + size_t shared_mem_size = Br * head_dim * sizeof(float) // Q_sm + + head_dim * Bc * sizeof(float) // K_T_sm + + Bc * head_dim * sizeof(float) // V_sm + + Br * head_dim * sizeof(float) // dO_sm + + head_dim * Bc * sizeof(float) // dK_T_sm + + Bc * head_dim * sizeof(float) // dV_sm + + Br * Bc * sizeof(float) // S_sm or (dP_sm when compute dP_sm = dO_i @ V_j^T \in R^{Br*Bc}) + + Br * Bc * sizeof(float) // P_sm or (dS_sm when compute dS_sm = P_sm_ij pointwise multiplied by (dP_sm_ij - D_i) \in R^{Br*Bc}) + + Br * sizeof(float) // L_i + + Br * sizeof(float); // D_sm (loaded from HBM) + + auto device = query->GetDevice(); + const auto &cuda_stream = dynamic_cast( + infini_train::core::GetDeviceGuardImpl(device.type())->GetStream(device)) + ->cuda_stream(); + + FlashAttentionBackwardKernel<<>>( + grad_query_ptr, grad_key_ptr, grad_value_ptr, + query_ptr, key_ptr, value_ptr, output_ptr, grad_output_ptr, + logsumexp_ptr, D_ptr, dropout_seed, attn_mask_ptr, + scale, is_causal, dropout_p, enable_gqa, + batch_size, seq_len_q, seq_len_k, num_heads, num_heads_kv, head_dim); +} + +/** + * FlashAttention Backward Function + * + * This is the main entry point for FlashAttention backward computation. + * It creates gradient tensors and launches the appropriate kernel based on data type. + * + * Args: + * grad_query: Gradient tensor for query + * grad_key: Gradient tensor for key + * grad_value: Gradient tensor for value + * query: Query tensor from forward pass + * key: Key tensor from forward pass + * value: Value tensor from forward pass + * output: Output tensor from forward pass + * grad_output: Gradient from upstream + * attn_mask: Optional attention mask tensor + * scale: Scaling factor + * is_causal: Whether causal masking was applied + * dropout_p: Dropout probability (not implemented) + * enable_gqa: Whether GQA was enabled + * + * Returns: + * Tuple of (grad_query, grad_key, grad_value) tensors + */ +template +std::vector> FlashAttentionBackwardImpl(const std::shared_ptr &grad_query, const std::shared_ptr &grad_key, + const std::shared_ptr &grad_value, const std::shared_ptr &query, + const std::shared_ptr &key, const std::shared_ptr &value, + const std::shared_ptr &output, const std::shared_ptr &grad_output, + const std::shared_ptr &logsumexp, + const std::shared_ptr &dropout_seed, + const std::shared_ptr &attn_mask, + float scale, bool is_causal, int64_t dropout_p, bool enable_gqa) { + auto dtype = query->Dtype(); + + // Create gradient tensors with same shapes as inputs + auto grad_query = std::make_shared(query->Dims(), dtype, query->GetDevice()); + auto grad_key = std::make_shared(key->Dims(), dtype, key->GetDevice()); + auto grad_value = std::make_shared(value->Dims(), dtype, value->GetDevice()); + + // Initialize gradients to zero + DispatchFunc(dtype, [=]() { grad_query->Fill(0); }, "CUDA FlashAttentionBackward"); + DispatchFunc(dtype, [=]() { grad_key->Fill(0); }, "CUDA FlashAttentionBackward"); + DispatchFunc(dtype, [=]() { grad_value->Fill(0); }, "CUDA FlashAttentionBackward"); + + // Get dropout seed value + unsigned long long dropout_seed_value = 0; + if (dropout_seed) { + dropout_seed_value = *static_cast(dropout_seed->DataPtr()); + } + + switch (dtype) { + DISPATCH_CASE(WRAP(LaunchFlashAttentionBackward(grad_query, grad_key, grad_value, query, key, value, output, grad_output, + logsumexp, dropout_seed_value, attn_mask, scale, is_causal, + dropout_p, enable_gqa);), + DataType::kFLOAT32) + DISPATCH_CASE(WRAP(LaunchFlashAttentionBackward(grad_query, grad_key, grad_value, query, key, value, output, grad_output, + logsumexp, dropout_seed_value, attn_mask, scale, is_causal, + dropout_p, enable_gqa);), + DataType::kBFLOAT16) + default: + LOG_LOC(FATAL, "CUDA FlashAttention backward: 'Unsupported data type'"); + } + + return {grad_query, grad_key, grad_value}; +} + +// Non-template wrapper functions for registration +FlashAttentionForwardOutput FlashAttentionForward(const std::shared_ptr &query, const std::shared_ptr &key, + const std::shared_ptr &value, + const std::shared_ptr &attn_mask, float scale, bool is_causal, + int64_t dropout_p, bool enable_gqa) { + auto dtype = query->Dtype(); + const auto &query_dims = query->Dims(); + + // Output shape: [batch_size, seq_len_q, num_heads, head_dim] + std::vector output_dims = {query_dims[0], query_dims[1], query_dims[2], query_dims[3]}; + auto output = std::make_shared(output_dims, dtype, query->GetDevice()); + + // Allocate logsumexp tensor for backward pass + // Shape: [batch_size, num_heads, seq_len_q] + std::vector logsumexp_dims = {query_dims[0], query_dims[2], query_dims[1]}; + auto logsumexp = std::make_shared(logsumexp_dims, DataType::kFLOAT32, query->GetDevice()); + float *logsumexp_ptr = static_cast(logsumexp->DataPtr()); + + // Allocate dropout_seed tensor for backward pass + // Shape: [1] + unsigned long long dropout_seed = 0; + std::shared_ptr dropout_seed_tensor; + if (dropout_p > 0) { + std::vector dropout_seed_dims = {1}; + dropout_seed_tensor = std::make_shared(dropout_seed_dims, DataType::kUINT64, query->GetDevice()); + dropout_seed = static_cast(std::time(nullptr)); + unsigned long long *dropout_seed_ptr = static_cast(dropout_seed_tensor->DataPtr()); + *dropout_seed_ptr = dropout_seed; + } + + switch (dtype) { + DISPATCH_CASE(WRAP(LaunchFlashAttentionForward(output, query, key, value, attn_mask, scale, is_causal, + dropout_p, enable_gqa, logsumexp_ptr, + dropout_seed_tensor ? static_cast(dropout_seed_tensor->DataPtr()) : nullptr);), + DataType::kFLOAT32) + DISPATCH_CASE(WRAP(LaunchFlashAttentionForward(output, query, key, value, attn_mask, scale, + is_causal, dropout_p, enable_gqa, logsumexp_ptr, + dropout_seed_tensor ? static_cast(dropout_seed_tensor->DataPtr()) : nullptr);), + DataType::kBFLOAT16) + default: + LOG_LOC(FATAL, "CUDA FlashAttention forward: 'Unsupported data type'"); + } + + FlashAttentionForwardOutput result; + result.output = output; + result.logsumexp = logsumexp; + result.dropout_seed = dropout_seed_tensor; + return result; +} + +std::vector> FlashAttentionBackward(const std::shared_ptr &grad_query, const std::shared_ptr &grad_key, + const std::shared_ptr &grad_value, const std::shared_ptr &query, + const std::shared_ptr &key, const std::shared_ptr &value, + const std::shared_ptr &output, const std::shared_ptr &grad_output, + const std::shared_ptr &logsumexp, + const std::shared_ptr &dropout_seed, + const std::shared_ptr &attn_mask, + float scale, bool is_causal, int64_t dropout_p, bool enable_gqa) { + auto dtype = query->Dtype(); + + // Create gradient tensors with same shapes as inputs + auto grad_query = std::make_shared(query->Dims(), dtype, query->GetDevice()); + auto grad_key = std::make_shared(key->Dims(), dtype, key->GetDevice()); + auto grad_value = std::make_shared(value->Dims(), dtype, value->GetDevice()); + + // Initialize gradients to zero + DispatchFunc(dtype, [=]() { grad_query->Fill(0); }, "CUDA FlashAttentionBackward"); + DispatchFunc(dtype, [=]() { grad_key->Fill(0); }, "CUDA FlashAttentionBackward"); + DispatchFunc(dtype, [=]() { grad_value->Fill(0); }, "CUDA FlashAttentionBackward"); + + // Get dropout seed value + unsigned long long dropout_seed_value = 0; + if (dropout_seed) { + dropout_seed_value = *static_cast(dropout_seed->DataPtr()); + } + + switch (dtype) { + DISPATCH_CASE(WRAP(LaunchFlashAttentionBackward(grad_query, grad_key, grad_value, query, key, value, output, grad_output, + logsumexp, dropout_seed_value, attn_mask, scale, is_causal, + dropout_p, enable_gqa);), + DataType::kFLOAT32) + DISPATCH_CASE(WRAP(LaunchFlashAttentionBackward(grad_query, grad_key, grad_value, query, key, value, output, grad_output, + logsumexp, dropout_seed_value, attn_mask, scale, is_causal, + dropout_p, enable_gqa);), + DataType::kBFLOAT16) + default: + LOG_LOC(FATAL, "CUDA FlashAttention backward: 'Unsupported data type'"); + } + + return {grad_query, grad_key, grad_value}; +} + +} // namespace infini_train::kernels::cuda + +// Register FlashAttention kernels with the dispatcher +#define REGISTER_CUDA_FLASHATTENTION_KERNEL(kernel_name) \ + REGISTER_KERNEL(infini_train::Device::DeviceType::kCUDA, kernel_name, infini_train::kernels::cuda::kernel_name) + +REGISTER_CUDA_FLASHATTENTION_KERNEL(FlashAttentionForward) +REGISTER_CUDA_FLASHATTENTION_KERNEL(FlashAttentionBackward) + +#undef REGISTER_CUDA_FLASHATTENTION_KERNEL diff --git a/infini_train/src/nn/functional.cc b/infini_train/src/nn/functional.cc index b02f185a..cf2d4ae6 100644 --- a/infini_train/src/nn/functional.cc +++ b/infini_train/src/nn/functional.cc @@ -2,12 +2,14 @@ #include #include +#include #include #include "infini_train/include/autograd/activations.h" #include "infini_train/include/autograd/elementwise.h" #include "infini_train/include/autograd/misc.h" #include "infini_train/include/autograd/reduction.h" +#include "infini_train/include/autograd/ScaledDotProductAttention.h" #include "infini_train/include/autograd/softmax.h" #include "infini_train/include/autograd/transform.h" #include "infini_train/include/nn/init.h" @@ -79,4 +81,13 @@ std::shared_ptr Softmax(const std::shared_ptr &input, int64_t di std::shared_ptr Sigmoid(const std::shared_ptr &input) { return std::make_shared()->Apply({input})[0]; } + +std::shared_ptr ScaledDotProductAttention(const std::shared_ptr &query, + const std::shared_ptr &key, + const std::shared_ptr &value, + const std::shared_ptr &attn_mask, + int64_t dropout_p, bool is_causal, + std::optional scale, bool enable_gqa) { + return std::make_shared(attn_mask, dropout_p, is_causal, scale, enable_gqa)->Apply({query, key, value})[0]; +} } // namespace infini_train::nn::function From 29e7f22db3ce458bfac965b55daed84197dd2f0c Mon Sep 17 00:00:00 2001 From: Aoshine999 <824059542@qq.com> Date: Sun, 8 Mar 2026 15:08:26 +0800 Subject: [PATCH 02/18] update net config --- example/gpt2/main.cc | 8 +++ example/gpt2/net.cc | 38 ++++++++---- example/gpt2/net.h | 1 + example/llama3/main.cc | 5 ++ example/llama3/net.cc | 61 ++++++++++--------- example/llama3/net.h | 2 +- .../src/kernels/cuda/flash_attention.cu | 59 +++++++++++------- 7 files changed, 110 insertions(+), 64 deletions(-) diff --git a/example/gpt2/main.cc b/example/gpt2/main.cc index 69f8ba7e..2b2f981d 100644 --- a/example/gpt2/main.cc +++ b/example/gpt2/main.cc @@ -71,6 +71,7 @@ DEFINE_uint32(tensor_parallel, 1, "Tensor Parallel world size"); DEFINE_bool(sequence_parallel, false, "Whether to enable Sequence Parallel"); DEFINE_uint32(pipeline_parallel, 1, "Pipeline Parallel world size, specified the number of PP stages."); DEFINE_uint32(virtual_pipeline_parallel, 1, "Number of chunks in PP stage."); +DEFINE_bool(flash, false, "Whether to enable FlashAttention in CausalSelfAttention"); // precision DEFINE_string(dtype, "float32", "precision used in training (float32/bfloat16)"); @@ -181,11 +182,18 @@ void Train(const nn::parallel::Rank &rank) { GPT2Config model_config; std::shared_ptr model = nullptr; if (!FLAGS_llmc_filepath.empty()) { + if (FLAGS_flash) { + LOG(WARNING) << "--flash is ignored when loading GPT2 from --llmc_filepath."; + } model = GPT2::FromLLMC(FLAGS_llmc_filepath); } else if (kModelToConfigs.count(FLAGS_model)) { model_config = kModelToConfigs.at(FLAGS_model); + model_config.flash = FLAGS_flash; model = std::make_shared(model_config); } else { + if (FLAGS_flash) { + LOG(WARNING) << "--flash is ignored when loading GPT2 from pretrained checkpoint."; + } model = GPT2::FromPretrained(kStrToModelType.at(FLAGS_model)); } diff --git a/example/gpt2/net.cc b/example/gpt2/net.cc index 8d497797..68aece37 100644 --- a/example/gpt2/net.cc +++ b/example/gpt2/net.cc @@ -15,6 +15,7 @@ #include "example/common/utils.h" #include "infini_train/include/device.h" +#include "infini_train/include/autograd/ScaledDotProductAttention.h" #include "infini_train/include/nn/functional.h" #include "infini_train/include/nn/init.h" #include "infini_train/include/nn/modules/container.h" @@ -105,18 +106,31 @@ CausalSelfAttention::Forward(const std::vectorView({B, T, local_n_head_, head_dim})->Transpose(1, 2); v = v->View({B, T, local_n_head_, head_dim})->Transpose(1, 2); - // (B, h_l, T, T) - auto att = q->Matmul(k->Transpose(-2, -1)) * (1.0 / std::sqrt(head_dim)); - // (1, 1, T, T) - auto mask = buffers_[kParamBiasName]->Slice({0, 0, 0, 0}, {1, 1, T, T}, {1, 1, 1, 1}); - // (1, 1, T, T) -> eq 0 -> (1, 1, T, T) -> masked_fill -> (B, h_l, T, T) - att = att->MaskedFill(mask == 0, -std::numeric_limits::infinity()); - // (B, h_l, T, T) - att = nn::function::Softmax(att, -1); - // (B, h_l, T, Dh) - auto y = att->Matmul(v); - // (B, h_l, T, Dh) -> (B, T, h_l, Dh) -> (B, T, local_C) - y = y->Transpose(1, 2)->Contiguous()->View({B, T, local_C}); + std::shared_ptr y = nullptr; + if (config_.flash) { + // FlashAttention expects (B, T, H, D) + auto q_flash = q->Transpose(1, 2); + auto k_flash = k->Transpose(1, 2); + auto v_flash = v->Transpose(1, 2); + auto y_flash = std::make_shared( + /*attn_mask=*/nullptr, /*dropout_p=*/0, /*is_causal=*/true, + /*scale=*/1.0 / std::sqrt(static_cast(head_dim)), /*enable_gqa=*/false) + ->Apply({q_flash, k_flash, v_flash})[0]; + y = y_flash->Contiguous()->View({B, T, local_C}); + } else { + // (B, h_l, T, T) + auto att = q->Matmul(k->Transpose(-2, -1)) * (1.0 / std::sqrt(head_dim)); + // (1, 1, T, T) + auto mask = buffers_[kParamBiasName]->Slice({0, 0, 0, 0}, {1, 1, T, T}, {1, 1, 1, 1}); + // (1, 1, T, T) -> eq 0 -> (1, 1, T, T) -> masked_fill -> (B, h_l, T, T) + att = att->MaskedFill(mask == 0, -std::numeric_limits::infinity()); + // (B, h_l, T, T) + att = nn::function::Softmax(att, -1); + // (B, h_l, T, Dh) + y = att->Matmul(v); + // (B, h_l, T, Dh) -> (B, T, h_l, Dh) -> (B, T, local_C) + y = y->Transpose(1, 2)->Contiguous()->View({B, T, local_C}); + } // Get full tensor // (B, T, local_C) -> RowParallelLinear(n_embd, n_embd) -> (B, T, C) diff --git a/example/gpt2/net.h b/example/gpt2/net.h index 4faf5451..7f246c22 100644 --- a/example/gpt2/net.h +++ b/example/gpt2/net.h @@ -19,6 +19,7 @@ struct GPT2Config { int64_t n_layer = 12; int64_t n_head = 12; int64_t n_embd = 768; + bool flash = false; }; class NewGELU : public infini_train::nn::CloneableModule { diff --git a/example/llama3/main.cc b/example/llama3/main.cc index 6d4c9a7b..22991eeb 100644 --- a/example/llama3/main.cc +++ b/example/llama3/main.cc @@ -70,6 +70,7 @@ DEFINE_uint32(tensor_parallel, 1, "Tensor Parallel world size"); DEFINE_bool(sequence_parallel, false, "Whether to enable Sequence Parallel"); DEFINE_uint32(pipeline_parallel, 1, "Pipeline Parallel world size, specified the number of PP stages."); DEFINE_uint32(virtual_pipeline_parallel, 1, "Number of chunks in PP stage."); +DEFINE_bool(flash, false, "Whether to enable FlashAttention in CausalSelfAttention"); // precision DEFINE_string(dtype, "float32", "precision used in training (float32/bfloat16)"); // precision check @@ -161,8 +162,12 @@ void Train(const nn::parallel::Rank &rank) { // ManualSeed(42); LLaMA3Config model_config = LLaMA3Config(); + model_config.flash = FLAGS_flash; std::shared_ptr model = nullptr; if (!FLAGS_llmc_filepath.empty()) { + if (FLAGS_flash) { + LOG(WARNING) << "--flash is ignored when loading LLaMA3 from --llmc_filepath."; + } model = LLaMA3::FromLLMC(FLAGS_llmc_filepath); } else { model = std::make_shared(model_config); diff --git a/example/llama3/net.cc b/example/llama3/net.cc index a50fb831..fe725992 100644 --- a/example/llama3/net.cc +++ b/example/llama3/net.cc @@ -14,6 +14,7 @@ #include "glog/logging.h" #include "example/common/utils.h" +#include "infini_train/include/autograd/ScaledDotProductAttention.h" #include "infini_train/include/device.h" #include "infini_train/include/nn/functional.h" #include "infini_train/include/nn/init.h" @@ -207,36 +208,38 @@ std::vector> CausalSelfAttention::Forward(const std::vec // TODO(zbl): use kv cache during inference // if (use_kv_) { ... } - // align n_head in GQA - // (B, T, KV_local, D) -> (B, T, H_local, D) via RepeatKV - k = RepeatKV(k, n_rep_); - v = RepeatKV(v, n_rep_); - - // (B, T, H_local, D) -> (B, H_local, T, D) - q = q->Transpose(1, 2); - k = k->Transpose(1, 2); - v = v->Transpose(1, 2); - - // TODO(zbl): support flash attention later - // if (flash_) { ... } - - // manual implementation of attention - // this materializes the large (T,T) matrix for all the queries and keys - - // q: (B, H_local, T, D) - // k: (B, H_local, T, D) -> (B, H_local, D, T) - // q @ k.T: (B, H_local, T, T) -> mul 1.0 / sqrt(D) -> (B, H_local, T, T) - auto att = q->Matmul(k->Transpose(-2, -1)) * (1.0 / std::sqrt(static_cast(D))); - if (mask) { - // mask: (1, 1, T, T) - att = att->MaskedFill(mask, std::numeric_limits::lowest()); + std::shared_ptr y = nullptr; + if (config_.flash) { + auto y_flash = std::make_shared( + /*attn_mask=*/nullptr, /*dropout_p=*/0, /*is_causal=*/true, + /*scale=*/1.0 / std::sqrt(static_cast(D)), /*enable_gqa=*/true) + ->Apply({q, k, v})[0]; + y = y_flash->Contiguous()->View({B, T, C_local}); + } else { + // align n_head in GQA + // (B, T, KV_local, D) -> (B, T, H_local, D) via RepeatKV + k = RepeatKV(k, n_rep_); + v = RepeatKV(v, n_rep_); + + // (B, T, H_local, D) -> (B, H_local, T, D) + q = q->Transpose(1, 2); + k = k->Transpose(1, 2); + v = v->Transpose(1, 2); + + // manual implementation of attention + // this materializes the large (T,T) matrix for all the queries and keys + auto att = q->Matmul(k->Transpose(-2, -1)) * (1.0 / std::sqrt(static_cast(D))); + if (mask) { + // mask: (1, 1, T, T) + att = att->MaskedFill(mask, std::numeric_limits::lowest()); + } + // (B, H_local, T, T) + att = nn::function::Softmax(att, -1); + // att: (B, H_local, T, T) @ v: (B, H_local, T, D) -> y: (B, H_local, T, D) + y = att->Matmul(v); + // (B, H_local, T, D) -> Transpose(1, 2) -> (B, T, H_local, D) -> (B, T, C_local) + y = y->Transpose(1, 2)->Contiguous()->View({B, T, C_local}); } - // (B, H_local, T, T) - att = nn::function::Softmax(att, -1); - // att: (B, H_local, T, T) @ v: (B, H_local, T, D) -> y: (B, H_local, T, D) - auto y = att->Matmul(v); - // (B, H_local, T, D) -> Transpose(1, 2) -> (B, T, H_local, D) -> (B, T, C_local) - y = y->Transpose(1, 2)->Contiguous()->View({B, T, C_local}); // output projection // (B, T, C_local) -> RowParallelLinear(C, C) -> (B, T, C) y = (*modules_[kCProjLayerName])({y})[0]; diff --git a/example/llama3/net.h b/example/llama3/net.h index 4496a68d..144a7e78 100644 --- a/example/llama3/net.h +++ b/example/llama3/net.h @@ -36,7 +36,7 @@ struct LLaMA3Config { // Inference bool use_kv = false; // kv cache - bool flash = false; // flash attention + bool flash = false; // enable flash attention path in CausalSelfAttention int64_t max_gen_batch_size = 4; // max batch size during inference }; diff --git a/infini_train/src/kernels/cuda/flash_attention.cu b/infini_train/src/kernels/cuda/flash_attention.cu index bdd86a27..3bedca75 100644 --- a/infini_train/src/kernels/cuda/flash_attention.cu +++ b/infini_train/src/kernels/cuda/flash_attention.cu @@ -74,6 +74,25 @@ __device__ T warp_reduce_max(T val){ return val; } +__device__ __forceinline__ bool FlashAttnDropoutKeep( + unsigned long long seed, + int batch_idx, int head_idx, int q_idx, int k_idx, + int q_heads, int target_seq_len, int src_seq_len, + float dropout_p) { + // Stateless RNG mapping for one attention element (q_idx, k_idx). + unsigned long long linear_idx = + (((static_cast(batch_idx) * static_cast(q_heads) + + static_cast(head_idx)) + * static_cast(target_seq_len) + + static_cast(q_idx)) + * static_cast(src_seq_len)) + + static_cast(k_idx); + curandStatePhilox4_32_10_t state; + curand_init(seed, linear_idx, 0, &state); + float rand_val = curand_uniform(&state); + return rand_val > dropout_p; +} + /** * FlashAttention Forward Kernel @@ -173,12 +192,6 @@ __global__ void FlashAttentionForwardKernel( } __syncthreads(); - // Initialize dropout random state with dropout_seed - curandStatePhilox4_32_10_t state; - if (dropout_p > 0) { - unsigned long long seq = bid_y * q_heads * target_seq_len + bid_x * target_seq_len + Br * bid_z + tid_y; - curand_init(dropout_seed, seq, 0, &state); - } /****************************end-of-preparation*************************/ /****************************main-loop**************************/ @@ -260,8 +273,11 @@ __global__ void FlashAttentionForwardKernel( // step-5.5: Apply dropout to P (using dropout_seed for reproducibility) if (dropout_p > 0 && tid_y < bound_tid_y && tid_x < bound_tid_x) { - float rand_val = curand_uniform(&state); - bool keep = rand_val > dropout_p; + int global_q_idx = Br * bid_z + tid_y; + int global_k_idx = Bc * j + tid_x; + bool keep = FlashAttnDropoutKeep( + dropout_seed, bid_y, bid_x, global_q_idx, global_k_idx, + q_heads, target_seq_len, src_seq_len, static_cast(dropout_p)); if (keep) { SP_AT(tid_y, tid_x) = SP_AT(tid_y, tid_x) / (1.0f - dropout_p); } else { @@ -421,13 +437,6 @@ __global__ void FlashAttentionBackwardKernel( #define L_sm_AT(y) L_sm[y] #define dQ_sm_AT(y, x) dQ_sm[y * head_dim + x] - // Initialize dropout random state - curandStatePhilox4_32_10_t state; - if (dropout_p > 0) { - unsigned long long seq = bid_y * q_heads * target_seq_len + bid_x * target_seq_len + Br * bid_z + tid_y; - curand_init(dropout_seed, seq, 0, &state); - } - /****************************Preparation*****************************/ // Initialize dQ_sm to 0 for accumulation @@ -559,8 +568,11 @@ __global__ void FlashAttentionBackwardKernel( for (int y = 0; y < Bc; ++y) { for (int x = 0; x < Bc; ++x) { if (tid_y < bound_tid_y && tid_x < bound_tid_x) { - float rand_val = curand_uniform(&state); - bool keep = rand_val > dropout_p; + int global_q_pos = q_tile_start + tid_y; + int global_k_pos = Bc * j + x; + bool keep = FlashAttnDropoutKeep( + dropout_seed, bid_y, bid_x, global_q_pos, global_k_pos, + q_heads, target_seq_len, src_seq_len, static_cast(dropout_p)); if (keep) { P_sm_AT(tid_y, x) = P_sm_AT(tid_y, x) / (1.0f - dropout_p); } else { @@ -610,8 +622,11 @@ __global__ void FlashAttentionBackwardKernel( for (int y = 0; y < Bc; ++y) { for (int x = 0; x < Bc; ++x) { if (tid_y < bound_tid_y && tid_x < bound_tid_x) { - float rand_val = curand_uniform(&state); - bool keep = rand_val > dropout_p; + int global_q_pos = q_tile_start + tid_y; + int global_k_pos = Bc * j + x; + bool keep = FlashAttnDropoutKeep( + dropout_seed, bid_y, bid_x, global_q_pos, global_k_pos, + q_heads, target_seq_len, src_seq_len, static_cast(dropout_p)); if (keep) { S_sm_AT(tid_y, x) = S_sm_AT(tid_y, x) / (1.0f - dropout_p); } else { @@ -757,7 +772,7 @@ template void LaunchFlashAttentionForward(const std::shared_ptr &output, const std::shared_ptr &query, const std::shared_ptr &key, const std::shared_ptr &value, const std::shared_ptr &attn_mask, float scale, bool is_causal, - int64_t dropout_p, bool enable_gqa, float *logsumexp_ptr, unsigned long long *dropout_seed_ptr) { + int64_t dropout_p, bool enable_gqa, float *logsumexp_ptr, unsigned long long dropout_seed) { const auto &query_dims = query->Dims(); const auto &key_dims = key->Dims(); @@ -997,11 +1012,11 @@ FlashAttentionForwardOutput FlashAttentionForward(const std::shared_ptr switch (dtype) { DISPATCH_CASE(WRAP(LaunchFlashAttentionForward(output, query, key, value, attn_mask, scale, is_causal, dropout_p, enable_gqa, logsumexp_ptr, - dropout_seed_tensor ? static_cast(dropout_seed_tensor->DataPtr()) : nullptr);), + dropout_seed);), DataType::kFLOAT32) DISPATCH_CASE(WRAP(LaunchFlashAttentionForward(output, query, key, value, attn_mask, scale, is_causal, dropout_p, enable_gqa, logsumexp_ptr, - dropout_seed_tensor ? static_cast(dropout_seed_tensor->DataPtr()) : nullptr);), + dropout_seed);), DataType::kBFLOAT16) default: LOG_LOC(FATAL, "CUDA FlashAttention forward: 'Unsupported data type'"); From 89366bbd5d0934b2956380f44024cb77c04286d8 Mon Sep 17 00:00:00 2001 From: Aoshine999 <820459542@qq.com> Date: Tue, 10 Mar 2026 15:49:13 +0800 Subject: [PATCH 03/18] complete synax error --- example/gpt2/main.cc | 2 +- .../src/autograd/ScaledDotProductAttention.cc | 121 +- .../src/kernels/cuda/flash_attention.cu | 49 +- .../src/kernels/cuda/flash_attention1.cu | 2218 ++++++++--------- scripts/test_config.json | 8 +- 5 files changed, 1124 insertions(+), 1274 deletions(-) diff --git a/example/gpt2/main.cc b/example/gpt2/main.cc index 2b2f981d..d1c82f5b 100644 --- a/example/gpt2/main.cc +++ b/example/gpt2/main.cc @@ -71,7 +71,7 @@ DEFINE_uint32(tensor_parallel, 1, "Tensor Parallel world size"); DEFINE_bool(sequence_parallel, false, "Whether to enable Sequence Parallel"); DEFINE_uint32(pipeline_parallel, 1, "Pipeline Parallel world size, specified the number of PP stages."); DEFINE_uint32(virtual_pipeline_parallel, 1, "Number of chunks in PP stage."); -DEFINE_bool(flash, false, "Whether to enable FlashAttention in CausalSelfAttention"); +DEFINE_bool(flash, true, "Whether to enable FlashAttention in CausalSelfAttention"); // precision DEFINE_string(dtype, "float32", "precision used in training (float32/bfloat16)"); diff --git a/infini_train/src/autograd/ScaledDotProductAttention.cc b/infini_train/src/autograd/ScaledDotProductAttention.cc index 9aced35f..33d2ba80 100644 --- a/infini_train/src/autograd/ScaledDotProductAttention.cc +++ b/infini_train/src/autograd/ScaledDotProductAttention.cc @@ -7,7 +7,6 @@ #include "infini_train/include/device.h" #include "infini_train/include/dispatcher.h" #include "infini_train/include/kernels/cuda/flash_attention.h" -#include "infini_train/include/nn/functional.h" #include "infini_train/include/tensor.h" namespace infini_train::autograd { @@ -27,39 +26,19 @@ ScaledDotProductAttention::Forward(const std::vector> &i const float scale = static_cast( scale_.has_value() ? *scale_ : 1.0 / std::sqrt(static_cast(query_last_dim))); - // Use FlashAttention kernel for CUDA device + // ScaledDotProductAttention only supports CUDA device auto device = query->GetDevice().type(); - if (device == Device::Device::DeviceType::kCUDA) { - // Note: is_causal and enable_gqa are supported in FlashAttention kernel - // Dropout is now supported using seed-based approach for reproducibility - flash_output_ = Dispatcher::Instance().Call( - {device, "FlashAttentionForward"}, query, key, value, attn_mask_, scale, is_causal_, dropout_p_, enable_gqa_); - - // Return output tensor only; saved_tensors_ will be set in SetupContext - return {flash_output_.output}; - } - - // Fallback to traditional implementation for CPU or other devices - CHECK_EQ(dropout_p_, 0) << "ScaledDotProductAttention dropout is not implemented yet"; - CHECK(!is_causal_) << "ScaledDotProductAttention causal mask is not implemented yet"; - CHECK(!enable_gqa_) << "ScaledDotProductAttention GQA mode is not implemented yet"; - - const int64_t key_rank = static_cast(key->Dims().size()); - auto key_t = key->Transpose(key_rank - 2, key_rank - 1); - - auto attn_scores = query->Matmul(key_t); - if (scale != 1.0f) { - attn_scores = attn_scores * scale; - } - if (attn_mask_) { - attn_scores = attn_scores + attn_mask_; - } - - auto attn_prob = nn::function::Softmax(attn_scores, -1); - auto output = attn_prob->Matmul(value); - + CHECK(device == Device::Device::DeviceType::kCUDA) + << "ScaledDotProductAttention only supports CUDA device. " + << "For CPU or other devices, please use traditional attention implementation."; + + // Note: is_causal and enable_gqa are supported in FlashAttention kernel + // Dropout is now supported using seed-based approach for reproducibility + flash_output_ = Dispatcher::Instance().Call( + {device, "FlashAttentionForward"}, query, key, value, attn_mask_, scale, is_causal_, dropout_p_, enable_gqa_); + // Return output tensor only; saved_tensors_ will be set in SetupContext - return {output}; + return {flash_output_.output}; } void ScaledDotProductAttention::SetupContext(const std::vector> &input_tensors, @@ -69,81 +48,35 @@ void ScaledDotProductAttention::SetupContext(const std::vectorGetDevice().type(); - if (device == Device::Device::DeviceType::kCUDA) { - // Save tensors for backward pass: - // - query, key, value: input tensors - // - output: forward output - // - logsumexp: logsumexp tensor for backward pass (from flash_output_) - // - dropout_seed: dropout seed tensor for backward pass (from flash_output_) - saved_tensors_ = {query, key, value, output, flash_output_.logsumexp, flash_output_.dropout_seed}; - } else { - // Fallback to traditional implementation for CPU or other devices - // Recompute attn_prob from output and value - const int64_t value_rank = static_cast(value->Dims().size()); - auto value_t = value->Transpose(value_rank - 2, value_rank - 1); - auto attn_prob = output->Matmul(value_t); - saved_tensors_ = {query, key, value, attn_prob}; - } + // Save tensors for backward pass: + // - query, key, value: input tensors + // - output: forward output + // - logsumexp: logsumexp tensor for backward pass (from flash_output_) + // - dropout_seed: dropout seed tensor for backward pass (from flash_output_) + saved_tensors_ = {query, key, value, output, flash_output_.logsumexp, flash_output_.dropout_seed}; } std::vector> ScaledDotProductAttention::Backward(const std::vector> &grad_outputs) { - CHECK_GE(saved_tensors_.size(), 4); + CHECK_EQ(saved_tensors_.size(), 6) << "FlashAttention backward expects 6 saved tensors"; CHECK_EQ(grad_outputs.size(), 1); const auto &query = saved_tensors_[0]; const auto &key = saved_tensors_[1]; const auto &value = saved_tensors_[2]; const auto &output = saved_tensors_[3]; + const auto &logsumexp = saved_tensors_[4]; + const auto &dropout_seed = saved_tensors_[5]; const auto &grad_output = grad_outputs[0]; - // Use FlashAttention backward kernel for CUDA device - auto device = query->GetDevice().type(); - if (device == Device::Device::DeviceType::kCUDA) { - CHECK_EQ(saved_tensors_.size(), 6) << "FlashAttention backward expects 6 saved tensors"; - const auto &logsumexp = saved_tensors_[4]; - const auto &dropout_seed = saved_tensors_[5]; - - const float scale = static_cast( - scale_.has_value() ? *scale_ : 1.0 / std::sqrt(static_cast(query->Dims().back()))); - - auto grad_tensors = Dispatcher::Instance().Call>>( - {device, "FlashAttentionBackward"}, query, key, value, output, grad_output, logsumexp, dropout_seed, attn_mask_, - scale, is_causal_, dropout_p_, enable_gqa_); - - return grad_tensors; - } - - // Fallback to traditional implementation for CPU or other devices - CHECK_EQ(saved_tensors_.size(), 4) << "Traditional backward expects 4 saved tensors"; - const auto &attn_prob = output; - const int64_t value_rank = static_cast(value->Dims().size()); - const int64_t key_rank = static_cast(key->Dims().size()); - const int64_t attn_rank = static_cast(attn_prob->Dims().size()); - - auto value_t = value->Transpose(value_rank - 2, value_rank - 1); - auto grad_attn_prob = grad_output->Matmul(value_t); - - // softmax backward: grad = (g - sum(g * y, dim=-1, keepdim=true)) * y - auto grad_dot = (grad_attn_prob * attn_prob)->Sum(-1, true); - auto grad_attn_scores = (grad_attn_prob - grad_dot) * attn_prob; - const float scale = static_cast( scale_.has_value() ? *scale_ : 1.0 / std::sqrt(static_cast(query->Dims().back()))); - if (scale != 1.0f) { - grad_attn_scores = grad_attn_scores * scale; - } - - auto grad_query = grad_attn_scores->Matmul(key); - - auto grad_attn_scores_t = grad_attn_scores->Transpose(attn_rank - 2, attn_rank - 1); - auto grad_key = grad_attn_scores_t->Matmul(query); - - auto attn_prob_t = attn_prob->Transpose(attn_rank - 2, attn_rank - 1); - auto grad_value = attn_prob_t->Matmul(grad_output); - - return {grad_query, grad_key, grad_value}; + + auto device = query->GetDevice().type(); + auto grad_tensors = Dispatcher::Instance().Call>>( + {device, "FlashAttentionBackward"}, query, key, value, output, grad_output, logsumexp, dropout_seed, attn_mask_, + scale, is_causal_, dropout_p_, enable_gqa_); + + return grad_tensors; } } // namespace infini_train::autograd diff --git a/infini_train/src/kernels/cuda/flash_attention.cu b/infini_train/src/kernels/cuda/flash_attention.cu index 3bedca75..c0ea807b 100644 --- a/infini_train/src/kernels/cuda/flash_attention.cu +++ b/infini_train/src/kernels/cuda/flash_attention.cu @@ -1,6 +1,6 @@ #include #include - +#include #include "glog/logging.h" #include "infini_train/include/common/cuda/common_cuda.h" @@ -8,6 +8,7 @@ #include "infini_train/include/core/runtime/device_guard.h" #include "infini_train/include/dispatcher.h" #include "infini_train/include/kernels/cuda/flash_attention.h" +#include "infini_train/include/nn/functional.h" #include "infini_train/include/tensor.h" #include "infini_train/src/core/runtime/cuda/cuda_runtime_common.h" @@ -222,9 +223,9 @@ __global__ void FlashAttentionForwardKernel( V_sm_AT(tid_y, idx) = 0.0; if (tid_y < bound_tid_x) { K_T_sm_AT(idx, tid_y) = float( - K[((((bid_y * src_seq_len) + (Bc * j + tid_y)) * kv_heads) + (bid_x / p)) * head_dim + idx]); + key[((((bid_y * src_seq_len) + (Bc * j + tid_y)) * kv_heads) + (bid_x / p)) * head_dim + idx]); V_sm_AT(tid_y, idx) = float( - V[((((bid_y * src_seq_len) + (Bc * j + tid_y)) * kv_heads) + (bid_x / p)) * head_dim + idx]); + value[((((bid_y * src_seq_len) + (Bc * j + tid_y)) * kv_heads) + (bid_x / p)) * head_dim + idx]); } } __syncthreads(); @@ -386,16 +387,13 @@ __global__ void FlashAttentionBackwardKernel( const int Br = blockDim.y; // Q纵向每块大小, 32 const int Bc = blockDim.x; // K/V纵向分块大小, 32 - const int Tr = gridDim.z; // Q纵向分块数 + // const int Tr = gridDim.z; // Q纵向分块数 const int Tc = (src_seq_len + Bc - 1) / Bc; // K/V纵向分块数 // Define shared memory extern __shared__ char shared_mem[]; char *ptr = shared_mem; - // D_sm[Br] - D values loaded from HBM - float *D_sm = reinterpret_cast(ptr); - ptr += Br * sizeof(float); // Q_sm[Br][head_dim], K_T_sm[head_dim][Bc], V_sm[Bc][head_dim] float *Q_sm = reinterpret_cast(ptr); @@ -405,9 +403,12 @@ __global__ void FlashAttentionBackwardKernel( float *V_sm = reinterpret_cast(ptr); ptr += Bc * head_dim * sizeof(float); - // dO_sm[Br][head_dim], dK_T_sm[head_dim][Bc], dV_sm[Bc][head_dim] + // dO_sm[Br][head_dim], dQ_sm[Br][head_dim], dK_T_sm[head_dim][Bc], dV_sm[Bc][head_dim] float *dO_sm = reinterpret_cast(ptr); ptr += Br * head_dim * sizeof(float); + // dQ_sm[Br][head_dim] - accumulated gradient for Q + float *dQ_sm = reinterpret_cast(ptr); + ptr += Br * head_dim * sizeof(float); float *dK_T_sm = reinterpret_cast(ptr); ptr += head_dim * Bc * sizeof(float); float *dV_sm = reinterpret_cast(ptr); @@ -417,25 +418,28 @@ __global__ void FlashAttentionBackwardKernel( float *S_sm = reinterpret_cast(ptr); ptr += Br * Bc * sizeof(float); float *P_sm = reinterpret_cast(ptr); - + ptr += Br * Bc * sizeof(float); + // L_sm[Br] - logsumexp values float *L_sm = reinterpret_cast(ptr); - - // dQ_sm[Br][head_dim] - accumulated gradient for Q - float *dQ_sm = reinterpret_cast(ptr); + ptr += Br * sizeof(float); + // D_sm[Br] - D values loaded from HBM + float *D_sm = reinterpret_cast(ptr); + ptr += Br * sizeof(float); + // Define access macros - #define D_sm_AT(y) D_sm[y] #define Q_sm_AT(y, x) Q_sm[y * head_dim + x] #define K_T_sm_AT(y, x) K_T_sm[y * Bc + x] #define V_sm_AT(y, x) V_sm[y * head_dim + x] #define dO_sm_AT(y, x) dO_sm[y * head_dim + x] + #define dQ_sm_AT(y, x) dQ_sm[y * head_dim + x] #define dK_T_sm_AT(y, x) dK_T_sm[y * Bc + x] #define dV_sm_AT(y, x) dV_sm[y * head_dim + x] #define S_sm_AT(y, x) S_sm[y * Bc + x] #define P_sm_AT(y, x) P_sm[y * Bc + x] #define L_sm_AT(y) L_sm[y] - #define dQ_sm_AT(y, x) dQ_sm[y * head_dim + x] + #define D_sm_AT(y) D_sm[y] /****************************Preparation*****************************/ @@ -482,9 +486,9 @@ __global__ void FlashAttentionBackwardKernel( if (tid_y < bound_tid_x) { int kv_head_idx = bid_x / p; K_T_sm_AT(idx, tid_y) = float( - key[((((bid_y * src_seq_len) + (Bc * j + tid_y)) * kv_heads + kv_head_idx) * head_dim + idx]); + key[((((bid_y * src_seq_len) + (Bc * j + tid_y)) * kv_heads + kv_head_idx)) * head_dim + idx]); V_sm_AT(tid_y, idx) = float( - value[((((bid_y * src_seq_len) + (Bc * j + tid_y)) * kv_heads + kv_head_idx) * head_dim + idx]); + value[((((bid_y * src_seq_len) + (Bc * j + tid_y)) * kv_heads + kv_head_idx)) * head_dim + idx]); } } __syncthreads(); @@ -593,7 +597,7 @@ __global__ void FlashAttentionBackwardKernel( for (int y = 0; y < Bc; ++y) { val += P_sm_AT(tid_y, y) * dO_sm_AT(tid_y, x); } - atomicAdd(&dV_sm_AT(y, x), val); + atomicAdd(&dV_sm_AT(tid_y, x), val); } } __syncthreads(); @@ -823,7 +827,7 @@ void LaunchFlashAttentionForward(const std::shared_ptr &output, const st // + Q_sm[Br][head_dim] (float) + K_T_sm[head_dim][Bc] (float) + V_sm[Bc][head_dim] (float) + O_sm[Br][head_dim] (float) size_t shared_mem_size = Br * Bc * sizeof(double) // SP + 4 * Br * sizeof(float) // m_prev, m_new, l_prev, l_new - + (Br + Bc + Bc + Br) * head_dim) * sizeof(float); // Q_sm, K_T_sm, V_sm, O_sm + + (Br + Bc + Bc + Br) * head_dim * sizeof(float); // Q_sm, K_T_sm, V_sm, O_sm // Note: Removed dropout_sm[Br][Bc] (bool) allocation auto device = output->GetDevice(); @@ -900,7 +904,7 @@ void LaunchFlashAttentionBackward(const std::shared_ptr &grad_query, con // D shape: [batch_size, seq_len_q, num_heads] // dO shape: [batch_size, seq_len_q, num_heads, head_dim] // O shape: [batch_size, seq_len_q, num_heads, head_dim] - auto D = function::Sum(grad_output * output, 3, false); + auto D = infini_train::nn::function::Sum(grad_output * output, 3, false); T *grad_query_ptr = static_cast(grad_query->DataPtr()); T *grad_key_ptr = static_cast(grad_key->DataPtr()); @@ -933,6 +937,7 @@ void LaunchFlashAttentionBackward(const std::shared_ptr &grad_query, con + head_dim * Bc * sizeof(float) // K_T_sm + Bc * head_dim * sizeof(float) // V_sm + Br * head_dim * sizeof(float) // dO_sm + + Br * head_dim * sizeof(float) // dQ_sm (accumulated gradient for Q) + head_dim * Bc * sizeof(float) // dK_T_sm + Bc * head_dim * sizeof(float) // dV_sm + Br * Bc * sizeof(float) // S_sm or (dP_sm when compute dP_sm = dO_i @ V_j^T \in R^{Br*Bc}) @@ -1029,9 +1034,9 @@ FlashAttentionForwardOutput FlashAttentionForward(const std::shared_ptr return result; } -std::vector> FlashAttentionBackward(const std::shared_ptr &grad_query, const std::shared_ptr &grad_key, - const std::shared_ptr &grad_value, const std::shared_ptr &query, - const std::shared_ptr &key, const std::shared_ptr &value, +std::vector> FlashAttentionBackward(const std::shared_ptr &query, + const std::shared_ptr &key, + const std::shared_ptr &value, const std::shared_ptr &output, const std::shared_ptr &grad_output, const std::shared_ptr &logsumexp, const std::shared_ptr &dropout_seed, diff --git a/infini_train/src/kernels/cuda/flash_attention1.cu b/infini_train/src/kernels/cuda/flash_attention1.cu index 75931887..9a78f802 100644 --- a/infini_train/src/kernels/cuda/flash_attention1.cu +++ b/infini_train/src/kernels/cuda/flash_attention1.cu @@ -1,1171 +1,1083 @@ -#include -#include - -#include "glog/logging.h" - -#include "infini_train/include/common/cuda/common_cuda.h" -#include "infini_train/include/common/cuda/kernel_helper.cuh" -#include "infini_train/include/core/runtime/device_guard.h" -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/kernels/cuda/flash_attention.h" -#include "infini_train/include/tensor.h" - -#include "infini_train/src/core/runtime/cuda/cuda_runtime_common.h" - -namespace infini_train::kernels::cuda { - -/** - * FlashAttention Forward Kernel - * - * This kernel implements the FlashAttention algorithm for efficient attention computation. - * It uses tiling and recomputation to reduce memory access and improve performance. - * - * Args: - * output: Output tensor of shape [batch_size, seq_len_q, num_heads, head_dim] - * query: Query tensor of shape [batch_size, seq_len_q, num_heads, head_dim] - * key: Key tensor of shape [batch_size, seq_len_k, num_heads_kv, head_dim] - * value: Value tensor of shape [batch_size, seq_len_k, num_heads_kv, head_dim] - * attn_mask: Optional attention mask tensor - * logsumexp: Save logsumexp for backward pass - * scale: Scaling factor for attention scores - * is_causal: Whether to apply causal masking - * enable_gqa: Whether to enable grouped-query attention - * dropout_p: Dropout probability - * dropout_seed: Random seed for dropout (for reproducibility) - * batch_size, target_seq_len, src_seq_len: Tensor dimensions - * q_heads, kv_heads, head_dim: Attention head dimensions - * - * Note: Actual kernel implementation is not included in this version. - */ -template -__device__ T warp_reduce_sum(T val){ -#pragma unroll//短循环自动展开,省去分支预测,提升效率 - for(int offset = 16; offset > 0; offset >>= 1){ - val += __shfl_down_sync(0xffffffff, val, offset); - } - return val; -} - -template -__device__ T myexp(T x) { - if constexpr(std::is_same::value) { - float fx = __half2float(x); - float result = expf(fx); - return __float2half(result); - } - else if constexpr(std::is_same::value) { - return expf(x); // expf返回float - } - else if constexpr(std::is_same::value) { - return exp(x); // exp返回double - } - else{//other types - return T(0); - } -} - -template -__device__ T warp_reduce_max(T val){ -#pragma unroll//短循环自动展开,省去分支预测,提升效率 - for(int offset = 16; offset > 0; offset >>= 1){ - T tmp = __shfl_down_sync(0xffffffff, val, offset); - val = (val > tmp) ? val : tmp; - } - return val; -} - - -/** - * FlashAttention Forward Kernel - * - * This kernel implements the FlashAttention algorithm for efficient attention computation. - * It uses tiling and recomputation to reduce memory access and improve performance. - * - * Args: - * output: Output tensor of shape [batch_size, seq_len_q, num_heads, head_dim] - * query: Query tensor of shape [batch_size, seq_len_q, num_heads, head_dim] - * key: Key tensor of shape [batch_size, seq_len_k, num_heads_kv, head_dim] - * value: Value tensor of shape [batch_size, seq_len_k, num_heads_kv, head_dim] - * attn_mask: Optional attention mask tensor - * logsumexp: Save logsumexp for backward pass - * scale: Scaling factor for attention scores - * is_causal: Whether to apply causal masking - * enable_gqa: Whether to enable grouped-query attention - * dropout_p: Dropout probability - * dropout_seed: Random seed for dropout (for reproducibility) - * batch_size, target_seq_len, src_seq_len: Tensor dimensions - * q_heads, kv_heads, head_dim: Attention head dimensions - * - * Note: Actual kernel implementation is not included in this version. - */ -template -__global__ void FlashAttentionForwardKernel( - T *output, const T *query, const T *key, const T *value, const T *attn_mask, - float *logsumexp, // Save logsumexp for backward pass - float scale, bool is_causal, bool enable_gqa, int64_t dropout_p, - unsigned long long dropout_seed, // Use dropout_seed instead of dropout_mask - int batch_size, int target_seq_len, int src_seq_len, - int q_heads, int kv_heads, int head_dim) { +// #include +// #include +// #include +// #include "glog/logging.h" + +// #include "infini_train/include/common/cuda/common_cuda.h" +// #include "infini_train/include/common/cuda/kernel_helper.cuh" +// #include "infini_train/include/core/runtime/device_guard.h" +// #include "infini_train/include/dispatcher.h" +// #include "infini_train/include/kernels/cuda/flash_attention.h" +// #include "infini_train/include/tensor.h" + +// #include "infini_train/src/core/runtime/cuda/cuda_runtime_common.h" + +// namespace infini_train::kernels::cuda { + +// /** +// * FlashAttention Forward Kernel +// * +// * This kernel implements the FlashAttention algorithm for efficient attention computation. +// * It uses tiling and recomputation to reduce memory access and improve performance. +// * +// * Args: +// * output: Output tensor of shape [batch_size, seq_len_q, num_heads, head_dim] +// * query: Query tensor of shape [batch_size, seq_len_q, num_heads, head_dim] +// * key: Key tensor of shape [batch_size, seq_len_k, num_heads_kv, head_dim] +// * value: Value tensor of shape [batch_size, seq_len_k, num_heads_kv, head_dim] +// * attn_mask: Optional attention mask tensor +// * logsumexp: Save logsumexp for backward pass +// * scale: Scaling factor for attention scores +// * is_causal: Whether to apply causal masking +// * enable_gqa: Whether to enable grouped-query attention +// * dropout_p: Dropout probability +// * dropout_seed: Random seed for dropout (for reproducibility) +// * batch_size, target_seq_len, src_seq_len: Tensor dimensions +// * q_heads, kv_heads, head_dim: Attention head dimensions +// * +// * Note: Actual kernel implementation is not included in this version. +// */ +// template +// __device__ T warp_reduce_sum(T val){ +// #pragma unroll//短循环自动展开,省去分支预测,提升效率 +// for(int offset = 16; offset > 0; offset >>= 1){ +// val += __shfl_down_sync(0xffffffff, val, offset); +// } +// return val; +// } + +// template +// __device__ T myexp(T x) { +// if constexpr(std::is_same::value) { +// float fx = __half2float(x); +// float result = expf(fx); +// return __float2half(result); +// } +// else if constexpr(std::is_same::value) { +// return expf(x); // expf返回float +// } +// else if constexpr(std::is_same::value) { +// return exp(x); // exp返回double +// } +// else{//other types +// return T(0); +// } +// } + +// template +// __device__ T warp_reduce_max(T val){ +// #pragma unroll//短循环自动展开,省去分支预测,提升效率 +// for(int offset = 16; offset > 0; offset >>= 1){ +// T tmp = __shfl_down_sync(0xffffffff, val, offset); +// val = (val > tmp) ? val : tmp; +// } +// return val; +// } + +// __device__ __forceinline__ bool FlashAttnDropoutKeep( +// unsigned long long seed, +// int batch_idx, int head_idx, int q_idx, int k_idx, +// int q_heads, int target_seq_len, int src_seq_len, +// float dropout_p) { +// // Stateless RNG mapping for one attention element (q_idx, k_idx). +// unsigned long long linear_idx = +// (((static_cast(batch_idx) * static_cast(q_heads) +// + static_cast(head_idx)) +// * static_cast(target_seq_len) +// + static_cast(q_idx)) +// * static_cast(src_seq_len)) +// + static_cast(k_idx); +// curandStatePhilox4_32_10_t state; +// curand_init(seed, linear_idx, 0, &state); +// float rand_val = curand_uniform(&state); +// return rand_val > dropout_p; +// } + + +// /** +// * FlashAttention Forward Kernel +// * +// * This kernel implements the FlashAttention algorithm for efficient attention computation. +// * It uses tiling and recomputation to reduce memory access and improve performance. +// * +// * Args: +// * output: Output tensor of shape [batch_size, seq_len_q, num_heads, head_dim] +// * query: Query tensor of shape [batch_size, seq_len_q, num_heads, head_dim] +// * key: Key tensor of shape [batch_size, seq_len_k, num_heads_kv, head_dim] +// * value: Value tensor of shape [batch_size, seq_len_k, num_heads_kv, head_dim] +// * attn_mask: Optional attention mask tensor +// * logsumexp: Save logsumexp for backward pass +// * scale: Scaling factor for attention scores +// * is_causal: Whether to apply causal masking +// * enable_gqa: Whether to enable grouped-query attention +// * dropout_p: Dropout probability +// * dropout_seed: Random seed for dropout (for reproducibility) +// * batch_size, target_seq_len, src_seq_len: Tensor dimensions +// * q_heads, kv_heads, head_dim: Attention head dimensions +// * +// * Note: Actual kernel implementation is not included in this version. +// */ +// template +// __global__ void FlashAttentionForwardKernel( +// T *output, const T *query, const T *key, const T *value, const T *attn_mask, +// float *logsumexp, // Save logsumexp for backward pass +// float scale, bool is_causal, bool enable_gqa, int64_t dropout_p, +// unsigned long long dropout_seed, // Use dropout_seed instead of dropout_mask +// int batch_size, int target_seq_len, int src_seq_len, +// int q_heads, int kv_heads, int head_dim) { - int tid_x = threadIdx.x; // 横向,blockDim.x列 (Bc) - int tid_y = threadIdx.y; // 纵向,blockDim.y行 (Br) - int bid_x = blockIdx.x; // x方向,总数 = #q_heads - int bid_y = blockIdx.y; // y方向,总数 = #batch - int bid_z = blockIdx.z; // z方向,总数 = Tr - const int p = q_heads / kv_heads; // 计算比例系数,GQA支持 - - const int Br = blockDim.y; // Q纵向每块大小, 32 - const int Bc = blockDim.x; // K/V纵向分块大小, 32 - const int Tc = gridDim.z; // 对应原始论文中K/V纵向分块数Tc,其中Bc = 32 - - // 定义一系列临时变量 - extern __shared__ char shared_mem[]; - char *ptr = shared_mem; +// int tid_x = threadIdx.x; // 横向,blockDim.x列 (Bc) +// int tid_y = threadIdx.y; // 纵向,blockDim.y行 (Br) +// int bid_x = blockIdx.x; // x方向,总数 = #q_heads +// int bid_y = blockIdx.y; // y方向,总数 = #batch +// int bid_z = blockIdx.z; // z方向,总数 = Tr +// const int p = q_heads / kv_heads; // 计算比例系数,GQA支持 + +// const int Br = blockDim.y; // Q纵向每块大小, 32 +// const int Bc = blockDim.x; // K/V纵向分块大小, 32 +// const int Tc = gridDim.z; // 对应原始论文中K/V纵向分块数Tc,其中Bc = 32 + +// // 定义一系列临时变量 +// extern __shared__ char shared_mem[]; +// char *ptr = shared_mem; - // 计算中间变量,包括S, P(复用为SP), m_prev, m_new, l_prev, l_new - double *SP = reinterpret_cast(ptr); // double SP[Br][Bc] - ptr += Br * Bc * sizeof(double); - float *m_prev = reinterpret_cast(ptr); // float m_prev[Br] - ptr += Br * sizeof(float); - float *m_new = reinterpret_cast(ptr); // float m_new[Br] - ptr += Br * sizeof(float); - float *l_prev = reinterpret_cast(ptr); // float l_prev[Br] - ptr += Br * sizeof(float); - float *l_new = reinterpret_cast(ptr); // float l_new[Br] - ptr += Br * sizeof(float); - - // 原始数据QKV和计算结果O; 全采用float - float *Q_sm = reinterpret_cast(ptr); // float Q_sm[Br][head_dim] - ptr += Br * head_dim * sizeof(float); - float *K_T_sm = reinterpret_cast(ptr); // float K_T_sm[head_dim][Bc] - ptr += head_dim * Bc * sizeof(float); - float *V_sm = reinterpret_cast(ptr); // float V_sm[Bc][head_dim] - ptr += Bc * head_dim * sizeof(float); - float *O_sm = reinterpret_cast(ptr); // float O_sm[Br][head_dim] - // Note: Removed dropout_sm shared memory allocation - - // 定义访问宏 -#define SP_AT(y, x) SP[y * Bc + x] -#define Q_sm_AT(y, x) Q_sm[y * head_dim + x] -#define K_T_sm_AT(y, x) K_T_sm[y * Bc + x] -#define V_sm_AT(y, x) V_sm[y * head_dim + x] -#define O_sm_AT(y, x) O_sm[y * head_dim + x] - // Note: Removed DROPOUT_SM_AT macro - - /****************************preparation**************************/ - int bound_tid_y = min(Br, target_seq_len - Br * bid_z); - - // preparation-1: load Qi from GM to SM, and reset Oi to 0 - for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { - O_sm_AT(tid_y, idx) = 0.0; - Q_sm_AT(tid_y, idx) = 0.0; - if (tid_y < bound_tid_y) { - Q_sm_AT(tid_y, idx) - = float(query[((((bid_y * target_seq_len) + (Br * bid_z + tid_y)) * q_heads) + bid_x) * head_dim + idx]); - } - } - __syncthreads(); - - // preparation-2: reset m_prev to -INFINITY and l_prev to 0 - if (tid_x == 0) { - m_prev[tid_y] = -8192.0; - l_prev[tid_y] = 0.0; - } - __syncthreads(); - - // Initialize dropout random state with dropout_seed - curandStatePhilox4_32_10_t state; - if (dropout_p > 0) { - unsigned long long seq = bid_y * q_heads * target_seq_len + bid_x * target_seq_len + Br * bid_z + tid_y; - curand_init(dropout_seed, seq, 0, &state); - } -/****************************end-of-preparation*************************/ - -/****************************main-loop**************************/ -#pragma unroll 4 - for (int j = 0; j < Tc; ++j) { // 对于每个K/V分块 - bool skip = (is_causal && bid_z < j); - if (skip) { // early exit, 直接跳过 - __syncthreads(); - continue; - } - - SP_AT(tid_y, tid_x) = -8192.0; - __syncthreads(); - int bound_tid_x = min(Bc, src_seq_len - Bc * j); - bool is_compute = true; // optimization: 分支处理,加速branch-resolving - if (is_causal) { - if (bid_z < j) { - is_compute = false; // 早期退出情况 - } else if (bid_z == j) { - is_compute = (tid_y >= tid_x); // 对角线以上 - } - } - - // step-1: load Ki, Vi from GM to SM -#pragma unroll - for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { - K_T_sm_AT(idx, tid_y) = 0.0; - V_sm_AT(tid_y, idx) = 0.0; - if (tid_y < bound_tid_x) { - K_T_sm_AT(idx, tid_y) = float( - K[((((bid_y * src_seq_len) + (Bc * j + tid_y)) * kv_heads) + (bid_x / p)) * head_dim + idx]); - V_sm_AT(tid_y, idx) = float( - V[((((bid_y * src_seq_len) + (Bc * j + tid_y)) * kv_heads) + (bid_x / p)) * head_dim + idx]); - } - } - __syncthreads(); - - // step-2: S = Q @ K.T, point-wise - if (tid_y < bound_tid_y && tid_x < bound_tid_x) { - float val0 = 0.0; - if (is_compute) { -#pragma unroll - for (int k = 0; k < head_dim; ++k) { - val0 += Q_sm_AT(tid_y, k) * K_T_sm_AT(k, tid_x); - } - SP_AT(tid_y, tid_x) = double(val0) * scale; - } - } - __syncthreads(); - - // step-3: m_new = max(m_prev, rowMax(S)) - float val1 = float(SP_AT(tid_y, tid_x)); - val1 = warp_reduce_max(val1); - if (tid_x == 0 && tid_y < bound_tid_y) { - m_new[tid_y] = (val1 > m_prev[tid_y]) ? val1 : m_prev[tid_y]; - } - __syncthreads(); - - // step-4: P = exp(S - m_new), point-wise - if (tid_y < bound_tid_y && tid_x < bound_tid_x) { - if (is_compute) { - SP_AT(tid_y, tid_x) = myexp(SP_AT(tid_y, tid_x) - double(m_new[tid_y])); - } else { - SP_AT(tid_y, tid_x) = 0.0; - } - } else { - SP_AT(tid_y, tid_x) = 0.0; - } - __syncthreads(); - - // step-5: l_new = exp(m_prev - m_new) * l_prev + rowSum(P) - float val2 = float(SP_AT(tid_y, tid_x)); - val2 = warp_reduce_sum(val2); - float exp_result = myexp(m_prev[tid_y] - m_new[tid_y]); - if (tid_x == 0 && tid_y < bound_tid_y) { - l_new[tid_y] = exp_result * l_prev[tid_y] + val2; - } - __syncthreads(); - - // step-5.5: Apply dropout to P (using dropout_seed for reproducibility) - if (dropout_p > 0 && tid_y < bound_tid_y && tid_x < bound_tid_x) { - float rand_val = curand_uniform(&state); - bool keep = rand_val > dropout_p; - if (keep) { - SP_AT(tid_y, tid_x) = SP_AT(tid_y, tid_x) / (1.0f - dropout_p); - } else { - SP_AT(tid_y, tid_x) = 0.0; - } - // Note: Removed dropout_sm mask saving to reduce memory overhead - } - __syncthreads(); - - // step-6: O = 1/(exp(m_prev - m_new)) * O + P @ V - if (tid_x < bound_tid_x && tid_y < bound_tid_y) { - for (int u = tid_x; u < head_dim; u += blockDim.x) { - float val3 = 0.0; -#pragma unroll - for (int w = 0; w < Bc; ++w) { - val3 += float(SP_AT(tid_y, w)) * V_sm_AT(w, u); - } - O_sm_AT(tid_y, u) = O_sm_AT(tid_y, u) * exp_result + val3; - } - } - __syncthreads(); - - // step-7: m_prev <- m_new; l_prev <- l_new - if (tid_x == 0 && tid_y < bound_tid_y) { - m_prev[tid_y] = m_new[tid_y]; - l_prev[tid_y] = l_new[tid_y]; - } - __syncthreads(); - } -/****************************end-of-main-loop**************************/ - -/****************************post-process****************************/ -// O(GM) = O/l_prev, aka O_sm /= l_prev and write Oi from SM to GM -// Also save logsumexp for backward pass -#pragma unroll - for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { - if (tid_y < bound_tid_y) { - output[((((bid_y * target_seq_len) + (Br * bid_z + tid_y)) * q_heads) + bid_x) * head_dim + idx] - = T(O_sm_AT(tid_y, idx) / float(l_prev[tid_y])); - } - } +// // 计算中间变量,包括S, P(复用为SP), m_prev, m_new, l_prev, l_new +// double *SP = reinterpret_cast(ptr); // double SP[Br][Bc] +// ptr += Br * Bc * sizeof(double); +// float *m_prev = reinterpret_cast(ptr); // float m_prev[Br] +// ptr += Br * sizeof(float); +// float *m_new = reinterpret_cast(ptr); // float m_new[Br] +// ptr += Br * sizeof(float); +// float *l_prev = reinterpret_cast(ptr); // float l_prev[Br] +// ptr += Br * sizeof(float); +// float *l_new = reinterpret_cast(ptr); // float l_new[Br] +// ptr += Br * sizeof(float); + +// // 原始数据QKV和计算结果O; 全采用float +// float *Q_sm = reinterpret_cast(ptr); // float Q_sm[Br][head_dim] +// ptr += Br * head_dim * sizeof(float); +// float *K_T_sm = reinterpret_cast(ptr); // float K_T_sm[head_dim][Bc] +// ptr += head_dim * Bc * sizeof(float); +// float *V_sm = reinterpret_cast(ptr); // float V_sm[Bc][head_dim] +// ptr += Bc * head_dim * sizeof(float); +// float *O_sm = reinterpret_cast(ptr); // float O_sm[Br][head_dim] +// // Note: Removed dropout_sm shared memory allocation + +// // 定义访问宏 +// #define SP_AT(y, x) SP[y * Bc + x] +// #define Q_sm_AT(y, x) Q_sm[y * head_dim + x] +// #define K_T_sm_AT(y, x) K_T_sm[y * Bc + x] +// #define V_sm_AT(y, x) V_sm[y * head_dim + x] +// #define O_sm_AT(y, x) O_sm[y * head_dim + x] +// // Note: Removed DROPOUT_SM_AT macro + +// /****************************preparation**************************/ +// int bound_tid_y = min(Br, target_seq_len - Br * bid_z); + +// // preparation-1: load Qi from GM to SM, and reset Oi to 0 +// for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { +// O_sm_AT(tid_y, idx) = 0.0; +// Q_sm_AT(tid_y, idx) = 0.0; +// if (tid_y < bound_tid_y) { +// Q_sm_AT(tid_y, idx) +// = float(query[((((bid_y * target_seq_len) + (Br * bid_z + tid_y)) * q_heads) + bid_x) * head_dim + idx]); +// } +// } +// __syncthreads(); + +// // preparation-2: reset m_prev to -INFINITY and l_prev to 0 +// if (tid_x == 0) { +// m_prev[tid_y] = -8192.0; +// l_prev[tid_y] = 0.0; +// } +// __syncthreads(); + +// /****************************end-of-preparation*************************/ + +// /****************************main-loop**************************/ +// #pragma unroll 4 +// for (int j = 0; j < Tc; ++j) { // 对于每个K/V分块 +// bool skip = (is_causal && bid_z < j); +// if (skip) { // early exit, 直接跳过 +// __syncthreads(); +// continue; +// } + +// SP_AT(tid_y, tid_x) = -8192.0; +// __syncthreads(); +// int bound_tid_x = min(Bc, src_seq_len - Bc * j); +// bool is_compute = true; // optimization: 分支处理,加速branch-resolving +// if (is_causal) { +// if (bid_z < j) { +// is_compute = false; // 早期退出情况 +// } else if (bid_z == j) { +// is_compute = (tid_y >= tid_x); // 对角线以上 +// } +// } + +// // step-1: load Ki, Vi from GM to SM +// #pragma unroll +// for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { +// K_T_sm_AT(idx, tid_y) = 0.0; +// V_sm_AT(tid_y, idx) = 0.0; +// if (tid_y < bound_tid_x) { +// K_T_sm_AT(idx, tid_y) = float( +// K[((((bid_y * src_seq_len) + (Bc * j + tid_y)) * kv_heads) + (bid_x / p)) * head_dim + idx]); +// V_sm_AT(tid_y, idx) = float( +// V[((((bid_y * src_seq_len) + (Bc * j + tid_y)) * kv_heads) + (bid_x / p)) * head_dim + idx]); +// } +// } +// __syncthreads(); + +// // step-2: S = Q @ K.T, point-wise +// if (tid_y < bound_tid_y && tid_x < bound_tid_x) { +// float val0 = 0.0; +// if (is_compute) { +// #pragma unroll +// for (int k = 0; k < head_dim; ++k) { +// val0 += Q_sm_AT(tid_y, k) * K_T_sm_AT(k, tid_x); +// } +// SP_AT(tid_y, tid_x) = double(val0) * scale; +// } +// } +// __syncthreads(); + +// // step-3: m_new = max(m_prev, rowMax(S)) +// float val1 = float(SP_AT(tid_y, tid_x)); +// val1 = warp_reduce_max(val1); +// if (tid_x == 0 && tid_y < bound_tid_y) { +// m_new[tid_y] = (val1 > m_prev[tid_y]) ? val1 : m_prev[tid_y]; +// } +// __syncthreads(); + +// // step-4: P = exp(S - m_new), point-wise +// if (tid_y < bound_tid_y && tid_x < bound_tid_x) { +// if (is_compute) { +// SP_AT(tid_y, tid_x) = myexp(SP_AT(tid_y, tid_x) - double(m_new[tid_y])); +// } else { +// SP_AT(tid_y, tid_x) = 0.0; +// } +// } else { +// SP_AT(tid_y, tid_x) = 0.0; +// } +// __syncthreads(); + +// // step-5: l_new = exp(m_prev - m_new) * l_prev + rowSum(P) +// float val2 = float(SP_AT(tid_y, tid_x)); +// val2 = warp_reduce_sum(val2); +// float exp_result = myexp(m_prev[tid_y] - m_new[tid_y]); +// if (tid_x == 0 && tid_y < bound_tid_y) { +// l_new[tid_y] = exp_result * l_prev[tid_y] + val2; +// } +// __syncthreads(); + +// // step-5.5: Apply dropout to P (using dropout_seed for reproducibility) +// if (dropout_p > 0 && tid_y < bound_tid_y && tid_x < bound_tid_x) { +// int global_q_idx = Br * bid_z + tid_y; +// int global_k_idx = Bc * j + tid_x; +// bool keep = FlashAttnDropoutKeep( +// dropout_seed, bid_y, bid_x, global_q_idx, global_k_idx, +// q_heads, target_seq_len, src_seq_len, static_cast(dropout_p)); +// if (keep) { +// SP_AT(tid_y, tid_x) = SP_AT(tid_y, tid_x) / (1.0f - dropout_p); +// } else { +// SP_AT(tid_y, tid_x) = 0.0; +// } +// // Note: Removed dropout_sm mask saving to reduce memory overhead +// } +// __syncthreads(); + +// // step-6: O = 1/(exp(m_prev - m_new)) * O + P @ V +// if (tid_x < bound_tid_x && tid_y < bound_tid_y) { +// for (int u = tid_x; u < head_dim; u += blockDim.x) { +// float val3 = 0.0; +// #pragma unroll +// for (int w = 0; w < Bc; ++w) { +// val3 += float(SP_AT(tid_y, w)) * V_sm_AT(w, u); +// } +// O_sm_AT(tid_y, u) = O_sm_AT(tid_y, u) * exp_result + val3; +// } +// } +// __syncthreads(); + +// // step-7: m_prev <- m_new; l_prev <- l_new +// if (tid_x == 0 && tid_y < bound_tid_y) { +// m_prev[tid_y] = m_new[tid_y]; +// l_prev[tid_y] = l_new[tid_y]; +// } +// __syncthreads(); +// } +// /****************************end-of-main-loop**************************/ + +// /****************************post-process****************************/ +// // O(GM) = O/l_prev, aka O_sm /= l_prev and write Oi from SM to GM +// // Also save logsumexp for backward pass +// #pragma unroll +// for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { +// if (tid_y < bound_tid_y) { +// output[((((bid_y * target_seq_len) + (Br * bid_z + tid_y)) * q_heads) + bid_x) * head_dim + idx] +// = T(O_sm_AT(tid_y, idx) / float(l_prev[tid_y])); +// } +// } - // Save logsumexp for backward pass - if (tid_x == 0 && tid_y < bound_tid_y) { - int logsumexp_idx = ((bid_y * q_heads + bid_x) * target_seq_len) + (Br * bid_z + tid_y); - logsumexp[logsumexp_idx] = m_prev[tid_y] + log(l_prev[tid_y]); - } - __syncthreads(); -/****************************end-of-post-process****************************/ - -// 取消访问宏定义 -#undef SP_AT -#undef Q_sm_AT -#undef K_T_sm_AT -#undef V_sm_AT -#undef O_sm_AT -} - -/** - * FlashAttention Backward Kernel - * - * This kernel implements the backward pass for FlashAttention. - * It computes gradients for query, key, and value tensors. - * - * Args: - * grad_query: Gradient for query tensor - * grad_key: Gradient for key tensor - * grad_value: Gradient for value tensor - * query: Query tensor from forward pass - * key: Key tensor from forward pass - * value: Value tensor from forward pass - * output: Output tensor from forward pass - * grad_output: Gradient from upstream - * logsumexp: Logsumexp tensor from forward pass - * dropout_seed: Dropout seed for reproducibility - * attn_mask: Optional attention mask tensor - * scale: Scaling factor for attention scores - * is_causal: Whether causal masking was applied - * dropout_p: Dropout probability - * enable_gqa: Whether GQA was enabled - * batch_size, target_seq_len, src_seq_len: Tensor dimensions - * q_heads, kv_heads, head_dim: Attention head dimensions - */ -template -__global__ void FlashAttentionBackwardKernel( - T *grad_query, T *grad_key, T *grad_value, - const T *query, const T *key, const T *value, - const T *output, const T *grad_output, - const float *logsumexp, - const float *D, // Precomputed D = rowsum(dO ∘ O) - unsigned long long dropout_seed, - const T *attn_mask, - float scale, bool is_causal, int64_t dropout_p, - bool enable_gqa, - int batch_size, int target_seq_len, int src_seq_len, - int q_heads, int kv_heads, int head_dim) { +// // Save logsumexp for backward pass +// if (tid_x == 0 && tid_y < bound_tid_y) { +// int logsumexp_idx = ((bid_y * q_heads + bid_x) * target_seq_len) + (Br * bid_z + tid_y); +// logsumexp[logsumexp_idx] = m_prev[tid_y] + log(l_prev[tid_y]); +// } +// __syncthreads(); +// /****************************end-of-post-process****************************/ + +// // 取消访问宏定义 +// #undef SP_AT +// #undef Q_sm_AT +// #undef K_T_sm_AT +// #undef V_sm_AT +// #undef O_sm_AT +// } + +// /** +// * FlashAttention Backward Kernel +// * +// * This kernel implements the backward pass for FlashAttention. +// * It computes gradients for query, key, and value tensors. +// * +// * Args: +// * grad_query: Gradient for query tensor +// * grad_key: Gradient for key tensor +// * grad_value: Gradient for value tensor +// * query: Query tensor from forward pass +// * key: Key tensor from forward pass +// * value: Value tensor from forward pass +// * output: Output tensor from forward pass +// * grad_output: Gradient from upstream +// * logsumexp: Logsumexp tensor from forward pass +// * dropout_seed: Dropout seed for reproducibility +// * attn_mask: Optional attention mask tensor +// * scale: Scaling factor for attention scores +// * is_causal: Whether causal masking was applied +// * dropout_p: Dropout probability +// * enable_gqa: Whether GQA was enabled +// * batch_size, target_seq_len, src_seq_len: Tensor dimensions +// * q_heads, kv_heads, head_dim: Attention head dimensions +// */ +// template +// __global__ void FlashAttentionBackwardKernel( +// T *grad_query, T *grad_key, T *grad_value, +// const T *query, const T *key, const T *value, +// const T *output, const T *grad_output, +// const float *logsumexp, +// const float *D, // Precomputed D = rowsum(dO ∘ O) +// unsigned long long dropout_seed, +// const T *attn_mask, +// float scale, bool is_causal, int64_t dropout_p, +// bool enable_gqa, +// int batch_size, int target_seq_len, int src_seq_len, +// int q_heads, int kv_heads, int head_dim) { - // Grid/block dimensions: grid_dims(num_heads_q, batch_size, Tr), block_dim(Bc, Br) - // where Br corresponds to thread block row index, Bc corresponds to column index - int tid_x = threadIdx.x; // 横向, blockDim.x列 (Bc) - int tid_y = threadIdx.y; // 纵向, blockDim.y行 (Br) - int bid_x = blockIdx.x; // x方向, 总数 = #q_heads - int bid_y = blockIdx.y; // y方向, 总数 = #batch - int bid_z = blockIdx.z; // z方向, 总数 = Tr - const int p = q_heads / kv_heads; // GQA比例系数 - - const int Br = blockDim.y; // Q纵向每块大小, 32 - const int Bc = blockDim.x; // K/V纵向分块大小, 32 - const int Tr = gridDim.z; // Q纵向分块数 - const int Tc = (src_seq_len + Bc - 1) / Bc; // K/V纵向分块数 - - // Define shared memory - extern __shared__ char shared_mem[]; - char *ptr = shared_mem; +// // Grid/block dimensions: grid_dims(num_heads_q, batch_size, Tr), block_dim(Bc, Br) +// // where Br corresponds to thread block row index, Bc corresponds to column index +// int tid_x = threadIdx.x; // 横向, blockDim.x列 (Bc) +// int tid_y = threadIdx.y; // 纵向, blockDim.y行 (Br) +// int bid_x = blockIdx.x; // x方向, 总数 = #q_heads +// int bid_y = blockIdx.y; // y方向, 总数 = #batch +// int bid_z = blockIdx.z; // z方向, 总数 = Tr +// const int p = q_heads / kv_heads; // GQA比例系数 + +// const int Br = blockDim.y; // Q纵向每块大小, 32 +// const int Bc = blockDim.x; // K/V纵向分块大小, 32 +// const int Tr = gridDim.z; // Q纵向分块数 +// const int Tc = (src_seq_len + Bc - 1) / Bc; // K/V纵向分块数 + +// // Define shared memory +// extern __shared__ char shared_mem[]; +// char *ptr = shared_mem; - // D_sm[Br] - D values loaded from HBM - float *D_sm = reinterpret_cast(ptr); - ptr += Br * sizeof(float); +// // D_sm[Br] - D values loaded from HBM +// float *D_sm = reinterpret_cast(ptr); +// ptr += Br * sizeof(float); - // Q_sm[Br][head_dim], K_T_sm[head_dim][Bc], V_sm[Bc][head_dim] - float *Q_sm = reinterpret_cast(ptr); - ptr += Br * head_dim * sizeof(float); - float *K_T_sm = reinterpret_cast(ptr); - ptr += head_dim * Bc * sizeof(float); - float *V_sm = reinterpret_cast(ptr); - ptr += Bc * head_dim * sizeof(float); +// // Q_sm[Br][head_dim], K_T_sm[head_dim][Bc], V_sm[Bc][head_dim] +// float *Q_sm = reinterpret_cast(ptr); +// ptr += Br * head_dim * sizeof(float); +// float *K_T_sm = reinterpret_cast(ptr); +// ptr += head_dim * Bc * sizeof(float); +// float *V_sm = reinterpret_cast(ptr); +// ptr += Bc * head_dim * sizeof(float); - // dO_sm[Br][head_dim], dK_T_sm[head_dim][Bc], dV_sm[Bc][head_dim] - float *dO_sm = reinterpret_cast(ptr); - ptr += Br * head_dim * sizeof(float); - float *dK_T_sm = reinterpret_cast(ptr); - ptr += head_dim * Bc * sizeof(float); - float *dV_sm = reinterpret_cast(ptr); - ptr += Bc * head_dim * sizeof(float); +// // dO_sm[Br][head_dim], dK_T_sm[head_dim][Bc], dV_sm[Bc][head_dim] +// float *dO_sm = reinterpret_cast(ptr); +// ptr += Br * head_dim * sizeof(float); +// float *dK_T_sm = reinterpret_cast(ptr); +// ptr += head_dim * Bc * sizeof(float); +// float *dV_sm = reinterpret_cast(ptr); +// ptr += Bc * head_dim * sizeof(float); - // S_sm[Br][Bc], P_sm[Br][Bc] - float *S_sm = reinterpret_cast(ptr); - ptr += Br * Bc * sizeof(float); - float *P_sm = reinterpret_cast(ptr); +// // S_sm[Br][Bc], P_sm[Br][Bc] +// float *S_sm = reinterpret_cast(ptr); +// ptr += Br * Bc * sizeof(float); +// float *P_sm = reinterpret_cast(ptr); - // L_sm[Br] - logsumexp values - float *L_sm = reinterpret_cast(ptr); +// // L_sm[Br] - logsumexp values +// float *L_sm = reinterpret_cast(ptr); - // dQ_sm[Br][head_dim] - accumulated gradient for Q - float *dQ_sm = reinterpret_cast(ptr); +// // dQ_sm[Br][head_dim] - accumulated gradient for Q +// float *dQ_sm = reinterpret_cast(ptr); - // Define access macros - #define D_sm_AT(y) D_sm[y] - #define Q_sm_AT(y, x) Q_sm[y * head_dim + x] - #define K_T_sm_AT(y, x) K_T_sm[y * Bc + x] - #define V_sm_AT(y, x) V_sm[y * head_dim + x] - #define dO_sm_AT(y, x) dO_sm[y * head_dim + x] - #define dK_T_sm_AT(y, x) dK_T_sm[y * Bc + x] - #define dV_sm_AT(y, x) dV_sm[y * head_dim + x] - #define S_sm_AT(y, x) S_sm[y * Bc + x] - #define P_sm_AT(y, x) P_sm[y * Bc + x] - #define L_sm_AT(y) L_sm[y] - #define dQ_sm_AT(y, x) dQ_sm[y * head_dim + x] - - // Initialize dropout random state - curandStatePhilox4_32_10_t state; - if (dropout_p > 0) { - unsigned long long seq = bid_y * q_heads * target_seq_len + bid_x * target_seq_len + Br * bid_z + tid_y; - curand_init(dropout_seed, seq, 0, &state); - } - - /****************************Preparation*****************************/ +// // Define access macros +// #define D_sm_AT(y) D_sm[y] +// #define Q_sm_AT(y, x) Q_sm[y * head_dim + x] +// #define K_T_sm_AT(y, x) K_T_sm[y * Bc + x] +// #define V_sm_AT(y, x) V_sm[y * head_dim + x] +// #define dO_sm_AT(y, x) dO_sm[y * head_dim + x] +// #define dK_T_sm_AT(y, x) dK_T_sm[y * Bc + x] +// #define dV_sm_AT(y, x) dV_sm[y * head_dim + x] +// #define S_sm_AT(y, x) S_sm[y * Bc + x] +// #define P_sm_AT(y, x) P_sm[y * Bc + x] +// #define L_sm_AT(y) L_sm[y] +// #define dQ_sm_AT(y, x) dQ_sm[y * head_dim + x] + +// /****************************Preparation*****************************/ - // Initialize dQ_sm to 0 for accumulation - for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { - dQ_sm_AT(tid_y, idx) = 0.0f; - } - __syncthreads(); - - // Load D_i from HBM to shared memory - int q_idx = Br * bid_z + tid_y; // Global query position within this head - if (q_idx < target_seq_len) { - int d_idx = ((bid_y * q_heads + bid_x) * target_seq_len) + q_idx; - D_sm_AT(tid_y) = D[d_idx]; - } else { - D_sm_AT(tid_y) = 0.0f; // Padding for out-of-bounds positions - } - __syncthreads(); - - // Get D_i from shared memory - float D_i = D_sm_AT(tid_y); - - /****************************Main Loop - Outer Loop over K/V tiles*****************************/ - for (int j = 0; j < Tc; ++j) { // For each K/V column tile +// // Initialize dQ_sm to 0 for accumulation +// for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { +// dQ_sm_AT(tid_y, idx) = 0.0f; +// } +// __syncthreads(); + +// // Load D_i from HBM to shared memory +// int q_idx = Br * bid_z + tid_y; // Global query position within this head +// if (q_idx < target_seq_len) { +// int d_idx = ((bid_y * q_heads + bid_x) * target_seq_len) + q_idx; +// D_sm_AT(tid_y) = D[d_idx]; +// } else { +// D_sm_AT(tid_y) = 0.0f; // Padding for out-of-bounds positions +// } +// __syncthreads(); + +// /****************************Main Loop - Outer Loop over K/V tiles*****************************/ +// for (int j = 0; j < Tc; ++j) { // For each K/V column tile - // Skip entire tile if causal and this tile is completely to the right - bool skip_tile = (is_causal && bid_z < j); - if (skip_tile) { - __syncthreads(); - continue; - } - - // Initialize dK_T_sm, dV_sm to 0 for this column tile - for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { - for (int y = 0; y < Bc; ++y) { - dK_T_sm_AT(idx, y) = 0.0f; - dV_sm_AT(y, idx) = 0.0f; - } - } - __syncthreads(); - - // Load K_j, V_j from HBM to shared memory - int bound_tid_x = min(Bc, src_seq_len - Bc * j); - for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { - K_T_sm_AT(idx, tid_y) = 0.0f; - V_sm_AT(tid_y, idx) = 0.0f; - if (tid_y < bound_tid_x) { - int kv_head_idx = bid_x / p; - K_T_sm_AT(idx, tid_y) = float( - key[((((bid_y * src_seq_len) + (Bc * j + tid_y)) * kv_heads + kv_head_idx) * head_dim + idx]); - V_sm_AT(tid_y, idx) = float( - value[((((bid_y * src_seq_len) + (Bc * j + tid_y)) * kv_heads + kv_head_idx) * head_dim + idx]); - } - } - __syncthreads(); - - /****************************Inner Loop - Loop over Q tiles*****************************/ - // For this column tile, iterate through all Q row tiles - for (int i = 0; i < Tr; ++i) { // For each Q row tile - - // Skip this Q tile if causal and it's completely to the left of current K tile - bool skip_q_tile = (is_causal && i > j); - if (skip_q_tile) { - __syncthreads(); - continue; - } - - // Load Q_i, dO_i, L_i for this row tile - int q_tile_start = Br * i; - int q_tile_end = min(q_tile_start + Br, target_seq_len); - int bound_tid_y = min(Br, q_tile_end - q_tile_start); - - int local_q_idx = Br * i + tid_y; - if (local_q_idx < bound_tid_y) { - // Load Q_i, dO_i, L_i from HBM - int global_q_idx = q_tile_start + tid_y; - int q_tensor_idx = ((bid_y * target_seq_len) + global_q_idx) * q_heads + bid_x; - - for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { - Q_sm_AT(tid_y, idx) = float(query[q_tensor_idx * head_dim + idx]); - dO_sm_AT(tid_y, idx) = float(grad_output[q_tensor_idx * head_dim + idx]); - L_sm_AT(tid_y) = logsumexp[((bid_y * q_heads + bid_x) * target_seq_len + global_q_idx]; - } - } - __syncthreads(); - - // Get D_i for this row - float D_i_row = D_sm_AT(tid_y); - - // Recompute S_ij = Q_i @ K_j^T - // Apply causal mask at element level if needed - #pragma unroll - for (int y = 0; y < Bc; ++y) { - for (int x = 0; x < Bc; ++x) { - float val = 0.0f; - if (tid_y < bound_tid_y && tid_x < bound_tid_x) { - bool is_compute = true; - if (is_causal && i == j) { - // On diagonal, only compute upper triangle - int global_q_pos = q_tile_start + tid_y; - int global_k_pos = Bc * j + x; - is_compute = (global_q_pos >= global_k_pos); - } - - if (is_compute) { - #pragma unroll - for (int k = 0; k < head_dim; ++k) { - val += Q_sm_AT(tid_y, k) * K_T_sm_AT(k, y); - } - } - } - S_sm_AT(tid_y, x) = val * scale; - } - } - __syncthreads(); - - // Recompute P_ij = exp(S_ij - L_i) - #pragma unroll - for (int y = 0; y < Bc; ++y) { - for (int x = 0; x < Bc; ++x) { - if (tid_y < bound_tid_y && tid_x < bound_tid_x) { - bool is_compute = true; - if (is_causal && i == j) { - int global_q_pos = q_tile_start + tid_y; - int global_k_pos = Bc * j + x; - is_compute = (global_q_pos >= global_k_pos); - } - - if (is_compute) { - P_sm_AT(tid_y, x) = myexp(S_sm_AT(tid_y, x) - L_sm_AT(tid_y)); - } else { - P_sm_AT(tid_y, x) = 0.0f; - } - } else { - P_sm_AT(tid_y, x) = 0.0f; - } - } - } - __syncthreads(); - - // Apply dropout to P_ij - if (dropout_p > 0) { - #pragma unroll - for (int y = 0; y < Bc; ++y) { - for (int x = 0; x < Bc; ++x) { - if (tid_y < bound_tid_y && tid_x < bound_tid_x) { - float rand_val = curand_uniform(&state); - bool keep = rand_val > dropout_p; - if (keep) { - P_sm_AT(tid_y, x) = P_sm_AT(tid_y, x) / (1.0f - dropout_p); - } else { - P_sm_AT(tid_y, x) = 0.0f; - } - } - } - } - __syncthreads(); - } - - // Compute dV_j += P_ij^T @ dO_i - if (tid_y < bound_tid_y) { - #pragma unroll - for (int x = 0; x < head_dim; x += blockDim.x) { - float val = 0.0f; - #pragma unroll - for (int y = 0; y < Bc; ++y) { - val += P_sm_AT(tid_y, y) * dO_sm_AT(tid_y, x); - } - atomicAdd(&dV_sm_AT(y, x), val); - } - } - __syncthreads(); - - // Compute dP_ij = dO_i @ V_j^T - #pragma unroll - for (int y = 0; y < Bc; ++y) { - for (int x = 0; x < Bc; ++x) { - float val = 0.0f; - if (tid_y < bound_tid_y && tid_x < bound_tid_x) { - #pragma unroll - for (int k = 0; k < head_dim; ++k) { - val += dO_sm_AT(tid_y, k) * V_sm_AT(y, k); - } - } - S_sm_AT(tid_y, x) = val; // Reuse S_sm as temporary storage for dP_ij - } else { - S_sm_AT(tid_y, x) = 0.0f; - } - } - __syncthreads(); - - // Apply dropout to dP_ij - if (dropout_p > 0) { - #pragma unroll - for (int y = 0; y < Bc; ++y) { - for (int x = 0; x < Bc; ++x) { - if (tid_y < bound_tid_y && tid_x < bound_tid_x) { - float rand_val = curand_uniform(&state); - bool keep = rand_val > dropout_p; - if (keep) { - S_sm_AT(tid_y, x) = S_sm_AT(tid_y, x) / (1.0f - dropout_p); - } else { - S_sm_AT(tid_y, x) = 0.0f; - } - } - } - } - __syncthreads(); - } - - // Compute dS_ij = P_ij * (dP_ij - D_i) - #pragma unroll - for (int y = 0; y < Bc; ++y) { - for (int x = 0; x < Bc; ++x) { - if (tid_y < bound_tid_y && tid_x < bound_tid_x) { - S_sm_AT(tid_y, x) = P_sm_AT(tid_y, x) * (S_sm_AT(tid_y, x) - D_i_row); - } else { - S_sm_AT(tid_y, x) = 0.0f; - } - } - } - __syncthreads(); - - // Compute dK_j += dS_ij^T @ Q_i - if (tid_x < head_dim) { - #pragma unroll - for (int y = 0; y < Bc; ++y) { - float val = 0.0f; - #pragma unroll - for (int x = 0; x < Br; ++x) { - val += S_sm_AT(x, y) * Q_sm_AT(x, tid_x); - } - atomicAdd(&dK_T_sm_AT(tid_x, y), val * scale); - } - } - __syncthreads(); - - // Compute dQ_i += dS_ij @ K_j - if (tid_y < bound_tid_y) { - #pragma unroll - for (int x = 0; x < head_dim; x += blockDim.x) { - float val = 0.0f; - #pragma unroll - for (int y = 0; y < Bc; ++y) { - val += S_sm_AT(tid_y, y) * K_T_sm_AT(x, y); - } - dQ_sm_AT(tid_y, x) += val * scale; - } - } - __syncthreads(); - } // End of inner loop over Q tiles - - // Write back dK_j, dV_j to HBM - // Note: For GQA, dK_j and dV_j need to be accumulated across multiple q-head blocks - int kv_head_idx = bid_x / p; - int k_tile_start = Bc * j; - int k_tile_end = min(k_tile_start + Bc, src_seq_len); +// // Skip entire tile if causal and this tile is completely to the right +// bool skip_tile = (is_causal && bid_z < j); +// if (skip_tile) { +// __syncthreads(); +// continue; +// } + +// // Initialize dK_T_sm, dV_sm to 0 for this column tile +// for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { +// for (int y = 0; y < Bc; ++y) { +// dK_T_sm_AT(idx, y) = 0.0f; +// dV_sm_AT(y, idx) = 0.0f; +// } +// } +// __syncthreads(); + +// // Load K_j, V_j from HBM to shared memory +// int bound_tid_x = min(Bc, src_seq_len - Bc * j); +// for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { +// K_T_sm_AT(idx, tid_y) = 0.0f; +// V_sm_AT(tid_y, idx) = 0.0f; +// if (tid_y < bound_tid_x) { +// int kv_head_idx = bid_x / p; +// K_T_sm_AT(idx, tid_y) = float( +// key[((((bid_y * src_seq_len) + (Bc * j + tid_y)) * kv_heads + kv_head_idx) * head_dim + idx]); +// V_sm_AT(tid_y, idx) = float( +// value[((((bid_y * src_seq_len) + (Bc * j + tid_y)) * kv_heads + kv_head_idx) * head_dim + idx]); +// } +// } +// __syncthreads(); + +// /****************************Single Q tile for this block*****************************/ +// // gridDim.z already indexes the Q row tile, so this block only handles i = bid_z. +// const int i = bid_z; +// int q_tile_start = Br * i; +// int q_tile_end = min(q_tile_start + Br, target_seq_len); +// int bound_tid_y = min(Br, q_tile_end - q_tile_start); + +// if (tid_y < bound_tid_y) { +// int global_q_idx = q_tile_start + tid_y; +// int q_tensor_idx = ((bid_y * target_seq_len) + global_q_idx) * q_heads + bid_x; + +// for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { +// Q_sm_AT(tid_y, idx) = float(query[q_tensor_idx * head_dim + idx]); +// dO_sm_AT(tid_y, idx) = float(grad_output[q_tensor_idx * head_dim + idx]); +// } +// if (tid_x == 0) { +// L_sm_AT(tid_y) = logsumexp[((bid_y * q_heads + bid_x) * target_seq_len) + global_q_idx]; +// } +// } +// __syncthreads(); + +// // Get D_i for this row. +// float D_i_row = D_sm_AT(tid_y); + +// // Recompute S_ij = Q_i @ K_j^T, apply causal mask at element level if needed. +// #pragma unroll +// for (int y = 0; y < Bc; ++y) { +// for (int x = 0; x < Bc; ++x) { +// float val = 0.0f; +// if (tid_y < bound_tid_y && tid_x < bound_tid_x) { +// bool is_compute = true; +// if (is_causal && i == j) { +// int global_q_pos = q_tile_start + tid_y; +// int global_k_pos = Bc * j + x; +// is_compute = (global_q_pos >= global_k_pos); +// } + +// if (is_compute) { +// #pragma unroll +// for (int k = 0; k < head_dim; ++k) { +// val += Q_sm_AT(tid_y, k) * K_T_sm_AT(k, y); +// } +// } +// } +// S_sm_AT(tid_y, x) = val * scale; +// } +// } +// __syncthreads(); + +// // Recompute P_ij = exp(S_ij - L_i). +// #pragma unroll +// for (int y = 0; y < Bc; ++y) { +// for (int x = 0; x < Bc; ++x) { +// if (tid_y < bound_tid_y && tid_x < bound_tid_x) { +// bool is_compute = true; +// if (is_causal && i == j) { +// int global_q_pos = q_tile_start + tid_y; +// int global_k_pos = Bc * j + x; +// is_compute = (global_q_pos >= global_k_pos); +// } + +// if (is_compute) { +// P_sm_AT(tid_y, x) = myexp(S_sm_AT(tid_y, x) - L_sm_AT(tid_y)); +// } else { +// P_sm_AT(tid_y, x) = 0.0f; +// } +// } else { +// P_sm_AT(tid_y, x) = 0.0f; +// } +// } +// } +// __syncthreads(); + +// // Apply dropout to P_ij. +// if (dropout_p > 0) { +// #pragma unroll +// for (int y = 0; y < Bc; ++y) { +// for (int x = 0; x < Bc; ++x) { +// if (tid_y < bound_tid_y && tid_x < bound_tid_x) { +// int global_q_pos = q_tile_start + tid_y; +// int global_k_pos = Bc * j + x; +// bool keep = FlashAttnDropoutKeep( +// dropout_seed, bid_y, bid_x, global_q_pos, global_k_pos, +// q_heads, target_seq_len, src_seq_len, static_cast(dropout_p)); +// if (keep) { +// P_sm_AT(tid_y, x) = P_sm_AT(tid_y, x) / (1.0f - dropout_p); +// } else { +// P_sm_AT(tid_y, x) = 0.0f; +// } +// } +// } +// } +// __syncthreads(); +// } + +// // Compute dV_j += P_ij^T @ dO_i. +// if (tid_y < bound_tid_y) { +// #pragma unroll +// for (int x = 0; x < head_dim; x += blockDim.x) { +// float val = 0.0f; +// #pragma unroll +// for (int y = 0; y < Bc; ++y) { +// val += P_sm_AT(tid_y, y) * dO_sm_AT(tid_y, x); +// } +// atomicAdd(&dV_sm_AT(y, x), val); +// } +// } +// __syncthreads(); + +// // Compute dP_ij = dO_i @ V_j^T. +// #pragma unroll +// for (int y = 0; y < Bc; ++y) { +// for (int x = 0; x < Bc; ++x) { +// float val = 0.0f; +// if (tid_y < bound_tid_y && tid_x < bound_tid_x) { +// #pragma unroll +// for (int k = 0; k < head_dim; ++k) { +// val += dO_sm_AT(tid_y, k) * V_sm_AT(y, k); +// } +// S_sm_AT(tid_y, x) = val; // Reuse S_sm as temporary storage for dP_ij +// } else { +// S_sm_AT(tid_y, x) = 0.0f; +// } +// } +// } +// __syncthreads(); + +// // Apply dropout to dP_ij. +// if (dropout_p > 0) { +// #pragma unroll +// for (int y = 0; y < Bc; ++y) { +// for (int x = 0; x < Bc; ++x) { +// if (tid_y < bound_tid_y && tid_x < bound_tid_x) { +// int global_q_pos = q_tile_start + tid_y; +// int global_k_pos = Bc * j + x; +// bool keep = FlashAttnDropoutKeep( +// dropout_seed, bid_y, bid_x, global_q_pos, global_k_pos, +// q_heads, target_seq_len, src_seq_len, static_cast(dropout_p)); +// if (keep) { +// S_sm_AT(tid_y, x) = S_sm_AT(tid_y, x) / (1.0f - dropout_p); +// } else { +// S_sm_AT(tid_y, x) = 0.0f; +// } +// } +// } +// } +// __syncthreads(); +// } + +// // Compute dS_ij = P_ij * (dP_ij - D_i). +// #pragma unroll +// for (int y = 0; y < Bc; ++y) { +// for (int x = 0; x < Bc; ++x) { +// if (tid_y < bound_tid_y && tid_x < bound_tid_x) { +// S_sm_AT(tid_y, x) = P_sm_AT(tid_y, x) * (S_sm_AT(tid_y, x) - D_i_row); +// } else { +// S_sm_AT(tid_y, x) = 0.0f; +// } +// } +// } +// __syncthreads(); + +// // Compute dK_j += dS_ij^T @ Q_i. +// if (tid_x < head_dim) { +// #pragma unroll +// for (int y = 0; y < Bc; ++y) { +// float val = 0.0f; +// #pragma unroll +// for (int x = 0; x < Br; ++x) { +// val += S_sm_AT(x, y) * Q_sm_AT(x, tid_x); +// } +// atomicAdd(&dK_T_sm_AT(tid_x, y), val * scale); +// } +// } +// __syncthreads(); + +// // Compute dQ_i += dS_ij @ K_j. +// if (tid_y < bound_tid_y) { +// #pragma unroll +// for (int x = 0; x < head_dim; x += blockDim.x) { +// float val = 0.0f; +// #pragma unroll +// for (int y = 0; y < Bc; ++y) { +// val += S_sm_AT(tid_y, y) * K_T_sm_AT(x, y); +// } +// dQ_sm_AT(tid_y, x) += val * scale; +// } +// } +// __syncthreads(); + +// // Write back dK_j, dV_j to HBM +// // Note: For GQA, dK_j and dV_j need to be accumulated across multiple q-head blocks +// int kv_head_idx = bid_x / p; +// int k_tile_start = Bc * j; +// int k_tile_end = min(k_tile_start + Bc, src_seq_len); - for (int y = 0; y < Bc; ++y) { - int global_k_idx = k_tile_start + y; - if (global_k_idx < src_seq_len) { - for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { - // dK: [batch_size, src_seq_len, kv_heads, head_dim] - int k_tensor_idx = ((bid_y * src_seq_len) + global_k_idx) * kv_heads + kv_head_idx; - grad_key[k_tensor_idx * head_dim + idx] += T(dK_T_sm_AT(idx, y)); +// for (int y = 0; y < Bc; ++y) { +// int global_k_idx = k_tile_start + y; +// if (global_k_idx < src_seq_len) { +// for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { +// // dK: [batch_size, src_seq_len, kv_heads, head_dim] +// int k_tensor_idx = ((bid_y * src_seq_len) + global_k_idx) * kv_heads + kv_head_idx; +// atomicAdd(&grad_key[k_tensor_idx * head_dim + idx], T(dK_T_sm_AT(idx, y))); - // dV: [batch_size, src_seq_len, kv_heads, head_dim] - grad_value[k_tensor_idx * head_dim + idx] += T(dV_sm_AT(y, idx)); - } - } - } - } // End of outer loop over K/V tiles - - // Write back dQ_i to HBM - int q_tile_start = Br * bid_z; - int q_tile_end = min(q_tile_start + Br, target_seq_len); - int bound_tid_y = min(Br, q_tile_end - q_tile_start); +// // dV: [batch_size, src_seq_len, kv_heads, head_dim] +// atomicAdd(&grad_value[k_tensor_idx * head_dim + idx], T(dV_sm_AT(y, idx))); +// } +// } +// } +// } // End of outer loop over K/V tiles + +// // Write back dQ_i to HBM +// int q_tile_start = Br * bid_z; +// int q_tile_end = min(q_tile_start + Br, target_seq_len); +// int bound_tid_y = min(Br, q_tile_end - q_tile_start); - if (tid_y < bound_tid_y) { - int global_q_idx = q_tile_start + tid_y; - int q_tensor_idx = ((bid_y * target_seq_len) + global_q_idx) * q_heads + bid_x; +// if (tid_y < bound_tid_y) { +// int global_q_idx = q_tile_start + tid_y; +// int q_tensor_idx = ((bid_y * target_seq_len) + global_q_idx) * q_heads + bid_x; - for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { - // dQ: [batch_size, target_seq_len, q_heads, head_dim] - grad_query[q_tensor_idx * head_dim + idx] += T(dQ_sm_AT(tid_y, idx)); - } - } - - // Undefine access macros - #undef D_sm_AT - #undef Q_sm_AT - #undef K_T_sm_AT - #undef V_sm_AT - #undef dO_sm_AT - #undef dK_T_sm_AT - #undef dV_sm_AT - #undef S_sm_AT - #undef P_sm_AT - #undef L_sm_AT - #undef dQ_sm_AT -} - -/** - * FlashAttention Forward Function - * - * This is the main entry point for FlashAttention forward computation. - * It creates the output tensor and launches the appropriate kernel based on data type. - * - * Args: - * query: Query tensor of shape [batch_size, seq_len_q, num_heads, head_dim] - * key: Key tensor of shape [batch_size, seq_len_k, num_heads_kv, head_dim] - * value: Value tensor of shape [batch_size, seq_len_k, num_heads_kv, head_dim] - * attn_mask: Optional attention mask tensor - * scale: Scaling factor for attention scores - * is_causal: Whether to apply causal masking - * dropout_p: Dropout probability - * enable_gqa: Whether to enable grouped-query attention - * - * Returns: - * Output tensor of shape [batch_size, seq_len_q, num_heads, head_dim] - * logsumexp: Logsumexp tensor for backward pass [batch_size, num_heads, seq_len_q] - * dropout_seed: Dropout seed for backward pass [1] - */ - -template -FlashAttentionForwardOutput FlashAttentionForwardImpl(const std::shared_ptr &query, const std::shared_ptr &key, - const std::shared_ptr &value, - const std::shared_ptr &attn_mask, float scale, bool is_causal, - int64_t dropout_p, bool enable_gqa) { - auto dtype = query->Dtype(); - const auto &query_dims = query->Dims(); - - // Output shape: [batch_size, seq_len_q, num_heads, head_dim] - std::vector output_dims = {query_dims[0], query_dims[1], query_dims[2], query_dims[3]}; - auto output = std::make_shared(output_dims, dtype, query->GetDevice()); - - // Allocate logsumexp tensor for backward pass - // Shape: [batch_size, num_heads, seq_len_q] - std::vector logsumexp_dims = {query_dims[0], query_dims[2], query_dims[1]}; - auto logsumexp = std::make_shared(logsumexp_dims, DataType::kFLOAT32, query->GetDevice()); - float *logsumexp_ptr = static_cast(logsumexp->DataPtr()); - - // Allocate dropout_seed tensor for backward pass - // Shape: [1] - unsigned long long dropout_seed = 0; - std::shared_ptr dropout_seed_tensor; - if (dropout_p > 0) { - std::vector dropout_seed_dims = {1}; - dropout_seed_tensor = std::make_shared(dropout_seed_dims, DataType::kUINT64, query->GetDevice()); - dropout_seed = static_cast(std::time(nullptr)); - unsigned long long *dropout_seed_ptr = static_cast(dropout_seed_tensor->DataPtr()); - *dropout_seed_ptr = dropout_seed; - } - - switch (dtype) { - DISPATCH_CASE(WRAP(LaunchFlashAttentionForward(output, query, key, value, attn_mask, scale, is_causal, - dropout_p, enable_gqa, logsumexp_ptr, dropout_seed_ptr);), - DataType::kFLOAT32) - DISPATCH_CASE(WRAP(LaunchFlashAttentionForward(output, query, key, value, attn_mask, scale, - is_causal, dropout_p, enable_gqa, logsumexp_ptr, dropout_seed_ptr);), - DataType::kBFLOAT16) - default: - LOG_LOC(FATAL, "CUDA FlashAttention forward: 'Unsupported data type'"); - } - - FlashAttentionForwardOutput result; - result.output = output; - result.logsumexp = logsumexp; - result.dropout_seed = dropout_seed_tensor; - return result; -} - -/** - * Launch FlashAttention Forward Kernel - * - * This function sets up grid and block dimensions and launches FlashAttention forward kernel. - * - * Args: - * output: Output tensor - * query: Query tensor - * key: Key tensor - * value: Value tensor - * attn_mask: Optional attention mask tensor - * scale: Scaling factor - * is_causal: Whether to apply causal masking - * dropout_p: Dropout probability - * enable_gqa: Whether to enable GQA - */ -template -void LaunchFlashAttentionForward(const std::shared_ptr &output, const std::shared_ptr &query, - const std::shared_ptr &key, const std::shared_ptr &value, - const std::shared_ptr &attn_mask, float scale, bool is_causal, - int64_t dropout_p, bool enable_gqa, float *logsumexp_ptr, unsigned long long *dropout_seed_ptr) { - - const auto &query_dims = query->Dims(); - const auto &key_dims = key->Dims(); - const auto &value_dims = value->Dims(); - - // Expected shapes: - // query: [batch_size, seq_len_q, num_heads, head_dim] - // key: [batch_size, seq_len_k, num_heads_kv, head_dim] - // value: [batch_size, seq_len_k, num_heads_kv, head_dim] - // output: [batch_size, seq_len_q, num_heads, head_dim] - - int64_t batch_size = query_dims[0]; - int64_t seq_len_q = query_dims[1]; - int64_t num_heads = query_dims[2]; - int64_t head_dim = query_dims[3]; - int64_t seq_len_k = key_dims[1]; - int64_t num_heads_kv = key_dims[2]; - - CHECK_EQ(key_dims[3], head_dim) << "Key head dimension must match query head dimension"; - CHECK_EQ(value_dims[3], head_dim) << "Value head dimension must match query head dimension"; - CHECK_EQ(value_dims[1], seq_len_k) << "Value sequence length must match key sequence length"; - CHECK_EQ(value_dims[2], num_heads_kv) << "Value number of KV heads must match key"; - - if (enable_gqa) { - CHECK(num_heads % num_heads_kv == 0) << "Number of query heads must be divisible by number of KV heads for GQA"; - } else { - CHECK_EQ(num_heads, num_heads_kv) << "Number of query and KV heads must match for standard attention"; - } - - T *output_ptr = static_cast(output->DataPtr()); - const T *query_ptr = static_cast(query->DataPtr()); - const T *key_ptr = static_cast(key->DataPtr()); - const T *value_ptr = static_cast(value->DataPtr()); - const T *attn_mask_ptr = attn_mask ? static_cast(attn_mask->DataPtr()) : nullptr; - - // Set up grid and block dimensions according to FlashAttention v2 - // block_dim(Br, Bc) where Br = Bc = 32 - // grid_dim(query_heads, batch_size, Tr) where Tr = ceil(seq_len_q / Br) - constexpr int Br = 32; - constexpr int Bc = 32; - int64_t Tr = (seq_len_q + Br - 1) / Br; +// for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { +// // dQ: [batch_size, target_seq_len, q_heads, head_dim] +// grad_query[q_tensor_idx * head_dim + idx] += T(dQ_sm_AT(tid_y, idx)); +// } +// } + +// // Undefine access macros +// #undef D_sm_AT +// #undef Q_sm_AT +// #undef K_T_sm_AT +// #undef V_sm_AT +// #undef dO_sm_AT +// #undef dK_T_sm_AT +// #undef dV_sm_AT +// #undef S_sm_AT +// #undef P_sm_AT +// #undef L_sm_AT +// #undef dQ_sm_AT +// } + +// /** +// * FlashAttention Forward Function +// * +// * This is the main entry point for FlashAttention forward computation. +// * It creates the output tensor and launches the appropriate kernel based on data type. +// * +// * Args: +// * query: Query tensor of shape [batch_size, seq_len_q, num_heads, head_dim] +// * key: Key tensor of shape [batch_size, seq_len_k, num_heads_kv, head_dim] +// * value: Value tensor of shape [batch_size, seq_len_k, num_heads_kv, head_dim] +// * attn_mask: Optional attention mask tensor +// * scale: Scaling factor for attention scores +// * is_causal: Whether to apply causal masking +// * dropout_p: Dropout probability +// * enable_gqa: Whether to enable grouped-query attention +// * +// * Returns: +// * Output tensor of shape [batch_size, seq_len_q, num_heads, head_dim] +// * logsumexp: Logsumexp tensor for backward pass [batch_size, num_heads, seq_len_q] +// * dropout_seed: Dropout seed for backward pass [1] +// */ + + +// /** +// * Launch FlashAttention Forward Kernel +// * +// * This function sets up grid and block dimensions and launches FlashAttention forward kernel. +// * +// * Args: +// * output: Output tensor +// * query: Query tensor +// * key: Key tensor +// * value: Value tensor +// * attn_mask: Optional attention mask tensor +// * scale: Scaling factor +// * is_causal: Whether to apply causal masking +// * dropout_p: Dropout probability +// * enable_gqa: Whether to enable GQA +// */ +// template +// void LaunchFlashAttentionForward(const std::shared_ptr &output, const std::shared_ptr &query, +// const std::shared_ptr &key, const std::shared_ptr &value, +// const std::shared_ptr &attn_mask, float scale, bool is_causal, +// int64_t dropout_p, bool enable_gqa, float *logsumexp_ptr, unsigned long long dropout_seed) { + +// const auto &query_dims = query->Dims(); +// const auto &key_dims = key->Dims(); +// const auto &value_dims = value->Dims(); + +// // Expected shapes: +// // query: [batch_size, seq_len_q, num_heads, head_dim] +// // key: [batch_size, seq_len_k, num_heads_kv, head_dim] +// // value: [batch_size, seq_len_k, num_heads_kv, head_dim] +// // output: [batch_size, seq_len_q, num_heads, head_dim] + +// int64_t batch_size = query_dims[0]; +// int64_t seq_len_q = query_dims[1]; +// int64_t num_heads = query_dims[2]; +// int64_t head_dim = query_dims[3]; +// int64_t seq_len_k = key_dims[1]; +// int64_t num_heads_kv = key_dims[2]; + +// CHECK_EQ(key_dims[3], head_dim) << "Key head dimension must match query head dimension"; +// CHECK_EQ(value_dims[3], head_dim) << "Value head dimension must match query head dimension"; +// CHECK_EQ(value_dims[1], seq_len_k) << "Value sequence length must match key sequence length"; +// CHECK_EQ(value_dims[2], num_heads_kv) << "Value number of KV heads must match key"; + +// if (enable_gqa) { +// CHECK(num_heads % num_heads_kv == 0) << "Number of query heads must be divisible by number of KV heads for GQA"; +// } else { +// CHECK_EQ(num_heads, num_heads_kv) << "Number of query and KV heads must match for standard attention"; +// } + +// T *output_ptr = static_cast(output->DataPtr()); +// const T *query_ptr = static_cast(query->DataPtr()); +// const T *key_ptr = static_cast(key->DataPtr()); +// const T *value_ptr = static_cast(value->DataPtr()); +// const T *attn_mask_ptr = attn_mask ? static_cast(attn_mask->DataPtr()) : nullptr; + +// // Set up grid and block dimensions according to FlashAttention v2 +// // block_dim(Br, Bc) where Br = Bc = 32 +// // grid_dim(query_heads, batch_size, Tr) where Tr = ceil(seq_len_q / Br) +// constexpr int Br = 32; +// constexpr int Bc = 32; +// int64_t Tr = (seq_len_q + Br - 1) / Br; - dim3 block_dims(Bc, Br); // (blockDim.x, blockDim.y) = (Bc, Br) - dim3 grid_dims(num_heads, batch_size, Tr); // (gridDim.x, gridDim.y, gridDim.z) = (num_heads, batch_size, Tr) - - // Calculate shared memory size (removed dropout_sm allocation) - // SP[Br][Bc] (double) + m_prev[Br] (float) + m_new[Br] (float) + l_prev[Br] (float) + l_new[Br] (float) - // + Q_sm[Br][head_dim] (float) + K_T_sm[head_dim][Bc] (float) + V_sm[Bc][head_dim] (float) + O_sm[Br][head_dim] (float) - size_t shared_mem_size = Br * Bc * sizeof(double) // SP - + 4 * Br * sizeof(float) // m_prev, m_new, l_prev, l_new - + (Br + Bc + Bc + Br) * head_dim) * sizeof(float); // Q_sm, K_T_sm, V_sm, O_sm - // Note: Removed dropout_sm[Br][Bc] (bool) allocation - - auto device = output->GetDevice(); - const auto &cuda_stream = dynamic_cast( - infini_train::core::GetDeviceGuardImpl(device.type())->GetStream(device)) - ->cuda_stream(); - - FlashAttentionForwardKernel<<>>( - output_ptr, query_ptr, key_ptr, value_ptr, attn_mask_ptr, - logsumexp_ptr, scale, is_causal, enable_gqa, dropout_p, dropout_seed, - batch_size, seq_len_q, seq_len_k, num_heads, num_heads_kv, head_dim); -} - -/** - * Launch FlashAttention Backward Kernel - * - * This function sets up grid and block dimensions and launches FlashAttention backward kernel. - * - * Args: - * grad_query: Gradient tensor for query - * grad_key: Gradient tensor for key - * grad_value: Gradient tensor for value - * query: Query tensor from forward pass - * key: Key tensor from forward pass - * value: Value tensor from forward pass - * output: Output tensor from forward pass - * grad_output: Gradient from upstream - * logsumexp: Logsumexp tensor from forward pass - * dropout_seed: Dropout seed for reproducibility - * attn_mask: Optional attention mask tensor - * scale: Scaling factor - * is_causal: Whether causal masking was applied - * dropout_p: Dropout probability - * enable_gqa: Whether GQA was enabled - */ -template -void LaunchFlashAttentionBackward(const std::shared_ptr &grad_query, const std::shared_ptr &grad_key, - const std::shared_ptr &grad_value, const std::shared_ptr &query, - const std::shared_ptr &key, const std::shared_ptr &value, - const std::shared_ptr &output, const std::shared_ptr &grad_output, - const std::shared_ptr &logsumexp, - unsigned long long dropout_seed, - const std::shared_ptr &attn_mask, - float scale, bool is_causal, int64_t dropout_p, bool enable_gqa) { - - const auto &query_dims = query->Dims(); - const auto &key_dims = key->Dims(); - - // Expected shapes: - // query: [batch_size, seq_len_q, num_heads, head_dim] - // key: [batch_size, seq_len_k, num_heads_kv, head_dim] - // value: [batch_size, seq_len_k, num_heads_kv, head_dim] - // output: [batch_size, seq_len_q, num_heads, head_dim] - - int64_t batch_size = query_dims[0]; - int64_t seq_len_q = query_dims[1]; - int64_t num_heads = query_dims[2]; - int64_t head_dim = query_dims[3]; - int64_t seq_len_k = key_dims[1]; - int64_t num_heads_kv = key_dims[2]; - - CHECK_EQ(key_dims[3], head_dim) << "Key head dimension must match query head dimension"; - CHECK_EQ(value->Dims()[3], head_dim) << "Value head dimension must match query head dimension"; - CHECK_EQ(value->Dims()[1], seq_len_k) << "Value sequence length must match key sequence length"; - CHECK_EQ(value->Dims()[2], num_heads_kv) << "Value number of KV heads must match key"; - - if (enable_gqa) { - CHECK(num_heads % num_heads_kv == 0) << "Number of query heads must be divisible by number of KV heads for GQA"; - } else { - CHECK_EQ(num_heads, num_heads_kv) << "Number of query and KV heads must match for standard attention"; - } - - // Precompute D = rowsum(dO ∘ O) before main backward loop - // D shape: [batch_size, seq_len_q, num_heads] - // dO shape: [batch_size, seq_len_q, num_heads, head_dim] - // O shape: [batch_size, seq_len_q, num_heads, head_dim] - auto D = function::Sum(grad_output * output, 3, false); - - T *grad_query_ptr = static_cast(grad_query->DataPtr()); - T *grad_key_ptr = static_cast(grad_key->DataPtr()); - T *grad_value_ptr = static_cast(grad_value->DataPtr()); - const T *query_ptr = static_cast(query->DataPtr()); - const T *key_ptr = static_cast(key->DataPtr()); - const T *value_ptr = static_cast(value->DataPtr()); - const T *output_ptr = static_cast(output->DataPtr()); - const T *grad_output_ptr = static_cast(grad_output->DataPtr()); - const float *logsumexp_ptr = static_cast(logsumexp->DataPtr()); - const float *D_ptr = static_cast(D->DataPtr()); - const T *attn_mask_ptr = attn_mask ? static_cast(attn_mask->DataPtr()) : nullptr; - - // Set up grid and block dimensions according to FlashAttention v2 backward - // block_dim(Bc, Br) where Bc = Br = 32 - // grid_dims(num_heads_q, batch_size, Tr) where Tr = ceil(seq_len_q / Br) - constexpr int Br = 32; - constexpr int Bc = 32; - int64_t Tr = (seq_len_q + Br - 1) / Br; - - dim3 block_dims(Bc, Br); // (blockDim.x, blockDim.y) = (Bc, Br) - dim3 grid_dims(num_heads, batch_size, Tr); // (gridDim.x, gridDim.y, gridDim.z) = (num_heads, batch_size, Tr) - - // Calculate shared memory size for backward pass - // Q_sm[Br][head_dim], K_T_sm[head_dim][Bc], V_sm[Bc][head_dim] - // dO_sm[Br][head_dim], dK_T_sm[head_dim][Bc], dV_sm[Bc][head_dim] - // S_sm[Br][Bc], P_sm[Br][Bc] - // D_sm[Br] - D values loaded from HBM to shared memory - size_t shared_mem_size = Br * head_dim * sizeof(float) // Q_sm - + head_dim * Bc * sizeof(float) // K_T_sm - + Bc * head_dim * sizeof(float) // V_sm - + Br * head_dim * sizeof(float) // dO_sm - + head_dim * Bc * sizeof(float) // dK_T_sm - + Bc * head_dim * sizeof(float) // dV_sm - + Br * Bc * sizeof(float) // S_sm or (dP_sm when compute dP_sm = dO_i @ V_j^T \in R^{Br*Bc}) - + Br * Bc * sizeof(float) // P_sm or (dS_sm when compute dS_sm = P_sm_ij pointwise multiplied by (dP_sm_ij - D_i) \in R^{Br*Bc}) - + Br * sizeof(float) // L_i - + Br * sizeof(float); // D_sm (loaded from HBM) - - auto device = query->GetDevice(); - const auto &cuda_stream = dynamic_cast( - infini_train::core::GetDeviceGuardImpl(device.type())->GetStream(device)) - ->cuda_stream(); - - FlashAttentionBackwardKernel<<>>( - grad_query_ptr, grad_key_ptr, grad_value_ptr, - query_ptr, key_ptr, value_ptr, output_ptr, grad_output_ptr, - logsumexp_ptr, D_ptr, dropout_seed, attn_mask_ptr, - scale, is_causal, dropout_p, enable_gqa, - batch_size, seq_len_q, seq_len_k, num_heads, num_heads_kv, head_dim); -} - -/** - * FlashAttention Backward Function - * - * This is the main entry point for FlashAttention backward computation. - * It creates gradient tensors and launches the appropriate kernel based on data type. - * - * Args: - * grad_query: Gradient tensor for query - * grad_key: Gradient tensor for key - * grad_value: Gradient tensor for value - * query: Query tensor from forward pass - * key: Key tensor from forward pass - * value: Value tensor from forward pass - * output: Output tensor from forward pass - * grad_output: Gradient from upstream - * attn_mask: Optional attention mask tensor - * scale: Scaling factor - * is_causal: Whether causal masking was applied - * dropout_p: Dropout probability (not implemented) - * enable_gqa: Whether GQA was enabled - * - * Returns: - * Tuple of (grad_query, grad_key, grad_value) tensors - */ -template -std::vector> FlashAttentionBackwardImpl(const std::shared_ptr &grad_query, const std::shared_ptr &grad_key, - const std::shared_ptr &grad_value, const std::shared_ptr &query, - const std::shared_ptr &key, const std::shared_ptr &value, - const std::shared_ptr &output, const std::shared_ptr &grad_output, - const std::shared_ptr &logsumexp, - const std::shared_ptr &dropout_seed, - const std::shared_ptr &attn_mask, - float scale, bool is_causal, int64_t dropout_p, bool enable_gqa) { - auto dtype = query->Dtype(); - - // Create gradient tensors with same shapes as inputs - auto grad_query = std::make_shared(query->Dims(), dtype, query->GetDevice()); - auto grad_key = std::make_shared(key->Dims(), dtype, key->GetDevice()); - auto grad_value = std::make_shared(value->Dims(), dtype, value->GetDevice()); - - // Initialize gradients to zero - DispatchFunc(dtype, [=]() { grad_query->Fill(0); }, "CUDA FlashAttentionBackward"); - DispatchFunc(dtype, [=]() { grad_key->Fill(0); }, "CUDA FlashAttentionBackward"); - DispatchFunc(dtype, [=]() { grad_value->Fill(0); }, "CUDA FlashAttentionBackward"); - - // Get dropout seed value - unsigned long long dropout_seed_value = 0; - if (dropout_seed) { - dropout_seed_value = *static_cast(dropout_seed->DataPtr()); - } - - switch (dtype) { - DISPATCH_CASE(WRAP(LaunchFlashAttentionBackward(grad_query, grad_key, grad_value, query, key, value, output, grad_output, - logsumexp, dropout_seed_value, attn_mask, scale, is_causal, - dropout_p, enable_gqa);), - DataType::kFLOAT32) - DISPATCH_CASE(WRAP(LaunchFlashAttentionBackward(grad_query, grad_key, grad_value, query, key, value, output, grad_output, - logsumexp, dropout_seed_value, attn_mask, scale, is_causal, - dropout_p, enable_gqa);), - DataType::kBFLOAT16) - default: - LOG_LOC(FATAL, "CUDA FlashAttention backward: 'Unsupported data type'"); - } - - return {grad_query, grad_key, grad_value}; -} - -// Non-template wrapper functions for registration -FlashAttentionForwardOutput FlashAttentionForward(const std::shared_ptr &query, const std::shared_ptr &key, - const std::shared_ptr &value, - const std::shared_ptr &attn_mask, float scale, bool is_causal, - int64_t dropout_p, bool enable_gqa) { - auto dtype = query->Dtype(); - const auto &query_dims = query->Dims(); - - // Output shape: [batch_size, seq_len_q, num_heads, head_dim] - std::vector output_dims = {query_dims[0], query_dims[1], query_dims[2], query_dims[3]}; - auto output = std::make_shared(output_dims, dtype, query->GetDevice()); - - // Allocate logsumexp tensor for backward pass - // Shape: [batch_size, num_heads, seq_len_q] - std::vector logsumexp_dims = {query_dims[0], query_dims[2], query_dims[1]}; - auto logsumexp = std::make_shared(logsumexp_dims, DataType::kFLOAT32, query->GetDevice()); - float *logsumexp_ptr = static_cast(logsumexp->DataPtr()); - - // Allocate dropout_seed tensor for backward pass - // Shape: [1] - unsigned long long dropout_seed = 0; - std::shared_ptr dropout_seed_tensor; - if (dropout_p > 0) { - std::vector dropout_seed_dims = {1}; - dropout_seed_tensor = std::make_shared(dropout_seed_dims, DataType::kUINT64, query->GetDevice()); - dropout_seed = static_cast(std::time(nullptr)); - unsigned long long *dropout_seed_ptr = static_cast(dropout_seed_tensor->DataPtr()); - *dropout_seed_ptr = dropout_seed; - } - - switch (dtype) { - DISPATCH_CASE(WRAP(LaunchFlashAttentionForward(output, query, key, value, attn_mask, scale, is_causal, - dropout_p, enable_gqa, logsumexp_ptr, - dropout_seed_tensor ? static_cast(dropout_seed_tensor->DataPtr()) : nullptr);), - DataType::kFLOAT32) - DISPATCH_CASE(WRAP(LaunchFlashAttentionForward(output, query, key, value, attn_mask, scale, - is_causal, dropout_p, enable_gqa, logsumexp_ptr, - dropout_seed_tensor ? static_cast(dropout_seed_tensor->DataPtr()) : nullptr);), - DataType::kBFLOAT16) - default: - LOG_LOC(FATAL, "CUDA FlashAttention forward: 'Unsupported data type'"); - } - - FlashAttentionForwardOutput result; - result.output = output; - result.logsumexp = logsumexp; - result.dropout_seed = dropout_seed_tensor; - return result; -} - -std::vector> FlashAttentionBackward(const std::shared_ptr &grad_query, const std::shared_ptr &grad_key, - const std::shared_ptr &grad_value, const std::shared_ptr &query, - const std::shared_ptr &key, const std::shared_ptr &value, - const std::shared_ptr &output, const std::shared_ptr &grad_output, - const std::shared_ptr &logsumexp, - const std::shared_ptr &dropout_seed, - const std::shared_ptr &attn_mask, - float scale, bool is_causal, int64_t dropout_p, bool enable_gqa) { - auto dtype = query->Dtype(); - - // Create gradient tensors with same shapes as inputs - auto grad_query = std::make_shared(query->Dims(), dtype, query->GetDevice()); - auto grad_key = std::make_shared(key->Dims(), dtype, key->GetDevice()); - auto grad_value = std::make_shared(value->Dims(), dtype, value->GetDevice()); - - // Initialize gradients to zero - DispatchFunc(dtype, [=]() { grad_query->Fill(0); }, "CUDA FlashAttentionBackward"); - DispatchFunc(dtype, [=]() { grad_key->Fill(0); }, "CUDA FlashAttentionBackward"); - DispatchFunc(dtype, [=]() { grad_value->Fill(0); }, "CUDA FlashAttentionBackward"); - - // Get dropout seed value - unsigned long long dropout_seed_value = 0; - if (dropout_seed) { - dropout_seed_value = *static_cast(dropout_seed->DataPtr()); - } - - switch (dtype) { - DISPATCH_CASE(WRAP(LaunchFlashAttentionBackward(grad_query, grad_key, grad_value, query, key, value, output, grad_output, - logsumexp, dropout_seed_value, attn_mask, scale, is_causal, - dropout_p, enable_gqa);), - DataType::kFLOAT32) - DISPATCH_CASE(WRAP(LaunchFlashAttentionBackward(grad_query, grad_key, grad_value, query, key, value, output, grad_output, - logsumexp, dropout_seed_value, attn_mask, scale, is_causal, - dropout_p, enable_gqa);), - DataType::kBFLOAT16) - default: - LOG_LOC(FATAL, "CUDA FlashAttention backward: 'Unsupported data type'"); - } - - return {grad_query, grad_key, grad_value}; -} - -} // namespace infini_train::kernels::cuda - -// Register FlashAttention kernels with the dispatcher -#define REGISTER_CUDA_FLASHATTENTION_KERNEL(kernel_name) \ - REGISTER_KERNEL(infini_train::Device::DeviceType::kCUDA, kernel_name, infini_train::kernels::cuda::kernel_name) - -REGISTER_CUDA_FLASHATTENTION_KERNEL(FlashAttentionForward) -REGISTER_CUDA_FLASHATTENTION_KERNEL(FlashAttentionBackward) - -#undef REGISTER_CUDA_FLASHATTENTION_KERNEL +// dim3 block_dims(Bc, Br); // (blockDim.x, blockDim.y) = (Bc, Br) +// dim3 grid_dims(num_heads, batch_size, Tr); // (gridDim.x, gridDim.y, gridDim.z) = (num_heads, batch_size, Tr) + +// // Calculate shared memory size (removed dropout_sm allocation) +// // SP[Br][Bc] (double) + m_prev[Br] (float) + m_new[Br] (float) + l_prev[Br] (float) + l_new[Br] (float) +// // + Q_sm[Br][head_dim] (float) + K_T_sm[head_dim][Bc] (float) + V_sm[Bc][head_dim] (float) + O_sm[Br][head_dim] (float) +// size_t shared_mem_size = Br * Bc * sizeof(double) // SP +// + 4 * Br * sizeof(float) // m_prev, m_new, l_prev, l_new +// + (Br + Bc + Bc + Br) * head_dim) * sizeof(float); // Q_sm, K_T_sm, V_sm, O_sm +// // Note: Removed dropout_sm[Br][Bc] (bool) allocation + +// auto device = output->GetDevice(); +// const auto &cuda_stream = dynamic_cast( +// infini_train::core::GetDeviceGuardImpl(device.type())->GetStream(device)) +// ->cuda_stream(); + +// FlashAttentionForwardKernel<<>>( +// output_ptr, query_ptr, key_ptr, value_ptr, attn_mask_ptr, +// logsumexp_ptr, scale, is_causal, enable_gqa, dropout_p, dropout_seed, +// batch_size, seq_len_q, seq_len_k, num_heads, num_heads_kv, head_dim); +// } + +// /** +// * Launch FlashAttention Backward Kernel +// * +// * This function sets up grid and block dimensions and launches FlashAttention backward kernel. +// * +// * Args: +// * grad_query: Gradient tensor for query +// * grad_key: Gradient tensor for key +// * grad_value: Gradient tensor for value +// * query: Query tensor from forward pass +// * key: Key tensor from forward pass +// * value: Value tensor from forward pass +// * output: Output tensor from forward pass +// * grad_output: Gradient from upstream +// * logsumexp: Logsumexp tensor from forward pass +// * dropout_seed: Dropout seed for reproducibility +// * attn_mask: Optional attention mask tensor +// * scale: Scaling factor +// * is_causal: Whether causal masking was applied +// * dropout_p: Dropout probability +// * enable_gqa: Whether GQA was enabled +// */ +// template +// void LaunchFlashAttentionBackward(const std::shared_ptr &grad_query, const std::shared_ptr &grad_key, +// const std::shared_ptr &grad_value, const std::shared_ptr &query, +// const std::shared_ptr &key, const std::shared_ptr &value, +// const std::shared_ptr &output, const std::shared_ptr &grad_output, +// const std::shared_ptr &logsumexp, +// unsigned long long dropout_seed, +// const std::shared_ptr &attn_mask, +// float scale, bool is_causal, int64_t dropout_p, bool enable_gqa) { + +// const auto &query_dims = query->Dims(); +// const auto &key_dims = key->Dims(); + +// // Expected shapes: +// // query: [batch_size, seq_len_q, num_heads, head_dim] +// // key: [batch_size, seq_len_k, num_heads_kv, head_dim] +// // value: [batch_size, seq_len_k, num_heads_kv, head_dim] +// // output: [batch_size, seq_len_q, num_heads, head_dim] + +// int64_t batch_size = query_dims[0]; +// int64_t seq_len_q = query_dims[1]; +// int64_t num_heads = query_dims[2]; +// int64_t head_dim = query_dims[3]; +// int64_t seq_len_k = key_dims[1]; +// int64_t num_heads_kv = key_dims[2]; + +// CHECK_EQ(key_dims[3], head_dim) << "Key head dimension must match query head dimension"; +// CHECK_EQ(value->Dims()[3], head_dim) << "Value head dimension must match query head dimension"; +// CHECK_EQ(value->Dims()[1], seq_len_k) << "Value sequence length must match key sequence length"; +// CHECK_EQ(value->Dims()[2], num_heads_kv) << "Value number of KV heads must match key"; + +// if (enable_gqa) { +// CHECK(num_heads % num_heads_kv == 0) << "Number of query heads must be divisible by number of KV heads for GQA"; +// } else { +// CHECK_EQ(num_heads, num_heads_kv) << "Number of query and KV heads must match for standard attention"; +// } + +// // Precompute D = rowsum(dO ∘ O) before main backward loop +// // D shape: [batch_size, seq_len_q, num_heads] +// // dO shape: [batch_size, seq_len_q, num_heads, head_dim] +// // O shape: [batch_size, seq_len_q, num_heads, head_dim] +// auto D = function::Sum(grad_output * output, 3, false); + +// T *grad_query_ptr = static_cast(grad_query->DataPtr()); +// T *grad_key_ptr = static_cast(grad_key->DataPtr()); +// T *grad_value_ptr = static_cast(grad_value->DataPtr()); +// const T *query_ptr = static_cast(query->DataPtr()); +// const T *key_ptr = static_cast(key->DataPtr()); +// const T *value_ptr = static_cast(value->DataPtr()); +// const T *output_ptr = static_cast(output->DataPtr()); +// const T *grad_output_ptr = static_cast(grad_output->DataPtr()); +// const float *logsumexp_ptr = static_cast(logsumexp->DataPtr()); +// const float *D_ptr = static_cast(D->DataPtr()); +// const T *attn_mask_ptr = attn_mask ? static_cast(attn_mask->DataPtr()) : nullptr; + +// // Set up grid and block dimensions according to FlashAttention v2 backward +// // block_dim(Bc, Br) where Bc = Br = 32 +// // grid_dims(num_heads_q, batch_size, Tr) where Tr = ceil(seq_len_q / Br) +// constexpr int Br = 32; +// constexpr int Bc = 32; +// int64_t Tr = (seq_len_q + Br - 1) / Br; + +// dim3 block_dims(Bc, Br); // (blockDim.x, blockDim.y) = (Bc, Br) +// dim3 grid_dims(num_heads, batch_size, Tr); // (gridDim.x, gridDim.y, gridDim.z) = (num_heads, batch_size, Tr) + +// // Calculate shared memory size for backward pass +// // Q_sm[Br][head_dim], K_T_sm[head_dim][Bc], V_sm[Bc][head_dim] +// // dO_sm[Br][head_dim], dK_T_sm[head_dim][Bc], dV_sm[Bc][head_dim] +// // S_sm[Br][Bc], P_sm[Br][Bc] +// // D_sm[Br] - D values loaded from HBM to shared memory +// size_t shared_mem_size = Br * head_dim * sizeof(float) // Q_sm +// + head_dim * Bc * sizeof(float) // K_T_sm +// + Bc * head_dim * sizeof(float) // V_sm +// + Br * head_dim * sizeof(float) // dO_sm +// + head_dim * Bc * sizeof(float) // dK_T_sm +// + Bc * head_dim * sizeof(float) // dV_sm +// + Br * Bc * sizeof(float) // S_sm or (dP_sm when compute dP_sm = dO_i @ V_j^T \in R^{Br*Bc}) +// + Br * Bc * sizeof(float) // P_sm or (dS_sm when compute dS_sm = P_sm_ij pointwise multiplied by (dP_sm_ij - D_i) \in R^{Br*Bc}) +// + Br * sizeof(float) // L_i +// + Br * sizeof(float); // D_sm (loaded from HBM) + +// auto device = query->GetDevice(); +// const auto &cuda_stream = dynamic_cast( +// infini_train::core::GetDeviceGuardImpl(device.type())->GetStream(device)) +// ->cuda_stream(); + +// FlashAttentionBackwardKernel<<>>( +// grad_query_ptr, grad_key_ptr, grad_value_ptr, +// query_ptr, key_ptr, value_ptr, output_ptr, grad_output_ptr, +// logsumexp_ptr, D_ptr, dropout_seed, attn_mask_ptr, +// scale, is_causal, dropout_p, enable_gqa, +// batch_size, seq_len_q, seq_len_k, num_heads, num_heads_kv, head_dim); +// } + +// /** +// * FlashAttention Backward Function +// * +// * This is the main entry point for FlashAttention backward computation. +// * It creates gradient tensors and launches the appropriate kernel based on data type. +// * +// * Args: +// * grad_query: Gradient tensor for query +// * grad_key: Gradient tensor for key +// * grad_value: Gradient tensor for value +// * query: Query tensor from forward pass +// * key: Key tensor from forward pass +// * value: Value tensor from forward pass +// * output: Output tensor from forward pass +// * grad_output: Gradient from upstream +// * attn_mask: Optional attention mask tensor +// * scale: Scaling factor +// * is_causal: Whether causal masking was applied +// * dropout_p: Dropout probability (not implemented) +// * enable_gqa: Whether GQA was enabled +// * +// * Returns: +// * Tuple of (grad_query, grad_key, grad_value) tensors +// */ + + +// // Non-template wrapper functions for registration +// FlashAttentionForwardOutput FlashAttentionForward(const std::shared_ptr &query, const std::shared_ptr &key, +// const std::shared_ptr &value, +// const std::shared_ptr &attn_mask, float scale, bool is_causal, +// int64_t dropout_p, bool enable_gqa) { +// auto dtype = query->Dtype(); +// const auto &query_dims = query->Dims(); + +// // Output shape: [batch_size, seq_len_q, num_heads, head_dim] +// std::vector output_dims = {query_dims[0], query_dims[1], query_dims[2], query_dims[3]}; +// auto output = std::make_shared(output_dims, dtype, query->GetDevice()); + +// // Allocate logsumexp tensor for backward pass +// // Shape: [batch_size, num_heads, seq_len_q] +// std::vector logsumexp_dims = {query_dims[0], query_dims[2], query_dims[1]}; +// auto logsumexp = std::make_shared(logsumexp_dims, DataType::kFLOAT32, query->GetDevice()); +// float *logsumexp_ptr = static_cast(logsumexp->DataPtr()); + +// // Allocate dropout_seed tensor for backward pass +// // Shape: [1] +// unsigned long long dropout_seed = 0; +// std::shared_ptr dropout_seed_tensor; +// if (dropout_p > 0) { +// std::vector dropout_seed_dims = {1}; +// dropout_seed_tensor = std::make_shared(dropout_seed_dims, DataType::kUINT64, query->GetDevice()); +// dropout_seed = static_cast(std::time(nullptr)); +// unsigned long long *dropout_seed_ptr = static_cast(dropout_seed_tensor->DataPtr()); +// *dropout_seed_ptr = dropout_seed; +// } + +// switch (dtype) { +// DISPATCH_CASE(WRAP(LaunchFlashAttentionForward(output, query, key, value, attn_mask, scale, is_causal, +// dropout_p, enable_gqa, logsumexp_ptr, +// dropout_seed);), +// DataType::kFLOAT32) +// DISPATCH_CASE(WRAP(LaunchFlashAttentionForward(output, query, key, value, attn_mask, scale, +// is_causal, dropout_p, enable_gqa, logsumexp_ptr, +// dropout_seed);), +// DataType::kBFLOAT16) +// default: +// LOG_LOC(FATAL, "CUDA FlashAttention forward: 'Unsupported data type'"); +// } + +// FlashAttentionForwardOutput result; +// result.output = output; +// result.logsumexp = logsumexp; +// result.dropout_seed = dropout_seed_tensor; +// return result; +// } + +// std::vector> FlashAttentionBackward(const std::shared_ptr &grad_query, const std::shared_ptr &grad_key, +// const std::shared_ptr &grad_value, const std::shared_ptr &query, +// const std::shared_ptr &key, const std::shared_ptr &value, +// const std::shared_ptr &output, const std::shared_ptr &grad_output, +// const std::shared_ptr &logsumexp, +// const std::shared_ptr &dropout_seed, +// const std::shared_ptr &attn_mask, +// float scale, bool is_causal, int64_t dropout_p, bool enable_gqa) { +// auto dtype = query->Dtype(); + +// // Create gradient tensors with same shapes as inputs +// auto grad_query = std::make_shared(query->Dims(), dtype, query->GetDevice()); +// auto grad_key = std::make_shared(key->Dims(), dtype, key->GetDevice()); +// auto grad_value = std::make_shared(value->Dims(), dtype, value->GetDevice()); + +// // Initialize gradients to zero +// DispatchFunc(dtype, [=]() { grad_query->Fill(0); }, "CUDA FlashAttentionBackward"); +// DispatchFunc(dtype, [=]() { grad_key->Fill(0); }, "CUDA FlashAttentionBackward"); +// DispatchFunc(dtype, [=]() { grad_value->Fill(0); }, "CUDA FlashAttentionBackward"); + +// // Get dropout seed value +// unsigned long long dropout_seed_value = 0; +// if (dropout_seed) { +// dropout_seed_value = *static_cast(dropout_seed->DataPtr()); +// } + +// switch (dtype) { +// DISPATCH_CASE(WRAP(LaunchFlashAttentionBackward(grad_query, grad_key, grad_value, query, key, value, output, grad_output, +// logsumexp, dropout_seed_value, attn_mask, scale, is_causal, +// dropout_p, enable_gqa);), +// DataType::kFLOAT32) +// DISPATCH_CASE(WRAP(LaunchFlashAttentionBackward(grad_query, grad_key, grad_value, query, key, value, output, grad_output, +// logsumexp, dropout_seed_value, attn_mask, scale, is_causal, +// dropout_p, enable_gqa);), +// DataType::kBFLOAT16) +// default: +// LOG_LOC(FATAL, "CUDA FlashAttention backward: 'Unsupported data type'"); +// } + +// return {grad_query, grad_key, grad_value}; +// } + +// } // namespace infini_train::kernels::cuda + +// // Register FlashAttention kernels with the dispatcher +// #define REGISTER_CUDA_FLASHATTENTION_KERNEL(kernel_name) \ +// REGISTER_KERNEL(infini_train::Device::DeviceType::kCUDA, kernel_name, infini_train::kernels::cuda::kernel_name) + +// REGISTER_CUDA_FLASHATTENTION_KERNEL(FlashAttentionForward) +// REGISTER_CUDA_FLASHATTENTION_KERNEL(FlashAttentionBackward) + +// #undef REGISTER_CUDA_FLASHATTENTION_KERNEL diff --git a/scripts/test_config.json b/scripts/test_config.json index 5659b516..3d66ff7b 100644 --- a/scripts/test_config.json +++ b/scripts/test_config.json @@ -1,10 +1,10 @@ { "variables": { "BUILD_DIR": "../build", - "GPT2_INPUT_BIN": "../../data/llmc/gpt2/tinyshakespeare/tiny_shakespeare_train.bin", - "GPT2_LLMC_FILEPATH": "../../data/llmc/gpt2/gpt2_124M.bin", - "LLAMA3_INPUT_BIN": "../../data/llmc/llama3/tinyshakespeare/tiny_shakespeare_train.bin", - "LLAMA3_LLMC_FILEPATH": "../../data/llmc/llama3/llama3.2_1B_fp32.bin", + "GPT2_INPUT_BIN": "/data/shared/InfiniTrain-dev/data/llmc/gpt2/tinyshakespeare/tiny_shakespeare_train.bin", + "GPT2_LLMC_FILEPATH": "/data/shared/InfiniTrain-dev/data/llmc/gpt2/gpt2_124M.bin", + "LLAMA3_INPUT_BIN": "/data/shared/InfiniTrain-dev/data/llmc/llama3/tinyshakespeare/tiny_shakespeare_train.bin", + "LLAMA3_LLMC_FILEPATH": "/data/shared/InfiniTrain-dev/data/llmc/llama3/llama3.2_1B_fp32.bin", "PROFILE_LOG_DIR": "./profile_logs", "LOG_DIR": "./logs", "COMPARE_LOG_DIR": "" From 26ea0120cffe0783f5324f214141e99e0a7c954d Mon Sep 17 00:00:00 2001 From: Aoshine999 <820459542@qq.com> Date: Tue, 10 Mar 2026 20:43:21 +0800 Subject: [PATCH 04/18] FlashAttention basic functions passed --- example/gpt2/net.cc | 3 ++- example/llama3/main.cc | 2 +- example/llama3/net.cc | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/example/gpt2/net.cc b/example/gpt2/net.cc index 68aece37..b033236c 100644 --- a/example/gpt2/net.cc +++ b/example/gpt2/net.cc @@ -393,7 +393,8 @@ std::shared_ptr GPT2::FromLLMC(const std::string &filepath) { .original_vocab_size = vocab_size, .n_layer = n_layer, .n_head = n_head, - .n_embd = n_embd}); + .n_embd = n_embd, + .flash = true}); LOG(INFO) << "magic: " << magic << " version: " << version << " block_size: " << block_size << " vocab_size: " << vocab_size << " n_layer: " << n_layer << " n_head: " << n_head diff --git a/example/llama3/main.cc b/example/llama3/main.cc index 22991eeb..963b1cac 100644 --- a/example/llama3/main.cc +++ b/example/llama3/main.cc @@ -70,7 +70,7 @@ DEFINE_uint32(tensor_parallel, 1, "Tensor Parallel world size"); DEFINE_bool(sequence_parallel, false, "Whether to enable Sequence Parallel"); DEFINE_uint32(pipeline_parallel, 1, "Pipeline Parallel world size, specified the number of PP stages."); DEFINE_uint32(virtual_pipeline_parallel, 1, "Number of chunks in PP stage."); -DEFINE_bool(flash, false, "Whether to enable FlashAttention in CausalSelfAttention"); +DEFINE_bool(flash, true, "Whether to enable FlashAttention in CausalSelfAttention"); // precision DEFINE_string(dtype, "float32", "precision used in training (float32/bfloat16)"); // precision check diff --git a/example/llama3/net.cc b/example/llama3/net.cc index fe725992..b2e6b4ee 100644 --- a/example/llama3/net.cc +++ b/example/llama3/net.cc @@ -499,6 +499,7 @@ std::shared_ptr LLaMA3::FromLLMC(const std::string &filepath) { .rope_theta = rope_theta, .use_scaled_rope = static_cast(use_scaled_rope), .norm_eps = norm_eps, + .flash = true, .max_gen_batch_size = max_gen_bs}); // ========== pp_size:num_stages; vpp_size: num_chunks_per_stage ========== From ff91ee3e06b2f9ff0139b87c489d280e01853370 Mon Sep 17 00:00:00 2001 From: Aoshine999 <820459542@qq.com> Date: Wed, 11 Mar 2026 00:05:43 +0800 Subject: [PATCH 05/18] test_result redirect --- scripts/run_models_and_profile.bash | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/run_models_and_profile.bash b/scripts/run_models_and_profile.bash index 1cf27935..363aef34 100755 --- a/scripts/run_models_and_profile.bash +++ b/scripts/run_models_and_profile.bash @@ -188,11 +188,11 @@ if [[ -n "$COMPARE_LOG_DIR" ]]; then # Run compare_loss.py echo -e "\n\033[1;33m[Running] compare_loss.py\033[0m" - python3 "${SCRIPT_DIR}/compare_loss.py" "$COMPARE_LOG_DIR" "$LOG_DIR" || true + python3 "${SCRIPT_DIR}/compare_loss.py" "$COMPARE_LOG_DIR" "$LOG_DIR" > compare_test/loss_comparison.log 2>&1 || true # Run compare_tps.py echo -e "\n\033[1;33m[Running] compare_tps.py\033[0m" - python3 "${SCRIPT_DIR}/compare_tps.py" "$COMPARE_LOG_DIR" "$LOG_DIR" || true + python3 "${SCRIPT_DIR}/compare_tps.py" "$COMPARE_LOG_DIR" "$LOG_DIR" > compare_test/tps_comparison.log 2>&1 || true echo -e "\n\033[1;32mComparison completed.\033[0m" else From 0b8c57af17fc8e88bc0f2de0ffd30d2313d9bd84 Mon Sep 17 00:00:00 2001 From: Aoshine999 <820459542@qq.com> Date: Wed, 11 Mar 2026 00:45:28 +0800 Subject: [PATCH 06/18] Modify run_models_and_profile.bash --- scripts/run_models_and_profile.bash | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/scripts/run_models_and_profile.bash b/scripts/run_models_and_profile.bash index 363aef34..5e4ef8c8 100755 --- a/scripts/run_models_and_profile.bash +++ b/scripts/run_models_and_profile.bash @@ -166,6 +166,21 @@ for ((id=0; id compare_test/loss_comparison.log 2>&1 || true + python3 "${SCRIPT_DIR}/compare_loss.py" "$COMPARE_LOG_DIR" "$LOG_DIR" > compare_logs/loss_comparison.log 2>&1 || true # Run compare_tps.py echo -e "\n\033[1;33m[Running] compare_tps.py\033[0m" - python3 "${SCRIPT_DIR}/compare_tps.py" "$COMPARE_LOG_DIR" "$LOG_DIR" > compare_test/tps_comparison.log 2>&1 || true + python3 "${SCRIPT_DIR}/compare_tps.py" "$COMPARE_LOG_DIR" "$LOG_DIR" > compare_logs/tps_comparison.log 2>&1 || true echo -e "\n\033[1;32mComparison completed.\033[0m" else From d19e67e824f58116d979b80940e3f8393977f3c4 Mon Sep 17 00:00:00 2001 From: Aoshine999 <820459542@qq.com> Date: Sat, 14 Mar 2026 19:59:45 +0800 Subject: [PATCH 07/18] change flash config --- example/gpt2/main.cc | 9 +- example/gpt2/net.cc | 4 +- example/gpt2/net.h | 2 +- example/llama3/main.cc | 9 +- example/llama3/net.cc | 4 +- example/llama3/net.h | 2 +- scripts/run_models_and_profile.bash | 5 +- scripts/test_config.json | 268 +--------------------------- 8 files changed, 19 insertions(+), 284 deletions(-) diff --git a/example/gpt2/main.cc b/example/gpt2/main.cc index d1c82f5b..501704b8 100644 --- a/example/gpt2/main.cc +++ b/example/gpt2/main.cc @@ -71,7 +71,7 @@ DEFINE_uint32(tensor_parallel, 1, "Tensor Parallel world size"); DEFINE_bool(sequence_parallel, false, "Whether to enable Sequence Parallel"); DEFINE_uint32(pipeline_parallel, 1, "Pipeline Parallel world size, specified the number of PP stages."); DEFINE_uint32(virtual_pipeline_parallel, 1, "Number of chunks in PP stage."); -DEFINE_bool(flash, true, "Whether to enable FlashAttention in CausalSelfAttention"); +DEFINE_bool(flash, false, "Whether to enable FlashAttention in CausalSelfAttention"); // precision DEFINE_string(dtype, "float32", "precision used in training (float32/bfloat16)"); @@ -181,11 +181,10 @@ void Train(const nn::parallel::Rank &rank) { // init the model, either from scratch or from OpenAI pretrained checkpoint GPT2Config model_config; std::shared_ptr model = nullptr; + LOG(INFO) << "Rank " << rank.GlobalRank() << ": FLAGS_flash = " << (FLAGS_flash ? "true" : "false"); if (!FLAGS_llmc_filepath.empty()) { - if (FLAGS_flash) { - LOG(WARNING) << "--flash is ignored when loading GPT2 from --llmc_filepath."; - } - model = GPT2::FromLLMC(FLAGS_llmc_filepath); + LOG(INFO) << "Rank " << rank.GlobalRank() << ": Loading GPT2 from LLMC file: " << FLAGS_llmc_filepath; + model = GPT2::FromLLMC(FLAGS_llmc_filepath, FLAGS_flash); } else if (kModelToConfigs.count(FLAGS_model)) { model_config = kModelToConfigs.at(FLAGS_model); model_config.flash = FLAGS_flash; diff --git a/example/gpt2/net.cc b/example/gpt2/net.cc index b033236c..3d777b0e 100644 --- a/example/gpt2/net.cc +++ b/example/gpt2/net.cc @@ -365,7 +365,7 @@ std::tuple DetermineAndCheckVersion(const std:: } } // namespace -std::shared_ptr GPT2::FromLLMC(const std::string &filepath) { +std::shared_ptr GPT2::FromLLMC(const std::string &filepath, bool flash) { if (!std::filesystem::exists(filepath)) { LOG(FATAL) << "File not found: " << filepath; } @@ -394,7 +394,7 @@ std::shared_ptr GPT2::FromLLMC(const std::string &filepath) { .n_layer = n_layer, .n_head = n_head, .n_embd = n_embd, - .flash = true}); + .flash = flash}); LOG(INFO) << "magic: " << magic << " version: " << version << " block_size: " << block_size << " vocab_size: " << vocab_size << " n_layer: " << n_layer << " n_head: " << n_head diff --git a/example/gpt2/net.h b/example/gpt2/net.h index 7f246c22..e429770a 100644 --- a/example/gpt2/net.h +++ b/example/gpt2/net.h @@ -141,7 +141,7 @@ class GPT2 : public infini_train::nn::CloneableModule { Forward(const std::vector> &x) override; static std::shared_ptr FromPretrained(ModelType model_type); - static std::shared_ptr FromLLMC(const std::string &filepath); + static std::shared_ptr FromLLMC(const std::string &filepath, bool flash = false); int GetChunkSize() const; diff --git a/example/llama3/main.cc b/example/llama3/main.cc index 963b1cac..5a6f372c 100644 --- a/example/llama3/main.cc +++ b/example/llama3/main.cc @@ -70,7 +70,7 @@ DEFINE_uint32(tensor_parallel, 1, "Tensor Parallel world size"); DEFINE_bool(sequence_parallel, false, "Whether to enable Sequence Parallel"); DEFINE_uint32(pipeline_parallel, 1, "Pipeline Parallel world size, specified the number of PP stages."); DEFINE_uint32(virtual_pipeline_parallel, 1, "Number of chunks in PP stage."); -DEFINE_bool(flash, true, "Whether to enable FlashAttention in CausalSelfAttention"); +DEFINE_bool(flash, false, "Whether to enable FlashAttention in CausalSelfAttention"); // precision DEFINE_string(dtype, "float32", "precision used in training (float32/bfloat16)"); // precision check @@ -164,11 +164,10 @@ void Train(const nn::parallel::Rank &rank) { LLaMA3Config model_config = LLaMA3Config(); model_config.flash = FLAGS_flash; std::shared_ptr model = nullptr; + LOG(INFO) << "Rank " << rank.GlobalRank() << ": FLAGS_flash = " << (FLAGS_flash ? "true" : "false"); if (!FLAGS_llmc_filepath.empty()) { - if (FLAGS_flash) { - LOG(WARNING) << "--flash is ignored when loading LLaMA3 from --llmc_filepath."; - } - model = LLaMA3::FromLLMC(FLAGS_llmc_filepath); + LOG(INFO) << "Rank " << rank.GlobalRank() << ": Loading LLaMA3 from LLMC file: " << FLAGS_llmc_filepath; + model = LLaMA3::FromLLMC(FLAGS_llmc_filepath, FLAGS_flash); } else { model = std::make_shared(model_config); } diff --git a/example/llama3/net.cc b/example/llama3/net.cc index b2e6b4ee..62a237a1 100644 --- a/example/llama3/net.cc +++ b/example/llama3/net.cc @@ -460,7 +460,7 @@ constexpr int32_t kLLaMA3Magic = 20240803; constexpr int32_t kLLaMA3FP32Version = 3; } // namespace -std::shared_ptr LLaMA3::FromLLMC(const std::string &filepath) { +std::shared_ptr LLaMA3::FromLLMC(const std::string &filepath, bool flash) { if (!std::filesystem::exists(filepath)) { LOG(FATAL) << "File not found: " << filepath; } @@ -499,7 +499,7 @@ std::shared_ptr LLaMA3::FromLLMC(const std::string &filepath) { .rope_theta = rope_theta, .use_scaled_rope = static_cast(use_scaled_rope), .norm_eps = norm_eps, - .flash = true, + .flash = flash, .max_gen_batch_size = max_gen_bs}); // ========== pp_size:num_stages; vpp_size: num_chunks_per_stage ========== diff --git a/example/llama3/net.h b/example/llama3/net.h index 144a7e78..76ea9837 100644 --- a/example/llama3/net.h +++ b/example/llama3/net.h @@ -179,7 +179,7 @@ class LLaMA3 : public infini_train::nn::CloneableModule { Forward(const std::vector> &x) override; static std::shared_ptr FromPretrained(ModelType model_type); - static std::shared_ptr FromLLMC(const std::string &filepath); + static std::shared_ptr FromLLMC(const std::string &filepath, bool flash = false); int GetChunkSize() const { return stage_info_.layer_ranges_per_chunk.size(); } diff --git a/scripts/run_models_and_profile.bash b/scripts/run_models_and_profile.bash index 5e4ef8c8..89d4973d 100755 --- a/scripts/run_models_and_profile.bash +++ b/scripts/run_models_and_profile.bash @@ -135,7 +135,10 @@ args_string_for_test() { jq -r --argjson i "$idx" ' .tests[$i].args | to_entries[] - | "--\(.key) \(.value|tostring)" + | if .value == true then "--\(.key)" + elif .value == false then "--no\(.key)" + else "--\(.key)=\(.value|tostring)" + end ' "$CONFIG_FILE" | paste -sd' ' - } diff --git a/scripts/test_config.json b/scripts/test_config.json index 3d66ff7b..ac07bd13 100644 --- a/scripts/test_config.json +++ b/scripts/test_config.json @@ -10,11 +10,6 @@ "COMPARE_LOG_DIR": "" }, "builds": [ - { - "id": "build_1", - "profile": false, - "cmd": "cmake -DUSE_CUDA=ON -DUSE_NCCL=ON .. && make -j" - }, { "id": "build_2", "profile": true, @@ -22,36 +17,6 @@ } ], "tests": [ - { - "id": "1", - "args": { - "dtype": "float32" - } - }, - { - "id": "1_bfloat16", - "args": { - "dtype": "bfloat16" - } - }, - { - "id": "2", - "args": { - "dtype": "float32", - "num_iteration": 10, - "batch_size": 80, - "total_batch_size": 5120 - } - }, - { - "id": "2_bfloat16", - "args": { - "dtype": "bfloat16", - "num_iteration": 10, - "batch_size": 80, - "total_batch_size": 5120 - } - }, { "id": "3", "args": { @@ -59,239 +24,8 @@ "nthread_per_process": 8, "num_iteration": 10, "batch_size": 10, - "total_batch_size": 5120 - } - }, - { - "id": "3_distopt", - "args": { - "dtype": "float32", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 10, - "total_batch_size": 5120, - "use_distributed_optimizer": true - } - }, - { - "id": "3_bfloat16", - "args": { - "dtype": "bfloat16", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 10, - "total_batch_size": 5120 - } - }, - { - "id": "3_bfloat16_distopt", - "args": { - "dtype": "bfloat16", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 10, - "total_batch_size": 5120, - "use_distributed_optimizer": true - } - }, - { - "id": "4", - "args": { - "dtype": "float32", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 40, - "total_batch_size": 5120, - "tensor_parallel": 4 - } - }, - { - "id": "4_distopt", - "args": { - "dtype": "float32", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 40, - "total_batch_size": 5120, - "tensor_parallel": 4, - "use_distributed_optimizer": true - } - }, - { - "id": "4_bfloat16", - "args": { - "dtype": "bfloat16", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 40, - "total_batch_size": 5120, - "tensor_parallel": 4 - } - }, - { - "id": "4_bfloat16_distopt", - "args": { - "dtype": "bfloat16", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 40, - "total_batch_size": 5120, - "tensor_parallel": 4, - "use_distributed_optimizer": true - } - }, - { - "id": "5", - "args": { - "dtype": "float32", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 40, - "total_batch_size": 5120, - "tensor_parallel": 4, - "sequence_parallel": true - } - }, - { - "id": "5_distopt", - "args": { - "dtype": "float32", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 40, - "total_batch_size": 5120, - "tensor_parallel": 4, - "sequence_parallel": true, - "use_distributed_optimizer": true - } - }, - { - "id": "5_bfloat16", - "args": { - "dtype": "bfloat16", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 40, - "total_batch_size": 5120, - "tensor_parallel": 4, - "sequence_parallel": true - } - }, - { - "id": "5_bfloat16_distopt", - "args": { - "dtype": "bfloat16", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 40, - "total_batch_size": 5120, - "tensor_parallel": 4, - "sequence_parallel": true, - "use_distributed_optimizer": true - } - }, - { - "id": "6", - "args": { - "dtype": "float32", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 10, - "total_batch_size": 5120, - "pipeline_parallel": 8 - } - }, - { - "id": "6_bfloat16", - "args": { - "dtype": "bfloat16", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 10, - "total_batch_size": 5120, - "pipeline_parallel": 8 - } - }, - { - "id": "7", - "args": { - "dtype": "float32", - "nthread_per_process": 4, - "num_iteration": 10, - "batch_size": 10, - "total_batch_size": 5120, - "pipeline_parallel": 4, - "virtual_pipeline_parallel": 2 - } - }, - { - "id": "7_bfloat16", - "args": { - "dtype": "bfloat16", - "nthread_per_process": 4, - "num_iteration": 10, - "batch_size": 10, - "total_batch_size": 5120, - "pipeline_parallel": 4, - "virtual_pipeline_parallel": 2 - } - }, - { - "id": "8", - "args": { - "dtype": "float32", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 40, - "total_batch_size": 5120, - "tensor_parallel": 2, - "sequence_parallel": true, - "pipeline_parallel": 2, - "virtual_pipeline_parallel": 2 - } - }, - { - "id": "8_distopt", - "args": { - "dtype": "float32", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 40, - "total_batch_size": 5120, - "tensor_parallel": 2, - "sequence_parallel": true, - "pipeline_parallel": 2, - "virtual_pipeline_parallel": 2, - "use_distributed_optimizer": true - } - }, - { - "id": "8_bfloat16", - "args": { - "dtype": "bfloat16", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 40, - "total_batch_size": 5120, - "tensor_parallel": 2, - "sequence_parallel": true, - "pipeline_parallel": 2, - "virtual_pipeline_parallel": 2 - } - }, - { - "id": "8_bfloat16_distopt", - "args": { - "dtype": "bfloat16", - "nthread_per_process": 8, - "num_iteration": 10, - "batch_size": 40, "total_batch_size": 5120, - "tensor_parallel": 2, - "sequence_parallel": true, - "pipeline_parallel": 2, - "virtual_pipeline_parallel": 2, - "use_distributed_optimizer": true + "flash": false } } ] From 23a96493e8fcb2ca68cd0f272784d3c090eef23a Mon Sep 17 00:00:00 2001 From: Aoshine999 <820459542@qq.com> Date: Sun, 15 Mar 2026 09:24:01 +0800 Subject: [PATCH 08/18] update bash script --- algo.tex | 75 +++++++ scripts/run_models_and_profile.bash | 62 ++++-- scripts/test_config.json | 16 +- scripts/test_config.json.bak | 299 ++++++++++++++++++++++++++++ 4 files changed, 432 insertions(+), 20 deletions(-) create mode 100644 algo.tex create mode 100644 scripts/test_config.json.bak diff --git a/algo.tex b/algo.tex new file mode 100644 index 00000000..cfb8861b --- /dev/null +++ b/algo.tex @@ -0,0 +1,75 @@ +\begin{algorithm}[H] + % \algsetup{linenosize=\tiny} + \caption{\small\label{alg:flash2_fwd}\sysname forward pass} + \begin{algorithmic}[1] + \REQUIRE Matrices $\vQ, \vK, \vV \in \mathbb{R}^{N \times d}$ in HBM, block sizes $B_c$, $B_r$. + \STATE \label{alg:stream_attn_split_qkv} Divide $\vQ$ into $T_r = \left\lceil\frac{N}{B_r} \right\rceil$ blocks $\vQ_1, \dots, \vQ_{T_r}$ of size $B_r \times d$ each, + and divide $\vK, \vV$ in to $T_c = \left\lceil \frac{N}{B_c} \right\rceil$ blocks $\vK_1, \dots, \vK_{T_c}$ and + $\vV_1, \dots, \vV_{T_c}$, of size $B_c \times d$ each. + \STATE Divide the output $\vO \in \mathbb{R}^{N \times d}$ into $T_r$ blocks $\vO_i, \dots, \vO_{T_r}$ of size + $B_r \times d$ each, and divide the logsumexp $L$ into $T_r$ blocks $L_i, \dots, L_{T_r}$ of size + $B_r$ each. + \FOR{$1 \le i \le T_r$} \label{alg:stream_attn_outer_loop} + \STATE \label{alg:stream_attn_load_q} Load $\vQ_i$ from HBM to on-chip SRAM. + \STATE \label{alg:stream_attn_init} On chip, initialize $\vO_{i}^{(0)} = (0)_{B_r \times d} \in \mathbb{R}^{B_r \times d}, \ell_{i}^{(0)} = (0)_{B_r} \in \mathbb{R}^{B_r}, m_{i}^{(0)} = (-\infty)_{B_r} \in \mathbb{R}^{B_r}$. + \FOR{$1 \le j \le T_c$} + \STATE \label{alg:stream_attn_load_kv} Load $\vK_j, \vV_j$ from HBM to on-chip SRAM. + \STATE \label{alg:stream_attn_qk} On chip, compute $\vS_{i}^{(j)} = \vQ_i \vK_j^T \in \mathbb{R}^{B_r \times B_c}$. + \STATE \label{alg:stream_attn_statistics} On chip, compute + $m_{i}^{(j)} = \mathrm{max}(m_{i}^{(j-1)}, \mathrm{rowmax}(\vS_{i}^{(j)})) \in \mathbb{R}^{B_r}$, $\tilde{\vP}_{i}^{(j)} = \exp(\vS_{i}^{(j)} - m_{i}^{(j)}) \in \mathbb{R}^{B_r \times B_c}$ (pointwise), + $\ell_{i}^{(j)} = e^{m_{i}^{j-1} - m_{i}^{(j)}} \ell_{i}^{(j-1)} + \mathrm{row sum}(\tilde{\vP}_{i}^{(j)}) \in \mathbb{R}^{B_r}$. + \STATE \label{alg:stream_attn_update} On chip, compute + $\vO_{i}^{(j)} = \diag(e^{m_{i}^{(j-1)} - m_{i}^{(j)}}) \vO_{i}^{(j-1)} + \tilde{\vP}_{i}^{(j)} \vV_j$. + \ENDFOR + \STATE On chip, compute $\vO_{i} = \diag(\ell_{i}^{(T_c)})^{-1} \vO_{i}^{(T_c)}$. + \STATE On chip, compute $L_{i} = m_{i}^{(T_c)} + \log(\ell_i^{(T_c)})$. + \STATE Write $\vO_{i}$ to HBM as the $i$-th block of $\vO$. + \STATE Write $L_{i}$ to HBM as the $i$-th block of $L$. + \ENDFOR + \STATE Return the output $\vO$ and the logsumexp $L$. + \end{algorithmic} +\end{algorithm} + + + + +\begin{algorithm}[h] + \caption{\small\label{alg:flash_bwd}\sysname Backward Pass} + \begin{algorithmic}[1] + \REQUIRE Matrices $\vQ, \vK, \vV, \vO, \vdO \in \mathbb{R}^{N \times d}$ in HBM, + vector $L \in \mathbb{R}^N$ in HBM, block sizes $B_c$, $B_r$. + \STATE Divide $\vQ$ into $T_r = \left\lceil\frac{N}{B_r} \right\rceil$ blocks $\vQ_1, \dots, \vQ_{T_r}$ of size $B_r \times d$ each, + and divide $\vK, \vV$ in to $T_c = \left\lceil \frac{N}{B_c} \right\rceil$ blocks $\vK_1, \dots, \vK_{T_c}$ and + $\vV_1, \dots, \vV_{T_c}$, of size $B_c \times d$ each. + \STATE Divide $\vO$ into $T_r$ blocks $\vO_i, \dots, \vO_{T_r}$ of size + $B_r \times d$ each, divide $\vdO$ into $T_r$ blocks $\vdO_i, \dots, \vdO_{T_r}$ + of size $B_r \times d$ each, and divide $L$ into $T_r$ blocks $L_i, \dots, L_{T_r}$ of size + $B_r$ each. + \STATE Initialize $\vdQ = (0)_{N \times d}$ in HBM and divide it into $T_r$ blocks $\vdQ_1, \dots, \vdQ_{T_r}$ of size $B_r \times d$ each. + Divide $\vdK, \vdV \in \mathbb{R}^{N \times d}$ in to $T_c$ blocks $\vdK_1, \dots, \vdK_{T_c}$ and + $\vdV_1, \dots, \vdV_{T_c}$, of size $B_c \times d$ each. + \STATE Compute $D = \mathrm{rowsum}(\vdO \circ \vO) \in \mathbb{R}^d$ (pointwise multiply), write + $D$ to HBM and divide it into $T_r$ blocks $D_1, \dots, D_{T_r}$ of size + $B_r$ each. + \FOR{$1 \le j \le T_c$} + \STATE Load $\vK_j, \vV_j$ from HBM to on-chip SRAM. + \STATE Initialize $\vdK_j = (0)_{B_c \times d}, \vdV_j = (0)_{B_c \times d}$ on SRAM. + \FOR{$1 \le i \le T_r$} + \STATE Load $\vQ_i, \vO_i, \vdO_i, \vdQ_i, L_i, D_i$ from HBM to on-chip SRAM. + \STATE On chip, compute $\vS_{i}^{(j)} = \vQ_i \vK_j^T \in \mathbb{R}^{B_r \times B_c}$. + \STATE On chip, compute $\vP_{i}^{(j)} = \exp(\vS_{ij} - L_{i}) \in \mathbb{R}^{B_r \times B_c}$. + \STATE On chip, compute + $\vdV_j \leftarrow \vdV_j + (\vP_{i}^{(j)})^\top \vdO_i \in \mathbb{R}^{B_c \times d}$. + \STATE On chip, compute + $\vdP_{i}^{(j)} = \vdO_{i} \vV_j^\top \in \mathbb{R}^{B_r \times B_c}$. + \STATE On chip, compute $\vdS_{i}^{(j)} = \vP_{i}^{(j)} \circ (\vdP_{i}^{(j)} - D_i) \in \mathbb{R}^{B_r \times B_c}$. + \STATE Load $\vdQ_i$ from HBM to SRAM, then on chip, update + $\vdQ_{i} \leftarrow \vdQ_i + \vdS_{i}^{(j)} \vK_j \in \mathbb{R}^{B_r \times d}$, and write + back to HBM. + \STATE On chip, compute $\vdK_{j} \leftarrow \vdK_j + {\vdS_{i}^{(j)}}^\top \vQ_i \in \mathbb{R}^{B_c \times d}$. + \ENDFOR + \STATE Write $\vdK_j, \vdV_j$ to HBM. + \ENDFOR + \STATE Return $\vdQ, \vdK, \vdV$. + \end{algorithmic} +\end{algorithm} \ No newline at end of file diff --git a/scripts/run_models_and_profile.bash b/scripts/run_models_and_profile.bash index 89d4973d..65b13ef0 100755 --- a/scripts/run_models_and_profile.bash +++ b/scripts/run_models_and_profile.bash @@ -4,6 +4,7 @@ set -e set -o pipefail CONFIG_FILE="${1:-test_config.json}" +export INFINI_FLASH_BF16_USE_FP32=0 # Dependencies check if ! command -v jq >/dev/null 2>&1; then @@ -167,29 +168,56 @@ for ((id=0; id Date: Sun, 15 Mar 2026 18:58:53 +0800 Subject: [PATCH 09/18] update data process script --- .../src/kernels/cuda/flash_attention.cu | 512 +++++++++--------- scripts/extract_training_stats.py | 426 +++++++++++++++ scripts/run_models_and_profile.bash | 76 ++- 3 files changed, 734 insertions(+), 280 deletions(-) create mode 100755 scripts/extract_training_stats.py diff --git a/infini_train/src/kernels/cuda/flash_attention.cu b/infini_train/src/kernels/cuda/flash_attention.cu index c0ea807b..63518538 100644 --- a/infini_train/src/kernels/cuda/flash_attention.cu +++ b/infini_train/src/kernels/cuda/flash_attention.cu @@ -1,5 +1,7 @@ #include #include +#include +#include #include #include "glog/logging.h" @@ -94,6 +96,71 @@ __device__ __forceinline__ bool FlashAttnDropoutKeep( return rand_val > dropout_p; } +template +__global__ void FlashAttnComputeDFusedKernel(const T *grad_output, const T *output, + float *D_fp32, int64_t rows, int head_dim) { + int64_t row = static_cast(blockIdx.x); + if (row >= rows) { + return; + } + + float local_sum = 0.0f; + int64_t base = row * static_cast(head_dim); + for (int k = threadIdx.x; k < head_dim; k += blockDim.x) { + local_sum += common::cuda::Cast(grad_output[base + k]) * common::cuda::Cast(output[base + k]); + } + + local_sum = warp_reduce_sum(local_sum); + + __shared__ float warp_sums[32]; // up to 1024 threads -> 32 warps + int lane = threadIdx.x & 31; + int warp_id = threadIdx.x >> 5; + int num_warps = (blockDim.x + 31) >> 5; + if (lane == 0) { + warp_sums[warp_id] = local_sum; + } + __syncthreads(); + + if (warp_id == 0) { + float block_sum = (lane < num_warps) ? warp_sums[lane] : 0.0f; + block_sum = warp_reduce_sum(block_sum); + if (lane == 0) { + D_fp32[row] = block_sum; + } + } +} + +template +std::shared_ptr ComputeFlashAttnDFused(const std::shared_ptr &grad_output, + const std::shared_ptr &output) { + const auto &dims = grad_output->Dims(); + CHECK_EQ(dims.size(), 4) << "FlashAttention backward expects grad_output rank=4"; + CHECK(output->Dims() == dims) << "FlashAttention backward expects output and grad_output same shape"; + + std::vector D_dims = {dims[0], dims[1], dims[2]}; // [B, S, H] + auto D_fp32 = std::make_shared(D_dims, DataType::kFLOAT32, grad_output->GetDevice()); + + int64_t rows = dims[0] * dims[1] * dims[2]; + int head_dim = static_cast(dims[3]); + CHECK_LE(rows, static_cast(std::numeric_limits::max())) + << "FlashAttention D fused kernel row count exceeds CUDA grid.x limit"; + + auto device = grad_output->GetDevice(); + const auto &cuda_stream = dynamic_cast( + infini_train::core::GetDeviceGuardImpl(device.type())->GetStream(device)) + ->cuda_stream(); + + constexpr int kThreads = 256; + dim3 grid_dims(static_cast(rows)); + FlashAttnComputeDFusedKernel<<>>( + static_cast(grad_output->DataPtr()), + static_cast(output->DataPtr()), + static_cast(D_fp32->DataPtr()), + rows, head_dim); + + return D_fp32; +} + /** * FlashAttention Forward Kernel @@ -136,7 +203,9 @@ __global__ void FlashAttentionForwardKernel( const int Br = blockDim.y; // Q纵向每块大小, 32 const int Bc = blockDim.x; // K/V纵向分块大小, 32 - const int Tc = gridDim.z; // 对应原始论文中K/V纵向分块数Tc,其中Bc = 32 + const int Tc = (src_seq_len + Bc - 1) / Bc; // 对应原始论文中K/V纵向分块数Tc,其中Bc = 32 + const float dropout_prob = static_cast(dropout_p); + const bool apply_dropout = (dropout_prob > 0.0f && dropout_prob < 1.0f); // 定义一系列临时变量 extern __shared__ char shared_mem[]; @@ -173,7 +242,8 @@ __global__ void FlashAttentionForwardKernel( // Note: Removed DROPOUT_SM_AT macro /****************************preparation**************************/ - int bound_tid_y = min(Br, target_seq_len - Br * bid_z); + int q_tile_start = Br * bid_z; + int bound_tid_y = min(Br, target_seq_len - q_tile_start); // preparation-1: load Qi from GM to SM, and reset Oi to 0 for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { @@ -188,7 +258,7 @@ __global__ void FlashAttentionForwardKernel( // preparation-2: reset m_prev to -INFINITY and l_prev to 0 if (tid_x == 0) { - m_prev[tid_y] = -8192.0; + m_prev[tid_y] = -INFINITY; l_prev[tid_y] = 0.0; } __syncthreads(); @@ -198,22 +268,22 @@ __global__ void FlashAttentionForwardKernel( /****************************main-loop**************************/ #pragma unroll 4 for (int j = 0; j < Tc; ++j) { // 对于每个K/V分块 - bool skip = (is_causal && bid_z < j); + int k_tile_start = Bc * j; + int q_max_pos = q_tile_start + bound_tid_y - 1; + bool skip = (is_causal && (bound_tid_y <= 0 || q_max_pos < k_tile_start)); if (skip) { // early exit, 直接跳过 __syncthreads(); continue; } - SP_AT(tid_y, tid_x) = -8192.0; + SP_AT(tid_y, tid_x) = -INFINITY; __syncthreads(); - int bound_tid_x = min(Bc, src_seq_len - Bc * j); + int bound_tid_x = min(Bc, src_seq_len - k_tile_start); bool is_compute = true; // optimization: 分支处理,加速branch-resolving - if (is_causal) { - if (bid_z < j) { - is_compute = false; // 早期退出情况 - } else if (bid_z == j) { - is_compute = (tid_y >= tid_x); // 对角线以上 - } + if (is_causal && tid_y < bound_tid_y && tid_x < bound_tid_x) { + int global_q_pos = q_tile_start + tid_y; + int global_k_pos = k_tile_start + tid_x; + is_compute = (global_q_pos >= global_k_pos); } // step-1: load Ki, Vi from GM to SM @@ -266,21 +336,24 @@ __global__ void FlashAttentionForwardKernel( // step-5: l_new = exp(m_prev - m_new) * l_prev + rowSum(P) float val2 = float(SP_AT(tid_y, tid_x)); val2 = warp_reduce_sum(val2); - float exp_result = myexp(m_prev[tid_y] - m_new[tid_y]); + float exp_result = 0.0f; + if (tid_y < bound_tid_y) { + exp_result = myexp(m_prev[tid_y] - m_new[tid_y]); + } if (tid_x == 0 && tid_y < bound_tid_y) { l_new[tid_y] = exp_result * l_prev[tid_y] + val2; } __syncthreads(); // step-5.5: Apply dropout to P (using dropout_seed for reproducibility) - if (dropout_p > 0 && tid_y < bound_tid_y && tid_x < bound_tid_x) { - int global_q_idx = Br * bid_z + tid_y; - int global_k_idx = Bc * j + tid_x; + if (apply_dropout && tid_y < bound_tid_y && tid_x < bound_tid_x) { + int global_q_idx = q_tile_start + tid_y; + int global_k_idx = k_tile_start + tid_x; bool keep = FlashAttnDropoutKeep( dropout_seed, bid_y, bid_x, global_q_idx, global_k_idx, - q_heads, target_seq_len, src_seq_len, static_cast(dropout_p)); + q_heads, target_seq_len, src_seq_len, dropout_prob); if (keep) { - SP_AT(tid_y, tid_x) = SP_AT(tid_y, tid_x) / (1.0f - dropout_p); + SP_AT(tid_y, tid_x) = SP_AT(tid_y, tid_x) / (1.0f - dropout_prob); } else { SP_AT(tid_y, tid_x) = 0.0; } @@ -289,11 +362,11 @@ __global__ void FlashAttentionForwardKernel( __syncthreads(); // step-6: O = 1/(exp(m_prev - m_new)) * O + P @ V - if (tid_x < bound_tid_x && tid_y < bound_tid_y) { + if (tid_y < bound_tid_y) { for (int u = tid_x; u < head_dim; u += blockDim.x) { float val3 = 0.0; #pragma unroll - for (int w = 0; w < Bc; ++w) { + for (int w = 0; w < bound_tid_x; ++w) { val3 += float(SP_AT(tid_y, w)) * V_sm_AT(w, u); } O_sm_AT(tid_y, u) = O_sm_AT(tid_y, u) * exp_result + val3; @@ -375,9 +448,6 @@ __global__ void FlashAttentionBackwardKernel( bool enable_gqa, int batch_size, int target_seq_len, int src_seq_len, int q_heads, int kv_heads, int head_dim) { - - // Grid/block dimensions: grid_dims(num_heads_q, batch_size, Tr), block_dim(Bc, Br) - // where Br corresponds to thread block row index, Bc corresponds to column index int tid_x = threadIdx.x; // 横向, blockDim.x列 (Bc) int tid_y = threadIdx.y; // 纵向, blockDim.y行 (Br) int bid_x = blockIdx.x; // x方向, 总数 = #q_heads @@ -387,14 +457,14 @@ __global__ void FlashAttentionBackwardKernel( const int Br = blockDim.y; // Q纵向每块大小, 32 const int Bc = blockDim.x; // K/V纵向分块大小, 32 - // const int Tr = gridDim.z; // Q纵向分块数 const int Tc = (src_seq_len + Bc - 1) / Bc; // K/V纵向分块数 + const float dropout_prob = static_cast(dropout_p); + const bool apply_dropout = (dropout_prob > 0.0f && dropout_prob < 1.0f); // Define shared memory extern __shared__ char shared_mem[]; char *ptr = shared_mem; - - + // Q_sm[Br][head_dim], K_T_sm[head_dim][Bc], V_sm[Bc][head_dim] float *Q_sm = reinterpret_cast(ptr); ptr += Br * head_dim * sizeof(float); @@ -402,335 +472,257 @@ __global__ void FlashAttentionBackwardKernel( ptr += head_dim * Bc * sizeof(float); float *V_sm = reinterpret_cast(ptr); ptr += Bc * head_dim * sizeof(float); - + // dO_sm[Br][head_dim], dQ_sm[Br][head_dim], dK_T_sm[head_dim][Bc], dV_sm[Bc][head_dim] float *dO_sm = reinterpret_cast(ptr); ptr += Br * head_dim * sizeof(float); - // dQ_sm[Br][head_dim] - accumulated gradient for Q float *dQ_sm = reinterpret_cast(ptr); ptr += Br * head_dim * sizeof(float); float *dK_T_sm = reinterpret_cast(ptr); ptr += head_dim * Bc * sizeof(float); float *dV_sm = reinterpret_cast(ptr); ptr += Bc * head_dim * sizeof(float); - + // S_sm[Br][Bc], P_sm[Br][Bc] float *S_sm = reinterpret_cast(ptr); ptr += Br * Bc * sizeof(float); float *P_sm = reinterpret_cast(ptr); ptr += Br * Bc * sizeof(float); - // L_sm[Br] - logsumexp values + // L_sm[Br], D_sm[Br] float *L_sm = reinterpret_cast(ptr); ptr += Br * sizeof(float); - // D_sm[Br] - D values loaded from HBM float *D_sm = reinterpret_cast(ptr); ptr += Br * sizeof(float); - // Define access macros - #define Q_sm_AT(y, x) Q_sm[y * head_dim + x] - #define K_T_sm_AT(y, x) K_T_sm[y * Bc + x] - #define V_sm_AT(y, x) V_sm[y * head_dim + x] - #define dO_sm_AT(y, x) dO_sm[y * head_dim + x] - #define dQ_sm_AT(y, x) dQ_sm[y * head_dim + x] - #define dK_T_sm_AT(y, x) dK_T_sm[y * Bc + x] - #define dV_sm_AT(y, x) dV_sm[y * head_dim + x] - #define S_sm_AT(y, x) S_sm[y * Bc + x] - #define P_sm_AT(y, x) P_sm[y * Bc + x] - #define L_sm_AT(y) L_sm[y] - #define D_sm_AT(y) D_sm[y] +#define Q_sm_AT(y, x) Q_sm[y * head_dim + x] +#define K_T_sm_AT(y, x) K_T_sm[y * Bc + x] +#define V_sm_AT(y, x) V_sm[y * head_dim + x] +#define dO_sm_AT(y, x) dO_sm[y * head_dim + x] +#define dQ_sm_AT(y, x) dQ_sm[y * head_dim + x] +#define dK_T_sm_AT(y, x) dK_T_sm[y * Bc + x] +#define dV_sm_AT(y, x) dV_sm[y * head_dim + x] +#define S_sm_AT(y, x) S_sm[y * Bc + x] +#define P_sm_AT(y, x) P_sm[y * Bc + x] +#define L_sm_AT(y) L_sm[y] +#define D_sm_AT(y) D_sm[y] /****************************Preparation*****************************/ - - // Initialize dQ_sm to 0 for accumulation + const int i = bid_z; + const int q_tile_start = Br * i; + const int bound_tid_y = min(Br, target_seq_len - q_tile_start); + for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { dQ_sm_AT(tid_y, idx) = 0.0f; + Q_sm_AT(tid_y, idx) = 0.0f; + dO_sm_AT(tid_y, idx) = 0.0f; + + if (tid_y < bound_tid_y) { + int global_q_idx = q_tile_start + tid_y; + int q_tensor_idx = ((bid_y * target_seq_len) + global_q_idx) * q_heads + bid_x; + Q_sm_AT(tid_y, idx) = float(query[q_tensor_idx * head_dim + idx]); + dO_sm_AT(tid_y, idx) = float(grad_output[q_tensor_idx * head_dim + idx]); + } } - __syncthreads(); - // Load D_i from HBM to shared memory - int q_idx = Br * bid_z + tid_y; // Global query position within this head - if (q_idx < target_seq_len) { - int d_idx = ((bid_y * q_heads + bid_x) * target_seq_len) + q_idx; - D_sm_AT(tid_y) = D[d_idx]; - } else { - D_sm_AT(tid_y) = 0.0f; // Padding for out-of-bounds positions + if (tid_x == 0) { + if (tid_y < bound_tid_y) { + int global_q_idx = q_tile_start + tid_y; + L_sm_AT(tid_y) = logsumexp[((bid_y * q_heads + bid_x) * target_seq_len) + global_q_idx]; + // D 的布局为 [batch_size, seq_len_q, q_heads] + int d_idx = ((bid_y * target_seq_len) + global_q_idx) * q_heads + bid_x; + D_sm_AT(tid_y) = D[d_idx]; + } else { + L_sm_AT(tid_y) = 0.0f; + D_sm_AT(tid_y) = 0.0f; + } } __syncthreads(); /****************************Main Loop - Outer Loop over K/V tiles*****************************/ - for (int j = 0; j < Tc; ++j) { // For each K/V column tile - - // Skip entire tile if causal and this tile is completely to the right - bool skip_tile = (is_causal && bid_z < j); + for (int j = 0; j < Tc; ++j) { // For each K/V column tile + int k_tile_start = Bc * j; + int bound_tid_x = min(Bc, src_seq_len - k_tile_start); + + int q_max_pos = q_tile_start + bound_tid_y - 1; + bool skip_tile = (is_causal && (bound_tid_y <= 0 || q_max_pos < k_tile_start)); if (skip_tile) { __syncthreads(); continue; } - // Initialize dK_T_sm, dV_sm to 0 for this column tile + // Initialize dK_T_sm, dV_sm for this column tile for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { - for (int y = 0; y < Bc; ++y) { - dK_T_sm_AT(idx, y) = 0.0f; - dV_sm_AT(y, idx) = 0.0f; - } + dK_T_sm_AT(idx, tid_y) = 0.0f; + dV_sm_AT(tid_y, idx) = 0.0f; } - __syncthreads(); - // Load K_j, V_j from HBM to shared memory - int bound_tid_x = min(Bc, src_seq_len - Bc * j); + // Load K_j, V_j for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { K_T_sm_AT(idx, tid_y) = 0.0f; V_sm_AT(tid_y, idx) = 0.0f; if (tid_y < bound_tid_x) { int kv_head_idx = bid_x / p; - K_T_sm_AT(idx, tid_y) = float( - key[((((bid_y * src_seq_len) + (Bc * j + tid_y)) * kv_heads + kv_head_idx)) * head_dim + idx]); - V_sm_AT(tid_y, idx) = float( - value[((((bid_y * src_seq_len) + (Bc * j + tid_y)) * kv_heads + kv_head_idx)) * head_dim + idx]); - } - } - __syncthreads(); - - /****************************Single Q tile for this block*****************************/ - // gridDim.z already indexes the Q row tile, so this block only handles i = bid_z. - const int i = bid_z; - int q_tile_start = Br * i; - int q_tile_end = min(q_tile_start + Br, target_seq_len); - int bound_tid_y = min(Br, q_tile_end - q_tile_start); - - if (tid_y < bound_tid_y) { - int global_q_idx = q_tile_start + tid_y; - int q_tensor_idx = ((bid_y * target_seq_len) + global_q_idx) * q_heads + bid_x; - - for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { - Q_sm_AT(tid_y, idx) = float(query[q_tensor_idx * head_dim + idx]); - dO_sm_AT(tid_y, idx) = float(grad_output[q_tensor_idx * head_dim + idx]); - } - if (tid_x == 0) { - L_sm_AT(tid_y) = logsumexp[((bid_y * q_heads + bid_x) * target_seq_len) + global_q_idx]; + int k_tensor_idx = ((bid_y * src_seq_len) + (k_tile_start + tid_y)) * kv_heads + kv_head_idx; + K_T_sm_AT(idx, tid_y) = float(key[k_tensor_idx * head_dim + idx]); + V_sm_AT(tid_y, idx) = float(value[k_tensor_idx * head_dim + idx]); } } __syncthreads(); - // Get D_i for this row. - float D_i_row = D_sm_AT(tid_y); - - // Recompute S_ij = Q_i @ K_j^T, apply causal mask at element level if needed. - #pragma unroll - for (int y = 0; y < Bc; ++y) { - for (int x = 0; x < Bc; ++x) { - float val = 0.0f; - if (tid_y < bound_tid_y && tid_x < bound_tid_x) { - bool is_compute = true; - if (is_causal && i == j) { - int global_q_pos = q_tile_start + tid_y; - int global_k_pos = Bc * j + x; - is_compute = (global_q_pos >= global_k_pos); - } + // Recompute S_ij and P_ij + if (tid_y < bound_tid_y && tid_x < bound_tid_x) { + int global_q_pos = q_tile_start + tid_y; + int global_k_pos = k_tile_start + tid_x; + bool is_compute = (!is_causal) || (global_q_pos >= global_k_pos); - if (is_compute) { - #pragma unroll - for (int k = 0; k < head_dim; ++k) { - val += Q_sm_AT(tid_y, k) * K_T_sm_AT(k, y); - } - } + float score = 0.0f; + if (is_compute) { +#pragma unroll + for (int k = 0; k < head_dim; ++k) { + score += Q_sm_AT(tid_y, k) * K_T_sm_AT(k, tid_x); } - S_sm_AT(tid_y, x) = val * scale; } + S_sm_AT(tid_y, tid_x) = score * scale; + P_sm_AT(tid_y, tid_x) = is_compute ? myexp(S_sm_AT(tid_y, tid_x) - L_sm_AT(tid_y)) : 0.0f; + } else { + S_sm_AT(tid_y, tid_x) = 0.0f; + P_sm_AT(tid_y, tid_x) = 0.0f; } __syncthreads(); - // Recompute P_ij = exp(S_ij - L_i). - #pragma unroll - for (int y = 0; y < Bc; ++y) { - for (int x = 0; x < Bc; ++x) { - if (tid_y < bound_tid_y && tid_x < bound_tid_x) { - bool is_compute = true; - if (is_causal && i == j) { - int global_q_pos = q_tile_start + tid_y; - int global_k_pos = Bc * j + x; - is_compute = (global_q_pos >= global_k_pos); - } - - if (is_compute) { - P_sm_AT(tid_y, x) = myexp(S_sm_AT(tid_y, x) - L_sm_AT(tid_y)); - } else { - P_sm_AT(tid_y, x) = 0.0f; - } - } else { - P_sm_AT(tid_y, x) = 0.0f; - } + // Apply dropout to P_ij + if (apply_dropout && tid_y < bound_tid_y && tid_x < bound_tid_x) { + int global_q_pos = q_tile_start + tid_y; + int global_k_pos = k_tile_start + tid_x; + bool keep = FlashAttnDropoutKeep( + dropout_seed, bid_y, bid_x, global_q_pos, global_k_pos, + q_heads, target_seq_len, src_seq_len, dropout_prob); + if (keep) { + P_sm_AT(tid_y, tid_x) = P_sm_AT(tid_y, tid_x) / (1.0f - dropout_prob); + } else { + P_sm_AT(tid_y, tid_x) = 0.0f; } } __syncthreads(); - // Apply dropout to P_ij. - if (dropout_p > 0) { - #pragma unroll - for (int y = 0; y < Bc; ++y) { - for (int x = 0; x < Bc; ++x) { - if (tid_y < bound_tid_y && tid_x < bound_tid_x) { - int global_q_pos = q_tile_start + tid_y; - int global_k_pos = Bc * j + x; - bool keep = FlashAttnDropoutKeep( - dropout_seed, bid_y, bid_x, global_q_pos, global_k_pos, - q_heads, target_seq_len, src_seq_len, static_cast(dropout_p)); - if (keep) { - P_sm_AT(tid_y, x) = P_sm_AT(tid_y, x) / (1.0f - dropout_p); - } else { - P_sm_AT(tid_y, x) = 0.0f; - } - } - } - } - __syncthreads(); - } - - // Compute dV_j += P_ij^T @ dO_i. - if (tid_y < bound_tid_y) { - #pragma unroll - for (int x = 0; x < head_dim; x += blockDim.x) { + // dV_j = P_ij^T @ dO_i + if (tid_y < bound_tid_x) { + for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { float val = 0.0f; - #pragma unroll - for (int y = 0; y < Bc; ++y) { - val += P_sm_AT(tid_y, y) * dO_sm_AT(tid_y, x); +#pragma unroll + for (int y = 0; y < Br; ++y) { + if (y < bound_tid_y) { + val += P_sm_AT(y, tid_y) * dO_sm_AT(y, idx); + } } - atomicAdd(&dV_sm_AT(tid_y, x), val); + dV_sm_AT(tid_y, idx) = val; } } __syncthreads(); - // Compute dP_ij = dO_i @ V_j^T. - #pragma unroll - for (int y = 0; y < Bc; ++y) { - for (int x = 0; x < Bc; ++x) { - float val = 0.0f; - if (tid_y < bound_tid_y && tid_x < bound_tid_x) { - #pragma unroll - for (int k = 0; k < head_dim; ++k) { - val += dO_sm_AT(tid_y, k) * V_sm_AT(y, k); - } - S_sm_AT(tid_y, x) = val; // Reuse S_sm as temporary storage for dP_ij - } else { - S_sm_AT(tid_y, x) = 0.0f; - } + // dP_ij = dO_i @ V_j^T (reuse S_sm) + if (tid_y < bound_tid_y && tid_x < bound_tid_x) { + float val = 0.0f; +#pragma unroll + for (int k = 0; k < head_dim; ++k) { + val += dO_sm_AT(tid_y, k) * V_sm_AT(tid_x, k); } + S_sm_AT(tid_y, tid_x) = val; + } else { + S_sm_AT(tid_y, tid_x) = 0.0f; } __syncthreads(); - // Apply dropout to dP_ij. - if (dropout_p > 0) { - #pragma unroll - for (int y = 0; y < Bc; ++y) { - for (int x = 0; x < Bc; ++x) { - if (tid_y < bound_tid_y && tid_x < bound_tid_x) { - int global_q_pos = q_tile_start + tid_y; - int global_k_pos = Bc * j + x; - bool keep = FlashAttnDropoutKeep( - dropout_seed, bid_y, bid_x, global_q_pos, global_k_pos, - q_heads, target_seq_len, src_seq_len, static_cast(dropout_p)); - if (keep) { - S_sm_AT(tid_y, x) = S_sm_AT(tid_y, x) / (1.0f - dropout_p); - } else { - S_sm_AT(tid_y, x) = 0.0f; - } - } - } + // Apply dropout to dP_ij + if (apply_dropout && tid_y < bound_tid_y && tid_x < bound_tid_x) { + int global_q_pos = q_tile_start + tid_y; + int global_k_pos = k_tile_start + tid_x; + bool keep = FlashAttnDropoutKeep( + dropout_seed, bid_y, bid_x, global_q_pos, global_k_pos, + q_heads, target_seq_len, src_seq_len, dropout_prob); + if (keep) { + S_sm_AT(tid_y, tid_x) = S_sm_AT(tid_y, tid_x) / (1.0f - dropout_prob); + } else { + S_sm_AT(tid_y, tid_x) = 0.0f; } - __syncthreads(); } + __syncthreads(); - // Compute dS_ij = P_ij * (dP_ij - D_i). - #pragma unroll - for (int y = 0; y < Bc; ++y) { - for (int x = 0; x < Bc; ++x) { - if (tid_y < bound_tid_y && tid_x < bound_tid_x) { - S_sm_AT(tid_y, x) = P_sm_AT(tid_y, x) * (S_sm_AT(tid_y, x) - D_i_row); - } else { - S_sm_AT(tid_y, x) = 0.0f; - } - } + // dS_ij = P_ij ∘ (dP_ij - D_i) + float D_i_row = (tid_y < bound_tid_y) ? D_sm_AT(tid_y) : 0.0f; + if (tid_y < bound_tid_y && tid_x < bound_tid_x) { + S_sm_AT(tid_y, tid_x) = P_sm_AT(tid_y, tid_x) * (S_sm_AT(tid_y, tid_x) - D_i_row); + } else { + S_sm_AT(tid_y, tid_x) = 0.0f; } __syncthreads(); - // Compute dK_j += dS_ij^T @ Q_i. - if (tid_x < head_dim) { - #pragma unroll - for (int y = 0; y < Bc; ++y) { + // dK_j = dS_ij^T @ Q_i + if (tid_y < bound_tid_x) { + for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { float val = 0.0f; - #pragma unroll - for (int x = 0; x < Br; ++x) { - val += S_sm_AT(x, y) * Q_sm_AT(x, tid_x); +#pragma unroll + for (int y = 0; y < Br; ++y) { + if (y < bound_tid_y) { + val += S_sm_AT(y, tid_y) * Q_sm_AT(y, idx); + } } - atomicAdd(&dK_T_sm_AT(tid_x, y), val * scale); + dK_T_sm_AT(idx, tid_y) = val * scale; } } __syncthreads(); - // Compute dQ_i += dS_ij @ K_j. + // dQ_i += dS_ij @ K_j if (tid_y < bound_tid_y) { - #pragma unroll - for (int x = 0; x < head_dim; x += blockDim.x) { + for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { float val = 0.0f; - #pragma unroll - for (int y = 0; y < Bc; ++y) { - val += S_sm_AT(tid_y, y) * K_T_sm_AT(x, y); +#pragma unroll + for (int x = 0; x < Bc; ++x) { + if (x < bound_tid_x) { + val += S_sm_AT(tid_y, x) * K_T_sm_AT(idx, x); + } } - dQ_sm_AT(tid_y, x) += val * scale; + dQ_sm_AT(tid_y, idx) += val * scale; } } __syncthreads(); // Write back dK_j, dV_j to HBM - // Note: For GQA, dK_j and dV_j need to be accumulated across multiple q-head blocks int kv_head_idx = bid_x / p; - int k_tile_start = Bc * j; - int k_tile_end = min(k_tile_start + Bc, src_seq_len); - - for (int y = 0; y < Bc; ++y) { - int global_k_idx = k_tile_start + y; - if (global_k_idx < src_seq_len) { - for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { - // dK: [batch_size, src_seq_len, kv_heads, head_dim] - int k_tensor_idx = ((bid_y * src_seq_len) + global_k_idx) * kv_heads + kv_head_idx; - atomicAdd(&grad_key[k_tensor_idx * head_dim + idx], T(dK_T_sm_AT(idx, y))); - - // dV: [batch_size, src_seq_len, kv_heads, head_dim] - atomicAdd(&grad_value[k_tensor_idx * head_dim + idx], T(dV_sm_AT(y, idx))); - } + if (tid_y < bound_tid_x) { + int global_k_idx = k_tile_start + tid_y; + int k_tensor_idx = ((bid_y * src_seq_len) + global_k_idx) * kv_heads + kv_head_idx; + for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { + atomicAdd(&grad_key[k_tensor_idx * head_dim + idx], T(dK_T_sm_AT(idx, tid_y))); + atomicAdd(&grad_value[k_tensor_idx * head_dim + idx], T(dV_sm_AT(tid_y, idx))); } } - } // End of outer loop over K/V tiles + __syncthreads(); + } // End of outer loop over K/V tiles // Write back dQ_i to HBM - int q_tile_start = Br * bid_z; - int q_tile_end = min(q_tile_start + Br, target_seq_len); - int bound_tid_y = min(Br, q_tile_end - q_tile_start); - if (tid_y < bound_tid_y) { int global_q_idx = q_tile_start + tid_y; int q_tensor_idx = ((bid_y * target_seq_len) + global_q_idx) * q_heads + bid_x; - for (int idx = tid_x; idx < head_dim; idx += blockDim.x) { - // dQ: [batch_size, target_seq_len, q_heads, head_dim] grad_query[q_tensor_idx * head_dim + idx] += T(dQ_sm_AT(tid_y, idx)); } } // Undefine access macros - #undef D_sm_AT - #undef Q_sm_AT - #undef K_T_sm_AT - #undef V_sm_AT - #undef dO_sm_AT - #undef dK_T_sm_AT - #undef dV_sm_AT - #undef S_sm_AT - #undef P_sm_AT - #undef L_sm_AT - #undef dQ_sm_AT +#undef D_sm_AT +#undef Q_sm_AT +#undef K_T_sm_AT +#undef V_sm_AT +#undef dO_sm_AT +#undef dK_T_sm_AT +#undef dV_sm_AT +#undef S_sm_AT +#undef P_sm_AT +#undef L_sm_AT +#undef dQ_sm_AT } /** @@ -799,6 +791,8 @@ void LaunchFlashAttentionForward(const std::shared_ptr &output, const st CHECK_EQ(value_dims[3], head_dim) << "Value head dimension must match query head dimension"; CHECK_EQ(value_dims[1], seq_len_k) << "Value sequence length must match key sequence length"; CHECK_EQ(value_dims[2], num_heads_kv) << "Value number of KV heads must match key"; + CHECK(dropout_p >= 0 && dropout_p < 1) + << "FlashAttention dropout_p must be in [0, 1). Current API is int64, so only 0 is representable."; if (enable_gqa) { CHECK(num_heads % num_heads_kv == 0) << "Number of query heads must be divisible by number of KV heads for GQA"; @@ -893,6 +887,8 @@ void LaunchFlashAttentionBackward(const std::shared_ptr &grad_query, con CHECK_EQ(value->Dims()[3], head_dim) << "Value head dimension must match query head dimension"; CHECK_EQ(value->Dims()[1], seq_len_k) << "Value sequence length must match key sequence length"; CHECK_EQ(value->Dims()[2], num_heads_kv) << "Value number of KV heads must match key"; + CHECK(dropout_p >= 0 && dropout_p < 1) + << "FlashAttention dropout_p must be in [0, 1). Current API is int64, so only 0 is representable."; if (enable_gqa) { CHECK(num_heads % num_heads_kv == 0) << "Number of query heads must be divisible by number of KV heads for GQA"; @@ -900,11 +896,9 @@ void LaunchFlashAttentionBackward(const std::shared_ptr &grad_query, con CHECK_EQ(num_heads, num_heads_kv) << "Number of query and KV heads must match for standard attention"; } - // Precompute D = rowsum(dO ∘ O) before main backward loop - // D shape: [batch_size, seq_len_q, num_heads] - // dO shape: [batch_size, seq_len_q, num_heads, head_dim] - // O shape: [batch_size, seq_len_q, num_heads, head_dim] - auto D = infini_train::nn::function::Sum(grad_output * output, 3, false); + // Fused D = rowsum(dO ∘ O) into float32, avoiding an intermediate tensor (dO * O). + // D_fp32 shape: [batch_size, seq_len_q, num_heads] + auto D_fp32 = ComputeFlashAttnDFused(grad_output, output); T *grad_query_ptr = static_cast(grad_query->DataPtr()); T *grad_key_ptr = static_cast(grad_key->DataPtr()); @@ -915,7 +909,7 @@ void LaunchFlashAttentionBackward(const std::shared_ptr &grad_query, con const T *output_ptr = static_cast(output->DataPtr()); const T *grad_output_ptr = static_cast(grad_output->DataPtr()); const float *logsumexp_ptr = static_cast(logsumexp->DataPtr()); - const float *D_ptr = static_cast(D->DataPtr()); + const float *D_ptr = static_cast(D_fp32->DataPtr()); const T *attn_mask_ptr = attn_mask ? static_cast(attn_mask->DataPtr()) : nullptr; // Set up grid and block dimensions according to FlashAttention v2 backward diff --git a/scripts/extract_training_stats.py b/scripts/extract_training_stats.py new file mode 100755 index 00000000..ad6086b8 --- /dev/null +++ b/scripts/extract_training_stats.py @@ -0,0 +1,426 @@ +#!/usr/bin/env python3 +""" +Extract training statistics from log files and display them in a table. + +Usage: + python extract_training_stats.py [options] + +Options: + --threshold-fp32: Loss difference threshold for fp32 (default: 1e-5) + --threshold-bf16: Loss difference threshold for bfloat16 (default: 1e-2) +""" + +import re +import sys +from pathlib import Path +from argparse import ArgumentParser +from collections import defaultdict + + +def get_dtype_from_filename(filename): + """Determine dtype from filename. Returns 'bfloat16' or 'fp32'.""" + return 'bfloat16' if '_bfloat16' in filename else 'fp32' + + +def get_flash_from_filename(filename, dir_path): + """Determine flash mode from filename and directory. Returns 'flash' or 'noflash'.""" + # First check if filename contains flash/noflash suffix + if '_flash' in filename: + return 'flash' + elif '_noflash' in filename: + return 'noflash' + # Otherwise, determine from directory name + dir_name = str(dir_path).lower() + if 'compare' in dir_name: + return 'flash' + else: + return 'noflash' + + +def get_disopt_from_filename(filename): + """Determine distributed optimizer from filename. Returns 'disopt' or 'nodisopt'.""" + return 'disopt' if '_distopt' in filename else 'nodisopt' + + +def get_model_from_filename(filename): + """Determine model name from filename. Returns 'gpt2' or 'llama3'.""" + if 'gpt2' in filename: + return 'gpt2' + elif 'llama3' in filename: + return 'llama3' + else: + return 'unknown' + + +def parse_command_line(line): + """Extract configuration from command line.""" + dp_match = re.search(r'--nthread_per_process=(\d+)', line) + dp = int(dp_match.group(1)) if dp_match else 1 + + tp_match = re.search(r'--tensor_parallel=(\d+)', line) + tp = int(tp_match.group(1)) if tp_match else 1 + + sp = 1 + if '--sequence_parallel' in line: + sp = 1 # SP is enabled, but value is not explicitly set + + pp_match = re.search(r'--pipeline_parallel=(\d+)', line) + pp = int(pp_match.group(1)) if pp_match else 1 + + disopt = '--use_distributed_optimizer' in line + + return { + 'dp': dp, + 'tp': tp, + 'sp': sp, + 'pp': pp, + 'disopt': disopt + } + + +def parse_log_file(file_path): + """Extract statistics from log file.""" + stats = { + 'losses': [], + 'times': [], + 'toks_per_sec': [], + 'peak_used': [], + 'peak_reserved': [], + 'config': None + } + + with open(file_path, 'r') as f: + for line in f: + # Parse command line + if line.startswith('[COMMAND]'): + stats['config'] = parse_command_line(line) + continue + + # Parse step information + match = re.search(r'step\s+(\d+)/\d+\s+\|\s+train loss\s+([\d.]+)\s+\|\s+lr\s+[\d.e+-]+\s+\|\s+\(([\d.]+)\s+ms\s+\|\s+(\d+)\s+tok/s.*peak used:\s+(\d+)\s+MB.*peak reserved:\s+(\d+)\s+MB.*DP=(\d+),\s+TP=(\d+),\s+SP=(\d+),\s+PP=(\d+)', line) + if match: + step = int(match.group(1)) + loss = float(match.group(2)) + time_ms = float(match.group(3)) + tok_per_sec = int(match.group(4)) + peak_used = int(match.group(5)) + peak_reserved = int(match.group(6)) + dp = int(match.group(7)) + tp = int(match.group(8)) + sp = int(match.group(9)) + pp = int(match.group(10)) + + stats['losses'].append((step, loss)) + stats['times'].append((step, time_ms)) + stats['toks_per_sec'].append((step, tok_per_sec)) + stats['peak_used'].append((step, peak_used)) + stats['peak_reserved'].append((step, peak_reserved)) + + return stats + + +def calculate_averages(stats, exclude_first_step=True): + """Calculate average values, optionally excluding first step.""" + losses = stats['losses'] + times = stats['times'] + toks_per_sec = stats['toks_per_sec'] + peak_used = stats['peak_used'] + peak_reserved = stats['peak_reserved'] + + start_idx = 1 if exclude_first_step else 0 + + avg_loss = sum(loss for _, loss in losses[start_idx:]) / len(losses[start_idx:]) if len(losses[start_idx:]) > 0 else 0 + avg_time = sum(time for _, time in times[start_idx:]) / len(times[start_idx:]) if len(times[start_idx:]) > 0 else 0 + avg_tok_per_sec = sum(tok for _, tok in toks_per_sec[start_idx:]) / len(toks_per_sec[start_idx:]) if len(toks_per_sec[start_idx:]) > 0 else 0 + avg_peak_used = sum(mem for _, mem in peak_used[start_idx:]) / len(peak_used[start_idx:]) if len(peak_used[start_idx:]) > 0 else 0 + avg_peak_reserved = sum(mem for _, mem in peak_reserved[start_idx:]) / len(peak_reserved[start_idx:]) if len(peak_reserved[start_idx:]) > 0 else 0 + + return { + 'avg_loss': avg_loss, + 'avg_time_ms': avg_time, + 'avg_tok_per_sec': avg_tok_per_sec, + 'avg_peak_used': avg_peak_used, + 'avg_peak_reserved': avg_peak_reserved + } + + +def main(): + parser = ArgumentParser(description='Extract training statistics from log files') + parser.add_argument('dir1', type=Path, help='First log directory') + parser.add_argument('dir2', type=Path, help='Second log directory') + parser.add_argument('--threshold-fp32', type=float, default=1e-5, help='Loss difference threshold for fp32 (default: 1e-5)') + parser.add_argument('--threshold-bf16', type=float, default=1e-2, help='Loss difference threshold for bfloat16 (default: 1e-2)') + parser.add_argument('--markdown', action='store_true', help='Output as markdown table') + parser.add_argument('--output', type=str, default='', help='Output file path (optional)') + parser.add_argument('--speedup', action='store_true', help='Output speedup ratio table') + args = parser.parse_args() + + # Get all log files from both directories + files1 = [(f, args.dir1) for f in args.dir1.glob('*.log') if not f.name.startswith('build') and 'comparison' not in f.name.lower()] + files2 = [(f, args.dir2) for f in args.dir2.glob('*.log') if not f.name.startswith('build') and 'comparison' not in f.name.lower()] + + # Combine files from both directories + all_files = files1 + files2 + + # Parse all files and collect statistics + results = [] + for file_path, dir_path in sorted(all_files, key=lambda x: (x[1], x[0].name)): + filename = file_path.name + model = get_model_from_filename(filename) + dtype = get_dtype_from_filename(filename) + flash = get_flash_from_filename(filename, dir_path) + disopt = get_disopt_from_filename(filename) + + # Extract test ID from filename (e.g., gpt2_3_bfloat16_distopt_profile.log -> 3) + test_id_match = re.search(r'_(\d+)_', filename) + test_id = test_id_match.group(1) if test_id_match else '' + + # Parse log file + stats = parse_log_file(file_path) + averages = calculate_averages(stats, exclude_first_step=True) + + # Get configuration + config = stats['config'] if stats['config'] else {} + dp = config.get('dp', 1) + tp = config.get('tp', 1) + sp = config.get('sp', 1) + pp = config.get('pp', 1) + + # Create row identifier (include test ID to distinguish different test configurations) + row_id = f"{model}_{dtype}_{flash}_{disopt}_{test_id}" + + results.append({ + 'row_id': row_id, + 'model': model, + 'dtype': dtype, + 'flash': flash, + 'disopt': disopt, + 'test_id': test_id, + 'avg_loss': averages['avg_loss'], + 'avg_time_ms': averages['avg_time_ms'], + 'avg_tok_per_sec': averages['avg_tok_per_sec'], + 'avg_peak_used': averages['avg_peak_used'], + 'avg_peak_reserved': averages['avg_peak_reserved'], + 'dp': dp, + 'tp': tp, + 'sp': sp, + 'pp': pp + }) + + # Sort results by row_id + results.sort(key=lambda x: x['row_id']) + + # Group by model, dtype, and disopt (excluding flash) + grouped_by_config = defaultdict(list) + for result in results: + key = f"{result['model']}_{result['dtype']}_{result['disopt']}" + grouped_by_config[key].append(result) + + # Prepare output + if args.markdown: + # Output as markdown table + markdown_lines = [] + + # Add header + markdown_lines.append("| Row ID | Avg Loss | Avg Time (ms) | Avg Tok/s | Peak Used (MB) | Peak Reserved (MB) | DP | TP | SP | PP |") + markdown_lines.append("|---------|----------|---------------|-----------|-----------------|-------------------|----|----|----|----|") + + # Print rows (flash and noflash interleaved) + for key in sorted(grouped_by_config.keys()): + group = grouped_by_config[key] + if len(group) >= 2: + # Separate flash and noflash results + flash_results = [r for r in group if r['flash'] == 'flash'] + noflash_results = [r for r in group if r['flash'] == 'noflash'] + + # Interleave flash and noflash results + for i in range(max(len(flash_results), len(noflash_results))): + if i < len(flash_results): + result = flash_results[i] + row = f"| {result['row_id']:<40} | {result['avg_loss']:<15.6f} | {result['avg_time_ms']:<15.2f} | {result['avg_tok_per_sec']:<15.2f} | {result['avg_peak_used']:<15.2f} | {result['avg_peak_reserved']:<15.2f} | {result['dp']:<5} | {result['tp']:<5} | {result['sp']:<5} | {result['pp']:<5} |" + markdown_lines.append(row) + if i < len(noflash_results): + result = noflash_results[i] + row = f"| {result['row_id']:<40} | {result['avg_loss']:<15.6f} | {result['avg_time_ms']:<15.2f} | {result['avg_tok_per_sec']:<15.2f} | {result['avg_peak_used']:<15.2f} | {result['avg_peak_reserved']:<15.2f} | {result['dp']:<5} | {result['tp']:<5} | {result['sp']:<5} | {result['pp']:<5} |" + markdown_lines.append(row) + else: + # Only one result, print it + for result in group: + row = f"| {result['row_id']:<40} | {result['avg_loss']:<15.6f} {result['avg_time_ms']:<15.2f} {result['avg_tok_per_sec']:<15.2f} {result['avg_peak_used']:<15.2f} | {result['avg_peak_reserved']:<15.2f} | {result['dp']:<5} | {result['tp']:<5} | {result['sp']:<5} | {result['pp']:<5} |" + markdown_lines.append(row) + + # Print markdown table + markdown_table = "\n".join(markdown_lines) + print(markdown_table) + + # Write to file if output path is specified + if args.output: + with open(args.output, 'w') as f: + f.write(markdown_table) + print(f"Markdown table saved to: {args.output}") + else: + # Print plain text table + print("\n" + "=" * 150) + print("Training Statistics Table (excluding step 1)") + print("=" * 150) + + # Print header + header = f"{'Row ID':<40} {'Avg Loss':<15} {'Avg Time (ms)':<15} {'Avg Tok/s':<15} {'Peak Used (MB)':<15} {'Peak Reserved (MB)':<15} {'DP':<5} {'TP':<5} {'SP':<5} {'PP':<5}" + print(header) + print("-" * 150) + + # Print rows (flash and noflash interleaved) + for key in sorted(grouped_by_config.keys()): + group = grouped_by_config[key] + if len(group) >= 2: + # Separate flash and noflash results + flash_results = [r for r in group if r['flash'] == 'flash'] + noflash_results = [r for r in group if r['flash'] == 'noflash'] + + # Interleave flash and noflash results + for i in range(max(len(flash_results), len(noflash_results))): + if i < len(flash_results): + result = flash_results[i] + row = f"{result['row_id']:<40} {result['avg_loss']:<15.6f} {result['avg_time_ms']:<15.2f} {result['avg_tok_per_sec']:<15.2f} {result['avg_peak_used']:<15.2f} {result['avg_peak_reserved']:<15.2f} {result['dp']:<5} {result['tp']:<5} {result['sp']:<5} {result['pp']:<5}" + print(row) + if i < len(noflash_results): + result = noflash_results[i] + row = f"{result['row_id']:<40} {result['avg_loss']:<15.6f} {result['avg_time_ms']:<15.2f} {result['avg_tok_per_sec']:<15.2f} {result['avg_peak_used']:<15.2f} {result['avg_peak_reserved']:<15.2f} {result['dp']:<5} {result['tp']:<5} {result['sp']:<5} {result['pp']:<5}" + print(row) + else: + # Only one result, print it + for result in group: + row = f"{result['row_id']:<40} {result['avg_loss']:<15.6f} {result['avg_time_ms']:<15.2f} {result['avg_tok_per_sec']:<15.2f} {result['avg_peak_used']:<15.2f} {result['avg_peak_reserved']:<15.2f} {result['dp']:<5} {result['tp']:<5} {result['sp']:<5} {result['pp']:<5}" + print(row) + + print("=" * 150) + + # Print comparison between flash and noflash + print("\nComparison between flash and noflash:") + print("=" * 150) + + for key, group in sorted(grouped_by_config.items()): + # Separate flash and noflash results + flash_results = [r for r in group if r['flash'] == 'flash'] + noflash_results = [r for r in group if r['flash'] == 'noflash'] + + if flash_results and noflash_results: + # Calculate averages for each group + avg_flash_loss = sum(r['avg_loss'] for r in flash_results) / len(flash_results) + avg_flash_time = sum(r['avg_time_ms'] for r in flash_results) / len(flash_results) + avg_flash_tok = sum(r['avg_tok_per_sec'] for r in flash_results) / len(flash_results) + avg_flash_peak_used = sum(r['avg_peak_used'] for r in flash_results) / len(flash_results) + avg_flash_peak_reserved = sum(r['avg_peak_reserved'] for r in flash_results) / len(flash_results) + + avg_noflash_loss = sum(r['avg_loss'] for r in noflash_results) / len(noflash_results) + avg_noflash_time = sum(r['avg_time_ms'] for r in noflash_results) / len(noflash_results) + avg_noflash_tok = sum(r['avg_tok_per_sec'] for r in noflash_results) / len(noflash_results) + avg_noflash_peak_used = sum(r['avg_peak_used'] for r in noflash_results) / len(noflash_results) + avg_noflash_peak_reserved = sum(r['avg_peak_reserved'] for r in noflash_results) / len(noflash_results) + + # Calculate differences + loss_diff = avg_flash_loss - avg_noflash_loss + time_diff = avg_flash_time - avg_noflash_time + tok_diff = avg_flash_tok - avg_noflash_tok + + loss_pct = (loss_diff / avg_noflash_loss * 100) if avg_noflash_loss > 0 else 0 + time_pct = (time_diff / avg_noflash_time * 100) if avg_noflash_time > 0 else 0 + tok_pct = (tok_diff / avg_noflash_tok * 100) if avg_noflash_tok > 0 else 0 + + print(f"\n{key}:") + print(f" Loss: {avg_flash_loss:.6f} vs {avg_noflash_loss:.6f} (diff: {loss_diff:+.6f}, {loss_pct:+.2f}%)") + print(f" Time: {avg_flash_time:.2f} vs {avg_noflash_time:.2f} ms (diff: {time_diff:+.2f}, {time_pct:+.2f}%)") + print(f" Tok/s: {avg_flash_tok:.2f} vs {avg_noflash_tok:.2f} (diff: {tok_diff:+.2f}, {tok_pct:+.2f}%)") + + # Calculate memory differences + mem_used_diff = avg_flash_peak_used - avg_noflash_peak_used + mem_reserved_diff = avg_flash_peak_reserved - avg_noflash_peak_reserved + + mem_used_pct = (mem_used_diff / avg_noflash_peak_used * 100) if avg_noflash_peak_used > 0 else 0 + mem_reserved_pct = (mem_reserved_diff / avg_noflash_peak_reserved * 100) if avg_noflash_peak_reserved > 0 else 0 + + print(f" Peak Used: {avg_flash_peak_used:.2f} vs {avg_noflash_peak_used:.2f} MB (diff: {mem_used_diff:+.2f}, {mem_used_pct:+.2f}%)") + print(f" Peak Reserved: {avg_flash_peak_reserved:.2f} vs {avg_noflash_peak_reserved:.2f} MB (diff: {mem_reserved_diff:+.2f}, {mem_reserved_pct:+.2f}%)") + + # Calculate memory differences + mem_used_diff = avg_flash_peak_used - avg_noflash_peak_used + mem_reserved_diff = avg_flash_peak_reserved - avg_noflash_peak_reserved + mem_used_pct = (mem_used_diff / avg_noflash_peak_used * 100) if avg_noflash_peak_used > 0 else 0 + mem_reserved_pct = (mem_reserved_diff / avg_noflash_peak_reserved * 100) if avg_noflash_peak_reserved > 0 else 0 + + # Output speedup ratio table if requested + if args.speedup: + print("\n" + "=" * 120) + print("Speedup Ratio Table (noflash_time / flash_time)") + print("=" * 120) + + # Group by model, dtype, disopt, and test_id + grouped_by_test = defaultdict(list) + for result in results: + key = f"{result['model']}_{result['dtype']}_{result['disopt']}_{result.get('test_id', '')}" + grouped_by_test[key].append(result) + + # Calculate speedup ratios for each test configuration + speedup_results = [] + for key, group in sorted(grouped_by_test.items()): + # Separate flash and noflash results + flash_results = [r for r in group if r['flash'] == 'flash'] + noflash_results = [r for r in group if r['flash'] == 'noflash'] + + if flash_results and noflash_results: + # Calculate average times for this test configuration + avg_flash_time = sum(r['avg_time_ms'] for r in flash_results) / len(flash_results) + avg_noflash_time = sum(r['avg_time_ms'] for r in noflash_results) / len(noflash_results) + + # Calculate speedup ratio + speedup = avg_noflash_time / avg_flash_time if avg_flash_time > 0 else 0 + + # Extract test_id from key + test_id = key.split('_')[-1] + + speedup_results.append({ + 'config': key, + 'speedup': speedup, + 'flash_time': avg_flash_time, + 'noflash_time': avg_noflash_time + }) + + # Sort by configuration name + speedup_results.sort(key=lambda x: x['config']) + + # Print speedup table + if args.markdown: + # Output as markdown table + markdown_lines = [] + markdown_lines.append("| Configuration | Speedup Ratio | Flash Time (ms) | Noflash Time (ms) |") + markdown_lines.append("|---------------|---------------|-----------------|-------------------|") + + for result in speedup_results: + row = f"| {result['config']:<37} | {result['speedup']:<13.2f} | {result['flash_time']:<15.2f} | {result['noflash_time']:<17.2f} |" + markdown_lines.append(row) + + # Print markdown table + markdown_table = "\n".join(markdown_lines) + print(markdown_table) + + # Write to file if output path is specified + if args.output: + output_path = args.output.replace('.md', '_speedup.md') + with open(output_path, 'w') as f: + f.write(markdown_table) + print(f"Speedup table saved to: {output_path}") + else: + # Print plain text table + header = f"{'Configuration':<37} {'Speedup Ratio':<15} {'Flash Time (ms)':<17} {'Noflash Time (ms)':<17}" + print(header) + print("-" * 120) + + for result in speedup_results: + row = f"{result['config']:<37} {result['speedup']:<15.2f} {result['flash_time']:<17.2f} {result['noflash_time']:<17.2f}" + print(row) + + print("=" * 120) + + +if __name__ == '__main__': + main() diff --git a/scripts/run_models_and_profile.bash b/scripts/run_models_and_profile.bash index 65b13ef0..25ec041e 100755 --- a/scripts/run_models_and_profile.bash +++ b/scripts/run_models_and_profile.bash @@ -3,7 +3,22 @@ set -e set -o pipefail -CONFIG_FILE="${1:-test_config.json}" +# Parse arguments +REBUILD=false +while [[ $# -gt 0 ]]; do + case $1 in + --rebuild) + REBUILD=true + shift + ;; + *) + CONFIG_FILE="$1" + shift + ;; + esac +done + +CONFIG_FILE="${CONFIG_FILE:-test_config.json}" export INFINI_FLASH_BF16_USE_FP32=0 # Dependencies check @@ -47,9 +62,11 @@ run_and_log() { local cmd="$1" local log_name="$2" local is_profile="$3" + local log_dir="$4" + local profile_log_dir="$5" local timestamp timestamp=$(date '+%Y-%m-%d %H:%M:%S') - local log_path="$(realpath "${LOG_DIR}/${log_name}.log")" + local log_path="$(realpath "${log_dir}/${log_name}.log")" echo -e "\033[1;32m============================================================\033[0m" echo -e "\033[1;36m[$timestamp] [Running] ${log_name}\033[0m" @@ -66,7 +83,7 @@ run_and_log() { # Notify if profiling mode is enabled if [[ "$is_profile" == "yes" ]]; then - echo -e "\033[1;35m[PROFILE MODE ON] Profiling logs will be saved to: ${PROFILE_LOG_DIR}\033[0m" + echo -e "\033[1;35m[PROFILE MODE ON] Profiling logs will be saved to: ${profile_log_dir}\033[0m" fi echo -e "\033[1;32m============================================================\033[0m" @@ -100,7 +117,7 @@ run_and_log() { # If profiling is enabled, move profiling files to the target directory if [[ "$is_profile" == "yes" ]]; then - move_profile_logs "$log_name" + move_profile_logs "$log_name" "$profile_log_dir" fi } @@ -108,14 +125,15 @@ run_and_log() { # Move profiling output logs move_profile_logs() { local prefix="$1" + local target_profile_log_dir="$2" # Move *.report.rankN files for report_file in "${BUILD_DIR}"/*.report.rank*; do if [[ -f "$report_file" ]]; then local base_name base_name=$(basename "$report_file") - mv "$report_file" "${PROFILE_LOG_DIR}/${prefix}_${base_name}" - echo "Moved $base_name to ${PROFILE_LOG_DIR}/${prefix}_${base_name}" + mv "$report_file" "${target_profile_log_dir}/${prefix}_${base_name}" + echo "Moved $base_name to ${target_profile_log_dir}/${prefix}_${base_name}" fi done @@ -124,8 +142,8 @@ move_profile_logs() { if [[ -f "$record_file" ]]; then local base_name base_name=$(basename "$record_file") - mv "$record_file" "${PROFILE_LOG_DIR}/${prefix}_${base_name}" - echo "Moved $base_name to ${PROFILE_LOG_DIR}/${prefix}_${base_name}" + mv "$record_file" "${target_profile_log_dir}/${prefix}_${base_name}" + echo "Moved $base_name to ${target_profile_log_dir}/${prefix}_${base_name}" fi done } @@ -154,9 +172,29 @@ for ((id=0; id compare_logs/loss_comparison.log 2>&1 || true + python3 "${SCRIPT_DIR}/compare_loss.py" "$COMPARE_LOG_DIR" "$LOG_DIR" --threshold-fp32 1e-1 --threshold-bf16 1e-1 > compare_logs/loss_comparison.log 2>&1 || true # Run compare_tps.py echo -e "\n\033[1;33m[Running] compare_tps.py\033[0m" - python3 "${SCRIPT_DIR}/compare_tps.py" "$COMPARE_LOG_DIR" "$LOG_DIR" > compare_logs/tps_comparison.log 2>&1 || true + python3 "${SCRIPT_DIR}/compare_tps.py" "$COMPARE_LOG_DIR" "$LOG_DIR" --threshold 0.20 > compare_logs/tps_comparison.log 2>&1 || true echo -e "\n\033[1;32mComparison completed.\033[0m" else From 96b8f0b5f44fa92e52f1882b820bedf190768747 Mon Sep 17 00:00:00 2001 From: Aoshine999 <820459542@qq.com> Date: Sun, 15 Mar 2026 18:59:25 +0800 Subject: [PATCH 10/18] update extract staus script --- scripts/extract_training_stats.py | 95 +++++++++++++++++++++++++++++-- 1 file changed, 90 insertions(+), 5 deletions(-) diff --git a/scripts/extract_training_stats.py b/scripts/extract_training_stats.py index ad6086b8..767bd05b 100755 --- a/scripts/extract_training_stats.py +++ b/scripts/extract_training_stats.py @@ -2,12 +2,46 @@ """ Extract training statistics from log files and display them in a table. +This script parses log files from training runs and extracts statistics such as +loss, time, tokens per second, and memory usage. It can compare +results from different configurations (e.g., flash vs noflash attention) +and calculate speedup ratios. + Usage: python extract_training_stats.py [options] +Arguments: + log_dir1: First log directory (typically contains noflash results) + log_dir2: Second log directory (typically contains flash results) + Options: --threshold-fp32: Loss difference threshold for fp32 (default: 1e-5) --threshold-bf16: Loss difference threshold for bfloat16 (default: 1e-2) + --markdown: Output tables in markdown format + --output: Specify output file path (for markdown tables) + --speedup: Output speedup ratio table (noflash_time / flash_time) + +Examples: + # Display training statistics in plain text + python extract_training_stats.py logs compare_logs + + # Display training statistics in markdown format + python extract_training_stats.py logs compare_logs --markdown + + # Display training statistics in markdown and save to file + python extract_training_stats.py logs compare_logs --markdown --output results.md + + # Display speedup ratio table + python extract_training_stats.py logs compare_logs --speedup + + # Display speedup ratio table in markdown and save to file + python extract_training_stats.py logs compare_logs --speedup --markdown --output results.md + +Output: + The script outputs three types of information: + 1. Training Statistics Table: Detailed statistics for each configuration + 2. Comparison between flash and noflash: Comparison of loss, time, and memory + 3. Speedup Ratio Table (if --speedup is specified): Speedup ratios for each configuration """ import re @@ -18,12 +52,29 @@ def get_dtype_from_filename(filename): - """Determine dtype from filename. Returns 'bfloat16' or 'fp32'.""" + """ + Determine dtype from filename. + + Args: + filename: Name of the log file + + Returns: + 'bfloat16' or 'fp32' + """ return 'bfloat16' if '_bfloat16' in filename else 'fp32' def get_flash_from_filename(filename, dir_path): - """Determine flash mode from filename and directory. Returns 'flash' or 'noflash'.""" + """ + Determine flash mode from filename and directory. + + Args: + filename: Name of the log file + dir_path: Path to the directory containing the log file + + Returns: + 'flash' or 'noflash' + """ # First check if filename contains flash/noflash suffix if '_flash' in filename: return 'flash' @@ -79,7 +130,16 @@ def parse_command_line(line): def parse_log_file(file_path): - """Extract statistics from log file.""" + """ + Extract statistics from log file. + + Args: + file_path: Path to the log file + + Returns: + Dictionary containing lists of losses, times, tokens per second, + peak memory usage, and configuration + """ stats = { 'losses': [], 'times': [], @@ -120,7 +180,19 @@ def parse_log_file(file_path): def calculate_averages(stats, exclude_first_step=True): - """Calculate average values, optionally excluding first step.""" + """ + Calculate average values, optionally excluding first step. + + Args: + stats: Dictionary containing lists of losses, times, tokens per second, + and peak memory usage + exclude_first_step: If True, exclude the first step from calculations + (default: True, as first step may have overhead) + + Returns: + Dictionary containing average values for loss, time, tokens per second, + and peak memory usage + """ losses = stats['losses'] times = stats['times'] toks_per_sec = stats['toks_per_sec'] @@ -145,6 +217,17 @@ def calculate_averages(stats, exclude_first_step=True): def main(): + """ + Main function to extract and display training statistics. + + This function: + 1. Parses command line arguments + 2. Reads log files from two directories + 3. Extracts statistics from each log file + 4. Groups results by configuration + 5. Displays tables and comparisons + 6. Optionally displays speedup ratios + """ parser = ArgumentParser(description='Extract training statistics from log files') parser.add_argument('dir1', type=Path, help='First log directory') parser.add_argument('dir2', type=Path, help='Second log directory') @@ -355,6 +438,7 @@ def main(): print("=" * 120) # Group by model, dtype, disopt, and test_id + # This allows us to calculate speedup for each individual test configuration grouped_by_test = defaultdict(list) for result in results: key = f"{result['model']}_{result['dtype']}_{result['disopt']}_{result.get('test_id', '')}" @@ -373,6 +457,7 @@ def main(): avg_noflash_time = sum(r['avg_time_ms'] for r in noflash_results) / len(noflash_results) # Calculate speedup ratio + # Speedup > 1.0 means flash is faster than noflash speedup = avg_noflash_time / avg_flash_time if avg_flash_time > 0 else 0 # Extract test_id from key @@ -385,7 +470,7 @@ def main(): 'noflash_time': avg_noflash_time }) - # Sort by configuration name + # Sort by configuration name (alphabetical order) speedup_results.sort(key=lambda x: x['config']) # Print speedup table From 6de51d70253ad1db6f6f9ada9283a2164ec0731f Mon Sep 17 00:00:00 2001 From: Aoshine999 <820459542@qq.com> Date: Mon, 16 Mar 2026 10:04:11 +0800 Subject: [PATCH 11/18] change test config --- scripts/test_config.json | 251 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 251 insertions(+) diff --git a/scripts/test_config.json b/scripts/test_config.json index a342320c..f8171b3a 100644 --- a/scripts/test_config.json +++ b/scripts/test_config.json @@ -17,6 +17,57 @@ } ], "tests": [ + { + "id": "1", + "args": { + "dtype": "float32" + } + }, + { + "id": "1_bfloat16", + "args": { + "dtype": "bfloat16" + } + }, + { + "id": "2", + "args": { + "dtype": "float32", + "num_iteration": 10, + "batch_size": 80, + "total_batch_size": 5120 + } + }, + { + "id": "2_bfloat16", + "args": { + "dtype": "bfloat16", + "num_iteration": 10, + "batch_size": 80, + "total_batch_size": 5120 + } + }, + { + "id": "3", + "args": { + "dtype": "float32", + "nthread_per_process": 8, + "num_iteration": 10, + "batch_size": 10, + "total_batch_size": 5120 + } + }, + { + "id": "3_distopt", + "args": { + "dtype": "float32", + "nthread_per_process": 8, + "num_iteration": 10, + "batch_size": 10, + "total_batch_size": 5120, + "use_distributed_optimizer": true + } + }, { "id": "3_bfloat16", "args": { @@ -37,6 +88,206 @@ "total_batch_size": 5120, "use_distributed_optimizer": true } + }, + { + "id": "4", + "args": { + "dtype": "float32", + "nthread_per_process": 8, + "num_iteration": 10, + "batch_size": 40, + "total_batch_size": 5120, + "tensor_parallel": 4 + } + }, + { + "id": "4_distopt", + "args": { + "dtype": "float32", + "nthread_per_process": 8, + "num_iteration": 10, + "batch_size": 40, + "total_batch_size": 5120, + "tensor_parallel": 4, + "use_distributed_optimizer": true + } + }, + { + "id": "4_bfloat16", + "args": { + "dtype": "bfloat16", + "nthread_per_process": 8, + "num_iteration": 10, + "batch_size": 40, + "total_batch_size": 5120, + "tensor_parallel": 4 + } + }, + { + "id": "4_bfloat16_distopt", + "args": { + "dtype": "bfloat16", + "nthread_per_process": 8, + "num_iteration": 10, + "batch_size": 40, + "total_batch_size": 5120, + "tensor_parallel": 4, + "use_distributed_optimizer": true + } + }, + { + "id": "5", + "args": { + "dtype": "float32", + "nthread_per_process": 8, + "num_iteration": 10, + "batch_size": 40, + "total_batch_size": 5120, + "tensor_parallel": 4, + "sequence_parallel": true + } + }, + { + "id": "5_distopt", + "args": { + "dtype": "float32", + "nthread_per_process": 8, + "num_iteration": 10, + "batch_size": 40, + "total_batch_size": 5120, + "tensor_parallel": 4, + "sequence_parallel": true, + "use_distributed_optimizer": true + } + }, + { + "id": "5_bfloat16", + "args": { + "dtype": "bfloat16", + "nthread_per_process": 8, + "num_iteration": 10, + "batch_size": 40, + "total_batch_size": 5120, + "tensor_parallel": 4, + "sequence_parallel": true + } + }, + { + "id": "5_bfloat16_distopt", + "args": { + "dtype": "bfloat16", + "nthread_per_process": 8, + "num_iteration": 10, + "batch_size": 40, + "total_batch_size": 5120, + "tensor_parallel": 4, + "sequence_parallel": true, + "use_distributed_optimizer": true + } + }, + { + "id": "6", + "args": { + "dtype": "float32", + "nthread_per_process": 8, + "num_iteration": 10, + "batch_size": 10, + "total_batch_size": 5120, + "pipeline_parallel": 8 + } + }, + { + "id": "6_bfloat16", + "args": { + "dtype": "bfloat16", + "nthread_per_process": 8, + "num_iteration": 10, + "batch_size": 10, + "total_batch_size": 5120, + "pipeline_parallel": 8 + } + }, + { + "id": "7", + "args": { + "dtype": "float32", + "nthread_per_process": 4, + "num_iteration": 10, + "batch_size": 10, + "total_batch_size": 5120, + "pipeline_parallel": 4, + "virtual_pipeline_parallel": 2 + } + }, + { + "id": "7_bfloat16", + "args": { + "dtype": "bfloat16", + "nthread_per_process": 4, + "num_iteration": 10, + "batch_size": 10, + "total_batch_size": 5120, + "pipeline_parallel": 4, + "virtual_pipeline_parallel": 2 + } + }, + { + "id": "8", + "args": { + "dtype": "float32", + "nthread_per_process": 8, + "num_iteration": 10, + "batch_size": 40, + "total_batch_size": 5120, + "tensor_parallel": 2, + "sequence_parallel": true, + "pipeline_parallel": 2, + "virtual_pipeline_parallel": 2 + } + }, + { + "id": "8_distopt", + "args": { + "dtype": "float32", + "nthread_per_process": 8, + "num_iteration": 10, + "batch_size": 40, + "total_batch_size": 5120, + "tensor_parallel": 2, + "sequence_parallel": true, + "pipeline_parallel": 2, + "virtual_pipeline_parallel": 2, + "use_distributed_optimizer": true + } + }, + { + "id": "8_bfloat16", + "args": { + "dtype": "bfloat16", + "nthread_per_process": 8, + "num_iteration": 10, + "batch_size": 40, + "total_batch_size": 5120, + "tensor_parallel": 2, + "sequence_parallel": true, + "pipeline_parallel": 2, + "virtual_pipeline_parallel": 2 + } + }, + { + "id": "8_bfloat16_distopt", + "args": { + "dtype": "bfloat16", + "nthread_per_process": 8, + "num_iteration": 10, + "batch_size": 40, + "total_batch_size": 5120, + "tensor_parallel": 2, + "sequence_parallel": true, + "pipeline_parallel": 2, + "virtual_pipeline_parallel": 2, + "use_distributed_optimizer": true + } } ] } From 325b8a5b243c8c0174163970f0157844cd88febc Mon Sep 17 00:00:00 2001 From: Aoshine999 <820459542@qq.com> Date: Mon, 16 Mar 2026 10:40:48 +0800 Subject: [PATCH 12/18] upload test stats --- run_help.md | 421 ++++++++++++++++++++++++++++++ scripts/extract_training_stats.py | 246 +++++++++-------- scripts/results.md | 82 ++++++ scripts/results_speedup.md | 42 +++ 4 files changed, 666 insertions(+), 125 deletions(-) create mode 100644 run_help.md create mode 100644 scripts/results.md create mode 100644 scripts/results_speedup.md diff --git a/run_help.md b/run_help.md new file mode 100644 index 00000000..58f84b0d --- /dev/null +++ b/run_help.md @@ -0,0 +1,421 @@ +# InfiniTrain 运行指南 + +本指南介绍如何使用 `scripts` 目录下的脚本进行模型训练、性能分析和结果统计。 + +--- + +## 快速开始 + +### 基本运行 + +最简单的运行方式: + +```bash +cd ~/InfiniTrain/scripts +bash run_models_and_profile.bash +``` + +这将使用默认配置文件 `test_config.json` 运行所有测试。 + +### 强制重新构建 + +如果需要重新构建项目: + +```bash +cd ~/InfiniTrain/scripts +bash run_models_and_profile.bash --rebuild +``` + +### 使用自定义配置文件 + +```bash +cd ~/InfiniTrain/scripts +bash run_models_and_profile.bash /path/to/custom_config.json +``` + +--- + +## 脚本说明 + +### 1. run_models_and_profile.bash + +主运行脚本,负责: +- 从配置文件读取测试配置 +- 构建项目(如果需要) +- 运行 GPT2 和 LLaMA3 模型测试 +- 收集训练日志和性能分析数据 +- 自动运行比较脚本 + +**依赖工具**: +- `jq`:用于解析 JSON 配置文件 + +**安装 jq**: +```bash +sudo apt-get install -y jq +``` + +**运行流程**: +1. 读取配置文件(默认:`test_config.json`) +2. 检查是否需要构建(或使用 `--rebuild` 强制重新构建) +3. 对每个测试配置,分别运行: + - GPT2 模型(noflash 模式) + - GPT2 模型(flash 模式) + - LLaMA3 模型(noflash 模式) + - LLaMA3 模型(flash 模式) +4. 如果启用了性能分析,收集性能报告 +5. 如果设置了 `COMPARE_LOG_DIR`,自动运行比较脚本 + +**日志输出**: +- 训练日志:`logs/` 目录(noflash 结果) +- 训练日志:`compare_logs/` 目录(flash 结果) +- 性能日志:`profile_logs/` 目录(noflash 结果) +- 性能日志:`compare_profile_logs/` 目录(flash 结果) + +--- + +### 2. test_config.json + +配置文件包含三个主要部分: + +#### 2.1 variables - 变量定义 + +```json +{ + "variables": { + "BUILD_DIR": "../build", + "GPT2_INPUT_BIN": "/path/to/gpt2/input.bin", + "GPT2_LLMC_FILEPATH": "/path/to/gpt2/model.bin", + "LLAMA3_INPUT_BIN": "/path/to/llama3/input.bin", + "LLAMA3_LLMC_FILEPATH": "/path/to/llama3/model.bin", + "PROFILE_LOG_DIR": "./profile_logs", + "LOG_DIR": "./logs", + "COMPARE_LOG_DIR": "" + } +} +``` + +**变量说明**: +- `BUILD_DIR`:构建目录路径 +- `GPT2_INPUT_BIN`:GPT2 输入数据文件路径 +- `GPT2_LLMC_FILEPATH`:GPT2 模型文件路径 +- `LLAMA3_INPUT_BIN`:LLaMA3 输入数据文件路径 +- `LLAMA3_LLMC_FILEPATH`:LLaMA3 模型文件路径 +- `PROFILE_LOG_DIR`:性能分析日志目录 +- `LOG_DIR`:训练日志目录 +- `COMPARE_LOG_DIR`:基准日志目录(用于比较,留空则不运行比较) + +#### 2.2 builds - 构建配置 + +```json +{ + "builds": [ + { + "id": "build_2", + "profile": true, + "cmd": "cmake -DUSE_CUDA=ON -DUSE_NCCL=ON -DPROFILE_MODE=ON .. && make -j" + } + ] +} +``` + +**配置说明**: +- `id`:构建 ID(用于日志命名) +- `profile`:是否启用性能分析(true/false) +- `cmd`:CMake 构建命令 + +#### 2.3 tests - 测试配置 + +每个测试包含以下参数: + +```json +{ + "id": "1", + "args": { + "dtype": "float32", + "num_iteration": 10, + "batch_size": 80, + "total_batch_size": 5120, + "nthread_per_process": 8, + "tensor_parallel": 4, + "sequence_parallel": true, + "pipeline_parallel": 8, + "virtual_pipeline_parallel": 2, + "use_distributed_optimizer": true + } +} +``` + +**参数说明**: +- `dtype`:数据类型(float32/bfloat16) +- `num_iteration`:迭代次数 +- `batch_size`:批次大小 +- `total_batch_size`:总批次大小 +- `nthread_per_process`:每进程线程数(数据并行) +- `tensor_parallel`:张量并行度 +- `sequence_parallel`:序列并行(true/false) +- `pipeline_parallel`:流水线并行度 +- `virtual_pipeline_parallel`:虚拟流水线并行度 +- `use_distributed_optimizer`:使用分布式优化器(true/false) + +**测试 ID 命名规则**: +- `1`:基础测试 +- `1_bfloat16`:基础测试(bfloat16) +- `2`:批次大小测试 +- `3`:数据并行测试 +- `3_distopt`:数据并行 + 分布式优化器 +- `4`:张量并行测试 +- `5`:张量并行 + 序列并行测试 +- `6`:流水线并行测试 +- `7`:虚拟流水线并行测试 +- `8`:混合并行测试 + +--- + +### 3. extract_training_stats.py + +从日志文件中提取训练统计信息,包括: +- 平均损失 +- 平均时间 +- 平均 tokens/s +- 峰值内存使用 +- 并行配置(DP、TP、SP、PP) + +**基本用法**: +```bash +python3 extract_training_stats.py +``` + +**选项**: +- `--threshold-fp32`:FP32 损失差异阈值(默认:1e-5) +- `--threshold-bf16`:BF16 损失差异阈值(默认:1e-2) +- `--markdown`:输出 Markdown 格式表格 +- `--output `:指定输出文件路径 +- `--speedup`:输出加速比表格 + +**示例**: + +```bash +# 显示训练统计信息和对比 +python3 extract_training_stats.py logs compare_logs + +# 输出 Markdown 格式 +python3 extract_training_stats.py logs compare_logs --markdown + +# 输出 Markdown 并保存到文件 +python3 extract_training_stats.py logs compare_logs --markdown --output results.md + +# 只显示加速比(不显示训练统计和对比信息) +python3 extract_training_stats.py logs compare_logs --speedup + +# 只显示加速比并保存为 Markdown +python3 extract_training_stats.py logs compare_logs --speedup --markdown --output speedup.md +``` + +**输出说明**: + +**不使用 `--speedup` 时**: + +1. **训练统计表**: + - Row ID:配置标识(模型_数据类型_flash/noflash_分布式优化器_测试ID) + - Avg Loss:平均损失 + - Avg Time:平均时间(ms) + - Avg Tok/s:平均 tokens/s + - Peak Used:峰值内存使用(MB) + - Peak Reserved:峰值内存保留(MB) + - DP/TP/SP/PP:并行配置 + +2. **Flash vs Noflash 对比**: + - 损失对比及差异百分比 + - 时间对比及差异百分比 + - Tokens/s 对比及差异百分比 + - 内存使用对比及差异百分比 + +**使用 `--speedup` 时**: + +3. **加速比表**: + - Configuration:配置名称 + - Speedup Ratio:加速比(noflash_time / flash_time) + - Flash Time:Flash 模式平均时间 + - Noflash Time:Noflash 模式平均时间 + +**加速比说明**: +- 加速比 > 1.0:Flash Attention 更快 +- 加速比 < 1.0:Flash Attention 更慢 + +--- + +### 4. compare_loss.py + +比较两个日志目录中的训练损失,用于验证正确性。 + +**基本用法**: +```bash +python3 compare_loss.py +``` + +**选项**: +- `--threshold-fp32`:FP32 损失差异阈值(默认:1e-5) +- `--threshold-bf16`:BF16 损失差异阈值(默认:1e-2) +- `--verbose`:显示所有文件的详细输出(包括通过的) + +**示例**: + +```bash +# 使用默认阈值比较 +python3 compare_loss.py logs compare_logs + +# 自定义阈值 +python3 compare_loss.py logs compare_logs --threshold-fp32 1e-5 --threshold-bf16 1e-2 + +# 显示详细输出 +python3 compare_loss.py logs compare_logs --verbose +``` + +**输出说明**: + +1. **文件差异**: + - 只在 dir1 中的文件 + - 只在 dir2 中的文件 + +2. **每个测试的对比**: + - 不匹配的步骤及损失值 + - 差异值和相对误差百分比 + - 匹配步骤数统计 + +3. **总体统计**: + - FP32 测试通过率 + - BF16 测试通过率 + - 总体通过率 + +**返回值**: +- 0:所有测试通过 +- 1:存在不匹配的测试 + +--- + +### 5. compare_tps.py + +比较两个日志目录中的 tokens per second,用于性能对比。 + +**基本用法**: +```bash +python3 compare_tps.py +``` + +**选项**: +- `--threshold`:相对误差阈值(默认:0.20 = 20%) +- `--verbose`:显示所有文件的详细输出(包括通过的) + +**示例**: + +```bash +# 使用默认阈值比较 +python3 compare_tps.py logs compare_logs + +# 自定义阈值 +python3 compare_tps.py logs compare_logs --threshold 0.15 + +# 显示详细输出 +python3 compare_tps.py logs compare_logs --verbose +``` + +**输出说明**: + +1. **文件差异**: + - 只在 dir1 中的文件 + - 只在 dir2 中的文件 + +2. **每个测试的对比**: + - 平均 tokens/s 对比 + - 相对误差百分比 + - 比较的步骤数(排除第一步) + +3. **总体统计**: + - 通过的测试用例数 + - 总测试用例数 + - 通过率 + +**注意**: +- 比较时会自动排除第一步(因为第一步可能有初始化开销) +- 相对误差计算:`|avg1 - avg2| / max(avg1, avg2)` + +**返回值**: +- 0:所有测试通过 +- 1:存在不匹配的测试 + +--- + +## 完整使用流程 + +### 1. 准备配置 + +编辑 `test_config.json`: +- 确保 `variables` 中的路径正确 +- 配置 `builds` 中的构建命令 +- 在 `tests` 中添加或修改测试配置 + +### 2. 运行测试 + +```bash +cd ~/InfiniTrain/scripts +bash run_models_and_profile.bash +``` + +### 3. 查看日志 + +- 训练日志:`logs/` 和 `compare_logs/` +- 性能日志:`profile_logs/` 和 `compare_profile_logs/` + +### 4. 分析结果 + +**提取训练统计**: +```bash +python3 extract_training_stats.py logs compare_logs +``` + +**验证正确性**: +```bash +python3 compare_loss.py logs compare_logs --threshold-fp32 1e-5 --threshold-bf16 1e-2 +``` + +**对比性能**: +```bash +python3 compare_tps.py logs compare_logs --threshold 0.20 +``` + +**计算加速比**: +```bash +python3 extract_training_stats.py logs compare_logs --speedup +``` + +--- + +## Flash Attention 对比说明 + +脚本会自动为每个测试运行两个版本: + +- **noflash**:不使用 Flash Attention + - 日志保存在 `logs/` 目录 + - 性能报告保存在 `profile_logs/` 目录 + +- **flash**:使用 Flash Attention + - 日志保存在 `compare_logs/` 目录 + - 性能报告保存在 `compare_profile_logs/` 目录 + +这种设计允许: +1. 验证 Flash Attention 的正确性(通过 compare_loss.py) +2. 对比 Flash Attention 的性能(通过 compare_tps.py) +3. 计算加速比(通过 extract_training_stats.py --speedup) + + + +## 日志文件命名规则 + +- 训练日志:`{model}_{test_id}_{dtype}_{distopt}.log` +- 性能日志:`{model}_{test_id}_{dtype}_{distopt}_profile.log` +- 性能报告:`{model}_{test_id}_{dtype}_{distopt}_profile_{model}.report.rank{N}` + +示例: +- `gpt2_1_bfloat16.log` +- `llama3_4_distopt_profile.log` +- `gpt2_5_bfloat16_distopt_profile_gpt2.report.rank0` diff --git a/scripts/extract_training_stats.py b/scripts/extract_training_stats.py index 767bd05b..d4949cc1 100755 --- a/scripts/extract_training_stats.py +++ b/scripts/extract_training_stats.py @@ -299,140 +299,136 @@ def main(): key = f"{result['model']}_{result['dtype']}_{result['disopt']}" grouped_by_config[key].append(result) - # Prepare output - if args.markdown: - # Output as markdown table - markdown_lines = [] - - # Add header - markdown_lines.append("| Row ID | Avg Loss | Avg Time (ms) | Avg Tok/s | Peak Used (MB) | Peak Reserved (MB) | DP | TP | SP | PP |") - markdown_lines.append("|---------|----------|---------------|-----------|-----------------|-------------------|----|----|----|----|") - - # Print rows (flash and noflash interleaved) - for key in sorted(grouped_by_config.keys()): - group = grouped_by_config[key] - if len(group) >= 2: - # Separate flash and noflash results - flash_results = [r for r in group if r['flash'] == 'flash'] - noflash_results = [r for r in group if r['flash'] == 'noflash'] - - # Interleave flash and noflash results - for i in range(max(len(flash_results), len(noflash_results))): - if i < len(flash_results): - result = flash_results[i] - row = f"| {result['row_id']:<40} | {result['avg_loss']:<15.6f} | {result['avg_time_ms']:<15.2f} | {result['avg_tok_per_sec']:<15.2f} | {result['avg_peak_used']:<15.2f} | {result['avg_peak_reserved']:<15.2f} | {result['dp']:<5} | {result['tp']:<5} | {result['sp']:<5} | {result['pp']:<5} |" - markdown_lines.append(row) - if i < len(noflash_results): - result = noflash_results[i] - row = f"| {result['row_id']:<40} | {result['avg_loss']:<15.6f} | {result['avg_time_ms']:<15.2f} | {result['avg_tok_per_sec']:<15.2f} | {result['avg_peak_used']:<15.2f} | {result['avg_peak_reserved']:<15.2f} | {result['dp']:<5} | {result['tp']:<5} | {result['sp']:<5} | {result['pp']:<5} |" + # Output speedup ratio table if requested + if args.speedup: + # Only output speedup ratio table + pass + else: + # Prepare output + if args.markdown: + # Output as markdown table + markdown_lines = [] + + # Add header + markdown_lines.append("| Row ID | Avg Loss | Avg Time (ms) | Avg Tok/s | Peak Used (MB) | Peak Reserved (MB) | DP | TP | SP | PP |") + markdown_lines.append("|---------|----------|---------------|-----------|-----------------|-------------------|----|----|----|----|") + + # Print rows (flash and noflash interleaved) + for key in sorted(grouped_by_config.keys()): + group = grouped_by_config[key] + if len(group) >= 2: + # Separate flash and noflash results + flash_results = [r for r in group if r['flash'] == 'flash'] + noflash_results = [r for r in group if r['flash'] == 'noflash'] + + # Interleave flash and noflash results + for i in range(max(len(flash_results), len(noflash_results))): + if i < len(flash_results): + result = flash_results[i] + row = f"| {result['row_id']:<40} | {result['avg_loss']:<15.6f} | {result['avg_time_ms']:<15.2f} | {result['avg_tok_per_sec']:<15.2f} | {result['avg_peak_used']:<15.2f} | {result['avg_peak_reserved']:<15.2f} | {result['dp']:<5} | {result['tp']:<5} | {result['sp']:<5} | {result['pp']:<5} |" + markdown_lines.append(row) + if i < len(noflash_results): + result = noflash_results[i] + row = f"| {result['row_id']:<40} | {result['avg_loss']:<15.6f} | {result['avg_time_ms']:<15.2f} | {result['avg_tok_per_sec']:<15.2f} | {result['avg_peak_used']:<15.2f} | {result['avg_peak_reserved']:<15.2f} | {result['dp']:<5} | {result['tp']:<5} | {result['sp']:<5} | {result['pp']:<5} |" + markdown_lines.append(row) + else: + # Only one result, print it + for result in group: + row = f"| {result['row_id']:<40} | {result['avg_loss']:<15.6f} {result['avg_time_ms']:<15.2f} {result['avg_tok_per_sec']:<15.2f} {result['avg_peak_used']:<15.2f} | {result['avg_peak_reserved']:<15.2f} | {result['dp']:<5} | {result['tp']:<5} | {result['sp']:<5} | {result['pp']:<5} |" markdown_lines.append(row) + + # Print markdown table + markdown_table = "\n".join(markdown_lines) + print(markdown_table) + + # Write to file if output path is specified + if args.output: + with open(args.output, 'w') as f: + f.write(markdown_table) + print(f"Markdown table saved to: {args.output}") + else: + # Print plain text table + print("\n" + "=" * 150) + print("Training Statistics Table (excluding step 1)") + print("=" * 150) + + # Print header + header = f"{'Row ID':<40} {'Avg Loss':<15} {'Avg Time (ms)':<15} {'Avg Tok/s':<15} {'Peak Used (MB)':<15} {'Peak Reserved (MB)':<15} {'DP':<5} {'TP':<5} {'SP':<5} {'PP':<5}" + print(header) + print("-" * 150) + + # Print rows (flash and noflash interleaved) + for key in sorted(grouped_by_config.keys()): + group = grouped_by_config[key] + if len(group) >= 2: + # Separate flash and noflash results + flash_results = [r for r in group if r['flash'] == 'flash'] + noflash_results = [r for r in group if r['flash'] == 'noflash'] + + # Interleave flash and noflash results + for i in range(max(len(flash_results), len(noflash_results))): + if i < len(flash_results): + result = flash_results[i] + row = f"{result['row_id']:<40} {result['avg_loss']:<15.6f} {result['avg_time_ms']:<15.2f} {result['avg_tok_per_sec']:<15.2f} {result['avg_peak_used']:<15.2f} {result['avg_peak_reserved']:<15.2f} {result['dp']:<5} {result['tp']:<5} {result['sp']:<5} {result['pp']:<5}" + print(row) + if i < len(noflash_results): + result = noflash_results[i] + row = f"{result['row_id']:<40} {result['avg_loss']:<15.6f} {result['avg_time_ms']:<15.2f} {result['avg_tok_per_sec']:<15.2f} {result['avg_peak_used']:<15.2f} {result['avg_peak_reserved']:<15.2f} {result['dp']:<5} {result['tp']:<5} {result['sp']:<5} {result['pp']:<5}" + print(row) else: # Only one result, print it for result in group: - row = f"| {result['row_id']:<40} | {result['avg_loss']:<15.6f} {result['avg_time_ms']:<15.2f} {result['avg_tok_per_sec']:<15.2f} {result['avg_peak_used']:<15.2f} | {result['avg_peak_reserved']:<15.2f} | {result['dp']:<5} | {result['tp']:<5} | {result['sp']:<5} | {result['pp']:<5} |" - markdown_lines.append(row) - - # Print markdown table - markdown_table = "\n".join(markdown_lines) - print(markdown_table) + row = f"{result['row_id']:<40} {result['avg_loss']:<15.6f} {result['avg_time_ms']:<15.2f} {result['avg_tok_per_sec']:<15.2f} {result['avg_peak_used']:<15.2f} {result['avg_peak_reserved']:<15.2f} {result['dp']:<5} {result['tp']:<5} {result['sp']:<5} {result['pp']:<5}" + print(row) - # Write to file if output path is specified - if args.output: - with open(args.output, 'w') as f: - f.write(markdown_table) - print(f"Markdown table saved to: {args.output}") - else: - # Print plain text table - print("\n" + "=" * 150) - print("Training Statistics Table (excluding step 1)") print("=" * 150) - # Print header - header = f"{'Row ID':<40} {'Avg Loss':<15} {'Avg Time (ms)':<15} {'Avg Tok/s':<15} {'Peak Used (MB)':<15} {'Peak Reserved (MB)':<15} {'DP':<5} {'TP':<5} {'SP':<5} {'PP':<5}" - print(header) - print("-" * 150) - - # Print rows (flash and noflash interleaved) - for key in sorted(grouped_by_config.keys()): - group = grouped_by_config[key] - if len(group) >= 2: - # Separate flash and noflash results - flash_results = [r for r in group if r['flash'] == 'flash'] - noflash_results = [r for r in group if r['flash'] == 'noflash'] - - # Interleave flash and noflash results - for i in range(max(len(flash_results), len(noflash_results))): - if i < len(flash_results): - result = flash_results[i] - row = f"{result['row_id']:<40} {result['avg_loss']:<15.6f} {result['avg_time_ms']:<15.2f} {result['avg_tok_per_sec']:<15.2f} {result['avg_peak_used']:<15.2f} {result['avg_peak_reserved']:<15.2f} {result['dp']:<5} {result['tp']:<5} {result['sp']:<5} {result['pp']:<5}" - print(row) - if i < len(noflash_results): - result = noflash_results[i] - row = f"{result['row_id']:<40} {result['avg_loss']:<15.6f} {result['avg_time_ms']:<15.2f} {result['avg_tok_per_sec']:<15.2f} {result['avg_peak_used']:<15.2f} {result['avg_peak_reserved']:<15.2f} {result['dp']:<5} {result['tp']:<5} {result['sp']:<5} {result['pp']:<5}" - print(row) - else: - # Only one result, print it - for result in group: - row = f"{result['row_id']:<40} {result['avg_loss']:<15.6f} {result['avg_time_ms']:<15.2f} {result['avg_tok_per_sec']:<15.2f} {result['avg_peak_used']:<15.2f} {result['avg_peak_reserved']:<15.2f} {result['dp']:<5} {result['tp']:<5} {result['sp']:<5} {result['pp']:<5}" - print(row) - - print("=" * 150) - - # Print comparison between flash and noflash - print("\nComparison between flash and noflash:") - print("=" * 150) - - for key, group in sorted(grouped_by_config.items()): - # Separate flash and noflash results - flash_results = [r for r in group if r['flash'] == 'flash'] - noflash_results = [r for r in group if r['flash'] == 'noflash'] + # Print comparison between flash and noflash + print("\nComparison between flash and noflash:") + print("=" * 150) - if flash_results and noflash_results: - # Calculate averages for each group - avg_flash_loss = sum(r['avg_loss'] for r in flash_results) / len(flash_results) - avg_flash_time = sum(r['avg_time_ms'] for r in flash_results) / len(flash_results) - avg_flash_tok = sum(r['avg_tok_per_sec'] for r in flash_results) / len(flash_results) - avg_flash_peak_used = sum(r['avg_peak_used'] for r in flash_results) / len(flash_results) - avg_flash_peak_reserved = sum(r['avg_peak_reserved'] for r in flash_results) / len(flash_results) - - avg_noflash_loss = sum(r['avg_loss'] for r in noflash_results) / len(noflash_results) - avg_noflash_time = sum(r['avg_time_ms'] for r in noflash_results) / len(noflash_results) - avg_noflash_tok = sum(r['avg_tok_per_sec'] for r in noflash_results) / len(noflash_results) - avg_noflash_peak_used = sum(r['avg_peak_used'] for r in noflash_results) / len(noflash_results) - avg_noflash_peak_reserved = sum(r['avg_peak_reserved'] for r in noflash_results) / len(noflash_results) - - # Calculate differences - loss_diff = avg_flash_loss - avg_noflash_loss - time_diff = avg_flash_time - avg_noflash_time - tok_diff = avg_flash_tok - avg_noflash_tok - - loss_pct = (loss_diff / avg_noflash_loss * 100) if avg_noflash_loss > 0 else 0 - time_pct = (time_diff / avg_noflash_time * 100) if avg_noflash_time > 0 else 0 - tok_pct = (tok_diff / avg_noflash_tok * 100) if avg_noflash_tok > 0 else 0 - - print(f"\n{key}:") - print(f" Loss: {avg_flash_loss:.6f} vs {avg_noflash_loss:.6f} (diff: {loss_diff:+.6f}, {loss_pct:+.2f}%)") - print(f" Time: {avg_flash_time:.2f} vs {avg_noflash_time:.2f} ms (diff: {time_diff:+.2f}, {time_pct:+.2f}%)") - print(f" Tok/s: {avg_flash_tok:.2f} vs {avg_noflash_tok:.2f} (diff: {tok_diff:+.2f}, {tok_pct:+.2f}%)") - - # Calculate memory differences - mem_used_diff = avg_flash_peak_used - avg_noflash_peak_used - mem_reserved_diff = avg_flash_peak_reserved - avg_noflash_peak_reserved - - mem_used_pct = (mem_used_diff / avg_noflash_peak_used * 100) if avg_noflash_peak_used > 0 else 0 - mem_reserved_pct = (mem_reserved_diff / avg_noflash_peak_reserved * 100) if avg_noflash_peak_reserved > 0 else 0 - - print(f" Peak Used: {avg_flash_peak_used:.2f} vs {avg_noflash_peak_used:.2f} MB (diff: {mem_used_diff:+.2f}, {mem_used_pct:+.2f}%)") - print(f" Peak Reserved: {avg_flash_peak_reserved:.2f} vs {avg_noflash_peak_reserved:.2f} MB (diff: {mem_reserved_diff:+.2f}, {mem_reserved_pct:+.2f}%)") + for key, group in sorted(grouped_by_config.items()): + # Separate flash and noflash results + flash_results = [r for r in group if r['flash'] == 'flash'] + noflash_results = [r for r in group if r['flash'] == 'noflash'] - # Calculate memory differences - mem_used_diff = avg_flash_peak_used - avg_noflash_peak_used - mem_reserved_diff = avg_flash_peak_reserved - avg_noflash_peak_reserved - mem_used_pct = (mem_used_diff / avg_noflash_peak_used * 100) if avg_noflash_peak_used > 0 else 0 - mem_reserved_pct = (mem_reserved_diff / avg_noflash_peak_reserved * 100) if avg_noflash_peak_reserved > 0 else 0 - - # Output speedup ratio table if requested - if args.speedup: + if flash_results and noflash_results: + # Calculate averages for each group + avg_flash_loss = sum(r['avg_loss'] for r in flash_results) / len(flash_results) + avg_flash_time = sum(r['avg_time_ms'] for r in flash_results) / len(flash_results) + avg_flash_tok = sum(r['avg_tok_per_sec'] for r in flash_results) / len(flash_results) + avg_flash_peak_used = sum(r['avg_peak_used'] for r in flash_results) / len(flash_results) + avg_flash_peak_reserved = sum(r['avg_peak_reserved'] for r in flash_results) / len(flash_results) + + avg_noflash_loss = sum(r['avg_loss'] for r in noflash_results) / len(noflash_results) + avg_noflash_time = sum(r['avg_time_ms'] for r in noflash_results) / len(noflash_results) + avg_noflash_tok = sum(r['avg_tok_per_sec'] for r in noflash_results) / len(noflash_results) + avg_noflash_peak_used = sum(r['avg_peak_used'] for r in noflash_results) / len(noflash_results) + avg_noflash_peak_reserved = sum(r['avg_peak_reserved'] for r in noflash_results) / len(noflash_results) + + # Calculate differences + loss_diff = avg_flash_loss - avg_noflash_loss + time_diff = avg_flash_time - avg_noflash_time + tok_diff = avg_flash_tok - avg_noflash_tok + + loss_pct = (loss_diff / avg_noflash_loss * 100) if avg_noflash_loss > 0 else 0 + time_pct = (time_diff / avg_noflash_time * 100) if avg_noflash_time > 0 else 0 + tok_pct = (tok_diff / avg_noflash_tok * 100) if avg_noflash_tok > 0 else 0 + + print(f"\n{key}:") + print(f" Loss: {avg_flash_loss:.6f} vs {avg_noflash_loss:.6f} (diff: {loss_diff:+.6f}, {loss_pct:+.2f}%)") + print(f" Time: {avg_flash_time:.2f} vs {avg_noflash_time:.2f} ms (diff: {time_diff:+.2f}, {time_pct:+.2f}%)") + print(f" Tok/s: {avg_flash_tok:.2f} vs {avg_noflash_tok:.2f} (diff: {tok_diff:+.2f}, {tok_pct:+.2f}%)") + + # Calculate memory differences + mem_used_diff = avg_flash_peak_used - avg_noflash_peak_used + mem_reserved_diff = avg_flash_peak_reserved - avg_noflash_peak_reserved + + mem_used_pct = (mem_used_diff / avg_noflash_peak_used * 100) if avg_noflash_peak_used > 0 else 0 + mem_reserved_pct = (mem_reserved_diff / avg_noflash_peak_reserved * 100) if avg_noflash_peak_reserved > 0 else 0 + + print(f" Peak Used: {avg_flash_peak_used:.2f} vs {avg_noflash_peak_used:.2f} MB (diff: {mem_used_diff:+.2f}, {mem_used_pct:+.2f}%)") + print(f" Peak Reserved: {avg_flash_peak_reserved:.2f} vs {avg_noflash_peak_reserved:.2f} MB (diff: {mem_reserved_diff:+.2f}, {mem_reserved_pct:+.2f}%)") print("\n" + "=" * 120) print("Speedup Ratio Table (noflash_time / flash_time)") print("=" * 120) diff --git a/scripts/results.md b/scripts/results.md new file mode 100644 index 00000000..7227200b --- /dev/null +++ b/scripts/results.md @@ -0,0 +1,82 @@ +| Row ID | Avg Loss | Avg Time (ms) | Avg Tok/s | Peak Used (MB) | Peak Reserved (MB) | DP | TP | SP | PP | +|---------|----------|---------------|-----------|-----------------|-------------------|----|----|----|----| +| gpt2_bfloat16_flash_disopt_3 | 4.799616 | 2494.86 | 2052.67 | 3407.22 | 3452.44 | 8 | 1 | 1 | 1 | +| gpt2_bfloat16_noflash_disopt_3 | 4.787657 | 2741.77 | 1868.11 | 3405.00 | 3456.00 | 8 | 1 | 1 | 1 | +| gpt2_bfloat16_flash_disopt_4 | 4.799871 | 2900.71 | 1766.33 | 2715.00 | 2748.44 | 8 | 4 | 1 | 1 | +| gpt2_bfloat16_noflash_disopt_4 | 4.788475 | 3211.90 | 1594.33 | 2713.00 | 2784.00 | 8 | 4 | 1 | 1 | +| gpt2_bfloat16_flash_disopt_5 | 4.799961 | 3520.25 | 1454.67 | 2398.00 | 2432.00 | 8 | 4 | 1 | 1 | +| gpt2_bfloat16_noflash_disopt_5 | 4.791147 | 4036.41 | 1290.11 | 2396.00 | 2464.00 | 8 | 4 | 1 | 1 | +| gpt2_bfloat16_flash_disopt_8 | 4.799024 | 2000.08 | 2561.89 | 2373.22 | 2528.00 | 8 | 2 | 1 | 2 | +| gpt2_bfloat16_noflash_disopt_8 | 4.791285 | 2128.22 | 2406.67 | 2326.00 | 2467.56 | 8 | 2 | 1 | 2 | +| gpt2_bfloat16_flash_nodisopt_3 | 4.800298 | 2901.57 | 1766.11 | 3407.22 | 3445.33 | 8 | 1 | 1 | 1 | +| gpt2_bfloat16_noflash_nodisopt_3 | 4.787223 | 3130.48 | 1636.11 | 3405.00 | 3456.00 | 8 | 1 | 1 | 1 | +| gpt2_bfloat16_flash_nodisopt_4 | 4.799593 | 3107.03 | 1648.78 | 2715.00 | 2744.89 | 8 | 4 | 1 | 1 | +| gpt2_bfloat16_noflash_nodisopt_4 | 4.788435 | 4551.77 | 1308.67 | 2713.00 | 2752.00 | 8 | 4 | 1 | 1 | +| gpt2_bfloat16_flash_nodisopt_5 | 4.799967 | 3734.20 | 1372.00 | 2398.00 | 2428.44 | 8 | 4 | 1 | 1 | +| gpt2_bfloat16_noflash_nodisopt_5 | 4.791135 | 4016.84 | 1275.78 | 2396.00 | 2464.00 | 8 | 4 | 1 | 1 | +| gpt2_bfloat16_flash_nodisopt_6 | 4.800104 | 4197.89 | 1220.33 | 1718.89 | 1820.44 | 8 | 1 | 1 | 8 | +| gpt2_bfloat16_noflash_nodisopt_6 | 4.788296 | 4675.14 | 1108.78 | 1684.00 | 1777.78 | 8 | 1 | 1 | 8 | +| gpt2_bfloat16_flash_nodisopt_7 | 4.800298 | 5575.07 | 918.44 | 2684.00 | 2805.33 | 4 | 1 | 1 | 4 | +| gpt2_bfloat16_noflash_nodisopt_7 | 4.788042 | 6254.28 | 823.00 | 2586.00 | 2705.78 | 4 | 1 | 1 | 4 | +| gpt2_bfloat16_flash_nodisopt_8 | 4.798747 | 2089.50 | 2453.44 | 2373.22 | 2510.22 | 8 | 2 | 1 | 2 | +| gpt2_bfloat16_noflash_nodisopt_8 | 4.790910 | 2150.97 | 2381.11 | 2326.00 | 2478.22 | 8 | 2 | 1 | 2 | +| gpt2_fp32_flash_disopt_3 | 4.836303 | 2215.36 | 2313.33 | 3626.22 | 3672.89 | 8 | 1 | 1 | 1 | +| gpt2_fp32_noflash_disopt_3 | 4.826354 | 2513.86 | 2037.56 | 3467.00 | 3520.00 | 8 | 1 | 1 | 1 | +| gpt2_fp32_flash_disopt_4 | 4.836304 | 2652.36 | 1933.11 | 2934.00 | 2961.78 | 8 | 4 | 1 | 1 | +| gpt2_fp32_noflash_disopt_4 | 4.826354 | 2913.33 | 1760.11 | 2774.00 | 2816.00 | 8 | 4 | 1 | 1 | +| gpt2_fp32_flash_disopt_5 | 4.836836 | 3305.40 | 1549.78 | 2616.89 | 2666.67 | 8 | 4 | 1 | 1 | +| gpt2_fp32_noflash_disopt_5 | 4.829048 | 3928.89 | 1378.33 | 2457.00 | 2496.00 | 8 | 4 | 1 | 1 | +| gpt2_fp32_flash_disopt_8 | 4.836634 | 1838.23 | 2786.78 | 2441.67 | 2535.11 | 8 | 2 | 1 | 2 | +| gpt2_fp32_noflash_disopt_8 | 4.827999 | 2004.15 | 2557.78 | 2350.00 | 2435.56 | 8 | 2 | 1 | 2 | +| gpt2_fp32_flash_nodisopt_3 | 4.836303 | 2535.03 | 2020.78 | 3626.22 | 3662.22 | 8 | 1 | 1 | 1 | +| gpt2_fp32_noflash_nodisopt_3 | 4.826354 | 2810.67 | 1822.44 | 3467.00 | 3520.00 | 8 | 1 | 1 | 1 | +| gpt2_fp32_flash_nodisopt_4 | 4.836304 | 2817.74 | 1818.22 | 2934.00 | 2954.67 | 8 | 4 | 1 | 1 | +| gpt2_fp32_noflash_nodisopt_4 | 4.826354 | 3049.61 | 1679.67 | 2774.00 | 2816.00 | 8 | 4 | 1 | 1 | +| gpt2_fp32_flash_nodisopt_5 | 4.836836 | 3392.83 | 1509.67 | 2616.89 | 2656.00 | 8 | 4 | 1 | 1 | +| gpt2_fp32_noflash_nodisopt_5 | 4.829048 | 3656.74 | 1400.67 | 2457.00 | 2496.00 | 8 | 4 | 1 | 1 | +| gpt2_fp32_flash_nodisopt_6 | 4.836304 | 3746.21 | 1378.67 | 2318.44 | 2368.00 | 8 | 1 | 1 | 8 | +| gpt2_fp32_noflash_nodisopt_6 | 4.826354 | 3947.83 | 1297.11 | 2248.00 | 2307.56 | 8 | 1 | 1 | 8 | +| gpt2_fp32_flash_nodisopt_7 | 4.836304 | 5285.93 | 980.22 | 3538.56 | 3587.56 | 4 | 1 | 1 | 4 | +| gpt2_fp32_noflash_nodisopt_7 | 4.826354 | 5558.65 | 921.22 | 3345.00 | 3416.89 | 4 | 1 | 1 | 4 | +| gpt2_fp32_flash_nodisopt_8 | 4.836634 | 3332.54 | 1536.78 | 2441.67 | 2531.56 | 8 | 2 | 1 | 2 | +| gpt2_fp32_noflash_nodisopt_8 | 4.827999 | 3560.96 | 1438.22 | 2350.00 | 2424.89 | 8 | 2 | 1 | 2 | +| llama3_bfloat16_flash_disopt_3 | 11.513694 | 6985.97 | 733.22 | 21899.89 | 22151.11 | 8 | 1 | 1 | 1 | +| llama3_bfloat16_noflash_disopt_3 | 3.687185 | 8803.02 | 587.44 | 21632.00 | 21824.00 | 8 | 1 | 1 | 1 | +| llama3_bfloat16_flash_disopt_4 | 12.108337 | 7048.58 | 726.67 | 13019.00 | 13258.67 | 8 | 4 | 1 | 1 | +| llama3_bfloat16_noflash_disopt_4 | 3.687813 | 8703.07 | 591.22 | 12995.00 | 13120.00 | 8 | 4 | 1 | 1 | +| llama3_bfloat16_flash_disopt_5 | 12.330883 | 7956.26 | 643.60 | 10984.60 | 11155.20 | 8 | 4 | 1 | 1 | +| llama3_bfloat16_noflash_disopt_5 | 3.687082 | 9673.23 | 530.89 | 10880.00 | 11008.00 | 8 | 4 | 1 | 1 | +| llama3_bfloat16_flash_disopt_8 | 12.023618 | 4801.31 | 1066.50 | 9924.00 | 10032.00 | 8 | 2 | 1 | 2 | +| llama3_bfloat16_noflash_disopt_8 | 3.687491 | 5915.92 | 873.44 | 10031.00 | 10122.67 | 8 | 2 | 1 | 2 | +| llama3_bfloat16_flash_nodisopt_3 | 11.473671 | 7028.92 | 731.67 | 31903.11 | 32156.44 | 8 | 1 | 1 | 1 | +| llama3_bfloat16_noflash_nodisopt_3 | 3.687393 | 10063.60 | 529.22 | 31636.00 | 31808.00 | 8 | 1 | 1 | 1 | +| llama3_bfloat16_flash_nodisopt_4 | 12.120937 | 7424.95 | 696.17 | 14570.17 | 14810.67 | 8 | 4 | 1 | 1 | +| llama3_bfloat16_noflash_nodisopt_4 | 3.687764 | 9143.74 | 563.22 | 14425.00 | 14560.00 | 8 | 4 | 1 | 1 | +| llama3_bfloat16_flash_nodisopt_5 | 11.832385 | 8049.24 | 636.33 | 12454.50 | 12624.00 | 8 | 4 | 1 | 1 | +| llama3_bfloat16_noflash_nodisopt_5 | 3.687394 | 9607.02 | 533.00 | 12309.00 | 12448.00 | 8 | 4 | 1 | 1 | +| llama3_bfloat16_flash_nodisopt_6 | 11.109162 | 9080.24 | 570.11 | 11371.89 | 11516.44 | 8 | 1 | 1 | 8 | +| llama3_bfloat16_noflash_nodisopt_6 | 3.687855 | 9991.38 | 515.89 | 11052.00 | 11175.11 | 8 | 1 | 1 | 8 | +| llama3_bfloat16_flash_nodisopt_7 | 12.343384 | 12997.81 | 394.00 | 13956.00 | 14080.00 | 4 | 1 | 1 | 4 | +| llama3_bfloat16_noflash_nodisopt_7 | 3.687177 | 16637.25 | 309.11 | 14608.00 | 14759.11 | 4 | 1 | 1 | 4 | +| llama3_bfloat16_flash_nodisopt_8 | 11.242361 | 5281.60 | 975.78 | 11777.67 | 11868.44 | 8 | 2 | 1 | 2 | +| llama3_bfloat16_noflash_nodisopt_8 | 3.687432 | 5902.93 | 868.11 | 11460.00 | 11559.11 | 8 | 2 | 1 | 2 | +| llama3_fp32_flash_disopt_3 | 3.696688 | 6454.74 | 797.11 | 22056.33 | 22275.56 | 8 | 1 | 1 | 1 | +| llama3_fp32_noflash_disopt_3 | 3.595062 | 8042.78 | 640.56 | 21789.00 | 21952.00 | 8 | 1 | 1 | 1 | +| llama3_fp32_flash_disopt_4 | 3.696686 | 8594.09 | 650.11 | 13419.33 | 13575.11 | 8 | 4 | 1 | 1 | +| llama3_fp32_noflash_disopt_4 | 3.595074 | 8257.09 | 620.44 | 13152.00 | 13216.00 | 8 | 4 | 1 | 1 | +| llama3_fp32_flash_disopt_5 | 3.696680 | 7835.74 | 656.33 | 11303.67 | 11459.56 | 8 | 4 | 1 | 1 | +| llama3_fp32_noflash_disopt_5 | 3.595054 | 10612.78 | 497.89 | 11036.00 | 11168.00 | 8 | 4 | 1 | 1 | +| llama3_fp32_flash_disopt_8 | 3.696683 | 4918.54 | 1041.22 | 11005.33 | 11114.67 | 8 | 2 | 1 | 2 | +| llama3_fp32_noflash_disopt_8 | 3.595056 | 5989.85 | 863.00 | 10798.00 | 10915.56 | 8 | 2 | 1 | 2 | +| llama3_fp32_flash_nodisopt_3 | 3.696689 | 8095.32 | 641.44 | 32059.89 | 32280.89 | 8 | 1 | 1 | 1 | +| llama3_fp32_noflash_nodisopt_3 | 3.595062 | 8868.17 | 580.44 | 31792.00 | 31936.00 | 8 | 1 | 1 | 1 | +| llama3_fp32_flash_nodisopt_4 | 3.696686 | 7001.37 | 736.89 | 14848.67 | 15000.89 | 8 | 4 | 1 | 1 | +| llama3_fp32_noflash_nodisopt_4 | 3.595074 | 8305.80 | 616.78 | 14581.00 | 14656.00 | 8 | 4 | 1 | 1 | +| llama3_fp32_flash_nodisopt_5 | 3.696680 | 7993.27 | 643.22 | 12732.89 | 12892.44 | 8 | 4 | 1 | 1 | +| llama3_fp32_noflash_nodisopt_5 | 3.595054 | 9290.61 | 551.33 | 12465.00 | 12608.00 | 8 | 4 | 1 | 1 | +| llama3_fp32_flash_nodisopt_6 | 3.696689 | 7677.79 | 671.67 | 13351.89 | 13475.56 | 8 | 1 | 1 | 8 | +| llama3_fp32_noflash_nodisopt_6 | 3.595062 | 8936.41 | 573.00 | 13119.00 | 13219.56 | 8 | 1 | 1 | 8 | +| llama3_fp32_flash_nodisopt_7 | 3.696688 | 12469.65 | 411.11 | 18195.78 | 18318.22 | 4 | 1 | 1 | 4 | +| llama3_fp32_noflash_nodisopt_7 | 3.595062 | 15277.34 | 335.89 | 17756.00 | 17873.78 | 4 | 1 | 1 | 4 | +| llama3_fp32_flash_nodisopt_8 | 3.696683 | 5354.28 | 974.67 | 12434.33 | 12551.11 | 8 | 2 | 1 | 2 | +| llama3_fp32_noflash_nodisopt_8 | 3.595056 | 7652.40 | 708.56 | 12227.00 | 12352.00 | 8 | 2 | 1 | 2 | \ No newline at end of file diff --git a/scripts/results_speedup.md b/scripts/results_speedup.md new file mode 100644 index 00000000..6660ec94 --- /dev/null +++ b/scripts/results_speedup.md @@ -0,0 +1,42 @@ +| Configuration | Speedup Ratio | Flash Time (ms) | Noflash Time (ms) | +|---------------|---------------|-----------------|-------------------| +| gpt2_bfloat16_disopt_3 | 1.10 | 2494.86 | 2741.77 | +| gpt2_bfloat16_disopt_4 | 1.11 | 2900.71 | 3211.90 | +| gpt2_bfloat16_disopt_5 | 1.15 | 3520.25 | 4036.41 | +| gpt2_bfloat16_disopt_8 | 1.06 | 2000.08 | 2128.22 | +| gpt2_bfloat16_nodisopt_3 | 1.08 | 2901.57 | 3130.48 | +| gpt2_bfloat16_nodisopt_4 | 1.46 | 3107.03 | 4551.77 | +| gpt2_bfloat16_nodisopt_5 | 1.08 | 3734.20 | 4016.84 | +| gpt2_bfloat16_nodisopt_6 | 1.11 | 4197.89 | 4675.14 | +| gpt2_bfloat16_nodisopt_7 | 1.12 | 5575.07 | 6254.28 | +| gpt2_bfloat16_nodisopt_8 | 1.03 | 2089.50 | 2150.97 | +| gpt2_fp32_disopt_3 | 1.13 | 2215.36 | 2513.86 | +| gpt2_fp32_disopt_4 | 1.10 | 2652.36 | 2913.33 | +| gpt2_fp32_disopt_5 | 1.19 | 3305.40 | 3928.89 | +| gpt2_fp32_disopt_8 | 1.09 | 1838.23 | 2004.15 | +| gpt2_fp32_nodisopt_3 | 1.11 | 2535.03 | 2810.67 | +| gpt2_fp32_nodisopt_4 | 1.08 | 2817.74 | 3049.61 | +| gpt2_fp32_nodisopt_5 | 1.08 | 3392.83 | 3656.74 | +| gpt2_fp32_nodisopt_6 | 1.05 | 3746.21 | 3947.83 | +| gpt2_fp32_nodisopt_7 | 1.05 | 5285.93 | 5558.65 | +| gpt2_fp32_nodisopt_8 | 1.07 | 3332.54 | 3560.96 | +| llama3_bfloat16_disopt_3 | 1.26 | 6985.97 | 8803.02 | +| llama3_bfloat16_disopt_4 | 1.23 | 7048.58 | 8703.07 | +| llama3_bfloat16_disopt_5 | 1.22 | 7956.26 | 9673.23 | +| llama3_bfloat16_disopt_8 | 1.23 | 4801.31 | 5915.92 | +| llama3_bfloat16_nodisopt_3 | 1.43 | 7028.92 | 10063.60 | +| llama3_bfloat16_nodisopt_4 | 1.23 | 7424.95 | 9143.74 | +| llama3_bfloat16_nodisopt_5 | 1.19 | 8049.24 | 9607.02 | +| llama3_bfloat16_nodisopt_6 | 1.10 | 9080.24 | 9991.38 | +| llama3_bfloat16_nodisopt_7 | 1.28 | 12997.81 | 16637.25 | +| llama3_bfloat16_nodisopt_8 | 1.12 | 5281.60 | 5902.93 | +| llama3_fp32_disopt_3 | 1.25 | 6454.74 | 8042.78 | +| llama3_fp32_disopt_4 | 0.96 | 8594.09 | 8257.09 | +| llama3_fp32_disopt_5 | 1.35 | 7835.74 | 10612.78 | +| llama3_fp32_disopt_8 | 1.22 | 4918.54 | 5989.85 | +| llama3_fp32_nodisopt_3 | 1.10 | 8095.32 | 8868.17 | +| llama3_fp32_nodisopt_4 | 1.19 | 7001.37 | 8305.80 | +| llama3_fp32_nodisopt_5 | 1.16 | 7993.27 | 9290.61 | +| llama3_fp32_nodisopt_6 | 1.16 | 7677.79 | 8936.41 | +| llama3_fp32_nodisopt_7 | 1.23 | 12469.65 | 15277.34 | +| llama3_fp32_nodisopt_8 | 1.43 | 5354.28 | 7652.40 | \ No newline at end of file From 465926dfb28a0ad12ad9f9b40f853e8ece105d45 Mon Sep 17 00:00:00 2001 From: Aoshine999 <820459542@qq.com> Date: Mon, 16 Mar 2026 15:48:32 +0800 Subject: [PATCH 13/18] delete some test file --- funcall.png | Bin 464160 -> 0 bytes run_help.md | 421 ------------------------------------- scripts/results.md | 82 -------- scripts/results_speedup.md | 42 ---- scripts/test_config.json | 5 + 5 files changed, 5 insertions(+), 545 deletions(-) delete mode 100644 funcall.png delete mode 100644 run_help.md delete mode 100644 scripts/results.md delete mode 100644 scripts/results_speedup.md diff --git a/funcall.png b/funcall.png deleted file mode 100644 index 4eb4c1451d8c028640dfb5f42f3e1827ec03a705..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 464160 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