查找金属纹理中的最小值和最大值

Finding the minimum and maximum value within a Metal texture

我有一个 MTLTexture 包含 16 位无符号整数 (MTLPixelFormatR16Uint)。值的范围从大约 7000 到 20000,其中 0 被用作 'nodata' 值,这就是它在下面的代码中被跳过的原因。我想找到最小值和最大值,以便我可以在 0-255 之间重新调整这些值。最终我会寻找基于数据直方图的最小值和最大值(它有一些异常值),但现在我只是简单地提取 min/max.

我可以从 GPU 读取数据到 CPU 并提取 min/max 值,但更愿意在 GPU 上执行此任务。

第一次尝试

命令编码器每个线程组分配 16x16 个线程,线程组的数量基于纹理大小(例如;宽度 = textureWidth / 16,高度 = textureHeight / 16)。

typedef struct {
    atomic_uint min;
    atomic_uint max;
} BandMinMax;

kernel void minMax(texture2d<ushort, access::read> band1 [[texture(0)]],
                   device BandMinMax &out [[buffer(0)]],
                   uint2 gid [[thread_position_in_grid]])
{
    ushort value = band1.read(gid).r;

    if (value != 0) {
        uint currentMin = atomic_load_explicit(&out.min, memory_order_relaxed);
        uint currentMax = atomic_load_explicit(&out.max, memory_order_relaxed);

        if (value > currentMax) {
            atomic_store_explicit(&out.max, value, memory_order_relaxed);
        }
        if (value < currentMin) {
            atomic_store_explicit(&out.min, value, memory_order_relaxed);
        }
    }
}

由此我得到了最小值和最大值,但对于同一数据集,最小值和最大值通常 return 不同的值。可以肯定的是,当有多个线程时,这是单个线程的最小值和最大值 运行。

第二次尝试

在之前的尝试的基础上,这次我存储了每个线程的单个 min/max 值,全部 256 (16x16)。

kernel void minMax(texture2d<ushort, access::read> band1 [[texture(0)]],
                   device BandMinMax *out [[buffer(0)]],
                   uint2 gid [[thread_position_in_grid]],
                   uint tid [[ thread_index_in_threadgroup ]])
{

    ushort value = band1.read(gid).r;

    if (value != 0) {
        uint currentMin = atomic_load_explicit(&out[tid].min, memory_order_relaxed);
        uint currentMax = atomic_load_explicit(&out[tid].max, memory_order_relaxed);

        if (value > currentMax) {
            atomic_store_explicit(&out[tid].max, value, memory_order_relaxed);
        }
        if (value < currentMin) {
            atomic_store_explicit(&out[tid].min, value, memory_order_relaxed);
        }
    }
}

这 return 是一个包含 256 组 min/max 值的数组。从这些我想我可以找到最小值中的最低值,但这似乎是一个糟糕的方法。希望能指出正确的方向,谢谢!

Metal Shading Language 具有原子比较和交换功能,您可以使用它来将内存位置的现有值与一个值进行比较,如果它们比较不相等,则替换该位置的值。有了这些,您可以创建一组原子比较和替换-if-[greater|less]-than 操作:

static void atomic_uint_exchange_if_less_than(volatile device atomic_uint *current, uint candidate)
{
    uint val;
    do {
        val = *((device uint *)current);
    } while ((candidate < val || val == 0) && !atomic_compare_exchange_weak_explicit(current,
                                                                                     &val,
                                                                                     candidate,
                                                                                     memory_order_relaxed,
                                                                                     memory_order_relaxed));
}

static void atomic_uint_exchange_if_greater_than(volatile device atomic_uint *current, uint candidate)
{
    uint val;
    do {
        val = *((device uint *)current);
    } while (candidate > val && !atomic_compare_exchange_weak_explicit(current,
                                                                       &val,
                                                                       candidate,
                                                                       memory_order_relaxed,
                                                                       memory_order_relaxed));
}

要应用这些,您可以创建一个缓冲区,其中每个线程组包含一个交错的最小值、最大值对。然后,在内核函数中,从纹理中读取并有条件地写入最小值和最大值:

kernel void min_max_per_threadgroup(texture2d<ushort, access::read> texture [[texture(0)]],
                                    device uint *mapBuffer [[buffer(0)]],
                                    uint2 tpig [[thread_position_in_grid]],
                                    uint2 tgpig [[threadgroup_position_in_grid]],
                                    uint2 tgpg [[threadgroups_per_grid]])
{
    ushort val = texture.read(tpig).r;

    device atomic_uint *atomicBuffer = (device atomic_uint *)mapBuffer;

    atomic_uint_exchange_if_less_than(atomicBuffer + ((tgpig[1] * tgpg[0] + tgpig[0]) * 2),
                                      val);

    atomic_uint_exchange_if_greater_than(atomicBuffer + ((tgpig[1] * tgpg[0] + tgpig[0]) * 2) + 1,
                                         val);
}

最后,运行 一个单独的内核来减少这个缓冲区并收集整个纹理的最终最小值、最大值:

kernel void min_max_reduce(constant uint *mapBuffer [[buffer(0)]],
                           device uint *reduceBuffer [[buffer(1)]],
                           uint2 tpig [[thread_position_in_grid]])
{
    uint minv = mapBuffer[tpig[0] * 2];
    uint maxv = mapBuffer[tpig[0] * 2 + 1];

    device atomic_uint *atomicBuffer = (device atomic_uint *)reduceBuffer;

    atomic_uint_exchange_if_less_than(atomicBuffer, minv);

    atomic_uint_exchange_if_greater_than(atomicBuffer + 1, maxv);
}

当然,您只能减少设备允许的总线程执行宽度(~256),因此您可能需要多次减少,每次减少数据的大小按最大线程执行宽度的一个因子进行操作。

免责声明:这可能不是最好的技术,但在我对 OS X 实现的有限测试中它似乎是正确的。它比在 Intel Iris Pro 上的 256x256 纹理上的天真 CPU 实现略快,但在 Nvidia GT 750M 上慢得多(因为调度开销)。