code wiki / (root) / nx_hw_simd_caps.nx

nx_hw_simd_caps.nx source

↩ module page · 201 lines · 7629 B

1// nx_hw_simd_caps.nx -- runtime SIMD capability detection. 2// 3// Reads /proc/cpuinfo and looks for vendor-specific flag tokens. 4// Sovereign: pure NishiLang, no libc, no /sys probe utilities, no 5// CPUID inline-asm dep. Works on every Linux arch the substrate 6// targets (x86_64, RV64, AArch64, etc.) -- /proc/cpuinfo is the 7// kernel's canonical CPU-feature reporting surface. 8// 9// HISTORICAL NOTE: this module was the smoke that surfaced the 10// gp-relative relaxation bug fixed 2026-05-16. When 6 statics 11// fall within a single 4 KiB BSS window, the assembler optimizes 12// auipc+addi global-address sequences into addi(gp)+addi, but 13// our minimal crt0 doesn't initialize gp, so reads/writes 14// dereferenced garbage. Fixed in riscv.c by always emitting 15// `.option norelax` -- the bug class is now structurally 16// impossible regardless of static count. See docs/NISHILANG_CONCURRENCY.md 17// "Pitfalls" section for the diagnosis trail. 18// 19// Why probe at runtime instead of compile-time: 20// - One binary can dispatch scalar / AVX2 / AVX-512 paths based 21// on the actual chip it runs on (vs N separate builds) 22// - Honest cardinal: refuse silent CPU-feature assumptions; ask 23// the kernel what it actually has 24// 25// Use: 26// if nx_hw_has_avx512() == 1 { use_avx512_path() } 27// else if nx_hw_has_avx2() == 1 { use_avx2_path() } 28// else { use_scalar_path() } 29// 30// Cost: ~4 KiB stack buffer + one sys_openat + one sys_read + 31// linear scan. Acceptable to call once at startup; cache in a 32// static for repeated checks. 33// 34// Caches: once probed, results are stored in static i64 slots so 35// subsequent calls are zero-syscall. 36 37// nx_safety_envelope: 38// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 39// sil_target: SIL1 40// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 41// verdict: NOT_YET_EVALUATED 42 43import "nx_syscalls.nx" 44 45const NX_CPUINFO_BUF: i64 = 8192 46 47static NX_HW_CAPS_INIT: i64 48static NX_HW_HAS_AVX2: i64 49static NX_HW_HAS_AVX512: i64 50static NX_HW_HAS_RVV: i64 51static NX_HW_HAS_NEON: i64 52static NX_HW_HAS_SSE4_2: i64 53 54// Open + read /proc/cpuinfo into a buffer. Returns bytes read or 55// negative errno. 56func _nx_hw_read_cpuinfo(buf: *u8, cap: i64) -> i64 { 57 // openat(AT_FDCWD=-100, path, O_RDONLY=0). 58 let path: *u8 = "/proc/cpuinfo" as *u8 59 let fd: i64 = __syscall(SYS_OPENAT, -100, path as i64, 0, 0, 0, 0) 60 if fd < 0 { return fd } 61 let n: i64 = sys_read(fd, buf, cap - 1) 62 sys_close(fd) 63 if n < 0 { return n } 64 buf[n] = 0 as u8 65 return n 66} 67 68// Scan buf for a token (whole word, surrounded by space / tab / 69// newline / start-of-buf). Returns 1 if found, 0 otherwise. 70func _nx_hw_has_token(buf: *u8, buf_len: i64, tok: *u8, tok_len: i64) -> i64 { 71 var i: i64 = 0 72 while i + tok_len <= buf_len { 73 // Check boundary on the left: i == 0 OR buf[i-1] is whitespace/newline. 74 var left_ok: i64 = 1 75 if i > 0 { 76 let lc: i64 = buf[i - 1] 77 if lc != 0x20 { 78 if lc != 0x09 { 79 if lc != 0x0A { 80 left_ok = 0 81 } 82 } 83 } 84 } 85 if left_ok == 1 { 86 var hit: i64 = 1 87 var j: i64 = 0 88 while j < tok_len { 89 if buf[i + j] != tok[j] { hit = 0; j = tok_len } 90 else { j = j + 1 } 91 } 92 if hit == 1 { 93 // Check boundary on the right. 94 let after: i64 = i + tok_len 95 if after >= buf_len { return 1 } 96 let rc: i64 = buf[after] 97 if rc == 0x20 { return 1 } 98 if rc == 0x09 { return 1 } 99 if rc == 0x0A { return 1 } 100 if rc == 0 { return 1 } 101 } 102 } 103 i = i + 1 104 } 105 return 0 106} 107 108// Populate token literal into a small buffer + return length. 109func _nx_hw_tok_lit(buf: *u8, s_byte_0: i64, s_byte_1: i64, s_byte_2: i64, 110 s_byte_3: i64, s_byte_4: i64, s_byte_5: i64, 111 s_byte_6: i64) -> i64 { 112 buf[0] = s_byte_0 as u8 113 if s_byte_1 == 0 { return 1 } 114 buf[1] = s_byte_1 as u8 115 if s_byte_2 == 0 { return 2 } 116 buf[2] = s_byte_2 as u8 117 if s_byte_3 == 0 { return 3 } 118 buf[3] = s_byte_3 as u8 119 if s_byte_4 == 0 { return 4 } 120 buf[4] = s_byte_4 as u8 121 if s_byte_5 == 0 { return 5 } 122 buf[5] = s_byte_5 as u8 123 if s_byte_6 == 0 { return 6 } 124 buf[6] = s_byte_6 as u8 125 return 7 126} 127 128// Lazy-init the capability cache by reading /proc/cpuinfo once and 129// scanning for the relevant flag tokens. 130func _nx_hw_caps_init() -> i64 { 131 if NX_HW_CAPS_INIT == 1 { return 0 } 132 let buf: *u8 = sys_mmap(NX_CPUINFO_BUF) 133 let n: i64 = _nx_hw_read_cpuinfo(buf, NX_CPUINFO_BUF) 134 if n <= 0 { 135 NX_HW_CAPS_INIT = 1 136 return -1 137 } 138 // Scan for each token. Reuse a small scratch buffer for tokens. 139 let tok: *u8 = sys_mmap(16) 140 141 let len_avx2: i64 = _nx_hw_tok_lit(tok, 0x61, 0x76, 0x78, 0x32, 0, 0, 0) // "avx2" 142 NX_HW_HAS_AVX2 = _nx_hw_has_token(buf, n, tok, len_avx2) 143 144 let len_avx512: i64 = _nx_hw_tok_lit(tok, 0x61, 0x76, 0x78, 0x35, 0x31, 0x32, 0x66) // "avx512f" 145 NX_HW_HAS_AVX512 = _nx_hw_has_token(buf, n, tok, len_avx512) 146 147 let len_sse: i64 = _nx_hw_tok_lit(tok, 0x73, 0x73, 0x65, 0x34, 0x5F, 0x32, 0) // "sse4_2" 148 NX_HW_HAS_SSE4_2 = _nx_hw_has_token(buf, n, tok, len_sse) 149 150 let len_rvv: i64 = _nx_hw_tok_lit(tok, 0x72, 0x76, 0x76, 0, 0, 0, 0) // "rvv" 151 NX_HW_HAS_RVV = _nx_hw_has_token(buf, n, tok, len_rvv) 152 153 let len_neon: i64 = _nx_hw_tok_lit(tok, 0x6E, 0x65, 0x6F, 0x6E, 0, 0, 0) // "neon" 154 NX_HW_HAS_NEON = _nx_hw_has_token(buf, n, tok, len_neon) 155 156 NX_HW_CAPS_INIT = 1 157 return 0 158} 159 160// Public probes. All are zero-cost after first call (cached). 161func nx_hw_has_avx2() -> i64 { _nx_hw_caps_init(); return NX_HW_HAS_AVX2 } 162func nx_hw_has_avx512() -> i64 { _nx_hw_caps_init(); return NX_HW_HAS_AVX512 } 163func nx_hw_has_sse4_2() -> i64 { _nx_hw_caps_init(); return NX_HW_HAS_SSE4_2 } 164func nx_hw_has_rvv() -> i64 { _nx_hw_caps_init(); return NX_HW_HAS_RVV } 165func nx_hw_has_neon() -> i64 { _nx_hw_caps_init(); return NX_HW_HAS_NEON } 166 167// Convenience: best available i64x4 SIMD level on this chip. 168// 0 = scalar only; 1 = SIMD i64x4 available (any of AVX2 / RVV / 169// NEON would work, though our codegen targets specific ones). 170func nx_hw_simd_i64x4_available() -> i64 { 171 _nx_hw_caps_init() 172 if NX_HW_HAS_AVX2 == 1 { return 1 } 173 if NX_HW_HAS_AVX512 == 1 { return 1 } 174 if NX_HW_HAS_RVV == 1 { return 1 } 175 if NX_HW_HAS_NEON == 1 { return 1 } 176 return 0 177} 178 179// ---- self-test --------------------------------------------------- 180// 181// Self-test main intentionally does only function calls, never 182// reads statics from main scope. The pre-existing static-i64-from- 183// main codegen bug (documented in NISHILANG_CONCURRENCY.md pitfalls) 184// makes that pattern segv -- route through non-main helpers. 185 186// Helper-routed self-check (caller may invoke any probe directly 187// now that the gp-relative bug is fixed via .option norelax in 188// riscv.c -- this helper is kept for legibility). 189func _caps_smoke_run() -> i64 { 190 let a2: i64 = nx_hw_has_avx2() 191 let a512: i64 = nx_hw_has_avx512() 192 let rvv: i64 = nx_hw_has_rvv() 193 let nn: i64 = nx_hw_has_neon() 194 let any: i64 = nx_hw_simd_i64x4_available() 195 return a2 + a512 + rvv + nn + any 196} 197 198func main() -> i64 { 199 if _caps_smoke_run() < 0 { return __syscall(93, 1, 0, 0, 0, 0, 0) } 200 return 0 201}