JuliaGPU / JuliaGPU/Metal.jl

Threadgroup atomics require all-atomic operation

Open
#217 3 comments 0 reactions 0 assignees View on GitHub
kernels
Dominant language
Julia
Stars
463
Forks
68
Avg merge
1d 19m
Merged PRs (30d)
32

Description

MWE:

```julia
using Metal

function local_kernel(a, expected::AbstractArray{T}, desired::T) where T
i = thread_position_in_grid_1d()
b = MtlThreadGroupArray(T, 16)

#b[i] = a[i]
Metal.atomic_store_explicit(pointer(b, i), Metal.atomic_load_explicit(pointer(a, i)))

while Metal.atomic_compare_exchange_weak_explicit(pointer(b, i), expected[i], desired) != expected[i]
# keep on trying
end

#a[i] = b[i]
Metal.atomic_store_explicit(pointer(a, i), Metal.atomic_load_explicit(pointer(b, i)))

return
end

function main(; T=Int32, n=16)
a = Metal.zeros(T, n)
expected = copy(a)
desired = T(42)
@metal threads=n local_kernel(a, expected, desired)
Array(a)
end
```

Note how the load and stores that initialize the threadgroup memory and copy it back to global memory need to be atomics for this example to work, even though every thread has its own dedicated memory address to act upon. Demoting those operations to regular array operations results in the final array containing all zeros.

This smells like an upstream bug, especially because the above pattern is impossible to replicate in Metal C (where `atomic_int` is used as element type, promoting all operations to atomic):

```metal
#include
using namespace metal;

kernel void local_kernel(device atomic_int* a [[ buffer(0) ]],
device int* expected [[ buffer(1) ]],
device int* desired [[ buffer(2) ]],
uint i [[ thread_position_in_grid ]]) {
threadgroup atomic_int b[16];
atomic_store_explicit(&b[i], atomic_load_explicit(&a[i], memory_order_relaxed), memory_order_relaxed);

int expectedValue = expected[i];
while (!atomic_compare_exchange_weak_explicit(&b[i], &expectedValue, desired[i], memory_order_relaxed, memory_order_relaxed)) {
// keep on trying
}
atomic_store_explicit(&a[i], atomic_load_explicit(&b[i], memory_order_relaxed), memory_order_relaxed);
}
```

```objc
#import

int main() {
id device = MTLCreateSystemDefaultDevice();

NSError *error = nil;
id library = [device newLibraryWithFile:@"atomic_xchg.metallib" error:&error];
id function = [library newFunctionWithName:@"local_kernel"];

id commandQueue = [device newCommandQueue];
id commandBuffer = [commandQueue commandBuffer];

id pipelineState = [device newComputePipelineStateWithFunction:function error:&error];

int n = 16;
id a = [device newBufferWithLength:n*sizeof(int) options:MTLResourceOptionCPUCacheModeDefault];
id expected = [device newBufferWithBytesNoCopy:malloc(n*sizeof(int)) length:n*sizeof(int) options:0 deallocator:nil];
int desiredValues[n];
for (int i = 0; i < n; i++) {
desiredValues[i] = 42;
}
id desired = [device newBufferWithBytes:&desiredValues length:n*sizeof(int) options:MTLResourceOptionCPUCacheModeDefault];

id encoder = [commandBuffer computeCommandEncoder];
[encoder setComputePipelineState:pipelineState];
[encoder setBuffer:a offset:0 atIndex:0];
[encoder setBuffer:expected offset:0 atIndex:1];
[encoder setBuffer:desired offset:0 atIndex:2];

MTLSize threadsPerGroup = MTLSizeMake(n, 1, 1);
MTLSize numThreadgroups = MTLSizeMake(1, 1, 1);
[encoder dispatchThreadgroups:numThreadgroups threadsPerThreadgroup:threadsPerGroup];
[encoder endEncoding];

[commandBuffer commit];
[commandBuffer waitUntilCompleted];

int *result = a.contents;
for (int i = 0; i < n; i++) {
NSLog(@"%d\n", result[i]);
}

return 0;
}
```

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.