I am on the Jetson Nano with CUDA 10. I have functions which iterates through pixels and modifies them to give me filtered versions of the image. I have a regular host version of the code which has the following functions:
It seems like this method really impacts the performance of the kernel. Using the CUDA Event API, I found that it was about 400 to 500 milliseconds slower. It runs around 700ms while not using function pointers takes about 200ms. (Note: I haven’t actually optimized the kernel yet. I am calling the kernel with <<<1, 1>>>. I wanted to get everything else in place before attempting to use the kernel effectively.). I tested this by just copying the function into my .cu code and calling this copied function in my kernel. From this post, it seems that nvcc does aggressive inlining which explains why function pointers are slow compared to explicit function calls. 500ms is pretty bad though. That post is 7 years old now so I was wondering if there’s anything I can do to optimize here that would narrow the performance gap. I really want to keep the function pointers so that I don’t have to have three nearly identical kernels while having their regular host counterparts.
If that’s not an option, is there some optimization for linked functions. My three filter functions are in a separate .h and .cu file which are made into an object file. Calling the function in this way offers a similar delay. Is there perhaps something in the compilation process that can be optimized. I compile my two .cu files with -dc, then I make a linker file with -dlink, and lastly I use g++ with those object files.
You could templatize the kernel using color as a template parameter, and then instantiate that three times, once per color. You could then invoke the entire kernel via a function pointer.
That’s not always the best strategy when doing a port of code to the GPU, which you are discovering. It’s a reasonable starting point, of course. But we must acknowledge (the facts here permit no other conclusion) that such a “mechanical” port starting point may result in performance issues, that may need to be addressed.
Yes, it certainly can. My guess in this case, based on the timing you’ve presented, is that your functions as tested are relatively simple. There is going to be a more-or-less fixed overhead for using your device-linked function-pointer method, and if the function you are calling is relatively trivial, and even worse you are calling that function many times over from the same kernel, you’re going to make the problem appear about as bad as it can get. On the other hand, if each of your functions that you are calling via device function pointers was itself using ~500 milliseconds per call, then I think the overhead would be negligible, in terms of overall percentage performance impact. But my guess is you have relatively trivial functions, and the percentage impact of the overhead is therefore large.
From a performance perspective, the best suggestion I can offer is to get those functions into the same compilation unit as your kernel, and eliminate the relocatable device code linking step. If you still want to have a “modicum of modularity” you can still define your color functions in separate files (wither .cu or .cuh, does not matter) and just#include those in the file that has the kernel function. (just means only include these files in other source files. Do not also add these files to the project or call the compiler on those files separately)
Not in CUDA 10.
The latest CUDA versions (e.g. CUDA 11.1) have some link-time-optimization features. The function pointer method might reduce the ability of the compiler to do link time optimization, however, so I would suggest it is something that needs to be tested to see if it provides any benefit for your specific use-case.
I suppose it may be worthwhile to point out that some additional inefficiency may arise in your approach when you proceed beyond a <<<1,1>>> kernel configuration. You haven’t posted valid C++ code (to wit: get pixel location;), but I imagine there is a possibility here that you may process colors RGBRGBRGB… etc. across threads. The function pointer approach here may also not play very will with that pattern across threads in a warp. But it’s not possible for me to say how much of a concern that may be without actual testing of code. The “cost” may already be evident/inherent in your measurement now.
Would a different kernel configuration cause issues with this sort of code? (This is also my first CUDA code that I’m doing so I’m not aware of these things yet.)
I guess I could just go with this way. But it sounds like I’ll still need three kernels, but each calls their respective functions? I guess it’s not a terrible tradeoff. Although my header file containing the three functions would still have __host__ __device__. So would I use the #ifdef __NVCC__ macro preceding the qualifier statement or is it some other macro?
The kernel in the above code is designed to run as a 2D grid-stride loop which means you can choose more-or-less arbitrary dimensions for the grid, including running on a single thread if you wish. When I run it on a single thread on my GTX960 the kernel takes ~188ms, pretty close to your ~200ms measurement report.
I see the Robert Crovella already posted example code. Since I had already prepared example code but didn’t get around to posting it until now, I’ll post it even though it is largely redundant. In this example, a templated kernel (__global__ function) operates on an array in one of three different ways. An array of three function pointers is initialized so each pointer points to one of the three possible instantiations of the kernel. The three operations are then applied in turn on an array of numerical data by iterating over these function pointers.
#include <cstdio>
#include <cstdlib>
#define N (5)
typedef void (*funcptr)(float *, int);
typedef enum {OP_SQRT, OP_SQR, OP_LOG, LAST} op_kind;
template<op_kind kind>
__global__ void apply_op (float* arr, int len)
{
int stride = gridDim.x * blockDim.x;
int tid = blockDim.x * blockIdx.x + threadIdx.x;
for (int i = tid; i < len; i += stride) {
if (kind == OP_SQRT) {
arr[i] = sqrtf (arr[i]);
} else if (kind == OP_SQR) {
arr[i] = arr[i] * arr[i];
} else if (kind == OP_LOG) {
arr[i] = logf (arr[i]);
}
}
}
int main (void)
{
float arr_host[N];
float *arr_dev = 0;
funcptr func[3] = {&apply_op<OP_SQRT>, &apply_op<OP_SQR>, &apply_op<OP_LOG>};
cudaMalloc ((void **)&arr_dev, sizeof(arr_dev[0])*N);
for (int i = 0; i < N; i++) {
arr_host[i] = i;
}
printf ("arr = ");
for (int j = 0; j < N; j++) {
printf ("% 15.8e ", arr_host[j]);
}
printf ("\n");
cudaMemcpy (arr_dev, arr_host, sizeof(arr_dev[0])*N, cudaMemcpyHostToDevice);
for (int i = OP_SQRT; i != LAST; i++) {
func[i]<<<1,1>>>(arr_dev, N);
cudaMemcpy (arr_host, arr_dev, sizeof(arr_host[0])*N, cudaMemcpyDeviceToHost);
printf ("arr = ");
for (int j = 0; j < N; j++) {
printf ("% 15.8e ", arr_host[j]);
}
printf ("\n");
}
cudaFree (arr_dev);
return EXIT_SUCCESS;
}
Thanks for the help. I’m getting the performance to be about 200ms now without having optimized the kernel.
One last question though. Isn’t the templatizing unnecessary? Since we’re using a switch statement, wouldn’t a parameter work just as well? Or is there some optimization in templatizing that I’m missing.
templating allows the switch statement to be removed at compile time. It ends up being something like a jump table, but the jump table can be eliminated at compile time. This may also allow the compiler to make other kinds of inline optimizations once it knows for the sure what the jump target will be and the code beyond it. Without that the compiler has to acknowledge that there are potentially (in this case) 3 different jump targets that have to be supported. How important that is depends on your code, but it’s not zero benefit.
By “parameter” I assume you mean compile-time visible parameter. AFAIK, if you used a compile-time visible parameter to guide the jump table, and you had only one instance of your kernel usage to account for, then the compiler could probably do a similar optimization. However the compiler, without the mechanism of templating, will not/cannot (AFAIK) generate 3 different instances of the same kernel. Therefore even if you had compile time visible parameters, if you intended to call the kernel in different situations with different parameters, in the same module, I know of know way for the compiler to avoid the run-time parameter check. Templating allows the compiler (in this case) to generate 3 different kernels, with 3 different entry points, each of which can be optimized for a particular path.
Also note that a compile-time visible parameter in host code is not necessarily going to have any effect on the device code compiler behavior. They are pretty much separate tools.
I am not a compiler engineer, so this is really on the edge of my knowledge. Others may wish to comment or know better. If it’s important, you have all the tools at your disposal to inspect compiler behavior, and draw your own conclusions.
So I was experimenting with your solution and hadn’t done all the grid and block stuff yet in the kernel. I finally got around to it and the code stopped working. I was getting an error from OpenCV, “CUDA driver version is insufficient for CUDA runtime version.” Based on other things on this forum that I have seen, this error doesn’t really make sense. Jetson Nano comes pre installed with CUDA 10 so I’m assuming everything with installation is fine there. I even reverted my code back to remove all the grid stuff and I still had the same error. I then thought maybe it’s a weird bug so I rebooted the Nano. Now I can’t seem to turn it on. I tried two monitors and the device starts up and sometimes I see some quick kernel messages. And then it gets stuck. I don’t know if the HDMI port broke or what, but the display sort of flashes like it’s recognized and then not recognized. This was also two different HDMI cables on two different monitors so I don’t think it’s the cable. Any thoughts on what happened?
Maybe sudo apt upgrade caused an issue. I was running that in the background and wasn’t paying attention to it. I didn’t bother checking if it was finished or not, but I would have figured a clean reboot from sudo reboot wouldn’t affect that really. Let me know if it would be better to ask this on the Jetson Nano forums.
EDIT:
The messages I saw one time were:
tegradc: tegradc.1: dpd enable lookup fail:-19
cp: not writing through dangling symlink 'etc/resolv.conf'
cgroup:cgroup2: unknown option "nsdelegate"
//i think the rest are normal
using random self ethernet address
using random self ethernet address
random: crng init done
random: 7 urandom warning(s) missed due to ratelimiting
using random self ethernet address
using random host ethernet address