code wiki / (root) / nx_tensor.nx

nx_tensor.nx source

↩ module page · 370 lines · 11961 B

1// nx_tensor.nx -- n-dimensional strided array primitive. 2// 3// Foundation brick of the sovereign-from-bits-up ML substrate per 4// nxc2/docs/MULTIMODAL_ORCHESTRATOR_ROADMAP.md iteration I1. Every 5// future ML kernel (BLAS, conv, attention, norm, activation) hangs 6// off this type. Dtype enum + 8-dim shape + row-major strides give 7// us the API contract that survives every future evolution -- new 8// dtypes, SIMD intrinsics, GPU backends -- without breaking callers. 9// 10// Per the sovereign-from-bits-up cardinal: NO GGML, NO PyTorch 11// dependency. Pure NishiLang on sys_mmap. The element storage is 12// a flat *u8 byte-buffer; the dtype tag tells callers how to 13// interpret it. Current build supports I64-backed only (Q-format 14// fixed-point per the Q10 substrate convention); future iterations 15// add I32/I16/I8/F16/BF16/F32/F64/Q4_0/Q4_K/Q5_K/Q8_0 in lock-step 16// with the compiler's f-type and SIMD landings. 17// 18// genealogy_id: ggml_internal_design + numpy_ndarray + dlpack 19// lineage_id: sovereign_tensor_v1 20 21// nx_safety_envelope: 22// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 23// sil_target: SIL1 24// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 25// verdict: NOT_YET_EVALUATED 26 27import "nx_syscalls.nx" 28import "nx_tier.nx" 29 30// ===== Sealed-enum: DType ========================================== 31// 32// One numeric tag per supported element type. Listed in priority 33// order for what we'll ship next, NOT for what's complete: 34// 35// I64 -- shipped (the only dtype that needs no compiler change) 36// I32 -- queued; needs nxc2 i32 alias propagation 37// I16 -- queued 38// I8 -- queued 39// F32 -- gated on compiler I2 (f-type native) 40// F16 -- gated on compiler I2 + half-emulation 41// BF16 -- gated on compiler I2 42// F64 -- gated on compiler I2 43// Q4_0 -- gated on quantization codec I6 44// Q4_K -- gated on I6 + k-block layout 45// Q5_K -- gated on I6 46// Q8_0 -- gated on I6 47 48const NX_DT_I64: nx_int = 0 // 8 bytes per element 49const NX_DT_I32: nx_int = 1 // 4 50const NX_DT_I16: nx_int = 2 // 2 51const NX_DT_I8: nx_int = 3 // 1 52const NX_DT_F32: nx_int = 4 // 4 (compiler-gated) 53const NX_DT_F16: nx_int = 5 // 2 54const NX_DT_BF16: nx_int = 6 // 2 55const NX_DT_F64: nx_int = 7 // 8 56const NX_DT_Q4_0: nx_int = 8 // packed 4-bit + 1 fp scale per block 57const NX_DT_Q4_K: nx_int = 9 58const NX_DT_Q5_K: nx_int = 10 59const NX_DT_Q8_0: nx_int = 11 60const NX_DT_N_KINDS: nx_int = 12 61 62func nx_dt_is_valid(d: nx_int) -> nx_int { 63 if d < 0 { return 0 } 64 if d >= NX_DT_N_KINDS { return 0 } 65 return 1 66} 67 68// Element size in bytes. Returns 0 for dtypes that are not yet 69// implementable (compiler/codec gated) so callers can refuse the 70// alloc with a structured error rather than crash. 71 72func nx_dt_element_bytes(d: nx_int) -> nx_int { 73 if d == NX_DT_I64 { return 8 } 74 if d == NX_DT_I32 { return 4 } 75 if d == NX_DT_I16 { return 2 } 76 if d == NX_DT_I8 { return 1 } 77 if d == NX_DT_F32 { return 0 } // compiler-gated; return 0 = not yet usable 78 if d == NX_DT_F16 { return 0 } 79 if d == NX_DT_BF16 { return 0 } 80 if d == NX_DT_F64 { return 0 } 81 if d == NX_DT_Q4_0 { return 0 } // quantization codec gated 82 if d == NX_DT_Q4_K { return 0 } 83 if d == NX_DT_Q5_K { return 0 } 84 if d == NX_DT_Q8_0 { return 0 } 85 return 0 86} 87 88func nx_dt_is_implemented(d: nx_int) -> nx_int { 89 if nx_dt_element_bytes(d) > 0 { return 1 } 90 return 0 91} 92 93// ===== Tensor struct ============================================== 94// 95// Up to 8 dimensions. Shape + stride live inline as fixed arrays 96// (no extra mmap for tiny tensor metadata). Each stride is in 97// ELEMENT units; byte offset = sum(stride[i] * idx[i]) * element_bytes. 98 99const NX_T_MAX_NDIM: nx_int = 8 100 101struct NxTensor { 102 dtype: nx_int, 103 ndim: nx_int, 104 numel: nx_int, 105 elt_bytes: nx_int, 106 storage: *u8, // flat byte buffer 107 storage_n: nx_int, // byte capacity 108 // Caller writes through accessor funcs. Shape + stride are 109 // separate small buffers because nxc2 struct fields can't be 110 // fixed-size inline arrays today. 111 shape: *i64, // [NX_T_MAX_NDIM] 112 stride: *i64 // [NX_T_MAX_NDIM] in elements 113} 114 115const NX_T_BYTES: nx_int = 64 // 8 fields * 8 116const NX_T_SHAPE_BYTES: nx_int = 64 // NX_T_MAX_NDIM * 8 117 118// ===== Sealed-enum: AllocVerdict ================================== 119// 120// Caller-readable error kind. Per the four-pillar discipline we 121// refuse structurally bad inputs at alloc time. 122 123const NX_T_OK: nx_int = 0 124const NX_T_ERR_BAD_DTYPE: nx_int = 1 // unknown or out-of-range dtype 125const NX_T_ERR_DTYPE_GATED: nx_int = 2 // dtype known but compiler/codec not yet supports it 126const NX_T_ERR_BAD_NDIM: nx_int = 3 // ndim < 0 or > NX_T_MAX_NDIM 127const NX_T_ERR_BAD_SHAPE: nx_int = 4 // any shape[i] <= 0 128const NX_T_ERR_OVERFLOW: nx_int = 5 // numel overflow i64 129const NX_T_ERR_N_VERDICTS: nx_int = 6 130 131func nx_t_verdict_is_valid(v: nx_int) -> nx_int { 132 if v < 0 { return 0 } 133 if v >= NX_T_ERR_N_VERDICTS { return 0 } 134 return 1 135} 136 137// ===== Row-major stride compute =================================== 138// 139// stride[ndim-1] = 1; stride[i] = stride[i+1] * shape[i+1] 140// Operates on caller-supplied output buffer. 141 142func nx_t_compute_strides_rowmajor(shape: *i64, ndim: nx_int, stride_out: *i64) -> nx_int { 143 if ndim <= 0 { return 0 } 144 stride_out[ndim - 1] = 1 145 var i: nx_int = ndim - 2 146 while i >= 0 { 147 stride_out[i] = stride_out[i + 1] * shape[i + 1] 148 i = i - 1 149 } 150 return 0 151} 152 153// ===== Alloc ======================================================= 154// 155// shape_in points to a caller-supplied i64 buffer of length ndim. 156// On success returns *NxTensor. On structural failure returns NULL 157// and writes the error verdict into err_out. 158 159func nx_t_alloc(dtype: nx_int, shape_in: *i64, ndim: nx_int, 160 err_out: *i64) -> *NxTensor { 161 err_out[0] = NX_T_OK 162 163 if nx_dt_is_valid(dtype) == 0 { 164 err_out[0] = NX_T_ERR_BAD_DTYPE 165 return 0 as *NxTensor 166 } 167 let eb: nx_int = nx_dt_element_bytes(dtype) 168 if eb <= 0 { 169 err_out[0] = NX_T_ERR_DTYPE_GATED 170 return 0 as *NxTensor 171 } 172 if ndim < 0 { 173 err_out[0] = NX_T_ERR_BAD_NDIM 174 return 0 as *NxTensor 175 } 176 if ndim > NX_T_MAX_NDIM { 177 err_out[0] = NX_T_ERR_BAD_NDIM 178 return 0 as *NxTensor 179 } 180 181 // numel = product(shape). Refuse non-positive shape entries. 182 var numel: nx_int = 1 183 var i: nx_int = 0 184 while i < ndim { 185 if shape_in[i] <= 0 { 186 err_out[0] = NX_T_ERR_BAD_SHAPE 187 return 0 as *NxTensor 188 } 189 let next: nx_int = numel * shape_in[i] 190 if next < numel { // wrap = overflow 191 err_out[0] = NX_T_ERR_OVERFLOW 192 return 0 as *NxTensor 193 } 194 numel = next 195 i = i + 1 196 } 197 198 let t: *NxTensor = (sys_mmap(NX_T_BYTES)) as *NxTensor 199 t.dtype = dtype 200 t.ndim = ndim 201 t.numel = numel 202 t.elt_bytes = eb 203 t.storage_n = numel * eb 204 t.storage = (sys_mmap(t.storage_n)) as *u8 205 t.shape = (sys_mmap(NX_T_SHAPE_BYTES)) as *i64 206 t.stride = (sys_mmap(NX_T_SHAPE_BYTES)) as *i64 207 208 var j: nx_int = 0 209 while j < ndim { 210 t.shape[j] = shape_in[j] 211 j = j + 1 212 } 213 // Zero unused shape slots so view/permute can't misread 214 while j < NX_T_MAX_NDIM { 215 t.shape[j] = 0 216 j = j + 1 217 } 218 nx_t_compute_strides_rowmajor(t.shape, ndim, t.stride) 219 var k: nx_int = ndim 220 while k < NX_T_MAX_NDIM { 221 t.stride[k] = 0 222 k = k + 1 223 } 224 return t 225} 226 227// ===== Zero-fill =================================================== 228 229func nx_t_fill_zero(t: *NxTensor) -> nx_int { 230 var i: nx_int = 0 231 while i < t.storage_n { 232 t.storage[i] = 0 233 i = i + 1 234 } 235 return 0 236} 237 238// ===== Index helpers (I64-only for v1) ============================= 239// 240// Compute byte offset from i64 index vector. idx is caller-supplied 241// length-ndim i64 buffer. 242 243func nx_t_flat_offset(t: *NxTensor, idx: *i64) -> nx_int { 244 var off: nx_int = 0 245 var i: nx_int = 0 246 while i < t.ndim { 247 off = off + idx[i] * t.stride[i] 248 i = i + 1 249 } 250 return off 251} 252 253// Get i64 element at idx. Caller responsible for dtype == I64. 254func nx_t_get_i64(t: *NxTensor, idx: *i64) -> nx_int { 255 let off_elt: nx_int = nx_t_flat_offset(t, idx) 256 let off_byte: nx_int = off_elt * t.elt_bytes 257 let p: *i64 = ((t.storage as nx_int) + off_byte) as *i64 258 return p[0] 259} 260 261// Set i64 element at idx. 262func nx_t_set_i64(t: *NxTensor, idx: *i64, v: nx_int) -> nx_int { 263 let off_elt: nx_int = nx_t_flat_offset(t, idx) 264 let off_byte: nx_int = off_elt * t.elt_bytes 265 let p: *i64 = ((t.storage as nx_int) + off_byte) as *i64 266 p[0] = v 267 return 0 268} 269 270// ===== Contiguity check ============================================ 271// 272// Row-major-contiguous iff stride[i] == prod(shape[i+1..]). Returns 273// 1 if contiguous, 0 if not. 274 275func nx_t_is_contiguous(t: *NxTensor) -> nx_int { 276 if t.ndim == 0 { return 1 } 277 var expected: nx_int = 1 278 var i: nx_int = t.ndim - 1 279 while i >= 0 { 280 if t.stride[i] != expected { return 0 } 281 expected = expected * t.shape[i] 282 i = i - 1 283 } 284 return 1 285} 286 287// ===== Reshape (only when contiguous and numel matches) ============ 288// 289// Returns 0 on success, -1 if the new shape's numel != t.numel or 290// the tensor is not contiguous. Reshape edits the tensor in-place 291// (caller must not have outstanding views of the old shape). 292 293func nx_t_reshape(t: *NxTensor, new_shape: *i64, new_ndim: nx_int) -> nx_int { 294 if new_ndim < 0 { return 0 - 1 } 295 if new_ndim > NX_T_MAX_NDIM { return 0 - 1 } 296 if nx_t_is_contiguous(t) == 0 { return 0 - 1 } 297 298 var new_numel: nx_int = 1 299 var i: nx_int = 0 300 while i < new_ndim { 301 if new_shape[i] <= 0 { return 0 - 1 } 302 new_numel = new_numel * new_shape[i] 303 i = i + 1 304 } 305 if new_numel != t.numel { return 0 - 1 } 306 307 var j: nx_int = 0 308 while j < new_ndim { 309 t.shape[j] = new_shape[j] 310 j = j + 1 311 } 312 while j < NX_T_MAX_NDIM { 313 t.shape[j] = 0 314 j = j + 1 315 } 316 t.ndim = new_ndim 317 nx_t_compute_strides_rowmajor(t.shape, t.ndim, t.stride) 318 var k: nx_int = t.ndim 319 while k < NX_T_MAX_NDIM { 320 t.stride[k] = 0 321 k = k + 1 322 } 323 return 0 324} 325 326// ===== Permute (transpose with arbitrary axis order) =============== 327// 328// permute_axes is an i64 buffer of length ndim giving the new axis 329// order. Returns a NEW *NxTensor sharing storage with t but with 330// permuted shape + stride. Original is not modified. 331// 332// Refuses if permute_axes is not a valid permutation of [0..ndim). 333 334func nx_t_permute(t: *NxTensor, permute_axes: *i64) -> *NxTensor { 335 // Verify the permutation is valid 336 let seen: *i64 = (sys_mmap(NX_T_SHAPE_BYTES)) as *i64 337 var i: nx_int = 0 338 while i < t.ndim { 339 let a: nx_int = permute_axes[i] 340 if a < 0 { return 0 as *NxTensor } 341 if a >= t.ndim { return 0 as *NxTensor } 342 if seen[a] != 0 { return 0 as *NxTensor } 343 seen[a] = 1 344 i = i + 1 345 } 346 347 let v: *NxTensor = (sys_mmap(NX_T_BYTES)) as *NxTensor 348 v.dtype = t.dtype 349 v.ndim = t.ndim 350 v.numel = t.numel 351 v.elt_bytes = t.elt_bytes 352 v.storage = t.storage 353 v.storage_n = t.storage_n 354 v.shape = (sys_mmap(NX_T_SHAPE_BYTES)) as *i64 355 v.stride = (sys_mmap(NX_T_SHAPE_BYTES)) as *i64 356 357 var j: nx_int = 0 358 while j < t.ndim { 359 let a: nx_int = permute_axes[j] 360 v.shape[j] = t.shape[a] 361 v.stride[j] = t.stride[a] 362 j = j + 1 363 } 364 while j < NX_T_MAX_NDIM { 365 v.shape[j] = 0 366 v.stride[j] = 0 367 j = j + 1 368 } 369 return v 370}