Repository navigation
Address guvectorize too slow for cuda target - #6889
Conversation
|
Started smoketest: EDIT: should be |
gmarkall
left a comment
There was a problem hiding this comment.
This is looking promising; there are some comments on the diff suggesting some changes.
It's good that there is a test ensuring this fires for inefficient uses of @guvectorize functions. I think there should also be a test of this using a normal kernel launch - I can't see a really good place to put this new test, but either in numba/cuda/tests/cudapy/test_errors.py or in a new file test_warnings.py.
|
|
||
| .. envvar:: NUMBA_CUDA_LOW_OCCUPANCY_WARNINGS | ||
|
|
||
| Enable warnings if the block size is too small relative to the number of |
There was a problem hiding this comment.
As I understand it, the warning is generated when the grid size is too small, not the block size. It's probably worth mentioning that the heuristic used here is (grid size) < 2 * (number of SMs), and that the absence of the warning doesn't necessarily imply a good grid size - larger multiples of the SM count may be more efficient.
There was a problem hiding this comment.
It may also be worth mentioning that disabling the warning will reduce the number of CUDA API calls, as it needs to check the number of SMs for the device in the current context to work out if occupancy is low (see comments below on https://github.com/numba/numba/pull/6889/files#diff-b44aea5e6ce50ecba71c563dda6f60805b174c5c048d9287c056d0f8d506a27fR790).
There was a problem hiding this comment.
Also it should be mentioned that this is on by default.
| # whether to warn about kernels where the block size will | ||
| # result in a inefficient mapping to GPUs. On by default. |
There was a problem hiding this comment.
Perhaps "Whether to warn about kernel launches where the grid size will underutilize the GPU due to low occupancy. On by default."?
| num_blocks = griddim[0] * griddim[1] * griddim[2] | ||
| ctx = get_context() | ||
| smcount = ctx.device.MULTIPROCESSOR_COUNT |
There was a problem hiding this comment.
I think I'd push this calculation inside an if config.LOW_OCCUPANCY_WARNINGS: condition - if a user is trying to reduce the number of CUDA API calls (get_context can be a little heavy in some use cases) they could then turn off LOW_OCCUPANCY_WARNINGS to save the call to get_context().
| """ | ||
| def setUp(self): | ||
| # Disable warnings about low gpu utilization for test suite | ||
| config.LOW_OCCUPANCY_WARNINGS = 0 |
There was a problem hiding this comment.
This permanently disables low occupancy warnings. To use a setUp() method for disabling the warning for CUDA test cases the following additions are needed:
- Memoizing the previous value of
config.LOW_OCCUPANCY_WARNINGSin thesetUp()method. - A
tearDown()method that restores the old value ofconfig.LOW_OCCUPANCY_WARNINGSafter the test has run.
A similar pattern is used in the EMM Plugin tests:
numba/numba/cuda/tests/cudadrv/test_emm_plugins.py
Lines 107 to 115 in 3497fb2
(note this is not exactly the same because of the way the EMM Plugin interface works, but the idea is that the state for the test is set up in setUp() and then a suitable state for subsequent tests restored in tearDown()).
| with override_config('LOW_OCCUPANCY_WARNINGS', 1): | ||
| with warnings.catch_warnings(record=True) as w: | ||
| warnings.simplefilter('always', NumbaPerformanceWarning) | ||
| ignore_internal_warnings() |
There was a problem hiding this comment.
What does ignore_internal_warnings() do here? I tried removing it and the test still seems to pass.
| copy2d(A, out=B) | ||
| self.assertTrue(np.allclose(A, B)) | ||
|
|
||
| def test_inefficient_guvectorize(self): |
There was a problem hiding this comment.
I think this could do with a comment explaining that the test creates an inefficient use of the GPU because the inputs are all mapped onto a single thread in a single block.
There was a problem hiding this comment.
(Note: this is resolved, but I can't resolve the comment because I don't have write access to this repo)
| smcount = ctx.device.MULTIPROCESSOR_COUNT | ||
|
|
||
| if (config.LOW_OCCUPANCY_WARNINGS) and (num_blocks < 2 * smcount): | ||
| msg = ("Number of blocks ({blocks}) that are < 2 * SM ({sm}) " |
There was a problem hiding this comment.
I think will generate inefficient kernel code is a bit confusing - how about phrasing the warning as Grid size (<size>) less than 2 * SM count (2 * (<SM count>) = (<SM count * 2>) will likely result in GPU underutilization due to low occupancy?
|
@gmarkall I believe all your review comments have been incorporated. |
stuartarchibald
left a comment
There was a problem hiding this comment.
Thanks for the patch, this is great feedback to give users, it's good to see this implemented. I've a few minor comments in addition to @gmarkall's review, once resolved looks good to merge. Thanks again!
|
|
||
| # Whether to warn about kernel launches where the grid size will | ||
| # underutilize the GPU due to low occupancy. On by default. | ||
| LOW_OCCUPANCY_WARNINGS = _readenv( |
There was a problem hiding this comment.
| LOW_OCCUPANCY_WARNINGS = _readenv( | |
| CUDA_LOW_OCCUPANCY_WARNINGS = _readenv( |
All other CUDA related config vars start with CUDA_ or have CUDA in their name, suggest calling this something like the above.
|
|
||
| def setUp(self): | ||
| self._low_occupancy_warnings = config.LOW_OCCUPANCY_WARNINGS | ||
| # Disable warnings about low gpu utilization for test suite |
There was a problem hiding this comment.
| # Disable warnings about low gpu utilization for test suite | |
| # Disable warnings about low gpu utilization in the test suite |
| grid_size = griddim[0] * griddim[1] * griddim[2] | ||
| if grid_size < 2 * smcount: | ||
| msg = ("Grid size ({grid}) < 2 * SM count ({sm}) " | ||
| "will likely result in GPU underutilization due" |
There was a problem hiding this comment.
| "will likely result in GPU underutilization due" | |
| "will likely result in GPU under utilization due" |
| ) | ||
|
|
||
| # Whether to warn about kernel launches where the grid size will | ||
| # underutilize the GPU due to low occupancy. On by default. |
There was a problem hiding this comment.
| # underutilize the GPU due to low occupancy. On by default. | |
| # under utilize the GPU due to low occupancy. On by default. |
| .. envvar:: NUMBA_CUDA_LOW_OCCUPANCY_WARNINGS | ||
|
|
||
| Enable warnings if the grid size is too small relative to the number of | ||
| steraming multiprocessors (SM). This option is on by default (1). |
There was a problem hiding this comment.
| steraming multiprocessors (SM). This option is on by default (1). | |
| streaming multiprocessors (SM). This option is on by default (default value is 1). |
| Enable warnings if the grid size is too small relative to the number of | ||
| steraming multiprocessors (SM). This option is on by default (1). | ||
|
|
||
| The heuristic checked is whether gridsize < 2 * (number of SMs). NOTE: The absence of |
There was a problem hiding this comment.
| The heuristic checked is whether gridsize < 2 * (number of SMs). NOTE: The absence of | |
| The heuristic checked is whether ``gridsize < 2 * (number of SMs)``. NOTE: The absence of |
| a warning does not imply a good gridsize relative to the number of SMs. Disabling | ||
| this warning will reduce the number of CUDA API calls, as the heuristic needs to | ||
| check the number of SMs for the device in the current context. |
There was a problem hiding this comment.
Does lead to a noticeable compilation speed gain?
There was a problem hiding this comment.
This is for launch speed rather than compilation speed. In the past there have been performance issues due to excessive getting of contexts, e.g.: #5189 - so I think if we add a get of the context in the launch pipeline, it's good to have a way around it as well.
Thanks for looking at these. I think they are all addressed, except this one (from the initial review):
|
@gmarkall I addressed this with the function 'test_efficient_guvectorize' in the file test_gufunc.py. |
|
Sorry, I forgot to respond earlier - I mean a test where you call a kernel like |
gmarkall
left a comment
There was a problem hiding this comment.
Thanks for the updated tests! I have a few comments regarding the kernel launch test - rather than annotating lines on the diff, I'll try and summarise here:
- The tests are quite complex compared to what's needed, resulting in the runtime being too long and the purpose of the tests being slightly obscured - the runtime on my machine is 40 seconds for
test_warnings.py. Since we're only trying to test whether the warning is generated for a low occupancy configuration, we don't need the kernel to do anything or create any arrays to pass into it. - I think that even if the test runtime is reduced a lot, the test still doesn't work in the CUDA simulator, because it doesn't generate performance warnings (and we shouldn't try to make it do so).
- The names of the tests are a bit vague about what is really tested -
inefficient_kernelimplies that the problem is inefficient code in the kernel, rather than the launch configuration. - A minor point: I try to not include
CUDAin the names of CUDA test classes, because it's redundant in the CUDA test suite - the main issue I have with it is having to type more when running individual classes from the command line :-)
I think the above can be resolved with the following implementation of the test class:
@skip_on_cudasim('cudasim does not raise performance warnings')
class TestWarnings(CUDATestCase):
def test_inefficient_launch_configuration(self):
@cuda.jit
def kernel():
pass
with override_config('CUDA_LOW_OCCUPANCY_WARNINGS', 1):
with warnings.catch_warnings(record=True) as w:
kernel[1, 1]()
self.assertEqual(w[0].category, NumbaPerformanceWarning)
self.assertIn('Grid size', str(w[0].message))
self.assertIn('2 * SM count', str(w[0].message))
def test_efficient_launch_configuration(self):
@cuda.jit
def kernel():
pass
with override_config('CUDA_LOW_OCCUPANCY_WARNINGS', 1):
with warnings.catch_warnings(record=True) as w:
kernel[256, 256]()
self.assertEqual(len(w), 0)There are also a couple of comments / questions on the diff to look at - one is related to the way setUp works, which turned out to be more complex than I first imagined it would be.
| def __init__(self, *args, **kwargs): | ||
| super().__init__(*args, **kwargs) | ||
| self._low_occupancy_warnings = config.CUDA_LOW_OCCUPANCY_WARNINGS |
There was a problem hiding this comment.
I first thought this __init__() method should be entirely superfluous, because the setUp() method sets self._low_occupancy_warnings, and is called before every test. However, when I removed it and ran the test suite, many tests errored in tearDown() due to self._low_occupancy_warnings not being set. This is because some CUDATestCase subclasses also define setUp(), which replaces the setUp() defined here. I think the right way round this is for the setUpClass() class method to ensure that for those classes, both the CUDATestCase.setUp() method and their setUp() method are called. The following can be used to ensure this:
| def __init__(self, *args, **kwargs): | |
| super().__init__(*args, **kwargs) | |
| self._low_occupancy_warnings = config.CUDA_LOW_OCCUPANCY_WARNINGS | |
| @classmethod | |
| def setUpClass(cls): | |
| # If the test class defines a setUp method, we need to run both | |
| # CUDATestCase.setUp (to disable low occupancy warnings) and its setUp | |
| if cls is not CUDATestCase and cls.setUp is not CUDATestCase.setUp: | |
| cls_setUp = cls.setUp | |
| def setUp_both(self, *args, **kwargs): | |
| # Call our setUp first so that low occupancy warnings are | |
| # disabled before anything else happens | |
| CUDATestCase.setUp(self) | |
| return cls_setUp(self, *args, **kwargs) | |
| cls.setUp = setUp_both |
There was a problem hiding this comment.
@gmarkall good catch on setUpClass. I don't think however, removing the __init __ method and it's implementation is correct. If you remove init then 'self._low_occupancy_warnings" will not be created as a instance variable.
There was a problem hiding this comment.
It will be created during the setUp() method - it doesn't need to be "declared" in __init__().
|
|
||
| with override_config('CUDA_LOW_OCCUPANCY_WARNINGS', 1): | ||
| with warnings.catch_warnings(record=True) as w: | ||
| warnings.simplefilter('always', NumbaPerformanceWarning) |
There was a problem hiding this comment.
What does this line do? (similar applies to all lines with warnings.simplefilter('always', NumbaPerformanceWarning) - I can't see that they have any effect)
| copy2d(A, out=B) | ||
| self.assertTrue(np.allclose(A, B)) | ||
|
|
||
| def test_inefficient_guvectorize(self): |
There was a problem hiding this comment.
(Note: this is resolved, but I can't resolve the comment because I don't have write access to this repo)
| a = np.random.rand(1024 * 1024 * 32).astype('float32') | ||
| b = np.random.rand(1024 * 1024 * 32).astype('float32') |
There was a problem hiding this comment.
Looking at the performance of the kernel test led me to also look into how long the guvectorize tests take - I think the data sizes for this and the test below are too large for unit tests, and they take about 5 seconds to run - can we reduce the size of the data please - e.g.:
| a = np.random.rand(1024 * 1024 * 32).astype('float32') | |
| b = np.random.rand(1024 * 1024 * 32).astype('float32') | |
| a = np.random.rand(1024 * 32).astype('float32') | |
| b = np.random.rand(1024 * 32).astype('float32') |
| a = np.random.rand(1024 * 1024 * 128).astype('float32').\ | ||
| reshape((1024 * 1024, 128)) | ||
| b = np.random.rand(1024 * 1024 * 128).astype('float32').\ | ||
| reshape((1024 * 1024, 128)) |
There was a problem hiding this comment.
Also to reduce the data size here please:
| a = np.random.rand(1024 * 1024 * 128).astype('float32').\ | |
| reshape((1024 * 1024, 128)) | |
| b = np.random.rand(1024 * 1024 * 128).astype('float32').\ | |
| reshape((1024 * 1024, 128)) | |
| a = np.random.rand(524288 * 2).astype('float32').reshape((524288, 2)) | |
| b = np.random.rand(524288 * 2).astype('float32').reshape((524288, 2)) |
The leading dimension of 524288 is needed to get a big enough grid size for a large GPU, so I reduced the trailing dimension down to 2 to keep the data size as small as possible.
|
@gmarkall should the tests in test_gufunc be skipped for the CUDA simulator (since no warnings are generated in the simulator), similar to the new ones in test_warning.py? |
gmarkall
left a comment
There was a problem hiding this comment.
Thanks for the updates - looks good to me!
@stuartarchibald Do you want another buildfarm run, or are you happy with this as-is?
|
... Actually - if a buildfarm run can be squeezed in just to be on the safe side, that would be good - is that possible? (@stuartarchibald / @esc) |
sklam
left a comment
There was a problem hiding this comment.
Tests are failing because of the following:
| Tests the DeviceRecord class with np.record host types | ||
| """ | ||
| def setUp(self): | ||
| CUDATestCase.setUp() |
There was a problem hiding this comment.
Missing self in the current spelling. My suggestion is to use super().setUp() instead.
There was a problem hiding this comment.
This will call TestCudaDeviceRecord.setUp(), which is what I was trying to avoid with the suggestion to use CUDATestCase.setUp() instead.
There was a problem hiding this comment.
In that case, it needs to be CUDATestCase.setUp(self)
There was a problem hiding this comment.
Example of errors:
======================================================================
ERROR: test_auto_device (numba.cuda.tests.cudadrv.test_cuda_devicerecord.TestCudaDeviceRecordWithRecord)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/siu/dev/numba/numba/cuda/tests/cudadrv/test_cuda_devicerecord.py", line 89, in setUp
CUDATestCase.setUp()
TypeError: setUp() missing 1 required positional argument: 'self'
======================================================================
ERROR: test_device_record_copy (numba.cuda.tests.cudadrv.test_cuda_devicerecord.TestCudaDeviceRecordWithRecord)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/siu/dev/numba/numba/cuda/tests/cudadrv/test_cuda_devicerecord.py", line 89, in setUp
CUDATestCase.setUp()
TypeError: setUp() missing 1 required positional argument: 'self'
======================================================================
ERROR: test_device_record_interface (numba.cuda.tests.cudadrv.test_cuda_devicerecord.TestCudaDeviceRecordWithRecord)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/siu/dev/numba/numba/cuda/tests/cudadrv/test_cuda_devicerecord.py", line 89, in setUp
CUDATestCase.setUp()
TypeError: setUp() missing 1 required positional argument: 'self'
======================================================================
ERROR: test_from_record_like (numba.cuda.tests.cudadrv.test_cuda_devicerecord.TestCudaDeviceRecordWithRecord)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/siu/dev/numba/numba/cuda/tests/cudadrv/test_cuda_devicerecord.py", line 89, in setUp
CUDATestCase.setUp()
TypeError: setUp() missing 1 required positional argument: 'self'
----------------------------------------------------------------------There was a problem hiding this comment.
Apologies for missing self in my earlier suggestion - I thought I tested my suggestions locally, but I must have done something not quite right!
stuartarchibald
left a comment
There was a problem hiding this comment.
Thanks for the patch and fixes @testhound.
Thanks for reviewing @gmarkall.
|
Buildfarm ID: |
Passed |
This pull request address: #6159. It addresses the issues by detecting when the blocksize for a kernel would be < 2 * Number of SM's. I also added a environment variable called NUMBA_CUDA_LOW_OCCUPANCY_WARNINGS that is enabled by default. I had to modify the the class CUDATestCase to disable the warning as many of the test cases trigger this warning.