|
|
Log in / Subscribe / Register

NUMA-aware qspinlocks

By Jonathan Corbet
April 12, 2021
While some parts of the core kernel reached a relatively stable "done" state years ago, others never really seem to be finished. One of the latter variety is undoubtedly the kernel's implementation of spinlocks, which arbitrate access to data at the lowest levels of the kernel. Lock performance can have a significant effect on the performance of the system as a whole, so optimization work can pay back big dividends. Lest one think that this work is finally done, the NUMA-aware qspinlock patch set shows how some more performance can be squeezed out of the kernel's spinlock implementation.

In its simplest form, a spinlock is a single word in memory, initially set to one. Any CPU wishing to acquire the lock will perform an atomic decrement-and-test operation; if the result is zero, the lock has been successfully taken. Otherwise the CPU will increment the value, then "spin" in tight loop until the operation succeeds. The kernel has long since left this sort of implementation behind, though, for a number of reasons, including performance. All those atomic operations on the lock word cause its cache line to be bounced around the system, slowing things considerably even if contention for the lock is light.

The current "qspinlock" implementation is based on MCS locks, which implement a queue of CPUs waiting for the lock as a simple linked list. Normally, linked lists are just the sort of data structure that one wants to avoid when cache efficiency is a concern, but nobody ever has to traverse this list. Instead, each CPU will spin on its own entry in the list, and only reach into the next entry to release the lock. See this article for a more complete description, complete with cheesy diagrams, of how MCS locks work.

MCS locks on NUMA systems

MCS locks seem nearly optimal; each CPU focuses on its own queue entry, so cache-line bouncing between processors is nearly eliminated. They are also fair; the queue of waiters ensures that no CPU is starved of access. But it seems that there is a way to do better, at least on non-uniform memory-access (NUMA) systems. Such machines are made up of multiple nodes, each of which contains some number of CPUs; memory attached to a CPU's node will be faster to access than memory attached to a remote node. Access to cached memory is (relatively) fast, of course, regardless of the node that memory is attached to, but moving cache lines between nodes is expensive, even more expensive than bouncing cache lines between CPUs on the same node. Thus, minimizing cache-line movement between NUMA nodes will be good for performance.

If a spinlock is released by a CPU on one node and subsequently acquired by a CPU on a different node, its cache line will have to move between the nodes. If, instead, a contended spinlock could be passed to another CPU on the same node, that expense will be avoided. That alone can make a difference, but it's worth remembering that spinlocks protect data structures. Two processors contending for a given lock are quite likely to be trying to access the same data, so moving the lock between nodes will also cause the cache lines for the protected data to move. For heavily contended data structures, the resulting slowdown can hurt.

The NUMA-aware qspinlock attempts to keep locks from bouncing between NUMA nodes by handing them off to another CPU on the same node whenever possible. To do this, the queue of CPUs waiting for the lock is split into two — a primary and secondary queue. If a CPU finds the lock unavailable, it will add itself to the primary queue and wait as usual. When a CPU gets to the head of the queue, though, it will look at the next CPU in line; if that next CPU is on a different NUMA node, it will be shunted over to the secondary queue.

In this way, the waiting CPUs will eventually be sorted into two queues, one of which (the primary queue) consists only of CPUs on the same node as the current owner of the lock, and one (the secondary) which contains all the rest. When a CPU releases the lock it will hand it to the next CPU in the primary queue, thus keeping the lock on the same NUMA node. The lock will only move to another node once the primary queue empties out, at which point the secondary queue will be moved to the primary and the process starts all over again.

Tweaks and benchmarks

There is an obvious pitfall with a scheme like this: if the lock is heavily contended, the primary queue may never empty out and the other nodes in the system will be starved for the lock. The solution to this problem is to make a note of the time when the first CPU was moved to the secondary queue. If the primary queue does not empty out for 10ms (by default), the entire secondary queue will be promoted to the head of the primary queue, thus forcing the lock to move to another node. The timeout can be changed (within a range of 1-100ms) with the numa_spinlock_threshold command-line parameter.

One optimization that has been added is called "shuffle reduction". If the lock is not all that heavily contended, the extra work of maintaining the secondary queue does not really buy anything. To mitigate this extra cost, the code uses a pseudo-random number generator to only try to create the secondary queue one time out of every 128 lock acquisitions. If the lock gets busy, that will happen relatively often, after which the secondary queue will be maintained until the primary queue empties again (or the above-mentioned timeout occurs).

Finally, the code exempts CPUs running in interrupt (or non-maskable interrupt) mode, and those running realtime tasks, from being pushed to the secondary queue. That allows these CPUs, which presumably have a higher priority, to acquire the lock relatively quickly even if they are running on the wrong NUMA node.

A number of benchmark results are included with the patch set. For lightly contended locks the performance benefits of NUMA awareness are relatively modest. As the number of contending threads grows, though, the speedup does as well, approaching a factor of two for ridiculously heavily contended loads.

This patch set has been through 14 revisions since it was first posted in January 2019. It has evolved quite a bit over that time as comments were raised and addressed; it would appear to be approaching a sort of steady state where it is getting close to being ready to merge. Given that this work has been pending for over two years already, though, and given that it makes significant changes to one of the kernel's fundamental synchronization primitives, it would not be surprising if it took a little longer yet before it hits the mainline.

This paper describes the NUMA-aware qspinlock algorithm in more detail, though the details of the implementation have diverged somewhat from what is described there.

Index entries for this article
KernelNUMA
KernelSpinlocks


to post comments

NUMA-aware qspinlocks

Posted Apr 12, 2021 19:38 UTC (Mon) by edeloget (subscriber, #88392) [Link]

I find it amazing that we still find new ways to implements something which is "as simple as" a spinlock (quoted because, well, spinlocks *looks* like simple primitives ; given the explaination in the article and the accompagning paper, it's very clear that they are not). I'd like to thank all the kernel developpers who are working on all these difficult to understand subjects in order to enhance our experience.

Thanks a lot.

NUMA-aware qspinlocks

Posted Apr 12, 2021 21:28 UTC (Mon) by kleptog (subscriber, #1183) [Link] (5 responses)

Maybe I'm missing something, but I would have thought that if there's a chance you'll be spinning for 10ms on a lock, then you're using the wrong kind of lock. That's the CPU basically wasting hundreds of millions of cycles it could have been doing other useful work. Or is this supposed to be a "never happen" scenario?

NUMA-aware qspinlocks

Posted Apr 12, 2021 22:49 UTC (Mon) by zlynx (guest, #2285) [Link] (2 responses)

I could have read it wrong, but I thought it was if the lock stays on the same NUMA node for 10ms, not a single lock spin of 10ms.

I could easily see a node stacking up new locks for a 10ms total runtime.

NUMA-aware qspinlocks

Posted Apr 12, 2021 23:05 UTC (Mon) by iabervon (subscriber, #722) [Link]

I think it's that someone on a different NUMA node is spinning for 10ms while the lock ping-pongs between CPUs on the same node continuously. I think this means that, over the course of 10ms, there are always at least 3 tasks that want the lock, but probably no task holds it very long; traditionally, all the tasks would get their turns soon, do an operation, and go back to waiting for another turn, but now, ones on the same node can get all of the time until this cuts in.

You used to be burning a ton of cycles spinning, but not all in a row, so you don't want to sleep; with this patch set, you might end up burning them all in a row due to unfairness, but the lucky tasks are getting the quick response at the very same time that you're wishing this was a sleeping lock.

NUMA-aware qspinlocks

Posted Apr 12, 2021 23:38 UTC (Mon) by flussence (guest, #85566) [Link]

That's my reading of it too. It's not that the lock is expected to burn up to 10ms of CPU time, it's an escape hatch for *if* it does, under the condition of a NUMA system being so loaded that it can't be freed within 10ms. Sounds like a dire situation and hopefully not a common one.

NUMA-aware qspinlocks

Posted Apr 13, 2021 2:11 UTC (Tue) by nickodell (subscriber, #125165) [Link]

This patch weakens the fairness of the spinlock, and weakening fairness is something which should be done carefully.

To quote Linus:

>Pretty much every time we picked an unfair - but fast - locking model in the kernel, we ended up regretting it eventually, and had to add fairness.

If there were no upper limit on how long the primary queue could occupy the spinlock, you could have a situation where two CPUs on the same node trade a spinlock back and forth forever, starving CPUs on other nodes. Having an upper limit on how long a single node can hold the spinlock is sensible. It should help with the worst-case latency spikes.

NUMA-aware qspinlocks

Posted Apr 17, 2021 8:15 UTC (Sat) by wtarreau (subscriber, #51152) [Link]

Not necessarily. It might almost never happen, justifying the usage of a cheap spinlock, but when it happens it could deadlock the whole system, so the protection is essential.

It is very common in distributed systems to see that some data remains of no interest for a long time, and suddenly it attracts 64 CPUs at once. It is important to handle such situations gracefully and that's extremely difficult.

NUMA-aware qspinlocks

Posted Apr 13, 2021 2:07 UTC (Tue) by Conan_Kudo (subscriber, #103240) [Link] (7 responses)

I just wish we could make NUMA go away. It's a terrible hack that makes running workloads more complicated than it needs to be. AMD's newest Ryzen processors don't require NUMA, POWER systems don't require NUMA, and Apple's M1 also don't require NUMA. It's pretty clear that NUMA is not a requirement for performant multi-core systems now.

NUMA-aware qspinlocks

Posted Apr 13, 2021 3:47 UTC (Tue) by Paf (subscriber, #91811) [Link]

Ummm. From a physical hardware perspective, the only way to achieve non-NUMA in larger systems is to slow everyone to the lowest common denominator.

But, also: Single socket Xeon setups *usually* have only one physical NUMA node. (There are reasons one might set up multiple logical ones.). For multi-socket ones, well, some resources are physically more distant from a given socket. So...yeah. NUMA.

NUMA-aware qspinlocks

Posted Apr 13, 2021 3:56 UTC (Tue) by Cyberax (✭ supporter ✭, #52523) [Link]

NUMA is not a "hack", it's basically an admission of reality that the memory access is not uniform. And AMD Ryzen most definitely does support NUMA.

NUMA-aware qspinlocks

Posted Apr 13, 2021 8:29 UTC (Tue) by chris_se (subscriber, #99706) [Link]

> AMD's newest Ryzen processors don't require NUMA,

For desktop CPUS: No, but it effectively implements NUMA internally due to the chiplet design. It hides it quite well, but I don't think their approach will scale to a lot more cores than they currently have in their desktop lineup. Their 16c/32t 5950X is probably the upper end of what's possible while more or less completely hiding NUMA.

And that does change once you get into the HEDT and/or server market where you have tons and tons of cores, and even multiple sockets. The speed of light will automatically impose NUMA on multi-socket systems unless you want to run them at really slow speeds. (And if you are not NUMA-aware you're effectively doing that.) AMD Threadripper (HEDT) and EPYC (server) are all NUMA designs.

> Apple's M1 also don't require NUMA.

Sure, but that's only 8 cores. And instead of NUMA Apple's M1 has its own can of worms when it comes to scheduling, because it's effectively a big.LITTLE type of system (though it's not exactly big.LITTLE), as half of the cores are high-performance, while the other half are lower performance, but lower power draw. Furthermore, because M1 is an SoC with integrated RAM, you're stuck with at most 16 GiB of RAM - both my laptop and desktop computer currently have more than that, and in a server context that's laughably low in current times. (And I'm not criticizing Apple for this specifically: for many tasks the M1 was designed for 16 GiB RAM is perfectly fine. But you can't compare that to server CPUs.)

> It's pretty clear that NUMA is not a requirement for performant multi-core systems now.

For up to 16 cores? Probably not. But for more than that: most likely. And in the end you have to consider the following: the CPU frequency will not drastically improve anymore. The IPC (instructions per clock) will still improve somewhat in the future, but I don't see that improving dramatically -- in the best case these improve by 10-20% every couple of years. So if you want more performance, the only real ways to improve that are specialized instructions for specific workloads and more cores. While the former will be quite useful for specific tasks (think AES-NI, for example), the latter is the only one that will be able to address generic computation needs. And that then by necessity means NUMA.

Especially for multi-socket systems, when it comes to latency (not throughput!) we are already starting to see designs that are limited by the speed of information transmission, which itself is limited by the speed of light. (Copper in _ideal_ situations transmits information at about 2/3rds of the speed of light.)

NUMA-aware qspinlocks

Posted Apr 13, 2021 11:40 UTC (Tue) by farnz (subscriber, #17727) [Link] (1 responses)

AMD EPYC systems require NUMA to get peak performance; multi-socket POWER systems require NUMA to get peak performance. And Apple M1 is a single socket solution; no single socket solution thus far requires NUMA.

NUMA is a reflection of the fact that not all memory is physically as close to the CPU as needed to be reachable in one clock tick; propagation delays in a vacuum at 5GHz result in a maximum radius of 3cm from source to destination and back, 6cm if the signal only needs to propagate one-way in that time, and that this is an unmanageable constraint for a complete system with a need for more than about 64 GiB DRAM per socket with today's process tech.

NUMA will definitely become less relevant as we get more RAM and more compute into a small volume, but it'll never completely die because it reflects the fact that circuits are limited in size by the speed of light.

NUMA-aware qspinlocks

Posted Apr 13, 2021 14:54 UTC (Tue) by Paf (subscriber, #91811) [Link]

“ And Apple M1 is a single socket solution; no single socket solution thus far requires NUMA.”

FWIW, this isn’t *quite* true. The now dead Xeon Phi (KNL) line of many-core chips had internal NUMA, in a few toggleable flavors, including “off”. (Some but not all were related to the behavior of the large on chip RAM. Some were just NUMA segmentation of the processor.)

That was an exception, though, and doubly so because that NUMA seemed to be more about hyper-optimization for compute tasks than general system needs. IE, it was not very non-uniform. (The extensive testing done at my employer suggested it didn’t matter much except for certain compute/simulation tasks. I wasn’t able to measure an effect for OS/system performance. That doesn’t mean there wasn’t one, but it was below any threshold we needed to care about. The “optimize the compute performance of this simulation” people cared though.)

NUMA-aware qspinlocks

Posted Apr 13, 2021 13:18 UTC (Tue) by flussence (guest, #85566) [Link]

NUMA is not as hard a requirement as it was in the 90s MP days, but it *is* the most understood topology as a result. The tooling to optimise workloads for Apple's asymmetric cores, or Ryzen's shared bus per CCX, or Intel's mesh topology just doesn't exist yet.

For that matter, Intel's the only one of those three that provides a complete driver stack (government backdoors notwithstanding). AMD's leaving an unknown amount of performance on the table by not upstreaming CPPC2 code, and the M1 has more undocumented purpose-specific coprocessors on it than a Sega Saturn; non-uniform memory is the least of its problems.

NUMA-aware qspinlocks

Posted Apr 15, 2021 17:35 UTC (Thu) by andresfreund (subscriber, #69562) [Link]

I wish the opposite were true. That more CPUs exposed that they effectively are NUMA. Obviously I'd like latency to be zero, but given that that won't happen, I'd rather be able to write software utilizing the latency differences.

NUMA-aware qspinlocks

Posted Apr 13, 2021 11:12 UTC (Tue) by k3ninho (subscriber, #50375) [Link] (1 responses)

> One optimization that has been added is called "shuffle reduction". If the lock is not all that heavily contended, the extra work of maintaining the secondary queue does not really buy anything. To mitigate this extra cost, the code uses a pseudo-random number generator to only try to create the secondary queue one time out of every 128 lock acquisitions. If the lock gets busy, that will happen relatively often, after which the secondary queue will be maintained until the primary queue empties again (or the above-mentioned timeout occurs). ...

>A number of benchmark results are included with the patch set. For lightly contended locks the performance benefits of NUMA awareness are relatively modest. As the number of contending threads grows, though, the speedup does as well, approaching a factor of two for ridiculously heavily contended loads.

It doesn't seem like it's needed yet, but this is a place for the 'sock-pairing lemma' wherein, if you're going to decide where to hang a wet sock on a drying rack, your optimal choice is to eliminate work pairing it later. Can the secondary tier be partitioned into NUMA zones and each task be grouped with those in its zone at the time you add it to that list? It would make sense, provided you take a strategy for picking the next zone to execute in when the primary list is empty. Apparent options are pseudo-randomly (don't predict the future, half the time you'll be better than average and half you won't), longest-list (to crank through those tasks that are waiting), or some measure of CPU busy-ness and queue depth (will that zone really be able to give all the tasks the access they ask for?).

The crucial problem -- as Amdahl's Law points out -- is to do the least work possible marshalling tasks so you leave time for the requested work.

K3n.

NUMA-aware qspinlocks

Posted Apr 13, 2021 11:35 UTC (Tue) by k3ninho (subscriber, #50375) [Link]

I read teh patch notes and they say that there's also a timely switch to the NUMA zone of the task waiting at the head of the secondary queue:

>We change the NUMA node preference after a waiter at the head of the secondary queue spins for a certain amount of time. We do that by flushing the secondary queue into the head of the primary queue, effectively changing the preference to the NUMA node of the waiter at the head of the secondary queue at the time of the flush.

I think that, over time, this will sort the secondary queue into blocks grouped by zone -- with 2 zones, your secondary list is the second zone; with 3, a second pass groups items in the third zone in a block before or after the list of items in the first zone; with 4, it takes until the third swap to group them. So maybe sorting upfront isn't needed.

K3n.


Copyright © 2021, Eklektix, Inc.
This article may be redistributed under the terms of the Creative Commons CC BY-SA 4.0 license
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds