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

GPU上快速实现无重叠布尔数组匹配的优化问询

布尔数组无重叠放置位置的GPU加速优化问题

现有大小两个2D NumPy布尔数组,需找出小数组在大数组上所有可放置的位置——要求小数组与大数组对应切片无同时为True的元素(类似无重叠放置物体),结果数组以左上角坐标标记位置且与大数组同形。

已实现的两种GPU方案

方案一:PyTorch卷积实现

利用卷积操作快速检测重叠,卷积结果为0的位置即为可放置位置:

import torch
import torch.nn.functional as F
import numpy as np

def get_placement_options(large_array, small_array):
    shape = large_array.shape

    # 转换为PyTorch CUDA张量
    large_array = torch.from_numpy(large_array).to('cuda:0').float()
    small_array = torch.from_numpy(small_array).to('cuda:0').float()

    # 卷积计算重叠情况
    possible_locations = (F.conv2d(large_array[None, None], small_array[None, None])[0, 0] < .5).cpu().numpy()
    
    result = np.zeros(shape, dtype='bool')
    result[:possible_locations.shape[0], :possible_locations.shape[1]] = possible_locations
    return result

方案二:CuPy自定义CUDA核+位打包实现

通过位打包将布尔数组转为int64,尝试用自定义CUDA核提升效率:

import cupy as cp
import numpy as np
from cupy.lib.stride_tricks import as_strided

class Placer:
    def __init__(self):
        kernel_code = """
        extern "C" {
        __global__ void compute(long long int* large_array, long long int* small_array, int* result, int rn, int rm, int n, int m, int k, int l) {
            int i = blockIdx.x * blockDim.x + threadIdx.x;  // 大数组中待计算的x坐标
            int j = blockIdx.y * blockDim.y + threadIdx.y;  // 大数组中待计算的y坐标
            
            if (i < rn && j < rm) {
            
                int r_i = i % 8;  // x方向的偏移量
                int r_j = j % 8;  // y方向的偏移量
                int sub_array_index = r_i * 8 * n * m + r_j * n * m;

                for (int p = 0; p < k; ++p) {
                    for (int q = 0; q < l; ++q) {
                        if ((small_array[p * l + q] & large_array[sub_array_index + ((i / 8)+p) * m + (j / 8)+q]) != 0) {
                            result[i * rm + j] = 0;
                            return;
                        }
                    }
                }
            }

        }
        }
        """

        # 编译核函数
        self.compiled_kernel = cp.RawKernel(kernel_code, 'compute')

    def __call__(self, large_array: np.ndarray, small_array: np.ndarray):
        
        # 初始化结果数组
        result_np = np.zeros_like(large_array)

        # 补全小数组至8的倍数,同时给大数组补相同的padding(补True,避免边界误判)
        padding = ((0, (8 - small_array.shape[0] % 8) % 8), (0, (8 - small_array.shape[1] % 8) % 8))
        small_array = np.pad(small_array, padding, mode='constant', constant_values=False)
        K, L = small_array.shape
        large_array = np.pad(large_array, padding, mode='constant', constant_values=True) 

        # 补全大数组至8的倍数
        padding = ((0, (8 - large_array.shape[0] % 8) % 8), (0, (8 - large_array.shape[1] % 8) % 8))
        large_array = np.pad(large_array, padding, mode='constant', constant_values=True) 
        N, M = large_array.shape
        
        # 生成大数组的所有8x8滑动窗口,打包为int64并存储所有偏移情况
        large_array_cp = cp.array(large_array)
        large_array_cp = cp.pad(self.sliding_window_view_cp(large_array_cp, (8, 8)), ((0, 7), (0, 7), (0, 0), (0, 0)), 'constant', constant_values=True).transpose((2, 3, 0, 1)).copy()
        large_array_cp = cp.packbits(large_array_cp.transpose((0, 1, 3, 2))).reshape(8, 8, large_array.shape[1], large_array.shape[0] // 8)
        large_array_cp = large_array_cp.transpose((0, 1, 3, 2)).copy().view('int64')

        # 将小数组打包为int64
        small_array = cp.array(np.packbits(small_array.copy(), axis=0).view(np.int64))
        
        # 调用核函数
        block = (32, 32, 1)
        grid = ((N-K+1 + block[0] - 1) // block[0], (M-L+1 + block[1] - 1) // block[1])
        result = cp.ones((N-K+1, M-L+1), dtype=cp.int32)
        self.compiled_kernel(grid=grid, block=block, args=(large_array_cp, small_array, result, N-K+1, M-L+1, N // 8, M // 8, K // 8, L // 8))
        
        # 等待GPU计算完成
        cp.cuda.stream.get_current_stream().synchronize()

        # 转换结果并填充到输出数组
        result = result.astype(cp.bool_).get()
        result_np[:result.shape[0], :result.shape[1]] = result

        return result_np

    @staticmethod
    def sliding_window_view_cp(arr, window_shape):
        output_shape = arr.shape[:-len(window_shape)] + tuple(i - j + 1 for i, j in zip(arr.shape[-len(window_shape):], window_shape))
        strides = arr.strides + arr.strides[-len(window_shape):]
        return as_strided(arr, shape=output_shape + window_shape, strides=strides)

性能问题

理论上位打包+自定义CUDA核的方案应具备更高效率,但实际测试中与PyTorch方案速度相近(分别耗时0.06秒、0.05秒),推测自定义CUDA核缺少关键优化,需进一步提速。

测试用例

large_array = np.random.choice(a=[False, True], size=(1000, 1000))
small_array = np.zeros((100, 100), dtype=bool)
small_array[-4:, -4:] = True
large_array[-260:, -260:] = False

有效位置示意图:大数组右下角区域存在大片可放置区域,其余位置因随机True值分布零散可放置点。

优化思路与方案

  • 内存访问优化
    • 改用共享内存预加载大数组的局部块,减少全局内存的重复访问。每个线程块加载对应区域的大数组数据到共享内存,线程间复用数据,降低内存延迟。
    • 调整数组存储布局,确保全局内存访问符合合并访问规则,避免非对齐访问带来的额外开销。
  • 位运算并行化
    • 利用64位整数的位运算一次性处理8x8块:将小数组的位打包块与大数组对应块做按位与,只要结果非零就标记不可放置,一次操作替代64次单元素检查,大幅减少循环次数。
  • 分支优化
    • 用掩码操作替代原核函数中的提前return分支:用变量记录冲突状态,循环结束后统一赋值结果,减少GPU分支发散带来的性能损耗。
  • 预处理优化
    • 对小数组做预处理,只记录其中True值的位置,核函数中仅检查大数组对应位置,避免遍历小数组的大量零位,减少计算量。
  • 简化预处理流程
    • 改用CuPy内置的cp.lib.stride_tricks.sliding_window_view替代自定义滑动窗口实现,减少手动内存操作带来的拷贝和性能损耗。

内容的提问来源于stack exchange,提问作者F.Wessels

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.19 08:27:11