C++多线程2——亲和性

一、多核CPU的结构

服务器的多核结构属于NUMA(Non-Uniform Memory Access,非一致性内存访问)架构。这里有一篇很好的介绍NUMA架构理解

以我们课题组的服务器为例,上面有两个“插槽”,即两个socket(物理概念),逻辑上称为节点node。

每个插槽10个物理核core,每个物理核通过超线程技术可以产生两个逻辑核processor。

因此我们服务器可以有$ 2 \times 10 \times 2=40$个线程 。

CPU亲和的意思是,假设我们产生40个线程,希望每个线程运行在他们各自的node上或者core上,这样可以减少程序运行时的内存搬运。

二、查看服务器配置

cat /proc/cpuinfo #查看所有processor的信息

core.jpg

这是最后一个逻辑核的信息,总共有40个逻辑核,该逻辑核在node1节点的12号cpu上(这里的编号不一定连续)。所有的逻辑核分布如下:

core2.jpg

因此我们设置线程时,可以绑在同一个node上,即绑在逻辑核编号为{0,2,…,18,20,22,…,38}的集合,或{1,3,…,19,21,23,…,39}的集合,让内存不要在Node0和Node1之间发生。

也可以绑在同一个core上,即绑在逻辑核编号为{0,20},{2,22},{4,24},…,或{19,39}的集合,这样不同物理核之间也不会用内存交换,提高运行速度。

可以用numactl程序查看这些核的分布情况,如没有可以安装sudo apt install numactl,但服务器上我们一般没有root权限,还可以这样查看:

cat /sys/devices/system/node/node0/cpulist
cat /sys/devices/system/node/node0/cpumap

三、设置pthread亲和性

还是用求和的例子说明:

#define _GNU_SOURCE
#include <stdio.h>
#include <sched.h>
#include <stdlib.h>
#include <pthread.h> 
#include <unistd.h>

const int S = 100000000;
int* arr;

typedef struct{
 int first;
 int last;
 int result;
}MY_ARGS;

void* mysum(void* args){
 int i;
 int s=0;
 MY_ARGS* my_args = (MY_ARGS*) args;
 int first = my_args->first;
 int last = my_args->last;

 for(i=first;i<last;i++){
 s = s + arr[i];
 }
 my_args -> result = s;
 return NULL;
}

int main(){
 int i;
 arr = malloc(sizeof(int) * S);
 for(i=0;i<S;i++){
 if(i%2==0)
 arr[i]=i;
 else
 arr[i]=-i;
 }

 //亲和性的process集合
 int blist[20]={1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33,35,37,39};
 cpu_set_t cpuset_1,cpuset_2;
 CPU_ZERO(&cpuset_1);
 CPU_ZERO(&cpuset_2);
 for(i=0;i<20;i++){
 CPU_SET(blist[i], &cpuset_1);
 CPU_SET(blist[i]-1, &cpuset_2);
 }
 //亲和性的属性设置
 pthread_attr_t attr1;
 pthread_attr_t attr2;
 pthread_attr_init(&attr1);
 pthread_attr_init(&attr2);
 pthread_attr_setaffinity_np(&attr1, sizeof(cpu_set_t), &cpuset_1);
 pthread_attr_setaffinity_np(&attr2, sizeof(cpu_set_t), &cpuset_2);

 // 产生两个线程
 pthread_t th1;
 pthread_t th2;

 int mid = S/2;
 MY_ARGS args1 = {0,mid,0};
 MY_ARGS args2 = {mid,S,0};

 pthread_create(&th1,&attr1,mysum,&args1);
 pthread_create(&th2,&attr2,mysum,&args2); 

 pthread_join(th1,NULL);
 pthread_join(th2,NULL);

 printf("sum: %d\n",args1.result + args2.result);
 return 0;
}

这里将两个线程绑分布绑在node0和node1节点,也可以都绑在同一个节点,因为每个节点最多能跑20个线程,问题不大。

更多实例可以参看我的gitee:pthread亲和性

原文C++多线程2——亲和性

### PointNet++在多线程环境下的报错解决方案 PointNet++ 是一种基于深度学习的方法,用于处理三维点云数据。当将其应用于多线程环境中时,可能会遇到各种错误,这些错误通常与资源竞争、内存管理或模型初始化有关。 以下是可能的原因分析以及对应的解决方案: #### 1. **资源共享冲突** 如果多个线程尝试同时访问同一份模型权重或共享的计算图,则可能导致资源竞争问题。这种情况下,可以通过引入锁机制来保护共享资源。 ```cpp std::mutex model_mutex; void thread_safe_predict(const std::vector<float>& point_cloud, float* result) { std::lock_guard<std::mutex> lock(model_mutex); // 加锁 // 调用 PointNet++ 的预测函数 predict_with_pointnet_plus(point_cloud, result); } ``` 上述代码通过 `std::mutex` 和 `std::lock_guard` 来确保只有一个线程能够修改共享状态[^1]。 --- #### 2. **CUDA 上下文切换问题** 在 GPU 计算中,不同的 CUDA 流(Stream)之间可能存在上下文切换的问题。这通常是由于未正确配置流而导致的。为了防止此类问题,可以在每个线程中创建独立的 CUDA 流并绑定到该线程。 ```cpp cudaStream_t stream; cudaStreamCreate(&stream); // 使用自定义流执行操作 predict_with_cuda_stream(point_cloud, result, stream); cudaStreamDestroy(stream); ``` 这样可以避免因全局默认流引起的同步问题[^3]。 --- #### 3. **模型加载重复** 在同一进程中多次加载相同的 PyTorch 或 TensorFlow 模型会消耗大量内存,并可能导致意外行为。建议只在一个主线程中加载一次模型,其他线程通过共享指针或其他方式重用此实例。 对于 Python 实现,可采用如下方法: ```python import torch from threading import Lock model_lock = Lock() pointnet_model = None def initialize_model(): global pointnet_model with model_lock: if pointnet_model is None: pointnet_model = load_pointnet_model() # 假设这是加载模型的函数 def threaded_prediction(point_cloud): initialize_model() with model_lock: output = pointnet_model(torch.tensor(point_cloud)) return output.detach().numpy() ``` 这里利用了 `threading.Lock` 防止多个线程同时初始化模型。 --- #### 4. **输入数据不一致** 在多线程场景下,如果不同线程传递给模型的数据格式或维度不统一,也可能引发运行时错误。因此,在调用模型之前应严格验证输入的有效性。 ```python def validate_input(point_cloud): assert isinstance(point_cloud, list), "Input must be a list" assert all(isinstance(x, tuple) and len(x) == 3 for x in point_cloud), \ "Each point should be a tuple of size 3" def safe_threaded_prediction(point_cloud): try: validate_input(point_cloud) return threaded_prediction(point_cloud) except AssertionError as e: print(f"Invalid input detected: {e}") return None ``` 以上代码片段展示了如何在实际预测前加入输入校验逻辑。 --- #### 5. **调度策略优化** 考虑到某些任务可能对硬件资源的需求较高,合理分配工作负载至关重要。Kubernetes 中提到的亲和性和反亲和性概念同样适用于本地多线程程序设计——即让 CPU 密集型任务尽可能分布在不同的核心上以减少干扰[^2]。 例如,在 Linux 平台上可通过设置进程 affinity 提升性能: ```c++ #include <sched.h> #include <pthread.h> void set_cpu_affinity(pthread_t tid, int cpu_id) { cpu_set_t cpuset; CPU_ZERO(&cpuset); CPU_SET(cpu_id, &cpuset); pthread_setaffinity_np(tid, sizeof(cpuset), &cpuset); } void* worker_function(void* arg) { // 设置当前线程的核心亲和性 set_cpu_affinity(pthread_self(), *(int*)arg); // 执行具体任务... return nullptr; } ``` 这种方法有助于降低缓存失效率并提高整体吞吐量。 --- ### 总结 针对 PointNet++ 在多线程环境下可能出现的各种异常情况,可以从以下几个方面入手解决问题:一是妥善管理公共资源;二是调整 CUDA 流配置规避潜在冲突;三是集中化模型加载流程减轻负担;四是强化前置检验环节保障数据质量;五是科学规划作业分布提升效率。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值