Skip to content

CUDA compare and swap with index - #8790

Merged
sklam merged 2 commits into
numba:mainfrom
ianthomas23:6702_cas
Mar 8, 2023
Merged

sklam merged 2 commits into
numba:mainfrom
ianthomas23:6702_cas

Conversation

@ianthomas23

Copy link
Copy Markdown
Contributor

Closes #6702.

Currently numba.cuda.atomic.compare_and_swap only operates on the first array element. This PR adds support for operating on any array element by index. Much of this PR is derived from a previous attempt (#7844) with the permission of the original author @bryevdv. The new function is called numba.cuda.atomic.cas and it takes one more argument (the array index) than compare_and_swap.

My use case is in datashader to create bespoke CUDA atomic operations that are more complicated than simple add or max. Datashader essentially writes to a 2D array, each element of which represents an output pixel, and multiple CUDA threads may write to the same pixel at the same time. The indexed cas function allows the use of a 2D integer array as an array of mutexes, one per pixel, to limit access to a single thread at a time per pixel.

For anyone interested, here is my "datashader lite" implementation in which each pixel is visited multiple times with different integer values. Each pixel stores the two maximum values, in decreasing order, of all visits to that pixel. Without the cas-based locking mechanism the checking and shuffling operation of the max values per pixel isn't atomic.

# Datashader-like max2.
# Locking a single pixel at a time using CUDA CAS mutex.
import cupy
from numba import cuda
import numpy as np

@cuda.jit(device=True)
def lock(mutex, index):  # 2D index here
    while cuda.atomic.cas(mutex, index, 0, 1) != 0:
        pass
    cuda.threadfence()  # This may not be necessary?

@cuda.jit(device=True)
def unlock(mutex, index):  # 2D index here
    cuda.threadfence()
    cuda.atomic.exch(mutex, index, 0)

@cuda.jit
def datashader_lite(x, y, val, max2, mutex):
    offset = cuda.grid(1)
    if offset < x.size:
        i, j, v = x[offset], y[offset], val[offset]
        lock(mutex, (j, i))

        # max aggregator would normally have configurable length, but here hard-coded as 2
        if max2[j, i, 0] == -1:
            max2[j, i, 0] = v
        elif v > max2[j, i, 0]:
            max2[j, i, 1] = max2[j, i, 0]
            max2[j, i, 0] = v
        elif v > max2[j, i, 1]:
            max2[j, i, 1] = v

        unlock(mutex, (j, i))

x   = cupy.array([0, 0, 1, 1, 2, 2,  0,  0,  1, 1, 2, 2])
y   = cupy.array([0, 0, 0, 0, 0, 0,  1,  1,  1, 1, 1, 1])
val = cupy.array([1, 2, 3, 4, 5, 6, 12, 11, 10, 9, 8, 7])

ny, nx = 2, 3  # Pixels in target array
max2 = cupy.full((ny, nx, 2), -1)  # -1 is the unset value
mutex = cupy.zeros((ny, nx), dtype=np.uint32)  # Supports integer dtypes
tpb = 10
bpg = int(np.ceil(len(x)/tpb))
datashader_lite[bpg, tpb](x, y, val, max2, mutex)
print('max2', max2)

Output using this PR is:

max2 [[[ 2  1]
  [ 4  3]
  [ 6  5]]

 [[12 11]
  [10  9]
  [ 8  7]]]

This is my first numba PR 😃

@ianthomas23
ianthomas23 requested a review from gmarkall as a code owner March 2, 2023 15:21
Comment thread numba/cuda/cudaimpl.py
@lower(stubs.atomic.cas, types.Array, types.intp, types.Any, types.Any)
@lower(stubs.atomic.cas, types.Array, types.Tuple, types.Any, types.Any)
@lower(stubs.atomic.cas, types.Array, types.UniTuple, types.Any, types.Any)
def ptx_atomic_cas(context, builder, sig, args):

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function could probably be combined with the previous one, but I haven't figured out how to do this yet.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One way to combine them would be to modify the original implementation so that it inserts the type of the idx parameter into the signature and a zero value into the args, then calls your new implementation - for example:

diff --git a/numba/cuda/cudaimpl.py b/numba/cuda/cudaimpl.py
index 5a81ea7ea..72ed22f53 100644
--- a/numba/cuda/cudaimpl.py
+++ b/numba/cuda/cudaimpl.py
@@ -920,22 +920,10 @@ def ptx_atomic_nanmin(context, builder, dtype, ptr, val):
 
 
 @lower(stubs.atomic.compare_and_swap, types.Array, types.Any, types.Any)
-def ptx_atomic_cas_tuple(context, builder, sig, args):
-    aryty, oldty, valty = sig.args
-    ary, old, val = args
-    dtype = aryty.dtype
-
-    lary = context.make_array(aryty)(context, builder, ary)
-    zero = context.get_constant(types.intp, 0)
-    ptr = cgutils.get_item_pointer(context, builder, aryty, lary, (zero,))
-
-    if aryty.dtype in (cuda.cudadecl.integer_numba_types):
-        lmod = builder.module
-        bitwidth = aryty.dtype.bitwidth
-        return nvvmutils.atomic_cmpxchg(builder, lmod, bitwidth, ptr, old, val)
-    else:
-        raise TypeError('Unimplemented atomic compare_and_swap '
-                        'with %s array' % dtype)
+def ptx_atomic_compare_and_swap(context, builder, sig, args):
+    sig = sig.return_type(sig.args[0], types.intp, sig.args[1], sig.args[2])
+    args = (args[0], context.get_constant(types.intp, 0), args[1], args[2])
+    return ptx_atomic_cas(context, builder, sig, args)
 
 
 @lower(stubs.atomic.cas, types.Array, types.intp, types.Any, types.Any)

(note also the change of name of the original function, which apparently made little sense anyway!)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, that is much more concise.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

np.testing.assert_equal(res, gold)

def check_compare_and_swap(self, n, fill, unfill, dtype):
def check_cas(self, n, fill, unfill, dtype, cas_func, ndim=1):

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is used to test both compare_and_swap and cas.

@stuartarchibald stuartarchibald added the Effort - medium Medium size effort needed label Mar 6, 2023
@stuartarchibald

Copy link
Copy Markdown
Contributor

@ianthomas23 Many thanks for the patch. @gmarkall any chance you could review this please?

@stuartarchibald stuartarchibald added the CUDA CUDA related issue/PR label Mar 6, 2023
@gmarkall

gmarkall commented Mar 6, 2023

Copy link
Copy Markdown
Member

gpuci run tests

@gmarkall gmarkall left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Many thanks for the PR! This looks good in general, and there are just a few small thoughts on the diff.

The reference docs should probably be updated to mention the new function too, in this section: https://github.com/numba/numba/blob/main/docs/source/cuda-reference/kernel.rst#synchronization-and-atomic-operations

Comment thread numba/cuda/simulator/kernelapi.py Outdated
maxlock = threading.Lock()
minlock = threading.Lock()
caslock = threading.Lock()
casindexlock = threading.Lock()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency I'd probably call this caslock and rename the existing caslock to compare_and_swaplock.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment thread numba/cuda/stubs.py Outdated
class cas(Stub):
"""cas(ary, idx, old, val)

Conditionally assign ``val`` to the element ary[idx] of an array

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should that refer to the position of the element rather than the value itself?:

Suggested change
Conditionally assign ``val`` to the element ary[idx] of an array
Conditionally assign ``val`` to the element ``idx`` of an array

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment thread numba/cuda/stubs.py Outdated
"""cas(ary, idx, old, val)

Conditionally assign ``val`` to the element ary[idx] of an array
``ary`` if the current value of ary[idx] matches ``old``.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For formatting consistency:

Suggested change
``ary`` if the current value of ary[idx] matches ``old``.
``ary`` if the current value of ``ary[idx]`` matches ``old``.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment thread numba/cuda/cudaimpl.py
@lower(stubs.atomic.cas, types.Array, types.intp, types.Any, types.Any)
@lower(stubs.atomic.cas, types.Array, types.Tuple, types.Any, types.Any)
@lower(stubs.atomic.cas, types.Array, types.UniTuple, types.Any, types.Any)
def ptx_atomic_cas(context, builder, sig, args):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One way to combine them would be to modify the original implementation so that it inserts the type of the idx parameter into the signature and a zero value into the args, then calls your new implementation - for example:

diff --git a/numba/cuda/cudaimpl.py b/numba/cuda/cudaimpl.py
index 5a81ea7ea..72ed22f53 100644
--- a/numba/cuda/cudaimpl.py
+++ b/numba/cuda/cudaimpl.py
@@ -920,22 +920,10 @@ def ptx_atomic_nanmin(context, builder, dtype, ptr, val):
 
 
 @lower(stubs.atomic.compare_and_swap, types.Array, types.Any, types.Any)
-def ptx_atomic_cas_tuple(context, builder, sig, args):
-    aryty, oldty, valty = sig.args
-    ary, old, val = args
-    dtype = aryty.dtype
-
-    lary = context.make_array(aryty)(context, builder, ary)
-    zero = context.get_constant(types.intp, 0)
-    ptr = cgutils.get_item_pointer(context, builder, aryty, lary, (zero,))
-
-    if aryty.dtype in (cuda.cudadecl.integer_numba_types):
-        lmod = builder.module
-        bitwidth = aryty.dtype.bitwidth
-        return nvvmutils.atomic_cmpxchg(builder, lmod, bitwidth, ptr, old, val)
-    else:
-        raise TypeError('Unimplemented atomic compare_and_swap '
-                        'with %s array' % dtype)
+def ptx_atomic_compare_and_swap(context, builder, sig, args):
+    sig = sig.return_type(sig.args[0], types.intp, sig.args[1], sig.args[2])
+    args = (args[0], context.get_constant(types.intp, 0), args[1], args[2])
+    return ptx_atomic_cas(context, builder, sig, args)
 
 
 @lower(stubs.atomic.cas, types.Array, types.intp, types.Any, types.Any)

(note also the change of name of the original function, which apparently made little sense anyway!)

Comment thread numba/cuda/tests/cudapy/test_atomics.py Outdated
Comment on lines +462 to +463
out = cuda.atomic.cas(res, gid, fill_val, ary[gid])
old[gid] = out

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whilst the original atomic_compare_and_swap uses this pattern, is it a little simpler as just:

Suggested change
out = cuda.atomic.cas(res, gid, fill_val, ary[gid])
old[gid] = out
old[gid] = cuda.atomic.cas(res, gid, fill_val, ary[gid])

(and is it worth updating the original function too?)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, and on the other two functions too.

Comment thread numba/cuda/tests/cudapy/test_atomics.py Outdated
Comment on lines +469 to +470
out = cuda.atomic.cas(res, gid, fill_val, ary[gid])
old[gid] = out

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar as above:

Suggested change
out = cuda.atomic.cas(res, gid, fill_val, ary[gid])
old[gid] = out
old[gid] = cuda.atomic.cas(res, gid, fill_val, ary[gid])

@gmarkall gmarkall added 4 - Waiting on author Waiting for author to respond to review and removed 3 - Ready for Review labels Mar 6, 2023
@ianthomas23

Copy link
Copy Markdown
Contributor Author

The reference docs should probably be updated to mention the new function too, in this section: https://github.com/numba/numba/blob/main/docs/source/cuda-reference/kernel.rst#synchronization-and-atomic-operations

I've added docs for numba.cuda.atomic.cas after the other atomic functions:

cas

I haven't added an entry for compare_and_swap as presumably we should be encouraging use of the new function instead.

indices for indexing into multiple dimensional arrays. The number of element
in ``idx`` must match the number of dimension of ``array``.

Returns the value of ``array[idx]`` before the storing the new value.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed a few extraneous the from this file, which aren't strictly speaking relevant to this PR. I can revert if preferred.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's great, many thanks for a nice little tidy-up!

@ianthomas23

Copy link
Copy Markdown
Contributor Author

I think I've addressed all review comments so far.

@gmarkall

gmarkall commented Mar 6, 2023

Copy link
Copy Markdown
Member

gpuci run tests

@gmarkall gmarkall added 4 - Waiting on reviewer Waiting for reviewer to respond to author and removed 4 - Waiting on author Waiting for author to respond to review labels Mar 6, 2023

@gmarkall gmarkall left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the quick updates and extra tidy-up - this is looking great! I think not documenting compare_and_swap was also the right decision - thanks for your thoughtfulness!

I'm going to approve this, with the anticipation that gpuCI passes, at which point it will be ready to merge. I think skipping the buildfarm is fine for this PR since that would only cover Windows in addition to gpuCI, and the implementation overlaps with existing patterns used in the target, I don't anticipate there is much chance of a Windows-specific issue.

@gmarkall gmarkall added 4 - Waiting on CI Review etc done, waiting for CI to finish 5 - Ready to merge Review and testing done, is ready to merge and removed 4 - Waiting on reviewer Waiting for reviewer to respond to author 4 - Waiting on CI Review etc done, waiting for CI to finish labels Mar 6, 2023
@gmarkall gmarkall added this to the Numba 0.57 RC milestone Mar 6, 2023
@sklam
sklam merged commit 291269b into numba:main Mar 8, 2023
@ianthomas23
ianthomas23 deleted the 6702_cas branch March 8, 2023 10:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

5 - Ready to merge Review and testing done, is ready to merge CUDA CUDA related issue/PR Effort - medium Medium size effort needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CUDA: Support compare and swap on array elements other than the first

5 participants