nx_compute_runner_test.nx source
↩ module page · 182 lines · 7267 B
1// nx_compute_runner_test.nx -- end-to-end loop closure.
2//
3// Build a real graph -> run it -> get real tensor output -> verify
4// via numeric oracle vs hand-computed reference. All pure NishiLang.
5// This is the "world class without comfyui annoyance but with the
6// power and more" deliverable: typed DAG + content-addressed nodes +
7// kernel dispatch + numerical verification, in one .nx program.
8
9import "nx_syscalls.nx"
10import "nx_tier.nx"
11import "nx_tensor.nx"
12import "nx_compute_node.nx"
13import "nx_compute_graph.nx"
14import "nx_blas_i64.nx"
15import "nx_compute_runner.nx"
16import "nx_numeric_oracle.nx"
17
18func main() -> nx_int {
19 let err: *i64 = (sys_mmap(8)) as *i64
20
21 // ===== Graph shape ===========================================
22 //
23 // INPUT (2x3) --\
24 // MATMUL -- RELU -- OUTPUT
25 // CONST (3x4) --/
26 //
27 // INPUT values:
28 // [[1, 2, 3],
29 // [4, 5, 6]]
30 // CONST values:
31 // [[1, 0, 1, 0],
32 // [0, 1, 0, 1],
33 // [1, 1, 1, 1]]
34 //
35 // MATMUL result (2x4):
36 // row 0:
37 // 1*1+2*0+3*1 = 4
38 // 1*0+2*1+3*1 = 5
39 // 1*1+2*0+3*1 = 4
40 // 1*0+2*1+3*1 = 5
41 // row 1:
42 // 4*1+5*0+6*1 = 10
43 // 4*0+5*1+6*1 = 11
44 // 4*1+5*0+6*1 = 10
45 // 4*0+5*1+6*1 = 11
46 //
47 // RELU(matmul) == matmul since all values are positive.
48
49 let g: *ComputeGraph = nx_cg_alloc(16)
50
51 let n0: *ComputeNode = nx_cn_alloc(0, NX_CN_NODE_INPUT, 0, 0, 1, 0)
52 nx_cg_add_node(g, n0)
53
54 let n1: *ComputeNode = nx_cn_alloc(0, NX_CN_NODE_CONST, 0, 0, 1, 0)
55 nx_cg_add_node(g, n1)
56
57 let n2: *ComputeNode = nx_cn_alloc(0, NX_CN_NODE_KERNEL, NX_CN_K_MATMUL, 2, 1, 0)
58 nx_cn_set_input(n2, 0, 0, 0)
59 nx_cn_set_input(n2, 1, 1, 0)
60 nx_cg_add_node(g, n2)
61
62 let n3: *ComputeNode = nx_cn_alloc(0, NX_CN_NODE_TENSOR_OP, NX_CN_OP_RELU, 1, 1, 0)
63 nx_cn_set_input(n3, 0, 2, 0)
64 nx_cg_add_node(g, n3)
65
66 let n4: *ComputeNode = nx_cn_alloc(0, NX_CN_NODE_OUTPUT, 0, 1, 0, 0)
67 nx_cn_set_input(n4, 0, 3, 0)
68 nx_cg_add_node(g, n4)
69
70 if nx_cg_validate(g) != NX_CG_OK { return 1 }
71 if nx_cg_toposort(g) != NX_CG_OK { return 2 }
72
73 // ===== Build the input + const tensors =======================
74 let s_input: *i64 = (sys_mmap(16)) as *i64
75 s_input[0] = 2; s_input[1] = 3
76 let input_t: *NxTensor = nx_t_alloc(NX_DT_I64, s_input, 2, err)
77 nx_t_fill_zero(input_t)
78 let idx: *i64 = (sys_mmap(64)) as *i64
79 idx[0] = 0; idx[1] = 0; nx_t_set_i64(input_t, idx, 1)
80 idx[0] = 0; idx[1] = 1; nx_t_set_i64(input_t, idx, 2)
81 idx[0] = 0; idx[1] = 2; nx_t_set_i64(input_t, idx, 3)
82 idx[0] = 1; idx[1] = 0; nx_t_set_i64(input_t, idx, 4)
83 idx[0] = 1; idx[1] = 1; nx_t_set_i64(input_t, idx, 5)
84 idx[0] = 1; idx[1] = 2; nx_t_set_i64(input_t, idx, 6)
85
86 let s_const: *i64 = (sys_mmap(16)) as *i64
87 s_const[0] = 3; s_const[1] = 4
88 let const_t: *NxTensor = nx_t_alloc(NX_DT_I64, s_const, 2, err)
89 nx_t_fill_zero(const_t)
90 idx[0] = 0; idx[1] = 0; nx_t_set_i64(const_t, idx, 1)
91 idx[0] = 0; idx[1] = 2; nx_t_set_i64(const_t, idx, 1)
92 idx[0] = 1; idx[1] = 1; nx_t_set_i64(const_t, idx, 1)
93 idx[0] = 1; idx[1] = 3; nx_t_set_i64(const_t, idx, 1)
94 idx[0] = 2; idx[1] = 0; nx_t_set_i64(const_t, idx, 1)
95 idx[0] = 2; idx[1] = 1; nx_t_set_i64(const_t, idx, 1)
96 idx[0] = 2; idx[1] = 2; nx_t_set_i64(const_t, idx, 1)
97 idx[0] = 2; idx[1] = 3; nx_t_set_i64(const_t, idx, 1)
98
99 // ===== Run the graph =========================================
100 let r: *ComputeRunner = nx_cr_alloc(g, 4, 4)
101 nx_cr_bind_input(r, input_t)
102 nx_cr_bind_const(r, const_t)
103
104 let rc: nx_int = nx_cr_run(r)
105 if rc != NX_CR_OK { return 3 }
106
107 // ===== Fetch the OUTPUT-node's tensor ========================
108 //
109 // Node 4 (OUTPUT) aliases the upstream RELU output.
110 let out_t: *NxTensor = nx_cr_get_output(r, 4, 0)
111 if (out_t as nx_int) == 0 { return 4 }
112 if out_t.ndim != 2 { return 5 }
113 if out_t.shape[0] != 2 { return 6 }
114 if out_t.shape[1] != 4 { return 7 }
115
116 // ===== Build the expected tensor and verify ==================
117 let expected: *NxTensor = nx_t_alloc(NX_DT_I64, s_const, 2, err) // 2x4
118 // Oops, s_const is 3x4. Build a fresh 2x4 shape buffer.
119 let s_out: *i64 = (sys_mmap(16)) as *i64
120 s_out[0] = 2; s_out[1] = 4
121 let expected2: *NxTensor = nx_t_alloc(NX_DT_I64, s_out, 2, err)
122 nx_t_fill_zero(expected2)
123 idx[0] = 0; idx[1] = 0; nx_t_set_i64(expected2, idx, 4)
124 idx[0] = 0; idx[1] = 1; nx_t_set_i64(expected2, idx, 5)
125 idx[0] = 0; idx[1] = 2; nx_t_set_i64(expected2, idx, 4)
126 idx[0] = 0; idx[1] = 3; nx_t_set_i64(expected2, idx, 5)
127 idx[0] = 1; idx[1] = 0; nx_t_set_i64(expected2, idx, 10)
128 idx[0] = 1; idx[1] = 1; nx_t_set_i64(expected2, idx, 11)
129 idx[0] = 1; idx[1] = 2; nx_t_set_i64(expected2, idx, 10)
130 idx[0] = 1; idx[1] = 3; nx_t_set_i64(expected2, idx, 11)
131
132 let witness: *i64 = (sys_mmap(NX_NO_WITNESS_FIELDS * 8)) as *i64
133 let verdict: nx_int = nx_no_check_bit_exact_i64(out_t, expected2, witness)
134 if verdict != NX_NO_VERDICT_EQUAL {
135 // The witness tells us exactly where it broke; bubble that
136 // info up via return code so the smoke can pinpoint.
137 return 10 + verdict
138 }
139
140 // ===== Negative test: mutate expected, re-verify, expect DIFFERS =
141 idx[0] = 0; idx[1] = 0; nx_t_set_i64(expected2, idx, 999)
142 let verdict2: nx_int = nx_no_check_bit_exact_i64(out_t, expected2, witness)
143 if verdict2 != NX_NO_VERDICT_DIFFERS { return 20 }
144 if witness[NX_NO_WITNESS_F_ACTUAL] != 4 { return 21 } // we computed 4
145 if witness[NX_NO_WITNESS_F_EXPECTED] != 999 { return 22 } // expected mutated to 999
146
147 // ===== Spot-check the matmul-output node directly ============
148 //
149 // Node 2 (MATMUL) should hold the same value (RELU is no-op when
150 // all values are positive).
151 let mm_out: *NxTensor = nx_cr_get_output(r, 2, 0)
152 idx[0] = 0; idx[1] = 0
153 if nx_t_get_i64(mm_out, idx) != 4 { return 30 }
154 idx[0] = 1; idx[1] = 1
155 if nx_t_get_i64(mm_out, idx) != 11 { return 31 }
156
157 // ===== Reproducibility: run again, same output ===============
158 //
159 // Re-allocate runner (input/const bindings are consumed by run);
160 // verify second run produces bit-identical output.
161 let r2: *ComputeRunner = nx_cr_alloc(g, 4, 4)
162 nx_cr_bind_input(r2, input_t)
163 nx_cr_bind_const(r2, const_t)
164 if nx_cr_run(r2) != NX_CR_OK { return 40 }
165 let out_t2: *NxTensor = nx_cr_get_output(r2, 4, 0)
166 let verdict3: nx_int = nx_no_check_bit_exact_i64(out_t, out_t2, witness)
167 if verdict3 != NX_NO_VERDICT_EQUAL { return 41 }
168
169 // ===== Negative test: missing input binding -> INPUT_MISSING ==
170 let r3: *ComputeRunner = nx_cr_alloc(g, 4, 4)
171 nx_cr_bind_const(r3, const_t) // no input bound
172 let rc3: nx_int = nx_cr_run(r3)
173 if rc3 != NX_CR_ERR_INPUT_MISSING { return 50 }
174
175 // ===== Verdict enum coverage =================================
176 if nx_cr_verdict_is_valid(NX_CR_OK) != 1 { return 60 }
177 if nx_cr_verdict_is_valid(NX_CR_ERR_KERNEL_FAILED) != 1 { return 61 }
178 if nx_cr_verdict_is_valid(NX_CR_N_VERDICTS) != 0 { return 62 }
179 if nx_cr_verdict_is_valid(0 - 1) != 0 { return 63 }
180
181 return 0
182}