You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

CUDA形态学膨胀等算法原地运算内存竞争问题咨询

CUDA形态学膨胀核函数原地运算内存竞争问题

问题描述

我在CUDA中编写了形态学膨胀核函数,当输入输出图像使用不同缓冲区时运行正常,但当输入输出指向同一块内存(即原地in-situ运算)时,出现了内存竞争问题。

已尝试的解决方案及失败原因

  • 方案a:使用协作组(cooperative groups)
    失败原因:因输入缓冲区为const指针,转型为void*参数时报编译错误,无法继续推进
  • 方案b:参考相关论文及多个网络资料的方案,使用互斥锁+原子加法
    失败原因:出现异常表现:使用16x16块,每块包含32x32线程,理论上块同步需将互斥量累加至256,但程序在原子加法执行48次后就阻塞
  • 方案c:参考上述同一论文的方案,使用无锁块间同步
    失败原因:看似没有实现块间同步效果,尽管直接使用了论文中的代码,仅通过添加部分__syncthreads()轻微缓解了竞争现象

相关代码

膨胀核函数代码

template <typename T>
__global__ void GenericDilate2dImg_knl(const ImageSizeInfo imgSizeInfo,
                                       volatile int* syncArrayIn, volatile int* syncArrayOut, 
                                       const unsigned long localSizeX, const unsigned long localSizeY,
                                       const int borderPolicyType, const T outOfImageValue,
                                       const struct StructuringElementInfo seInfo,
                                       const T* pInBuf, T* pOutBuf)
{
    // 从imgSizeInfo提取sizeX、sizeY等参数
    SPLIT_SIZES_FROM_STRUCT(imgSizeInfo)

    // 声明共享缓冲区pSharedBuf
    extern __shared__ char pSharedMem[];
    T* pSharedBuf = reinterpret_cast<T*>(pSharedMem);

    const unsigned long x = blockDim.x * blockIdx.x + threadIdx.x;
    const unsigned long y = blockDim.y * blockIdx.y + threadIdx.y;
    const unsigned long planIdx = blockDim.z * blockIdx.z + threadIdx.z;

    const unsigned long nbPlans = sizeZ * sizeC * sizeT;

    const unsigned long idx = x + y * sizeX + planIdx * sizeX*sizeY;

    // 将输入图像数据拷贝到共享内存
    if (x < blockDim.x * gridDim.x && y < blockDim.y * gridDim.y && planIdx < blockDim.z * gridDim.z) {
        copyDataToSharedMemory2d(pInBuf, sizeX, sizeY, planIdx,
                                 localSizeX, localSizeY, 
                                 seInfo._paddingX, seInfo._paddingY,
                                 borderPolicyType, outOfImageValue,
                                 pSharedBuf);
    }

    // 等待拷贝完成
    if (pInBuf == pOutBuf) {
        // 原地运算场景下的网格同步
        //__gpu_sync(gridDim.x * gridDim.y);        // 使用互斥锁
        __gpu_sync2(1, syncArrayIn, syncArrayOut);  // 使用无锁屏障
    }
    else
        // 输入输出缓冲区指向不同数据,仅需同步块内线程
        __syncthreads();

    // 计算图像内像素的卷积
    if (x < sizeX && y < sizeY && planIdx < nbPlans) {
        T vMax = 0;
        for (unsigned int curCoefIdx = 0; curCoefIdx < seInfo._nbOffsets; ++curCoefIdx) {
            const unsigned int sx = threadIdx.x + seInfo._paddingX + seInfo._pOffsetsX[curCoefIdx];
            const unsigned int sy = threadIdx.y + seInfo._paddingY + seInfo._pOffsetsY[curCoefIdx];
            const unsigned long sidx = sx + sy * localSizeX;
            const T curVal = pSharedBuf[sidx];
            vMax = (vMax > curVal ? vMax : curVal);
        }

        // 写入结果
        pOutBuf[idx] = vMax;
    }
}

全局内存到共享内存的拷贝函数

template <typename T>
__device__ void copyDataToSharedMemory2d(const T* pInBuf,
                                         const unsigned long sizeX, const unsigned long sizeY, const unsigned long planIdx,
                                         const unsigned long localSizeX, const unsigned long localSizeY,
                                         const int paddingX, const int paddingY,
                                         const int borderPolicyType, const T outOfImageValue,
                                         T* pSharedBuf)
{
    const int x = blockDim.x * blockIdx.x + threadIdx.x;
    const int y = blockDim.y * blockIdx.y + threadIdx.y;
    const int localX = threadIdx.x;
    const int localY = threadIdx.y;

    // 逐tile填充共享缓冲区,tile与线程组大小关联
    const unsigned int groupSizeX = blockDim.x;
    const unsigned int groupSizeY = blockDim.y;

    // 遍历每个tile
    for (int offsetY = 0; offsetY < localSizeY; offsetY += groupSizeY) {
        int curLocalY = localY + offsetY;
        int curGlobalY = y + offsetY - paddingY;
        for (int offsetX = 0; offsetX < localSizeX; offsetX += groupSizeX) {
            int curLocalX = localX + offsetX;
            int curGlobalX = x + offsetX - paddingX;

            // 当前坐标在共享子图像范围内时赋值
            if (curLocalX < localSizeX && curLocalY < localSizeY) {
                const int idx = curLocalX + curLocalY * localSizeX;
                pSharedBuf[idx] = getPixel2d(pInBuf, sizeX, sizeY, curGlobalX, curGlobalY, planIdx, borderPolicyType, outOfImageValue);
            }
        }
    }
}

像素读取函数(处理边界)

template <typename T>
__device__
T getPixel2d(const T* pInBuf,
             const unsigned long sizeX, const unsigned long sizeY,
             const int x, const int y, const int z,
             const int borderPolicyType, const T outOfImageValue)
{
    int x_inside = x;
    if (x < 0 || x >= sizeX) {
        switch (borderPolicyType) {
        case 0:// 图像外使用常量值
            return outOfImageValue;
        case 1:// 图像外使用边界像素值
            if (x < 0)
                x_inside = 0;
            else // x >= sizeX
                x_inside = sizeX - 1;
            break;
        case 2:// 镜像效果
            if (x < 0)
                x_inside = -(x + 1);
            else // x >= sizeX
                x_inside = sizeX - ((x - sizeX) + 1);
            break;
        }
    }

    // 处理y坐标边界
    int y_inside = y;
    if (y < 0 || y >= sizeY) {
        switch (borderPolicyType) {
        case 0:// 图像外使用常量值
            return outOfImageValue;
        case 1:// 图像外使用边界像素值
            if (y < 0)
                y_inside = 0;
            else // y >= sizeY
                y_inside = sizeY - 1;
            break;
        case 2:// 镜像效果
            if (y < 0)
                y_inside = -(y + 1);
            else // y >= sizeY
                y_inside = sizeY - ((y - sizeY) + 1);
            break;
        default: break;
        }
    }

    return pInBuf[x_inside + y_inside * sizeX + z * sizeX * sizeY];
}

块间同步函数

// 使用互斥锁实现
__device__ volatile int g_mutex;
__device__ void __gpu_sync(int goalVal) {
    // 块内线程ID
    int tid_in_block = threadIdx.x * blockDim.y + threadIdx.y;
    // 仅使用0号线程完成同步
    if (tid_in_block == 0) {
        atomicAdd((int*)&g_mutex, 1);
        printf("[%d] %d Vs %d\n", blockIdx.x * gridDim.y + blockIdx.y, g_mutex, goalVal);
        // 等待所有块完成累加,g_mutex等于goalVal时结束等待
        while (g_mutex < goalVal) {
            ;// 空转
        }
    }
    __syncthreads();
}

// 无锁屏障实现
__device__ void __gpu_sync2(int goalVal, volatile int* Arrayin, volatile int* Arrayout) {
    // 块内线程ID
    int tid_in_blk = threadIdx.x * blockDim.y + threadIdx.y;
    int nBlockNum = gridDim.x * gridDim.y;
    int bid = blockIdx.x * gridDim.y + blockIdx.y;
    // 仅使用0号线程完成同步
    if (tid_in_blk == 0) {
        Arrayin[bid] = goalVal;
    }
    if (bid == 1) {
        if (tid_in_blk < nBlockNum) {
            while (Arrayin[tid_in_blk] != goalVal) {
                ;// 空转
            }
        }
        __syncthreads();
        if (tid_in_blk < nBlockNum) {
            Arrayout[tid_in_blk] = goalVal;
        }
    }
    if (tid_in_blk == 0) {
        while (Arrayout[bid] != goalVal) {
            ;// 空转
        }
    }
    __syncthreads();
}

问题现象

原地运算得到的结果图像如下:
GPU膨胀运算结果
使用了11x15的结构元素,共享缓冲区大小为(nbThreadsPerBlock+2*paddindX) * (nbThreadsPerBlock+2*paddindY)。错误结果(箭头标注处)出现在部分块的顶部,位置和数值均固定,不符合内存竞争通常出现的随机错误表现。
运行环境:图像大小为510x509,运行显卡为NVidia Quadro RTX 5000。

问题疑问

是否有更优的原地运算实现方案,或是当前网格同步失效的原因是什么?

解答

网格同步失效的核心原因

你自定义的块间同步机制不符合CUDA的执行模型:NVIDIA GPU的块调度是无序的,且RTX 5000(图灵架构)的SM最多同时驻留几十个块,你启动的256个块无法全部同时在SM上运行,已经运行的块会在空转等待未调度的块时直接造成死锁,这就是互斥锁方案累加48次就阻塞的直接原因(48接近你显卡单SM驻留块上限×SM总数)。
无锁同步方案同样存在该问题,且你使用的无锁代码依赖块ID为1的块先被调度,一旦该块后调度,所有其他块都会直接卡死,同步完全失效。

正确的原地膨胀实现方案

无需复杂的网格同步,直接按以下步骤修改即可:

  1. 原地运算场景下,在核函数启动前申请一块和输入图像等大的临时全局内存
  2. 将核函数的输出先写入临时缓冲区,所有块计算完成后,再用cudaMemcpy将临时缓冲区的数据拷贝回原输入缓冲区,释放临时内存即可。
    如果需要完全避免临时内存开销,可以使用流并分阶段处理图像块,保证相邻块计算时不会互相覆盖需要的原始数据,但开发复杂度更高,常规场景下临时缓冲区方案性价比最高。

针对协作组方案的编译问题

const指针转型报错是语法问题,协作组的网格同步功能不需要修改输入缓冲区指针,你只需将协作组同步逻辑加在共享内存拷贝完成后、结果写入全局内存前即可,注意使用协作组网格同步需要用cudaLaunchCooperativeKernel启动核函数,且需要确认显卡支持该特性(你的RTX 5000支持)。

内容的提问来源于stack exchange,提问作者ractiv

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.25 09:54:03