code wiki / (root) / nx_vae_decode_stage.nx

nx_vae_decode_stage.nx source

↩ module page · 212 lines · 8293 B

1// nx_vae_decode_stage.nx -- one stage of a VAE decoder. 2// 3// L4 composer. Closes the latent -> pixel path: VAE decoder = N 4// stacked stages of (ResBlock + optional 2x upsample), ending with 5// final GroupNorm + SiLU + 3x3 conv to RGB. This primitive 6// is one such stage; caller composes the full decoder. 7// 8// ===== Canonical SD VAE decoder topology ========================= 9// 10// Input: latent [N, 4, H, W] (typical: 64x64 for 512x512 image) 11// 12// stage_in: conv2d 4 -> 512 ch 13// stage_mid: ResBlock 512 -> 512 (no upsample) 14// stage_up_0: ResBlock 512 -> 512, upsample 2x 15// stage_up_1: ResBlock 512 -> 512, upsample 2x 16// stage_up_2: ResBlock 512 -> 256, upsample 2x 17// stage_up_3: ResBlock 256 -> 128, no upsample 18// stage_out: GroupNorm + SiLU + conv2d 128 -> 3 19// 20// Output: image [N, 3, 8H, 8W] 21// 22// v1 substrate ships THIS PRIMITIVE for one (ResBlock + optional 23// upsample) stage. The CHANNEL CHANGE (e.g. 512 -> 256) is queued 24// for v2 (needs 1x1 skip conv in the ResBlock). v1 covers the 25// same-channel-count stages which are the majority. 26// 27// Bits-up composition (pure): 28// nx_unet_block_forward (L4 ResBlock, shipped 6ede5571) 29// nx_upsample_2x (L3 upsample, shipped dbd26d43) 30// NxTensor (L1) 31// nx_loop (control) 32// 33// genealogy_id: kingma_2014_vae + rombach_2022_stable_diffusion_autoencoder 34// lineage_id: substrate_vae_decode_stage_v1 35 36// nx_safety_envelope: 37// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 38// sil_target: SIL1 39// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 40// verdict: NOT_YET_EVALUATED 41 42import "nx_syscalls.nx" 43import "nx_tier.nx" 44import "nx_loop.nx" 45import "nx_tensor.nx" 46import "nx_unet_block.nx" 47import "nx_upsample.nx" 48 49// ===== Sealed-enum: VaeDecodeStageVerdict ========================= 50 51const NX_VDS_OK: nx_int = 0 52const NX_VDS_ERR_BAD_DIMS: nx_int = 1 53const NX_VDS_ERR_SHAPE_MISMATCH: nx_int = 2 54const NX_VDS_ERR_INTERNAL: nx_int = 3 55const NX_VDS_N_VERDICTS: nx_int = 4 56 57func nx_vds_verdict_is_valid(v: nx_int) -> nx_int { 58 if v < 0 { return 0 } 59 if v >= NX_VDS_N_VERDICTS { return 0 } 60 return 1 61} 62 63// ===== Stage forward ============================================= 64// 65// input: [N, C, H, W] Q10 66// W_1, W_2: ResBlock conv weights (each [C, C, 3, 3]) 67// gamma_1, beta_1, gamma_2, beta_2: GroupNorm scale + bias [C] 68// n_groups: GroupNorm group count (typically 32) 69// do_upsample: 1 = upsample 2x at the end; 0 = no upsample 70// upsample_filter: NX_UP_NEAREST or NX_UP_BILINEAR 71// 72// output: [N, C, H, W] Q10 if do_upsample=0 73// [N, C, 2H, 2W] Q10 if do_upsample=1 74// resblock_scratch: scratch tensor for ResBlock intermediate 75// [N, C, H, W] 76// post_resblock: output of the ResBlock before upsample 77// [N, C, H, W] 78// 79// When do_upsample=1 we put the ResBlock output in `post_resblock` 80// and upsample into `output`. When do_upsample=0 we put the 81// ResBlock output directly in `output` and `post_resblock` is 82// unused (caller can pass it as a throwaway). 83 84func nx_vae_decode_stage_forward( 85 input: *NxTensor, 86 W_1: *NxTensor, gamma_1: *i64, beta_1: *i64, 87 W_2: *NxTensor, gamma_2: *i64, beta_2: *i64, 88 n_groups: nx_int, 89 do_upsample: nx_int, 90 upsample_filter: nx_int, 91 post_resblock: *NxTensor, 92 resblock_scratch: *NxTensor, 93 output: *NxTensor) -> nx_int { 94 95 // Step 1: ResBlock. Output target depends on do_upsample. 96 if do_upsample == 1 { 97 let v_rb: nx_int = nx_unet_block_forward( 98 input, W_1, gamma_1, beta_1, 99 W_2, gamma_2, beta_2, 100 n_groups, post_resblock, resblock_scratch) 101 if v_rb != NX_UB_OK { return NX_VDS_ERR_INTERNAL } 102 103 // Step 2: 2x upsample into the final output buffer. 104 let v_up: nx_int = nx_upsample_2x(post_resblock, output, upsample_filter) 105 if v_up != NX_UP_OK { return NX_VDS_ERR_INTERNAL } 106 } 107 if do_upsample == 0 { 108 // No upsample -- ResBlock writes directly to output. 109 let v_rb: nx_int = nx_unet_block_forward( 110 input, W_1, gamma_1, beta_1, 111 W_2, gamma_2, beta_2, 112 n_groups, output, resblock_scratch) 113 if v_rb != NX_UB_OK { return NX_VDS_ERR_INTERNAL } 114 } 115 return NX_VDS_OK 116} 117 118// ===== Self-test ================================================== 119// 120// Tiny stage smoke: [N=1, C=4, H=2, W=2] -> ResBlock (zero weights) 121// + upsample 2x = [1, 4, 4, 4]. 122// With zero ResBlock weights, ResBlock output == input (residual 123// pass-through). Upsample 2x then doubles spatial dims. 124// 125// Closed-form invariants: 126// (a) do_upsample=1: output dims = [N, C, 2H, 2W] 127// (b) do_upsample=0: output dims = [N, C, H, W]; zero-conv leaves 128// input intact via residual 129// (c) Verdict gate 130 131func main() -> i64 { 132 let N: nx_int = 1 133 let C: nx_int = 4 134 let H: nx_int = 2 135 let W: nx_int = 2 136 137 let in_sh: *nx_int = sys_mmap(4 * 8) as *nx_int 138 in_sh[0]=N; in_sh[1]=C; in_sh[2]=H; in_sh[3]=W 139 let mid_sh: *nx_int = sys_mmap(4 * 8) as *nx_int 140 mid_sh[0]=N; mid_sh[1]=C; mid_sh[2]=H; mid_sh[3]=W 141 let up_sh: *nx_int = sys_mmap(4 * 8) as *nx_int 142 up_sh[0]=N; up_sh[1]=C; up_sh[2]=H*2; up_sh[3]=W*2 143 let wt_sh: *nx_int = sys_mmap(4 * 8) as *nx_int 144 wt_sh[0]=C; wt_sh[1]=C; wt_sh[2]=3; wt_sh[3]=3 145 146 let err: *nx_int = sys_mmap(8) as *nx_int 147 err[0] = 0 148 let input: *NxTensor = nx_t_alloc(NX_DT_I64, in_sh, 4, err) 149 let post_rb: *NxTensor = nx_t_alloc(NX_DT_I64, mid_sh, 4, err) 150 let scratch: *NxTensor = nx_t_alloc(NX_DT_I64, mid_sh, 4, err) 151 let upscaled: *NxTensor = nx_t_alloc(NX_DT_I64, up_sh, 4, err) 152 let W_1: *NxTensor = nx_t_alloc(NX_DT_I64, wt_sh, 4, err) 153 let W_2: *NxTensor = nx_t_alloc(NX_DT_I64, wt_sh, 4, err) 154 if err[0] != 0 { return 5 } 155 156 // Fill input with a pattern. 157 let pi: *i64 = input.storage as *i64 158 var i: nx_int = 0 159 while i < N * C * H * W { pi[i] = (i + 1) * 50; i = i + 1 } 160 161 // Unit gammas, zero betas. 162 let gamma_1: *i64 = sys_mmap(C * 8) as *i64 163 let gamma_2: *i64 = sys_mmap(C * 8) as *i64 164 let beta_1: *i64 = sys_mmap(C * 8) as *i64 165 let beta_2: *i64 = sys_mmap(C * 8) as *i64 166 var k: nx_int = 0 167 while k < C { gamma_1[k] = 1024; gamma_2[k] = 1024; beta_1[k] = 0; beta_2[k] = 0; k = k + 1 } 168 169 // --- (a) Stage with upsample --- 170 let v_a: nx_int = nx_vae_decode_stage_forward( 171 input, W_1, gamma_1, beta_1, W_2, gamma_2, beta_2, 172 2, 1, NX_UP_NEAREST, post_rb, scratch, upscaled) 173 if v_a != NX_VDS_OK { return 10 + v_a } 174 // Upscaled output dims: [1, 4, 4, 4] = 64 elements. 175 // Zero conv -> residual -> post_rb == input. Then nearest upsample 176 // 2x: each input pixel replicated 2x2. The post_rb[0] = input[0] = 50. 177 // After nearest 2x: upscaled[0..3] (= upscaled row 0 cols 0..3 of 178 // channel 0) should be [50, 50, ?, ?] (where ? comes from input 179 // column 1). Verify simple pattern. 180 let pu: *i64 = upscaled.storage as *i64 181 if pu[0] != 50 { return 20 } 182 if pu[1] != 50 { return 21 } 183 // Pixel (row 0, col 2) in upsampled = input row 0 col 1. 184 // input col 1 of channel 0 (N=1, C=4, H=2, W=2 layout, contiguous): 185 // input flat index for (n=0, c=0, h=0, w=1) = 0 + 0 + 0 + 1 = 1. 186 // pi[1] = 100. 187 if pu[2] != 100 { return 22 } 188 189 // --- (b) Stage without upsample --- 190 let out_no_up: *NxTensor = nx_t_alloc(NX_DT_I64, mid_sh, 4, err) 191 if err[0] != 0 { return 30 } 192 let v_b: nx_int = nx_vae_decode_stage_forward( 193 input, W_1, gamma_1, beta_1, W_2, gamma_2, beta_2, 194 2, 0, NX_UP_NEAREST, post_rb, scratch, out_no_up) 195 if v_b != NX_VDS_OK { return 40 + v_b } 196 // Zero conv -> residual -> output == input. 197 let po: *i64 = out_no_up.storage as *i64 198 var j: nx_int = 0 199 while j < N * C * H * W { 200 if po[j] != pi[j] { return 50 } 201 j = j + 1 202 } 203 204 // --- (c) Verdict gate --- 205 var vi: nx_int = 0 206 while vi < NX_VDS_N_VERDICTS { 207 if nx_vds_verdict_is_valid(vi) != 1 { return 60 + vi } 208 vi = vi + 1 209 } 210 211 return 0 212}