创建仓库
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 2.2.3 NUMA 感知的内存亲和性控制 (NUMA-Aware Memory Affinity Control)
|
||||
date created: 星期四, 十一月 20日 2025, 10:14:01 晚上
|
||||
date modified: 星期四, 十一月 20日 2025, 10:14:41 晚上
|
||||
---
|
||||
|
||||
# 2.2.3 NUMA 感知的内存亲和性控制 (NUMA-Aware Memory Affinity Control)
|
||||
|
||||
### 一、 约束输入与对齐 (Constraints & Alignment)
|
||||
|
||||
基于第一章的审计报告,我们面临以下**硬性物理约束**:
|
||||
|
||||
1. **CPU 拓扑**:
|
||||
- **Node 0**: CPU 0-15
|
||||
- **Node 1**: CPU 16-31
|
||||
2. **GPU 位置**:Iluvatar MR-V100 物理挂载在 **Node 1** 上。
|
||||
3. **OS 策略**:`numa_balancing` 已被禁用。这意味着我们不能指望操作系统自动把内存迁移到正确的节点,**必须**手动管理。
|
||||
4. **性能陷阱**:如果 Host 内存分配在 Node 0,而 DMA 引擎在 GPU (Node 1) 上,DMA 读取将必须穿过片间互联总线 (Inter-Chip Interconnect),这通常只有本地内存带宽的一半甚至更低。
|
||||
|
||||
-----
|
||||
|
||||
### 二、 权衡分析与选项呈现 (Trade-off Matrix)
|
||||
|
||||
#### 议题:如何强制内存与计算位于 Node 1?
|
||||
|
||||
| 选项 | A. 仅依赖 `numactl` (进程级绑定) | B. 代码级硬亲和性 (线程级绑定) | C. `mbind` / `set_mempolicy` (API 级内存绑定) |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **机制** | 在启动命令前加 `numactl --cpunodebind=1 --membind=1`。 | 在 C++ 代码中调用 `pthread_setaffinity_np` 将关键线程钉死在 Core 16-31。 | 在调用 `malloc` / `cudaMallocHost` 前设置内存分配策略。 |
|
||||
| **可靠性** | **高**。这是最稳健的保底方案,确保进程内所有内存页都在 Node 1。 | **极高**。可以精细控制哪个线程跑在哪个核(如 I/O 线程绑 Core 16, Worker 绑 Core 17-20)。 | **中**。`cudaMallocHost` 的行为可能受驱动实现影响,不如 `numactl` 强制有效。 |
|
||||
| **灵活性** | 低。整个进程被限制在半个 CPU 上。 | 高。允许非关键线程(如日志、监控)漂移到 Node 0。 | 高。允许精细控制每块内存的位置。 |
|
||||
| **实施成本** | 零代码修改。运维配置即可。 | 需要修改 `ExecutionEngine` 代码。 | 需要修改内存池代码。 |
|
||||
|
||||
-----
|
||||
|
||||
### 三、 基线确立与实施规范
|
||||
|
||||
为了达成 **P0 级的性能稳定性**,我们采取 **“运维强制 + 代码辅助”** 的双重保险策略。
|
||||
|
||||
#### 1\. 运维基线:全进程约束 (Process-Level)
|
||||
|
||||
- **决策**:所有雷达信号处理进程 **必须** 通过 `numactl` 启动。
|
||||
- **命令规范**:
|
||||
|
||||
```bash
|
||||
# 强制 CPU 和 内存 都在 Node 1
|
||||
numactl --cpunodebind=1 --membind=1 ./main_app
|
||||
```
|
||||
|
||||
- **论证**:这是最底层的安全网。即使代码写错了,OS 也不会把内存分配到 Node 0 去,只会报 OOM (Out of Memory),这比“默默变慢”更容易排查。
|
||||
|
||||
#### 2\. 代码基线:线程亲和性 (Thread-Level)
|
||||
|
||||
- **决策**:在 `ExecutionEngine` 中启动 I/O 线程和 Worker 线程时,**显式设置 CPU 亲和性**。
|
||||
- **资源规划 (示例)**:
|
||||
- **Core 16 (Node 1)**: `DataReceiver` 的 **I/O 线程** (独占,处理中断聚合后的高速包)。
|
||||
- **Core 17-24 (Node 1)**: `SignalProcessor` 的 **计算/Worker 线程** (负责 CUDA API 调用和数据封包)。
|
||||
- **Core 0-15 (Node 0)**: *非关键路径*(日志落盘、监控数据聚合、显控交互)。需要注意,虽然 `numactl` 限制了 `--cpunodebind=1`,但我们可以通过 `numactl --preferred=1` 或者在代码中用 `sched_setaffinity` 突破限制,将非实时任务扔回 Node 0(如果确实需要利用那 16 个核)。**但在 V1.0 阶段,建议简单化,全部限制在 Node 1。**
|
||||
- **C++ 实现规范**:
|
||||
|
||||
```cpp
|
||||
void set_thread_affinity(int core_id) {
|
||||
cpu_set_t cpuset;
|
||||
CPU_ZERO(&cpuset);
|
||||
CPU_SET(core_id, &cpuset);
|
||||
// 必须检查返回值,确保绑定成功
|
||||
if (pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpuset) != 0) {
|
||||
// 记录致命错误,因为实时性无法保证
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3\. 内存分配时机:First-Touch 原则
|
||||
|
||||
- **决策**:鉴于我们使用了 `cudaMallocHost`,CUDA 驱动通常会在**调用分配函数的那个线程**所在的 NUMA 节点上分配物理内存(或者遵循进程的 `membind` 策略)。
|
||||
- **规范**:**必须在 `initialize()` 阶段,且在已经绑定了 CPU 亲和性的线程中** 执行 `cudaMallocHost`。
|
||||
- *错误做法*:在主线程(可能还没绑定核)分配内存池,然后传递给工作线程。
|
||||
- *正确做法*:主线程先将自己绑定到 Node 1,或者通过 `numactl` 启动,然后再初始化 `MemoryPool`。
|
||||
|
||||
-----
|
||||
|
||||
### 总结与下一步行动
|
||||
|
||||
我们已经确立了:
|
||||
|
||||
1. **怎么分**:`cudaMallocHost` + Pinned + Mapped (2.2.1)
|
||||
2. **怎么传**:双流乒乓 + 重叠 (2.2.2)
|
||||
3. **在哪传**:**NUMA Node 1** (通过 `numactl` + 线程绑定) (2.2.3)
|
||||
|
||||
现在,物理层和传输层的地基已经打牢。下一步,我们需要讨论 **2.2.4 统一虚拟寻址与零拷贝技术 (UVA & Zero-Copy)**。这将决定我们在某些特定场景下(如传输波控码或小批量参数),是否可以**完全省去** `cudaMemcpy`,直接让 GPU " 伸手 " 到 Host 内存里拿数据。
|
||||
|
||||
**提问**:您是否确认 **“numactl 强制绑定 Node 1 + 关键线程显式钉核”** 的基线?确认后我们进入 2.2.4。
|
||||
Reference in New Issue
Block a user