Hacker News
points by proc0 3 days ago | 0 comments
Nice breakdown. I've been wondering why it's so complicated to program on the GPU compared to the CPU. Is it a language paradigm issue or maybe a hardware design limitation? I always think of this when looking at the graphic APIs, espeically Vulkan.
[–] modeless 3 days ago
It's mostly hardware design limitations. GPU hardware is way more diverse than CPU hardware, so you need to distribute shaders as either source code or a portable intermediate representation, in both cases compiled at runtime for the user's actual GPU. There are also many fixed function parts of the graphics pipeline with wildly different behavior on different hardware which you need to abstract over. And it all runs in parallel with the CPU, often with a separate pool of RAM, so you need implicit or explicit synchronization and data transfer which adds a lot of complexity especially in error cases.

If you really want to see why all the cruft is there, and how much you could remove if you dropped support for older hardware, look no further than Sebastian Aaltonen's "No Graphics API" blog post (which he actually implemented too): https://www.sebastianaaltonen.com/blog/no-graphics-api/

[–] flohofwoe 3 days ago
I think most of it comes down to 'historical accidents' tbh.

E.g. if CPUs would have closed ISAs and heavily guarded IPs, you'd also need a 'driver' and an 'OpenCPU API' to make any use of them (of course the public ISA of a CPU is essentially the driver interface baked into the hardware).

The other big difference is focus on decent single-thread performance for CPUs versus performing thousands of small tasks in parallel on GPUs, but each one slowly. This difference causes a different programming model (e.g. running an entire application in a single 'main' function from start to finish, versus spawning small specialized tasks which are running across hundreds of threads).

And the specific problem with Vulkan is that it tried to solve two conflicting requirements: on one hand being an explicit low-level API without 'magic abstractions', on the other hand supporting very different GPU architectures (especially the mobile GPU zoo). The resulting mess was quite predictable to be honest...

[–] david-gpu 2 days ago
You underestimate the effect of two factors.

First, GPUs aren't simply "highly parallel CPUs", they also contain a whole lot of fixed function hardware that is specialized for real-time computer graphics. This hardware isn't exposed as "fancy instructions", it is set up in other ways, which is achieved through a device driver.

Second: GPUs are coprocessor, separate from the CPU where another OS is running. This also necessitates a device driver to manage scheduling, synchronization, etc. with the main OS. Because, in case you were not aware, the GPU itself runs its own little operating system, although it is rarely called that.

[–] flohofwoe 2 days ago
> they also contain a whole lot of fixed function hardware that is specialized for real-time computer graphics. This hardware isn't exposed as "fancy instructions", it is set up in other ways, which is achieved through a device driver.

Those hardwired fixed-function parts are getting fewer and less important though (ok, and then raytracing units entered the scene...). Also AFAIK something like a texture sampling operation is indeed a 'fancy instruction'(?), even though the actual texture sampling is done by a 'fixed function' hardware unit.

> This also necessitates a device driver to manage scheduling, synchronization, etc. with the main OS.

I bet this type of GPU/CPU handshaking work could also be baked into the hardware (similar to CPU mirocode) and exposed via a standardized memory mapped IO interface (or whatever the current mechanism is for letting the CPU control the GPU). E.g. a CPU also implements similar protocols to talk to the rest of the computer system (e.g. memory and other CPUs), but this is mainly implemented as "hardware protocols" - drivers "just" offer more flexibility for hardware vendors because they can delegate the standardization into software.

...also a lot of the "CPU / GPU software ecosystem" was probably shaped by CPU and GPU vendors historically being different companies, and once the big consolidation started, the conventions were already set in stone.

[–] david-gpu 2 days ago
> Those hardwired fixed-function parts are getting fewer and less important though

There is more machinery internally than what the user can see. Even "low level" APIs hide quite a lot of the internal complexity.

> Also AFAIK something like a texture sampling operation is indeed a 'fancy instruction'(?)

Yes, and no. The "I want to sample this texture" part looks like a fancy instruction to the shader processor. But there is a fair amount of setup that needs to be done before and after, and that is the responsibility of the device driver. Stuff like: where is the data, how is it arranged in memory, how it is to be sampled, etc.

As you point out, the texture units are coprocessors inside the coprocessor that is the GPU itself. It is turtles all the way down. Think of a GPU as a whole network of processors inside a chip, some of which are hidden from the application programmer.

> I bet this type of GPU/CPU handshaking work could also be baked into the hardware (similar to CPU mirocode) and exposed via a standardized memory mapped IO interface (or whatever the current mechanism is for letting the CPU control the GPU)

Attempts have been made. Last time I checked, they had not been very successful, but I have not kept up with the latest developments after retirement.

The way this normally works is that there is a circular command buffer where the CPU inserts commands and the GPU consumes them. It works; there is nothing wrong with it. It is the obvious way to handle asynchronous coprocessors.

> E.g. a CPU also implements similar protocols to talk to the rest of the computer system (e.g. memory and other CPUs), but this is mainly implemented as "hardware protocols" - drivers "just" offer more flexibility for hardware vendors because they can delegate the standardization into software.

I suspect you underestimate the amount of setup that is necessary to drive a GPU. And the amount of asynchronous resource management involved.

Let me put it this other way: the people doing this for a living for decades know what they are doing. There have been attempts to try to make GPUs that behave more like CPUs in terms of how they are driven, and they have not been very successful so far, to my knowledge. AMD acquired ATI back in 2006, so it is not like there hasn't been enough time for the concept to cook in the oven. See also Intel's Larrabee.

[–] flohofwoe 2 days ago
Very interesting. Thanks for the insights! ...and yeah, Larrabee is an interesting case, I actually wonder whether it's time for another attempt :)
[–] gryn 3 days ago
part of the reason is historical cruft & history path dependency. GPUs didn't start as general computing device for parallelism but a device for the specific purpose of accelerating graphics with very specific fixed functions for that job and only that. with time that fixed pipeline became more complex and more configurable, not fixed/more flexible and hacks were in parallel used to make use of it for tasks outside graphics those hacks were later made into something more formal (CUDA/OpenCL).

CUDA being the winner (closed source, not really usable on non-nvidia GPUs) between those 2 and nvidia not wanting to open their competitive advantage to the competition for the niche of non gamings uses / scientific computing (and later AI, which became a huge win for nvidia).

vulkan came around after as this open thing (based on a previous attempt by AMD called mantle) that's trying to break down the leaky abstractions and the divide between language for graphics & video games and separate language/frameworks for gpu non-graphic computing with the promise of giving more control and more room for squeezing performance from the hardware at the cost of more complexity with the idea that you'll probably build your own abstractions on top of it for your subset of applications.

If GPU were from the get go designed as a parallelism tool first and video game tools second the hardware architecture and the language would probably be different than today and simpler (and if the competition between the duopoly didn't sabotage things)

don't forget that the drivers for GPUs would until recently get updates to patch specific video games for performance issues (not sure if they still do that ?)

[–] kuschku 2 days ago
> If GPU were from the get go designed as a parallelism tool first and video game tools second the hardware architecture and the language would probably be different than today and simpler (and if the competition between the duopoly didn't sabotage things)

Sony tried that with the PS3's Cell processor, just a wise parallel vector processor instead of dedicated hardware.

If you knew how to use it, you could get great results, which is how Naughty Dog's games became so spectacular. But if you just tried to port an existing game, things went downhill quickly, which is why GabeN talked on stage how much he hated the PS3.

Oh and this complexity led to lots of games just switching to one of the existing engines that supported it natively, and started the modern dominance of Unreal Engine. But it's also why engines introduced more parallel architectures, which a few years later became very useful when quad-core processors in PCs became mainstream.

[–] locknitpicker 3 days ago
> If GPU were from the get go designed as a parallelism tool first and video game tools second the hardware architecture and the language would probably be different than today and simpler (...)

The market demand was for video game tools, though.

People didn't wanted faster compute, they wanted their computer games to work smoothly on the hardware they already own. Therefore, they preferred to spend 100-200€ on a point upgrade, much like RAM, than scrapping their 2k€ PC.

To give people a reason to buy these upgrades, games had to use them. Therefore video cards were designed specially for the usecases and development flow that game developers already followed.

If it wasn't for the video game market, NVidia wouldn't exist as we know it today.

[–] flohofwoe 2 days ago
> don't forget that the drivers for GPUs would until recently get updates to patch specific video games for performance issues (not sure if they still do that ?)

I bet it's all the same if not worse, and that's why GPU drivers are closing in on the gigabyte "sound barrier" (and I guess on Linux that part is played by Proton... e.g. I bet there's a ton of game-specific 'workarounds' in Proton, if not for performance, then at least to fix 3D API misuse).

[–] proc0 3 days ago
Good summary, thanks. Sounds like it's all still pretty early, even for graphics but much more for parallel compute, which seems the direction the hardware is going in. I'm sure there is a CUDA equivalent for CPU but I would guess it's all the way back in the 70s or earlier. Hopefully open standards gets some traction.
[–] pdpi 2 days ago
One thing that helped me understand it was to reframe it as a network application, where you have a client running on the CPU and a server running on the GPU (the shaders).

A bunch of the complexity comes from having to write both client and server, and the communication protocol between them, then some more comes from the client being responsible for spinning up the server. As with every network service, client/server traffic is the slowest part of the architecture, so a bunch of complexity then comes from having to minimise the amount of network calls (or draw calls, in this case).

Then you hit the part where all of this is performance critical (soft-realtime requirements), so the whole thing is geared towards “writing your own userspace IP stack in C++” rather than “writing a flask application in Python”.

[–] socalgal2 3 days ago
The thing that makes it "hard" is that it's all indirect. You compile a program for the GPU (a shader). Then you have to setup data (textures and buffers) for it. Then, you need to spend a bunch of time telling the GPU how to use that data, offsets where each piece of data is, what formats the data is in,, how to advance to the next piece of data, etc.. etc..

There are a few libraries that try to abstract over all this indirection but all the graphics APIs themselves are lower-level so you're left to do all of that yourself.

https://docs.swmansion.com/TypeGPU/

https://github.com/google/toucan

[–] WithinReason 2 days ago
If you care about every last bit of performance, CPUs are difficult to program for as well, possibly more difficult than a GPU.

Generally GPUs punish you more by bad performance if you don't optimise your code.

[–] avaer 3 days ago
It's not more complicated, you have things like CUDA which is basically C/C++ that compiles to GPU.

But GPUs have thousands of cores, which means if you want any benefit, the smallest data type you must use is something like array[n * 1024], or your GPU is wasted doing nothing. So you must shape your programs into that memory model or you're just wasting your time (and GPU).

That's >50% of the problem.

[–] proc0 3 days ago
Right, that makes sense. There's a ton of overhead in the setup though, I'm assuming that's because everything runs on the CPU by default. Is this then an OS design choice? Thanks for your input.
[–] avaer 3 days ago
There can be overhead, though GPU memory + drivers are some of the most optimized software in the world.

Some OS/hardware have separate GPU memory (Nvidia), some have unified memory (Macos, modern consoles), but moving memory is usually not the problem.

Things don't "run on CPU by default", it's up to the programmer where to run it. But the CPU and GPU paradigm of "running a program" are very different, since on GPU you're running SIMD (i.e. running your code on arrays) or else there is no point to use a GPU.

[–] tvbusy 3 days ago
For me, if-branching is the hardest to wrap my head around. "If"s are so natural to CPU programming that it's second nature to any developer but on GPU, it's expensive.
[–] __del__ 3 days ago
instead of iterating over an array and if-branching to one of two paths for each item, conceive of your conditionals as logic operations which test for set membership (i.e. "is this array item a member of the set i care about?") and with this array mask of "do the thing or don't do the thing" flags, you can mutate your data 0 or 1 times.

the key for me is thinking in terms of sets—much of your conditional logic is probably about sorting things into sets anyway.

[–] dahart 2 days ago
Vulkan, OpenGL and DirectX make GPU programming more difficult than it has to be for multiple reasons, some of which other people here have mentioned. They are also low level, focused on high performance, and are primarily designed and written for game engines. Making them easy to learn for newcomers is pretty low on the priority list. ShaderToy makes it easy by taking care of everything except the GLSL, you might like it. You can also try CUDA, which is generally a lot easier than Vulkan. For a really easy/fun time with CUDA, try Warp - it’s CUDA in Python.
[–] johnnyanmac 3 days ago
It's partially historical; I'm sure if we redesigned GPGPU today from the ground up, we'd have an entirely different pipeline. And partially due to hardware vendor lock-in; you can't really design an alternative to Vulkan/d3d12 even if you had the skill and drive to do so.

But also partly in the fact that the GPU is specialized for certain operations over a CPU. It wants and need to obtain data in a different way from a CPU to maximize throughput. So language constructs are designed around making sure developers stay on this fast path. The complexity can be seen as some intentional gatekeeping to signal that this kind of programming is only for specific kinds of problems.

[–] Pannoniae 2 days ago
actually yes you could design an alternative API and even implement it....

there's HW references + the linux drivers for AMD (both UMD and KMD), there's refs for NV and their open linux KMD (no UMD though), intel also has HW refs and open drivers for linux

I think it would be possible to put together as long as you don't mind only working for certain hardware, like a prototype you know

I might give it a shot to make a proof-of-concept "high level low level" graphics API ngl if no one else feels like so...

I think there's space for a "two-level" design like have the driver consist of the HW-specific parts, expose things in terms of GPU VAs, explicit sync, explicit memory management, command buffers etc. then have a high-level wrapper over it to provide vaguely GL-ish semantics, and you could intermix the two APIs, obviously you wouldn't be able to get the VA for a high-level object because that'd break the autotracking, if you use low-level you're responsible for syncing, cycling etc.