Bandwidth increases nearly linearly with adding dimms until you get to 1 dimm per memory channel. After that there's very small improvements related to more open pages. More open pages doesn't help with pure bandwidth benchmarks though.
Typically there are NUMA-aware memory allocators. I don't believe glibc is but mimalloc and the non gperftools tcmalloc should be. The Linux kernel is NUMA-aware and will try to avoid shifting work to a different NUMA node but it may depending on what else is happening - it's a generic algorithm trying to balance CPU utilization, latency, and memory bandwidth. If you're using a higher level language like Go or Java then there may be more involved. Java has a NUMA aware allocator while Go's design requires it to also have a NUMA aware scheduler which I don't believe it does.
You could go extreme and start pinning your threads to specific CPUs to tune the code manually instead of relying on the kernel if you know memory bandwidth is extremely important and you won't have much CPU contention to worry about in terms of getting work scheduled in a timely manner.
All that being said, you typically also need to design your application from the ground up to be NUMA aware to take full advantage so that you can set up your allocations to happen on the right zone & whatnot.
I expected a bit more, my guess would have been around 120 GB/s. I've been playing with LLMs and this hardware is about as fast as memory gets on consumer Intel without overclocking.
>You can double this amount of bandwidth with NUMA-aware code, but it will require further engineering.
So is this benchmark really useful? Seems like it's engineered enough that it probably isn't relevant for most use cases (like running java, go or python services), and not engineered enough so it's not relevant for performant systems like databases. (That said NUMA simply isn't that hard, even with Java which is also NUMA aware).
Yes, you always have to know your problem well enough to make sure that you pick a benchmark which is measuring your bottleneck. Most people don’t have this kind of perfect linear read activity but some do, and I think that if I did I’d probably be playing with the thread counts to see whether high-core machines were bottlenecking at some point where diminishing returns kicked in making it more cost-effective to get a second system rather than one really big one.
I changed data_volume to 8GB and ran on my 4 year old laptop (Amd Ryzen 4800H + 2 x 8GB 3200MHz DDR4 running NixOS with Linux 6.1.71)
I get around 36 GB/s
Either because it is a laptop CPU or (less likely) because it is an older Zen 2 might prevent it to reach the DRAM throughput.
Running the test on a desktop Zen 3 (5900X) with slower ECC DDR4-2666, i.e. with a maximum throughput of 42.7 GB/s, provides for 2 or more threads a throughput around 39 GB/s, with a maximum of 39.5 GB/s at 4 threads.
4 x 32GB for now. I need to investigate manual OC as EXPO doesn't work with all 4 slots populated. Another option is to try the 192GB 4x48GB Corsair kits at 5200.
7950x with 4x32GB @ 3800MHz here. Getting similar results except my single thread performance is the worst (around 40), otherwise it looks almost identical. I assume my poor single thread perf is due to having capped max CPU core clock at 4.5Ghz. Getting 4 sticks to boot was a pain, haven't really bothered to change anything once I finally got it running stable.
Not many codes are bandwidth bound, but you can't say without knowing the use case.
Another factor is mac's have many more memory channels on the m1/2/3 pro and m1/2/3 max. So they can have many more memory transactions, where on most PCs and x86-64 laptops you get 2 cache misses per memory latency (on the order of 70ns), where macs will get many more. So under various workloads all the cores spend much less time waiting on memory latency.
Interesting stuff, although I'll admit when I read the headline I took it to mean the literal `large` instance types, which are actually quite small as AWS lingo goes. Whereas the author means actual LARGE instances, ie, an entire box with no hypervisor layer.
Note that you can get a significant speedup by using "restrict" on the pointer in the "sum()" function to tell the compiler that there is no mutation or aliasing happening during the call's lifetime.
On Godbolt, with a size of 100MB (due to limits), this improves the speed using LLVM 17 (https://godbolt.org/z/6dW1h8aev) from:
That __restrict isn't useful to the compiler in this function, since it can already infer that there is no mutation from the access patterns: it's a leaf function with only non-atomic reads and no writes. (It's mostly only useful when you interleave reading from one pointer with writing to another with the same type, or when you call an opaque function before reading from a pointer.) Indeed, if you look at the generated assembly, the two are identical. The timing differences are likely random variation.
> You can double this amount of bandwidth with NUMA-aware code, but it will require further engineering.
If you have non uniform memory, you may be shooting yourself in the foot. Author used a dual socket system. If you are not careful, and your code is not NUMA aware, you may end up with less memory bandwidth for your application, than a single-socket would have. Can be avoided by prefixing `numactl --cpubind=0` to your command
Increasing the data volume for CCX43 to 48GB increases the bandwidth using 16 cores to 75.1. Memory bandwidth seems to scale pretty well with the number of cores on ARM. Interesting that the bandwidth for the shared AMD system is that much higher than the dedicated system
49 comments
[ 407 ms ] story [ 1720 ms ] threadAnother 1.5 times greater speed comes from DDR5-4800 vs. DDR4-3200.
The rest may be from virtualization and other overheads.
The r6i.metal instance says up to 50 Gbps on Network bandwidth: https://aws.amazon.com/ec2/instance-types/r6i/
You could go extreme and start pinning your threads to specific CPUs to tune the code manually instead of relying on the kernel if you know memory bandwidth is extremely important and you won't have much CPU contention to worry about in terms of getting work scheduled in a timely manner.
All that being said, you typically also need to design your application from the ground up to be NUMA aware to take full advantage so that you can set up your allocations to happen on the right zone & whatnot.
Screenshot showing usage: https://i.imgur.com/okdqgG9.png
So is this benchmark really useful? Seems like it's engineered enough that it probably isn't relevant for most use cases (like running java, go or python services), and not engineered enough so it's not relevant for performant systems like databases. (That said NUMA simply isn't that hard, even with Java which is also NUMA aware).
Running the test on a desktop Zen 3 (5900X) with slower ECC DDR4-2666, i.e. with a maximum throughput of 42.7 GB/s, provides for 2 or more threads a throughput around 39 GB/s, with a maximum of 39.5 GB/s at 4 threads.
$ ./a.out
Another factor is mac's have many more memory channels on the m1/2/3 pro and m1/2/3 max. So they can have many more memory transactions, where on most PCs and x86-64 laptops you get 2 cache misses per memory latency (on the order of 70ns), where macs will get many more. So under various workloads all the cores spend much less time waiting on memory latency.
On Godbolt, with a size of 100MB (due to limits), this improves the speed using LLVM 17 (https://godbolt.org/z/6dW1h8aev) from:
To (https://godbolt.org/z/sh3489Mxv)Running on an 2xAMD EPYC 7763 64-Core Processor, with 256GB of ram and 256 threads (EuroHPC LUMI standard CPU node): ~38.
Running on my Macbook Pro M1 16GB on battery power, but with 8GB of data to not swap as I have other apps running: ~59.
On S23 Ultra (Snapdragon 8 Gen 2+, 12GB LPDDR5X @4200MHz)
On a NanoPi-R6C (RockChip RK3588s @2.4Ghz + 8GB LPDDR4X)If you have non uniform memory, you may be shooting yourself in the foot. Author used a dual socket system. If you are not careful, and your code is not NUMA aware, you may end up with less memory bandwidth for your application, than a single-socket would have. Can be avoided by prefixing `numactl --cpubind=0` to your command
16-core ARM CAX41
root@ubuntu-32gb-fsn1-1:~# ./a.out
1 17.8
2 35.6
3 49.9
4 64.9
5 79.0
6 92.1
7 101.7
8 110.2
9 117.6
10 124.0
11 130.4
12 136.2
13 140.9
14 143.8
15 149.2
16 152.9
16 Core AMD shared CPX51
root@ubuntu-32gb-fsn1-1:~# ./a.out
1 18.9
2 37.5
3 54.3
4 65.4
5 77.2
6 92.8
7 100.8
8 92.1
9 95.1
10 105.7
11 93.4
12 100.9
13 89.9
14 97.0
15 99.1
16 107.8
16 core AMD dedicated CCX43
root@ubuntu-64gb-fsn1-1:~# ./a.out
1 36.3
2 71.6
3 56.8
4 50.8
5 63.4
6 57.2
7 53.4
8 50.9
9 55.2
10 61.3
11 64.4
12 65.3
13 66.5
14 68.8
15 69.2
16 64.4
Increasing the data volume for CCX43 to 48GB increases the bandwidth using 16 cores to 75.1. Memory bandwidth seems to scale pretty well with the number of cores on ARM. Interesting that the bandwidth for the shared AMD system is that much higher than the dedicated system