Files
Inbox/系统基座文件/2/2.2/2.2.3 NUMA 感知的内存亲和性控制 (NUMA-Aware Memory Affinity Control).md
2025-12-11 07:24:36 +08:00

5.3 KiB
Raw Blame History

tags, aliases, date created, date modified
tags aliases date created date modified
2.2.3 NUMA 感知的内存亲和性控制 (NUMA-Aware Memory Affinity Control)
星期四, 十一月 20日 2025, 10:14:01 晚上 星期四, 十一月 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 启动。

  • 命令规范

    # 强制 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): DataReceiverI/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++ 实现规范

    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 原则

  • 决策:鉴于我们使用了 cudaMallocHostCUDA 驱动通常会在调用分配函数的那个线程所在的 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。