nx_hw.nx source
↩ module page · 151 lines · 6762 B
1// nx_hw.nx -- runtime hardware probes (CPU count, cache line, page).
2//
3// SOVEREIGN: every probe goes through a real syscall or CPUID-class
4// instruction. No /proc parsing, no libc sysconf. Same code paths
5// on RV64 + x86_64 because the syscalls are number-portable.
6//
7// FOUNDATION for [[feedback-dynamic-hw-sizing-no-hardcoded-thread-counts]]:
8// every threading / parallel / pool / partition primitive in the
9// substrate must derive its sizing from THESE functions, not from
10// hardcoded literals.
11//
12// Syscall numbers used:
13// 123 = sched_getaffinity (rv64/asm-generic) -- cpu count. The
14// compiler's x86ctx_rv64_to_x86_64_syscall table translates
15// 123 -> x86_64 204 (row added + blessed 2026-07-07).
16// HISTORY: the original constant 122 was WRONG twice over --
17// rv64 122 is sched_SETaffinity, and 122 was not in the swap
18// table so it passed through to x86_64 setfsgid, which
19// SUCCEEDS, leaves the mask all-zero, popcounts to 0, and
20// made this probe silently report 1 CPU forever (every
21// auto-sized pool ran single-threaded; caught by
22// nx_conv2d_mt_gate check 10, 2026-07-07, interim-fixed with
23// raw 204 pass-through, then made portable via the table
24// row the same day).
25//
26// Cache-line probe is currently a sealed-enum default (64 B, the
27// near-universal value across x86 / ARM / RISC-V); CPUID-based
28// probing on x86 lands when we add an inline-asm intrinsic.
29// Page size is fixed at 4 KiB (Linux RV64 + x86_64 default).
30// Larger pages (2 MiB hugepages) handled by future nx_hugepage.nx.
31
32// nx_safety_envelope:
33// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
34// sil_target: SIL1
35// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
36// verdict: NOT_YET_EVALUATED
37
38import "nx_syscalls.nx"
39const NX_MAGIC_4096: i64 = 4096
40
41const NX_SYS_SCHED_GETAFFINITY: i64 = 123
42
43// Maximum CPUs we'll report. cpu_set_t is a bitmask; 1024 is the
44// glibc default upper bound. 128 bytes = 1024 bits. If you have
45// more cores than that, congrats -- bump this constant.
46const NX_HW_CPUSET_BYTES: i64 = 128
47const NX_HW_CPUSET_BITS: i64 = 1024
48
49// Returns the number of logical CPUs the calling process is
50// allowed to run on. This respects cgroup CPU pinning (e.g.
51// containerized deploys with --cpus=2), so a thread pool sized
52// to this count actually fits the budget rather than spawning
53// 64 threads on a 2-CPU container.
54//
55// On error (very rare; old kernels returned -EINVAL for too-small
56// cpusetsize, but 128 B is way past that), returns 1 as a safe
57// minimum. Callers can MAX(this, 1) again if they want.
58func nx_hw_cpu_count() -> i64 {
59 let mask: *u8 = sys_mmap(NX_HW_CPUSET_BYTES)
60 var i: i64 = 0
61 while i < NX_HW_CPUSET_BYTES { mask[i] = 0 as u8; i = i + 1 }
62
63 let r: i64 = __syscall(NX_SYS_SCHED_GETAFFINITY, 0,
64 NX_HW_CPUSET_BYTES, mask as i64, 0, 0, 0)
65
66 // Pop-count the bits set in mask[0..NX_HW_CPUSET_BYTES].
67 // Naive byte loop -- ~128 cycles total.
68 var count: i64 = 0
69 if r >= 0 {
70 var b: i64 = 0
71 while b < NX_HW_CPUSET_BYTES {
72 var byte: i64 = mask[b]
73 // Brian Kernighan's bit count.
74 while byte != 0 {
75 byte = byte & (byte - 1)
76 count = count + 1
77 }
78 b = b + 1
79 }
80 }
81 // Single exit so the mask page is always freed (this is called
82 // per pool creation, not once at startup -- no per-call leaks).
83 sys_munmap(mask, NX_HW_CPUSET_BYTES)
84 if count < 1 { return 1 }
85 return count
86}
87
88// Cache line size in bytes. Returns 64 (universal default for
89// x86, ARM, modern RISC-V). Use this for false-sharing-avoidance
90// padding between concurrent counters and for SIMD load alignment
91// hints. CPUID-based probing on x86 (leaf 0x80000006) and
92// platform-config probing on RV64 land when nxc2 grows raw inline
93// asm; until then this is the safe-default value.
94// MEASURED as of 2026-07-30 (was `return 64`, a literal). The header above deferred this to "when nxc2
95// grows raw inline asm" -- THAT BLOCKER WAS STALE: __cpuid_ebx is a COMPILER INTRINSIC (OP_CPUID_EBX=133,
96// emitted as raw 0F A2), so no import and no layer dependency is involved, and CPUID.1:EBX has carried
97// the answer all along in bits 15:8 = CLFLUSH line size in 8-BYTE UNITS.
98// Decode proven by nx_cpuid_gate T17/T19/T21 (64B and 128B both decode; a zero field REFUSES).
99// Returns -1 UNMEASURED when the field is 0 rather than a plausible 64: a caller sizing cache-blocked
100// loops must be able to tell MEASURED from ASSUMED, which is the whole point for a spore landing on
101// unknown silicon. LAW: A CONSTANT THAT CANNOT FAIL IS NOT A PROBE.
102const NX_HW_BYTE_MASK: i64 = 255
103const NX_HW_CLFLUSH_UNIT: i64 = 8
104
105func nx_hw_cache_line_size() -> i64 {
106 let ebx: i64 = __cpuid_ebx(1, 0)
107 if ebx < 0 { return 0 - 1 }
108 let f: i64 = (ebx >> 8) & NX_HW_BYTE_MASK
109 if f == 0 { return 0 - 1 }
110 return f * NX_HW_CLFLUSH_UNIT
111}
112
113// Logical processors per package -- the SMT fact the envelope never had. nx_hw_envelope reports
114// "cores: 8" for what is 8 THREADS on 4 CORES, so anything sizing a vector pool off that number
115// over-subscribes 2x on exactly the workload that shares an FPU. CPUID.1:EBX bits 23:16.
116// -1 UNMEASURED on a zero field. Proven by nx_cpuid_gate T18/T20.
117func nx_hw_logical_procs() -> i64 {
118 let ebx: i64 = __cpuid_ebx(1, 0)
119 if ebx < 0 { return 0 - 1 }
120 let f: i64 = (ebx >> 16) & NX_HW_BYTE_MASK
121 if f == 0 { return 0 - 1 }
122 return f
123}
124
125// Page size in bytes. Linux RV64 + x86_64 = 4 KiB. Used by mmap
126// (which rounds up internally), but also by nx_arena / nx_buf for
127// page-aligned allocations and by SIMD code that wants to avoid
128// straddling a TLB entry.
129func nx_hw_page_size() -> i64 {
130 return NX_MAGIC_4096
131}
132
133// Suggested worker count for a CPU-bound thread pool. Same as
134// nx_hw_cpu_count() today; future enhancement: subtract 1 if a
135// dedicated I/O thread is in play, or scale down on hyperthreaded
136// systems where physical cores < logical cores.
137func nx_hw_worker_count() -> i64 {
138 return nx_hw_cpu_count()
139}
140
141// ---- self-test ---------------------------------------------------
142
143func main() -> i64 {
144 let n: i64 = nx_hw_cpu_count()
145 if n < 1 { return __syscall(93, 1, 0, 0, 0, 0, 0) }
146 if n > NX_HW_CPUSET_BITS { return __syscall(93, 2, 0, 0, 0, 0, 0) }
147 if nx_hw_cache_line_size() != 64 { return __syscall(93, 3, 0, 0, 0, 0, 0) }
148 if nx_hw_page_size() != NX_MAGIC_4096 { return __syscall(93, 4, 0, 0, 0, 0, 0) }
149 if nx_hw_worker_count() != n { return __syscall(93, 5, 0, 0, 0, 0, 0) }
150 return 0
151}