source
flyology_numa
0.1.0-devDevelopment onlyMemory-node topology reporting for Ada
0.1.0-dev
DevelopmentSource documentation
README
flyology_numa
Reports the memory-node structure of the host to Ada programs.
A machine with more than one processor package usually attaches memory to each of them. Memory attached to the package a thread runs on is reached faster than memory attached to another. This crate reports which memory nodes the host has, which processors are attached to each, how far apart the host declares them to be, and which of them the running process may use.
This crate is standalone. It does not depend on the Flyology runtime, and the runtime does not depend on it.
Flyology_NUMA reports the structure, Flyology_NUMA.Placement acts on it,
and Flyology_NUMA.Pools puts it where Ada already expects to be told where
memory comes from — the storage pool of an access type.
Flyology is experimental. See Boundaries.
Reading the host
with Ada.Text_IO;
with Flyology_NUMA;
procedure Show_Nodes is
package NUMA renames Flyology_NUMA;
begin
for Node of NUMA.Allowed_Nodes loop
Ada.Text_IO.Put_Line
("node" & NUMA.Node_Id'Image (Node)
& " has" & Natural'Image (NUMA.Count (NUMA.Processors_Of (Node)))
& " processors");
end loop;
end Show_Nodes;
Every query is answerable on every supported host. A host with no
memory-node structure reports one node holding every processor, because such
a host genuinely has one memory domain. Support reports whether the
structure was read from the host or is that single-domain description:
if NUMA.Support.Source = NUMA.Single_Domain then
-- Nothing here to place memory across.
null;
end if;
What the host may not answer
Individual facts can be missing even when the node structure was read. A query that the host does not answer reports that, rather than a plausible number:
type Byte_Query (Available : Boolean := False) is record
case Available is
when True => Bytes : Byte_Count;
when False => null;
end case;
end record;
Value_Or makes the caller name the fallback it wants:
Size : constant NUMA.Byte_Count :=
NUMA.Value_Or (NUMA.Memory_Bytes (Node), Fallback => 0);
Online nodes and permitted nodes
Online_Nodes reports what the host has. Allowed_Nodes reports what this
process may allocate on. They differ when a control group restricts the
process, and the difference is not visible from the host description
alone: a container is shown the whole machine's node list while
cpuset.mems limits what it may use. Allowed_Nodes is the set to act on.
if NUMA.Support.Restricted then
-- Fewer nodes are usable than the host has online.
null;
end if;
Node numbers are the host's own
Node and processor numbers match sysfs paths, /proc/self/status, and
numactl output, so a number reported here is the number to look up
elsewhere. They are sparse: a host with three nodes may number them 0, 2
and 5. Iterate a reported set rather than a numeric range.
Nodes without processors, and packages without one node
Two structures that older descriptions of NUMA do not cover, and that this crate reports directly:
- A node may carry memory and have no processor attached — a memory
expander, or a high-bandwidth tier.
Has_Processorsreports false andProcessors_Ofreturns an empty set. Such a node is a legal target for memory but is never local to any thread. - One processor package may be divided into several memory nodes. The
host then declares a small but non-local distance, such as 11, between two
nodes of one package. A value above 10 therefore does not mean a different
package. Compare
Package_Ofwhen package identity is what matters.
Placing memory
Reading a description always succeeds. Acting on one often does not, so the two are separate packages and support is reported before anything is tried:
case NUMA.Placement.Support is
when NUMA.Placement.Supported => null; -- go ahead
when NUMA.Placement.Unsupported_Host => null; -- no such facility here
when NUMA.Placement.Denied => null; -- facility exists, we may not
end case;
Denied is worth keeping distinct from Unsupported_Host. A container
sandbox commonly refuses these calls while the machine underneath has memory
nodes and other processes are using them. Reporting that as "this host has no
NUMA" would be wrong, and it is the failure most easily mistaken for one.
Placement governs where pages come from. It acts on whole pages, so a range
must begin on a Page_Size boundary:
NUMA.Placement.Apply_To
(Base => Region_Base,
Length => Region_Length,
Policy => NUMA.Placement.Interleaved,
Nodes => NUMA.Allowed_Nodes,
Result => Outcome);
Interleaved spreads pages over the set in rotation, trading a nearby node's
latency for the combined transfer rate of several — the right choice for
memory every node reads. Bound draws only from the set and fails rather
than spilling, which can mean running out of memory while other nodes still
have some. Preferred spills instead of failing, but the host's interface
takes a single node there, so only the lowest-numbered node in the set is
preferred; use Bound or Interleaved to name several.
Local and Unrestricted take no nodes. Passing a set alongside them is
harmless — it is dropped before the host sees it, because the host rejects a
policy of either kind that arrives carrying one.
Every operation reports an outcome and none of them raise:
type Placement_Outcome is
(Applied, Not_Supported, Not_Permitted, Unusable_Nodes,
Unaligned, Insufficient_Memory, Failed);
Apply_To_Thread places what the calling thread allocates from then on,
whatever the source. Node_Of_Address reports which node holds a page —
but asking about an untouched page causes it to be acquired, because the
host answers by placing it.
Allocating on a node
Ada already has a place to say where an object's memory comes from. This crate supplies a storage pool whose subpools are memory nodes, so the node is named at the point the object is created:
Arena : Flyology_NUMA.Pools.Node_Pool
(Policy => NUMA.Placement.Bound, Extent => 1024 * 1024);
type Sample_Access is access Sample with Storage_Pool => Arena;
Item : constant Sample_Access :=
new (Flyology_NUMA.Pools.On_Node (Arena, Near)) Sample'(...);
The pool asks the host for whole pages and places them before handing any out — placing them afterwards would leave whatever had already been written where the host first put it. Memory from the ordinary heap cannot be used for this: it may begin partway into a page whose remainder belongs to something else, and placing that page would move memory the crate does not own.
Allocation advances through pages already obtained. Freeing a single object does nothing; memory returns to the host when the subpool is deallocated or the pool goes out of scope. That suits a body of memory built up and discarded together, which is what a node-bound arena usually is.
Placement_Reached reports whether the pages really landed on the node, so
a host that could not place them is distinguishable from one that did:
if not Flyology_NUMA.Pools.Placement_Reached (Arena, Near) then
-- Allocation worked; the memory is simply wherever the host chose.
null;
end if;
Naming Ada processors
Ada numbers processors from one; hosts number them from zero. GNAT bridges the two by adding one, without checking that the host's numbering is consecutive. A host with an offline processor breaks that assumption, and the arithmetic then names a processor the caller did not intend.
To_CPU performs the same mapping and declines when discovery saw numbering
it cannot carry:
CPU : constant System.Multiprocessors.CPU_Range := NUMA.To_CPU (Processor);
-- Not_A_Specific_CPU when the host's processor numbers have a gap.
Boundaries
- Whole pages. A node pool obtains memory a page at a time, so a pool with a small extent still holds at least one page per node it is used for.
- No per-object free.
Deallocateon a node pool does nothing. Memory returns to the host when the subpool is deallocated or the pool is finalized. A plainnewwith no subpool named raisesProgram_Error; name a subpool withOn_Node. - Memory, not threads. Placement governs where pages come from. It does
not move a running thread; a thread is placed with the
CPUaspect or a dispatching domain, using the processors reported for a node. - Later allocations.
Apply_Togoverns the pages a range acquires from then on. Pages it already holds stay where they are unlessMoveis requested, and moving pages shared with another process needs privileges this process may not have. - Read once. Discovery runs while the package elaborates and is kept for the life of the process. Nodes added or removed afterwards are not seen.
- Linux and macOS. Linux is read from
/sys/devices/system/nodeand/proc/self/status, and placed through the kernel's memory-policy calls. Those calls have no C library wrapper, and their numbers differ per architecture — the x86-64 table and the table newer architectures share disagree on the order of the policy calls — so an architecture whose numbers this crate does not record reportsUnsupported_Hostrather than calling by a number borrowed from elsewhere. x86-64, AArch64 and RISC-V 64 are recorded. - macOS reports one node and places nothing. It describes no memory-node structure and offers no way to draw memory from a chosen part of the machine. The performance and efficiency processor clusters it does describe are not memory nodes — they share one memory domain — and are not reported here.
- Limits. Nodes above 63 and processors above 4095 are not represented.
A host that exceeds either reports
Support.Completeas false, so a partial answer can be told apart from a whole one. - Distances are ordinal. They order node pairs. They are declared by firmware, are not latencies, and are frequently declared poorly.
Build and test
alr build
./scripts/test.sh
The suite has two parts. One checks the description reported for the host
running the tests, against invariants every host must satisfy. The other
reads recorded host descriptions from tests/fixtures/ — two packages of
one node each, two packages each divided into two nodes, a node with no
processor, sparse node numbering, a control-group restriction, a gap in
processor numbering, and several descriptions this crate must refuse or
report as incomplete.
The recorded descriptions matter because the interesting cases cannot be reached otherwise: a development machine has one node, and a container on one is single-node too. Reading them keeps node numbering, distance-row alignment, and restriction handling under test on every host rather than only on hardware that happens to have them.
Recorded descriptions cover reading. They cannot cover placing, which needs a kernel that really has several nodes. For that:
./scripts/multinode-check.sh
It boots guests with two and four memory nodes — the four-node guest with uneven distances and one node carrying memory with no processor attached — and runs the host suite inside them. The parts of the suite that choose between nodes do nothing on a one-node machine and run here: memory bound to each node in turn, interleaving spread across every node, and a node proving further from another than from itself.
The guest runs the architecture it is tested for, so the placement calls it makes are the ones that architecture's table names. x86-64 and AArch64 hosts are both handled.
It needs a Linux host, the matching qemu-system-*, busybox, and a kernel
image (FLYOLOGY_NUMA_KERNEL, or the first /boot/vmlinuz-*). It is not
part of ./scripts/test.sh for that reason.