From 7533e15fc74cdb945171f315b305149e08a048a8 Mon Sep 17 00:00:00 2001 From: Youmu Liu <105042599+Nitload-NSI@users.noreply.github.com> Date: Tue, 3 Mar 2026 18:15:21 +0800 Subject: [PATCH] Working on L4, now on swiss. FMA_TILE is re arranged. --- .../HighPerformance/Memory/L4_design.md | 346 ++++++++++++++++++ 1 file changed, 346 insertions(+) create mode 100644 StgSharp.Common/HighPerformance/Memory/L4_design.md diff --git a/StgSharp.Common/HighPerformance/Memory/L4_design.md b/StgSharp.Common/HighPerformance/Memory/L4_design.md new file mode 100644 index 00000000..5ff4ca1f --- /dev/null +++ b/StgSharp.Common/HighPerformance/Memory/L4_design.md @@ -0,0 +1,346 @@ +# L4 软件缓存器设计方案 + +> 项目:StgSharp +> 作者:Nitload +> 版本:草案 v0.1 +> 日期:2026-03-03 + +--- + +## 1. 目的 + +### 1.1 问题背景 + +StgSharp 的矩阵计算系统正从"全局单一上下文表"迁移为"逐线程上下文"架构, +以适配混合架构 CPU(如 Intel P-Core / E-Core)的调度特性。 +迁移后每个线程独立持有计算上下文,但矩阵数据仍可能由 HLSF 分配器 +或全局 `malloc` 分配在远端 NUMA 节点上,导致跨节点访问延迟显著增大。 + +### 1.2 解决方案 + +L4 缓存器是一个纯软件实现的**线程本地数据缓存层**, +位于 CPU 硬件缓存(L1/L2/L3)与主存之间的逻辑位置。 +其核心思路:将远端 NUMA 内存中的数据**按需搬运**到线程本地的连续内存区域中, +消除跨 NUMA 节点的访问延迟。 + +### 1.3 设计目标 + +- 为逐线程矩阵计算上下文提供 NUMA 友好的本地数据访问 +- 固定容量(5-10 MB),不做运行时扩容,确定性延迟 +- 透明的指针重定向:上层代码通过原始指针查找,L4 返回本地缓存指针 +- 基于热度的缓存逐出策略,保留高频访问数据 + +--- + +## 2. 整体架构 + +``` +线程 N 的数据访问路径: + + 矩阵计算代码 + │ + │ 原始指针 (HLSF/malloc 分配,可能在远端 NUMA) + ▼ + ┌─────────────────────────────────────────────┐ + │ L4 软件缓存器 │ + │ │ + │ ┌──────────┐ 查找 ┌────────────────┐ │ + │ │Swiss Table├────────→│ 缓存命中 │ │ + │ │(映射表) │ │ 返回本地指针 │ │ + │ └──────────┘ └────────────────┘ │ + │ │ │ + │ │ 未命中 │ + │ ▼ │ + │ ┌──────────┐ 分配 ┌────────────────┐ │ + │ │Slab 分配器├───────→│ 本地缓存行 │ │ + │ │(缓存行池) │ │ (1024B 对齐) │ │ + │ └──────────┘ └────────────────┘ │ + │ │ │ │ + │ │ 空间不足 │ 搬运数据 │ + │ ▼ ▼ │ + │ ┌──────────┐ ┌────────────────┐ │ + │ │逐出环 │ │ memcpy 远端→本地│ │ + │ │(4 级热度) │ └────────────────┘ │ + │ └──────────┘ │ + │ │ │ + │ │ 逐出冷数据(不写回) │ + │ ▼ │ + │ 释放缓存行,注销映射 │ + └─────────────────────────────────────────────┘ + │ + ▼ + 返回本地缓存指针(同一 NUMA 节点,低延迟访问) +``` + +### 2.1 关键参数 + +| 参数 | 值 | 说明 | +|-------------------|-----------------------|-----------------------------------| +| 缓存区总容量 | 5-10 MB | 线程本地连续内存 | +| 缓存行大小 | 1024 Bytes | = 8 个 `double MAT_KERNEL` 的大小 | +| 映射表类型 | Swiss Table | 1024 桶 × 256 槽,SIMD 加速 | +| 缓存行存储池 | Slab Allocator | 非并发、固定容量、不自动扩容 | +| 逐出策略 | 4 级热度环形链表 | 基于访问频率的近似 LFU | +| 写回策略 | 无(只读缓存) | 逐出时直接丢弃,不写回远端 | + +--- + +## 3. 重要组件 + +### 3.1 Swiss Table(指针映射表) + +**职责**:维护 `原始指针 → 缓存指针` 的映射关系。 + +- 键(Key):原始指针 `nint`,指向 HLSF/malloc 分配的远端数据 +- 值(Value):缓存指针 `nint`,指向本地 Slab 分配的缓存行 +- 哈希:使用硬件 `CRC32C` 指令对 64 位指针计算哈希 + - 低 10 位 → 桶索引(1024 个桶) + - 第 8-14 位 → H2 控制字节(用于 SIMD 预筛选) +- 扫描:SSE2 `pmovmskb` 指令并行比较 16 个控制字节, + 未来可升级为 AVX-512(每次 64 字节,扫描次数从 16 降至 4) +- 墓碑机制:删除时 ctrl 设为 `0xFE`,保护探测链完整性 +- 容量:1024 × 256 = 262,144 条映射,远大于缓存行总数(~5000-10000 行), + 保证极低的哈希碰撞率 + +### 3.2 Slab Allocator(缓存行存储池) + +**职责**:管理固定大小(1024B)缓存行的分配与回收。 + +- 模式:**非并发、固定容量**(`Nonconcurrent Fixed` 模式), + 区别于现有 `SlabAllocator` 的 Chunked 和 Sequential 模式 +- 不自动扩容:容量在初始化时确定(总缓存区大小 / 1024), + 分配失败时触发逐出流程而非扩容 +- 分配复杂度:O(1),从空闲栈弹出 +- 回收复杂度:O(1),压回空闲栈 +- 对齐:所有缓存行 1024B 对齐,确保不跨硬件缓存行边界 + +### 3.3 逐出环(4 级热度环形链表) + +**职责**:追踪缓存行的访问热度,决定逐出优先级。 + +结构: + +``` +逐出环 — 4 个热度级别的环形数组 + + ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ + │ H=0 │───→│ H=1 │───→│ H=2 │───→│ H=3 │──┐ + │(冷) │ │ │ │ │ │(热) │ │ + └──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘ │ + │ │ │ │ │ + │ 环形首尾相连 │ + └──────────────────────────────────────┘ + +每个热度级别是一个单链表桶: + + H=0: [行A] → [行B] → [行C] → null + 桶顶 桶底 + + H=1: [行D] → [行E] → null + H=2: [行F] → null + H=3: [行G] → [行H] → null +``` + +### 3.4 缓存行元数据 + +每个被缓存的数据行需要维护以下元数据(存储在 Slab 分配的额外结构中, +不占用缓存行本身的 1024 字节): + +- 原始指针(用于 Swiss Table 反向注销) +- 缓存指针(本地地址) +- 所在热度级别 +- 单链表的 next 指针 + +--- + +## 4. 操作流程 + +### 4.1 缓存查找(命中) + +1. 计算原始指针的 CRC32C 哈希 +2. Swiss Table 查找:SIMD 扫描 ctrl → 比较 key → 返回缓存指针 +3. 热度提升:将该缓存行从当前桶移至下一级桶的桶底 +4. 同时将下一级桶的桶顶移至当前桶的桶底(热度守恒) +5. 返回缓存指针,上层代码直接访问本地数据 + +### 4.2 缓存未命中 + +1. Swiss Table 查找未命中 +2. 从 Slab 分配器申请一个 1024B 缓存行 +3. 若 Slab 无空闲 → 触发逐出(见 4.3) +4. `memcpy` 远端数据到本地缓存行 +5. 在 Swiss Table 中插入 `原始指针 → 缓存指针` 映射 +6. 将缓存行加入逐出环的 H=0 桶底 +7. 返回缓存指针 + +### 4.3 缓存逐出 + +1. 取 H=0 桶的桶顶 +2. 若 H=0 非空 → 逐出该缓存行��� + - 从 Swiss Table 中删除映射(ctrl 设为 `0xFE`) + - 将缓存行归还 Slab 分配器 + - **不写回远端**(只读缓存语义) +3. 若 H=0 为空 → 旋转环形链表: + - 环形数组整体前移,所有缓存行热度降低一级 + - 重复步骤 1,直到 H=0 非空 +4. 最坏情况:旋转 3 次(4 级热度),保证一定能逐出 + +### 4.4 热度更新规则 + +``` +访问缓存行 X(当前在 H=k 桶中): + + 若 k < 3: + 将 X 从 H=k 桶移至 H=k+1 桶底 (X 热度提升) + 将 H=k+1 桶顶移至 H=k 桶底 (某行热度下降,守恒) + + 若 k == 3: + X 已在最高热度,不移动 +``` + +这个设计保证了: +- 每次访问只涉及两个链表的头尾操作,O(1) +- 热度级别总数恒定,不会无限膨胀 +- 高频访问的缓存行自然沉淀在 H=3,不易被逐出 + +--- + +## 5. 独特性与优势 + +### 5.1 NUMA 感知的线程本地缓存 + +与 CPU 硬件缓存不同,L4 缓存器在**软件层面**感知 NUMA 拓扑。 +硬件 L3 缓存对 NUMA 的处理依赖处理器微架构, +而 L4 通过显式的 `memcpy` 搬运保证数据一定位于当前线程所在 NUMA 节点, +消除了不可预测的远端访问延迟。 + +### 5.2 缓存行大小与矩阵 Kernel 对齐 + +1024B 的缓存行大小不是任意选取的: +- 1 个 `double MAT_KERNEL`(4×4 double 矩阵)= 128 Bytes +- 1 个缓存行 = 8 个 `MAT_KERNEL` = 恰好是一个 Panel 计算单元的数据量 +- 矩阵乘法的微内核(`MAT_PANEL_sse` / `MAT_PANEL_avx`) + 每次处理的数据量与缓存行完全对齐 +- 避免了一次矩阵运算需要跨多个缓存行的情况 + +### 5.3 只读语义简化设计 + +不实现写回策略是经过考量的设计决策: +- 矩阵计算中,输入矩阵在计算过程中不被修改 +- 缓存的数据是远端数据的**只读副本** +- 逐出时直接丢弃,无需脏位追踪、写回队列、一致性协议 +- 如果远端数据被修改,L4 的行为是"下次未命中时重新搬运最新数据", + 这对矩阵计算场景是可接受的 + +### 5.4 固定容量的确定性 + +- Slab 分配器不扩容 → 内存占用上界已知 +- 逐出环保证 O(1) 逐出 → 延迟上界已知 +- Swiss Table 固定 262,144 槽位 → 查找延迟上界已知 +- 整个 L4 的最坏延迟 = 查找 + 逐出 + 分配 + memcpy,全部可静态计算 + +### 5.5 与 StgSharp 现有基础设施的复用 + +| L4 组件 | 复用的 StgSharp 基础设施 | +|----------------|----------------------------------------| +| 映射表 SIMD | `SIMDID` 架构检测体系 | +| 缓存行分配 | `SlabAllocator` 的新模式 | +| 哈希计算 | 与 native 层 `city_hash_simplify_sse` 同族 | +| 内存分配 | `NativeMemory` 对齐分配 | +| 未来多平台支持 | `IBucketScanner` + NRGA 编译管道 | + +### 5.6 开创性分析 + +经过对现有主流数值计算库的调研,L4 缓存器的设计在已知公开实现中 +**没有直接的先例或等价物**。 + +#### 现有库的 NUMA 策略对比 + +| 库 / 框架 | NUMA 策略 | 软件缓存层 | 映射表 | 逐出策略 | +|-------------------|---------------------------------------------|-----------|-----------|-----------| +| Intel MKL | 线程绑核 + `numactl` 外部控制 | 无 | 无 | 无 | +| OpenBLAS | 线程亲和性 + 分块对齐硬件缓存 | 无 | 无 | 无 | +| BLIS | 多级分块(MC×KC×NC)对齐 L1/L2/L3 | 无 | 无 | 无 | +| Eigen | 无显式 NUMA 处理 | 无 | 无 | 无 | +| cuBLAS / rocBLAS | GPU 显存即为本地缓存(硬件解决) | 无 | 无 | 无 | +| **StgSharp L4** | **软件管理的线程本地缓存** | **有** | **Swiss Table** | **4 级热度环** | + +#### 差异的本质 + +现有库处理 NUMA 的标准做法是两种: + +1. **分配时绑定**:通过 `numactl --membind` 或 `mbind()` 系统调用, + 在分配时将内存锁定到特定 NUMA 节点。 + 缺陷:需要调用者(而非库本身)感知 NUMA 拓扑, + 且对已分配的远端数据无能为力。 + +2. **分块对齐硬件缓存**:通过精心选择分块参数(MC、KC、NC), + 使矩阵子块恰好填满 L1/L2/L3 硬件缓存。 + 缺陷:依赖硬件缓存对 NUMA 的透明处理, + 在跨节点访问时硬件缓存的延迟惩罚不可控。 + +L4 缓存器的独特之处在于**不信任硬件缓存对 NUMA 的处理**, +而是在软件层面显式地将数据搬运到线程本地内存。 +这在 CPU 端数值计算库中是一种全新的方法。 + +#### 为什么没有人做过 + +1. **传统 BLAS 库假设内存已经就位**: + BLAS 接口定义为 `dgemm(A, B, C)`, + 调用者负责保证 A/B/C 的内存位置合理。 + 库本身只负责计算,不负责数据搬运。 + +2. **硬件缓存在多数场景下够用**: + 对于单路或双路服务器,L3 缓存的 NUMA 惩罚通常在 1.5-2x, + 尚可接受。只有在多路(4P/8P)或混合架构 CPU 上, + NUMA 惩罚才会恶化到���须在软件层面解决。 + +3. **复杂度高**: + 实现一个完整的软件缓存层需要协调哈希表、内存分配器、 + 逐出策略三个子系统。大多数数值计算库选择将这个问题 + 留给操作系统或用户空间的 NUMA 工具。 + +4. **GPU 时代转移了注意力**: + 过去十年,高性能计算的焦点转向 GPU。 + GPU 的显存本身就是"本地缓存",NUMA 问题被硬件解决了。 + CPU 端的 NUMA 优化因此缺乏投入。 + +#### StgSharp L4 的定位 + +L4 缓存器填补了一个特定的空白: +**在 CPU 端、线程级别、由库自身管理的 NUMA 数据缓存**。 +它不依赖调用者的 NUMA 感知,不依赖操作系统的页面迁移, +也不依赖硬件缓存的透明处理——而是将 NUMA 数据局部性 +作为库内部的一等公民来管理。 + +这种设计尤其适配 StgSharp 所面对的场景: +混合架构 CPU(P-Core / E-Core 分布在不同 NUMA 域) ++ 逐线程独立计算上下文 ++ 数据由外部分配器(HLSF / malloc)分配在不可预测的位置。 +在这种场景下,"分配时绑定"策略失效(因为数据已经被分配了), +"分块对齐硬件缓存"策略不够(因为硬件缓存不感知线程迁移), +L4 软件缓存层成为唯一可行的解决方案。 + +--- + +## 6. 未来工作 + +- [ ] L4 根类型实现(组合 Swiss Table、Slab、逐出环) +- [ ] Slab Allocator `Nonconcurrent Fixed` 模式实现 +- [ ] Swiss Table ARM NEON 实现 +- [ ] Swiss Table AVX-512 升级(256 槽扫描从 16 次降至 4 次) +- [ ] 墓碑累积监控与自动 rehash 策略 +- [ ] 性能基准测试(NUMA 本地 vs 远端访问延迟对比) +- [ ] 与逐线程矩阵上下文的集成验证 +- [ ] 远期:写回策略评估(当计算模式扩展到原地修改时) +- [ ] 远期:PPC64LE 平台的 VSX 原生 Swiss Table 扫描支持 + +--- + +## 7. 参考 + +- Google Abseil Swiss Tables Design Notes +- StgSharp HLSF Allocator 设计文档 +- StgSharp SIMD Architecture (`SIMDID` 体系) +- StgSharp NRGA Roadmap +- StgSharp `SlabAllocator` 实现 \ No newline at end of file