Generic GPU Kernels
mikeinnes.github.io
mikeinnes.github.io
If you were a pro GPU programmer and had 10 years, Futhark would be maybe 10x slower. But just like we do not program in assembly when making critically fast software, most non-simple things are easier written in this.
How fast can Futhark be compared to a standard CUDA loop with a few arithmetic, load and save operations? Basically, suppose you're doing simple gathering and scattering?
@arthas, the author, at some point made comparisons against implementations and it was a most twice as slow, but often faster.
One of Julia's benefits is that the compiler is hackable so high level abstractions can be experimented with in user space.
like here: https://www.youtube.com/watch?v=x_I6Bu7IDJo ?
Even 10 years ago, back in OpenCL 1.0 days or just as CUDA was getting started, you'd need to use __shared__ memory to really benefit from GPU coding.
Over the past 10 years, GPU programming has become even more detailed: permute / bpermute, warp/wavefront-level voting (vote / ballot operations), warp/wavefront-level coordination (__activemask()), and of course most recently: BFloat16 matrix multiplication / sparse matrix multiplication.
---------
There's some degree of abstraction building going on inside the CUDA-world. You can use cooperative groups to abstract a lot of the wavefront-level stuff away, but not entirely yet. As such, even if you use cooperative groups, you end up needing a mental model of why these operations are efficient before you really benefit.
CUDA cub abstracts a lot of operations away, removing the tedious exercise of making your own prefix sum / scan operation whenever you dip down to this level... but CUDA cub still requires the programmer to learn these tricks before cub is useful.
---------
What we really want, is for a programming interface where "CPU" programmers can add custom code to GPU kernels that adds flexibility and loses very little speed... but doesn't require CPU programmers to spend lots of time learning the GPU-programming tricks of their own.
And such an interface doesn't exist yet in any form. Outside of like... Tensorflow (except that only works for neural net programming, and not for general purpose programming).
-------
The speed thing is very important. C++ AMP, by Microsoft, was a pretty decent interface by 2012 standards (competitive with CUDA and OpenCL at the time), but C++ AMP was criticized as much slower than OpenCL/CUDA, so the programming community stopped using C++ AMP.
If you're writing GPU code that's slower than OpenCL / CUDA, then people will simply ask : why aren't you using OpenCL/CUDA ???
Its hard to sell "programmer convenience" and "simpler thinking process" to your boss.
Closest I know of is https://halide-lang.org/ And, that is specialized around images.
C++ AMP in particular was a generic interface behind a template<> class: Array<> and ArrayView<>.
Array<> always existed on GPU-side (be it GPU#0 or GPU#1), while ArrayView<> was your abstraction to read/write into that Array<>.
Array<SomeClass> foo = createSomeArray();
ArrayView<SomeClass> bar = foo;
This could be CPU-side or GPU side code (foo could be on GPU#0, while bar could be GPU#1). While you can use C++ AMP code to manually manage when the copies happen, the idea is to simplify the logic down so that CPU-programmers didn't have to think as hard.In this case, "bar" is bound to foo, and bar will remain in sync with foo through async programming behind the scenes. bar[25] = SomeClass(baz); will change bar in this context, but the Array<> foo will only update at the designated synchronization points (similar to CUDA Streams).
------
So its a lot easier than reading/writing cudaMemcpy() in the right spots, in the right order, in the right CUDA Streams. But probably less efficient, and therefore going to be less popular than using CUDA directly.
The current ambition of NV is trying to make it as painless as possible, by relying on shared virtual memory much more than before.
https://www.olcf.ornl.gov/wp-content/uploads/2021/06/OLCF_Us...
With OpenMP (also supported in AMD’s ROCm AOMP) and C++17 standard parallelism support. Legate w/ cuNumeric goes to another direction with implementing a numpy-compatible interface, but GPU accelerated.
Also deriving from the limits of a SIMD abstraction with 1 IP per thread for the sake of guaranteed forward progress (as such changing the programming model needed for the code to be functional).
The underlying machine is implemented as SIMT, so people should still be aware of the associated performance pitfalls. However this allows for better compatibility and more code sharing possible than before.
Microsoft’s problem with C++ AMP is that FXIL as present in D3D11 was just too limited and had too many performance pitfalls I’m afraid, if only it got to see the light of day with D3D12 + DXIL later…
Shared-virtual-memory is an OpenCL term. I think the NVidia term is "Unified Memory", but yes, I get what you're trying to say.
I think that makes the mistake too far in the simplicity direction. Its over-simplified.
Like, think about the following code:
DataStructure data = foo(); // cudaMallocManaged() / Shared-virtual Memory
bar(data);
baz(data);
In a shared-virtual memory situation, you'd probably have data sitting on CPU-side, and then SVM over the data around.With a little bit more code, you can have...
Array<DataStructure> data = foo();
ArrayView<DataStructure> dataview = data;
bar(data);
baz(data);
In this case, the data exists on GPU-side (explicitly: all Array<> in C++Amp are GPU-allocated). If foo, bar and baz are all GPU-functions, then the data never moves (no cudaMemcpy involved).If bar is a CPU-function, you get the copy back-and-forth for correctness. But that still leaves room for you to convert bar from CPU-code into GPU-code without affecting this section of code... giving you the ability to smoothly improve performance as your project matures.
-------
CUDA / OpenCL default memory is too granular.
Datastructure* data = cudaMalloc(...); // GPU allocation
Datastructure* localData = malloc(...);
cudaMemcpy(localData, data, stream-stuff, etc. etc.)
// ugggh... too much. Lots of steps to do anything, thinking about streams and async and signals.
-------Finding the right compromise between performance and simplicity is key. CUDA/OpenCL have both something that's too simple(SVM / Unified Memory), and too complicated (cudaMalloc + cudaMemcpy). The answer IMO lies in-between somehow.
It’s sadly the only choice around for maximum code reuse… with then manually optimising the parts that need to be later through profiling, as developer time is quite limited.
On systems where the CPU and GPU are coherent such SoCs w/ CPU and GPU on the same die, Infinity Fabric-linked GPUs or NVlink linked ones (Power9/Grace), that option is also quite attractive.
The memory manager measures the frequency of access from the CPU and GPU to (try to) locate where a buffer should be stored or moved dynamically.
However, on PCIe linked GPUs, reliance on heuristics is more necessary to reach good perf levels.
You can also ask to have your memory allocation migrated to the GPU on-demand with cudaMemPrefetchAsync (finally implemented in ROCm 4.5 too, which has unified memory on AMD cards) before doing a kernel launch for the data that you’ll use.
(See https://on-demand.gputechconf.com/gtc/2017/presentation/s728... for the changes that happened in Volta on that front)
The goal is to make the use of regular cudaMalloc/hipMalloc not necessary for good performance in a wide variety of scenarios. The explicit storage location APIs will stay available for when more optimisation is necessary.
communications operations were explicit through the use of indirection on these indices. I doubt its the case on a modern gpu, but aside from the router, performance on the CM was deterministic so you really could understand what you were going to get from the source.
completely agree with you, general parallel programming languages are a thing and really pretty valuable to try to exploit all that machinery. Chapel is the only recent work I'm familiar with.
I'm pretty sure you are talking about Lisp* and C*, which were the "star" languages of the connection machine.
And GPUs are structured to send a whole lot of memory to many processors and I think that doing this efficiently is an inherently harder problem than sending memory to a few complex processors through a pipeline that's complex, yes, but designed to make things feel easy.
Indeed cool to program julia directly on the GPU and Julia on GPU and this has further evolved since then, see https://juliagpu.org/