code wiki / (root) / nx_hw.nx

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}