Thread
Commits
GET /api/v1/messages/:b64id/commits
the thread's linked commits as JSON, with link sources.
API reference →
-
Sort guc_parameters.dat alphabetically by name
- fce7c73fba4e 19 (unreleased) cited
-
Adding basic NUMA awareness
Tomas Vondra <tomas@vondra.me> — 2025-07-01T19:07:00Z
Hi, This is a WIP version of a patch series I'm working on, adding some basic NUMA awareness for a couple parts of our shared memory (shared buffers, etc.). It's based on Andres' experimental patches he spoke about at pgconf.eu 2024 [1], and while it's improved and polished in various ways, it's still experimental. But there's a recent thread aiming to do something similar [2], so better to share it now so that we can discuss both approaches. This patch set is a bit more ambitious, handling NUMA in a way to allow smarter optimizations later, so I'm posting it in a separate thread. The series is split into patches addressing different parts of the shared memory, starting (unsurprisingly) from shared buffers, then buffer freelists and ProcArray. There's a couple additional parts, but those are smaller / addressing miscellaneous stuff. Each patch has a numa_ GUC, intended to enable/disable that part. This is meant to make development easier, not as a final interface. I'm not sure how exactly that should look. It's possible some combinations of GUCs won't work, etc. Each patch should have a commit message explaining the intent and implementation, and then also detailed comments explaining various challenges and open questions. But let me go over the basics, and discuss some of the design choices and open questions that need solving. 1) v1-0001-NUMA-interleaving-buffers.patch This is the main thing when people think about NUMA - making sure the shared buffers are allocated evenly on all the nodes, not just on a single node (which can happen easily with warmup). The regular memory interleaving would address this, but it also has some disadvantages. Firstly, it's oblivious to the contents of the shared memory segment, and we may not want to interleave everything. It's also oblivious to alignment of the items (a buffer can easily end up "split" on multiple NUMA nodes), or relationship between different parts (e.g. there's a BufferBlock and a related BufferDescriptor, and those might again end up on different nodes). So the patch handles this by explicitly mapping chunks of shared buffers to different nodes - a bit like interleaving, but in larger chunks. Ideally each node gets (1/N) of shared buffers, as a contiguous chunk. It's a bit more complicated, because the patch distributes both the blocks and descriptors, in the same way. So a buffer and it's descriptor always end on the same NUMA node. This is one of the reasons why we need to map larger chunks, because NUMA works on page granularity, and the descriptors are tiny - many fit on a memory page. There's a secondary benefit of explicitly assigning buffers to nodes, using this simple scheme - it allows quickly determining the node ID given a buffer ID. This is helpful later, when building freelist. The patch is fairly simple. Most of the complexity is about picking the chunk size, and aligning the arrays (so that it nicely aligns with memory pages). The patch has a GUC "numa_buffers_interleave", with "off" by default. 2) v1-0002-NUMA-localalloc.patch This simply sets "localalloc" when initializing a backend, so that all memory allocated later is local, not interleaved. Initially this was necessary because the patch set the allocation policy to interleaving before initializing shared memory, and we didn't want to interleave the private memory. But that's no longer the case - the explicit mapping to nodes does not have this issue. I'm keeping the patch for convenience, it allows experimenting with numactl etc. The patch has a GUC "numa_localalloc", with "off" by default. 3) v1-0003-freelist-Don-t-track-tail-of-a-freelist.patch Minor optimization. Andres noticed we're tracking the tail of buffer freelist, without using it. So the patch removes that. 4) v1-0004-NUMA-partition-buffer-freelist.patch Right now we have a single freelist, and in busy instances that can be quite contended. What's worse, the freelist may trash between different CPUs, NUMA nodes, etc. So the idea is to have multiple freelists on subsets of buffers. The patch implements multiple strategies how the list can be split (configured using "numa_partition_freelist" GUC), for experimenting: * node - One list per NUMA node. This is the most natural option, because we now know which buffer is on which node, so we can ensure a list for a node only has buffers from that list. * cpu - One list per CPU. Pretty simple, each CPU gets it's own list. * pid - Similar to "cpu", but the processes are mapped to lists based on PID, not CPU ID. * none - nothing, sigle freelist Ultimately, I think we'll want to go with "node", simply because it aligns with the buffer interleaving. But there are improvements needed. The main challenge is that with multiple smaller lists, a process can't really use the whole shared buffers. So a single backed will only use part of the memory. The more lists there are, the worse this effect is. This is also why I think we won't use the other partitioning options, because there's going to be more CPUs than NUMA nodes. Obviously, this needs solving even with NUMA nodes - we need to allow a single backend to utilize the whole shared buffers if needed. There should be a way to "steal" buffers from other freelists (if the "regular" freelist is empty), but the patch does not implement this. Shouldn't be hard, I think. The other missing part is clocksweep - there's still just a single instance of clocksweep, feeding buffers to all the freelists. But that's clearly a problem, because the clocksweep returns buffers from all NUMA nodes. The clocksweep really needs to be partitioned the same way as a freelists, and each partition will operate on a subset of buffers (from the right NUMA node). I do have a separate experimental patch doing something like that, I need to make it part of this branch. 5) v1-0005-NUMA-interleave-PGPROC-entries.patch Another area that seems like it might benefit from NUMA is PGPROC, so I gave it a try. It turned out somewhat challenging. Similarly to buffers we have two pieces that need to be located in a coordinated way - PGPROC entries and fast-path arrays. But we can't use the same approach as for buffers/descriptors, because (a) Neither of those pieces aligns with memory page size (PGPROC is ~900B, fast-path arrays are variable length). (b) We could pad PGPROC entries e.g. to 1KB, but that'd still require rather high max_connections before we use multiple huge pages. The fast-path arrays are less of a problem, because those tend to be larger, and are accessed through pointers, so we can just adjust that. So what I did instead is splitting the whole PGPROC array into one array per NUMA node, and one array for auxiliary processes and 2PC xacts. So with 4 NUMA nodes there are 5 separate arrays, for example. Each array is a multiple of memory pages, so we may waste some of the memory. But that's simply how NUMA works - page granularity. This however makes one particular thing harder - in a couple places we accessed PGPROC entries through PROC_HDR->allProcs, which was pretty much just one large array. And GetNumberFromPGProc() relied on array arithmetics to determine procnumber. With the array partitioned, this can't work the same way. But there's a simple solution - if we turn allProcs into an array of *pointers* to PGPROC arrays, there's no issue. All the places need a pointer anyway. And then we need an explicit procnumber field in PGPROC, instead of calculating it. There's a chance this have negative impact on code that accessed PGPROC very often, but so far I haven't seen such cases. But if you can come up with such examples, I'd like to see those. There's another detail - when obtaining a PGPROC entry in InitProcess(), we try to get an entry from the same NUMA node. And only if that doesn't work, we grab the first one from the list (there's still just one PGPROC freelist, I haven't split that - maybe we should?). This has a GUC "numa_procs_interleave", again "off" by default. It's not quite correct, though, because the partitioning happens always. It only affects the PGPROC lookup. (In a way, this may be a bit broken.) 6) v1-0006-NUMA-pin-backends-to-NUMA-nodes.patch This is an experimental patch, that simply pins the new process to the NUMA node obtained from the freelist. Driven by GUC "numa_procs_pin" (default: off). Summary ------- So this is what I have at the moment. I've tried to organize the patches in the order of importance, but that's just my guess. It's entirely possible there's something I missed, some other order might make more sense, etc. There's also the question how this is related to other patches affecting shared memory - I think the most relevant one is the "shared buffers online resize" by Ashutosh, simply because it touches the shared memory. I don't think the splitting would actually make some things simpler, or maybe more flexible - in particular, it'd allow us to enable huge pages only for some regions (like shared buffers), and keep the small pages e.g. for PGPROC. So that'd be good. But there'd also need to be some logic to "rework" how shared buffers get mapped to NUMA nodes after resizing. It'd be silly to start with memory on 4 nodes (25% each), resize shared buffers to 50% and end up with memory only on 2 of the nodes (because the other 2 nodes were originally assigned the upper half of shared buffers). I don't have a clear idea how this would be done, but I guess it'd require a bit of code invoked sometime after the resize. It'd already need to rebuild the freelists in some way, I guess. The other thing I haven't thought about very much is determining on which CPUs/nodes the instance is allowed to run. I assume we'd start by simply inherit/determine that at the start through libnuma, not through some custom PG configuration (which the patch [2] proposed to do). regards [1] https://www.youtube.com/watch?v=V75KpACdl6E [2] https://www.postgresql.org/message-id/CAKZiRmw6i1W1AwXxa-Asrn8wrVcVH3TO715g_MCoowTS9rkGyw%40mail.gmail.com -- Tomas Vondra
-
Re: Adding basic NUMA awareness
Ashutosh Bapat <ashutosh.bapat.oss@gmail.com> — 2025-07-02T11:37:28Z
On Wed, Jul 2, 2025 at 12:37 AM Tomas Vondra <tomas@vondra.me> wrote: > > > 3) v1-0003-freelist-Don-t-track-tail-of-a-freelist.patch > > Minor optimization. Andres noticed we're tracking the tail of buffer > freelist, without using it. So the patch removes that. > The patches for resizing buffers use the lastFreeBuffer to add new buffers to the end of free list when expanding it. But we could as well add it at the beginning of the free list. This patch seems almost independent of the rest of the patches. Do you need it in the rest of the patches? I understand that those patches don't need to worry about maintaining lastFreeBuffer after this patch. Is there any other effect? If we are going to do this, let's do it earlier so that buffer resizing patches can be adjusted. > > There's also the question how this is related to other patches affecting > shared memory - I think the most relevant one is the "shared buffers > online resize" by Ashutosh, simply because it touches the shared memory. I have added Dmitry to this thread since he has written most of the shared memory handling code. > > I don't think the splitting would actually make some things simpler, or > maybe more flexible - in particular, it'd allow us to enable huge pages > only for some regions (like shared buffers), and keep the small pages > e.g. for PGPROC. So that'd be good. The resizing patches split the shared buffer related structures into separate memory segments. I think that itself will help enabling huge pages for some regions. Would that help in your case? > > But there'd also need to be some logic to "rework" how shared buffers > get mapped to NUMA nodes after resizing. It'd be silly to start with > memory on 4 nodes (25% each), resize shared buffers to 50% and end up > with memory only on 2 of the nodes (because the other 2 nodes were > originally assigned the upper half of shared buffers). > > I don't have a clear idea how this would be done, but I guess it'd > require a bit of code invoked sometime after the resize. It'd already > need to rebuild the freelists in some way, I guess. Yes, there's code to build the free list. I think we will need code to remap the buffers and buffer descriptor. -- Best Wishes, Ashutosh Bapat
-
Re: Adding basic NUMA awareness
Tomas Vondra <tomas@vondra.me> — 2025-07-02T12:36:31Z
On 7/2/25 13:37, Ashutosh Bapat wrote: > On Wed, Jul 2, 2025 at 12:37 AM Tomas Vondra <tomas@vondra.me> wrote: >> >> >> 3) v1-0003-freelist-Don-t-track-tail-of-a-freelist.patch >> >> Minor optimization. Andres noticed we're tracking the tail of buffer >> freelist, without using it. So the patch removes that. >> > > The patches for resizing buffers use the lastFreeBuffer to add new > buffers to the end of free list when expanding it. But we could as > well add it at the beginning of the free list. > > This patch seems almost independent of the rest of the patches. Do you > need it in the rest of the patches? I understand that those patches > don't need to worry about maintaining lastFreeBuffer after this patch. > Is there any other effect? > > If we are going to do this, let's do it earlier so that buffer > resizing patches can be adjusted. > My patches don't particularly rely on this bit, it would work even with lastFreeBuffer. I believe Andres simply noticed the current code does not use lastFreeBuffer, it just maintains is, so he removed that as an optimization. I don't know how significant is the improvement, but if it's measurable we could just do that independently of our patches. >> >> There's also the question how this is related to other patches affecting >> shared memory - I think the most relevant one is the "shared buffers >> online resize" by Ashutosh, simply because it touches the shared memory. > > I have added Dmitry to this thread since he has written most of the > shared memory handling code. > Thanks. >> >> I don't think the splitting would actually make some things simpler, or >> maybe more flexible - in particular, it'd allow us to enable huge pages >> only for some regions (like shared buffers), and keep the small pages >> e.g. for PGPROC. So that'd be good. > > The resizing patches split the shared buffer related structures into > separate memory segments. I think that itself will help enabling huge > pages for some regions. Would that help in your case? > Indirectly. My patch can work just fine with a single segment, but being able to enable huge pages only for some of the segments seems better. >> >> But there'd also need to be some logic to "rework" how shared buffers >> get mapped to NUMA nodes after resizing. It'd be silly to start with >> memory on 4 nodes (25% each), resize shared buffers to 50% and end up >> with memory only on 2 of the nodes (because the other 2 nodes were >> originally assigned the upper half of shared buffers). >> >> I don't have a clear idea how this would be done, but I guess it'd >> require a bit of code invoked sometime after the resize. It'd already >> need to rebuild the freelists in some way, I guess. > > Yes, there's code to build the free list. I think we will need code to > remap the buffers and buffer descriptor. > Right. The good thing is that's just "advisory" information, it doesn't break anything if it's temporarily out of sync. We don't need to "stop" everything to remap the buffers to other nodes, or anything like that. Or at least I think so. It's one thing to "flip" the target mapping (determining which node a buffer should be on), and actually migrating the buffers. The first part can be done instantaneously, the second part can happen in the background over a longer time period. I'm not sure how you're rebuilding the freelist. Presumably it can contain buffers that are no longer valid (after shrinking). How is that handled to not break anything? I think the NUMA variant would do exactly the same thing, except that there's multiple lists. regards -- Tomas Vondra
-
Re: Adding basic NUMA awareness
Ashutosh Bapat <ashutosh.bapat.oss@gmail.com> — 2025-07-03T14:07:18Z
On Wed, Jul 2, 2025 at 6:06 PM Tomas Vondra <tomas@vondra.me> wrote: > > I'm not sure how you're rebuilding the freelist. Presumably it can > contain buffers that are no longer valid (after shrinking). How is that > handled to not break anything? I think the NUMA variant would do exactly > the same thing, except that there's multiple lists. Before shrinking the buffers, we walk the free list removing any buffers that are going to be removed. When expanding, by linking the new buffers in the order and then adding those to the already existing free list. 0005 patch in [1] has the code for the same. [1] https://www.postgresql.org/message-id/my4hukmejato53ef465ev7lk3sqiqvneh7436rz64wmtc7rbfj%40hmuxsf2ngov2 -- Best Wishes, Ashutosh Bapat
-
Re: Adding basic NUMA awareness
Dmitry Dolgov <9erthalion6@gmail.com> — 2025-07-03T14:49:34Z
> On Wed, Jul 02, 2025 at 05:07:28PM +0530, Ashutosh Bapat wrote: > > There's also the question how this is related to other patches affecting > > shared memory - I think the most relevant one is the "shared buffers > > online resize" by Ashutosh, simply because it touches the shared memory. > > I have added Dmitry to this thread since he has written most of the > shared memory handling code. Thanks! I like the idea behind this patch series. I haven't read it in details yet, but I can imagine both patches (interleaving and online resizing) could benefit from each other. In online resizing we've introduced a possibility to use multiple shared mappings for different types of data, maybe it would be convenient to use the same interface to create separate mappings for different NUMA nodes as well. Using a separate shared mapping per NUMA node would also make resizing easier, since it would be more straightforward to fit an increased segment into NUMA boundaries. > > I don't think the splitting would actually make some things simpler, or > > maybe more flexible - in particular, it'd allow us to enable huge pages > > only for some regions (like shared buffers), and keep the small pages > > e.g. for PGPROC. So that'd be good. > > The resizing patches split the shared buffer related structures into > separate memory segments. I think that itself will help enabling huge > pages for some regions. Would that help in your case? Right, separate segments would allow to mix and match huge pages with pages of regular size. It's not implemented in the latest version of online resizing patch, purely to reduce complexity and maintain the same invariant (everything is either using huge pages or not) -- but we could do it other way around as well.
-
Re: Adding basic NUMA awareness
Jakub Wartak <jakub.wartak@enterprisedb.com> — 2025-07-04T11:05:05Z
On Tue, Jul 1, 2025 at 9:07 PM Tomas Vondra <tomas@vondra.me> wrote: Hi! > 1) v1-0001-NUMA-interleaving-buffers.patch [..] > It's a bit more complicated, because the patch distributes both the > blocks and descriptors, in the same way. So a buffer and it's descriptor > always end on the same NUMA node. This is one of the reasons why we need > to map larger chunks, because NUMA works on page granularity, and the > descriptors are tiny - many fit on a memory page. Oh, now I get it! OK, let's stick to this one. > I don't think the splitting would actually make some things simpler, or > maybe more flexible - in particular, it'd allow us to enable huge pages > only for some regions (like shared buffers), and keep the small pages > e.g. for PGPROC. So that'd be good. You have made assumption that this is good, but small pages (4KB) are not hugetlb, and are *swappable* (Transparent HP are swappable too, manually allocated ones as with mmap(MMAP_HUGETLB) are not)[1]. The most frequent problem I see these days are OOMs, and it makes me believe that making certain critical parts of shared memory being swappable just to make pagesize granular is possibly throwing the baby out with the bathwater. I'm thinking about bad situations like: some wrong settings of vm.swapiness that people keep (or distros keep?) and general inability of PG to restrain from allocating more memory in some cases. > The other thing I haven't thought about very much is determining on > which CPUs/nodes the instance is allowed to run. I assume we'd start by > simply inherit/determine that at the start through libnuma, not through > some custom PG configuration (which the patch [2] proposed to do). 0. I think that we could do better, some counter arguments to no-configuration-at-all: a. as Robert & Bertrand already put it there after review: let's say I want just to run on NUMA #2 node, so here I would need to override systemd's script ExecStart= to include that numactl (not elegant?). I could also use `CPUAffinity=1,3,5,7..` but that's all, and it is even less friendly. Also it probably requires root to edit/reload systemd, while having GUC for this like in my proposal makes it more smooth (I think?) b. wouldn't it be better if that stayed as drop-in rather than always on? What if there's a problem, how do you disable those internal optimizations if they do harm in some cases? (or let's say I want to play with MPOL_INTERLEAVE_WEIGHTED?). So at least boolean numa_buffers_interleave would be nice? c. What if I want my standby (walreceiver+startup/recovery) to run with NUMA affinity to get better performance (I'm not going to hack around systemd script every time, but I could imagine changing numa=X,Y,Z after restart/before promotion) d. Now if I would be forced for some reason to do that numactl(1) voodoo, and use the those above mentioned overrides and PG wouldn't be having GUC (let's say I would use `numactl --weighted-interleave=0,1`), then: > 2) v1-0002-NUMA-localalloc.patch > This simply sets "localalloc" when initializing a backend, so that all > memory allocated later is local, not interleaved. Initially this was > necessary because the patch set the allocation policy to interleaving > before initializing shared memory, and we didn't want to interleave the > private memory. But that's no longer the case - the explicit mapping to > nodes does not have this issue. I'm keeping the patch for convenience, > it allows experimenting with numactl etc. .. .is not accurate anymore and we would require to have that in (still with GUC) ? Thoughts? I can add that mine part into Your's patches if you want. Way too quick review and some very fast benchmark probes, I've concentrated only on v1-0001 and v1-0005 (efficiency of buffermgmt would be too new topic for me), but let's start: 1. normal pgbench -S (still with just s_b@4GB), done many tries, consistent benefit for the patch with like +8..10% boost on generic run: numa_buffers_interleave=off numa_pgproc_interleave=on(due that always on "if"), s_b just on 1 NUMA node (might happen) latency average = 0.373 ms latency stddev = 0.237 ms initial connection time = 45.899 ms tps = 160242.147877 (without initial connection time) numa_buffers_interleave=on numa_pgproc_interleave=on latency average = 0.345 ms latency stddev = 0.373 ms initial connection time = 44.485 ms tps = 177564.686094 (without initial connection time) 2. Tested it the same way as I did for mine(problem#2 from Andres's presentation): 4s32c128t, s_b=4GB (on 128GB), prewarm test (with seqconcurrscans.pgb as earlier) default/numa_buffers_interleave=off latency average = 1375.478 ms latency stddev = 1141.423 ms initial connection time = 46.104 ms tps = 45.868075 (without initial connection time) numa_buffers_interleave=on latency average = 838.128 ms latency stddev = 498.787 ms initial connection time = 43.437 ms tps = 75.413894 (without initial connection time) and i've repeated the the same test (identical conditions) with my patch, got me slightly more juice: latency average = 727.717 ms latency stddev = 410.767 ms initial connection time = 45.119 ms tps = 86.844161 (without initial connection time) (but mine didn't get that boost from normal pgbench as per #1 pgbench -S -- my numa='all' stays @ 160k TPS just as numa_buffers_interleave=off), so this idea is clearly better. So should I close https://commitfest.postgresql.org/patch/5703/ and you'll open a new one or should I just edit the #5703 and alter it and add this thread too? 3. Patch is not calling interleave on PQ shmem, do we want to add that in as some next item like v1-0007? Question is whether OS interleaving makes sense there ? I believe it does there, please see my thread (NUMA_pq_cpu_pinning_results.txt), the issue is that PQ workers are being spawned by postmaster and may end up on different NUMA nodes randomly, so actually OS-interleaving that memory reduces jitter there (AKA bandwidth-over-latency). My thinking is that one cannot expect static/forced CPU-to-just-one-NUMA-node assignment for backend and it's PQ workers, because it is impossible have always available CPU power there in that NUMA node, so it might be useful to interleave that shared mem there too (as separate patch item?) 4 In BufferManagerShmemInit() you call numa_num_configured_nodes() (also in v1-0005). My worry is should we may put some known-limitations docs (?) from start and mention that if the VM is greatly resized and NUMA numa nodes appear, they might not be used until restart? 5. In v1-0001, pg_numa_interleave_memory() + * XXX no return value, to make this fail on error, has to use + * numa_set_strict Yes, my patch has those numa_error() and numa_warn() handlers too in pg_numa. Feel free to use it for better UX. + * XXX Should we still touch the memory first, like with numa_move_pages, + * or is that not necessary? It's not necessary to touch after numa_tonode_memory() (wrapper around numa_interleave_memory()), if it is going to be used anyway it will be correctly placed to best of my knowledge. 6. diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c Accidental indents (also fails to apply) 7. We miss the pg_numa_* shims, but for sure that's for later and also avoid those Linux specific #ifdef USE_LIBNUMA and so on? 8. v1-0005 2x + /* if (numa_procs_interleave) */ Ha! it's a TRAP! I've uncommented it because I wanted to try it out without it (just by setting GUC off) , but "MyProc->sema" is NULL : 2025-07-04 12:31:08.103 CEST [28754] LOG: starting PostgreSQL 19devel on x86_64-linux, compiled by gcc-12.2.0, 64-bit [..] 2025-07-04 12:31:08.109 CEST [28754] LOG: io worker (PID 28755) was terminated by signal 11: Segmentation fault 2025-07-04 12:31:08.109 CEST [28754] LOG: terminating any other active server processes 2025-07-04 12:31:08.114 CEST [28754] LOG: shutting down because "restart_after_crash" is off 2025-07-04 12:31:08.116 CEST [28754] LOG: database system is shut down [New LWP 28755] [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". Core was generated by `postgres: io worker '. Program terminated with signal SIGSEGV, Segmentation fault. #0 __new_sem_wait_fast (definitive_result=1, sem=sem@entry=0x0) at ./nptl/sem_waitcommon.c:136 136 ./nptl/sem_waitcommon.c: No such file or directory. (gdb) where #0 __new_sem_wait_fast (definitive_result=1, sem=sem@entry=0x0) at ./nptl/sem_waitcommon.c:136 #1 __new_sem_trywait (sem=sem@entry=0x0) at ./nptl/sem_wait.c:81 #2 0x00005561918e0cac in PGSemaphoreReset (sema=0x0) at ../src/backend/port/posix_sema.c:302 #3 0x0000556191970553 in InitAuxiliaryProcess () at ../src/backend/storage/lmgr/proc.c:992 #4 0x00005561918e51a2 in AuxiliaryProcessMainCommon () at ../src/backend/postmaster/auxprocess.c:65 #5 0x0000556191940676 in IoWorkerMain (startup_data=<optimized out>, startup_data_len=<optimized out>) at ../src/backend/storage/aio/method_worker.c:393 #6 0x00005561918e8163 in postmaster_child_launch (child_type=child_type@entry=B_IO_WORKER, child_slot=20086, startup_data=startup_data@entry=0x0, startup_data_len=startup_data_len@entry=0, client_sock=client_sock@entry=0x0) at ../src/backend/postmaster/launch_backend.c:290 #7 0x00005561918ea09a in StartChildProcess (type=type@entry=B_IO_WORKER) at ../src/backend/postmaster/postmaster.c:3973 #8 0x00005561918ea308 in maybe_adjust_io_workers () at ../src/backend/postmaster/postmaster.c:4404 [..] (gdb) print *MyProc->sem Cannot access memory at address 0x0 9. v1-0006: is this just a thought or serious candidate? I can imagine it can easily blow-up with some backends somehow requesting CPUs only from one NUMA node, while the second node being idle. Isn't it better just to leave CPU scheduling, well, to the CPU scheduler? The problem is that you have tools showing overall CPU usage, even mpstat(1) per CPU , but no tools for per-NUMA node CPU util%, so it would be hard for someone to realize that this is happening. -J. [1] - https://www.kernel.org/doc/Documentation/vm/hugetlbpage.txt -
Re: Adding basic NUMA awareness
Tomas Vondra <tomas@vondra.me> — 2025-07-04T18:12:01Z
On 7/4/25 13:05, Jakub Wartak wrote: > On Tue, Jul 1, 2025 at 9:07 PM Tomas Vondra <tomas@vondra.me> wrote: > > Hi! > >> 1) v1-0001-NUMA-interleaving-buffers.patch > [..] >> It's a bit more complicated, because the patch distributes both the >> blocks and descriptors, in the same way. So a buffer and it's descriptor >> always end on the same NUMA node. This is one of the reasons why we need >> to map larger chunks, because NUMA works on page granularity, and the >> descriptors are tiny - many fit on a memory page. > > Oh, now I get it! OK, let's stick to this one. > >> I don't think the splitting would actually make some things simpler, or >> maybe more flexible - in particular, it'd allow us to enable huge pages >> only for some regions (like shared buffers), and keep the small pages >> e.g. for PGPROC. So that'd be good. > > You have made assumption that this is good, but small pages (4KB) are > not hugetlb, and are *swappable* (Transparent HP are swappable too, > manually allocated ones as with mmap(MMAP_HUGETLB) are not)[1]. The > most frequent problem I see these days are OOMs, and it makes me > believe that making certain critical parts of shared memory being > swappable just to make pagesize granular is possibly throwing the baby > out with the bathwater. I'm thinking about bad situations like: some > wrong settings of vm.swapiness that people keep (or distros keep?) and > general inability of PG to restrain from allocating more memory in > some cases. > I haven't observed such issues myself, or maybe I didn't realize it's happening. Maybe it happens, but it'd be good to see some data showing that, or a reproducer of some sort. But let's say it's real. I don't think we should use huge pages merely to ensure something is not swapped out. The "not swappable" is more of a limitation of huge pages, not an advantage. You can't just choose to make them swappable. Wouldn't it be better to keep using 4KB pages, but lock the memory using mlock/mlockall? >> The other thing I haven't thought about very much is determining on >> which CPUs/nodes the instance is allowed to run. I assume we'd start by >> simply inherit/determine that at the start through libnuma, not through >> some custom PG configuration (which the patch [2] proposed to do). > > 0. I think that we could do better, some counter arguments to > no-configuration-at-all: > > a. as Robert & Bertrand already put it there after review: let's say I > want just to run on NUMA #2 node, so here I would need to override > systemd's script ExecStart= to include that numactl (not elegant?). I > could also use `CPUAffinity=1,3,5,7..` but that's all, and it is even > less friendly. Also it probably requires root to edit/reload systemd, > while having GUC for this like in my proposal makes it more smooth (I > think?) > > b. wouldn't it be better if that stayed as drop-in rather than always > on? What if there's a problem, how do you disable those internal > optimizations if they do harm in some cases? (or let's say I want to > play with MPOL_INTERLEAVE_WEIGHTED?). So at least boolean > numa_buffers_interleave would be nice? > > c. What if I want my standby (walreceiver+startup/recovery) to run > with NUMA affinity to get better performance (I'm not going to hack > around systemd script every time, but I could imagine changing > numa=X,Y,Z after restart/before promotion) > > d. Now if I would be forced for some reason to do that numactl(1) > voodoo, and use the those above mentioned overrides and PG wouldn't be > having GUC (let's say I would use `numactl > --weighted-interleave=0,1`), then: > I'm not against doing something like this, but I don't plan to do that in V1. I don't have a clear idea what configurability is actually needed, so it's likely I'd do the interface wrong. >> 2) v1-0002-NUMA-localalloc.patch >> This simply sets "localalloc" when initializing a backend, so that all >> memory allocated later is local, not interleaved. Initially this was >> necessary because the patch set the allocation policy to interleaving >> before initializing shared memory, and we didn't want to interleave the >> private memory. But that's no longer the case - the explicit mapping to >> nodes does not have this issue. I'm keeping the patch for convenience, >> it allows experimenting with numactl etc. > > .. .is not accurate anymore and we would require to have that in > (still with GUC) ? > Thoughts? I can add that mine part into Your's patches if you want. > I'm sorry, I don't understand what's the question :-( > Way too quick review and some very fast benchmark probes, I've > concentrated only on v1-0001 and v1-0005 (efficiency of buffermgmt > would be too new topic for me), but let's start: > > 1. normal pgbench -S (still with just s_b@4GB), done many tries, > consistent benefit for the patch with like +8..10% boost on generic > run: > > numa_buffers_interleave=off numa_pgproc_interleave=on(due that > always on "if"), s_b just on 1 NUMA node (might happen) > latency average = 0.373 ms > latency stddev = 0.237 ms > initial connection time = 45.899 ms > tps = 160242.147877 (without initial connection time) > > numa_buffers_interleave=on numa_pgproc_interleave=on > latency average = 0.345 ms > latency stddev = 0.373 ms > initial connection time = 44.485 ms > tps = 177564.686094 (without initial connection time) > > 2. Tested it the same way as I did for mine(problem#2 from Andres's > presentation): 4s32c128t, s_b=4GB (on 128GB), prewarm test (with > seqconcurrscans.pgb as earlier) > default/numa_buffers_interleave=off > latency average = 1375.478 ms > latency stddev = 1141.423 ms > initial connection time = 46.104 ms > tps = 45.868075 (without initial connection time) > > numa_buffers_interleave=on > latency average = 838.128 ms > latency stddev = 498.787 ms > initial connection time = 43.437 ms > tps = 75.413894 (without initial connection time) > > and i've repeated the the same test (identical conditions) with my > patch, got me slightly more juice: > latency average = 727.717 ms > latency stddev = 410.767 ms > initial connection time = 45.119 ms > tps = 86.844161 (without initial connection time) > > (but mine didn't get that boost from normal pgbench as per #1 > pgbench -S -- my numa='all' stays @ 160k TPS just as > numa_buffers_interleave=off), so this idea is clearly better. Good, thanks for the testing. I should have done something like this when I posted my patches, but I forgot about that (and the email felt too long anyway). But this actually brings an interesting question. What exactly should we expect / demand from these patches? In my mind it'd primarily about predictability and stability of results. For example, the results should not depend on how was the database warmed up - was it done by a single backend or many backends? Was it restarted, or what? I could probably warmup the system very carefully to ensure it's balanced. The patches mean I don't need to be that careful. > So should I close https://commitfest.postgresql.org/patch/5703/ > and you'll open a new one or should I just edit the #5703 and alter it > and add this thread too? > Good question. It's probably best to close the original entry as "withdrawn" and I'll add a new entry. Sounds OK? > 3. Patch is not calling interleave on PQ shmem, do we want to add that > in as some next item like v1-0007? Question is whether OS interleaving > makes sense there ? I believe it does there, please see my thread > (NUMA_pq_cpu_pinning_results.txt), the issue is that PQ workers are > being spawned by postmaster and may end up on different NUMA nodes > randomly, so actually OS-interleaving that memory reduces jitter there > (AKA bandwidth-over-latency). My thinking is that one cannot expect > static/forced CPU-to-just-one-NUMA-node assignment for backend and > it's PQ workers, because it is impossible have always available CPU > power there in that NUMA node, so it might be useful to interleave > that shared mem there too (as separate patch item?) > Excellent question. I haven't thought about this at all. I agree it probably makes sense to interleave this memory, in some way. I don't know what's the perfect scheme, though. wild idea: Would it make sense to pin the workers to the same NUMA node as the leader? And allocate all memory only from that node? > 4 In BufferManagerShmemInit() you call numa_num_configured_nodes() > (also in v1-0005). My worry is should we may put some > known-limitations docs (?) from start and mention that > if the VM is greatly resized and NUMA numa nodes appear, they might > not be used until restart? > Yes, this is one thing I need some feedback on. The patches mostly assume there are no disabled nodes, that the set of allowed nodes does not change, etc. I think for V1 that's a reasonable limitation. But let's say we want to relax this a bit. How do we learn about the change, after a node/CPU gets disabled? For some parts it's not that difficult (e.g. we can "remap" buffers/descriptors) in the background. But for other parts that's not practical. E.g. we can't rework how the PGPROC gets split. But while discussing this with Andres yesterday, he had an interesting suggestion - to always use e.g. 8 or 16 partitions, then partition this by NUMA node. So we'd have 16 partitions, and with 4 nodes the 0-3 would go to node 0, 4-7 would go to node 1, etc. The advantage is that if a node gets disabled, we can rebuild just this small "mapping" and not the 16 partitions. And the partitioning may be helpful even without NUMA. Still have to figure out the details, but seems it might help. > 5. In v1-0001, pg_numa_interleave_memory() > > + * XXX no return value, to make this fail on error, has to use > + * numa_set_strict > > Yes, my patch has those numa_error() and numa_warn() handlers too in > pg_numa. Feel free to use it for better UX. > > + * XXX Should we still touch the memory first, like > with numa_move_pages, > + * or is that not necessary? > > It's not necessary to touch after numa_tonode_memory() (wrapper around > numa_interleave_memory()), if it is going to be used anyway it will be > correctly placed to best of my knowledge. > > 6. diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c > > Accidental indents (also fails to apply) > > 7. We miss the pg_numa_* shims, but for sure that's for later and also > avoid those Linux specific #ifdef USE_LIBNUMA and so on? > Right, we need to add those. Or actually, we need to think about how we'd do this for non-NUMA systems. I wonder if we even want to just build everything the "old way" (without the partitions, etc.). But per the earlier comment, the partitioning seems beneficial even on non-NUMA systems, so maybe the shims are good enough OK. > 8. v1-0005 2x + /* if (numa_procs_interleave) */ > > Ha! it's a TRAP! I've uncommented it because I wanted to try it out > without it (just by setting GUC off) , but "MyProc->sema" is NULL : > > 2025-07-04 12:31:08.103 CEST [28754] LOG: starting PostgreSQL > 19devel on x86_64-linux, compiled by gcc-12.2.0, 64-bit > [..] > 2025-07-04 12:31:08.109 CEST [28754] LOG: io worker (PID 28755) > was terminated by signal 11: Segmentation fault > 2025-07-04 12:31:08.109 CEST [28754] LOG: terminating any other > active server processes > 2025-07-04 12:31:08.114 CEST [28754] LOG: shutting down because > "restart_after_crash" is off > 2025-07-04 12:31:08.116 CEST [28754] LOG: database system is shut down > > [New LWP 28755] > [Thread debugging using libthread_db enabled] > Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". > Core was generated by `postgres: io worker '. > Program terminated with signal SIGSEGV, Segmentation fault. > #0 __new_sem_wait_fast (definitive_result=1, sem=sem@entry=0x0) > at ./nptl/sem_waitcommon.c:136 > 136 ./nptl/sem_waitcommon.c: No such file or directory. > (gdb) where > #0 __new_sem_wait_fast (definitive_result=1, sem=sem@entry=0x0) > at ./nptl/sem_waitcommon.c:136 > #1 __new_sem_trywait (sem=sem@entry=0x0) at ./nptl/sem_wait.c:81 > #2 0x00005561918e0cac in PGSemaphoreReset (sema=0x0) at > ../src/backend/port/posix_sema.c:302 > #3 0x0000556191970553 in InitAuxiliaryProcess () at > ../src/backend/storage/lmgr/proc.c:992 > #4 0x00005561918e51a2 in AuxiliaryProcessMainCommon () at > ../src/backend/postmaster/auxprocess.c:65 > #5 0x0000556191940676 in IoWorkerMain (startup_data=<optimized > out>, startup_data_len=<optimized out>) at > ../src/backend/storage/aio/method_worker.c:393 > #6 0x00005561918e8163 in postmaster_child_launch > (child_type=child_type@entry=B_IO_WORKER, child_slot=20086, > startup_data=startup_data@entry=0x0, > startup_data_len=startup_data_len@entry=0, > client_sock=client_sock@entry=0x0) at > ../src/backend/postmaster/launch_backend.c:290 > #7 0x00005561918ea09a in StartChildProcess > (type=type@entry=B_IO_WORKER) at > ../src/backend/postmaster/postmaster.c:3973 > #8 0x00005561918ea308 in maybe_adjust_io_workers () at > ../src/backend/postmaster/postmaster.c:4404 > [..] > (gdb) print *MyProc->sem > Cannot access memory at address 0x0 > Yeah, good catch. I'll look into that next week. > 9. v1-0006: is this just a thought or serious candidate? I can imagine > it can easily blow-up with some backends somehow requesting CPUs only > from one NUMA node, while the second node being idle. Isn't it better > just to leave CPU scheduling, well, to the CPU scheduler? The problem > is that you have tools showing overall CPU usage, even mpstat(1) per > CPU , but no tools for per-NUMA node CPU util%, so it would be hard > for someone to realize that this is happening. > Mostly experimental, for benchmarking etc. I agree we may not want to mess with the task scheduling too much. Thanks for the feedback! regards -- Tomas Vondra
-
Re: Adding basic NUMA awareness - Preliminary feedback and outline for an extensible approach
Cédric Villemain <cedric.villemain@data-bene.io> — 2025-07-05T07:09:00Z
Hi Tomas, I haven't yet had time to fully read all the work and proposals around NUMA and related features, but I hope to catch up over the summer. However, I think it's important to share some thoughts before it's too late, as you might find them relevant to the NUMA management code. > 6) v1-0006-NUMA-pin-backends-to-NUMA-nodes.patch > > This is an experimental patch, that simply pins the new process to the > NUMA node obtained from the freelist. > > Driven by GUC "numa_procs_pin" (default: off). In my work on more careful PostgreSQL resource management, I've come to the conclusion that we should avoid pushing policy too deeply into the PostgreSQL core itself. Therefore, I'm quite skeptical about integrating NUMA-specific management directly into core PostgreSQL in such a way. We are working on a PROFILE and PROFILE MANAGER specification to provide PostgreSQL with only the APIs and hooks needed so that extensions can manage whatever they want externally. The basic syntax (not meant to be discussed here, and even the names might change) is roughly as follows, just to illustrate the intent: CREATE PROFILE MANAGER manager_name [IF NOT EXISTS] [ HANDLER handler_function | NO HANDLER ] [ VALIDATOR validator_function | NO VALIDATOR ] [ OPTIONS ( option 'value' [, ... ] ) ] CREATE PROFILE profile_name [IF NOT EXISTS] USING profile_manager SET key = value [, key = value]... [USING profile_manager SET key = value [, key = value]...] [...]; CREATE PROFILE MAPPING [IF NOT EXISTS] FOR PROFILE profile_name [MATCH [ ALL | ANY ] ( [ROLE role_name], [BACKEND TYPE backend_type], [DATABASE database_name], [APPLICATION appname] )]; ## PROFILE RESOLUTION ORDER 1. ALTER ROLE IN DATABASE 2. ALTER ROLE 3. ALTER DATABASE 4. First matching PROFILE MAPPING (global or specific) 5. No profile (fallback) As currently designed, this approach allows quite a lot of flexibility: * pg_psi is used to ensure the spec is suitable for a cgroup profile manager (moving PIDs as needed; NUMA and cgroups could work well together, see e.g. this Linux kernel summary: https://blogs.oracle.com/linux/post/numa-balancing ) * Someone else could implement support for Windows or BSD specifics. * Others might use it to integrate PostgreSQL's own resources (e.g., "areas" of shared buffers) into policies. Hope this perspective is helpful. Best regards, -- Cédric Villemain +33 6 20 30 22 52 https://www.Data-Bene.io PostgreSQL Support, Expertise, Training, R&D -
Re: Adding basic NUMA awareness - Preliminary feedback and outline for an extensible approach
Tomas Vondra <tomas@vondra.me> — 2025-07-07T12:01:53Z
On 7/5/25 09:09, Cédric Villemain wrote: > Hi Tomas, > > > I haven't yet had time to fully read all the work and proposals around > NUMA and related features, but I hope to catch up over the summer. > > However, I think it's important to share some thoughts before it's too > late, as you might find them relevant to the NUMA management code. > > >> 6) v1-0006-NUMA-pin-backends-to-NUMA-nodes.patch >> >> This is an experimental patch, that simply pins the new process to the >> NUMA node obtained from the freelist. >> >> Driven by GUC "numa_procs_pin" (default: off). > > > In my work on more careful PostgreSQL resource management, I've come to > the conclusion that we should avoid pushing policy too deeply into the > PostgreSQL core itself. Therefore, I'm quite skeptical about integrating > NUMA-specific management directly into core PostgreSQL in such a way. > > > We are working on a PROFILE and PROFILE MANAGER specification to provide > PostgreSQL with only the APIs and hooks needed so that extensions can > manage whatever they want externally. > > The basic syntax (not meant to be discussed here, and even the names > might change) is roughly as follows, just to illustrate the intent: > > > CREATE PROFILE MANAGER manager_name [IF NOT EXISTS] > [ HANDLER handler_function | NO HANDLER ] > [ VALIDATOR validator_function | NO VALIDATOR ] > [ OPTIONS ( option 'value' [, ... ] ) ] > > CREATE PROFILE profile_name > [IF NOT EXISTS] > USING profile_manager > SET key = value [, key = value]... > [USING profile_manager > SET key = value [, key = value]...] > [...]; > > CREATE PROFILE MAPPING > [IF NOT EXISTS] > FOR PROFILE profile_name > [MATCH [ ALL | ANY ] ( > [ROLE role_name], > [BACKEND TYPE backend_type], > [DATABASE database_name], > [APPLICATION appname] > )]; > > ## PROFILE RESOLUTION ORDER > > 1. ALTER ROLE IN DATABASE > 2. ALTER ROLE > 3. ALTER DATABASE > 4. First matching PROFILE MAPPING (global or specific) > 5. No profile (fallback) > > As currently designed, this approach allows quite a lot of flexibility: > > * pg_psi is used to ensure the spec is suitable for a cgroup profile > manager (moving PIDs as needed; NUMA and cgroups could work well > together, see e.g. this Linux kernel summary: https://blogs.oracle.com/ > linux/post/numa-balancing ) > > * Someone else could implement support for Windows or BSD specifics. > > * Others might use it to integrate PostgreSQL's own resources (e.g., > "areas" of shared buffers) into policies. > > Hope this perspective is helpful. Can you explain how you want to manage this by an extension defined at the SQL level, when most of this stuff has to be done when setting up shared memory, which is waaaay before we have any access to catalogs? regards -- Tomas Vondra
-
Re: Adding basic NUMA awareness
Jakub Wartak <jakub.wartak@enterprisedb.com> — 2025-07-07T12:31:28Z
Hi Tomas, some more thoughts after the weekend: On Fri, Jul 4, 2025 at 8:12 PM Tomas Vondra <tomas@vondra.me> wrote: > > On 7/4/25 13:05, Jakub Wartak wrote: > > On Tue, Jul 1, 2025 at 9:07 PM Tomas Vondra <tomas@vondra.me> wrote: > > > > Hi! > > > >> 1) v1-0001-NUMA-interleaving-buffers.patch > > [..] > >> It's a bit more complicated, because the patch distributes both the > >> blocks and descriptors, in the same way. So a buffer and it's descriptor > >> always end on the same NUMA node. This is one of the reasons why we need > >> to map larger chunks, because NUMA works on page granularity, and the > >> descriptors are tiny - many fit on a memory page. > > > > Oh, now I get it! OK, let's stick to this one. > > > >> I don't think the splitting would actually make some things simpler, or > >> maybe more flexible - in particular, it'd allow us to enable huge pages > >> only for some regions (like shared buffers), and keep the small pages > >> e.g. for PGPROC. So that'd be good. > > > > You have made assumption that this is good, but small pages (4KB) are > > not hugetlb, and are *swappable* (Transparent HP are swappable too, > > manually allocated ones as with mmap(MMAP_HUGETLB) are not)[1]. The > > most frequent problem I see these days are OOMs, and it makes me > > believe that making certain critical parts of shared memory being > > swappable just to make pagesize granular is possibly throwing the baby > > out with the bathwater. I'm thinking about bad situations like: some > > wrong settings of vm.swapiness that people keep (or distros keep?) and > > general inability of PG to restrain from allocating more memory in > > some cases. > > > > I haven't observed such issues myself, or maybe I didn't realize it's > happening. Maybe it happens, but it'd be good to see some data showing > that, or a reproducer of some sort. But let's say it's real. > > I don't think we should use huge pages merely to ensure something is not > swapped out. The "not swappable" is more of a limitation of huge pages, > not an advantage. You can't just choose to make them swappable. > > Wouldn't it be better to keep using 4KB pages, but lock the memory using > mlock/mlockall? In my book, not being swappable is a win (it's hard for me to imagine when it could be beneficial to swap out parts of s_b). I was trying to think about it and also got those: Anyway mlock() probably sounds like it, but e.g. Rocky 8.10 by default has max locked memory (ulimit -l) as low as 64kB due to systemd's DefaultLimitMEMLOCK, but Debian/Ubuntu have those at higher values. Wasn't expecting that - those are bizzare low values. I think we would need something like (10000*900)/1024/1024 or more, but with each PGPROC on a separate page that would be even way more? Another thing with 4kB pages: there's this big assumption now made that once we arrive in InitProcess() we won't ever change NUMA node, so we stick to the PGPROC from where we started (based on getcpu(2)). Let's assume CPU scheduler reassigned us to differnt node, but we have now this 4kB patch ready for PGPROC in theory and this means we would need to rely on the NUMA autobalancing doing it's job to migrate that 4kB page from node to node (to get better local accesses instead of remote ones). The questions in my head are now like that: - but we have asked intially asked those PGPROC pages to be localized on certain node (they have policy), so they won't autobalance? We would need to somewhere call getcpu() again notice the difference and unlocalize (clear the NUMA/mbind() policy) for the PGPROC page? - mlocked() as above says stick to physical RAM page (?) , so it won't move? - after what time kernel's autobalancing would migrate that page since switching the active CPU<->node? I mean do we execute enough reads on this page? BTW: to move this into pragmatic real, what's the most one-liner/trivial way to exercise/stress PGPROC? > >> The other thing I haven't thought about very much is determining on > >> which CPUs/nodes the instance is allowed to run. I assume we'd start by > >> simply inherit/determine that at the start through libnuma, not through > >> some custom PG configuration (which the patch [2] proposed to do). > > > > 0. I think that we could do better, some counter arguments to > > no-configuration-at-all: > > > > a. as Robert & Bertrand already put it there after review: let's say I > > want just to run on NUMA #2 node, so here I would need to override > > systemd's script ExecStart= to include that numactl (not elegant?). I > > could also use `CPUAffinity=1,3,5,7..` but that's all, and it is even > > less friendly. Also it probably requires root to edit/reload systemd, > > while having GUC for this like in my proposal makes it more smooth (I > > think?) > > > > b. wouldn't it be better if that stayed as drop-in rather than always > > on? What if there's a problem, how do you disable those internal > > optimizations if they do harm in some cases? (or let's say I want to > > play with MPOL_INTERLEAVE_WEIGHTED?). So at least boolean > > numa_buffers_interleave would be nice? > > > > c. What if I want my standby (walreceiver+startup/recovery) to run > > with NUMA affinity to get better performance (I'm not going to hack > > around systemd script every time, but I could imagine changing > > numa=X,Y,Z after restart/before promotion) > > > > d. Now if I would be forced for some reason to do that numactl(1) > > voodoo, and use the those above mentioned overrides and PG wouldn't be > > having GUC (let's say I would use `numactl > > --weighted-interleave=0,1`), then: > > > > I'm not against doing something like this, but I don't plan to do that > in V1. I don't have a clear idea what configurability is actually > needed, so it's likely I'd do the interface wrong. > > >> 2) v1-0002-NUMA-localalloc.patch > >> This simply sets "localalloc" when initializing a backend, so that all > >> memory allocated later is local, not interleaved. Initially this was > >> necessary because the patch set the allocation policy to interleaving > >> before initializing shared memory, and we didn't want to interleave the > >> private memory. But that's no longer the case - the explicit mapping to > >> nodes does not have this issue. I'm keeping the patch for convenience, > >> it allows experimenting with numactl etc. > > > > .. .is not accurate anymore and we would require to have that in > > (still with GUC) ? > > Thoughts? I can add that mine part into Your's patches if you want. > > > > I'm sorry, I don't understand what's the question :-( That patch reference above, it was a chain of thought from step "d". What I had in mind was that you cannot remove the patch `v1-0002-NUMA-localalloc.patch` from the scope if forcing people to use numactl by not having enough configurability on the PG side. That is: if someone will have to use systemd+numactl --interleave/--weighted-interleave then, he will also need to have a way to use numa_localalloc=on (to override the new/user's policy default, otherwise local mem allocations are also going to be interleaved, and we are back to square one). Which brings me to a point why instead of this toggle, should include the configuration properly inside from start (it's not that hard apparently). > > Way too quick review and some very fast benchmark probes, I've > > concentrated only on v1-0001 and v1-0005 (efficiency of buffermgmt > > would be too new topic for me), but let's start: > > > > 1. normal pgbench -S (still with just s_b@4GB), done many tries, > > consistent benefit for the patch with like +8..10% boost on generic > > run: > > [.. removed numbers] > > But this actually brings an interesting question. What exactly should we > expect / demand from these patches? In my mind it'd primarily about > predictability and stability of results. > > For example, the results should not depend on how was the database > warmed up - was it done by a single backend or many backends? Was it > restarted, or what? I could probably warmup the system very carefully to > ensure it's balanced. The patches mean I don't need to be that careful. Well, pretty much the same here. I was after minimizing "stddev" (to have better predictability of results, especially across restarts) and increasing available bandwidth [which is pretty much related]. Without our NUMA work, PG can just put that s_b on any random node or spill randomly from to another (depending on size of allocation request). > > So should I close https://commitfest.postgresql.org/patch/5703/ > > and you'll open a new one or should I just edit the #5703 and alter it > > and add this thread too? > > > > Good question. It's probably best to close the original entry as > "withdrawn" and I'll add a new entry. Sounds OK? Sure thing, marked it as `Returned with feedback`, this approach seems to be much more advanced. > > 3. Patch is not calling interleave on PQ shmem, do we want to add that > > in as some next item like v1-0007? Question is whether OS interleaving > > makes sense there ? I believe it does there, please see my thread > > (NUMA_pq_cpu_pinning_results.txt), the issue is that PQ workers are > > being spawned by postmaster and may end up on different NUMA nodes > > randomly, so actually OS-interleaving that memory reduces jitter there > > (AKA bandwidth-over-latency). My thinking is that one cannot expect > > static/forced CPU-to-just-one-NUMA-node assignment for backend and > > it's PQ workers, because it is impossible have always available CPU > > power there in that NUMA node, so it might be useful to interleave > > that shared mem there too (as separate patch item?) > > > > Excellent question. I haven't thought about this at all. I agree it > probably makes sense to interleave this memory, in some way. I don't > know what's the perfect scheme, though. > > wild idea: Would it make sense to pin the workers to the same NUMA node > as the leader? And allocate all memory only from that node? I'm trying to convey exactly the opposite message or at least that it might depend on configuration. Please see https://www.postgresql.org/message-id/CAKZiRmxYMPbQ4WiyJWh%3DVuw_Ny%2BhLGH9_9FaacKRJvzZ-smm%2Bw%40mail.gmail.com (btw it should read there that I don't indent spend a lot of thime on PQ), but anyway: I think we should NOT pin the PQ workers the same NODE as you do not know if there's CPU left there (same story as with v1-0006 here). I'm just proposing quick OS-based interleaving of PQ shm if using all nodes, literally: @@ -334,6 +336,13 @@ dsm_impl_posix(dsm_op op, dsm_handle handle, Size request_size, } *mapped_address = address; *mapped_size = request_size; + + /* We interleave memory only at creation time. */ + if (op == DSM_OP_CREATE && numa->setting > NUMA_OFF) { + elog(DEBUG1, "interleaving shm mem @ %p size=%zu", *mapped_address, *mapped_size); + pg_numa_interleave_memptr(*mapped_address, *mapped_size, numa->nodes); + } + Because then if memory is interleaved you have probably less variance for memory access. But also from that previous thread: "So if anything: - latency-wise: it would be best to place leader+all PQ workers close to s_b, provided s_b fits NUMA shared/huge page memory there and you won't need more CPU than there's on that NUMA node... (assuming e.g. hosting 4 DBs on 4-sockets each on it's own, it would be best to pin everything including shm, but PQ workers too) - capacity/TPS-wise or s_b > NUMA: just interleave to maximize bandwidth and get uniform CPU performance out of this" So wild idea was: maybe PQ shm interleaving should on NUMA configuration (if intereavling to all nodes, then interleave normally, but if configuration sets to just 1 NUMA node, it automatically binds there -- there was '@' support for that in my patch). > > 4 In BufferManagerShmemInit() you call numa_num_configured_nodes() > > (also in v1-0005). My worry is should we may put some > > known-limitations docs (?) from start and mention that > > if the VM is greatly resized and NUMA numa nodes appear, they might > > not be used until restart? > > > > Yes, this is one thing I need some feedback on. The patches mostly > assume there are no disabled nodes, that the set of allowed nodes does > not change, etc. I think for V1 that's a reasonable limitation. Sure! > But let's say we want to relax this a bit. How do we learn about the > change, after a node/CPU gets disabled? For some parts it's not that > difficult (e.g. we can "remap" buffers/descriptors) in the background. > But for other parts that's not practical. E.g. we can't rework how the > PGPROC gets split. > > But while discussing this with Andres yesterday, he had an interesting > suggestion - to always use e.g. 8 or 16 partitions, then partition this > by NUMA node. So we'd have 16 partitions, and with 4 nodes the 0-3 would > go to node 0, 4-7 would go to node 1, etc. The advantage is that if a > node gets disabled, we can rebuild just this small "mapping" and not the > 16 partitions. And the partitioning may be helpful even without NUMA. > > Still have to figure out the details, but seems it might help. Right, no idea how the shared_memory remapping patch will work (how/when the s_b change will be executed), but we could somehow mark that number of NUMA zones could be rechecked during SIGHUP (?) and then just simple compare check if old_numa_num_configured_nodes == new_numa_num_configured_nodes is true. Anyway, I think it's way too advanced for now, don't you think? (like CPU ballooning [s_b itself] is rare, and NUMA ballooning seems to be super-wild-rare). As for the rest, forgot to include this too: getcpu() - this really needs a portable pg_getcpu() wrapper. -J. -
Re: Adding basic NUMA awareness - Preliminary feedback and outline for an extensible approach
Cédric Villemain <cedric.villemain@data-bene.io> — 2025-07-07T14:51:00Z
>> * Others might use it to integrate PostgreSQL's own resources (e.g., >> "areas" of shared buffers) into policies. >> >> Hope this perspective is helpful. > > Can you explain how you want to manage this by an extension defined at > the SQL level, when most of this stuff has to be done when setting up > shared memory, which is waaaay before we have any access to catalogs? I should have said module instead, I didn't follow carefully but at some point there were discussion about shared buffers resized "on-line". Anyway, it was just to give some few examples, maybe this one is to be considered later (I'm focused on cgroup/psi, and precisely reassigning PIDs as needed). -- Cédric Villemain +33 6 20 30 22 52 https://www.Data-Bene.io PostgreSQL Support, Expertise, Training, R&D
-
Re: Adding basic NUMA awareness - Preliminary feedback and outline for an extensible approach
Tomas Vondra <tomas@vondra.me> — 2025-07-07T22:35:43Z
On 7/7/25 16:51, Cédric Villemain wrote: >>> * Others might use it to integrate PostgreSQL's own resources (e.g., >>> "areas" of shared buffers) into policies. >>> >>> Hope this perspective is helpful. >> >> Can you explain how you want to manage this by an extension defined at >> the SQL level, when most of this stuff has to be done when setting up >> shared memory, which is waaaay before we have any access to catalogs? > > I should have said module instead, I didn't follow carefully but at some > point there were discussion about shared buffers resized "on-line". > Anyway, it was just to give some few examples, maybe this one is to be > considered later (I'm focused on cgroup/psi, and precisely reassigning > PIDs as needed). > I don't know. I have a hard time imagining what exactly would the policies / profiles do exactly to respond to changes in the system utilization. And why should that interfere with this patch ... The main thing patch series aims to implement is partitioning different pieces of shared memory (buffers, freelists, ...) to better work for NUMA. I don't think there's that many ways to do this, and I doubt it makes sense to make this easily customizable from external modules of any kind. I can imagine providing some API allowing to isolate the instance on selected NUMA nodes, but that's about it. Yes, there's some relation to the online resizing of shared buffers, in which case we need to "refresh" some of the information. But AFAICS it's not very extensive (on top of what already needs to happen after the resize), and it'd happen within the boundaries of the partitioning scheme. There's not that much flexibility. The last bit (pinning backends to a NUMA node) is experimental, and mostly intended for easier evaluation of the earlier parts (e.g. to limit the noise when processes get moved to a CPU from a different NUMA node, and so on). regards -- Tomas Vondra
-
Re: Adding basic NUMA awareness - Preliminary feedback and outline for an extensible approach
Andres Freund <andres@anarazel.de> — 2025-07-07T23:25:35Z
Hi, On 2025-07-05 07:09:00 +0000, Cédric Villemain wrote: > In my work on more careful PostgreSQL resource management, I've come to the > conclusion that we should avoid pushing policy too deeply into the > PostgreSQL core itself. Therefore, I'm quite skeptical about integrating > NUMA-specific management directly into core PostgreSQL in such a way. I think it's actually the opposite - whenever we pushed stuff like this outside of core it has hurt postgres substantially. Not having replication in core was a huge mistake. Not having HA management in core is probably the biggest current adoption hurdle for postgres. To deal better with NUMA we need to improve memory placement and various algorithms, in an interrelated way - that's pretty much impossible to do outside of core. Greetings, Andres Freund
-
Re: Adding basic NUMA awareness - Preliminary feedback and outline for an extensible approach
Cédric Villemain <cedric.villemain@data-bene.io> — 2025-07-08T01:47:00Z
> On 7/7/25 16:51, Cédric Villemain wrote: >>>> * Others might use it to integrate PostgreSQL's own resources (e.g., >>>> "areas" of shared buffers) into policies. >>>> >>>> Hope this perspective is helpful. >>> >>> Can you explain how you want to manage this by an extension defined at >>> the SQL level, when most of this stuff has to be done when setting up >>> shared memory, which is waaaay before we have any access to catalogs? >> >> I should have said module instead, I didn't follow carefully but at some >> point there were discussion about shared buffers resized "on-line". >> Anyway, it was just to give some few examples, maybe this one is to be >> considered later (I'm focused on cgroup/psi, and precisely reassigning >> PIDs as needed). >> > > I don't know. I have a hard time imagining what exactly would the > policies / profiles do exactly to respond to changes in the system > utilization. And why should that interfere with this patch ... > > The main thing patch series aims to implement is partitioning different > pieces of shared memory (buffers, freelists, ...) to better work for > NUMA. I don't think there's that many ways to do this, and I doubt it > makes sense to make this easily customizable from external modules of > any kind. I can imagine providing some API allowing to isolate the > instance on selected NUMA nodes, but that's about it. > > Yes, there's some relation to the online resizing of shared buffers, in > which case we need to "refresh" some of the information. But AFAICS it's > not very extensive (on top of what already needs to happen after the > resize), and it'd happen within the boundaries of the partitioning > scheme. There's not that much flexibility. > > The last bit (pinning backends to a NUMA node) is experimental, and > mostly intended for easier evaluation of the earlier parts (e.g. to > limit the noise when processes get moved to a CPU from a different NUMA > node, and so on). The backend pinning can be done by replacing your patch on proc.c to call an external profile manager doing exactly the same thing maybe ? Similar to: pmroutine = GetPmRoutineForInitProcess(); if (pmroutine != NULL && pmroutine->init_process != NULL) pmroutine->init_process(MyProc); ... pmroutine = GetPmRoutineForInitAuxilliary(); if (pmroutine != NULL && pmroutine->init_auxilliary != NULL) pmroutine->init_auxilliary(MyProc); Added on some rare places should cover most if not all the requirement around process placement (process_shared_preload_libraries() is called earlier in the process creation I believe). -- Cédric Villemain +33 6 20 30 22 52 https://www.Data-Bene.io PostgreSQL Support, Expertise, Training, R&D
-
Re: Adding basic NUMA awareness - Preliminary feedback and outline for an extensible approach
Cédric Villemain <cedric.villemain@data-bene.io> — 2025-07-08T01:55:00Z
Hi Andres, > Hi, > > On 2025-07-05 07:09:00 +0000, Cédric Villemain wrote: >> In my work on more careful PostgreSQL resource management, I've come to the >> conclusion that we should avoid pushing policy too deeply into the >> PostgreSQL core itself. Therefore, I'm quite skeptical about integrating >> NUMA-specific management directly into core PostgreSQL in such a way. > > I think it's actually the opposite - whenever we pushed stuff like this > outside of core it has hurt postgres substantially. Not having replication in > core was a huge mistake. Not having HA management in core is probably the > biggest current adoption hurdle for postgres. > > To deal better with NUMA we need to improve memory placement and various > algorithms, in an interrelated way - that's pretty much impossible to do > outside of core. Except the backend pinning which is easy to achieve, thus my comment on the related patch. I'm not claiming NUMA memory and all should be managed outside of core (though I didn't read other patches yet). -- Cédric Villemain +33 6 20 30 22 52 https://www.Data-Bene.io PostgreSQL Support, Expertise, Training, R&D
-
Re: Adding basic NUMA awareness - Preliminary feedback and outline for an extensible approach
Cédric Villemain <cedric.villemain@data-bene.io> — 2025-07-08T02:14:00Z
>> On 7/7/25 16:51, Cédric Villemain wrote: >>>>> * Others might use it to integrate PostgreSQL's own resources (e.g., >>>>> "areas" of shared buffers) into policies. >>>>> >>>>> Hope this perspective is helpful. >>>> >>>> Can you explain how you want to manage this by an extension defined at >>>> the SQL level, when most of this stuff has to be done when setting up >>>> shared memory, which is waaaay before we have any access to catalogs? >>> >>> I should have said module instead, I didn't follow carefully but at some >>> point there were discussion about shared buffers resized "on-line". >>> Anyway, it was just to give some few examples, maybe this one is to be >>> considered later (I'm focused on cgroup/psi, and precisely reassigning >>> PIDs as needed). >>> >> >> I don't know. I have a hard time imagining what exactly would the >> policies / profiles do exactly to respond to changes in the system >> utilization. And why should that interfere with this patch ... >> >> The main thing patch series aims to implement is partitioning different >> pieces of shared memory (buffers, freelists, ...) to better work for >> NUMA. I don't think there's that many ways to do this, and I doubt it >> makes sense to make this easily customizable from external modules of >> any kind. I can imagine providing some API allowing to isolate the >> instance on selected NUMA nodes, but that's about it. >> >> Yes, there's some relation to the online resizing of shared buffers, in >> which case we need to "refresh" some of the information. But AFAICS it's >> not very extensive (on top of what already needs to happen after the >> resize), and it'd happen within the boundaries of the partitioning >> scheme. There's not that much flexibility. >> >> The last bit (pinning backends to a NUMA node) is experimental, and >> mostly intended for easier evaluation of the earlier parts (e.g. to >> limit the noise when processes get moved to a CPU from a different NUMA >> node, and so on). > > The backend pinning can be done by replacing your patch on proc.c to > call an external profile manager doing exactly the same thing maybe ? > > Similar to: > pmroutine = GetPmRoutineForInitProcess(); > if (pmroutine != NULL && > pmroutine->init_process != NULL) > pmroutine->init_process(MyProc); > > ... > > pmroutine = GetPmRoutineForInitAuxilliary(); > if (pmroutine != NULL && > pmroutine->init_auxilliary != NULL) > pmroutine->init_auxilliary(MyProc); > > Added on some rare places should cover most if not all the requirement > around process placement (process_shared_preload_libraries() is called > earlier in the process creation I believe). > After a first read I think this works for patches 002 and 005. For this last one, InitProcGlobal() may setup things as you do but then expose the choice a bit later, basically in places where you added the if condition on the GUC: numa_procs_interleave). -- Cédric Villemain +33 6 20 30 22 52 https://www.Data-Bene.io PostgreSQL Support, Expertise, Training, R&D
-
Re: Adding basic NUMA awareness
Andres Freund <andres@anarazel.de> — 2025-07-08T03:04:48Z
Hi, On 2025-07-04 13:05:05 +0200, Jakub Wartak wrote: > On Tue, Jul 1, 2025 at 9:07 PM Tomas Vondra <tomas@vondra.me> wrote: > > I don't think the splitting would actually make some things simpler, or > > maybe more flexible - in particular, it'd allow us to enable huge pages > > only for some regions (like shared buffers), and keep the small pages > > e.g. for PGPROC. So that'd be good. > > You have made assumption that this is good, but small pages (4KB) are > not hugetlb, and are *swappable* (Transparent HP are swappable too, > manually allocated ones as with mmap(MMAP_HUGETLB) are not)[1]. The > most frequent problem I see these days are OOMs, and it makes me > believe that making certain critical parts of shared memory being > swappable just to make pagesize granular is possibly throwing the baby > out with the bathwater. I'm thinking about bad situations like: some > wrong settings of vm.swapiness that people keep (or distros keep?) and > general inability of PG to restrain from allocating more memory in > some cases. The reason it would be advantageous to put something like the procarray onto smaller pages is that otherwise the entire procarray (unless particularly large) ends up on a single NUMA node, increasing the latency for backends on every other numa node and increasing memory traffic on that node. Greetings, Andres Freund
-
Re: Adding basic NUMA awareness
Tomas Vondra <tomas@vondra.me> — 2025-07-08T12:27:12Z
On 7/8/25 05:04, Andres Freund wrote: > Hi, > > On 2025-07-04 13:05:05 +0200, Jakub Wartak wrote: >> On Tue, Jul 1, 2025 at 9:07 PM Tomas Vondra <tomas@vondra.me> wrote: >>> I don't think the splitting would actually make some things simpler, or >>> maybe more flexible - in particular, it'd allow us to enable huge pages >>> only for some regions (like shared buffers), and keep the small pages >>> e.g. for PGPROC. So that'd be good. >> >> You have made assumption that this is good, but small pages (4KB) are >> not hugetlb, and are *swappable* (Transparent HP are swappable too, >> manually allocated ones as with mmap(MMAP_HUGETLB) are not)[1]. The >> most frequent problem I see these days are OOMs, and it makes me >> believe that making certain critical parts of shared memory being >> swappable just to make pagesize granular is possibly throwing the baby >> out with the bathwater. I'm thinking about bad situations like: some >> wrong settings of vm.swapiness that people keep (or distros keep?) and >> general inability of PG to restrain from allocating more memory in >> some cases. > > The reason it would be advantageous to put something like the procarray onto > smaller pages is that otherwise the entire procarray (unless particularly > large) ends up on a single NUMA node, increasing the latency for backends on > every other numa node and increasing memory traffic on that node. > That's why the patch series splits the procarray into multiple pieces, so that it can be properly distributed on multiple NUMA nodes even with huge pages. It requires adjusting a couple places accessing the entries, but it surprised me how limited the impact was. If we could selectively use 4KB pages for parts of the shared memory, maybe this wouldn't be necessary. But it's not too annoying. The thing I'm not sure about is how much this actually helps with the traffic between node. Sure, if we pick a PGPROC from the same node, and the task does not get moved, it'll be local traffic. But if the task moves, there'll be traffic. I don't have any estimates how often this happens, e.g. for older tasks. regards -- Tomas Vondra
-
Re: Adding basic NUMA awareness - Preliminary feedback and outline for an extensible approach
Tomas Vondra <tomas@vondra.me> — 2025-07-08T12:34:59Z
On 7/8/25 03:55, Cédric Villemain wrote: > Hi Andres, > >> Hi, >> >> On 2025-07-05 07:09:00 +0000, Cédric Villemain wrote: >>> In my work on more careful PostgreSQL resource management, I've come >>> to the >>> conclusion that we should avoid pushing policy too deeply into the >>> PostgreSQL core itself. Therefore, I'm quite skeptical about integrating >>> NUMA-specific management directly into core PostgreSQL in such a way. >> >> I think it's actually the opposite - whenever we pushed stuff like this >> outside of core it has hurt postgres substantially. Not having >> replication in >> core was a huge mistake. Not having HA management in core is probably the >> biggest current adoption hurdle for postgres. >> >> To deal better with NUMA we need to improve memory placement and various >> algorithms, in an interrelated way - that's pretty much impossible to do >> outside of core. > > Except the backend pinning which is easy to achieve, thus my comment on > the related patch. > I'm not claiming NUMA memory and all should be managed outside of core > (though I didn't read other patches yet). > But an "optimal backend placement" seems to very much depend on where we placed the various pieces of shared memory. Which the external module will have trouble following, I suspect. I still don't have any idea what exactly would the external module do, how would it decide where to place the backend. Can you describe some use case with an example? Assuming we want to actually pin tasks from within Postgres, what I think might work is allowing modules to "advise" on where to place the task. But the decision would still be done by core. regards -- Tomas Vondra
-
Re: Adding basic NUMA awareness
Andres Freund <andres@anarazel.de> — 2025-07-08T12:56:06Z
Hi, On 2025-07-08 14:27:12 +0200, Tomas Vondra wrote: > On 7/8/25 05:04, Andres Freund wrote: > > On 2025-07-04 13:05:05 +0200, Jakub Wartak wrote: > > The reason it would be advantageous to put something like the procarray onto > > smaller pages is that otherwise the entire procarray (unless particularly > > large) ends up on a single NUMA node, increasing the latency for backends on > > every other numa node and increasing memory traffic on that node. > > > > That's why the patch series splits the procarray into multiple pieces, > so that it can be properly distributed on multiple NUMA nodes even with > huge pages. It requires adjusting a couple places accessing the entries, > but it surprised me how limited the impact was. Sure, you can do that, but it does mean that iterations over the procarray now have an added level of indirection... > The thing I'm not sure about is how much this actually helps with the > traffic between node. Sure, if we pick a PGPROC from the same node, and > the task does not get moved, it'll be local traffic. But if the task > moves, there'll be traffic. I don't have any estimates how often this > happens, e.g. for older tasks. I think the most important bit is to not put everything onto one numa node, otherwise the chance of increased latency for *everyone* due to the increased memory contention is more likely to hurt. Greetings, Andres Freund
-
Re: Adding basic NUMA awareness - Preliminary feedback and outline for an extensible approach
Cédric Villemain <cedric.villemain@data-bene.io> — 2025-07-08T16:06:00Z
> On 7/8/25 03:55, Cédric Villemain wrote: >> Hi Andres, >> >>> Hi, >>> >>> On 2025-07-05 07:09:00 +0000, Cédric Villemain wrote: >>>> In my work on more careful PostgreSQL resource management, I've come >>>> to the >>>> conclusion that we should avoid pushing policy too deeply into the >>>> PostgreSQL core itself. Therefore, I'm quite skeptical about integrating >>>> NUMA-specific management directly into core PostgreSQL in such a way. >>> >>> I think it's actually the opposite - whenever we pushed stuff like this >>> outside of core it has hurt postgres substantially. Not having >>> replication in >>> core was a huge mistake. Not having HA management in core is probably the >>> biggest current adoption hurdle for postgres. >>> >>> To deal better with NUMA we need to improve memory placement and various >>> algorithms, in an interrelated way - that's pretty much impossible to do >>> outside of core. >> >> Except the backend pinning which is easy to achieve, thus my comment on >> the related patch. >> I'm not claiming NUMA memory and all should be managed outside of core >> (though I didn't read other patches yet). >> > > But an "optimal backend placement" seems to very much depend on where we > placed the various pieces of shared memory. Which the external module > will have trouble following, I suspect. > > I still don't have any idea what exactly would the external module do, > how would it decide where to place the backend. Can you describe some > use case with an example? > > Assuming we want to actually pin tasks from within Postgres, what I > think might work is allowing modules to "advise" on where to place the > task. But the decision would still be done by core. Possibly exactly what you're doing in proc.c when managing allocation of process, but not hardcoded in postgresql (patches 02, 05 and 06 are good candidates), I didn't get that they require information not available to any process executing code from a module. Parts of your code where you assign/define policy could be in one or more relevant routines of a "numa profile manager", like in an initProcessRoutine(), and registered in pmroutine struct: pmroutine = GetPmRoutineForInitProcess(); if (pmroutine != NULL && pmroutine->init_process != NULL) pmroutine->init_process(MyProc); This way it's easier to manage alternative policies, and also to be able to adjust when hardware and linux kernel changes. -- Cédric Villemain +33 6 20 30 22 52 https://www.Data-Bene.io PostgreSQL Support, Expertise, Training, R&D -
Re: Adding basic NUMA awareness - Preliminary feedback and outline for an extensible approach
Tomas Vondra <tomas@vondra.me> — 2025-07-08T21:26:06Z
On 7/8/25 18:06, Cédric Villemain wrote: > > > > > > >> On 7/8/25 03:55, Cédric Villemain wrote: >>> Hi Andres, >>> >>>> Hi, >>>> >>>> On 2025-07-05 07:09:00 +0000, Cédric Villemain wrote: >>>>> In my work on more careful PostgreSQL resource management, I've come >>>>> to the >>>>> conclusion that we should avoid pushing policy too deeply into the >>>>> PostgreSQL core itself. Therefore, I'm quite skeptical about >>>>> integrating >>>>> NUMA-specific management directly into core PostgreSQL in such a way. >>>> >>>> I think it's actually the opposite - whenever we pushed stuff like this >>>> outside of core it has hurt postgres substantially. Not having >>>> replication in >>>> core was a huge mistake. Not having HA management in core is >>>> probably the >>>> biggest current adoption hurdle for postgres. >>>> >>>> To deal better with NUMA we need to improve memory placement and >>>> various >>>> algorithms, in an interrelated way - that's pretty much impossible >>>> to do >>>> outside of core. >>> >>> Except the backend pinning which is easy to achieve, thus my comment on >>> the related patch. >>> I'm not claiming NUMA memory and all should be managed outside of core >>> (though I didn't read other patches yet). >>> >> >> But an "optimal backend placement" seems to very much depend on where we >> placed the various pieces of shared memory. Which the external module >> will have trouble following, I suspect. >> >> I still don't have any idea what exactly would the external module do, >> how would it decide where to place the backend. Can you describe some >> use case with an example? >> >> Assuming we want to actually pin tasks from within Postgres, what I >> think might work is allowing modules to "advise" on where to place the >> task. But the decision would still be done by core. > > Possibly exactly what you're doing in proc.c when managing allocation of > process, but not hardcoded in postgresql (patches 02, 05 and 06 are good > candidates), I didn't get that they require information not available to > any process executing code from a module. > Well, it needs to understand how some other stuff (especially PGPROC entries) is distributed between nodes. I'm not sure how much of this internal information we want to expose outside core ... > Parts of your code where you assign/define policy could be in one or > more relevant routines of a "numa profile manager", like in an > initProcessRoutine(), and registered in pmroutine struct: > > pmroutine = GetPmRoutineForInitProcess(); > if (pmroutine != NULL && > pmroutine->init_process != NULL) > pmroutine->init_process(MyProc); > > This way it's easier to manage alternative policies, and also to be able > to adjust when hardware and linux kernel changes. > I'm not against making this extensible, in some way. But I still struggle to imagine a reasonable alternative policy, where the external module gets the same information and ends up with a different decision. So what would the alternate policy look like? What use case would the module be supporting? regards -- Tomas Vondra
-
Re: Adding basic NUMA awareness - Preliminary feedback and outline for an extensible approach
Cédric Villemain <cedric.villemain@data-bene.io> — 2025-07-09T06:40:00Z
> On 7/8/25 18:06, Cédric Villemain wrote: >> >> >> >> >> >> >>> On 7/8/25 03:55, Cédric Villemain wrote: >>>> Hi Andres, >>>> >>>>> Hi, >>>>> >>>>> On 2025-07-05 07:09:00 +0000, Cédric Villemain wrote: >>>>>> In my work on more careful PostgreSQL resource management, I've come >>>>>> to the >>>>>> conclusion that we should avoid pushing policy too deeply into the >>>>>> PostgreSQL core itself. Therefore, I'm quite skeptical about >>>>>> integrating >>>>>> NUMA-specific management directly into core PostgreSQL in such a way. >>>>> >>>>> I think it's actually the opposite - whenever we pushed stuff like this >>>>> outside of core it has hurt postgres substantially. Not having >>>>> replication in >>>>> core was a huge mistake. Not having HA management in core is >>>>> probably the >>>>> biggest current adoption hurdle for postgres. >>>>> >>>>> To deal better with NUMA we need to improve memory placement and >>>>> various >>>>> algorithms, in an interrelated way - that's pretty much impossible >>>>> to do >>>>> outside of core. >>>> >>>> Except the backend pinning which is easy to achieve, thus my comment on >>>> the related patch. >>>> I'm not claiming NUMA memory and all should be managed outside of core >>>> (though I didn't read other patches yet). >>>> >>> >>> But an "optimal backend placement" seems to very much depend on where we >>> placed the various pieces of shared memory. Which the external module >>> will have trouble following, I suspect. >>> >>> I still don't have any idea what exactly would the external module do, >>> how would it decide where to place the backend. Can you describe some >>> use case with an example? >>> >>> Assuming we want to actually pin tasks from within Postgres, what I >>> think might work is allowing modules to "advise" on where to place the >>> task. But the decision would still be done by core. >> >> Possibly exactly what you're doing in proc.c when managing allocation of >> process, but not hardcoded in postgresql (patches 02, 05 and 06 are good >> candidates), I didn't get that they require information not available to >> any process executing code from a module. >> > > Well, it needs to understand how some other stuff (especially PGPROC > entries) is distributed between nodes. I'm not sure how much of this > internal information we want to expose outside core ... > >> Parts of your code where you assign/define policy could be in one or >> more relevant routines of a "numa profile manager", like in an >> initProcessRoutine(), and registered in pmroutine struct: >> >> pmroutine = GetPmRoutineForInitProcess(); >> if (pmroutine != NULL && >> pmroutine->init_process != NULL) >> pmroutine->init_process(MyProc); >> >> This way it's easier to manage alternative policies, and also to be able >> to adjust when hardware and linux kernel changes. >> > > I'm not against making this extensible, in some way. But I still > struggle to imagine a reasonable alternative policy, where the external > module gets the same information and ends up with a different decision. > > So what would the alternate policy look like? What use case would the > module be supporting? That's the whole point: there are very distinct usages of PostgreSQL in the field. And maybe not all of them will require the policy defined by PostgreSQL core. May I ask the reverse: what prevent external modules from taking those decisions ? There are already a lot of area where external code can take over PostgreSQL processing, like Neon is doing. There are some very early processing for memory setup that I can see as a current blocker, and here I'd refer a more compliant NUMA api as proposed by Jakub so it's possible to arrange based on workload, hardware configuration or other matters. Reworking to get distinct segment and all as you do is great, and combo of both approach probably of great interest. There is also this weighted interleave discussed and probably much more to come in this area in Linux. I think some points raised already about possible distinct policies, I am precisely claiming that it is hard to come with one good policy with limited setup options, thus requirement to keep that flexible enough (hooks, api, 100 GUc ?). There is an EPYC story here also, given the NUMA setup can vary depending on BIOS setup, associated NUMA policy must probably take that into account (L3 can be either real cache or 4 extra "local" NUMA nodes - with highly distinct access cost from a RAM module). Does that change how PostgreSQL will place memory and process? Is it important or of interest ? -- Cédric Villemain +33 6 20 30 22 52 https://www.Data-Bene.io PostgreSQL Support, Expertise, Training, R&D
-
Re: Adding basic NUMA awareness - Preliminary feedback and outline for an extensible approach
Bertrand Drouvot <bertranddrouvot.pg@gmail.com> — 2025-07-09T08:09:16Z
Hi, On Wed, Jul 09, 2025 at 06:40:00AM +0000, Cédric Villemain wrote: > > On 7/8/25 18:06, Cédric Villemain wrote: > > I'm not against making this extensible, in some way. But I still > > struggle to imagine a reasonable alternative policy, where the external > > module gets the same information and ends up with a different decision. > > > > So what would the alternate policy look like? What use case would the > > module be supporting? > > > That's the whole point: there are very distinct usages of PostgreSQL in the > field. And maybe not all of them will require the policy defined by > PostgreSQL core. > > May I ask the reverse: what prevent external modules from taking those > decisions ? There are already a lot of area where external code can take > over PostgreSQL processing, like Neon is doing. > > There are some very early processing for memory setup that I can see as a > current blocker, and here I'd refer a more compliant NUMA api as proposed by > Jakub so it's possible to arrange based on workload, hardware configuration > or other matters. Reworking to get distinct segment and all as you do is > great, and combo of both approach probably of great interest. I think that Tomas's approach helps to have more "predictable" performance expectations, I mean more consistent over time, fewer "surprises". While your approach (and Jakub's one)) could help to get performance gains depending on a "known" context (so less generic). So, probably having both could make sense but I think that they serve different purposes. Regards, -- Bertrand Drouvot PostgreSQL Contributors Team RDS Open Source Databases Amazon Web Services: https://aws.amazon.com
-
Re: Adding basic NUMA awareness
Jakub Wartak <jakub.wartak@enterprisedb.com> — 2025-07-09T10:04:00Z
On Tue, Jul 8, 2025 at 2:56 PM Andres Freund <andres@anarazel.de> wrote: > > Hi, > > On 2025-07-08 14:27:12 +0200, Tomas Vondra wrote: > > On 7/8/25 05:04, Andres Freund wrote: > > > On 2025-07-04 13:05:05 +0200, Jakub Wartak wrote: > > > The reason it would be advantageous to put something like the procarray onto > > > smaller pages is that otherwise the entire procarray (unless particularly > > > large) ends up on a single NUMA node, increasing the latency for backends on > > > every other numa node and increasing memory traffic on that node. > > > Sure thing, I fully understand the motivation and underlying reason (without claiming that I understand the exact memory access patterns that involve procarray/PGPROC/etc and hotspots involved from PG side). Any single-liner pgbench help for how to really easily stress the PGPROC or procarray? > > That's why the patch series splits the procarray into multiple pieces, > > so that it can be properly distributed on multiple NUMA nodes even with > > huge pages. It requires adjusting a couple places accessing the entries, > > but it surprised me how limited the impact was. Yes, and we are discussing if it is worth getting into smaller pages for such usecases (e.g. 4kB ones without hugetlb with 2MB hugepages or what more even more waste 1GB hugetlb if we dont request 2MB for some small structs: btw, we have ability to select MAP_HUGE_2MB vs MAP_HUGE_1GB). I'm thinking about two problems: - 4kB are swappable and mlock() potentially (?) disarms NUMA autobalacning - using libnuma often leads to MPOL_BIND which disarms NUMA autobalancing, BUT apparently there are set_mempolicy(2)/mbind(2) and since 5.12+ kernel they can take additional flag MPOL_F_NUMA_BALANCING(!), so this looks like it has potential to move memory anyway (if way too many tasks are relocated, so would be memory?). It is available only in recent libnuma as numa_set_membind_balancing(3), but sadly there's no way via libnuma to do mbind(MPOL_F_NUMA_BALANCING) for a specific addr only? I mean it would have be something like MPOL_F_NUMA_BALANCING | MPOL_PREFERRED? (select one node from many for each node while still allowing balancing?), but in [1][2] (2024) it is stated that "It's not legitimate (yet) to use MPOL_PREFERRED + MPOL_F_NUMA_BALANCING.", but maybe stuff has been improved since then. Something like: PGPROC/procarray 2MB page for node#1 - mbind(addr1, MPOL_F_NUMA_BALANCING | MPOL_PREFERRED, [0,1]); PGPROC/procarray 2MB page for node#2 - mbind(addr2, MPOL_F_NUMA_BALANCING | MPOL_PREFERRED, [1,0]); > Sure, you can do that, but it does mean that iterations over the procarray now > have an added level of indirection... So the most efficient would be the old-way (no indirections) vs NUMA-way? Can this be done without #ifdefs at all? > > The thing I'm not sure about is how much this actually helps with the > > traffic between node. Sure, if we pick a PGPROC from the same node, and > > the task does not get moved, it'll be local traffic. But if the task > > moves, there'll be traffic. With MPOL_F_NUMA_BALANCING, that should "auto-tune" in the worst case? > > I don't have any estimates how often this happens, e.g. for older tasks. We could measure, kernel 6.16+ has per PID numa_task_migrated in /proc/{PID}/sched , but I assume we would have to throw backends >> VCPUs at it, to simulate reality and do some "waves" between different activity periods of certain pools (I can imagine worst case scenario: a) pgbench "a" open $VCPU connections, all idle, with scripto to sleep for a while b) pgbench "b" open some $VCPU new connections to some other DB, all active from start (tpcbb or readonly) c) manually ping CPUs using taskset for each PID all from "b" to specific NUMA node #2 -- just to simulate unfortunate app working on every 2nd conn d) pgbench "a" starts working and hits CPU imbalance -- e.g. NUMA node #1 is idle, #2 is full, CPU scheduler starts puting "a" backends on CPUs from #1 , and we should notice PIDs being migrated) > I think the most important bit is to not put everything onto one numa node, > otherwise the chance of increased latency for *everyone* due to the increased > memory contention is more likely to hurt. -J. p.s. I hope i did write in an understandable way, because I had many interruptions, so if anything is unclear please let me know. [1] - https://lkml.org/lkml/2024/7/3/352 [2] - https://lkml.rescloud.iu.edu/2402.2/03227.html -
Re: Adding basic NUMA awareness
Andres Freund <andres@anarazel.de> — 2025-07-09T16:35:13Z
Hi, On 2025-07-02 14:36:31 +0200, Tomas Vondra wrote: > On 7/2/25 13:37, Ashutosh Bapat wrote: > > On Wed, Jul 2, 2025 at 12:37 AM Tomas Vondra <tomas@vondra.me> wrote: > >> > >> > >> 3) v1-0003-freelist-Don-t-track-tail-of-a-freelist.patch > >> > >> Minor optimization. Andres noticed we're tracking the tail of buffer > >> freelist, without using it. So the patch removes that. > >> > > > > The patches for resizing buffers use the lastFreeBuffer to add new > > buffers to the end of free list when expanding it. But we could as > > well add it at the beginning of the free list. Yea, I don't see any point in adding buffers to the tail instead of to the front. We probably want more recently used buffers at the front, since they (and the associated BufferDesc) are more likely to be in a CPU cache. > > This patch seems almost independent of the rest of the patches. Do you > > need it in the rest of the patches? I understand that those patches > > don't need to worry about maintaining lastFreeBuffer after this patch. > > Is there any other effect? > > > > If we are going to do this, let's do it earlier so that buffer > > resizing patches can be adjusted. > > > > My patches don't particularly rely on this bit, it would work even with > lastFreeBuffer. I believe Andres simply noticed the current code does > not use lastFreeBuffer, it just maintains is, so he removed that as an > optimization. Optimiziation / simplification. When building multiple freelists it was harder to maintain the tail pointer, and since it was never used... +1 to just applying that part. > I don't know how significant is the improvement, but if it's measurable we > could just do that independently of our patches. I doubt it's really an improvement in any realistic scenario, but it's also not a regression in any way, since it's never used... FWIW, I've started to wonder if we shouldn't just get rid of the freelist entirely. While clocksweep is perhaps minutely slower in a single thread than the freelist, clock sweep scales *considerably* better [1]. As it's rather rare to be bottlenecked on clock sweep speed for a single thread (rather then IO or memory copy overhead), I think it's worth favoring clock sweep. Also needing to switch between getting buffers from the freelist and the sweep makes the code more expensive. I think just having the buffer in the sweep, with a refcount / usagecount of zero would suffice. That seems particularly advantageous if we invest energy in making the clock sweep deal well with NUMA systems, because we don't need have both a NUMA aware freelist and a NUMA aware clock sweep. Greetings, Andres Freund [1] A single pg_prewarm of a large relation shows a difference between using the freelist and not that's around the noise level, whereas 40 parallel pg_prewarms of seperate relations is over 5x faster when disabling the freelist. For the test: - I modified pg_buffercache_evict_* to put buffers onto the freelist - Ensured all of shared buffers is allocated by querying pg_shmem_allocations_numa, as otherwise the workload is dominated by the kernel zeroing out buffers - used shared_buffers bigger than the data - data for single threaded is 9.7GB, data for the parallel case is 40 relations of 610MB each. - in the single threaded case I pinned postgres to a single core, to make sure core-to-core variation doesn't play a role - single threaded case c=1 && psql -Xq -c "select pg_buffercache_evict_all()" -c 'SELECT numa_node, sum(size), count(*) FROM pg_shmem_allocations_numa WHERE size != 0 GROUP BY numa_node;' && pgbench -n -P1 -c$c -j$c -f <(echo "SELECT pg_prewarm('copytest_large');") -t1 concurrent case: c=40 && psql -Xq -c "select pg_buffercache_evict_all()" -c 'SELECT numa_node, sum(size), count(*) FROM pg_shmem_allocations_numa WHERE size != 0 GROUP BY numa_node;' && pgbench -n -P1 -c$c -j$c -f <(echo "SELECT pg_prewarm('copytest_:client_id');") -t1 -
Re: Adding basic NUMA awareness
Greg Burd <greg@burd.me> — 2025-07-09T16:55:51Z
On Jul 9 2025, at 12:35 pm, Andres Freund <andres@anarazel.de> wrote: > FWIW, I've started to wonder if we shouldn't just get rid of the freelist > entirely. While clocksweep is perhaps minutely slower in a single > thread than > the freelist, clock sweep scales *considerably* better [1]. As it's rather > rare to be bottlenecked on clock sweep speed for a single thread > (rather then > IO or memory copy overhead), I think it's worth favoring clock sweep. Hey Andres, thanks for spending time on this. I've worked before on freelist implementations (last one in LMDB) and I think you're onto something. I think it's an innovative idea and that the speed difference will either be lost in the noise or potentially entirely mitigated by avoiding duplicate work. > Also needing to switch between getting buffers from the freelist and > the sweep > makes the code more expensive. I think just having the buffer in the sweep, > with a refcount / usagecount of zero would suffice. If you're not already coding this, I'll jump in. :) > That seems particularly advantageous if we invest energy in making the clock > sweep deal well with NUMA systems, because we don't need have both a NUMA > aware freelist and a NUMA aware clock sweep. 100% agree here, very clever approach adapting clock sweep to a NUMA world. best. -greg > > Greetings, > > Andres Freund
-
Re: Adding basic NUMA awareness
Andres Freund <andres@anarazel.de> — 2025-07-09T17:13:04Z
Hi, On 2025-07-09 12:04:00 +0200, Jakub Wartak wrote: > On Tue, Jul 8, 2025 at 2:56 PM Andres Freund <andres@anarazel.de> wrote: > > On 2025-07-08 14:27:12 +0200, Tomas Vondra wrote: > > > On 7/8/25 05:04, Andres Freund wrote: > > > > On 2025-07-04 13:05:05 +0200, Jakub Wartak wrote: > > > > The reason it would be advantageous to put something like the procarray onto > > > > smaller pages is that otherwise the entire procarray (unless particularly > > > > large) ends up on a single NUMA node, increasing the latency for backends on > > > > every other numa node and increasing memory traffic on that node. > > > > > > Sure thing, I fully understand the motivation and underlying reason > (without claiming that I understand the exact memory access patterns > that involve procarray/PGPROC/etc and hotspots involved from PG side). > Any single-liner pgbench help for how to really easily stress the > PGPROC or procarray? Unfortunately it's probably going to be slightly more complicated workloads that show the effect - the very simplest cases don't go iterate through the procarray itself anymore. > > > That's why the patch series splits the procarray into multiple pieces, > > > so that it can be properly distributed on multiple NUMA nodes even with > > > huge pages. It requires adjusting a couple places accessing the entries, > > > but it surprised me how limited the impact was. > > Yes, and we are discussing if it is worth getting into smaller pages > for such usecases (e.g. 4kB ones without hugetlb with 2MB hugepages or > what more even more waste 1GB hugetlb if we dont request 2MB for some > small structs: btw, we have ability to select MAP_HUGE_2MB vs > MAP_HUGE_1GB). I'm thinking about two problems: > - 4kB are swappable and mlock() potentially (?) disarms NUMA autobalacning I'm not really bought into this being a problem. If your system has enough pressure to swap out the PGPROC array, you're so hosed that this won't make a difference. > - using libnuma often leads to MPOL_BIND which disarms NUMA > autobalancing, BUT apparently there are set_mempolicy(2)/mbind(2) and > since 5.12+ kernel they can take additional flag > MPOL_F_NUMA_BALANCING(!), so this looks like it has potential to move > memory anyway (if way too many tasks are relocated, so would be > memory?). It is available only in recent libnuma as > numa_set_membind_balancing(3), but sadly there's no way via libnuma to > do mbind(MPOL_F_NUMA_BALANCING) for a specific addr only? I mean it > would have be something like MPOL_F_NUMA_BALANCING | MPOL_PREFERRED? > (select one node from many for each node while still allowing > balancing?), but in [1][2] (2024) it is stated that "It's not > legitimate (yet) to use MPOL_PREFERRED + MPOL_F_NUMA_BALANCING.", but > maybe stuff has been improved since then. > > Something like: > PGPROC/procarray 2MB page for node#1 - mbind(addr1, > MPOL_F_NUMA_BALANCING | MPOL_PREFERRED, [0,1]); > PGPROC/procarray 2MB page for node#2 - mbind(addr2, > MPOL_F_NUMA_BALANCING | MPOL_PREFERRED, [1,0]); I'm rather doubtful that it's a good idea to combine numa awareness with numa balancing. Numa balancing adds latency and makes it much more expensive for userspace to act in a numa aware way, since it needs to regularly update its knowledge about where memory resides. > > Sure, you can do that, but it does mean that iterations over the procarray now > > have an added level of indirection... > > So the most efficient would be the old-way (no indirections) vs > NUMA-way? Can this be done without #ifdefs at all? If we used 4k pages for the procarray we would just have ~4 procs on one page, if that range were marked as interleaved, it'd probably suffice. > > > The thing I'm not sure about is how much this actually helps with the > > > traffic between node. Sure, if we pick a PGPROC from the same node, and > > > the task does not get moved, it'll be local traffic. But if the task > > > moves, there'll be traffic. > > With MPOL_F_NUMA_BALANCING, that should "auto-tune" in the worst case? I doubt that NUMA balancing is going to help a whole lot here, there are too many procs on one page for that to be helpful. One thing that might be worth doing is to *increase* the size of PGPROC by moving other pieces of data that are keyed by ProcNumber into PGPROC. I think the main thing to avoid is the case where all of PGPROC, buffer mapping table, ... resides on one NUMA node (e.g. because it's the one postmaster was scheduled on), as the increased memory traffic will lead to queries on that node being slower than the other node. Greetings, Andres Freund
-
Re: Adding basic NUMA awareness
Andres Freund <andres@anarazel.de> — 2025-07-09T17:23:06Z
Hi, On 2025-07-09 12:55:51 -0400, Greg Burd wrote: > On Jul 9 2025, at 12:35 pm, Andres Freund <andres@anarazel.de> wrote: > > > FWIW, I've started to wonder if we shouldn't just get rid of the freelist > > entirely. While clocksweep is perhaps minutely slower in a single > > thread than > > the freelist, clock sweep scales *considerably* better [1]. As it's rather > > rare to be bottlenecked on clock sweep speed for a single thread > > (rather then > > IO or memory copy overhead), I think it's worth favoring clock sweep. > > Hey Andres, thanks for spending time on this. I've worked before on > freelist implementations (last one in LMDB) and I think you're onto > something. I think it's an innovative idea and that the speed > difference will either be lost in the noise or potentially entirely > mitigated by avoiding duplicate work. Agreed. FWIW, just using clock sweep actually makes things like DROP TABLE perform better because it doesn't need to maintain the freelist anymore... > > Also needing to switch between getting buffers from the freelist and > > the sweep > > makes the code more expensive. I think just having the buffer in the sweep, > > with a refcount / usagecount of zero would suffice. > > If you're not already coding this, I'll jump in. :) My experimental patch is literally a four character addition ;), namely adding "0 &&" to the relevant code in StrategyGetBuffer(). Obviously a real patch would need to do some more work than that. Feel free to take on that project, I am not planning on tackling that in near term. There's other things around this that could use some attention. It's not hard to see clock sweep be a bottleneck in concurrent workloads - partially due to the shared maintenance of the clock hand. A NUMAed clock sweep would address that. However, we also maintain StrategyControl->numBufferAllocs, which is a significant contention point and would not necessarily be removed by a NUMAificiation of the clock sweep. Greetings, Andres Freund
-
Re: Adding basic NUMA awareness - Preliminary feedback and outline for an extensible approach
Andres Freund <andres@anarazel.de> — 2025-07-09T17:57:36Z
Hi, On 2025-07-08 16:06:00 +0000, Cédric Villemain wrote: > > Assuming we want to actually pin tasks from within Postgres, what I > > think might work is allowing modules to "advise" on where to place the > > task. But the decision would still be done by core. > > Possibly exactly what you're doing in proc.c when managing allocation of > process, but not hardcoded in postgresql (patches 02, 05 and 06 are good > candidates), I didn't get that they require information not available to any > process executing code from a module. > Parts of your code where you assign/define policy could be in one or more > relevant routines of a "numa profile manager", like in an > initProcessRoutine(), and registered in pmroutine struct: > > pmroutine = GetPmRoutineForInitProcess(); > if (pmroutine != NULL && > pmroutine->init_process != NULL) > pmroutine->init_process(MyProc); > > This way it's easier to manage alternative policies, and also to be able to > adjust when hardware and linux kernel changes. I am doubtful this makes sense - as you can see patch 05 needs to change a fair bit of core code to make this work, there's no way we can delegate much of that to an extension. But even if it's doable, I think it's *very* premature to focus on such extensibility at this point - we need to get the basics into a mergeable state, if you then want to argue for adding extensibility, we can do that at this stage. Trying to design this for extensibility from the get go, where that extensibility is very unlikely to be used widely, seems rather likely to just tank this entire project without getting us anything in return. Greetings, Andres Freund
-
Re: Adding basic NUMA awareness
Andres Freund <andres@anarazel.de> — 2025-07-09T19:42:26Z
Hi, Thanks for working on this! I think it's an area we have long neglected... On 2025-07-01 21:07:00 +0200, Tomas Vondra wrote: > Each patch has a numa_ GUC, intended to enable/disable that part. This > is meant to make development easier, not as a final interface. I'm not > sure how exactly that should look. It's possible some combinations of > GUCs won't work, etc. Wonder if some of it might be worth putting into a multi-valued GUC (like debug_io_direct). > 1) v1-0001-NUMA-interleaving-buffers.patch > > This is the main thing when people think about NUMA - making sure the > shared buffers are allocated evenly on all the nodes, not just on a > single node (which can happen easily with warmup). The regular memory > interleaving would address this, but it also has some disadvantages. > > Firstly, it's oblivious to the contents of the shared memory segment, > and we may not want to interleave everything. It's also oblivious to > alignment of the items (a buffer can easily end up "split" on multiple > NUMA nodes), or relationship between different parts (e.g. there's a > BufferBlock and a related BufferDescriptor, and those might again end up > on different nodes). Two more disadvantages: With OS interleaving postgres doesn't (not easily at least) know about what maps to what, which means postgres can't do stuff like numa aware buffer replacement. With OS interleaving the interleaving is "too fine grained", with pages being mapped at each page boundary, making it less likely for things like one strategy ringbuffer to reside on a single numa node. I wonder if we should *increase* the size of shared_buffers whenever huge pages are in use and there's padding space due to the huge page boundaries. Pretty pointless to waste that memory if we can instead use if for the buffer pool. Not that big a deal with 2MB huge pages, but with 1GB huge pages... > 4) v1-0004-NUMA-partition-buffer-freelist.patch > > Right now we have a single freelist, and in busy instances that can be > quite contended. What's worse, the freelist may trash between different > CPUs, NUMA nodes, etc. So the idea is to have multiple freelists on > subsets of buffers. The patch implements multiple strategies how the > list can be split (configured using "numa_partition_freelist" GUC), for > experimenting: > > * node - One list per NUMA node. This is the most natural option, > because we now know which buffer is on which node, so we can ensure a > list for a node only has buffers from that list. > > * cpu - One list per CPU. Pretty simple, each CPU gets it's own list. > > * pid - Similar to "cpu", but the processes are mapped to lists based on > PID, not CPU ID. > > * none - nothing, sigle freelist > > Ultimately, I think we'll want to go with "node", simply because it > aligns with the buffer interleaving. But there are improvements needed. I think we might eventually want something more granular than just "node" - the freelist (and the clock sweep) can become a contention point even within one NUMA node. I'm imagining something like an array of freelists/clocksweep states, where the current numa node selects a subset of the array and the cpu is used to choose the entry within that list. But we can do that later, that should be a fairly simple extension of what you're doing. > The other missing part is clocksweep - there's still just a single > instance of clocksweep, feeding buffers to all the freelists. But that's > clearly a problem, because the clocksweep returns buffers from all NUMA > nodes. The clocksweep really needs to be partitioned the same way as a > freelists, and each partition will operate on a subset of buffers (from > the right NUMA node). > > I do have a separate experimental patch doing something like that, I > need to make it part of this branch. I'm really curious about that patch, as I wrote elsewhere in this thread, I think we should just get rid of the freelist alltogether. Even if we don't do so, in a steady state system the clock sweep is commonly much more important than the freelist... > 5) v1-0005-NUMA-interleave-PGPROC-entries.patch > > Another area that seems like it might benefit from NUMA is PGPROC, so I > gave it a try. It turned out somewhat challenging. Similarly to buffers > we have two pieces that need to be located in a coordinated way - PGPROC > entries and fast-path arrays. But we can't use the same approach as for > buffers/descriptors, because > > (a) Neither of those pieces aligns with memory page size (PGPROC is > ~900B, fast-path arrays are variable length). > (b) We could pad PGPROC entries e.g. to 1KB, but that'd still require > rather high max_connections before we use multiple huge pages. We should probably pad them regardless? Right now sizeof(PGPROC) happens to be multiple of 64 (i.e. the most common cache line size), but that hasn't always been the case, and isn't the case on systems with 128 bit cachelines like common ARMv8 systems. And having one cacheline hold one backends fast path states and another backend's xmin doesn't sound like a recipe for good performance. Seems like we should also do some reordering of the contents within PGPROC. We have e.g. have very frequently changing data (->waitStatus, ->lwWaiting) in the same caceheline as almost immutable data (->pid, ->pgxactoff, ->databaseId,). > So what I did instead is splitting the whole PGPROC array into one array > per NUMA node, and one array for auxiliary processes and 2PC xacts. So > with 4 NUMA nodes there are 5 separate arrays, for example. Each array > is a multiple of memory pages, so we may waste some of the memory. But > that's simply how NUMA works - page granularity. Theoretically we could use the "padding" memory at the end of each NUMA node's PGPROC array to for the 2PC entries, for those we presumably don't care for locality. Not sure it's worth the complexity though. For a while I thought I had a better solution: Given that we're going to waste all the "padding" memory, why not just oversize the PGPROC array so that it spans the required number of NUMA nodes? The problem is that that would lead to ProcNumbers to get much larger, and we do have other arrays that are keyed by ProcNumber. Which probably makes this not so great an idea. > This however makes one particular thing harder - in a couple places we > accessed PGPROC entries through PROC_HDR->allProcs, which was pretty > much just one large array. And GetNumberFromPGProc() relied on array > arithmetics to determine procnumber. With the array partitioned, this > can't work the same way. > > But there's a simple solution - if we turn allProcs into an array of > *pointers* to PGPROC arrays, there's no issue. All the places need a > pointer anyway. And then we need an explicit procnumber field in PGPROC, > instead of calculating it. > > There's a chance this have negative impact on code that accessed PGPROC > very often, but so far I haven't seen such cases. But if you can come up > with such examples, I'd like to see those. I'd not be surprised if there were overhead, adding a level of indirection to things like ProcArrayGroupClearXid(), GetVirtualXIDsDelayingChkpt(), SignalVirtualTransaction() probably won't be free. BUT: For at least some of these a better answer might be to add additional "dense" arrays like we have for xids etc, so they don't need to trawl through PGPROCs. > There's another detail - when obtaining a PGPROC entry in InitProcess(), > we try to get an entry from the same NUMA node. And only if that doesn't > work, we grab the first one from the list (there's still just one PGPROC > freelist, I haven't split that - maybe we should?). I guess it might be worth partitioning the freelist, iterating through a few thousand links just to discover that there's no free proc on the current numa node, while holding a spinlock, doesn't sound great. Even if it's likely rarely a huge issue compared to other costs. > The other thing I haven't thought about very much is determining on > which CPUs/nodes the instance is allowed to run. I assume we'd start by > simply inherit/determine that at the start through libnuma, not through > some custom PG configuration (which the patch [2] proposed to do). That seems like the right thing to me. One thing that this patchset afaict doesn't address so far is that there is a fair bit of other important shared memory that this patch doesn't set up intelligently e.g. the buffer mapping table itself (but there are loads of other cases). Because we touch a lot of that memory during startup, most it will be allocated on whatever NUMA node postmaster was scheduled. I suspect that the best we can do for parts of shared memory where we don't have explicit NUMA awareness is to default to an interleave policy. > From 9712e50d6d15c18ea2c5fcf457972486b0d4ef53 Mon Sep 17 00:00:00 2001 > From: Tomas Vondra <tomas@vondra.me> > Date: Tue, 6 May 2025 21:12:21 +0200 > Subject: [PATCH v1 1/6] NUMA: interleaving buffers > > Ensure shared buffers are allocated from all NUMA nodes, in a balanced > way, instead of just using the node where Postgres initially starts, or > where the kernel decides to migrate the page, etc. With pre-warming > performed by a single backend, this can easily result in severely > unbalanced memory distribution (with most from a single NUMA node). > > The kernel would eventually move some of the memory to other nodes > (thanks to zone_reclaim), but that tends to take a long time. So this > patch improves predictability, reduces the time needed for warmup > during benchmarking, etc. It's less dependent on what the CPU > scheduler does, etc. FWIW, I don't think zone_reclaim_mode will commonly do that? Even if enabled, which I don't think it is anymore by default. At least huge pages can't be reclaimed by the kernel, but even when not using huge pages, I think the only scenario where that would happen is if shared_buffers were swapped out. Numa balancing might eventually "fix" such an imbalance though. > diff --git a/src/backend/storage/buffer/buf_init.c b/src/backend/storage/buffer/buf_init.c > index ed1dc488a42..2ad34624c49 100644 > --- a/src/backend/storage/buffer/buf_init.c > +++ b/src/backend/storage/buffer/buf_init.c > @@ -14,9 +14,17 @@ > */ > #include "postgres.h" > > +#ifdef USE_LIBNUMA > +#include <numa.h> > +#include <numaif.h> > +#endif > + I wonder how much of this we should try to put into port/pg_numa.c. Having direct calls to libnuma code all over the backend will make it rather hard to add numa awareness for hypothetical platforms not using libnuma compatible interfaces. > +/* number of buffers allocated on the same NUMA node */ > +static int64 numa_chunk_buffers = -1; Given that NBuffers is a 32bit quantity, this probably doesn't need to be 64bit... Anyway, I'm not going to review on that level going forward, the patch is probably in too early a state for that. > @@ -71,18 +92,80 @@ BufferManagerShmemInit(void) > foundDescs, > foundIOCV, > foundBufCkpt; > + Size mem_page_size; > + Size buffer_align; > + > + /* > + * XXX A bit weird. Do we need to worry about postmaster? Could this even > + * run outside postmaster? I don't think so. It can run in single user mode - but that shouldn't prevent us from using pg_get_shmem_pagesize(). > + * XXX Another issue is we may get different values than when sizing the > + * the memory, because at that point we didn't know if we get huge pages, > + * so we assumed we will. Shouldn't cause crashes, but we might allocate > + * shared memory and then not use some of it (because of the alignment > + * that we don't actually need). Not sure about better way, good for now. > + */ Ugh, not seeing a great way to deal with that either. > + * XXX Maybe with (mem_page_size > PG_IO_ALIGN_SIZE), we don't need to > + * align to mem_page_size? Especially for very large huge pages (e.g. 1GB) > + * that doesn't seem quite worth it. Maybe we should simply align to > + * BLCKSZ, so that buffers don't get split? Still, we might interfere with > + * other stuff stored in shared memory that we want to allocate on a > + * particular NUMA node (e.g. ProcArray). > + * > + * XXX Maybe with "too large" huge pages we should just not do this, or > + * maybe do this only for sufficiently large areas (e.g. shared buffers, > + * but not ProcArray). I think that's right - there's no point in using 1GB pages for anything other than shared_buffers, we should allocate shared_buffers separately. > +/* > + * Determine the size of memory page. > + * > + * XXX This is a bit tricky, because the result depends at which point we call > + * this. Before the allocation we don't know if we succeed in allocating huge > + * pages - but we have to size everything for the chance that we will. And then > + * if the huge pages fail (with 'huge_pages=try'), we'll use the regular memory > + * pages. But at that point we can't adjust the sizing. > + * > + * XXX Maybe with huge_pages=try we should do the sizing twice - first with > + * huge pages, and if that fails, then without them. But not for this patch. > + * Up to this point there was no such dependency on huge pages. Doing it twice sounds somewhat nasty - but perhaps we could just have the shmem size infrastructure compute two different numbers, one for use with huge pages and one without? > +static int64 > +choose_chunk_buffers(int NBuffers, Size mem_page_size, int num_nodes) > +{ > + int64 num_items; > + int64 max_items; > + > + /* make sure the chunks will align nicely */ > + Assert(BLCKSZ % sizeof(BufferDescPadded) == 0); > + Assert(mem_page_size % sizeof(BufferDescPadded) == 0); > + Assert(((BLCKSZ % mem_page_size) == 0) || ((mem_page_size % BLCKSZ) == 0)); > + > + /* > + * The minimum number of items to fill a memory page with descriptors and > + * blocks. The NUMA allocates memory in pages, and we need to do that for > + * both buffers and descriptors. > + * > + * In practice the BLCKSZ doesn't really matter, because it's much larger > + * than BufferDescPadded, so the result is determined buffer descriptors. > + * But it's clearer this way. > + */ > + num_items = Max(mem_page_size / sizeof(BufferDescPadded), > + mem_page_size / BLCKSZ); > + > + /* > + * We shouldn't use chunks larger than NBuffers/num_nodes, because with > + * larger chunks the last NUMA node would end up with much less memory (or > + * no memory at all). > + */ > + max_items = (NBuffers / num_nodes); > + > + /* > + * Did we already exceed the maximum desirable chunk size? That is, will > + * the last node get less than one whole chunk (or no memory at all)? > + */ > + if (num_items > max_items) > + elog(WARNING, "choose_chunk_buffers: chunk items exceeds max (%ld > %ld)", > + num_items, max_items); > + > + /* grow the chunk size until we hit the max limit. */ > + while (2 * num_items <= max_items) > + num_items *= 2; Something around this logic leads to a fair bit of imbalance - I started postgres with huge_page_size=1GB, shared_buffers=4GB on a 2 node system and that results in postgres[4188255][1]=# SELECT * FROM pg_shmem_allocations_numa WHERE name in ('Buffer Blocks', 'Buffer Descriptors'); ┌────────────────────┬───────────┬────────────┐ │ name │ numa_node │ size │ ├────────────────────┼───────────┼────────────┤ │ Buffer Blocks │ 0 │ 5368709120 │ │ Buffer Blocks │ 1 │ 1073741824 │ │ Buffer Descriptors │ 0 │ 1073741824 │ │ Buffer Descriptors │ 1 │ 1073741824 │ └────────────────────┴───────────┴────────────┘ (4 rows) With shared_buffers=8GB postgres failed to start, even though 16 1GB huge pages are available, as 18GB were requested. After increasing the limit, the top allocations were as follows: postgres[4189384][1]=# SELECT * FROM pg_shmem_allocations ORDER BY allocated_size DESC LIMIT 5; ┌──────────────────────┬─────────────┬────────────┬────────────────┐ │ name │ off │ size │ allocated_size │ ├──────────────────────┼─────────────┼────────────┼────────────────┤ │ Buffer Blocks │ 1192223104 │ 9663676416 │ 9663676416 │ │ PGPROC structures │ 10970279808 │ 3221733342 │ 3221733376 │ │ Fast-Path Lock Array │ 14192013184 │ 3221396544 │ 3221396608 │ │ Buffer Descriptors │ 51372416 │ 1140850688 │ 1140850688 │ │ (null) │ 17468590976 │ 785020032 │ 785020032 │ └──────────────────────┴─────────────┴────────────┴────────────────┘ With a fair bit of imbalance: postgres[4189384][1]=# SELECT * FROM pg_shmem_allocations_numa WHERE name in ('Buffer Blocks', 'Buffer Descriptors'); ┌────────────────────┬───────────┬────────────┐ │ name │ numa_node │ size │ ├────────────────────┼───────────┼────────────┤ │ Buffer Blocks │ 0 │ 8589934592 │ │ Buffer Blocks │ 1 │ 2147483648 │ │ Buffer Descriptors │ 0 │ 0 │ │ Buffer Descriptors │ 1 │ 2147483648 │ └────────────────────┴───────────┴────────────┘ (4 rows) Note that the buffer descriptors are all on node 1. > +/* > + * Calculate the NUMA node for a given buffer. > + */ > +int > +BufferGetNode(Buffer buffer) > +{ > + /* not NUMA interleaving */ > + if (numa_chunk_buffers == -1) > + return -1; > + > + return (buffer / numa_chunk_buffers) % numa_nodes; > +} FWIW, this is likely rather expensive - when not a compile time constant, divisions and modulo can take a fair number of cycles. > +/* > + * pg_numa_interleave_memory > + * move memory to different NUMA nodes in larger chunks > + * > + * startptr - start of the region (should be aligned to page size) > + * endptr - end of the region (doesn't need to be aligned) > + * mem_page_size - size of the memory page size > + * chunk_size - size of the chunk to move to a single node (should be multiple > + * of page size > + * num_nodes - number of nodes to allocate memory to > + * > + * XXX Maybe this should use numa_tonode_memory and numa_police_memory instead? > + * That might be more efficient than numa_move_pages, as it works on larger > + * chunks of memory, not individual system pages, I think. > + * > + * XXX The "interleave" name is not quite accurate, I guess. > + */ > +static void > +pg_numa_interleave_memory(char *startptr, char *endptr, > + Size mem_page_size, Size chunk_size, > + int num_nodes) > +{ Seems like this should be in pg_numa.c? > diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c > index 69b6a877dc9..c07de903f76 100644 > --- a/src/bin/pgbench/pgbench.c > +++ b/src/bin/pgbench/pgbench.c I assume those changes weren't intentionally part of this patch... > From 6505848ac8359c8c76dfbffc7150b6601ab07601 Mon Sep 17 00:00:00 2001 > From: Tomas Vondra <tomas@vondra.me> > Date: Thu, 22 May 2025 18:38:41 +0200 > Subject: [PATCH v1 4/6] NUMA: partition buffer freelist > > Instead of a single buffer freelist, partition into multiple smaller > lists, to reduce lock contention, and to spread the buffers over all > NUMA nodes more evenly. > > There are four strategies, specified by GUC numa_partition_freelist > > * none - single long freelist, should work just like now > > * node - one freelist per NUMA node, with only buffers from that node > > * cpu - one freelist per CPU > > * pid - freelist determined by PID (same number of freelists as 'cpu') > > When allocating a buffer, it's taken from the correct freelist (e.g. > same NUMA node). > > Note: This is (probably) more important than partitioning ProcArray. > > +/* > + * Represents one freelist partition. > + */ > +typedef struct BufferStrategyFreelist > +{ > + /* Spinlock: protects the values below */ > + slock_t freelist_lock; > + > + /* > + * XXX Not sure why this needs to be aligned like this. Need to ask > + * Andres. > + */ > + int firstFreeBuffer __attribute__((aligned(64))); /* Head of list of > + * unused buffers */ > + > + /* Number of buffers consumed from this list. */ > + uint64 consumed; > +} BufferStrategyFreelist; I think this might be a leftover from measuring performance of a *non* partitioned freelist. I saw unnecessar contention between BufferStrategyControl->{nextVictimBuffer,buffer_strategy_lock,numBufferAllocs} and was testing what effect the simplest avoidance scheme has. I don't this should be part of this patchset. > > /* > * The shared freelist control information. > @@ -39,8 +66,6 @@ typedef struct > */ > pg_atomic_uint32 nextVictimBuffer; > > - int firstFreeBuffer; /* Head of list of unused buffers */ > - > /* > * Statistics. These counters should be wide enough that they can't > * overflow during a single bgwriter cycle. > @@ -51,13 +76,27 @@ typedef struct > /* > * Bgworker process to be notified upon activity or -1 if none. See > * StrategyNotifyBgWriter. > + * > + * XXX Not sure why this needs to be aligned like this. Need to ask > + * Andres. Also, shouldn't the alignment be specified after, like for > + * "consumed"? > */ > - int bgwprocno; > + int __attribute__((aligned(64))) bgwprocno; > + > + BufferStrategyFreelist freelists[FLEXIBLE_ARRAY_MEMBER]; > } BufferStrategyControl; Here the reason was that it's silly to put almost-readonly data (like bgwprocno) onto the same cacheline as very frequently modified data like ->numBufferAllocs. That causes unnecessary cache misses in many StrategyGetBuffer() calls, as another backend's StrategyGetBuffer() will always have modified ->numBufferAllocs and either ->buffer_strategy_lock or ->nextVictimBuffer. > > +static BufferStrategyFreelist * > +ChooseFreeList(void) > +{ > + unsigned cpu; > + unsigned node; > + int rc; > + > + int freelist_idx; > + > + /* freelist not partitioned, return the first (and only) freelist */ > + if (numa_partition_freelist == FREELIST_PARTITION_NONE) > + return &StrategyControl->freelists[0]; > + > + /* > + * freelist is partitioned, so determine the CPU/NUMA node, and pick a > + * list based on that. > + */ > + rc = getcpu(&cpu, &node); > + if (rc != 0) > + elog(ERROR, "getcpu failed: %m"); Probably should put this into somewhere abstracted away... > + /* > + * Pick the freelist, based on CPU, NUMA node or process PID. This matches > + * how we built the freelists above. > + * > + * XXX Can we rely on some of the values (especially strategy_nnodes) to > + * be a power-of-2? Then we could replace the modulo with a mask, which is > + * likely more efficient. > + */ > + switch (numa_partition_freelist) > + { > + case FREELIST_PARTITION_CPU: > + freelist_idx = cpu % strategy_ncpus; As mentioned earlier, modulo is rather expensive for something executed so frequently... > + break; > + > + case FREELIST_PARTITION_NODE: > + freelist_idx = node % strategy_nnodes; > + break; Here we shouldn't need modulo, right? > + > + case FREELIST_PARTITION_PID: > + freelist_idx = MyProcPid % strategy_ncpus; > + break; > + > + default: > + elog(ERROR, "unknown freelist partitioning value"); > + } > + > + return &StrategyControl->freelists[freelist_idx]; > +} > /* size of lookup hash table ... see comment in StrategyInitialize */ > size = add_size(size, BufTableShmemSize(NBuffers + NUM_BUFFER_PARTITIONS)); > > /* size of the shared replacement strategy control block */ > - size = add_size(size, MAXALIGN(sizeof(BufferStrategyControl))); > + size = add_size(size, MAXALIGN(offsetof(BufferStrategyControl, freelists))); > + > + /* > + * Allocate one frelist per CPU. We might use per-node freelists, but the > + * assumption is the number of CPUs is less than number of NUMA nodes. > + * > + * FIXME This assumes the we have more CPUs than NUMA nodes, which seems > + * like a safe assumption. But maybe we should calculate how many elements > + * we actually need, depending on the GUC? Not a huge amount of memory. FWIW, I don't think that's a safe assumption anymore. With CXL we can get a) PCIe attached memory and b) remote memory as a separate NUMA nodes, and that very well could end up as more NUMA nodes than cores. Ugh, -ETOOLONG. Gotta schedule some other things... Greetings, Andres Freund -
Re: Adding basic NUMA awareness
Jakub Wartak <jakub.wartak@enterprisedb.com> — 2025-07-10T10:15:52Z
On Wed, Jul 9, 2025 at 7:13 PM Andres Freund <andres@anarazel.de> wrote: > > Yes, and we are discussing if it is worth getting into smaller pages > > for such usecases (e.g. 4kB ones without hugetlb with 2MB hugepages or > > what more even more waste 1GB hugetlb if we dont request 2MB for some > > small structs: btw, we have ability to select MAP_HUGE_2MB vs > > MAP_HUGE_1GB). I'm thinking about two problems: > > - 4kB are swappable and mlock() potentially (?) disarms NUMA autobalacning > > I'm not really bought into this being a problem. If your system has enough > pressure to swap out the PGPROC array, you're so hosed that this won't make a > difference. OK I need to bend here, yet still part of me believes that the situation where we have hugepages (for 'Buffer Blocks') and yet some smaller more, but way critical structs are more likely to be swapped out due to pressure of some backend-gone-wild random mallocs() is unhealthy to me (especially the fact the OS might prefer swapping on per node rather than global picture) > I'm rather doubtful that it's a good idea to combine numa awareness with numa > balancing. Numa balancing adds latency and makes it much more expensive for > userspace to act in a numa aware way, since it needs to regularly update its > knowledge about where memory resides. Well the problem is that backends come here and go to random CPUs often (migrated++ on very high backend counts and non-uniform workloads in terms of backend-CPU usage), but the autobalancing doesn't need to be on or off for everything. It could be autobalancing for a certain memory region and it is not affecting the app in any way (well, other than those minor page faulting, literally ). > If we used 4k pages for the procarray we would just have ~4 procs on one page, > if that range were marked as interleaved, it'd probably suffice. OK, this sounds like the best and simplest proposal to me, yet the patch doesn't do OS-based interleaving for those today. Gonna try that mlock() sooner or later... ;) -J.
-
Re: Adding basic NUMA awareness
Jakub Wartak <jakub.wartak@enterprisedb.com> — 2025-07-10T10:16:41Z
On Wed, Jul 9, 2025 at 9:42 PM Andres Freund <andres@anarazel.de> wrote: > On 2025-07-01 21:07:00 +0200, Tomas Vondra wrote: > > Each patch has a numa_ GUC, intended to enable/disable that part. This > > is meant to make development easier, not as a final interface. I'm not > > sure how exactly that should look. It's possible some combinations of > > GUCs won't work, etc. > > Wonder if some of it might be worth putting into a multi-valued GUC (like > debug_io_direct). Long-term or for experimentation? Also please see below as it is related: [..] > FWIW, I don't think that's a safe assumption anymore. With CXL we can get a) > PCIe attached memory and b) remote memory as a separate NUMA nodes, and that > very well could end up as more NUMA nodes than cores. In my earlier apparently very way too naive approach, I've tried to handle this CXL scenario, but I'm afraid this cannot be done without further configuration, please see review/use cases [1] and [2] -J. [1] https://www.postgresql.org/message-id/attachment/178119/v4-0001-Add-capability-to-interleave-shared-memory-across.patch - just see sgml/GUC and we have numa_parse_nodestring(3) [2] https://www.postgresql.org/message-id/aAKPMrX1Uq6quKJy%40ip-10-97-1-34.eu-west-3.compute.internal
-
Re: Adding basic NUMA awareness
Greg Burd <greg@burd.me> — 2025-07-10T12:13:43Z
> On Jul 9, 2025, at 1:23 PM, Andres Freund <andres@anarazel.de> wrote: > > Hi, > > On 2025-07-09 12:55:51 -0400, Greg Burd wrote: >> On Jul 9 2025, at 12:35 pm, Andres Freund <andres@anarazel.de> wrote: >> >>> FWIW, I've started to wonder if we shouldn't just get rid of the freelist >>> entirely. While clocksweep is perhaps minutely slower in a single >>> thread than >>> the freelist, clock sweep scales *considerably* better [1]. As it's rather >>> rare to be bottlenecked on clock sweep speed for a single thread >>> (rather then >>> IO or memory copy overhead), I think it's worth favoring clock sweep. >> >> Hey Andres, thanks for spending time on this. I've worked before on >> freelist implementations (last one in LMDB) and I think you're onto >> something. I think it's an innovative idea and that the speed >> difference will either be lost in the noise or potentially entirely >> mitigated by avoiding duplicate work. > > Agreed. FWIW, just using clock sweep actually makes things like DROP TABLE > perform better because it doesn't need to maintain the freelist anymore... > > >>> Also needing to switch between getting buffers from the freelist and >>> the sweep >>> makes the code more expensive. I think just having the buffer in the sweep, >>> with a refcount / usagecount of zero would suffice. >> >> If you're not already coding this, I'll jump in. :) > > My experimental patch is literally a four character addition ;), namely adding > "0 &&" to the relevant code in StrategyGetBuffer(). > > Obviously a real patch would need to do some more work than that. Feel free > to take on that project, I am not planning on tackling that in near term. > I started on this last night, making good progress. Thanks for the inspiration. I'll create a new thread to track the work and cross-reference when I have something reasonable to show (hopefully later today). > There's other things around this that could use some attention. It's not hard > to see clock sweep be a bottleneck in concurrent workloads - partially due to > the shared maintenance of the clock hand. A NUMAed clock sweep would address > that. Working on it. Other than NUMA-fying clocksweep there is a function have_free_buffer() that might be a tad tricky to re-implement efficiently and/or make NUMA aware. Or maybe I can remove that too? It is used in autoprewarm.c and possibly other extensions, but no where else in core. > However, we also maintain StrategyControl->numBufferAllocs, which is a > significant contention point and would not necessarily be removed by a > NUMAificiation of the clock sweep. Yep, I noted this counter and its potential for contention too. Fortunately, it seems like it is only used so that "bgwriter can estimate the rate of buffer consumption" which to me opens the door to a less accurate partitioned counter, perhaps something lock-free (no mutex/CAS) that is bucketed then combined when read. A quick look at bufmgr.c indicates that recent_allocs (which is StrategyControl->numBufferAllocs) is used to track a "moving average" and other voodoo there I've yet to fully grok. Any thoughts on this approximate count approach? Also, what are your thoughts on updating the algorithm to CLOCK-Pro [1] while I'm there? I guess I'd have to try it out, measure it a lot and see if there are any material benefits. Maybe I'll keep that for a future patch, or at least layer it... back to work! > Greetings, > > Andres Freund best. -greg [1] https://www.usenix.org/legacy/publications/library/proceedings/usenix05/tech/general/full_papers/jiang/jiang_html/html.html
-
Re: Adding basic NUMA awareness
Bertrand Drouvot <bertranddrouvot.pg@gmail.com> — 2025-07-10T14:17:21Z
Hi, On Wed, Jul 09, 2025 at 03:42:26PM -0400, Andres Freund wrote: > Hi, > > Thanks for working on this! Indeed, thanks! > On 2025-07-01 21:07:00 +0200, Tomas Vondra wrote: > > 1) v1-0001-NUMA-interleaving-buffers.patch > > > > This is the main thing when people think about NUMA - making sure the > > shared buffers are allocated evenly on all the nodes, not just on a > > single node (which can happen easily with warmup). The regular memory > > interleaving would address this, but it also has some disadvantages. > > > > Firstly, it's oblivious to the contents of the shared memory segment, > > and we may not want to interleave everything. It's also oblivious to > > alignment of the items (a buffer can easily end up "split" on multiple > > NUMA nodes), or relationship between different parts (e.g. there's a > > BufferBlock and a related BufferDescriptor, and those might again end up > > on different nodes). > > Two more disadvantages: > > With OS interleaving postgres doesn't (not easily at least) know about what > maps to what, which means postgres can't do stuff like numa aware buffer > replacement. > > With OS interleaving the interleaving is "too fine grained", with pages being > mapped at each page boundary, making it less likely for things like one > strategy ringbuffer to reside on a single numa node. > > There's a secondary benefit of explicitly assigning buffers to nodes, > > using this simple scheme - it allows quickly determining the node ID > > given a buffer ID. This is helpful later, when building freelist. I do think this is a big advantage as compare to the OS interleaving. > I wonder if we should *increase* the size of shared_buffers whenever huge > pages are in use and there's padding space due to the huge page > boundaries. Pretty pointless to waste that memory if we can instead use if for > the buffer pool. Not that big a deal with 2MB huge pages, but with 1GB huge > pages... I think that makes sense, except maybe for operations that need to scan the whole buffer pool (i.e related to BUF_DROP_FULL_SCAN_THRESHOLD)? > > 5) v1-0005-NUMA-interleave-PGPROC-entries.patch > > > > Another area that seems like it might benefit from NUMA is PGPROC, so I > > gave it a try. It turned out somewhat challenging. Similarly to buffers > > we have two pieces that need to be located in a coordinated way - PGPROC > > entries and fast-path arrays. But we can't use the same approach as for > > buffers/descriptors, because > > > > (a) Neither of those pieces aligns with memory page size (PGPROC is > > ~900B, fast-path arrays are variable length). > > > (b) We could pad PGPROC entries e.g. to 1KB, but that'd still require > > rather high max_connections before we use multiple huge pages. > > Right now sizeof(PGPROC) happens to be multiple of 64 (i.e. the most common > cache line size) Oh right, it's currently 832 bytes and the patch extends that to 840 bytes. With a bit of reordering: diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 5cb1632718e..2ed2f94202a 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -194,8 +194,6 @@ struct PGPROC * vacuum must not remove tuples deleted by * xid >= xmin ! */ - int procnumber; /* index in ProcGlobal->allProcs */ - int pid; /* Backend's process ID; 0 if prepared xact */ int pgxactoff; /* offset into various ProcGlobal->arrays with @@ -243,6 +241,7 @@ struct PGPROC /* Support for condition variables. */ proclist_node cvWaitLink; /* position in CV wait list */ + int procnumber; /* index in ProcGlobal->allProcs */ /* Info about lock the process is currently waiting for, if any. */ /* waitLock and waitProcLock are NULL if not currently waiting. */ @@ -268,6 +267,7 @@ struct PGPROC */ XLogRecPtr waitLSN; /* waiting for this LSN or higher */ int syncRepState; /* wait state for sync rep */ + int numa_node; dlist_node syncRepLinks; /* list link if process is in syncrep queue */ /* @@ -321,9 +321,6 @@ struct PGPROC PGPROC *lockGroupLeader; /* lock group leader, if I'm a member */ dlist_head lockGroupMembers; /* list of members, if I'm a leader */ dlist_node lockGroupLink; /* my member link, if I'm a member */ - - /* NUMA node */ - int numa_node; }; That could be back to 832 (the order does not make sense logically speaking though). Regards, -- Bertrand Drouvot PostgreSQL Contributors Team RDS Open Source Databases Amazon Web Services: https://aws.amazon.com -
Re: Adding basic NUMA awareness - Preliminary feedback and outline for an extensible approach
Tomas Vondra <tomas@vondra.me> — 2025-07-10T15:20:50Z
On 7/9/25 08:40, Cédric Villemain wrote: >> On 7/8/25 18:06, Cédric Villemain wrote: >>> >>> >>> >>> >>> >>> >>>> On 7/8/25 03:55, Cédric Villemain wrote: >>>>> Hi Andres, >>>>> >>>>>> Hi, >>>>>> >>>>>> On 2025-07-05 07:09:00 +0000, Cédric Villemain wrote: >>>>>>> In my work on more careful PostgreSQL resource management, I've come >>>>>>> to the >>>>>>> conclusion that we should avoid pushing policy too deeply into the >>>>>>> PostgreSQL core itself. Therefore, I'm quite skeptical about >>>>>>> integrating >>>>>>> NUMA-specific management directly into core PostgreSQL in such a >>>>>>> way. >>>>>> >>>>>> I think it's actually the opposite - whenever we pushed stuff like >>>>>> this >>>>>> outside of core it has hurt postgres substantially. Not having >>>>>> replication in >>>>>> core was a huge mistake. Not having HA management in core is >>>>>> probably the >>>>>> biggest current adoption hurdle for postgres. >>>>>> >>>>>> To deal better with NUMA we need to improve memory placement and >>>>>> various >>>>>> algorithms, in an interrelated way - that's pretty much impossible >>>>>> to do >>>>>> outside of core. >>>>> >>>>> Except the backend pinning which is easy to achieve, thus my >>>>> comment on >>>>> the related patch. >>>>> I'm not claiming NUMA memory and all should be managed outside of core >>>>> (though I didn't read other patches yet). >>>>> >>>> >>>> But an "optimal backend placement" seems to very much depend on >>>> where we >>>> placed the various pieces of shared memory. Which the external module >>>> will have trouble following, I suspect. >>>> >>>> I still don't have any idea what exactly would the external module do, >>>> how would it decide where to place the backend. Can you describe some >>>> use case with an example? >>>> >>>> Assuming we want to actually pin tasks from within Postgres, what I >>>> think might work is allowing modules to "advise" on where to place the >>>> task. But the decision would still be done by core. >>> >>> Possibly exactly what you're doing in proc.c when managing allocation of >>> process, but not hardcoded in postgresql (patches 02, 05 and 06 are good >>> candidates), I didn't get that they require information not available to >>> any process executing code from a module. >>> >> >> Well, it needs to understand how some other stuff (especially PGPROC >> entries) is distributed between nodes. I'm not sure how much of this >> internal information we want to expose outside core ... >> >>> Parts of your code where you assign/define policy could be in one or >>> more relevant routines of a "numa profile manager", like in an >>> initProcessRoutine(), and registered in pmroutine struct: >>> >>> pmroutine = GetPmRoutineForInitProcess(); >>> if (pmroutine != NULL && >>> pmroutine->init_process != NULL) >>> pmroutine->init_process(MyProc); >>> >>> This way it's easier to manage alternative policies, and also to be able >>> to adjust when hardware and linux kernel changes. >>> >> >> I'm not against making this extensible, in some way. But I still >> struggle to imagine a reasonable alternative policy, where the external >> module gets the same information and ends up with a different decision. >> >> So what would the alternate policy look like? What use case would the >> module be supporting? > > > That's the whole point: there are very distinct usages of PostgreSQL in > the field. And maybe not all of them will require the policy defined by > PostgreSQL core. > > May I ask the reverse: what prevent external modules from taking those > decisions ? There are already a lot of area where external code can take > over PostgreSQL processing, like Neon is doing. > The complexity of making everything extensible in an arbitrary way. To make it extensible in a useful, we need to have a reasonably clear idea what aspects need to be extensible, and what's the goal. > There are some very early processing for memory setup that I can see as > a current blocker, and here I'd refer a more compliant NUMA api as > proposed by Jakub so it's possible to arrange based on workload, > hardware configuration or other matters. Reworking to get distinct > segment and all as you do is great, and combo of both approach probably > of great interest. There is also this weighted interleave discussed and > probably much more to come in this area in Linux. > > I think some points raised already about possible distinct policies, I > am precisely claiming that it is hard to come with one good policy with > limited setup options, thus requirement to keep that flexible enough > (hooks, api, 100 GUc ?). > I'm sorry, I don't want to sound too negative, but "I want arbitrary extensibility" is not a very useful feedback. I've asked you to give some examples of policies that'd customize some of the NUMA stuff. > There is an EPYC story here also, given the NUMA setup can vary > depending on BIOS setup, associated NUMA policy must probably take that > into account (L3 can be either real cache or 4 extra "local" NUMA nodes > - with highly distinct access cost from a RAM module). > Does that change how PostgreSQL will place memory and process? Is it > important or of interest ? > So how exactly would the policy handle this? Right now we're entirely oblivious to L3, or on-CPU caches in general. We don't even consider the size of L3 when sizing hash tables in a hashjoin etc. regards -- Tomas Vondra
-
Re: Adding basic NUMA awareness
Tomas Vondra <tomas@vondra.me> — 2025-07-10T15:31:45Z
On 7/9/25 19:23, Andres Freund wrote: > Hi, > > On 2025-07-09 12:55:51 -0400, Greg Burd wrote: >> On Jul 9 2025, at 12:35 pm, Andres Freund <andres@anarazel.de> wrote: >> >>> FWIW, I've started to wonder if we shouldn't just get rid of the freelist >>> entirely. While clocksweep is perhaps minutely slower in a single >>> thread than >>> the freelist, clock sweep scales *considerably* better [1]. As it's rather >>> rare to be bottlenecked on clock sweep speed for a single thread >>> (rather then >>> IO or memory copy overhead), I think it's worth favoring clock sweep. >> >> Hey Andres, thanks for spending time on this. I've worked before on >> freelist implementations (last one in LMDB) and I think you're onto >> something. I think it's an innovative idea and that the speed >> difference will either be lost in the noise or potentially entirely >> mitigated by avoiding duplicate work. > > Agreed. FWIW, just using clock sweep actually makes things like DROP TABLE > perform better because it doesn't need to maintain the freelist anymore... > > >>> Also needing to switch between getting buffers from the freelist and >>> the sweep >>> makes the code more expensive. I think just having the buffer in the sweep, >>> with a refcount / usagecount of zero would suffice. >> >> If you're not already coding this, I'll jump in. :) > > My experimental patch is literally a four character addition ;), namely adding > "0 &&" to the relevant code in StrategyGetBuffer(). > > Obviously a real patch would need to do some more work than that. Feel free > to take on that project, I am not planning on tackling that in near term. > > > There's other things around this that could use some attention. It's not hard > to see clock sweep be a bottleneck in concurrent workloads - partially due to > the shared maintenance of the clock hand. A NUMAed clock sweep would address > that. However, we also maintain StrategyControl->numBufferAllocs, which is a > significant contention point and would not necessarily be removed by a > NUMAificiation of the clock sweep. > Wouldn't it make sense to partition the numBufferAllocs too, though? I don't remember if my hacky experimental patch NUMA-partitioning did that or I just thought about doing that, but why wouldn't that be enough? Places that need the "total" count would have to sum the counters, but it seemed to me most of the places would be fine with the "local" count for that partition. If we also make sure to "sync" the clocksweeps so as to not work on just a single partition, that might be enough ... regards -- Tomas Vondra
-
Re: Adding basic NUMA awareness
Andres Freund <andres@anarazel.de> — 2025-07-11T16:06:13Z
Hi, On 2025-07-10 17:31:45 +0200, Tomas Vondra wrote: > On 7/9/25 19:23, Andres Freund wrote: > > There's other things around this that could use some attention. It's not hard > > to see clock sweep be a bottleneck in concurrent workloads - partially due to > > the shared maintenance of the clock hand. A NUMAed clock sweep would address > > that. However, we also maintain StrategyControl->numBufferAllocs, which is a > > significant contention point and would not necessarily be removed by a > > NUMAificiation of the clock sweep. > > > > Wouldn't it make sense to partition the numBufferAllocs too, though? I > don't remember if my hacky experimental patch NUMA-partitioning did that > or I just thought about doing that, but why wouldn't that be enough? It could be solved together with partitioning, yes - that's what I was trying to reference with the emphasized bit in "would *not necessarily* be removed by a NUMAificiation of the clock sweep". Greetings, Andres Freund
-
Re: Adding basic NUMA awareness
Andres Freund <andres@anarazel.de> — 2025-07-11T16:14:06Z
Hi, On 2025-07-10 14:17:21 +0000, Bertrand Drouvot wrote: > On Wed, Jul 09, 2025 at 03:42:26PM -0400, Andres Freund wrote: > > I wonder if we should *increase* the size of shared_buffers whenever huge > > pages are in use and there's padding space due to the huge page > > boundaries. Pretty pointless to waste that memory if we can instead use if for > > the buffer pool. Not that big a deal with 2MB huge pages, but with 1GB huge > > pages... > > I think that makes sense, except maybe for operations that need to scan > the whole buffer pool (i.e related to BUF_DROP_FULL_SCAN_THRESHOLD)? I don't think the increases here are big enough for that to matter, unless perhaps you're using 1GB huge pages. But if you're concerned about dropping tables very fast (i.e. you're running schema change heavy regression tests), you're not going to use 1GB huge pages. > > > 5) v1-0005-NUMA-interleave-PGPROC-entries.patch > > > > > > Another area that seems like it might benefit from NUMA is PGPROC, so I > > > gave it a try. It turned out somewhat challenging. Similarly to buffers > > > we have two pieces that need to be located in a coordinated way - PGPROC > > > entries and fast-path arrays. But we can't use the same approach as for > > > buffers/descriptors, because > > > > > > (a) Neither of those pieces aligns with memory page size (PGPROC is > > > ~900B, fast-path arrays are variable length). > > > > > (b) We could pad PGPROC entries e.g. to 1KB, but that'd still require > > > rather high max_connections before we use multiple huge pages. > > > > Right now sizeof(PGPROC) happens to be multiple of 64 (i.e. the most common > > cache line size) > > Oh right, it's currently 832 bytes and the patch extends that to 840 bytes. I don't think the patch itself is the problem - it really is just happenstance that it's a multiple of the line size right now. And it's not on common Armv8 platforms... > With a bit of reordering: > > That could be back to 832 (the order does not make sense logically speaking > though). I don't think shrinking the size in a one-off way just to keep the "accidental" size-is-multiple-of-64 property is promising. It'll just get broken again. I think we should: a) pad the size of PGPROC to a cache line (or even to a subsequent power of 2, to make array access cheaper, right now that involves actual multiplications rather than shifts or indexed `lea` instructions). That's probably just a pg_attribute_aligned b) Reorder PGPROC to separate frequently modified from almost-read-only data, to increase cache hit ratio. Greetings, Andres Freund
-
Re: Adding basic NUMA awareness
Greg Burd <greg@burd.me> — 2025-07-11T17:34:26Z
> On Jul 10, 2025, at 8:13 AM, Burd, Greg <greg@burd.me> wrote: > > >> On Jul 9, 2025, at 1:23 PM, Andres Freund <andres@anarazel.de> wrote: >> >> Hi, >> >> On 2025-07-09 12:55:51 -0400, Greg Burd wrote: >>> On Jul 9 2025, at 12:35 pm, Andres Freund <andres@anarazel.de> wrote: >>> >>>> FWIW, I've started to wonder if we shouldn't just get rid of the freelist >>>> entirely. While clocksweep is perhaps minutely slower in a single >>>> thread than >>>> the freelist, clock sweep scales *considerably* better [1]. As it's rather >>>> rare to be bottlenecked on clock sweep speed for a single thread >>>> (rather then >>>> IO or memory copy overhead), I think it's worth favoring clock sweep. >>> >>> Hey Andres, thanks for spending time on this. I've worked before on >>> freelist implementations (last one in LMDB) and I think you're onto >>> something. I think it's an innovative idea and that the speed >>> difference will either be lost in the noise or potentially entirely >>> mitigated by avoiding duplicate work. >> >> Agreed. FWIW, just using clock sweep actually makes things like DROP TABLE >> perform better because it doesn't need to maintain the freelist anymore... >> >> >>>> Also needing to switch between getting buffers from the freelist and >>>> the sweep >>>> makes the code more expensive. I think just having the buffer in the sweep, >>>> with a refcount / usagecount of zero would suffice. >>> >>> If you're not already coding this, I'll jump in. :) >> >> My experimental patch is literally a four character addition ;), namely adding >> "0 &&" to the relevant code in StrategyGetBuffer(). >> >> Obviously a real patch would need to do some more work than that. Feel free >> to take on that project, I am not planning on tackling that in near term. >> > > I started on this last night, making good progress. Thanks for the inspiration. I'll create a new thread to track the work and cross-reference when I have something reasonable to show (hopefully later today). > >> There's other things around this that could use some attention. It's not hard >> to see clock sweep be a bottleneck in concurrent workloads - partially due to >> the shared maintenance of the clock hand. A NUMAed clock sweep would address >> that. > > Working on it. For archival sake, and to tie up loose ends I'll link from here to a new thread I just started that proposes the removal of the freelist and the buffer_strategy_lock [1]. That patch set doesn't address any NUMA-related tasks directly, but it should remove some pain when working in that direction by removing code that requires partitioning and locking and... best. -greg [1] https://postgr.es/m/E2D6FCDC-BE98-4F95-B45E-699C3E17BA10@burd.me
-
Re: Adding basic NUMA awareness
Tomas Vondra <tomas@vondra.me> — 2025-07-17T21:11:16Z
Hi, Here's a v2 of the patch series, with a couple changes: * I simplified the various freelist partitioning by keeping only the "node" partitioning (so the cpu/pid strategies are gone). Those were meant for experimenting, but it made the code more complicated so I ditched it. * I changed the freelist partitioning scheme a little bit, based on the discussion in this thread. Instead of having a single "partition" per NUMA node, there's not a minimum number of partitions (set to 4). So even if your system is not NUMA, you'll have 4 of them. If you have 2 nodes, you'll still have 4, and each node will get 2. With 3 nodes we get 6 partitions (we need 2 per node, and we want to keep the number equal to keep things simple). Once the number of nodes exceeds 4, the heuristics switches to one partition per node. I'm aware there's a discussion about maybe simply removing freelists entirely. If that happens, this becomes mostly irrelevant, of course. The code should also make sure the freelists "agree" with how the earlier patch mapped the buffers to NUMA nodes, i.e. the freelist should only contain buffers from the "correct" NUMA node, etc. I haven't paid much attention to this - I believe it should work for "nice" values of shared buffers (when it evenly divides between nodes). But I'm sure it's possible to confuse that (won't cause crashes, but inefficiency). * There's now a patch partitioning clocksweep, using the same scheme as the freelists. I came to the conclusion it doesn't make much sense to partition these things differently - I can't think of a reason why that would be advantageous, and it makes it easier to reason about. The clocksweep partitioning is somewhat harder, because it affects BgBufferSync() and related code. With the partitioning we now have multiple "clock hands" for different ranges of buffers, and the clock sweep needs to consider that. I modified BgBufferSync to simply loop through the ClockSweep partitions, and do a small cleanup for each. It does work, as in "it doesn't crash". But this part definitely needs review to make sure I got the changes to the "pacing" right. * This new freelist/clocksweep partitioning scheme is however harder to disable. I now realize the GUC may quite do the trick, and there even is not a GUC for the clocksweep. I need to think about this, but I'm not even how feasible it'd be to have two separate GUCs (because of how these two pieces are intertwined). For now if you want to test without the partitioning, you need to skip the patch. I did some quick perf testing on my old xeon machine (2 NUMA nodes), and the results are encouraging. For a read-only pgbench (2x shared buffers, within RAM), I saw an increase from 1.1M tps to 1.3M. Not crazy, but not bad considering the patch is more about consistency than raw throughput. For a read-write pgbench I however saw some strange drops/increases of throughput. I suspect this might be due to some thinko in the clocksweep partitioning, but I'll need to take a closer look. regards -- Tomas Vondra
-
Re: Adding basic NUMA awareness
Tomas Vondra <tomas@vondra.me> — 2025-07-17T21:14:52Z
On 7/4/25 20:12, Tomas Vondra wrote: > On 7/4/25 13:05, Jakub Wartak wrote: >> ... >> >> 8. v1-0005 2x + /* if (numa_procs_interleave) */ >> >> Ha! it's a TRAP! I've uncommented it because I wanted to try it out >> without it (just by setting GUC off) , but "MyProc->sema" is NULL : >> >> 2025-07-04 12:31:08.103 CEST [28754] LOG: starting PostgreSQL >> 19devel on x86_64-linux, compiled by gcc-12.2.0, 64-bit >> [..] >> 2025-07-04 12:31:08.109 CEST [28754] LOG: io worker (PID 28755) >> was terminated by signal 11: Segmentation fault >> 2025-07-04 12:31:08.109 CEST [28754] LOG: terminating any other >> active server processes >> 2025-07-04 12:31:08.114 CEST [28754] LOG: shutting down because >> "restart_after_crash" is off >> 2025-07-04 12:31:08.116 CEST [28754] LOG: database system is shut down >> >> [New LWP 28755] >> [Thread debugging using libthread_db enabled] >> Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". >> Core was generated by `postgres: io worker '. >> Program terminated with signal SIGSEGV, Segmentation fault. >> #0 __new_sem_wait_fast (definitive_result=1, sem=sem@entry=0x0) >> at ./nptl/sem_waitcommon.c:136 >> 136 ./nptl/sem_waitcommon.c: No such file or directory. >> (gdb) where >> #0 __new_sem_wait_fast (definitive_result=1, sem=sem@entry=0x0) >> at ./nptl/sem_waitcommon.c:136 >> #1 __new_sem_trywait (sem=sem@entry=0x0) at ./nptl/sem_wait.c:81 >> #2 0x00005561918e0cac in PGSemaphoreReset (sema=0x0) at >> ../src/backend/port/posix_sema.c:302 >> #3 0x0000556191970553 in InitAuxiliaryProcess () at >> ../src/backend/storage/lmgr/proc.c:992 >> #4 0x00005561918e51a2 in AuxiliaryProcessMainCommon () at >> ../src/backend/postmaster/auxprocess.c:65 >> #5 0x0000556191940676 in IoWorkerMain (startup_data=<optimized >> out>, startup_data_len=<optimized out>) at >> ../src/backend/storage/aio/method_worker.c:393 >> #6 0x00005561918e8163 in postmaster_child_launch >> (child_type=child_type@entry=B_IO_WORKER, child_slot=20086, >> startup_data=startup_data@entry=0x0, >> startup_data_len=startup_data_len@entry=0, >> client_sock=client_sock@entry=0x0) at >> ../src/backend/postmaster/launch_backend.c:290 >> #7 0x00005561918ea09a in StartChildProcess >> (type=type@entry=B_IO_WORKER) at >> ../src/backend/postmaster/postmaster.c:3973 >> #8 0x00005561918ea308 in maybe_adjust_io_workers () at >> ../src/backend/postmaster/postmaster.c:4404 >> [..] >> (gdb) print *MyProc->sem >> Cannot access memory at address 0x0 >> > > Yeah, good catch. I'll look into that next week. > I've been unable to reproduce this issue, but I'm not sure what settings you actually used for this instance. Can you give me more details how to reproduce this? regards -- Tomas Vondra
-
Re: Adding basic NUMA awareness
Andres Freund <andres@anarazel.de> — 2025-07-18T16:46:44Z
Hi, On 2025-07-17 23:11:16 +0200, Tomas Vondra wrote: > Here's a v2 of the patch series, with a couple changes: Not a deep look at the code, just a quick reply. > * I changed the freelist partitioning scheme a little bit, based on the > discussion in this thread. Instead of having a single "partition" per > NUMA node, there's not a minimum number of partitions (set to 4). So I assume s/not/now/? > * There's now a patch partitioning clocksweep, using the same scheme as > the freelists. Nice! > I came to the conclusion it doesn't make much sense to partition these > things differently - I can't think of a reason why that would be > advantageous, and it makes it easier to reason about. Agreed. > The clocksweep partitioning is somewhat harder, because it affects > BgBufferSync() and related code. With the partitioning we now have > multiple "clock hands" for different ranges of buffers, and the clock > sweep needs to consider that. I modified BgBufferSync to simply loop > through the ClockSweep partitions, and do a small cleanup for each. That probably makes sense for now. It might need a bit of a larger adjustment at some point, but ... > * This new freelist/clocksweep partitioning scheme is however harder to > disable. I now realize the GUC may quite do the trick, and there even is > not a GUC for the clocksweep. I need to think about this, but I'm not > even how feasible it'd be to have two separate GUCs (because of how > these two pieces are intertwined). For now if you want to test without > the partitioning, you need to skip the patch. I think it's totally fair to enable/disable them at the same time. They're so closely related, that I don't think it really makes sense to measure them separately. > I did some quick perf testing on my old xeon machine (2 NUMA nodes), and > the results are encouraging. For a read-only pgbench (2x shared buffers, > within RAM), I saw an increase from 1.1M tps to 1.3M. Not crazy, but not > bad considering the patch is more about consistency than raw throughput. Personally I think an 1.18x improvement on a relatively small NUMA machine is really rather awesome. > For a read-write pgbench I however saw some strange drops/increases of > throughput. I suspect this might be due to some thinko in the clocksweep > partitioning, but I'll need to take a closer look. Was that with pinning etc enabled or not? > From c4d51ab87b92f9900e37d42cf74980e87b648a56 Mon Sep 17 00:00:00 2001 > From: Tomas Vondra <tomas@vondra.me> > Date: Sun, 8 Jun 2025 18:53:12 +0200 > Subject: [PATCH v2 5/7] NUMA: clockweep partitioning > > @@ -475,13 +525,17 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state, bool *from_r > /* > * Nothing on the freelist, so run the "clock sweep" algorithm > * > - * XXX Should we also make this NUMA-aware, to only access buffers from > - * the same NUMA node? That'd probably mean we need to make the clock > - * sweep NUMA-aware, perhaps by having multiple clock sweeps, each for a > - * subset of buffers. But that also means each process could "sweep" only > - * a fraction of buffers, even if the other buffers are better candidates > - * for eviction. Would that also mean we'd have multiple bgwriters, one > - * for each node, or would one bgwriter handle all of that? > + * XXX Note that ClockSweepTick() is NUMA-aware, i.e. it only looks at > + * buffers from a single partition, aligned with the NUMA node. That > + * means it only accesses buffers from the same NUMA node. > + * > + * XXX That also means each process "sweeps" only a fraction of buffers, > + * even if the other buffers are better candidates for eviction. Maybe > + * there should be some logic to "steal" buffers from other freelists > + * or other nodes? I think we *definitely* need "stealing" from other clock sweeps, whenever there's a meaningful imbalance between the different sweeps. I don't think we need to be overly precise about it, a small imbalance won't have that much of an effect. But clearly it doesn't make sense to say that one backend can only fill buffers in the current partition, that'd lead to massive performance issues in a lot of workloads. The hardest thing probably is to make the logic for when to check foreign clock sweeps cheap enough. One way would be to do it whenever a sweep wraps around, that'd probably amortize the cost sufficiently, and I don't think it'd be too imprecise, as we'd have processed that set of buffers in a row without partitioning as well. But it'd probably be too coarse when determining for how long to use a foreign sweep instance. But we probably could address that by rechecking the balanace more frequently when using a foreign partition. Another way would be to have bgwriter manage this. Whenever it detects that one ring is too far ahead, it could set a "avoid this partition" bit, which would trigger backends that natively use that partition to switch to foreign partitions that don't currently have that bit set. I suspect there's a problem with that approach though, I worry that the amount of time that bgwriter spends in BgBufferSync() may sometimes be too long, leading to too much imbalance. Greetings, Andres Freund
-
Re: Adding basic NUMA awareness
Tomas Vondra <tomas@vondra.me> — 2025-07-18T20:48:00Z
On 7/18/25 18:46, Andres Freund wrote: > Hi, > > On 2025-07-17 23:11:16 +0200, Tomas Vondra wrote: >> Here's a v2 of the patch series, with a couple changes: > > Not a deep look at the code, just a quick reply. > > >> * I changed the freelist partitioning scheme a little bit, based on the >> discussion in this thread. Instead of having a single "partition" per >> NUMA node, there's not a minimum number of partitions (set to 4). So > > I assume s/not/now/? > Yes. > >> * There's now a patch partitioning clocksweep, using the same scheme as >> the freelists. > > Nice! > > >> I came to the conclusion it doesn't make much sense to partition these >> things differently - I can't think of a reason why that would be >> advantageous, and it makes it easier to reason about. > > Agreed. > > >> The clocksweep partitioning is somewhat harder, because it affects >> BgBufferSync() and related code. With the partitioning we now have >> multiple "clock hands" for different ranges of buffers, and the clock >> sweep needs to consider that. I modified BgBufferSync to simply loop >> through the ClockSweep partitions, and do a small cleanup for each. > > That probably makes sense for now. It might need a bit of a larger adjustment at some point, but ... > I couldn't think of something fundamentally better and not too complex. I suspect we might want to use multiple bgwriters in the future, and this scheme seems to be reasonably well suited for that too. I'm also thinking about having some sort of "unified" partitioning scheme for all the places partitioning shared buffers. Right now each of the places does it on it's own, i.e. buff_init, freelist and clocksweep all have their code splitting NBuffers into partitions. And it should align. Because what would be the benefit if it didn't? But I guess having three variants of the same code seems a bit pointless. I think buff_init should build a common definition of buffer partitions, and the remaining parts should use that as the source of truth ... > >> * This new freelist/clocksweep partitioning scheme is however harder to >> disable. I now realize the GUC may quite do the trick, and there even is >> not a GUC for the clocksweep. I need to think about this, but I'm not >> even how feasible it'd be to have two separate GUCs (because of how >> these two pieces are intertwined). For now if you want to test without >> the partitioning, you need to skip the patch. > > I think it's totally fair to enable/disable them at the same time. They're so > closely related, that I don't think it really makes sense to measure them > separately. > Yeah, that's a fair point. > >> I did some quick perf testing on my old xeon machine (2 NUMA nodes), and >> the results are encouraging. For a read-only pgbench (2x shared buffers, >> within RAM), I saw an increase from 1.1M tps to 1.3M. Not crazy, but not >> bad considering the patch is more about consistency than raw throughput. > > Personally I think an 1.18x improvement on a relatively small NUMA machine is > really rather awesome. > True, but I want to stress out it's just one quick (& simple test). Much more testing is needed before I can make reliable claims. > >> For a read-write pgbench I however saw some strange drops/increases of >> throughput. I suspect this might be due to some thinko in the clocksweep >> partitioning, but I'll need to take a closer look. > > Was that with pinning etc enabled or not? > IIRC it was with everything enabled, except for numa_procs_pin (which pins backend to NUMA node). I found that to actually harm performance in some of the tests (even just read-only ones), resulting in uneven usage of cores and lower throughput. > > >> From c4d51ab87b92f9900e37d42cf74980e87b648a56 Mon Sep 17 00:00:00 2001 >> From: Tomas Vondra <tomas@vondra.me> >> Date: Sun, 8 Jun 2025 18:53:12 +0200 >> Subject: [PATCH v2 5/7] NUMA: clockweep partitioning >> > > >> @@ -475,13 +525,17 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state, bool *from_r >> /* >> * Nothing on the freelist, so run the "clock sweep" algorithm >> * >> - * XXX Should we also make this NUMA-aware, to only access buffers from >> - * the same NUMA node? That'd probably mean we need to make the clock >> - * sweep NUMA-aware, perhaps by having multiple clock sweeps, each for a >> - * subset of buffers. But that also means each process could "sweep" only >> - * a fraction of buffers, even if the other buffers are better candidates >> - * for eviction. Would that also mean we'd have multiple bgwriters, one >> - * for each node, or would one bgwriter handle all of that? >> + * XXX Note that ClockSweepTick() is NUMA-aware, i.e. it only looks at >> + * buffers from a single partition, aligned with the NUMA node. That >> + * means it only accesses buffers from the same NUMA node. >> + * >> + * XXX That also means each process "sweeps" only a fraction of buffers, >> + * even if the other buffers are better candidates for eviction. Maybe >> + * there should be some logic to "steal" buffers from other freelists >> + * or other nodes? > > I think we *definitely* need "stealing" from other clock sweeps, whenever > there's a meaningful imbalance between the different sweeps. > > I don't think we need to be overly precise about it, a small imbalance won't > have that much of an effect. But clearly it doesn't make sense to say that one > backend can only fill buffers in the current partition, that'd lead to massive > performance issues in a lot of workloads. > Agreed. > The hardest thing probably is to make the logic for when to check foreign > clock sweeps cheap enough. > > One way would be to do it whenever a sweep wraps around, that'd probably > amortize the cost sufficiently, and I don't think it'd be too imprecise, as > we'd have processed that set of buffers in a row without partitioning as > well. But it'd probably be too coarse when determining for how long to use a > foreign sweep instance. But we probably could address that by rechecking the > balanace more frequently when using a foreign partition. > What you mean by "it"? What would happen after a sweep wraps around? > Another way would be to have bgwriter manage this. Whenever it detects that > one ring is too far ahead, it could set a "avoid this partition" bit, which > would trigger backends that natively use that partition to switch to foreign > partitions that don't currently have that bit set. I suspect there's a > problem with that approach though, I worry that the amount of time that > bgwriter spends in BgBufferSync() may sometimes be too long, leading to too > much imbalance. > I'm afraid having hard "avoid" flags would lead to sudden and unexpected changes in performance as we enable/disable partitions. I think a good solution should "smooth it out" somehow, e.g. by not having a true/false flag, but having some sort of "preference" factor with values between (0.0, 1.0) which says how much we should use that partition. I was imagining something like this: Say we know the number of buffers allocated for each partition (in the last round), and we (or rather the BgBufferSync) calculate: coefficient = 1.0 - (nallocated_partition / nallocated) and then use that to "correct" which partition to allocate buffers from. Or maybe just watch how far from the "fair share" we were in the last interval, and gradually increase/decrease the "partition preference" which would say how often we need to "steal" from other partitions. E.g. we find nallocated_partition is 2x the fair share, i.e. nallocated_partition / (nallocated / nparts) = 2.0 Then we say 25% of the time look at some other partition, to "cut" the imbalance in half. And then repeat that in the next cycle, etc. So a process would look at it's "home partition" by default, but it's "roll a dice" first and if above the calculated probability it'd pick some other partition instead (this would need to be done so that it gets balanced overall). If the bgwriter interval is too long, maybe the recalculation could be triggered regularly after any of the clocksweeps wraps around, or after some number of allocations, or something like that. regards -- Tomas Vondra -
Re: Adding basic NUMA awareness
Andres Freund <andres@anarazel.de> — 2025-07-18T21:03:59Z
Hi, On 2025-07-18 22:48:00 +0200, Tomas Vondra wrote: > On 7/18/25 18:46, Andres Freund wrote: > >> For a read-write pgbench I however saw some strange drops/increases of > >> throughput. I suspect this might be due to some thinko in the clocksweep > >> partitioning, but I'll need to take a closer look. > > > > Was that with pinning etc enabled or not? > > > > IIRC it was with everything enabled, except for numa_procs_pin (which > pins backend to NUMA node). I found that to actually harm performance in > some of the tests (even just read-only ones), resulting in uneven usage > of cores and lower throughput. FWIW, I really doubt that something like numa_procs_pin is viable outside of very narrow niches until we have a *lot* more infrastructure in place. Like PG would need to be threaded, we'd need a separation between thread and connection and an executor that'd allow us to switch from working on one query to working on another query. > > The hardest thing probably is to make the logic for when to check foreign > > clock sweeps cheap enough. > > > > One way would be to do it whenever a sweep wraps around, that'd probably > > amortize the cost sufficiently, and I don't think it'd be too imprecise, as > > we'd have processed that set of buffers in a row without partitioning as > > well. But it'd probably be too coarse when determining for how long to use a > > foreign sweep instance. But we probably could address that by rechecking the > > balanace more frequently when using a foreign partition. > > > > What you mean by "it"? it := Considering switching back from using a "foreign" clock sweep instance whenever the sweep wraps around. > What would happen after a sweep wraps around? The scenario I'm worried about is this: 1) a bunch of backends read buffers on numa node A, using the local clock sweep instance 2) due to all of that activity, the clock sweep advances much faster than the clock sweep for numa node B 3) the clock sweep on A wraps around, we discover the imbalance, and all the backend switch to scanning on numa node B, moving that clock sweep ahead much more aggressively 4) clock sweep on B wraps around, there's imbalance the other way round now, so they all switch back to A > > Another way would be to have bgwriter manage this. Whenever it detects that > > one ring is too far ahead, it could set a "avoid this partition" bit, which > > would trigger backends that natively use that partition to switch to foreign > > partitions that don't currently have that bit set. I suspect there's a > > problem with that approach though, I worry that the amount of time that > > bgwriter spends in BgBufferSync() may sometimes be too long, leading to too > > much imbalance. > > > > I'm afraid having hard "avoid" flags would lead to sudden and unexpected > changes in performance as we enable/disable partitions. I think a good > solution should "smooth it out" somehow, e.g. by not having a true/false > flag, but having some sort of "preference" factor with values between > (0.0, 1.0) which says how much we should use that partition. Yea, I think that's a fair worry. > I was imagining something like this: > > Say we know the number of buffers allocated for each partition (in the > last round), and we (or rather the BgBufferSync) calculate: > > coefficient = 1.0 - (nallocated_partition / nallocated) > > and then use that to "correct" which partition to allocate buffers from. > Or maybe just watch how far from the "fair share" we were in the last > interval, and gradually increase/decrease the "partition preference" > which would say how often we need to "steal" from other partitions. > > E.g. we find nallocated_partition is 2x the fair share, i.e. > > nallocated_partition / (nallocated / nparts) = 2.0 > > Then we say 25% of the time look at some other partition, to "cut" the > imbalance in half. And then repeat that in the next cycle, etc. > > So a process would look at it's "home partition" by default, but it's > "roll a dice" first and if above the calculated probability it'd pick > some other partition instead (this would need to be done so that it gets > balanced overall). That does sound reasonable. > If the bgwriter interval is too long, maybe the recalculation could be > triggered regularly after any of the clocksweeps wraps around, or after > some number of allocations, or something like that. I'm pretty sure the bgwriter might not be often enough and not predictably frequently running for that. Greetings, Andres Freund
-
Re: Adding basic NUMA awareness
Jakub Wartak <jakub.wartak@enterprisedb.com> — 2025-07-25T10:27:11Z
On Thu, Jul 17, 2025 at 11:15 PM Tomas Vondra <tomas@vondra.me> wrote: > > On 7/4/25 20:12, Tomas Vondra wrote: > > On 7/4/25 13:05, Jakub Wartak wrote: > >> ... > >> > >> 8. v1-0005 2x + /* if (numa_procs_interleave) */ > >> > >> Ha! it's a TRAP! I've uncommented it because I wanted to try it out > >> without it (just by setting GUC off) , but "MyProc->sema" is NULL : > >> > >> 2025-07-04 12:31:08.103 CEST [28754] LOG: starting PostgreSQL > >> 19devel on x86_64-linux, compiled by gcc-12.2.0, 64-bit > >> [..] > >> 2025-07-04 12:31:08.109 CEST [28754] LOG: io worker (PID 28755) > >> was terminated by signal 11: Segmentation fault > >> 2025-07-04 12:31:08.109 CEST [28754] LOG: terminating any other > >> active server processes > >> 2025-07-04 12:31:08.114 CEST [28754] LOG: shutting down because > >> "restart_after_crash" is off > >> 2025-07-04 12:31:08.116 CEST [28754] LOG: database system is shut down > >> > >> [New LWP 28755] > >> [Thread debugging using libthread_db enabled] > >> Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". > >> Core was generated by `postgres: io worker '. > >> Program terminated with signal SIGSEGV, Segmentation fault. > >> #0 __new_sem_wait_fast (definitive_result=1, sem=sem@entry=0x0) > >> at ./nptl/sem_waitcommon.c:136 > >> 136 ./nptl/sem_waitcommon.c: No such file or directory. > >> (gdb) where > >> #0 __new_sem_wait_fast (definitive_result=1, sem=sem@entry=0x0) > >> at ./nptl/sem_waitcommon.c:136 > >> #1 __new_sem_trywait (sem=sem@entry=0x0) at ./nptl/sem_wait.c:81 > >> #2 0x00005561918e0cac in PGSemaphoreReset (sema=0x0) at > >> ../src/backend/port/posix_sema.c:302 > >> #3 0x0000556191970553 in InitAuxiliaryProcess () at > >> ../src/backend/storage/lmgr/proc.c:992 > >> #4 0x00005561918e51a2 in AuxiliaryProcessMainCommon () at > >> ../src/backend/postmaster/auxprocess.c:65 > >> #5 0x0000556191940676 in IoWorkerMain (startup_data=<optimized > >> out>, startup_data_len=<optimized out>) at > >> ../src/backend/storage/aio/method_worker.c:393 > >> #6 0x00005561918e8163 in postmaster_child_launch > >> (child_type=child_type@entry=B_IO_WORKER, child_slot=20086, > >> startup_data=startup_data@entry=0x0, > >> startup_data_len=startup_data_len@entry=0, > >> client_sock=client_sock@entry=0x0) at > >> ../src/backend/postmaster/launch_backend.c:290 > >> #7 0x00005561918ea09a in StartChildProcess > >> (type=type@entry=B_IO_WORKER) at > >> ../src/backend/postmaster/postmaster.c:3973 > >> #8 0x00005561918ea308 in maybe_adjust_io_workers () at > >> ../src/backend/postmaster/postmaster.c:4404 > >> [..] > >> (gdb) print *MyProc->sem > >> Cannot access memory at address 0x0 > >> > > > > Yeah, good catch. I'll look into that next week. > > > > I've been unable to reproduce this issue, but I'm not sure what settings > you actually used for this instance. Can you give me more details how to > reproduce this? Better late than never, well feel free to partially ignore me, i've missed that it is known issue as per FIXME there, but I would just rip out that commented out `if(numa_proc_interleave)` from FastPathLockShmemSize() and PGProcShmemSize() unless you want to save those memory pages of course (in case of no-NUMA). If you do want to save those pages I think we have problem: For complete picture, steps: 1. patch -p1 < v2-0001-NUMA-interleaving-buffers.patch 2. patch -p1 < v2-0006-NUMA-interleave-PGPROC-entries.patch BTW the pgbench accidentinal ident is still there (part of v2-0001 patch)) 14 out of 14 hunks FAILED -- saving rejects to file src/bin/pgbench/pgbench.c.rej 3. As I'm just applying 0001 and 0006, I've got two simple rejects, but fixed it (due to not applying missing numa_ freelist patches). That's intentional on my part, because I wanted to play just with those two. 4. Then I uncomment those two "if (numa_procs_interleave)" related for optional memory shm initialization - add_size() and so on (that have XXX comment above that it is causing bootstrap issues) 5. initdb with numa_procs_interleave=on, huge_pages = on (!), start, it is ok 6. restart with numa_procs_interleave=off, which gets me to every bg worker crashing e.g.: (gdb) where #0 __new_sem_wait_fast (definitive_result=1, sem=sem@entry=0x0) at ./nptl/sem_waitcommon.c:136 #1 __new_sem_trywait (sem=sem@entry=0x0) at ./nptl/sem_wait.c:81 #2 0x0000563e2d6e4d5c in PGSemaphoreReset (sema=0x0) at ../src/backend/port/posix_sema.c:302 #3 0x0000563e2d774d93 in InitAuxiliaryProcess () at ../src/backend/storage/lmgr/proc.c:995 #4 0x0000563e2d6e9252 in AuxiliaryProcessMainCommon () at ../src/backend/postmaster/auxprocess.c:65 #5 0x0000563e2d6eb683 in CheckpointerMain (startup_data=<optimized out>, startup_data_len=<optimized out>) at ../src/backend/postmaster/checkpointer.c:190 #6 0x0000563e2d6ec363 in postmaster_child_launch (child_type=child_type@entry=B_CHECKPOINTER, child_slot=249, startup_data=startup_data@entry=0x0, startup_data_len=startup_data_len@entry=0, client_sock=client_sock@entry=0x0) at ../src/backend/postmaster/launch_backend.c:290 #7 0x0000563e2d6ee29a in StartChildProcess (type=type@entry=B_CHECKPOINTER) at ../src/backend/postmaster/postmaster.c:3973 #8 0x0000563e2d6f17a6 in PostmasterMain (argc=argc@entry=3, argv=argv@entry=0x563e377cc0e0) at ../src/backend/postmaster/postmaster.c:1386 #9 0x0000563e2d4948fc in main (argc=3, argv=0x563e377cc0e0) at ../src/backend/main/main.c:231 notice sema=0x0, because: #3 0x000056050928cd93 in InitAuxiliaryProcess () at ../src/backend/storage/lmgr/proc.c:995 995 PGSemaphoreReset(MyProc->sem); (gdb) print MyProc $1 = (PGPROC *) 0x7f09a0c013b0 (gdb) print MyProc->sem $2 = (PGSemaphore) 0x0 or with printfs: 2025-07-25 11:17:23.683 CEST [21772] LOG: in InitProcGlobal PGPROC=0x7f9de827b880 requestSize=148770 // after proc && ptr manipulation: 2025-07-25 11:17:23.683 CEST [21772] LOG: in InitProcGlobal PGPROC=0x7f9de827bdf0 requestSize=148770 procs=0x7f9de827b880 ptr=0x7f9de827bdf0 [..initialization of aux PGPROCs i=0.., still fromInitProcGlobal(), each gets proper sem allocated as one would expect:] [..for i loop:] 2025-07-25 11:17:23.689 CEST [21772] LOG: i=136 , proc=0x7f9de8600000, proc->sem=0x7f9da4e04438 2025-07-25 11:17:23.689 CEST [21772] LOG: i=137 , proc=0x7f9de8600348, proc->sem=0x7f9da4e044b8 2025-07-25 11:17:23.689 CEST [21772] LOG: i=138 , proc=0x7f9de8600690, proc->sem=0x7f9da4e04538 [..but then in the children codepaths, out of the blue in InitAuxilaryProcess the whole MyProc looks like it would memsetted to zeros:] 2025-07-25 11:17:23.693 CEST [21784] LOG: auxiliary process using MyProc=0x7f9de8600000 auxproc=0x7f9de8600000 proctype=0 MyProcPid=21784 MyProc->sem=(nil) above got pgproc slot i=136 with addr 0x7f9de8600000 and later that auxiliary is launched but somehow something NULLified ->sem there (according to gdb , everything is zero there) 7. Original patch v2-0006 (with commented out 2x if numa_procs_interleave), behaves OK, so in my case here with 1x NUMA node that gives add_size(.., 1+1 * 2MB)=4MB 2025-07-25 11:38:54.131 CEST [23939] LOG: in InitProcGlobal PGPROC=0x7f25cbe7b880 requestSize=4343074 2025-07-25 11:38:54.132 CEST [23939] LOG: in InitProcGlobal PGPROC=0x7f25cbe7bdf0 requestSize=4343074 procs=0x7f25cbe7b880 ptr=0x7f25cbe7bdf0 so something is zeroing out all those MyProc structures apparently on startup (probably due to some wrong alignment maybe somewhere ?) I was thinking about trapping via mprotect() this single i=136 0x7f9de8600000 PGPROC to see what is resetting it, but oh well, mprotect() works only on whole pages... -J. -
Re: Adding basic NUMA awareness
Tomas Vondra <tomas@vondra.me> — 2025-07-25T10:51:41Z
On 7/25/25 12:27, Jakub Wartak wrote: > On Thu, Jul 17, 2025 at 11:15 PM Tomas Vondra <tomas@vondra.me> wrote: >> >> On 7/4/25 20:12, Tomas Vondra wrote: >>> On 7/4/25 13:05, Jakub Wartak wrote: >>>> ... >>>> >>>> 8. v1-0005 2x + /* if (numa_procs_interleave) */ >>>> >>>> Ha! it's a TRAP! I've uncommented it because I wanted to try it out >>>> without it (just by setting GUC off) , but "MyProc->sema" is NULL : >>>> >>>> 2025-07-04 12:31:08.103 CEST [28754] LOG: starting PostgreSQL >>>> 19devel on x86_64-linux, compiled by gcc-12.2.0, 64-bit >>>> [..] >>>> 2025-07-04 12:31:08.109 CEST [28754] LOG: io worker (PID 28755) >>>> was terminated by signal 11: Segmentation fault >>>> 2025-07-04 12:31:08.109 CEST [28754] LOG: terminating any other >>>> active server processes >>>> 2025-07-04 12:31:08.114 CEST [28754] LOG: shutting down because >>>> "restart_after_crash" is off >>>> 2025-07-04 12:31:08.116 CEST [28754] LOG: database system is shut down >>>> >>>> [New LWP 28755] >>>> [Thread debugging using libthread_db enabled] >>>> Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". >>>> Core was generated by `postgres: io worker '. >>>> Program terminated with signal SIGSEGV, Segmentation fault. >>>> #0 __new_sem_wait_fast (definitive_result=1, sem=sem@entry=0x0) >>>> at ./nptl/sem_waitcommon.c:136 >>>> 136 ./nptl/sem_waitcommon.c: No such file or directory. >>>> (gdb) where >>>> #0 __new_sem_wait_fast (definitive_result=1, sem=sem@entry=0x0) >>>> at ./nptl/sem_waitcommon.c:136 >>>> #1 __new_sem_trywait (sem=sem@entry=0x0) at ./nptl/sem_wait.c:81 >>>> #2 0x00005561918e0cac in PGSemaphoreReset (sema=0x0) at >>>> ../src/backend/port/posix_sema.c:302 >>>> #3 0x0000556191970553 in InitAuxiliaryProcess () at >>>> ../src/backend/storage/lmgr/proc.c:992 >>>> #4 0x00005561918e51a2 in AuxiliaryProcessMainCommon () at >>>> ../src/backend/postmaster/auxprocess.c:65 >>>> #5 0x0000556191940676 in IoWorkerMain (startup_data=<optimized >>>> out>, startup_data_len=<optimized out>) at >>>> ../src/backend/storage/aio/method_worker.c:393 >>>> #6 0x00005561918e8163 in postmaster_child_launch >>>> (child_type=child_type@entry=B_IO_WORKER, child_slot=20086, >>>> startup_data=startup_data@entry=0x0, >>>> startup_data_len=startup_data_len@entry=0, >>>> client_sock=client_sock@entry=0x0) at >>>> ../src/backend/postmaster/launch_backend.c:290 >>>> #7 0x00005561918ea09a in StartChildProcess >>>> (type=type@entry=B_IO_WORKER) at >>>> ../src/backend/postmaster/postmaster.c:3973 >>>> #8 0x00005561918ea308 in maybe_adjust_io_workers () at >>>> ../src/backend/postmaster/postmaster.c:4404 >>>> [..] >>>> (gdb) print *MyProc->sem >>>> Cannot access memory at address 0x0 >>>> >>> >>> Yeah, good catch. I'll look into that next week. >>> >> >> I've been unable to reproduce this issue, but I'm not sure what settings >> you actually used for this instance. Can you give me more details how to >> reproduce this? > > Better late than never, well feel free to partially ignore me, i've > missed that it is known issue as per FIXME there, but I would just rip > out that commented out `if(numa_proc_interleave)` from > FastPathLockShmemSize() and PGProcShmemSize() unless you want to save > those memory pages of course (in case of no-NUMA). If you do want to > save those pages I think we have problem: > > For complete picture, steps: > > 1. patch -p1 < v2-0001-NUMA-interleaving-buffers.patch > 2. patch -p1 < v2-0006-NUMA-interleave-PGPROC-entries.patch > > BTW the pgbench accidentinal ident is still there (part of v2-0001 patch)) > 14 out of 14 hunks FAILED -- saving rejects to file > src/bin/pgbench/pgbench.c.rej > > 3. As I'm just applying 0001 and 0006, I've got two simple rejects, > but fixed it (due to not applying missing numa_ freelist patches). > That's intentional on my part, because I wanted to play just with > those two. > > 4. Then I uncomment those two "if (numa_procs_interleave)" related for > optional memory shm initialization - add_size() and so on (that have > XXX comment above that it is causing bootstrap issues) > Ah, I didn't realize you uncommented these "if" conditions. In that case the crash is not very surprising, because the actual initialization in InitProcGlobal ignores the GUCs and just assumes it's enabled. But without the extra padding that likely messes up something. Or something allocated later "overwrites" the some of the memory. I need to clean this up, to actually consider the GUC properly. FWIW I do have a new patch version that I plan to share in a day or two, once I get some numbers. It didn't change this particular part, though, it's more about the buffers/freelists/clocksweep. I'll work on PGPROC next, I think. regards -- Tomas Vondra
-
Re: Adding basic NUMA awareness
Tomas Vondra <tomas@vondra.me> — 2025-07-28T14:19:07Z
Hi, Here's a somewhat cleaned up v3 of this patch series, with various improvements and a lot of cleanup. Still WIP, but I hope it resolves the various crashes reported for v2, but it still requires --with-libnuma (it won't build without it). I'm aware there's an ongoing discussion about removing the freelists, and changing the clocksweep in some way. If that happens, the relevant parts of this series will need some adjustment, of course. I haven't looked into that yet, I plan to review those patches soon. main changes in v3 ------------------ 1) I've introduced "registry" of the buffer partitions (imagine a small array of structs), serving as a source of truth for places that need info about the partitions (range of buffers, ...). With v2 there was no "shared definition" - the shared buffers, freelist and clocksweep did their own thing. But per the discussion it doesn't really make much sense for to partition buffers in different ways. So in v3 the 0001 patch defines the partitions, records them in shared memory (in a small array), and the later parts just reuse this. I also added a pg_buffercache_partitions() listing the partitions, with first/last buffer, etc. The freelist/clocksweep patches add additional information. 2) The PGPROC part introduces a similar registry, even though there are no other patches building on this. But it seemed useful to have a clear place recording this info. There's also a view pg_buffercache_pgproc. The pg_buffercache location is a bit bogus - it has nothing to do with buffers, but it was good enough for now. 3) The PGPROC partitioning is reworked and should fix the crash with the GUC set to "off". 4) This still doesn't do anything about "balancing" the clocksweep. I have some ideas how to do that, I'll work on that next. simple benchmark ---------------- I did a simple benchmark, measuring pgbench throughput with scale still fitting into RAM, but much larger (~2x) than shared buffers. See the attached test script, testing builds with more and more of the patches. I'm attaching results from two different machines (the "usual" 2P xeon and also a much larger cloud instance with EPYC/Genoa) - both the raw CSV files, with average tps and percentiles, and PDFs. The PDFs also have a comparison either to the "preceding" build (right side), or to master (below the table). There's results for the three "pgbench pinning" strategies, and that can have pretty significant impact (colocated generally performs much better than either "none" or "random"). For the "bigger" machine (wiuth 176 cores) the incremental results look like this (for pinning=none, i.e. regular pgbench): mode s_b buffers localal no-tail freelist sweep pgproc pinning ==================================================================== prepared 16GB 99% 101% 100% 103% 111% 99% 102% 32GB 98% 102% 99% 103% 107% 101% 112% 8GB 97% 102% 100% 102% 101% 101% 106% -------------------------------------------------------------------- simple 16GB 100% 100% 99% 105% 108% 99% 108% 32GB 98% 101% 100% 103% 100% 101% 97% 8GB 100% 100% 101% 99% 100% 104% 104% The way I read this is that the first three patches have about no impact on throughput. Then freelist partitioning and (especially) clocksweep partitioning can help quite a bit. pgproc is again close to ~0%, and PGPROC pinning can help again (but this part is merely experimental). For the xeon the differences (in either direction) are much smaller, so I'm not going to post it here. It's in the PDF, though. I think this looks reasonable. The way I see this patch series is not about improving peak throughput, but more about reducing imbalance and making the behavior more consistent. The results are more a confirmation there's not some sort of massive overhead, somewhere. But I'll get to this in a minute. To quantify this kind of improvement, I think we'll need tests that intentionally cause (or try to) imbalance. If you have ideas for such tests, let me know. overhead of partitioning calculation ------------------------------------ Regarding the "overhead", while the results look mostly OK, I think we'll need to rethink the partitioning scheme - particularly how the partition size is calculated. The current scheme has to use %, which can be somewhat expensive. The 0001 patch calculates a "chunk size", which is the smallest number of buffers it can "assign" to a NUMA node. This depends on how many buffer descriptors fit onto a single memory page, and it's either 512KB (with 4KB pages), or 256MB (with 2MB huge pages). And then each NUMA node gets multiple chunks, to cover shared_buffers/num_nodes. But this can be an arbitrary number - it minimizes the imbalance, but it also forces the use of % and / in the formulas. AFAIK if we required the partitions to be 2^k multiples of the chunk size, we could switch to using shifts and masking. Which is supposed to be much faster. But I haven't measured this, and the cost is that some of the nodes could get much less memory. Maybe that's fine. reserving number of huge pages ------------------------------ The other thing I realized is that partitioning buffers with huge pages is quite tricky, and can easily lead to SIGBUS when accessing the memory later. The crashes I saw happen like this: 1) figure # of pages needed (using shared_memory_size_in_huge_pages) This can be 16828 for shared_buffers=32GB. 2) make sure there's enough huge pages echo 16828 > /proc/sys/vm/nr_hugepages 3) start postgres - everything seems to works just fine 4) query pg_buffercache_numa - triggers SIGBUS accessing memory for a valid buffer (usually ~2GB from the end) It took me ages to realize what's happening, but it's very simple. The nr_hugepages is a global limit, but it's also translated into limits for each NUMA node. So when you write 16828 to it, in a 4-node system each node gets 1/4 of that. See $ numastat -cm Then we do the mmap(), and everything looks great, because there really is enough huge pages and the system can allocate memory from any NUMA node it needs. And then we come around, and do the numa_tonode_memory(). And that's where the issues start, because AFAIK this does not check the per-node limit of huge pages in any way. It just appears to work. And then later, when we finally touch the buffer, it tries to actually allocate the memory on the node, and realizes there's not enough huge pages. And triggers the SIGBUS. You may ask why the per-node limit is too low. We still need just shared_memory_size_in_huge_pages, right? And if we were partitioning the whole memory segment, that'd be true. But we only to that for shared buffers, and there's a lot of other shared memory - could be 1-2GB or so, depending on the configuration. And this gets placed on one of the nodes, and it counts against the limit on that particular node. And so it doesn't have enough huge pages to back the partition of shared buffers. The only way around this I found is by inflating the number of huge pages, significantly above the shared_memory_size_in_huge_pages value. Just to make sure the nodes get enough huge pages. I don't know what to do about this. It's quite annoying. If we only used huge pages for the partitioned parts, this wouldn't be a problem. I also realize this can be used to make sure the memory is balanced on NUMA systems. Because if you set nr_hugepages, the kernel will ensure the shared memory is distributed on all the nodes. It won't have the benefits of "coordinating" the buffers and buffer descriptors, and so on. But it will be balanced. regards -- Tomas Vondra -
Re: Adding basic NUMA awareness
Jakub Wartak <jakub.wartak@enterprisedb.com> — 2025-07-30T08:29:06Z
On Mon, Jul 28, 2025 at 4:22 PM Tomas Vondra <tomas@vondra.me> wrote: Hi Tomas, just a quick look here: > 2) The PGPROC part introduces a similar registry, [..] > > There's also a view pg_buffercache_pgproc. The pg_buffercache location > is a bit bogus - it has nothing to do with buffers, but it was good > enough for now. If you are looking for better names: pg_shmem_pgproc_numa would sound like a more natural name. > 3) The PGPROC partitioning is reworked and should fix the crash with the > GUC set to "off". Thanks! > simple benchmark > ---------------- [..] > There's results for the three "pgbench pinning" strategies, and that can > have pretty significant impact (colocated generally performs much better > than either "none" or "random"). Hint: real world is that network cards are usually located on some PCI slot that is assigned to certain node (so traffic is flowing from/to there), so probably it would make some sense to put pgbench outside this machine and remove this as "variable" anyway and remove the need for that pgbench --pin-cpus in script. In optimal conditions: most optimized layout would be probably to have 2 cards on separate PCI slots, each for different node and some LACP between those, with xmit_hash_policy allowing traffic distribution on both of those cards -- usually there's not just single IP/MAC out there talking to/from such server, so that would be real-world (or lack of) affinity. Also classic pgbench workload, seems to be poor fit for testing it out (at least v3-0001 buffers), there I would propose sticking to just lots of big (~s_b size) full table seq scans to put stress on shared memory. Classic pgbench is usually not there enough to put serious bandwidth on the interconnect by my measurements. > For the "bigger" machine (wiuth 176 cores) the incremental results look > like this (for pinning=none, i.e. regular pgbench): > > > mode s_b buffers localal no-tail freelist sweep pgproc pinning > ==================================================================== > prepared 16GB 99% 101% 100% 103% 111% 99% 102% > 32GB 98% 102% 99% 103% 107% 101% 112% > 8GB 97% 102% 100% 102% 101% 101% 106% > -------------------------------------------------------------------- > simple 16GB 100% 100% 99% 105% 108% 99% 108% > 32GB 98% 101% 100% 103% 100% 101% 97% > 8GB 100% 100% 101% 99% 100% 104% 104% > > The way I read this is that the first three patches have about no impact > on throughput. Then freelist partitioning and (especially) clocksweep > partitioning can help quite a bit. pgproc is again close to ~0%, and > PGPROC pinning can help again (but this part is merely experimental). Isn't the "pinning" column representing just numa_procs_pin=on ? (shouldn't it be tested with numa_procs_interleave = on?) [..] > To quantify this kind of improvement, I think we'll need tests that > intentionally cause (or try to) imbalance. If you have ideas for such > tests, let me know. Some ideas: 1. concurrent seq scans hitting s_b-sized table 2. one single giant PX-enabled seq scan with $VCPU workers (stresses the importance of interleaving dynamic shm for workers) 3. select txid_current() with -M prepared? > reserving number of huge pages > ------------------------------ [..] > It took me ages to realize what's happening, but it's very simple. The > nr_hugepages is a global limit, but it's also translated into limits for > each NUMA node. So when you write 16828 to it, in a 4-node system each > node gets 1/4 of that. See > > $ numastat -cm > > Then we do the mmap(), and everything looks great, because there really > is enough huge pages and the system can allocate memory from any NUMA > node it needs. Yup, similiar story as with OOMs just for per-zone/node. > And then we come around, and do the numa_tonode_memory(). And that's > where the issues start, because AFAIK this does not check the per-node > limit of huge pages in any way. It just appears to work. And then later, > when we finally touch the buffer, it tries to actually allocate the > memory on the node, and realizes there's not enough huge pages. And > triggers the SIGBUS. I think that's why options for strict policy numa allocation exist and I had the option to use it in my patches (anyway with one big call to numa_interleave_memory() for everything it was much more simpler and just not micromanaging things). Good reads are numa(3) but e.g. mbind(2) underneath will tell you that e.g. `Before Linux 5.7. MPOL_MF_STRICT was ignored on huge page mappings.` (I was on 6.14.x, but it could be happening for you too if you start using it). Anyway, numa_set_strict() is just wrapper around setting this exact flag Anyway remember that volatile pg_numa_touch_mem_if_required()? - maybe that should be always called in your patch series to pre-populate everything during startup, so that others testing will get proper guaranteed layout, even without issuing any pg_buffercache calls. > The only way around this I found is by inflating the number of huge > pages, significantly above the shared_memory_size_in_huge_pages value. > Just to make sure the nodes get enough huge pages. > > I don't know what to do about this. It's quite annoying. If we only used > huge pages for the partitioned parts, this wouldn't be a problem. Meh, sacrificing a couple of huge pages (worst-case 1GB ?) just to get NUMA affinity, seems like a logical trade-off, doesn't it? But postgres -C shared_memory_size_in_huge_pages still works OK to establish the exact count for vm.nr_hugepages, right? Regards, -J.
-
Re: Adding basic NUMA awareness
Tomas Vondra <tomas@vondra.me> — 2025-07-30T11:10:45Z
On 7/30/25 10:29, Jakub Wartak wrote: > On Mon, Jul 28, 2025 at 4:22 PM Tomas Vondra <tomas@vondra.me> wrote: > > Hi Tomas, > > just a quick look here: > >> 2) The PGPROC part introduces a similar registry, [..] >> >> There's also a view pg_buffercache_pgproc. The pg_buffercache location >> is a bit bogus - it has nothing to do with buffers, but it was good >> enough for now. > > If you are looking for better names: pg_shmem_pgproc_numa would sound > like a more natural name. > >> 3) The PGPROC partitioning is reworked and should fix the crash with the >> GUC set to "off". > > Thanks! > >> simple benchmark >> ---------------- > [..] >> There's results for the three "pgbench pinning" strategies, and that can >> have pretty significant impact (colocated generally performs much better >> than either "none" or "random"). > > Hint: real world is that network cards are usually located on some PCI > slot that is assigned to certain node (so traffic is flowing from/to > there), so probably it would make some sense to put pgbench outside > this machine and remove this as "variable" anyway and remove the need > for that pgbench --pin-cpus in script. In optimal conditions: most > optimized layout would be probably to have 2 cards on separate PCI > slots, each for different node and some LACP between those, with > xmit_hash_policy allowing traffic distribution on both of those cards > -- usually there's not just single IP/MAC out there talking to/from > such server, so that would be real-world (or lack of) affinity. > The pgbench pinning certainly reduces some of the noise / overhead you get when using multiple machines. I use it to "isolate" patches, and make the effects more visible. > Also classic pgbench workload, seems to be poor fit for testing it out > (at least v3-0001 buffers), there I would propose sticking to just > lots of big (~s_b size) full table seq scans to put stress on shared > memory. Classic pgbench is usually not there enough to put serious > bandwidth on the interconnect by my measurements. > Yes, that's possible. The simple pgbench workload is a bit of a "worst case" for the NUMA patches, in that it's can benefit less from the improvements, and it's also fairly sensitive to regressions. I plan to do more tests with other types of workloads, like the one doing a lot of large sequential scans, etc. >> For the "bigger" machine (wiuth 176 cores) the incremental results look >> like this (for pinning=none, i.e. regular pgbench): >> >> >> mode s_b buffers localal no-tail freelist sweep pgproc pinning >> ==================================================================== >> prepared 16GB 99% 101% 100% 103% 111% 99% 102% >> 32GB 98% 102% 99% 103% 107% 101% 112% >> 8GB 97% 102% 100% 102% 101% 101% 106% >> -------------------------------------------------------------------- >> simple 16GB 100% 100% 99% 105% 108% 99% 108% >> 32GB 98% 101% 100% 103% 100% 101% 97% >> 8GB 100% 100% 101% 99% 100% 104% 104% >> >> The way I read this is that the first three patches have about no impact >> on throughput. Then freelist partitioning and (especially) clocksweep >> partitioning can help quite a bit. pgproc is again close to ~0%, and >> PGPROC pinning can help again (but this part is merely experimental). > > Isn't the "pinning" column representing just numa_procs_pin=on ? > (shouldn't it be tested with numa_procs_interleave = on?) > Maybe I don't understand the question, but the last column (pinning) compares two builds. 1) Build with all the patches up to "pgproc interleaving" (and all of the GUCs set to "on"). 2) Build with all the patches from (1), and "pinning" too (again, all GUCs set to "on). Or do I misunderstand the question? > [..] >> To quantify this kind of improvement, I think we'll need tests that >> intentionally cause (or try to) imbalance. If you have ideas for such >> tests, let me know. > > Some ideas: > 1. concurrent seq scans hitting s_b-sized table > 2. one single giant PX-enabled seq scan with $VCPU workers (stresses > the importance of interleaving dynamic shm for workers) > 3. select txid_current() with -M prepared? > Thanks. I think we'll try something like (1), but it'll need to be a bit more elaborate, because scans on tables larger than 1/4 shared buffers use a small circular buffer. >> reserving number of huge pages >> ------------------------------ > [..] >> It took me ages to realize what's happening, but it's very simple. The >> nr_hugepages is a global limit, but it's also translated into limits for >> each NUMA node. So when you write 16828 to it, in a 4-node system each >> node gets 1/4 of that. See >> >> $ numastat -cm >> >> Then we do the mmap(), and everything looks great, because there really >> is enough huge pages and the system can allocate memory from any NUMA >> node it needs. > > Yup, similiar story as with OOMs just for per-zone/node. > >> And then we come around, and do the numa_tonode_memory(). And that's >> where the issues start, because AFAIK this does not check the per-node >> limit of huge pages in any way. It just appears to work. And then later, >> when we finally touch the buffer, it tries to actually allocate the >> memory on the node, and realizes there's not enough huge pages. And >> triggers the SIGBUS. > > I think that's why options for strict policy numa allocation exist and > I had the option to use it in my patches (anyway with one big call to > numa_interleave_memory() for everything it was much more simpler and > just not micromanaging things). Good reads are numa(3) but e.g. > mbind(2) underneath will tell you that e.g. `Before Linux 5.7. > MPOL_MF_STRICT was ignored on huge page mappings.` (I was on 6.14.x, > but it could be happening for you too if you start using it). Anyway, > numa_set_strict() is just wrapper around setting this exact flag > > Anyway remember that volatile pg_numa_touch_mem_if_required()? - maybe > that should be always called in your patch series to pre-populate > everything during startup, so that others testing will get proper > guaranteed layout, even without issuing any pg_buffercache calls. > I think I tried using numa_set_strict, but it didn't change the behavior (i.e. the numa_tonode_memory didn't error out). >> The only way around this I found is by inflating the number of huge >> pages, significantly above the shared_memory_size_in_huge_pages value. >> Just to make sure the nodes get enough huge pages. >> >> I don't know what to do about this. It's quite annoying. If we only used >> huge pages for the partitioned parts, this wouldn't be a problem. > > Meh, sacrificing a couple of huge pages (worst-case 1GB ?) just to get > NUMA affinity, seems like a logical trade-off, doesn't it? > But postgres -C shared_memory_size_in_huge_pages still works OK to > establish the exact count for vm.nr_hugepages, right? > Well, yes and no. It tells you the exact number of huge pages, but it does not tell you how much you need to inflate it to account for the non-shared buffer part that may get allocated on a random node. regards -- Tomas Vondra
-
Re: Adding basic NUMA awareness
Tomas Vondra <tomas@vondra.me> — 2025-08-04T14:24:40Z
Hi, Here's an updated version of the patch series. The main improvement is the new 0006 patch, adding "adaptive balancing" of allocations. I'll also share some results from a workload doing a lot of allocations. adaptive balancing of allocations --------------------------------- Imagine each backend only allocates buffers from the partition on the same NUMA node. E.g. you have 4 NUMA nodes (i.e. 4 partitions), and a backend only allocates buffers from "home" partition (on the same NUMA node). This is what the earlier patch versions did, and with many backends that's mostly fine (assuming the backends get spread over all the NUMA nodes). But if there's only few backends doing the allocations, this can result in very inefficient use of shared buffers - a single backend would be limited to 25% of buffers, even if the rest is unused. There needs to be some say to "redirect" excess allocations to other partitions, so that the partitions are utilized about the same. This is what the 0006 patch aims to do (I kept is separate, but it should probably get merged into the "clocksweep partitioning" in the end). The balancing is fairly simple: (1) It tracks the number of allocations "requested" from each partition. (2) In regular intervals (by bgwriter) calculate the "fair share" per partition, and determine what fraction of "requests" to handle from the partition itself, and how many to redirect to other partitions. (3) Calculate coefficients to drive this for each partition. I emphasize (1) talks about "requests", not the actual allocations. Some of the requests could have been redirected to different partitions, and be counted as allocations there. We want to balance allocations, but it relies on the requests. To give you a simple example - imagine there are 2 partitions with this number of allocation requests: P1: 900,000 requests P2: 100,000 requests This means the "fair share" is 500,000 allocations, so P1 needs to redirect some requests to P2. And we end with these weights: P1: [ 55, 45] P2: [ 0, 100] Assuming the workload does not shift in some dramatic way, this should result in both partitions handling ~500k allocations. It's not hard to extend this algorithm to more partitions. For more details see StrategySyncBalance(), which recalculates this. There are a couple open questions, like: * The algorithm combines the old/new weights by averaging, to add a bit of hysteresis. Right now it's a simple average with 0.5 weight, to dampen sudden changes. I think it works fine (in the long run), but I'm open to suggestions how to do this better. * There's probably additional things we should consider when deciding where to redirect the allocations. For example, we may have multiple partitions per NUMA node, in which case it's better to redirect to that node as many allocations as possible. The current patch ignores this. * The partitions may have slightly different sizes, but the balancing ignores that for now. This is not very difficult to address. clocksweep benchmark -------------------- I ran a simple benchmark focused on allocation-heavy workloads, namely large concurrent sequential scans. The attached scripts generate a number of 1GB tables, and then run concurrent sequential scans with shared buffers set to 60%, 75%, 90% and 110% of the total dataset size. I did this for master, and with the NUMA patches applied (and the GUCs set to 'on'). I also increased tried with the of partitions increased to 16 (so a NUMA node got multiple partitions). There are results from three machines 1) ryzen - small non-NUMA system, mostly to see if there's regressions 2) xeon - older 2-node NUMA system 3) hb176 - big EPYC system with 176 cores / 4 NUMA nodes The script records detailed TPS stats (e.g. percentiles), I'm attaching CSV files with complete results, and some PDFs with charts summarizing that (I'll get to that in a minute). For the EPYC, the average tps for the three builds looks like this: clients | master numa numa-16 | numa numa-16 ----------|--------------------------------|--------------------- 8 | 20 27 26 | 133% 129% 16 | 23 39 45 | 170% 193% 24 | 23 48 58 | 211% 252% 32 | 21 57 68 | 268% 321% 40 | 21 56 76 | 265% 363% 48 | 22 59 82 | 270% 375% 56 | 22 66 88 | 296% 397% 64 | 23 62 93 | 277% 411% 72 | 24 68 95 | 277% 389% 80 | 24 72 95 | 295% 391% 88 | 25 71 98 | 283% 392% 96 | 26 74 97 | 282% 369% 104 | 26 74 97 | 282% 367% 112 | 27 77 95 | 287% 355% 120 | 28 77 92 | 279% 335% 128 | 27 75 89 | 277% 328% That's not bad - the clocksweep partitioning increases the throughput 2-3x. Having 16 partitions (instead of 4) helps yet a bit more, to 3-4x. This is for shared buffers set to 60% of the dataset, which depends on the number of clients / tables. With 64 clients/tables, there's 64GB of data, and shared buffers are set to ~39GB. The results for 75% and 90% follow the same pattern. For 110% there's much less impact - there are no allocations, so this has to be thanks to the other NUMA patches. The charts in the attached PDFs add a bit more detail, with various percentiles (of per-second throughput). The bands are roughly quartiles: 5-25%, 25-50%, 50-75%, 75-95%. The thick middle line is the median. There's only charts for 60%, 90% and 110% shared buffers, for fit it on a single page. There 75% is not very different. For ryzen there's little difference. Not surprising, it's not a NUMA system. So this is positive result, as there's no regression. For xeon the patches help a little bit. Again, not surprising. It's a fairly old system (~2016), and the differences between NUMA nodes are not that significant. For epyc (hb176), the differences are pretty massive. regards -- Tomas Vondra -
Re: Adding basic NUMA awareness
Tomas Vondra <tomas@vondra.me> — 2025-08-07T09:24:18Z
Hi! Here's a slightly improved version of the patch series. The main improvement is related to rebalancing partitions of different sizes (which can happen because the sizes have to be a multiple of some minimal "chunk" determined by memory page size etc.). Part 0009 deals with that by adjusting the allocations by partition size. It works OK, but it's also true it matters less as the shared_buffers size increases (as the relative difference between large/small partition gets smaller). The other improvements are related to the pg_buffercache_partitions view, showing the weights and (running) totals of allocations. I plan to take a break from this patch series for a while, so this would be a good time to take a look, do a review, run some tests etc. ;-) One detail about the balancing I forgot to mention in my last message is how the patch "distributes" allocations to match the balancing weights. Consider for example the example weights from that message: P1: [ 55, 45] P2: [ 0, 100] Imagine a backend located on P1 requests allocation of a buffer. The weights say 55% buffers should be allocated from P1, and 45% should be redirected to P2. One way to achieve that would be generating a random number in [1, 100], and if it's [1,55] then P1, otherwise P2. The patch does a much simpler thing - treat the weight as a "budget", i.e. number of buffers to allocate before proceeding to the "next" partition. So it allocates 55 buffers from P1, then 45 buffers from P2, and then goes back to P1 in a round-robin way. The advantage is it can do away without a PRNG. There's two things I'm not entirely sure about: 1) memory model - I'm not quite sure the current code ensures updates to weights are properly "communicated" to the other processes. That is, if the bgwriter recalculates the weights, will the other backends see the new weights right away? Using a stale weights won't cause "failures", the consequence is just a bit of imbalance. But it shouldn't stay like that for too long, so maybe it'd be good to add some memory barriers or something like that. 2) I'm a bit unsure what "NUMA nodes" actually means. The patch mostly assumes each core / piece of RAM is assigned to a particular NUMA node. For the buffer partitioning the patch mostly cares about memory, as it "locates" the buffers on different NUMA nodes. Which works mostly OK (ignoring the issues with huge pages described in previous message). But it also cares about the cores (and the node for each core), because it uses that to pick the right partition for a backend. And here the situation is less clear, because the CPUs don't need to be assigned to a particular node, even on a NUMA system. Consider the rpi5 NUMA layout: $ numactl --hardware available: 8 nodes (0-7) node 0 cpus: 0 1 2 3 node 0 size: 992 MB node 0 free: 274 MB node 1 cpus: 0 1 2 3 node 1 size: 1019 MB node 1 free: 327 MB node 2 cpus: 0 1 2 3 node 2 size: 1019 MB node 2 free: 321 MB node 3 cpus: 0 1 2 3 node 3 size: 955 MB node 3 free: 251 MB node 4 cpus: 0 1 2 3 node 4 size: 1019 MB node 4 free: 332 MB node 5 cpus: 0 1 2 3 node 5 size: 1019 MB node 5 free: 342 MB node 6 cpus: 0 1 2 3 node 6 size: 1019 MB node 6 free: 352 MB node 7 cpus: 0 1 2 3 node 7 size: 1014 MB node 7 free: 339 MB node distances: node 0 1 2 3 4 5 6 7 0: 10 10 10 10 10 10 10 10 1: 10 10 10 10 10 10 10 10 2: 10 10 10 10 10 10 10 10 3: 10 10 10 10 10 10 10 10 4: 10 10 10 10 10 10 10 10 5: 10 10 10 10 10 10 10 10 6: 10 10 10 10 10 10 10 10 7: 10 10 10 10 10 10 10 10 This says there are 8 NUMA nodes, each with ~1GB of RAM. But the 4 cores are not assigned to particular nodes - each core is mapped to all 8 NUMA nodes. I'm not sure what to do about this (or how getcpu() or libnuma handle this). And can the situation be even more complicated? regards -- Tomas Vondra
-
Re: Adding basic NUMA awareness
Tomas Vondra <tomas@vondra.me> — 2025-08-07T09:36:10Z
On 8/7/25 11:24, Tomas Vondra wrote: > Hi! > > Here's a slightly improved version of the patch series. > Ah, I made a mistake when generating the patches. The 0001 and 0002 patches are not part of the NUMA stuff, it's just something related to benchmarking (addressing unrelated bottlenecks etc.). The actual NUMA patches start with 0003. Also, 0007, 0008 and 0009 should ultimately be collapsed into a single patch. It's all about the clocksweep partitioning, I only kept those separate to make it easier to see the changes and review. regards -- Tomas Vondra
-
Re: Adding basic NUMA awareness
Andres Freund <andres@anarazel.de> — 2025-08-09T00:25:40Z
Hi, On 2025-08-07 11:24:18 +0200, Tomas Vondra wrote: > 2) I'm a bit unsure what "NUMA nodes" actually means. The patch mostly > assumes each core / piece of RAM is assigned to a particular NUMA node. There are systems in which some NUMA nodes do *not* contain any CPUs. E.g. if you attach memory via a CXL/PCIe add-in card, rather than via the CPUs memory controller. In that case numactl -H (and obviously also the libnuma APIs) will report that the numa node is not associated with any CPU. I don't currently have live access to such a system, but this PR piece happens to have numactl -H output: https://lenovopress.lenovo.com/lp2184-implementing-cxl-memory-on-linux-on-thinksystem-v4-servers > numactl -H > available: 4 nodes (0-3) > node 0 cpus: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 > node 0 size: 1031904 MB > node 0 free: 1025554 MB > node 1 cpus: 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 > node 1 size: 1032105 MB > node 1 free: 1024244 MB > node 2 cpus: > node 2 size: 262144 MB > node 2 free: 262143 MB > node 3 cpus: > node 3 size: 262144 MB > node 3 free: 262142 MB > node distances: > node 0 1 2 3 > 0: 10 21 14 24 > 1: 21 10 24 14 > 2: 14 24 10 26 > 3: 24 14 26 10 Note that node 2 & 3 don't have associated CPUs (and higher access costs). I don't think this is common enough to worry about from a performance POV, but we probably shouldn't crash if we encounter it... > But it also cares about the cores (and the node for each core), because > it uses that to pick the right partition for a backend. And here the > situation is less clear, because the CPUs don't need to be assigned to a > particular node, even on a NUMA system. Consider the rpi5 NUMA layout: > > $ numactl --hardware > available: 8 nodes (0-7) > node 0 cpus: 0 1 2 3 > node 0 size: 992 MB > node 0 free: 274 MB > node 1 cpus: 0 1 2 3 > node 1 size: 1019 MB > node 1 free: 327 MB > ... > node 0 1 2 3 4 5 6 7 > 0: 10 10 10 10 10 10 10 10 > 1: 10 10 10 10 10 10 10 10 > 2: 10 10 10 10 10 10 10 10 > 3: 10 10 10 10 10 10 10 10 > 4: 10 10 10 10 10 10 10 10 > 5: 10 10 10 10 10 10 10 10 > 6: 10 10 10 10 10 10 10 10 > 7: 10 10 10 10 10 10 10 10 > This says there are 8 NUMA nodes, each with ~1GB of RAM. But the 4 cores > are not assigned to particular nodes - each core is mapped to all 8 NUMA > nodes. FWIW, you can get a different version of this with AMD Epyc too, if "L3 LLC as NUMA" is enabled. > I'm not sure what to do about this (or how getcpu() or libnuma handle this). I don't immediately see any libnuma functions that would care? I also am somewhat curious about what getcpu() returns for the current node... Greetings, Andres Freund
-
Re: Adding basic NUMA awareness
Tomas Vondra <tomas@vondra.me> — 2025-08-12T11:04:07Z
On 8/9/25 02:25, Andres Freund wrote: > Hi, > > On 2025-08-07 11:24:18 +0200, Tomas Vondra wrote: >> 2) I'm a bit unsure what "NUMA nodes" actually means. The patch mostly >> assumes each core / piece of RAM is assigned to a particular NUMA node. > > There are systems in which some NUMA nodes do *not* contain any CPUs. E.g. if > you attach memory via a CXL/PCIe add-in card, rather than via the CPUs memory > controller. In that case numactl -H (and obviously also the libnuma APIs) will > report that the numa node is not associated with any CPU. > > I don't currently have live access to such a system, but this PR piece happens > to have numactl -H output: > https://lenovopress.lenovo.com/lp2184-implementing-cxl-memory-on-linux-on-thinksystem-v4-servers >> numactl -H >> available: 4 nodes (0-3) >> node 0 cpus: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 >> node 0 size: 1031904 MB >> node 0 free: 1025554 MB >> node 1 cpus: 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 >> node 1 size: 1032105 MB >> node 1 free: 1024244 MB >> node 2 cpus: >> node 2 size: 262144 MB >> node 2 free: 262143 MB >> node 3 cpus: >> node 3 size: 262144 MB >> node 3 free: 262142 MB >> node distances: >> node 0 1 2 3 >> 0: 10 21 14 24 >> 1: 21 10 24 14 >> 2: 14 24 10 26 >> 3: 24 14 26 10 > > Note that node 2 & 3 don't have associated CPUs (and higher access costs). > > I don't think this is common enough to worry about from a performance POV, but > we probably shouldn't crash if we encounter it... > Right. I don't think the current patch would crash - I can't test it, but I don't see why it would crash. In the worst case it'd end up with partitions that are not ideal. The question is more what would an ideal partitioning for buffers and PGPROC look like. Any opinions? For PGPROC, it's simple - it doesn't make sense to allocate partitions for nodes without CPUs. For buffers, it probably does not really matter if a node does not have any CPUs. If a node does not have any CPUs, that does not mean we should not put any buffers on it. After all, CXL will never have any CPUs (at least I think that's the case), and not using it for shared buffers would be a bit strange. Although, it could still be used for page cache. Maybe it should be "tiered" a bit more? The patch differentiates only between partitions on "my" NUMA node vs. every other partition. Maybe it should have more layers? That'd make the "balancing" harder. But I wanted to make it a smarter when handling cases with multiple partitions per NUMA node. > >> But it also cares about the cores (and the node for each core), because >> it uses that to pick the right partition for a backend. And here the >> situation is less clear, because the CPUs don't need to be assigned to a >> particular node, even on a NUMA system. Consider the rpi5 NUMA layout: >> >> $ numactl --hardware >> available: 8 nodes (0-7) >> node 0 cpus: 0 1 2 3 >> node 0 size: 992 MB >> node 0 free: 274 MB >> node 1 cpus: 0 1 2 3 >> node 1 size: 1019 MB >> node 1 free: 327 MB >> ... >> node 0 1 2 3 4 5 6 7 >> 0: 10 10 10 10 10 10 10 10 >> 1: 10 10 10 10 10 10 10 10 >> 2: 10 10 10 10 10 10 10 10 >> 3: 10 10 10 10 10 10 10 10 >> 4: 10 10 10 10 10 10 10 10 >> 5: 10 10 10 10 10 10 10 10 >> 6: 10 10 10 10 10 10 10 10 >> 7: 10 10 10 10 10 10 10 10 >> This says there are 8 NUMA nodes, each with ~1GB of RAM. But the 4 cores >> are not assigned to particular nodes - each core is mapped to all 8 NUMA >> nodes. > > FWIW, you can get a different version of this with AMD Epyc too, if "L3 LLC as > NUMA" is enabled. > > >> I'm not sure what to do about this (or how getcpu() or libnuma handle this). > > I don't immediately see any libnuma functions that would care? > Not sure what "care" means here. I don't think it's necessarily broken, it's more about the APIs not making the situation very clear (or convenient). How do you determine nodes for a CPU, for example? The closest thing I see is numa_node_of_cpu(), but that only returns a single node. Or how would you determine the number of nodes with CPUs (so that we create PGPROC partitions only for those)? I suppose that requires literally walking all the nodes. > I also am somewhat curious about what getcpu() returns for the current node... > It seems it only returns node 0. The cpu changes, but the node does not. regards -- Tomas Vondra
-
Re: Adding basic NUMA awareness
Andres Freund <andres@anarazel.de> — 2025-08-12T14:24:15Z
Hi, On 2025-08-12 13:04:07 +0200, Tomas Vondra wrote: > Right. I don't think the current patch would crash - I can't test it, > but I don't see why it would crash. In the worst case it'd end up with > partitions that are not ideal. The question is more what would an ideal > partitioning for buffers and PGPROC look like. Any opinions? > > For PGPROC, it's simple - it doesn't make sense to allocate partitions > for nodes without CPUs. > > For buffers, it probably does not really matter if a node does not have > any CPUs. If a node does not have any CPUs, that does not mean we should > not put any buffers on it. After all, CXL will never have any CPUs (at > least I think that's the case), and not using it for shared buffers > would be a bit strange. Although, it could still be used for page cache. For CXL memory to be really usable, I think we'd need nontrivial additional work. CXL memory has considerably higher latency and lower throughput. You'd *never* want things like BufferDescs or such on such nodes. And even the buffered data itself, you'd want to make sure that frequently used data, e.g. inner index pages, never end up on it. Which leads to: > Maybe it should be "tiered" a bit more? Yes, for proper CXL support, we'd need a component that explicitly demotes and promotes pages from "real" memory to CXL memory and the other way round. The demotion is relatively easy, you'd probably just do it whenever you'd otherwise throw out a victim buffer. When to promote back is harder... > The patch differentiates only between partitions on "my" NUMA node vs. every > other partition. Maybe it should have more layers? Given the relative unavailability of CXL memory systems, I think just not crashing is good enough for now... > >> I'm not sure what to do about this (or how getcpu() or libnuma handle this). > > > > I don't immediately see any libnuma functions that would care? > > > > Not sure what "care" means here. I don't think it's necessarily broken, > it's more about the APIs not making the situation very clear (or > convenient). What I mean is that I was looking through the libnuma functions and didn't see any that would be affected by having multiple "local" NUMA nodes. But: > How do you determine nodes for a CPU, for example? The closest thing I > see is numa_node_of_cpu(), but that only returns a single node. Or how > would you determine the number of nodes with CPUs (so that we create > PGPROC partitions only for those)? I suppose that requires literally > walking all the nodes. I didn't think of numa_node_of_cpu(). As long as numa_node_of_cpu() returns *something* I think it may be good enough. Nobody uses an RPi for high-throughput postgres workloads with a lot of memory. Slightly sub-optimal mappings should really not matter. I'm kinda wondering if we should deal with such fake numa systems by detecting them and disabling our numa support. Greetings, Andres Freund
-
Re: Adding basic NUMA awareness
Tomas Vondra <tomas@vondra.me> — 2025-08-12T15:06:50Z
On 8/12/25 16:24, Andres Freund wrote: > Hi, > > On 2025-08-12 13:04:07 +0200, Tomas Vondra wrote: >> Right. I don't think the current patch would crash - I can't test it, >> but I don't see why it would crash. In the worst case it'd end up with >> partitions that are not ideal. The question is more what would an ideal >> partitioning for buffers and PGPROC look like. Any opinions? >> >> For PGPROC, it's simple - it doesn't make sense to allocate partitions >> for nodes without CPUs. >> >> For buffers, it probably does not really matter if a node does not have >> any CPUs. If a node does not have any CPUs, that does not mean we should >> not put any buffers on it. After all, CXL will never have any CPUs (at >> least I think that's the case), and not using it for shared buffers >> would be a bit strange. Although, it could still be used for page cache. > > For CXL memory to be really usable, I think we'd need nontrivial additional > work. CXL memory has considerably higher latency and lower throughput. You'd > *never* want things like BufferDescs or such on such nodes. And even the > buffered data itself, you'd want to make sure that frequently used data, > e.g. inner index pages, never end up on it. > OK, let's keep that out of scope for these patches and assume we're dealing only with local memory. CXL could still be used by the OS for page cache, of whatever. What does that mean for the patch, though. Does it need a way to configure which nodes to use? I argued to leave this to the OS/numactl, and we'd just use whatever is made available to Postgres. But maybe we'll need something within Postgres after all? FWIW there's work needed a actually inherit NUMA info from the OS. Right now the patches just use all NUMA nodes, indexed by 0 ... (N-1) etc. I like the "registry" concept I used for buffer/PGPROC partitions, it made the patches much simpler. Maybe we should use something like that for NUMA info too. That is, at startup build a record of the NUMA layout, and use this as source of truth everywhere (instead of using libnuma from all those places). > Which leads to: > >> Maybe it should be "tiered" a bit more? > > Yes, for proper CXL support, we'd need a component that explicitly demotes and > promotes pages from "real" memory to CXL memory and the other way round. The > demotion is relatively easy, you'd probably just do it whenever you'd > otherwise throw out a victim buffer. When to promote back is harder... > Sounds very much like page cache (but that only works for buffered I/O). > >> The patch differentiates only between partitions on "my" NUMA node vs. every >> other partition. Maybe it should have more layers? > > Given the relative unavailability of CXL memory systems, I think just not > crashing is good enough for now... > The lowest of bars ;-) > >>>> I'm not sure what to do about this (or how getcpu() or libnuma handle this). >>> >>> I don't immediately see any libnuma functions that would care? >>> >> >> Not sure what "care" means here. I don't think it's necessarily broken, >> it's more about the APIs not making the situation very clear (or >> convenient). > > What I mean is that I was looking through the libnuma functions and didn't see > any that would be affected by having multiple "local" NUMA nodes. But: > My question is a bit of a "reverse" to this. That is, how do we even find (with libnuma) there are multiple local nodes? > >> How do you determine nodes for a CPU, for example? The closest thing I >> see is numa_node_of_cpu(), but that only returns a single node. Or how >> would you determine the number of nodes with CPUs (so that we create >> PGPROC partitions only for those)? I suppose that requires literally >> walking all the nodes. > > I didn't think of numa_node_of_cpu(). > Yeah. I think most of the libnuma API is designed for each CPU belonging to single NUMA node. I suppose we'd need to use numa_node_to_cpus() to build this kind of information ourselves. > As long as numa_node_of_cpu() returns *something* I think it may be good > enough. Nobody uses an RPi for high-throughput postgres workloads with a lot > of memory. Slightly sub-optimal mappings should really not matter. > I'm not really concerned about rpi, or the performance on it. I only use it as an example of system with "weird" NUMA layout. > I'm kinda wondering if we should deal with such fake numa systems by detecting > them and disabling our numa support. > That'd be an option too, if we can identify such systems. We could do that while building the "NUMA registry" I mentioned earlier. regards -- Tomas Vondra
-
Re: Adding basic NUMA awareness
Andres Freund <andres@anarazel.de> — 2025-08-13T15:16:24Z
Hi, On 2025-08-07 11:24:18 +0200, Tomas Vondra wrote: > The patch does a much simpler thing - treat the weight as a "budget", > i.e. number of buffers to allocate before proceeding to the "next" > partition. So it allocates 55 buffers from P1, then 45 buffers from P2, > and then goes back to P1 in a round-robin way. The advantage is it can > do away without a PRNG. I think that's a good plan. A few comments about the clock sweep patch: - It'd be easier to review if BgBufferSync() weren't basically re-indented wholesale. Maybe you could instead move the relevant code to a helper function that's called by BgBufferSync() for each clock? - I think choosing a clock sweep partition in every tick would likely show up in workloads that do a lot of buffer replacement, particularly if buffers in the workload often have a high usagecount (and thus more ticks are used). Given that your balancing approach "sticks" with a partition for a while, could we perhaps only choose the partition after exhausting that budget? - I don't really understand what > + /* > + * Buffers that should have been allocated in this partition (but might > + * have been redirected to keep allocations balanced). > + */ > + pg_atomic_uint32 numRequestedAllocs; > + is intended for. Adding yet another atomic increment for every clock sweep tick seems rather expensive... - I wonder if the balancing budgets being relatively low will be good enough. It's not too hard to imagine that this frequent "partition choosing" will be bad in buffer access heavy workloads. But it's probably the right approach until we've measured it being a problem. - It'd be interesting to do some very simple evaluation like a single pg_prewarm() of a relation that's close to the size of shared buffers and verify that we don't end up evicting newly read in buffers. I think your approach should work, but verifying that... I wonder if we could make some of this into tests somehow. It's pretty easy to break this kind of thing and not notice, as everything just continues to work, just a tad slower. Greetings, Andres Freund
-
Re: Adding basic NUMA awareness
Tomas Vondra <tomas@vondra.me> — 2025-08-13T16:36:17Z
On 8/13/25 17:16, Andres Freund wrote: > Hi, > > On 2025-08-07 11:24:18 +0200, Tomas Vondra wrote: >> The patch does a much simpler thing - treat the weight as a "budget", >> i.e. number of buffers to allocate before proceeding to the "next" >> partition. So it allocates 55 buffers from P1, then 45 buffers from P2, >> and then goes back to P1 in a round-robin way. The advantage is it can >> do away without a PRNG. > > I think that's a good plan. > > > A few comments about the clock sweep patch: > > - It'd be easier to review if BgBufferSync() weren't basically re-indented > wholesale. Maybe you could instead move the relevant code to a helper > function that's called by BgBufferSync() for each clock? > True, I'll rework it like that. > - I think choosing a clock sweep partition in every tick would likely show up > in workloads that do a lot of buffer replacement, particularly if buffers > in the workload often have a high usagecount (and thus more ticks are used). > Given that your balancing approach "sticks" with a partition for a while, > could we perhaps only choose the partition after exhausting that budget? > That should be possible, yes. By "exhausting budget" you mean going through all the partitions, right? > - I don't really understand what > >> + /* >> + * Buffers that should have been allocated in this partition (but might >> + * have been redirected to keep allocations balanced). >> + */ >> + pg_atomic_uint32 numRequestedAllocs; >> + > > is intended for. > > Adding yet another atomic increment for every clock sweep tick seems rather > expensive... > For the balancing (to calculate the budgets), we need to know the number of allocation requests for each partition, before some of the requests got redirected to other partitions. We can't use the number of "actual" allocations. But it seems useful to have both - one to calculate the budgets, the other to monitor how balanced the result is. I haven't seen the extra atomic in profiles, even on workloads that do a lot of buffer allocations (e.g. seqscan with datasets > shared buffers). But if that happens, I think there are ways to mitigate that. > > - I wonder if the balancing budgets being relatively low will be good > enough. It's not too hard to imagine that this frequent "partition choosing" > will be bad in buffer access heavy workloads. But it's probably the right > approach until we've measured it being a problem. > I don't follow. How would making the budgets higher change any of this? Anyway, I think choosing the partitions less frequently - e.g. only after consuming budget for the current partition, or going "full cycle", would make this a non-issue. > > - It'd be interesting to do some very simple evaluation like a single > pg_prewarm() of a relation that's close to the size of shared buffers and > verify that we don't end up evicting newly read in buffers. I think your > approach should work, but verifying that... > Will try. > I wonder if we could make some of this into tests somehow. It's pretty easy > to break this kind of thing and not notice, as everything just continues to > work, just a tad slower. > Do you mean a test that'd be a part of make check, or a standalone test? AFAICS any meaningful test would need to be fairly expensive, so probably not a good fit for make check. regards -- Tomas Vondra
-
Re: Adding basic NUMA awareness
Tomas Vondra <tomas@vondra.me> — 2025-09-11T08:32:40Z
Hi, Here's a fresh version of the NUMA patch series. There's a number of substantial improvements: 1) Rebase to current master, particularly on top of 2c7894052759 which removed the freelist. The patch that partitioned the freelist is gone. 2) A bunch of fixes, and it now passes CI workflows on github, and all other testing I did. Of course, more testing is needed. 3) It builds with/without libnuma support, and so on. 4) The separate GUCs were replaced by a single list GUC, similar to what we do for debug_io_direct. The GUC is called debug_numa, and accepts "buffets" and "procs", to partition the two areas. I'm considering adding a "clock-sweep" option in a future patch. I didn't do that here, because the buffer partitioning is already enabled by "buffers", and the clock-sweep just builds on that (partitioning the same way, pretty much). 5) It also works with EXEC_BACKEND, but this turned out to be a bit more challenging than expected. The trouble is some of the parameters (e.g. memory page size) are used both in the "size" and "init" phases, and we need to be extra careful to not make something "inconsistent". For example, we may get confused about the memory page size. The "size" happens before allocation, and at that point we don't know if we succeed in getting enough huge pages. When "init" happens, we already know that, so our "memory page size" could be different. We must be careful, e.g. to not need more memory than we requested. This is a general problem, but the EXEC_BACKEND makes it a bit trickier. In regular fork() case we can simply set some global variables in "size" and use them later in "init", but that doesn't work for EXEC_BACKEND. The current approach simply does the calculations twice, in a way that should end with the same results. But I'm not 100% happy with it, and I suspect it just confirms we should store the results in shmem memory (which is what I called "NUMA registry" before). That's still a TODO. 6) BufferDescPadded is 64B everywhere. Originally, the padding was applied only on 64-bit platforms, and on 32-bit systems the struct was left at 60B. But that is not compatible with buffer partitioning, which relies on the memory page being a multiple of BufferDescPadded. Perhaps it could be relaxed (so that a BufferDesc might span two memory pages), but this seems like the cleanes solution. I don't expect it to make any measurable difference. 7) I've moved some of the code to BgBufferSyncPartition, to make review easier (without all the indentation changes). 8) I've realized some of the TAP tests occasionally fail with ERROR: no unpinned buffers and I think I know why. Some of the tests set shared_buffers to a very low value - like 1MB or even 128kB, and StrategyGetBuffer() may search only a single partition (but not always). We may run out of unpinned buffers in that one partition. This apparently happens more easily on rpi5, due to the weird NUMA layout (there are 8 nodes with memory, but getcpu() reports node 0 for all cores). I suspect the correct fix is to ensure StrategyGetBuffer() scans all partitions, if there are no unpinned buffers in the current one. On realistic setups this shouldn't happen very often, I think. The other issue I just realized is that StrategyGetBuffer() recalculates the partition index over and over, which seems unnecessary (and possibly expensive, due to the modulo). And it also does too many loops, because it used NBuffers instead of the partition size. I'll fix those later. 9) I'm keeping the cloc-sweep balancing patches separate for now. In the end all the clock-sweep patches should be merged, but it keeps the changes easier to review this way, I think. 10) There's not many changes in the PGPROC partitioning patch. I merely fixed issues that broke it on EXEC_BACKEND, and did some smaller tweaks. 11) I'm not including the experimental patches to pin backends to CPUs (or nodes), and so on. It's clear those are unlikely to go in, so it'd be just a distraction. 12) What's the right / portable way to determine the current CPU for a process? The clock-sweep / PGPROC patches need this to pick the right partition, but it's not clear to me which API is the most portable. In the end I used sched_getcpu(), and then numa_node_of_cpu(), but maybe there's a better way. regards -- Tomas Vondra -
Re: Adding basic NUMA awareness
Tomas Vondra <tomas@vondra.me> — 2025-09-11T15:41:23Z
On 9/11/25 10:32, Tomas Vondra wrote: > ... > > For example, we may get confused about the memory page size. The "size" > happens before allocation, and at that point we don't know if we succeed > in getting enough huge pages. When "init" happens, we already know that, > so our "memory page size" could be different. We must be careful, e.g. > to not need more memory than we requested. I forgot to mention the other issue with huge pages on NUMA. I already reported [1] it's trivial to crash with a SIGBUS, because (1) huge pages get reserved on all NUMA nodes (evenly) (2) the decision whether to use huge pages is done by mmap(), which only needs to check if there are enough huge pages in total (3) numa_tonode_memory is called later, and does not verify if the target node has enough free pages (I'm not sure it should / can) (4) we only partition (and locate to NUMA nodes) some of the memory, and the rest (which is much smaller, but still sizeable) is likely causing "imbalance" - it gets placed on one (random) node, and it then does not have enough space for the stuff we explicitly placed there (5) then at some point we try accessing one of the shared buffers, that triggers page fault, tries to get a huge page on the NUMA node, realizes there are no free huge pages, and crashes with SIGBUS It clearly is not an option to just let it crash, but I still don't have a great idea how to address it. The only idea I have is to manually interleave the whole shared memory (when using huge pages), page by page, so that this imbalance does not happen. But it's harder than it looks, because we don't necessarily partition everything evenly. For example, one node can get a smaller chunk of shared buffers, because we try to partition buffers and buffers descriptors in a "nice" way. The PGPROC stuff is also not distributed quite evenly (e.g. aux/2pc entries are not mapped to any node). A different approach would be to calculate how many per-node huge pages we'll need (for the stuff we partition explicitly - buffers and PGPROC), and then the rest of the memory that can get placed on any node. And require the "maximum" number of pages that can get placed on any node. But that's annoying wasteful, because every other node will end up with unusable memory. regards [1] https://www.postgresql.org/message-id/71a46484-053c-4b81-ba32-ddac050a8b5d%40vondra.me -- Tomas Vondra
-
Re: Adding basic NUMA awareness
Tomas Vondra <tomas@vondra.me> — 2025-09-18T21:04:45Z
On 9/11/25 10:32, Tomas Vondra wrote: > ... > > 8) I've realized some of the TAP tests occasionally fail with > > ERROR: no unpinned buffers > > and I think I know why. Some of the tests set shared_buffers to a very > low value - like 1MB or even 128kB, and StrategyGetBuffer() may search > only a single partition (but not always). We may run out of unpinned > buffers in that one partition. > > This apparently happens more easily on rpi5, due to the weird NUMA > layout (there are 8 nodes with memory, but getcpu() reports node 0 for > all cores). > > I suspect the correct fix is to ensure StrategyGetBuffer() scans all > partitions, if there are no unpinned buffers in the current one. On > realistic setups this shouldn't happen very often, I think. > > The other issue I just realized is that StrategyGetBuffer() recalculates > the partition index over and over, which seems unnecessary (and possibly > expensive, due to the modulo). And it also does too many loops, because > it used NBuffers instead of the partition size. I'll fix those later. Here's a version fixing this issue (in the 0006 part). It modifies StrategyGetBuffer() to walk through all the partitions, in a round-robin manner. The way it steps to the next partition is a bit ugly, but it works and I'll think about some better way. I haven't done anything about the other issue (the one with huge pages reserved on NUMA nodes, and SIGBUS). regards -- Tomas Vondra
-
Re: Adding basic NUMA awareness
Alexey Makhmutov <a.makhmutov@postgrespro.ru> — 2025-10-12T23:58:38Z
Hi Tomas, Thank you very much for working on this problem and the entire line of patches prepared! I've tried to play with these patches a little and here are some my observations and suggestions. In the current implementation we try to use all available NUMA nodes on the machine, however it's often useful to limit the database only to a set of specific nodes, so that other nodes can be used for other processes. In my testing I was trying to use one node out of four for the client program, so I'd liked to limit the database to the remaining nodes. I use a systemd service with AllowedMemoryNodes/AllowedCPUs to start the cluster, so the obvious choice for me was to use the 'numa_get_membind' function instead of 'numa_num_configured_nodes' to get the list of usable nodes. However, it is much easier to work with logical nodes in the [0; n] range inside the PG code, so I've decided to add mapping between 'logical nodes' (0-n in PG) to a set of physical nodes actually returned by 'numa_get_membind'. We may need to map number in both directions, so two translation tables are allocated and filled at the first usage of 'pg_numa' functions. It also seems to be a good idea to isolate all 'libnuma' calls inside 'pg_numa.c', so to keep all 'numa_...' calls in it and this also allows us to hide this mapping in static functions. Here is the patch, which I've used to test this idea: https://github.com/Lerm/postgres/commit/9ec625c2bf564f5432375ec1d7ad02e4b2559161. This idea probably could be extended by adding some view to expose this mapping to the user (at least for testing purposes) and allow to explicitly override this mapping with a GUC setting. With such GUC setting we would be able to control PG memory usage on NUMA nodes without the need for systemd resource control or numactl parameters. Next, I've noticed some problems related to the size alignment for 'numa_tonode_memory' call in 'pg_numa_move_to_node' function. The documentation for the 'numa_tonode_memory' says that 'The size argument will be rounded up to a multiple of the system page size'. However this does not work well with huge pages as alignment is performed for the default kernel page size (i.e. 4K in most cases). If addr + size value (rounded to the default page size) does not cover the entire huge page, then such invocation seems to be processed incorrectly and allocation policy is not applied for next pages access in such segment. At least this was the behavior I've observed on Debian 12 / 6.1.40 kernel (i.e. '/proc/<pid>/numa_maps' shows that the segment contains pages from wrong nodes). There are two location at which we could face such situation in current patches. First is related to buffers partitions mapping. With current code we basically ensure that combined size of all partitions for a single node is aligned to (huge) page size (as size is bound to the number of descriptors on one page), but individual partition is not explicitly aligned to this size. So, we could get the situation in which single page is split between adjacent partitions (e.g. 32GB buffers split by 3 nodes). With current code we will try to map each partition independently, which will results in unaligned calls to 'numa_tonode_memory', so resulting mapping will be incorrect. We could either try to choose size for individual partition to align it to the desired page size or map all the partitions for a single node using a single 'pg_numa_move_to_node' invocation. During testing I've used the second approach, so here is the change to implement such logic: https://github.com/Lerm/postgres/commit/ee8b3603afd6d89e67b755dadc8e4c25ffba88be. The second location which could expose the same problem is related to the mapping of PGPROC arrays in 'pgproc_partition_init': here we need to align pointer to the end PGPROC partition. There seems to be also two additional problems with PGPROC partitioning: we need to account additional padding pages in 'PGProcShmemSize' (using the same logic as with fplocks) and we should not call 'MemSet(ptr, 0, ...)' prior to partitions mapping call (otherwise it will be mapped to current node). Here is a potential change, which tries to address these problems: https://github.com/Lerm/postgres/commit/eaf12776f59ff150735d0f187595fc8ce3f0a872. There are also some potential problems with buffers distribution between nodes. I have a feeling that current logic in 'buffer_partitions_prepare' does not work correctly if number of buffers is enough to cover just a single partition per node, but total number of nodes is below MIN_BUFFER_PARTITIONS (i.e. 2 or 3). In this case we will set 'numa_can_partition' to 'true', but will allocate 2 partitions per node (so, 4 or 6 partitions in total), while we can fill just 2 or 3 partition and leaving remaining partitions empty. This should violate the last assert check, as last partition will get zero buffers in this case. Another issue is related to the usage of 1GB pages, as minimal size for buffers partitions is limited by the minimal number of buffer descriptors in a single page. For 2MB pages this gives 2097152 / 64 * 8K = 256M as minimal size for partition, but for 1GB page the minimal size is equal to 1GB / 64 * 8K = 128GB. So, if we assume 4 as minimal number of partitions, then for 2MB pages we need just 1GB for shared_buffers to enable partitioning (which seems a perfectly fine minimal limit for most cases), but for 1GB pages we need to allocate at least 512GB to allow buffers partitioning. Certainly, 1GB pages are usually used on large machines with large number of buffers allocated, but still it may be useful to allow configurations with 32GB or 64GB buffer cache to use both 1GB pages and buffers partitioning at the same time. However, I don't see an easy way to achieve this with the current logic. We either need to allow usage of different page sizes here (i.e. 2MB for descriptors and 1GB for buffers) or combine both buffers and its descriptors in a single object (i.e. 'buffer chunk', which cover enough buffers and their descriptors to fit into one or several memory pages), effectively replacing both buffers and descriptors arrays with an array of such 'chunks'. The latter solution may also help with dynamic buffer cache resizing (as we may just add additional 'chunks' in this case) and also increase TLB-hits with 1GB page (as both descriptor and its buffer will be likely located in the same page). However, both these changes seems to be quite large. I've tried also to run some benchmarks on my server: I've got some improvements in 'pgbench/tpcb-like'results - about 8%, but only with backends pinning to NUMA node (i.e. adjusting your previous pinning patch to 'debug_numa' GUC: https://github.com/Lerm/postgres/commit/5942a3e12c7c501aa9febb63972a039e7ce00c20). For 'select-only' scenario the gain is more substantial (about 15%), but these tests are tricky, as they are more sensitive to other server settings and specific functions layout in compiled code, so they need more checks. Thank you again for sharing these patches! Thanks, Alexey
-
Re: Adding basic NUMA awareness
Tomas Vondra <tomas@vondra.me> — 2025-10-13T11:09:20Z
On 10/13/25 01:58, Alexey Makhmutov wrote: > Hi Tomas, > > Thank you very much for working on this problem and the entire line of > patches prepared! I've tried to play with these patches a little and > here are some my observations and suggestions. > > In the current implementation we try to use all available NUMA nodes on > the machine, however it's often useful to limit the database only to a > set of specific nodes, so that other nodes can be used for other > processes. In my testing I was trying to use one node out of four for > the client program, so I'd liked to limit the database to the remaining > nodes. I use a systemd service with AllowedMemoryNodes/AllowedCPUs to > start the cluster, so the obvious choice for me was to use the > 'numa_get_membind' function instead of 'numa_num_configured_nodes' to > get the list of usable nodes. However, it is much easier to work with > logical nodes in the [0; n] range inside the PG code, so I've decided to > add mapping between 'logical nodes' (0-n in PG) to a set of physical > nodes actually returned by 'numa_get_membind'. We may need to map number > in both directions, so two translation tables are allocated and filled > at the first usage of 'pg_numa' functions. It also seems to be a good > idea to isolate all 'libnuma' calls inside 'pg_numa.c', so to keep all > 'numa_...' calls in it and this also allows us to hide this mapping in > static functions. Here is the patch, which I've used to test this idea: > https://github.com/Lerm/postgres/ > commit/9ec625c2bf564f5432375ec1d7ad02e4b2559161. This idea probably > could be extended by adding some view to expose this mapping to the user > (at least for testing purposes) and allow to explicitly override this > mapping with a GUC setting. With such GUC setting we would be able to > control PG memory usage on NUMA nodes without the need for systemd > resource control or numactl parameters. > I've argued to keep this out of scope for v1, to keep it smaller and simpler. I'm not against adding that feature, though. If someone writes a patch to support this. I suppose the commit you linked is a step in that direction. I agree we should isolate libnuma calls to pg_numa.{c,h}. I wasn't quite consistent when doing that. > Next, I've noticed some problems related to the size alignment for > 'numa_tonode_memory' call in 'pg_numa_move_to_node' function. The > documentation for the 'numa_tonode_memory' says that 'The size > argument will be rounded up to a multiple of the system page size'. > However this does not work well with huge pages as alignment is > performed for the default kernel page size (i.e. 4K in most cases). If > addr + size value (rounded to the default page size) does not cover the > entire huge page, then such invocation seems to be processed incorrectly > and allocation policy is not applied for next pages access in such > segment. At least this was the behavior I've observed on Debian 12 / > 6.1.40 kernel (i.e. '/proc/<pid>/numa_maps' shows that the segment > contains pages from wrong nodes). > I'm not sure I understand. Are you suggesting there's a bug in the patch, the kernel, or somewhere else? There's definitely a possibility of confusion with huge pages, no doubt about that. The default "system page size" is 4KB, but we need to process whole huge pages. But this is exactly why (with hugepages) the code aligns everything to huge page boundary, and sizes everything as a multiple of huge page. At least I think so. Maybe I remember wrong? > There are two location at which we could face such situation in current > patches. First is related to buffers partitions mapping. With current > code we basically ensure that combined size of all partitions for a > single node is aligned to (huge) page size (as size is bound to the > number of descriptors on one page), but individual partition is not > explicitly aligned to this size. So, we could get the situation in which > single page is split between adjacent partitions (e.g. 32GB buffers > split by 3 nodes). With current code we will try to map each partition > independently, which will results in unaligned calls to > 'numa_tonode_memory', so resulting mapping will be incorrect. We could > either try to choose size for individual partition to align it to the > desired page size or map all the partitions for a single node using a > single 'pg_numa_move_to_node' invocation. During testing I've used the > second approach, so here is the change to implement such logic: https:// > github.com/Lerm/postgres/commit/ee8b3603afd6d89e67b755dadc8e4c25ffba88be. > Can you actually demonstrate this? The code does these two things: * calculate min_node_buffers so that buffers/descriptors are a multiple of page size (either 4K or huge page) * align buffers and descriptors to memory page TYPEALIGN(buffer_align, ...) I believe this is sufficient to ensure nothing gets split / mapped incorrectly. Maybe this fails sometimes? > The second location which could expose the same problem is related to > the mapping of PGPROC arrays in 'pgproc_partition_init': here we need to > align pointer to the end PGPROC partition. There seems to be also two > additional problems with PGPROC partitioning: we need to account > additional padding pages in 'PGProcShmemSize' (using the same logic as > with fplocks) and we should not call 'MemSet(ptr, 0, ...)' prior to > partitions mapping call (otherwise it will be mapped to current node). > Here is a potential change, which tries to address these problems: > https://github.com/Lerm/postgres/commit/ > eaf12776f59ff150735d0f187595fc8ce3f0a872. > So you're saying pgproc_partition_init() should not do just this ptr = (char *) ptr + num_procs * sizeof(PGPROC); but align the pointer to numa_page_size too? Sounds reasonable. Yeah, PGProcShmemSize() should have added huge pages for each partition, just like FastPathLockShmemSize(). Seems like a bug. I don't think the memset() is a problem. Yes, it might map it to the current node, but so what - the numa_tonode_memory() will just move it to the correct one. > There are also some potential problems with buffers distribution between > nodes. I have a feeling that current logic in > 'buffer_partitions_prepare' does not work correctly if number of buffers > is enough to cover just a single partition per node, but total number of > nodes is below MIN_BUFFER_PARTITIONS (i.e. 2 or 3). In this case we will > set 'numa_can_partition' to 'true', but will allocate 2 partitions per > node (so, 4 or 6 partitions in total), while we can fill just 2 or 3 > partition and leaving remaining partitions empty. This should violate > the last assert check, as last partition will get zero buffers in this > case. Another issue is related to the usage of 1GB pages, as minimal > size for buffers partitions is limited by the minimal number of buffer > descriptors in a single page. For 2MB pages this gives 2097152 / 64 * 8K > = 256M as minimal size for partition, but for 1GB page the minimal size > is equal to 1GB / 64 * 8K = 128GB. So, if we assume 4 as minimal number > of partitions, then for 2MB pages we need just 1GB for shared_buffers to > enable partitioning (which seems a perfectly fine minimal limit for most > cases), but for 1GB pages we need to allocate at least 512GB to allow > buffers partitioning. Certainly, 1GB pages are usually used on large > machines with large number of buffers allocated, but still it may be > useful to allow configurations with 32GB or 64GB buffer cache to use > both 1GB pages and buffers partitioning at the same time. However, I > don't see an easy way to achieve this with the current logic. We either > need to allow usage of different page sizes here (i.e. 2MB for > descriptors and 1GB for buffers) or combine both buffers and its > descriptors in a single object (i.e. 'buffer chunk', which cover enough > buffers and their descriptors to fit into one or several memory pages), > effectively replacing both buffers and descriptors arrays with an array > of such 'chunks'. The latter solution may also help with dynamic buffer > cache resizing (as we may just add additional 'chunks' in this case) and > also increase TLB-hits with 1GB page (as both descriptor and its buffer > will be likely located in the same page). However, both these changes > seems to be quite large. > I'll look at handling the case with shared_buffers being too small to allow partitioning. There well might be a bug. We should simply disable partitioning in such cases. As for 1GB huge pages, I don't see a good way to support configurations with small buffers in these cases. To me it seems acceptable to say that if you want 1GB huge pages, you should have a lot of memory and shared buffers large enough. I'm not against supporting such systems, if we can come up with a good partitioning scheme. When I tried to come up with a scheme like that, it always came with a substantial complexity & cost. The main challenge was that it forced splitting the array of buffer descriptors, similarly to what the PGPROC partitioning does. And that made buffer access so much more complex / expensive it seemed not worth it. I was worried about impact on systems without NUMA partitioning. > I've tried also to run some benchmarks on my server: I've got some > improvements in 'pgbench/tpcb-like'results - about 8%, but only with > backends pinning to NUMA node (i.e. adjusting your previous pinning > patch to 'debug_numa' GUC: https://github.com/Lerm/postgres/ > commit/5942a3e12c7c501aa9febb63972a039e7ce00c20). For 'select-only' > scenario the gain is more substantial (about 15%), but these tests are > tricky, as they are more sensitive to other server settings and specific > functions layout in compiled code, so they need more checks. > What kind of hardware was that? What/how many cpus, NUMA nodes, how much memory, what storage? FWIW the main purpose of these patches was not so much throughput improvement, but making the behavior more stable / consistent. regards -- Tomas Vondra -
Re: Adding basic NUMA awareness
Alexey Makhmutov <a.makhmutov@postgrespro.ru> — 2025-10-13T18:34:39Z
On 10/13/25 14:09, Tomas Vondra wrote: > I'm not sure I understand. Are you suggesting there's a bug in the patch, the kernel, or somewhere else? We need to ensure that both addr and (addr + size) are aligned to the page size of the target mapping during 'numa_tonode_memory' invocation, otherwise it may produce unexpected results. > But this is exactly why (with hugepages) the code aligns everything to huge page boundary, and sizes everything as a multiple of huge page. At least I think so. Maybe I remember wrong? I assume that there are places in the current patch, which could perform such unaligned mapping. See below for samples. > Can you actually demonstrate this? This issue is related to the calculation of partition size for buffer descriptors in case we have multiple partitions per node. Currently we ensure that each node gets number of buffers, which fits into whole memory pages, but if we have several partitions per node, then there is no guarantee that partition size will be properly aligned for descriptors. We could observe this problem only if we have multiple partitions per node and with MIN_BUFFER_PARTITIONS equal to 4, this issue can potentially affect only configurations with 2 or 3 nodes. Two examples here: first, let's assume we want to have shared_buffers set to 32GB with 3 NUMA nodes and 2MB pages. The NBuffers will be 4,194,304, min_node_buffers will be 32,768 and num_partitions_per_node will be 2 (so, 6 partitions in total). NBuffers/min_node_buffers = 128, so the nearest multiplier for min_node_buffers which allow us to cover all buffers with 3 nodes is 43 (42*3 = 126, 43*3 = 129). The num_buffers_per_node is 43*min_node_buffers and it is aligned to page size, but we need to split it between two partitions, so each gets 41.5*min_node_buffers buffers. This still allow us to split buffers itself by page boundary, but descriptor partitions will be split just in the middle of the page. Here is the log for such configuration: NUMA: buffers 4194304 partitions 6 num_nodes 3 per_node 2 buffers_per_node 1409024 (min 32768) NUMA: buffer 0 node 0 partition 0 buffers 704512 first 0 last 704511 NUMA: buffer 1 node 0 partition 1 buffers 704512 first 704512 last 1409023 NUMA: buffer 2 node 1 partition 0 buffers 704512 first 1409024 last 2113535 NUMA: buffer 3 node 1 partition 1 buffers 704512 first 2113536 last 2818047 NUMA: buffer 4 node 2 partition 0 buffers 688128 first 2818048 last 3506175 NUMA: buffer 5 node 2 partition 1 buffers 688128 first 3506176 last 4194303 NUMA: buffer_partitions_init: 0 => 0 buffers 704512 start 0x7ff7c8c00000 end 0x7ff920c00000 (size 5771362304) NUMA: buffer_partitions_init: 0 => 0 descriptors 704512 start 0x7ff7b8a00000 end 0x7ff7bb500000 (size 45088768) mbind: Invalid argument NUMA: buffer_partitions_init: 1 => 0 buffers 704512 start 0x7ff920c00000 end 0x7ffa78c00000 (size 5771362304) NUMA: buffer_partitions_init: 1 => 0 descriptors 704512 start 0x7ff7bb500000 end 0x7ff7be000000 (size 45088768) mbind: Invalid argument NUMA: buffer_partitions_init: 2 => 1 buffers 704512 start 0x7ffa78c00000 end 0x7ffbd0c00000 (size 5771362304) NUMA: buffer_partitions_init: 2 => 1 descriptors 704512 start 0x7ff7be000000 end 0x7ff7c0b00000 (size 45088768) mbind: Invalid argument NUMA: buffer_partitions_init: 3 => 1 buffers 704512 start 0x7ffbd0c00000 end 0x7ffd28c00000 (size 5771362304) NUMA: buffer_partitions_init: 3 => 1 descriptors 704512 start 0x7ff7c0b00000 end 0x7ff7c3600000 (size 45088768) mbind: Invalid argument NUMA: buffer_partitions_init: 4 => 2 buffers 688128 start 0x7ffd28c00000 end 0x7ffe78c00000 (size 5637144576) NUMA: buffer_partitions_init: 4 => 2 descriptors 688128 start 0x7ff7c3600000 end 0x7ff7c6000000 (size 44040192) NUMA: buffer_partitions_init: 5 => 2 buffers 688128 start 0x7ffe78c00000 end 0x7fffc8c00000 (size 5637144576) NUMA: buffer_partitions_init: 5 => 2 descriptors 688128 start 0x7ff7c6000000 end 0x7ff7c8a00000 (size 44040192) Another example: 2 nodes and 15872MB shared_buffers. Again, NBuffers/min_node_buffers=62, so num_buffers_per_node is 31*min_node_buffers, which gives each partition 15.5*min_node_buffers. Here is the log output: NUMA: buffers 2031616 partitions 4 num_nodes 2 per_node 2 buffers_per_node 1015808 (min 32768) NUMA: buffer 0 node 0 partition 0 buffers 507904 first 0 last 507903 NUMA: buffer 1 node 0 partition 1 buffers 507904 first 507904 last 1015807 NUMA: buffer 2 node 1 partition 0 buffers 507904 first 1015808 last 1523711 NUMA: buffer 3 node 1 partition 1 buffers 507904 first 1523712 last 2031615 NUMA: buffer_partitions_init: 0 => 0 buffers 507904 start 0x7ffbf9c00000 end 0x7ffcf1c00000 (size 4160749568) NUMA: buffer_partitions_init: 0 => 0 descriptors 507904 start 0x7ffbf1e00000 end 0x7ffbf3d00000 (size 32505856) mbind: Invalid argument NUMA: buffer_partitions_init: 1 => 0 buffers 507904 start 0x7ffcf1c00000 end 0x7ffde9c00000 (size 4160749568) NUMA: buffer_partitions_init: 1 => 0 descriptors 507904 start 0x7ffbf3d00000 end 0x7ffbf5c00000 (size 32505856) mbind: Invalid argument NUMA: buffer_partitions_init: 2 => 1 buffers 507904 start 0x7ffde9c00000 end 0x7ffee1c00000 (size 4160749568) NUMA: buffer_partitions_init: 2 => 1 descriptors 507904 start 0x7ffbf5c00000 end 0x7ffbf7b00000 (size 32505856) mbind: Invalid argument NUMA: buffer_partitions_init: 3 => 1 buffers 507904 start 0x7ffee1c00000 end 0x7fffd9c00000 (size 4160749568) NUMA: buffer_partitions_init: 3 => 1 descriptors 507904 start 0x7ffbf7b00000 end 0x7ffbf9a00000 (size 32505856) mbind: Invalid argument > So you're saying pgproc_partition_init() should not do just this > ptr = (char *) ptr + num_procs * sizeof(PGPROC); > but align the pointer to numa_page_size too? Sounds reasonable. Yes, that's exactly my point, otherwise we could violate the alignment rule for 'numa_tonode_memory'. Here is an extraction from the log for system with 2 nodes, 2000 max_connections and 2MB pages: NUMA: pgproc backends 2056 num_nodes 2 per_node 1028 NUMA: pgproc_init_partition procs 0x7fffe7800000 endptr 0x7fffe78d2d20 num_procs 1028 node 0 mbind: Invalid argument NUMA: pgproc_init_partition procs 0x7fffe7a00000 endptr 0x7fffe7ad2d20 num_procs 1028 node 1 mbind: Invalid argument NUMA: pgproc_init_partition procs 0x7fffe7c00000 endptr 0x7fffe7c07cb0 num_procs 38 node -1 mbind: Invalid argument mbind: Invalid argument > I don't think the memset() is a problem. Yes, it might map it to the current node, but so what - the numa_tonode_memory() will just move it to the correct one. Well, the 'numa_tonode_memory' call does not move pages to the target node. It just sets the policy for mapping, so system will actually try to provide page from the correct node once we touch it. However, if the page is already faulted, then it won't be affected by this mapping, so that's why it works faster compared to 'numa_move_pages'. As stated in libnuma documentation: * numa_tonode_memory() put memory on a specific node. The constraints described for numa_interleave_memory() apply here too. * numa_interleave_memory() interleaves size bytes of memory page by page from start on nodes specified in nodemask. <...> This is a lower level function to interleave allocated but not yet faulted in memory. Not yet faulted in means the memory is allocated using mmap(2) or shmat(2), but has not been accessed by the current process yet. <...> If the numa_set_strict() flag is true then the operation will cause a numa_error if there were already pages in the mapping that do not follow the policy. I assume, that for the regular page kernel may rebalance memory in the future (not immediately), but not for hugepages. So, we really don't want to touch the memory area before we call the 'numa_tonode_memory'. This can be easily tested with the simple program: #include <stdio.h> #include <numa.h> #include <sys/mman.h> #include <linux/mman.h> #define MAP_SIZE 2*1024*1024 int main(int argc, char** argv) { void* ptr1 = mmap(NULL, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS | MAP_HUGETLB | MAP_HUGE_2MB, -1, 0); void* ptr2 = mmap(NULL, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS | MAP_HUGETLB | MAP_HUGE_2MB, -1, 0); /* Fault first page */ memset(ptr1, 1, MAP_SIZE); /* Move to node 1 */ numa_tonode_memory(ptr1, MAP_SIZE, 1); numa_tonode_memory(ptr2, MAP_SIZE, 1); /* Fault second page */ memset(ptr2, 1, MAP_SIZE); /* Wait */ printf("ptr1=%lx\nptr2=\%lx\nPress Enter to continue...\n",ptr1,ptr2); getchar(); munmap(ptr2, MAP_SIZE); munmap(ptr1, MAP_SIZE); return 0; } Running it on the first node: # gcc -o test_mem test_mem.c -lnuma # taskset -c 0 ./test_mem ptr1=7ffff7a00000 ptr2=7ffff7800000 Press Enter to continue... From another terminal: # grep huge /proc/`pgrep test_mem`/numa_maps 7ffff7800000 bind:1 file=/anon_hugepage\040(deleted) huge dirty=1 N1=1 kernelpagesize_kB=2048 7ffff7a00000 bind:1 file=/anon_hugepage\040(deleted) huge dirty=1 N0=1 kernelpagesize_kB=2048 So, while policy (bind:1) is set for both mappings, but only the second one (which was not touched before the 'numa_tonode_memory' invocation) is actualy located on node 1 rather than 0. > What kind of hardware was that? What/how many cpus, NUMA nodes, how much memory, what storage? Of course, that's valid question. I probably should not have commented on performance side without providing full data, while I was still trying to measure it and it was just preliminary runs. Sorry for that. Thanks, Alexey -
Re: Adding basic NUMA awareness
Tomas Vondra <tomas@vondra.me> — 2025-10-15T17:02:38Z
Hi, Here's an updated patch series, addressing (some) of the issues. I've kept the changes in separate patches, to make the changes easier to review and discuss. On 10/13/25 20:34, Alexey Makhmutov wrote: > On 10/13/25 14:09, Tomas Vondra wrote: > >> I'm not sure I understand. Are you suggesting there's a bug in the > patch, the kernel, or somewhere else? > > We need to ensure that both addr and (addr + size) are aligned to the > page size of the target mapping during 'numa_tonode_memory' invocation, > otherwise it may produce unexpected results. > Hmm. The libnuma docs about numa_intereave_memory says: ... The size argument will be rounded up to a multiple of the system page size. ... Which I interpreted that it does all the necessary rounding. But if this ignores huge pages (i.e. "system page size" is 4K, not a HP size), then aligning the size explicitly is would be needed. This would be pretty annoying, though. It'd mean we can't rely on any rounding done by libnuma, at least for code that might use huge pages. Nevertheless, the updated patches should address both cases ... >> But this is exactly why (with hugepages) the code aligns everything to > huge page boundary, and sizes everything as a multiple of huge page. At > least I think so. Maybe I remember wrong? > > I assume that there are places in the current patch, which could perform > such unaligned mapping. See below for samples. > >> Can you actually demonstrate this? > > This issue is related to the calculation of partition size for buffer > descriptors in case we have multiple partitions per node. Currently we > ensure that each node gets number of buffers, which fits into whole > memory pages, but if we have several partitions per node, then there is > no guarantee that partition size will be properly aligned for > descriptors. We could observe this problem only if we have multiple > partitions per node and with MIN_BUFFER_PARTITIONS equal to 4, this > issue can potentially affect only configurations with 2 or 3 nodes. > > Two examples here: first, let's assume we want to have shared_buffers > set to 32GB with 3 NUMA nodes and 2MB pages. The NBuffers will be > 4,194,304, min_node_buffers will be 32,768 and num_partitions_per_node > will be 2 (so, 6 partitions in total). NBuffers/min_node_buffers = 128, > so the nearest multiplier for min_node_buffers which allow us to cover > all buffers with 3 nodes is 43 (42*3 = 126, 43*3 = 129). The > num_buffers_per_node is 43*min_node_buffers and it is aligned to page > size, but we need to split it between two partitions, so each gets > 41.5*min_node_buffers buffers. This still allow us to split buffers > itself by page boundary, but descriptor partitions will be split just in > the middle of the page. Here is the log for such configuration: > NUMA: buffers 4194304 partitions 6 num_nodes 3 per_node 2 > buffers_per_node 1409024 (min 32768) > ... I see. I was really puzzled how could a node get chunk of buffers that's not a multiple of page size, because min_node_buffers was meant to guarantee that. But now I realize it's not about per-node buffers, it's about individual partitions. Initially I thought the right way to fix this is to use min_node_buffers for each partitions, not for nodes. But that would increase the amount of memory needed for NUMA partitioning to work. I practice that wouldn't be an issue, because it'd still be only ~1GB (with 2MB huge pages), and the relevant systems will have way more. But then I realized it's we don't need to map the partitions one by one. We can simply map all partitions for the whole NUMA node at once, and then we don't have this problem at all. The attached 0007 patch does this to fix the issue. And I just noticed this is pretty much exactly how you fixed this in your commit ee8b360. The last partition may still not have the size aligned, though, because may not be a multiple of min_node_buffers. > > Another example: 2 nodes and 15872MB shared_buffers. Again, NBuffers/ > min_node_buffers=62, so num_buffers_per_node is 31*min_node_buffers, > which gives each partition 15.5*min_node_buffers. Here is the log output: > NUMA: buffers 2031616 partitions 4 num_nodes 2 per_node 2 > buffers_per_node 1015808 (min 32768) > ... > mbind: Invalid argument > NUMA: buffer_partitions_init: 3 => 1 buffers 507904 start 0x7ffee1c00000 > end 0x7fffd9c00000 (size 4160749568) > NUMA: buffer_partitions_init: 3 => 1 descriptors 507904 start > 0x7ffbf7b00000 end 0x7ffbf9a00000 (size 32505856) > mbind: Invalid argument > >> So you're saying pgproc_partition_init() should not do just this >> ptr = (char *) ptr + num_procs * sizeof(PGPROC); >> but align the pointer to numa_page_size too? Sounds reasonable. > > Yes, that's exactly my point, otherwise we could violate the alignment > rule for 'numa_tonode_memory'. Here is an extraction from the log for > system with 2 nodes, 2000 max_connections and 2MB pages: Should be fixed by 0010 by explicitly aligning the size like this. It's a bit more extensive than your eaf1277. BTW what's the mbind failures about? Is that something we check, at least in memory > >> I don't think the memset() is a problem. Yes, it might map it to the > current node, but so what - the numa_tonode_memory() will just move it > to the correct one. > > Well, the 'numa_tonode_memory' call does not move pages to the target > node. It just sets the policy for mapping, so system will actually try > to provide page from the correct node once we touch it. However, if the > page is already faulted, then it won't be affected by this mapping, so > that's why it works faster compared to 'numa_move_pages'. As stated in > libnuma documentation: > * numa_tonode_memory() put memory on a specific node. The constraints > described for numa_interleave_memory() apply here too. > * numa_interleave_memory() interleaves size bytes of memory page by > page from start on nodes specified in nodemask. <...> This is a lower > level function to interleave allocated but not yet faulted in memory. > Not yet faulted in means the memory is allocated using mmap(2) or > shmat(2), but has not been accessed by the current process yet. <...> > If the numa_set_strict() flag is true then the operation will cause a > numa_error if there were already pages in the mapping that do not follow > the policy. > Point taken. The 0009 fixes this by moving the MemSet() to after the partitioning. At that point the policy is already set. There's a couple more fixes. 0008 improves handling of cases that don't allow NUMA partitioning (like when shared_buffers are too small). 0011 adds the missing padding to PGProcShmemSize, which you also fixed in one of your commits. 0012 reduces logging in clock-sweep balancing, which on idle systems was annoyingly verbose. I keps 0006 separate for now. It got broken by 5e89985928, and the conflicts were fairly extensive. Better keep it separate a bit longer. regards -- Tomas Vondra -
Re: Adding basic NUMA awareness
Tomas Vondra <tomas@vondra.me> — 2025-10-15T17:15:47Z
On 10/13/25 13:09, Tomas Vondra wrote: > On 10/13/25 01:58, Alexey Makhmutov wrote: >> Hi Tomas, >> >> Thank you very much for working on this problem and the entire line of >> patches prepared! I've tried to play with these patches a little and >> here are some my observations and suggestions. >> >> In the current implementation we try to use all available NUMA nodes on >> the machine, however it's often useful to limit the database only to a >> set of specific nodes, so that other nodes can be used for other >> processes. In my testing I was trying to use one node out of four for >> the client program, so I'd liked to limit the database to the remaining >> nodes. I use a systemd service with AllowedMemoryNodes/AllowedCPUs to >> start the cluster, so the obvious choice for me was to use the >> 'numa_get_membind' function instead of 'numa_num_configured_nodes' to >> get the list of usable nodes. However, it is much easier to work with >> logical nodes in the [0; n] range inside the PG code, so I've decided to >> add mapping between 'logical nodes' (0-n in PG) to a set of physical >> nodes actually returned by 'numa_get_membind'. We may need to map number >> in both directions, so two translation tables are allocated and filled >> at the first usage of 'pg_numa' functions. It also seems to be a good >> idea to isolate all 'libnuma' calls inside 'pg_numa.c', so to keep all >> 'numa_...' calls in it and this also allows us to hide this mapping in >> static functions. Here is the patch, which I've used to test this idea: >> https://github.com/Lerm/postgres/ >> commit/9ec625c2bf564f5432375ec1d7ad02e4b2559161. This idea probably >> could be extended by adding some view to expose this mapping to the user >> (at least for testing purposes) and allow to explicitly override this >> mapping with a GUC setting. With such GUC setting we would be able to >> control PG memory usage on NUMA nodes without the need for systemd >> resource control or numactl parameters. >> > > I've argued to keep this out of scope for v1, to keep it smaller and > simpler. I'm not against adding that feature, though. If someone writes > a patch to support this. I suppose the commit you linked is a step in > that direction. > On second thought, I probably spoke too soon ... What I wanted to keep out of scope for v1 is ability to pick NUMA nodes from Postgres, e.g. setting a GUC to limit which NUMA nodes to use, etc. But that's not what you proposed here, clearly. You're saying we should find which NUMA nodes the process is allowed to run, and use those. Instead of just using all *configured* nodes. And I agree with that. I'll take a look at your commit 9ec625c. I'm not sure it's a good idea to have our internal "logical" node ID, and a mapping to external node ID values (exposed by the libnuma). I was thinking maybe we should use just the external IDs, but it's true that'd be tricky when iterating through nodes, etc. So maybe having such mapping is a good approach. Another thing I wasn't sure about is checking for memory-only nodes. For example rpi5 has a NUMA node for each 1GB of memory, and each CPU is mapped to all those nodes. For buffers this probably does not matter, but we probably should not use those NUMA nodes for PGPROC partitioning. regards -- Tomas Vondra
-
Re: Adding basic NUMA awareness
Tomas Vondra <tomas@vondra.me> — 2025-10-31T11:57:33Z
Hi, here's a significantly reworked version of this patch series. I had a couple discussions about these patches at pgconf.eu last week, and one interesting suggestion was that maybe it'd be easier to the clock-sweep partitioning first, in a NUMA-oblivious way. And then add the NUMA stuff later. The logic is that this way we could ignore some of the hard stuff (e.g. handling huge page reservation), while still reducing clocksweep contention. Which we speculated might be the main benefit anyway. The attached patches do this. 0001 - Introduces a simplified version of the "buffer partition registry" (think array in shmem, storing info about ranges of shared buffer). The partitions are calculated as simple fraction of shared buffers. There's no need to align the partitions to memory pages etc. 0002-0005 - Does the clock-sweep partitioning. I chose to keep this split into smaller increments, to keep the patches easier to review. 0006 - Make the partitioning NUMA-aware. This used to be part of 0001, but now it's moved on top of the clock-sweep stuff. It ensures the partitions are properly aligned to memory pages, and all that. 0007 - PGPROC partitioning. This made the 0001 patch much simpler/smaller - it used to be ~50kB, now it's 15kB (and most of the complexity is in 0006). The question however is how this performs, or how much of the benefit was due to NUMA-awareness and how much was due to just partitioning clock-sweep. I repeated the benchmark from [1], doing concurrent sequential scans to put significant pressure on buffer replacements, and I got this: hp clients | master | sweep sweep-16 | numa numa-16 =============|==========|===================|=============== off 16 | 24 | 46 46 | 33 40 32 | 33 | 53 51 | 45 51 48 | 38 | 51 61 | 46 56 64 | 41 | 56 75 | 47 65 80 | 47 | 53 77 | 48 71 96 | 45 | 54 80 | 47 66 112 | 45 | 52 83 | 44 65 128 | 43 | 55 81 | 39 48 -------------|----------|-------------------|--------------- on 16 | 26 | 47 47 | 35 42 32 | 33 | 49 52 | 40 49 48 | 39 | 52 63 | 43 57 64 | 42 | 53 72 | 43 66 80 | 43 | 54 81 | 46 71 96 | 48 | 58 80 | 49 73 112 | 51 | 58 78 | 51 76 128 | 55 | 60 83 | 52 76 "hp" means huge pages, the compared branches are: - master - current master - sweep - patches up to 0005, default number of partitions (4) - sweep-16 - patches up to 0005, 16 partitions - numa - patches up to 0006, default number of partitions (4) - numa-16 - patches up to 0006, 16 partitions Compared to master, the results look like this: hp clients | sweep sweep-16 | numa numa-16 ==============|====================|================ off 16 | 192% 192% | 138% 167% 32 | 161% 155% | 136% 155% 48 | 132% 160% | 121% 145% 64 | 137% 183% | 115% 159% 80 | 113% 164% | 102% 151% 96 | 120% 177% | 104% 146% 112 | 116% 184% | 98% 144% 128 | 128% 186% | 90% 110% --------------|--------------------|---------------- on 16 | 181% 181% | 135% 162% 32 | 148% 158% | 121% 148% 48 | 133% 161% | 110% 144% 64 | 126% 171% | 102% 157% 80 | 126% 188% | 107% 165% 96 | 121% 167% | 102% 152% 112 | 114% 153% | 100% 149% 128 | 109% 151% | 95% 138% The attached PDF has more results for runs with somewhat modified parameters, but the overall it's very similar to these numbers. I think this confirms most of the benefit really comes from just partitioning clock-sweep, and it's mostly independent of the NUMA stuff. In fact, the NUMA partitioning is often slower. Some of this may be due to inefficiencies in the patch (e.g. division in formula calculating the partition index, etc.). So I think this looks quite promising ... There are a couple unsolved issues, though. While running the tests, I ran into a bunch of weird issues. I saw two types of failures: 1) Bad address ----------------------------------------------------------------------- 2025-10-30 15:24:21.195 UTC [2038558] LOG: could not read blocks 114543..114558 in file "base/16384/16588": Bad address 2025-10-30 15:24:21.195 UTC [2038558] STATEMENT: SELECT * FROM t_41 OFFSET 1000000000 2025-10-30 15:24:21.195 UTC [2038523] LOG: could not read blocks 119981..119996 in file "base/16384/16869": Bad address 2025-10-30 15:24:21.195 UTC [2038523] CONTEXT: completing I/O on behalf of process 2038464 2025-10-30 15:24:21.195 UTC [2038523] STATEMENT: SELECT * FROM t_96 OFFSET 1000000000 2025-10-30 15:24:21.195 UTC [2038492] LOG: could not read blocks 118226..118232 in file "base/16384/16478": Bad address 2025-10-30 15:24:21.195 UTC [2038492] STATEMENT: SELECT * FROM t_19 OFFSET 1000000000 2025-10-30 15:24:21.196 UTC [2038477] LOG: could not read blocks 120515..120517 in file "base/16384/16945": Bad address 2025-10-30 15:24:21.196 UTC [2038477] CONTEXT: completing I/O on behalf of process 2038545 2025-10-30 15:24:21.196 UTC [2038477] STATEMENT: SELECT * FROM t_111 OFFSET 1000000000 ----------------------------------------------------------------------- 2) Operation canceled ----------------------------------------------------------------------- 2025-10-31 10:57:21.742 UTC [2685933] LOG: could not read blocks 159..174 in file "base/16384/16398": Operation canceled 2025-10-31 10:57:21.742 UTC [2685933] STATEMENT: SELECT * FROM t_3 OFFSET 1000000000 2025-10-31 10:57:21.742 UTC [2685933] LOG: could not read blocks 143..158 in file "base/16384/16398": Operation canceled 2025-10-31 10:57:21.742 UTC [2685933] STATEMENT: SELECT * FROM t_3 OFFSET 1000000000 2025-10-31 10:57:21.781 UTC [2685933] ERROR: could not read blocks 143..158 in file "base/16384/16398": Operation canceled 2025-10-31 10:57:21.781 UTC [2685933] STATEMENT: SELECT * FROM t_3 OFFSET 1000000000 ----------------------------------------------------------------------- I'm still not sure what's causing these, and it's happening rarely and randomly, so it's hard to catch and reproduce. I'd welcome suggestions what to look for / what might be the issue. I did run the whole test under valgrind to make sure there's nothing obviously broken, but that found no issues. Of course, it's much slower under valgrind, so maybe it just didn't hit the issue. I suspect the "bad address" might be just a different symptom of the issues with reserving huge pages I already mentioned [2]. I assume io_uring might try using huge pages internally, and then it fails because postgres also reserves huge pages. I have no idea what "operation canceled" might be about. I'm not entirely sure if this affect all patches, or just the patches with NUMA partitioning. Or if this happens with huge pages. I'll do more runs to test specifically this. But it does seem to be specific to io_uring - or at least the canceled issue. I haven't seen it after switching to "worker". [1] https://www.postgresql.org/message-id/51e51832-7f47-412a-a1a6-b972101cc8cb%40vondra.me [2] https://www.postgresql.org/message-id/1d57d68d-b178-415a-ba11-be0c3714638e%40vondra.me regards -- Tomas Vondra -
Re: Adding basic NUMA awareness
Jakub Wartak <jakub.wartak@enterprisedb.com> — 2025-11-04T12:10:58Z
On Fri, Oct 31, 2025 at 12:57 PM Tomas Vondra <tomas@vondra.me> wrote: > > Hi, > > here's a significantly reworked version of this patch series. > > I had a couple discussions about these patches at pgconf.eu last week,[..] I've just had a quick look at this and oh, my, I've started getting into this partitioned clocksweep and that's ambitious! Yes, this sequencing of patches makes it much more understandable. Anyway I've spotted some things, attempted to fix some and have some basic questions too (so small baby steps, all of this was on 4s/4 NUMA nodes with HP on) -- the 000X refers to question/issue/bug in specific patchset file: 0001: you mention 'debug_numa = buffers' in commitmsg, but there's nothing there like that? it comes with 0006 0002: dunno, but wouldn't it make some educational/debugging sense to add a debug function returning clocksweep partition index (calculate_partition_index) for backend? (so that we know which partition we are working on right now) 0003: those two "elog(INFO, "rebalance skipped:" should be at DEBUG2+ IMHO (they are way too verbose during runs) 0006a: Needs update - s/patches later in the patch series/patches earlier in the patch series/ 0006b: IMHO longer term, we should hide some complexity of those calls via src/port numa shims (pg_numa_sched_cpu()?) 0006c: after GUC commit fce7c73fba4e5, apply complains with: error: patch failed: src/backend/utils/misc/guc_parameters.dat:906 error: src/backend/utils/misc/guc_parameters.dat: patch does not apply 0007a: pg_buffercache_pgproc returns pgproc_ptr and fastpath_ptr in bigint and not hex? I've wanted to adjust that to TEXTOID, but instead I've thought it is going to be simpler to use to_hex() -- see 0009 attached. 0007b: pg_buffercache_pgproc -- nitpick, but maybe it would be better called pg_shm_pgproc? 0007c with check_numa='buffers,procs' throws 'mbind Invalid argument' during start: 2025-11-04 10:02:27.055 CET [58464] DEBUG: NUMA: pgproc_init_partition procs 0x7f8d30400000 endptr 0x7f8d30800000 num_procs 2523 node 0 2025-11-04 10:02:27.057 CET [58464] DEBUG: NUMA: pgproc_init_partition procs 0x7f8d30800000 endptr 0x7f8d30c00000 num_procs 2523 node 1 2025-11-04 10:02:27.059 CET [58464] DEBUG: NUMA: pgproc_init_partition procs 0x7f8d30c00000 endptr 0x7f8d31000000 num_procs 2523 node 2 2025-11-04 10:02:27.061 CET [58464] DEBUG: NUMA: pgproc_init_partition procs 0x7f8d31000000 endptr 0x7f8d31400000 num_procs 2523 node 3 2025-11-04 10:02:27.062 CET [58464] DEBUG: NUMA: pgproc_init_partition procs 0x7f8d31400000 endptr 0x7f8d31407cb0 num_procs 38 node -1 mbind: Invalid argument mbind: Invalid argument mbind: Invalid argument mbind: Invalid argument 0007d: so we probably need numa_warn()/numa_error() wrappers (this was initially part of NUMA observability patches but got removed during the course of action), I'm attaching 0008. With that you'll get something a little more up to our standards: 2025-11-04 10:27:07.140 CET [59696] DEBUG: fastpath_parititon_init node = 3, ptr = 0x7f4f4d400000, endptr = 0x7f4f4d4b1660 2025-11-04 10:27:07.140 CET [59696] WARNING: libnuma: ERROR: mbind 0007e: elog DEBUG says it's pg_proc_init_partition but it's pgproc_partition_init() actually ;) 0007f: The "mbind: Invalid argument"" issue itself with the below addition: +elog(DEBUG1, "NUMA: fastpath_partition_init ptr %p endptr %p num_procs %d node %d", ptr, endptr, num_procs, node); showed this: 2025-11-04 11:30:51.089 CET [61841] DEBUG: NUMA: fastpath_partition_init ptr 0x7f39eea00000 endptr 0x7f39eeab1660 num_procs 2523 node 0 2025-11-04 11:30:51.089 CET [61841] WARNING: libnuma: ERROR: mbind 2025-11-04 11:30:51.089 CET [61841] DEBUG: NUMA: fastpath_partition_init ptr 0x7f39eec00000 endptr 0x7f39eecb1660 num_procs 2523 node 1 2025-11-04 11:30:51.089 CET [61841] WARNING: libnuma: ERROR: mbind 2025-11-04 11:30:51.089 CET [61841] DEBUG: NUMA: fastpath_partition_init ptr 0x7f39eee00000 endptr 0x7f39eeeb1660 num_procs 2523 node 2 2025-11-04 11:30:51.089 CET [61841] WARNING: libnuma: ERROR: mbind [..] Meanwhile it's full hugepage size (e.g. 0x7f39eec00000−0x7f39eea00000 = 2MB) $ grep --color 7f39ee[ace] /proc/61841/smaps 7f39ee800000-7f39eea00000 rw-s 87de00000 00:11 122710 /anon_hugepage (deleted) 7f39eea00000-7f39eec00000 rw-s 87e000000 00:11 122710 /anon_hugepage (deleted) 7f39eec00000-7f39eee00000 rw-s 87e200000 00:11 122710 /anon_hugepage (deleted) 7f39eee00000-7f39ef000000 rw-s 87e400000 00:11 122710 /anon_hugepage (deleted) but mbind() was called for just 0x7f39eeab1660−0x7f39eea00000 = 0xB1660 = 726624 bytes, but if adjust blindly endptr in that fastpath_partition_init() to be "char *endptr = ptr + 2*1024*1024;" (HP) it doesn't complain anymore and I get success: 2025-11-04 12:08:30.147 CET [62352] DEBUG: NUMA: fastpath_partition_init ptr 0x7f7bf7000000 endptr 0x7f7bf7200000 num_procs 2523 node 0 2025-11-04 12:08:30.147 CET [62352] DEBUG: NUMA: fastpath_partition_init ptr 0x7f7bf7200000 endptr 0x7f7bf7400000 num_procs 2523 node 1 2025-11-04 12:08:30.147 CET [62352] DEBUG: NUMA: fastpath_partition_init ptr 0x7f7bf7400000 endptr 0x7f7bf7600000 num_procs 2523 node 2 2025-11-04 12:08:30.147 CET [62352] DEBUG: NUMA: fastpath_partition_init ptr 0x7f7bf7600000 endptr 0x7f7bf7800000 num_procs 2523 node 3 2025-11-04 12:08:30.147 CET [62352] DEBUG: NUMA: fastpath_partition_init ptr 0x7f7bf7800000 endptr 0x7f7bf7a00000 num_procs 38 node -1 2025-11-04 12:08:30.239 CET [62352] LOG: starting PostgreSQL 19devel on x86_64-linux, compiled by gcc-12.2.0, 64-bit 0006d: I've got one SIGBUS during a call to select pg_buffercache_numa_pages(); and it looks like that memory accessed is simply not mapped? (bug) Program received signal SIGBUS, Bus error. pg_buffercache_numa_pages (fcinfo=0x561a97e8e680) at ../contrib/pg_buffercache/pg_buffercache_pages.c:386 386 pg_numa_touch_mem_if_required(ptr); (gdb) print ptr $1 = 0x7f4ed0200000 <error: Cannot access memory at address 0x7f4ed0200000> (gdb) where #0 pg_buffercache_numa_pages (fcinfo=0x561a97e8e680) at ../contrib/pg_buffercache/pg_buffercache_pages.c:386 #1 0x0000561a672a0efe in ExecMakeFunctionResultSet (fcache=0x561a97e8e5d0, econtext=econtext@entry=0x561a97e8dab8, argContext=0x561a97ec62a0, isNull=0x561a97e8e578, isDone=isDone@entry=0x561a97e8e5c0) at ../src/backend/executor/execSRF.c:624 [..] Postmaster had still attached shm (visible via smaps), and if you compare closely 0x7f4ed0200000 against sorted smaps: 7f4921400000-7f4b21400000 rw-s 252600000 00:11 151111 /anon_hugepage (deleted) 7f4b21400000-7f4d21400000 rw-s 452600000 00:11 151111 /anon_hugepage (deleted) 7f4d21400000-7f4f21400000 rw-s 652600000 00:11 151111 /anon_hugepage (deleted) 7f4f21400000-7f4f4bc00000 rw-s 852600000 00:11 151111 /anon_hugepage (deleted) 7f4f4bc00000-7f4f4c000000 rw-s 87ce00000 00:11 151111 /anon_hugepage (deleted) it's NOT there at all (there's no mmap region starting with 0x"7f4e" ). It looks like because pg_buffercache_numa_pages() is not aware of this new mmaped() regions and instead does simple loop over all NBuffers with "for (char *ptr = startptr; ptr < endptr; ptr += os_page_size)"? 0006e: I'm seeking confirmation, but is this the issue we have discussed on PgconfEU related to lack of detection of Mems_allowed, right? e.g. $ numactl --membind="0,1" --cpunodebind="0,1" /usr/pgsql19/bin/pg_ctl -D /path start still shows 4 NUMA nodes used. Current patches use numa_num_configured_nodes(), but it says 'This count includes any nodes that are currently DISABLED'. So I was wondering if I could help by migrating towards numa_num_task_nodes() / numa_get_mems_allowed()? It's the same as You wrote earlier to Alexy? > But that's not what you proposed here, clearly. You're saying we should > find which NUMA nodes the process is allowed to run, and use those. > Instead of just using all *configured* nodes. And I agree with that. So are you already on it ? > There are a couple unsolved issues, though. While running the tests, I > ran into a bunch of weird issues. I saw two types of failures: > 1) Bad address > 2) Operation canceled I did run (with io_uring) a short test(< 10min with -c 128) and didn't get those. Could you please share specific tips/workload for reproducing this? That's all for today, I hope it helps a little. -J. -
Re: Adding basic NUMA awareness
Tomas Vondra <tomas@vondra.me> — 2025-11-04T21:21:24Z
On 11/4/25 13:10, Jakub Wartak wrote: > On Fri, Oct 31, 2025 at 12:57 PM Tomas Vondra <tomas@vondra.me> wrote: >> >> Hi, >> >> here's a significantly reworked version of this patch series. >> >> I had a couple discussions about these patches at pgconf.eu last week,[..] > > I've just had a quick look at this and oh, my, I've started getting > into this partitioned clocksweep and that's ambitious! Yes, this > sequencing of patches makes it much more understandable. Anyway I've > spotted some things, attempted to fix some and have some basic > questions too (so small baby steps, all of this was on 4s/4 NUMA nodes > with HP on) -- the 000X refers to question/issue/bug in specific > patchset file: > > 0001: you mention 'debug_numa = buffers' in commitmsg, but there's > nothing there like that? it comes with 0006 > Right, I forgot to remove that reference. > 0002: dunno, but wouldn't it make some educational/debugging sense to > add a debug function returning clocksweep partition index > (calculate_partition_index) for backend? (so that we know which > partition we are working on right now) > Perhaps. I didn't need that, but it might be interesting during development. I probably would not keep that in the final version. > 0003: those two "elog(INFO, "rebalance skipped:" should be at DEBUG2+ > IMHO (they are way too verbose during runs) > Agreed. > 0006a: Needs update - s/patches later in the patch series/patches > earlier in the patch series/ > Agreed. > 0006b: IMHO longer term, we should hide some complexity of those calls > via src/port numa shims (pg_numa_sched_cpu()?) > Yeah, there's definitely room for moving more of the code to src/port. > 0006c: after GUC commit fce7c73fba4e5, apply complains with: > error: patch failed: src/backend/utils/misc/guc_parameters.dat:906 > error: src/backend/utils/misc/guc_parameters.dat: patch does not apply > Will fix. > 0007a: pg_buffercache_pgproc returns pgproc_ptr and fastpath_ptr in > bigint and not hex? I've wanted to adjust that to TEXTOID, but instead > I've thought it is going to be simpler to use to_hex() -- see 0009 > attached. > I don't know. I added simply because it might be useful for development, but we probably don't want to expose these pointers at all. > 0007b: pg_buffercache_pgproc -- nitpick, but maybe it would be better > called pg_shm_pgproc? > Right. It does not belong to pg_buffercache at all, I just added it there because I've been messing with that code already. > 0007c with check_numa='buffers,procs' throws 'mbind Invalid argument' > during start: > > 2025-11-04 10:02:27.055 CET [58464] DEBUG: NUMA: > pgproc_init_partition procs 0x7f8d30400000 endptr 0x7f8d30800000 > num_procs 2523 node 0 > 2025-11-04 10:02:27.057 CET [58464] DEBUG: NUMA: > pgproc_init_partition procs 0x7f8d30800000 endptr 0x7f8d30c00000 > num_procs 2523 node 1 > 2025-11-04 10:02:27.059 CET [58464] DEBUG: NUMA: > pgproc_init_partition procs 0x7f8d30c00000 endptr 0x7f8d31000000 > num_procs 2523 node 2 > 2025-11-04 10:02:27.061 CET [58464] DEBUG: NUMA: > pgproc_init_partition procs 0x7f8d31000000 endptr 0x7f8d31400000 > num_procs 2523 node 3 > 2025-11-04 10:02:27.062 CET [58464] DEBUG: NUMA: > pgproc_init_partition procs 0x7f8d31400000 endptr 0x7f8d31407cb0 > num_procs 38 node -1 > mbind: Invalid argument > mbind: Invalid argument > mbind: Invalid argument > mbind: Invalid argument > I'll take a look, but I don't recall seeing such errors. > 0007d: so we probably need numa_warn()/numa_error() wrappers (this was > initially part of NUMA observability patches but got removed during > the course of action), I'm attaching 0008. With that you'll get > something a little more up to our standards: > 2025-11-04 10:27:07.140 CET [59696] DEBUG: > fastpath_parititon_init node = 3, ptr = 0x7f4f4d400000, endptr = > 0x7f4f4d4b1660 > 2025-11-04 10:27:07.140 CET [59696] WARNING: libnuma: ERROR: mbind > Not sure. > 0007e: elog DEBUG says it's pg_proc_init_partition but it's > pgproc_partition_init() actually ;) > > 0007f: The "mbind: Invalid argument"" issue itself with the below addition: > +elog(DEBUG1, "NUMA: fastpath_partition_init ptr %p endptr %p > num_procs %d node %d", ptr, endptr, num_procs, node); > showed this: > 2025-11-04 11:30:51.089 CET [61841] DEBUG: NUMA: > fastpath_partition_init ptr 0x7f39eea00000 endptr 0x7f39eeab1660 > num_procs 2523 node 0 > 2025-11-04 11:30:51.089 CET [61841] WARNING: libnuma: ERROR: mbind > 2025-11-04 11:30:51.089 CET [61841] DEBUG: NUMA: > fastpath_partition_init ptr 0x7f39eec00000 endptr 0x7f39eecb1660 > num_procs 2523 node 1 > 2025-11-04 11:30:51.089 CET [61841] WARNING: libnuma: ERROR: mbind > 2025-11-04 11:30:51.089 CET [61841] DEBUG: NUMA: > fastpath_partition_init ptr 0x7f39eee00000 endptr 0x7f39eeeb1660 > num_procs 2523 node 2 > 2025-11-04 11:30:51.089 CET [61841] WARNING: libnuma: ERROR: mbind > [..] > > Meanwhile it's full hugepage size (e.g. 0x7f39eec00000−0x7f39eea00000 = 2MB) > $ grep --color 7f39ee[ace] /proc/61841/smaps > 7f39ee800000-7f39eea00000 rw-s 87de00000 00:11 122710 > /anon_hugepage (deleted) > 7f39eea00000-7f39eec00000 rw-s 87e000000 00:11 122710 > /anon_hugepage (deleted) > 7f39eec00000-7f39eee00000 rw-s 87e200000 00:11 122710 > /anon_hugepage (deleted) > 7f39eee00000-7f39ef000000 rw-s 87e400000 00:11 122710 > /anon_hugepage (deleted) > > but mbind() was called for just 0x7f39eeab1660−0x7f39eea00000 = > 0xB1660 = 726624 bytes, but if adjust blindly endptr in that > fastpath_partition_init() to be "char *endptr = ptr + 2*1024*1024;" > (HP) it doesn't complain anymore and I get success: > 2025-11-04 12:08:30.147 CET [62352] DEBUG: NUMA: > fastpath_partition_init ptr 0x7f7bf7000000 endptr 0x7f7bf7200000 > num_procs 2523 node 0 > 2025-11-04 12:08:30.147 CET [62352] DEBUG: NUMA: > fastpath_partition_init ptr 0x7f7bf7200000 endptr 0x7f7bf7400000 > num_procs 2523 node 1 > 2025-11-04 12:08:30.147 CET [62352] DEBUG: NUMA: > fastpath_partition_init ptr 0x7f7bf7400000 endptr 0x7f7bf7600000 > num_procs 2523 node 2 > 2025-11-04 12:08:30.147 CET [62352] DEBUG: NUMA: > fastpath_partition_init ptr 0x7f7bf7600000 endptr 0x7f7bf7800000 > num_procs 2523 node 3 > 2025-11-04 12:08:30.147 CET [62352] DEBUG: NUMA: > fastpath_partition_init ptr 0x7f7bf7800000 endptr 0x7f7bf7a00000 > num_procs 38 node -1 > 2025-11-04 12:08:30.239 CET [62352] LOG: starting PostgreSQL > 19devel on x86_64-linux, compiled by gcc-12.2.0, 64-bit > Hmm, so it seems like another hugepage-related issue. The mbind manpage says this about "len": EINVAL An invalid value was specified for flags or mode; or addr + len was less than addr; or addr is not a multiple of the system page size. I don't think that requires (addr+len) to be a multiple of page size, but maybe that is required. > 0006d: I've got one SIGBUS during a call to select > pg_buffercache_numa_pages(); and it looks like that memory accessed is > simply not mapped? (bug) > > Program received signal SIGBUS, Bus error. > pg_buffercache_numa_pages (fcinfo=0x561a97e8e680) at > ../contrib/pg_buffercache/pg_buffercache_pages.c:386 > 386 pg_numa_touch_mem_if_required(ptr); > (gdb) print ptr > $1 = 0x7f4ed0200000 <error: Cannot access memory at address 0x7f4ed0200000> > (gdb) where > #0 pg_buffercache_numa_pages (fcinfo=0x561a97e8e680) at > ../contrib/pg_buffercache/pg_buffercache_pages.c:386 > #1 0x0000561a672a0efe in ExecMakeFunctionResultSet > (fcache=0x561a97e8e5d0, econtext=econtext@entry=0x561a97e8dab8, > argContext=0x561a97ec62a0, isNull=0x561a97e8e578, > isDone=isDone@entry=0x561a97e8e5c0) at > ../src/backend/executor/execSRF.c:624 > [..] > > Postmaster had still attached shm (visible via smaps), and if you > compare closely 0x7f4ed0200000 against sorted smaps: > > 7f4921400000-7f4b21400000 rw-s 252600000 00:11 151111 > /anon_hugepage (deleted) > 7f4b21400000-7f4d21400000 rw-s 452600000 00:11 151111 > /anon_hugepage (deleted) > 7f4d21400000-7f4f21400000 rw-s 652600000 00:11 151111 > /anon_hugepage (deleted) > 7f4f21400000-7f4f4bc00000 rw-s 852600000 00:11 151111 > /anon_hugepage (deleted) > 7f4f4bc00000-7f4f4c000000 rw-s 87ce00000 00:11 151111 > /anon_hugepage (deleted) > > it's NOT there at all (there's no mmap region starting with > 0x"7f4e" ). It looks like because pg_buffercache_numa_pages() is not > aware of this new mmaped() regions and instead does simple loop over > all NBuffers with "for (char *ptr = startptr; ptr < endptr; ptr += > os_page_size)"? > I'm confused. How could that mapping be missing? Was this with huge pages / how many did you reserve on the nodes? Maybe there were not enough huge pages left on one of the nodes? I believe I got some SIGBUS in those cases. > 0006e: > I'm seeking confirmation, but is this the issue we have discussed > on PgconfEU related to lack of detection of Mems_allowed, right? e.g. > $ numactl --membind="0,1" --cpunodebind="0,1" > /usr/pgsql19/bin/pg_ctl -D /path start > still shows 4 NUMA nodes used. Current patches use > numa_num_configured_nodes(), but it says 'This count includes any > nodes that are currently DISABLED'. So I was wondering if I could help > by migrating towards numa_num_task_nodes() / numa_get_mems_allowed()? > It's the same as You wrote earlier to Alexy? > If "mems_allowed" refers to nodes allowing memory allocation, then yes, this would be one way to get into that issue. Oh, is this what happened in 0006d? > > But that's not what you proposed here, clearly. You're saying we should > > find which NUMA nodes the process is allowed to run, and use those. > > Instead of just using all *configured* nodes. And I agree with that. > > So are you already on it ? > >> There are a couple unsolved issues, though. While running the tests, I >> ran into a bunch of weird issues. I saw two types of failures: >> 1) Bad address >> 2) Operation canceled > > I did run (with io_uring) a short test(< 10min with -c 128) and didn't > get those. Could you please share specific tips/workload for > reproducing this? > I did get a couple of "operation canceled" failures, but only on fairly old kernel versions (6.1 which came as default with the VM). I heard some suggestions this is a bug in older kernels - I don't have any link to a bug report / fix, though. But I've been unable to reproduce this on 6.17, so maybe it's true. For me the failures always happened 10 seconds after the start of the benchmark (and starting the instance), so it's probably sufficient to keep the runs ~20 seconds (and maybe restart in between?). But even then it's fairly rare. I've seen ~10 failures for 500 runs. I haven't seen more "bad address" cases, I have no idea why. I'm still guessing it's related to huge pages, so maybe I happened to reserve enough of them. regards -- Tomas Vondra
-
Re: Adding basic NUMA awareness
Jakub Wartak <jakub.wartak@enterprisedb.com> — 2025-11-06T14:02:57Z
On Tue, Nov 4, 2025 at 10:21 PM Tomas Vondra <tomas@vondra.me> wrote: Hi Tomas, > > 0007a: pg_buffercache_pgproc returns pgproc_ptr and fastpath_ptr in > > bigint and not hex? I've wanted to adjust that to TEXTOID, but instead > > I've thought it is going to be simpler to use to_hex() -- see 0009 > > attached. > > > > I don't know. I added simply because it might be useful for development, > but we probably don't want to expose these pointers at all. > > > 0007b: pg_buffercache_pgproc -- nitpick, but maybe it would be better > > called pg_shm_pgproc? > > > > Right. It does not belong to pg_buffercache at all, I just added it > there because I've been messing with that code already. Please keep them in for at least for some time (perhaps standalone patch marked as not intended to be commited would work?). I find the view extermely useful as it will allow us pinpointing local-vs-remote NUMA fetches (we need to know the addres). > > 0007c with check_numa='buffers,procs' throws 'mbind Invalid argument' > > during start: > > > > 2025-11-04 10:02:27.055 CET [58464] DEBUG: NUMA: > > pgproc_init_partition procs 0x7f8d30400000 endptr 0x7f8d30800000 > > num_procs 2523 node 0 > > 2025-11-04 10:02:27.057 CET [58464] DEBUG: NUMA: > > pgproc_init_partition procs 0x7f8d30800000 endptr 0x7f8d30c00000 > > num_procs 2523 node 1 > > 2025-11-04 10:02:27.059 CET [58464] DEBUG: NUMA: > > pgproc_init_partition procs 0x7f8d30c00000 endptr 0x7f8d31000000 > > num_procs 2523 node 2 > > 2025-11-04 10:02:27.061 CET [58464] DEBUG: NUMA: > > pgproc_init_partition procs 0x7f8d31000000 endptr 0x7f8d31400000 > > num_procs 2523 node 3 > > 2025-11-04 10:02:27.062 CET [58464] DEBUG: NUMA: > > pgproc_init_partition procs 0x7f8d31400000 endptr 0x7f8d31407cb0 > > num_procs 38 node -1 > > mbind: Invalid argument > > mbind: Invalid argument > > mbind: Invalid argument > > mbind: Invalid argument > > > > I'll take a look, but I don't recall seeing such errors. > Alexy also reported this earlier, here https://www.postgresql.org/message-id/92e23c85-f646-4bab-b5e0-df30d8ddf4bd%40postgrespro.ru (just use HP, set some high max_connections). I've double checked this too , numa_tonode_memory() len needs to HP size. > > 0007d: so we probably need numa_warn()/numa_error() wrappers (this was > > initially part of NUMA observability patches but got removed during > > the course of action), I'm attaching 0008. With that you'll get > > something a little more up to our standards: > > 2025-11-04 10:27:07.140 CET [59696] DEBUG: > > fastpath_parititon_init node = 3, ptr = 0x7f4f4d400000, endptr = > > 0x7f4f4d4b1660 > > 2025-11-04 10:27:07.140 CET [59696] WARNING: libnuma: ERROR: mbind > > > > Not sure. Any particular objections? We need to somehow emit them into the logs. > > 0007f: The "mbind: Invalid argument"" issue itself with the below addition: [..] > > > > but mbind() was called for just 0x7f39eeab1660−0x7f39eea00000 = > > 0xB1660 = 726624 bytes, but if adjust blindly endptr in that > > fastpath_partition_init() to be "char *endptr = ptr + 2*1024*1024;" > > (HP) it doesn't complain anymore and I get success: [..] > > Hmm, so it seems like another hugepage-related issue. The mbind manpage > says this about "len": > > EINVAL An invalid value was specified for flags or mode; or addr + len > was less than addr; or addr is not a multiple of the system page size. > > I don't think that requires (addr+len) to be a multiple of page size, > but maybe that is required. I do think that 'system page size' means above HP page size, but this time it's just for fastpath_partition_init(), the earlier one seems to aligned fine (?? -- i havent really checked but there's no error) > > 0006d: I've got one SIGBUS during a call to select > > pg_buffercache_numa_pages(); and it looks like that memory accessed is > > simply not mapped? (bug) > > > > Program received signal SIGBUS, Bus error. > > pg_buffercache_numa_pages (fcinfo=0x561a97e8e680) at > > ../contrib/pg_buffercache/pg_buffercache_pages.c:386 > > 386 pg_numa_touch_mem_if_required(ptr); > > (gdb) print ptr > > $1 = 0x7f4ed0200000 <error: Cannot access memory at address 0x7f4ed0200000> > > (gdb) where > > #0 pg_buffercache_numa_pages (fcinfo=0x561a97e8e680) at > > ../contrib/pg_buffercache/pg_buffercache_pages.c:386 > > #1 0x0000561a672a0efe in ExecMakeFunctionResultSet > > (fcache=0x561a97e8e5d0, econtext=econtext@entry=0x561a97e8dab8, > > argContext=0x561a97ec62a0, isNull=0x561a97e8e578, > > isDone=isDone@entry=0x561a97e8e5c0) at > > ../src/backend/executor/execSRF.c:624 > > [..] > > > > Postmaster had still attached shm (visible via smaps), and if you > > compare closely 0x7f4ed0200000 against sorted smaps: > > > > 7f4921400000-7f4b21400000 rw-s 252600000 00:11 151111 > > /anon_hugepage (deleted) > > 7f4b21400000-7f4d21400000 rw-s 452600000 00:11 151111 > > /anon_hugepage (deleted) > > 7f4d21400000-7f4f21400000 rw-s 652600000 00:11 151111 > > /anon_hugepage (deleted) > > 7f4f21400000-7f4f4bc00000 rw-s 852600000 00:11 151111 > > /anon_hugepage (deleted) > > 7f4f4bc00000-7f4f4c000000 rw-s 87ce00000 00:11 151111 > > /anon_hugepage (deleted) > > > > it's NOT there at all (there's no mmap region starting with > > 0x"7f4e" ). It looks like because pg_buffercache_numa_pages() is not > > aware of this new mmaped() regions and instead does simple loop over > > all NBuffers with "for (char *ptr = startptr; ptr < endptr; ptr += > > os_page_size)"? > > > > I'm confused. How could that mapping be missing? Was this with huge > pages / how many did you reserve on the nodes? OK I made and error and paritally got it correct (it crashes reliably) and partially mislead You, appologies, let me explain. There were two questions for me: a) why we make single mmap() and after numa_tonode_memory() we get plenty of mappings b) why we get SIGBUS (I've thought they are not continus, but they are after triple-checking) ad a) My testing shows that on HP,as stated initially ("all of this was on 4s/4 NUMA nodes with HP on"). That's what the codes does, you get single mmaps() (resulting in single entry in smaps), but afte noda_tonode_memory() there's many of them. Even on laptop: System has 1 NUMA nodes (0 to 0). Attempting to allocate 8.000000 MB of HugeTLB memory... Successfully allocated HugeTLB memory at 0x755828800000, smaps before: 755828800000-755829000000 rw-s 00000000 00:11 259808 /anon_hugepage (deleted) Pinning first part (from 0x755828800000) to NUMA node 0... smaps after: 755828800000-755828c00000 rw-s 00000000 00:11 259808 /anon_hugepage (deleted) 755828c00000-755829000000 rw-s 00400000 00:11 259808 /anon_hugepage (deleted) Pinning second part (from 0x755828c00000) to NUMA node 0... smaps after: 755828800000-755828c00000 rw-s 00000000 00:11 259808 /anon_hugepage (deleted) 755828c00000-755829000000 rw-s 00400000 00:11 259808 /anon_hugepage (deleted) It gets even more funny, below I have 8MB HP=on, but just issue 2x numa_tonode_memory(for len 2MB on 4MB ptr to node0) (two times for ptr, second time in half of that): System has 1 NUMA nodes (0 to 0). Attempting to allocate 8.000000 MB of HugeTLB memory... Successfully allocated HugeTLB memory at 0x7302dda00000, smaps before: 7302dda00000-7302de200000 rw-s 00000000 00:11 284859 /anon_hugepage (deleted) Pinning first part (from 0x7302dda00000) to NUMA node 0... smaps after: 7302dda00000-7302ddc00000 rw-s 00000000 00:11 284859 /anon_hugepage (deleted) 7302ddc00000-7302de200000 rw-s 00200000 00:11 284859 /anon_hugepage (deleted) Pinning second part (from 0x7302dde00000) to NUMA node 0... smaps after: 7302dda00000-7302ddc00000 rw-s 00000000 00:11 284859 /anon_hugepage (deleted) 7302ddc00000-7302dde00000 rw-s 00200000 00:11 284859 /anon_hugepage (deleted) 7302dde00000-7302de000000 rw-s 00400000 00:11 284859 /anon_hugepage (deleted) 7302de000000-7302de200000 rw-s 00600000 00:11 284859 /anon_hugepage (deleted) Why 4 instead of 1? Because some mappings are now "default" becauswe their policy was not altered: $ grep huge /proc/$(pidof testnumammapsplit)/numa_maps 7302dda00000 bind:0 file=/anon_hugepage\040(deleted) huge 7302ddc00000 default file=/anon_hugepage\040(deleted) huge 7302dde00000 bind:0 file=/anon_hugepage\040(deleted) huge 7302de000000 default file=/anon_hugepage\040(deleted) huge Back to originnal error, they are consecutive regions and earlier problem is error: 0x7f4ed0200000 <error: Cannot access memory at address 0x7f4ed0200000> start: 0x7f4921400000 end: 0x7f4f4c000000 so it fits into that range (that was my mistate earlier, using just grep not checking are they really within that), but... > Maybe there were not enough huge pages left on one of the nodes? ad b) right, something like that. I've investigated that SIGBUS there (it's going to be long): with shared_buffers=32GB, huge_pages 17715 (+1 from what postgres -C shared_memory_size_in_huge_pages returns), right after startup, but no touch: Program received signal SIGBUS, Bus error. pg_buffercache_numa_pages (fcinfo=0x5572038790b8) at ../contrib/pg_buffercache/pg_buffercache_pages.c:386 386 pg_numa_touch_mem_if_required(ptr); (gdb) where #0 pg_buffercache_numa_pages (fcinfo=0x5572038790b8) at ../contrib/pg_buffercache/pg_buffercache_pages.c:386 #1 0x00005571f54ddb7d in ExecMakeTableFunctionResult (setexpr=0x557203870d40, econtext=0x557203870ba8, argContext=<optimized out>, expectedDesc=0x557203870f80, randomAccess=false) at ../src/backend/executor/execSRF.c:234 [..] (gdb) print ptr $1 = 0x7f6cf8400000 <error: Cannot access memory at address 0x7f6cf8400000> (gdb) then it shows?! no available hugepage on one of the nodes (while gdb is hanging and preving autorestart): root@swiatowid:/sys/devices/system/node# grep -r -i HugePages_Free node*/meminfo node0/meminfo:Node 0 HugePages_Free: 299 node1/meminfo:Node 1 HugePages_Free: 299 node2/meminfo:Node 2 HugePages_Free: 299 node3/meminfo:Node 3 HugePages_Free: 0 but they are equal in terms of size: node0/meminfo:Node 0 HugePages_Total: 4429 node1/meminfo:Node 1 HugePages_Total: 4429 node2/meminfo:Node 2 HugePages_Total: 4429 node3/meminfo:Node 3 HugePages_Total: 4428 smaps shows that this address (7f6cf8400000) is mapped in this mapping: 7f6b49c00000-7f6d49c00000 rw-s 652600000 00:11 86064 /anon_hugepage (deleted) numa_maps for this region shows this is this mapping on node3 (notice N3 + bind:3 matches lack of memory on Node 3 HugePAges_Free): 7f6b49c00000 bind:3 file=/anon_hugepage\040(deleted) huge dirty=3444 N3=3444 kernelpagesize_kB=2048 the surrounding area of this looks like that: 7f6549c00000 bind:0 file=/anon_hugepage\040(deleted) huge dirty=4096 N0=4096 kernelpagesize_kB=2048 7f6749c00000 bind:1 file=/anon_hugepage\040(deleted) huge dirty=4096 N1=4096 kernelpagesize_kB=2048 7f6949c00000 bind:2 file=/anon_hugepage\040(deleted) huge dirty=4096 N2=4096 kernelpagesize_kB=2048 7f6b49c00000 bind:3 file=/anon_hugepage\040(deleted) huge dirty=3444 N3=3444 kernelpagesize_kB=2048 <-- this is the one 7f6d49c00000 default file=/anon_hugepage\040(deleted) huge dirty=107 mapmax=6 N3=107 kernelpagesize_kB=2048 Notice it's just N3=3444, while the others are much larger. So something was using that hugepages memory on N3: # grep kernelpagesize_kB=2048 /proc/1679/numa_maps | grep -Po N[0-4]=[0-9]+ | sort N0=2 N0=4096 N1=2 N1=4096 N2=2 N2=4096 N3=1 N3=1 N3=1 N3=1 N3=107 N3=13 N3=3 N3=3444 So per above it's not there (at least not as 2MB HP). But the number of mappings is wild there! (node where it is failing has plenty of memory, no hugepage memory left, but it has like 40k+ of small mappings!) # grep -Po 'N[0-3]=' /proc/1679/numa_maps | sort | uniq -c 17 N0= 10 N1= 3 N2= 40434 N3= most of them are `anon_inode:[io_uring]` (and I had max_connections=10k). You may ask why in spite of Andres optimization for reducing number segments for uring, it's not working for me ? Well I've just noticed way too silent failure to active this (altough I'm on 6.14.x): 2025-11-06 13:34:49.128 CET [1658] DEBUG: can't use combined memory mapping for io_uring, kernel or liburing too old and I dont have io_uring_queue_init_mem()/HAVE_LIBURING_QUEUE_INIT_MEM apparently on liburing-2.3 (Debian's default). See [1] for more info (fix is not commited yet sadly). Next try, now with io_method = worker and right before start: root@swiatowid:/sys/devices/system/node# grep -r -i HugePages_Total node*/meminfo node0/meminfo:Node 0 HugePages_Total: 4429 node1/meminfo:Node 1 HugePages_Total: 4429 node2/meminfo:Node 2 HugePages_Total: 4429 node3/meminfo:Node 3 HugePages_Total: 4428 and HugePages_Free were 100% (if postgresql was down). After start (but without doing anything else): root@swiatowid:/sys/devices/system/node# grep -r -i HugePages_Free node*/meminfo node0/meminfo:Node 0 HugePages_Free: 4393 node1/meminfo:Node 1 HugePages_Free: 4395 node2/meminfo:Node 2 HugePages_Free: 4395 node3/meminfo:Node 3 HugePages_Free: 3446 So sadly the picture is the same (something stole my HP on N3 and it's PostgreSQL on it's own). After some time of investigating that ("who stole my hugepage across whole OS"), I've just added MAP_POPULATE to the mix of PG_MMAP_FLAGS and got this after start: root@swiatowid:/sys/devices/system/node# grep -r -i HugePages_Free node*/meminfo node0/meminfo:Node 0 HugePages_Free: 0 node1/meminfo:Node 1 HugePages_Free: 0 node2/meminfo:Node 2 HugePages_Free: 0 node3/meminfo:Node 3 HugePages_Free: 1 and then the SELECT to pg_buffercache_numa works fine(!). Another ways that I have found to eliminate that SIGBUS a. Would be to throw much more HugePages (so that node does not run to HugePages_Free), but that's not real option. b. Then I've reminded myself that I could be running custom kernel with experimental CONFIG_READ_ONLY_THP_FOR_FS (to reduce iTLB misses tranparently with specially linked PG; will double check exact stuff later), so I've thrown never into /sys/kernel/mm/transparent_hugepage/enabled and defrag too (yes , disabled THP) and with that -- drumroll -- that SELECT works. The very same PG picture after startup (where earlier it would crash), now after SELECT it looks like that: root@swiatowid:/sys/devices/system/node# grep -r -i HugePages_Free node*/meminfo node0/meminfo:Node 0 HugePages_Free: 83 node1/meminfo:Node 1 HugePages_Free: 0 node2/meminfo:Node 2 HugePages_Free: 81 node3/meminfo:Node 3 HugePages_Free: 82 Hope that helps a little. To me it sounds like THP used that memory somehow and we've also wanted to use. With numa_interleave_ptr() that wouldn't be a problem because probably it would something else available, but not here as we indicated exact node. > > 0006e: > > I'm seeking confirmation, but is this the issue we have discussed > > on PgconfEU related to lack of detection of Mems_allowed, right? e.g. > > $ numactl --membind="0,1" --cpunodebind="0,1" > > /usr/pgsql19/bin/pg_ctl -D /path start > > still shows 4 NUMA nodes used. Current patches use > > numa_num_configured_nodes(), but it says 'This count includes any > > nodes that are currently DISABLED'. So I was wondering if I could help > > by migrating towards numa_num_task_nodes() / numa_get_mems_allowed()? > > It's the same as You wrote earlier to Alexy? > > > > If "mems_allowed" refers to nodes allowing memory allocation, then yes, > this would be one way to get into that issue. Oh, is this what happened > in 0006d? OK, thanks for confirmation. No, 0006d was about normal numactl run, without --membind. > I did get a couple of "operation canceled" failures, but only on fairly > old kernel versions (6.1 which came as default with the VM). OK, I'll try to see that later too. btw QQ regarding partitioned clockwise as I had thought: does this opens a road towards multiple BGwriters? (outside of this $thread/v1/PoC) -J. [1] - https://www.postgresql.org/message-id/CAKZiRmzxj6Lt1w2ffDoUmN533TgyDeYVULEH1PQFLRyBJSFP6w%40mail.gmail.com -
Re: Adding basic NUMA awareness
Tomas Vondra <tomas@vondra.me> — 2025-11-11T11:52:01Z
Hi, here's a rebased patch series, fixing most of the smaller issues from v20251101, and making cfbot happy (hopefully). On 11/6/25 15:02, Jakub Wartak wrote: > On Tue, Nov 4, 2025 at 10:21 PM Tomas Vondra <tomas@vondra.me> wrote: > > Hi Tomas, > >>> 0007a: pg_buffercache_pgproc returns pgproc_ptr and fastpath_ptr in >>> bigint and not hex? I've wanted to adjust that to TEXTOID, but instead >>> I've thought it is going to be simpler to use to_hex() -- see 0009 >>> attached. >>> >> >> I don't know. I added simply because it might be useful for development, >> but we probably don't want to expose these pointers at all. >> >>> 0007b: pg_buffercache_pgproc -- nitpick, but maybe it would be better >>> called pg_shm_pgproc? >>> >> >> Right. It does not belong to pg_buffercache at all, I just added it >> there because I've been messing with that code already. > > Please keep them in for at least for some time (perhaps standalone > patch marked as not intended to be commited would work?). I find the > view extermely useful as it will allow us pinpointing local-vs-remote > NUMA fetches (we need to know the addres). > Are you referring to the _pgproc view specifically, or also to the view with buffer partitions? I don't intend to remove the view for shared buffers, that's indeed useful. >>> 0007c with check_numa='buffers,procs' throws 'mbind Invalid argument' >>> during start: >>> >>> 2025-11-04 10:02:27.055 CET [58464] DEBUG: NUMA: >>> pgproc_init_partition procs 0x7f8d30400000 endptr 0x7f8d30800000 >>> num_procs 2523 node 0 >>> 2025-11-04 10:02:27.057 CET [58464] DEBUG: NUMA: >>> pgproc_init_partition procs 0x7f8d30800000 endptr 0x7f8d30c00000 >>> num_procs 2523 node 1 >>> 2025-11-04 10:02:27.059 CET [58464] DEBUG: NUMA: >>> pgproc_init_partition procs 0x7f8d30c00000 endptr 0x7f8d31000000 >>> num_procs 2523 node 2 >>> 2025-11-04 10:02:27.061 CET [58464] DEBUG: NUMA: >>> pgproc_init_partition procs 0x7f8d31000000 endptr 0x7f8d31400000 >>> num_procs 2523 node 3 >>> 2025-11-04 10:02:27.062 CET [58464] DEBUG: NUMA: >>> pgproc_init_partition procs 0x7f8d31400000 endptr 0x7f8d31407cb0 >>> num_procs 38 node -1 >>> mbind: Invalid argument >>> mbind: Invalid argument >>> mbind: Invalid argument >>> mbind: Invalid argument >>> >> >> I'll take a look, but I don't recall seeing such errors. >> > > Alexy also reported this earlier, here > https://www.postgresql.org/message-id/92e23c85-f646-4bab-b5e0-df30d8ddf4bd%40postgrespro.ru > (just use HP, set some high max_connections). I've double checked this > too , numa_tonode_memory() len needs to HP size. > OK, I'll investigate this. >>> 0007d: so we probably need numa_warn()/numa_error() wrappers (this was >>> initially part of NUMA observability patches but got removed during >>> the course of action), I'm attaching 0008. With that you'll get >>> something a little more up to our standards: >>> 2025-11-04 10:27:07.140 CET [59696] DEBUG: >>> fastpath_parititon_init node = 3, ptr = 0x7f4f4d400000, endptr = >>> 0x7f4f4d4b1660 >>> 2025-11-04 10:27:07.140 CET [59696] WARNING: libnuma: ERROR: mbind >>> >> >> Not sure. > > Any particular objections? We need to somehow emit them into the logs. > No idea, I think it'd be better to make sure this failure can't happen, but maybe it's not possible. I don't understand the mbind failure well enough. >>> 0007f: The "mbind: Invalid argument"" issue itself with the below addition: > [..] >>> >>> but mbind() was called for just 0x7f39eeab1660−0x7f39eea00000 = >>> 0xB1660 = 726624 bytes, but if adjust blindly endptr in that >>> fastpath_partition_init() to be "char *endptr = ptr + 2*1024*1024;" >>> (HP) it doesn't complain anymore and I get success: > [..] >> >> Hmm, so it seems like another hugepage-related issue. The mbind manpage >> says this about "len": >> >> EINVAL An invalid value was specified for flags or mode; or addr + len >> was less than addr; or addr is not a multiple of the system page size. >> >> I don't think that requires (addr+len) to be a multiple of page size, >> but maybe that is required. > > I do think that 'system page size' means above HP page size, but this > time it's just for fastpath_partition_init(), the earlier one seems to > aligned fine (?? -- i havent really checked but there's no error) > Hmmm, ok. Will check. But maybe let's not focus too much on the PGPROC partitioning, I don't think that's likely to go into 19. >>> 0006d: I've got one SIGBUS during a call to select >>> pg_buffercache_numa_pages(); and it looks like that memory accessed is >>> simply not mapped? (bug) >>> >>> Program received signal SIGBUS, Bus error. >>> pg_buffercache_numa_pages (fcinfo=0x561a97e8e680) at >>> ../contrib/pg_buffercache/pg_buffercache_pages.c:386 >>> 386 pg_numa_touch_mem_if_required(ptr); >>> (gdb) print ptr >>> $1 = 0x7f4ed0200000 <error: Cannot access memory at address 0x7f4ed0200000> >>> (gdb) where >>> #0 pg_buffercache_numa_pages (fcinfo=0x561a97e8e680) at >>> ../contrib/pg_buffercache/pg_buffercache_pages.c:386 >>> #1 0x0000561a672a0efe in ExecMakeFunctionResultSet >>> (fcache=0x561a97e8e5d0, econtext=econtext@entry=0x561a97e8dab8, >>> argContext=0x561a97ec62a0, isNull=0x561a97e8e578, >>> isDone=isDone@entry=0x561a97e8e5c0) at >>> ../src/backend/executor/execSRF.c:624 >>> [..] >>> >>> Postmaster had still attached shm (visible via smaps), and if you >>> compare closely 0x7f4ed0200000 against sorted smaps: >>> >>> 7f4921400000-7f4b21400000 rw-s 252600000 00:11 151111 >>> /anon_hugepage (deleted) >>> 7f4b21400000-7f4d21400000 rw-s 452600000 00:11 151111 >>> /anon_hugepage (deleted) >>> 7f4d21400000-7f4f21400000 rw-s 652600000 00:11 151111 >>> /anon_hugepage (deleted) >>> 7f4f21400000-7f4f4bc00000 rw-s 852600000 00:11 151111 >>> /anon_hugepage (deleted) >>> 7f4f4bc00000-7f4f4c000000 rw-s 87ce00000 00:11 151111 >>> /anon_hugepage (deleted) >>> >>> it's NOT there at all (there's no mmap region starting with >>> 0x"7f4e" ). It looks like because pg_buffercache_numa_pages() is not >>> aware of this new mmaped() regions and instead does simple loop over >>> all NBuffers with "for (char *ptr = startptr; ptr < endptr; ptr += >>> os_page_size)"? >>> >> >> I'm confused. How could that mapping be missing? Was this with huge >> pages / how many did you reserve on the nodes? > > > OK I made and error and paritally got it correct (it crashes reliably) > and partially mislead You, appologies, let me explain. There were two > questions for me: > a) why we make single mmap() and after numa_tonode_memory() we get > plenty of mappings > b) why we get SIGBUS (I've thought they are not continus, but they are > after triple-checking) > > ad a) My testing shows that on HP,as stated initially ("all of this > was on 4s/4 NUMA nodes with HP on"). That's what the codes does, you > get single mmaps() (resulting in single entry in smaps), but afte > noda_tonode_memory() there's many of them. Even on laptop: > > System has 1 NUMA nodes (0 to 0). > Attempting to allocate 8.000000 MB of HugeTLB memory... > Successfully allocated HugeTLB memory at 0x755828800000, smaps before: > 755828800000-755829000000 rw-s 00000000 00:11 259808 > /anon_hugepage (deleted) > Pinning first part (from 0x755828800000) to NUMA node 0... > smaps after: > 755828800000-755828c00000 rw-s 00000000 00:11 259808 > /anon_hugepage (deleted) > 755828c00000-755829000000 rw-s 00400000 00:11 259808 > /anon_hugepage (deleted) > Pinning second part (from 0x755828c00000) to NUMA node 0... > smaps after: > 755828800000-755828c00000 rw-s 00000000 00:11 259808 > /anon_hugepage (deleted) > 755828c00000-755829000000 rw-s 00400000 00:11 259808 > /anon_hugepage (deleted) > > It gets even more funny, below I have 8MB HP=on, but just issue 2x > numa_tonode_memory(for len 2MB on 4MB ptr to node0) (two times for > ptr, second time in half of that): > > System has 1 NUMA nodes (0 to 0). > Attempting to allocate 8.000000 MB of HugeTLB memory... > Successfully allocated HugeTLB memory at 0x7302dda00000, smaps before: > 7302dda00000-7302de200000 rw-s 00000000 00:11 284859 > /anon_hugepage (deleted) > Pinning first part (from 0x7302dda00000) to NUMA node 0... > smaps after: > 7302dda00000-7302ddc00000 rw-s 00000000 00:11 284859 > /anon_hugepage (deleted) > 7302ddc00000-7302de200000 rw-s 00200000 00:11 284859 > /anon_hugepage (deleted) > Pinning second part (from 0x7302dde00000) to NUMA node 0... > smaps after: > 7302dda00000-7302ddc00000 rw-s 00000000 00:11 284859 > /anon_hugepage (deleted) > 7302ddc00000-7302dde00000 rw-s 00200000 00:11 284859 > /anon_hugepage (deleted) > 7302dde00000-7302de000000 rw-s 00400000 00:11 284859 > /anon_hugepage (deleted) > 7302de000000-7302de200000 rw-s 00600000 00:11 284859 > /anon_hugepage (deleted) > > Why 4 instead of 1? Because some mappings are now "default" becauswe > their policy was not altered: > > $ grep huge /proc/$(pidof testnumammapsplit)/numa_maps > 7302dda00000 bind:0 file=/anon_hugepage\040(deleted) huge > 7302ddc00000 default file=/anon_hugepage\040(deleted) huge > 7302dde00000 bind:0 file=/anon_hugepage\040(deleted) huge > 7302de000000 default file=/anon_hugepage\040(deleted) huge > > Back to originnal error, they are consecutive regions and earlier problem is > > error: 0x7f4ed0200000 <error: Cannot access memory at address 0x7f4ed0200000> > start: 0x7f4921400000 > end: 0x7f4f4c000000 > > so it fits into that range (that was my mistate earlier, using just > grep not checking are they really within that), but... > >> Maybe there were not enough huge pages left on one of the nodes? > > ad b) right, something like that. I've investigated that SIGBUS there > (it's going to be long): > > with shared_buffers=32GB, huge_pages 17715 (+1 from what postgres -C > shared_memory_size_in_huge_pages returns), right after startup, but no > touch: > > Program received signal SIGBUS, Bus error. > pg_buffercache_numa_pages (fcinfo=0x5572038790b8) at > ../contrib/pg_buffercache/pg_buffercache_pages.c:386 > 386 pg_numa_touch_mem_if_required(ptr); > (gdb) where > #0 pg_buffercache_numa_pages (fcinfo=0x5572038790b8) at > ../contrib/pg_buffercache/pg_buffercache_pages.c:386 > #1 0x00005571f54ddb7d in ExecMakeTableFunctionResult > (setexpr=0x557203870d40, econtext=0x557203870ba8, > argContext=<optimized out>, expectedDesc=0x557203870f80, > randomAccess=false) at ../src/backend/executor/execSRF.c:234 > [..] > (gdb) print ptr > $1 = 0x7f6cf8400000 <error: Cannot access memory at address 0x7f6cf8400000> > (gdb) > > > then it shows?! no available hugepage on one of the nodes (while gdb > is hanging and preving autorestart): > > root@swiatowid:/sys/devices/system/node# grep -r -i HugePages_Free node*/meminfo > node0/meminfo:Node 0 HugePages_Free: 299 > node1/meminfo:Node 1 HugePages_Free: 299 > node2/meminfo:Node 2 HugePages_Free: 299 > node3/meminfo:Node 3 HugePages_Free: 0 > > but they are equal in terms of size: > node0/meminfo:Node 0 HugePages_Total: 4429 > node1/meminfo:Node 1 HugePages_Total: 4429 > node2/meminfo:Node 2 HugePages_Total: 4429 > node3/meminfo:Node 3 HugePages_Total: 4428 > > smaps shows that this address (7f6cf8400000) is mapped in this mapping: > 7f6b49c00000-7f6d49c00000 rw-s 652600000 00:11 86064 > /anon_hugepage (deleted) > > numa_maps for this region shows this is this mapping on node3 (notice > N3 + bind:3 matches lack of memory on Node 3 HugePAges_Free): > 7f6b49c00000 bind:3 file=/anon_hugepage\040(deleted) huge dirty=3444 > N3=3444 kernelpagesize_kB=2048 > > the surrounding area of this looks like that: > > 7f6549c00000 bind:0 file=/anon_hugepage\040(deleted) huge dirty=4096 > N0=4096 kernelpagesize_kB=2048 > 7f6749c00000 bind:1 file=/anon_hugepage\040(deleted) huge dirty=4096 > N1=4096 kernelpagesize_kB=2048 > 7f6949c00000 bind:2 file=/anon_hugepage\040(deleted) huge dirty=4096 > N2=4096 kernelpagesize_kB=2048 > 7f6b49c00000 bind:3 file=/anon_hugepage\040(deleted) huge dirty=3444 > N3=3444 kernelpagesize_kB=2048 <-- this is the one > 7f6d49c00000 default file=/anon_hugepage\040(deleted) huge dirty=107 > mapmax=6 N3=107 kernelpagesize_kB=2048 > > Notice it's just N3=3444, while the others are much larger. So > something was using that hugepages memory on N3: > > # grep kernelpagesize_kB=2048 /proc/1679/numa_maps | grep -Po > N[0-4]=[0-9]+ | sort > N0=2 > N0=4096 > N1=2 > N1=4096 > N2=2 > N2=4096 > N3=1 > N3=1 > N3=1 > N3=1 > N3=107 > N3=13 > N3=3 > N3=3444 > > So per above it's not there (at least not as 2MB HP). But the number > of mappings is wild there! (node where it is failing has plenty of > memory, no hugepage memory left, but it has like 40k+ of small > mappings!) > > # grep -Po 'N[0-3]=' /proc/1679/numa_maps | sort | uniq -c > 17 N0= > 10 N1= > 3 N2= > 40434 N3= > > most of them are `anon_inode:[io_uring]` (and I had > max_connections=10k). You may ask why in spite of Andres optimization > for reducing number segments for uring, it's not working for me ? Well > I've just noticed way too silent failure to active this (altough I'm > on 6.14.x): > 2025-11-06 13:34:49.128 CET [1658] DEBUG: can't use combined > memory mapping for io_uring, kernel or liburing too old > and I dont have io_uring_queue_init_mem()/HAVE_LIBURING_QUEUE_INIT_MEM > apparently on liburing-2.3 (Debian's default). See [1] for more info > (fix is not commited yet sadly). > > Next try, now with io_method = worker and right before start: > > root@swiatowid:/sys/devices/system/node# grep -r -i HugePages_Total > node*/meminfo > node0/meminfo:Node 0 HugePages_Total: 4429 > node1/meminfo:Node 1 HugePages_Total: 4429 > node2/meminfo:Node 2 HugePages_Total: 4429 > node3/meminfo:Node 3 HugePages_Total: 4428 > and HugePages_Free were 100% (if postgresql was down). After start > (but without doing anything else): > root@swiatowid:/sys/devices/system/node# grep -r -i HugePages_Free node*/meminfo > node0/meminfo:Node 0 HugePages_Free: 4393 > node1/meminfo:Node 1 HugePages_Free: 4395 > node2/meminfo:Node 2 HugePages_Free: 4395 > node3/meminfo:Node 3 HugePages_Free: 3446 > > So sadly the picture is the same (something stole my HP on N3 and it's > PostgreSQL on it's own). After some time of investigating that ("who > stole my hugepage across whole OS"), I've just added MAP_POPULATE to > the mix of PG_MMAP_FLAGS and got this after start: > > root@swiatowid:/sys/devices/system/node# grep -r -i HugePages_Free node*/meminfo > node0/meminfo:Node 0 HugePages_Free: 0 > node1/meminfo:Node 1 HugePages_Free: 0 > node2/meminfo:Node 2 HugePages_Free: 0 > node3/meminfo:Node 3 HugePages_Free: 1 > > and then the SELECT to pg_buffercache_numa works fine(!). > > Another ways that I have found to eliminate that SIGBUS > a. Would be to throw much more HugePages (so that node does not run to > HugePages_Free), but that's not real option. > b. Then I've reminded myself that I could be running custom kernel > with experimental CONFIG_READ_ONLY_THP_FOR_FS (to reduce iTLB misses > tranparently with specially linked PG; will double check exact stuff > later), so I've thrown never into > /sys/kernel/mm/transparent_hugepage/enabled and defrag too (yes , > disabled THP) and with that -- drumroll -- that SELECT works. The very > same PG picture after startup (where earlier it would crash), now > after SELECT it looks like that: > > root@swiatowid:/sys/devices/system/node# grep -r -i HugePages_Free node*/meminfo > node0/meminfo:Node 0 HugePages_Free: 83 > node1/meminfo:Node 1 HugePages_Free: 0 > node2/meminfo:Node 2 HugePages_Free: 81 > node3/meminfo:Node 3 HugePages_Free: 82 > > Hope that helps a little. To me it sounds like THP used that memory > somehow and we've also wanted to use. With numa_interleave_ptr() that > wouldn't be a problem because probably it would something else > available, but not here as we indicated exact node. > >>> 0006e: >>> I'm seeking confirmation, but is this the issue we have discussed >>> on PgconfEU related to lack of detection of Mems_allowed, right? e.g. >>> $ numactl --membind="0,1" --cpunodebind="0,1" >>> /usr/pgsql19/bin/pg_ctl -D /path start >>> still shows 4 NUMA nodes used. Current patches use >>> numa_num_configured_nodes(), but it says 'This count includes any >>> nodes that are currently DISABLED'. So I was wondering if I could help >>> by migrating towards numa_num_task_nodes() / numa_get_mems_allowed()? >>> It's the same as You wrote earlier to Alexy? >>> >> >> If "mems_allowed" refers to nodes allowing memory allocation, then yes, >> this would be one way to get into that issue. Oh, is this what happened >> in 0006d? > > OK, thanks for confirmation. No, 0006d was about normal numactl run, > without --membind. > I didn't have time to look into all this info about mappings, io_uring yet, so no response from me. >> I did get a couple of "operation canceled" failures, but only on fairly >> old kernel versions (6.1 which came as default with the VM). > > OK, I'll try to see that later too. > > btw QQ regarding partitioned clockwise as I had thought: does this > opens a road towards multiple BGwriters? (outside of this > $thread/v1/PoC) > I don't think the clocksweep partitioning is required for multiple bgwriters, but it might make it easier. regards -- Tomas Vondra -
Re: Adding basic NUMA awareness
Jakub Wartak <jakub.wartak@enterprisedb.com> — 2025-11-17T09:23:50Z
On Tue, Nov 11, 2025 at 12:52 PM Tomas Vondra <tomas@vondra.me> wrote: > > Hi, > > here's a rebased patch series, fixing most of the smaller issues from > v20251101, and making cfbot happy (hopefully). Hi Tomas, > >>> 0007b: pg_buffercache_pgproc -- nitpick, but maybe it would be better > >>> called pg_shm_pgproc? > >>> > >> > >> Right. It does not belong to pg_buffercache at all, I just added it > >> there because I've been messing with that code already. > > > > Please keep them in for at least for some time (perhaps standalone > > patch marked as not intended to be commited would work?). I find the > > view extermely useful as it will allow us pinpointing local-vs-remote > > NUMA fetches (we need to know the addres). > > > > Are you referring to the _pgproc view specifically, or also to the view > with buffer partitions? I don't intend to remove the view for shared > buffers, that's indeed useful. Both, even the _pgproc. > Hmmm, ok. Will check. But maybe let's not focus too much on the PGPROC > partitioning, I don't think that's likely to go into 19. Oh ok. > >>> 0006d: I've got one SIGBUS during a call to select > >>> pg_buffercache_numa_pages(); and it looks like that memory accessed is > >>> simply not mapped? (bug) [..] > I didn't have time to look into all this info about mappings, io_uring > yet, so no response from me. > Ok, so the proper HP + SIGBUS explanation: Appologies, earlier I wrote that disabling THP does workaround this, but I've probably made an error there and used wrong binary back there (with MAP_POPULATE in PG_MMAP_FLAGS), so please ignore that. 1. Before starting PG, with shared_buffers=32GB, huge_pages=on (2MB ones), vm.nr_hugepages=17715, 4 NUMA nodes, kernel 6.14.x, max_connections=10k, wal_buffers=1GB: node0/hugepages/hugepages-2048kB/free_hugepages:4429 node1/hugepages/hugepages-2048kB/free_hugepages:4429 node2/hugepages/hugepages-2048kB/free_hugepages:4429 node3/hugepages/hugepages-2048kB/free_hugepages:4428 2. Just startup the PG with the older NUMA patchset 20251101. There will be deficit across NUMA nodes right after startup, mostly one node NUMA will allocate much more: node0/hugepages/hugepages-2048kB/free_hugepages:4397 node1/hugepages/hugepages-2048kB/free_hugepages:3453 node2/hugepages/hugepages-2048kB/free_hugepages:4397 node3/hugepages/hugepages-2048kB/free_hugepages:4396 3. Check layout of NUMA maps for postmaster PID 7fc9cb200000 default file=/anon_hugepage\040(deleted) huge dirty=517 mapmax=8 N1=517 kernelpagesize_kB=2048 [!!!] 7fca0d600000 bind:0 file=/anon_hugepage\040(deleted) huge dirty=32 mapmax=2 N0=32 kernelpagesize_kB=2048 7fca11600000 bind:1 file=/anon_hugepage\040(deleted) huge dirty=32 mapmax=2 N1=32 kernelpagesize_kB=2048 7fca15600000 bind:2 file=/anon_hugepage\040(deleted) huge dirty=32 mapmax=2 N2=32 kernelpagesize_kB=2048 7fca19600000 bind:3 file=/anon_hugepage\040(deleted) huge dirty=32 mapmax=2 N3=32 kernelpagesize_kB=2048 7fca1d600000 default file=/anon_hugepage\040(deleted) huge 7fca1d800000 bind:0 file=/anon_hugepage\040(deleted) huge 7fcc1d800000 bind:1 file=/anon_hugepage\040(deleted) huge 7fce1d800000 bind:2 file=/anon_hugepage\040(deleted) huge 7fd01d800000 bind:3 file=/anon_hugepage\040(deleted) huge 7fd21d800000 default file=/anon_hugepage\040(deleted) huge dirty=425 mapmax=8 N1=425 kernelpagesize_kB=2048 [!!!] So your patch doesn't do anything special for anything other than Buffer Blocks and PGPROC in the above picture, so the the default mmap() just keeps on with "default" NUMA policy which takes per above (517+425) * 2MB = ~1884 MB of really used memory as per N1 entires. PG does touch those regions on startup, but it doesnt really touch Buffer Blocks. Anyway, this causes the missing amount of free huge pages on the N1 (generates pressure on this Node 1). So as it stands, the patchset is missing some form balancing to use equal memory across nodes: - each node to be forced to get certain amount of BufferBlocks/NUMA nodes blocks - yet we do nothing and leave at the "defaults" the others regions (e..g $SegHDR (start of shm) .. first Buffers Block), as those are placed on the current node (due default policy), which in causes turns this memory overallocation imbalance (so in the example N1 will get Buffer Blocks + everything else, but that only happens on real access not during mmap() due to lazy/first touch policy) Currently, any launch of anything that touches imbalanced NUMA node memory with deficit (N1 above) - use of pg_shm_allocations, pg_buffercache - it will cause stress there and end up in SIGBUS. This looks by design on Linux kernel side: exc:page_fault() -> do_user_addr_fault() -> do_sigbus() AKA force_sig_fault(). But, if I hack pg to hack do interleave (or just numactl --interleave=all ... ) to effectivley interleave those 3 "default" regions instead, so I'll get "interleave" like that: 7fb2dd000000 interleave:0-3 file=/anon_hugepage\040(deleted) huge dirty=517 mapmax=8 N0=129 N1=132 N2=128 N3=128 kernelpagesize_kB=2048 7fb31f400000 bind:0 file=/anon_hugepage\040(deleted) huge dirty=32 mapmax=2 N0=32 kernelpagesize_kB=2048 7fb323400000 bind:1 file=/anon_hugepage\040(deleted) huge dirty=32 mapmax=2 N1=32 kernelpagesize_kB=2048 7fb327400000 bind:2 file=/anon_hugepage\040(deleted) huge dirty=32 mapmax=2 N2=32 kernelpagesize_kB=2048 7fb32b400000 bind:3 file=/anon_hugepage\040(deleted) huge dirty=32 mapmax=2 N3=32 kernelpagesize_kB=2048 7fb32f400000 interleave:0-3 file=/anon_hugepage\040(deleted) huge 7fb32f600000 bind:0 file=/anon_hugepage\040(deleted) huge 7fb52f600000 bind:1 file=/anon_hugepage\040(deleted) huge 7fb72f600000 bind:2 file=/anon_hugepage\040(deleted) huge 7fb92f600000 bind:3 file=/anon_hugepage\040(deleted) huge 7fbb2f600000 interleave:0-3 file=/anon_hugepage\040(deleted) huge dirty=425 N0=106 N1=106 N2=105 N3=108 kernelpagesize_kB=2048 then even after fully touching everything (via select to pg_shm_allocations), it'll run, I'll get much better balance, and wont have SIGBUS issues: node0/hugepages/hugepages-2048kB/free_hugepages:23 node1/hugepages/hugepages-2048kB/free_hugepages:23 node2/hugepages/hugepages-2048kB/free_hugepages:23 node3/hugepages/hugepages-2048kB/free_hugepages:22 This somehow demonstrates that enough free memory is out there, it's just imbalance that causes SIGBUS. I hope this somehow hopefully answers one of Your's main questions as per in the very first messages what we should do with remaining shared_buffer members. I would like to hear your thoughts on this, before I start benchmarking this for real as I didnt want to bench it yet, as such interleaving could alter the the test results. Other things I've noticed: - smaps Size: && Shared_Hugetlb: reporting are a lie and are showing really touched memory, not assigned memory - same goes for procfs's numa_maps, ignore the N[0-3] sizes, it's only "really used", not assigned - the best is just to manually calculate size from pointers/address range itself -J.
-
Re: Adding basic NUMA awareness
Tomas Vondra <tomas@vondra.me> — 2025-11-17T17:28:34Z
On 11/17/25 10:23, Jakub Wartak wrote: > On Tue, Nov 11, 2025 at 12:52 PM Tomas Vondra <tomas@vondra.me> wrote: >> >> Hi, >> >> here's a rebased patch series, fixing most of the smaller issues from >> v20251101, and making cfbot happy (hopefully). > > Hi Tomas, > >>>>> 0007b: pg_buffercache_pgproc -- nitpick, but maybe it would be better >>>>> called pg_shm_pgproc? >>>>> >>>> >>>> Right. It does not belong to pg_buffercache at all, I just added it >>>> there because I've been messing with that code already. >>> >>> Please keep them in for at least for some time (perhaps standalone >>> patch marked as not intended to be commited would work?). I find the >>> view extermely useful as it will allow us pinpointing local-vs-remote >>> NUMA fetches (we need to know the addres). >>> >> >> Are you referring to the _pgproc view specifically, or also to the view >> with buffer partitions? I don't intend to remove the view for shared >> buffers, that's indeed useful. > > Both, even the _pgproc. > > >> Hmmm, ok. Will check. But maybe let's not focus too much on the PGPROC >> partitioning, I don't think that's likely to go into 19. > > Oh ok. > >>>>> 0006d: I've got one SIGBUS during a call to select >>>>> pg_buffercache_numa_pages(); and it looks like that memory accessed is >>>>> simply not mapped? (bug) > [..] >> I didn't have time to look into all this info about mappings, io_uring >> yet, so no response from me. >> > > Ok, so the proper HP + SIGBUS explanation: > > Appologies, earlier I wrote that disabling THP does workaround this, > but I've probably made an error there and used wrong binary back there > (with MAP_POPULATE in PG_MMAP_FLAGS), so please ignore that. > > 1. Before starting PG, with shared_buffers=32GB, huge_pages=on (2MB > ones), vm.nr_hugepages=17715, 4 NUMA nodes, kernel 6.14.x, > max_connections=10k, wal_buffers=1GB: > > node0/hugepages/hugepages-2048kB/free_hugepages:4429 > node1/hugepages/hugepages-2048kB/free_hugepages:4429 > node2/hugepages/hugepages-2048kB/free_hugepages:4429 > node3/hugepages/hugepages-2048kB/free_hugepages:4428 > > 2. Just startup the PG with the older NUMA patchset 20251101. There > will be deficit across NUMA nodes right after startup, mostly one node > NUMA will allocate much more: > > node0/hugepages/hugepages-2048kB/free_hugepages:4397 > node1/hugepages/hugepages-2048kB/free_hugepages:3453 > node2/hugepages/hugepages-2048kB/free_hugepages:4397 > node3/hugepages/hugepages-2048kB/free_hugepages:4396 > > 3. Check layout of NUMA maps for postmaster PID > > 7fc9cb200000 default file=/anon_hugepage\040(deleted) huge dirty=517 > mapmax=8 N1=517 kernelpagesize_kB=2048 [!!!] > 7fca0d600000 bind:0 file=/anon_hugepage\040(deleted) huge dirty=32 > mapmax=2 N0=32 kernelpagesize_kB=2048 > 7fca11600000 bind:1 file=/anon_hugepage\040(deleted) huge dirty=32 > mapmax=2 N1=32 kernelpagesize_kB=2048 > 7fca15600000 bind:2 file=/anon_hugepage\040(deleted) huge dirty=32 > mapmax=2 N2=32 kernelpagesize_kB=2048 > 7fca19600000 bind:3 file=/anon_hugepage\040(deleted) huge dirty=32 > mapmax=2 N3=32 kernelpagesize_kB=2048 > 7fca1d600000 default file=/anon_hugepage\040(deleted) huge > 7fca1d800000 bind:0 file=/anon_hugepage\040(deleted) huge > 7fcc1d800000 bind:1 file=/anon_hugepage\040(deleted) huge > 7fce1d800000 bind:2 file=/anon_hugepage\040(deleted) huge > 7fd01d800000 bind:3 file=/anon_hugepage\040(deleted) huge > 7fd21d800000 default file=/anon_hugepage\040(deleted) huge dirty=425 > mapmax=8 N1=425 kernelpagesize_kB=2048 [!!!] > > So your patch doesn't do anything special for anything other than > Buffer Blocks and PGPROC in the above picture, so the the default > mmap() just keeps on with "default" NUMA policy which takes per above > (517+425) * 2MB = ~1884 MB of really used memory as per N1 entires. PG > does touch those regions on startup, but it doesnt really touch Buffer > Blocks. Anyway, this causes the missing amount of free huge pages on > the N1 (generates pressure on this Node 1). > > So as it stands, the patchset is missing some form balancing to use > equal memory across nodes: > - each node to be forced to get certain amount of BufferBlocks/NUMA nodes blocks > - yet we do nothing and leave at the "defaults" the others regions > (e..g $SegHDR (start of shm) .. first Buffers Block), as those are > placed on the current node (due default policy), which in causes turns > this memory overallocation imbalance (so in the example N1 will get > Buffer Blocks + everything else, but that only happens on real access > not during mmap() due to lazy/first touch policy) > > Currently, any launch of anything that touches imbalanced NUMA node > memory with deficit (N1 above) - use of pg_shm_allocations, > pg_buffercache - it will cause stress there and end up in SIGBUS. > This looks by design on Linux kernel side: exc:page_fault() -> > do_user_addr_fault() -> do_sigbus() AKA force_sig_fault(). But, if I > hack pg to hack do interleave (or just numactl --interleave=all ... ) > to effectivley interleave those 3 "default" regions instead, so I'll > get "interleave" like that: > > 7fb2dd000000 interleave:0-3 file=/anon_hugepage\040(deleted) huge > dirty=517 mapmax=8 N0=129 N1=132 N2=128 N3=128 kernelpagesize_kB=2048 > 7fb31f400000 bind:0 file=/anon_hugepage\040(deleted) huge dirty=32 > mapmax=2 N0=32 kernelpagesize_kB=2048 > 7fb323400000 bind:1 file=/anon_hugepage\040(deleted) huge dirty=32 > mapmax=2 N1=32 kernelpagesize_kB=2048 > 7fb327400000 bind:2 file=/anon_hugepage\040(deleted) huge dirty=32 > mapmax=2 N2=32 kernelpagesize_kB=2048 > 7fb32b400000 bind:3 file=/anon_hugepage\040(deleted) huge dirty=32 > mapmax=2 N3=32 kernelpagesize_kB=2048 > 7fb32f400000 interleave:0-3 file=/anon_hugepage\040(deleted) huge > 7fb32f600000 bind:0 file=/anon_hugepage\040(deleted) huge > 7fb52f600000 bind:1 file=/anon_hugepage\040(deleted) huge > 7fb72f600000 bind:2 file=/anon_hugepage\040(deleted) huge > 7fb92f600000 bind:3 file=/anon_hugepage\040(deleted) huge > 7fbb2f600000 interleave:0-3 file=/anon_hugepage\040(deleted) huge > dirty=425 N0=106 N1=106 N2=105 N3=108 kernelpagesize_kB=2048 > > then even after fully touching everything (via select to > pg_shm_allocations), it'll run, I'll get much better balance, and wont > have SIGBUS issues: > > node0/hugepages/hugepages-2048kB/free_hugepages:23 > node1/hugepages/hugepages-2048kB/free_hugepages:23 > node2/hugepages/hugepages-2048kB/free_hugepages:23 > node3/hugepages/hugepages-2048kB/free_hugepages:22 > > This somehow demonstrates that enough free memory is out there, it's > just imbalance that causes SIGBUS. I hope this somehow hopefully > answers one of Your's main questions as per in the very first messages > what we should do with remaining shared_buffer members. I would like > to hear your thoughts on this, before I start benchmarking this for > real as I didnt want to bench it yet, as such interleaving could alter > the the test results. > Thanks for investigating this. If I understand the findings correctly, it agrees with my imprecise explanation in [1], right? There I said: > ... > You may ask why the per-node limit is too low. We still need just > shared_memory_size_in_huge_pages, right? And if we were partitioning > the whole memory segment, that'd be true. But we only to that for > shared buffers, and there's a lot of other shared memory - could be > 1-2GB or so, depending on the configuration. > > And this gets placed on one of the nodes, and it counts against the > limit on that particular node. And so it doesn't have enough huge > pages to back the partition of shared buffers. > ... Which I think is mostly the same thing you're saying, and you have the maps to support it. In any case, I think setting "interleave" as the default policy, and then overriding it for the areas we partition explicitly (buffers, pgproc), seems like the right solution. The only other solution would be balance it ourselves, but how is that different from interleaving? So I think this makes sense, and you can do --interleave=all for the benchmark. [1] https://www.postgresql.org/message-id/71a46484-053c-4b81-ba32-ddac050a8b5d%40vondra.me I suppose we may need to adjust shared_memory_size_in_huge_pages, because the interleave followed by explicit partitioning may still leave behind a bit of imbalance. It should be only a couple pages, but I haven't done the math yet. regards -- Tomas Vondra
-
Re: Adding basic NUMA awareness
Tomas Vondra <tomas@vondra.me> — 2025-11-21T18:49:25Z
Hi, Here's an updated version of the patch series. It fixes a bunch of issues in pg_buffercache_pages.c - duplicate attnums and a incorrect array length. The main change is in 0006 - it sets the default allocation policy for shmem to interleaving, before doing the explicit partitioning for shared buffers. It does it by calling numa_set_membind before the mmap(), and then numa_interleave_memory() on the allocated shmem. It does this to allow using MAP_POPULATE - but that's commented out by default. This does seem to solve the SIGBUS failures for me. I still think there might be a small chance of hitting that, because of locating an extra "boundary" page on one of the nodes. But it should be solvable by reserving a couple more pages. Jakub, what do you think? regards -- Tomas Vondra
-
Re: Adding basic NUMA awareness
Jakub Wartak <jakub.wartak@enterprisedb.com> — 2025-11-25T14:12:46Z
Hi Tomas! [..] > Which I think is mostly the same thing you're saying, and you have the maps to support it. Right, the thread is kind of long, you were right back then, well but at least we've got a solid explanation with data. > Here's an updated version of the patch series. Just for double confirmation, I've used those ones (v20251121*) and they indeed interleaved parts of shm memory. > It fixes a bunch of issues in pg_buffercache_pages.c - duplicate attnums > and a incorrect array length. You'll need to rebase again, pg_buffercache_numa got updated again on Monday and clashes with 0006. > The main change is in 0006 - it sets the default allocation policy for > shmem to interleaving, before doing the explicit partitioning for shared > buffers. It does it by calling numa_set_membind before the mmap(), and > then numa_interleave_memory() on the allocated shmem. It does this to > allow using MAP_POPULATE - but that's commented out by default. > > This does seem to solve the SIGBUS failures for me. I still think there > might be a small chance of hitting that, because of locating an extra > "boundary" page on one of the nodes. But it should be solvable by > reserving a couple more pages. I can confirm, never got any SIGBUS during the later described benchmarks, so it's much better now. > Jakub, what do you think? On one side not using MAP_POPULATE gives instant startup, but on the other it gives much better predictability latencies especially fresh after starting up (this might matter to folks who like to benchmark -- us?, but initially I've just used it as a simple hack to touch memory). I would be wary of using MAP_POPULATE with s_b when it would be sized in hundreths of GBs, it could take minutes in startup, which would be terrible if someone would hit SIGSEGV on production and expect restart_after_crash=true to save him. I mean WAL redo crash would be terrible, but that would be terrible * 2. Also pretty long-term with DIO, we'll get much bigger s_b anyway (hopefully), so it would hurt even more, so I think that would be a bad path(?) I've benchmarked the thing in two scenarios (readonly pgbench < s_b size across variations of code and connections and 2nd one with seqconcurrrentscans) in solid stable conditions: 4s32c64t == 4 NUMA nodes, 128GB RAM, 31GB shared_buffers dbsize ~29GB, 6.14.x, no idle CPU states, no turbo boost, and so on, literally great home heater when there's -3C outside!) The data is baseline "100%" for master along with HP on/off (so it's showing diff % from respective HP setting): scenario I: pgbench -S connections branch HP 1 8 64 128 1024 master off 100.00% 100.00% 100.00% 100.00% 100.00% master on 100.00% 100.00% 100.00% 100.00% 100.00% numa16 off 99.13% 100.46% 99.66% 99.44% 89.60% numa16 on 101.80% 100.89% 99.36% 99.89% 93.43% numa4 off 96.82% 100.61% 99.37% 99.92% 94.41% numa4 on 101.83% 100.61% 99.35% 99.69% 101.48% pgproc16 off 99.13% 100.84% 99.38% 99.85% 91.15% pgproc16 on 101.72% 101.40% 99.72% 100.14% 95.20% pgproc4 off 98.63% 101.44% 100.05% 100.14% 90.97% pgproc4 on 101.05% 101.46% 99.92% 100.31% 97.60% sweep16 off 99.53% 101.14% 100.71% 100.75% 101.52% sweep16 on 97.63% 102.49% 100.42% 100.75% 105.56% sweep4 off 99.43% 101.59% 100.06% 100.45% 104.63% sweep4 on 97.69% 101.59% 100.70% 100.69% 104.70% I would consider everything +/- 3% as noise (technically each branch was a different compilation/ELF binary, as changing this #define required to do so to get 4 vs 16; please see attached script). I miss the explanation why without HP it deteriorates so much with for c=1024 with the patches. scenario II: pgbench -f seqconcurrscans.pgb; 64 partitions from pgbench --partitions=64 -i -s 2000 [~29GB] being hammered in modulo without PQ by: \set num (:client_id % 8) + 1 select sum(octet_length(filler)) from pgbench_accounts_:num; connections branch HP 1 8 64 128 master off 100.00% 100.00% 100.00% 100.00% master on 100.00% 100.00% 100.00% 100.00% numa16 off 115.62% 108.87% 101.08% 111.56% numa16 on 107.68% 104.90% 102.98% 105.51% numa4 off 113.55% 111.41% 101.45% 113.10% numa4 on 107.90% 106.60% 103.68% 106.98% pgproc16 off 111.70% 108.27% 98.69% 109.36% pgproc16 on 106.98% 100.69% 101.98% 103.42% pgproc4 off 112.41% 106.15% 100.03% 112.03% pgproc4 on 106.73% 105.77% 103.74% 101.13% sweep16 off 100.63% 100.38% 98.41% 103.46% sweep16 on 109.03% 99.15% 101.17% 99.19% sweep4 off 102.04% 101.16% 101.71% 91.86% sweep4 on 108.33% 101.69% 97.14% 100.92% The benefit varies with like +3-10% depending on connection count. Quite frankly I was expecting a little bit more, especially after re-reading [1]. Maybe you preloaded it there using pg_prewarm? (here I've randomly warmed it using pgbench). Probably it's something with my test, I'll take yet another look hopefully soon. The good thing is that it never crashed and I haven't seen any errors like "Bad address" probably related to AIO as you saw in [1], perhaps I wasn't using uring. 0007 (PROCs) still complains with "mbind: Invalid argument" (aligment issue) -J. [1] - https://www.postgresql.org/message-id/e4d7e6fc-b5c5-4288-991c-56219db2edd5%40vondra.me -
Re: Adding basic NUMA awareness
Tomas Vondra <tomas@vondra.me> — 2025-11-26T16:19:12Z
On 11/25/25 15:12, Jakub Wartak wrote: > Hi Tomas! > > [..] >> Which I think is mostly the same thing you're saying, and you have the maps to support it. > > Right, the thread is kind of long, you were right back then, well but > at least we've got a solid explanation with data. > >> Here's an updated version of the patch series. > > Just for double confirmation, I've used those ones (v20251121*) and > they indeed interleaved parts of shm memory. > >> It fixes a bunch of issues in pg_buffercache_pages.c - duplicate attnums >> and a incorrect array length. > > You'll need to rebase again, pg_buffercache_numa got updated again on > Monday and clashes with 0006. > Rebased patch series attached. >> The main change is in 0006 - it sets the default allocation policy for >> shmem to interleaving, before doing the explicit partitioning for shared >> buffers. It does it by calling numa_set_membind before the mmap(), and >> then numa_interleave_memory() on the allocated shmem. It does this to >> allow using MAP_POPULATE - but that's commented out by default. >> >> This does seem to solve the SIGBUS failures for me. I still think there >> might be a small chance of hitting that, because of locating an extra >> "boundary" page on one of the nodes. But it should be solvable by >> reserving a couple more pages. > > I can confirm, never got any SIGBUS during the later described > benchmarks, so it's much better now. > Good! >> Jakub, what do you think? > > On one side not using MAP_POPULATE gives instant startup, but on the > other it gives much better predictability latencies especially fresh > after starting up (this might matter to folks who like to benchmark -- > us?, but initially I've just used it as a simple hack to touch > memory). I would be wary of using MAP_POPULATE with s_b when it would > be sized in hundreths of GBs, it could take minutes in startup, which > would be terrible if someone would hit SIGSEGV on production and > expect restart_after_crash=true to save him. I mean WAL redo crash > would be terrible, but that would be terrible * 2. Also pretty > long-term with DIO, we'll get much bigger s_b anyway (hopefully), so > it would hurt even more, so I think that would be a bad path(?) > I think the MAP_POPULATE should be optional, enabled by GUC. > I've benchmarked the thing in two scenarios (readonly pgbench < s_b > size across variations of code and connections and 2nd one with > seqconcurrrentscans) in solid stable conditions: 4s32c64t == 4 NUMA > nodes, 128GB RAM, 31GB shared_buffers dbsize ~29GB, 6.14.x, no idle > CPU states, no turbo boost, and so on, literally great home heater > when there's -3C outside!) > > The data is baseline "100%" for master along with HP on/off (so it's > showing diff % from respective HP setting): > > scenario I: pgbench -S > > connections > branch HP 1 8 64 128 1024 > master off 100.00% 100.00% 100.00% 100.00% 100.00% > master on 100.00% 100.00% 100.00% 100.00% 100.00% > numa16 off 99.13% 100.46% 99.66% 99.44% 89.60% > numa16 on 101.80% 100.89% 99.36% 99.89% 93.43% > numa4 off 96.82% 100.61% 99.37% 99.92% 94.41% > numa4 on 101.83% 100.61% 99.35% 99.69% 101.48% > pgproc16 off 99.13% 100.84% 99.38% 99.85% 91.15% > pgproc16 on 101.72% 101.40% 99.72% 100.14% 95.20% > pgproc4 off 98.63% 101.44% 100.05% 100.14% 90.97% > pgproc4 on 101.05% 101.46% 99.92% 100.31% 97.60% > sweep16 off 99.53% 101.14% 100.71% 100.75% 101.52% > sweep16 on 97.63% 102.49% 100.42% 100.75% 105.56% > sweep4 off 99.43% 101.59% 100.06% 100.45% 104.63% > sweep4 on 97.69% 101.59% 100.70% 100.69% 104.70% > > I would consider everything +/- 3% as noise (technically each branch > was a different compilation/ELF binary, as changing this #define > required to do so to get 4 vs 16; please see attached script). I miss > the explanation why without HP it deteriorates so much with for c=1024 > with the patches. I wouldn't expect a big difference for "pgbench -S". That workload has so much other fairly expensive stuff (e.g. initializing index scans etc.), the cost of buffer replacement is going to be fairly limited. The regressions for numa/pgproc patches with 1024 clients are annoying, but how realistic is such scenario? With 32/64 CPUs, having 1024 active connections is a substantial overload. If we can fix this, great. But I think such regression may be OK if we get benefits for reasonable setups (with fewer clients). I don't know why it's happening, though. I haven't been testing cases with so many clients (compared to number of CPUs). > > scenario II: pgbench -f seqconcurrscans.pgb; 64 partitions from > pgbench --partitions=64 -i -s 2000 [~29GB] being hammered in modulo > without PQ by: > \set num (:client_id % 8) + 1 > select sum(octet_length(filler)) from pgbench_accounts_:num; > > connections > branch HP 1 8 64 128 > master off 100.00% 100.00% 100.00% 100.00% > master on 100.00% 100.00% 100.00% 100.00% > numa16 off 115.62% 108.87% 101.08% 111.56% > numa16 on 107.68% 104.90% 102.98% 105.51% > numa4 off 113.55% 111.41% 101.45% 113.10% > numa4 on 107.90% 106.60% 103.68% 106.98% > pgproc16 off 111.70% 108.27% 98.69% 109.36% > pgproc16 on 106.98% 100.69% 101.98% 103.42% > pgproc4 off 112.41% 106.15% 100.03% 112.03% > pgproc4 on 106.73% 105.77% 103.74% 101.13% > sweep16 off 100.63% 100.38% 98.41% 103.46% > sweep16 on 109.03% 99.15% 101.17% 99.19% > sweep4 off 102.04% 101.16% 101.71% 91.86% > sweep4 on 108.33% 101.69% 97.14% 100.92% > > The benefit varies with like +3-10% depending on connection count. > Quite frankly I was expecting a little bit more, especially after > re-reading [1]. Maybe you preloaded it there using pg_prewarm? (here > I've randomly warmed it using pgbench). Probably it's something with > my test, I'll take yet another look hopefully soon. The good thing is > that it never crashed and I haven't seen any errors like "Bad address" > probably related to AIO as you saw in [1], perhaps I wasn't using > uring. > Hmmm. I'd have expected better results for this workload. So I tried re-running my seqscan benchmark on the 176-core instance, and I got this: clients master 0001 0002 0003 0004 0005 0006 0007 ----------------------------------------------------------------- 64 44 43 35 40 53 53 46 45 96 55 54 42 47 57 58 53 53 128 59 59 46 50 58 58 57 60 clients 0001 0002 0003 0004 0005 0006 0007 -------------------------------------------------------- 64 98% 79% 92% 122% 122% 105% 104% 96 99% 76% 86% 104% 105% 97% 97% 128 99% 77% 84% 98% 98% 97% 101% I did the benchmark for individual parts of the patch series. There's a clear (~20%) speedup for 0005, but 0006 and 0007 make it go away. The 0002/0003 regress it quite a bit. And with 128 clients there's no improvement at all. This was with the default number of partitions (i.e. 4). If I increase the number to 16, I get this: clients master 0001 0002 0003 0004 0005 0006 0007 ----------------------------------------------------------------- 64 44 43 69 82 87 87 78 79 96 55 54 65 85 91 91 86 86 128 59 59 66 77 83 83 82 86 clients 0001 0002 0003 0004 0005 0006 0007 -------------------------------------------------------- 64 99% 158% 189% 199% 199% 180% 180% 96 100% 119% 156% 167% 167% 157% 158% 128 99% 112% 130% 140% 140% 139% 145% And with 32 partitions, I get this: clients master 0001 0002 0003 0004 0005 0006 0007 ----------------------------------------------------------------- 64 44 44 88 91 90 90 84 84 96 55 54 89 93 93 92 90 91 128 59 59 85 84 86 85 88 87 clients 0001 0002 0003 0004 0005 0006 0007 -------------------------------------------------------- 64 100% 202% 208% 207% 207% 193% 193% 96 100% 163% 169% 171% 168% 165% 166% 128 99% 144% 142% 146% 144% 149% 146% Those are clearly much better results, so I guess the default number of partitions may be too low. What bothers me is that this seems like a very narrow benchmark. I mean, few systems are doing concurrent seqscans putting this much pressure on buffer replacement. And once the plans start to do other stuff, the contention on clock sweep seems to go down substantially (as shown by the read-only pgbench). So the question is - is this really worth it? > 0007 (PROCs) still complains with "mbind: Invalid argument" (aligment issue) > Should be fixed by the attached patches. The 0006 patch has an issue with mbind too, but it was visible only when the buffers were not a nice multiple of memory pages (and multiples of 1GB are fine). This also moves the memset() until after placing the PGPROC partitions to different NUMA nodes. The results above are from v20251121. I'll rerun the tests with the nw version of the patches. But it can only change the 0006/0007 results, of course. The 0001-0005 are the same. regards -- Tomas Vondra -
Re: Adding basic NUMA awareness
Jakub Wartak <jakub.wartak@enterprisedb.com> — 2025-12-02T12:26:33Z
On Wed, Nov 26, 2025 at 5:19 PM Tomas Vondra <tomas@vondra.me> wrote: > Rebased patch series attached. Thanks. BTW still with the old patchset series, One additional thing that I've found out related to interleave is that in CreateAnonymousSegment() with the default check_debug='', we still issue numa_interleave_memory(ptr..). It should be optional (this also affects earlier calls too). Tiny patch attached. > I think the MAP_POPULATE should be optional, enabled by GUC. OK, but you mean it's a new option to debug_numa, right? (not some separate) so debug_numa='prefault' then? > > I would consider everything +/- 3% as noise (technically each branch > > was a different compilation/ELF binary, as changing this #define > > required to do so to get 4 vs 16; please see attached script). I miss > > the explanation why without HP it deteriorates so much with for c=1024 > > with the patches. > > I wouldn't expect a big difference for "pgbench -S". That workload has > so much other fairly expensive stuff (e.g. initializing index scans > etc.), the cost of buffer replacement is going to be fairly limited. Right. OK, so I've got the seqconcurrentscans comparison done right, that is when prewarmed and not naturally filled: @master, 29GB/s mem bandwidth latency average = 1255.572 ms latency stddev = 417.162 ms tps = 50.451925 (without initial connection time) @v20251121 patchset, 41GB/s (~10GB/s per socket) latency average = 719.931 ms latency stddev = 14.874 ms tps = 88.362091 (without initial connection time) The main PMC difference seems to be much lower "backend cycles idle" (51% master and vs 31% for the NUMA debug_numa="buffers,procs", so less is waiting on memory, thus it gets that speedup and better IPC). Anyway, the biggest gripe right now (at least to me) is reliable benchmarking. Below runs are all apples and oranges comparisons (they measure different stuff although looks the same initially) - restart and just select pg_shmem_allocations_numa or prewarm puts everything into 1 NUMA node with check_numa='', because of prefaulting happening during select-view case - restart and pgbench -i -s XX (same issue as above) then pgbench - you get the same, everything on potential one NUMA node (because pgbench prefaults just on one) - restart and pgbench -c 64.. with debug_numa='' (off) MIGHT get random NUMA layout, how's that is supposed to be deterministic? at least with debug_numa='buffers' you get determinism.. - the shared_buffers size vs size of dataset read, the moment you start doing something CPU intensive (or like calling syscalls just for VFS cache), the benefit seems to disappear at least on my hardware Anyway, depending on the scenario I could get varied results like 34tps .. 88tps here. The debug_numa='buffers,..' gives just assurance of the proper layout of shared memory is there (one could even argue that such performance deviations across runs are bug ;)). > The regressions for numa/pgproc patches with 1024 clients are annoying, > but how realistic is such scenario? With 32/64 CPUs, having 1024 active > connections is a substantial overload. If we can fix this, great. But I > think such regression may be OK if we get benefits for reasonable setups > (with fewer clients). > > I don't know why it's happening, though. I haven't been testing cases > with so many clients (compared to the number of CPUs). The only thing in my mind about deterioration of high-connection count (AKA -c 1024 scenario) with pgprocs, would be related to the question you raised in 0007 "Note: The scheduler may migrate the process to a different CPU/node later. Maybe we should consider pinning the process to the node?" I think the answer is yes, so to fetch MyProc based on sched_getcpu() and then maybe with additional numa_flags & new PROCS_PIN_NODE simply numa_run_on_node(node)? I've tried this: pgbench -c 1024 -j 64 -P 1 -T 30 -S -M prepared got: @numa-empty-debug_numa ~434k TPS, ~12k CPU migrations/second @numa+buffers+pgproc ~412k TPS, 7-8k CPU migrations/second @numa+buffers+pgproc+pinnode ~434k TPS, still with 7-8k CPU migrations/second (so same) but I've verified for the last one, with bpftrace on that tracepoint:sched:sched_migrate_task did not performed node-to-node process bounces anymore (it did for pgbench but not for postgres itself with this numa_run_on_node()) > > scenario II: pgbench -f seqconcurrscans.pgb; 64 partitions from > > pgbench --partitions=64 -i -s 2000 [~29GB] being hammered in modulo [..] > > Hmmm. I'd have expected better results for this workload. So I tried > re-running my seqscan benchmark on the 176-core instance, and I got this: [..] Thanks! > I did the benchmark for individual parts of the patch series. There's a > clear (~20%) speedup for 0005, but 0006 and 0007 make it go away. The > 0002/0003 regress it quite a bit. And with 128 clients there's no > improvement at all. [..] > Those are clearly much better results, so I guess the default number of > partitions may be too low. > > What bothers me is that this seems like a very narrow benchmark. I mean, > few systems are doing concurrent seqscans putting this much pressure on > buffer replacement. And once the plans start to do other stuff, the > contention on clock sweep seems to go down substantially (as shown by > the read-only pgbench). So the question is - is this really worth it? Are you thinking here about whole NUMA patchset or just clocksweep? I think multiple clocksweep are just not shining because other bottlenecks hammer the efficiency here. Andres talk about it exactly here https://youtu.be/V75KpACdl6E?t=1990 (He mentions out of order execution, I see btrees in reports as top#1). So maybe it's just too early to see the results of this optimization? As for classic readonly pgbench -S I still see roughly 1:8 local to remote (!) DRAM access (1 <-> 3 sockets) even with those patches, so potentially something could be improved in far future for sure (that would require some memaddr monitoring for most remote DRAM misses <-> pg inter-shm ptr mapping; think of pg_shmem_allocations_numa with local/remote counters or maybe just fallback to perf-c2c). To sum up, IMHO I understand this $thread's NUMA implementation as: - it's strictly a guard mechanism to get determinism (for most cases) -- it fixes "imbalance" - no performance boost for OLTP as such - for analytics it could be win (in-memory workloads; well PG is not fully built for this, but it could be one day/or already is with 3rd party TAMs and extensions), and: -- we can provide performance jump for seqconcurrentjobs or memory fitting workloads (patchset does this already). Note: I think PG will eventually get into such classes in the longer run, we are just ahead with NUMA, but PG is without proper vectorized executor stuff. -- we could further enhance PQ here: the leader and PQ workers would stick to the same NUMA node with some affinity (the earlier thread measurements for this [1] -- we could have session GUC to enable this for planned big PQ whole-NUMA SELECTs; this would be probably done close to dsm_impl_posix()) - new idea: we could allow exposing tables(spaces) into NUMA nodes or make it per-user toggle too while we are at it (imagine HTAP-like workloads: NUMA node #0 for OLTP, node #1 for analytics). Sounds cool and rather easy and has valid use, but dunno if that would be really useful? Way out of scope: - superlocking btress that Andres mentioned on his presentation -J. [1] - https://www.postgresql.org/message-id/attachment/178120/NUMA_pq_cpu_pinning_results.txt
-
Re: Adding basic NUMA awareness
Tomas Vondra <tomas@vondra.me> — 2025-12-08T20:02:27Z
Hi, I've spent the last couple days considering what to do about this patch series in this thread. The approach assumes partitioning shared memory in a NUMA-aware way has enough benefits to justify the extra complexity. I'm getting somewhat skeptical about that being a good trade off. We've been able to demonstrate some significant benefits of the patches. See for example the results from about a month ago [1], showing that in some cases the throughput almost doubles. But I'm skeptical about this for two reasons: * Most of the benefit comes from patches unrelated to NUMA. The initial patches partition clockweep, in a NUMA oblivious way. In fact, applying the NUMA patches often *reduces* the throughput. So if we're concerned about contention on the clocksweep hand, we could apply just these first patches. That way we wouldn't have to deal with huge pages. * Furthermore, I'm not quite sure clocksweep really is a bottleneck in realistic cases. The benchmark used in this thread does many concurrent sequential scans, on data that exceeds shared buffers / fits into RAM. Perhaps that happens, but I doubt it's all that common. I've been unable to demonstrate any benefits on other workloads, even if there's a lot of buffer misses / reads into shared buffers. As soon as the query starts doing something else, the clocksweep contention becomes a non-issue. Consider for example read-only pgbench with database much larger than shared buffers (but still within RAM). The cost of the index scans (and other nodes) seems to reduce the pressure on clocksweep. So I'm skeptical about clocksweep pressure being a serious issue, except for some very narrow benchmarks (like the concurrent seqscan test). And even if this happened for some realistic cases, partitioning the buffers in a NUMA-oblivious way seems to do the trick. When discussing this stuff off list, it was suggested this might help with the scenario Andres presented in [3], where the throughput improves a lot with multiple databases. I've not observed that in practice, and I don't think these patches really can help with that. That scenario is about buffer lock contention, not clocksweep contention. With a single database there's nothing to do - there's one contended page, located on a single node. There'll be contention, the no matter which node it ends up on. With multiple databases (or multiple root pages), it either happens to work by chance (if the buffers happen to be from different nodes), or it would require figuring out which buffers are busy, and place them on different nodes. But the patches did not even try to do anything like that. So it still was a matter of chance. That does not mean we don't need to worry about NUMA. There's still the issue of misbalancing the allocation - with memory coming from just one node, etc. Which is an issue because it "overloads" the memory subsystem on that particular node. But that can be solved simply by interleaving the shared memory segment. That sidesteps most of the complexity - it does not need to figure out how to partition buffers, does not need to worry about huge pages, etc. This is not a new idea - it's more or less what Jakub Wartak initially proposed in [2], before I hijacked the thread into the more complex approach. Attached is a tiny patch doing mostly what Jakub did, except that it does two things. First, it allows interleaving the shared memory on all relevant NUMA nodes (per numa_get_mems_allowed). Second, it allows populating all memory by setting MAP_POPULATE in mmap(). There's a new GUC to enable each of these. I think we should try this (much simpler) approach first, or something close to it. Sorry for dragging everyone into a much more complex approach, which now seems to be a dead end. regards [1] https://www.postgresql.org/message-id/e4d7e6fc-b5c5-4288-991c-56219db2edd5@vondra.me [2] https://www.postgresql.org/message-id/CAKZiRmw6i1W1AwXxa-Asrn8wrVcVH3TO715g_MCoowTS9rkGyw@mail.gmail.com [3] https://youtu.be/V75KpACdl6E?t=2120 -- Tomas Vondra
-
Re: Adding basic NUMA awareness
Andres Freund <andres@anarazel.de> — 2026-01-10T01:42:36Z
Hi, On 2025-12-08 21:02:27 +0100, Tomas Vondra wrote: > * Most of the benefit comes from patches unrelated to NUMA. The initial > patches partition clockweep, in a NUMA oblivious way. In fact, applying > the NUMA patches often *reduces* the throughput. So if we're concerned > about contention on the clocksweep hand, we could apply just these first > patches. That way we wouldn't have to deal with huge pages. > * Furthermore, I'm not quite sure clocksweep really is a bottleneck in > realistic cases. The benchmark used in this thread does many concurrent > sequential scans, on data that exceeds shared buffers / fits into RAM. > Perhaps that happens, but I doubt it's all that common. I think this misses that this isn't necessarily about peak throughput under concurrent contention. Consider this scenario: 1) shared buffers is already allocated from a kernel POV, i.e. pages reside on some numa node instead of having to be allocated on the first access 2) one backend does a scan of scan of a relation [largely] not in shared buffers Whether the buffers for the ringbuffer (if the relation is > NBuffers/4) or for the entire relation (if smaller) is allocated on the same node as the backend makes a quite substantial difference. I see about a 25% difference even on a small-ish numa system. Partitioned clocksweep makes it vastly more likely that data is on the local numa node. If you simulate different locality modes with numactl, I can see pretty drastic differences for the processing of individual queries, both with parallel and non-parallel processing. psql -Xq -c 'SELECT pg_buffercache_evict_all();' -c 'SELECT numa_node, sum(size) FROM pg_shmem_allocations_numa GROUP BY 1;' && perf stat --per-socket -M memory_bandwidth_read,memory_bandwidth_write -a psql -c 'SELECT sum(abalance) FROM pgbench_accounts;' membind 0, cpunodebind 1, max_parallel_workers_per_gather=0: S0 6 341,635,792 UNC_M_CAS_COUNT.WR # 4276.9 MB/s memory_bandwidth_write S0 20 5,116,381,542 duration_time S0 6 255,977,795 UNC_M_CAS_COUNT.RD # 3204.6 MB/s memory_bandwidth_read S0 20 5,116,391,355 duration_time S1 6 2,418,579 UNC_M_CAS_COUNT.WR # 30.3 MB/s memory_bandwidth_write S1 6 115,511,123 UNC_M_CAS_COUNT.RD # 1446.1 MB/s memory_bandwidth_read 5.112286670 seconds time elapsed membind 1, cpunodebind 1, max_parallel_workers_per_gather=0: S0 6 16,528,154 UNC_M_CAS_COUNT.WR # 248.1 MB/s memory_bandwidth_write S0 20 4,267,078,201 duration_time S0 6 40,327,670 UNC_M_CAS_COUNT.RD # 605.4 MB/s memory_bandwidth_read S0 20 4,267,088,762 duration_time S1 6 116,925,559 UNC_M_CAS_COUNT.WR # 1755.2 MB/s memory_bandwidth_write S1 6 244,251,242 UNC_M_CAS_COUNT.RD # 3666.5 MB/s memory_bandwidth_read 4.263442844 seconds time elapsed interleave 0,1, cpunodebind 1, max_parallel_workers_per_gather=0: S0 6 196,713,044 UNC_M_CAS_COUNT.WR # 2757.4 MB/s memory_bandwidth_write S0 20 4,569,805,767 duration_time S0 6 167,497,804 UNC_M_CAS_COUNT.RD # 2347.9 MB/s memory_bandwidth_read S0 20 4,569,816,439 duration_time S1 6 81,992,696 UNC_M_CAS_COUNT.WR # 1149.3 MB/s memory_bandwidth_write S1 6 192,265,269 UNC_M_CAS_COUNT.RD # 2695.1 MB/s memory_bandwidth_read 4.565722468 seconds time elapsed membind 0, cpunodebind 1, max_parallel_workers_per_gather=8: S0 6 336,538,518 UNC_M_CAS_COUNT.WR # 24130.2 MB/s memory_bandwidth_write S0 20 895,976,459 duration_time S0 6 238,663,716 UNC_M_CAS_COUNT.RD # 17112.4 MB/s memory_bandwidth_read S0 20 895,986,193 duration_time S1 6 2,594,371 UNC_M_CAS_COUNT.WR # 186.0 MB/s memory_bandwidth_write S1 6 113,981,673 UNC_M_CAS_COUNT.RD # 8172.6 MB/s memory_bandwidth_read 0.892594989 seconds time elapsed membind 1, cpunodebind 1, max_parallel_workers_per_gather=8: S0 6 3,492,673 UNC_M_CAS_COUNT.WR # 322.0 MB/s memory_bandwidth_write S0 20 698,175,650 duration_time S0 6 5,363,152 UNC_M_CAS_COUNT.RD # 494.4 MB/s memory_bandwidth_read S0 20 698,187,522 duration_time S1 6 117,181,190 UNC_M_CAS_COUNT.WR # 10802.4 MB/s memory_bandwidth_write S1 6 251,059,297 UNC_M_CAS_COUNT.RD # 23144.0 MB/s memory_bandwidth_read 0.694253637 seconds time elapsed interleave 0,1, cpunodebind 1, max_parallel_workers_per_gather=8: S0 6 170,352,086 UNC_M_CAS_COUNT.WR # 13767.3 MB/s memory_bandwidth_write S0 20 797,166,139 duration_time S0 6 121,646,666 UNC_M_CAS_COUNT.RD # 9831.1 MB/s memory_bandwidth_read S0 20 797,175,899 duration_time S1 6 60,099,863 UNC_M_CAS_COUNT.WR # 4857.1 MB/s memory_bandwidth_write S1 6 182,035,468 UNC_M_CAS_COUNT.RD # 14711.5 MB/s memory_bandwidth_read 0.791915733 seconds time elapsed You're never going to be quite as good when actually using both NUMA nodes, but at least simple workloads like the above should be able to get a lot closer to the good number from above than we currently are. Maybe the problem is that the patchset doesn't actually quite work right now? I checked out numa-20251111 and ran a query for a 1GB table in a 40GB s_b system: there's not much more locality with debug_numa=buffers, than without (roughly 55% on one node, 45% on the other). Making it not surprising that the results aren't great. > I've been unable to demonstrate any benefits on other workloads, even if > there's a lot of buffer misses / reads into shared buffers. As soon as > the query starts doing something else, the clocksweep contention becomes > a non-issue. Consider for example read-only pgbench with database much > larger than shared buffers (but still within RAM). The cost of the index > scans (and other nodes) seems to reduce the pressure on clocksweep. > > So I'm skeptical about clocksweep pressure being a serious issue, except > for some very narrow benchmarks (like the concurrent seqscan test). And > even if this happened for some realistic cases, partitioning the buffers > in a NUMA-oblivious way seems to do the trick. I think you're over-indexing on the contention aspect and under-indexing on the locality benefits. > When discussing this stuff off list, it was suggested this might help > with the scenario Andres presented in [3], where the throughput improves > a lot with multiple databases. I've not observed that in practice, and I > don't think these patches really can help with that. That scenario is > about buffer lock contention, not clocksweep contention. Buffer content and buffer headers being on your local node makes access faster... > Attached is a tiny patch doing mostly what Jakub did, except that it > does two things. First, it allows interleaving the shared memory on all > relevant NUMA nodes (per numa_get_mems_allowed). Second, it allows > populating all memory by setting MAP_POPULATE in mmap(). There's a new > GUC to enable each of these. > I think we should try this (much simpler) approach first, or something > close to it. Sorry for dragging everyone into a much more complex > approach, which now seems to be a dead end. I'm somewhat doubtful that interleaving is going to be good enough without some awareness of which buffers to preferrably use. Additionally, without huge pages, there are significant negative performance effects due to each buffer being split across two numa nodes. Greetings, Andres Freund -
Re: Adding basic NUMA awareness
Tomas Vondra <tomas@vondra.me> — 2026-01-12T23:58:49Z
On 1/10/26 02:42, Andres Freund wrote: > Hi, > > On 2025-12-08 21:02:27 +0100, Tomas Vondra wrote: >> * Most of the benefit comes from patches unrelated to NUMA. The initial >> patches partition clockweep, in a NUMA oblivious way. In fact, applying >> the NUMA patches often *reduces* the throughput. So if we're concerned >> about contention on the clocksweep hand, we could apply just these first >> patches. That way we wouldn't have to deal with huge pages. > >> * Furthermore, I'm not quite sure clocksweep really is a bottleneck in >> realistic cases. The benchmark used in this thread does many concurrent >> sequential scans, on data that exceeds shared buffers / fits into RAM. >> Perhaps that happens, but I doubt it's all that common. > > I think this misses that this isn't necessarily about peak throughput under > concurrent contention. Consider this scenario: > > 1) shared buffers is already allocated from a kernel POV, i.e. pages reside on > some numa node instead of having to be allocated on the first access > > 2) one backend does a scan of scan of a relation [largely] not in shared > buffers > > Whether the buffers for the ringbuffer (if the relation is > NBuffers/4) or > for the entire relation (if smaller) is allocated on the same node as the > backend makes a quite substantial difference. I see about a 25% difference > even on a small-ish numa system. > > Partitioned clocksweep makes it vastly more likely that data is on the local > numa node. > > If you simulate different locality modes with numactl, I can see pretty > drastic differences for the processing of individual queries, both with > parallel and non-parallel processing. > > > psql -Xq -c 'SELECT pg_buffercache_evict_all();' -c 'SELECT numa_node, sum(size) FROM pg_shmem_allocations_numa GROUP BY 1;' && perf stat --per-socket -M memory_bandwidth_read,memory_bandwidth_write -a psql -c 'SELECT sum(abalance) FROM pgbench_accounts;' > > membind 0, cpunodebind 1, max_parallel_workers_per_gather=0: > S0 6 341,635,792 UNC_M_CAS_COUNT.WR # 4276.9 MB/s memory_bandwidth_write > S0 20 5,116,381,542 duration_time > S0 6 255,977,795 UNC_M_CAS_COUNT.RD # 3204.6 MB/s memory_bandwidth_read > S0 20 5,116,391,355 duration_time > S1 6 2,418,579 UNC_M_CAS_COUNT.WR # 30.3 MB/s memory_bandwidth_write > S1 6 115,511,123 UNC_M_CAS_COUNT.RD # 1446.1 MB/s memory_bandwidth_read > > 5.112286670 seconds time elapsed > > > membind 1, cpunodebind 1, max_parallel_workers_per_gather=0: > S0 6 16,528,154 UNC_M_CAS_COUNT.WR # 248.1 MB/s memory_bandwidth_write > S0 20 4,267,078,201 duration_time > S0 6 40,327,670 UNC_M_CAS_COUNT.RD # 605.4 MB/s memory_bandwidth_read > S0 20 4,267,088,762 duration_time > S1 6 116,925,559 UNC_M_CAS_COUNT.WR # 1755.2 MB/s memory_bandwidth_write > S1 6 244,251,242 UNC_M_CAS_COUNT.RD # 3666.5 MB/s memory_bandwidth_read > > 4.263442844 seconds time elapsed > > > interleave 0,1, cpunodebind 1, max_parallel_workers_per_gather=0: > > S0 6 196,713,044 UNC_M_CAS_COUNT.WR # 2757.4 MB/s memory_bandwidth_write > S0 20 4,569,805,767 duration_time > S0 6 167,497,804 UNC_M_CAS_COUNT.RD # 2347.9 MB/s memory_bandwidth_read > S0 20 4,569,816,439 duration_time > S1 6 81,992,696 UNC_M_CAS_COUNT.WR # 1149.3 MB/s memory_bandwidth_write > S1 6 192,265,269 UNC_M_CAS_COUNT.RD # 2695.1 MB/s memory_bandwidth_read > > 4.565722468 seconds time elapsed > > > membind 0, cpunodebind 1, max_parallel_workers_per_gather=8: > S0 6 336,538,518 UNC_M_CAS_COUNT.WR # 24130.2 MB/s memory_bandwidth_write > S0 20 895,976,459 duration_time > S0 6 238,663,716 UNC_M_CAS_COUNT.RD # 17112.4 MB/s memory_bandwidth_read > S0 20 895,986,193 duration_time > S1 6 2,594,371 UNC_M_CAS_COUNT.WR # 186.0 MB/s memory_bandwidth_write > S1 6 113,981,673 UNC_M_CAS_COUNT.RD # 8172.6 MB/s memory_bandwidth_read > > 0.892594989 seconds time elapsed > > > membind 1, cpunodebind 1, max_parallel_workers_per_gather=8: > S0 6 3,492,673 UNC_M_CAS_COUNT.WR # 322.0 MB/s memory_bandwidth_write > S0 20 698,175,650 duration_time > S0 6 5,363,152 UNC_M_CAS_COUNT.RD # 494.4 MB/s memory_bandwidth_read > S0 20 698,187,522 duration_time > S1 6 117,181,190 UNC_M_CAS_COUNT.WR # 10802.4 MB/s memory_bandwidth_write > S1 6 251,059,297 UNC_M_CAS_COUNT.RD # 23144.0 MB/s memory_bandwidth_read > > 0.694253637 seconds time elapsed > > > interleave 0,1, cpunodebind 1, max_parallel_workers_per_gather=8: > > S0 6 170,352,086 UNC_M_CAS_COUNT.WR # 13767.3 MB/s memory_bandwidth_write > S0 20 797,166,139 duration_time > S0 6 121,646,666 UNC_M_CAS_COUNT.RD # 9831.1 MB/s memory_bandwidth_read > S0 20 797,175,899 duration_time > S1 6 60,099,863 UNC_M_CAS_COUNT.WR # 4857.1 MB/s memory_bandwidth_write > S1 6 182,035,468 UNC_M_CAS_COUNT.RD # 14711.5 MB/s memory_bandwidth_read > > 0.791915733 seconds time elapsed > > > > You're never going to be quite as good when actually using both NUMA nodes, > but at least simple workloads like the above should be able to get a lot > closer to the good number from above than we currently are. > I see no such improvements, unfortunately. Even when I explicitly pin memory and cpus to different nodes using numactl. Consider a simple experiment, starting an instance either like this: numactl --membind=0 --cpunodebind=0 pg_ctl -D /mnt/data/data-numa start or like this numactl --membind=0 --cpunodebind=1 pg_ctl -D /mnt/data/data-numa start on a 2-node NUMA cluster. To the best of my knowledge this means that either both the memory and all pg processes (including the backend) are on node 0, of memory is on node 0 and backend is on node 1. And then I initialized pgbench with scale that is much larger than shared buffers, but fits into RAM. So cached, but definitely > NB/4. And then I ran select * from pgbench_accounts offset 1000000000; which does a sequential scan with the circular buffer you mention abobe I've made all reasonable precautions to stabilize the results, like enabling huge pages (both for shared memory and binaries), disabling checksums, ... And I ran that on an Azure instance D96v6 with EPYC 9V74. This was with scale 10000 (~150GB), shared_buffers=8GB. And I get this: worker / 32 ----------- numactl --membind=0 --cpunodebind=0 pg_ctl ... Time: 26280.437 ms (00:26.280) Time: 26177.165 ms (00:26.177) Time: 26182.222 ms (00:26.182) Time: 26174.421 ms (00:26.174) Time: 26216.989 ms (00:26.217) numactl --membind=0 --cpunodebind=1 pg_ctl ... Time: 26412.878 ms (00:26.413) Time: 26413.332 ms (00:26.413) Time: 26202.899 ms (00:26.203) Time: 26412.627 ms (00:26.413) Time: 26484.962 ms (00:26.485) io_uring -------- numactl --membind=0 --cpunodebind=0 pg_ctl ... Time: 26286.977 ms (00:26.287) Time: 26499.830 ms (00:26.500) Time: 26629.990 ms (00:26.630) Time: 26443.147 ms (00:26.443) numactl --membind=0 --cpunodebind=1 pg_ctl ... Time: 26727.655 ms (00:26.728) Time: 26787.456 ms (00:26.787) Time: 26484.260 ms (00:26.484) Time: 26250.737 ms (00:26.251) Time: 26208.913 ms (00:26.209) I don't see any difference. To rule out any virtualization weirdness, I did the same experiment on my old Xeon machine (also 2-node NUMA), just with a smaller scale (2000) and shared_buffers=4GB. And that gave me: xeon scale=2000 nochecksums worker / 32 ----------- numactl --membind=0 --cpunodebind=0 pg_ctl ... Time: 5519.728 ms (00:05.520) Time: 5570.215 ms (00:05.570) Time: 5568.233 ms (00:05.568) Time: 5556.465 ms (00:05.556) Time: 5517.420 ms (00:05.517) numactl --membind=0 --cpunodebind=1 pg_ctl ... Time: 5639.281 ms (00:05.639) Time: 5657.822 ms (00:05.658) Time: 5653.077 ms (00:05.653) Time: 5647.780 ms (00:05.648) Time: 5647.288 ms (00:05.647) io_uring -------- numactl --membind=0 --cpunodebind=0 pg_ctl ... Time: 7517.920 ms (00:07.518) Time: 7180.628 ms (00:07.181) Time: 7162.801 ms (00:07.163) Time: 7164.827 ms (00:07.165) Time: 7177.757 ms (00:07.178) numactl --membind=0 --cpunodebind=1 pg_ctl ... Time: 7622.372 ms (00:07.622) Time: 7571.923 ms (00:07.572) Time: 7571.966 ms (00:07.572) Time: 7568.269 ms (00:07.568) Time: 7558.195 ms (00:07.558) If I squint a little bit, there's difference for io_uring. But it's not even 5%, definitely not 25%. > > > Maybe the problem is that the patchset doesn't actually quite work right now? > I checked out numa-20251111 and ran a query for a 1GB table in a 40GB s_b > system: there's not much more locality with debug_numa=buffers, than without > (roughly 55% on one node, 45% on the other). Making it not surprising that the > results aren't great. > Hard to say, but I'd guess that's because of the clocksweep balancing. Which ensures that we don't overload a single NUMA node. Imagine an instance with a single connection - it can't allocate from a single NUMA node, because that'd mean it'll only ever use 50% of available cache. Which does not seem great. Maybe there's a better way to address this. > > >> I've been unable to demonstrate any benefits on other workloads, even if >> there's a lot of buffer misses / reads into shared buffers. As soon as >> the query starts doing something else, the clocksweep contention becomes >> a non-issue. Consider for example read-only pgbench with database much >> larger than shared buffers (but still within RAM). The cost of the index >> scans (and other nodes) seems to reduce the pressure on clocksweep. >> >> So I'm skeptical about clocksweep pressure being a serious issue, except >> for some very narrow benchmarks (like the concurrent seqscan test). And >> even if this happened for some realistic cases, partitioning the buffers >> in a NUMA-oblivious way seems to do the trick. > > I think you're over-indexing on the contention aspect and under-indexing on > the locality benefits. > I've been unable to demonstrate meaningful benefits of locality (like in the example above), while I've been able to show benefits of reducing the clocksweep contention. It's entirely possible I'm doing it wrong or missing something, of course. > >> When discussing this stuff off list, it was suggested this might help >> with the scenario Andres presented in [3], where the throughput improves >> a lot with multiple databases. I've not observed that in practice, and I >> don't think these patches really can help with that. That scenario is >> about buffer lock contention, not clocksweep contention. > > Buffer content and buffer headers being on your local node makes access > faster... > That was my expectation too, but I haven't seen meaningful improvements in any benchmark. For example in the benchmark I presented earlier, all the memory is on node 0 (so both headers and buffers). And there does not seem to be any measurable difference when accessing it from node 0 vs. node 1. So why would it matter than header may be on node 0 and buffer on node 1? > >> Attached is a tiny patch doing mostly what Jakub did, except that it >> does two things. First, it allows interleaving the shared memory on all >> relevant NUMA nodes (per numa_get_mems_allowed). Second, it allows >> populating all memory by setting MAP_POPULATE in mmap(). There's a new >> GUC to enable each of these. > >> I think we should try this (much simpler) approach first, or something >> close to it. Sorry for dragging everyone into a much more complex >> approach, which now seems to be a dead end. > > I'm somewhat doubtful that interleaving is going to be good enough without > some awareness of which buffers to preferrably use. Additionally, without huge > pages, there are significant negative performance effects due to each buffer > being split across two numa nodes. > I'm rather skeptical this being worth it without huge pages. If you're trying to get the best performance on a NUMA machine (with is likely big with a lot of RAM), then huge pages are a huge improvement on their own. I'd even say this NUMA stuff might/should require huge_pages=on. -- Tomas Vondra
-
Re: Adding basic NUMA awareness
Andres Freund <andres@anarazel.de> — 2026-01-13T00:10:00Z
Hi, On 2026-01-13 00:58:49 +0100, Tomas Vondra wrote: > On 1/10/26 02:42, Andres Freund wrote: > > psql -Xq -c 'SELECT pg_buffercache_evict_all();' -c 'SELECT numa_node, sum(size) FROM pg_shmem_allocations_numa GROUP BY 1;' && perf stat --per-socket -M memory_bandwidth_read,memory_bandwidth_write -a psql -c 'SELECT sum(abalance) FROM pgbench_accounts;' > And then I initialized pgbench with scale that is much larger than > shared buffers, but fits into RAM. So cached, but definitely > NB/4. And > then I ran > > select * from pgbench_accounts offset 1000000000; > > which does a sequential scan with the circular buffer you mention abobe Did you try it with the query I suggested? One plausible reason why you did not see an effect with your query is that with a huge offset you actually never deform the tuple, which is an important and rather latency sensitive path. Greetings, Andres Freund
-
Re: Adding basic NUMA awareness
Andres Freund <andres@anarazel.de> — 2026-01-13T00:24:34Z
Hi, On 2026-01-12 19:10:00 -0500, Andres Freund wrote: > On 2026-01-13 00:58:49 +0100, Tomas Vondra wrote: > > On 1/10/26 02:42, Andres Freund wrote: > > > psql -Xq -c 'SELECT pg_buffercache_evict_all();' -c 'SELECT numa_node, sum(size) FROM pg_shmem_allocations_numa GROUP BY 1;' && perf stat --per-socket -M memory_bandwidth_read,memory_bandwidth_write -a psql -c 'SELECT sum(abalance) FROM pgbench_accounts;' > > > And then I initialized pgbench with scale that is much larger than > > shared buffers, but fits into RAM. So cached, but definitely > NB/4. And > > then I ran > > > > select * from pgbench_accounts offset 1000000000; > > > > which does a sequential scan with the circular buffer you mention abobe > > Did you try it with the query I suggested? One plausible reason why you did > not see an effect with your query is that with a huge offset you actually > never deform the tuple, which is an important and rather latency sensitive > path. Btw, this doesn't need anywhere close to as much data, it should be visible as soon as you're >> L3. To show why SELECT * FROM pgbench_accounts OFFSET 100000000 doesn't show an effect but SELECT sum(abalance) FROM pgbench_accounts; does, just look at the difference using the perf command I posted. Here on a scale 200. numactl --membind 0 --cpunodebind 0 offset: S0 6 47,138,135 UNC_M_CAS_COUNT.WR # 3884.1 MB/s memory_bandwidth_write S0 20 780,343,577 duration_time S0 6 61,685,331 UNC_M_CAS_COUNT.RD # 5082.8 MB/s memory_bandwidth_read S0 20 780,353,818 duration_time S1 6 1,238,568 UNC_M_CAS_COUNT.WR # 102.1 MB/s memory_bandwidth_write S1 6 1,475,224 UNC_M_CAS_COUNT.RD # 121.6 MB/s memory_bandwidth_read 0.776715450 seconds time elapsed agg: S0 6 53,145,706 UNC_M_CAS_COUNT.WR # 2000.8 MB/s memory_bandwidth_write S0 20 1,706,046,493 duration_time S0 6 111,390,488 UNC_M_CAS_COUNT.RD # 4193.5 MB/s memory_bandwidth_read S0 20 1,706,057,341 duration_time S1 6 3,968,454 UNC_M_CAS_COUNT.WR # 149.4 MB/s memory_bandwidth_write S1 6 4,026,212 UNC_M_CAS_COUNT.RD # 151.6 MB/s memory_bandwidth_read numactl --membind 0 --cpunodebind 1 offset: S0 6 91,982,003 UNC_M_CAS_COUNT.WR # 7036.4 MB/s memory_bandwidth_write S0 20 842,785,290 duration_time S0 6 113,076,316 UNC_M_CAS_COUNT.RD # 8650.1 MB/s memory_bandwidth_read S0 20 842,797,430 duration_time S1 6 1,545,612 UNC_M_CAS_COUNT.WR # 118.2 MB/s memory_bandwidth_write S1 6 2,354,087 UNC_M_CAS_COUNT.RD # 180.1 MB/s memory_bandwidth_read 0.836623794 seconds time elapsed agg: S0 6 133,267,754 UNC_M_CAS_COUNT.WR # 3980.9 MB/s memory_bandwidth_write S0 20 2,146,221,284 duration_time S0 6 159,951,549 UNC_M_CAS_COUNT.RD # 4777.9 MB/s memory_bandwidth_read S0 20 2,146,233,675 duration_time S1 6 71,543,708 UNC_M_CAS_COUNT.WR # 2137.1 MB/s memory_bandwidth_write S1 6 49,584,957 UNC_M_CAS_COUNT.RD # 1481.2 MB/s memory_bandwidth_read 2.142535432 seconds time elapsed Note how much bigger the absolute numbers of reads and writes are for the aggregate compared to the offset. Interestingly I do see a performance difference, albeit a smaller one, even with OFFSET. I see similar numbers on two different 2 socket machines. Greetings, Andres Freund -
Re: Adding basic NUMA awareness
Tomas Vondra <tomas@vondra.me> — 2026-01-13T00:51:09Z
On 1/13/26 01:10, Andres Freund wrote: > Hi, > > On 2026-01-13 00:58:49 +0100, Tomas Vondra wrote: >> On 1/10/26 02:42, Andres Freund wrote: >>> psql -Xq -c 'SELECT pg_buffercache_evict_all();' -c 'SELECT numa_node, sum(size) FROM pg_shmem_allocations_numa GROUP BY 1;' && perf stat --per-socket -M memory_bandwidth_read,memory_bandwidth_write -a psql -c 'SELECT sum(abalance) FROM pgbench_accounts;' > >> And then I initialized pgbench with scale that is much larger than >> shared buffers, but fits into RAM. So cached, but definitely > NB/4. And >> then I ran >> >> select * from pgbench_accounts offset 1000000000; >> >> which does a sequential scan with the circular buffer you mention abobe > > Did you try it with the query I suggested? One plausible reason why you did > not see an effect with your query is that with a huge offset you actually > never deform the tuple, which is an important and rather latency sensitive > path. > I did try with the agg query too, and there's still no difference on either machine. I can't do the perf on the Azure VM, because the Ubuntu is image is borked and does not allow installing the package. But on my xeon I can do the perf, and that gives me this: numactl --membind=0 --cpunodebind=0 ~/builds/master-test/bin/pg_ctl ----------------------------------------------------------------------- S0 1 24,677,226 UNC_M_CAS_COUNT.WR # 79.0 MB/s ... idth_write S0 1 20,001,829,522 ns duration_time ... S0 1 972,631,426 UNC_M_CAS_COUNT.RD # 3112.2 MB/s ... idth_read S0 1 20,001,822,807 ns duration_time ... S1 1 15,602,233 UNC_M_CAS_COUNT.WR # 49.9 MB/s ... idth_write S1 1 712,431,146 UNC_M_CAS_COUNT.RD # 2279.6 MB/s ... idth_read numactl --membind=0 --cpunodebind=1 ~/builds/master-test/bin/pg_ctl ----------------------------------------------------------------------- S0 1 47,931,019 UNC_M_CAS_COUNT.WR # 153.4 MB/s ... idth_write S0 1 20,002,933,380 ns duration_time ... S0 1 1,007,386,994 UNC_M_CAS_COUNT.RD # 3223.2 MB/s ... idth_read S0 1 20,002,927,341 ns duration_time ... S1 1 10,310,201 UNC_M_CAS_COUNT.WR # 33.0 MB/s ... idth_write S1 1 714,826,668 UNC_M_CAS_COUNT.RD # 2287.2 MB/s ... idth_read so there is a little bit of a difference for some stats, but not much. FWIW this is from perf stat --per-socket -M memory_bandwidth_read,memory_bandwidth_write -a -- sleep 20 while the agg query runs in a loop. cheers -- Tomas Vondra
-
Re: Adding basic NUMA awareness
Andres Freund <andres@anarazel.de> — 2026-01-13T01:08:31Z
Hi, On 2026-01-13 01:51:09 +0100, Tomas Vondra wrote: > On 1/13/26 01:10, Andres Freund wrote: > > Hi, > > > > On 2026-01-13 00:58:49 +0100, Tomas Vondra wrote: > >> On 1/10/26 02:42, Andres Freund wrote: > >>> psql -Xq -c 'SELECT pg_buffercache_evict_all();' -c 'SELECT numa_node, sum(size) FROM pg_shmem_allocations_numa GROUP BY 1;' && perf stat --per-socket -M memory_bandwidth_read,memory_bandwidth_write -a psql -c 'SELECT sum(abalance) FROM pgbench_accounts;' > > > >> And then I initialized pgbench with scale that is much larger than > >> shared buffers, but fits into RAM. So cached, but definitely > NB/4. And > >> then I ran > >> > >> select * from pgbench_accounts offset 1000000000; > >> > >> which does a sequential scan with the circular buffer you mention abobe > > > > Did you try it with the query I suggested? One plausible reason why you did > > not see an effect with your query is that with a huge offset you actually > > never deform the tuple, which is an important and rather latency sensitive > > path. > > > > I did try with the agg query too, and there's still no difference on > either machine. Could you provide numactl --hardware for both? There may be more than two numa nodes on a system with 2 sockets, due to one socket being split into two - in which case the latency between 0,1 might be a lot lower than say 0 and 3. > I can't do the perf on the Azure VM, because the Ubuntu is image is > borked and does not allow installing the package. But on my xeon I can > do the perf, and that gives me this: > > numactl --membind=0 --cpunodebind=0 ~/builds/master-test/bin/pg_ctl > ----------------------------------------------------------------------- > S0 1 24,677,226 UNC_M_CAS_COUNT.WR # 79.0 MB/s ... idth_write > S0 1 20,001,829,522 ns duration_time ... > S0 1 972,631,426 UNC_M_CAS_COUNT.RD # 3112.2 MB/s ... idth_read > S0 1 20,001,822,807 ns duration_time ... > S1 1 15,602,233 UNC_M_CAS_COUNT.WR # 49.9 MB/s ... idth_write > S1 1 712,431,146 UNC_M_CAS_COUNT.RD # 2279.6 MB/s ... idth_read > > > numactl --membind=0 --cpunodebind=1 ~/builds/master-test/bin/pg_ctl > ----------------------------------------------------------------------- > S0 1 47,931,019 UNC_M_CAS_COUNT.WR # 153.4 MB/s ... idth_write > S0 1 20,002,933,380 ns duration_time ... > S0 1 1,007,386,994 UNC_M_CAS_COUNT.RD # 3223.2 MB/s ... idth_read > S0 1 20,002,927,341 ns duration_time ... > S1 1 10,310,201 UNC_M_CAS_COUNT.WR # 33.0 MB/s ... idth_write > S1 1 714,826,668 UNC_M_CAS_COUNT.RD # 2287.2 MB/s ... idth_read > > so there is a little bit of a difference for some stats, but not much. > > > FWIW this is from > > perf stat --per-socket -M memory_bandwidth_read,memory_bandwidth_write > -a -- sleep 20 > > while the agg query runs in a loop. FWIW doing one perf stat for each execution is preferrable for comparison, because otherwise you can hide large differences in total number of memory accesses if the runtimes for the queries in the two "numa configurations" are different. Greetings, Andres Freund
-
Re: Adding basic NUMA awareness
Tomas Vondra <tomas@vondra.me> — 2026-01-13T01:13:40Z
On 1/13/26 01:24, Andres Freund wrote: > Hi, > > On 2026-01-12 19:10:00 -0500, Andres Freund wrote: >> On 2026-01-13 00:58:49 +0100, Tomas Vondra wrote: >>> On 1/10/26 02:42, Andres Freund wrote: >>>> psql -Xq -c 'SELECT pg_buffercache_evict_all();' -c 'SELECT numa_node, sum(size) FROM pg_shmem_allocations_numa GROUP BY 1;' && perf stat --per-socket -M memory_bandwidth_read,memory_bandwidth_write -a psql -c 'SELECT sum(abalance) FROM pgbench_accounts;' >> >>> And then I initialized pgbench with scale that is much larger than >>> shared buffers, but fits into RAM. So cached, but definitely > NB/4. And >>> then I ran >>> >>> select * from pgbench_accounts offset 1000000000; >>> >>> which does a sequential scan with the circular buffer you mention abobe >> >> Did you try it with the query I suggested? One plausible reason why you did >> not see an effect with your query is that with a huge offset you actually >> never deform the tuple, which is an important and rather latency sensitive >> path. > > Btw, this doesn't need anywhere close to as much data, it should be visible as > soon as you're >> L3. > > To show why > SELECT * FROM pgbench_accounts OFFSET 100000000 > doesn't show an effect but > SELECT sum(abalance) FROM pgbench_accounts; > > does, just look at the difference using the perf command I posted. Here on a > scale 200. > OK, I tried with smaller scale (and larger shared buffers, to make the data set smaller than NBuffers/4). On the azure VM (scale 200, 32GB sb), there's still no difference: numactl --membind 0 --cpunodebind 0 297.770 ms numactl --membind 0 --cpunodebind 1 297.924 ms and on xeon (scale 100, 8GB sb), there's a bit of a difference: numactl --membind 0 --cpunodebind 0 236.451 ms numactl --membind 0 --cpunodebind 1 298.418 ms So roughly 20%. There's also a bigger difference in the perf, about 5944.3 MB/s vs. 5202.3 MB/s. > > Interestingly I do see a performance difference, albeit a smaller one, even > with OFFSET. I see similar numbers on two different 2 socket machines. > I wonder how significant is the number of sockets. The Azure is a single socket with 2 NUMA nodes, so maybe the latency differences are not significant enough to affect this kind of tests. The xeon is a 2-socket machine, but it's also older (~10y). regards -- Tomas Vondra
-
Re: Adding basic NUMA awareness
Tomas Vondra <tomas@vondra.me> — 2026-01-13T01:25:57Z
On 1/13/26 02:08, Andres Freund wrote: > Hi, > > On 2026-01-13 01:51:09 +0100, Tomas Vondra wrote: >> On 1/13/26 01:10, Andres Freund wrote: >>> Hi, >>> >>> On 2026-01-13 00:58:49 +0100, Tomas Vondra wrote: >>>> On 1/10/26 02:42, Andres Freund wrote: >>>>> psql -Xq -c 'SELECT pg_buffercache_evict_all();' -c 'SELECT numa_node, sum(size) FROM pg_shmem_allocations_numa GROUP BY 1;' && perf stat --per-socket -M memory_bandwidth_read,memory_bandwidth_write -a psql -c 'SELECT sum(abalance) FROM pgbench_accounts;' >>> >>>> And then I initialized pgbench with scale that is much larger than >>>> shared buffers, but fits into RAM. So cached, but definitely > NB/4. And >>>> then I ran >>>> >>>> select * from pgbench_accounts offset 1000000000; >>>> >>>> which does a sequential scan with the circular buffer you mention abobe >>> >>> Did you try it with the query I suggested? One plausible reason why you did >>> not see an effect with your query is that with a huge offset you actually >>> never deform the tuple, which is an important and rather latency sensitive >>> path. >>> >> >> I did try with the agg query too, and there's still no difference on >> either machine. > > Could you provide numactl --hardware for both? There may be more than two > numa nodes on a system with 2 sockets, due to one socket being split into two > - in which case the latency between 0,1 might be a lot lower than say 0 and 3. > xeon: available: 2 nodes (0-1) node 0 cpus: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 node 0 size: 32066 MB node 0 free: 13081 MB node 1 cpus: 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 node 1 size: 32210 MB node 1 free: 17764 MB node distances: node 0 1 0: 10 21 1: 21 10 azure/epyc: available: 2 nodes (0-1) node 0 cpus: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 node 0 size: 193412 MB node 0 free: 147949 MB node 1 cpus: 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 node 1 size: 193513 MB node 1 free: 151577 MB node distances: node 0 1 0: 10 11 1: 11 10 > >> I can't do the perf on the Azure VM, because the Ubuntu is image is >> borked and does not allow installing the package. But on my xeon I can >> do the perf, and that gives me this: >> >> numactl --membind=0 --cpunodebind=0 ~/builds/master-test/bin/pg_ctl >> ----------------------------------------------------------------------- >> S0 1 24,677,226 UNC_M_CAS_COUNT.WR # 79.0 MB/s ... idth_write >> S0 1 20,001,829,522 ns duration_time ... >> S0 1 972,631,426 UNC_M_CAS_COUNT.RD # 3112.2 MB/s ... idth_read >> S0 1 20,001,822,807 ns duration_time ... >> S1 1 15,602,233 UNC_M_CAS_COUNT.WR # 49.9 MB/s ... idth_write >> S1 1 712,431,146 UNC_M_CAS_COUNT.RD # 2279.6 MB/s ... idth_read >> >> >> numactl --membind=0 --cpunodebind=1 ~/builds/master-test/bin/pg_ctl >> ----------------------------------------------------------------------- >> S0 1 47,931,019 UNC_M_CAS_COUNT.WR # 153.4 MB/s ... idth_write >> S0 1 20,002,933,380 ns duration_time ... >> S0 1 1,007,386,994 UNC_M_CAS_COUNT.RD # 3223.2 MB/s ... idth_read >> S0 1 20,002,927,341 ns duration_time ... >> S1 1 10,310,201 UNC_M_CAS_COUNT.WR # 33.0 MB/s ... idth_write >> S1 1 714,826,668 UNC_M_CAS_COUNT.RD # 2287.2 MB/s ... idth_read >> >> so there is a little bit of a difference for some stats, but not much. >> >> >> FWIW this is from >> >> perf stat --per-socket -M memory_bandwidth_read,memory_bandwidth_write >> -a -- sleep 20 >> >> while the agg query runs in a loop. > > FWIW doing one perf stat for each execution is preferrable for comparison, > because otherwise you can hide large differences in total number of memory > accesses if the runtimes for the queries in the two "numa configurations" are > different. > Good point, I'll do that next time. But in this case they are not all that different, I think. regards -- Tomas Vondra
-
Re: Adding basic NUMA awareness
Jakub Wartak <jakub.wartak@enterprisedb.com> — 2026-01-13T13:26:37Z
On Tue, Jan 13, 2026 at 2:13 AM Tomas Vondra <tomas@vondra.me> wrote: > > On 1/13/26 01:24, Andres Freund wrote: > > Hi, > > > > On 2026-01-12 19:10:00 -0500, Andres Freund wrote: > >> On 2026-01-13 00:58:49 +0100, Tomas Vondra wrote: > >>> On 1/10/26 02:42, Andres Freund wrote: > >>>> psql -Xq -c 'SELECT pg_buffercache_evict_all();' -c 'SELECT numa_node, sum(size) FROM pg_shmem_allocations_numa GROUP BY 1;' && perf stat --per-socket -M memory_bandwidth_read,memory_bandwidth_write -a psql -c 'SELECT sum(abalance) FROM pgbench_accounts;' > >> > >>> And then I initialized pgbench with scale that is much larger than > >>> shared buffers, but fits into RAM. So cached, but definitely > NB/4. And > >>> then I ran > >>> > >>> select * from pgbench_accounts offset 1000000000; > >>> > >>> which does a sequential scan with the circular buffer you mention abobe > >> > >> Did you try it with the query I suggested? One plausible reason why you did > >> not see an effect with your query is that with a huge offset you actually > >> never deform the tuple, which is an important and rather latency sensitive > >> path. > > > > Btw, this doesn't need anywhere close to as much data, it should be visible as > > soon as you're >> L3. > > > > To show why > > SELECT * FROM pgbench_accounts OFFSET 100000000 > > doesn't show an effect but > > SELECT sum(abalance) FROM pgbench_accounts; > > > > does, just look at the difference using the perf command I posted. Here on a > > scale 200. > > > > OK, I tried with smaller scale (and larger shared buffers, to make the > data set smaller than NBuffers/4). > > On the azure VM (scale 200, 32GB sb), there's still no difference: > [..] > and on xeon (scale 100, 8GB sb), there's a bit of a difference: > [..] > > So roughly 20%. There's also a bigger difference in the perf, about > 5944.3 MB/s vs. 5202.3 MB/s. > > > > > Interestingly I do see a performance difference, albeit a smaller one, even > > with OFFSET. I see similar numbers on two different 2 socket machines. > > > > I wonder how significant is the number of sockets. The Azure is a single > socket with 2 NUMA nodes, so maybe the latency differences are not > significant enough to affect this kind of tests. > > The xeon is a 2-socket machine, but it's also older (~10y). My 0.02$ from intentionally having even more slow hardware with even more sockets where some effects are even more visible (maybe this helps the $thread) There are two ways we could benefit from NUMA multi-socket systems: a) potentially getting lower latency b) avoid hitting interconnect max bandwidth limitations - so it started with offline discussion yesterday that earlier we have seen some yield from pgbench -S results, however I could not reproduce the yield from [1] NUMAv2 -- I was trying to replicate that result "I did some quick perf testing on my old xeon machine (2 NUMA nodes), and the results are encouraging. For a read-only pgbench (2x shared buffers, within RAM), I saw an increase from 1.1M tps to 1.3M." [1]. I was using numa_buffers_interleave=on,numa_localalloc=on,numa_partition_freelist=node,numa_procs_interleave=on,numa_procs_pin=off, no HPs (due to "Bad address" bug in that patchset), two -i pgbench scales (2000, 500), with and without checksums, with/without -M prepared. More or less it's always like that: jul17__64__off_pgbench.log:tps = 564153.278183 (without initial connection time) numav2__64__off_pgbench.log:tps = 562068.655263 (without initial connection time) so that probably rules out that we have recently introduced a bug into the patchset (but that may mean that 1.1->1.3M boost was something else?) - so per above and in my opinion, both on master or all patchsets here, classic OLTP pgbench (-S) is way too CPU computation heavy even with -M prepared to see NUMA latency effects. Affects *both* NUMAv2 (Aug2025)[1] and Nov 2025 patchset versions. Even getting 0.5M tps on pgbench scale -i 500 ends up using way less traffic before hitting the QPI (interconnect) max link bandwidth (just 3.8-4.2 GB/s in my case, but pgbench -S can consume just max 1.2GB/s assuming proper interleaving). - the single seqscan "SELECT sum(abalance) FROM pgbench_accounts;" problem (or lack of it -- with single session) is that with standard master, you may end up having data on just a single NUMA node. If that system is idle and running just 1 instance of this query, I'm getting like ~750MB/s of memory reads from the socket where the data is located (again , much below the limit of the interconnect [3.8-4.2GB/s]) - so yes when I do "synchronize_seqscans=off", and I start throwing those seqscans from *other* sockets [--cpubindnodes 1,2,3 (while that relation is hosted on node 0's DRAM)], of course I can see choke point on the interconnect, (so it can feed data @ ~4.2 GB/s and even more for short periods of time, but that's it). That's the problem1(optimization1) realistically we can solve here I think, with interleaving and that has been shown here multiple times: I can get simply much more juice in the above scenario, because I have access to better aggregated bandwidth for seqscans like that and simply put more CPU cores (from CPUs on nodes 1,2,3) to feed that data from RAM on node #0. It also helps achieve less deviation in latency in such conditions. To add more fun Linux kernel likes to put randomly that shm segments (e.g. if it fits single NUMA free hugepages, it will be put there OR it uses some spanning across nodes , but not interleaving and depending on relation you have it here or there). So far, so good as far interleaving is concerned. - however the above might be simply not true on single-socket NUMA systems (EPYC) or just more modern multi-socket (but still same chassis NUMA systems) - so EPYC again?(~500GB/s wild interconnect)? - as the interconnect there might be not present (1s, BIOS/UEFI may have setting to expose group of cores [CCD] as NUMA) OR have the bandwidth and there's no realistic chokepoint for us there, so it wouldn't make sense to benchmark using such hardware to spot the effects. - So the remaining question remains, what about having better locality/latency of let's say ~100ns on local DRAM vs 3x as much as the remote [3][4] - how much we can gain? (BTW this seems to be a pretty universal rule of thumb -> 3x factor for most hardware, see [5). If just shm is remote to the CPU for this query I'm getting: remote: Time: 1925.820 ms local: Time: 1796.678 ms so 1.071x. and in some different situations if both shm and heap memory are remote to the CPU for this query I was getting: local: Time: 9773.179 ms (00:09.773) remote: Time: 12154.636 ms (00:12.155) +/- 300ms (+ I can see more traffic on sockets). So technically I should be getting this ~7%..22% profit due to lower latency if I would be fetching just ONLY local memory (but with NUMA we are not doing it right? we are interleaving - so we hit all sockets most of the time to fetch data). So think about benchmarking: maybe we would have to be using multiple pgbench_accounts_N (not just one, but each per backend/CPU pgbench_accounts_$CPU?), together larger than s_b to cause natural evictions, as this would then cause the partitioned clocksweep to make it more likely that data is on the local NUMA node and keep it hot for some time while avoiding any access to VFS cache (so stick to reading only s_b)? (and we could measure local vs remote access ratio too along the way). Also just for research, we should disable clocksweep balancing. It should be enough to demonstrate that the patch is working, right? (so 1 backend should be just reading from 1 NUMA node mostly and that should be producing some yield and it should be having high local access ratio and just build up from there if necessary rather than enabling all of the v20251126* patches) XXX but eviction would be followed up by something like pread() from VFS, and what about if that VFS cache is also on another node? XXX those BAS strategies are just pain and yet another thing to care about, couldn't we have some debug GUC to turn them off (debug_benchmarking=true)? -J. [1] - https://www.postgresql.org/message-id/3223cdcd-6d16-4e90-a3a6-b957f762dc5a%40vondra.me [2] - mlc --bandwidth_matrix: //that's sequential memory reads, but with -U it is pretty close (90%) to the below ones Bandwidths are in MB/sec (1 MB/sec = 1,000,000 Bytes/sec) Using all the threads from each core if Hyper-threading is enabled Using Read-only traffic type Numa node Numa node 0 1 2 3 0 22677.4 3784.2 3720.6 4199.1 1 3855.7 22463.1 4226.8 3732.0 2 3713.7 4228.3 21816.6 3886.3 3 4190.7 3692.9 3796.3 22673.0 [3] mlc --latency_matrix: Measuring idle latencies (in ns)... Numa node Numa node 0 1 2 3 0 96.7 296.0 300.7 292.5 1 289.7 96.9 289.7 303.8 2 309.9 289.3 97.0 291.0 3 300.0 303.7 296.5 97.1 [4] numactl --hardware lies a little bit (real latency vs below numbers): node distances: node 0 1 2 3 0: 10 20 30 20 1: 20 10 20 30 2: 30 20 10 20 3: 20 30 20 10 [5] https://github.com/nviennot/core-to-core-latency?tab=readme-ov-file#dual-sockets-results -
Re: Adding basic NUMA awareness
Andres Freund <andres@anarazel.de> — 2026-01-13T14:14:17Z
Hi, On 2026-01-13 02:13:40 +0100, Tomas Vondra wrote: > On the azure VM (scale 200, 32GB sb), there's still no difference: One possibility is that the host is configured with memory interleaving. That configures the memory so that physical memory addresses interleave between the different NUMA nodes, instead of really being node local. That can help avoid bad performance characteristics for NUMA naive applications. I don't quite know how to figure that out though, particularly from within a VM :(. Even something like https://github.com/nviennot/core-to-core-latency or intel's mlc will not necessarily be helpful, because it depends on which node the measured cacheline ends up on. But I'd probably still test it, just to see whether you're observing very different latencies between the systems. > > Interestingly I do see a performance difference, albeit a smaller one, even > > with OFFSET. I see similar numbers on two different 2 socket machines. > > > > I wonder how significant is the number of sockets. The Azure is a single > socket with 2 NUMA nodes, so maybe the latency differences are not > significant enough to affect this kind of tests. Ah, yes, a single socket machine might not show that much of an increase, at least in simpler cases. One of my workstations has two sockets, but each socket has two numa nodes, the latency difference between the same numa node and the other numa node in the same socket is small, but the difference to the other socket is ~1.5x. Using intel's mlc: Measuring idle latencies for sequential access (in ns)... Numa node Numa node 0 1 2 3 0 98.6 106.9 157.6 167.9 1 105.8 99.4 158.4 170.5 2 157.2 167.4 103.6 105.6 3 158.4 171.2 104.5 104.3 So there's a about a 2-10ns latency difference between 0,1 and 2,3, but about a 50-60ns diffence across sockets... > The xeon is a 2-socket machine, but it's also older (~10y). It's perhaps worth noting that memory access latency has been *in*creasing in the last generation or two of hardware... Greetings, Andres Freund -
Re: Adding basic NUMA awareness
Andres Freund <andres@anarazel.de> — 2026-01-13T14:37:15Z
Hi, On 2026-01-13 14:26:37 +0100, Jakub Wartak wrote: > - so per above and in my opinion, both on master or all patchsets > here, classic OLTP pgbench (-S) is way too CPU computation heavy even > with -M prepared to see NUMA latency effects. I don't think it's that it's too CPU computation heavy, it's that it's very latency sensitive to a small number of cachelines (ProcArrayLock, buffer mapping table locks, btree inner pages), which will fundamentally have to reside on one of the nodes. For pgbench -S to benefit we'd first need to address at the very least the btree root page contention. > - the single seqscan "SELECT sum(abalance) FROM pgbench_accounts;" > problem (or lack of it -- with single session) is that with standard > master, you may end up having data on just a single NUMA node. If that > system is idle and running just 1 instance of this query, I'm getting > like ~750MB/s of memory reads from the socket where the data is > located (again , much below the limit of the interconnect > [3.8-4.2GB/s]) The limit of the interconnect should be pretty much irrelevant for a single query, you're pretty much never going to hit that query. On my ~8 year old workstation (2x Xeon Gold 5215), with slow-ish RAM: ./mlc --bandwidth_matrix Intel(R) Memory Latency Checker - v3.11a Command line parameters: --bandwidth_matrix Using buffer size of 100.000MiB/thread for reads and an additional 100.000MiB/thread for writes Measuring Memory Bandwidths between nodes within system Bandwidths are in MB/sec (1 MB/sec = 1,000,000 Bytes/sec) Using all the threads from each core if Hyper-threading is enabled Using Read-only traffic type Numa node Numa node 0 1 0 65173.3 34092.5 1 33726.3 71244.1 If you're seeing ~3.5GB/s, as your [2] indicates, something either is wrong with that system, or it's so old that it's useless for benchmarking. That's worse than single core node-node numbers I've gotten on 10yo hardware. The reason you're just getting 750MB is presumably because you're *latency* limited, not because you're bandwidth limited. The problem is that our deforming code has a, currently, unpredictable memory access at the start that cannot meaningfully be hidden by out of order execution (because it determines the address of the first column to actually deform, which cannot be hidden by speculative execution). > - however the above might be simply not true on single-socket NUMA > systems (EPYC) EPYC supports both single and dual socket systems. And intel has numa-within-a-socket too... > or just more modern multi-socket (but still same chassis NUMA systems) - so > EPYC again?(~500GB/s wild interconnect)? I don't think EPYC, even in the newer iterations, has anywhere near a 500GB/s interconnect. But it's really irrelevant, latency is the main factor, not bandwidth. > So technically I should be getting this ~7%..22% profit due to lower > latency if I would be fetching just ONLY local memory (but with NUMA > we are not doing it right? we are interleaving - so we hit all sockets > most of the time to fetch data) We should *not* be interleaving unnecessarily, precisely because of this. We should use the partitioned clock sweep to default to using local memory as long as possible. Greetings, Andres Freund -
Re: Adding basic NUMA awareness
Tomas Vondra <tomas@vondra.me> — 2026-01-14T23:26:47Z
On 1/13/26 15:14, Andres Freund wrote: > Hi, > > On 2026-01-13 02:13:40 +0100, Tomas Vondra wrote: >> On the azure VM (scale 200, 32GB sb), there's still no difference: > > One possibility is that the host is configured with memory interleaving. That > configures the memory so that physical memory addresses interleave between the > different NUMA nodes, instead of really being node local. That can help avoid > bad performance characteristics for NUMA naive applications. > > I don't quite know how to figure that out though, particularly from within a > VM :(. Even something like https://github.com/nviennot/core-to-core-latency > or intel's mlc will not necessarily be helpful, because it depends on which > node the measured cacheline ends up on. > > But I'd probably still test it, just to see whether you're observing very > different latencies between the systems. > I did this on the two Azure instances I've been using for testing (D96 and HB176), and I got this: D96 (v6): Numa node Numa node 0 1 0 129.9 129.9 1 128.3 128.1 HB176 (v4): Numa node Numa node 0 1 2 3 0 107.3 116.8 207.3 207.0 1 120.5 110.6 207.5 207.1 2 207.0 207.2 107.8 116.8 3 204.4 204.7 117.7 107.9 I guess this confirms that D96 is mostly useless for evaluation of the NUMA patches. This is a single-socket machine, with one NUMA node per chiplet (I assume), and there's about no difference in latency. For HB176 there clearly seems to be a difference of ~90ns between the sockets, i.e. the latency about doubles in some cases. Each socket has two chiplets - and there the story is about the same as on D96. I did this on my old-ish Xeon too, and it's somewhere in between. There clearly is difference between the sockets, but it's smaller than on HB176. Which matches with your observation that the latency is really increasing over time. I doubt the interleaving mode is enabled. It clearly is not enabled on the HB176 machine (otherwise we wouldn't see the difference, I think), and the smaller instance can be explained by having a single socket. I've attached the complete mlc results, for completeness. I've also done bigger SQL test with pinning the memory/backends to different nodes, for a range of scales and the two queries (agg and offset). I'm attaching results for scale 100 and 10000 from D96 and HB176 instances. The numbers are timings per query (avg latency reported by pgbench). I think this mostly aligns with the mlc results - the D96 shows no difference, while HB176 shows clear differences when memory/cpu get pinned to different sockets (but not chiplets in the same socket). But there are some interesting details too, particularly when it comes to behavior of the two queries. The "offset" query is affected by latency even with no parallelism (max_parallel_workers_per_gather=0), and it shows ~30% hit for cross-socket runs. But for "agg" there's no difference in that case, and the hit is visible only with 4 or 8 workers. That's interesting. Anyway, my plan at this point is to revive the old patch (before changing direction to the simple patch), and see if we can observe a difference on the "right" hardware. Maybe some of the results with no improvements were due to this. This workload seems much more realistic. regards -- Tomas Vondra -
Re: Adding basic NUMA awareness
Andres Freund <andres@anarazel.de> — 2026-01-15T00:01:38Z
Hi, On 2026-01-15 00:26:47 +0100, Tomas Vondra wrote: > D96 (v6): > > Numa node > Numa node 0 1 > 0 129.9 129.9 > 1 128.3 128.1 I wonder if D96 has turned on memory interleaving... These numbers are so close to each other that they're a tad hard to believe. > > HB176 (v4): > > Numa node > Numa node 0 1 2 3 > 0 107.3 116.8 207.3 207.0 > 1 120.5 110.6 207.5 207.1 > 2 207.0 207.2 107.8 116.8 > 3 204.4 204.7 117.7 107.9 > > I guess this confirms that D96 is mostly useless for evaluation of the > NUMA patches. This is a single-socket machine, with one NUMA node per > chiplet (I assume), and there's about no difference in latency. > For HB176 there clearly seems to be a difference of ~90ns between the > sockets, i.e. the latency about doubles in some cases. Each socket has > two chiplets - and there the story is about the same as on D96. It looks to me like within a socket there is a latency difference of about 10ns? Only when going between sockets there's no difference between which of the remote nodes is accessed - which makes sense to me. For newer single-node EPYC https://chipsandcheese.com/p/amds-epyc-9355p-inside-a-32-core has some numbers for within socket latencies. They also see about 10ns between inside-socket-local and inside-socket-remote. > I did this on my old-ish Xeon too, and it's somewhere in between. There > clearly is difference between the sockets, but it's smaller than on > HB176. Which matches with your observation that the latency is really > increasing over time. FWIW https://chipsandcheese.com/p/a-look-into-intel-xeon-6s-memory has some numbers in the "NUMA/Chiplet Characteristics" too. One aspect in it caught my eye: > Thus accesses to a remote NUMA node are only cached by the remote die’s > L3. Accessing the L3 on an adjacent die increases latency by about 24 > ns. Crossing two die boundaries adds a similar penalty, increasing latency > to nearly 80 ns for a L3 hit Afaict that translates to an L3 hit consistently taking 80ns when accessing remote memory, that's quite something. > I doubt the interleaving mode is enabled. It clearly is not enabled on > the HB176 machine (otherwise we wouldn't see the difference, I think), > and the smaller instance can be explained by having a single socket. As you say, there obviously is no interleaving on the HB176. I do wonder about the D96, but ... I wonder if the configuration is somehow visible in MSRs... > The numbers are timings per query (avg latency reported by pgbench). I > think this mostly aligns with the mlc results - the D96 shows no > difference, while HB176 shows clear differences when memory/cpu get > pinned to different sockets (but not chiplets in the same socket). Yea, that makes sense. > But there are some interesting details too, particularly when it comes > to behavior of the two queries. The "offset" query is affected by > latency even with no parallelism (max_parallel_workers_per_gather=0), > and it shows ~30% hit for cross-socket runs. But for "agg" there's no > difference in that case, and the hit is visible only with 4 or 8 > workers. That's interesting. Huh, that *is* interesting. I guess the hardware prefetchers are good enough to prefetch of the tuple headers in this case, possibly because the tuples are small and regular enough that the hardware prefetchers manage to prefetch everything in time? E.g. https://docs.amd.com/api/khub/documents/goX~9ubv8i5r60A_Qrp3Rw/content documents "L1 Stride Prefetcher" as > The prefetcher uses the L1 cache memory access history of individual > instructions to fetch additional lines when each access is a constant > distance from the previous. Greetings, Andres Freund
-
Re: Adding basic NUMA awareness
Jakub Wartak <jakub.wartak@enterprisedb.com> — 2026-01-21T10:30:03Z
On Thu, Jan 15, 2026 at 12:26 AM Tomas Vondra <tomas@vondra.me> wrote: > [..] > > Anyway, my plan at this point is to revive the old patch (before > changing direction to the simple patch), and see if we can observe a > difference on the "right" hardware. Maybe some of the results with no > improvements were due to this. This workload seems much more realistic. > I think I have an answer why Your patch is misbehaving, but I might be wrong. First my 4s16c64t hw numbers from master back from ~21st Nov 2025 (so here without Your's patch), s_b=8GB, huge_pages, pgbench -i scale 150 (so it flies under the radar NBuffers/4), which gave me ~1922MB pgbench_accounts, and then I do the select sum(abalance): --membind=0 --cpunodebind=0 latency average = 2468.321 ms, stddev = 0.479 ms S0 @ 825MB/s (uncore_imc/cas_count_read/) --membind=0 --cpunodebind=1 latency average = 2780.653 ms, stddev = 2.080 ms S0 @ 729MB/s (uncore_imc/cas_count_read/) (2 socket hops as old UPI/QPI had max 2 interlinks) --membind=0 --cpunodebind=2 latency average = 2811.500 ms stddev = 1.958 ms --membind=0 --cpunodebind=3 latency average = 2777.305 ms stddev = 1.314 ms So in ideal conditions I should be getting a 13-14% boost if all is well. However with the patchset (v20251121, debug_numa='buffers,procs'), it gets somewhat worse numbers (worse than above than on master). --membind=0 --cpunodebind=0 (we know that patchset code will "interleave" anyway) latency average = 2885.806 ms stddev = 20.349 ms and we are reading RAM from four sockets, each @ 170-180 MB/s (total ~720MB/s) The patch spread the 1922MB relation (which would fit into one node) into many: postgres# select numa_node, count(*) from pg_buffercache_numa where bufferid in (select bufferid from pg_buffercache where relfilenode = (select relfilenode from pg_class where relname = 'pgbench_accounts')) group by 1 order by 2; numa_node | count -----------+------- 2 | 55404 3 | 55405 1 | 55415 0 | 79678 Also the pg_buffercache_partitions.weights indicates "{24,24,24,24}" (%) split. Now if I do this command-trick `migratepages(1) <pid> 1-3 0`, it really disarms our patchset semi-manual-interleave (numa_maps bind:1-3 to real numa 0) and it *does* restore performance from 2900ms to 2600ms, therefore it just means it is non optimal memory placement (by clocksweep balancing). So the question is why such a table (1922MB with 245902 relpages) would end up spreading so quickly to the other sockets? I've assumed that hitting 4 sockets instead of 1 will be too slow and disarm the patchset partitioned clocksweep balancing like below: @@ -497,7 +497,9 @@ StrategyGetBuffer(BufferAccessStrategy strategy, [...] - sweep = ChooseClockSweep(true); + sweep = ChooseClockSweep(false); and this in next tries gets me perfect (thanks to no balancing) weight there: postgres=# select partition, numa_node, total_allocs, num_allocs, weights from pg_buffercache_partitions; partition | numa_node | total_allocs | num_allocs | weights -----------+-----------+--------------+------------+------------- 0 | 0 | 246186 | 0 | {100,0,0,0} 1 | 1 | 0 | 0 | {0,100,0,0} 2 | 2 | 0 | 0 | {0,0,100,0} 3 | 3 | 0 | 0 | {0,0,0,100} which yields (compare to initial measurements of 2468 .. 2780.. 2811ms): latency average = 2737.440 ms latency stddev = 5.715 ms socket 0 reliably outputs 725MB/s constant (other are idle as expected) I think we may simply need to (re?)think of strategy on how/when to distribute, because even If I fill just 112 MB of data fresh after startup, weight will change from 100,0,0,0... postgres=# create table tmp1 as select id, repeat('A', 1024) t from generate_series(1, 100000) as id; SELECT 100000 -- 112 MB postgres=# \dt+ tmp1 [..] and then scan it, I'm already having just 62% of local affinity (again my whole s_b is 8GB, and per-NUMA-node is like 2GB, so we are under radar of NBuffers/4 too): postgres=# select partition, numa_node, total_allocs, num_allocs, weights from pg_buffercache_partitions; partition | numa_node | total_allocs | num_allocs | weights -----------+-----------+--------------+------------+--------------- 0 | 0 | 2587 | 1 | {62,12,12,12} 1 | 1 | 0 | 0 | {0,100,0,0} 2 | 2 | 0 | 0 | {0,0,100,0} 3 | 3 | 0 | 0 | {0,0,0,100} -- double-confirmation: postgres=# select numa_node, count(*), count(*) * 100.0 / sum(count(*)) OVER() AS pct from pg_buffercache_numa where bufferid in ( select bufferid from pg_buffercache where relfilenode = (select relfilenode from pg_class where relname = 'tmp1') ) group by 1 order by 2; numa_node | count | pct -----------+-------+--------------------- 3 | 1680 | 11.7130307467057101 2 | 1683 | 11.7339468730391132 1 | 1690 | 11.7827511678170536 0 | 9290 | 64.7702712124381231 so of course during the scanning in loop, we are stressing all the sockets here pretty much. It gets even worse from there, as if I use multiple tmp* tables like that from a single backend (but total size << 1GB), I end up with "{24,24,24,24}" split (but all of them would fit my node). Of course all of the above was written with assumption for getting most of the latency , single backend and not having backends from different nodes. But now If I would hypothetically benchmark pgbench -S on master(from Nov) against Yourpatchset from back then, with low number of backends I would be comparing single-node-hugepage allocation (on random node, because it would fit) vspatchset doing interleaving memory. But if kernel would migrate all those backends (in case of master) toward the node where most of s_b is located, Your patchset simply couldn't win this. This brings me to a point where I'm suspicious of this clock-sweep balancing idea (partitioning is fine, it's just the balancing which seems to be kicking too prematurely). BTW: much earlier You seem to have benchmarked My thoughts for today are like following how it should work if You want to have "demonstrate any benefits on other workloads": 1. We should do not pin backends to specific CPU/numa nodes, as the kernel should be free to move the processes closer to the data more requested (it knows the state of memory transfers and CPU util% across nodes better than we do). - we query for sched_getcpu() and get node - we stick to PgProc and Buffers from that node and that's all (we seem to be doing just that in the patchset, great!) 2. We then should try to stick to the local node as much as possible till it's almost full and maybe only then try to start using remote memory as a last resort. Or maybe even try to avoid it at all costs. - wouldn't eviction (as the first baby step) be more preferable rather than using remote memory? - we get local affinity boost, so no distribution to the other partitions (unless absolutely necessary?) - ring buffers should protect somehow with one backend filling all RAM memory on the node (well except pg_prewarm? maybe we should adjust it so it intentionally interleaves from start?) -- related: NBuffers / 4 magic number drives me nuts, but maybe we should tweak to take into account the number of nodes too (NBuffers / nodes / 4?) to avoid filling whole node - we would get more I/O as there would be potentially lower chance to find data in memory on that node(?), so we would need somehow to counter this - maybe that's a little bit sci-fi, or I'm going to be flamed here for it, but we could potentially track local vs remote usagecount (Buffer state seems to be using 54 bits, so we could add 4 bits more to track "remote" access, AKA usagecount_remote? but we seem to not have space to track the exact origin/remote node). If we would track local vs remote, we would have have some input as if interleaving makes sense or not, wouldn't we? (that would somehow be tracked on per relation/"blockset", just food for thought, dunno if we even have infra for that) -J. -
Re: Adding basic NUMA awareness
Tomas Vondra <tomas@vondra.me> — 2026-06-05T12:52:35Z
Hi, Here's an updated version of the NUMA patch series, based on some recent discussions about this (some at pgconf.dev, but not only that), The main change is I significantly simplified some of the parts. Whe patch from 20251126 was ~190K, the new version is maybe 100K, so about half. Some of that is thanks to dropping the PGPROC partitioning entirely, but the remaining parts are smaller too. I realize it's not a great metric, of course. In this message I'll explain the changes since 20251126. I'm yet to do a thorough performance evaluation and see if it helps, I'll post that in the next couple days. The current patch series has these parts: v20260605-0001-Add-shmem_populate-and-shmem_interleave-GU.patch ------------- Somewhat unrelated, I find this useful for benchmarking and as a baseline (what would happen if we just interleaved the shared segment). v20260605-0002-Infrastructure-for-partitioning-of-shared-.patch ------------- Just adds a small registry of partitions (ranges of shared buffers), stored in shared memory, and pg_buffercache interface to inspect it. Merely a foundation for the following patches. v20260605-0003-NUMA-shared-buffers-partitioning.patch ------------- The interesting part, that places some of the partitions to NUMA nodes. v20260605-0004-clock-sweep-basic-partitioning.patch v20260605-0005-clock-sweep-balancing-of-allocations.patch v20260605-0006-clock-sweep-scan-all-partitions.patch ------------- Patches that gradually partition clock-sweep. Ultimately, it should probably be squashed into a single commit (each commit fixes some sort of issue in the naive partitioning in 0004). But I kept them separate because it's easier to review / understand what the issue is. what changed? ------------- First, I dropped the PGPROC partitioning. We may revisit that in the future (not sure), but for now it was just a distraction and I see it as less impactful than shared buffers / clock-sweep. I also simplified the GUC to use a single on/off parameter (instead of the debug_io_direct-like approach). We can revisit that, but for now this seems more convenient. The most significant change in the remaining parts is simplification of the shared buffer partitioning. In particular, the partitioning is now "best-effort" when it maps memory to shared buffers. Let me remind that NUMA works at memory page granularity - we can't map arbitrary ranges of memory to a node, it needs to be whole memory pages. The 20251126 patch went into great lengths to (a) make sure BufferBlocks and BufferDescriptors start at memory page boundary, are the partitions are also properly aligned (both for blocks and descriptors). That was a lot of code, it needs to happen even before we know if huge pages are used, partitions might have been of (very) different sizes, and so on. The new patch abandons this "perfect" partitioning, and instead does a best-effort. It splits the buffers as evenly as possible, i.e. all partitions have (NBuffers/npartitions) buffers, and then locates as much memory as possible to a selected NUMA node. With 4K pages, that's always the whole partition. With huge pages (which is expected of relevant NUMA systems), there may be a couple buffers at the beginning/end of a partition. But it's less than one memory page, per partition, and we expect the systems to have 10s or 100s of GBs, so in the bigger scheme of things it's negligible (fractions of a percent). For buffer descriptors the math is a bit worse - descriptors need much less memory, but even there it should not be more than ~1%. Seems perfectly fine to me. Or rather, the extra complexity does not seem worth the possible benefit. This also allowed dropping a part of the "clock-sweep partitioning" patches, dealing with cases when the partitions are of different sizes. With this new best-effort scheme the difference is at most 1 buffer, and we can just ignore that. questions --------- At this point, my main question is whether there's a better way to partition clock-sweep and/or do the balancing of allocations between partitions. I believe it does work, but I have a feeling there might be a more elegant way to do this kind of stuff (like an established balancing algorithm of some sort). The other thing I need to verify is how this behaves with kernel.nr_hugepages. With some earlier versions it was easy to end in a situation where everything seemed to work, but then much later the kernel realized it does not have enough huge pages on a particular NUMA node and crashed with a segfault (or was it sigbus?). Of course, the other question is performance validation - does it even help? I plan to repeat the various experiments mentioned in this thread (by Andres and others) on available NUMA machines. But if someone has an idea for another benchmark (and/or what metric to measure, not just the usual duration), let me know. regards -- Tomas Vondra
-
Re: Adding basic NUMA awareness
Jakub Wartak <jakub.wartak@enterprisedb.com> — 2026-06-16T08:16:00Z
On Fri, Jun 5, 2026 at 2:52 PM Tomas Vondra <tomas@vondra.me> wrote: > > Hi, Hi Tomas, thanks for working on this. > Here's an updated version of the NUMA patch series, based on some recent > discussions about this (some at pgconf.dev, but not only that), [..] 1. 005 says: + * XXX We should enforce this in bufmgr.c, when initializing the partitions. + */ +#define MAX_BUFFER_PARTITIONS 32 but there isn't direct any check for checking if pg_numa_get_max_node() -> numa_max_node() is not getting higher than allowed here. In theory this could happen I think if ClockSweepPartitionIndex() would return numa = numa_node_of_cpu() on some hypothethical very high-end setup (with plenty of sub-NUMA nodes) and that would cause accesing .balance[] without bounds. 2. If we have in 004 struct ClockSweep with nextVictimBuffer, shouldn't this be padded/aligned somehow later in BufferStrategyControl which does ClockSweep sweeps[FLEXIBLE_ARRAY_MEMBER]; to avoid contention/false sharing? (comments says it should be but it doesn't seem so?), maybe the comment should be TODO for now? I have not quantified any potential benefit With pahole after some hassle I've got: struct ClockSweep { slock_t clock_sweep_lock; /* 0 1 */ /* XXX 3 bytes hole, try to pack */ int32 node; /* 4 4 */ int32 firstBuffer; /* 8 4 */ int32 numBuffers; /* 12 4 */ pg_atomic_uint32 nextVictimBuffer; /* 16 4 */ uint32 completePasses; /* 20 4 */ pg_atomic_uint32 numBufferAllocs; /* 24 4 */ pg_atomic_uint32 numRequestedAllocs; /* 28 4 */ pg_atomic_uint64 numTotalAllocs; /* 32 8 */ pg_atomic_uint64 numTotalRequestedAllocs; /* 40 8 */ uint8 balance[32]; /* 48 32 */ /* size: 80, cachelines: 2, members: 11 */ /* sum members: 77, holes: 1, sum holes: 3 */ /* last cacheline: 16 bytes */ }; maybe with smaller MAX_BUFFER_PARTITIONS we could pack this into size=64 ? 3. In 004 sched_getcpu() is used and mentioned how to check if it is available But my $0.02 (maybe not that important), but I've at least saw once where (on EC2?) some clock_gettime() was very slow and that was because it was not available in VDSO. It's usually some mix of kernel <-> arch <-> libc (not always glibc?) compatibility matrix issue. My worry is that StrategyGetBuffer() -> ChooseClockSweep() -> ClockSweepPartitionIndex() -> sched_getcpu() would be available, but slow and it would mean real syscall price (and that's not once there per buffer). I'm also somehow thinking other platforms (FreeBSD comes to mind, but I haven't checked further). The point is: wouldn't it be cheaper that to be refreshed from time to time instead otherwise we risk some slow code on non-x86_64, but I doubt how proliferated is e.g. ARM64 with NUMA.. Or alternative is to have pg_test_numa proggie and this would be measuring certain things about NUMA including timing of sched_getcpu (just like pg_test_timing does for time), at least that could explain why somebody's system/platform is slow. 4. Patch has problem (without fix for #8) that when number of available huge pages in the OS is greatly higher than shared_memory_size_in_huge_pages it will use only first NUMA node. This might be a problem when starting mulitple DBs (they will occupy first available NUMA): ### with s_b=8GB and nr_hugepages=1500 it's OK # find /sys/devices/system/node/ -name nr_hugepages -exec grep -H . {} \; | grep 2048 | sort /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages:1250 /sys/devices/system/node/node1/hugepages/hugepages-2048kB/nr_hugepages:1250 /sys/devices/system/node/node2/hugepages/hugepages-2048kB/nr_hugepages:1250 /sys/devices/system/node/node3/hugepages/hugepages-2048kB/nr_hugepages:1250 ## note the correct split below for N0/N1.. # grep huge /proc/`pgrep -f /usr/pgsql19/bin/postgres`/numa_maps 7fb1b4400000 default file=/anon_hugepage\040(deleted) huge dirty=4269 mapmax=6 N0=1250 N1=1250 N2=519 N3=1250 kernelpagesize_kB=2048 ### still s_b=8GB but nr_hugepages = 19000 (~37GB), it ends all on N0=4269 # find /sys/devices/system/node/ -name nr_hugepages -exec grep -H . {} \; | grep 2048 | sort /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages:4750 /sys/devices/system/node/node1/hugepages/hugepages-2048kB/nr_hugepages:4750 /sys/devices/system/node/node2/hugepages/hugepages-2048kB/nr_hugepages:4750 /sys/devices/system/node/node3/hugepages/hugepages-2048kB/nr_hugepages:4750 ## all on N0... # grep huge /proc/`pgrep -f /usr/pgsql19/bin/postgres`/numa_maps 7ff3a7a00000 default file=/anon_hugepage\040(deleted) huge dirty=4269 mapmax=6 N0=4269 kernelpagesize_kB=2048 I was even thinking go to lengths and add code for inspecting that /sys on some later date that the kernel NUMA hugepages are really distributed on the nodes as they should be (it's easy to end up on just 1 node out of many; allocating via sysctl -w <higher> and then <lower> allocation is easy way to force hugepages just to 1 node instead of many :o). I've hit the problem multiple times, so we should bail out if we want NUMA and the Buffer Blocks were just put on 1 node (instead of many). 5. In 005 we could mention more clealry what's the difference between those 3: numRequestedAllocs, numTotalAllocs, numTotalRequestedAllocs in the defintion to make it easier to read, maybe copy-cat those earlier descriptions there too as we already have: + * The balancing happens in intervals - it adjusts future allocations + * based on stats about recent allocations, namely: + * + * - numBufferAllocs - number of allocations served by a partition + * + * - numRequestedAllocs - number of allocatios requested in a partition 6. While at it, it would be helpful if we could reset the pg_buffercache_partitions stats in some way (pg_buffercache_partitions is very usefull).. or is there way to plug into main pg_reset functions? 7. If I add basic error checking for mbind() then it complains a lot, like below with annotated strace -ffe mbind to show the point: [pid 2856] mbind(0x7fd8d0e00000, 2145386496, MPOL_BIND, [], 0, MPOL_MF_MOVE) = -1 EINVAL (Invalid argument) WARNING: mbind(): Invalid argument WARNING: buffers descriptors for node 0 not well aligned [0x7fd8cccf5000,0x7fd8cdcf4fc1] aligned [0x7fd8cce00000,0x7fd8cdc00000] [pid 2856] mbind(0x7fd8cce00000, 14680064, MPOL_BIND, [], 0, MPOL_MF_MOVE) = -1 EINVAL (Invalid argument) WARNING: mbind(): Invalid argument WARNING: buffers for node 1 not well aligned [0x7fd950cf5000,0x7fd9d0cf5000] aligned [0x7fd950e00000,0x7fd9d0c00000] [pid 2856] mbind(0x7fd950e00000, 2145386496, MPOL_BIND, 0x5589057ded00, 1, MPOL_MF_MOVE) = -1 EINVAL (Invalid argument) WARNING: mbind(): Invalid argument WARNING: buffers descriptors for node 1 not well aligned [0x7fd8cdcf5000,0x7fd8cecf4fc1] aligned [0x7fd8cde00000,0x7fd8cec00000] [..] but with pg_numa.c fixed like below (node should be size): ret = mbind(startptr, (endptr - startptr), - MPOL_BIND, nodemask->maskp, node, MPOL_MF_MOVE); + MPOL_BIND, nodemask->maskp, nodemask->size, MPOL_MF_MOVE); it doesn't report errors anymore and suprisngly hugepages in numa_maps are altered from: 7fb1b4400000 default file=/anon_hugepage\040(deleted) huge dirty=4269 mapmax=6 N0=1250 N1=1250 N2=519 N3=1250 kernelpagesize_kB=2048 to explicit "binds": 7f8540000000 default file=/anon_hugepage\040(deleted) huge dirty=25 mapmax=6 N0=25 kernelpagesize_kB=2048 7f8543200000 bind:0 file=/anon_hugepage\040(deleted) huge dirty=7 mapmax=3 N0=7 kernelpagesize_kB=2048 7f8544000000 default file=/anon_hugepage\040(deleted) huge dirty=1 mapmax=2 N0=1 kernelpagesize_kB=2048 7f8544200000 bind:1 file=/anon_hugepage\040(deleted) huge dirty=7 mapmax=2 N1=7 kernelpagesize_kB=2048 7f8545000000 default file=/anon_hugepage\040(deleted) huge dirty=1 mapmax=2 N0=1 kernelpagesize_kB=2048 7f8545200000 bind:2 file=/anon_hugepage\040(deleted) huge dirty=7 mapmax=2 N2=7 kernelpagesize_kB=2048 7f8546000000 default file=/anon_hugepage\040(deleted) huge dirty=1 mapmax=2 N0=1 kernelpagesize_kB=2048 7f8546200000 bind:3 file=/anon_hugepage\040(deleted) huge dirty=7 mapmax=2 N3=7 kernelpagesize_kB=2048 7f8547000000 default file=/anon_hugepage\040(deleted) huge dirty=1 mapmax=3 N0=1 kernelpagesize_kB=2048 7f8547200000 bind:0 file=/anon_hugepage\040(deleted) huge dirty=1023 mapmax=2 N0=1023 kernelpagesize_kB=2048 7f85c7000000 default file=/anon_hugepage\040(deleted) huge dirty=1 N0=1 kernelpagesize_kB=2048 7f85c7200000 bind:1 file=/anon_hugepage\040(deleted) huge dirty=1023 N1=1023 kernelpagesize_kB=2048 7f8647000000 default file=/anon_hugepage\040(deleted) huge dirty=1 N1=1 kernelpagesize_kB=2048 7f8647200000 bind:2 file=/anon_hugepage\040(deleted) huge dirty=1023 N2=1023 kernelpagesize_kB=2048 7f86c7000000 default file=/anon_hugepage\040(deleted) huge dirty=1 N3=1 kernelpagesize_kB=2048 7f86c7200000 bind:3 file=/anon_hugepage\040(deleted) huge dirty=1023 N3=1023 kernelpagesize_kB=2048 7f8747000000 default file=/anon_hugepage\040(deleted) huge dirty=117 mapmax=6 N2=117 kernelpagesize_kB=2048 so lots of VMAs were created (it could affect performance in some way, I think for sure it would affect for worse fork() rates by postmaster for new conns). To me it looks like there's plenty of "N[0..3]=1" with "default" indicating single HP page being somehow left/missed in address calculations, but I haven't pressed this harder. NOTE: the patch works even without this fix, but I believe if got non-0 we cannot reliably trust the optimizer memory layout has been deployed (I suspect it's some kind luck it sharded the shm based on number of hugepages available) > questions > --------- > > At this point, my main question is whether there's a better way to > partition clock-sweep and/or do the balancing of allocations between > partitions. I believe it does work, but I have a feeling there might be > a more elegant way to do this kind of stuff (like an established > balancing algorithm of some sort). 8. The crux of this email and stuff I wanted to further discuss, when server is started with on this 4-NUMA box with * numactl --cpunodebind=0 pg_ctl start # so that all backends fork()ing will be on node#0 * the shm split onto 4 nodes properly * s_b still just 8GB (with ideal split), * DB size ~15GB with 8 pgbench partitions (and fully in VFS cache) * pgbench -c 8 -j 8 postgres -T 20 -P 1 -f seqconcurrscans.pgb with: \set num (:client_id % 8) + 1 select sum(octet_length(filler)) from pgbench_accounts_:num; * mpstat repors correctly just node#0 used a. with the patch for GUCs with numa on and defaults two clocksweep settings on, I'm getting: latency average = 3252.254 ms latency stddev = 72.011 ms b. with debug_clocksweep_balance=off, I'm realiably getting latency average = 2688.742 ms latency stddev = 61.738 ms so IMHO clocksweep partitioning is cool, but if we are discussing the current balancing logic leaves some juice on the table from the most optimized variant (~1.2x) with ~90ns:270ns (local vs remote latency). In the picture above it was 8 backends accessing 8x 1.6GB tables (lower than NBuffers / 4). Dunno if it should be optimized further, certainly we'll get reports from quick benchmarks run by people that PG 20 could be *slower* because.. well, they got (sub)optimal layout during startup (all HP on 1 node and some query hitting just that one query with local affinity and this is visible to naked eye). I was re-reading thread and Andres also wrote "We should use the partitioned clock sweep to default to using local memory as long as possible." So two further ideas: I. BufferAccessStrategy: we could derrive affinity from the BAS strategy itself, couldn't we? If we are using capped ring buffer, we could indicate that we want it just from local node as priority disregarding weights (?). Same goes for BAS_VACUUM (why would one it on remote NUMA?). With BAS_BULKREAD there would be some potential issue with sync-scan-table code though. With BAS_BULKWRITE e.g. CTAS/CREATE INDEX it makes lot of sense too. prewarm could be hacked to use some new special BAS_DISTRIBUTE or something for ideal distribution amongst all NUMA nodes. II. what if we could track if the relation is just all-local-access? Another idea is that if we would know that's it's just us working on some relation (created by us; or it's not being touched remotley) then we could ask for local-only memory affinity. So something like this: a. in case of locally-only access rels => ask for local memory first if that fails failback to weighted RR (so to to weights) b. in case other rels => weighted RR (so to to weights) directly The tracking of the fact that Buffer was accessed just locally or remotley itself is not hard to imagine (by using some free "bits" in BufferDesc. "state" where refcount/usagecount itself are stored, well at least 4 bits for my 4 nodes, but there's plenty of left there), I have some PoC for that but that's just per-Buffer tracking of "was this Buffer accessed by remote nodes", but I'm completley lost how to make transition to the is-the-relation-being-accessed-accross-NUMA-nodes info to drive such optimization (we would need some shared infra just for tracking such info; assuming up to 2^31 or 2^32 relations [OID?] and just using at least >= 2..4 bits, that's already huge number: we are talking GBs of shm mem). BTW: I've been experimenting with this patchset and added couple of things (see attached), and with I'm able to get optimal just by forcing affinity too using that earlier bench: latency average = 2512.929 ms latency stddev = 97.525 ms and that was with pure 100% affinity to my local node: select pg_buffercache_set_partition(0, '{100,0,0,0}'); debug_clocksweep_balance_recalc=off debug_clocksweep_balance=on debug_clocksweep_scan_all_partitions=on (so it's another proof that code is fine, it's just algorithm that would have to adjusted) For benchmarks with pgbench -S for 100% local affinity vs 100% remote (I can do that with that pg_buffercache_set_partition() of mine), I'm getting just +/- 1-2k TPS (42-43k TPS vs 41-42k TPS), so not much, __but__ I've spotted some another bug in from where we are fetching memory from unoptimal places if we are not on node#0, I'll need to dig into that more though. Another thing is that pgbench -S runs are much less demanding in terms of memory bandwidth used (under <1GB/s here vs 6-8GB/s for seqconcurrscans.sql using the same amout of cores). > The other thing I need to verify is how this behaves with > kernel.nr_hugepages. With some earlier versions it was easy to end in a > situation where everything seemed to work, but then much later the > kernel realized it does not have enough huge pages on a particular NUMA > node and crashed with a segfault (or was it sigbus?). It was SIGBUS and with this patchset I think we are fine: I have never witnessed this one crashing with SIGBUS. > Of course, the other question is performance validation - does it even > help? I plan to repeat the various experiments mentioned in this thread > (by Andres and others) on available NUMA machines. But if someone has an > idea for another benchmark (and/or what metric to measure, not just the > usual duration), let me know. See above, but I think we would have to fix at at least: mbind() failure, and those VMAs disconnected regions. -J. -
Re: Adding basic NUMA awareness
Tomas Vondra <tomas@vondra.me> — 2026-06-16T12:39:45Z
On 6/16/26 10:16, Jakub Wartak wrote: > On Fri, Jun 5, 2026 at 2:52 PM Tomas Vondra <tomas@vondra.me> wrote: >> >> Hi, > > Hi Tomas, thanks for working on this. > >> Here's an updated version of the NUMA patch series, based on some recent >> discussions about this (some at pgconf.dev, but not only that), > [..] > > 1. 005 says: > > + * XXX We should enforce this in bufmgr.c, when initializing the partitions. > + */ > +#define MAX_BUFFER_PARTITIONS 32 > > but there isn't direct any check for checking if pg_numa_get_max_node() -> > numa_max_node() is not getting higher than allowed here. In theory this could > happen I think if ClockSweepPartitionIndex() would return > numa = numa_node_of_cpu() > on some hypothethical very high-end setup (with plenty of sub-NUMA nodes) > and that would cause accesing .balance[] without bounds. > Yes, this should be capped to the MAX_BUFFER_PARTITIONS. > 2. If we have in 004 struct ClockSweep with nextVictimBuffer, shouldn't > this be padded/aligned somehow later in BufferStrategyControl which does > ClockSweep sweeps[FLEXIBLE_ARRAY_MEMBER]; > to avoid contention/false sharing? (comments says it should be but it > doesn't seem so?), maybe the comment should be TODO for now? I have not > quantified any potential benefit > > With pahole after some hassle I've got: > struct ClockSweep { > slock_t clock_sweep_lock; /* 0 1 */ > > /* XXX 3 bytes hole, try to pack */ > > int32 node; /* 4 4 */ > int32 firstBuffer; /* 8 4 */ > int32 numBuffers; /* 12 4 */ > pg_atomic_uint32 nextVictimBuffer; /* 16 4 */ > uint32 completePasses; /* 20 4 */ > pg_atomic_uint32 numBufferAllocs; /* 24 4 */ > pg_atomic_uint32 numRequestedAllocs; /* 28 4 */ > pg_atomic_uint64 numTotalAllocs; /* 32 8 */ > pg_atomic_uint64 numTotalRequestedAllocs; /* 40 8 */ > uint8 balance[32]; /* 48 32 */ > > /* size: 80, cachelines: 2, members: 11 */ > /* sum members: 77, holes: 1, sum holes: 3 */ > /* last cacheline: 16 bytes */ > }; > maybe with smaller MAX_BUFFER_PARTITIONS we could pack this into size=64 ? > Possibly. Im not entirely happy with making the ClockSweep struct so much larger, but I haven't found a better way to store the counters needed for balancing. The only thing I can think of is storing it outside the struct, and maybe that's the right thing to do. But that assumes the current balancing approach is the right one. > 3. In 004 sched_getcpu() is used and mentioned how to check if it is available > > But my $0.02 (maybe not that important), but I've at least saw once where > (on EC2?) some clock_gettime() was very slow and that was because it was not > available in VDSO. It's usually some mix of kernel <-> arch <-> libc (not > always glibc?) compatibility matrix issue. My worry is that StrategyGetBuffer() > -> ChooseClockSweep() -> ClockSweepPartitionIndex() -> sched_getcpu() would be > available, but slow and it would mean real syscall price (and that's not once > there per buffer). I'm also somehow thinking other platforms (FreeBSD comes to > mind, but I haven't checked further). The point is: wouldn't it be cheaper > that to be refreshed from time to time instead otherwise we risk some slow > code on non-x86_64, but I doubt how proliferated is e.g. ARM64 with NUMA.. > Or alternative is to have pg_test_numa proggie and this would be measuring > certain things about NUMA including timing of sched_getcpu (just like > pg_test_timing does for time), at least that could explain why somebody's > system/platform is slow. > Yes, I think we may need some sort of caching for this / check only sometimes. I haven't seen it to matter, but that may be luck and on other systems / platforms it may be worse. > 4. Patch has problem (without fix for #8) that when number of available huge > pages in the OS is greatly higher than shared_memory_size_in_huge_pages it > will use only first NUMA node. This might be a problem when starting mulitple > DBs (they will occupy first available NUMA): > > ### with s_b=8GB and nr_hugepages=1500 it's OK > > # find /sys/devices/system/node/ -name nr_hugepages -exec grep -H . {} > \; | grep 2048 | sort > /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages:1250 > /sys/devices/system/node/node1/hugepages/hugepages-2048kB/nr_hugepages:1250 > /sys/devices/system/node/node2/hugepages/hugepages-2048kB/nr_hugepages:1250 > /sys/devices/system/node/node3/hugepages/hugepages-2048kB/nr_hugepages:1250 > > ## note the correct split below for N0/N1.. > # grep huge /proc/`pgrep -f /usr/pgsql19/bin/postgres`/numa_maps > 7fb1b4400000 default file=/anon_hugepage\040(deleted) huge dirty=4269 > mapmax=6 N0=1250 N1=1250 N2=519 N3=1250 kernelpagesize_kB=2048 > > ### still s_b=8GB but nr_hugepages = 19000 (~37GB), it ends all on N0=4269 > # find /sys/devices/system/node/ -name nr_hugepages -exec grep -H . {} > \; | grep 2048 | sort > /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages:4750 > /sys/devices/system/node/node1/hugepages/hugepages-2048kB/nr_hugepages:4750 > /sys/devices/system/node/node2/hugepages/hugepages-2048kB/nr_hugepages:4750 > /sys/devices/system/node/node3/hugepages/hugepages-2048kB/nr_hugepages:4750 > ## all on N0... > # grep huge /proc/`pgrep -f /usr/pgsql19/bin/postgres`/numa_maps > 7ff3a7a00000 default file=/anon_hugepage\040(deleted) huge dirty=4269 > mapmax=6 N0=4269 kernelpagesize_kB=2048 > > I was even thinking go to lengths and add code for inspecting that /sys on > some later date that the kernel NUMA hugepages are really distributed > on the nodes as they should be (it's easy to end up on just 1 node out of > many; allocating via sysctl -w <higher> and then <lower> allocation is easy > way to force hugepages just to 1 node instead of many :o). I've hit the > problem multiple times, so we should bail out if we want NUMA and the > Buffer Blocks were just put on 1 node (instead of many). > How come the pg_numa_bind_to_node() calls don't move the parts to the correct node? If something is already using huge pages on the other nodes, then sure, it will fail. But I think that's OK - it's a best-effort thing. Maybe we should exit instead in this case? > 5. In 005 we could mention more clealry what's the difference between > those 3: numRequestedAllocs, numTotalAllocs, numTotalRequestedAllocs > in the defintion to make it easier to read, maybe copy-cat those earlier > descriptions there too as we already have: > > + * The balancing happens in intervals - it adjusts future allocations > + * based on stats about recent allocations, namely: > + * > + * - numBufferAllocs - number of allocations served by a partition > + * > + * - numRequestedAllocs - number of allocatios requested in a partition > I agree the explanation for this is not entirely clear. > 6. While at it, it would be helpful if we could reset the > pg_buffercache_partitions stats in some way (pg_buffercache_partitions > is very usefull).. or is there way to plug into main pg_reset functions? > Hmm, I was afraid it'd interfere with the balancing. I'm not sure it makes sense to reset just some of the fields - it'd make it much harder to interpret the counters. I'll think abou this. > 7. If I add basic error checking for mbind() then it complains a lot, like > below with annotated strace -ffe mbind to show the point: > > [pid 2856] mbind(0x7fd8d0e00000, 2145386496, MPOL_BIND, [], 0, > MPOL_MF_MOVE) = -1 EINVAL (Invalid argument) > WARNING: mbind(): Invalid argument > WARNING: buffers descriptors for node 0 not well aligned > [0x7fd8cccf5000,0x7fd8cdcf4fc1] aligned > [0x7fd8cce00000,0x7fd8cdc00000] > > [pid 2856] mbind(0x7fd8cce00000, 14680064, MPOL_BIND, [], 0, > MPOL_MF_MOVE) = -1 EINVAL (Invalid argument) > WARNING: mbind(): Invalid argument > WARNING: buffers for node 1 not well aligned > [0x7fd950cf5000,0x7fd9d0cf5000] aligned > [0x7fd950e00000,0x7fd9d0c00000] > > [pid 2856] mbind(0x7fd950e00000, 2145386496, MPOL_BIND, > 0x5589057ded00, 1, MPOL_MF_MOVE) = -1 EINVAL (Invalid argument) > WARNING: mbind(): Invalid argument > WARNING: buffers descriptors for node 1 not well aligned > [0x7fd8cdcf5000,0x7fd8cecf4fc1] aligned > [0x7fd8cde00000,0x7fd8cec00000] > [..] > > but with pg_numa.c fixed like below (node should be size): > ret = mbind(startptr, (endptr - startptr), > - MPOL_BIND, nodemask->maskp, node, MPOL_MF_MOVE); > + MPOL_BIND, nodemask->maskp, > nodemask->size, MPOL_MF_MOVE); > I think this is a silly bug on my side, clearly the nodemask should have size > 0 even for node 0. > it doesn't report errors anymore and suprisngly hugepages in numa_maps are > altered from: > 7fb1b4400000 default file=/anon_hugepage\040(deleted) huge dirty=4269 > mapmax=6 N0=1250 N1=1250 N2=519 N3=1250 kernelpagesize_kB=2048 > > to explicit "binds": > 7f8540000000 default file=/anon_hugepage\040(deleted) huge dirty=25 > mapmax=6 N0=25 kernelpagesize_kB=2048 > 7f8543200000 bind:0 file=/anon_hugepage\040(deleted) huge dirty=7 > mapmax=3 N0=7 kernelpagesize_kB=2048 > 7f8544000000 default file=/anon_hugepage\040(deleted) huge dirty=1 > mapmax=2 N0=1 kernelpagesize_kB=2048 > 7f8544200000 bind:1 file=/anon_hugepage\040(deleted) huge dirty=7 > mapmax=2 N1=7 kernelpagesize_kB=2048 > 7f8545000000 default file=/anon_hugepage\040(deleted) huge dirty=1 > mapmax=2 N0=1 kernelpagesize_kB=2048 > 7f8545200000 bind:2 file=/anon_hugepage\040(deleted) huge dirty=7 > mapmax=2 N2=7 kernelpagesize_kB=2048 > 7f8546000000 default file=/anon_hugepage\040(deleted) huge dirty=1 > mapmax=2 N0=1 kernelpagesize_kB=2048 > 7f8546200000 bind:3 file=/anon_hugepage\040(deleted) huge dirty=7 > mapmax=2 N3=7 kernelpagesize_kB=2048 > 7f8547000000 default file=/anon_hugepage\040(deleted) huge dirty=1 > mapmax=3 N0=1 kernelpagesize_kB=2048 > 7f8547200000 bind:0 file=/anon_hugepage\040(deleted) huge dirty=1023 > mapmax=2 N0=1023 kernelpagesize_kB=2048 > 7f85c7000000 default file=/anon_hugepage\040(deleted) huge dirty=1 > N0=1 kernelpagesize_kB=2048 > 7f85c7200000 bind:1 file=/anon_hugepage\040(deleted) huge dirty=1023 > N1=1023 kernelpagesize_kB=2048 > 7f8647000000 default file=/anon_hugepage\040(deleted) huge dirty=1 > N1=1 kernelpagesize_kB=2048 > 7f8647200000 bind:2 file=/anon_hugepage\040(deleted) huge dirty=1023 > N2=1023 kernelpagesize_kB=2048 > 7f86c7000000 default file=/anon_hugepage\040(deleted) huge dirty=1 > N3=1 kernelpagesize_kB=2048 > 7f86c7200000 bind:3 file=/anon_hugepage\040(deleted) huge dirty=1023 > N3=1023 kernelpagesize_kB=2048 > 7f8747000000 default file=/anon_hugepage\040(deleted) huge dirty=117 > mapmax=6 N2=117 kernelpagesize_kB=2048 > > so lots of VMAs were created (it could affect performance in some way, I think > for sure it would affect for worse fork() rates by postmaster for new conns). > Isn't this 4 VMAs for buffers ad 4 VMAs for buffer descriptors? Or maybe it's the parts that could not be mapped due to insufficient alignment? > To me it looks like there's plenty of "N[0..3]=1" with "default" indicating > single HP page being somehow left/missed in address calculations, but I > haven't pressed this harder. > > NOTE: the patch works even without this fix, but I believe if got non-0 we > cannot reliably trust the optimizer memory layout has been deployed (I suspect > it's some kind luck it sharded the shm based on number of hugepages available) > I'm not sure I understand what you mean. What non-0? > >> questions >> --------- >> >> At this point, my main question is whether there's a better way to >> partition clock-sweep and/or do the balancing of allocations between >> partitions. I believe it does work, but I have a feeling there might be >> a more elegant way to do this kind of stuff (like an established >> balancing algorithm of some sort). > > > 8. The crux of this email and stuff I wanted to further discuss, when server > is started with on this 4-NUMA box with > * numactl --cpunodebind=0 pg_ctl start # so that all backends fork()ing will be > on node#0 > * the shm split onto 4 nodes properly > * s_b still just 8GB (with ideal split), > * DB size ~15GB with 8 pgbench partitions (and fully in VFS cache) > * pgbench -c 8 -j 8 postgres -T 20 -P 1 -f seqconcurrscans.pgb with: > \set num (:client_id % 8) + 1 > select sum(octet_length(filler)) from pgbench_accounts_:num; > * mpstat repors correctly just node#0 used > > a. with the patch for GUCs with numa on and defaults two clocksweep settings > on, I'm getting: > > latency average = 3252.254 ms > latency stddev = 72.011 ms > > b. with debug_clocksweep_balance=off, I'm realiably getting > > latency average = 2688.742 ms > latency stddev = 61.738 ms > > so IMHO clocksweep partitioning is cool, but if we are discussing the current > balancing logic leaves some juice on the table from the most optimized variant > (~1.2x) with ~90ns:270ns (local vs remote latency). In the picture above it > was 8 backends accessing 8x 1.6GB tables (lower than NBuffers / 4). > How does this compare to master, i.e. without these NUMA patches? > Dunno if it should be optimized further, certainly we'll get reports from > quick benchmarks run by people that PG 20 could be *slower* because.. well, > they got (sub)optimal layout during startup (all HP on 1 node and some > query hitting just that one query with local affinity and this is visible > to naked eye). I was re-reading thread and Andres also wrote "We should use > the partitioned clock sweep to default to using local memory as long as > possible." > > So two further ideas: > > I. BufferAccessStrategy: we could derrive affinity from the BAS strategy > itself, couldn't we? If we are using capped ring buffer, we could indicate > that we want it just from local node as priority disregarding weights (?). > Same goes for BAS_VACUUM (why would one it on remote NUMA?). With BAS_BULKREAD > there would be some potential issue with sync-scan-table code though. > With BAS_BULKWRITE e.g. CTAS/CREATE INDEX it makes lot of sense too. > prewarm could be hacked to use some new special BAS_DISTRIBUTE or something > for ideal distribution amongst all NUMA nodes. > Yes, I think it might make sense to disable balancing in these cases. > II. what if we could track if the relation is just all-local-access? > > Another idea is that if we would know that's it's just us working on some > relation (created by us; or it's not being touched remotley) then we could > ask for local-only memory affinity. So something like this: > > a. in case of locally-only access rels => > ask for local memory first > if that fails failback to weighted RR (so to to weights) > b. in case other rels => weighted RR (so to to weights) directly > > The tracking of the fact that Buffer was accessed just locally or remotley > itself is not hard to imagine (by using some free "bits" in BufferDesc. > "state" where refcount/usagecount itself are stored, well at least 4 bits > for my 4 nodes, but there's plenty of left there), I have some PoC for > that but that's just per-Buffer tracking of "was this Buffer accessed by > remote nodes", but I'm completley lost how to make transition to the > is-the-relation-being-accessed-accross-NUMA-nodes info to drive such > optimization (we would need some shared infra just for tracking such info; > assuming up to 2^31 or 2^32 relations [OID?] and just using at least >= > 2..4 bits, that's already huge number: we are talking GBs of shm mem). >> BTW: I've been experimenting with this patchset and added couple of things > (see attached), and with I'm able to get optimal just by forcing affinity > too using that earlier bench: > > latency average = 2512.929 ms > latency stddev = 97.525 ms > > and that was with pure 100% affinity to my local node: > > select pg_buffercache_set_partition(0, '{100,0,0,0}'); > debug_clocksweep_balance_recalc=off > debug_clocksweep_balance=on > debug_clocksweep_scan_all_partitions=on > > (so it's another proof that code is fine, it's just algorithm that would > have to adjusted) > > For benchmarks with pgbench -S for 100% local affinity vs 100% remote > (I can do that with that pg_buffercache_set_partition() of mine), I'm > getting just +/- 1-2k TPS (42-43k TPS vs 41-42k TPS), so not much, __but__ > I've spotted some another bug in from where we are fetching memory from > unoptimal places if we are not on node#0, I'll need to dig into that more > though. Another thing is that pgbench -S runs are much less demanding in > terms of memory bandwidth used (under <1GB/s here vs 6-8GB/s for > seqconcurrscans.sql using the same amout of cores). > No opinion. I need to look at this closer. >> The other thing I need to verify is how this behaves with >> kernel.nr_hugepages. With some earlier versions it was easy to end in a >> situation where everything seemed to work, but then much later the >> kernel realized it does not have enough huge pages on a particular NUMA >> node and crashed with a segfault (or was it sigbus?). > > It was SIGBUS and with this patchset I think we are fine: I have never > witnessed this one crashing with SIGBUS. > Good. But I wonder if allocating just the precise number of huge pages (per shared_memory_size_in_huge_pages) can prevent moving the partitions to the correct node. >> Of course, the other question is performance validation - does it even >> help? I plan to repeat the various experiments mentioned in this thread >> (by Andres and others) on available NUMA machines. But if someone has an >> idea for another benchmark (and/or what metric to measure, not just the >> usual duration), let me know. > > See above, but I think we would have to fix at at least: mbind() failure, > and those VMAs disconnected regions. > Yes, the mbind() failure is a bug. regards -- Tomas Vondra -
Re: Adding basic NUMA awareness
Jakub Wartak <jakub.wartak@enterprisedb.com> — 2026-06-17T12:13:03Z
On Tue, Jun 16, 2026 at 2:39 PM Tomas Vondra <tomas@vondra.me> wrote: > > > > On 6/16/26 10:16, Jakub Wartak wrote: > > On Fri, Jun 5, 2026 at 2:52 PM Tomas Vondra <tomas@vondra.me> wrote: > >> > >> Hi, > > > > Hi Tomas, thanks for working on this. > > > >> Here's an updated version of the NUMA patch series, based on some recent > >> discussions about this (some at pgconf.dev, but not only that), > > [..] > > > > 1. 005 says: > > > > + * XXX We should enforce this in bufmgr.c, when initializing the partitions. > > + */ > > +#define MAX_BUFFER_PARTITIONS 32 > > > > but there isn't direct any check for checking if pg_numa_get_max_node() -> > > numa_max_node() is not getting higher than allowed here. In theory this could > > happen I think if ClockSweepPartitionIndex() would return > > numa = numa_node_of_cpu() > > on some hypothethical very high-end setup (with plenty of sub-NUMA nodes) > > and that would cause accesing .balance[] without bounds. > > > > Yes, this should be capped to the MAX_BUFFER_PARTITIONS. > > > 2. If we have in 004 struct ClockSweep with nextVictimBuffer, shouldn't > > this be padded/aligned somehow later in BufferStrategyControl which does > > ClockSweep sweeps[FLEXIBLE_ARRAY_MEMBER]; > > to avoid contention/false sharing? (comments says it should be but it > > doesn't seem so?), maybe the comment should be TODO for now? I have not > > quantified any potential benefit > > > > With pahole after some hassle I've got: > > struct ClockSweep { [..] > > pg_atomic_uint32 nextVictimBuffer; /* 16 4 */ [..] > > /* size: 80, cachelines: 2, members: 11 */ > > /* sum members: 77, holes: 1, sum holes: 3 */ > > /* last cacheline: 16 bytes */ > > }; > > maybe with smaller MAX_BUFFER_PARTITIONS we could pack this into size=64 ? > > > > Possibly. Im not entirely happy with making the ClockSweep struct so > much larger, but I haven't found a better way to store the counters > needed for balancing. The only thing I can think of is storing it > outside the struct, and maybe that's the right thing to do. > > But that assumes the current balancing approach is the right one. Yeah, I'm just not sure if there is some wasted performance due to false-sharing in very heavy benchmark scenarios (in theory the nextVictimBuffer could bounce rather heavily). > > 3. In 004 sched_getcpu() is used and mentioned how to check if it is available > > > > But my $0.02 (maybe not that important), but I've at least saw once where > > (on EC2?) some clock_gettime() was very slow and that was because it was not > > available in VDSO. It's usually some mix of kernel <-> arch <-> libc (not > > always glibc?) compatibility matrix issue. My worry is that StrategyGetBuffer() > > -> ChooseClockSweep() -> ClockSweepPartitionIndex() -> sched_getcpu() would be > > available, but slow and it would mean real syscall price (and that's not once > > there per buffer). I'm also somehow thinking other platforms (FreeBSD comes to > > mind, but I haven't checked further). The point is: wouldn't it be cheaper > > that to be refreshed from time to time instead otherwise we risk some slow > > code on non-x86_64, but I doubt how proliferated is e.g. ARM64 with NUMA.. > > Or alternative is to have pg_test_numa proggie and this would be measuring > > certain things about NUMA including timing of sched_getcpu (just like > > pg_test_timing does for time), at least that could explain why somebody's > > system/platform is slow. > > > > Yes, I think we may need some sort of caching for this / check only > sometimes. I haven't seen it to matter, but that may be luck and on > other systems / platforms it may be worse. Okay, so maybe an action point for us much later would be to try this on 2s+ ARM or some other much more rare setup just to see if we need to do it at all (perhaps annotate it with TODO, I have short memory ;)) > > 4. Patch has problem (without fix for #8) that when number of available huge > > pages in the OS is greatly higher than shared_memory_size_in_huge_pages it > > will use only first NUMA node. This might be a problem when starting mulitple > > DBs (they will occupy first available NUMA): > > > > ### with s_b=8GB and nr_hugepages=1500 it's OK > > > > # find /sys/devices/system/node/ -name nr_hugepages -exec grep -H . {} > > \; | grep 2048 | sort > > /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages:1250 > > /sys/devices/system/node/node1/hugepages/hugepages-2048kB/nr_hugepages:1250 > > /sys/devices/system/node/node2/hugepages/hugepages-2048kB/nr_hugepages:1250 > > /sys/devices/system/node/node3/hugepages/hugepages-2048kB/nr_hugepages:1250 > > > > ## note the correct split below for N0/N1.. > > # grep huge /proc/`pgrep -f /usr/pgsql19/bin/postgres`/numa_maps > > 7fb1b4400000 default file=/anon_hugepage\040(deleted) huge dirty=4269 > > mapmax=6 N0=1250 N1=1250 N2=519 N3=1250 kernelpagesize_kB=2048 > > > > ### still s_b=8GB but nr_hugepages = 19000 (~37GB), it ends all on N0=4269 > > # find /sys/devices/system/node/ -name nr_hugepages -exec grep -H . {} > > \; | grep 2048 | sort > > /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages:4750 > > /sys/devices/system/node/node1/hugepages/hugepages-2048kB/nr_hugepages:4750 > > /sys/devices/system/node/node2/hugepages/hugepages-2048kB/nr_hugepages:4750 > > /sys/devices/system/node/node3/hugepages/hugepages-2048kB/nr_hugepages:4750 > > ## all on N0... > > # grep huge /proc/`pgrep -f /usr/pgsql19/bin/postgres`/numa_maps > > 7ff3a7a00000 default file=/anon_hugepage\040(deleted) huge dirty=4269 > > mapmax=6 N0=4269 kernelpagesize_kB=2048 > > > > I was even thinking go to lengths and add code for inspecting that /sys on > > some later date that the kernel NUMA hugepages are really distributed > > on the nodes as they should be (it's easy to end up on just 1 node out of > > many; allocating via sysctl -w <higher> and then <lower> allocation is easy > > way to force hugepages just to 1 node instead of many :o). I've hit the > > problem multiple times, so we should bail out if we want NUMA and the > > Buffer Blocks were just put on 1 node (instead of many). > > > > How come the pg_numa_bind_to_node() calls don't move the parts to the > correct node? Well, if we were not checking error code mbind() it could behave in non-deterministic way I think (you ask for MPOL_BIND, maybe it does something, maybe it doesnt work but we continued anyway). > If something is already using huge pages on the other nodes, then sure, > it will fail. But I think that's OK - it's a best-effort thing. Maybe we > should exit instead in this case? Yes, I think we should exit with FATAL. > > 5. In 005 we could mention more clealry what's the difference between > > those 3: numRequestedAllocs, numTotalAllocs, numTotalRequestedAllocs > > in the defintion to make it easier to read, maybe copy-cat those earlier > > descriptions there too as we already have: > > > > + * The balancing happens in intervals - it adjusts future allocations > > + * based on stats about recent allocations, namely: > > + * > > + * - numBufferAllocs - number of allocations served by a partition > > + * > > + * - numRequestedAllocs - number of allocatios requested in a partition > > > > I agree the explanation for this is not entirely clear. > > > 6. While at it, it would be helpful if we could reset the > > pg_buffercache_partitions stats in some way (pg_buffercache_partitions > > is very usefull).. or is there way to plug into main pg_reset functions? > > > > Hmm, I was afraid it'd interfere with the balancing. I'm not sure it > makes sense to reset just some of the fields - it'd make it much harder > to interpret the counters. I'll think abou this. Well, small hint: I find the the view very, very usefull in explaining where/why memory is being allocated from. Maybe if we want to include it in final version, then it might be worth (later on) to do it, if that won't be included I'm fine just doing \watch 1 in psql to see what's happening. Another hint is that maybe it doesnt belong to pg_buffercache and would make it easier to integrate into pg_stat_reset*() -- dunno, how/ if extension can plug into it. > > 7. If I add basic error checking for mbind() then it complains a lot, like > > below with annotated strace -ffe mbind to show the point: > >[..] > > I think this is a silly bug on my side, clearly the nodemask should have > size > 0 even for node 0. > > > it doesn't report errors anymore and suprisngly hugepages in numa_maps are > > altered from: > > 7fb1b4400000 default file=/anon_hugepage\040(deleted) huge dirty=4269 > > mapmax=6 N0=1250 N1=1250 N2=519 N3=1250 kernelpagesize_kB=2048 > > > > to explicit "binds": > > 7f8540000000 default file=/anon_hugepage\040(deleted) huge dirty=25 > > mapmax=6 N0=25 kernelpagesize_kB=2048 > > 7f8543200000 bind:0 file=/anon_hugepage\040(deleted) huge dirty=7 > > mapmax=3 N0=7 kernelpagesize_kB=2048 > > 7f8544000000 default file=/anon_hugepage\040(deleted) huge dirty=1 > > mapmax=2 N0=1 kernelpagesize_kB=2048 > > 7f8544200000 bind:1 file=/anon_hugepage\040(deleted) huge dirty=7 > > mapmax=2 N1=7 kernelpagesize_kB=2048 > > 7f8545000000 default file=/anon_hugepage\040(deleted) huge dirty=1 > > mapmax=2 N0=1 kernelpagesize_kB=2048 > > 7f8545200000 bind:2 file=/anon_hugepage\040(deleted) huge dirty=7 > > mapmax=2 N2=7 kernelpagesize_kB=2048 > > 7f8546000000 default file=/anon_hugepage\040(deleted) huge dirty=1 > > mapmax=2 N0=1 kernelpagesize_kB=2048 > > 7f8546200000 bind:3 file=/anon_hugepage\040(deleted) huge dirty=7 > > mapmax=2 N3=7 kernelpagesize_kB=2048 > > 7f8547000000 default file=/anon_hugepage\040(deleted) huge dirty=1 > > mapmax=3 N0=1 kernelpagesize_kB=2048 > > 7f8547200000 bind:0 file=/anon_hugepage\040(deleted) huge dirty=1023 > > mapmax=2 N0=1023 kernelpagesize_kB=2048 > > 7f85c7000000 default file=/anon_hugepage\040(deleted) huge dirty=1 > > N0=1 kernelpagesize_kB=2048 > > 7f85c7200000 bind:1 file=/anon_hugepage\040(deleted) huge dirty=1023 > > N1=1023 kernelpagesize_kB=2048 > > 7f8647000000 default file=/anon_hugepage\040(deleted) huge dirty=1 > > N1=1 kernelpagesize_kB=2048 > > 7f8647200000 bind:2 file=/anon_hugepage\040(deleted) huge dirty=1023 > > N2=1023 kernelpagesize_kB=2048 > > 7f86c7000000 default file=/anon_hugepage\040(deleted) huge dirty=1 > > N3=1 kernelpagesize_kB=2048 > > 7f86c7200000 bind:3 file=/anon_hugepage\040(deleted) huge dirty=1023 > > N3=1023 kernelpagesize_kB=2048 > > 7f8747000000 default file=/anon_hugepage\040(deleted) huge dirty=117 > > mapmax=6 N2=117 kernelpagesize_kB=2048 > > > > so lots of VMAs were created (it could affect performance in some way, I think > > for sure it would affect for worse fork() rates by postmaster for new conns). > > > > Isn't this 4 VMAs for buffers ad 4 VMAs for buffer descriptors? Or maybe > it's the parts that could not be mapped due to insufficient alignment? Yes, like with patchset I'm getting WARNINGs: WARNING: buffers for node 0 not well aligned [0x7efc100f5000,0x7efc900f5000] aligned [0x7efc10200000,0x7efc90000000] WARNING: buffers descriptors for node 0 not well aligned [0x7efc0c0f4f80,0x7efc0d0f4f41] aligned [0x7efc0c200000,0x7efc0d000000] WARNING: buffers for node 1 not well aligned [0x7efc900f5000,0x7efd100f5000] aligned [0x7efc90200000,0x7efd10000000] WARNING: buffers descriptors for node 1 not well aligned [0x7efc0d0f4f80,0x7efc0e0f4f41] aligned [0x7efc0d200000,0x7efc0e000000] WARNING: buffers for node 2 not well aligned [0x7efd100f5000,0x7efd900f5000] aligned [0x7efd10200000,0x7efd90000000] WARNING: buffers descriptors for node 2 not well aligned [0x7efc0e0f4f80,0x7efc0f0f4f41] aligned [0x7efc0e200000,0x7efc0f000000] WARNING: buffers for node 3 not well aligned [0x7efd900f5000,0x7efe100f5000] aligned [0x7efd90200000,0x7efe10000000] WARNING: buffers descriptors for node 3 not well aligned [0x7efc0f0f4f80,0x7efc100f4f41] aligned [0x7efc0f200000,0x7efc10000000] LOG: starting PostgreSQL 19beta1 on x86_64-linux, compiled by gcc-12.2.0, 64-bit relevant numa_maps for postmaster: 7efc0c200000 bind:0 file=/anon_hugepage\040(deleted) huge dirty=7 mapmax=2 N0=7 kernelpagesize_kB=2048 7efc0d000000 default file=/anon_hugepage\040(deleted) huge dirty=1 mapmax=2 N3=1 kernelpagesize_kB=2048 7efc0d200000 bind:1 file=/anon_hugepage\040(deleted) huge dirty=7 mapmax=2 N1=7 kernelpagesize_kB=2048 7efc0e000000 default file=/anon_hugepage\040(deleted) huge dirty=1 mapmax=2 N3=1 kernelpagesize_kB=2048 7efc0e200000 bind:2 file=/anon_hugepage\040(deleted) huge dirty=7 mapmax=2 N2=7 kernelpagesize_kB=2048 7efc0f000000 default file=/anon_hugepage\040(deleted) huge dirty=1 mapmax=2 N3=1 kernelpagesize_kB=2048 7efc0f200000 bind:3 file=/anon_hugepage\040(deleted) huge dirty=7 mapmax=2 N3=7 kernelpagesize_kB=2048 7efc10000000 default file=/anon_hugepage\040(deleted) huge dirty=1 mapmax=3 N3=1 kernelpagesize_kB=2048 7efc10200000 bind:0 file=/anon_hugepage\040(deleted) huge dirty=1023 N0=1023 kernelpagesize_kB=2048 7efc90000000 default file=/anon_hugepage\040(deleted) huge dirty=1 N3=1 kernelpagesize_kB=2048 7efc90200000 bind:1 file=/anon_hugepage\040(deleted) huge dirty=1023 N1=1023 kernelpagesize_kB=2048 7efd10000000 default file=/anon_hugepage\040(deleted) huge dirty=1 N0=1 kernelpagesize_kB=2048 7efd10200000 bind:2 file=/anon_hugepage\040(deleted) huge dirty=1023 N2=1023 kernelpagesize_kB=2048 7efd90000000 default file=/anon_hugepage\040(deleted) huge dirty=1 N2=1 kernelpagesize_kB=2048 7efd90200000 bind:3 file=/anon_hugepage\040(deleted) huge dirty=1023 N3=1023 kernelpagesize_kB=2048 7efe10000000 default file=/anon_hugepage\040(deleted) huge dirty=116 mapmax=6 N1=116 kernelpagesize_kB=2048 so if You take just 7efc10200000..7efc90000000 (from buffers@node0, 1st line) and zoom in/grep You'll get: 7efc10200000 bind:0 file=/anon_hugepage\040(deleted) huge dirty=1023 N0=1023 kernelpagesize_kB=2048 7efc90000000 default file=/anon_hugepage\040(deleted) huge dirty=1 N3=1 kernelpagesize_kB=2048 so @ 7efc90000000 it's Node3=1 hugepage (so it's wrong by 1x 2048kb offset) so it contains the tail of node 0 buffers and the head of node - and is excluded from both mbind() calls. At first I've spotted this in 003/ BufferPartitionsInit() with: cstartptr = (char *) &BufferDescriptors[buff_first]; endptr = (char *) &BufferDescriptors[buff_last] + 1; // <-- BUG? with endptr = (char *) &BufferDescriptors[buff_last + 1]; but got the same issue, so I've forced the mbind() to use 2x TYPEALIGN_DOWN to cover with policy everything and got: 7fc134e00000 default file=/anon_hugepage\040(deleted) huge dirty=24 mapmax=6 N3=24 kernelpagesize_kB=2048 7fc137e00000 bind:0 file=/anon_hugepage\040(deleted) huge dirty=8 mapmax=4 N0=8 kernelpagesize_kB=2048 7fc138e00000 bind:1 file=/anon_hugepage\040(deleted) huge dirty=8 mapmax=2 N1=8 kernelpagesize_kB=2048 7fc139e00000 bind:2 file=/anon_hugepage\040(deleted) huge dirty=8 mapmax=3 N2=8 kernelpagesize_kB=2048 7fc13ae00000 bind:3 file=/anon_hugepage\040(deleted) huge dirty=8 mapmax=2 N3=8 kernelpagesize_kB=2048 7fc13be00000 bind:0 file=/anon_hugepage\040(deleted) huge dirty=1024 N0=1024 kernelpagesize_kB=2048 7fc1bbe00000 bind:1 file=/anon_hugepage\040(deleted) huge dirty=1024 N1=1024 kernelpagesize_kB=2048 7fc23be00000 bind:2 file=/anon_hugepage\040(deleted) huge dirty=1024 mapmax=2 N2=1024 kernelpagesize_kB=2048 7fc2bbe00000 bind:3 file=/anon_hugepage\040(deleted) huge dirty=1024 N3=1024 kernelpagesize_kB=2048 7fc33be00000 default file=/anon_hugepage\040(deleted) huge dirty=116 mapmax=6 N1=116 kernelpagesize_kB=2048 that was with: @@ -351,7 +351,7 @@ BufferPartitionsInit(void) } /* best effort: align the pointers, so that the mbind() works */ - startptr = (char *) TYPEALIGN(numa_page_size, startptr); + startptr = (char *) TYPEALIGN_DOWN(numa_page_size, startptr); endptr = (char *) TYPEALIGN_DOWN(numa_page_size, endptr); @@ -374,7 +374,7 @@ BufferPartitionsInit(void) } /* best effort: align the pointers, so that the mbind() works */ - startptr = (char *) TYPEALIGN(numa_page_size, startptr); + startptr = (char *) TYPEALIGN_DOWN(numa_page_size, startptr); endptr = (char *) TYPEALIGN_DOWN(numa_page_size, endptr); Looks way better, but it was wild guess (TBH I think I've prefered the previous patchset version where the input shm addresses were already aligned, but if You and others say it's easier route then sure) > > To me it looks like there's plenty of "N[0..3]=1" with "default" indicating > > single HP page being somehow left/missed in address calculations, but I > > haven't pressed this harder. > > > > NOTE: the patch works even without this fix, but I believe if got non-0 we > > cannot reliably trust the optimizer memory layout has been deployed (I suspect > > it's some kind luck it sharded the shm based on number of hugepages available) > > > > I'm not sure I understand what you mean. What non-0? Non-0 return code from mbind() as the mbind() failed with -1, but it did somehow alter numa pages probably(?), but without creating specific "isolated" VMA explicit "bind" policy, probably maybe it did not even work at all.. > > > >> questions > >> --------- > >> > >> At this point, my main question is whether there's a better way to > >> partition clock-sweep and/or do the balancing of allocations between > >> partitions. I believe it does work, but I have a feeling there might be > >> a more elegant way to do this kind of stuff (like an established > >> balancing algorithm of some sort). > > > > > > 8. The crux of this email and stuff I wanted to further discuss, when server > > is started with on this 4-NUMA box with > > * numactl --cpunodebind=0 pg_ctl start # so that all backends fork()ing will be > > on node#0 > > * the shm split onto 4 nodes properly > > * s_b still just 8GB (with ideal split), > > * DB size ~15GB with 8 pgbench partitions (and fully in VFS cache) > > * pgbench -c 8 -j 8 postgres -T 20 -P 1 -f seqconcurrscans.pgb with: > > \set num (:client_id % 8) + 1 > > select sum(octet_length(filler)) from pgbench_accounts_:num; > > * mpstat repors correctly just node#0 used > > > > a. with the patch for GUCs with numa on and defaults two clocksweep settings > > on, I'm getting: > > > > latency average = 3252.254 ms > > latency stddev = 72.011 ms > > > > b. with debug_clocksweep_balance=off, I'm realiably getting > > > > latency average = 2688.742 ms > > latency stddev = 61.738 ms > > > > so IMHO clocksweep partitioning is cool, but if we are discussing the current > > balancing logic leaves some juice on the table from the most optimized variant > > (~1.2x) with ~90ns:270ns (local vs remote latency). In the picture above it > > was 8 backends accessing 8x 1.6GB tables (lower than NBuffers / 4). > > > > How does this compare to master, i.e. without these NUMA patches? I had some problems comparing, but with "perfect setup" that includes the following: - pgbench/clients on node#0 - backends running on node#0 - hugepages memory on node#0..3, but with with this patchset and those goodies: debug_clocksweep_balance=on debug_clocksweep_balance_recalc=off debug_clocksweep_scan_all_partitions=on (so technically node0 backends accessing just Buffers / Buffers Desc from node0, technically node0 weights: "{100,0,0,0}") - with today's new discovery for me that Linux's kernel VFS cache is also having also first-touch (!) NUMA policy and really important here (so VFS cached data also alters results of testing wildly!), so I had to force unloading VFS cache and force-loading it into node#0, I've was getting for seqconcurrscans: latency average = 2701.705 ms latency stddev = 111.608 ms vs master, huh, but which scenario? the default one without any affinity? - assuming you get the split shm split like "N0=1059 N1=1299 N2=879 N3=1031" but that's appeneded-only (so not interleaved (?)) and you even risk having shm placed on just __one__ node (if it is big enough and free enough) - if you go straight to benchmarking it (CPU hits random nodes) latency average = 3439.200 ms latency stddev = 580.501 ms - with backends forked() to node#0 (numactl --cpunodebind=0 pg_ctl start) latency average = 4937.543 ms latency stddev = 573.841 ms because of random VFS cache placement (I imagine it as flow of on node0 CPUs a. nextVictimBuffer contention b. getBuffer() - fetch random shm memory from random NUMA node c. pread() - fetch from VFS cache but from *remote* NUMA node ) - with backends forked() to node#0 and pinned VFS cached fully on node#0 too latency average = 4518.651 ms latency stddev = 797.369 ms (but this is still Buffers from other nodes) - same as above above and numactl interleaved shm: latency average = 3792.016 ms latency stddev = 825.186 ms - same as above and interleaved shm, but without pining to CPUs on specifc node and ensure random VFS cache vs nodes: latency average = 2913.813 ms latency stddev = 352.552 ms - but the moment you read anything reads base/ into VFS cache to particular node (imagine pg_prewarm or even just tar) assuming it was not there it also pins that to that node memory and you'll get: latency average = 3594.180 ms latency stddev = 851.949 ms > > Dunno if it should be optimized further, certainly we'll get reports from > > quick benchmarks run by people that PG 20 could be *slower* because.. well, > > they got (sub)optimal layout during startup (all HP on 1 node and some > > query hitting just that one query with local affinity and this is visible > > to naked eye). I was re-reading thread and Andres also wrote "We should use > > the partitioned clock sweep to default to using local memory as long as > > possible." > > > > So two further ideas: > > > > I. BufferAccessStrategy: we could derrive affinity from the BAS strategy > > itself, couldn't we? If we are using capped ring buffer, we could indicate > > that we want it just from local node as priority disregarding weights (?). > > Same goes for BAS_VACUUM (why would one it on remote NUMA?). With BAS_BULKREAD > > there would be some potential issue with sync-scan-table code though. > > With BAS_BULKWRITE e.g. CTAS/CREATE INDEX it makes lot of sense too. > > prewarm could be hacked to use some new special BAS_DISTRIBUTE or something > > for ideal distribution amongst all NUMA nodes. > > > > Yes, I think it might make sense to disable balancing in these cases. OK, I did not code anything of that as > > II. what if we could track if the relation is just all-local-access? > > > > Another idea is that if we would know that's it's just us working on some > > relation (created by us; or it's not being touched remotley) then we could > > ask for local-only memory affinity. So something like this: > > > > a. in case of locally-only access rels => > > ask for local memory first > > if that fails failback to weighted RR (so to to weights) > > b. in case other rels => weighted RR (so to to weights) directly > > > > The tracking of the fact that Buffer was accessed just locally or remotley > > itself is not hard to imagine (by using some free "bits" in BufferDesc. > > "state" where refcount/usagecount itself are stored, well at least 4 bits > > for my 4 nodes, but there's plenty of left there), I have some PoC for > > that but that's just per-Buffer tracking of "was this Buffer accessed by > > remote nodes", but I'm completley lost how to make transition to the > > is-the-relation-being-accessed-accross-NUMA-nodes info to drive such > > optimization (we would need some shared infra just for tracking such info; > > assuming up to 2^31 or 2^32 relations [OID?] and just using at least >= > > 2..4 bits, that's already huge number: we are talking GBs of shm mem). > >> BTW: I've been experimenting with this patchset and added couple of things > > (see attached), and with I'm able to get optimal just by forcing affinity > > too using that earlier bench: > > > > latency average = 2512.929 ms > > latency stddev = 97.525 ms > > > > and that was with pure 100% affinity to my local node: > > > > select pg_buffercache_set_partition(0, '{100,0,0,0}'); > > debug_clocksweep_balance_recalc=off > > debug_clocksweep_balance=on > > debug_clocksweep_scan_all_partitions=on > > > > (so it's another proof that code is fine, it's just algorithm that would > > have to adjusted) > > > > For benchmarks with pgbench -S for 100% local affinity vs 100% remote > > (I can do that with that pg_buffercache_set_partition() of mine), I'm > > getting just +/- 1-2k TPS (42-43k TPS vs 41-42k TPS), so not much, __but__ > > I've spotted some another bug in from where we are fetching memory from > > unoptimal places if we are not on node#0, I'll need to dig into that more > > though. Another thing is that pgbench -S runs are much less demanding in > > terms of memory bandwidth used (under <1GB/s here vs 6-8GB/s for > > seqconcurrscans.sql using the same amout of cores). > > > > No opinion. I need to look at this closer. Great ! > >> The other thing I need to verify is how this behaves with > >> kernel.nr_hugepages. With some earlier versions it was easy to end in a > >> situation where everything seemed to work, but then much later the > >> kernel realized it does not have enough huge pages on a particular NUMA > >> node and crashed with a segfault (or was it sigbus?). > > > > It was SIGBUS and with this patchset I think we are fine: I have never > > witnessed this one crashing with SIGBUS. > > > > Good. But I wonder if allocating just the precise number of huge pages > (per shared_memory_size_in_huge_pages) can prevent moving the partitions > to the correct node. > Not sure I understand (?) how's that related to SIGBUS? -J. -
Re: Adding basic NUMA awareness
Tomas Vondra <tomas@vondra.me> — 2026-06-24T20:26:29Z
Hi, Here's an updated patch series, with only minor changes to fix the mbind issues: 1) It uses the correct nodemask size, so that the mbind actually binds the partition to the right node. 2) It aligns the start/end pointers so that there no pages are left with the default memory policy. So now there should be only the bind:N entries, not the single-page "default" ones. This means the last page of a partition can be mapped to a different node, but that seems fine (in the end it could have happened with the old approach too). I've also included Jakub's "goodies" patch with the additional GUCs. Those seem potentially useful to development. I have some results from a new round of benchmarks, and it's a bit disappointing. Or rather, there seem to be some issues that I can't figure out, causing regressions. Consider a very simple test, doing a lot of sequential scans to put a fair amount of pressure on the clocksweep / buffer replacement. There's a .tgz with the benchmark script attached, but it does about this: * Initialize a pgbench database with scale 2000 (so ~30GB, about twice the shared buffers). * Uses --partitions=100, so that the partitions are small enough not to trigger the 1/4 threshold (i.e. not use circular buffers). * Does runs with custom script, forcing sequential scans of the table, with two queries: select count(1) from pgbench_accounts; select * from pgbench_accounts offset 1000000000; Those are called "count" and "offset" in the results. The script forces serial sequential scans (no index scans, no parallelism), and does runs with 1, 8 and 32 clients (this is an old-ish xeon with 44 physical cores on two sockets, 2 NUMA nodes). I did runs with "master" and the all the 7 patches, with the NUMA stuff enabled/disabled since 0003 (which adds it). See the two PDFs with more complete results, but here's the "count" query for a subset of the patches (the omitted ones behave similarly to what's shown here). This chart is for median latency (in milliseconds): clients master 0003 0004 0003/on 0004/on ------------------------------------------------------------- 1 12767 12582 14509 12807 15307 8 14383 14355 14149 14069 16165 32 14756 15198 14836 14984 17128 -------------------------------------------------------- 1 103% 114% 100% 120% 8 101% 98% 98% 112% 32 102% 101% 102% 116% The percentages are compared to "master", the columns with "/on" are with shared_buffers_numa=on. Clearly, there's no chance with 0003 (which binds shared buffer partitions to NUMA nodes, even if that's enabled). The differences are within noise, pretty much, for all client counts. Then 0004 gets applied, which partitions the clock sweep. And well, that doesn't go particularly well. There is a bit of a regression even with numa=off, but it kinda recovers with the following patches. But with numa=on, there's a consistent ~10% regression (give or take). I've spent a fair bit of time investigating what's causing this, but so far I have nothing. I assume it's something silly in the patches partitioning the clocksweep, or maybe the approach is flawed in some way. Not sure :-( regards -- Tomas Vondra -
Re: Adding basic NUMA awareness
Jakub Wartak <jakub.wartak@enterprisedb.com> — 2026-06-25T12:19:36Z
On Wed, Jun 24, 2026 at 10:29 PM Tomas Vondra <tomas@vondra.me> wrote: Hi, > Here's an updated patch series, with only minor changes to fix the mbind > issues: [..] > I've also included Jakub's "goodies" patch with the additional GUCs. > Those seem potentially useful to development. Cool! > I have some results from a new round of benchmarks, and it's a bit > disappointing. Or rather, there seem to be some issues that I can't > figure out, causing regressions. [..] > This chart is for median latency (in milliseconds): > > clients master 0003 0004 0003/on 0004/on > ------------------------------------------------------------- > 1 12767 12582 14509 12807 15307 > 8 14383 14355 14149 14069 16165 > 32 14756 15198 14836 14984 17128 > -------------------------------------------------------- > 1 103% 114% 100% 120% > 8 101% 98% 98% 112% > 32 102% 101% 102% 116% > I haven't tried it yet, however I can spot some things: No crystal clear idea why, but in the script I can see that you have io_method = io_uring and are not dropping_caches, so IMHO it is too complex interaction at this stage. One hint: such setup is going to be problematic for proving numbers. On the meeting I've tried to describe that I've been using io_method = sync instead of 'worker' to get more predicitable results (together with echo 3 > drop_caches), because then it is that backend's CPU/$NODE doing that pread()/pwrite() -- or any other operating performing the load -- it is going to put that file onto that_specific_$NODE -- so even if you have sequence like: pgbench -i pg_ctl restart pgbench -c XX then pgbench -i even with shared_buffers_numa=on will spread into many nodes the Buffers, yet after the restart the VFS cache portion of the data will still reside on single specific $NODE that wrote it to the filesystem (due to local-first-tocuh-affinity even for VFS cache), so any further reading: VFS cache --pread()--> s_b will take the hit of remote interconnect with some probablity depending on where the new backends are running. Also with worker it is even worse as we have those memory queue in between. I think we even can have this: file in VFS cache @ node0 --because of first touch policy (pgbench -i/prewarm) io worker @ node1 --hits latency from node0 and node2 shm io worker queue @ node2 --well client backend @ node 3 --puts into shm io worker on node2 Therefore I'm sticking to 'sync' to ease the pain... but with uring, I suspect the situation is kind of similiar as we call io_uring_submit(), and we may endup using io-wq kernel threads, and we have those submission/receive (memory) queues that are located somewhere (that is on some node) too. I think, we simply lack affinity for IO/NUMA for all io modes except sync, but it's too early I suspect and way outside of scope for this $thread. I've started thinking about it just last week, so... (but hopefully I'll be able to ship helper fscachenuma.c to show layout of file across VFS caches on nodes next week I hope) Maybe some other suggestions: Q1) Maybe some crosschecks first? # balance should be equal between nodes even for baseline # linux kernel has tendency to fit shm into one if it fits find /sys/devices/system/node*/ -name 'free_hugepages' -exec grep -H . {} \; # check N0 and N1 even for default policy, might also reveal imbalance # lots of RAM and too big huge_pages allows fitting whole shm into just N0 # see point 4 from [1] grep /anon_h /proc/$SOMEREALBACKENDPID/numa_maps # then during pgbench -c run maybe those: mpstat -N ALL 1 perf stat -a -e uncore_imc/cas_count_read/,uncore_imc/cas_count_write/ \ --per-socket -I 1000 # or -M memory_bandwidth_read,memory_bandwidth_write (it might reveal that problem I've described above about io_method: even with pgbench -c 1 you might be reading from all sockets/wrong sockets instead of the correct one with affinity) I like to pin CPUs to just one node for pgbench -c <NUMBER_OF_CPUS/NUMBER_OF_NODES> [to saturate one node only] and start server also with CPU pining [or use this debug_numa_node to force] to that one node and cross-check what's being read (using perf) and usually I have to disarm clock balancing and override weights using pg_buffercache_set_partition() to also force weight to stay local only - only then I'm able to outrun master. That's how this idea was born that if we are only working on node $N with some relations then let's use only node $N's Buffers. But I have 90us:~280us local vs remote latency, so it's probably way easier for me to see results even without disabling CPU-idle-states/turboboost/etc. Q2) Dunno, but 0007 is not changing anything in runtime and you get huge discrepeancy results when going 0006 -> 0007 for clients=1 (see 128% -> 112%). Literally, as the same code but different rebuild (ELF image) would be having vastly different layout enough to cause perf issues? Hopefully next week I'll try to repro those numbers to see if I can help more. -J. [1] - https://www.postgresql.org/message-id/CAKZiRmzo9xnJSgO4b26DTZqPuObcQ-6ncay%2BmOEKs9rzCkegUA%40mail.gmail.com -
Re: Adding basic NUMA awareness
Tomas Vondra <tomas@vondra.me> — 2026-06-25T13:49:00Z
On 6/25/26 14:19, Jakub Wartak wrote: > On Wed, Jun 24, 2026 at 10:29 PM Tomas Vondra <tomas@vondra.me> wrote: > > Hi, > >> Here's an updated patch series, with only minor changes to fix the mbind >> issues: > [..] >> I've also included Jakub's "goodies" patch with the additional GUCs. >> Those seem potentially useful to development. > > Cool! > >> I have some results from a new round of benchmarks, and it's a bit >> disappointing. Or rather, there seem to be some issues that I can't >> figure out, causing regressions. > [..] >> This chart is for median latency (in milliseconds): >> >> clients master 0003 0004 0003/on 0004/on >> ------------------------------------------------------------- >> 1 12767 12582 14509 12807 15307 >> 8 14383 14355 14149 14069 16165 >> 32 14756 15198 14836 14984 17128 >> -------------------------------------------------------- >> 1 103% 114% 100% 120% >> 8 101% 98% 98% 112% >> 32 102% 101% 102% 116% >> > > I haven't tried it yet, however I can spot some things: > > No crystal clear idea why, but in the script I can see that you have > io_method = io_uring and are not dropping_caches, so IMHO it is too complex > interaction at this stage. > By caches I assume you mean page cache? The test is meant so simulate a cached system, copying data between shared buffers and page cache. My expectation is that once we start hitting I/O, it'll completely hide most differences due to NUMA. > One hint: such setup is going to be problematic for proving numbers. > On the meeting I've tried to describe that I've been using io_method = sync > instead of 'worker' to get more predicitable results (together with echo 3 >> drop_caches), because then it is that backend's CPU/$NODE doing that > pread()/pwrite() -- or any other operating performing the load -- > it is going to put that file onto that_specific_$NODE -- > so even if you have sequence like: > pgbench -i > pg_ctl restart > pgbench -c XX > Hmm, I missed that point during the meeting. I wonder if "worker" is interacting with the NUMA somehow (I mean, does it load it into the right node?). But I'm using io_uring, and it's not clear to me why sync would be better for benchmarking? Ultimately, we need to make sure it works well with io_uring anyway, right? Even if "sync" happens to be better for benchmarking (or even for NUMA stuff), we have to make it work with worker/io_uring. Because that's what practical systems use. > then pgbench -i even with shared_buffers_numa=on will spread into many > nodes the Buffers, yet after the restart the VFS cache portion of the data > will still reside on single specific $NODE that wrote it to the filesystem > (due to local-first-tocuh-affinity even for VFS cache), so any further reading: > VFS cache --pread()--> s_b will take the hit of remote interconnect with > some probablity depending on where the new backends are running. Also > with worker it is even worse as we have those memory queue in between. I > think we even can have this: > > file in VFS cache @ node0 --because of first touch policy (pgbench -i/prewarm) > io worker @ node1 --hits latency from node0 and node2 > shm io worker queue @ node2 --well > client backend @ node 3 --puts into shm io worker on node2 > > Therefore I'm sticking to 'sync' to ease the pain... but with uring, I suspect > the situation is kind of similiar as we call io_uring_submit(), and we > may endup using io-wq kernel threads, and we have those submission/receive > (memory) queues that are located somewhere (that is on some node) too. > > I think, we simply lack affinity for IO/NUMA for all io modes except sync, but > it's too early I suspect and way outside of scope for this $thread. I've > started thinking about it just last week, so... (but hopefully I'll be able > to ship helper fscachenuma.c to show layout of file across VFS caches on nodes > next week I hope) > Ah, you're suggesting the page cache stuff will be placed on a single NUMA node? That may be true, it's a good point. And maybe it could skew the results in a bad way. Still, that would be the case even without the NUMA partitioning, no? > Maybe some other suggestions: > > Q1) Maybe some crosschecks first? > # balance should be equal between nodes even for baseline > # linux kernel has tendency to fit shm into one if it fits > find /sys/devices/system/node*/ -name 'free_hugepages' -exec > grep -H . {} \; > > # check N0 and N1 even for default policy, might also reveal imbalance > # lots of RAM and too big huge_pages allows fitting whole shm > into just N0 > # see point 4 from [1] > grep /anon_h /proc/$SOMEREALBACKENDPID/numa_maps > > # then during pgbench -c run maybe those: > mpstat -N ALL 1 > perf stat -a -e uncore_imc/cas_count_read/,uncore_imc/cas_count_write/ \ > --per-socket -I 1000 # or -M > memory_bandwidth_read,memory_bandwidth_write > > (it might reveal that problem I've described above about io_method: > even with pgbench -c 1 you might be reading from all sockets/wrong sockets > instead of the correct one with affinity) > I'll try, but if you could try running some experiments on your own, that might be helpful. > I like to pin CPUs to just one node for pgbench -c > <NUMBER_OF_CPUS/NUMBER_OF_NODES> > [to saturate one node only] and start server also with CPU pining > [or use this debug_numa_node to force] to that one node and cross-check > what's being read (using perf) and usually I have to disarm clock balancing > and override weights using pg_buffercache_set_partition() to also force > weight to stay local only - only then I'm able to outrun master. That's > how this idea was born that if we are only working on node $N with > some relations > then let's use only node $N's Buffers. But I have 90us:~280us > local vs remote > latency, so it's probably way easier for me to see results even without > disabling CPU-idle-states/turboboost/etc. > > Q2) Dunno, but 0007 is not changing anything in runtime and you get huge > discrepeancy results when going 0006 -> 0007 for clients=1 (see > 128% -> 112%). > Literally, as the same code but different rebuild (ELF image) > would be having > vastly different layout enough to cause perf issues? > > Hopefully next week I'll try to repro those numbers to see if I can > help more. > Thank you! That'd be great. regards -- Tomas Vondra -
Re: Adding basic NUMA awareness
Jakub Wartak <jakub.wartak@enterprisedb.com> — 2026-06-29T07:42:49Z
On Thu, Jun 25, 2026 at 3:49 PM Tomas Vondra <tomas@vondra.me> wrote: > > >> I have some results from a new round of benchmarks, and it's a bit > >> disappointing. Or rather, there seem to be some issues that I can't > >> figure out, causing regressions. > > [..] > >> This chart is for median latency (in milliseconds): > >> > >> clients master 0003 0004 0003/on 0004/on > >> ------------------------------------------------------------- > >> 1 12767 12582 14509 12807 15307 > >> 8 14383 14355 14149 14069 16165 > >> 32 14756 15198 14836 14984 17128 > >> -------------------------------------------------------- > >> 1 103% 114% 100% 120% > >> 8 101% 98% 98% 112% > >> 32 102% 101% 102% 116% > >> > > > > I haven't tried it yet, however I can spot some things: > > > > No crystal clear idea why, but in the script I can see that you have > > io_method = io_uring and are not dropping_caches, so IMHO it is too complex > > interaction at this stage. > > > > By caches I assume you mean page cache? The test is meant so simulate a > cached system, copying data between shared buffers and page cache. My > expectation is that once we start hitting I/O, it'll completely hide > most differences due to NUMA. No, it wont completley hide it, those differences at least here still matter (AFAIR right now like +/- 10% here) > > One hint: such setup is going to be problematic for proving numbers. > > On the meeting I've tried to describe that I've been using io_method = sync > > instead of 'worker' to get more predicitable results (together with echo 3 > >> drop_caches), because then it is that backend's CPU/$NODE doing that > > pread()/pwrite() -- or any other operating performing the load -- > > it is going to put that file onto that_specific_$NODE -- > > so even if you have sequence like: > > pgbench -i > > pg_ctl restart > > pgbench -c XX > > > > Hmm, I missed that point during the meeting. I wonder if "worker" is > interacting with the NUMA somehow (I mean, does it load it into the > right node?). But I'm using io_uring, and it's not clear to me why sync > would be better for benchmarking? > > Ultimately, we need to make sure it works well with io_uring anyway, > right? Even if "sync" happens to be better for benchmarking (or even for > NUMA stuff), we have to make it work with worker/io_uring. Because > that's what practical systems use. Yes, we need to make work with more advanced, but I don't think we are there yet (we'll need some more patches in orde rto demonstrate it reliably). > > then pgbench -i even with shared_buffers_numa=on will spread into many > > nodes the Buffers, yet after the restart the VFS cache portion of the data > > will still reside on single specific $NODE that wrote it to the filesystem > > (due to local-first-tocuh-affinity even for VFS cache), > > [.. blabla , use io_method=sync ] > > > > Ah, you're suggesting the page cache stuff will be placed on a single > NUMA node? That may be true, it's a good point. And maybe it could skew > the results in a bad way. I've just published [0], see for yourself: This happens especiall after pgbench -i, so: pgbench -i # pagecache placement on one NUMA node pg_ctl restart pgbench -c XX is day and night different than let's say: pgbench -i echo 3 > drop_caches pg_ctl restart pgbench -c XX # pagecache placement happens by many backends # potentially many NUMA nodes > Still, that would be the case even without the NUMA partitioning, no? Right, in my experience we should not benchmark against master started with the default pg_ctl (that's is without numactl --interleave=all) because it is confusing to reason about it due how the s_b could laid out without that interleaving. I mean later we can switch to that default, but IMHO not yet. > > Maybe some other suggestions: > > > > Q1) Maybe some crosschecks first? > > # balance should be equal between nodes even for baseline > > # linux kernel has tendency to fit shm into one if it fits > > find /sys/devices/system/node*/ -name 'free_hugepages' -exec > > grep -H . {} \; > > > > # check N0 and N1 even for default policy, might also reveal imbalance > > # lots of RAM and too big huge_pages allows fitting whole shm > > into just N0 > > # see point 4 from [1] > > grep /anon_h /proc/$SOMEREALBACKENDPID/numa_maps > > > > # then during pgbench -c run maybe those: > > mpstat -N ALL 1 > > perf stat -a -e uncore_imc/cas_count_read/,uncore_imc/cas_count_write/ \ > > --per-socket -I 1000 # or -M > > memory_bandwidth_read,memory_bandwidth_write > > > > (it might reveal that problem I've described above about io_method: > > even with pgbench -c 1 you might be reading from all sockets/wrong sockets > > instead of the correct one with affinity) > > > > I'll try, but if you could try running some experiments on your own, > that might be helpful. [..] > > Hopefully next week I'll try to repro those numbers to see if I can > > help more. > > > > Thank you! That'd be great. Yeah, I'll try my best, we'll see how it goes. Right now I've just dropped that fscachenuma proggie to aid us in troubleshooting. -J. [0] - https://github.com/jakubwartakEDB/fscachenuma -
Re: Adding basic NUMA awareness
Jakub Wartak <jakub.wartak@enterprisedb.com> — 2026-06-30T12:51:18Z
On Mon, Jun 29, 2026 at 9:42 AM Jakub Wartak <jakub.wartak@enterprisedb.com> wrote: > > On Thu, Jun 25, 2026 at 3:49 PM Tomas Vondra <tomas@vondra.me> wrote: > > > > >> I have some results from a new round of benchmarks, and it's a bit > > >> disappointing. Or rather, there seem to be some issues that I can't > > >> figure out, causing regressions. > > > [..] > > >> This chart is for median latency (in milliseconds): > > >> > > >> clients master 0003 0004 0003/on 0004/on > > >> ------------------------------------------------------------- > > >> 1 12767 12582 14509 12807 15307 > > >> 8 14383 14355 14149 14069 16165 > > >> 32 14756 15198 14836 14984 17128 > > >> -------------------------------------------------------- > > >> 1 103% 114% 100% 120% > > >> 8 101% 98% 98% 112% > > >> 32 102% 101% 102% 116% > > >> [..lots of variables..] > > I'll try, but if you could try running some experiments on your own, > > that might be helpful. > [..] > > > Hopefully next week I'll try to repro those numbers to see if I can > > > help more. > > > > > > > Thank you! That'd be great. > > Yeah, I'll try my best, we'll see how it goes. Right now I've just dropped > that fscachenuma proggie to aid us in troubleshooting. > > -J. > > [0] - https://github.com/jakubwartakEDB/fscachenuma Hi Tomas, OK, so I've run couple of tests and modified run.sh and also tried to fix some inefficiencies spotted while testing this. Note the attached performance matrix is in TPS (so more is better). Raw results/CSV and scripts are attached too. * run2 = 2 workloads, partitioned pgbench_accounts * run3 = just pgbenchS w/o partitioning + warmup * run4 = semi-like pgbenchS w/o partitioning but 100k rows + warmup One important modification in those run shell scripts is that they clean page-cache (drop_cached) as mentioned earlier to avoid false results where everything would on node#N after pgbench -i ran. Probably I did not get any regressions you've got, because of this. Or better diff -u run*.sh scripts. The "inst-optimized" is just the same patchset (so "inst-patchset") + crude attempt in 0008 to make further smooth out things and avoid regressions while I've been working on this. 0008 does couple of things: a. implements CPU/node caching instead quering it every single buffer. Even if on x86_64 that is optimized by vdso/kernel to avoid the real syscall, the semi-syscall tax seems to be visible when fetching lots of buffers. 128 is arbitrary and still kind of low (128*8kB=1MB, and we are doing hundreths of MB/s; while rescheduling happened only every couple of seconds). b1. minimize the attempt to use other partittions till some threshold ( and then it relies on the scan-all-partitions) b2. avoids selecting idle partitions (defined as avg_allocs/2) - if there are low allocations there it is debatable if cache utilization is better or sticking to lower latency is better (e.g. in some workloads buffer reuse is close to 0, so lower latency is clearly better) Results are attached, some observations: 0.There were vast differences in how pg_ctl is started (interleaved or not), so I've decided in the end to show relative to both situations. 1.In run2/seqconcurrscans I've saturated my interconnect and that's why it's giving 129-155% there. I don't have access physiscal hw, but I suspect that modern 2socket EPYC5 has like ~614GB/s per socket RAM bandwidth, but the max oneway bandwith of the interconnect is around ~220GB/s ( no way to provie it), so *IF* with hundreths of cores we would be able fetch at this rate we could saturte modern hardware too that way (and we birefly touched related topic: batched executor, accelerating it so fast those effects could be more easily achieveable) 2.run3 has no partiitioning because according to perf and my eyes, it spent time not on the buffers itself (thus it was way heavier on CPU [partitioning] than on memory...), so that's how run3 was born without partitions :D 3.The warmup is critical for run3/pgbenchS, as I've noticed that depending on ${luck} if you start the "master" (baseline w/o interleaving) and pgbench it right away everything might land on node0 (s_b, pagecache), so "master" was basically cheating in benchmarks vs especially Your's patchset where it was spreading way too soon. Having drop_caches, additional warump and only then proper pgbench kind of reduces that luck-factor. In general I think all runs with c=1 seem to have kind of low singal-to-noise ratio. I was thinking about pinning to always stick to the same NUMA node from start to win against master just for this c=1 scenarios, but "meh". 3b. in short for pgbench -S we can gain like 2-5% 4.run4 was made just to prove that workload fetching more buffers, than the standard pgbench -S (1 row?), seems to be the key to prove optimizations in 0008 (other than showing good benefits for seqconcurrscans of course). So run4 just shows benefit compared to 0001-0007 alone. Stil on the table: 1. maybe even better balancing is possible (?), but this one is seems enough? I'm out of other ideas, well other than the "shared-relation-use-by-foreign-node" idea described much earlier (but I won't be able to pull that off), so I'm not entering this rabbit hole any deeper. 2. Digging into io_method=worker optimizations (answering question: are they necessary?) Maybe I'll throw in run5 quite soon, this is going to be crucial to answer. 3. Potentially mentioned earlier BAS strategies (forcing just use of local partitions for known-to-be-only-local-users: CTAS/VACCUM/etc), but I'm afarid that's not for me as I would certainly break/violate some invisible to me boundary. Maybe You could run those run*.sh with master vs inst-patchset/optimized? (I'm not sure, maybe there's even different factor at play too...) -J. -
Re: Adding basic NUMA awareness
Jakub Wartak <jakub.wartak@enterprisedb.com> — 2026-07-02T09:24:21Z
On Tue, Jun 30, 2026 at 2:51 PM Jakub Wartak <jakub.wartak@enterprisedb.com> wrote: > > On Mon, Jun 29, 2026 at 9:42 AM Jakub Wartak > <jakub.wartak@enterprisedb.com> wrote: > > > > On Thu, Jun 25, 2026 at 3:49 PM Tomas Vondra <tomas@vondra.me> wrote: > > > > > > >> I have some results from a new round of benchmarks, and it's a bit > > > >> disappointing. Or rather, there seem to be some issues that I can't > > > >> figure out, causing regressions. > > > > [..] > > > >> This chart is for median latency (in milliseconds): > > > >> > > > >> clients master 0003 0004 0003/on 0004/on > > > >> ------------------------------------------------------------- > > > >> 1 12767 12582 14509 12807 15307 > > > >> 8 14383 14355 14149 14069 16165 > > > >> 32 14756 15198 14836 14984 17128 > > > >> -------------------------------------------------------- > > > >> 1 103% 114% 100% 120% > > > >> 8 101% 98% 98% 112% > > > >> 32 102% 101% 102% 116% > > > >> > > [..lots of variables..] > > > > I'll try, but if you could try running some experiments on your own, > > > that might be helpful. > > [..] > > > > Hopefully next week I'll try to repro those numbers to see if I can > > > > help more. > > > > > > > > > > Thank you! That'd be great. > > > > Yeah, I'll try my best, we'll see how it goes. Right now I've just dropped > > that fscachenuma proggie to aid us in troubleshooting. > > > > -J. > > > > [0] - https://github.com/jakubwartakEDB/fscachenuma > > Hi Tomas, > > OK, so I've run couple of tests and modified run.sh and also tried to fix > some inefficiencies spotted while testing this. Note the attached > performance matrix is in TPS (so more is better). Raw results/CSV and > scripts are attached too. > > * run2 = 2 workloads, partitioned pgbench_accounts > * run3 = just pgbenchS w/o partitioning + warmup > * run4 = semi-like pgbenchS w/o partitioning but 100k rows + warmup > [..] > > Stil on the table: > > 1. maybe even better balancing is possible (?), but this one is seems enough? > I'm out of other ideas, well other than the > "shared-relation-use-by-foreign-node" idea described much earlier (but > I won't be able to pull that off), so I'm not entering this rabbit hole > any deeper. See below, seems like not needed (?) > 2. Digging into io_method=worker optimizations (answering question: are they > necessary?) Maybe I'll throw in run5 quite soon, this is going to be > crucial to answer. OK, I'm attaching are results from mine runs 5 and 6: - only seqconcurrscans was tested, well because for other workloads io_worker method was not getting load for those workers (only seq scans were offloaded) - checksums were disabled, because IMHO that would be unfair comparision (AFAIR there are offloaded) - those optimizations for 0008 "optimized (numa=on, bal=on)" easily beat "patched (numa=on, bal=on)" and seem to be crucial. We get like 1.2x-1.4x across every io_method, but only with 0008. - even when then doing just those logical fully cached reads from fully VFS cached case, io_urings shines (I've added raw TPS number to show this, compare across tables e.g. io_uring vs sync 13.378/8.993=1.487x for io_uring with NUMA, but for master's for io_uring:sync it was just 8.79/7.389 = 1.189x without NUMA); seems like io_uring is more lightweight to show more benefits of remote memory latencies - there's some more juice to get out of the balancer for 0-reuse workloads (but IMHO it's pointless to squeeze more, it's hard already) - I was probably wrong when expecting that io_worker's worker processes/queues should get NUMA affinity. They don't need to be apparently for me to see benefits (maybe they could be and it would even better, but meh). So with ruling io_method impact (I speculated earlier that his could be it), this means that you were either hitting lack of opimizations needed from 0008 or were impacted by lack of drop_caches before the runs > Maybe You could run those run*.sh with master vs inst-patchset/optimized? > (I'm not sure, maybe there's even different factor at play too...) This is seems to be crucial now, to double confirm the results / loaded-tested on your hw with 0008. (but that hardware really needs to have effective latency difference between at least 2 NUMA nodes -- Intel's mlc is good for this); maybe also tweak those 125% inside 0008 to some other values, I've got 4 nodes, so 100/4=25%) > 3. Potentially mentioned earlier BAS strategies (forcing just use of local > partitions for known-to-be-only-local-users: CTAS/VACCUM/etc), but I'm > afarid that's not for me as I would certainly break/violate some > invisible to me boundary. And this one is still potentially on the table as nice thing to have. -J.