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}