nx_upsample.nx source
↩ module page · 355 lines · 13845 B
1// nx_upsample.nx -- 2x upsample for 4D feature maps.
2//
3// Closes the missing-dep gap for VAE decoder + UNet up-blocks.
4// Modern image-gen architectures use 2x nearest-neighbor or
5// 2x bilinear upsample inside the decoder stages:
6//
7// VAE decoder: 4 stages of 2x upsample (8x total = 64x64 latent
8// -> 512x512 image)
9// UNet up-path: 2x upsample at each scale level
10// Super-resolution networks: 2x or 4x upsample
11//
12// L3 canonical primitive composing:
13// NxTensor (L1, 4D NCHW input/output)
14// nx_loop.LoopVerdict (control)
15// -- no other deps; pure pixel-replication / linear-interp math
16//
17// ===== Tensor layout =============================================
18//
19// Input: [N, C, H, W] NCHW Q10
20// Output: [N, C, 2H, 2W] 2x upscaled in spatial dims; channels
21// unchanged
22//
23// ===== Filter modes ==============================================
24//
25// NX_UP_NEAREST -- each output pixel = nearest input pixel.
26// Blocky but cheap. Common in VAE decoder when
27// followed by a 3x3 conv that smooths the
28// artifacts.
29// NX_UP_BILINEAR -- 4-tap bilinear from the 4 nearest input pixels.
30// Smoother; slightly more expensive.
31//
32// ===== Pixel-center convention ===================================
33//
34// Output pixel (oy, ox) maps to input pixel at:
35// iy = (oy + 0.5) / 2 - 0.5
36// ix = (ox + 0.5) / 2 - 0.5
37//
38// This is the "align_corners=False" convention from PyTorch -- the
39// standard for VAE-class upsampling. In Q10 fixed-point:
40// iy_q10 = (oy + 1) * Q10 / 2 - Q10 / 2 ; if (... ) ... simplifies
41// We compute integer iy + fractional bit separately.
42//
43// genealogy_id: pytorch_upsample_2d_2017 + opencv_resize_2000 +
44// neural_super_resolution_ledig_2016
45// lineage_id: substrate_upsample_v1
46
47// nx_safety_envelope:
48// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
49// sil_target: SIL1
50// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
51// verdict: NOT_YET_EVALUATED
52
53import "nx_syscalls.nx"
54import "nx_tier.nx"
55import "nx_loop.nx"
56import "nx_tensor.nx"
57
58// ===== Constants ==================================================
59
60const NX_UP_Q10: nx_int = 1024
61
62// ===== Sealed-enum: FilterMode ====================================
63
64const NX_UP_NEAREST: nx_int = 0
65const NX_UP_BILINEAR: nx_int = 1
66const NX_UP_FILTER_N: nx_int = 2
67
68func nx_up_filter_is_valid(k: nx_int) -> nx_int {
69 if k < 0 { return 0 }
70 if k >= NX_UP_FILTER_N { return 0 }
71 return 1
72}
73
74// ===== Sealed-enum: UpsampleVerdict ===============================
75
76const NX_UP_OK: nx_int = 0
77const NX_UP_ERR_BAD_DTYPE: nx_int = 1
78const NX_UP_ERR_BAD_NDIM: nx_int = 2
79const NX_UP_ERR_SHAPE_MISMATCH: nx_int = 3
80const NX_UP_ERR_NOT_CONTIGUOUS: nx_int = 4
81const NX_UP_ERR_BAD_FILTER: nx_int = 5
82const NX_UP_N_VERDICTS: nx_int = 6
83
84func nx_up_verdict_is_valid(v: nx_int) -> nx_int {
85 if v < 0 { return 0 }
86 if v >= NX_UP_N_VERDICTS { return 0 }
87 return 1
88}
89
90// ===== Nearest-neighbor 2x ========================================
91
92func _up_nearest(input: *NxTensor, output: *NxTensor) -> nx_int {
93 let N: nx_int = input.shape[0]
94 let C: nx_int = input.shape[1]
95 let H: nx_int = input.shape[2]
96 let W: nx_int = input.shape[3]
97 let H2: nx_int = H * 2
98 let W2: nx_int = W * 2
99
100 let pi: *i64 = input.storage as *i64
101 let po: *i64 = output.storage as *i64
102 let in_batch: nx_int = C * H * W
103 let in_chan: nx_int = H * W
104 let out_batch: nx_int = C * H2 * W2
105 let out_chan: nx_int = H2 * W2
106
107 var n: nx_int = 0
108 var n_iter: nx_int = 0
109 var n_verdict: nx_int = NX_LOOP_RUNNING
110 let N_BUDGET: nx_int = N
111 while n_verdict == NX_LOOP_RUNNING && n_iter < N_BUDGET {
112 var c: nx_int = 0
113 var c_iter: nx_int = 0
114 var c_verdict: nx_int = NX_LOOP_RUNNING
115 let C_BUDGET: nx_int = C
116 while c_verdict == NX_LOOP_RUNNING && c_iter < C_BUDGET {
117 var oy: nx_int = 0
118 var oy_iter: nx_int = 0
119 var oy_verdict: nx_int = NX_LOOP_RUNNING
120 let OY_BUDGET: nx_int = H2
121 while oy_verdict == NX_LOOP_RUNNING && oy_iter < OY_BUDGET {
122 // Nearest input row: oy / 2.
123 let iy: nx_int = oy / 2
124 var ox: nx_int = 0
125 var ox_iter: nx_int = 0
126 var ox_verdict: nx_int = NX_LOOP_RUNNING
127 let OX_BUDGET: nx_int = W2
128 while ox_verdict == NX_LOOP_RUNNING && ox_iter < OX_BUDGET {
129 let ix: nx_int = ox / 2
130 let src: nx_int = n * in_batch + c * in_chan + iy * W + ix
131 let dst: nx_int = n * out_batch + c * out_chan + oy * W2 + ox
132 po[dst] = pi[src]
133 ox = ox + 1
134 ox_iter = ox_iter + 1
135 }
136 oy = oy + 1
137 oy_iter = oy_iter + 1
138 }
139 c = c + 1
140 c_iter = c_iter + 1
141 }
142 n = n + 1
143 n_iter = n_iter + 1
144 }
145 return NX_UP_OK
146}
147
148// ===== Bilinear 2x ================================================
149//
150// align_corners=False convention. Each output pixel (oy, ox) maps
151// to fractional input coords:
152// iy_q10 = oy * Q10 / 2 + Q10 / 4 - Q10 / 2 = (2*oy + 1) * Q10 / 4 - Q10 / 2
153// Simpler: iy_int = (oy - 1) / 2 if oy odd else oy / 2 - 1; we just
154// compute it carefully below.
155//
156// Bilinear:
157// iy_q10 = ((oy * 2 + 1) * Q10) / 4 - Q10/2 (i.e. centre-shift)
158// iy_int = iy_q10 / Q10 (floor)
159// frac_y = iy_q10 - iy_int * Q10
160// (clamp iy_int to [0, H-1] and iy_int+1 to [0, H-1]; same for x)
161
162func _up_bilinear(input: *NxTensor, output: *NxTensor) -> nx_int {
163 let N: nx_int = input.shape[0]
164 let C: nx_int = input.shape[1]
165 let H: nx_int = input.shape[2]
166 let W: nx_int = input.shape[3]
167 let H2: nx_int = H * 2
168 let W2: nx_int = W * 2
169
170 let pi: *i64 = input.storage as *i64
171 let po: *i64 = output.storage as *i64
172 let in_batch: nx_int = C * H * W
173 let in_chan: nx_int = H * W
174 let out_batch: nx_int = C * H2 * W2
175 let out_chan: nx_int = H2 * W2
176
177 var n: nx_int = 0
178 var n_iter: nx_int = 0
179 var n_verdict: nx_int = NX_LOOP_RUNNING
180 let N_BUDGET: nx_int = N
181 while n_verdict == NX_LOOP_RUNNING && n_iter < N_BUDGET {
182 var c: nx_int = 0
183 var c_iter: nx_int = 0
184 var c_verdict: nx_int = NX_LOOP_RUNNING
185 let C_BUDGET: nx_int = C
186 while c_verdict == NX_LOOP_RUNNING && c_iter < C_BUDGET {
187 var oy: nx_int = 0
188 var oy_iter: nx_int = 0
189 var oy_verdict: nx_int = NX_LOOP_RUNNING
190 let OY_BUDGET: nx_int = H2
191 while oy_verdict == NX_LOOP_RUNNING && oy_iter < OY_BUDGET {
192 // Fractional input row centre.
193 let iy_centre_q10: nx_int = ((oy * 2 + 1) * NX_UP_Q10) / 4 - NX_UP_Q10 / 2
194 var iy_int: nx_int = iy_centre_q10 / NX_UP_Q10
195 if iy_centre_q10 < 0 {
196 if iy_centre_q10 - iy_int * NX_UP_Q10 != 0 { iy_int = iy_int - 1 }
197 }
198 let frac_y: nx_int = iy_centre_q10 - iy_int * NX_UP_Q10
199 // Clamp to valid input rows.
200 var iy0: nx_int = iy_int
201 if iy0 < 0 { iy0 = 0 }
202 if iy0 >= H { iy0 = H - 1 }
203 var iy1: nx_int = iy_int + 1
204 if iy1 < 0 { iy1 = 0 }
205 if iy1 >= H { iy1 = H - 1 }
206
207 var ox: nx_int = 0
208 var ox_iter: nx_int = 0
209 var ox_verdict: nx_int = NX_LOOP_RUNNING
210 let OX_BUDGET: nx_int = W2
211 while ox_verdict == NX_LOOP_RUNNING && ox_iter < OX_BUDGET {
212 let ix_centre_q10: nx_int = ((ox * 2 + 1) * NX_UP_Q10) / 4 - NX_UP_Q10 / 2
213 var ix_int: nx_int = ix_centre_q10 / NX_UP_Q10
214 if ix_centre_q10 < 0 {
215 if ix_centre_q10 - ix_int * NX_UP_Q10 != 0 { ix_int = ix_int - 1 }
216 }
217 let frac_x: nx_int = ix_centre_q10 - ix_int * NX_UP_Q10
218 var ix0: nx_int = ix_int
219 if ix0 < 0 { ix0 = 0 }
220 if ix0 >= W { ix0 = W - 1 }
221 var ix1: nx_int = ix_int + 1
222 if ix1 < 0 { ix1 = 0 }
223 if ix1 >= W { ix1 = W - 1 }
224
225 let base: nx_int = n * in_batch + c * in_chan
226 let v00: i64 = pi[base + iy0 * W + ix0]
227 let v10: i64 = pi[base + iy0 * W + ix1]
228 let v01: i64 = pi[base + iy1 * W + ix0]
229 let v11: i64 = pi[base + iy1 * W + ix1]
230 let one_minus_x: nx_int = NX_UP_Q10 - frac_x
231 let one_minus_y: nx_int = NX_UP_Q10 - frac_y
232 let top: nx_int = (v00 * one_minus_x + v10 * frac_x) / NX_UP_Q10
233 let bot: nx_int = (v01 * one_minus_x + v11 * frac_x) / NX_UP_Q10
234 let val: nx_int = (top * one_minus_y + bot * frac_y) / NX_UP_Q10
235 let dst: nx_int = n * out_batch + c * out_chan + oy * W2 + ox
236 po[dst] = val
237 ox = ox + 1
238 ox_iter = ox_iter + 1
239 }
240 oy = oy + 1
241 oy_iter = oy_iter + 1
242 }
243 c = c + 1
244 c_iter = c_iter + 1
245 }
246 n = n + 1
247 n_iter = n_iter + 1
248 }
249 return NX_UP_OK
250}
251
252// ===== Public dispatch ===========================================
253
254func nx_upsample_2x(input: *NxTensor, output: *NxTensor, filter: nx_int) -> nx_int {
255 if input.dtype != NX_DT_I64 { return NX_UP_ERR_BAD_DTYPE }
256 if output.dtype != NX_DT_I64 { return NX_UP_ERR_BAD_DTYPE }
257 if input.ndim != 4 { return NX_UP_ERR_BAD_NDIM }
258 if output.ndim != 4 { return NX_UP_ERR_BAD_NDIM }
259 if input.shape[0] != output.shape[0] { return NX_UP_ERR_SHAPE_MISMATCH }
260 if input.shape[1] != output.shape[1] { return NX_UP_ERR_SHAPE_MISMATCH }
261 if input.shape[2] * 2 != output.shape[2] { return NX_UP_ERR_SHAPE_MISMATCH }
262 if input.shape[3] * 2 != output.shape[3] { return NX_UP_ERR_SHAPE_MISMATCH }
263 if nx_t_is_contiguous(input) == 0 { return NX_UP_ERR_NOT_CONTIGUOUS }
264 if nx_t_is_contiguous(output) == 0 { return NX_UP_ERR_NOT_CONTIGUOUS }
265 if filter == NX_UP_NEAREST { return _up_nearest(input, output) }
266 if filter == NX_UP_BILINEAR { return _up_bilinear(input, output) }
267 return NX_UP_ERR_BAD_FILTER
268}
269
270// ===== Self-test ==================================================
271//
272// 1x1x2x2 input upsampled to 1x1x4x4.
273// Input: [[10, 20],
274// [30, 40]]
275// Nearest -> [[10,10,20,20],[10,10,20,20],[30,30,40,40],[30,30,40,40]]
276// Bilinear -> smooth gradient between corners.
277//
278// Closed-form invariants:
279// (a) Nearest 2x doubles dims, replicates pixels
280// (b) Bilinear 2x preserves input corner values (with align_corners=False
281// convention, output corners ARE input corners after rounding)
282// (c) Bad filter -> ERR_BAD_FILTER
283// (d) Bad shape (output H != 2 * input H) -> ERR_SHAPE_MISMATCH
284// (e) Verdict + filter sealed-enum gates
285
286func main() -> i64 {
287 let in_sh: *nx_int = sys_mmap(4 * 8) as *nx_int
288 in_sh[0]=1; in_sh[1]=1; in_sh[2]=2; in_sh[3]=2
289 let out_sh: *nx_int = sys_mmap(4 * 8) as *nx_int
290 out_sh[0]=1; out_sh[1]=1; out_sh[2]=4; out_sh[3]=4
291
292 let err: *nx_int = sys_mmap(8) as *nx_int
293 err[0] = 0
294 let xt: *NxTensor = nx_t_alloc(NX_DT_I64, in_sh, 4, err)
295 let yt: *NxTensor = nx_t_alloc(NX_DT_I64, out_sh, 4, err)
296 if err[0] != 0 { return 5 }
297
298 // Input: [10, 20; 30, 40] in Q10 (but values are unscaled for the
299 // smoke; we just want bit-exact replication for nearest).
300 let pi: *i64 = xt.storage as *i64
301 pi[0] = 10
302 pi[1] = 20
303 pi[2] = 30
304 pi[3] = 40
305
306 // --- (a) Nearest 2x ---
307 let v_n: nx_int = nx_upsample_2x(xt, yt, NX_UP_NEAREST)
308 if v_n != NX_UP_OK { return 10 + v_n }
309 let po: *i64 = yt.storage as *i64
310 if po[0] != 10 { return 20 } // (0,0)
311 if po[1] != 10 { return 21 } // (0,1)
312 if po[2] != 20 { return 22 } // (0,2)
313 if po[3] != 20 { return 23 } // (0,3)
314 if po[4] != 10 { return 24 } // (1,0)
315 if po[7] != 20 { return 25 }
316 if po[8] != 30 { return 26 }
317 if po[15] != 40 { return 27 }
318
319 // --- (b) Bilinear 2x ---
320 // Just verify it produces SOMETHING reasonable; bilinear of [10,20;30,40]
321 // should have output values between 10 and 40. Centre pixels = ~25.
322 let v_b: nx_int = nx_upsample_2x(xt, yt, NX_UP_BILINEAR)
323 if v_b != NX_UP_OK { return 30 + v_b }
324 var k: nx_int = 0
325 while k < 16 {
326 if po[k] < 0 { return 40 }
327 if po[k] > 100 { return 41 }
328 k = k + 1
329 }
330
331 // --- (c) Bad filter ---
332 let v_bf: nx_int = nx_upsample_2x(xt, yt, 99)
333 if v_bf != NX_UP_ERR_BAD_FILTER { return 50 }
334
335 // --- (d) Bad shape ---
336 let bad_sh: *nx_int = sys_mmap(4 * 8) as *nx_int
337 bad_sh[0]=1; bad_sh[1]=1; bad_sh[2]=3; bad_sh[3]=4 // wrong H
338 let bad: *NxTensor = nx_t_alloc(NX_DT_I64, bad_sh, 4, err)
339 let v_bs: nx_int = nx_upsample_2x(xt, bad, NX_UP_NEAREST)
340 if v_bs != NX_UP_ERR_SHAPE_MISMATCH { return 60 }
341
342 // --- (e) Verdict + filter gates ---
343 var vi: nx_int = 0
344 while vi < NX_UP_N_VERDICTS {
345 if nx_up_verdict_is_valid(vi) != 1 { return 70 + vi }
346 vi = vi + 1
347 }
348 var fi: nx_int = 0
349 while fi < NX_UP_FILTER_N {
350 if nx_up_filter_is_valid(fi) != 1 { return 80 + fi }
351 fi = fi + 1
352 }
353
354 return 0
355}